Commit graph

471 commits

Author SHA1 Message Date
Dave
47d58db0c9 feat(mcp): add price_list.add_items tool for agent-driven catalogue upsert
Add a new MCP tool that lets the agent read a supplier PDF, quote PDF,
or pasted supplier email with its native vision, extract the per-line
fields, and commit them to the price_list catalogue in one round-trip.

Why: the only pre-existing path was operator upload via /pricelist ->
n8n ingest -> raw_price_list -> AI extraction -> price_list. That flow
is async, human-in-the-loop, and gates on n8n being healthy. This
tool gives the agent a synchronous shortcut for ad-hoc catalogue
updates -- paste a PDF, get the catalogue updated, no async hops.

The agent does the OCR; the tool does the commit. No LLM work moves
into the MCP server.

Behaviour:

- Dedupe by (sku, supplier) -- exact match. Different suppliers with
  the same SKU get separate rows.
- Upsert policy: cost_price overwrites (latest price wins).
  Description, selling_price, unit, category, subcategory, brand,
  markup, stock_qty, search_text, sku_normalised also patched in
  place on update.
- sku_normalised auto-computed (uppercase, strip non-alphanumeric --
  same canonical form price-list-match.ts uses).
- search_text auto-computed (lowercased, space-separated concat of
  sku + description + brand + supplier + category + subcategory +
  synonyms).
- Skipped items returned with per-item reason (missing_sku,
  missing_supplier, invalid_cost_price, pb_write_failed). Never
  silent.
- Bulk cap 500 items per call; larger lists rejected with
  too_many_items reason.
- source_file_id + source_label supported so new rows can be
  traced back to a raw_price_list upload (optional).
- Supplier stored verbatim on price_list.supplier (free-text field,
  no FK resolution -- same as the live schema).

Files:

- src/lib/server/price-list-service.ts (new) -- addPriceListItems +
  buildSearchText + computeSellingPrice helpers
- src/lib/server/price-list-service.test.ts (new) -- 17 tests
  covering validation, upsert, dedupe filter shape, normalisation
- src/app/api/mcp/route.ts -- new tool definition + dispatch block
- src/app/api/mcp/route.test.ts -- 2 dispatch tests + tool-list
  assertion update
- .agents/skills/wfdash-giellie/SKILL.md -- new section "8. Price-
  list catalogue" documenting input/output shapes + failure modes

Tests:

- 134/134 pass across the affected files
- 1775/1778 in the full suite (3 pre-existing failures unrelated
  to this change: pocketbase-admin.test.ts PB mock mismatch,
  quotes/items/[id]/attachments/upload/route.test.ts timeout,
  supplier-form.test.tsx race -- all reproducible on main)
- npm run build clean
2026-08-10 12:45:05 +02:00
Dave
e9ffa6df4e feat(quotes/process): add mobile card layout for quote items
Replace the cramped horizontal table layout on mobile viewports with responsive vertical cards for each quote item. The new `ProcessItemCard` component provides full-width form fields, properly sized 44px tap targets for all actions, and maintains full parity with the desktop table implementation by reusing the same state handlers and callbacks. Add comprehensive test coverage for the new component, and fix existing mobile overflow issues in the quote process page header and table layout.
2026-08-10 10:47:36 +02:00
Dave
d9c60aff6d feat: add split views, stock picking slips, and invoice viewer
Comprehensive changes including:
- Refactor picking slip line handling for multiple document types across print endpoints
- Add new `/api/v1/stock-purchases/picking-slip` API for stock booking picking slips
- Implement reusable Splitter component and useSplitRatio hook for resizable panels
- Add floating/fullscreen invoice viewer to collection confirm page
- Add real-time stock booking update event for invoicing tab
- Add full test coverage for all new components and APIs
- Include temporary chat attachment images for testing purposes
2026-08-09 23:16:55 +02:00
Dave
9eef1fbd56 feat(mcp): add partial=true flag to PO invoice matching for buyer-side mark-and-flip
When the supplier ships a partial invoice (BREATHER on PO26080502STK was
not invoiced this round, but the other 7 lines were), the buyer wants
to:
  - mark and flip what we can confirm came in
  - leave the rest for the buyer to reconcile manually

Adding a new 'partial: true' flag to stock_purchases.match_and_receive_from_invoice
that:
  - widens the verdict gate to allow 'unmatched_po_lines' and 'cost_mismatch'
    to proceed when partial=true (the buyer is explicitly accepting these)
  - passes per-line quantity_received:0 overrides for unmatched PO lines
    into the existing receiveStockPurchase(items) override path, so only
    matched lines get booked into stock_ledger
  - surfaces 'skipped_po_line_ids' in the response so the buyer sees
    exactly what was held back
  - leaves the customer-PO path unchanged (every matched line is stamped,
    partial semantics only apply to stock purchases for now)

Hard stops regardless of partial: qty_mismatch on matched lines,
unmatched_invoice_lines, supplier_mismatch, wrong_status.

Test coverage: +1 dispatch test (partial flag forwarding) plus the
existing 'partial: false' assertion on the dry-run dispatch test.

Live-validated on PO26080502STK + invoice WR248939 (G-Man #1, 7 lines):
- 6 matched CIRCLIP/KM/MB lines booked, qty_received bumped
- BREATHER 3/4 line skipped via qty_received=0, surfaced in
  skipped_po_line_ids for manual reconciliation
- PO status flipped sent -> received
2026-08-09 20:41:27 +02:00
SJCoetz
87cead1bcf feat(mcp): expand stock purchase invoice matching service with 5 phases
Build out the buyer-side PO reconciliation workflow that landed in
60d3204 ('stock purchase invoice reconciliation tool') into a full
3-document × 2-PO-kind matching service. The tool now covers every
real-life supplier document the buyer can receive.

Renamed the service entry point from matchAndReceiveFromInvoice to
matchAndReceiveFromPoDocument (old name kept as a back-compat alias).
The MCP tool name stays stock_purchases.match_and_receive_from_invoice
so existing callers don't break; the input arg shape changed from
'invoice:' to 'document:' (old 'invoice:' arg still accepted for
back-compat).

Phase 1 — supplier_sku backfill (prep work, not in service code)
Phase 2 — auto-detect stock vs customer POs (single tool, both kinds)
Phase 3 — GRN (delivery-note) mode via new mode: 'grn' flag
Phase 4 — pack-size inference (Orapi case: per-unit vs per-pack)
Phase 5 — PO reference normalisation (PO202608017 vs PO-2026-08-017)

Also adds 3 new MCP tools for the collection-confirm buyer workflow:
collection_confirm.list, collection_confirm.upload_invoice,
collection_confirm.confirm_order_items.

Docs: updated .agents/skills/wfdash-giellie/SKILL.md and
src/app/api/mcp/FEATURE-REQUESTS.md to describe the new modes and
shape. 1,661/1,664 vitest tests pass; the 3 failing tests are
pre-existing (PocketBase SDK mock mismatch in pocketbase-admin.test,
5s timeout in quotes/items/[id]/attachments/upload/route.test —
both reproducible on main without this change).
2026-08-09 20:23:36 +02:00
SJCoetz
f88f451d6e fix(invoicing): print bundle fails on production due to self-fetch
The /api/invoicing/print-bundle route was doing a self-fetch through
its own public URL to download the invoice PDF it needed to bundle:

  fetch(`${origin}/api/v1/files/order_items/${id}/${token}`)

That works on local because Node inside the container can hit
localhost:3000 as a loopback. On production (Coolify / Docker) the
container's fetch to its own public hostname fails — DNS resolves to
an external IP that doesn't route back, the outbound TCP is blocked,
or the TLS handshake fails. Node's fetch throws 'fetch failed' at
that point, which bubbles up to the browser as a 500 toast.

Fix: fetch the invoice PDF directly from PocketBase with admin auth,
the same pattern /api/v1/files/... uses internally and that
delivery-note-pdf already uses. Drop the now-unused 'origin'
parameter from handlePrintBundle and the GET/POST wrappers.

Tests updated to mock pb.files.getURL + pb.authStore.token and assert
the new PB file URL + Authorization header.

All 65 tests across the print-bundle, picking-slip, view-model, and
invoicing page suites pass. The 2 pre-existing pocketbase-admin.test
failures are unrelated (reproducible on main without this change).
2026-08-09 20:23:26 +02:00
SJCoetz
60d320411d feat(mcp): add stock purchase invoice reconciliation tool
Add the full buyer-side supplier invoice reconciliation workflow:
- Include the new `stock_purchases.match_and_receive_from_invoice` MCP tool for deterministic PO-invoice matching
- Create core matching service with strict, rule-based line matching (sku → supplier_sku → description overlap)
- Support dry-run default mode to validate matches before committing
- Add automatic attachment handling with standard `supplier_invoice_*` naming convention
- Add full test suite for the matching logic
- Update documentation and agent skill profile to document the new tool and workflow

Pre-existing test failures (3 total) are unrelated to this change.
2026-08-09 16:59:44 +02:00
SJCoetz
63048e51c9 feat: add AI quote processing pipeline and admin settings UI
Add complete end-to-end AI-powered quote extraction workflow:
- Disable PB SDK auto-cancellation to support parallel batch requests
- Add required runtime dependencies for AI processing and document parsing
- Implement AI settings backend, operator UI, and REST API routes
- Add quote ingestion, insight population, and manual re-run AI endpoints
- Build service layers for quote items, AI settings, and structured item insights
- Fix attachment upload handling and quote item history blob parsing
- Add supporting migrations, smoke tests, and shared utility functions
- Export previously private helper functions for cross-service reuse
2026-08-09 16:17:29 +02:00
SJCoetz
b52de77f8c fix(sprint): filter stale quote items, add cleanup migrations
- Update sprint page query to only show unprocessed items from active (new/claimed) quotes, plus orphans
- Add one-shot migration to mark existing stale unprocessed quote items as processed
- Add one-shot migration to fix duplicate Q202608235 quote number entries
- Refine comments in the sprint page component for better maintainability
2026-08-07 20:15:46 +02:00
SJCoetz
30cc4fc5d2 Merge feat/order-item-context-block: replace AI Insights with deterministic context block
Co-authored-by: Orca <help@stably.ai>
2026-08-07 07:17:35 +02:00
SJCoetz
2de8cf1a56 chore(context): lint + build adjustments
Co-authored-by: Orca <help@stably.ai>
2026-08-07 03:04:28 +02:00
SJCoetz
f5185d3a2c feat(context): swap orders-page AI history ⓘ for context popover
Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:59:27 +02:00
SJCoetz
995b7ed675 feat(context): swap sprint modal AI Insights for context panel
Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:58:47 +02:00
SJCoetz
c5fbb7422f test(context): OrderItemContextPopoverTrigger UI tests
Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:55:39 +02:00
SJCoetz
f75c1b2bbd feat(context): OrderItemContextPopoverTrigger (ⓘ on orders page)
Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:55:14 +02:00
SJCoetz
b3a73c0040 test(context): OrderItemContextPanel UI tests
Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:54:24 +02:00
SJCoetz
efd2ad6540 feat(context): OrderItemContextPanel (inline expandable, 4 rows)
Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:53:21 +02:00
SJCoetz
5291fd2fd1 test(context): /api/v1/order-items/[id]/context route tests
Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:50:44 +02:00
SJCoetz
7ad7375dd2 feat(context): GET /api/v1/order-items/[id]/context route
Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:46:18 +02:00
SJCoetz
351c1b2b96 test(context): getOrderItemContext unit tests (null, quote, stock low/out, invoiced)
Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:42:57 +02:00
SJCoetz
ced25be47a feat(context): implement getOrderItemContext (5 PB reads, fuzzy fallback)
Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:34:45 +02:00
SJCoetz
18afe7d2d9 feat(context): add order-item-context-service skeleton + types
Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:23:45 +02:00
Claude
357decbb2b docs(plan): order-item context block — 12 TDD tasks
Implementation plan for the spec at
docs/superpowers/specs/2026-08-07-order-item-context-block-design.md.

Tasks: backend service skeleton + impl + tests, /api/v1/.../context
endpoint + tests, OrderItemContextPanel + tests,
OrderItemContextPopoverTrigger + tests, sprint modal wire-in, orders
page wire-in, full test suite + build smoke.

Each task follows a TDD cycle: write failing test, run to confirm,
implement minimal code, run to confirm pass, commit.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:18:28 +02:00
Claude
3f82b0784d docs(spec): order-item context block (sprint modal + orders page)
Replaces the AI Insights section in order-sprint-process-modal with a
deterministic PocketBase-derived context block (last quote, last order,
last invoiced, stock). Same backend exposed as a popover trigger on
the orders detail page, replacing the existing AI-history ⓘ there.

Backend: new src/lib/server/order-item-context-service.ts +
GET /api/v1/order-items/[id]/context. Reuses pure helpers from
item-history-service (normaliseSkuForHistory, findBestDescriptionMatch,
aggregateSuppliers, computeCheapest).

UI: new <OrderItemContextPanel/> (inline expandable for the sprint
modal) and <OrderItemContextPopoverTrigger/> (ⓘ popover for orders
page). Apply = cost + supplier only; selling stays locked to the order.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-07 02:16:31 +02:00
SJCoetz
f35779369a fix(stock-purchase-items): drop dead drv_collected_at write to unblock build
The markLineCollected service and the stock-orders page's
handleAssignDriver (delivery-sentinel auto-collect path) both
wrote `drv_collected_at` to `stock_purchase_items`, but that
field exists on `order_items`, not on `stock_purchase_items`
(verified against pb_schema-2.json). Per AGENTS.md "select
fields are not self-service", PB would 500 both writes at
runtime. The TS error from the service was the build break
flagged in the 2026-08-06 23:34 deployment.

The page.tsx write was a latent runtime bug — TypeScript
never caught it because PB's loose `update` patch typing
accepts any field name. Both writes are removed; the
collection moment is captured by PB's `updated` timestamp on
the line row, which is sufficient for the auto-collect audit
trail. Inline comments point to FEATURE-REQUESTS as the
follow-up if a dedicated collection-date field is wanted
later (would need a PB admin schema change).

Also drops the matching test assertions that read
`drv_collected_at` off the response and replaces them with
a comment explaining the absence.

Tests: vitest 59/59 across stock-purchase-items-service +
stock-orders/[id]; npm run build compiles clean
(Next.js 16.2.6 Turbopack + TypeScript).

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-07 01:39:03 +02:00
SJCoetz
fe8a3c6872 feat: rework invoicing stock booking section
Move StockBookingSection from above the tab strip into the To Invoice tab's card list for consistent visual styling. Add expandable, lazy-loaded purchase order line items with session-based caching to reduce redundant network requests. Update component UI, styling, and documentation, and fix test mocks to prevent conflation of stock_purchases and order_items collection calls. Add ZAR currency formatting helper for displaying stock line item costs.
2026-08-07 01:07:56 +02:00
SJCoetz
c4125ee40a feat: add split-invoice stock PO workflow and supporting tooling
Ship full split-invoice stock purchase order workflow:
- Add 5 new MCP tools for per-line stock purchase item management and catalogue updates
- Add idempotent database migration adding sku, unit, and supplier_sku fields to stock_purchase_items
- Implement service layers for all new operations, including auto-closing the parent PO when all lines are fully collected
- Add complete test coverage for new services and API routes
- Update all relevant documentation including MCP API specs, agent instructions, and feature request logs
2026-08-07 00:53:29 +02:00
SJCoetz
d167f81ad5 feat(stock-orders): inline cost_price editing on lines table
Co-authored-by: Orca <help@stably.ai>
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-06 23:36:27 +02:00
SJCoetz
d5ed390de4 feat(pb): add stock_purchases.additional_attachments file field (migration)
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:32:12 +02:00
SJCoetz
bbfe20ec2d docs: register stock_purchases.add_attachment in feature backlog + API reference
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:29:55 +02:00
SJCoetz
dd57d29de3 feat(stock-orders): attachments card on detail page
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:29:00 +02:00
SJCoetz
f89bbb3a35 feat(mcp): add stock_purchases.add_attachment tool
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:24:00 +02:00
SJCoetz
ccf04bc39f feat(api): add stock-purchase attachments delete route
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:21:25 +02:00
SJCoetz
a72c5ca0a7 feat(api): add stock-purchase attachments upload route
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:21:00 +02:00
SJCoetz
70c158b374 feat(stock-purchases): add additional_attachments service helpers
Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:20:26 +02:00
SJCoetz
9e118a3b5f feat(stock-purchase): split warehouse confirm from accountant booking
Repositioned booking responsibility: warehouse confirms physical
collection only (records quantity_received + flips header to
"received"); the accountant books into inventory via the new
invoicing flow once the supplier invoice is processed. This way
the payment-cycle reasoning lives at the booking step, not at
receipt.

Changes
- Migration: add booked_at + booked_by to stock_purchases
- Service: bookStockFromInvoice (accountant-side, splits from
  bookStockRows)
- API route: POST /api/v1/invoicing/stock-book
- UI: stock rows surfaced on invoicing page (StockBookingSection)
- UI: stock-purchase detail page
  - Driver picker on header (sentinel "delivery" user auto-collects
    every line, real drivers set assigned_to_driver)
  - "Confirm collection" replaces "Mark received" (warehouse-side
    only — no inventory writes)
  - Cancel button with confirmation dialog
- collection-confirm: confirmStockRows stripped of inventory writes;
  the prior /api/v1/collection-confirm/book-stock route was renamed
  to /confirm-stock and is now strictly the warehouse-side confirm
- Tests for all new flows (5/5 passing on the stock-order detail
  page; 10/10 on StockBookingSection)

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 23:01:15 +02:00
SJCoetz
df87094eb9 chore(playwright): add 2026-08-06 test artifacts
Add captured Playwright page state snapshots and console logs from the 2026-08-06 test session, stored in the .playwright-mcp directory.
2026-08-06 19:38:39 +02:00
SJCoetz
bf54511db1 fix(buyer-tasks): make card layout mobile-friendly
At <=640px the buyer-tasks card content was crushed by the action
column sitting beside it: the PO number badge broke mid-string
('PO-2026-08-042' wrapping to 4 lines), supplier names wrapped to
6 lines, and the new 'Sent to ...' caption overlapped the PO
badge on the PO Sent tab.

Three responsive tweaks, all using Tailwind breakpoint utilities:

1. Outer card container switches from 'flex items-start' to
   'flex flex-col sm:flex-row sm:items-start'. The action column
   drops below the info column on mobile, giving the info all the
   available width and the buttons full-width tap targets.

2. PO number badge gains 'whitespace-nowrap'. The badge stays
   atomic; the parent's 'flex-wrap' carries the whole badge to a
   new line when it doesn't fit.

3. PO Sent right column: buttons stack vertically on mobile
   ('flex-col sm:flex-row' + 'flex-1 sm:flex-none' on each
   button) and the caption switches from 'text-right' to
   'text-left sm:text-right' so it sits under the buttons.

Desktop layout is unchanged — verified at 1280px that the
side-by-side buttons, right-aligned caption, and inline single
button all still render exactly as before.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 18:21:38 +02:00
SJCoetz
d2ffbea6bc feat(buyer-tasks): show recipient + send date on PO Sent card
Adds a small muted caption under the Re-Send / Mark as ready buttons
on the PO Sent tab showing the email address the PO was sent to and
the send timestamp (en-GB short format like '5 Aug 2026, 14:23').

Data sources come from the existing BuyerTaskGroup.emailPayload
exposed in commit f61c173 — no new PB fields, no new types. The
caption only renders when sentAt is present, so legacy rows (or a
fresh draft flipped to po_sent without a successful send) stay clean.

Visible on mail_failed too: the operator can see who we tried to
reach and when, alongside the existing 'Mail failed' banner.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 18:16:22 +02:00
SJCoetz
f3052bfd40 Merge branch 'feat/buyer-tasks-resend-task1'
Adds a Re-Send button next to Mark as ready on the PO Sent tab of the buyer-tasks page. The button opens the existing SendPoModal in resend mode, pre-filled from the PO's previous email_payload (to/cc/bcc/subject/body). Also migrates the hardcoded purple Mark-as-ready color to the themed primary token, and fixes the mail-failed 'Resend' label that previously opened the wrong dialog.
2026-08-06 18:11:59 +02:00
SJCoetz
add7ab3302 fix(send-po-modal): seed default body when resend has no prior email_payload
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 18:10:08 +02:00
SJCoetz
abdc3a4d9f fix(send-po-modal): preserve previous message body on resend (don't overwrite with default template)
Co-authored-by: Orca <help@stably.ai>
2026-08-06 18:06:22 +02:00
SJCoetz
f61c173a1a fix(buyer-tasks): surface PO email_payload + attachments on group for resend modal prefill
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 18:01:58 +02:00
SJCoetz
6008f17c9a feat(buyer-tasks): add Re-Send button on PO Sent tab + theme-token cleanup
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-06 17:43:17 +02:00
SJCoetz
ff94086ec0 docs(plan): add implementation plan for Re-Send button on PO Sent tab
Co-authored-by: Orca <help@stably.ai>
2026-08-06 17:34:28 +02:00
SJCoetz
3a954982d0 docs(spec): add Re-Send button on PO Sent tab of buyer-tasks
Co-authored-by: Orca <help@stably.ai>
2026-08-06 17:33:25 +02:00
SJCoetz
7f477ca949 fix: resolve PO bug, add sku, improve webhooks
Add `sku` to the order_item.patch MCP tool whitelist and update its documentation
Fix stuck stock POs in buyer tasks po_sent tab by filtering out downstream statuses
Rewrite margin approval webhook to use fire-and-forget dispatch to avoid UI blocking, add server-side error logging
Add MCP feature requests and local setup documentation
2026-08-06 13:13:57 +02:00
SJCoetz
4a69b57ec2 docs(pdf): remove stray duplicate 'from quoteSubtotal() in two ways:' line
Co-authored-by: Orca <help@stably.ai>
2026-08-06 12:22:22 +02:00
SJCoetz
68dcdd565d chore(pdf): silence unused-vars on intentional totalAmount destructure
Co-authored-by: Orca <help@stably.ai>
2026-08-06 12:21:00 +02:00
SJCoetz
180288e6e3 test(pdf): add render smoke tests for PO PDF (covers VAT totals)
Co-authored-by: Orca <help@stably.ai>
2026-08-06 12:13:52 +02:00