HamzaAri's picture
๐Ÿš€ Deploy ScanMenu - Production-ready SaaS web app for digital restaurant menus & QR ordering
e1ef9fc verified
'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>
);
}