CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 26s
CI / Build server (push) Successful in 58s
CI / Build client (push) Successful in 1m4s
CI / Playwright E2E tests (push) Successful in 1m47s
CI / Build and push Docker image (push) Successful in 45s
CI / Notify (push) Successful in 2s
103 lines
4.8 KiB
TypeScript
103 lines
4.8 KiB
TypeScript
import type { StoreMenuDish } from "../../../types";
|
||
|
||
/** Normalizace pro porovnání: malá písmena, bez diakritiky, emoji, čísel položek („28." / „13A.") a interpunkce. */
|
||
export function normalizeDishName(text: string): string {
|
||
return text
|
||
.normalize('NFD')
|
||
.replace(/[\u0300-\u036f]/g, '')
|
||
.toLowerCase()
|
||
.replace(/^\s*\*?\s*\d+[a-z]?\s*[.)]\s*/, '')
|
||
.replace(/[^a-z0-9]+/g, ' ')
|
||
.trim();
|
||
}
|
||
|
||
export type MatchedPart = {
|
||
/** Původní text části poznámky */
|
||
text: string,
|
||
quantity: number,
|
||
dish?: StoreMenuDish,
|
||
};
|
||
|
||
export type NoteMatch = {
|
||
parts: MatchedPart[],
|
||
/** Cena v Kč dle nabídky — jen pokud byly nalezeny všechny části a mají cenu */
|
||
total?: number,
|
||
};
|
||
|
||
/** Množství zapsané jako „2x burger", „2 × burger" nebo „burger 2x". */
|
||
function parseQuantity(text: string): { quantity: number, rest: string } {
|
||
const prefix = text.match(/^\s*(\d+)\s*[x×]\s*(.+)$/i);
|
||
if (prefix) return { quantity: Number(prefix[1]), rest: prefix[2] };
|
||
const suffix = text.match(/^(.+?)\s*[x×]?\s*(\d+)\s*[x×]\s*$/i);
|
||
if (suffix) return { quantity: Number(suffix[2]), rest: suffix[1] };
|
||
return { quantity: 1, rest: text };
|
||
}
|
||
|
||
/** Najde položku nabídky k textu — přesná shoda, jinak nejbližší položka obsahující text (či naopak). */
|
||
function findDish(text: string, dishes: { dish: StoreMenuDish, norm: string }[]): StoreMenuDish | undefined {
|
||
const norm = normalizeDishName(text);
|
||
if (norm.length < 3) return undefined;
|
||
const exact = dishes.find(d => d.norm === norm);
|
||
if (exact) return exact.dish;
|
||
const candidates = dishes.filter(d => d.norm.length >= 3 && (d.norm.includes(norm) || norm.includes(d.norm)));
|
||
// Nejbližší délkou = nejspecifičtější shoda („Kebab" vs. „Kebab box s hranolky")
|
||
candidates.sort((a, b) => Math.abs(a.norm.length - norm.length) - Math.abs(b.norm.length - norm.length));
|
||
return candidates[0]?.dish;
|
||
}
|
||
|
||
/**
|
||
* Rozpozná v poznámce člena objednávky položky z nabídky podniku („Twister speciál + 2x Hranolky")
|
||
* a spočítá jejich cenu. Nejprve zkusí celou poznámku (názvy jídel mohou obsahovat „+").
|
||
*/
|
||
export function matchNoteToMenu(note: string | undefined, menu: StoreMenuDish[]): NoteMatch | undefined {
|
||
if (!note?.trim() || menu.length === 0) return undefined;
|
||
const dishes = menu.map(dish => ({ dish, norm: normalizeDishName(dish.name) }));
|
||
const whole = dishes.find(d => d.norm === normalizeDishName(note));
|
||
const parts: MatchedPart[] = whole
|
||
? [{ text: note.trim(), quantity: 1, dish: whole.dish }]
|
||
: note.split('+').map(p => p.trim()).filter(Boolean).map(text => {
|
||
const { quantity, rest } = parseQuantity(text);
|
||
return { text, quantity, dish: findDish(rest, dishes) };
|
||
});
|
||
if (!parts.some(p => p.dish)) return undefined;
|
||
const complete = parts.every(p => p.dish?.price != null);
|
||
return {
|
||
parts,
|
||
total: complete ? parts.reduce((sum, p) => sum + p.dish!.price! * p.quantity, 0) : undefined,
|
||
};
|
||
}
|
||
|
||
/** Počet položek v poznámce objednávky — části oddělené „+", včetně množství („2x Hranolky" = 2). */
|
||
export function countNoteItems(note: string | undefined): number {
|
||
if (!note?.trim()) return 0;
|
||
return note.split('+').map(p => p.trim()).filter(Boolean)
|
||
.reduce((sum, part) => sum + parseQuantity(part).quantity, 0);
|
||
}
|
||
|
||
/**
|
||
* Části poznámky oddělené „+" (tak, jak je uživatel zapsal). Pokud celá poznámka
|
||
* odpovídá jednomu jídlu z nabídky (název může obsahovat „+"), je to jediná položka.
|
||
*/
|
||
export function splitNoteItems(note: string | undefined, menu: StoreMenuDish[] = []): string[] {
|
||
const trimmed = (note ?? '').trim();
|
||
if (trimmed && menu.some(d => normalizeDishName(d.name) === normalizeDishName(trimmed))) return [trimmed];
|
||
return trimmed.split('+').map(p => p.trim()).filter(Boolean);
|
||
}
|
||
|
||
/**
|
||
* Odebere z poznámky jeden kus položky na daném indexu: „2x Hranolky" → „Hranolky",
|
||
* jinak celou část. Vrátí novou poznámku a cenu odebraného kusu dle nabídky (Kč), je-li známa.
|
||
*/
|
||
export function removeNoteItem(note: string | undefined, index: number, menu: StoreMenuDish[]): { note: string, removedPrice?: number } {
|
||
const parts = splitNoteItems(note, menu);
|
||
const part = parts[index];
|
||
if (part == null) return { note: note ?? '' };
|
||
const { quantity, rest } = parseQuantity(part);
|
||
const dishes = menu.map(dish => ({ dish, norm: normalizeDishName(dish.name) }));
|
||
const dish = dishes.find(d => d.norm === normalizeDishName(part))?.dish ?? findDish(rest, dishes);
|
||
if (quantity > 2) parts[index] = `${quantity - 1}x ${rest.trim()}`;
|
||
else if (quantity === 2) parts[index] = rest.trim();
|
||
else parts.splice(index, 1);
|
||
return { note: parts.join(' + '), removedPrice: dish?.price };
|
||
}
|