WFDASH-WEBAPP/docs/superpowers/plans/2026-07-07-collection-pool-cluster-sort.md
SJCoetz 5f1838051f feat(collection-pool): add nearby supplier cluster sort mode
Add optional nearby-supplier cluster sort mode to the collection pool table, keeping operational sort as the default. Also includes:
- Update UserSearch component to support fallback selected label for pre-selected values
- Refactor AssignDriverDialog into a separate inner component with intent and pre-filled driver label support
- Add assignment badge helper types and functions
- Add full test coverage for new sort logic and component behaviors
- Update implementation plans and documentation files
- Fix minor code consistency and lint issues across touched files
2026-07-07 21:56:29 +02:00

351 lines
12 KiB
Markdown

# Collection Pool Cluster Sort Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add an optional `Nearby suppliers` sort mode to the collection-pool table while keeping the current operational sort as the default.
**Architecture:** Extend the existing collection-pool view-model helpers to support a selectable sort mode and a stable cluster sort key derived from the current proximity-marker pipeline. Then add a compact sort control in `page.tsx` and validate that the unified list, filters, and row actions still behave correctly in both modes.
**Tech Stack:** Next.js app router, React 19 client components, TypeScript, Tailwind CSS, Vitest, browser validation
---
## File Structure
- Modify: `src/app/(app)/collection-pool/view-model.ts`
- Add explicit sort-mode types and helper logic for cluster-aware row ordering.
- Modify: `src/app/(app)/collection-pool/view-model.test.ts`
- Add focused failing tests for the new cluster sort behavior.
- Modify: `src/app/(app)/collection-pool/page.tsx`
- Add sort-mode UI state and pass the selected sort mode into the view-model.
- Modify: `docs/superpowers/plans/2026-07-07-collection-pool-cluster-sort.md`
- Check off steps during implementation.
### Task 1: Add Cluster Sort Support To The View Model
**Files:**
- Modify: `src/app/(app)/collection-pool/view-model.ts`
- Modify: `src/app/(app)/collection-pool/view-model.test.ts`
- [x] **Step 1: Write the failing tests for the new sort mode**
```ts
import { describe, expect, it } from "vitest";
import {
buildVisibleRows,
type CollectionPoolSortMode,
type CollectionPoolViewRow,
} from "./view-model";
function makeRow(overrides: Partial<CollectionPoolViewRow> = {}): CollectionPoolViewRow {
return {
id: "row-1",
description: "Wheel hub kit",
updated: "2026-07-05T08:00:00.000Z",
item_status: "collection_pool",
drv_id: "",
assigned_at: "",
ready_for_collection: false,
supplierId: "sup-1",
supplierName: "Alpha Bearings",
companyName: "Anglo",
customerName: "North Pit",
poNumber: "PO-1",
orderNumber: "SO-1",
...overrides,
};
}
describe("buildVisibleRows sort modes", () => {
it("keeps operational sort as the default mode", () => {
const rows = buildVisibleRows(
[
makeRow({ id: "future", item_status: "assigned", drv_id: "drv-2", assigned_at: "2026-07-08T00:00:00.000Z" }),
makeRow({ id: "unassigned", clusterSortKey: "cluster-2" }),
makeRow({ id: "today", item_status: "assigned", drv_id: "drv-1", assigned_at: "2026-07-07T00:00:00.000Z" }),
],
{ todayYmd: "2026-07-07", searchText: "", readyFilter: "all", sortMode: "operational" },
);
expect(rows.map((row) => row.id)).toEqual(["today", "unassigned", "future"]);
});
it("sorts by cluster key before operational order in nearby mode", () => {
const rows = buildVisibleRows(
[
makeRow({ id: "cluster-b-unassigned", clusterSortKey: "cluster-b" }),
makeRow({
id: "cluster-a-today",
clusterSortKey: "cluster-a",
item_status: "assigned",
drv_id: "drv-1",
assigned_at: "2026-07-07T00:00:00.000Z",
}),
makeRow({
id: "cluster-a-future",
clusterSortKey: "cluster-a",
item_status: "assigned",
drv_id: "drv-2",
assigned_at: "2026-07-08T00:00:00.000Z",
}),
],
{ todayYmd: "2026-07-07", searchText: "", readyFilter: "all", sortMode: "nearby" },
);
expect(rows.map((row) => row.id)).toEqual([
"cluster-a-today",
"cluster-a-future",
"cluster-b-unassigned",
]);
});
});
```
- [x] **Step 2: Run the test to verify it fails**
Run: `npm run test:run -- "src/app/(app)/collection-pool/view-model.test.ts"`
Expected: FAIL because `sortMode` and `clusterSortKey` are not defined in the current helper types/logic.
- [x] **Step 3: Add the new sort-mode and cluster-key types**
```ts
export type CollectionPoolSortMode = "operational" | "nearby";
export type CollectionPoolViewRow = {
id: string;
description?: string;
updated?: string;
item_status?: string;
drv_id?: string;
assigned_at?: string;
ready_for_collection?: boolean | null;
supplierId: string;
supplierName?: string;
companyName?: string;
customerName?: string;
poNumber?: string;
orderNumber?: string;
clusterSortKey?: string;
};
```
- [x] **Step 4: Update `buildVisibleRows()` to support both sort modes**
```ts
export function buildVisibleRows(
rows: CollectionPoolViewRow[],
options: {
todayYmd: string;
searchText: string;
readyFilter: "all" | "ready" | "unready";
sortMode: CollectionPoolSortMode;
},
): CollectionPoolViewRow[] {
const query = options.searchText.trim().toLowerCase();
const filtered = rows.filter((row) => {
if (options.readyFilter === "ready" && !row.ready_for_collection) return false;
if (options.readyFilter === "unready" && row.ready_for_collection) return false;
if (!query) return true;
return [
row.supplierName ?? "",
row.description ?? "",
row.poNumber ?? "",
row.orderNumber ?? "",
row.customerName ?? "",
row.companyName ?? "",
].some((value) => value.toLowerCase().includes(query));
});
return filtered.sort((a, b) => {
if (options.sortMode === "nearby") {
const clusterDiff = (a.clusterSortKey ?? "zzz") .localeCompare(b.clusterSortKey ?? "zzz");
if (clusterDiff !== 0) return clusterDiff;
}
const stateDiff =
ASSIGNMENT_SORT_ORDER[getAssignmentState(a, options.todayYmd)] -
ASSIGNMENT_SORT_ORDER[getAssignmentState(b, options.todayYmd)];
if (stateDiff !== 0) return stateDiff;
const updatedA = a.updated ?? "";
const updatedB = b.updated ?? "";
if (updatedA !== updatedB) return updatedA.localeCompare(updatedB);
return `${a.supplierName ?? ""}|${a.poNumber ?? ""}|${a.description ?? ""}`.localeCompare(
`${b.supplierName ?? ""}|${b.poNumber ?? ""}|${b.description ?? ""}`,
);
});
}
```
- [x] **Step 5: Run the tests to verify they pass**
Run: `npm run test:run -- "src/app/(app)/collection-pool/view-model.test.ts"`
Expected: PASS with the new sort-mode coverage green.
- [ ] **Step 6: Commit**
```bash
git add "src/app/(app)/collection-pool/view-model.ts" "src/app/(app)/collection-pool/view-model.test.ts"
git commit -m "test: add collection pool cluster sort coverage"
```
### Task 2: Wire Cluster Sort Into The Collection Pool Page
**Files:**
- Modify: `src/app/(app)/collection-pool/page.tsx`
- Modify: `src/app/(app)/collection-pool/view-model.ts`
- [ ] **Step 1: Add sort-mode state to the page**
```ts
const [sortMode, setSortMode] = useState<CollectionPoolSortMode>("operational");
```
- [ ] **Step 2: Derive a stable cluster sort key alongside the existing marker map**
```ts
const supplierMarkerMap = useMemo(() => {
if (!supplierCoords) return new Map<string, string>();
return buildProximityMarkerMap(
Array.from(supplierCoords.entries()).map(([supplierId, coord]) => ({
supplierId,
lat: coord.lat,
lng: coord.lng,
})),
2,
);
}, [supplierCoords]);
const clusterSortKeyForSupplier = useCallback(
(supplierId: string) => supplierMarkerMap.get(supplierId) ?? `solo-${supplierId}`,
[supplierMarkerMap],
);
```
- [ ] **Step 3: Pass the selected sort mode and cluster key into `buildVisibleRows()`**
```ts
const visibleRows = useMemo(() => {
if (!rows) return null;
return buildVisibleRows(
rows.map((row) => {
const supplierId = row.expand?.purchase_order?.supplier ?? "unknown";
return {
...row,
supplierId,
supplierName: row.expand?.purchase_order?.expand?.supplier?.suppliername ?? "Unknown supplier",
companyName: row.expand?.order?.expand?.company?.company ?? "",
customerName: row.expand?.order?.expand?.customer?.customer ?? "",
poNumber: row.expand?.purchase_order?.po_number ?? "",
orderNumber: row.expand?.order?.order_number ?? "",
clusterSortKey: clusterSortKeyForSupplier(supplierId),
};
}),
{
todayYmd,
searchText: filterText,
readyFilter,
sortMode,
},
) as CollectionPoolVisibleRow[];
}, [rows, todayYmd, filterText, readyFilter, sortMode, clusterSortKeyForSupplier]);
```
- [ ] **Step 4: Add the compact sort control to the filter bar**
```tsx
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<span className="font-medium uppercase tracking-wide">Sort</span>
<div
role="radiogroup"
aria-label="Sort collection pool"
className="inline-flex rounded-md border bg-card shadow-sm"
>
{([
{ id: "operational", label: "Operational" },
{ id: "nearby", label: "Nearby suppliers" },
] as const).map((opt) => (
<button
key={opt.id}
type="button"
role="radio"
aria-checked={sortMode === opt.id}
onClick={() => setSortMode(opt.id)}
className={cn(
"px-3 py-1 text-xs transition-colors",
sortMode === opt.id
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:bg-muted",
)}
>
{opt.label}
</button>
))}
</div>
</div>
```
- [ ] **Step 5: Run lint on the touched files**
Run: `npm run lint -- "src/app/(app)/collection-pool/page.tsx" "src/app/(app)/collection-pool/view-model.ts" "src/app/(app)/collection-pool/view-model.test.ts"`
Expected: PASS with no ESLint errors.
- [ ] **Step 6: Commit**
```bash
git add "src/app/(app)/collection-pool/page.tsx" "src/app/(app)/collection-pool/view-model.ts" "src/app/(app)/collection-pool/view-model.test.ts"
git commit -m "feat: add cluster sort to collection pool"
```
### Task 3: Validate The New Sort Mode In The Browser
**Files:**
- Modify: `docs/superpowers/plans/2026-07-07-collection-pool-cluster-sort.md`
- [ ] **Step 1: Run the focused test suite**
Run: `npm run test:run -- "src/app/(app)/collection-pool/view-model.test.ts"`
Expected: PASS.
- [ ] **Step 2: Run the final lint pass**
Run: `npm run lint -- "src/app/(app)/collection-pool/page.tsx" "src/app/(app)/collection-pool/view-model.ts" "src/app/(app)/collection-pool/view-model.test.ts"`
Expected: PASS.
- [ ] **Step 3: Perform manual browser validation**
```text
Open /collection-pool and verify:
1. The page defaults to Sort = Operational.
2. Switching to Nearby suppliers changes row order without adding group headers.
3. Same-cluster suppliers appear together.
4. Search still filters correctly in both sort modes.
5. Ready only and Awaiting only still work in both sort modes.
6. Assign Driver, Reschedule, Mark Ready, and Mark Awaiting still work after reordering.
```
- [ ] **Step 4: Commit**
```bash
git add "docs/superpowers/plans/2026-07-07-collection-pool-cluster-sort.md" "src/app/(app)/collection-pool/page.tsx" "src/app/(app)/collection-pool/view-model.ts" "src/app/(app)/collection-pool/view-model.test.ts"
git commit -m "test: validate collection pool cluster sort"
```
## Self-Review
- Spec coverage:
- new sort control: Task 2
- operational default: Task 1 and Task 3
- nearby-supplier ordering: Task 1 and Task 2
- no return of grouped UI: Task 2 and Task 3
- validation of filters/actions in both modes: Task 3
- Placeholder scan:
- No `TBD`, `TODO`, or vague "handle later" language remains.
- Type consistency:
- `CollectionPoolSortMode`, `clusterSortKey`, and `buildVisibleRows()` use the same names and signatures across all tasks.