'use client'; /** * /orderfromtable — Single reusable table ordering page (QR driven) * * This page is the ONLY customer-facing order page for physical tables. * All 80 table QR codes point here with ?table=backaplan-001 etc. * * - Strict validation of table param (branch + 001-040) * - Local item selection (independent of global cart) * - On "Send order" we only show confirmation (no backend call yet) * - Payload is prepared exactly in the shape the future POST /api/table-orders will expect */ import React, { useMemo, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { motion, AnimatePresence } from 'framer-motion'; import { getMenuPosterSrc, logoUrl, SITE_ASSETS } from '@/lib/assets'; import { buildTableOrderPayload } from '@/application/table-order/table-order-use-cases'; import { parseTableId } from '@/domain/table/table-id'; import { addOrderLine, calculateLineTotal, calculateOrderTotal, formatLineQuantityLabel, removeOrderLine, updateOrderLineQuantity, type OrderLine, } from '@/domain/shared/order-line'; import { buildCartLineFromMenuItem } from '@/application/cart/cart-line-builder'; import { useMenu } from '@/presentation/providers/menu-provider'; import type { MenuItem } from '@/domain/menu/entities'; import { playAddSound, playSuccessSound } from '@/infrastructure/audio/web-audio-sound-adapter'; import { useLanguage } from '@/presentation/providers/language-provider'; import { getTranslation } from '@/presentation/i18n/translations'; import { getMenuItemDescription, getMenuItemName, localizeOrderLineName, } from '@/application/i18n/menu-localization'; import Navbar from '@/components/Navbar'; import Footer from '@/components/Footer'; import { Search, Plus, Minus, X, CheckCircle, UtensilsCrossed } from 'lucide-react'; export default function OrderFromTablePage() { const searchParams = useSearchParams(); const tableParam = searchParams.get('table'); const { language } = useLanguage(); const t = getTranslation(language); const to = t.tableOrder; const parsed = useMemo(() => parseTableId(tableParam), [tableParam]); const { allItems: allMenuItems } = useMenu(); // Local order state (NOT the global cart) const [selected, setSelected] = useState([]); const [search, setSearch] = useState(''); const [vegOnly, setVegOnly] = useState(false); const [notes, setNotes] = useState(''); const [isSending, setIsSending] = useState(false); const [sent, setSent] = useState(false); const isValid = parsed.valid; const branchLabel = isValid ? (parsed.branch === 'askim' ? to.branchAskim : to.branchBackaplan) : ''; const tableDisplay = isValid ? parsed.tableNumber : ''; // Filtered dishes for the selector grid (same data source as everywhere else) const filteredDishes = useMemo(() => { const query = search.toLowerCase().trim(); return allMenuItems.filter((item) => { if (vegOnly && !item.isVegetarian) return false; if (!query) return true; const localizedName = getMenuItemName(language, item).toLowerCase(); const localizedDescription = getMenuItemDescription(language, t, item).toLowerCase(); return ( localizedName.includes(query) || localizedDescription.includes(query) || item.name.toLowerCase().includes(query) ); }); }, [allMenuItems, search, vegOnly, language, t]); const selectedTotal = calculateOrderTotal(selected); function addDish(dish: MenuItem) { playAddSound(); setSelected((prev) => addOrderLine(prev, buildCartLineFromMenuItem(dish))); } function updateQty(id: string, newQty: number) { setSelected((prev) => updateOrderLineQuantity(prev, id, newQty)); } function removeItem(id: string) { setSelected((prev) => removeOrderLine(prev, id)); } function clearSelection() { setSelected([]); setNotes(''); } async function handleSendOrder() { if (!isValid || selected.length === 0) return; setIsSending(true); const orderPayload = buildTableOrderPayload( parsed.branch, parsed.tableNumber, parsed.tableIdentifier, selected, notes ); // For now we do NOT call any backend. // This log + comment makes future integration trivial. // TODO: replace the block below with real fetch when backend is ready. console.log('[TABLE ORDER] Prepared payload (will be POSTed to /api/table-orders):', orderPayload); // Simulate a tiny delay for nice UX (feels like it is being sent) await new Promise((r) => setTimeout(r, 420)); playSuccessSound(); setIsSending(false); setSent(true); } function resetForAnotherOrder() { setSent(false); setSelected([]); setNotes(''); setSearch(''); setVegOnly(false); } // ====================== INVALID QR STATE ====================== if (!isValid) { return (

{to.invalidTitle}

{to.invalidMessage}

Go to homepage
); } // ====================== CONFIRMATION STATE ====================== if (sent) { return (
{branchLabel.toUpperCase()} • {to.tableLabel.toUpperCase()} {tableDisplay}

{to.confirmationTitle}

{to.confirmationMessage}

Browse full menu

{branchLabel} — {to.tableLabel} {tableDisplay}

); } // ====================== MAIN ORDERING UI ====================== return (
{/* Table header bar */}
Shahi Kitchen
{branchLabel}
{to.tableLabel} {tableDisplay}
ORDER FROM TABLE
0 ? 'pb-36 lg:pb-24' : 'pb-24'}`}> {/* Intro */}
{to.tableLabel.toUpperCase()} {tableDisplay}

{to.selectTitle}

{to.selectSubtitle}

{/* DISH SELECTOR — below summary on mobile for faster checkout */}
{/* Filters */}
setSearch(e.target.value)} placeholder={to.searchPlaceholder} className="w-full rounded-2xl border border-[#EDE6D9] bg-white py-3 ps-11 pe-4 text-sm placeholder:text-[#8A8478] focus:border-[#B38B4D] focus:ring-1 focus:ring-[#B38B4D]/20 outline-none" />
{/* Dish grid */}
{filteredDishes.length > 0 ? ( filteredDishes.map((dish) => { const inOrder = selected.some((s) => s.id === dish.id); const poster = getMenuPosterSrc(dish, 'optimized'); const dishName = getMenuItemName(language, dish); const dishDescription = getMenuItemDescription(language, t, dish); return ( addDish(dish)} className="group text-left flex gap-4 rounded-2xl border border-[#EDE6D9] bg-white p-3 active:border-[#B38B4D] hover:border-[#c99a2e]/50 transition overflow-hidden" >
{dishName} { const target = e.currentTarget as HTMLImageElement; target.src = SITE_ASSETS.dishes.defaultPoster; }} />
{dishName}
{dish.pricing === 'weight' ? ( <> {dish.pricePerHalfKg} kr / ½ kg {dish.pricePerKg} {t.menu.perKg} ) : ( <> {dish.price} kr )}
{dishDescription && (
{dishDescription}
)}
{inOrder ? 'Add another' : 'Add to order'}
); }) ) : (
No dishes match your search.
)}
{/* ORDER SUMMARY (first on mobile, sticky on desktop) */}
{to.yourOrder}
{selected.length > 0 && ( )}
{selected.length === 0 ? (
🍽️

{to.emptyOrder}

) : (
{selected.map((item) => (
{localizeOrderLineName(language, item)}
{item.pricingMode === 'weight' ? `${item.pricePerHalfKg ?? item.price} kr / ½ kg · ${formatLineQuantityLabel(item)}` : `${item.price} kr × ${item.quantity}`} {' '}= {calculateLineTotal(item)} kr
{formatLineQuantityLabel(item)}
))}
)} {/* Notes */}