diff --git a/src/app/(app)/collection-pool/components.tsx b/src/app/(app)/collection-pool/components.tsx
new file mode 100644
index 0000000..ef19d51
--- /dev/null
+++ b/src/app/(app)/collection-pool/components.tsx
@@ -0,0 +1,347 @@
+"use client";
+
+import Link from "next/link";
+import { AlertTriangle, ChevronDown, Loader2, Truck } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import type { OrderItem } from "@/lib/types";
+import {
+ type CollectionPoolGroup,
+ getAssignmentBadge,
+ getAssignmentState,
+ getPoolAgeLabel,
+} from "./view-model";
+
+type CollectionPoolRow = OrderItem & {
+ ready_for_collection?: boolean | null;
+ expand?: {
+ purchase_order?: {
+ id: string;
+ po_number?: string;
+ supplier?: string;
+ expand?: { supplier?: { id: string; suppliername?: string; email?: string } };
+ status?: string;
+ };
+ order?: {
+ id: string;
+ order_number?: string;
+ company?: string;
+ customer?: string;
+ expand?: {
+ company?: { id: string; company?: string };
+ customer?: { id: string; customer?: string };
+ };
+ };
+ };
+};
+
+export type CollectionPoolVisibleRow = CollectionPoolRow & {
+ supplierId: string;
+ supplierName: string;
+ companyName: string;
+ customerName: string;
+ poNumber: string;
+ orderNumber: string;
+};
+
+function markerTokenToClassName(token?: string): string {
+ switch (token) {
+ case "marker-blue":
+ return "bg-blue-500";
+ case "marker-emerald":
+ return "bg-emerald-500";
+ case "marker-violet":
+ return "bg-violet-500";
+ case "marker-amber":
+ return "bg-amber-500";
+ default:
+ return "bg-slate-300";
+ }
+}
+
+export function PoolRow({
+ row,
+ busy,
+ markerToken,
+ todayYmd,
+ selected,
+ onToggleSelected,
+ onOpenAssign,
+ onToggleReady,
+}: {
+ row: CollectionPoolVisibleRow;
+ busy: boolean;
+ markerToken?: string;
+ todayYmd: string;
+ selected: boolean;
+ onToggleSelected: (rowId: string) => void;
+ onOpenAssign: (row: CollectionPoolVisibleRow) => void;
+ onToggleReady: (row: CollectionPoolVisibleRow, ready: boolean) => void;
+}) {
+ const assignmentState = getAssignmentState(row, todayYmd);
+ const assignmentBadge = getAssignmentBadge(assignmentState);
+ const isAssigned = assignmentState !== "unassigned";
+ const readyLabel = row.ready_for_collection ? "Ready" : "Pending";
+ const assignedDateLabel = row.assigned_at
+ ? new Date(row.assigned_at).toLocaleDateString(undefined, {
+ weekday: "short",
+ day: "numeric",
+ month: "short",
+ })
+ : null;
+
+ return (
+
+
+
+
+
+
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"
+ />
+
+
{row.supplierName}
+
{row.description || "(no description)"}
+
+ {row.customerName || "No customer"}
+ /
+ {row.companyName || "No company"}
+
+ {!row.purchase_order && (
+
+
+
This row has no purchase order link. The PO may have been deleted.
+
+ )}
+
+
+
+
+
+ Order
+
+ {row.orderNumber || row.order}
+
+
+
{row.poNumber || "No PO"}
+
+ {getPoolAgeLabel(row.updated)}
+
+ {row.updated && (
+
+ In pool since {new Date(row.updated).toLocaleDateString(undefined, {
+ day: "numeric",
+ month: "short",
+ })}
+
+ )}
+ {assignedDateLabel &&
Scheduled {assignedDateLabel}
}
+
+
+
+
+ {assignmentBadge.label}
+
+
+ {readyLabel}
+
+ {row.notes && row.notes.trim().length > 0 && (
+
+ Notes
+
+ {row.notes.length > 280 ? `${row.notes.slice(0, 280)}…` : row.notes}
+
+
+ )}
+
+
+
+
+
+ {busy && (
+
+
+ Updating...
+
+ )}
+
+
+
+ );
+}
+
+export function SupplierGroup({
+ group,
+ busy,
+ todayYmd,
+ selectedIds,
+ collapsed,
+ onToggleRowSelected,
+ onToggleSectionSelected,
+ onToggleCollapsed,
+ onOpenAssign,
+ 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 & { rows: CollectionPoolVisibleRow[] };
+ busy: boolean;
+ todayYmd: string;
+ selectedIds: Set;
+ 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 ${rowIds.length} stops`
+ : selectedCount > 0
+ ? `Assign ${selectedCount} selected`
+ : `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 (
+
+
+
+
+ {group.rows.map((row) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/app/(app)/collection-pool/page.tsx b/src/app/(app)/collection-pool/page.tsx
index a427f4f..4be236b 100644
--- a/src/app/(app)/collection-pool/page.tsx
+++ b/src/app/(app)/collection-pool/page.tsx
@@ -1,14 +1,7 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import {
- AlertTriangle,
- ChevronDown,
- Loader2,
- Search,
- Truck,
-} from "lucide-react";
-import Link from "next/link";
+import { Search, Truck } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
@@ -34,6 +27,8 @@ import {
getPoolAgeLabel,
} from "./view-model";
+import { PoolRow, SupplierGroup, type CollectionPoolVisibleRow } from "./components";
+
type CollectionPoolRow = OrderItem & {
ready_for_collection?: boolean | null;
expand?: {
@@ -57,15 +52,6 @@ type CollectionPoolRow = OrderItem & {
};
};
-type CollectionPoolVisibleRow = CollectionPoolRow & {
- supplierId: string;
- supplierName: string;
- companyName: string;
- customerName: string;
- poNumber: string;
- orderNumber: string;
-};
-
type CollectionPoolAssignDialogState = {
row: CollectionPoolVisibleRow;
initialDriverId: string;
@@ -81,21 +67,6 @@ function assignedAtToYmd(iso: string | undefined | null): string | undefined {
return d.toISOString().slice(0, 10);
}
-function markerTokenToClassName(token?: string): string {
- switch (token) {
- case "marker-blue":
- return "bg-blue-500";
- case "marker-emerald":
- return "bg-emerald-500";
- case "marker-violet":
- return "bg-violet-500";
- case "marker-amber":
- return "bg-amber-500";
- default:
- return "bg-slate-300";
- }
-}
-
function EmptyState({ message }: { message: string }) {
return (
@@ -683,289 +654,3 @@ export default function CollectionPoolPage() {
);
}
-
-function PoolRow({
- row,
- busy,
- markerToken,
- todayYmd,
- selected,
- onToggleSelected,
- onOpenAssign,
- onToggleReady,
-}: {
- row: CollectionPoolVisibleRow;
- busy: boolean;
- markerToken?: string;
- todayYmd: string;
- selected: boolean;
- onToggleSelected: (rowId: string) => void;
- onOpenAssign: (row: CollectionPoolVisibleRow) => void;
- onToggleReady: (row: CollectionPoolVisibleRow, ready: boolean) => void;
-}) {
- const assignmentState = getAssignmentState(row, todayYmd);
- const assignmentBadge = getAssignmentBadge(assignmentState);
- const isAssigned = assignmentState !== "unassigned";
- const readyLabel = row.ready_for_collection ? "Ready" : "Pending";
- const assignedDateLabel = row.assigned_at
- ? new Date(row.assigned_at).toLocaleDateString(undefined, {
- weekday: "short",
- day: "numeric",
- month: "short",
- })
- : null;
-
- return (
-
-
-
-
-
-
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"
- />
-
-
{row.supplierName}
-
{row.description || "(no description)"}
-
- {row.customerName || "No customer"}
- /
- {row.companyName || "No company"}
-
- {!row.purchase_order && (
-
-
-
This row has no purchase order link. The PO may have been deleted.
-
- )}
-
-
-
-
-
- Order
-
- {row.orderNumber || row.order}
-
-
-
{row.poNumber || "No PO"}
-
- {getPoolAgeLabel(row.updated)}
-
- {row.updated && (
-
- In pool since {new Date(row.updated).toLocaleDateString(undefined, {
- day: "numeric",
- month: "short",
- })}
-
- )}
- {assignedDateLabel &&
Scheduled {assignedDateLabel}
}
-
-
-
-
- {assignmentBadge.label}
-
-
- {readyLabel}
-
- {row.notes && row.notes.trim().length > 0 && (
-
- Notes
-
- {row.notes.length > 280 ? `${row.notes.slice(0, 280)}…` : row.notes}
-
-
- )}
-
-
-
-
-
- {busy && (
-
-
- Updating...
-
- )}
-
-
-
- );
-}
-
-function SupplierGroup({
- group,
- busy,
- todayYmd,
- selectedIds,
- collapsed,
- onToggleRowSelected,
- onToggleSectionSelected,
- onToggleCollapsed,
- onOpenAssign,
- 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 & { rows: CollectionPoolVisibleRow[] };
- busy: boolean;
- todayYmd: string;
- selectedIds: Set;
- 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 ${rowIds.length} stops`
- : selectedCount > 0
- ? `Assign ${selectedCount} selected`
- : `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 (
-
-
-
-
- {group.rows.map((row) => (
-
- ))}
-
-
-
- );
-}