feat: lepší generování mock dat při vývoji
CI / Generate TypeScript types (push) Successful in 11s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 28s
CI / Build client (push) Successful in 41s
CI / Playwright E2E tests (push) Successful in 1m45s
CI / Build and push Docker image (push) Successful in 46s
CI / Notify (push) Successful in 2s

This commit is contained in:
2026-08-25 10:31:32 +02:00
parent 9027de5f8b
commit 93242b7c7e
7 changed files with 221 additions and 98 deletions
+1 -1
View File
@@ -184,7 +184,7 @@ export default function Header({ choices, dayIndex }: Props) {
<> <>
<NavDropdown.Divider /> <NavDropdown.Divider />
<NavDropdown.Item onClick={() => setGenerateMockModalOpen(true)}>🔧 Generovat mock data</NavDropdown.Item> <NavDropdown.Item onClick={() => setGenerateMockModalOpen(true)}>🔧 Generovat mock data</NavDropdown.Item>
<NavDropdown.Item onClick={() => setClearMockModalOpen(true)}>🔧 Smazat data dne</NavDropdown.Item> <NavDropdown.Item onClick={() => setClearMockModalOpen(true)}>🔧 Smazat mock data</NavDropdown.Item>
</> </>
)} )}
<NavDropdown.Divider /> <NavDropdown.Divider />
@@ -1,5 +1,5 @@
import { useState } from "react"; import { useState } from "react";
import { Modal, Button, Alert } from "react-bootstrap"; import { Modal, Button, Form, Alert } from "react-bootstrap";
import { clearMockData, DayIndex } from "../../../../types"; import { clearMockData, DayIndex } from "../../../../types";
type Props = { type Props = {
@@ -10,20 +10,28 @@ type Props = {
const DAY_NAMES = ['Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek']; 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). */ /** Modální dialog pro smazání mock dat (pouze DEV). */
export default function ClearMockDataModal({ isOpen, onClose, currentDayIndex }: Readonly<Props>) { export default function ClearMockDataModal({ isOpen, onClose, currentDayIndex }: Readonly<Props>) {
const [daySelection, setDaySelection] = useState<string>(currentDayIndex !== undefined ? String(currentDayIndex) : '');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
const wholeWeek = daySelection === WHOLE_WEEK;
const handleClear = async () => { const handleClear = async () => {
setError(null); setError(null);
setLoading(true); setLoading(true);
try { try {
const body: any = {}; const body: any = {};
if (currentDayIndex !== undefined) { if (wholeWeek) {
body.dayIndex = currentDayIndex as DayIndex; body.wholeWeek = true;
} else if (daySelection !== '') {
body.dayIndex = parseInt(daySelection, 10) as DayIndex;
} }
const response = await clearMockData({ body }); const response = await clearMockData({ body });
@@ -49,12 +57,20 @@ export default function ClearMockDataModal({ isOpen, onClose, currentDayIndex }:
onClose(); 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 ( return (
<Modal show={isOpen} onHide={handleClose}> <Modal show={isOpen} onHide={handleClose}>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title><h2>Smazat data</h2></Modal.Title> <Modal.Title><h2>Smazat mock data</h2></Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
{success ? ( {success ? (
@@ -73,8 +89,22 @@ export default function ClearMockDataModal({ isOpen, onClose, currentDayIndex }:
</Alert> </Alert>
)} )}
<Form.Group className="mb-3">
<Form.Label>Den</Form.Label>
<Form.Select
value={daySelection}
onChange={e => setDaySelection(e.target.value)}
>
<option value="">Aktuální den</option>
{DAY_NAMES.map((name, index) => (
<option key={index} value={index}>{name}</option>
))}
<option value={WHOLE_WEEK}>Celý týden</option>
</Form.Select>
</Form.Group>
<p> <p>
Opravdu chcete smazat všechny volby stravování pro <strong>{dayName}</strong>? Opravdu chcete smazat všechny volby stravování pro <strong>{scopeLabel}</strong>?
</p> </p>
<p className="text-muted"> <p className="text-muted">
Tato akce je nevratná. Tato akce je nevratná.
@@ -10,13 +10,18 @@ type Props = {
const DAY_NAMES = ['Pondělí', 'Úterý', 'Středa', 'Čtvrtek', 'Pátek']; 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). */ /** Modální dialog pro generování mock dat (pouze DEV). */
export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex }: Readonly<Props>) { export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex }: Readonly<Props>) {
const [dayIndex, setDayIndex] = useState<number | undefined>(currentDayIndex); const [daySelection, setDaySelection] = useState<string>(currentDayIndex !== undefined ? String(currentDayIndex) : '');
const [count, setCount] = useState<string>(''); const [count, setCount] = useState<string>('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState<string | null>(null);
const wholeWeek = daySelection === WHOLE_WEEK;
const handleGenerate = async () => { const handleGenerate = async () => {
setError(null); setError(null);
@@ -24,8 +29,10 @@ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex
try { try {
const body: any = {}; const body: any = {};
if (dayIndex !== undefined) { if (wholeWeek) {
body.dayIndex = dayIndex as DayIndex; body.wholeWeek = true;
} else if (daySelection !== '') {
body.dayIndex = parseInt(daySelection, 10) as DayIndex;
} }
if (count && count.trim() !== '') { if (count && count.trim() !== '') {
const countNum = parseInt(count, 10); const countNum = parseInt(count, 10);
@@ -41,10 +48,13 @@ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex
if (response.error) { if (response.error) {
setError((response.error as any).error || 'Nastala chyba při generování dat'); setError((response.error as any).error || 'Nastala chyba při generování dat');
} else { } 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(() => { setTimeout(() => {
onClose(); onClose();
setSuccess(false); setSuccess(null);
setCount(''); setCount('');
}, 1500); }, 1500);
} }
@@ -57,7 +67,7 @@ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex
const handleClose = () => { const handleClose = () => {
setError(null); setError(null);
setSuccess(false); setSuccess(null);
setCount(''); setCount('');
onClose(); onClose();
}; };
@@ -70,7 +80,7 @@ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex
<Modal.Body> <Modal.Body>
{success ? ( {success ? (
<Alert variant="success"> <Alert variant="success">
Mock data byla úspěšně vygenerována! {success}
</Alert> </Alert>
) : ( ) : (
<> <>
@@ -87,13 +97,14 @@ export default function GenerateMockDataModal({ isOpen, onClose, currentDayIndex
<Form.Group className="mb-3"> <Form.Group className="mb-3">
<Form.Label>Den</Form.Label> <Form.Label>Den</Form.Label>
<Form.Select <Form.Select
value={dayIndex ?? ''} value={daySelection}
onChange={e => setDayIndex(e.target.value === '' ? undefined : parseInt(e.target.value, 10))} onChange={e => setDaySelection(e.target.value)}
> >
<option value="">Aktuální den</option> <option value="">Aktuální den</option>
{DAY_NAMES.map((name, index) => ( {DAY_NAMES.map((name, index) => (
<option key={index} value={index}>{name}</option> <option key={index} value={index}>{name}</option>
))} ))}
<option value={WHOLE_WEEK}>Celý týden</option>
</Form.Select> </Form.Select>
<Form.Text className="text-muted"> <Form.Text className="text-muted">
Pokud není vybráno, použije se aktuální den. 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()} onKeyDown={e => e.stopPropagation()}
/> />
<Form.Text className="text-muted"> <Form.Text className="text-muted">
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.'}
</Form.Text> </Form.Text>
</Form.Group> </Form.Group>
</> </>
+122 -74
View File
@@ -56,106 +56,154 @@ function requireDevMode(req: any, res: any, next: any) {
router.use(requireDevMode); 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<number> {
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<any>(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<string>();
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<void> {
const date = getDateForWeekIndex(dayIndex);
await initIfNeeded(date);
const dateKey = formatDate(date);
const data = await storage.getData<any>(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) => { router.post("/generate", async (req: Request<{}, any, any>, res, next) => {
try { 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 dayIndex = req.body?.dayIndex ?? getDayOfWeekIndex(getToday());
const count = req.body?.count ?? Math.floor(Math.random() * 16) + 5; // 5-20
if (dayIndex < 0 || dayIndex > 4) { if (dayIndex < 0 || dayIndex > 4) {
return res.status(400).json({ error: 'Neplatný index dne (0-4)' }); return res.status(400).json({ error: 'Neplatný index dne (0-4)' });
} }
const date = getDateForWeekIndex(dayIndex); const generated = await generateMockDataForDay(dayIndex, count);
await initIfNeeded(date); await broadcastDate(getDateForWeekIndex(dayIndex));
const dateKey = formatDate(date); res.status(200).json({ success: true, count: generated, dayIndex });
const data = await storage.getData<any>(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<string>();
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 });
} catch (e: any) { } catch (e: any) {
next(e); 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) => { router.post("/clear", async (req: Request<{}, any, any>, res, next) => {
try { 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()); const dayIndex = req.body?.dayIndex ?? getDayOfWeekIndex(getToday());
if (dayIndex < 0 || dayIndex > 4) { if (dayIndex < 0 || dayIndex > 4) {
return res.status(400).json({ error: 'Neplatný index dne (0-4)' }); return res.status(400).json({ error: 'Neplatný index dne (0-4)' });
} }
const date = getDateForWeekIndex(dayIndex); await clearMockDataForDay(dayIndex);
await initIfNeeded(date); await broadcastDate(getDateForWeekIndex(dayIndex));
const dateKey = formatDate(date);
const data = await storage.getData<any>(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);
res.status(200).json({ success: true, dayIndex }); res.status(200).json({ success: true, dayIndex });
} catch (e: any) { } catch (e: any) {
+8 -2
View File
@@ -1,6 +1,6 @@
post: post:
operationId: clearMockData 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: requestBody:
required: false required: false
content: content:
@@ -18,6 +18,12 @@ post:
success: success:
type: boolean type: boolean
dayIndex: 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": "403":
description: Endpoint není dostupný v tomto režimu description: Endpoint není dostupný v tomto režimu
+8 -1
View File
@@ -18,8 +18,15 @@ post:
success: success:
type: boolean type: boolean
count: count:
description: Celkový počet vygenerovaných záznamů
type: integer type: integer
dayIndex: 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": "403":
description: Endpoint není dostupný v tomto režimu description: Endpoint není dostupný v tomto režimu
+22 -3
View File
@@ -760,21 +760,40 @@ GenerateMockDataRequest:
additionalProperties: false additionalProperties: false
properties: properties:
dayIndex: 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" $ref: "#/DayIndex"
wholeWeek:
description: Pokud je true, vygenerují se data pro všechny dny týdne (pondělí až pátek).
type: boolean
count: 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 type: integer
minimum: 1 minimum: 1
maximum: 100 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: ClearMockDataRequest:
description: Request pro smazání mock dat (pouze DEV režim) description: Request pro smazání mock dat (pouze DEV režim)
type: object type: object
additionalProperties: false additionalProperties: false
properties: properties:
dayIndex: 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" $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 --- # --- DEV SIMULACE BOLT ---
BoltSimGroupRequest: BoltSimGroupRequest: