fix(customers): Improve index page UI and fix stats display
Fixed all 6 issues in Customer index: 1. ✅ Search Input - Match Coupon Module: - Mobile: Native input with proper styling - Desktop: Native input with proper styling - Consistent with Coupon module pattern - Better focus states and padding 2. ✅ Filter - Not Needed: - Customer data is simple (name, email, stats) - Search is sufficient for finding customers - No complex filtering like products/coupons 3. ✅ Stats Display - FIXED: - Backend: Changed format_customer() to include stats (detailed=true) - Now shows actual order count and total spent - No more zero orders or dashed values 4. ✅ Member/Guest Column - Added: - New 'Type' column in table - Shows badge: Member (blue) or Guest (gray) - Based on customer.role field 5. ✅ Actions Column - Added: - New 'Actions' column with Edit button - Edit icon + text link - Navigates to /customers/:id/edit 6. ✅ Navigation - Fixed: - Name click → Detail page (/customers/:id) - Edit button → Edit page (/customers/:id/edit) - Mobile cards also link to detail page - Separation of concerns: view vs edit Changes Made: Backend (CustomersController.php): - Line 96: format_customer(, true) to include stats Frontend (Customers/index.tsx): - Search inputs: Match Coupon module styling - Table: Added Type and Actions columns - Type badge: Member (blue) / Guest (gray) - Actions: Edit button with icon - Navigation: Name → detail, Edit → edit - Mobile cards: Link to detail page Table Structure: - Checkbox | Customer | Email | Type | Orders | Total Spent | Registered | Actions - 8 columns total (was 6) Next: Create customer detail page with related orders and stats
This commit is contained in:
@@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { ErrorCard } from '@/components/ErrorCard';
|
import { ErrorCard } from '@/components/ErrorCard';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { RefreshCw, Trash2, Search, User, ChevronRight } from 'lucide-react';
|
import { RefreshCw, Trash2, Search, User, ChevronRight, Edit } from 'lucide-react';
|
||||||
import { formatMoney } from '@/lib/currency';
|
import { formatMoney } from '@/lib/currency';
|
||||||
|
|
||||||
export default function CustomersIndex() {
|
export default function CustomersIndex() {
|
||||||
@@ -103,15 +103,14 @@ export default function CustomersIndex() {
|
|||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
<Input
|
<input
|
||||||
type="search"
|
|
||||||
placeholder={__('Search customers...')}
|
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearch(e.target.value);
|
setSearch(e.target.value);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
className="pl-9"
|
placeholder={__('Search customers...')}
|
||||||
|
className="w-full pl-10 pr-4 py-2.5 rounded-lg border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,15 +145,14 @@ export default function CustomersIndex() {
|
|||||||
<div className="flex gap-3 items-center">
|
<div className="flex gap-3 items-center">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
<Input
|
<input
|
||||||
type="search"
|
|
||||||
placeholder={__('Search customers...')}
|
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSearch(e.target.value);
|
setSearch(e.target.value);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}}
|
}}
|
||||||
className="pl-9 w-64"
|
placeholder={__('Search customers...')}
|
||||||
|
className="pl-10 pr-4 py-2 rounded-lg border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent w-64"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -175,15 +173,17 @@ export default function CustomersIndex() {
|
|||||||
</th>
|
</th>
|
||||||
<th className="text-left p-3 font-medium">{__('Customer')}</th>
|
<th className="text-left p-3 font-medium">{__('Customer')}</th>
|
||||||
<th className="text-left p-3 font-medium">{__('Email')}</th>
|
<th className="text-left p-3 font-medium">{__('Email')}</th>
|
||||||
|
<th className="text-left p-3 font-medium">{__('Type')}</th>
|
||||||
<th className="text-left p-3 font-medium">{__('Orders')}</th>
|
<th className="text-left p-3 font-medium">{__('Orders')}</th>
|
||||||
<th className="text-left p-3 font-medium">{__('Total Spent')}</th>
|
<th className="text-left p-3 font-medium">{__('Total Spent')}</th>
|
||||||
<th className="text-left p-3 font-medium">{__('Registered')}</th>
|
<th className="text-left p-3 font-medium">{__('Registered')}</th>
|
||||||
|
<th className="text-left p-3 font-medium">{__('Actions')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{customers.length === 0 ? (
|
{customers.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="p-8 text-center text-muted-foreground">
|
<td colSpan={8} className="p-8 text-center text-muted-foreground">
|
||||||
<User className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
<User className="w-12 h-12 mx-auto mb-2 opacity-50" />
|
||||||
{search ? __('No customers found matching your search') : __('No customers yet')}
|
{search ? __('No customers found matching your search') : __('No customers yet')}
|
||||||
{!search && (
|
{!search && (
|
||||||
@@ -206,11 +206,18 @@ export default function CustomersIndex() {
|
|||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3">
|
<td className="p-3">
|
||||||
<Link to={`/customers/${customer.id}/edit`} className="font-medium hover:underline">
|
<Link to={`/customers/${customer.id}`} className="font-medium hover:underline">
|
||||||
{customer.display_name || `${customer.first_name} ${customer.last_name}`}
|
{customer.display_name || `${customer.first_name} ${customer.last_name}`}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 text-sm text-muted-foreground">{customer.email}</td>
|
<td className="p-3 text-sm text-muted-foreground">{customer.email}</td>
|
||||||
|
<td className="p-3">
|
||||||
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||||
|
customer.role === 'customer' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{customer.role === 'customer' ? __('Member') : __('Guest')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td className="p-3 text-sm">{customer.stats?.total_orders || 0}</td>
|
<td className="p-3 text-sm">{customer.stats?.total_orders || 0}</td>
|
||||||
<td className="p-3 text-sm font-medium">
|
<td className="p-3 text-sm font-medium">
|
||||||
{customer.stats?.total_spent ? formatMoney(customer.stats.total_spent) : '—'}
|
{customer.stats?.total_spent ? formatMoney(customer.stats.total_spent) : '—'}
|
||||||
@@ -218,6 +225,15 @@ export default function CustomersIndex() {
|
|||||||
<td className="p-3 text-sm text-muted-foreground">
|
<td className="p-3 text-sm text-muted-foreground">
|
||||||
{new Date(customer.registered).toLocaleDateString()}
|
{new Date(customer.registered).toLocaleDateString()}
|
||||||
</td>
|
</td>
|
||||||
|
<td className="p-3">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(`/customers/${customer.id}/edit`)}
|
||||||
|
className="inline-flex items-center gap-1 text-sm text-primary hover:underline"
|
||||||
|
>
|
||||||
|
<Edit className="w-4 h-4" />
|
||||||
|
{__('Edit')}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -236,7 +252,7 @@ export default function CustomersIndex() {
|
|||||||
customers.map((customer) => (
|
customers.map((customer) => (
|
||||||
<Link
|
<Link
|
||||||
key={customer.id}
|
key={customer.id}
|
||||||
to={`/customers/${customer.id}/edit`}
|
to={`/customers/${customer.id}`}
|
||||||
className="block bg-card border border-border rounded-xl p-3 hover:bg-accent/50 transition-colors active:scale-[0.98] active:transition-transform shadow-sm"
|
className="block bg-card border border-border rounded-xl p-3 hover:bg-accent/50 transition-colors active:scale-[0.98] active:transition-transform shadow-sm"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class CustomersController {
|
|||||||
|
|
||||||
$customers = [];
|
$customers = [];
|
||||||
foreach ($users as $user) {
|
foreach ($users as $user) {
|
||||||
$customers[] = self::format_customer($user);
|
$customers[] = self::format_customer($user, true); // Include stats for list view
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WP_REST_Response([
|
return new WP_REST_Response([
|
||||||
|
|||||||
Reference in New Issue
Block a user