feat(collection-pool): add SupplierGroup component

This commit is contained in:
SJCoetz 2026-07-10 23:52:50 +02:00
parent b6ed9f2685
commit c17f7c662e

View file

@ -26,7 +26,9 @@ import {
getAssignmentBadge,
buildProximityMarkerMap,
buildVisibleRows,
type CollectionPoolGroup,
type CollectionPoolSortMode,
type CollectionPoolViewRow,
getAssignmentState,
getPoolAgeLabel,
} from "./view-model";
@ -718,3 +720,149 @@ function PoolRow({
</li>
);
}
function SupplierGroup({
group,
busy,
todayYmd,
selectedIds,
collapsed,
onToggleRowSelected,
onToggleSectionSelected,
onToggleCollapsed,
onOpenAssign,
onOpenBulkAssign,
onToggleReady,
}: {
group: Omit<CollectionPoolGroup, "rows"> & { rows: CollectionPoolVisibleRow[] };
busy: boolean;
todayYmd: string;
selectedIds: Set<string>;
collapsed: boolean;
onToggleRowSelected: (rowId: string) => void;
onToggleSectionSelected: (rowIds: string[]) => void;
onToggleCollapsed: (supplierId: string) => void;
onOpenAssign: (row: CollectionPoolVisibleRow) => void;
onOpenBulkAssign: (supplierId: string, supplierName: string, rowIds: string[]) => void;
onToggleReady: (row: CollectionPoolVisibleRow, ready: boolean) => void;
}) {
const rowIds = group.rows.map((r) => r.id);
const selectedCount = rowIds.filter((id) => selectedIds.has(id)).length;
const allSelected = selectedCount === rowIds.length && rowIds.length > 0;
const someSelected = selectedCount > 0 && !allSelected;
const allAssigned = group.rows.every(
(r) => getAssignmentState(r, todayYmd) !== "unassigned",
);
const eyebrowText = (() => {
if (group.topPoNumbers.length === 0) return null;
const head = group.topPoNumbers.map((p) => `PO ${p}`).join(", ");
return group.remainingPoCount > 0
? `${head} (+${group.remainingPoCount} more)`
: head;
})();
const assignLabel = allSelected
? `Assign all ${rowIds.length} ${rowIds.length === 1 ? "stop" : "stops"}`
: selectedCount > 0
? `Assign ${selectedCount} selected`
: `Assign all ${rowIds.length} ${rowIds.length === 1 ? "stop" : "stops"}`;
return (
<section
data-od-id={`collection-pool-supplier-${group.supplierId}`}
className="border-b last:border-b-0"
>
<header
className={cn(
"sticky top-0 z-10 flex flex-wrap items-center gap-3 border-b bg-muted/30 px-4 py-2.5",
"lg:flex-nowrap",
)}
>
<span
aria-hidden
className={cn("size-2 shrink-0 rounded-full", markerTokenToClassName(group.markerToken))}
/>
<div className="flex min-w-0 flex-1 flex-wrap items-baseline gap-x-2 gap-y-0.5">
<p className="truncate text-sm font-semibold text-foreground">
{group.supplierName || "Unknown supplier"}
</p>
<span className="text-xs text-muted-foreground">·</span>
<span className="text-xs text-muted-foreground">
{rowIds.length} {rowIds.length === 1 ? "stop" : "stops"}
</span>
{eyebrowText && (
<>
<span className="text-xs text-muted-foreground">·</span>
<span className="truncate text-xs text-muted-foreground">{eyebrowText}</span>
</>
)}
</div>
<div className="flex items-center gap-2">
{rowIds.length > 0 && (
<input
type="checkbox"
checked={allSelected}
ref={(el) => {
if (el) el.indeterminate = someSelected;
}}
onChange={() => onToggleSectionSelected(rowIds)}
disabled={busy}
aria-label={`Select all stops at ${group.supplierName || "unknown supplier"}`}
className="size-4 shrink-0 cursor-pointer accent-primary disabled:cursor-not-allowed"
/>
)}
<Button
size="sm"
variant="default"
onClick={() => onOpenBulkAssign(group.supplierId, group.supplierName, rowIds)}
disabled={busy || rowIds.length === 0 || allAssigned}
title={allAssigned ? "All stops already assigned" : undefined}
>
{assignLabel}
</Button>
<button
type="button"
onClick={() => onToggleCollapsed(group.supplierId)}
aria-label={collapsed ? `Expand ${group.supplierName || "supplier"} section` : `Collapse ${group.supplierName || "supplier"} section`}
aria-expanded={!collapsed}
className="inline-flex h-8 w-8 items-center justify-center rounded-md border bg-background text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
>
<span
aria-hidden
className={cn(
"inline-block text-sm leading-none transition-transform duration-150 ease-out",
collapsed && "-rotate-90",
)}
>
▼
</span>
</button>
</div>
</header>
<div
className={cn(
"grid overflow-hidden transition-[max-height] duration-150 ease-out",
collapsed ? "max-h-0" : "max-h-[2000px]",
)}
aria-hidden={collapsed}
>
<ul className="divide-y">
{group.rows.map((row) => (
<PoolRow
key={row.id}
row={row}
busy={busy}
markerToken={group.markerToken}
todayYmd={todayYmd}
selected={selectedIds.has(row.id)}
onToggleSelected={onToggleRowSelected}
onOpenAssign={onOpenAssign}
onToggleReady={onToggleReady}
/>
))}
</ul>
</div>
</section>
);
}