diff --git a/src/app/(app)/collection-pool/view-model.test.ts b/src/app/(app)/collection-pool/view-model.test.ts index d5ff862..b018482 100644 --- a/src/app/(app)/collection-pool/view-model.test.ts +++ b/src/app/(app)/collection-pool/view-model.test.ts @@ -2,7 +2,9 @@ import { describe, expect, it } from "vitest"; import { buildProximityMarkerMap, + buildSupplierGroups, buildVisibleRows, + type CollectionPoolGroup, type CollectionPoolSortMode, getAssignmentBadge, getAssignmentState, @@ -263,3 +265,52 @@ describe("buildVisibleRows sort modes (nearby pinning)", () => { expect(out.map((r) => r.id)).toEqual(["a", "b", "no-coords-1", "no-coords-2"]); }); }); + +describe("buildSupplierGroups", () => { + it("returns an empty array for empty input", () => { + expect(buildSupplierGroups([], new Map())).toEqual([]); + }); + + it("folds rows by supplierId in input order", () => { + const rows = [ + makeRow({ id: "a", supplierId: "sup-1", supplierName: "Acme", poNumber: "PO-1" }), + makeRow({ id: "b", supplierId: "sup-2", supplierName: "Beta", poNumber: "PO-2" }), + makeRow({ id: "c", supplierId: "sup-1", supplierName: "Acme", poNumber: "PO-3" }), + ]; + const groups = buildSupplierGroups(rows, new Map()); + expect(groups.map((g) => g.supplierId)).toEqual(["sup-1", "sup-2"]); + expect(groups[0].rows.map((r) => r.id)).toEqual(["a", "c"]); + expect(groups[1].rows.map((r) => r.id)).toEqual(["b"]); + }); + + it("captures the top two PO numbers and reports the remaining count", () => { + const rows = [ + makeRow({ supplierId: "sup-1", poNumber: "PO-1" }), + makeRow({ supplierId: "sup-1", poNumber: "PO-2" }), + makeRow({ supplierId: "sup-1", poNumber: "PO-3" }), + makeRow({ supplierId: "sup-1", poNumber: "PO-4" }), + makeRow({ supplierId: "sup-1", poNumber: "PO-4" }), // duplicate PO + ]; + const [group] = buildSupplierGroups(rows, new Map()); + expect(group.topPoNumbers).toEqual(["PO-1", "PO-2"]); + expect(group.remainingPoCount).toBe(2); + }); + + it("uses marker token from supplierMarkerMap", () => { + const map = new Map([["sup-1", "marker-blue"]]); + const [group] = buildSupplierGroups( + [makeRow({ supplierId: "sup-1" })], + map, + ); + expect(group.markerToken).toBe("marker-blue"); + }); + + it("returns empty eyebrow when no rows have a PO number", () => { + const [group] = buildSupplierGroups( + [makeRow({ supplierId: "sup-1", poNumber: "" })], + new Map(), + ); + expect(group.topPoNumbers).toEqual([]); + expect(group.remainingPoCount).toBe(0); + }); +}); diff --git a/src/app/(app)/collection-pool/view-model.ts b/src/app/(app)/collection-pool/view-model.ts index 64c8278..8faa1d4 100644 --- a/src/app/(app)/collection-pool/view-model.ts +++ b/src/app/(app)/collection-pool/view-model.ts @@ -124,6 +124,59 @@ export function buildVisibleRows( }); } +export type CollectionPoolGroup = { + supplierId: string; + supplierName: string; + /** Cluster colour token from the proximity map (may be undefined). */ + markerToken?: string; + /** First two distinct PO numbers for the header eyebrow. */ + topPoNumbers: string[]; + /** Number of distinct POs beyond the top two. */ + remainingPoCount: number; + /** Rows belonging to this group, in render order. */ + rows: CollectionPoolViewRow[]; +}; + +/** + * Folds already-sorted rows into per-supplier groups, preserving input + * order. The caller is responsible for sorting rows by `supplierName` + * (use `buildVisibleRows` first); this helper does not re-sort. + */ +export function buildSupplierGroups( + rows: CollectionPoolViewRow[], + supplierMarkerMap: Map, +): CollectionPoolGroup[] { + const groups: CollectionPoolGroup[] = []; + const bySupplier = new Map(); + + for (const row of rows) { + let group = bySupplier.get(row.supplierId); + if (!group) { + group = { + supplierId: row.supplierId, + supplierName: row.supplierName ?? "", + markerToken: supplierMarkerMap.get(row.supplierId), + topPoNumbers: [], + remainingPoCount: 0, + rows: [], + }; + bySupplier.set(row.supplierId, group); + groups.push(group); + } + group.rows.push(row); + } + + for (const group of groups) { + const uniquePos = Array.from( + new Set(group.rows.map((r) => r.poNumber ?? "").filter(Boolean)), + ); + group.topPoNumbers = uniquePos.slice(0, 2); + group.remainingPoCount = Math.max(0, uniquePos.length - 2); + } + + return groups; +} + export function buildProximityMarkerMap( suppliers: Array<{ supplierId: string; lat: number; lng: number }>, radiusKm: number,