quest-vorteq/src/components/requests/cart-items-table.tsx
Lorentz Hinrichsen e60e07c9a5
Some checks failed
Build and Deploy / build (push) Successful in 4m39s
Build and Deploy / deploy (push) Failing after 1s
feat: add shipment request (C-013) and allocation request (C-014) cart workflows
Multi-step cart workflows for both shipment and allocation requests:
- Ship-to address selection from Epicor portal_CustomerShipToAddresses
- Inventory browser dialog (shipment) / coil browser dialog (allocation)
- Cart persistence in DB with duplicate detection
- Details form (order#, PO#, pickup date, instructions, email recipients)
- Review & submit with confirmation dialog
- Cancel with permission gate and confirmation
- Shared components: stepper, ship-to selector, cart table, header form, review
- 16 API routes, 4 services, 13 components, 6 pages
- Phase 2 complete: 16/16 tasks done

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 10:37:38 -05:00

246 lines
8.6 KiB
TypeScript

'use client';
import type {
ShipRequestDetailItem,
AllocRequestDetailItem,
} from '@/types/requests';
import {
Table,
TableBody,
TableCell,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Plus, Trash2, ShoppingCart } from 'lucide-react';
import { useState } from 'react';
type ShipItemRow = ShipRequestDetailItem & { _type: 'ship' };
type AllocItemRow = AllocRequestDetailItem & { _type: 'alloc' };
type Props = {
items: (ShipRequestDetailItem | AllocRequestDetailItem)[];
variant: 'ship' | 'alloc';
onRemoveItem: (detailId: string) => Promise<void>;
onUpdateItem: (detailId: string, data: { quantity?: number; notes?: string | null }) => Promise<void>;
onOpenBrowser: () => void;
readOnly?: boolean;
};
export function CartItemsTable({
items,
variant,
onRemoveItem,
onUpdateItem,
onOpenBrowser,
readOnly = false,
}: Props) {
const [removingId, setRemovingId] = useState<string | null>(null);
const [editingId, setEditingId] = useState<string | null>(null);
const [editQty, setEditQty] = useState<string>('');
const [editNotes, setEditNotes] = useState<string>('');
const handleRemove = async (id: string) => {
setRemovingId(id);
try {
await onRemoveItem(id);
} finally {
setRemovingId(null);
}
};
const startEdit = (item: ShipRequestDetailItem | AllocRequestDetailItem) => {
setEditingId(item.id);
setEditQty(String(item.quantity));
setEditNotes(item.notes || '');
};
const saveEdit = async (id: string) => {
await onUpdateItem(id, {
quantity: parseFloat(editQty) || 0,
notes: editNotes || null,
});
setEditingId(null);
};
const cancelEdit = () => {
setEditingId(null);
};
const isShipItem = (
item: ShipRequestDetailItem | AllocRequestDetailItem
): item is ShipRequestDetailItem => {
return 'plant' in item;
};
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<ShoppingCart className="h-5 w-5" />
Cart Items ({items.length})
</CardTitle>
{!readOnly && (
<Button onClick={onOpenBrowser} size="sm">
<Plus className="mr-2 h-4 w-4" />
Add Items
</Button>
)}
</div>
</CardHeader>
<CardContent>
{items.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
<ShoppingCart className="mb-2 h-8 w-8 opacity-40" />
<p>No items in cart</p>
{!readOnly && (
<Button
variant="outline"
className="mt-3"
onClick={onOpenBrowser}
>
<Plus className="mr-2 h-4 w-4" />
Browse Inventory
</Button>
)}
</div>
) : (
<div className="overflow-hidden rounded-md border">
<Table>
<thead>
<tr className="bg-teal-700 text-white">
<th className="px-4 py-2 text-left text-xs font-semibold uppercase">
Part #
</th>
<th className="px-4 py-2 text-left text-xs font-semibold uppercase">
Lot #
</th>
{variant === 'ship' ? (
<>
<th className="px-4 py-2 text-left text-xs font-semibold uppercase">
Plant
</th>
<th className="px-4 py-2 text-left text-xs font-semibold uppercase">
Warehouse
</th>
</>
) : (
<th className="px-4 py-2 text-left text-xs font-semibold uppercase">
Coil #
</th>
)}
<th className="px-4 py-2 text-right text-xs font-semibold uppercase">
Qty
</th>
<th className="px-4 py-2 text-left text-xs font-semibold uppercase">
Notes
</th>
{!readOnly && (
<th className="px-4 py-2 text-center text-xs font-semibold uppercase">
Actions
</th>
)}
</tr>
</thead>
<TableBody>
{items.map((item, i) => {
const isEditing = editingId === item.id;
return (
<TableRow
key={item.id}
className={i % 2 === 0 ? 'bg-muted/30' : ''}
>
<TableCell className="font-medium">
{item.part_num}
</TableCell>
<TableCell>{item.lot_num || '-'}</TableCell>
{variant === 'ship' && isShipItem(item) ? (
<>
<TableCell>{item.plant}</TableCell>
<TableCell>{item.warehouse || '-'}</TableCell>
</>
) : !isShipItem(item) ? (
<TableCell>{item.coil_number || '-'}</TableCell>
) : null}
<TableCell className="text-right">
{isEditing ? (
<Input
type="number"
value={editQty}
onChange={(e) => setEditQty(e.target.value)}
className="h-8 w-24 text-right"
min={0}
step="any"
/>
) : (
<span
className={!readOnly ? 'cursor-pointer hover:underline' : ''}
onClick={() => !readOnly && startEdit(item)}
>
{item.quantity}
</span>
)}
</TableCell>
<TableCell>
{isEditing ? (
<Input
value={editNotes}
onChange={(e) => setEditNotes(e.target.value)}
className="h-8"
placeholder="Notes..."
/>
) : (
<span
className={!readOnly ? 'cursor-pointer hover:underline' : ''}
onClick={() => !readOnly && startEdit(item)}
>
{item.notes || '-'}
</span>
)}
</TableCell>
{!readOnly && (
<TableCell className="text-center">
{isEditing ? (
<div className="flex justify-center gap-1">
<Button
size="sm"
variant="outline"
onClick={() => saveEdit(item.id)}
>
Save
</Button>
<Button
size="sm"
variant="ghost"
onClick={cancelEdit}
>
Cancel
</Button>
</div>
) : (
<Button
size="sm"
variant="ghost"
onClick={() => handleRemove(item.id)}
disabled={removingId === item.id}
className="text-destructive hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
)}
</TableCell>
)}
</TableRow>
);
})}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
);
}