fix(customers): Add tabs to detail page and fix orders loading
Fixed 2 critical issues: 1. ✅ Orders Not Loading: Backend (OrdersController.php): - Added customer_id parameter support - Lines 300-304: Filter orders by customer - Uses WooCommerce customer_id arg Frontend (Detail.tsx): - Already passing customer_id correctly - Orders will now load properly 2. ✅ Added Tabs for Better Organization: Implemented 3-tab layout: **Overview Tab:** - Stats cards: Total Orders, Total Spent, Registered - Contact information (email, phone) - Clean, focused view **Orders Tab:** - Full order history (not just 10) - Order count display - Better empty state - All orders clickable to detail **Address Tab:** - Billing address (full details) - Shipping address (full details) - Company names if available - Phone in billing section - Empty states for missing addresses Benefits: ✅ Clean, organized, contextual data per tab ✅ No information overload ✅ Easy navigation between sections ✅ Better mobile experience ✅ Consistent with modern admin UX Technical: - Uses shadcn/ui Tabs component - Responsive grid layouts - Proper empty states - Type-safe with TypeScript Result: Customer detail page is now properly organized with working order history!
This commit is contained in:
@@ -10,6 +10,7 @@ import { ErrorCard } from '@/components/ErrorCard';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ArrowLeft, Edit, Mail, Calendar, ShoppingBag, DollarSign, User, MapPin } from 'lucide-react';
|
||||
import { formatMoney } from '@/lib/currency';
|
||||
|
||||
@@ -17,6 +18,7 @@ export default function CustomerDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const customerId = parseInt(id || '0', 10);
|
||||
const [activeTab, setActiveTab] = React.useState('overview');
|
||||
|
||||
// Fetch customer data
|
||||
const customerQuery = useQuery({
|
||||
@@ -89,9 +91,9 @@ export default function CustomerDetail() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6 pb-6">
|
||||
{/* Customer Info Card */}
|
||||
{/* Customer Info Header */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-start justify-between mb-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center">
|
||||
<User className="w-8 h-8 text-primary" />
|
||||
@@ -109,45 +111,53 @@ export default function CustomerDetail() {
|
||||
{customer.role === 'customer' ? __('Member') : __('Guest')}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="overview">{__('Overview')}</TabsTrigger>
|
||||
<TabsTrigger value="orders">{__('Orders')}</TabsTrigger>
|
||||
<TabsTrigger value="address">{__('Address')}</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Overview Tab */}
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
{/* Stats Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-4 rounded-lg bg-muted/50">
|
||||
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
||||
<ShoppingBag className="w-4 h-4" />
|
||||
<span className="text-sm">{__('Total Orders')}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{customer.stats?.total_orders || 0}</div>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-2 text-muted-foreground mb-2">
|
||||
<ShoppingBag className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">{__('Total Orders')}</span>
|
||||
</div>
|
||||
<div className="text-3xl font-bold">{customer.stats?.total_orders || 0}</div>
|
||||
</Card>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50">
|
||||
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
||||
<DollarSign className="w-4 h-4" />
|
||||
<span className="text-sm">{__('Total Spent')}</span>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-2 text-muted-foreground mb-2">
|
||||
<DollarSign className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">{__('Total Spent')}</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">
|
||||
<div className="text-3xl font-bold">
|
||||
{customer.stats?.total_spent ? formatMoney(customer.stats.total_spent) : formatMoney(0)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-muted/50">
|
||||
<div className="flex items-center gap-2 text-muted-foreground mb-1">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span className="text-sm">{__('Registered')}</span>
|
||||
</div>
|
||||
<div className="text-lg font-medium">
|
||||
{new Date(customer.registered).toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Contact & Address Info */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center gap-2 text-muted-foreground mb-2">
|
||||
<Calendar className="w-5 h-5" />
|
||||
<span className="text-sm font-medium">{__('Registered')}</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold">
|
||||
{new Date(customer.registered).toLocaleDateString('id-ID', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Contact Info */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
@@ -167,52 +177,33 @@ export default function CustomerDetail() {
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Billing Address */}
|
||||
{customer.billing && (customer.billing.address_1 || customer.billing.city) && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<MapPin className="w-5 h-5" />
|
||||
{__('Billing Address')}
|
||||
</h3>
|
||||
<div className="text-sm space-y-1">
|
||||
{customer.billing.address_1 && <div>{customer.billing.address_1}</div>}
|
||||
{customer.billing.address_2 && <div>{customer.billing.address_2}</div>}
|
||||
<div>
|
||||
{[customer.billing.city, customer.billing.state, customer.billing.postcode]
|
||||
.filter(Boolean)
|
||||
.join(', ')}
|
||||
</div>
|
||||
{customer.billing.country && <div>{customer.billing.country}</div>}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recent Orders */}
|
||||
{/* Orders Tab */}
|
||||
<TabsContent value="orders" className="space-y-4">
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">{__('Recent Orders')}</h3>
|
||||
{orders.length > 0 && (
|
||||
<Link to={`/orders?customer_id=${customerId}`} className="text-sm text-primary hover:underline">
|
||||
{__('View all orders')}
|
||||
</Link>
|
||||
)}
|
||||
<h3 className="text-lg font-semibold">{__('Order History')}</h3>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{orders.length} {__('orders')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ordersQuery.isLoading ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-16 w-full" />
|
||||
<Skeleton className="h-16 w-full" />
|
||||
<Skeleton className="h-16 w-full" />
|
||||
</div>
|
||||
) : orders.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<ShoppingBag className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||
<p>{__('No orders yet')}</p>
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<ShoppingBag className="w-16 h-16 mx-auto mb-3 opacity-50" />
|
||||
<p className="text-lg font-medium">{__('No orders yet')}</p>
|
||||
<p className="text-sm mt-1">{__('This customer hasn\'t placed any orders')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{orders.slice(0, 10).map((order: any) => (
|
||||
{orders.map((order: any) => (
|
||||
<Link
|
||||
key={order.id}
|
||||
to={`/orders/${order.id}`}
|
||||
@@ -247,6 +238,79 @@ export default function CustomerDetail() {
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Address Tab */}
|
||||
<TabsContent value="address" className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Billing Address */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<MapPin className="w-5 h-5" />
|
||||
{__('Billing Address')}
|
||||
</h3>
|
||||
{customer.billing && (customer.billing.address_1 || customer.billing.city) ? (
|
||||
<div className="space-y-1">
|
||||
{customer.billing.first_name && customer.billing.last_name && (
|
||||
<div className="font-medium">
|
||||
{customer.billing.first_name} {customer.billing.last_name}
|
||||
</div>
|
||||
)}
|
||||
{customer.billing.company && (
|
||||
<div className="text-sm text-muted-foreground">{customer.billing.company}</div>
|
||||
)}
|
||||
{customer.billing.address_1 && <div>{customer.billing.address_1}</div>}
|
||||
{customer.billing.address_2 && <div>{customer.billing.address_2}</div>}
|
||||
<div>
|
||||
{[customer.billing.city, customer.billing.state, customer.billing.postcode]
|
||||
.filter(Boolean)
|
||||
.join(', ')}
|
||||
</div>
|
||||
{customer.billing.country && <div>{customer.billing.country}</div>}
|
||||
{customer.billing.phone && (
|
||||
<div className="mt-3 pt-3 border-t">
|
||||
<div className="text-sm text-muted-foreground">{__('Phone')}</div>
|
||||
<div>{customer.billing.phone}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">{__('No billing address')}</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Shipping Address */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<MapPin className="w-5 h-5" />
|
||||
{__('Shipping Address')}
|
||||
</h3>
|
||||
{customer.shipping && (customer.shipping.address_1 || customer.shipping.city) ? (
|
||||
<div className="space-y-1">
|
||||
{customer.shipping.first_name && customer.shipping.last_name && (
|
||||
<div className="font-medium">
|
||||
{customer.shipping.first_name} {customer.shipping.last_name}
|
||||
</div>
|
||||
)}
|
||||
{customer.shipping.company && (
|
||||
<div className="text-sm text-muted-foreground">{customer.shipping.company}</div>
|
||||
)}
|
||||
{customer.shipping.address_1 && <div>{customer.shipping.address_1}</div>}
|
||||
{customer.shipping.address_2 && <div>{customer.shipping.address_2}</div>}
|
||||
<div>
|
||||
{[customer.shipping.city, customer.shipping.state, customer.shipping.postcode]
|
||||
.filter(Boolean)
|
||||
.join(', ')}
|
||||
</div>
|
||||
{customer.shipping.country && <div>{customer.shipping.country}</div>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-muted-foreground">{__('No shipping address')}</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -297,6 +297,12 @@ class OrdersController {
|
||||
$args['include'] = [ absint( $m[1] ) ];
|
||||
}
|
||||
|
||||
// Optional customer filter
|
||||
$customer_id = $req->get_param( 'customer_id' );
|
||||
if ( $customer_id ) {
|
||||
$args['customer_id'] = absint( $customer_id );
|
||||
}
|
||||
|
||||
$result = wc_get_orders( $args );
|
||||
|
||||
// If we had a two-sided date range, filter results manually to avoid OrdersTableQuery fatal.
|
||||
|
||||
Reference in New Issue
Block a user