feat(collection-pool): add buildSupplierGroups helper

This commit is contained in:
SJCoetz 2026-07-10 23:39:05 +02:00
parent cdaa2e2b25
commit 4662347ca0
2 changed files with 104 additions and 0 deletions

View file

@ -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);
});
});

View file

@ -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<string, string>,
): CollectionPoolGroup[] {
const groups: CollectionPoolGroup[] = [];
const bySupplier = new Map<string, CollectionPoolGroup>();
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,