WFDASH-WEBAPP/package.json

116 lines
6.4 KiB
JSON
Raw Normal View History

{
"name": "wfdash",
"version": "0.1.0",
"private": true,
"engines": {
"node": ">=20.9.0"
},
"scripts": {
"dev": "next dev --webpack",
2026-09-01 22:41:31 +00:00
"build": "next build --webpack",
"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",
"pb:migrate:quote-attachments": "npx tsx scripts/migrations/create-quote-attachments-and-replies.ts",
"pb:migrate:quote-items-user-edited": "npx tsx scripts/migrations/add-quote-items-user-edited.ts",
"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:pos-slip-layouts": "npx tsx scripts/migrations/2026-09-04-create-pos-slip-layouts.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-order-delivery-note-match": "npx tsx scripts/migrations/2026-08-29-add-order-delivery-note-match-fields.ts",
feat(quotes): split attachments into internal vs outbound with operator upload UI Splits the quote attachment model along the seam that actually matters: 'operator-only references' (drawings the operator consulted, supplier emails they filed away) vs 'customer-facing files' (supplier quotes, drawings, NDAs that need to ride along on the email). Schema (PB): new `internal_attachments` + `outbound_attachments` fields on the `quotes` collection, maxSelect 10 each. Existing `attachments` field kept for back-compat but is no longer the source of truth for outbound emails. API: - GET/POST/DELETE /api/v1/quotes/[id]/internal-attachments[/{filename}] - GET/POST/DELETE /api/v1/quotes/[id]/outbound-attachments[/{filename}] - POST /api/v1/quotes/[id]/attachments/upload kept for legacy `attachments` MCP: `quotes.send_email` description now correctly states `selectedAttachmentIds` are PB file tokens from `outbound_attachments` (supplier quotes, drawings, NDAs), not IDs of the legacy `attachments` collection. Frontend: `ProcessOutboundAttachmentsCard` component lets the operator upload/remove customer-facing files during the process page; approval + process pages now read the new fields. Outbound Send workflow (deployed as `PIEAkPYYYZLGl6Pj`): - Old workflow (id `WF_Email_Send_SMTP`) and stale repo file (`WFDASH — Quote Outbound Send.json`) replaced. - New revision intersects `quotes.outbound_attachments` with `email_outbound_attachment_ids`, downloads each from `/api/files/quotes/<id>/<token>`, and emits `options.attachments: [{name, content, type}]` consumed by `Send Email (SMTP)` (PO Send pattern). - Failure path flipped from `send_failed` to `mail_failed` so `retryQuoteEmail` can find failed sends (it only accepts the latter). Previously failed sends were stranded. Email Monitor (v5) workflow fixes: - Reply-branch auth: any authenticated request now passes through; pre-fix bug rejected branches that the IMAP trigger hadn't seen yet. - Reply detection: bulletproof subject regex (/Q-?\d{4}-?\d{2}-?\d{3}/) accepts both legacy `Q-2026-09-246` and current `Q202609246` formats; canonicalised digits-only for PB `~` substring match. Delivery notes: new pricing endpoint at /api/v1/delivery-notes/items/[id]/pricing + service-layer methods. Snapshots kept for forensics: PRE-FIX-REPLY-{BRANCH-AUTH,DETECTION}.json and push-payload.baseline.json are the previously-deployed workflow shapes, retained so we can diff/rollback if regression hits. Verified: - PUT workflow PIEAkPYYYZLGl6Pj -> 200, active, 25 nodes - quotes-service.test.ts: 117 pass (4 pre-existing failures unchanged) - mcp/route.test.ts: 118 pass (2 pre-existing failures unchanged) - node --check on new Code node JS passes Docs: docs/quotes-outbound-attachments-deploy.md covers the deploy procedure; docs/api-mcp-layer.md description sync.
2026-09-09 11:43:56 +00:00
"pb:migrate:quote-attachment-fields": "npx tsx scripts/migrations/2026-09-09-add-quotes-attachment-fields.ts",
"pb:migrate:quote-attachment-fields-maxselect": "npx tsx scripts/migrations/2026-09-09-fix-quote-attachment-fields-maxselect.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",
"pb:migrate:raw-price-list": "npx tsx scripts/migrations/2026-06-30-add-raw-price-list.ts",
"pb:migrate:pricelist-notes": "npx tsx scripts/migrations/2026-06-30-add-pricelist-notes.ts",
"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",
"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",
"pb:migrate:sprint-categories": "npx tsx scripts/migrations/2026-07-02-add-sprint-categories-and-claims.ts",
"pb:migrate:po-pdf-tokens": "npx tsx scripts/migrations/2026-07-09-po-pdf-tokens.ts",
"pb:migrate:po-additional-attachments": "npx tsx scripts/migrations/2026-07-09-po-additional-attachments.ts",
"pb:migrate:stock-line-drv-id": "npx tsx scripts/migrations/2026-07-15-add-stock-line-drv-id.ts",
"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",
"pb:migrate:stock-purchase-booked-fields": "npx tsx scripts/migrations/2026-08-06-add-stock-purchase-booked-fields.ts",
"pb:migrate:stock-purchase-items-catalogue-fields": "npx tsx scripts/migrations/2026-08-07-stock-purchase-items-catalogue-fields.ts",
"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",
"pb:migrate:ai-settings": "npx tsx scripts/migrations/2026-08-09-add-ai-settings.ts",
"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",
"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",
"smoke:quotes-ai-pipeline": "npx tsx scripts/smoke/quotes-ai-pipeline.ts",
"smoke:ai-settings": "npx tsx scripts/smoke/ai-settings-round-trip.ts",
"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": {
"@anthropic-ai/sdk": "^0.116.0",
"@base-ui/react": "^1.5.0",
"@radix-ui/react-slot": "^1.2.4",
"@react-pdf/renderer": "^4.5.1",
"@types/leaflet": "^1.9.21",
"@types/pdf-parse": "^1.1.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"csv-parse": "^7.0.2",
"jspdf": "^2.5.2",
"leaflet": "^1.9.4",
"lucide-react": "^1.16.0",
"mammoth": "^1.12.0",
"next": "16.2.6",
"next-themes": "^0.4.6",
"pdf-lib": "^1.17.1",
"pdf-parse": "^2.4.5",
"pdfjs-dist": "^5.4.296",
"pocketbase": "^0.26.9",
"react": "19.2.4",
"react-markdown": "^9.0.1",
"remark-gfm": "^4.0.0",
"react-dom": "19.2.4",
"react-leaflet": "^5.0.0",
"shadcn": "^4.8.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.6.0",
"tw-animate-css": "^1.4.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@tailwindcss/postcss": "^4",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@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"
}
}