From 93242b7c7efa66a5b07a4fba17120b16b9495c27 Mon Sep 17 00:00:00 2001 From: Martin Berka Date: Tue, 25 Aug 2026 10:31:32 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20lep=C5=A1=C3=AD=20generov=C3=A1n=C3=AD?= =?UTF-8?q?=20mock=20dat=20p=C5=99i=20v=C3=BDvoji?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/Header.tsx | 2 +- .../components/modals/ClearMockDataModal.tsx | 42 +++- .../modals/GenerateMockDataModal.tsx | 35 +++- server/src/routes/devRoutes.ts | 196 +++++++++++------- types/paths/dev/clear.yml | 10 +- types/paths/dev/generate.yml | 9 +- types/schemas/_index.yml | 25 ++- 7 files changed, 221 insertions(+), 98 deletions(-) diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index 49acaa2..5243ecb 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -184,7 +184,7 @@ export default function Header({ choices, dayIndex }: Props) { <> setGenerateMockModalOpen(true)}>🔧 Generovat mock data - setClearMockModalOpen(true)}>🔧 Smazat data dne + setClearMockModalOpen(true)}>🔧 Smazat mock data )} diff --git a/client/src/components/modals/ClearMockDataModal.tsx b/client/src/components/modals/ClearMockDataModal.tsx index e1b9492..935d3bd 100644 --- a/client/src/components/modals/ClearMockDataModal.tsx +++ b/client/src/components/modals/ClearMockDataModal.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Modal, Button, Alert } from "react-bootstrap"; +import { Modal, Button, Form, Alert } from "react-bootstrap"; import { clearMockData, DayIndex } from "../../../../types"; type Props = { @@ -10,20 +10,28 @@ type Props = { const DAY_NAMES = ['Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek']; +/** Hodnota v selectu dne pro smazání dat celého týdne. */ +const WHOLE_WEEK = 'week'; + /** Modální dialog pro smazání mock dat (pouze DEV). */ export default function ClearMockDataModal({ isOpen, onClose, currentDayIndex }: Readonly) { + const [daySelection, setDaySelection] = useState(currentDayIndex !== undefined ? String(currentDayIndex) : ''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(false); + const wholeWeek = daySelection === WHOLE_WEEK; + const handleClear = async () => { setError(null); setLoading(true); try { const body: any = {}; - if (currentDayIndex !== undefined) { - body.dayIndex = currentDayIndex as DayIndex; + if (wholeWeek) { + body.wholeWeek = true; + } else if (daySelection !== '') { + body.dayIndex = parseInt(daySelection, 10) as DayIndex; } const response = await clearMockData({ body }); @@ -49,12 +57,20 @@ export default function ClearMockDataModal({ isOpen, onClose, currentDayIndex }: onClose(); }; - const dayName = currentDayIndex !== undefined ? DAY_NAMES[currentDayIndex] : 'aktuální den'; + // Popis rozsahu mazání pro potvrzovací otázku + let scopeLabel: string; + if (wholeWeek) { + scopeLabel = 'celý týden (pondělí až pátek)'; + } else if (daySelection !== '') { + scopeLabel = DAY_NAMES[parseInt(daySelection, 10)]; + } else { + scopeLabel = 'aktuální den'; + } return ( -

Smazat data

+

Smazat mock data

{success ? ( @@ -73,8 +89,22 @@ export default function ClearMockDataModal({ isOpen, onClose, currentDayIndex }: )} + + Den + setDaySelection(e.target.value)} + > + + {DAY_NAMES.map((name, index) => ( + + ))} + + + +

- Opravdu chcete smazat všechny volby stravování pro {dayName}? + Opravdu chcete smazat všechny volby stravování pro {scopeLabel}?

Tato akce je nevratná. diff --git a/client/src/components/modals/GenerateMockDataModal.tsx b/client/src/components/modals/GenerateMockDataModal.tsx index cb0cc30..c88713f 100644 --- a/client/src/components/modals/GenerateMockDataModal.tsx +++ b/client/src/components/modals/GenerateMockDataModal.tsx @@ -10,13 +10,18 @@ type Props = { const DAY_NAMES = ['Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek']; +/** Hodnota v selectu dne pro generování napříč celým týdnem. */ +const WHOLE_WEEK = 'week'; + /** Modální dialog pro generování mock dat (pouze DEV). */ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex }: Readonly) { - const [dayIndex, setDayIndex] = useState(currentDayIndex); + const [daySelection, setDaySelection] = useState(currentDayIndex !== undefined ? String(currentDayIndex) : ''); const [count, setCount] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const [success, setSuccess] = useState(false); + const [success, setSuccess] = useState(null); + + const wholeWeek = daySelection === WHOLE_WEEK; const handleGenerate = async () => { setError(null); @@ -24,8 +29,10 @@ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex try { const body: any = {}; - if (dayIndex !== undefined) { - body.dayIndex = dayIndex as DayIndex; + if (wholeWeek) { + body.wholeWeek = true; + } else if (daySelection !== '') { + body.dayIndex = parseInt(daySelection, 10) as DayIndex; } if (count && count.trim() !== '') { const countNum = parseInt(count, 10); @@ -41,10 +48,13 @@ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex if (response.error) { setError((response.error as any).error || 'Nastala chyba při generování dat'); } else { - setSuccess(true); + const total = (response.data as any)?.count; + setSuccess(wholeWeek + ? `Mock data byla vygenerována pro celý týden (celkem ${total} záznamů).` + : 'Mock data byla úspěšně vygenerována!'); setTimeout(() => { onClose(); - setSuccess(false); + setSuccess(null); setCount(''); }, 1500); } @@ -57,7 +67,7 @@ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex const handleClose = () => { setError(null); - setSuccess(false); + setSuccess(null); setCount(''); onClose(); }; @@ -70,7 +80,7 @@ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex {success ? ( - Mock data byla úspěšně vygenerována! + {success} ) : ( <> @@ -87,13 +97,14 @@ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex Den setDayIndex(e.target.value === '' ? undefined : parseInt(e.target.value, 10))} + value={daySelection} + onChange={e => setDaySelection(e.target.value)} > {DAY_NAMES.map((name, index) => ( ))} + Pokud není vybráno, použije se aktuální den. @@ -112,7 +123,9 @@ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex onKeyDown={e => e.stopPropagation()} /> - Pokud není zadáno, vybere se náhodný počet 5-20. + {wholeWeek + ? 'Zadaný počet se vygeneruje pro každý den týdne. Pokud není zadáno, vybere se pro každý den náhodný počet 5-20.' + : 'Pokud není zadáno, vybere se náhodný počet 5-20.'} diff --git a/server/src/routes/devRoutes.ts b/server/src/routes/devRoutes.ts index a33a131..a95d4d7 100644 --- a/server/src/routes/devRoutes.ts +++ b/server/src/routes/devRoutes.ts @@ -56,106 +56,154 @@ function requireDevMode(req: any, res: any, next: any) { router.use(requireDevMode); +/** Indexy všech pracovních dnů týdne (pondělí až pátek). */ +const WEEK_DAY_INDEXES = [0, 1, 2, 3, 4]; + /** - * Vygeneruje mock data pro testování. + * Vygeneruje mock data pro jeden den týdne. + * @returns počet skutečně vygenerovaných záznamů + */ +async function generateMockDataForDay(dayIndex: number, requestedCount?: number): Promise { + const count = requestedCount ?? Math.floor(Math.random() * 16) + 5; // 5-20 + + const date = getDateForWeekIndex(dayIndex); + await initIfNeeded(date); + + const dateKey = formatDate(date); + const data = await storage.getData(dateKey); + + // Získání menu restaurací pro vybraný den + const menus: { [key: string]: any } = {}; + for (const restaurant of RESTAURANTS_WITH_MENU) { + const menu = await getRestaurantMenu(restaurant as any, date); + if (menu?.food?.length) { + menus[restaurant] = menu.food; + } + } + + // Vygenerování náhodných uživatelů + const usedNames = new Set(); + for (let i = 0; i < count && usedNames.size < MOCK_NAMES.length; i++) { + // Vybereme náhodné jméno, které ještě nebylo použito + let name: string; + do { + name = MOCK_NAMES[Math.floor(Math.random() * MOCK_NAMES.length)]; + } while (usedNames.has(name)); + usedNames.add(name); + + // Vybereme náhodnou volbu stravování + const choice = LUNCH_CHOICES[Math.floor(Math.random() * LUNCH_CHOICES.length)]; + + // Inicializace struktury pro volbu + data.choices[choice] ??= {}; + + const userChoice: any = { + trusted: false, + selectedFoods: [], + }; + + // Pokud má restaurace menu, vybereme náhodné jídlo + if (RESTAURANTS_WITH_MENU.includes(choice) && menus[choice]?.length) { + const foods = menus[choice]; + // Vybereme náhodné jídlo (ne polévku) + const mainFoods = foods.filter((f: any) => !f.isSoup); + if (mainFoods.length > 0) { + const randomFoodIndex = foods.indexOf(mainFoods[Math.floor(Math.random() * mainFoods.length)]); + userChoice.selectedFoods = [randomFoodIndex]; + } + } + + data.choices[choice][name] = userChoice; + } + + await storage.setData(dateKey, data); + + return usedNames.size; +} + +/** Smaže všechny volby (a pizza day) pro jeden den týdne. */ +async function clearMockDataForDay(dayIndex: number): Promise { + const date = getDateForWeekIndex(dayIndex); + await initIfNeeded(date); + + const dateKey = formatDate(date); + const data = await storage.getData(dateKey); + + // Vymažeme všechny volby i aktivní pizza day + data.choices = {}; + delete data.pizzaDay; + + await storage.setData(dateKey, data); +} + +/** Rozešle klientům aktuální data pro dané datum. */ +async function broadcastDate(date: Date) { + const clientData = await getData(date); + getWebsocket().emit("message", clientData); +} + +/** + * Vygeneruje mock data pro testování - pro jeden den, nebo pro celý týden. */ router.post("/generate", async (req: Request<{}, any, any>, res, next) => { try { + const wholeWeek = req.body?.wholeWeek === true; + const count: number | undefined = req.body?.count; + + if (wholeWeek) { + // Pro každý den generujeme zvlášť - pokud není zadán počet, bude pro každý den náhodný + const days: { dayIndex: number, count: number }[] = []; + for (const dayIndex of WEEK_DAY_INDEXES) { + days.push({ dayIndex, count: await generateMockDataForDay(dayIndex, count) }); + } + + // Klientům stačí poslat data dnešního dne, ostatní si dotáhnou při přepnutí + await broadcastDate(getToday()); + + const total = days.reduce((sum, day) => sum + day.count, 0); + return res.status(200).json({ success: true, count: total, days }); + } + const dayIndex = req.body?.dayIndex ?? getDayOfWeekIndex(getToday()); - const count = req.body?.count ?? Math.floor(Math.random() * 16) + 5; // 5-20 if (dayIndex < 0 || dayIndex > 4) { return res.status(400).json({ error: 'Neplatný index dne (0-4)' }); } - const date = getDateForWeekIndex(dayIndex); - await initIfNeeded(date); + const generated = await generateMockDataForDay(dayIndex, count); + await broadcastDate(getDateForWeekIndex(dayIndex)); - const dateKey = formatDate(date); - const data = await storage.getData(dateKey); - - // Získání menu restaurací pro vybraný den - const menus: { [key: string]: any } = {}; - for (const restaurant of RESTAURANTS_WITH_MENU) { - const menu = await getRestaurantMenu(restaurant as any, date); - if (menu?.food?.length) { - menus[restaurant] = menu.food; - } - } - - // Vygenerování náhodných uživatelů - const usedNames = new Set(); - for (let i = 0; i < count && usedNames.size < MOCK_NAMES.length; i++) { - // Vybereme náhodné jméno, které ještě nebylo použito - let name: string; - do { - name = MOCK_NAMES[Math.floor(Math.random() * MOCK_NAMES.length)]; - } while (usedNames.has(name)); - usedNames.add(name); - - // Vybereme náhodnou volbu stravování - const choice = LUNCH_CHOICES[Math.floor(Math.random() * LUNCH_CHOICES.length)]; - - // Inicializace struktury pro volbu - data.choices[choice] ??= {}; - - const userChoice: any = { - trusted: false, - selectedFoods: [], - }; - - // Pokud má restaurace menu, vybereme náhodné jídlo - if (RESTAURANTS_WITH_MENU.includes(choice) && menus[choice]?.length) { - const foods = menus[choice]; - // Vybereme náhodné jídlo (ne polévku) - const mainFoods = foods.filter((f: any) => !f.isSoup); - if (mainFoods.length > 0) { - const randomFoodIndex = foods.indexOf(mainFoods[Math.floor(Math.random() * mainFoods.length)]); - userChoice.selectedFoods = [randomFoodIndex]; - } - } - - data.choices[choice][name] = userChoice; - } - - await storage.setData(dateKey, data); - - // Odeslat aktualizovaná data přes WebSocket - const clientData = await getData(date); - getWebsocket().emit("message", clientData); - - res.status(200).json({ success: true, count: usedNames.size, dayIndex }); + res.status(200).json({ success: true, count: generated, dayIndex }); } catch (e: any) { next(e); } }); /** - * Smaže všechny volby pro daný den. + * Smaže všechny volby pro daný den, nebo pro celý týden. */ router.post("/clear", async (req: Request<{}, any, any>, res, next) => { try { + const wholeWeek = req.body?.wholeWeek === true; + + if (wholeWeek) { + for (const dayIndex of WEEK_DAY_INDEXES) { + await clearMockDataForDay(dayIndex); + } + + await broadcastDate(getToday()); + + return res.status(200).json({ success: true, days: WEEK_DAY_INDEXES.map(dayIndex => ({ dayIndex })) }); + } + const dayIndex = req.body?.dayIndex ?? getDayOfWeekIndex(getToday()); if (dayIndex < 0 || dayIndex > 4) { return res.status(400).json({ error: 'Neplatný index dne (0-4)' }); } - const date = getDateForWeekIndex(dayIndex); - await initIfNeeded(date); - - const dateKey = formatDate(date); - const data = await storage.getData(dateKey); - - // Vymažeme všechny volby i aktivní pizza day - data.choices = {}; - delete data.pizzaDay; - - await storage.setData(dateKey, data); - - // Odeslat aktualizovaná data přes WebSocket - const clientData = await getData(date); - getWebsocket().emit("message", clientData); + await clearMockDataForDay(dayIndex); + await broadcastDate(getDateForWeekIndex(dayIndex)); res.status(200).json({ success: true, dayIndex }); } catch (e: any) { diff --git a/types/paths/dev/clear.yml b/types/paths/dev/clear.yml index cebe229..965fc30 100644 --- a/types/paths/dev/clear.yml +++ b/types/paths/dev/clear.yml @@ -1,6 +1,6 @@ post: operationId: clearMockData - summary: Smazání všech voleb pro daný den (pouze DEV režim) + summary: Smazání všech voleb pro daný den nebo celý týden (pouze DEV režim) requestBody: required: false content: @@ -18,6 +18,12 @@ post: success: type: boolean dayIndex: - type: integer + description: Index dne, pro který byla data smazána (jen při mazání jednoho dne) + $ref: "../../schemas/_index.yml#/DayIndex" + days: + description: Seznam dnů, pro které byla data smazána (jen při mazání celého týdne) + type: array + items: + $ref: "../../schemas/_index.yml#/MockDataDayResult" "403": description: Endpoint není dostupný v tomto režimu diff --git a/types/paths/dev/generate.yml b/types/paths/dev/generate.yml index a4b1ab4..15a2320 100644 --- a/types/paths/dev/generate.yml +++ b/types/paths/dev/generate.yml @@ -18,8 +18,15 @@ post: success: type: boolean count: + description: Celkový počet vygenerovaných záznamů type: integer dayIndex: - type: integer + description: Index dne, pro který byla data vygenerována (jen při generování pro jeden den) + $ref: "../../schemas/_index.yml#/DayIndex" + days: + description: Rozpis vygenerovaných záznamů po dnech (jen při generování pro celý týden) + type: array + items: + $ref: "../../schemas/_index.yml#/MockDataDayResult" "403": description: Endpoint není dostupný v tomto režimu diff --git a/types/schemas/_index.yml b/types/schemas/_index.yml index 619b2c6..aaa57ad 100644 --- a/types/schemas/_index.yml +++ b/types/schemas/_index.yml @@ -760,21 +760,40 @@ GenerateMockDataRequest: additionalProperties: false properties: dayIndex: - description: Index dne v týdnu (0 = pondělí, 4 = pátek). Pokud není zadán, použije se aktuální den. + description: Index dne v týdnu (0 = pondělí, 4 = pátek). Pokud není zadán, použije se aktuální den. Ignoruje se, pokud je nastaveno wholeWeek. $ref: "#/DayIndex" + wholeWeek: + description: Pokud je true, vygenerují se data pro všechny dny týdne (pondělí až pátek). + type: boolean count: - description: Počet záznamů k vygenerování. Pokud není zadán, vybere se náhodný počet 5-20. + description: Počet záznamů k vygenerování (u celého týdne pro každý den zvlášť). Pokud není zadán, vybere se pro každý den náhodný počet 5-20. type: integer minimum: 1 maximum: 100 +MockDataDayResult: + description: Výsledek generování/mazání mock dat pro jeden den + type: object + additionalProperties: false + required: + - dayIndex + properties: + dayIndex: + description: Index dne v týdnu, pro který byla akce provedena + $ref: "#/DayIndex" + count: + description: Počet vygenerovaných záznamů pro daný den + type: integer ClearMockDataRequest: description: Request pro smazání mock dat (pouze DEV režim) type: object additionalProperties: false properties: dayIndex: - description: Index dne v týdnu (0 = pondělí, 4 = pátek). Pokud není zadán, použije se aktuální den. + description: Index dne v týdnu (0 = pondělí, 4 = pátek). Pokud není zadán, použije se aktuální den. Ignoruje se, pokud je nastaveno wholeWeek. $ref: "#/DayIndex" + wholeWeek: + description: Pokud je true, smažou se data všech dnů týdne (pondělí až pátek). + type: boolean # --- DEV SIMULACE BOLT --- BoltSimGroupRequest: