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
+18
View File
@@ -28,6 +28,24 @@ function findGroup(data: ClientData, id: string): OrderGroup | undefined {
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> {
const stores = await getStores();
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 { parseToken } from "../utils";
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";
const router = express.Router();
@@ -11,6 +11,13 @@ 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 } = req.body ?? {};
+6
View File
@@ -29,4 +29,10 @@ export interface StorageInterface {
* @param data data pro uložení
*/
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);
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);
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.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.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.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;
}, () => {
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 }>('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(() => {