import express, { Request } from "express"; import { getDateForWeekIndex, getData, isRestaurantDisabled, getActiveRestaurants, getRestaurantMenu, getToday, initIfNeeded } from "../service"; import { formatDate, getDayOfWeekIndex } from "../utils"; import getStorage from "../storage"; import { getWebsocket } from "../websocket"; import { getLogin } from "../auth"; import { parseToken } from "../utils"; import webpush from 'web-push'; import { ClientData, GroupState, MealSlot, OrderGroupMember, Restaurant, TrackingProvider } from "../../../types/gen/types.gen"; import { getStores } from "../stores"; import { createGroup } from "../groups"; import { startBoltSimulation, advanceBoltSimulation, setBoltSimulationStep, stopBoltSimulationByGroup, getBoltSimulation, } from "../boltSimulator"; import { checkOrderTracking } from "../orderTracking"; const router = express.Router(); const storage = getStorage(); const ENVIRONMENT = process.env.NODE_ENV ?? 'production'; // Seznam náhodných jmen pro generování mock dat const MOCK_NAMES = [ 'Alice', 'Bob', 'Charlie', 'David', 'Eva', 'Filip', 'Gita', 'Honza', 'Ivana', 'Jakub', 'Kamila', 'Lukáš', 'Markéta', 'Nikola', 'Ondřej', 'Petra', 'Quido', 'Radek', 'Simona', 'Tomáš', 'Ursula', 'Viktor', 'Wanda', 'Xaver', 'Yvona', 'Zdeněk', 'Aneta', 'Boris', 'Cecílie', 'Daniel' ]; // Volby stravování pro mock data const LUNCH_CHOICES = [ 'SLADOVNICKA', 'TECHTOWER', 'ZASTAVKAUMICHALA', 'SENKSERIKOVA', 'OBJEDNAVAM', 'NEOBEDVAM', 'ROZHODUJI', ].filter(choice => !isRestaurantDisabled(choice as Restaurant)); // Náhodné názvy jídel do poznámek mock objednávek const MOCK_ORDER_FOODS = [ 'Malá kuřecí tortilla', 'Jalapenos burger', 'Chicken Palak', 'Pad Thai s kuřecím', 'Bún bò nam bo', 'Trhané hovězí v bulce', 'Caesar salát s kuřetem', 'Kuřecí vindaloo', 'Smažený sýr s hranolkami', 'Pizza Diavola', 'Burrito s trhaným masem', 'Ramen s vepřovým bůčkem', 'Falafel wrap v pitě', 'Krůtí steak s bramborovou kaší', 'Gyros se zeleninou', 'Losos s grilovanou zeleninou', 'Svíčková na smetaně s knedlíkem', 'Kuřecí kung pao', 'Poke bowl s tuňákem', 'Vepřový řízek s bramborovým salátem', ]; /** Výchozí hodnoty dialogu pro generování mock objednávek. */ const DEFAULT_ORDER_MEMBER_COUNT = 3; const DEFAULT_MIN_AMOUNT = 100; const DEFAULT_MAX_AMOUNT = 1000; /** Nejvyšší povolený počet osob doplněných do jedné skupiny. */ const MAX_ORDER_MEMBER_COUNT = 50; /** Nejvyšší povolená částka (v Kč) — musí odpovídat OpenAPI definici. */ const MAX_ORDER_AMOUNT = 100000; // Restaurace s menu const RESTAURANTS_WITH_MENU = getActiveRestaurants(); /** * Middleware pro kontrolu DEV režimu */ function requireDevMode(req: any, res: any, next: any) { if (ENVIRONMENT !== 'development' && ENVIRONMENT !== 'test') { return res.status(403).json({ error: 'Tento endpoint je dostupný pouze ve vývojovém režimu' }); } next(); } 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 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 as Restaurant) && 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()); if (dayIndex < 0 || dayIndex > 4) { return res.status(400).json({ error: 'Neplatný index dne (0-4)' }); } const generated = await generateMockDataForDay(dayIndex, count); await broadcastDate(getDateForWeekIndex(dayIndex)); res.status(200).json({ success: true, count: generated, dayIndex }); } catch (e: any) { next(e); } }); /** * 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)' }); } await clearMockDataForDay(dayIndex); await broadcastDate(getDateForWeekIndex(dayIndex)); res.status(200).json({ success: true, dayIndex }); } catch (e: any) { next(e); } }); /** Vrátí obsah push reminder registry (pro ladění). */ router.get("/pushRegistry", async (_req, res, next) => { try { const registry = await storage.getData('push_reminder_registry') ?? {}; const sanitized = Object.fromEntries( Object.entries(registry).map(([login, entry]: [string, any]) => [ login, { time: entry.time, endpoint: entry.subscription?.endpoint?.slice(0, 60) + '…' } ]) ); res.status(200).json(sanitized); } catch (e: any) { next(e) } }); /** Okamžitě odešle test push notifikaci přihlášenému uživateli (pro ladění). */ router.post("/testPush", async (req, res, next) => { const login = getLogin(parseToken(req)); try { const registry = await storage.getData('push_reminder_registry') ?? {}; const entry = registry[login]; if (!entry) { return res.status(404).json({ error: `Uživatel ${login} nemá uloženou push subscription. Nastav připomínku v nastavení.` }); } const publicKey = process.env.VAPID_PUBLIC_KEY; const privateKey = process.env.VAPID_PRIVATE_KEY; const subject = process.env.VAPID_SUBJECT; if (!publicKey || !privateKey || !subject) { return res.status(503).json({ error: 'VAPID klíče nejsou nastaveny' }); } webpush.setVapidDetails(subject, publicKey, privateKey); await webpush.sendNotification( entry.subscription, JSON.stringify({ title: 'Luncher test', body: 'Push notifikace fungují!', login }) ); res.status(200).json({ ok: true }); } catch (e: any) { next(e) } }); // --- DEV mock data pro sekci Objednání --- /** Vrátí náhodné celé číslo z intervalu . */ function randomInt(min: number, max: number): number { return min + Math.floor(Math.random() * (max - min + 1)); } /** Vrátí náhodný prvek neprázdného pole. */ function randomItem(items: Type[]): Type { return items[randomInt(0, items.length - 1)]; } /** * Vybere jméno, které ve skupině ještě není použité. * Po vyčerpání seznamu jmen přidává číselný sufix (Alice2, Alice3, …). */ function pickMemberName(members: Record): string { const free = MOCK_NAMES.filter(name => !members[name]); if (free.length > 0) { return randomItem(free); } const base = randomItem(MOCK_NAMES); let suffix = 2; while (members[`${base}${suffix}`]) { suffix++; } return `${base}${suffix}`; } /** Rozešle klientům aktuální data sekce Objednání (dnešní den). */ async function broadcastExtra() { getWebsocket().emit("message", await getData(getToday(), MealSlot.EXTRA)); } /** * Vygeneruje mock data pro sekci Objednání — vždy jen pro aktuální den. * Do každé existující skupiny doplní zadaný počet osob s náhodnou částkou * a názvem náhodného jídla v poznámce. Pokud žádná skupina neexistuje, * založí jednu z náhodně vybraného obchodu. */ router.post("/generateOrders", async (req: Request<{}, any, any>, res, next) => { try { const login = getLogin(parseToken(req)); const count: number = req.body?.count ?? DEFAULT_ORDER_MEMBER_COUNT; const minAmount: number = req.body?.minAmount ?? DEFAULT_MIN_AMOUNT; const maxAmount: number = req.body?.maxAmount ?? DEFAULT_MAX_AMOUNT; if (!Number.isInteger(count) || count < 1 || count > MAX_ORDER_MEMBER_COUNT) { return res.status(400).json({ error: `Počet osob musí být celé číslo mezi 1 a ${MAX_ORDER_MEMBER_COUNT}` }); } if (!Number.isInteger(minAmount) || !Number.isInteger(maxAmount) || minAmount < 0 || maxAmount > MAX_ORDER_AMOUNT || minAmount > maxAmount) { return res.status(400).json({ error: 'Neplatný rozsah částek' }); } const stores = await getStores(); if (stores.length === 0) { return res.status(400).json({ error: 'Není nadefinován žádný obchod — mock data pro objednávání nelze vygenerovat' }); } const key = `${formatDate(getToday())}_extra`; const existing = await storage.getData(key); let createdGroupId: string | undefined; // Bez skupiny není kam objednávky doplnit — jednu založíme z náhodně vybraného obchodu if (!existing?.groups?.length) { const store = randomItem(stores); const urls = store.urls ?? []; const created = await createGroup(login, store.name, undefined, urls.length > 0 ? randomItem(urls) : undefined); createdGroupId = created.groups?.[created.groups.length - 1]?.id; } const results: { id: string, name: string, added: number, created?: boolean }[] = []; await storage.updateData(key, current => { const data = current!; // Mutátor může být (u Redis) zavolán opakovaně — výsledky proto vždy sestavíme znovu results.length = 0; for (const group of data.groups ?? []) { for (let i = 0; i < count; i++) { group.members[pickMemberName(group.members)] = { amount: randomInt(minAmount, maxAmount) * 100, note: randomItem(MOCK_ORDER_FOODS), }; } results.push({ id: group.id, name: group.name, added: count, ...(group.id === createdGroupId ? { created: true } : {}), }); } return data; }); await broadcastExtra(); res.status(200).json({ success: true, count: results.reduce((sum, group) => sum + group.added, 0), groups: results, }); } catch (e: any) { next(e); } }); // --- DEV simulace sledování Bolt Food (Wolt simulaci zatím nemá) --- /** Nastaví/zruší sledovací token skupiny a zajistí stav ORDERED. Vrátí aktualizovaná data. */ async function applyBoltToken(groupId: string, token: string | undefined): Promise { const key = `${formatDate(getToday())}_extra`; return storage.updateData(key, current => { const d = current; const group = d?.groups?.find(g => g.id === groupId); if (!group) throw new Error('Skupina nebyla nalezena'); group.trackingProvider = token ? TrackingProvider.BOLT : undefined; group.trackingCode = token; if (token) { group.state = GroupState.ORDERED; } else { group.trackingOrderState = undefined; group.trackingCourierState = undefined; } return d!; }); } /** Spustí simulaci sledování Bolt pro skupinu a provede první poll. */ router.post("/bolt/simulate", async (req: Request<{}, any, any>, res, next) => { try { const groupId = req.body?.groupId; if (!groupId) return res.status(400).json({ error: 'Chybí groupId' }); const token = startBoltSimulation(groupId); await applyBoltToken(groupId, token); await checkOrderTracking(); // okamžitý první poll → stav "accepted" + websocket res.status(200).json({ success: true, token, simulation: getBoltSimulation(groupId) }); } catch (e: any) { next(e); } }); /** Posune simulaci na další krok a přepošle aktualizovaný stav klientům. */ router.post("/bolt/advance", async (req: Request<{}, any, any>, res, next) => { try { const groupId = req.body?.groupId; if (!groupId) return res.status(400).json({ error: 'Chybí groupId' }); advanceBoltSimulation(groupId); await checkOrderTracking(); res.status(200).json({ success: true, simulation: getBoltSimulation(groupId) }); } catch (e: any) { next(e); } }); /** Nastaví konkrétní stav simulace (ruční override, např. pro stav "cancelled"). */ router.post("/bolt/state", async (req: Request<{}, any, any>, res, next) => { try { const { groupId, order_state, courier_state, etaSeconds } = req.body ?? {}; if (!groupId || !order_state) return res.status(400).json({ error: 'Chybí groupId nebo order_state' }); setBoltSimulationStep(groupId, { order_state, courier_state, etaSeconds }); await checkOrderTracking(); res.status(200).json({ success: true, simulation: getBoltSimulation(groupId) }); } catch (e: any) { next(e); } }); /** Spustí jeden tik scheduleru okamžitě (bez čekání na interval). */ router.post("/bolt/poll", async (_req, res, next) => { try { await checkOrderTracking(); res.status(200).json({ success: true }); } catch (e: any) { next(e); } }); /** Ukončí simulaci skupiny a odebere sledovací token. */ router.delete("/bolt/simulate", async (req: Request<{}, any, any>, res, next) => { try { const groupId = req.body?.groupId; if (!groupId) return res.status(400).json({ error: 'Chybí groupId' }); stopBoltSimulationByGroup(groupId); const updated = await applyBoltToken(groupId, undefined); getWebsocket()?.emit('message', updated); res.status(200).json({ success: true }); } catch (e: any) { next(e); } }); export default router;