diff --git a/src/app/(app)/collection-pool/components.tsx b/src/app/(app)/collection-pool/components.tsx
index 0de0e7f..ba50fa3 100644
--- a/src/app/(app)/collection-pool/components.tsx
+++ b/src/app/(app)/collection-pool/components.tsx
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
-import { AlertTriangle, CheckCircle2, ChevronDown, Loader2, Truck } from "lucide-react";
+import { AlertTriangle, CheckCircle2, ChevronDown, FileText, Loader2, Truck } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
@@ -59,6 +59,15 @@ export type CollectionPoolVisibleRow = CollectionPoolRow & {
customerName: string;
poNumber: string;
orderNumber: string;
+ /**
+ * PB id of the parent purchase order (`order_items.purchase_order`, or
+ * the stock purchase for `source: "stock"` rows). Present whenever a
+ * PO exists, which is what makes the PO number clickable — the
+ * download route is keyed on the record id, not the number.
+ */
+ purchaseOrderId?: string | null;
+ /** Resolved driver display name for `drv_id`; see the view model. */
+ driverName?: string;
};
function markerTokenToClassName(token?: string): string {
@@ -98,7 +107,7 @@ export function PoolRow({
onMarkStockCollected: (row: CollectionPoolVisibleRow) => void;
}) {
const assignmentState = getAssignmentState(row, todayYmd);
- const assignmentBadge = getAssignmentBadge(assignmentState);
+ const assignmentBadge = getAssignmentBadge(assignmentState, row.driverName);
const isAssigned = assignmentState !== "unassigned";
const readyLabel = row.ready_for_collection ? "Ready" : "Pending";
const assignedDateLabel = row.assigned_at
@@ -189,7 +198,35 @@ export function PoolRow({
)}
-
{row.poNumber || "No PO"}
+ {/* PO number → the PO PDF. `download=1` makes the GET route
+ stream the latest version as an attachment rather than
+ inline; `version` is intentionally omitted so this always
+ resolves to the CURRENT document, which is the one the
+ supplier was actually sent.
+
+ Customer rows ONLY. `source: "stock"` rows hang off a
+ `stock_purchase`, which is a different collection — the
+ purchase-orders PDF route is keyed on `purchase_orders`
+ records and would 404 on a stock_purchase id. Stock POs
+ live as PB files under `stock_purchases/{id}/{filename}`
+ and have no download route yet, so their number stays as
+ plain text rather than shipping a broken link. */}
+ {!isStock && row.purchaseOrderId ? (
+
+
+ {row.poNumber || `PO ${row.purchaseOrderId.slice(0, 8)}`}
+
+ ) : (
+
+ {row.poNumber || "No PO"}
+
+ )}
{getPoolAgeLabel(row.updated)}
@@ -202,6 +239,17 @@ export function PoolRow({
)}
{assignedDateLabel && Scheduled {assignedDateLabel}
}
+ {/* Named driver on its own line. The badge says "Assigned
+ to Ruan" — this is the redundant-but-scannable version,
+ and it carries the date, so an operator scanning the
+ column can read driver + when without decoding the pill
+ colour. Suppressed when the lookup failed (no name), so
+ we never show a bare PB id. */}
+ {row.driverName && (
+
+ Driver: {row.driverName}
+
+ )}
diff --git a/src/app/(app)/collection-pool/page.tsx b/src/app/(app)/collection-pool/page.tsx
index 55538e5..7618cdd 100644
--- a/src/app/(app)/collection-pool/page.tsx
+++ b/src/app/(app)/collection-pool/page.tsx
@@ -451,6 +451,17 @@ export default function CollectionPoolPage() {
customerName: row.expand?.order?.expand?.customer?.customer ?? "",
poNumber: row.expand?.purchase_order?.po_number ?? "",
orderNumber: row.expand?.order?.order_number ?? "",
+ // The PO record id is what the download route is keyed on
+ // (`/api/v1/purchase-orders/{id}/pdf`). Both sources funnel
+ // their parent PO through `expand.purchase_order` — see the
+ // stock-row mapping in `load()` — so one field covers both.
+ purchaseOrderId: row.expand?.purchase_order?.id ?? null,
+ // `driversById` is the same map the assign dialog pre-fills
+ // from, so the name shown on the row and the name shown in
+ // the dialog can't drift. Left undefined when the user
+ // lookup hasn't resolved yet (or failed) — the badge then
+ // falls back to date-only wording rather than showing an id.
+ driverName: row.drv_id ? driversById[row.drv_id] : undefined,
clusterSortKey: clusterSortKeyForSupplier(supplierId),
};
}),
@@ -461,7 +472,7 @@ export default function CollectionPoolPage() {
sortMode,
},
) as CollectionPoolVisibleRow[];
- }, [rows, todayYmd, filterText, readyFilter, sortMode, clusterSortKeyForSupplier]);
+ }, [rows, todayYmd, filterText, readyFilter, sortMode, clusterSortKeyForSupplier, driversById]);
const groups = useMemo<
| (Omit & { rows: CollectionPoolVisibleRow[] })[]
diff --git a/src/app/(app)/collection-pool/view-model.test.ts b/src/app/(app)/collection-pool/view-model.test.ts
index 47db608..26d72fd 100644
--- a/src/app/(app)/collection-pool/view-model.test.ts
+++ b/src/app/(app)/collection-pool/view-model.test.ts
@@ -86,6 +86,87 @@ describe("getAssignmentBadge", () => {
"bg-violet-100 text-violet-800 ring-1 ring-violet-200/60 dark:bg-violet-500/15 dark:text-violet-300 dark:ring-violet-400/30",
});
});
+
+ it("names the driver when a resolved name is supplied", () => {
+ // The operator's question on this page is "who has this stop?",
+ // so the name becomes the headline and the date is the suffix.
+ expect(getAssignmentBadge("assigned_today", "Ruan").label).toBe(
+ "Assigned to Ruan",
+ );
+ expect(getAssignmentBadge("assigned_future", "Ruan").label).toBe(
+ "Ruan · scheduled",
+ );
+ });
+
+ it("keeps the date-only fallback for an unresolved driver", () => {
+ // `drv_id` is a PB record id and must never reach the operator —
+ // "assigned to krpyec9l" is not actionable. Fall back to the
+ // previous wording instead of printing the id.
+ expect(getAssignmentBadge("assigned_today", undefined).label).toBe(
+ "Assigned today",
+ );
+ expect(getAssignmentBadge("assigned_today", "").label).toBe("Assigned today");
+ expect(getAssignmentBadge("assigned_today", " ").label).toBe(
+ "Assigned today",
+ );
+ });
+
+ it("ignores the driver name when the row is unassigned", () => {
+ // Defensive: a stale drv_id on a re-pooled row must not claim a
+ // driver is holding it.
+ expect(getAssignmentBadge("unassigned", "Ruan").label).toBe("Unassigned");
+ });
+});
+
+describe("buildVisibleRows — driver search", () => {
+ it("matches a row by the driver's display name", () => {
+ const rows = buildVisibleRows(
+ [
+ {
+ id: "a",
+ supplierId: "s1",
+ supplierName: "Afrisam",
+ description: "Hex bolt M16",
+ drv_id: "drv1",
+ driverName: "Ruan",
+ ready_for_collection: true,
+ },
+ {
+ id: "b",
+ supplierId: "s1",
+ supplierName: "Afrisam",
+ description: "Nut M16",
+ drv_id: "drv2",
+ driverName: "Tiaan",
+ ready_for_collection: true,
+ },
+ ],
+ { todayYmd: "2026-10-14", searchText: "ruan", readyFilter: "all" },
+ );
+ expect(rows.map((r) => r.id)).toEqual(["a"]);
+ });
+
+ it("does not match on a PB record id posing as a name", () => {
+ const rows = buildVisibleRows(
+ [
+ {
+ id: "a",
+ supplierId: "s1",
+ supplierName: "Afrisam",
+ description: "Hex bolt",
+ drv_id: "krpyec9lblra0qz",
+ driverName: undefined,
+ ready_for_collection: true,
+ },
+ ],
+ {
+ todayYmd: "2026-10-14",
+ searchText: "krpyec9l",
+ readyFilter: "all",
+ },
+ );
+ expect(rows).toHaveLength(0);
+ });
});
describe("buildVisibleRows", () => {
diff --git a/src/app/(app)/collection-pool/view-model.ts b/src/app/(app)/collection-pool/view-model.ts
index f3d2956..795183a 100644
--- a/src/app/(app)/collection-pool/view-model.ts
+++ b/src/app/(app)/collection-pool/view-model.ts
@@ -20,6 +20,18 @@ export type CollectionPoolViewRow = {
updated?: string;
item_status?: string;
drv_id?: string;
+ /**
+ * Resolved display name for `drv_id`, e.g. "Ruan".
+ *
+ * `drv_id` is a PB record id, so it is useless to an operator. The page
+ * already loads the user list into a `driversById` map (used to
+ * pre-fill the assign dialog) — this carries that same name onto the
+ * row so the badge can say WHO has the stop rather than only that one
+ * is assigned. Undefined when the row has no driver or the user
+ * lookup failed; the badge then degrades to the old label rather than
+ * rendering a raw id.
+ */
+ driverName?: string;
assigned_at?: string;
ready_for_collection?: boolean | null;
/** Stock-only: PB id of the parent `stock_purchase`. */
@@ -71,17 +83,35 @@ export function getPoolAgeLabel(updatedAt: string | undefined, now = new Date())
return `${Math.floor(hours / 24)}d`;
}
-export function getAssignmentBadge(state: AssignmentState): AssignmentBadge {
+/**
+ * Badge for a row's assignment state.
+ *
+ * `driverName` is the resolved display name from `CollectionPoolViewRow`.
+ * When present it becomes the headline — "Assigned to Ruan" — because the
+ * operator's question is "who has this?", not "is this assigned?". The
+ * today/future distinction is kept as a suffix so the scheduling
+ * signal survives.
+ *
+ * Without a resolvable name the badge falls back to the previous
+ * date-only wording. It never renders the PB record id: an operator
+ * cannot act on "assigned to krpyec9l".
+ */
+export function getAssignmentBadge(
+ state: AssignmentState,
+ driverName?: string,
+): AssignmentBadge {
+ const who = (driverName || "").trim();
+
switch (state) {
case "assigned_today":
return {
- label: "Assigned today",
+ label: who ? `Assigned to ${who}` : "Assigned today",
className:
"bg-blue-100 text-blue-800 ring-1 ring-blue-200/60 dark:bg-blue-500/15 dark:text-blue-300 dark:ring-blue-400/30",
};
case "assigned_future":
return {
- label: "Assigned future",
+ label: who ? `${who} · scheduled` : "Assigned future",
className:
"bg-violet-100 text-violet-800 ring-1 ring-violet-200/60 dark:bg-violet-500/15 dark:text-violet-300 dark:ring-violet-400/30",
};
@@ -120,6 +150,10 @@ export function buildVisibleRows(
row.orderNumber ?? "",
row.customerName ?? "",
row.companyName ?? "",
+ // Typing a driver's name is the natural way to answer "what
+ // has Ruan got?" on this page — the badge shows it, so the
+ // filter must too.
+ row.driverName ?? "",
].some((value) => value.toLowerCase().includes(query));
})
.sort((a, b) => {