feat: opakovaný výběr jídla zvýší počet kusů (2x)
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 27s
CI / Build client (push) Successful in 40s
CI / Playwright E2E tests (push) Successful in 1m50s
CI / Build and push Docker image (push) Successful in 44s
CI / Notify (push) Successful in 1s
CI / Generate TypeScript types (push) Successful in 10s
CI / Server unit tests (push) Successful in 25s
CI / Build server (push) Successful in 27s
CI / Build client (push) Successful in 40s
CI / Playwright E2E tests (push) Successful in 1m50s
CI / Build and push Docker image (push) Successful in 44s
CI / Notify (push) Successful in 1s
This commit is contained in:
@@ -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
|
// 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 part = match && match.parts.length === items.length ? match.parts[i] : undefined;
|
||||||
const price = part?.dish?.price;
|
const price = part?.dish?.price;
|
||||||
|
const quantity = part?.quantity ?? 1;
|
||||||
return (
|
return (
|
||||||
<span key={`${i}-${text}`} className="menu-hint-item">
|
<span key={`${i}-${text}`} className="menu-hint-item">
|
||||||
{text}{price != null && <small> {price} Kč</small>}
|
{text}{price != null && <small> {quantity > 1 ? `${quantity}× ${price} = ${quantity * price}` : price} Kč</small>}
|
||||||
<button type="button" onClick={() => onRemoveItem(i)} title={`Odebrat 1× ${text}`} aria-label={`Odebrat ${text}`}>×</button>
|
<button type="button" onClick={() => onRemoveItem(i)} title={`Odebrat 1× ${text}`} aria-label={`Odebrat ${text}`}>×</button>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
import MenuHintCell from '../components/MenuHintCell';
|
import MenuHintCell from '../components/MenuHintCell';
|
||||||
import EaterBadge from '../components/EaterBadge';
|
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 { computeFeeShare, computeMemberTotal, countActiveMembers } from '../utils/groupFees';
|
||||||
import { getStoreUrls, isHttpUrl, storeUrlLabel } from '../utils/storeUrls';
|
import { getStoreUrls, isHttpUrl, storeUrlLabel } from '../utils/storeUrls';
|
||||||
import { EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from '../context/socket';
|
import { EVENT_MESSAGE, EVENT_PENDING_QR, SocketContext } from '../context/socket';
|
||||||
@@ -332,9 +332,9 @@ export default function OrderGroupsPage() {
|
|||||||
const getGroupMenu = (group: OrderGroup): StoreMenuDish[] | undefined =>
|
const getGroupMenu = (group: OrderGroup): StoreMenuDish[] | undefined =>
|
||||||
Object.entries(storeMenus).find(([name]) => name.toLowerCase() === group.name.toLowerCase())?.[1] ?? 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 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;
|
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 }) } }));
|
return refresh(() => updateGroupMember({ body: { id: group.id, login, note, ...(amount != null && { amount }) } }));
|
||||||
};
|
};
|
||||||
@@ -690,7 +690,7 @@ export default function OrderGroupsPage() {
|
|||||||
<tr key={memberLogin}>
|
<tr key={memberLogin}>
|
||||||
<td>
|
<td>
|
||||||
<span className="user-info">
|
<span className="user-info">
|
||||||
<EaterBadge items={countNoteItems(member.note)} />
|
<EaterBadge items={countNoteItems(member.note, getGroupMenu(group) ?? [])} />
|
||||||
<strong>{memberLogin}</strong>
|
<strong>{memberLogin}</strong>
|
||||||
{memberLogin === group.creatorLogin && (
|
{memberLogin === group.creatorLogin && (
|
||||||
<OverlayTrigger placement="top" overlay={<Tooltip>Zakladatel / objednávající</Tooltip>}>
|
<OverlayTrigger placement="top" overlay={<Tooltip>Zakladatel / objednávající</Tooltip>}>
|
||||||
|
|||||||
@@ -52,13 +52,12 @@ function findDish(text: string, dishes: { dish: StoreMenuDish, norm: string }[])
|
|||||||
export function matchNoteToMenu(note: string | undefined, menu: StoreMenuDish[]): NoteMatch | undefined {
|
export function matchNoteToMenu(note: string | undefined, menu: StoreMenuDish[]): NoteMatch | undefined {
|
||||||
if (!note?.trim() || menu.length === 0) return undefined;
|
if (!note?.trim() || menu.length === 0) return undefined;
|
||||||
const dishes = menu.map(dish => ({ dish, norm: normalizeDishName(dish.name) }));
|
const dishes = menu.map(dish => ({ dish, norm: normalizeDishName(dish.name) }));
|
||||||
const whole = dishes.find(d => d.norm === normalizeDishName(note));
|
const parts: MatchedPart[] = splitNoteItems(note, menu).map(text => {
|
||||||
const parts: MatchedPart[] = whole
|
const exact = dishes.find(d => d.norm === normalizeDishName(text));
|
||||||
? [{ text: note.trim(), quantity: 1, dish: whole.dish }]
|
if (exact) return { text, quantity: 1, dish: exact.dish };
|
||||||
: note.split('+').map(p => p.trim()).filter(Boolean).map(text => {
|
const { quantity, rest } = parseQuantity(text);
|
||||||
const { quantity, rest } = parseQuantity(text);
|
return { text, quantity, dish: findDish(rest, dishes) };
|
||||||
return { text, quantity, dish: findDish(rest, dishes) };
|
});
|
||||||
});
|
|
||||||
if (!parts.some(p => p.dish)) return undefined;
|
if (!parts.some(p => p.dish)) return undefined;
|
||||||
const complete = parts.every(p => p.dish?.price != null);
|
const complete = parts.every(p => p.dish?.price != null);
|
||||||
return {
|
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). */
|
/** 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 {
|
export function countNoteItems(note: string | undefined, menu: StoreMenuDish[] = []): number {
|
||||||
if (!note?.trim()) return 0;
|
return splitNoteItems(note, menu).reduce((sum, part) => sum + parseQuantity(part).quantity, 0);
|
||||||
return note.split('+').map(p => p.trim()).filter(Boolean)
|
|
||||||
.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.
|
* 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[] {
|
export function splitNoteItems(note: string | undefined, menu: StoreMenuDish[] = []): string[] {
|
||||||
const trimmed = (note ?? '').trim();
|
const tokens = (note ?? '').split('+').map(p => p.trim()).filter(Boolean);
|
||||||
if (trimmed && menu.some(d => normalizeDishName(d.name) === normalizeDishName(trimmed))) return [trimmed];
|
if (menu.length === 0 || tokens.length < 2) return tokens;
|
||||||
return trimmed.split('+').map(p => p.trim()).filter(Boolean);
|
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);
|
else parts.splice(index, 1);
|
||||||
return { note: parts.join(' + '), removedPrice: dish?.price };
|
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(' + ');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user