From da9b3e8b58393ea1587356787854d7b4c284c822 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Sat, 11 Jul 2026 00:13:20 +0200 Subject: [PATCH] refactor(collection-pool): split PoolRow + SupplierGroup into components.tsx --- src/app/(app)/collection-pool/components.tsx | 347 +++++++++++++++++++ src/app/(app)/collection-pool/page.tsx | 321 +---------------- 2 files changed, 350 insertions(+), 318 deletions(-) create mode 100644 src/app/(app)/collection-pool/components.tsx 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.supplierName || "Unknown supplier"} +

    + + · {group.rows.length} {group.rows.length === 1 ? "stop" : "stops"} + {eyebrowText ? <> · {eyebrowText} : null} + +
    + + + +
    +
    +
    +
      + {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.supplierName || "Unknown supplier"} -

    - - · {group.rows.length} {group.rows.length === 1 ? "stop" : "stops"} - {eyebrowText ? <> · {eyebrowText} : null} - -
    - - - -
    -
    -
    -
      - {group.rows.map((row) => ( - - ))} -
    -
    -
    - ); -}