feat: večeře (extra meal slot)
CI / Generate TypeScript types (pull_request) Successful in 11s
CI / Generate TypeScript types (push) Successful in 36s
CI / Server unit tests (pull_request) Successful in 25s
CI / Build client (pull_request) Successful in 37s
CI / Server unit tests (push) Successful in 22s
CI / Build server (push) Successful in 1m0s
CI / Build client (push) Successful in 37s
CI / Build server (pull_request) Successful in 3m14s
CI / Playwright E2E tests (push) Successful in 1m18s
CI / Build and push Docker image (push) Has been skipped
CI / Notify (push) Successful in 2s
CI / Playwright E2E tests (pull_request) Successful in 10m34s
CI / Build and push Docker image (pull_request) Has been skipped
CI / Notify (pull_request) Has been skipped

- Nová stránka /vecere pro evidenci extra jídla (večeře/pozdní oběd)
- MealSlot enum (obed/extra), oddělený storage namespace YYYY-MM-DD_extra
- slot parametr na všech food endpointech a GET /api/data
- Push reminder: přechod na 60min cooldown, login v payloadu místo endpointu
- server: slot?: string → slot?: MealSlot, enum konstanty místo literálů
- Jest testy izolace extra/obed storage namespace
- Aktualizace changelogů (saláty, SINGLE_PAYMENT, večeře)
This commit is contained in:
2026-05-06 21:06:25 +02:00
parent 5f903797f1
commit 774be3df6d
22 changed files with 441 additions and 107 deletions
+1 -1
View File
@@ -189,7 +189,7 @@ router.post("/testPush", async (req, res, next) => {
webpush.setVapidDetails(subject, publicKey, privateKey);
await webpush.sendNotification(
entry.subscription,
JSON.stringify({ title: 'Luncher test', body: 'Push notifikace fungují!' })
JSON.stringify({ title: 'Luncher test', body: 'Push notifikace fungují!', login })
);
res.status(200).json({ ok: true });
} catch (e: any) { next(e) }
+24 -6
View File
@@ -4,7 +4,7 @@ import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices,
import { getDayOfWeekIndex, parseToken, getFirstWorkDayOfWeek } from "../utils";
import { getWebsocket } from "../websocket";
import { callNotifikace } from "../notifikace";
import { AddChoiceData, ChangeDepartureTimeData, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen";
import { AddChoiceData, ChangeDepartureTimeData, MealSlot, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen";
// RateLimit na refresh endpoint
@@ -69,11 +69,21 @@ const parseValidateFutureDayIndex = (req: Request<{}, any, AddChoiceData["body"]
return dayIndex;
}
const parseSlot = (body: Record<string, any>): MealSlot | undefined => {
const slot = body?.slot;
if (slot != null && slot !== MealSlot.OBED && slot !== MealSlot.EXTRA) {
throw Error(`Neplatný slot: ${slot}`);
}
return slot ?? undefined;
};
const router = express.Router();
router.post("/addChoice", async (req: Request<{}, any, AddChoiceData["body"]>, res, next) => {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
let slot: MealSlot | undefined;
try { slot = parseSlot(req.body); } catch (e: any) { return res.status(400).json({ error: e.message }); }
let date = undefined;
if (req.body.dayIndex != null) {
let dayIndex;
@@ -85,7 +95,7 @@ router.post("/addChoice", async (req: Request<{}, any, AddChoiceData["body"]>, r
date = getDateForWeekIndex(dayIndex);
}
try {
const data = await addChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date);
const data = await addChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date, slot);
getWebsocket().emit("message", data);
return res.status(200).json(data);
} catch (e: any) { next(e) }
@@ -94,6 +104,8 @@ router.post("/addChoice", async (req: Request<{}, any, AddChoiceData["body"]>, r
router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesData["body"]>, res, next) => {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
let slot: MealSlot | undefined;
try { slot = parseSlot(req.body); } catch (e: any) { return res.status(400).json({ error: e.message }); }
let date = undefined;
if (req.body.dayIndex != null) {
let dayIndex;
@@ -105,7 +117,7 @@ router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesData["bo
date = getDateForWeekIndex(dayIndex);
}
try {
const data = await removeChoices(login, trusted, req.body.locationKey, date);
const data = await removeChoices(login, trusted, req.body.locationKey, date, slot);
getWebsocket().emit("message", data);
res.status(200).json(data);
} catch (e: any) { next(e) }
@@ -114,6 +126,8 @@ router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesData["bo
router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceData["body"]>, res, next) => {
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
let slot: MealSlot | undefined;
try { slot = parseSlot(req.body); } catch (e: any) { return res.status(400).json({ error: e.message }); }
let date = undefined;
if (req.body.dayIndex != null) {
let dayIndex;
@@ -125,7 +139,7 @@ router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceData["body
date = getDateForWeekIndex(dayIndex);
}
try {
const data = await removeChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date);
const data = await removeChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date, slot);
getWebsocket().emit("message", data);
res.status(200).json(data);
} catch (e: any) { next(e) }
@@ -135,6 +149,8 @@ router.post("/updateNote", async (req: Request<{}, any, UpdateNoteData["body"]>,
const login = getLogin(parseToken(req));
const trusted = getTrusted(parseToken(req));
const note = req.body.note;
let slot: MealSlot | undefined;
try { slot = parseSlot(req.body); } catch (e: any) { return res.status(400).json({ error: e.message }); }
try {
if (note && note.length > 70) {
throw Error("Poznámka může mít maximálně 70 znaků");
@@ -149,7 +165,7 @@ router.post("/updateNote", async (req: Request<{}, any, UpdateNoteData["body"]>,
}
date = getDateForWeekIndex(dayIndex);
}
const data = await updateNote(login, trusted, note, date);
const data = await updateNote(login, trusted, note, date, slot);
getWebsocket().emit("message", data);
res.status(200).json(data);
} catch (e: any) { next(e) }
@@ -184,8 +200,10 @@ router.post("/jdemeObed", async (req, res, next) => {
router.post("/updateBuyer", async (req, res, next) => {
const login = getLogin(parseToken(req));
let slot: MealSlot | undefined;
try { slot = parseSlot(req.body ?? {}); } catch (e: any) { return res.status(400).json({ error: e.message }); }
try {
const data = await updateBuyer(login);
const data = await updateBuyer(login, slot);
getWebsocket().emit("message", data);
res.status(200).json({});
} catch (e: any) { next(e) }
+3 -10
View File
@@ -2,7 +2,7 @@ import express, { Request } from "express";
import { getLogin } from "../auth";
import { parseToken } from "../utils";
import { getNotificationSettings, saveNotificationSettings } from "../notifikace";
import { subscribePush, unsubscribePush, getVapidPublicKey, findLoginByEndpoint } from "../pushReminder";
import { subscribePush, unsubscribePush, getVapidPublicKey } from "../pushReminder";
import { addChoice } from "../service";
import { getWebsocket } from "../websocket";
import { UpdateNotificationSettingsData } from "../../../types";
@@ -66,17 +66,10 @@ router.post("/push/unsubscribe", async (req, res, next) => {
} catch (e: any) { next(e) }
});
/** Rychlá akce z push notifikace — nastaví volbu bez JWT (identita přes subscription endpoint). */
/** Rychlá akce z push notifikace — nastaví volbu NEOBEDVAM pro přihlášeného uživatele. */
router.post("/push/quickChoice", async (req, res, next) => {
try {
const { endpoint } = req.body;
if (!endpoint) {
return res.status(400).json({ error: "Nebyl předán endpoint" });
}
const login = await findLoginByEndpoint(endpoint);
if (!login) {
return res.status(404).json({ error: "Subscription nenalezena" });
}
const login = getLogin(parseToken(req));
const data = await addChoice(login, false, 'NEOBEDVAM', undefined, undefined);
getWebsocket().emit("message", data);
res.status(200).json({});