feat(collection-pool): render supplier-grouped sections
This commit is contained in:
parent
8e4289b104
commit
5984f2f814
1 changed files with 75 additions and 42 deletions
|
|
@ -118,6 +118,12 @@ export default function CollectionPoolPage() {
|
|||
const [readyDialogRow, setReadyDialogRow] = useState<CollectionPoolVisibleRow | null>(null);
|
||||
const [readyDialogError, setReadyDialogError] = useState<string | null>(null);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [collapsedSuppliers, setCollapsedSuppliers] = useState<Set<string>>(new Set());
|
||||
const [bulkAssignDialog, setBulkAssignDialog] = useState<{
|
||||
supplierId: string;
|
||||
supplierName: string;
|
||||
rowIds: string[];
|
||||
} | null>(null);
|
||||
|
||||
const toggleRowSelected = useCallback((rowId: string) => {
|
||||
setSelectedIds((prev) => {
|
||||
|
|
@ -141,6 +147,25 @@ export default function CollectionPoolPage() {
|
|||
});
|
||||
}, []);
|
||||
|
||||
const toggleCollapsed = useCallback((supplierId: string) => {
|
||||
setCollapsedSuppliers((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(supplierId)) next.delete(supplierId);
|
||||
else next.add(supplierId);
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const openBulkAssign = useCallback(
|
||||
(supplierId: string, supplierName: string, rowIds: string[]) => {
|
||||
if (rowIds.length === 0) return;
|
||||
setBulkAssignDialog({ supplierId, supplierName, rowIds });
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const closeBulkAssign = useCallback(() => setBulkAssignDialog(null), []);
|
||||
|
||||
const closeAssignDialog = useCallback(() => setAssignDialog(null), []);
|
||||
|
||||
const openAssignForRow = useCallback((row: CollectionPoolVisibleRow) => {
|
||||
|
|
@ -270,6 +295,21 @@ export default function CollectionPoolPage() {
|
|||
) as CollectionPoolVisibleRow[];
|
||||
}, [rows, todayYmd, filterText, readyFilter, sortMode, clusterSortKeyForSupplier]);
|
||||
|
||||
const groups = useMemo<
|
||||
| (Omit<CollectionPoolGroup, "rows"> & { rows: CollectionPoolVisibleRow[] })[]
|
||||
| null
|
||||
>(() => {
|
||||
if (!visibleRows) return null;
|
||||
// `buildSupplierGroups` types its rows as `CollectionPoolViewRow[]`,
|
||||
// but every row in `visibleRows` is a `CollectionPoolVisibleRow` (a
|
||||
// superset of `CollectionPoolViewRow` — see the cast above). The
|
||||
// widening per-row is sound.
|
||||
return buildSupplierGroups(visibleRows, supplierMarkerMap).map((g) => ({
|
||||
...g,
|
||||
rows: g.rows as CollectionPoolVisibleRow[],
|
||||
}));
|
||||
}, [visibleRows, supplierMarkerMap]);
|
||||
|
||||
const updateItemStatus = useCallback(
|
||||
async (
|
||||
itemId: string,
|
||||
|
|
@ -483,51 +523,44 @@ export default function CollectionPoolPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{rows === null && (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-20 w-full animate-pulse rounded-md bg-muted/30"
|
||||
aria-label="Loading..."
|
||||
/>
|
||||
))}
|
||||
<div className="overflow-hidden rounded-xl border bg-card">
|
||||
<div className="hidden grid-cols-[minmax(0,1.35fr)_minmax(0,0.95fr)_auto_auto] gap-4 border-b bg-muted/20 px-4 py-2 pl-[22px] text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground lg:grid">
|
||||
<span>Collection Item</span>
|
||||
<span>Order Detail</span>
|
||||
<span>Status</span>
|
||||
<span className="text-right">Actions</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{rows !== null && rows.length === 0 && (
|
||||
<EmptyState message="No items in the collection pool. Items land here after a PO email is sent successfully." />
|
||||
)}
|
||||
|
||||
{rows !== null && rows.length > 0 && visibleRows && visibleRows.length === 0 && (
|
||||
<EmptyState message="No items match the current filters." />
|
||||
)}
|
||||
|
||||
{rows !== null && visibleRows && visibleRows.length > 0 && (
|
||||
<div className="overflow-hidden rounded-xl border bg-card">
|
||||
<div className="hidden grid-cols-[minmax(0,1.35fr)_minmax(0,0.95fr)_auto_auto] gap-4 border-b bg-muted/20 px-4 py-2 pl-[22px] text-[11px] font-medium uppercase tracking-[0.16em] text-muted-foreground lg:grid">
|
||||
<span>Collection Item</span>
|
||||
<span>Order Detail</span>
|
||||
<span>Status</span>
|
||||
<span className="text-right">Actions</span>
|
||||
</div>
|
||||
<ul className="divide-y" data-od-id="collection-pool-unified-list">
|
||||
{visibleRows.map((row) => (
|
||||
<PoolRow
|
||||
key={row.id}
|
||||
row={row}
|
||||
busy={busyId === row.id}
|
||||
markerToken={supplierMarkerMap.get(row.supplierId)}
|
||||
todayYmd={todayYmd}
|
||||
selected={selectedIds.has(row.id)}
|
||||
onToggleSelected={toggleRowSelected}
|
||||
onOpenAssign={openAssignForRow}
|
||||
onToggleReady={handleToggleReady}
|
||||
{groups === null ? (
|
||||
<div className="space-y-2 p-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-20 w-full animate-pulse rounded-md bg-muted/30"
|
||||
aria-label="Loading..."
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : groups.length === 0 ? (
|
||||
<EmptyState message="No items match the current filters." />
|
||||
) : (
|
||||
groups.map((group) => (
|
||||
<SupplierGroup
|
||||
key={group.supplierId}
|
||||
group={group}
|
||||
busy={busyId !== null}
|
||||
todayYmd={todayYmd}
|
||||
selectedIds={selectedIds}
|
||||
collapsed={collapsedSuppliers.has(group.supplierId)}
|
||||
onToggleRowSelected={toggleRowSelected}
|
||||
onToggleSectionSelected={toggleSectionSelected}
|
||||
onToggleCollapsed={toggleCollapsed}
|
||||
onOpenAssign={openAssignForRow}
|
||||
onOpenBulkAssign={openBulkAssign}
|
||||
onToggleReady={handleToggleReady}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AssignDriverDialog
|
||||
open={assignDialog !== null}
|
||||
|
|
|
|||
Loading…
Reference in a new issue