Commit graph

48 commits

Author SHA1 Message Date
d3132ab7f6 feat(collection-confirm): collected stock orders go through Collection Confirm
The driver app records the full quantity as received when it collects a
stock pickup, and Collection Confirm only listed stock lines that were
not fully received — so collected stock orders skipped it and sat at
"sent", outside the accountant's booking queue, until someone clicked
Mark received (29 stuck on 2026-10-19).

- STOCK_ON_PAGE_FILTER / stockLineOnPage: a collected stock line stays on
  Collection Confirm until its stock order is received; cancelled orders
  never show. Shared by the list and the AI check (so those lines get
  photos read and ticks too).
- Confirm stock: markStockOrdersReceived flips each stock order to
  "received" once every line is collected/booked (→ booking queue);
  orders with lines still out stay open, reported as stillOpen. The
  route returns { updatedIds, received, stillOpen }; the toast says so.
- Stock photos: `npm run pb:migrate:stock-photos` makes
  stock_purchases.additional_attachments multi-file;
  addStockPurchaseAttachment, Move photo and Delete photo add/remove with
  `+`/`-` instead of replacing the single slot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 17:03:33 +02:00
f017a7f985 fix: number customer POs one at a time (duplicate PO numbers)
PO numbers were "read the month's highest, add one", so POs for several
suppliers on one order created together got the same number — 34 numbers
by 2026-10-19 (e.g. PO202610008LIF for BarloWorld CAT and John Ratcliffe,
created 71 ms apart).

- createPurchaseOrderWithNumber (po-number.ts): numbers and creates POs
  one at a time in the server, retries (up to 5) when the unique index
  rejects a clash; ignores any po_number passed in.
- buyer-tasks / MCP generate_po use it.
- The Process page's Send PO and in-stock PO buttons now call
  POST /api/v1/orders/[id]/purchase-orders (browser session) instead of
  numbering in the browser; the client generatePONumber is removed from
  pb-helpers.
- npm run pb:migrate:po-number-unique adds a partial unique index on
  purchase_orders.po_number for POs created from then on; the 34 existing
  duplicates are left as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:08:12 +02:00
16cf5e2746 feat(collection-ai): card check, tick and list passthrough
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 02:14:53 +02:00
e6cac6e447 feat(collection-ai): data model, settings, migration
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 01:18:10 +02:00
f0e7848eed feat(quote-assistant): data model, settings fields, migration
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 19:07:52 +02:00
7256de8859 feat(pos): refund a whole sale, put the goods back, print a credit slip
A customer who brings goods back needs the sale reversed: the items go
back into stock and the money comes back off the till. There was no way
to mark a POS sale as refunded, so nothing stopped the same sale being
refunded twice — which credits the customer twice and puts the same
stock back on the shelf twice.

Refunds are WHOLE-SALE only by operator decision; no partial refunds.
That is what lets `pos_sales.refunded` be a boolean instead of a
per-line refund table. Three columns: `refunded`, `refunded_at`,
`refund_amount`, stamped from the sale's post-discount total, which is
what the customer actually paid.

DOUBLE-REFUND GUARD, twice over, because this is the one failure that
costs real money:

  - `refunded` is read before anything else, and a flagged sale returns
    409 with no stock writes at all
  - the flag is stamped BEFORE any stock write, so a second concurrent
    tap sees it and bails. Writing stock first and flagging last would
    let both taps through.

A stock write that fails part-way does NOT roll the flag back. The
customer has their money either way, so re-running would double-credit
them; the failed lines come back in `stockErrors` for a manual count
correction, which is strictly better than a second refund.

Stock needs no schema: `return` was already in STOCK_REASON_VALUES,
just never used. Rows are written with POSITIVE change_qty and
reference_type "pos_sale_refund", leaving the original
customer_fulfillment rows untouched so the sale's own audit trail still
shows what was originally rung up.

cashup.html had expected drawer = sum of cash sales, so a cash refund
would have shown as a false variance on every cash-up after it. It now
subtracts cash refunds, and refunded sales contribute nothing to
takings for any tender.

The tender is read off the sale, never off the request — an earlier
version accepted it in the body, which would have let a caller assert
that a cash sale was really an EFT and talk the drawer out of balance.

At the till: tap still reprints, hold a sale row for 600ms to refund —
moving money sits behind a gesture you cannot do by accident. Refunded
sales stay in the list, dimmed and badged, because deleting them would
destroy the audit trail. The sheet names the items and says plainly what
happens next ("Hand back R10.57 in cash. The drawer count for this day
will drop by that amount.").

The credit slip reuses the sale renderer and the whole printer path with
`refund: true`, so there is no second printer to configure — but note
there were THREE render paths to fix (on-screen, Bluetooth text, ePOS
XML), and missing the third would have printed "TOTAL" on a credit note.

Migration applied to live PB and verified idempotent; field shapes
round-tripped on a real sale and restored. 10 new tests, 148 POS tests
pass, tsc clean, build passes. sw.js v34 -> v35 (history, cashup,
receipt and epos-print are all cache-first in the shell).
2026-10-07 23:41:09 +02:00
a499265b26 feat(pos): raise a system delivery note on an account sale
An account sale at the till used to produce a delivery-note-shaped slip that
was rendered entirely on the device and printed on the 80mm thermal printer.
Nothing reached the server, so the paperwork the client signed had no record
behind it. Cash / card / EFT are untouched and still get the thermal slip.

An account sale now creates a real `delivery_notes` record. The DN is raised
server-side INSIDE `checkoutSale` rather than by a follow-up call from the
till, because the offline queue (`flushQueue`) re-posts `/checkout` and
nothing else — a client-side second call would be silently dropped for every
sale taken while the network was down, which is exactly when a till is
flakiest.

`pos_sales.delivery_note` is the idempotency anchor that makes it
exactly-once: on a replay it hands back the existing DN instead of making a
second one, and a sale whose first attempt left the field blank retries. It
self-heals.

Deliberately non-fatal: if the DN can't be created the sale still succeeds.
The money and the stock movement are real by then; failing the checkout would
leave the drawer disagreeing with the stock ledger. The till is told instead.

Also in here:
  - DN number collision retry. The generator reads the highest existing number
    and adds one, which is not atomic, while PB holds a unique index on it. The
    POS adds a second concurrent writer, so two creates could collide and 500
    on a benign condition.
  - GET /api/v1/pos/sales now expands customer, customer.company and
    delivery_note so history.html can rebuild the DN card on a reprint. The
    nested customer.company matters — customer.company is itself a relation,
    so without it the card printed a raw PB id where the name belongs.
  - "Print slip" is hidden for account sales; that tender produces a delivery
    note, not a slip.

New GET /api/v1/pos/delivery-note/[number]/pdf streams the A4 original behind
assertPosAuth, so the till's bearer key stays its only credential. It does not
point at the CRM's unauthenticated invoicing route.

Verified end to end against a live note: DN26100401, correct company, both
lines with pricing, PDF attached, stock decremented once, and a replay of the
same client_ref returned the same DN without a duplicate.
2026-10-04 13:15:29 +02:00
b35159d859 feat(giellie): chat store and giellie_chats migration
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-30 23:44:55 +02:00
7555196166 feat(audit): implement audit trail for delivery notes and orders
- Added an audit trail feature that logs every create, update, and delete action on delivery notes and orders, capturing detailed changes per field.
- Introduced the `AuditTimeline` component to display the history of changes for delivery notes and orders, enhancing traceability.
- Updated the `page.tsx` files for delivery notes and orders to fetch and display audit logs, ensuring users can view the history of modifications.
- Enhanced the `package.json` with a new migration script for setting up the audit log functionality.

This feature improves accountability and transparency in the management of delivery notes and orders.
2026-09-30 01:48:44 +02:00
cd359b75a9 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 13:43:56 +02:00
1bd659869d chore(pos): add pos_slip_layouts PB collection for the slip layout editor
One row per letterhead, holding the reorderable/toggleable block
list for each thermal slip (sale, delivery note, cash-up) plus the
delivery note's default pricing toggle. Migration already run
against the live PocketBase instance.
2026-09-04 12:46:17 +02:00
aed3f5c861 feat: implement discount functionality in POS system
Added support for discounts in the POS checkout process, allowing operators to apply either a fixed amount or a percentage off the gross total. Updated the state management to include discount details and modified the receipt and cart rendering to display pre- and post-discount values. Enhanced the checkout API to handle discount inputs and ensure proper calculations. Updated relevant tests to cover new discount logic.
2026-09-03 12:19:21 +02:00
e0d1356da5 feat(orders): add delivery-note repeat-order match fields
Five new fields on the `orders` collection:

- potential_delivery_note (relation → delivery_notes, single) — the
  DN whose items exactly matched this order's items
- potential_delivery_note_number (text) — denormalized copy of the
  matched DN's delivery_note_number, written alongside the relation
  so the orders list can render the number without a second `expand`
  (PB issues a separate IN query per expanded relation —
  orders-service.ts:253 documents why this is avoided here)
- delivery_note_match_confidence (number) — always 1 when set, kept
  for shape parity with quote_match_confidence
- delivery_note_match_method (text) — 'exact_items' or 'no_match',
  text not select for the same forward-compatibility reason
  quote_match_method is text
- delivery_note_matching_attempted (bool) — audit flag

Idempotent — each step detects its own 'already done' state.
Verified by re-running after the first apply.

Plan-staleness fix: PB stores relation options FLAT at the top
level of the field object (collectionId / cascadeDelete /
minSelect / maxSelect), not nested under an `options` key. The
plan's snippet had the nested form and PB rejected it with
'fields.N.collectionId Cannot be blank.' Verified the correct
shape against the live orders.potential_quote field before fixing.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 12:57:16 +02:00
Dave
437ed3529c feat(collection-confirm): implement confirmation dialog for stock collection
- Introduced a new `ConfirmStockDialog` to handle stock collection confirmations, allowing operators to review per-line quantities and any short lines before finalizing the action.
- Updated the `CollectionConfirmPage` to manage the new confirmation state and trigger the dialog upon clicking the "Confirm collection" button.
- Enhanced tests to cover the new dialog functionality, ensuring correct behavior for both full and short quantity lines during the confirmation process.
- Improved documentation to clarify the flow and user interactions with the new confirmation dialog.
2026-08-17 11:41:55 +02:00
Dave
3b8bc0fc35 feat(delivery-notes, migrations): add tabs, grouping, markup, and fix DN relations
- Add shared env loader for migration scripts to load .env and .env.local files at module init
- Add migration script to fix broken delivery note relation fields and backfill missing company data
- Update delivery notes list page: add status filter tabs, company-based collapsible grouping, increased fetch limit, and improved global search
- Add per-item cost price, markup calculations, and total summary rows for cost, selling price, markup, and margin to delivery note detail pages
2026-08-14 09:41:14 +02:00
Dave
397df8e2b2 fix: correct quote expiry handling for PocketBase's 0 default
PocketBase returns 0 for unset number fields, which broke the original
null-coalescing check in effectiveValidityDays, leading to PDFs showing
"Valid until: today" for all new quotes created via email ingest.

Changes include:
- Update effectiveValidityDays to treat 0, negatives, null/undefined as falling back to 30-day default
- Add test cases for 0 and negative validity_days inputs
- Explicitly set validity_days to QUOTE_VALID_DAYS when creating new quotes
- Add one-time idempotent migration script to backfill legacy invalid validity values
- Refactor item intel popover patch logic to use PocketBase SDK for authenticated requests
- Add n8n workflow for AI-powered item insight matching
2026-08-11 08:32:17 +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
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
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
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
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
db1e7d6f76 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 19:01:31 +02:00
SJCoetz
720f01e5b6 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 18:17:59 +02:00
SJCoetz
b84aefc2e2 feat: unify buyer task flows by supporting both customer orders and stock purchases in mark-ready API and UI, and fix PDF table layout responsiveness. 2026-08-03 18:52:30 +02:00
SJCoetz
7a68cafe35 chore: add logos, opencode config and build assets
This commit includes the temporary Next.js build cache files, public brand
logos, and project configuration added in this change.
2026-07-27 08:21:20 +02:00
SJCoetz
3d3a012174 fix(api,layout): sprint 500 chunk + add 9 missing top-level agent routes + metadataBase; drop unused collection_pool feature; quote counter fix migration
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.
2026-07-26 22:18:49 +02:00
SJCoetz
d306b75bdb feat(stock-purchases,driver-app): add stock purchase workflow and driver pickup support
Add complete stock purchase order workflow including:
- Status tracking with sending/mail_failed states and UI labels
- Server-side PDF rendering and document storage
- File proxy API support for stock PO attachments
- Send outcome webhook callback for email notifications

Add driver app support:
- Unified stop list API for both customer deliveries and stock pickups
- Status update endpoint for collected quantities and problem reporting
- Collection map page support for stock pickup locations

Include matching PocketBase migrations, type definitions, and stock inventory fulfillment logic.
2026-07-18 22:43:50 +02:00
SJCoetz
71f7671e89 feat: add stock line support to collection pool
Add full integration for stock supplier pickup lines into the collection pool workflow:
- Add two idempotent database migrations for stock line drv_id and state tracking
- Update stock purchase number format to POyyMMddNNSTK convention
- Extend collection pool to load both customer order items and stock purchase items
- Add stock-specific UI, driver assignment flow, and mark collected action
- Update type definitions and service logic for stock purchases and line items
- Add comprehensive test coverage for all new workflows
2026-07-16 20:41:42 +02:00
SJCoetz
e9cf8aaf04 feat(stock): implement stock management workflow
This commit adds the complete v2/v3 stock management functionality:
- add new stock-specific item statuses and update the status visual map
- implement full stock purchase API suite (create, list, retrieve, send, receive)
- reorganize sidebar navigation to group stock-related pages together
- add stock item CRUD APIs, status utilities, low stock filtering, and surplus booking logic
- add database migration for the `stock_items.target_qty` field
- update type definitions and add comprehensive tests for all new stock components
2026-07-15 21:53:20 +02:00
SJCoetz
cdaa2e2b25 fix, feat, style: standardize quote numbers, polish UI
- 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
2026-07-10 22:21:50 +02:00
SJCoetz
3c32c01bb9 feat(po-attachments): add migration for additional_attachments field 2026-07-09 23:22:23 +02:00
SJCoetz
5d222ff281 feat(po-pdf): add token backfill migration script 2026-07-09 22:28:50 +02:00
SJCoetz
b9828def38 feat(collection-pool): add unified redesigned collection pool view
Remove redundant Assign Drivers top-level navigation link since driver assignment is now embedded directly in the collection pool page per-row, and add an Other nav section for leftover navigation items
Extract collection pool view logic into a dedicated view-model module with full Vitest test coverage for assignment state classification, row sorting, pool age labeling, and proximity marker grouping
Flatten the collection pool page into a single unified list replacing the old grouped assigned/unassigned layout, with updated row UI showing all required operational details and context-aware actions
Add database migration to add optional `collection_photo` field to order_items for driver pickup proof photos
Add complete design documentation, planning artifacts, and brainstorming session files for the collection pool redesign project
2026-07-07 20:49:31 +02:00
SJCoetz
17ae928331 refactor: update pool UI, fix tiles and config
overhaul the collection pool page's ready status UI: split the clickable chip into a static status indicator and dedicated toggle button with loading state, improving user clarity between status and actionable elements
update all related copy (toast messages, filter labels, button text) to use consistent "awaiting supplier confirmation" terminology across the interface
remove invalid `transparent` prop from traffic tile layers in both the collection map and traffic tiles components, update comments to reflect Leaflet does not support this option
add wildcard to allowedDevOrigins in next.config.ts to simplify local development testing across different network interfaces
add a comprehensive database backfill script to fix assigned order items missing the `ready_for_collection` flag, ensuring drivers see their correct assigned stops in the driver app
2026-07-07 18:01:43 +02:00
SJCoetz
7f924ccc45 refactor: switch hardcoded UI colors to semantic theme tokens
Overhaul the app's color system by replacing all hardcoded Tailwind color classes with semantic theme variables to ensure consistent theming across all components, pages, and tests.

Additional changes:
- Add assigned date support for order item status updates: add `assignedDate` field to the order item status PATCH API, implement server-side normalization/validation, and add corresponding tests
- Refactor purchase order PDF workflows to use the consolidated `purchase_orders.documents` field instead of the legacy separate `purchase_order_pdfs` collection
- Fix quote number parsing and formatting to support both dashed (Q-YYYY-MM-NNN) and dashless (QYYYYMMNNN) formats
- Rename `driver_pool` item status to `collection_pool` across the codebase
- Add null handling for order item quantities and pricing to display "—" for missing values
- Improve filter tab styling: make mobile tabs scrollable, prevent text wrapping, hide scrollbar
- Add new utility scripts, API routes, migrations, and polish small UI/UX fixes including tsconfig updates and theme hook improvements
2026-07-07 17:22:03 +02:00
SJCoetz
05abe499b0 feat(sprint): add new sprint pricing workflow and supporting code
Add full sprint pricing workflow and supporting infrastructure:
- New sidebar navigation entry for the sprint dashboard
- PocketBase category collection schema and migration script
- Server API routes for sprint item claims, attachments, counts, and file proxying
- Dev identity utilities and UI switcher for multi-user testing
- Reusable FilterTabs component replacing ItemStatusTabs
- Updated TypeScript types for sprint-related entities

Also include:
- Fix PocketBase file URL method casing (getUrl → getURL) across codebase
- Add development utility, smoke test, and helper scripts
- Update package.json with new sprint migration command
2026-07-03 07:29:43 +02:00
SJCoetz
9cdcb3b096 feat: implement sku-based catalogue pricing matching workflow
This commit adds the full end-to-end SKU pricing match flow for quote items:
- Add new QuoteItem and PriceListRow fields for SKU and pricing metadata
- Add sku_normalised to price_list for robust format-agnostic SKU matching
- Refactor price list search to use modular search builder and filter helpers
- Add database migrations, backfill scripts, and new package commands
- Update catalogue search UI and fix quote list accidental click behavior
- Add comprehensive unit, smoke, and end-to-end test suites
- Update documentation for the new price search and extraction logic
2026-06-30 21:20:53 +02:00
SJCoetz
1fb67efaee 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 12:10:27 +02:00
SJCoetz
b75873a51b 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 11:49:48 +02:00
SJCoetz
81c9f64158 feat(pricelist): notes field for AI instructions + file_name on upload
Operators can now type per-upload instructions for the AI extraction
step in the Pricelist upload form. The notes go into a new
`raw_price_list.notes` field that the n8n workflow reads and
injects into the LLM user message as per-upload context.

Common uses:
  - "Brand is JEI" → AI stamps every item with brand="JEI"
  - "Apply 30% markup" → Code node regexes this out for selling_price
  - "All prices in ZAR excl. VAT" → AI uses ZAR
  - "Page 4 has discount codes" → AI skips those rows

Also auto-populates `raw_price_list.file_name` (the original
browser-side filename) on upload — PB slugifies the upload to a
token stored inside supplier_file, so without this field the
operator saw the slugged version. Existing rows backfilled by the
admin script.

The `log_summary` field is now reserved for the AI's write-back
summary (set by the n8n workflow after extraction). It was
previously operator-editable on the form, which was wrong —
overwriting it clobbered the AI's output.

New migration: scripts/migrations/2026-06-30-add-pricelist-notes.ts
(idempotent, only adds `notes` if missing — `file_name` was
added manually via the PB admin UI before this commit).

doc/pricelist-extraction.md updated with the n8n LLM user message
shape, the markup regex, and the notes field's role in the pipeline.
2026-06-30 11:09:27 +02:00
SJCoetz
7f70258037 feat(management): new Pricelist page with drag-drop upload + queue table
Adds /pricelist under the Management section. Operators drag a supplier
price-list file (PDF / CSV / text / image) into the upload zone — or
use the explicit Choose file button — and it lands in PocketBase as an
attachment on the raw_price_list collection. The table below shows the
queue: file, supplier (relation to suppliers), status badge, log
summary preview, upload date, and a delete action per row.

Layout matches the rest of the app: Card for the upload zone, DataTable
for the queue, DeleteDialog for the destructive action. Live updates
via the PB subscribe channel so other tabs / the n8n ingest workflow
show new rows without a manual refresh.

PB schema is sourced from docs/pocketbase/raw_price_list.collection.json.
The migration script is idempotent: it creates the collection from the
schema file on a fresh deploy, and on a pre-existing one (the live PB
already had it, with PB's default null/superuser-only rules) it relaxes
the auth rules to @request.auth.id != '' so any logged-in user can
CRUD — matching every other collection in the app.

Field names match the live PB collection (discovered via admin API):
  - supplier_file (the file)
  - supplier_name (relation to suppliers)
  - status        (select: Pending / Processing / Completed / Failed)
  - log_summary   (free text)
2026-06-30 09:14:36 +02:00
SJCoetz
6c07a91693 refactor: drop order-level status, shift to item-driven workflow
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
2026-06-29 13:06:01 +02:00
SJCoetz
588e9762c2 feat: add quote templates, improved email, new statuses
Add complete quote template functionality, enhanced outbound email sending, new quote statuses, and supporting infrastructure:
- Add new quote statuses (template, sending, mail_failed) with tailored UI styling for status pills
- Implement full template workflows: save real quotes as templates, create new quotes from existing templates
- Add support for custom quote dates and extended validity periods on generated PDF documents
- Build out improved email pipeline with persisted compose fields and retry functionality for failed sends
- Add attachment direction tracking to categorize inbound, outbound, and generated quote attachments
- Fix tooltip component to properly handle Radix `asChild` prop to avoid React DOM attribute errors
- Add comprehensive test coverage, schema migrations, and update sample test and production data
2026-06-24 22:57:01 +02:00
SJCoetz
2c6155abbb feat: add AI quote item rewriter and supporting UI/UX changes
- Add `user_edited` field to QuoteItem type to gate AI rewrites on user-edited content
- Add idempotent migration script to add and backfill the `user_edited` field across quote items
- Replace plain Select components with searchable SupplierSearch combobox across order and quote pages
- Add working days calculation utility and aged quote warning badges for overdue quotes
- Implement expandable row previews in the quotes list view
- Add full AI rewrite workflow: backend API endpoint, frontend modal, and n8n workflow definition
- Update package scripts for the new migration
2026-06-22 18:29:59 +02:00
SJCoetz
41b5fc4cbf feat(quotes): add email-to-quote pipeline with attachments and replies 2026-06-16 20:46:28 +02:00
SJCoetz
c4e1c62882 chore: standardize line endings and add full test + config scaffolding
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.
2026-06-11 20:30:02 +02:00
SJCoetz
fd1571e294 Add PocketBase collections, APIs & migration docs
Introduce PocketBase workflow support, migration tooling, server APIs and documentation. Adds a set of PocketBase collection JSONs (order_items, order_items_v2, collection_pool, delivery_notes, delivery_note_items, repairs) and a large import manifest for workflow collections; includes docs describing MCP/API layer, migration audit, order-item schema and rollout plans. Adds server-side agent/MCP endpoints and services (api/mcp, api/v1/orders, collection-pool, order-items status, health) plus helper modules (pocketbase-admin, orders-service, order-item-status, agent-auth) and a migration script to create order_items_v2/collection_pool. UI: new pages for collection-pool, delivery-notes, repairs and several updates to orders/quotes pages, sidebar, tooltip and pb helpers/types. Also updates package.json (migration script and related changes) and global styles. Required env vars and suggested migration command are documented (e.g. NEXT_PUBLIC_POCKETBASE_URL, PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, ORDER_ITEMS_COLLECTION; run npm run pb:migrate:orders-v2).
2026-05-25 18:39:33 +02:00