2026-06-11 18:30:02 +00:00
|
|
|
{
|
|
|
|
|
"name": "wfdash",
|
|
|
|
|
"version": "0.1.0",
|
|
|
|
|
"private": true,
|
|
|
|
|
"engines": {
|
|
|
|
|
"node": ">=20.9.0"
|
|
|
|
|
},
|
|
|
|
|
"scripts": {
|
|
|
|
|
"dev": "next dev --webpack",
|
|
|
|
|
"build": "next build",
|
|
|
|
|
"start": "next start",
|
|
|
|
|
"lint": "eslint",
|
|
|
|
|
"setup": "npx tsx scripts/setup-pocketbase.ts",
|
|
|
|
|
"pb:migrate:orders-v2": "npx tsx scripts/migrations/create-order-items-v2-and-collection-pool.ts",
|
2026-06-16 18:46:28 +00:00
|
|
|
"pb:migrate:quote-attachments": "npx tsx scripts/migrations/create-quote-attachments-and-replies.ts",
|
2026-06-22 16:29:59 +00:00
|
|
|
"pb:migrate:quote-items-user-edited": "npx tsx scripts/migrations/add-quote-items-user-edited.ts",
|
2026-06-29 11:06:01 +00:00
|
|
|
"pb:migrate:order-status-in-stock": "echo 'deprecated: orders.status was dropped 2026-06-27. No-op.' && exit 0",
|
|
|
|
|
"pb:migrate:orders-rep": "npx tsx scripts/migrations/2026-06-26-add-orders-rep.ts",
|
|
|
|
|
"pb:migrate:drop-orders-status": "npx tsx scripts/migrations/2026-06-27-drop-orders-status-and-wipe.ts",
|
|
|
|
|
"pb:migrate:add-order-quote-match": "npx tsx scripts/migrations/2026-06-27-add-order-quote-match-fields.ts",
|
|
|
|
|
"pb:migrate:add-purchase-order-pdfs": "npx tsx scripts/migrations/2026-06-28-add-purchase-order-pdfs.ts",
|
|
|
|
|
"pb:migrate:add-companies-coordinates": "npx tsx scripts/migrations/2026-06-28-add-companies-coordinates.ts",
|
|
|
|
|
"pb:migrate:convert-customer-fields": "npx tsx scripts/migrations/2026-06-28-convert-customer-fields.ts",
|
2026-06-30 07:14:36 +00:00
|
|
|
"pb:migrate:raw-price-list": "npx tsx scripts/migrations/2026-06-30-add-raw-price-list.ts",
|
2026-06-30 09:09:27 +00:00
|
|
|
"pb:migrate:pricelist-notes": "npx tsx scripts/migrations/2026-06-30-add-pricelist-notes.ts",
|
feat(pricelist): Catalogue tab with search + category/brand filters
Adds a second tab on /pricelist that shows the structured
`price_list` collection (AI-extracted line items) instead of the
raw PDF uploads. Operators can now:
- Search across SKU, description, and brand
- Filter by category (twist_drill, hss_cutter, countersink, ...)
- Filter by brand (JEI, TurboSteel, TurboTough, ...)
- See cost vs selling price side-by-side in ZAR
- Click a row's 'Source' link to jump back to the raw PDF
The distinct filter values are pulled from a one-shot getList(1, 500)
on mount, with hardcoded fallbacks (FALLBACK_CATEGORIES /
FALLBACK_BRANDS) so the dropdowns are useful even before any
extractions have run.
Layout: Tabs (Raw Uploads | Catalogue) wrap both views; the
header + page chrome stay shared. The DeleteDialog stays outside
the Tabs since it's a modal.
Also ships an idempotent migration to add `source_file` (text)
to `price_list` — used by the n8n workflow to track which raw
PDF each extracted row came from, and by the verify-after-POST
step the workflow uses to decide whether to flip
`raw_price_list.status` to Completed vs Failed.
Price type is in PriceListRow (shared subset) and PriceList
(full record). Supplier names are resolved client-side via
getAllSuppliers() since the live schema has supplier as a text
field rather than a relation.
Tests: 229/230 (same pre-existing failure, unrelated). TypeScript
clean.
2026-06-30 09:49:48 +00:00
|
|
|
"pb:migrate:price-list-source-file": "npx tsx scripts/migrations/2026-06-30-add-price-list-source-file.ts",
|
feat(pricelist): Catalogue search hits every field (incl. synonyms)
The Catalogue tab's search box used to OR across just sku,
description, and brand. Now it hits:
- sku, description, brand, supplier, category, subcategory
(PB's case-insensitive `~` operator, each indexed)
- search_text (a denormalised text blob)
The denormalised column is the key part: PB can't substring-
match a JSON field, so without this, synonyms from
`specifications.synonyms` ("slugger cutter", "turbo tough",
etc.) would never match in the search box. The n8n workflow's
Code node now flattens every searchable bit — including the
synonyms array — into search_text on every POST. The 46 pre-
existing rows were backfilled once with the same formula.
The `price_list.supplier` field also stored the PB id on the
46 pre-existing rows (n8n was setting the resolved name, but
the original LLM tests that seeded the table didn't). Fixed
that in the same backfill — supplier now reads 'JEISA -
WESTSCOTT GROUP' instead of 'q6hpwbxsicyoiuu', which makes
the supplier filter dropdown work AND makes
'supplier ~ "jeisa"' match.
Migration `2026-06-30-add-price-list-search-text`:
- Adds `search_text` (text) column
- Adds `idx_pl_search_text` non-unique index
- Idempotent — re-runs are safe and self-heal the index
Updates:
- `getPriceLists` helper ORs across all 6 columns + search_text
- `PriceList` TS type gains `search_text: string`
- `pricelist-extraction.md` Code node snippet shows how the
n8n workflow should populate `search_text` on every POST
- Search input placeholder reads 'Search anything — SKU,
description, brand, supplier, synonyms…'
Tests: 229/230 (same pre-existing failure, unrelated).
TypeScript clean.
2026-06-30 10:10:27 +00:00
|
|
|
"pb:migrate:price-list-search-text": "npx tsx scripts/migrations/2026-06-30-add-price-list-search-text.ts",
|
2026-06-30 19:20:53 +00:00
|
|
|
"pb:backfill:price-list-search-text": "npx tsx scripts/migrations/2026-06-30-backfill-price-list-search-text.ts",
|
|
|
|
|
"pb:migrate:price-list-sku-normalised": "npx tsx scripts/migrations/2026-06-30-add-price-list-sku-normalised.ts",
|
|
|
|
|
"pb:migrate:quote-items-sku": "npx tsx scripts/migrations/2026-06-30-add-quote-items-sku-and-pricing.ts",
|
|
|
|
|
"pb:migrate:rfq-outbound": "npx tsx scripts/migrations/2026-06-30-create-rfq-outbound.ts",
|
2026-07-09 21:22:23 +00:00
|
|
|
"pb:migrate:sprint-categories": "npx tsx scripts/migrations/2026-07-02-add-sprint-categories-and-claims.ts",
|
2026-07-09 20:28:50 +00:00
|
|
|
"pb:migrate:po-pdf-tokens": "npx tsx scripts/migrations/2026-07-09-po-pdf-tokens.ts",
|
2026-07-09 21:22:23 +00:00
|
|
|
"pb:migrate:po-additional-attachments": "npx tsx scripts/migrations/2026-07-09-po-additional-attachments.ts",
|
2026-07-16 18:41:42 +00:00
|
|
|
"pb:migrate:stock-line-drv-id": "npx tsx scripts/migrations/2026-07-15-add-stock-line-drv-id.ts",
|
2026-07-18 20:43:50 +00:00
|
|
|
"pb:migrate:stock-purchase-documents-history": "npx tsx scripts/migrations/2026-07-16-add-stock-purchase-documents-history.ts",
|
|
|
|
|
"pb:migrate:stock-purchase-send-statuses": "npx tsx scripts/migrations/2026-07-16-add-stock-purchase-send-statuses.ts",
|
|
|
|
|
"pb:migrate:stock-purchase-problem-fields": "npx tsx scripts/migrations/2026-07-17-add-stock-purchase-problem-fields.ts",
|
perf(orders): server-side tab filter, slim expand payload, single combined count query
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>
2026-08-04 16:17:59 +00:00
|
|
|
"pb:migrate:order-items-item-status-index": "npx tsx scripts/migrations/2026-08-04-add-order-items-item-status-index.ts",
|
2026-08-06 21:01:15 +00:00
|
|
|
"pb:migrate:stock-purchase-booked-fields": "npx tsx scripts/migrations/2026-08-06-add-stock-purchase-booked-fields.ts",
|
2026-08-06 22:53:29 +00:00
|
|
|
"pb:migrate:stock-purchase-items-catalogue-fields": "npx tsx scripts/migrations/2026-08-07-stock-purchase-items-catalogue-fields.ts",
|
2026-08-09 14:17:29 +00:00
|
|
|
"pb:migrate:quotes-ai-status": "npx tsx scripts/migrations/2026-08-08-add-quotes-ai-status.ts",
|
perf(quotes): server-side tab filter, slim expand, combined counts + bonus bugfix
Same shape as the orders refactor \u2014 applied to the quotes page,
with a fix for a long-standing tab-count bug along the way.
# Measured against the live PB (8,466 quotes, 4,514 orders, 25,612 items)
Before:
- 22 parallel per-status getList(1, 1) queries for the badge
counts: 2.6s wall time (no quotes.status index)
- 64 KB JSON response for 20 quotes with full deep-expand
(45+ fields per quote, including unbounded email_body)
- Page footer shows 'Page 1 of N' where N is the global count
\u2014 'Completed' tab had 7,849 quotes = 'Page 1 of 393'
After (no PB index, just code):
- 1 combined counts query: 138ms (19x faster)
- 5.5 KB JSON response (12x smaller)
- Server-side tab filter: page 1 of N reflects the tab's set
- Cancelled tab badge now correctly counts rejected+cancelled
# Server-side read model
- getQuotesDashboardData in src/lib/server/quotes-service.ts
- tab-scoped list + global counts in one round-trip
- tab param drives a server-side filter (status IN (...) for
workflow tabs, relation filter for delivery_note, is_template
for templates, excludeTemplate for 'all')
- pagination reflects the active tab's matching set, not the
global ~8.5k quote count
- getQuoteStatusTabCounts: one combined getFullList with all
17 counted statuses ORed (fields: 'status'), deduped in memory.
Replaces 22 separate per-status scans. Cached 60s on the server
with bypassCache option for future realtime refetches.
- /api/v1/quotes/list thin route: validates tab against
QUOTE_TAB_ORDER, threads tab + nocache params to the service.
# Slim wire payload
- fields param on the orders list: just the columns the page reads
+ per-expand limits (quote.quote_number doesn't need
email_body, company.company doesn't need its address field, etc.)
- Dropped the customer.company 3-level expand (the page never read
expand.customer.expand.company \u2014 only expand.company.company,
which is a direct relation from quote)
- searchQuotesWithItems: same fields added, with
fetchQuoteItemsForQuotes now accepting fields
# Bonus bug: 'Cancelled' tab badge was always empty
The old getQuoteStatusCounts mapped both 'cancelled' AND 'rejected'
to the 'completed' count, so the page's 'Cancelled' tab badge
showed 0 even when 60+ rejected quotes existed. The page's
tabFilters separately grouped them under 'cancelled', so a quote
that was rejected showed up in the list but not the count.
The new QUOTE_TAB_STATUSES in the service layer is the canonical
mapping (cancelled absorbs both statuses). Page constants and
service constants now derive from the same source \u2014 no more drift.
# Index migration
scripts/migrations/2026-08-04-add-quotes-status-index.ts. Idempotent.
The live PB has no quotes.status index today (verified by hitting
/api/collections/quotes). With the index, the combined counts
query drops from 138ms to sub-10ms on the live instance.
# Tests
6 new tests in quotes-service.test.ts:
- QUOTE_TAB_STATUSES contract (cancelled absorbs rejected,
no empty status lists unless special-cased)
- getQuoteStatusTabCounts buckets correctly into tabs
- Cache hit returns the same counts on second call
- bypassCache: true sees new state
- getQuotesDashboardData end-to-end: tab-scoped items +
global counts
- Regression guard for the cancelled/rejected bug
Mock infrastructure upgrade: the quotes-service mock's where
function is now a proper recursive-descent filter parser (handles
nested || inside parens, empty quoted values). getList mock now
returns totalPages.
Full suite: 1312/1312 pass.
Co-authored-by: Orca <help@stably.ai>
2026-08-04 17:01:31 +00:00
|
|
|
"pb:migrate:quotes-status-index": "npx tsx scripts/migrations/2026-08-04-add-quotes-status-index.ts",
|
2026-08-09 14:17:29 +00:00
|
|
|
"pb:migrate:ai-settings": "npx tsx scripts/migrations/2026-08-09-add-ai-settings.ts",
|
2026-06-16 18:46:28 +00:00
|
|
|
"smoke:counter": "npx tsx scripts/smoke/counter.ts",
|
|
|
|
|
"smoke:counter-concurrent": "npx tsx scripts/smoke/counter-concurrent.ts",
|
|
|
|
|
"smoke:create-from-email": "npx tsx scripts/smoke/create-from-email.ts",
|
2026-06-29 11:06:01 +00:00
|
|
|
"smoke:management": "npx tsx scripts/smoke/management-flow.ts",
|
|
|
|
|
"smoke:mcp-management": "npx tsx scripts/smoke/mcp-management.ts",
|
|
|
|
|
"smoke:rest-management": "npx tsx scripts/smoke/rest-management.ts",
|
2026-08-09 14:17:29 +00:00
|
|
|
"smoke:quotes-ai-pipeline": "npx tsx scripts/smoke/quotes-ai-pipeline.ts",
|
|
|
|
|
"smoke:ai-settings": "npx tsx scripts/smoke/ai-settings-round-trip.ts",
|
2026-06-11 18:30:02 +00:00
|
|
|
"test": "vitest",
|
|
|
|
|
"test:run": "vitest run",
|
|
|
|
|
"test:coverage": "vitest run --coverage",
|
|
|
|
|
"e2e": "playwright test",
|
|
|
|
|
"e2e:ui": "playwright test --ui",
|
|
|
|
|
"e2e:install": "playwright install --with-deps chromium"
|
|
|
|
|
},
|
|
|
|
|
"dependencies": {
|
2026-08-09 14:17:29 +00:00
|
|
|
"@anthropic-ai/sdk": "^0.116.0",
|
2026-06-11 18:30:02 +00:00
|
|
|
"@base-ui/react": "^1.5.0",
|
|
|
|
|
"@radix-ui/react-slot": "^1.2.4",
|
2026-06-23 06:30:38 +00:00
|
|
|
"@react-pdf/renderer": "^4.5.1",
|
2026-06-11 18:30:02 +00:00
|
|
|
"@types/leaflet": "^1.9.21",
|
2026-08-09 14:17:29 +00:00
|
|
|
"@types/pdf-parse": "^1.1.5",
|
2026-06-11 18:30:02 +00:00
|
|
|
"class-variance-authority": "^0.7.1",
|
|
|
|
|
"clsx": "^2.1.1",
|
|
|
|
|
"cmdk": "^1.1.1",
|
2026-08-09 14:17:29 +00:00
|
|
|
"csv-parse": "^7.0.2",
|
2026-07-09 07:44:55 +00:00
|
|
|
"jspdf": "^2.5.2",
|
2026-06-11 18:30:02 +00:00
|
|
|
"leaflet": "^1.9.4",
|
|
|
|
|
"lucide-react": "^1.16.0",
|
2026-08-09 14:17:29 +00:00
|
|
|
"mammoth": "^1.12.0",
|
2026-06-11 18:30:02 +00:00
|
|
|
"next": "16.2.6",
|
|
|
|
|
"next-themes": "^0.4.6",
|
2026-07-09 20:28:50 +00:00
|
|
|
"pdf-lib": "^1.17.1",
|
2026-08-09 14:17:29 +00:00
|
|
|
"pdf-parse": "^2.4.5",
|
|
|
|
|
"pdfjs-dist": "^5.4.296",
|
2026-06-11 18:30:02 +00:00
|
|
|
"pocketbase": "^0.26.9",
|
|
|
|
|
"react": "19.2.4",
|
|
|
|
|
"react-dom": "19.2.4",
|
|
|
|
|
"react-leaflet": "^5.0.0",
|
|
|
|
|
"shadcn": "^4.8.0",
|
|
|
|
|
"sonner": "^2.0.7",
|
|
|
|
|
"tailwind-merge": "^3.6.0",
|
2026-08-09 14:17:29 +00:00
|
|
|
"tw-animate-css": "^1.4.0",
|
|
|
|
|
"xlsx": "^0.18.5"
|
2026-06-11 18:30:02 +00:00
|
|
|
},
|
|
|
|
|
"devDependencies": {
|
|
|
|
|
"@playwright/test": "^1.60.0",
|
|
|
|
|
"@tailwindcss/postcss": "^4",
|
|
|
|
|
"@testing-library/dom": "^10.4.1",
|
2026-07-07 23:35:00 +00:00
|
|
|
"@testing-library/jest-dom": "^6.9.1",
|
2026-06-11 18:30:02 +00:00
|
|
|
"@testing-library/react": "^16.3.2",
|
2026-07-11 22:40:41 +00:00
|
|
|
"@testing-library/user-event": "^14.6.1",
|
2026-06-11 18:30:02 +00:00
|
|
|
"@types/node": "^20",
|
|
|
|
|
"@types/react": "^19",
|
|
|
|
|
"@types/react-dom": "^19",
|
|
|
|
|
"@vitejs/plugin-react": "^6.0.2",
|
|
|
|
|
"eslint": "^9",
|
|
|
|
|
"eslint-config-next": "16.2.6",
|
|
|
|
|
"jsdom": "^29.1.1",
|
|
|
|
|
"tailwindcss": "^4",
|
|
|
|
|
"typescript": "^5",
|
|
|
|
|
"vite-tsconfig-paths": "^6.1.1",
|
|
|
|
|
"vitest": "^4.1.8"
|
|
|
|
|
}
|
|
|
|
|
}
|