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
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:
35 files changed
+3979
-15
No files matched your search
+57
-1
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user