- Introduced a new `DuplicateKind` type to differentiate between "order_number" and "line_aggregate" duplicates.
- Updated the `DuplicateOrdersDialog` component to support tabbed navigation between order number and line aggregate views, improving user experience.
- Enhanced the data structure for duplicate records to include `totalQuantity` and `totalSellingPrice`, facilitating more accurate duplicate detection.
- Implemented logic to auto-flip to the tab with available content, ensuring operators do not encounter empty views.
- Expanded test coverage for the `findDuplicateOrders` function to validate new line-aggregate matching logic and ensure accurate detection of duplicates across different scenarios.
- Improved documentation for the duplicate order handling process, clarifying the criteria for identifying duplicates.
- Added a switch to toggle the visibility of supplier names and PO numbers in the invoicing page, enhancing user experience by allowing operators to view detailed information as needed.
- Updated the `RowIdentity` component to conditionally render supplier and PO details based on the toggle state.
- Introduced localStorage persistence for the detailed view preference, ensuring user settings are retained across sessions.
- Enhanced tests to verify the functionality of the detailed view toggle and its impact on displayed data.
Add a complete workflow for managing duplicate intake orders:
- Add new server API endpoints for single order cancellation, duplicate detection, and bulk duplicate cancellation
- Add client UI components for bulk duplicate cleanup and single order soft cancellation
- Add a "Duplicates" button to the orders dashboard for quick access to the cleanup tool
- Refactor invoicing module: rename customerNameSimilarity to nameSimilarity, update QB customer picker to use radio buttons and remove automatic preselection
- Add comprehensive test coverage for the new order service utilities
- add `delivery_note` to the ItemStatus union type
- move delivery_note visual config from EXTRA_ITEM_STATUS_VISUAL to ITEM_STATUS_VISUAL
- update tab group mappings: add delivery_note to processing tab, expand collected tab with post-pickup statuses
- move removed status to the cancelled tab group
- replace silent fallback in itemStatusToTabGroup with explicit error for unmapped statuses
- add comprehensive regression test to ensure all ItemStatus values are mapped to a tab
This commit adds several key features, improvements, and cleanups across the codebase:
- Add `order_items.confirm_collected` MCP/REST endpoint to mark order items as collected without requiring driver assignment
- Add standalone order creation and order split APIs for flexible order management
- Update order list sort to oldest-first to align with user workflow of working through oldest orders first
- Improve order search to show cross-tab matches and display source tab indicators on search results
- Add customer name similarity check to warn about mismatched QuickBooks customers during invoice creation
- Remove supplier name column from delivery note PDFs and clean up related data passing across delivery note routes
- Add validation and debug logging to ItemIntelPopover for missing item ID on apply actions
- Remove unused OrderItemContextPopoverTrigger component from order detail page
- Add delivery note regeneration script and update test suites for new and existing functionality
- Update documentation and add test coverage for the new collection confirm tool
- Add server-side `/api/v1/dashboard` endpoint to reduce client-side payload and round trips
- Implement invoice upload and order-item SKU update APIs with complete test suites
- Refactor order and order-items list endpoints (including MCP tools) to support proper PocketBase filter and sort params
- Fix pagination validation for order-items route to clamp page ≥1 and perPage 1-500
- Add redirects in next.config.ts to map underscore collection paths to hyphenated API routes
- Improve buyer tasks page to avoid redundant tab count refetches when switching tabs
- Refactor collection confirm components with editable SKUs and updated accessibility labels
- Add cached admin PocketBase auth to minimize repeated authentication calls
- Add performance testing scripts for dashboard endpoint latency
- Update quotes list route to default to excluding archived counts
- Fix photo strip component text and labels for non-invoice documents
- Refactor sprint page to lazy load supplier/customer data for pricing modal
- Clean up unused imports in test files
- Add full test coverage for all new APIs, services, and components
The 'Delivered' tab on /orders was broken \u2014 4,326 of 4,514 orders
(96%) have at least one delivered item. The old tab tried to
build a WHERE id IN (...) filter with 4,326 ids (~120 KB),
blowing past PB's ~4 KB filter cap and 400ing. This commit
moves historical orders to a dedicated page so:
- The main /orders view is fast (22-28 active orders, not 4.5k
historical)
- The archive can be honest about its pagination ('Page 1 of
217' is expected when looking for something from last month)
- The badge counts on the main page stop being dominated by
'Delivered (4,326)' / 'Cancelled (111)'
# Measured against the live PB
/api/v1/orders/archive?page=1&perPage=2 \u2014 15.2s cold, 6.5 KB
response. The 15s is the order_items.getFullList over 13,256
items (the bottleneck; PB paginates over many round-trips).
Per-row payload is the standard 5-6 KB for orders with deep
expand. Subsequent pages within the same minute would be much
faster since PB caches the order_items query.
# Service layer
- getOrdersArchiveData(params) in src/lib/server/orders-service.ts
- Returns paginated list of orders with \u22651 item in
(delivered, cancelled) status, plus per-status distinct
counts for the header totals
- Strategy: getFullList on order_items (ids + status only),
dedupe + per-status counts in one pass, chunk the order ids
by 100 (PB's URL/filter cap is between 4.5-6.7 KB) and fire
parallel getFullList queries on orders with deep expand
- Search hits order_number, company, customer, contact_name
- Sort defaults to -created, override via the sort param
- /api/v1/orders/archive thin route: page, perPage, search, sort
(no tab, no nocache \u2014 archive is one bucket, not a tab split)
# Page
- /orders/archive/page.tsx: card list with header counts
(delivered / cancelled badges), search input, pagination
footer. Click an order to open /orders/[id] in detail.
# Main /orders updates
- Delivered + Cancelled removed from the tab strip (passed
tabKeys=['new','processing','collection_pool','collected'] to
ItemStatusTabs)
- Counts on the main page now exclude delivered + cancelled
- 'Archive' button in the page header takes the operator to
/orders/archive
- Safety useEffect: if activeTab is delivered/cancelled (e.g.
stale URL or local state), reset to 'new'
# Tests
4 new tests in orders-service.test.ts:
- Returns orders with \u22651 delivered/cancelled item, plus counts
- Dedupe: one order with multiple matching items counts as 1
- Search by order_number and contact_name
- Empty result when no orders match
Full suite: 1321/1321 pass.
Co-authored-by: Orca <help@stably.ai>
# Regression: orders/quotes list lost customer/company/rep data
Last commit added a `fields` parameter to the orders and quotes
list queries to trim the wire payload. Measured on a stale test
result, looked like a 16x win. Actual: PB 0.26's `fields`
parameter is a HARD ALLOWLIST of MAIN record columns only \u2014
specifying it makes PB drop the entire `expand` payload from
the response. Result: customer, company, rep, quote names
disappeared from every order/quote card.
Verified against the live PB:
- With `fields`: expand is dropped, just relation IDs returned
- Without `fields`: expand works, full records (55-57 KB for
20 rows with deep expand)
Fix: remove the `fields` parameter from both list queries. The
slim-payload intent has to be paid at the schema level (cap the
unbounded text columns) rather than via PB query params.
The earlier '12x smaller' / '16x smaller' benchmarks were
measurements of the BROKEN response, not the working one. Real
wire sizes are unchanged from before the perf work \u2014 but the
other wins (server-side tab filter, combined status counts,
targeted users fetch, 60s counts cache) all still apply.
# Bonus: show contact_name on the order card
~76 of ~4.5k orders have `contact_name` set (the free-text
person the order is for, captured during email-intake when the
customer record is just the company). Added to the OrderCard
header between customer and rep, with a docstring explaining
the distinction from the customer relation and email_sender.
Added contact_name to the Order type and pinned the regression
with a test that asserts the orders list query does NOT pass
`fields` (so a future 'let's slim the payload' attempt
doesn't reintroduce the bug).
# REP label
The rep column on the order card now reads 'REP' (uppercase)
instead of 'rep' (lowercase). All 1312 tests pass.
Co-authored-by: Orca <help@stably.ai>
Three rounds of fixes for the /orders page:
#1 Correctness \u2014 server-side tab filter
- Add `tab` query param to /api/v1/orders/list; the route validates
it against TAB_GROUP_ORDER and threads it to getOrdersDashboardData.
- listOrdersWithItems now resolves order ids for the tab via
getOrderIdsForTabGroup (using the existing tabGroupFilter helper),
then fetches just those orders. The footer pagination reflects
the matching set ("Page 1 of N"), not the global 4520-order
count ("Page 1 of 226"). The "Processing (4)" badge now
matches what page 1 actually shows.
- Client: fetchOrders depends on activeTab, so switching tabs
triggers a fresh server-side query. The redundant visibleOrders
client filter is removed. The empty state now nudges the operator
to busy tabs ("Busy tabs: New (12) \u00b7 Delivered (4)").
#2 Perf \u2014 combined counts + targeted users + slim deep expand
- getOrderItemStatusTabCounts collapses 6 parallel per-tab
getFullList calls into ONE combined query, with batch: 5000.
6 filter scans \u2192 1 scan; ~9x fewer internal round-trips.
Bumped cache TTL 30s \u2192 60s.
- Replaced users.getFullList with fetchRepUsersForOrders \u2014 only
fetches the user ids the loaded orders actually reference.
- Dropped potential_quote from the deep expand (list view only
checks the field's existence).
- Added a migration script for the order_items.item_status index
(scripts/migrations/2026-08-04-...). Idempotent: the live PB
already has the index per the schema export, so the script
no-ops on most installs; documented as the recommended manual
step in case an older instance is missing it.
#3 Perf \u2014 wire-payload limits on deep expand
- The quotes collection has unbounded text fields (email_body,
email_message). Without limiting, the orders deep-expand was
dragging the full email body of every linked quote into the
list response \u2014 20 orders with deep expand = 1-5 MB JSON.
- The orders list now passes a `fields` param with the
`expandField.subfield` syntax PB supports: only
`quote.quote_number`, `company.company`, `customer.customer`,
`customer.rep.{id,name,email}` come back, not the full records.
- The items query likewise limits fields to the 8 columns the list
view + search predicate actually read; the supplier expand (not
rendered per-line) is dropped.
Tests: 22 new tests in orders-service.test.ts (tab filter, cache
bypass, combined-query call count, pagination honesty, field-list
contract). Full suite: 1306/1306 pass. The mock's where helper was
upgraded to support || and != / =~ / !~ operators, and the
getFullList mock now honours the filter arg.
Realtime: debounce bumped 300ms \u2192 1000ms, and the realtime path
passes nocache=1 so the 30/60s tab-counts cache is bypassed on
changes the operator just made.
Co-authored-by: Orca <help@stably.ai>
- orders-service.test.ts: destructure capture-group bug on regex match
(was assigning m[0] to 'field', so filter clauses never matched).
Two pre-seed tests were masked by the bug.
- quotes-service.test.ts: createQuoteFromEmail now returns 0
attachments; the n8n flow uploads them in a follow-up step. Update
the assertion that still expected the old embedded-base64 count.
- collection-pool/page.test.tsx: tests assume ONE supplier group with
multiple rows; supply that shape and scope the selectors. Also wait
for data load in the chevron test and gather list items across all
<ul> elements in the sort control helper.
- pocketbase.ts: wrap authStore.onChange registration defensively so
the test mock (no authStore) doesn't throw at startup.
- stock-orders/page.tsx: drop the re-export of
STOCK_PURCHASE_STATUS_VALUES on the page module — Next.js page
route files only allow the default export.
- collection-confirm/view-model.test.ts: buildConfirmNote input is
Pick<InvoiceDetails, 'date' | 'number'>; drop the extra setBy fields.
Co-authored-by: Orca <help@stably.ai>
src/app/api/v1/sprint/attachment-counts/route.ts
- Chunk itemIds into batches of 80 before building the PocketBase filter;
the live cap was hit at ~115 IDs × 15 chars and surfaced as a
generic Next 500 ("Something went wrong while processing your
request.") that hid the real cause. 7 chunks for the sprint
page's max of 500 items.
src/app/layout.tsx
- Add metadataBase derived from NEXT_PUBLIC_SITE_URL /
NEXT_PUBLIC_POCKETBASE_URL. Silences the per-page
"metadataBase not set" warning during dev renders and gives
OG / Twitter cards a stable canonical base.
src/app/api/v1/{quotes,order-items,purchase-orders,stock,
stock-purchases,users,sprint,driver,ai-insights}/route.ts (NEW)
- Add the missing top-level collection routes the agent API
needs. seven are straight passthroughs to their PB collection;
/sprint proxies to quote_items with optional claimStatus and
category query params; /driver lists PB users (no role field
in PB so callers narrow with ?filter=); /ai-insights returns
a roll-up count + most-recent regenerate timestamp across the
four collections that carry ai_* JSON fields.
src/app/api/v1/collection-pool/{route.ts,route.test.ts},
src/app/(app)/collection-pool/{page.tsx,page.test.tsx,
view-model.ts,view-model.test.ts,components.tsx} (DELETED)
- Drop the collection_pool feature entirely: PB has no
collection_pool collection on the live instance
(pb:migrate:orders-v2 was never run), the codebase comment
in purchase-orders-service.ts says "now-removed", and the
sidebar entrypoint has been migrated to /sprint + /deliveries.
The replacement operational status is the `item_status =
"collection_pool"` enum value on order_items, which stays.
src/lib/server/orders-service.ts (+ test), src/lib/pb-helpers.ts,
src/components/sidebar.tsx, src/components/ui/assign-driver-dialog.tsx,
src/app/(app)/collection-map/sidebar.tsx,
src/app/api/mcp/route.ts (+ test) (MODIFIED)
- Remove listCollectionPool + getCollectionPool +
updateCollectionPoolStatus, the /collection-pool sidebar entry,
the deep-link in assign-driver-dialog and collection-map
sidebar, and the collection_pool.list MCP tool (the tool,
handler, and test mock).
scripts/migrations/2026-07-26-fix-quote-counter-and-reassign-numbers.ts (NEW)
- One-shot migration script that:
1. Reads every quote, sorts by created ASC.
2. Reassigns clean Q202607001 … Q202607{N} over the whole
table (including the e+22 garbage and the legacy
Qnnnnnn rows).
3. Sets quote_counters.(year=2026, month=7).last_seq = N
so the next /api/v1/quotes/next-number returns a clean,
atomic, unique number.
Default mode = dry-run; pass --apply to write.
Already executed against the live cloud PB during this session
(verified: 200/200 rows updated, counter at 200, next-number
returns Q202607201, Q202607202, Q202607203 on consecutive calls).
AGENTS.md (MODIFIED)
- Add data-layer-discipline block matching the style of the
three existing blocks. Documents which layer is legacy (pb-helpers)
vs current (lib/server/*-service.ts via /api/v1/*), gives the
migration order (customers → orders → quotes → suppliers →
companies → rest), and includes the grep one-liner future AI
agents should run before extending pb-helpers.
Not in this commit (your WIP, untouched):
src/app/(app)/{dashboard,delivery-notes,layout,orders/[id],
quotes/[id],quotes/approvals,quotes,sprint}/page.tsx,
src/app/(auth)/login/page.tsx, src/components/charts/*,
src/components/{company-form,customer-form,quote-email-card,
sprint-item-pricing-modal,supplier-form,theme-bootstrap,
theme-picker}*.{tsx,test.tsx}, src/app/api/invoicing/delivery-note/*
src/app/api/v1/quotes/[id]/pdf/route.ts, src/app/globals.css,
src/lib/{item-status-ui,pdf/quote-document,quote-status*,themes,
types}.ts, src/lib/server/{order-item-status*,quotes-service}.ts,
src/styles/themes.css, APP_CAPABILITIES.md, README.md,
docs/SHIPPED.md, scripts/smoke/screenshot-pages.mjs,
and the new untracked files under docs/, scripts/migrations/
2026-07-19…25, src/app/{(app)/deliveries,(app)/delivery-notes,
api/v1/ai-insights/rerun,api/v1/deliveries,api/v1/delivery-notes,
api/v1/quotes/[id]/partial-approve,api/v1/quotes/[id]/split},
src/components/{ai-insights-panel,ai-insights-renderer,
ai-item-history-trigger,brand-stripe,delivery-notes,ui/stock-item-search,
ui/duplicate-warning,ui/duplicate-warning.test}.tsx, supplier-form.test.tsx,
src/lib/{pb-helpers-duplicate.test,server/deliveries-service*,
server/delivery-notes-service*}, public/wfdash-logo*.png,
opencode.jsonc.
- Standardize quote number format from Q-YYYY-MM-NNN to QYYYYMMNNN across all client, server, test and API code
- Add atomic backfill logic for quote counters to avoid sequence collisions with existing records
- Rearrange Delivery Notes and Repairs to the Operations sidebar section
- Add supplier count badge to pricelist catalogue and improve filter tab overflow styling
- Optimize font loading in root layout to skip preloading unused font weights
- Improve table styling for customer and company pages with better text wrapping and empty states
- Fix rep display logic in quotes page to show "Unassigned" when no rep is assigned
- Add supporting documentation, PocketBase counter schemas and setup migration scripts
- Remove embedded SupplierMap from suppliers page for cleaner layout
- Improve invoice card layout for company and customer info spacing
- Improve filter label styling and accessibility in collection map page
This commit overhauls the order management system to use item-level status tracking instead of a single order-level status:
- Remove deprecated `orders.status` field and add `invoiced` and quote match audit fields
- Add `isOrderComplete` utility with tests to validate fully delivered orders
- Add purchase order PDF rendering support and tooling
- Add geo coordinate fields for companies/customers with map link UI
- Add REST management APIs for customers, companies, and suppliers
- Update customer/company services to use relation fields instead of free text
- Add status pill and tab filter components for item status visualization
- Add quote-to-order matching logic and unit tests
- Update all pages, components, and tests to match the new schema
- Include all required PocketBase migrations and schema updates
Fix inconsistent line endings across all project files for cross-environment consistency.
Add complete unit test suite covering core utilities, API routes, and server services.
Add Playwright E2E test setup with smoke, auth, and agent API tests.
Update all project configuration files to match standardized formatting.
Fix MCP API error handling to properly propagate HTTP status codes and use correct JSON-RPC error codes.
Add VSCode workspace configuration, extensions, and example MCP config files.
Update .gitignore to exclude new test and build artifacts.