feat: zvýraznění dnů v historii obsahujících objednávky
CI / Generate TypeScript types (push) Successful in 12s
CI / Server unit tests (push) Successful in 20s
CI / Build server (push) Successful in 24s
CI / Build client (push) Successful in 36s
CI / Playwright E2E tests (push) Successful in 1m17s
CI / Build and push Docker image (push) Successful in 40s
CI / Notify (push) Successful in 2s

This commit is contained in:
2026-06-05 14:50:33 +02:00
parent fb84bff687
commit f28f127a92
13 changed files with 284 additions and 12 deletions
+1
View File
@@ -18,6 +18,7 @@
"bootstrap": "^5.3.8", "bootstrap": "^5.3.8",
"react": "^19.2.0", "react": "^19.2.0",
"react-bootstrap": "^2.10.10", "react-bootstrap": "^2.10.10",
"react-datepicker": "^9.1.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-jwt": "^1.3.0", "react-jwt": "^1.3.0",
"react-modal": "^3.16.3", "react-modal": "^3.16.3",
+91 -3
View File
@@ -284,9 +284,97 @@ body {
margin-bottom: 16px; margin-bottom: 16px;
gap: 16px; gap: 16px;
input[type="date"] { // react-datepicker obaluje input do wrapperu necháme ho zabrat jen potřebnou šířku
text-align: center; .react-datepicker-wrapper {
font-weight: 600; width: auto;
}
.order-date-input {
width: 160px;
cursor: pointer;
}
}
// Zvýraznění dnů, ve kterých existuje alespoň jedna objednávka tečka pod číslem dne
.react-datepicker__day.luncher-order-day {
position: relative;
font-weight: 700;
&::after {
content: "";
position: absolute;
left: 50%;
bottom: 2px;
transform: translateX(-50%);
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--luncher-primary, #0d6efd);
}
// U vybraného dne (tmavé pozadí) je tečka světlá, aby byla vidět
&.react-datepicker__day--selected::after,
&.react-datepicker__day--keyboard-selected::after {
background: #fff;
}
}
// Vybraný den používá akcentovou barvu aplikace (v obou režimech), místo výchozí modré
.react-datepicker__day--selected,
.react-datepicker__day--keyboard-selected {
background-color: var(--luncher-primary);
color: #fff;
&:hover {
background-color: var(--luncher-primary-hover);
}
}
// Tmavý režim kalendáře (react-datepicker) navázáno na CSS proměnné motivu
[data-bs-theme="dark"] {
.react-datepicker {
background-color: var(--luncher-bg-card);
border-color: var(--luncher-border);
color: var(--luncher-text);
}
.react-datepicker__header {
background-color: var(--luncher-bg-hover);
border-bottom-color: var(--luncher-border);
}
.react-datepicker__current-month,
.react-datepicker__day-name,
.react-datepicker__day,
.react-datepicker-year-header {
color: var(--luncher-text);
}
.react-datepicker__day:hover,
.react-datepicker__month-text:hover {
background-color: var(--luncher-bg-hover);
}
.react-datepicker__day--today {
color: var(--luncher-primary);
}
.react-datepicker__day--disabled,
.react-datepicker__day--outside-month {
color: var(--luncher-text-muted);
}
// Šipky pro přepínání měsíců
.react-datepicker__navigation-icon::before {
border-color: var(--luncher-text-secondary);
}
// Špička popoveru (SVG) míří do hlavičky sladíme barvy.
// !important kvůli vyšší specificitě knihovního pravidla [data-placement].
.react-datepicker__triangle {
fill: var(--luncher-bg-hover) !important;
color: var(--luncher-bg-hover) !important;
stroke: var(--luncher-border) !important;
} }
} }
+42 -8
View File
@@ -3,9 +3,12 @@ import { Alert, Badge, Button, Card, Form, Modal, OverlayTrigger, Table, Tooltip
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrashCan } from '@fortawesome/free-regular-svg-icons'; import { faTrashCan } from '@fortawesome/free-regular-svg-icons';
import { faBasketShopping, faChevronLeft, faChevronRight, faCircleCheck, faClockRotateLeft, faGear, faLock, faLockOpen, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons'; import { faBasketShopping, faChevronLeft, faChevronRight, faCircleCheck, faClockRotateLeft, faGear, faLock, faLockOpen, faSearch, faUserPlus } from '@fortawesome/free-solid-svg-icons';
import DatePicker, { registerLocale } from 'react-datepicker';
import { cs } from 'date-fns/locale';
import 'react-datepicker/dist/react-datepicker.css';
import { import {
ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember, PendingQr, ClientData, GroupState, MealSlot, OrderGroup, OrderGroupMember, PendingQr,
getData, createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, getData, createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, getOrderDates,
} from '../../../types'; } from '../../../types';
import { computeFeeShare, computeMemberTotal, countActiveMembers } from '../utils/groupFees'; import { computeFeeShare, computeMemberTotal, countActiveMembers } from '../utils/groupFees';
import { EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from '../context/socket'; import { EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from '../context/socket';
@@ -24,6 +27,9 @@ import PendingPayments from '../components/PendingPayments';
const SLOT = MealSlot.EXTRA; const SLOT = MealSlot.EXTRA;
const TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/; const TIME_REGEX = /^([01]\d|2[0-3]):[0-5]\d$/;
// Český lokál pro date picker (názvy měsíců/dnů, pondělí jako první den)
registerLocale('cs', cs);
/** Vrátí ISO datum (YYYY-MM-DD) posunuté o předaný počet dní. */ /** Vrátí ISO datum (YYYY-MM-DD) posunuté o předaný počet dní. */
function shiftIsoDate(iso: string, days: number): string { function shiftIsoDate(iso: string, days: number): string {
const date = new Date(`${iso}T00:00:00`); const date = new Date(`${iso}T00:00:00`);
@@ -31,6 +37,11 @@ function shiftIsoDate(iso: string, days: number): string {
return formatDate(date); return formatDate(date);
} }
/** Převede ISO datum (YYYY-MM-DD) na lokální Date (půlnoc), nebo null. */
function isoToDate(iso?: string): Date | null {
return iso ? new Date(`${iso}T00:00:00`) : null;
}
function stateBadge(state: GroupState) { function stateBadge(state: GroupState) {
const map: Record<GroupState, { bg: string; label: string }> = { const map: Record<GroupState, { bg: string; label: string }> = {
[GroupState.OPEN]: { bg: 'success', label: 'Otevřeno' }, [GroupState.OPEN]: { bg: 'success', label: 'Otevřeno' },
@@ -53,6 +64,8 @@ export default function OrderGroupsPage() {
const [todayIso, setTodayIso] = useState<string | undefined>(); const [todayIso, setTodayIso] = useState<string | undefined>();
// Ref pro socket handler aby věděl, zda se zobrazuje historie (na ní se živé aktualizace neaplikují) // Ref pro socket handler aby věděl, zda se zobrazuje historie (na ní se živé aktualizace neaplikují)
const selectedDateRef = useRef<string | undefined>(undefined); const selectedDateRef = useRef<string | undefined>(undefined);
// ISO data dnů, ve kterých existuje aspoň jedna objednávka (pro zvýraznění v date pickeru)
const [orderDates, setOrderDates] = useState<string[]>([]);
const [newGroupName, setNewGroupName] = useState(''); const [newGroupName, setNewGroupName] = useState('');
const [creating, setCreating] = useState(false); const [creating, setCreating] = useState(false);
const [adminModalOpen, setAdminModalOpen] = useState(false); const [adminModalOpen, setAdminModalOpen] = useState(false);
@@ -78,6 +91,12 @@ export default function OrderGroupsPage() {
} }
}; };
// Načte dny s objednávkou pro zvýraznění v date pickeru
const fetchOrderDates = async () => {
const r = await getOrderDates();
if (r.data?.dates) setOrderDates(r.data.dates);
};
useEffect(() => { useEffect(() => {
selectedDateRef.current = selectedDate; selectedDateRef.current = selectedDate;
}, [selectedDate]); }, [selectedDate]);
@@ -87,6 +106,11 @@ export default function OrderGroupsPage() {
fetchData(selectedDate); fetchData(selectedDate);
}, [auth?.login, selectedDate]); }, [auth?.login, selectedDate]);
useEffect(() => {
if (!auth?.login) return;
fetchOrderDates();
}, [auth?.login]);
useEffect(() => { useEffect(() => {
socket.on(EVENT_MESSAGE, (newData: ClientData) => { socket.on(EVENT_MESSAGE, (newData: ClientData) => {
// Živé aktualizace se týkají vždy dneška při zobrazení historie je ignorujeme // Živé aktualizace se týkají vždy dneška při zobrazení historie je ignorujeme
@@ -139,6 +163,8 @@ export default function OrderGroupsPage() {
socket.emit?.('message', result.data as ClientData); socket.emit?.('message', result.data as ClientData);
} }
await fetchData(); await fetchData();
// Sada dnů s objednávkou se mohla změnit (vytvoření/smazání skupiny)
fetchOrderDates();
return true; return true;
}; };
@@ -265,6 +291,11 @@ export default function OrderGroupsPage() {
setSelectedDate(todayIso != null && value >= todayIso ? undefined : value); setSelectedDate(todayIso != null && value >= todayIso ? undefined : value);
}; };
// Dny s objednávkou jako Date objekty pro zvýraznění v kalendáři
const highlightedOrderDates = orderDates
.map(d => isoToDate(d))
.filter((d): d is Date => d != null);
return ( return (
<div className="app-container"> <div className="app-container">
<Header choices={data.choices} /> <Header choices={data.choices} />
@@ -282,13 +313,16 @@ export default function OrderGroupsPage() {
<span title="Předchozí den"> <span title="Předchozí den">
<FontAwesomeIcon icon={faChevronLeft} onClick={() => goToDay(-1)} /> <FontAwesomeIcon icon={faChevronLeft} onClick={() => goToDay(-1)} />
</span> </span>
<Form.Control <DatePicker
type="date" selected={isoToDate(displayedIso)}
value={displayedIso ?? ''} onChange={(d: Date | null) => handleDatePick(d ? formatDate(d) : '')}
max={todayIso} maxDate={isoToDate(todayIso) ?? undefined}
onChange={e => handleDatePick(e.target.value)} highlightDates={[{ 'luncher-order-day': highlightedOrderDates }]}
className={isReadOnly ? 'text-muted' : ''} locale="cs"
style={{ maxWidth: 200 }} dateFormat="d. M. yyyy"
calendarStartDay={1}
popperPlacement="bottom"
className={`form-control text-center fw-semibold order-date-input ${isReadOnly ? 'text-muted' : ''}`}
/> />
<span title="Následující den"> <span title="Následující den">
<FontAwesomeIcon <FontAwesomeIcon
+55
View File
@@ -428,6 +428,42 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b"
integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ== integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==
"@floating-ui/core@^1.7.5":
version "1.7.5"
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.5.tgz#d4af157a03330af5a60e69da7a4692507ada0622"
integrity sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==
dependencies:
"@floating-ui/utils" "^0.2.11"
"@floating-ui/dom@^1.7.6":
version "1.7.6"
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.6.tgz#f915bba5abbb177e1f227cacee1b4d0634b187bf"
integrity sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==
dependencies:
"@floating-ui/core" "^1.7.5"
"@floating-ui/utils" "^0.2.11"
"@floating-ui/react-dom@^2.1.8":
version "2.1.8"
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.8.tgz#5fb5a20d10aafb9505f38c24f38d00c8e1598893"
integrity sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==
dependencies:
"@floating-ui/dom" "^1.7.6"
"@floating-ui/react@^0.27.15":
version "0.27.19"
resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.27.19.tgz#d8d5d895b7cb97dac370bfbf55f3e630878fdf1f"
integrity sha512-31B8h5mm8YxotlE7/AU/PhNAl8eWxAmjL/v2QOxroDNkTFLk3Uu82u63N3b6TXa4EGJeeZLVcd/9AlNlVqzeog==
dependencies:
"@floating-ui/react-dom" "^2.1.8"
"@floating-ui/utils" "^0.2.11"
tabbable "^6.0.0"
"@floating-ui/utils@^0.2.11":
version "0.2.11"
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.11.tgz#a269e055e40e2f45873bae9d1a2fdccbd314ea3f"
integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==
"@fortawesome/fontawesome-common-types@7.1.0": "@fortawesome/fontawesome-common-types@7.1.0":
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz#a4e0b7e40073d5fdef41182da1bc216a05875659" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz#a4e0b7e40073d5fdef41182da1bc216a05875659"
@@ -1216,6 +1252,11 @@ d3-timer@^3.0.1:
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
date-fns@^4.1.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.4.0.tgz#806539edf45c616b2b76b5f78b88c56ed3c7e036"
integrity sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==
debug@^4.1.0, debug@^4.3.1, debug@~4.4.1: debug@^4.1.0, debug@^4.3.1, debug@~4.4.1:
version "4.4.3" version "4.4.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a"
@@ -1626,6 +1667,15 @@ react-bootstrap@^2.10.10:
uncontrollable "^7.2.1" uncontrollable "^7.2.1"
warning "^4.0.3" warning "^4.0.3"
react-datepicker@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-9.1.0.tgz#638c636780ae98f1930f87e1f76850de5fc37d5c"
integrity sha512-lOp+m5bc+ttgtB5MHEjwiVu4nlp4CvJLS/PG1OiOe5pmg9kV73pEqO8H0Geqvg2E8gjqTaL9eRhSe+ZpeKP3nA==
dependencies:
"@floating-ui/react" "^0.27.15"
clsx "^2.1.1"
date-fns "^4.1.0"
react-dom@^19.2.0: react-dom@^19.2.0:
version "19.2.3" version "19.2.3"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17"
@@ -1881,6 +1931,11 @@ supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
tabbable@^6.0.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.4.0.tgz#36eb7a06d80b3924a22095daf45740dea3bf5581"
integrity sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==
tiny-invariant@^1.3.3: tiny-invariant@^1.3.3:
version "1.3.3" version "1.3.3"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
+18
View File
@@ -28,6 +28,24 @@ function findGroup(data: ClientData, id: string): OrderGroup | undefined {
return data.groups?.find(g => g.id === id); return data.groups?.find(g => g.id === id);
} }
/**
* Vrátí seznam ISO dat (YYYY-MM-DD), pro která existuje alespoň jedna objednávková skupina.
* Slouží ke zvýraznění dnů v date pickeru na stránce objednávání.
*/
export async function getOrderDates(): Promise<string[]> {
const EXTRA_SUFFIX = '_extra';
const keys = await storage.listKeys(EXTRA_SUFFIX);
const dates: string[] = [];
for (const key of keys) {
if (!key.endsWith(EXTRA_SUFFIX)) continue;
const data = await storage.getData<ClientData>(key);
if (data?.groups && data.groups.length > 0) {
dates.push(key.slice(0, -EXTRA_SUFFIX.length));
}
}
return dates.sort();
}
export async function createGroup(creatorLogin: string, name: string, date?: Date): Promise<ClientData> { export async function createGroup(creatorLogin: string, name: string, date?: Date): Promise<ClientData> {
const stores = await getStores(); const stores = await getStores();
if (!stores.some(s => s.toLowerCase() === name.trim().toLowerCase())) { if (!stores.some(s => s.toLowerCase() === name.trim().toLowerCase())) {
+8 -1
View File
@@ -2,7 +2,7 @@ import express, { Request } from "express";
import { getLogin } from "../auth"; import { getLogin } from "../auth";
import { parseToken } from "../utils"; import { parseToken } from "../utils";
import { getWebsocket } from "../websocket"; import { getWebsocket } from "../websocket";
import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, updateGroupFees } from "../groups"; import { createGroup, deleteGroup, addGroupMember, removeGroupMember, updateGroupMember, setGroupState, updateGroupTimes, updateGroupFees, getOrderDates } from "../groups";
import { GroupState } from "../../../types/gen/types.gen"; import { GroupState } from "../../../types/gen/types.gen";
const router = express.Router(); const router = express.Router();
@@ -11,6 +11,13 @@ function broadcastExtra(data: any) {
getWebsocket().emit("message", data); 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) => { router.post("/create", async (req: Request, res, next) => {
const login = getLogin(parseToken(req)); const login = getLogin(parseToken(req));
const { name } = req.body ?? {}; const { name } = req.body ?? {};
+6
View File
@@ -29,4 +29,10 @@ export interface StorageInterface {
* @param data data pro uložení * @param data data pro uložení
*/ */
setData<Type>(key: string, data: Type): Promise<void>; setData<Type>(key: string, data: Type): Promise<void>;
/**
* Vrátí seznam všech klíčů, případně jen těch obsahujících předaný podřetězec.
* @param contains volitelný podřetězec, který musí klíč obsahovat (např. '_extra')
*/
listKeys(contains?: string): Promise<string[]>;
} }
+5
View File
@@ -29,4 +29,9 @@ export default class JsonStorage implements StorageInterface {
db.set(key, data); db.set(key, data);
return Promise.resolve(); return Promise.resolve();
} }
listKeys(contains?: string): Promise<string[]> {
const keys = Object.keys(db.JSON());
return Promise.resolve(contains ? keys.filter(k => k.includes(contains)) : keys);
}
} }
+5
View File
@@ -24,4 +24,9 @@ export default class MemoryStorage implements StorageInterface {
store.set(key, data); store.set(key, data);
return Promise.resolve(); return Promise.resolve();
} }
listKeys(contains?: string): Promise<string[]> {
const keys = Array.from(store.keys());
return Promise.resolve(contains ? keys.filter(k => k.includes(contains)) : keys);
}
} }
+12
View File
@@ -31,4 +31,16 @@ export default class RedisStorage implements StorageInterface {
await client.json.set(key, '.', data as any); await client.json.set(key, '.', data as any);
await client.json.get(key); await client.json.get(key);
} }
async listKeys(contains?: string): Promise<string[]> {
// SCAN je bezpečnější než KEYS na produkci (neblokuje server)
const match = contains ? `*${contains}*` : '*';
const keys: string[] = [];
for await (const key of client.scanIterator({ MATCH: match, COUNT: 100 })) {
// node-redis v4 vrací buď string, nebo (novější verze) pole stringů
if (Array.isArray(key)) keys.push(...key);
else keys.push(key);
}
return keys;
}
} }
+20
View File
@@ -26,6 +26,10 @@ const implementations: [string, () => StorageInterface, () => void][] = [
inst.hasData = async (key: string) => Promise.resolve((inst as any).db.has(key)); inst.hasData = async (key: string) => Promise.resolve((inst as any).db.has(key));
inst.getData = async (key: string) => (inst as any).db.get(key); inst.getData = async (key: string) => (inst as any).db.get(key);
inst.setData = async (key: string, data: any) => { (inst as any).db.set(key, data); return Promise.resolve(); }; inst.setData = async (key: string, data: any) => { (inst as any).db.set(key, data); return Promise.resolve(); };
inst.listKeys = async (contains?: string) => {
const keys = Object.keys((inst as any).db.JSON());
return contains ? keys.filter((k: string) => k.includes(contains)) : keys;
};
return inst; return inst;
}, () => { }, () => {
if (fs.existsSync(tempDbPath)) { if (fs.existsSync(tempDbPath)) {
@@ -76,6 +80,22 @@ describe.each(implementations)('%s splňuje StorageInterface kontrakt', (name, f
expect((await storage.getData<{ val: string }>('a'))?.val).toBe('A'); expect((await storage.getData<{ val: string }>('a'))?.val).toBe('A');
expect((await storage.getData<{ val: string }>('b'))?.val).toBe('B'); expect((await storage.getData<{ val: string }>('b'))?.val).toBe('B');
}); });
test('listKeys vrátí všechny uložené klíče', async () => {
await storage.setData('2024-01-01_extra', {});
await storage.setData('2024-01-02', {});
const keys = await storage.listKeys();
expect(keys).toContain('2024-01-01_extra');
expect(keys).toContain('2024-01-02');
});
test('listKeys filtruje podle podřetězce', async () => {
await storage.setData('2024-01-01_extra', {});
await storage.setData('2024-01-02_extra', {});
await storage.setData('2024-01-02', {});
const keys = await storage.listKeys('_extra');
expect(keys.sort()).toEqual(['2024-01-01_extra', '2024-01-02_extra']);
});
}); });
afterAll(() => { afterAll(() => {
+2
View File
@@ -82,6 +82,8 @@ paths:
$ref: "./paths/changelogs/getChangelogs.yml" $ref: "./paths/changelogs/getChangelogs.yml"
# Skupiny objednávek (/api/groups) # Skupiny objednávek (/api/groups)
/groups/dates:
$ref: "./paths/groups/getOrderDates.yml"
/groups/create: /groups/create:
$ref: "./paths/groups/createGroup.yml" $ref: "./paths/groups/createGroup.yml"
/groups/delete: /groups/delete:
+19
View File
@@ -0,0 +1,19 @@
get:
operationId: getOrderDates
summary: Vrátí seznam dnů, pro které existuje alespoň jedna objednávková skupina.
responses:
"200":
description: Seznam dnů s objednávkou
content:
application/json:
schema:
type: object
required:
- dates
properties:
dates:
description: Pole ISO dat (YYYY-MM-DD) s alespoň jednou skupinou
type: array
items:
type: string
format: date