feat(collection-pool): add per-row selection state and checkbox

- selectedIds Set state plus toggleRowSelected / toggleSectionSelected callbacks
- PoolRow signature extended with selected + onToggleSelected
- New flex wrapper around supplier/description block with leading checkbox input
- bg-primary/5 row tint applied when selected
- aria-label composes supplier + description for screen readers
This commit is contained in:
SJCoetz 2026-07-10 23:50:17 +02:00
parent 7ce4445865
commit b6ed9f2685

View file

@ -114,6 +114,29 @@ export default function CollectionPoolPage() {
const [assignDialog, setAssignDialog] = useState<CollectionPoolAssignDialogState | null>(null);
const [readyDialogRow, setReadyDialogRow] = useState<CollectionPoolVisibleRow | null>(null);
const [readyDialogError, setReadyDialogError] = useState<string | null>(null);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const toggleRowSelected = useCallback((rowId: string) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(rowId)) next.delete(rowId);
else next.add(rowId);
return next;
});
}, []);
const toggleSectionSelected = useCallback((rowIds: string[]) => {
setSelectedIds((prev) => {
const allSelected = rowIds.every((id) => prev.has(id));
const next = new Set(prev);
if (allSelected) {
for (const id of rowIds) next.delete(id);
} else {
for (const id of rowIds) next.add(id);
}
return next;
});
}, []);
const closeAssignDialog = useCallback(() => setAssignDialog(null), []);
@ -493,6 +516,8 @@ export default function CollectionPoolPage() {
busy={busyId === row.id}
markerToken={supplierMarkerMap.get(row.supplierId)}
todayYmd={todayYmd}
selected={selectedIds.has(row.id)}
onToggleSelected={toggleRowSelected}
onOpenAssign={openAssignForRow}
onToggleReady={handleToggleReady}
/>
@ -553,6 +578,8 @@ function PoolRow({
busy,
markerToken,
todayYmd,
selected,
onToggleSelected,
onOpenAssign,
onToggleReady,
}: {
@ -560,6 +587,8 @@ function PoolRow({
busy: boolean;
markerToken?: string;
todayYmd: string;
selected: boolean;
onToggleSelected: (rowId: string) => void;
onOpenAssign: (row: CollectionPoolVisibleRow) => void;
onToggleReady: (row: CollectionPoolVisibleRow, ready: boolean) => void;
}) {
@ -576,24 +605,40 @@ function PoolRow({
: null;
return (
<li className="grid grid-cols-[6px_minmax(0,1fr)]" aria-busy={busy}>
<li
className={cn(
"grid grid-cols-[6px_minmax(0,1fr)]",
selected && "bg-primary/5",
)}
aria-busy={busy}
>
<span className={cn("min-h-full", markerTokenToClassName(markerToken))} aria-hidden />
<div className="grid gap-4 p-4 lg:grid-cols-[minmax(0,1.35fr)_minmax(0,0.95fr)_auto_auto] lg:items-center">
<div className="min-w-0">
<p className="truncate text-sm font-semibold text-foreground">{row.supplierName}</p>
<p className="mt-1 truncate text-sm text-foreground">{row.description || "(no description)"}</p>
<div className="mt-1 flex flex-wrap gap-x-2 gap-y-1 text-xs text-muted-foreground">
<span>{row.customerName || "No customer"}</span>
<span className="text-border">/</span>
<span>{row.companyName || "No company"}</span>
</div>
{!row.purchase_order && (
<div className="mt-2 flex items-start gap-2 rounded-md border border-amber-200/80 bg-amber-50/70 px-2.5 py-2 text-[11px] leading-5 text-amber-900">
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span>This row has no purchase order link. The PO may have been deleted.</span>
<div className="flex min-w-0 items-start gap-2">
<input
type="checkbox"
checked={selected}
onChange={() => onToggleSelected(row.id)}
disabled={busy}
aria-label={`Select ${row.description ?? "item"} at ${row.supplierName ?? "unknown supplier"}`}
className="mt-1 size-4 shrink-0 cursor-pointer accent-primary disabled:cursor-not-allowed"
/>
<div className="min-w-0">
<p className="truncate text-sm font-semibold text-foreground">{row.supplierName}</p>
<p className="mt-1 truncate text-sm text-foreground">{row.description || "(no description)"}</p>
<div className="mt-1 flex flex-wrap gap-x-2 gap-y-1 text-xs text-muted-foreground">
<span>{row.customerName || "No customer"}</span>
<span className="text-border">/</span>
<span>{row.companyName || "No company"}</span>
</div>
)}
{!row.purchase_order && (
<div className="mt-2 flex items-start gap-2 rounded-md border border-amber-200/80 bg-amber-50/70 px-2.5 py-2 text-[11px] leading-5 text-amber-900">
<AlertTriangle className="mt-0.5 h-3.5 w-3.5 shrink-0" />
<span>This row has no purchase order link. The PO may have been deleted.</span>
</div>
)}
</div>
</div>
<div className="min-w-0 text-sm">