feat: spin the wheel, objednavani scrape menu, historie cen
CI / Generate TypeScript types (push) Successful in 11s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 29s
CI / Build client (push) Successful in 39s
CI / Playwright E2E tests (push) Successful in 1m52s
CI / Build and push Docker image (push) Successful in 51s
CI / Notify (push) Successful in 2s

This commit is contained in:
batmanisko committed 2026-09-23 15:41:15 +02:00
1 parent 682deaf20b
commit e281615cc3
35 files changed
+3979 -15

No files matched your search

+57 -1
View File
@@ -1,4 +1,4 @@
import { Store } from "../../types/gen/types.gen";
import { ClientData, Store } from "../../types/gen/types.gen";
import getStorage from "./storage";
const storage = getStorage();
@@ -151,3 +151,59 @@ export async function removeStore(name: string, heslo: string): Promise<Store[]>
await storage.setData(STORES_KEY, updated);
return updated;
}
/** Maximální počet jídel vrácených pro jeden obchod v {@link getStoreDishes}. */
const MAX_DISHES_PER_STORE = 40;
/** Rozpoznání polévek v poznámkách objednávek — ty se do kola štěstí nezahrnují. */
/** Jídla historicky objednaná u daného obchodu. */
export type StoreDishes = { store: string, dishes: string[] };
const SOUP_REGEX = /pol[eé]vk|v[ýy]var|soup|\bpho\b|ramen|tom yum|kulajda|gul[aá][sš]ovka/i;
/**
* Vrátí pro každý povolený obchod seznam jídel, která si u něj uživatelé v minulosti
* objednali (z poznámek členů skupinových objednávek). Slouží jako záloha pro druhé
* kolo „kola štěstí", pokud se nabídku z Bolt Food/Wolt nepodaří stáhnout.
* Jídla jsou seřazena dle četnosti, bez duplicit a bez polévek.
*/
export async function getStoreDishes(): Promise<StoreDishes[]> {
const stores = await getStores();
const counts = new Map<string, Map<string, { name: string, count: number }>>();
for (const store of stores) {
counts.set(store.name.toLowerCase(), new Map());
}
const keys = await storage.listKeys('_extra');
for (const key of keys) {
if (!key.endsWith('_extra')) continue;
const data = await storage.getData<ClientData>(key);
for (const group of data?.groups ?? []) {
const dishes = counts.get(group.name?.toLowerCase());
if (!dishes) continue;
for (const member of Object.values(group.members ?? {})) {
const dish = normalizeDish(member?.note);
if (!dish || SOUP_REGEX.test(dish)) continue;
const dishKey = dish.toLowerCase();
const existing = dishes.get(dishKey);
if (existing) existing.count++;
else dishes.set(dishKey, { name: dish, count: 1 });
}
}
}
return stores.map(store => ({
store: store.name,
dishes: [...counts.get(store.name.toLowerCase())!.values()]
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name))
.slice(0, MAX_DISHES_PER_STORE)
.map(d => d.name),
}));
}
/** Očistí poznámku člena objednávky (mezery, číslo položky z menu typu „28. ") na název jídla; poznámky nezačínající písmenem/číslicí zahodí. */
function normalizeDish(note?: string): string | undefined {
if (!note) return undefined;
const cleaned = note.replace(/\s+/g, ' ').replace(/^\d+\s*[.)]\s*/, '').trim();
// Poznámky typu „⬇️ totéž" nebo „(bez cibule)" nejsou názvy jídel
if (!/^[\p{L}\p{N}]/u.test(cleaned)) return undefined;
return cleaned.length >= 2 && cleaned.length <= 120 ? cleaned : undefined;
}