Compare commits
4 Commits
a1b1eed86d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
e9696f722c
|
|||
|
fdeb2636c2
|
|||
|
82ed16715f
|
|||
| 44cf749bc9 |
@@ -82,8 +82,11 @@ COPY --from=builder /build/client/dist ./public
|
|||||||
# Zkopírování produkčních .env serveru
|
# Zkopírování produkčních .env serveru
|
||||||
COPY /server/.env.production ./server
|
COPY /server/.env.production ./server
|
||||||
|
|
||||||
# Zkopírování konfigurace easter eggů
|
# Zkopírování changelogů (seznamu novinek)
|
||||||
RUN if [ -f /server/.easter-eggs.json ]; then cp /server/.easter-eggs.json ./server/; fi
|
COPY /server/changelogs ./server/changelogs
|
||||||
|
|
||||||
|
# Zkopírování konfigurace easter eggů a changelogů
|
||||||
|
RUN if [ -f ./server/.easter-eggs.json ]; then cp ./server/.easter-eggs.json ./server/; fi
|
||||||
|
|
||||||
# Export /data/db.json do složky /data
|
# Export /data/db.json do složky /data
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
|
|||||||
@@ -18,8 +18,12 @@ COPY ./server/dist ./
|
|||||||
# Vykopírování sestaveného klienta
|
# Vykopírování sestaveného klienta
|
||||||
COPY ./client/dist ./public
|
COPY ./client/dist ./public
|
||||||
|
|
||||||
# Zkopírování konfigurace easter eggů
|
# Zkopírování changelogů (seznamu novinek)
|
||||||
RUN if [ -f ./server/.easter-eggs.json ]; then cp ./server/.easter-eggs.json ./server/; fi
|
COPY ./server/changelogs ./server/changelogs
|
||||||
|
|
||||||
|
# Zkopírování konfigurace easter eggů a changelogů
|
||||||
|
RUN if [ -f ./server/.easter-eggs.json ]; then cp ./server/.easter-eggs.json ./server/; fi \
|
||||||
|
&& if [ -d ./server/changelogs ]; then cp -r ./server/changelogs ./server/changelogs; fi
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
|
|||||||
@@ -287,6 +287,7 @@ function App() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await addChoice({ body: { locationKey: location, foodIndex, dayIndex } });
|
await addChoice({ body: { locationKey: location, foodIndex, dayIndex } });
|
||||||
|
await tryAutoSelectDepartureTime();
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
alert(`Chyba při změně volby: ${error.message || error}`);
|
alert(`Chyba při změně volby: ${error.message || error}`);
|
||||||
}
|
}
|
||||||
@@ -313,6 +314,10 @@ function App() {
|
|||||||
foodChoiceRef.current.value = "";
|
foodChoiceRef.current.value = "";
|
||||||
}
|
}
|
||||||
choiceRef.current?.blur();
|
choiceRef.current?.blur();
|
||||||
|
// Automatický výběr času odchodu pouze pro restaurace s menu
|
||||||
|
if (Object.keys(Restaurant).includes(locationKey)) {
|
||||||
|
await tryAutoSelectDepartureTime();
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
alert(`Chyba při změně volby: ${error.message || error}`);
|
alert(`Chyba při změně volby: ${error.message || error}`);
|
||||||
// Reset výběru zpět
|
// Reset výběru zpět
|
||||||
@@ -337,6 +342,7 @@ function App() {
|
|||||||
const locationKey = choiceRef.current.value as LunchChoice;
|
const locationKey = choiceRef.current.value as LunchChoice;
|
||||||
if (auth?.login) {
|
if (auth?.login) {
|
||||||
await addChoice({ body: { locationKey, foodIndex: Number(event.target.value), dayIndex } });
|
await addChoice({ body: { locationKey, foodIndex: Number(event.target.value), dayIndex } });
|
||||||
|
await tryAutoSelectDepartureTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,6 +391,42 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCreatePizzaDay = async () => {
|
||||||
|
if (!window.confirm('Opravdu chcete založit Pizza day?')) return;
|
||||||
|
setLoadingPizzaDay(true);
|
||||||
|
await createPizzaDay().then(() => setLoadingPizzaDay(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeletePizzaDay = async () => {
|
||||||
|
if (!window.confirm('Opravdu chcete smazat Pizza day? Budou smazány i všechny dosud zadané objednávky.')) return;
|
||||||
|
await deletePizzaDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLockPizzaDay = async () => {
|
||||||
|
if (!window.confirm('Opravdu chcete uzamknout objednávky? Po uzamčení nebude možné přidávat ani odebírat objednávky.')) return;
|
||||||
|
await lockPizzaDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUnlockPizzaDay = async () => {
|
||||||
|
if (!window.confirm('Opravdu chcete odemknout objednávky? Uživatelé budou moci opět upravovat své objednávky.')) return;
|
||||||
|
await unlockPizzaDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFinishOrder = async () => {
|
||||||
|
if (!window.confirm('Opravdu chcete označit objednávky jako objednané? Objednávky zůstanou zamčeny.')) return;
|
||||||
|
await finishOrder();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReturnToLocked = async () => {
|
||||||
|
if (!window.confirm('Opravdu chcete vrátit stav zpět do "uzamčeno" (před objednáním)?')) return;
|
||||||
|
await lockPizzaDay();
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFinishDelivery = async () => {
|
||||||
|
if (!window.confirm(`Opravdu chcete označit pizzy jako doručené?${settings?.bankAccount && settings?.holderName ? ' Uživatelům bude vygenerován QR kód pro platbu.' : ''}`)) return;
|
||||||
|
await finishDelivery({ body: { bankAccount: settings?.bankAccount, bankAccountHolder: settings?.holderName } });
|
||||||
|
}
|
||||||
|
|
||||||
const pizzaSuggestions = useMemo(() => {
|
const pizzaSuggestions = useMemo(() => {
|
||||||
if (!data?.pizzaList) {
|
if (!data?.pizzaList) {
|
||||||
return [];
|
return [];
|
||||||
@@ -432,6 +474,16 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automaticky nastaví preferovaný čas odchodu 10:45, pokud tento čas ještě nenastal (nebo jde o budoucí den)
|
||||||
|
const tryAutoSelectDepartureTime = async () => {
|
||||||
|
const preferredTime = "10:45" as DepartureTime;
|
||||||
|
const isToday = dayIndex === data?.todayDayIndex;
|
||||||
|
if ((!isToday || isInTheFuture(preferredTime)) && departureChoiceRef.current) {
|
||||||
|
departureChoiceRef.current.value = preferredTime;
|
||||||
|
await changeDepartureTime({ body: { time: preferredTime, dayIndex } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDayChange = async (dayIndex: number) => {
|
const handleDayChange = async (dayIndex: number) => {
|
||||||
setDayIndex(dayIndex);
|
setDayIndex(dayIndex);
|
||||||
dayIndexRef.current = dayIndex;
|
dayIndexRef.current = dayIndex;
|
||||||
@@ -582,7 +634,7 @@ function App() {
|
|||||||
</Form.Select>
|
</Form.Select>
|
||||||
<small>Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí.</small>
|
<small>Je možné vybrat jen jednu možnost. Výběr jiné odstraní předchozí.</small>
|
||||||
{foodChoiceList && !closed && <>
|
{foodChoiceList && !closed && <>
|
||||||
<p className="mt-3">Na co dobrého? <small style={{ color: 'var(--luncher-text-muted)' }}>(nepovinné)</small></p>
|
<p className="mt-3">Na co dobrého?</p>
|
||||||
<Form.Select ref={foodChoiceRef} onChange={doAddFoodChoice}>
|
<Form.Select ref={foodChoiceRef} onChange={doAddFoodChoice}>
|
||||||
<option value="">Vyber jídlo...</option>
|
<option value="">Vyber jídlo...</option>
|
||||||
{foodChoiceList.map((food, index) => <option key={food.name} value={index}>{food.name}</option>)}
|
{foodChoiceList.map((food, index) => <option key={food.name} value={index}>{food.name}</option>)}
|
||||||
@@ -593,7 +645,7 @@ function App() {
|
|||||||
<Form.Select ref={departureChoiceRef} onChange={handleChangeDepartureTime}>
|
<Form.Select ref={departureChoiceRef} onChange={handleChangeDepartureTime}>
|
||||||
<option value="">Vyber čas...</option>
|
<option value="">Vyber čas...</option>
|
||||||
{Object.values(DepartureTime)
|
{Object.values(DepartureTime)
|
||||||
.filter(time => isInTheFuture(time))
|
.filter(time => dayIndex !== data.todayDayIndex || isInTheFuture(time))
|
||||||
.map(time => <option key={time} value={time}>{time}</option>)}
|
.map(time => <option key={time} value={time}>{time}</option>)}
|
||||||
</Form.Select>
|
</Form.Select>
|
||||||
</>}
|
</>}
|
||||||
@@ -708,10 +760,7 @@ function App() {
|
|||||||
</span>
|
</span>
|
||||||
:
|
:
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={async () => {
|
<Button onClick={handleCreatePizzaDay}>Založit Pizza day</Button>
|
||||||
setLoadingPizzaDay(true);
|
|
||||||
await createPizzaDay().then(() => setLoadingPizzaDay(false));
|
|
||||||
}}>Založit Pizza day</Button>
|
|
||||||
<Button variant="outline-primary" onClick={doJdemeObed}>Jdeme na oběd!</Button>
|
<Button variant="outline-primary" onClick={doJdemeObed}>Jdeme na oběd!</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -730,12 +779,8 @@ function App() {
|
|||||||
{
|
{
|
||||||
data.pizzaDay.creator === auth.login &&
|
data.pizzaDay.creator === auth.login &&
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<Button variant="danger" title="Smaže kompletně pizza day, včetně dosud zadaných objednávek." onClick={async () => {
|
<Button variant="danger" title="Smaže kompletně pizza day, včetně dosud zadaných objednávek." onClick={handleDeletePizzaDay}>Smazat Pizza day</Button>
|
||||||
await deletePizzaDay();
|
<Button title={noOrders ? "Nelze uzamknout - neexistuje žádná objednávka" : "Zamezí přidávat/odebírat objednávky. Použij před samotným objednáním, aby již nemohlo docházet ke změnám."} disabled={noOrders} onClick={handleLockPizzaDay}>Uzamknout</Button>
|
||||||
}}>Smazat Pizza day</Button>
|
|
||||||
<Button title={noOrders ? "Nelze uzamknout - neexistuje žádná objednávka" : "Zamezí přidávat/odebírat objednávky. Použij před samotným objednáním, aby již nemohlo docházet ke změnám."} disabled={noOrders} onClick={async () => {
|
|
||||||
await lockPizzaDay();
|
|
||||||
}}>Uzamknout</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
@@ -746,12 +791,8 @@ function App() {
|
|||||||
<p>Objednávky jsou uzamčeny uživatelem <strong>{data.pizzaDay.creator}</strong></p>
|
<p>Objednávky jsou uzamčeny uživatelem <strong>{data.pizzaDay.creator}</strong></p>
|
||||||
{data.pizzaDay.creator === auth.login &&
|
{data.pizzaDay.creator === auth.login &&
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<Button variant="secondary" title="Umožní znovu editovat objednávky." onClick={async () => {
|
<Button variant="secondary" title="Umožní znovu editovat objednávky." onClick={handleUnlockPizzaDay}>Odemknout</Button>
|
||||||
await unlockPizzaDay();
|
<Button title={noOrders ? "Nelze objednat - neexistuje žádná objednávka" : "Použij po objednání. Objednávky zůstanou zamčeny."} disabled={noOrders} onClick={handleFinishOrder}>Objednáno</Button>
|
||||||
}}>Odemknout</Button>
|
|
||||||
<Button title={noOrders ? "Nelze objednat - neexistuje žádná objednávka" : "Použij po objednání. Objednávky zůstanou zamčeny."} disabled={noOrders} onClick={async () => {
|
|
||||||
await finishOrder();
|
|
||||||
}}>Objednáno</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
@@ -762,12 +803,8 @@ function App() {
|
|||||||
<p>Pizzy byly objednány uživatelem <strong>{data.pizzaDay.creator}</strong></p>
|
<p>Pizzy byly objednány uživatelem <strong>{data.pizzaDay.creator}</strong></p>
|
||||||
{data.pizzaDay.creator === auth.login &&
|
{data.pizzaDay.creator === auth.login &&
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<Button variant="secondary" title="Vrátí stav do předchozího kroku (před objednáním)." onClick={async () => {
|
<Button variant="secondary" title="Vrátí stav do předchozího kroku (před objednáním)." onClick={handleReturnToLocked}>Vrátit do "uzamčeno"</Button>
|
||||||
await lockPizzaDay();
|
<Button title="Nastaví stav na 'Doručeno' - koncový stav." onClick={handleFinishDelivery}>Doručeno</Button>
|
||||||
}}>Vrátit do "uzamčeno"</Button>
|
|
||||||
<Button title="Nastaví stav na 'Doručeno' - koncový stav." onClick={async () => {
|
|
||||||
await finishDelivery({ body: { bankAccount: settings?.bankAccount, bankAccountHolder: settings?.holderName } });
|
|
||||||
}}>Doručeno</Button>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -11,16 +11,12 @@ import GenerateMockDataModal from "./modals/GenerateMockDataModal";
|
|||||||
import ClearMockDataModal from "./modals/ClearMockDataModal";
|
import ClearMockDataModal from "./modals/ClearMockDataModal";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
import { STATS_URL } from "../AppRoutes";
|
import { STATS_URL } from "../AppRoutes";
|
||||||
import { FeatureRequest, getVotes, updateVote, LunchChoices } from "../../../types";
|
import { FeatureRequest, getVotes, updateVote, LunchChoices, getChangelogs } from "../../../types";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
import { faSun, faMoon } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { formatDateString } from "../Utils";
|
||||||
|
|
||||||
const CHANGELOG = [
|
const LAST_SEEN_CHANGELOG_KEY = "lastChangelogDate";
|
||||||
"Nový moderní design aplikace",
|
|
||||||
"Oprava parsování Sladovnické a TechTower",
|
|
||||||
"Možnost označit se jako objednávající u volby \"budu objednávat\"",
|
|
||||||
"Možnost generovat QR kódy pro platby (i mimo Pizza day)",
|
|
||||||
];
|
|
||||||
|
|
||||||
const IS_DEV = process.env.NODE_ENV === 'development';
|
const IS_DEV = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
@@ -38,6 +34,7 @@ export default function Header({ choices, dayIndex }: Props) {
|
|||||||
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
|
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
|
||||||
const [refreshMenuModalOpen, setRefreshMenuModalOpen] = useState<boolean>(false);
|
const [refreshMenuModalOpen, setRefreshMenuModalOpen] = useState<boolean>(false);
|
||||||
const [changelogModalOpen, setChangelogModalOpen] = useState<boolean>(false);
|
const [changelogModalOpen, setChangelogModalOpen] = useState<boolean>(false);
|
||||||
|
const [changelogEntries, setChangelogEntries] = useState<Record<string, string[]>>({});
|
||||||
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false);
|
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false);
|
||||||
const [generateMockModalOpen, setGenerateMockModalOpen] = useState<boolean>(false);
|
const [generateMockModalOpen, setGenerateMockModalOpen] = useState<boolean>(false);
|
||||||
const [clearMockModalOpen, setClearMockModalOpen] = useState<boolean>(false);
|
const [clearMockModalOpen, setClearMockModalOpen] = useState<boolean>(false);
|
||||||
@@ -71,6 +68,19 @@ export default function Header({ choices, dayIndex }: Props) {
|
|||||||
}
|
}
|
||||||
}, [auth?.login]);
|
}, [auth?.login]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!auth?.login) return;
|
||||||
|
const lastSeen = localStorage.getItem(LAST_SEEN_CHANGELOG_KEY) ?? undefined;
|
||||||
|
getChangelogs({ query: lastSeen ? { since: lastSeen } : {} }).then(response => {
|
||||||
|
const entries = response.data;
|
||||||
|
if (!entries || Object.keys(entries).length === 0) return;
|
||||||
|
setChangelogEntries(entries);
|
||||||
|
setChangelogModalOpen(true);
|
||||||
|
const newestDate = Object.keys(entries).sort((a, b) => b.localeCompare(a))[0];
|
||||||
|
localStorage.setItem(LAST_SEEN_CHANGELOG_KEY, newestDate);
|
||||||
|
});
|
||||||
|
}, [auth?.login]);
|
||||||
|
|
||||||
const closeSettingsModal = () => {
|
const closeSettingsModal = () => {
|
||||||
setSettingsModalOpen(false);
|
setSettingsModalOpen(false);
|
||||||
}
|
}
|
||||||
@@ -197,7 +207,17 @@ export default function Header({ choices, dayIndex }: Props) {
|
|||||||
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
|
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
|
||||||
<NavDropdown.Item onClick={handleQrMenuClick}>Generování QR kódů</NavDropdown.Item>
|
<NavDropdown.Item onClick={handleQrMenuClick}>Generování QR kódů</NavDropdown.Item>
|
||||||
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
|
||||||
<NavDropdown.Item onClick={() => setChangelogModalOpen(true)}>Novinky</NavDropdown.Item>
|
<NavDropdown.Item onClick={() => {
|
||||||
|
getChangelogs().then(response => {
|
||||||
|
const entries = response.data ?? {};
|
||||||
|
setChangelogEntries(entries);
|
||||||
|
setChangelogModalOpen(true);
|
||||||
|
const dates = Object.keys(entries).sort((a, b) => b.localeCompare(a));
|
||||||
|
if (dates.length > 0) {
|
||||||
|
localStorage.setItem(LAST_SEEN_CHANGELOG_KEY, dates[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}>Novinky</NavDropdown.Item>
|
||||||
{IS_DEV && (
|
{IS_DEV && (
|
||||||
<>
|
<>
|
||||||
<NavDropdown.Divider />
|
<NavDropdown.Divider />
|
||||||
@@ -237,16 +257,24 @@ export default function Header({ choices, dayIndex }: Props) {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Modal show={changelogModalOpen} onHide={() => setChangelogModalOpen(false)}>
|
<Modal show={changelogModalOpen} onHide={() => setChangelogModalOpen(false)} size="lg">
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title><h2>Novinky</h2></Modal.Title>
|
<Modal.Title><h2>Novinky</h2></Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
|
{Object.keys(changelogEntries).sort((a, b) => b.localeCompare(a)).map(date => (
|
||||||
|
<div key={date}>
|
||||||
|
<strong>{formatDateString(date)}</strong>
|
||||||
<ul>
|
<ul>
|
||||||
{CHANGELOG.map((item, index) => (
|
{changelogEntries[date].map((item, index) => (
|
||||||
<li key={index}>{item}</li>
|
<li key={index}>{item}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{Object.keys(changelogEntries).length === 0 && (
|
||||||
|
<p>Žádné novinky.</p>
|
||||||
|
)}
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
<Button variant="secondary" onClick={() => setChangelogModalOpen(false)}>
|
<Button variant="secondary" onClick={() => setChangelogModalOpen(false)}>
|
||||||
|
|||||||
4
server/changelogs/2025-01-07.json
Normal file
4
server/changelogs/2025-01-07.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[
|
||||||
|
"Zimní atmosféra",
|
||||||
|
"Skrytí podniku U Motlíků"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-01-15.json
Normal file
3
server/changelogs/2025-01-15.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Přidání restaurace Zastávka u Michala"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-01-29.json
Normal file
3
server/changelogs/2025-01-29.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Přidání restaurace Pivovarský šenk Šeříková"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-02-19.json
Normal file
3
server/changelogs/2025-02-19.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Možnost výběru podniku/jídla kliknutím"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-02-27.json
Normal file
3
server/changelogs/2025-02-27.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Stránka se statistikami nejoblíbenějších voleb"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-03-05.json
Normal file
3
server/changelogs/2025-03-05.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Zobrazení počtu osob u každé volby"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-03-20.json
Normal file
3
server/changelogs/2025-03-20.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Migrace na generované OpenApi"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-03-21.json
Normal file
3
server/changelogs/2025-03-21.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Odebrání zimní atmosféry"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-08-01.json
Normal file
3
server/changelogs/2025-08-01.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Možnost ručního přenačtení menu"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-10-06.json
Normal file
3
server/changelogs/2025-10-06.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Parsování a zobrazení alergenů"
|
||||||
|
]
|
||||||
4
server/changelogs/2025-10-11.json
Normal file
4
server/changelogs/2025-10-11.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[
|
||||||
|
"Oddělení přenačtení menu do vlastního dialogu",
|
||||||
|
"Podzimní atmosféra"
|
||||||
|
]
|
||||||
3
server/changelogs/2025-11-03.json
Normal file
3
server/changelogs/2025-11-03.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Možnost převzetí poznámky ostatních uživatelů"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-01-09.json
Normal file
3
server/changelogs/2026-01-09.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Zimní atmosféra"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-01-13.json
Normal file
3
server/changelogs/2026-01-13.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Možnost označit se jako objednávající u volby \"Budu objednávat\""
|
||||||
|
]
|
||||||
3
server/changelogs/2026-01-30.json
Normal file
3
server/changelogs/2026-01-30.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Podpora dark mode"
|
||||||
|
]
|
||||||
7
server/changelogs/2026-02-04.json
Normal file
7
server/changelogs/2026-02-04.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
"Redesign aplikace pomocí Claude Code",
|
||||||
|
"Zobrazení uplynulého týdne i o víkendu",
|
||||||
|
"Podpora Discord, ntfy a Teams notifikací (v Nastavení)",
|
||||||
|
"Trvalé zobrazení QR kódů do ručního zavření",
|
||||||
|
"Zobrazení nejvíce požadovaných funkcí (na stránce Statistiky)"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-02-10.json
Normal file
3
server/changelogs/2026-02-10.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Zobrazení sekce Pizza day pouze při volbě \"Pizza day\""
|
||||||
|
]
|
||||||
3
server/changelogs/2026-02-20.json
Normal file
3
server/changelogs/2026-02-20.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Možnost generování obecných QR kódů pro platby i mimo Pizza day (v uživatelském menu)"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-03-04.json
Normal file
3
server/changelogs/2026-03-04.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Podpora push notifikací pro připomenutí výběru oběda (v nastavení)"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-03-05.json
Normal file
3
server/changelogs/2026-03-05.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Oprava detekce zastaralého menu"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-03-08.json
Normal file
3
server/changelogs/2026-03-08.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Automatické zobrazení dialogu s dosud nezobrazenými novinkami"
|
||||||
|
]
|
||||||
3
server/changelogs/2026-03-09.json
Normal file
3
server/changelogs/2026-03-09.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[
|
||||||
|
"Automatický výběr výchozího času preferovaného odchodu"
|
||||||
|
]
|
||||||
@@ -18,6 +18,7 @@ import statsRoutes from "./routes/statsRoutes";
|
|||||||
import notificationRoutes from "./routes/notificationRoutes";
|
import notificationRoutes from "./routes/notificationRoutes";
|
||||||
import qrRoutes from "./routes/qrRoutes";
|
import qrRoutes from "./routes/qrRoutes";
|
||||||
import devRoutes from "./routes/devRoutes";
|
import devRoutes from "./routes/devRoutes";
|
||||||
|
import changelogRoutes from "./routes/changelogRoutes";
|
||||||
|
|
||||||
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
||||||
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
||||||
@@ -165,6 +166,7 @@ app.use("/api/stats", statsRoutes);
|
|||||||
app.use("/api/notifications", notificationRoutes);
|
app.use("/api/notifications", notificationRoutes);
|
||||||
app.use("/api/qr", qrRoutes);
|
app.use("/api/qr", qrRoutes);
|
||||||
app.use("/api/dev", devRoutes);
|
app.use("/api/dev", devRoutes);
|
||||||
|
app.use("/api/changelogs", changelogRoutes);
|
||||||
|
|
||||||
app.use('/stats', express.static('public'));
|
app.use('/stats', express.static('public'));
|
||||||
app.use(express.static('public'));
|
app.use(express.static('public'));
|
||||||
|
|||||||
50
server/src/routes/changelogRoutes.ts
Normal file
50
server/src/routes/changelogRoutes.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import express, { Request, Response } from "express";
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const CHANGELOGS_DIR = path.resolve(__dirname, "../../changelogs");
|
||||||
|
|
||||||
|
// In-memory cache: datum → seznam změn
|
||||||
|
const cache: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
function loadAllChangelogs(): Record<string, string[]> {
|
||||||
|
let files: string[];
|
||||||
|
try {
|
||||||
|
files = fs.readdirSync(CHANGELOGS_DIR).filter(f => f.endsWith(".json"));
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const date = file.replace(".json", "");
|
||||||
|
if (!cache[date]) {
|
||||||
|
const content = fs.readFileSync(path.join(CHANGELOGS_DIR, file), "utf-8");
|
||||||
|
cache[date] = JSON.parse(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get("/", (req: Request, res: Response) => {
|
||||||
|
const all = loadAllChangelogs();
|
||||||
|
const since = typeof req.query.since === "string" ? req.query.since : undefined;
|
||||||
|
|
||||||
|
// Seřazení od nejnovějšího po nejstarší
|
||||||
|
const sortedDates = Object.keys(all).sort((a, b) => b.localeCompare(a));
|
||||||
|
|
||||||
|
const filteredDates = since
|
||||||
|
? sortedDates.filter(date => date > since)
|
||||||
|
: sortedDates;
|
||||||
|
|
||||||
|
const result: Record<string, string[]> = {};
|
||||||
|
for (const date of filteredDates) {
|
||||||
|
result[date] = all[date];
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -77,6 +77,10 @@ paths:
|
|||||||
/voting/stats:
|
/voting/stats:
|
||||||
$ref: "./paths/voting/getVotingStats.yml"
|
$ref: "./paths/voting/getVotingStats.yml"
|
||||||
|
|
||||||
|
# Changelog (/api/changelogs)
|
||||||
|
/changelogs:
|
||||||
|
$ref: "./paths/changelogs/getChangelogs.yml"
|
||||||
|
|
||||||
# DEV endpointy (/api/dev)
|
# DEV endpointy (/api/dev)
|
||||||
/dev/generate:
|
/dev/generate:
|
||||||
$ref: "./paths/dev/generate.yml"
|
$ref: "./paths/dev/generate.yml"
|
||||||
|
|||||||
21
types/paths/changelogs/getChangelogs.yml
Normal file
21
types/paths/changelogs/getChangelogs.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
get:
|
||||||
|
operationId: getChangelogs
|
||||||
|
summary: Vrátí seznam změn (changelog). Pokud není předáno datum, vrátí všechny změny. Pokud je předáno datum, vrátí pouze změny po tomto datu.
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: since
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Datum (formát YYYY-MM-DD) od kterého se mají vrátit změny (exkluzivně). Pokud není předáno, vrátí se všechny změny.
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Slovník kde klíčem je datum (YYYY-MM-DD) a hodnotou seznam změn k danému datu. Seřazeno od nejnovějšího po nejstarší.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
Reference in New Issue
Block a user