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

12 KiB

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

  • Step 1: Write the failing tests for the new sort mode

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",
    ]);
  });
});
  • 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.

  • Step 3: Add the new sort-mode and cluster-key types
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;
};
  • Step 4: Update buildVisibleRows() to support both sort modes
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 ?? ""}`,
    );
  });
}
  • 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
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

const [sortMode, setSortMode] = useState<CollectionPoolSortMode>("operational");
  • Step 2: Derive a stable cluster sort key alongside the existing marker map
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()
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
<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
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
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
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.