From dbf9f423101a9a9c1b3290a9c187bbc82d027d67 Mon Sep 17 00:00:00 2001 From: dwindown Date: Fri, 21 Nov 2025 00:31:10 +0700 Subject: [PATCH] feat(customers): Add customer detail page with stats and orders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created comprehensive customer detail page: Features: ✅ Customer Info Card: - Avatar with User icon - Name and email display - Member/Guest badge - Stats grid: Total Orders, Total Spent, Registered date ✅ Contact Information: - Email address - Phone number (if available) ✅ Billing Address: - Full address display - City, state, postcode - Country ✅ Recent Orders Section: - Shows last 10 orders - Order number, status badge, date - Total amount and item count - Clickable to view order details - Link to view all orders ✅ Page Header: - Customer name as title - Back button (to customers list) - Edit button (to edit page) ✅ Navigation: - Name in index → Detail page (/customers/:id) - Edit button → Edit page (/customers/:id/edit) - Order cards → Order detail (/orders/:id) ✅ Loading & Error States: - Skeleton loaders while fetching - Error card with retry option - Empty state for no orders Technical: - Uses OrdersApi to fetch customer orders - Filters completed/processing orders for stats - Responsive grid layout - Consistent with other detail pages (Orders) - TypeScript with proper type annotations Files: - Created: routes/Customers/Detail.tsx - Updated: App.tsx (added route) - Updated: routes/Customers/index.tsx (links to detail) Result: Complete customer profile view with order history! --- admin-spa/src/App.tsx | 2 + admin-spa/src/routes/Customers/Detail.tsx | 252 ++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 admin-spa/src/routes/Customers/Detail.tsx diff --git a/admin-spa/src/App.tsx b/admin-spa/src/App.tsx index efac767..700a0b4 100644 --- a/admin-spa/src/App.tsx +++ b/admin-spa/src/App.tsx @@ -24,6 +24,7 @@ import CouponEdit from '@/routes/Coupons/Edit'; import CustomersIndex from '@/routes/Customers'; import CustomerNew from '@/routes/Customers/New'; import CustomerEdit from '@/routes/Customers/Edit'; +import CustomerDetail from '@/routes/Customers/Detail'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { LayoutDashboard, ReceiptText, Package, Tag, Users, Settings as SettingsIcon, Maximize2, Minimize2, Loader2 } from 'lucide-react'; import { Toaster } from 'sonner'; @@ -486,6 +487,7 @@ function AppRoutes() { } /> } /> } /> + } /> {/* More */} } /> diff --git a/admin-spa/src/routes/Customers/Detail.tsx b/admin-spa/src/routes/Customers/Detail.tsx new file mode 100644 index 0000000..9864288 --- /dev/null +++ b/admin-spa/src/routes/Customers/Detail.tsx @@ -0,0 +1,252 @@ +import React from 'react'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; +import { __ } from '@/lib/i18n'; +import { CustomersApi } from '@/lib/api/customers'; +import { OrdersApi } from '@/lib/api'; +import { showErrorToast, getPageLoadErrorMessage } from '@/lib/errorHandling'; +import { usePageHeader } from '@/contexts/PageHeaderContext'; +import { ErrorCard } from '@/components/ErrorCard'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Card } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { ArrowLeft, Edit, Mail, Calendar, ShoppingBag, DollarSign, User, MapPin } from 'lucide-react'; +import { formatMoney } from '@/lib/currency'; + +export default function CustomerDetail() { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const customerId = parseInt(id || '0', 10); + + // Fetch customer data + const customerQuery = useQuery({ + queryKey: ['customer', customerId], + queryFn: () => CustomersApi.get(customerId), + enabled: !!customerId, + }); + + // Fetch customer orders + const ordersQuery = useQuery({ + queryKey: ['customer-orders', customerId], + queryFn: () => OrdersApi.list({ customer_id: customerId, per_page: 100 }), + enabled: !!customerId, + }); + + const customer = customerQuery.data; + const orders = ordersQuery.data?.orders || []; + const { setPageHeader, clearPageHeader } = usePageHeader(); + + // Page header + React.useEffect(() => { + if (!customer) { + clearPageHeader(); + return; + } + + const actions = ( +
+ + +
+ ); + + setPageHeader( + customer.display_name || `${customer.first_name} ${customer.last_name}`, + actions + ); + + return () => clearPageHeader(); + }, [customer, customerId, navigate, setPageHeader, clearPageHeader]); + + // Loading state + if (customerQuery.isLoading) { + return ( +
+ + +
+ ); + } + + // Error state + if (customerQuery.isError || !customer) { + return ( + customerQuery.refetch()} + /> + ); + } + + // Calculate stats from orders + const completedOrders = orders.filter((o: any) => o.status === 'completed' || o.status === 'processing'); + const totalSpent = completedOrders.reduce((sum: number, order: any) => sum + parseFloat(order.total || '0'), 0); + + return ( +
+ {/* Customer Info Card */} + +
+
+
+ +
+
+

+ {customer.display_name || `${customer.first_name} ${customer.last_name}`} +

+

{customer.email}

+
+
+ + {customer.role === 'customer' ? __('Member') : __('Guest')} + +
+ + {/* Stats Grid */} +
+
+
+ + {__('Total Orders')} +
+
{customer.stats?.total_orders || 0}
+
+ +
+
+ + {__('Total Spent')} +
+
+ {customer.stats?.total_spent ? formatMoney(customer.stats.total_spent) : formatMoney(0)} +
+
+ +
+
+ + {__('Registered')} +
+
+ {new Date(customer.registered).toLocaleDateString('id-ID', { + year: 'numeric', + month: 'long', + day: 'numeric' + })} +
+
+
+
+ + {/* Contact & Address Info */} +
+ {/* Contact Info */} + +

+ + {__('Contact Information')} +

+
+
+
{__('Email')}
+
{customer.email}
+
+ {customer.billing?.phone && ( +
+
{__('Phone')}
+
{customer.billing.phone}
+
+ )} +
+
+ + {/* Billing Address */} + {customer.billing && (customer.billing.address_1 || customer.billing.city) && ( + +

+ + {__('Billing Address')} +

+
+ {customer.billing.address_1 &&
{customer.billing.address_1}
} + {customer.billing.address_2 &&
{customer.billing.address_2}
} +
+ {[customer.billing.city, customer.billing.state, customer.billing.postcode] + .filter(Boolean) + .join(', ')} +
+ {customer.billing.country &&
{customer.billing.country}
} +
+
+ )} +
+ + {/* Recent Orders */} + +
+

{__('Recent Orders')}

+ {orders.length > 0 && ( + + {__('View all orders')} + + )} +
+ + {ordersQuery.isLoading ? ( +
+ + +
+ ) : orders.length === 0 ? ( +
+ +

{__('No orders yet')}

+
+ ) : ( +
+ {orders.slice(0, 10).map((order: any) => ( + +
+
+
+ #{order.number} + + {order.status} + +
+
+ {new Date(order.date_created).toLocaleDateString('id-ID')} +
+
+
+
{formatMoney(parseFloat(order.total || '0'))}
+
+ {order.line_items?.length || 0} {__('items')} +
+
+
+ + ))} +
+ )} +
+
+ ); +}