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:
dwindown
2025-11-21 00:37:11 +07:00
parent dbf9f42310
commit 46e7e6f7c9
2 changed files with 192 additions and 122 deletions

View File

@@ -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,144 +111,206 @@ export default function CustomerDetail() {
{customer.role === 'customer' ? __('Member') : __('Guest')}
</span>
</div>
{/* 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>
</div>
<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>
</div>
<div className="text-2xl 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">
{/* Contact Info */}
<Card className="p-6">
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
<Mail className="w-5 h-5" />
{__('Contact Information')}
</h3>
<div className="space-y-3">
<div>
<div className="text-sm text-muted-foreground">{__('Email')}</div>
<div className="font-medium">{customer.email}</div>
</div>
{customer.billing?.phone && (
<div>
<div className="text-sm text-muted-foreground">{__('Phone')}</div>
<div className="font-medium">{customer.billing.phone}</div>
</div>
)}
</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>
{/* Billing Address */}
{customer.billing && (customer.billing.address_1 || customer.billing.city) && (
{/* Overview Tab */}
<TabsContent value="overview" className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<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>
<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-3xl font-bold">
{customer.stats?.total_spent ? formatMoney(customer.stats.total_spent) : formatMoney(0)}
</div>
</Card>
<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">
<MapPin className="w-5 h-5" />
{__('Billing Address')}
<Mail className="w-5 h-5" />
{__('Contact Information')}
</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 className="space-y-3">
<div>
{[customer.billing.city, customer.billing.state, customer.billing.postcode]
.filter(Boolean)
.join(', ')}
<div className="text-sm text-muted-foreground">{__('Email')}</div>
<div className="font-medium">{customer.email}</div>
</div>
{customer.billing.country && <div>{customer.billing.country}</div>}
{customer.billing?.phone && (
<div>
<div className="text-sm text-muted-foreground">{__('Phone')}</div>
<div className="font-medium">{customer.billing.phone}</div>
</div>
)}
</div>
</Card>
)}
</div>
</TabsContent>
{/* Recent Orders */}
<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>
)}
</div>
{/* 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">{__('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" />
</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>
) : (
<div className="space-y-2">
{orders.slice(0, 10).map((order: any) => (
<Link
key={order.id}
to={`/orders/${order.id}`}
className="block p-4 rounded-lg border border-border hover:bg-accent/50 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-3">
<span className="font-medium">#{order.number}</span>
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
order.status === 'completed' ? 'bg-green-100 text-green-800' :
order.status === 'processing' ? 'bg-blue-100 text-blue-800' :
order.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{order.status}
</span>
{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-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.map((order: any) => (
<Link
key={order.id}
to={`/orders/${order.id}`}
className="block p-4 rounded-lg border border-border hover:bg-accent/50 transition-colors"
>
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="flex items-center gap-3">
<span className="font-medium">#{order.number}</span>
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${
order.status === 'completed' ? 'bg-green-100 text-green-800' :
order.status === 'processing' ? 'bg-blue-100 text-blue-800' :
order.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{order.status}
</span>
</div>
<div className="text-sm text-muted-foreground mt-1">
{new Date(order.date_created).toLocaleDateString('id-ID')}
</div>
</div>
<div className="text-right">
<div className="font-bold">{formatMoney(parseFloat(order.total || '0'))}</div>
<div className="text-sm text-muted-foreground">
{order.line_items?.length || 0} {__('items')}
</div>
</div>
</div>
<div className="text-sm text-muted-foreground mt-1">
{new Date(order.date_created).toLocaleDateString('id-ID')}
</Link>
))}
</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>
<div className="text-right">
<div className="font-bold">{formatMoney(parseFloat(order.total || '0'))}</div>
<div className="text-sm text-muted-foreground">
{order.line_items?.length || 0} {__('items')}
{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>
</Link>
))}
) : (
<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>
)}
</Card>
</TabsContent>
</Tabs>
</div>
);
}

View File

@@ -296,6 +296,12 @@ class OrdersController {
if ( $search && preg_match( '/^#?(\\d+)$/', $search, $m ) ) {
$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 );