feat(collection-pool): add SupplierGroup component
This commit is contained in:
parent
b6ed9f2685
commit
c17f7c662e
1 changed files with 148 additions and 0 deletions
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue