feat: Standardize table UI across Orders and Products modules

**Issue:**
- Orders and Products had inconsistent table styling
- Orders: px-3 py-2, no hover, no header bg
- Products: p-3, hover effect, header bg

**Solution:**
1. Added comprehensive Table/List UI Standards to PROJECT_SOP.md
2. Updated Orders table to match Products standard

**Changes to PROJECT_SOP.md:**
- Added "Table/List UI Standards" section
- Defined required classes for all table elements
- Specified padding: p-3 (NOT px-3 py-2)
- Specified header: bg-muted/50 + font-medium
- Specified rows: hover:bg-muted/30
- Added empty state and mobile card patterns

**Changes to Orders/index.tsx:**
- Container: border-border bg-card → border (match Products)
- Header: border-b → bg-muted/50 + border-b
- Header cells: px-3 py-2 → p-3 font-medium text-left
- Body rows: Added hover:bg-muted/30
- Body cells: px-3 py-2 → p-3
- Empty state: px-3 py-12 → p-8 text-muted-foreground

**Result:**
 Consistent padding across all modules (p-3)
 Consistent header styling (bg-muted/50 + font-medium)
 Consistent hover effects (hover:bg-muted/30)
 Consistent container styling (overflow-hidden)
 Documented standard for future modules
This commit is contained in:
dwindown
2025-11-20 10:14:39 +07:00
parent b592d50829
commit e267e3c2b2
2 changed files with 96 additions and 21 deletions

View File

@@ -260,6 +260,81 @@ When updating an existing module to follow this pattern:
</Toolbar> </Toolbar>
``` ```
#### Table/List UI Standards
All CRUD list pages MUST follow these consistent UI patterns:
**Table Structure:**
```tsx
<div className="hidden md:block rounded-lg border overflow-hidden">
<table className="w-full">
<thead className="bg-muted/50">
<tr className="border-b">
<th className="w-12 p-3">{/* Checkbox */}</th>
<th className="text-left p-3 font-medium">{__('Column')}</th>
{/* ... more columns */}
</tr>
</thead>
<tbody>
<tr className="border-b hover:bg-muted/30 last:border-0">
<td className="p-3">{/* Cell content */}</td>
{/* ... more cells */}
</tr>
</tbody>
</table>
</div>
```
**Required Classes:**
| Element | Classes | Purpose |
|---------|---------|---------|
| **Container** | `rounded-lg border overflow-hidden` | Rounded corners, border, hide overflow |
| **Table** | `w-full` | Full width |
| **Header Row** | `bg-muted/50` + `border-b` | Light background, bottom border |
| **Header Cell** | `p-3 font-medium text-left` | Padding, bold, left-aligned |
| **Body Row** | `border-b hover:bg-muted/30 last:border-0` | Border, hover effect, remove last border |
| **Body Cell** | `p-3` | Consistent padding (NOT `px-3 py-2`) |
| **Checkbox Column** | `w-12 p-3` | Fixed width for checkbox |
| **Actions Column** | `text-right p-3` or `text-center p-3` | Right/center aligned |
**Empty State Pattern:**
```tsx
<tr>
<td colSpan={columnCount} className="p-8 text-center text-muted-foreground">
<IconComponent className="w-12 h-12 mx-auto mb-2 opacity-50" />
{primaryMessage}
{helperText && <p className="text-sm mt-1">{helperText}</p>}
</td>
</tr>
```
**Mobile Card Pattern:**
```tsx
<div className="md:hidden space-y-2">
{items.map(item => (
<Card key={item.id} className="p-4">
{/* Card content */}
</Card>
))}
</div>
```
**Rules:**
1. ✅ **Always use `p-3`** for table cells (NOT `px-3 py-2`)
2. ✅ **Always add `hover:bg-muted/30`** to body rows
3. ✅ **Always use `bg-muted/50`** for table headers
4. ✅ **Always use `font-medium`** for header cells
5. ✅ **Always use `last:border-0`** to remove last row border
6. ✅ **Always use `overflow-hidden`** on table container
7. ❌ **Never mix padding styles** between modules
8. ❌ **Never omit hover effects** on interactive rows
**Responsive Behavior:**
- Desktop: Show table with `hidden md:block`
- Mobile: Show cards with `md:hidden`
- Both views must support same actions (select, edit, delete)
#### Variable Product Handling in Order Forms #### Variable Product Handling in Order Forms
When adding products to orders, variable products MUST follow the Tokopedia/Shopee pattern: When adding products to orders, variable products MUST follow the Tokopedia/Shopee pattern:

View File

@@ -392,11 +392,11 @@ export default function Orders() {
</div> </div>
{/* Desktop: Table */} {/* Desktop: Table */}
<div className="hidden md:block rounded-lg border border-border bg-card overflow-auto"> <div className="hidden md:block rounded-lg border overflow-hidden">
<table className="min-w-[800px] w-full text-sm"> <table className="min-w-[800px] w-full text-sm">
<thead className="border-b"> <thead className="bg-muted/50">
<tr className="text-left"> <tr className="border-b">
<th className="px-3 py-2 w-12"> <th className="w-12 p-3">
<Checkbox <Checkbox
checked={allSelected} checked={allSelected}
onCheckedChange={toggleAll} onCheckedChange={toggleAll}
@@ -404,39 +404,39 @@ export default function Orders() {
className={someSelected ? 'data-[state=checked]:bg-gray-400' : ''} className={someSelected ? 'data-[state=checked]:bg-gray-400' : ''}
/> />
</th> </th>
<th className="px-3 py-2">{__('Order')}</th> <th className="text-left p-3 font-medium">{__('Order')}</th>
<th className="px-3 py-2">{__('Date')}</th> <th className="text-left p-3 font-medium">{__('Date')}</th>
<th className="px-3 py-2">{__('Customer')}</th> <th className="text-left p-3 font-medium">{__('Customer')}</th>
<th className="px-3 py-2">{__('Items')}</th> <th className="text-left p-3 font-medium">{__('Items')}</th>
<th className="px-3 py-2">{__('Status')}</th> <th className="text-left p-3 font-medium">{__('Status')}</th>
<th className="px-3 py-2 text-right">{__('Total')}</th> <th className="text-right p-3 font-medium">{__('Total')}</th>
<th className="px-3 py-2 text-center">{__('Actions')}</th> <th className="text-center p-3 font-medium">{__('Actions')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{filteredOrders.map((row) => ( {filteredOrders.map((row) => (
<tr key={row.id} className="border-b last:border-0"> <tr key={row.id} className="border-b hover:bg-muted/30 last:border-0">
<td className="px-3 py-2"> <td className="p-3">
<Checkbox <Checkbox
checked={selectedIds.includes(row.id)} checked={selectedIds.includes(row.id)}
onCheckedChange={() => toggleRow(row.id)} onCheckedChange={() => toggleRow(row.id)}
aria-label={__('Select order')} aria-label={__('Select order')}
/> />
</td> </td>
<td className="px-3 py-2"> <td className="p-3">
<Link className="underline underline-offset-2" to={`/orders/${row.id}`}>#{row.number}</Link> <Link className="underline underline-offset-2" to={`/orders/${row.id}`}>#{row.number}</Link>
</td> </td>
<td className="px-3 py-2 min-w-32"> <td className="p-3 min-w-32">
<span title={row.date ?? ""}> <span title={row.date ?? ""}>
{formatRelativeOrDate(row.date_ts)} {formatRelativeOrDate(row.date_ts)}
</span> </span>
</td> </td>
<td className="px-3 py-2">{row.customer || '—'}</td> <td className="p-3">{row.customer || '—'}</td>
<td className="px-3 py-2"> <td className="p-3">
<ItemsCell row={row} /> <ItemsCell row={row} />
</td> </td>
<td className="px-3 py-2"><StatusBadge value={row.status} /></td> <td className="p-3"><StatusBadge value={row.status} /></td>
<td className="px-3 py-2 text-right tabular-nums font-mono"> <td className="p-3 text-right tabular-nums font-mono">
{formatMoney(row.total, { {formatMoney(row.total, {
currency: row.currency || store.currency, currency: row.currency || store.currency,
symbol: row.currency_symbol || store.symbol, symbol: row.currency_symbol || store.symbol,
@@ -446,7 +446,7 @@ export default function Orders() {
decimals: store.decimals, decimals: store.decimals,
})} })}
</td> </td>
<td className="px-3 py-2 text-center space-x-2"> <td className="p-3 text-center space-x-2">
<Link className="btn text-sm underline underline-offset-2" to={`/orders/${row.id}`}>{__('Open')}</Link> <Link className="btn text-sm underline underline-offset-2" to={`/orders/${row.id}`}>{__('Open')}</Link>
<Link className="btn text-sm underline underline-offset-2" to={`/orders/${row.id}/edit`}>{__('Edit')}</Link> <Link className="btn text-sm underline underline-offset-2" to={`/orders/${row.id}/edit`}>{__('Edit')}</Link>
</td> </td>
@@ -455,7 +455,7 @@ export default function Orders() {
{filteredOrders.length === 0 && ( {filteredOrders.length === 0 && (
<tr> <tr>
<td className="px-3 py-12 text-center" colSpan={8}> <td className="p-8 text-center text-muted-foreground" colSpan={8}>
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<PackageOpen className="w-8 h-8 opacity-40" /> <PackageOpen className="w-8 h-8 opacity-40" />
<div className="font-medium">{__('No orders found')}</div> <div className="font-medium">{__('No orders found')}</div>