"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 { Minus, Plus, Search, Trash2, 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 { 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, })); } /** Small square thumb on menu tiles: text | img (RTL-aware via flex order). */ function PosMenuThumbnail({ imageUrl, kind, hasVideo, }: { imageUrl?: string | null; kind: ReturnType; hasVideo?: boolean; }) { return (
{hasVideo ? ( ) : null}
); } 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 { items, addItem, removeItem, updateQty, couponCode, appliedCoupon, setCouponCode, setAppliedCoupon, clearCoupon, tableId, setTableId, 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] ); useEffect(() => { const tid = searchParams.get("tableId"); if (tid) setTableId(tid); }, [searchParams, setTableId]); 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 [showTransferPicker, setShowTransferPicker] = useState(false); 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]); 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); }) .catch(() => undefined); }, [ cafeId, urlOrderId, menuById, activeOrderId, activeOrderDisplayNumber, items.length, hydrateFromOrder, setTableId, ]); 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) => { 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, activeOrderDisplayNumber, clearSession, t, ] ); 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 handleSelectMenuItem = (item: MenuItem) => { addItem(item); }; 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); // Can't process payment for a local (offline) 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; const canSubmitOrder = pendingCount > 0 && (!!tableId || !!customerId || guestName.trim().length > 0); if (!cafeId) return null; return (
{posMode === "pay" ? ( ) : (
{cafeId ? : null}
{/* Menu panel */}
{cafeId ? ( ) : null} {activeOrderId ? (

{t("sessionActive")} · {t("order")}{" "} {activeOrderDisplayNumber ? String(activeOrderDisplayNumber) : formatOrderNumber({ id: activeOrderId })}

) : null} {reservationId && reservationGuest ? (
{t("reservationBanner", { name: reservationGuest })}
) : null}
setItemSearch(e.target.value)} placeholder={t("searchItemsPlaceholder")} aria-label={t("searchItems")} className="h-9 ps-9 pe-9" /> {itemSearch ? ( ) : null}
{loadingCategories ? Array.from({ length: 3 }).map((_, i) => ( )) : categories?.map((c) => ( ))}
{showItemsLoading ? Array.from({ length: 8 }).map((_, i) => ( )) : filteredItems.length === 0 && isSearchingItems ? (

{t("searchNoResults")}

) : filteredItems.map((item) => ( ))}
{/* Cart sidebar */}
{t("takeOrder")} {tableId ? ( {t("table")}{" "} {tables?.find((tbl) => tbl.id === tableId)?.number ?? "—"} ) : ( {t("selectTableBoard")} )}
{cafeId ? ( ) : null} {activeOrderId && tableId ? ( ) : null}
{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}
)) )}
{ 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 ? (

{t("needTableOrName")}

) : null} {items.some((line) => !line.isVoided) ? ( ) : null}
)} {kitchenSlip ? ( setKitchenSlip(null)} /> ) : null} {showTransferPicker ? (

{t("selectTargetTable")}

{freeTransferTables.length === 0 ? (

{t("noOrderOnTable")}

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