From e66437322fd0ed2137964a535474e1602c5c42b8 Mon Sep 17 00:00:00 2001 From: Dave Date: Mon, 31 Aug 2026 16:34:37 +0200 Subject: [PATCH] Revert "feat(orders): add Duplicates tab surfacing flagged duplicate orders" This reverts commit b4855a9c9ff8aacedd44a5f13d9aed226ae70fa0. --- src/app/(app)/orders/page.tsx | 40 +------- src/lib/item-status-ui.ts | 30 +----- src/lib/server/orders-service.ts | 162 ++----------------------------- 3 files changed, 12 insertions(+), 220 deletions(-) diff --git a/src/app/(app)/orders/page.tsx b/src/app/(app)/orders/page.tsx index 61080df..b321ff6 100644 --- a/src/app/(app)/orders/page.tsx +++ b/src/app/(app)/orders/page.tsx @@ -72,11 +72,6 @@ const EMPTY_COUNTS: Record = { completed: 0, delivered: 0, cancelled: 0, - // Cross-cutting review tab — counted separately by - // `getOrderItemStatusTabCounts` (a PB filter on order-header - // duplicate fields, not the item-status scan). Always present in - // the shape so `Record` stays satisfied. - duplicates: 0, }; const SEARCH_DEBOUNCE_MS = 250; @@ -353,18 +348,11 @@ export default function OrdersPage() { slice (last COMPLETED_WINDOW_DAYS, see orders-service.ts) — orders that fall out of the window roll down to /orders/archive automatically. */} - {/* The `duplicates` tab is the cross-cutting review surface — - every order with any of the three duplicate-detection flags - set. Sits at the end of the strip (not in TAB_GROUP_ORDER's - status-driven slots) so it doesn't visually compete with the - active-workflow tabs. See src/lib/item-status-ui.ts for the - purple #a855f7 visual + the `duplicates: []` placeholder in - `TAB_GROUP_ITEM_STATUSES`. */} {/* Search — matches order number, company, customer, item description, or item price */} @@ -399,32 +387,6 @@ export default function OrdersPage() {

{error}

- ) : orders.length === 0 && activeTab === "duplicates" && !searching ? ( - // Duplicates empty state — no flags set. Distinct from the - // generic "no orders" copy so the operator knows the matcher - // is healthy (i.e. it ran and found nothing) rather than - // silently broken. The "Re-run backfill" link is intentionally - // a no-op for now (the matchers run on every order write and - // on intake); kept as a future hook for a manual batched - // backfill tool. - - - -

No duplicates detected

-

- The matcher runs on every new order. If you expected a duplicate, run the - backfill to re-scan existing orders. -

- -
-
) : orders.length === 0 ? ( diff --git a/src/lib/item-status-ui.ts b/src/lib/item-status-ui.ts index 0746633..ff8a84c 100644 --- a/src/lib/item-status-ui.ts +++ b/src/lib/item-status-ui.ts @@ -112,19 +112,8 @@ export type TabGroup = | "collected" | "completed" | "delivered" - | "cancelled" - | "duplicates"; + | "cancelled"; -// Display order on /orders. Status tabs come first (the operator's -// primary workflow), then `duplicates` last so the cross-cutting -// review tab sits at the end of the strip and doesn't visually -// compete with the active-workflow tabs. -// -// Why "last" and not "first": the duplicates tab is a manual-review -// surface, not an action queue. Operators reach for it when an -// investigation warrants it — not as part of the daily -// "what's next to process?" sweep. Sinking it to the end matches -// the actual scan order. export const TAB_GROUP_ORDER: TabGroup[] = [ "new", "processing", @@ -134,7 +123,6 @@ export const TAB_GROUP_ORDER: TabGroup[] = [ "completed", "delivered", "cancelled", - "duplicates", ]; export const TAB_GROUP_ITEM_STATUSES: Record = { @@ -178,14 +166,6 @@ export const TAB_GROUP_ITEM_STATUSES: Record = { // the other non-completions so the active workflow tabs don't carry // them. The /orders/archive page surfaces both buckets. cancelled: ["cancelled", "removed"], - // `duplicates` is NOT an item-status group — it's a cross-cutting - // filter on order-header fields (order_number_duplicate / - // potential_duplicate_order / potential_delivery_note). The empty - // array keeps `Record` satisfied; the - // service layer (orders-service.ts) intercepts `tabGroup === - // "duplicates"` and routes to a PB filter on the orders - // collection instead of `tabGroupFilter` against order_items. - duplicates: [], }; /** @@ -249,14 +229,6 @@ export const TAB_GROUP_VISUAL: Record = { completed: { hex: "#064e3b", label: "Completed" }, delivered: { hex: "#047857", label: "Delivered" }, cancelled: { hex: "#e11d48", label: "Cancelled" }, - // Duplicates: deliberately a vibrant purple (#a855f7) that doesn't - // collide with collection_pool's #7c3aed — the collection_pool - // purple is deeper and reserved for "items waiting in the pool", - // while duplicates' brighter purple signals "investigate me" without - // matching any operational state. Sits at the end of the tab strip - // (see TAB_GROUP_ORDER) so the cross-cutting review surface doesn't - // visually compete with the active-workflow tabs. - duplicates: { hex: "#a855f7", label: "Duplicates" }, }; /* ============================================================================ diff --git a/src/lib/server/orders-service.ts b/src/lib/server/orders-service.ts index 7f55d16..70dfa3b 100644 --- a/src/lib/server/orders-service.ts +++ b/src/lib/server/orders-service.ts @@ -122,62 +122,6 @@ export function completedTabFilter(now: Date = new Date()): string { ].join(" || "); } -/** - * PB filter fragment for the `duplicates` tab. - * - * Three independent flag columns on the `orders` collection capture - * the duplicate-detection matchers (see the *-link-service.ts files): - * - `order_number_duplicate` — exact-order-number collision (any - * status, no time limit). Stamped by `suggestOrderNumberDuplicate`. - * - `potential_duplicate_order` — another order with matching line - * items for the same company, last 60 days, regardless of either - * order's status. Stamped by `suggestOrderDuplicate`. - * - `potential_delivery_note` — a past delivery note with matching - * items for the same company, last 60 days. Stamped by - * `suggestDeliveryNoteForOrder`. - * - * Each field is set to either a record id (when there's a match) or - * `null` (when the matcher ran and found nothing — or hasn't run yet). - * We OR three `!= null && != ""` predicates so an order with ANY of - * the three flags set qualifies. The `!= ""` half is belt-and-braces - * safety against a stray empty-string write (PB stores `""` for - * "checked, no match" on text fields like `potential_duplicate_order_number` - * but the relation fields themselves use `null`). - * - * The fragment lives at the orders-collection level (the duplicate - * flags are header fields on orders, not on items), so it's a PB - * `getList` filter — not the item-level `tabGroupFilter` used by - * every other tab. `listOrdersWithItems` and - * `getOrderItemStatusTabCounts` both branch on `tabGroup === - * "duplicates"` to route here. - */ -export function duplicateOrderFilter(): string { - return [ - `(order_number_duplicate != "" && order_number_duplicate != null)`, - `(potential_duplicate_order != "" && potential_duplicate_order != null)`, - `(potential_delivery_note != "" && potential_delivery_note != null)`, - ].join(" || "); -} - -/** - * True when the order has any of the three duplicate-detection flags - * set. Mirrors `duplicateOrderFilter`'s OR — used by the search-match - * loop to fold the duplicates bucket into `tabGroups` so the - * `searchCounts[tab]++` tally reports hits on the duplicates tab when - * the operator searches while standing on it. - * - * Empty strings are treated as "unset" (matches PB filter semantics - * — `!= ""` excludes them). A relation field set to a 15-char id is - * a positive match. - */ -export function orderHasAnyDuplicateFlag(order: Order): boolean { - return Boolean( - (order.order_number_duplicate && order.order_number_duplicate !== "") || - (order.potential_duplicate_order && order.potential_duplicate_order !== "") || - (order.potential_delivery_note && order.potential_delivery_note !== ""), - ); -} - /** Zero-filled per-tab counts. Spread it; never mutate the original. */ const EMPTY_TAB_COUNTS: Record = Object.freeze( Object.fromEntries(TAB_GROUP_ORDER.map((tab) => [tab, 0])), @@ -332,65 +276,24 @@ export async function listOrdersWithItems( // permanently trim the payload. const expand = "company,customer,customer.rep,quote"; - // Tab filter path — four sub-cases: + // Tab filter path — three sub-cases: // (a) no tab, no search → original PB-paginated path // (b) tab + no search → resolve matching order ids, then // PB-paginate the filtered set (small tabs // fall through to `getFullList`+slice when // the `id = "..." || ...` filter would // exceed PB's URL length cap) - // (c) any search (non-duplicates tab) → candidate set is the ACTIVE - // order set (union of the four workflow - // tabs), which is ~91 orders on live PB. - // We fetch those, match in memory, and - // collapse to a single page. Scoping by - // active id set (rather than "first 500 - // by sort") is what makes search - // independent of sort direction — see - // ORDER_SEARCH_HARD_CAP. - // (d) duplicates tab → header-field filter on the orders - // collection (`duplicateOrderFilter`). - // Not item-status-driven, so the - // `getOrderIdsForTabGroup` / - // `getActiveOrderIds` machinery doesn't - // apply. With search active, scope to the - // duplicate set first (small on live PB — - // dozens, not thousands) and filter in - // memory; without search, pass the filter - // straight to PB and let it paginate. + // (c) any search → candidate set is the ACTIVE order set + // (union of the four workflow tabs), which + // is ~91 orders on live PB. We fetch those, + // match in memory, and collapse to a single + // page. Scoping by active id set (rather + // than "first 500 by sort") is what makes + // search independent of sort direction — + // see ORDER_SEARCH_HARD_CAP. let ordersPage: { items: Order[]; totalItems: number; totalPages: number; page: number; perPage: number }; - if (tabGroup === "duplicates") { - // (d) Duplicates tab — the cross-cutting review surface that lists - // every order with any of the three duplicate-detection flags set. - // PB's own filter+pagination handles the non-search case; the - // search case fetches the full duplicate set (typically small) and - // narrows in memory. - const dupFilter = duplicateOrderFilter(); - if (!search) { - ordersPage = await pb.collection(ORDER_COLLECTION).getList( - page, - perPage, - { filter: dupFilter, sort, expand }, - ); - } else { - // Cap mirrors ORDER_SEARCH_HARD_CAP — duplicates are a small - // set on live PB but the cap is the same defence-in-depth - // ceiling used by the active-set search path above. - const allDupOrders = await pb.collection(ORDER_COLLECTION).getList( - 1, - ORDER_SEARCH_HARD_CAP, - { filter: dupFilter, sort, expand }, - ); - ordersPage = { - items: allDupOrders.items, - totalItems: allDupOrders.items.length, - totalPages: 1, - page: 1, - perPage: ORDER_SEARCH_HARD_CAP, - }; - } - } else if (search) { + if (search) { // (c) Search — over the active set only. `tabGroup` is NOT applied // here: we need every active match in order to compute the per-tab // match counts below. The tab narrowing happens after counting. @@ -481,16 +384,6 @@ export async function listOrdersWithItems( for (const item of orderItems) { tabGroups.add(itemStatusToTabGroup(item.item_status)); } - // Cross-cutting `duplicates` bucket — the duplicates tab isn't - // item-status-driven, so we add it to `tabGroups` based on the - // order-header duplicate flags. This is what lets the search - // match counts (and the per-tab badge) report a hit on the - // duplicates tab when the user actively searches while standing - // on it. Without this, `searchCounts.duplicates` would always - // be 0 and the tab badge would mislead the operator. - if (orderHasAnyDuplicateFlag(order)) { - tabGroups.add("duplicates"); - } // Tally the match into every tab it appears in, then narrow. Order // matters — counting after the `continue` below would only ever @@ -824,41 +717,6 @@ export async function getOrderItemStatusTabCounts( counts.cancelled = 0; } - // Duplicates count — separate query because the duplicate-detection - // flags live on `orders` (header), not `order_items` (line). The - // count query is a fields-only `getList(1, 1, ...)` so PB returns - // just the `totalItems` we need without materialising the rows. - // - // Always counted regardless of `includeArchivedTabs` — the - // duplicates tab is a cross-cutting review surface, not an - // active-workflow tab. A duplicate flag on a delivered or cancelled - // order is just as worth surfacing as one on a `new` order (the - // historical-order version is what tells the operator "we - // accidentally booked this twice, the second one is sitting on the - // archive"). - // - // Independent of the item-status scan above — both populate the - // same `counts` record but draw from disjoint sources. - try { - const dupTotal = await pb - .collection(ORDER_COLLECTION) - .getList<{ id: string }>(1, 1, { - filter: duplicateOrderFilter(), - fields: "id", - }); - counts.duplicates = dupTotal.totalItems; - } catch (err) { - // Don't let a count-query failure poison the rest of the - // tab badges — the item-status counts are still useful. - // Logged so a future regression (e.g. a typo'd field name in - // duplicateOrderFilter) is visible without breaking the page. - console.error( - "getOrderItemStatusTabCounts: duplicates count query failed:", - err instanceof Error ? err.message : String(err), - ); - counts.duplicates = 0; - } - cachedTabCounts.set(cacheKey, { value: counts, expiresAt: Date.now() + TAB_COUNTS_CACHE_TTL_MS,