diff --git a/client/src/components/MenuHintCell.tsx b/client/src/components/MenuHintCell.tsx index af9a9f9..64388a8 100644 --- a/client/src/components/MenuHintCell.tsx +++ b/client/src/components/MenuHintCell.tsx @@ -69,9 +69,10 @@ export default function MenuHintCell({ menu, note, amount, canEdit, onPick, onUs // Rozpoznaná položka ze stejného rozdělení poznámky — kvůli ceně na čipu const part = match && match.parts.length === items.length ? match.parts[i] : undefined; const price = part?.dish?.price; + const quantity = part?.quantity ?? 1; return ( - {text}{price != null && {price} Kč} + {text}{price != null && {quantity > 1 ? `${quantity}× ${price} = ${quantity * price}` : price} Kč} ); diff --git a/client/src/pages/OrderGroupsPage.tsx b/client/src/pages/OrderGroupsPage.tsx index 8b7b922..8f55f04 100644 --- a/client/src/pages/OrderGroupsPage.tsx +++ b/client/src/pages/OrderGroupsPage.tsx @@ -12,7 +12,7 @@ import { } from '../../../types'; import MenuHintCell from '../components/MenuHintCell'; import EaterBadge from '../components/EaterBadge'; -import { countNoteItems, removeNoteItem } from '../utils/menuMatch'; +import { addNoteItem, countNoteItems, removeNoteItem } from '../utils/menuMatch'; import { computeFeeShare, computeMemberTotal, countActiveMembers } from '../utils/groupFees'; import { getStoreUrls, isHttpUrl, storeUrlLabel } from '../utils/storeUrls'; import { EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from '../context/socket'; @@ -332,9 +332,9 @@ export default function OrderGroupsPage() { const getGroupMenu = (group: OrderGroup): StoreMenuDish[] | undefined => Object.entries(storeMenus).find(([name]) => name.toLowerCase() === group.name.toLowerCase())?.[1] ?? undefined; - /** Výběr položky z nabídky — připojí ji k poznámce a její cenu přičte k částce. */ + /** Výběr položky z nabídky — připojí ji k poznámce (opakovaný výběr zvýší počet „2x") a cenu přičte k částce. */ const handlePickDish = (group: OrderGroup, login: string, member: OrderGroupMember, dish: StoreMenuDish) => { - const note = member.note?.trim() ? `${member.note.trim()} + ${dish.name}` : dish.name; + const note = addNoteItem(member.note, dish.name, getGroupMenu(group) ?? []); const amount = dish.price != null ? (member.amount ?? 0) + Math.round(dish.price * 100) : member.amount; return refresh(() => updateGroupMember({ body: { id: group.id, login, note, ...(amount != null && { amount }) } })); }; @@ -690,7 +690,7 @@ export default function OrderGroupsPage() { - + {memberLogin} {memberLogin === group.creatorLogin && ( Zakladatel / objednávající}> diff --git a/client/src/utils/menuMatch.ts b/client/src/utils/menuMatch.ts index 6de2b7c..1837499 100644 --- a/client/src/utils/menuMatch.ts +++ b/client/src/utils/menuMatch.ts @@ -52,13 +52,12 @@ function findDish(text: string, dishes: { dish: StoreMenuDish, norm: string }[]) 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) }; - }); + const parts: MatchedPart[] = splitNoteItems(note, menu).map(text => { + const exact = dishes.find(d => d.norm === normalizeDishName(text)); + if (exact) return { text, quantity: 1, dish: exact.dish }; + 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 { @@ -68,10 +67,8 @@ export function matchNoteToMenu(note: string | undefined, menu: StoreMenuDish[]) } /** 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); +export function countNoteItems(note: string | undefined, menu: StoreMenuDish[] = []): number { + return splitNoteItems(note, menu).reduce((sum, part) => sum + parseQuantity(part).quantity, 0); } /** @@ -79,9 +76,19 @@ export function countNoteItems(note: string | undefined): number { * 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); + const tokens = (note ?? '').split('+').map(p => p.trim()).filter(Boolean); + if (menu.length === 0 || tokens.length < 2) return tokens; + const names = new Set(menu.map(d => normalizeDishName(d.name))); + const isDish = (text: string) => names.has(normalizeDishName(text)) || names.has(normalizeDishName(parseQuantity(text).rest)); + // Sousední části, které dohromady tvoří název jídla s „+" („Stripsy 6 Ks + Coleslaw"), spojíme zpět + const result: string[] = []; + for (let i = 0; i < tokens.length;) { + let j = tokens.length; + while (j > i + 1 && !isDish(tokens.slice(i, j).join(' + '))) j--; + result.push(tokens.slice(i, j).join(' + ')); + i = j; + } + return result; } /** @@ -100,3 +107,17 @@ export function removeNoteItem(note: string | undefined, index: number, menu: St else parts.splice(index, 1); return { note: parts.join(' + '), removedPrice: dish?.price }; } + +/** + * Přidá do poznámky jeden kus jídla. Pokud už v poznámce je (i s počtem, např. „2x Hranolky"), + * zvýší jeho počet („Hranolky" → „2x Hranolky"), jinak ho připojí za „+". + */ +export function addNoteItem(note: string | undefined, dishName: string, menu: StoreMenuDish[] = []): string { + const parts = splitNoteItems(note, menu); + const target = normalizeDishName(dishName); + const index = parts.findIndex(p => normalizeDishName(p) === target || normalizeDishName(parseQuantity(p).rest) === target); + if (index < 0) return [...parts, dishName].join(' + '); + const existing = normalizeDishName(parts[index]) === target ? { quantity: 1, rest: parts[index] } : parseQuantity(parts[index]); + parts[index] = `${existing.quantity + 1}x ${existing.rest.trim()}`; + return parts.join(' + '); +}