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
351 lines
12 KiB
Markdown
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.
|