fix(collection-pool): align SupplierGroup with spec (a11y, chevron, assign)

This commit is contained in:
SJCoetz 2026-07-10 23:55:23 +02:00
parent c17f7c662e
commit 8e4289b104

View file

@ -3,6 +3,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
AlertTriangle,
ChevronDown,
Loader2,
Search,
Truck,
@ -23,12 +24,12 @@ import type { OrderItem } from "@/lib/types";
import { cn } from "@/lib/utils";
import {
getAssignmentBadge,
buildProximityMarkerMap,
buildSupplierGroups,
buildVisibleRows,
type CollectionPoolGroup,
type CollectionPoolSortMode,
type CollectionPoolViewRow,
getAssignmentBadge,
getAssignmentState,
getPoolAgeLabel,
} from "./view-model";
@ -734,6 +735,11 @@ function SupplierGroup({
onOpenBulkAssign,
onToggleReady,
}: {
// `group.rows` is typed as `CollectionPoolViewRow[]` in the helper, but
// `PoolRow` (this page's existing row component) expects the richer
// `CollectionPoolVisibleRow` (which extends `OrderItem`). The widening is
// sound because `buildSupplierGroups` is fed `CollectionPoolVisibleRow[]`
// — see Task 7 wiring.
group: Omit<CollectionPoolGroup, "rows"> & { rows: CollectionPoolVisibleRow[] };
busy: boolean;
todayYmd: string;
@ -763,43 +769,39 @@ function SupplierGroup({
})();
const assignLabel = allSelected
? `Assign all ${rowIds.length} ${rowIds.length === 1 ? "stop" : "stops"}`
? `Assign ${rowIds.length} stops`
: selectedCount > 0
? `Assign ${selectedCount} selected`
: `Assign all ${rowIds.length} ${rowIds.length === 1 ? "stop" : "stops"}`;
: `Assign all ${rowIds.length}`;
const onAssignClick = () => {
const ids = allSelected || selectedCount === 0 ? rowIds : rowIds.filter((id) => selectedIds.has(id));
onOpenBulkAssign(group.supplierId, group.supplierName, ids);
};
return (
<section
aria-labelledby={`supplier-${group.supplierId}`}
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",
)}
>
<header className="sticky top-0 z-10 flex flex-wrap items-center gap-3 border-b bg-muted/30 px-4 py-2.5">
<span
className={cn("size-2.5 shrink-0 rounded-full", markerTokenToClassName(group.markerToken))}
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 && (
<h3
id={`supplier-${group.supplierId}`}
className="text-sm font-semibold text-foreground"
>
{group.supplierName || "Unknown supplier"}
</h3>
<span className="text-xs text-muted-foreground">
· {group.rows.length} {group.rows.length === 1 ? "stop" : "stops"}
{eyebrowText ? <> · {eyebrowText}</> : null}
</span>
<div className="ml-auto flex items-center gap-2">
<label className="inline-flex cursor-pointer items-center gap-1.5 text-xs text-muted-foreground">
<input
type="checkbox"
checked={allSelected}
@ -808,46 +810,39 @@ function SupplierGroup({
}}
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"
className="size-4 cursor-pointer accent-primary disabled:cursor-not-allowed"
aria-label={`Select all ${group.rows.length} stops at ${group.supplierName || "unknown supplier"}`}
/>
)}
Select all
</label>
<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}
onClick={onAssignClick}
disabled={busy || allAssigned}
title={allAssigned ? "Every stop at this supplier is already assigned" : undefined}
>
<Truck className="mr-1.5 size-3.5" />
{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"
aria-controls={`supplier-${group.supplierId}-body`}
className="inline-flex h-7 w-7 items-center justify-center rounded-md 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>
<ChevronDown className={cn("size-4 transition-transform", collapsed && "-rotate-90")} />
</button>
</div>
</header>
<div
id={`supplier-${group.supplierId}-body`}
className={cn(
"grid overflow-hidden transition-[max-height] duration-150 ease-out",
collapsed ? "max-h-0" : "max-h-[2000px]",
"grid overflow-hidden transition-[grid-template-rows] duration-150 ease-out",
collapsed ? "grid-rows-[0fr]" : "grid-rows-[1fr]",
)}
aria-hidden={collapsed}
>
<ul className="divide-y">
<ul className="divide-y min-h-0">
{group.rows.map((row) => (
<PoolRow
key={row.id}