"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useRouter, useSearchParams, usePathname } from "next/navigation"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTranslations, useLocale } from "next-intl"; import { ChevronLeft, ChevronRight, Minus, Package, Plus, Search, ShoppingCart, Trash2, UtensilsCrossed, Users, Video, X, } from "lucide-react"; import { apiGet, apiPatch, apiPost, ApiClientError } from "@/lib/api/client"; import type { MenuCategory, MenuItem, Order, Table, TableBoardItem, QueueTicket, } from "@/lib/api/types"; import { useAuthStore } from "@/lib/stores/auth.store"; import { useCafeSettings } from "@/lib/hooks/use-cafe-settings"; import { useCartStore, type CartItem } from "@/lib/stores/cart.store"; import type { OrderType } from "@/lib/stores/cart.store"; import { formatCurrency, formatNumber } from "@/lib/format"; import { formatOrderNumber } from "@/lib/order-number"; import { iranMobileForApi } from "@/lib/phone"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { useIsRtl } from "@/lib/use-is-rtl"; import { useBranchStore } from "@/lib/stores/branch.store"; import { getOrCreateTerminalId } from "@/lib/terminal"; import { MenuItemLabels } from "@/components/menu/menu-item-labels"; import { CategoryVisual } from "@/components/menu/category-visual"; import { getMenuPrimaryName, menuItemMatchesSearch } from "@/lib/menu-display"; import { Input } from "@/components/ui/input"; import { LabeledField } from "@/components/ui/labeled-field"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { MenuItemMedia } from "@/components/menu/menu-item-media"; import { buildCategoryNameMap, inferMenuItemKind, } from "@/lib/menu-item-image"; import { PosPayPanel } from "@/components/pos/pos-pay-panel"; import { PosTableBoard } from "@/components/pos/pos-table-board"; import { PosCustomerPicker } from "@/components/pos/pos-customer-picker"; import { PosSlipModal, type KitchenSlipLine } from "@/components/pos/pos-slip-modal"; import { PosQueueBar } from "@/components/pos/pos-queue-bar"; import { branchMenuItemToMenuItem, getBranchMenu, } from "@/lib/api/branch-menu"; import { requestPosPayment, posDeviceErrorMessage } from "@/lib/api/pos-device"; import { orderAmountDue, submitOrderToApi, isLocalOrder } from "@/lib/pos/submit-order"; import { useSyncQueueStore } from "@/lib/stores/sync-queue.store"; import { useConfirm } from "@/components/providers/confirm-provider"; const TAX_RATE = 0.09; function buildKitchenLines( pending: { menuItemId: string; quantity: number; notes?: string }[], cartItems: CartItem[] ): KitchenSlipLine[] { return pending.map((p) => { const line = cartItems.find((i) => i.menuItem.id === p.menuItemId); return { name: line?.menuItem.name ?? p.menuItemId, quantity: p.quantity, notes: p.notes, }; }); } function cartToKitchenLines(cartItems: CartItem[]): KitchenSlipLine[] { return cartItems .filter((i) => !i.isVoided && i.quantity > 0) .map((i) => ({ name: i.menuItem.name, quantity: i.quantity, notes: i.notes, })); } // ─── Order Type Picker ─────────────────────────────────────────────────────── function OrderTypePicker({ onSelect, t, }: { onSelect: (type: OrderType) => void; t: ReturnType>; }) { return (

{t("orderTypePicker")}

{/* Table */} {/* Counter */} {/* Takeaway */}
); } // ─── Order Type Badge ───────────────────────────────────────────────────────── function OrderTypeBadge({ orderType, tableNumber, onClick, t, }: { orderType: OrderType; tableNumber?: number | string | null; onClick?: () => void; t: ReturnType>; }) { const label = orderType === "table" ? `${t("table")} ${tableNumber ?? "—"}` : orderType === "counter" ? t("counterBadge") : t("takeawayBadge"); const cls = orderType === "table" ? "border-primary bg-primary/10 text-primary hover:bg-primary/20" : orderType === "counter" ? "border-blue-400 bg-blue-50 text-blue-700 hover:bg-blue-100" : "border-amber-400 bg-amber-50 text-amber-700 hover:bg-amber-100"; return ( ); } // ─── Main Component ─────────────────────────────────────────────────────────── export function PosScreen() { const t = useTranslations("pos"); const tQueue = useTranslations("queue"); const tErrors = useTranslations("errors"); const tCommon = useTranslations("common"); const tDashboard = useTranslations("dashboard"); const locale = useLocale(); const isRtl = useIsRtl(); const numberLocale = locale === "en" ? "en-US" : "fa-IR"; const cafeId = useAuthStore((s) => s.user?.cafeId); const { data: cafeSettings } = useCafeSettings(cafeId); const cafeName = cafeSettings?.name ?? tDashboard("cafeName"); const userRole = useAuthStore((s) => s.user?.role); const isManager = userRole === "Manager" || userRole === "Owner"; const branchId = useBranchStore((s) => s.branchId); const setBranchId = useBranchStore((s) => s.setBranchId); const queryClient = useQueryClient(); const confirmDialog = useConfirm(); const isOnline = useSyncQueueStore((s) => s.isOnline); const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); const reservationId = searchParams.get("reservationId"); const reservationGuest = searchParams.get("guestName"); const urlOrderId = searchParams.get("orderId"); const [kitchenSlip, setKitchenSlip] = useState<{ lines: KitchenSlipLine[]; orderId?: string; tableNumber?: string | number | null; guestName?: string | null; } | null>(null); const [posMode, setPosMode] = useState<"order" | "pay">("order"); const [showTablePicker, setShowTablePicker] = useState(false); const [showTransferPicker, setShowTransferPicker] = useState(false); const { items, addItem, removeItem, updateQty, setNotes, couponCode, appliedCoupon, setCouponCode, setAppliedCoupon, clearCoupon, tableId, setTableId, orderType, setOrderType, activeOrderId, activeOrderDisplayNumber, setActiveOrderId, customerId, guestName, setGuestName, guestPhone, setGuestPhone, setCustomer, clearCustomer, hydrateFromOrder, getPendingLines, clearCart, clearSession, subtotal, } = useCartStore(); const syncUrl = useCallback( (tid: string | null, oid: string | null) => { const params = new URLSearchParams(searchParams.toString()); if (tid) params.set("tableId", tid); else params.delete("tableId"); if (oid) params.set("orderId", oid); else params.delete("orderId"); const qs = params.toString(); router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false }); }, [pathname, router, searchParams] ); // Restore tableId + infer orderType from URL params useEffect(() => { const tid = searchParams.get("tableId"); if (tid) { setTableId(tid); if (!orderType) setOrderType("table"); } }, [searchParams, setTableId, setOrderType, orderType]); useEffect(() => { if (urlOrderId) setActiveOrderId(urlOrderId); }, [urlOrderId, setActiveOrderId]); useEffect(() => { if (reservationGuest) setGuestName(reservationGuest); }, [reservationGuest, setGuestName]); useEffect(() => { if (!cafeId) return; apiPost(`/api/cafes/${cafeId}/terminals/register`, { terminalId: getOrCreateTerminalId(), }).catch(() => undefined); }, [cafeId]); const [selectedCategory, setSelectedCategory] = useState("all"); const [itemSearch, setItemSearch] = useState(""); const [orderMessage, setOrderMessage] = useState(null); const [couponMessage, setCouponMessage] = useState<{ type: "success" | "error"; text: string; } | null>(null); const { data: branches = [] } = useQuery({ queryKey: ["branches", cafeId], queryFn: () => apiGet<{ id: string; name: string }[]>(`/api/cafes/${cafeId}/branches`), enabled: !!cafeId, }); useEffect(() => { if (branches.length === 0) return; const valid = branchId && branches.some((b) => b.id === branchId); if (!valid) setBranchId(branches[0]!.id); }, [branches, branchId, setBranchId]); const orderBranchId = useMemo(() => { if (branches.length === 0) return null; if (branchId && branches.some((b) => b.id === branchId)) return branchId; return branches[0]?.id ?? null; }, [branchId, branches]); const { data: categories, isLoading: loadingCategories } = useQuery({ queryKey: ["menu-categories", cafeId], queryFn: () => apiGet(`/api/cafes/${cafeId}/menu/categories`), enabled: !!cafeId, }); const { data: globalMenuItems, isLoading: loadingGlobalItems } = useQuery({ queryKey: ["menu-items", cafeId, selectedCategory], queryFn: () => { const qs = selectedCategory !== "all" ? `?categoryId=${selectedCategory}` : ""; return apiGet(`/api/cafes/${cafeId}/menu/items${qs}`); }, enabled: !!cafeId && !orderBranchId, }); const { data: branchMenuRows, isLoading: loadingBranchMenu } = useQuery({ queryKey: ["branch-menu", cafeId, orderBranchId, selectedCategory], queryFn: () => getBranchMenu(cafeId!, orderBranchId!), enabled: !!cafeId && !!orderBranchId, }); const menuItems = useMemo(() => { if (orderBranchId && branchMenuRows) { const mapped = branchMenuRows.map(branchMenuItemToMenuItem); if (selectedCategory === "all") return mapped; return mapped.filter((i) => i.categoryId === selectedCategory); } return globalMenuItems; }, [orderBranchId, branchMenuRows, globalMenuItems, selectedCategory]); const loadingItems = orderBranchId ? loadingBranchMenu : loadingGlobalItems; const { data: allMenuItems, isLoading: loadingAllCatalog } = useQuery({ queryKey: ["branch-menu-all", cafeId, orderBranchId], queryFn: async () => { if (orderBranchId && cafeId) { const rows = await getBranchMenu(cafeId, orderBranchId); return rows.map(branchMenuItemToMenuItem); } return apiGet(`/api/cafes/${cafeId}/menu/items`); }, enabled: !!cafeId, }); const menuById = useMemo(() => { const map = new Map(); for (const m of allMenuItems ?? menuItems ?? []) { map.set(m.id, m); } return map; }, [allMenuItems, menuItems]); // Hydrate from URL order useEffect(() => { if (!cafeId || !urlOrderId || menuById.size === 0) return; if (activeOrderId === urlOrderId && items.length > 0) return; apiGet(`/api/cafes/${cafeId}/orders/${urlOrderId}`) .then((order) => { hydrateFromOrder(order, menuById); if (order.tableId) { setTableId(order.tableId); setOrderType("table"); } else if (!orderType) { setOrderType("counter"); } }) .catch(() => undefined); }, [ cafeId, urlOrderId, menuById, activeOrderId, items.length, hydrateFromOrder, setTableId, setOrderType, orderType, ]); const sessionPatchRef = useRef | null>(null); useEffect(() => { if (!cafeId || !activeOrderId) return; if (sessionPatchRef.current) clearTimeout(sessionPatchRef.current); sessionPatchRef.current = setTimeout(() => { apiPatch(`/api/cafes/${cafeId}/orders/${activeOrderId}/session`, { guestName: guestName.trim() || null, guestPhone: iranMobileForApi(guestPhone) ?? null, customerId: customerId ?? null, }).catch(() => undefined); }, 600); return () => { if (sessionPatchRef.current) clearTimeout(sessionPatchRef.current); }; }, [guestName, guestPhone, customerId, activeOrderId, cafeId]); const handleTableSelect = useCallback( (table: TableBoardItem, activeOrder: Order | null) => { setShowTablePicker(false); if (activeOrder) { setTableId(table.id); hydrateFromOrder(activeOrder, menuById); syncUrl(table.id, activeOrder.id); setOrderMessage(t("sessionActive")); return; } const hadOpenSession = !!useCartStore.getState().activeOrderId; if (hadOpenSession) { clearSession(); } else { setActiveOrderId(null); } setTableId(table.id); syncUrl(table.id, null); setOrderMessage(null); }, [ setTableId, hydrateFromOrder, menuById, syncUrl, setActiveOrderId, clearSession, t, ] ); // Handle order type selection from the picker const handleOrderTypeSelect = useCallback( (type: OrderType) => { setOrderType(type); if (type === "table") { setShowTablePicker(true); } }, [setOrderType] ); // Go back to type picker (with clear) const handleBackToTypePicker = useCallback(() => { clearSession(); syncUrl(null, null); setOrderMessage(null); setCouponMessage(null); }, [clearSession, syncUrl]); const tablesQuery = orderBranchId ? `?branchId=${encodeURIComponent(orderBranchId)}` : ""; const { data: tables } = useQuery({ queryKey: ["tables", cafeId, orderBranchId], queryFn: () => apiGet(`/api/cafes/${cafeId}/tables${tablesQuery}`), enabled: !!cafeId, }); const { data: boardTables = [] } = useQuery({ queryKey: ["tables-board", cafeId, orderBranchId, "transfer"], queryFn: () => apiGet( `/api/cafes/${cafeId}/tables/board${tablesQuery}` ), enabled: !!cafeId && showTransferPicker, }); const voidItemMutation = useMutation({ mutationFn: async (orderItemId: string) => { if (!cafeId || !activeOrderId) throw new Error("no session"); return apiPatch( `/api/cafes/${cafeId}/orders/${activeOrderId}/items/${orderItemId}/void`, {} ); }, onSuccess: async (order) => { hydrateFromOrder(order, menuById); queryClient.invalidateQueries({ queryKey: ["tables-board", cafeId] }); queryClient.invalidateQueries({ queryKey: ["orders-open", cafeId] }); }, onError: () => setOrderMessage(t("voidError")), }); const transferTableMutation = useMutation({ mutationFn: async (targetTableId: string) => { if (!cafeId || !activeOrderId) throw new Error("no session"); return apiPost(`/api/cafes/${cafeId}/orders/${activeOrderId}/transfer`, { targetTableId, }); }, onSuccess: async (order) => { setShowTransferPicker(false); hydrateFromOrder(order, menuById); if (order.tableId) setTableId(order.tableId); syncUrl(order.tableId ?? null, order.id); setOrderMessage(t("transferSuccess")); queryClient.invalidateQueries({ queryKey: ["tables-board", cafeId] }); queryClient.invalidateQueries({ queryKey: ["orders-open", cafeId] }); }, onError: (err: Error) => { const code = err instanceof ApiClientError ? err.code : ""; if (code === "TABLE_OCCUPIED") setOrderMessage(t("tableOccupied")); else if (code === "TABLE_CLEANING") setOrderMessage(t("tableNotAvailable")); else setOrderMessage(t("transferError")); }, }); const handleVoidItem = async (orderItemId: string) => { const ok = await confirmDialog({ description: t("confirmVoid"), variant: "destructive", confirmLabel: tCommon("confirm"), }); if (!ok) return; voidItemMutation.mutate(orderItemId); }; const freeTransferTables = boardTables.filter( (tbl) => tbl.status === "Free" && tbl.id !== tableId ); const itemSearchQuery = itemSearch.trim(); const isSearchingItems = itemSearchQuery.length > 0; const catalogForSearch = useMemo( () => allMenuItems ?? menuItems ?? [], [allMenuItems, menuItems] ); const filteredItems = useMemo(() => { const base = isSearchingItems ? catalogForSearch : (menuItems ?? []); return base.filter((i) => { if (!i.isAvailable) return false; if (!isSearchingItems) return true; return menuItemMatchesSearch(i, itemSearchQuery, locale); }); }, [catalogForSearch, menuItems, isSearchingItems, itemSearchQuery, locale]); const showItemsLoading = isSearchingItems ? loadingAllCatalog : loadingItems; const categoryNameById = useMemo( () => buildCategoryNameMap(categories ?? []), [categories] ); const itemVisualKind = (item: MenuItem) => inferMenuItemKind(item.categoryId, categoryNameById.get(item.categoryId)); const sub = subtotal(); const discount = appliedCoupon?.discountAmount ?? 0; const taxable = Math.max(0, sub - discount); const tax = Math.round(taxable * TAX_RATE); const total = taxable + tax; const couponErrorKey = (code: string) => { const map: Record = { COUPON_NOT_FOUND: "couponInvalid", COUPON_INACTIVE: "couponInvalid", COUPON_EXPIRED: "couponExpired", COUPON_NOT_STARTED: "couponNotStarted", COUPON_LIMIT_REACHED: "couponLimitReached", COUPON_MIN_ORDER: "couponMinOrder", CART_EMPTY: "couponCartEmpty", COUPON_REQUIRED: "couponRequired", COUPON_NO_DISCOUNT: "couponInvalid", }; return map[code] ?? "couponInvalid"; }; const validateCoupon = useMutation({ mutationFn: async () => { if (!cafeId) throw new Error("no cafe"); return apiPost<{ couponId: string; code: string; discountAmount: number; }>(`/api/cafes/${cafeId}/coupons/validate`, { code: couponCode.trim(), subtotal: subtotal(), }); }, onSuccess: (data) => { setAppliedCoupon({ id: data.couponId, code: data.code, discountAmount: data.discountAmount, }); setCouponMessage({ type: "success", text: t("couponApplied", { code: data.code, amount: formatCurrency(data.discountAmount, numberLocale), }), }); }, onError: (err: Error) => { setAppliedCoupon(null); const code = err instanceof ApiClientError ? err.code : "COUPON_NOT_FOUND"; setCouponMessage({ type: "error", text: t(couponErrorKey(code)) }); }, }); const openKitchenSlip = useCallback(() => { const pending = getPendingLines(); const lines = pending.length > 0 ? buildKitchenLines(pending, items) : cartToKitchenLines(items); if (lines.length === 0) return; setKitchenSlip({ lines, orderId: activeOrderId ?? undefined, tableNumber: tables?.find((tbl) => tbl.id === tableId)?.number ?? null, guestName: guestName.trim() || null, }); }, [getPendingLines, items, activeOrderId, tables, tableId, guestName]); const submitOrder = useMutation({ mutationFn: async () => { if (!cafeId || items.length === 0) throw new Error("empty"); const cart = useCartStore.getState(); const pending = cart.getPendingLines(); if (pending.length === 0) throw new Error("nothing pending"); const kitchenLines = buildKitchenLines(pending, cart.items); const order = await submitOrderToApi({ cafeId, orderBranchId: orderBranchId ?? undefined, cart, reservationId, cartItems: cart.items, }); return { order, kitchenLines }; }, onMutate: () => ({ hadSession: !!useCartStore.getState().activeOrderId }), onSuccess: ({ order, kitchenLines }, _, context) => { hydrateFromOrder(order, menuById); syncUrl(order.tableId ?? tableId, order.id); setCouponMessage(null); if (kitchenLines.length > 0) { setKitchenSlip({ lines: kitchenLines, orderId: order.id, tableNumber: order.tableNumber ?? null, guestName: order.guestName ?? (guestName.trim() || null), }); } const baseMsg = context?.hadSession ? t("addToOrder") : t("orderPlaced"); setOrderMessage(baseMsg); void apiPost(`/api/cafes/${cafeId}/queue/next`, { branchId: orderBranchId, customerLabel: order.guestName ?? (guestName.trim() || undefined), orderId: order.id, }) .then((ticket) => { setOrderMessage( `${baseMsg} · ${t("queueNumber", { number: ticket.number })}` ); queryClient.invalidateQueries({ queryKey: ["queue-today"] }); }) .catch(() => undefined); queryClient.invalidateQueries({ queryKey: ["orders-live", cafeId] }); queryClient.invalidateQueries({ queryKey: ["orders-open", cafeId] }); queryClient.invalidateQueries({ queryKey: ["tables-board", cafeId] }); if (reservationId) { queryClient.invalidateQueries({ queryKey: ["reservations", cafeId] }); } }, onError: (err: Error) => { if (err instanceof ApiClientError) { if (err.code === "ORDER_NOT_FOUND" || err.code === "ORDER_NOT_OPEN") { setActiveOrderId(null); syncUrl(tableId, null); } const key = err.code === "TABLE_NOT_AVAILABLE" ? "tableNotAvailable" : err.code === "TABLE_OCCUPIED" ? "tableOccupied" : err.code === "PLAN_LIMIT_REACHED" ? "planLimit" : err.code === "INVALID_ORDER" ? "orderInvalid" : err.code === "ORDER_NOT_OPEN" ? "orderNotOpen" : err.code === "ORDER_NOT_FOUND" ? "orderNotOpen" : err.code === "VALIDATION_ERROR" ? "orderValidation" : "orderError"; setOrderMessage( err.code === "VALIDATION_ERROR" ? err.message : key === "planLimit" ? tErrors("planLimit") : t(key) ); return; } if (err.message === "nothing pending") { setOrderMessage(t("nothingPending")); return; } setOrderMessage(t("orderError")); }, }); const submitOrderAndPay = useMutation({ mutationFn: async () => { if (!cafeId || items.length === 0) throw new Error("empty"); const cart = useCartStore.getState(); const pending = cart.getPendingLines(); if (pending.length === 0) throw new Error("nothing pending"); const kitchenLines = buildKitchenLines(pending, cart.items); const order = await submitOrderToApi({ cafeId, orderBranchId: orderBranchId ?? undefined, cart, reservationId, cartItems: cart.items, }); const due = orderAmountDue(order); if (isLocalOrder(order.id)) return { order, kitchenLines }; const payBranchId = order.branchId ?? orderBranchId; if (due > 0) { if (!payBranchId) throw new Error("no branch"); await requestPosPayment(cafeId, payBranchId, order.id, due); await apiPost(`/api/cafes/${cafeId}/orders/${order.id}/payments`, { payments: [{ method: "Card", amount: due }], }); } return { order, kitchenLines }; }, onMutate: () => ({ hadSession: !!useCartStore.getState().activeOrderId }), onSuccess: ({ order, kitchenLines }, _, context) => { hydrateFromOrder(order, menuById); syncUrl(order.tableId ?? tableId, order.id); setCouponMessage(null); if (kitchenLines.length > 0) { setKitchenSlip({ lines: kitchenLines, orderId: order.id, tableNumber: order.tableNumber ?? null, guestName: order.guestName ?? (guestName.trim() || null), }); } const baseMsg = context?.hadSession ? t("orderPaidAdd") : t("orderPaidNew"); setOrderMessage(baseMsg); queryClient.invalidateQueries({ queryKey: ["orders-live", cafeId] }); queryClient.invalidateQueries({ queryKey: ["orders-open", cafeId] }); queryClient.invalidateQueries({ queryKey: ["tables-board", cafeId] }); if (reservationId) { queryClient.invalidateQueries({ queryKey: ["reservations", cafeId] }); } }, onError: (err: Error) => { if (err instanceof ApiClientError) { if ( err.code === "POS_DEVICE_CONNECTION_FAILED" || err.code === "POS_DEVICE_TIMEOUT" || err.code === "POS_DEVICE_REJECTED" || err.code.startsWith("POS_DEVICE") ) { setOrderMessage(posDeviceErrorMessage(err, t)); return; } if (err.code === "ORDER_NOT_FOUND" || err.code === "ORDER_NOT_OPEN") { setActiveOrderId(null); syncUrl(tableId, null); } const key = err.code === "TABLE_NOT_AVAILABLE" ? "tableNotAvailable" : err.code === "TABLE_OCCUPIED" ? "tableOccupied" : err.code === "PLAN_LIMIT_REACHED" ? "planLimit" : err.code === "INVALID_ORDER" ? "orderInvalid" : err.code === "ORDER_NOT_OPEN" ? "orderNotOpen" : err.code === "ORDER_NOT_FOUND" ? "orderNotOpen" : err.code === "VALIDATION_ERROR" ? "orderValidation" : "orderError"; setOrderMessage( err.code === "VALIDATION_ERROR" ? err.message : key === "planLimit" ? tErrors("planLimit") : t(key) ); return; } if (err.message === "nothing pending") { setOrderMessage(t("nothingPending")); return; } if (err.message === "no branch") { setOrderMessage(t("posDeviceNoBranch")); return; } setOrderMessage(t("payError")); }, }); const pendingCount = getPendingLines().length; const isOrderBusy = submitOrder.isPending || submitOrderAndPay.isPending; // Counter/takeaway orders don't require a table const canSubmitOrder = pendingCount > 0 && (orderType === "counter" || orderType === "takeaway" || !!tableId || !!customerId || guestName.trim().length > 0); // Show order type picker when there's no active session const showTypePicker = posMode === "order" && orderType === null && items.length === 0 && !urlOrderId && !activeOrderId; // The current table number for display const currentTableNumber = tables?.find((tbl) => tbl.id === tableId)?.number; if (!cafeId) return null; return (
{/* ── Top bar: mode switcher ─────────────────────────────────────────── */}
{/* ── Pay mode ──────────────────────────────────────────────────────── */} {posMode === "pay" ? ( ) : showTypePicker ? ( /* ── Order type picker ──────────────────────────────────────────── */ ) : ( /* ── Order screen ───────────────────────────────────────────────── */
{/* Order screen header */}
{/* Back / new order button */} {/* Order type badge — tappable to change table */} {orderType ? ( setShowTablePicker(true) : undefined } t={t} /> ) : null} {/* Active order number */} {activeOrderId ? ( # {activeOrderDisplayNumber ? String(activeOrderDisplayNumber) : formatOrderNumber({ id: activeOrderId })} ) : null}
{/* Queue bar */} {cafeId ? ( ) : null}
{/* Reservation banner */} {reservationId && reservationGuest ? (
{t("reservationBanner", { name: reservationGuest })}
) : null} {/* ── Main split: menu + cart ──────────────────────────────────── */}
{/* ── Menu panel ────────────────────────────────────────────── */}
{/* Search bar */}
setItemSearch(e.target.value)} placeholder={t("searchItemsPlaceholder")} aria-label={t("searchItems")} className="h-10 ps-9 pe-9" /> {itemSearch ? ( ) : null}
{/* Horizontal scrolling category tabs — no wrap */}
{loadingCategories ? Array.from({ length: 4 }).map((_, i) => ( )) : categories?.map((c) => ( ))}
{/* Product grid — bigger cards with image area */}
{showItemsLoading ? Array.from({ length: 8 }).map((_, i) => ( )) : filteredItems.length === 0 && isSearchingItems ? (

{t("searchNoResults")}

) : filteredItems.map((item) => { const qty = items.find( (ci) => ci.menuItem.id === item.id )?.quantity; return ( ); })}
{/* ── Cart sidebar ──────────────────────────────────────────── */} {/* Cart header: title + table/type badge */}
{t("takeOrder")}
{orderType === "table" ? ( tableId ? ( ) : ( ) ) : orderType ? ( {orderType === "counter" ? t("counterBadge") : t("takeawayBadge")} ) : null}
{/* Customer picker */} {cafeId ? ( ) : null} {/* Transfer table (for table orders with active session) */} {activeOrderId && tableId ? ( ) : null} {/* Assign table button (for counter orders) */} {orderType === "counter" && activeOrderId ? ( ) : null}
{/* Cart items */}
{items.length === 0 ? (

{t("emptyCart")}

) : ( items.map((line) => (

{line.isVoided ? ( {t("voided")} ) : ( formatCurrency( line.menuItem.price * line.quantity, numberLocale ) )}

e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} > {isManager && line.orderItemId && !line.isVoided && activeOrderId ? ( ) : null} {!line.isVoided ? ( <> {formatNumber(line.quantity, numberLocale)} ) : null}
{!line.isVoided && ( setNotes(line.menuItem.id, e.target.value)} onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()} placeholder={t("itemNotePlaceholder")} className="w-full rounded-md border border-border/70 bg-background px-2 py-1 text-[11px] placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary/40" /> )}
)) )}
{/* Totals + actions */}
{/* Coupon row */}
{ setCouponCode(e.target.value); if (couponMessage) setCouponMessage(null); }} onKeyDown={(e) => { if ( e.key === "Enter" && !appliedCoupon && couponCode.trim() && !validateCoupon.isPending ) { e.preventDefault(); validateCoupon.mutate(); } }} disabled={!!appliedCoupon} dir="ltr" className="h-8 text-end text-sm" /> {appliedCoupon ? ( ) : ( )}
{couponMessage ? (

{couponMessage.text}

) : null} {appliedCoupon ? (
{t("couponActive", { code: appliedCoupon.code })} -{formatCurrency(discount, numberLocale)}
) : null}
{t("subtotal")} {formatCurrency(sub, numberLocale)}
{discount > 0 ? (
{t("discount")} -{formatCurrency(discount, numberLocale)}
) : null}
{t("tax")} {formatCurrency(tax, numberLocale)}
{t("total")} {formatCurrency(total, numberLocale)}
{!isOnline ? (

{t("offlineQueueNotice")}

) : null} {orderMessage ? (

{orderMessage}

) : null} {!canSubmitOrder && items.length > 0 && orderType !== "counter" && orderType !== "takeaway" ? (

{t("needTableOrName")}

) : null} {items.some((line) => !line.isVoided) ? ( ) : null}
)} {/* ── Table picker modal (for Table orders & counter assign) ────────── */} {showTablePicker ? (

{t("selectTableBoard")}

{cafeId ? ( ) : null}
) : null} {/* ── Kitchen slip modal ────────────────────────────────────────────── */} {kitchenSlip ? ( setKitchenSlip(null)} /> ) : null} {/* ── Transfer table modal ──────────────────────────────────────────── */} {showTransferPicker ? (

{t("selectTargetTable")}

{freeTransferTables.length === 0 ? (

{t("noOrderOnTable")}

) : ( freeTransferTables.map((tbl) => ( )) )}
) : null}
); }