| 'use client'; |
|
|
| import { DashboardLayout } from '@/components/layout/dashboard-layout'; |
| import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; |
| import { Button } from '@/components/ui/button'; |
| import { Badge } from '@/components/ui/badge'; |
| import { |
| Clock, |
| MapPin, |
| ShoppingBag, |
| Truck, |
| Phone, |
| Mail, |
| ChevronRight, |
| CheckCircle2, |
| XCircle, |
| ChefHat, |
| Bell, |
| ArrowRight, |
| Filter, |
| Search, |
| } from 'lucide-react'; |
| import { demoOrders, demoOrderItems } from '@/lib/demo-data'; |
| import { formatCurrency, formatRelativeTime, getOrderStatusColor, getOrderTypeLabel, cn } from '@/lib/utils'; |
| import { useState } from 'react'; |
| import { Order, OrderStatus } from '@/types/database'; |
| import { Input } from '@/components/ui/input'; |
|
|
| const statusFlow: OrderStatus[] = ['pending', 'confirmed', 'preparing', 'ready', 'delivered']; |
|
|
| function OrderCard({ order, isSelected, onClick }: { order: Order; isSelected: boolean; onClick: () => void }) { |
| const items = demoOrderItems.filter((i) => i.order_id === order.id); |
| const typeIcons = { |
| dine_in: MapPin, |
| takeaway: ShoppingBag, |
| delivery: Truck, |
| }; |
| const TypeIcon = typeIcons[order.order_type]; |
|
|
| return ( |
| <div |
| onClick={onClick} |
| className={cn( |
| 'cursor-pointer rounded-xl border p-4 transition-all', |
| isSelected |
| ? 'border-emerald-500 bg-emerald-50/50 shadow-sm ring-1 ring-emerald-500/20 dark:bg-emerald-950/20' |
| : 'border-zinc-200/60 bg-white hover:border-zinc-300 hover:shadow-sm dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700' |
| )} |
| > |
| <div className="flex items-start justify-between"> |
| <div className="flex items-center gap-2"> |
| <div className={cn('flex h-9 w-9 items-center justify-center rounded-lg', getOrderStatusColor(order.status))}> |
| <TypeIcon className="h-4 w-4" /> |
| </div> |
| <div> |
| <div className="flex items-center gap-2"> |
| <span className="text-sm font-bold text-zinc-900 dark:text-zinc-100">#{order.id.split('-')[1]}</span> |
| <Badge className={cn('text-[10px]', getOrderStatusColor(order.status))} variant="outline"> |
| {order.status} |
| </Badge> |
| </div> |
| <p className="text-xs text-zinc-500">{order.customer_name}</p> |
| </div> |
| </div> |
| <div className="text-right"> |
| <p className="text-sm font-bold text-zinc-900 dark:text-zinc-100">{formatCurrency(order.total)}</p> |
| <p className="text-[10px] text-zinc-400">{formatRelativeTime(order.created_at)}</p> |
| </div> |
| </div> |
| |
| <div className="mt-3 flex items-center justify-between"> |
| <div className="flex items-center gap-2 text-xs text-zinc-500"> |
| <span>{getOrderTypeLabel(order.order_type)}</span> |
| {order.table_number && <span>โข Table {order.table_number}</span>} |
| <span>โข {items.length} items</span> |
| </div> |
| <ChevronRight className="h-4 w-4 text-zinc-300" /> |
| </div> |
| </div> |
| ); |
| } |
|
|
| function OrderDetail({ order }: { order: Order }) { |
| const items = demoOrderItems.filter((i) => i.order_id === order.id); |
| const currentStatusIndex = statusFlow.indexOf(order.status); |
|
|
| return ( |
| <div className="space-y-6"> |
| {/* Header */} |
| <div className="flex items-start justify-between"> |
| <div> |
| <div className="flex items-center gap-3"> |
| <h2 className="text-xl font-bold text-zinc-900 dark:text-zinc-100">Order #{order.id.split('-')[1]}</h2> |
| <Badge className={cn('text-xs', getOrderStatusColor(order.status))} variant="outline"> |
| {order.status} |
| </Badge> |
| </div> |
| <p className="mt-1 text-sm text-zinc-500">{formatRelativeTime(order.created_at)}</p> |
| </div> |
| </div> |
| |
| {/* Status Timeline */} |
| {order.status !== 'cancelled' && ( |
| <div className="rounded-xl border border-zinc-200/60 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900"> |
| <div className="flex items-center justify-between"> |
| {statusFlow.map((status, index) => { |
| const isComplete = index <= currentStatusIndex; |
| const isCurrent = index === currentStatusIndex; |
| return ( |
| <div key={status} className="flex flex-1 items-center"> |
| <div className="flex flex-col items-center gap-1"> |
| <div className={cn( |
| 'flex h-8 w-8 items-center justify-center rounded-full border-2 transition-all', |
| isComplete ? 'border-emerald-500 bg-emerald-500 text-white' : 'border-zinc-200 text-zinc-400 dark:border-zinc-700', |
| isCurrent && 'ring-4 ring-emerald-100 dark:ring-emerald-900/30' |
| )}> |
| {isComplete ? <CheckCircle2 className="h-4 w-4" /> : <span className="text-xs">{index + 1}</span>} |
| </div> |
| <span className={cn('text-[10px] font-medium capitalize', isComplete ? 'text-emerald-600' : 'text-zinc-400')}> |
| {status} |
| </span> |
| </div> |
| {index < statusFlow.length - 1 && ( |
| <div className={cn('mx-1 h-0.5 flex-1', index < currentStatusIndex ? 'bg-emerald-500' : 'bg-zinc-200 dark:bg-zinc-700')} /> |
| )} |
| </div> |
| ); |
| })} |
| </div> |
| </div> |
| )} |
| |
| {/* Customer Info */} |
| <div className="rounded-xl border border-zinc-200/60 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900"> |
| <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Customer</h3> |
| <div className="mt-3 space-y-2"> |
| <div className="flex items-center gap-2 text-sm text-zinc-600"> |
| <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-zinc-100 dark:bg-zinc-800"> |
| <span className="text-xs font-bold">{order.customer_name?.split(' ').map(n => n[0]).join('')}</span> |
| </div> |
| <span className="font-medium">{order.customer_name}</span> |
| </div> |
| {order.customer_phone && ( |
| <div className="flex items-center gap-2 text-sm text-zinc-500"> |
| <Phone className="h-4 w-4" /> {order.customer_phone} |
| </div> |
| )} |
| {order.customer_email && ( |
| <div className="flex items-center gap-2 text-sm text-zinc-500"> |
| <Mail className="h-4 w-4" /> {order.customer_email} |
| </div> |
| )} |
| {order.delivery_address && ( |
| <div className="flex items-center gap-2 text-sm text-zinc-500"> |
| <MapPin className="h-4 w-4" /> {order.delivery_address} |
| </div> |
| )} |
| </div> |
| </div> |
| |
| {/* Items */} |
| <div className="rounded-xl border border-zinc-200/60 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900"> |
| <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">Items</h3> |
| <div className="mt-3 space-y-3"> |
| {items.map((item) => ( |
| <div key={item.id} className="flex items-center justify-between"> |
| <div className="flex items-center gap-3"> |
| <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-emerald-50 text-sm font-bold text-emerald-600 dark:bg-emerald-900/30"> |
| {item.quantity}x |
| </div> |
| <div> |
| <p className="text-sm font-medium text-zinc-900 dark:text-zinc-100">{item.product_name}</p> |
| {item.options && Object.entries(item.options).map(([key, value]) => ( |
| <p key={key} className="text-xs text-zinc-500">{key}: {String(value)}</p> |
| ))} |
| </div> |
| </div> |
| <p className="text-sm font-semibold text-zinc-900 dark:text-zinc-100">{formatCurrency(item.total_price)}</p> |
| </div> |
| ))} |
| </div> |
| |
| <div className="mt-4 space-y-1.5 border-t border-zinc-100 pt-4 dark:border-zinc-800"> |
| <div className="flex justify-between text-sm text-zinc-500"> |
| <span>Subtotal</span> |
| <span>{formatCurrency(order.subtotal)}</span> |
| </div> |
| <div className="flex justify-between text-sm text-zinc-500"> |
| <span>Tax</span> |
| <span>{formatCurrency(order.tax)}</span> |
| </div> |
| <div className="flex justify-between text-base font-bold text-zinc-900 dark:text-zinc-100"> |
| <span>Total</span> |
| <span>{formatCurrency(order.total)}</span> |
| </div> |
| </div> |
| </div> |
| |
| {/* Action Buttons */} |
| {order.status !== 'delivered' && order.status !== 'cancelled' && ( |
| <div className="flex gap-2"> |
| {order.status === 'pending' && ( |
| <> |
| <Button variant="primary" className="flex-1"> |
| <CheckCircle2 className="h-4 w-4" /> Confirm Order |
| </Button> |
| <Button variant="destructive" className="flex-1"> |
| <XCircle className="h-4 w-4" /> Cancel |
| </Button> |
| </> |
| )} |
| {order.status === 'confirmed' && ( |
| <Button variant="primary" className="flex-1"> |
| <ChefHat className="h-4 w-4" /> Start Preparing |
| </Button> |
| )} |
| {order.status === 'preparing' && ( |
| <Button variant="primary" className="flex-1"> |
| <Bell className="h-4 w-4" /> Mark Ready |
| </Button> |
| )} |
| {order.status === 'ready' && ( |
| <Button variant="primary" className="flex-1"> |
| <CheckCircle2 className="h-4 w-4" /> Mark Delivered |
| </Button> |
| )} |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|
| export default function OrdersPage() { |
| const [selectedOrder, setSelectedOrder] = useState<Order>(demoOrders[0]); |
| const [statusFilter, setStatusFilter] = useState<string>('all'); |
| const [search, setSearch] = useState(''); |
|
|
| const filteredOrders = demoOrders.filter((o) => { |
| if (statusFilter !== 'all' && o.status !== statusFilter) return false; |
| if (search && !o.customer_name?.toLowerCase().includes(search.toLowerCase()) && !o.id.includes(search)) return false; |
| return true; |
| }); |
|
|
| const statusCounts = { |
| all: demoOrders.length, |
| pending: demoOrders.filter((o) => o.status === 'pending').length, |
| confirmed: demoOrders.filter((o) => o.status === 'confirmed').length, |
| preparing: demoOrders.filter((o) => o.status === 'preparing').length, |
| ready: demoOrders.filter((o) => o.status === 'ready').length, |
| delivered: demoOrders.filter((o) => o.status === 'delivered').length, |
| }; |
|
|
| return ( |
| <DashboardLayout> |
| <div className="space-y-6"> |
| {/* Header */} |
| <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> |
| <div> |
| <h1 className="text-2xl font-bold tracking-tight text-zinc-900 dark:text-white sm:text-3xl">Orders</h1> |
| <p className="mt-1 text-sm text-zinc-500">Manage and track customer orders in real-time.</p> |
| </div> |
| <div className="flex items-center gap-2"> |
| <div className="flex items-center gap-1.5 rounded-full bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"> |
| <span className="relative flex h-2 w-2"> |
| <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-75" /> |
| <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" /> |
| </span> |
| Live |
| </div> |
| </div> |
| </div> |
| |
| {/* Status Filter Tabs */} |
| <div className="flex gap-2 overflow-x-auto pb-2 scrollbar-none"> |
| {Object.entries(statusCounts).map(([status, count]) => ( |
| <button |
| key={status} |
| onClick={() => setStatusFilter(status)} |
| className={cn( |
| 'flex items-center gap-1.5 whitespace-nowrap rounded-xl px-4 py-2 text-sm font-medium transition-all', |
| statusFilter === status |
| ? 'bg-zinc-900 text-white shadow-sm dark:bg-white dark:text-zinc-900' |
| : 'bg-white text-zinc-600 hover:bg-zinc-50 border border-zinc-200 dark:bg-zinc-900 dark:border-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-800' |
| )} |
| > |
| <span className="capitalize">{status}</span> |
| <span className={cn( |
| 'rounded-full px-1.5 py-0.5 text-[10px] font-bold', |
| statusFilter === status ? 'bg-white/20 text-white dark:bg-zinc-900/30 dark:text-zinc-900' : 'bg-zinc-100 text-zinc-500 dark:bg-zinc-800' |
| )}> |
| {count} |
| </span> |
| </button> |
| ))} |
| </div> |
| |
| {/* Two Column Layout */} |
| <div className="grid gap-6 lg:grid-cols-5"> |
| {/* Order List */} |
| <div className="space-y-3 lg:col-span-2"> |
| <div className="relative"> |
| <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400" /> |
| <Input |
| placeholder="Search orders..." |
| value={search} |
| onChange={(e) => setSearch(e.target.value)} |
| className="pl-10" |
| /> |
| </div> |
| <div className="space-y-2 max-h-[calc(100vh-320px)] overflow-y-auto pr-1 scrollbar-thin"> |
| {filteredOrders.map((order) => ( |
| <OrderCard |
| key={order.id} |
| order={order} |
| isSelected={selectedOrder?.id === order.id} |
| onClick={() => setSelectedOrder(order)} |
| /> |
| ))} |
| </div> |
| </div> |
| |
| {/* Order Detail */} |
| <div className="lg:col-span-3"> |
| <div className="sticky top-24"> |
| {selectedOrder ? ( |
| <OrderDetail order={selectedOrder} /> |
| ) : ( |
| <div className="flex flex-col items-center justify-center py-20 text-center"> |
| <ShoppingBag className="h-12 w-12 text-zinc-200" /> |
| <p className="mt-4 text-sm text-zinc-500">Select an order to view details</p> |
| </div> |
| )} |
| </div> |
| </div> |
| </div> |
| </div> |
| </DashboardLayout> |
| ); |
| } |
|
|