"use client"; import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; import useSWR from "swr"; import Link from "next/link"; import { Card, CardContent, CardHeader, CardTitle, CardDescription, } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { ArrowLeft, Plus, Pencil, Trash2, Mail, Phone, MapPin, User, FileText, Euro, ExternalLink, Package, BarChart3, } from "lucide-react"; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell, } from "recharts"; import { Progress } from "@/components/ui/progress"; import { useTableSort } from "@/hooks/use-table-sort"; import { SortableTableHead } from "@/components/sortable-table-head"; import { CustomerDialog } from "@/components/customer-dialog"; import { toast } from "sonner"; import { formatCurrency } from "@/lib/format"; import type { Customer, PurchaseOrder } from "@/lib/types"; const fetcher = (url: string) => fetch(url).then((r) => r.json()); const statusConfig: Record = { draft: { label: "Draft", variant: "secondary" }, confirmed: { label: "Confirmed", variant: "default" }, in_production: { label: "In Production", variant: "outline" }, shipped: { label: "Shipped", variant: "default" }, completed: { label: "Completed", variant: "default" }, cancelled: { label: "Cancelled", variant: "destructive" }, }; export default function CustomerDetailPage() { const params = useParams(); const router = useRouter(); const customerId = params.id as string; const [editOpen, setEditOpen] = useState(false); const [deleteOrderTarget, setDeleteOrderTarget] = useState(null); const [statsPeriod, setStatsPeriod] = useState<"week" | "month" | "year">("month"); const { data: customer, mutate: mutateCustomer } = useSWR( `/api/customers/${customerId}`, fetcher ); const { data: orders = [], mutate: mutateOrders } = useSWR( `/api/orders?customerId=${customerId}`, fetcher ); const { sorted: sortedOrders, sort: orderSort, toggleSort: toggleOrderSort } = useTableSort(orders); interface OnOrderItem { product_id: number; product_name: string; sku: string; total_ordered: number; total_shipped: number; on_order: number; } interface ProducedItem { product_id: number; product_name: string; sku: string; total_produced: number; } interface TimelineItem { bucket: string; product_name: string; sku: string; quantity: number; } interface StatsData { onOrder: OnOrderItem[]; produced: ProducedItem[]; producedTimeline: TimelineItem[]; } const { data: statsData } = useSWR( `/api/customers/${customerId}/stats?period=${statsPeriod}`, fetcher ); async function handleDeleteOrder() { if (!deleteOrderTarget) return; try { const res = await fetch(`/api/orders/${deleteOrderTarget.id}`, { method: "DELETE", }); if (!res.ok) throw new Error("Failed to delete"); toast.success("Purchase order deleted"); mutateOrders(); } catch { toast.error("Failed to delete order"); } finally { setDeleteOrderTarget(null); } } if (!customer) { return (

Loading customer...

); } const totalValue = orders.reduce( (sum, o) => sum + (o.status !== "cancelled" ? Number(o.total_value || 0) : 0), 0 ); const activeOrders = orders.filter( (o) => !["completed", "cancelled"].includes(o.status) ).length; return (
{/* Header */}

{customer.name}

{customer.code}

Customer since{" "} {new Date(customer.created_at).toLocaleDateString()}

{/* Customer Info + Stats */}
Contact Information {customer.contact_person && (
{customer.contact_person}
)} {customer.email && ( )} {customer.phone && (
{customer.phone}
)} {customer.address && (
{customer.address}
)} {customer.notes && (
{customer.notes}
)} {!customer.contact_person && !customer.email && !customer.phone && !customer.address && (

No contact information added yet.

)}
Active Orders

{activeOrders}

of {orders.length} total

Total Revenue

{formatCurrency(totalValue)}

excl. cancelled orders

{/* On Order + Production Stats */}
{/* On Order by Product */}
Units On Order Pending quantities by product across active orders
{statsData?.onOrder && statsData.onOrder.length > 0 ? ( Product Ordered Shipped Pending {statsData.onOrder.map((item) => (
{item.product_name} {item.sku}
{Number(item.total_ordered).toLocaleString()} {Number(item.total_shipped).toLocaleString()} {Number(item.on_order).toLocaleString()}
))}
) : (
No pending orders
)}
{/* Produced / Shipped by Product */}
Units Produced Shipped quantities by product
{statsData?.produced && statsData.produced.length > 0 ? (
{/* Bar chart */}
{statsPeriod === "year" && statsData.producedTimeline && statsData.producedTimeline.length > 0 ? (() => { const BAR_COLORS = [ "#6366f1", "#22c55e", "#3b82f6", "#f59e0b", "#a855f7", "#ef4444", ]; // Pivot timeline into {month, product1: qty, product2: qty, ...} const productSkus = [...new Set(statsData.producedTimeline.map((t) => t.sku))]; const productNames: Record = {}; statsData.producedTimeline.forEach((t) => { productNames[t.sku] = t.product_name; }); const bucketMap = new Map>(); statsData.producedTimeline.forEach((t) => { const d = new Date(t.bucket); const label = d.toLocaleDateString("en-GB", { month: "short", year: "2-digit" }); if (!bucketMap.has(label)) { const row: Record = { month: label }; productSkus.forEach((s) => { row[s] = 0; }); bucketMap.set(label, row); } const row = bucketMap.get(label)!; row[t.sku] = (Number(row[t.sku]) || 0) + Number(t.quantity); }); const chartData = Array.from(bucketMap.values()); return ( [ `${value.toLocaleString()} units`, productNames[name] || name, ]} /> productNames[value] || value} wrapperStyle={{ fontSize: "11px" }} /> {productSkus.map((sku, i) => ( ))} ); })() : ( ({ name: item.sku, fullName: item.product_name, units: Number(item.total_produced), }))} margin={{ top: 5, right: 5, left: 0, bottom: 5 }} > [ `${value.toLocaleString()} units`, props.payload?.fullName || "Units", ]} /> {statsData.produced.map((_entry, index) => { const colors = [ "#6366f1", "#22c55e", "#3b82f6", "#f59e0b", "#a855f7", "#ef4444", ]; return ( ); })} )}
{/* Summary table below chart */}
{statsData.produced.map((item) => (

{item.sku}

{Number(item.total_produced).toLocaleString()}

units

))}
) : (
No production data for this period
)}
{/* Purchase Orders Table */} Purchase Orders
Fulfillment Actions {sortedOrders.length === 0 ? ( No purchase orders yet. Click "New Order" to create one. ) : ( sortedOrders.map((order) => { const sc = statusConfig[order.status] || statusConfig.draft; return ( {order.po_number} {order.customer_ref || "-"} {sc.label} {new Date(order.order_date).toLocaleDateString()} {order.due_date ? new Date(order.due_date).toLocaleDateString() : "-"} {Number(order.item_count || 0)} {formatCurrency(Number(order.total_value || 0))} {(() => { const ordered = Number(order.total_ordered || 0); const shipped = Number(order.total_shipped || 0); const pct = ordered > 0 ? Math.round((shipped / ordered) * 100) : 0; if (order.status === "cancelled") return -; return (
{shipped}/{ordered} {pct}%
); })()}
); }) )}
mutateCustomer()} /> setDeleteOrderTarget(null)} > Delete Purchase Order Are you sure you want to delete PO " {deleteOrderTarget?.po_number}"? This will also remove all associated line items. Cancel Delete
); }