feat: /objednani – skupinové objednávky s QR platbou
CI / Generate TypeScript types (push) Successful in 18s
CI / Generate TypeScript types (pull_request) Successful in 9s
CI / Server unit tests (push) Successful in 21s
CI / Build client (push) Successful in 40s
CI / Server unit tests (pull_request) Successful in 21s
CI / Build server (pull_request) Successful in 24s
CI / Build server (push) Has been cancelled
CI / Playwright E2E tests (push) Has been cancelled
CI / Build and push Docker image (push) Has been cancelled
CI / Notify (push) Has been cancelled
CI / Build client (pull_request) Has been cancelled
CI / Playwright E2E tests (pull_request) Has been cancelled
CI / Build and push Docker image (pull_request) Has been cancelled
CI / Notify (pull_request) Has been cancelled

Nahrazuje /vecere novou stránkou /objednani. Místo jednoho
OBJEDNAVAM bucketu umožňuje vytvářet více skupin, kde každá
objednává z jiného obchodu.

- Skupiny mají stavový automat: open → locked → ordered
- Obchody spravuje admin heslem (ADMIN_PASSWORD env var)
  přes modal „Správa obchodů"
- Při stavu ordered zakladatel generuje QR kódy platby
  (nový PayForGroupModal – volné částky bez menu)
- PayForAllModal (oběd) upraven: plátce nyní vidí svůj
  vlastní díl jako informační řádek
- Nové testy: stores.test.ts + groups.test.ts (36 testů)
This commit is contained in:
2026-05-07 07:05:01 +02:00
parent 774be3df6d
commit 936b33cc80
28 changed files with 1641 additions and 242 deletions
+93
View File
@@ -0,0 +1,93 @@
import express, { Request } from "express";
import { getLogin } from "../auth";
import { parseToken } from "../utils";
import { getWebsocket } from "../websocket";
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState } from "../groups";
import { GroupState } from "../../../types/gen/types.gen";
const router = express.Router();
function broadcastExtra(data: any) {
getWebsocket().emit("message", data);
}
router.post("/create", async (req: Request, res, next) => {
const login = getLogin(parseToken(req));
const { name } = req.body ?? {};
if (!name || typeof name !== 'string') {
return res.status(400).json({ error: 'Nebyl předán název skupiny' });
}
try {
const data = await createGroup(login, name);
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' });
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<string, any> = {};
if (amount !== undefined) patch.amount = amount;
if (note !== undefined) patch.note = note;
if (surchargeText !== undefined) patch.surchargeText = surchargeText;
if (surchargeAmount !== undefined) 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); }
});
export default router;