import express, { Request } from "express"; import { getLogin } from "../auth"; import { parseToken } from "../utils"; import { getWebsocket } from "../websocket"; import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, updateGroupFees, setGroupTracking, getOrderDates } from "../groups"; import { GroupState } from "../../../types/gen/types.gen"; import { checkOrderTracking } from "../orderTracking"; import { getGroupLimits } from "../limits"; const router = express.Router(); // Pojistky proti překlepům (dvojklik, omylem vložený text) — limity viz limits.ts (lze přepsat env) /** Vrátí chybovou hlášku, pokud částka (v haléřích) není celé číslo v rozsahu 0 až limit. */ function invalidAmount(value: unknown, label: string): string | undefined { const { maxAmountKc } = getGroupLimits(); if (!Number.isInteger(value) || (value as number) < 0) return `Neplatná výše: ${label}`; if ((value as number) > maxAmountKc * 100) return `${label} může být nejvýše ${maxAmountKc} Kč`; return undefined; } function broadcastExtra(data: any) { getWebsocket().emit("message", data); } router.get("/dates", async (_req, res, next) => { try { const dates = await getOrderDates(); res.status(200).json({ dates }); } catch (e: any) { next(e); } }); router.post("/create", async (req: Request, res, next) => { const login = getLogin(parseToken(req)); const { name, url } = req.body ?? {}; if (!name || typeof name !== 'string') { return res.status(400).json({ error: 'Nebyl předán název skupiny' }); } const limits = getGroupLimits(); if (name.length > limits.maxNameLength) { return res.status(400).json({ error: `Název skupiny může mít nejvýše ${limits.maxNameLength} znaků` }); } if (url != null && (typeof url !== 'string' || url.length > limits.maxUrlLength)) { return res.status(400).json({ error: 'Neplatná URL nabídky' }); } try { const data = await createGroup(login, name, undefined, url); broadcastExtra(data); res.status(200).json(data); } catch (e: any) { next(e); } }); router.post("/delete", async (req: Request, res, next) => { const login = getLogin(parseToken(req)); const { id } = req.body ?? {}; if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' }); try { const data = await deleteGroup(login, id); broadcastExtra(data); res.status(200).json(data); } catch (e: any) { next(e); } }); router.post("/addMember", async (req: Request, res, next) => { const login = getLogin(parseToken(req)); const { id, login: targetLogin } = req.body ?? {}; if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' }); if (targetLogin !== undefined && (typeof targetLogin !== 'string' || targetLogin.trim() === '')) { return res.status(400).json({ error: 'Neplatný login uživatele' }); } const target = targetLogin ?? login; try { const data = await addGroupMember(login, id, target); broadcastExtra(data); res.status(200).json(data); } catch (e: any) { next(e); } }); router.post("/removeMember", async (req: Request, res, next) => { const login = getLogin(parseToken(req)); const { id, login: targetLogin } = req.body ?? {}; if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' }); if (!targetLogin) return res.status(400).json({ error: 'Nebyl předán login uživatele' }); try { const data = await removeGroupMember(login, id, targetLogin); broadcastExtra(data); res.status(200).json(data); } catch (e: any) { next(e); } }); router.post("/updateMember", async (req: Request, res, next) => { const login = getLogin(parseToken(req)); const { id, login: targetLogin, amount, note, surchargeText, surchargeAmount } = req.body ?? {}; if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' }); if (!targetLogin) return res.status(400).json({ error: 'Nebyl předán login uživatele' }); const patch: Record = {}; if (amount !== undefined) { const error = invalidAmount(amount, 'Částka'); if (error) return res.status(400).json({ error }); patch.amount = amount; } if (note !== undefined) { if (typeof note !== 'string') return res.status(400).json({ error: 'Neplatná poznámka' }); const { maxNoteLength } = getGroupLimits(); if (note.length > maxNoteLength) { return res.status(400).json({ error: `Poznámka může mít nejvýše ${maxNoteLength} znaků` }); } patch.note = note; } if (surchargeText !== undefined) { if (typeof surchargeText !== 'string') return res.status(400).json({ error: 'Neplatný text příplatku' }); const { maxSurchargeTextLength } = getGroupLimits(); if (surchargeText.length > maxSurchargeTextLength) { return res.status(400).json({ error: `Popis příplatku může mít nejvýše ${maxSurchargeTextLength} znaků` }); } patch.surchargeText = surchargeText; } if (surchargeAmount !== undefined) { const error = invalidAmount(surchargeAmount, 'Příplatek'); if (error) return res.status(400).json({ error }); patch.surchargeAmount = surchargeAmount; } try { const data = await updateGroupMember(login, id, targetLogin, patch); broadcastExtra(data); res.status(200).json(data); } catch (e: any) { next(e); } }); router.post("/setState", async (req: Request, res, next) => { const login = getLogin(parseToken(req)); const { id, state } = req.body ?? {}; if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' }); if (!state || !Object.values(GroupState).includes(state)) { return res.status(400).json({ error: 'Neplatný stav skupiny' }); } try { const data = await setGroupState(login, id, state as GroupState); broadcastExtra(data); res.status(200).json(data); } catch (e: any) { next(e); } }); router.post("/updateFees", async (req: Request, res, next) => { const login = getLogin(parseToken(req)); const { id, fees, shipping, tip, discountType, discountValue } = req.body ?? {}; if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' }); for (const [value, label] of [[fees, 'Poplatky'], [shipping, 'Doprava'], [tip, 'Spropitné']] as const) { const error = value !== undefined ? invalidAmount(value, label) : undefined; if (error) return res.status(400).json({ error }); } if (discountType !== undefined && discountType !== '' && !['percent', 'fixed'].includes(discountType)) { return res.status(400).json({ error: 'Neplatný typ slevy' }); } if (discountValue !== undefined) { if (!Number.isInteger(discountValue) || discountValue < 0) { return res.status(400).json({ error: 'Neplatná výše slevy' }); } // Procentní sleva nad 100 % by vedla k záporným částkám if (discountType === 'percent' && discountValue > 100) { return res.status(400).json({ error: 'Procentní sleva může být nejvýše 100 %' }); } const { maxAmountKc } = getGroupLimits(); if (discountType !== 'percent' && discountValue > maxAmountKc * 100) { return res.status(400).json({ error: `Sleva může být nejvýše ${maxAmountKc} Kč` }); } } try { const data = await updateGroupFees(login, id, fees, shipping, tip, discountType, discountValue); broadcastExtra(data); res.status(200).json(data); } catch (e: any) { next(e); } }); router.post("/updateTimes", async (req: Request, res, next) => { const login = getLogin(parseToken(req)); const { id, orderedAt, deliveryAt } = req.body ?? {}; if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' }); const timeRegex = /^([01]\d|2[0-3]):[0-5]\d$/; if (orderedAt !== undefined && orderedAt !== '' && !timeRegex.test(orderedAt)) { return res.status(400).json({ error: 'Neplatný formát času objednání (očekáváno HH:MM)' }); } if (deliveryAt !== undefined && deliveryAt !== '' && !timeRegex.test(deliveryAt)) { return res.status(400).json({ error: 'Neplatný formát času doručení (očekáváno HH:MM)' }); } try { const data = await updateGroupTimes(login, id, orderedAt, deliveryAt); broadcastExtra(data); res.status(200).json(data); } catch (e: any) { next(e); } }); router.post("/setTracking", async (req: Request, res, next) => { const login = getLogin(parseToken(req)); const { id, shareUrl } = req.body ?? {}; if (!id) return res.status(400).json({ error: 'Nebylo předáno ID skupiny' }); if (shareUrl !== undefined && (typeof shareUrl !== 'string' || shareUrl.length > getGroupLimits().maxUrlLength)) { return res.status(400).json({ error: 'Neplatný odkaz na sledování objednávky' }); } try { const data = await setGroupTracking(login, id, shareUrl); broadcastExtra(data); res.status(200).json(data); // Okamžitý poll, ať uživatel nečeká na další tik scheduleru if (shareUrl) { checkOrderTracking().catch(e => console.error('Sledování objednávek: okamžitý poll selhal', e)); } } catch (e: any) { next(e); } }); export default router;