Commit graph

392 commits

Author SHA1 Message Date
08f6005fd5 feat(invoicing): muted rendering + billable-submit guard for non-invoiceable rows
Task 6 of the non-invoiceable-order-extras plan.

Both modals (QB submit + delivery note submit) — same shape change:

- Excluded rows render muted with a 'Not invoiced' chip in the
  row metadata. The muted state overrides 'bg-card' even when
  included, so the operator doesn't mistake them for billable.
- Their include-checkbox is checked and disabled — the row must
  ride along for the n8n status flip, and the operator must not
  be able to remove it or add it to the bill. onChange defensively
  bails when the flag is set, in addition to the disabled prop.
- New 'billableCount' guard: when every billable row has been
  unchecked, submit is replaced with 'No billable lines' label
  and the click handler toasts 'This order has no billable lines.'
  The existing 'noneIncluded' check still catches the operator
  unchecking everything (including the excluded rows the disabled
  checkbox blocks them from unchecking in practice); the new guard
  is the per-mode subset check.

The existing 'noneIncluded' check isn't sufficient because excluded
rows are always in 'includedRowIds' (they have to ride along), so
includedRowIds.size === 0 is unreachable in practice — the billable
subset can still be empty.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 17:30:19 +02:00
7320bfdce6 feat(invoicing): split order_item_ids from items for non-invoiceable rows
Task 5 of the non-invoiceable-order-extras plan.

buildQbInvoicePayload now keeps order_item_ids built from ALL
rows (so n8n's 'Build PocketBase Invoice Update' node flips
excluded rows to invoiced alongside their siblings — the order
closes normally), and builds items from BILLABLE rows only (so
QuickBooks never learns about the excluded lines).

billableRows helper exported for sharing with the invoice UI
(Task 6) and the delivery-note route (Task 7) — three callers,
one definition of 'billable'.

Added exclude_from_invoice to RawInvoicingRow AND the
INVOICING_FIELDS projection. The projection is load-bearing:
omit it and PB returns undefined, every extra silently bills
the customer. Confirmed by inspection and by the failing
'mixed set' test if the projection drifts.

4 tests + 1 mutation-test prove the split:
- mixed: excluded ids in order_item_ids, absent from items
- all-billable: identical output to the no-flag case
- all-excluded: empty items, full order_item_ids
- billableRows helper unit tests
- mutation (items built from input.rows): mixed and all-excluded
  tests fail

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 17:26:46 +02:00
8b23cc1b6a feat(orders): bypass margin gate for non-invoiceable extras
Task 4 of the non-invoiceable-order-extras plan.

The page-level margin gate (process/page.tsx) short-circuits when
the item has exclude_from_invoice. An excluded line has
selling_price forced to 0 (Task 2), so the computed markup is
always -100 and the gate would wedge every extra into
approval_needed and block the order. Bypassing the gate keeps the
operational flow unblocked.

Suppressed low-margin colouring for excluded rows — a red
'-100%' chip on every extra is noise. The 'Not invoiced' badge
(T3) replaces the price chip visually.

Server-side guard in requestOrderItemMarginApproval refuses to gate
an excluded item with code 'excluded_from_invoice' (status 422).
Belt-and-braces: the MCP path can't weaponise the approval queue
to push an excluded row through manager review. Checked BEFORE
the missing_prices guard so an excluded item with selling=0 throws
the descriptive error rather than the misleading missing_prices
one.

2 tests + 1 mutation-test prove both sides of the bypass:
- excluded: throws 'excluded_from_invoice', webhook NOT fired
- non-excluded: proceeds to approval_needed, webhook DOES fire
- mutation (guard disabled): the excluded test fails

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 17:21:28 +02:00
50e75e6955 feat(orders): add-item affordance + sheet on the process page
Task 3 of the non-invoiceable-order-extras plan.

- New POST /api/v1/orders/{id}/items route wraps
  createOrderItemForOrder. Same-origin browser auth (PB session
  cookie), mirrors the posture of /quote after the order-quote
  migration.
- AddItemSheet component on the process page: right-side Sheet
  (mirrors PriceEditSheet's layout) with description / quantity /
  cost / selling / supplier / notes + the load-bearing
  'exclude_from_invoice' checkbox. When ticked, the selling-price
  input is forced to 0 and disabled, a 'Not invoiced' badge replaces
  the margin chip so a -100% red chip on every extra is noise.
- Add Item button below the item card stack — always available,
  not gated on item_status. The empty-state copy above ('No line
  items on this order. Add items before processing.') now points at
  a real action.
- Existing excluded rows render with a muted 'Not invoiced' badge
  in the card stack so the operator can spot them at a glance
  without opening the sheet.
- The server (Task 2) forces selling_price=0 from the flag
  regardless of caller input — the client forcing here is for UX
  consistency.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 17:18:30 +02:00
370a270b4c feat(orders): show a repeat-order pill when an order matches a delivery note
Blue Truck pill on the orders list, distinct from the amber quote
suggestion and the teal linked quote. Reads
potential_delivery_note_number (denormalized, no expand needed) —
the service writes the relation and the number in the same update
call so the list query doesn't pay for a second expanded relation
(see orders-service.ts:253 for why the codebase avoids that).

Additive — an order can show both a quote pill and a delivery-note
pill at once; they're independent facts (a repeat-order
observation, not a provenance link).

Added potential_delivery_note: string and
potential_delivery_note_number: string to the Order interface in
src/lib/types.ts so the page gets the new fields via full-record
return from getOrdersDashboardData (no fields: projection in the
list path).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 12:59:45 +02:00
3df1c1874b feat(orders): add on-demand delivery-note match re-check route
Thin wrapper around suggestDeliveryNoteForOrder(id). The route
mirrors the quote matcher's /quote-match endpoint for the case
where an order's items were edited after intake, or the order
predates the automatic intake check.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 12:58:56 +02:00
623cfefc0d feat(orders): show quote line notes and attachments on the order detail page
Closes the same vanishing-buyer-notes bug B12 fixed on the process
page, this time on the order detail page that operators see first.

- Inline row indicators (MessageSquare icon for a buyer note,
  Paperclip + count for per-line attachments) inside the Description
  TableCell. Hidden when the line has no signal. Click toggles a
  QuoteLineContext panel below the row.
- QuoteLineContext panel wrapped in a Fragment-keyed extra row,
  colSpan=8 (matches the eight TableHead columns: Description, Qty,
  Selling, Supplier, Cost, Total, Status, Edit). The plan called
  for colSpan=9, but the Driver column was removed 2026-08-24 and
  the live count is 8.
- quoteAttachments intentionally empty in the detail-page panel —
  order-level attachments have their own card below the table.

Migration off pb-helpers (AGENTS.md §'data access layer discipline'):
- /api/v1/orders/{id}/quote route now treats empty quote_id as the
  unlink path (clears quote + potential_quote + matching fields),
  and dropped assertAgentAuth so the page can call it from a
  browser session (admin auth happens via the PB session cookie at
  the PocketBase layer; the MCP forwarder still works without that
  gate because the MCP route gates at its own boundary).
- /api/v1/orders/{id}/quote-match is a thin wrapper around
  suggestQuoteForOrder (returns the new SuggestQuoteResult shape —
  best.quoteId / best.quoteNumber, not the old best.quote.id /
  best.quote.quote_number).
- Page migrated four pb-helpers calls: findMatchingQuotesForOrder →
  POST quote-match, applyOrderQuoteMatch → merge into quote-match +
  QuoteMatchCard accepts the result via SuggestionCard from the
  fetched order, clearOrderQuoteMatch → POST quote with empty
  quote_id, setOrderQuoteManually → POST quote with the picked id.
- runMatch signature simplified — accept-vs-suggest was the old
  split; the new endpoint only ever suggests. 'Accept' now goes
  through linkQuoteManually via POST /quote.
- Stale matcher block (4 functions + 3 helpers + re-exports +
  imports) deleted from pb-helpers.ts. The pure scoring helpers now
  import directly from @/lib/server/order-quote-match.

Known follow-up: QuoteMatchCard still reads from matchResult (a
QuoteMatchResult shape). Clearing matchResult after runMatch means
the SuggestionCard renders empty until the next fetchOrder lands.
Reads from order.expand.potential_quote after that. A future task
can rewrite the card to drop the in-memory result entirely.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 12:45:02 +02:00
dda1999fb6 fix(orders): resolve quote line pairing server-side on the process page
This is the actual fix for the vanishing-buyer-notes bug the matcher
service exists to prevent. Before this commit, the process page did
an exact/substring description match on every render — and silently
hid the buyer note whenever the PO's wording differed from the
quote's, even after the matcher had resolved a confident pair.

- Server-resolved linePairs now drives the per-item lookup in three
  places (inline quote preview row, the price-edit sheet's matched
  prop, and copyCostFromMatch).
- findMatchedQuoteItem — the local exact/substring guesser that was
  the root cause — is deleted. No other callers exist.
- QuoteLineContext renders description (when different), buyer note,
  per-line and quote-level attachments, plus the 'possible match'
  banner + confirm button. Replaces a hand-rolled block that had
  drifted from the orders detail page.
- QuoteLineNoMatch renders when the order has a quote relation but
  this specific order line didn't pair to any quote line —
  previously this case rendered nothing, which was indistinguishable
  from 'the quote line had no note'.
- onConfirmPair POSTs to /api/v1/orders/{id}/quote-line (Task 10)
  and re-runs fetchAll so the next render sees the pair as
  'confident'. Only exposed for tier === 'possible'.
- The Pricing source grid stays inline — it's order-side data
  (SKU / pricing_source / matched_price_list_id / pricing_notes
  round-trip from quote_items onto order_items), not part of the
  quote context, and is editable on the order.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 12:31:24 +02:00
23b50a32a9 feat(orders): serve resolved quote-line pairing to the order UI
The order detail page and process page need the pairing, not the
matcher — until now the process page was doing an exact/substring
guess at render time and silently hiding buyer notes when the PO's
wording differed from the quote's.

- matched-attachments response gains linePairs (keyed by order_item_id,
  carrying quote_item_id, confidence, tier). pairOrderLines is called
  defensively — a pairing failure cannot kill the attachments
  response (catch returns null → empty linePairs object, the rest of
  the payload still serves).
- POST /api/v1/orders/[id]/quote-line writes the operator's confirm
  decision (or unlink with empty quote_item_id) onto
  order_items.quote_item. Guards against patching a line that
  belongs to a different order (404 otherwise).

Both routes are additive — the existing matched-attachments fields are
unchanged, the MCP orders.list_attachments consumer keeps working.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 12:20:16 +02:00
63e4a654bf refactor(orders): extract AttachmentList into a shared component
Pure move — bodies copied verbatim from process/page.tsx into
src/components/orders/attachment-list.tsx. The order detail page
(Task 13) will now import the same component instead of growing a
second copy.

The 4 icon imports (FileText, Image as ImageIcon, FileQuestion,
ExternalLink) became unused after the move and were removed from
process/page.tsx. Re-exported from the new file alongside the
MatchedAttachment type, attachmentHref helper, and inferKind helper.

No behaviour change. tsc clean. No new lint errors in the touched
files.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 12:16:31 +02:00
e374513fba feat(mcp): expose orders.quote_match_sweep
Hermes (n8n) drops the AGENT_API_KEY on direct REST calls, so anything
n8n needs to run must exist as an MCP tool. This forwards the agent's
JSON-RPC call to POST /api/v1/orders/quote-match/sweep, mirroring the
existing orders.set_quote forwarding pattern.

Updated the tools/list expected-list test to include the new tool so
the catalog stays self-documenting.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 08:46:28 +02:00
4e54fd6218 feat(orders): add quote-match backlog sweep with age-gap reporting
The matcher only runs on orders created from now on. This sweeps the
historical backlog plus any order whose lines changed after creation.

- sweepUnmatchedOrders in order-quote-link-service.ts walks
  quote_matching_attempted = false && quote = '' (the codebase's
  pattern for an empty relation, verified against orders-service.ts
  usage) in batches, projecting only id (the wider order fields are
  re-fetched inside suggestQuoteForOrder).
- bucketAgeGaps exposes the histogram that lets FUZZY_WINDOW_DAYS be
  re-tuned from real data instead of the 90-day estimate.
- POST /api/v1/orders/quote-match/sweep is the agent-auth surface
  with maxDuration=300 for the 500-order worst case.

Tests cover the filter shape, the field projection (load-bearing —
over-projection wastes wire bytes), the no-op on no-match path, the
fault tolerance (one row throwing doesn't kill the batch), and all
six histogram bucket boundaries including the ≤ N vs < N edges.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-30 08:45:23 +02:00
fd5c3c8510 fix(quotes): stop the detail page's tab strips colliding
Moving the admin panels below the items table left them at a third of
the row each (~186px of tab strip), which is too narrow for the labels
they carry. "Outbound" plus its count needs ~71px against the ~58px it
was given, so the three Attachments labels overlapped each other; the
same squeeze hit "Email Status" on the Activity card.

Lays the panels out two-up instead: Attachments spans both columns on
its own row (its three tabs, dropzone and file list want the space),
with Linked Orders and Activity sharing the row beneath at half width.
Both strips now have several times the headroom they need, measured at
a 1044px viewport, and mobile is unchanged (single column, no
page-level horizontal scroll).

Also drops the `ml-1.5` from the attachment count spans — TabsTrigger
already applies `gap-1.5` between its children, so the margin was
double-spacing and contributed to the overflow — and adds
`tabular-nums` so the tab widths stay steady as counts tick past 9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 09:25:45 +02:00
53f9aaaee4 refactor(quotes): give the detail page's items table the full width
The detail page was a 3-column grid with the Items card in a
lg:col-span-2 and the admin panels beside it. That left the items
table ~430px for a table whose natural width is ~790px, so pricing a
quote meant scrolling the table sideways.

Stacks the page instead: the items table (the actual work surface)
takes the full width, and Attachments / Linked Orders / Activity sit
3-up underneath, where they read fine at a third of the width because
they're reference panels rather than things you act on while pricing.

Two width trims make the table fit outright rather than merely fit
better:
  - Description's min-width 360px → 280px. It's a floor, not a target;
    the table is table-layout:auto so Description still absorbs any
    width the fixed columns leave. 360 was forcing overflow on its own.
  - The "Processed" header is whitespace-nowrap, so the word set a
    ~78px floor on a column whose content is a 2-3 character badge.
    Renamed to "Done" (title="Processed") at w-[64px].

Measured at a 1044px viewport: the table went from 110px of horizontal
overflow to 0. Mobile is unaffected — it uses the stacked card list,
and the admin grid collapses to one column.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 09:17:07 +02:00
42179229aa fix(quotes): repair misaligned items table on the quote detail page
The table had 9 header cells against 8 body cells: a stray duplicate
"Description" header sat above the one carrying min-w-[360px]. Every
column therefore rendered one position off its label (the "Qty" header
sat over the supplier cell, "Supplier" over cost, and so on), and the
360px reserved for Description landed on Qty instead.

The visible symptom was a long description crushed into ~99px and
wrapped one word per line down a tall thin column, while the Qty cell
sat in 360px of empty space.

Removing the stray header restores the header/cell alignment and gives
Description its intended 360px. Also switches the row action from a
"Update price" text button to an icon-only button with the same title
and an aria-label, which trims the table's natural width from 911px to
789px — the card it lives in is only ~430px wide, so every pixel is
horizontal scrolling the operator has to do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 09:00:35 +02:00
7c3083c017 feat(quotes): add quote-level "Quoted before?" panel to the process page
When a mine puts a spot out to tender, several engineering companies
each ask us to price the same item list. Before pricing a new one the
operator wants to see what the earlier quote went out at.

The per-line answer already existed: populateItemIntel powers the ⓘ
popover on each row and returns recent_quote (prior quote number,
company, selling price, days ago). This adds no new matching logic —
it runs that same lookup across every line of a quote and rolls the
results up, so the operator gets one panel instead of clicking ⓘ on
each row.

What the roll-up adds over the per-line popover:
  - "N of M lines have been quoted before" at a glance
  - relatedQuotes ranked by HOW MANY of this quote's lines they also
    carried; a prior quote covering 5 of 5 is very likely the same
    tender from a different bidder, one covering 1 of 5 is a shared
    commodity line

Two honesty details, both driven by what the first live run showed:
  - match confidence is surfaced as close/likely/loose. The underlying
    matcher accepts anything above 0.5, and a live check returned a
    R150 hit against a magnetic base drill — a loose row must not look
    as trustworthy as a close one.
  - an unpriced prior quote renders "unpriced", never R0.00, which
    would read as "we quoted it at zero".

Read-only: passes write:false so viewing the panel doesn't persist
intel onto rows the operator only looked at. On-demand rather than
auto-loading, since it costs one intel computation per line
(~400-650ms each, batched 5 at a time, capped at 40 lines).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 08:52:19 +02:00
113735d0ba fix(quotes): speed up archive search and add item preview to archive rows
The archive's search fetched the entire recency-windowed matching set
(thousands of rows once "Include older" is on) and substring-filtered
it in memory, taking several seconds per keystroke; it now delegates
to the same ranked pushdown search service the main /quotes list uses.
Also fixes pagination being hidden while searching (results past page
1 were unreachable) and the match count showing the current page's
size instead of the true total.

Adds the chevron item-preview toggle to archive rows (extracted into
a shared QuoteItemsExpanded component, backed by a new GET
/api/v1/quotes/[id]/items route), removes a duplicate "Customer or
Company" label in the new-quote form, and drops the stray
warning-colored search box styling that leaked from DataTable onto
every list page that uses it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 02:30:14 +02:00
0efbae8dc5 feat(quotes): enhance search functionality and user experience
- Updated the search input handling to improve responsiveness by integrating a debounced search mechanism, reducing latency from 550 ms to approximately 300 ms.
- Added keyboard shortcut support for focusing the search box, enhancing accessibility and usability for frequent users.
- Improved the search placeholder to reflect all searchable fields, providing clearer guidance to operators.
- Introduced a clear-search button to streamline user interactions, allowing quick resets of the search input.
- Enhanced the display of search results with a live result count badge, providing immediate feedback on search outcomes.

These changes collectively enhance the search experience for operators, making it more intuitive and efficient.
2026-08-28 20:13:44 +02:00
5eed862007 feat(api): enhance order creation with optional email metadata fields
- Added `email_sender`, `email_subject`, and `email_body` fields to the order creation input schema for improved traceability of inbound emails.
- Updated logic to include these fields in the order creation process, allowing operators to track the source of orders without needing follow-up updates.
- Enhanced documentation to reflect the new optional fields and their purpose in the order management workflow.

This update improves the audit trail for orders and enhances user experience by linking orders directly to their source emails.
2026-08-28 13:00:31 +02:00
dc0139a62c feat(api): add support for optional custom order_number in order creation
- Introduced `order_number` field in the order creation input schema, allowing users to provide a custom identifier (e.g., PO/reference).
- Implemented logic to handle empty or whitespace values, defaulting to auto-generated order numbers.
- Added error handling for order_number collisions, returning structured error responses for better client-side handling.
- Updated documentation to reflect the new order_number functionality and its implications.

This enhancement improves order management flexibility and user experience.
2026-08-28 12:30:38 +02:00
11415d4784 refactor(boss-dashboard): drive tab change from click handler, stabilize fetchData via window ref
Fixes react-hooks/set-state-in-effect lint error in the tab-change useEffect.

- Tab buttons now call selectWindow(w) directly instead of relying on a
  useEffect that synchronously calls setState. selectWindow flips the
  state, closes the sheet, and triggers the refetch from one event handler.
- fetchData now reads activeWindow via a ref synced by a tiny useEffect
  (the canonical React pattern — eslint flagged the inline ref-write as
  'Cannot update ref during render'). With no useCallback deps, fetchData
  identity is stable, so the realtime subscription no longer churns on
  every tab switch (a perf concern flagged in Task 15).
- The mount-only initial fetch useEffect is preserved with a justified
  eslint-disable on the set-state-in-effect rule (no SSR data pass-through
  on this client-only page, so the initial fetch has no user event to
  hang off).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-28 00:33:14 +02:00
a74cb17940 feat(boss-dashboard): add PB realtime subscription with 1s debounce 2026-08-28 00:27:19 +02:00
ab0aa8ec8d fix(boss-dashboard): close sheet on backdrop click + tab switch 2026-08-28 00:26:24 +02:00
9101871fef feat(boss-dashboard): add rep-activity bottom sheet 2026-08-28 00:24:46 +02:00
44f93feb13 feat(boss-dashboard): add drivers section 2026-08-28 00:23:32 +02:00
eb3f801167 feat(boss-dashboard): add reps list section 2026-08-28 00:22:30 +02:00
ca6d43e0e8 feat(boss-dashboard): add order totals strip (Total / Delivered / Invoiced) 2026-08-28 00:21:36 +02:00
b07327eae5 feat(boss-dashboard): add time tab bar (Today / Week / Month) 2026-08-28 00:20:18 +02:00
16efafc111 fix(boss-dashboard): guard fetch races, surface errors, tighten response type 2026-08-28 00:19:12 +02:00
f09f9163c0 feat(boss-dashboard): add /m page + dashboard-view skeleton 2026-08-28 00:16:33 +02:00
fcbc085723 feat(boss-dashboard): add /(mobile)/m layout with viewport + manifest link 2026-08-28 00:15:36 +02:00
15206eaf49 test(boss-dashboard): add 500/null-fallback/edge-case coverage + tighten mock hygiene 2026-08-28 00:14:06 +02:00
e9ce5f4fc4 test(boss-dashboard): add route handler tests for dashboard and rep-activity 2026-08-28 00:10:00 +02:00
f243132dc0 refactor(boss-dashboard): use shared assertBrowserSession + cap page 2026-08-28 00:07:57 +02:00
70807e34fe feat(boss-dashboard): add /api/v1/boss-dashboard/rep-activity route handler 2026-08-28 00:05:20 +02:00
8ecacf78fe feat(boss-dashboard): add /api/v1/boss-dashboard route handler 2026-08-28 00:02:51 +02:00
Dave
dc84ce0f30 feat(quotes): resolve customer from person_hint when rep forwards email
Gary (and any future rep) forwards inbound RFQs to a shared mailbox.
Today the customer linked to the resulting quote is the rep (or the
Unknown Sender sentinel), because the only resolution path is exact
email match — the from_email resolves to Gary, not the actual customer.

Add person_hint to EmailIntakePayload. The AI extraction layer reads
the subject / body / signature for the customer being quoted FOR and
fills person_hint. findOrCreateCustomer now walks three resolution
passes in order:

  1. Exact email match (existing behaviour — direct RFQs unchanged)
  2. person_hint resolution: case-insensitive substring on
     customer.customer, with a companyHint tie-break when multiple
     candidates exist
  3. Existing sender-based create-new path (Gary wins, quoter
     reassigns in the WFDASH UI)

MCP schema for quotes.create_from_email surfaces the new field with
doc explaining the rep-forwarded case. /api/v1/quotes/from-email
doc updated. AI Agent system prompt in the n8n workflow doc now
extracts person_hint with worked examples.

Also extends the mock filter parser to accept PB's bare ~ operator
(production code uses ~; the mock previously only accepted =~).

5 new unit tests + 2 integration tests for the new path.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 10:45:51 +02:00
Dave
4d8382c87f fix(quotes): wire no-quote toggle into process-page UI gates
The server-side PDF route was already no-quote-aware — when
`quote.is_no_quote === true` it bypasses the pricing gate
(`pdf/route.ts:156, 238, 240`) and the PDF component renders "Not
quoted" cells + "—" totals (`quote-document.tsx:239, 242, 276/280/285`).
But the client side was ignoring the flag, so flipping the toggle
looked like a no-op. Five one-file fixes to close the gap:

1. `itemsReady` now short-circuits to true when no-quote is on.
   Before: `itemsReady = items.length > 0 && missingSupplier === 0 &&
   missingPricing === 0;` (always evaluated pricing). After: prepend
   `isNoQuote ||` so the Generate Quote button enables regardless of
   cost/selling prices. (Bug A)

2. `applyPricing` markup>0 guard now bypassed when no-quote is on.
   Before: toast "Markup must be greater than 0" + refuse save when
   markup ≤ 0 — blocked the operator from saving a 0/0/0 row. After:
   `if (!isNoQuote && ...)` so the operator can save the row. (Bug B)

3. `handlePricingChange` auto-derive now skipped when no-quote is on.
   The cost↔selling recompute on every keystroke (`value * (1 + markup
   / 100)` and inverse) was firing even when no-quote was set, which
   is what produced the "I set 0/0/1% markup and it shows 1 cent"
   artefact. Three branches (markup/cost/selling) now short-circuit
   to pass-through under no-quote. (Bug C)

4. Helper text under the Generate Quote PDF button now explains
   "No-quote letter — prices ignored on PDF (renders 'Not quoted'
   cells)" when the toggle is on, instead of the misleading
   "Price every item + assign a supplier before generating".

5. Second helper line under the toggle's label explaining the full
   UI flow when on: "the pricing form stays editable but isn't
   required, and the PDF renders 'Not quoted' cells with no totals."

Operator workflow now: flip toggle ON → enter description on a row
at 0/0/0 → save → click Generate Quote → PDF renders decline letter
with "Not quoted" cells and no totals. No PB migration needed
(`is_no_quote` boolean column already present per `pb-exports/pb_schema-2.json:2178`).

Build: clean. Typecheck: 0 errors. No tests added (manual flow; no
existing test exercises this toggle path).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 08:00:17 +02:00
Dave
7986784eaf chore(tests): fix ~60 pre-existing TS errors across 11 test files
Typecheck cleanup after the deploy blocker push — `tsc --noEmit` was
silent on `d11a184` baseline but ~60 errors hid across test files
that only the compiler surfaces (the build was clean because test
files are excluded from the prod bundle). Touched:

- src/app/api/mcp/route.test.ts (16) — generic on `vi.fn()` for
  tuple inference; `as unknown as` two-step cast on record literals
- src/app/api/mcp/po-agent-workflow.test.ts (1) — spread instead of
  duplicate key
- src/components/buyer-tasks/buyer-task-group-card.test.tsx (5) —
  `PurchaseOrderEmailPayload` now requires `subject`/`body`
- src/components/collection-confirm/confirm-stock-dialog.test.tsx (1)
  — removed shadowed `id` from `makeRow` overrides
- src/components/item-intel-popover.test.tsx (2) — missing row
  fields + boolean→object for `verified`
- src/components/quotes/process-item-card.test.tsx (2) — full
  QuoteItem stub; generic on `vi.fn()`
- src/components/sprint-item-pricing-modal.test.tsx (3) — full
  SprintItem/QuoteItemAttachment/QuoteAttachment stubs
- src/lib/server/orders-service-review.test.ts (~60) — annotated
  `getOne` return type to break union widening on the expand option
- src/lib/server/orders-service.test.ts (2) — removed bogus
  `includeArchivedCounts` param; `rfq_number` now whitelisted on Order
- src/lib/server/quotes-service.test.ts (3) — two-step cast on
  Quote header; `unit?` now on QuoteItem
- src/lib/server/stock-purchase-items-service.test.ts (~10) — typed
  `pbMock` via `StockPurchaseItemsPb` so `*WithPb(pb,...)` checks

Also added two optional fields to `src/lib/types.ts`:
- `QuoteItem.unit?` — pre-2026 rows have no unit, so it's optional
- `Order.rfq_number?` — populated by the n8n email agent, inherited
  verbatim by `splitOrder`

Build: clean. Typecheck: 0 errors. Tests: 14 pre-existing runtime
failures + 1 pre-existing unhandled error in `supplier-form.test.tsx`
remain — confirmed they exist on `d11a184` baseline, so they are
outside the cleanup scope and unchanged by this commit.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 04:43:40 +02:00
Dave
d11a1848e8 feat(quotes): finish createQuote refactor for operator-supplied quote_number
The /api/v1/quotes/create route was wired to accept operator-supplied
quote_number + rfq_number + quote_notes + notes + status, but the
service-layer side never matched: QuoteNumberExistsError didn't exist
and CreateQuoteInput ignored the new fields. Production deploy was
failing on `Module has no exported member 'QuoteNumberExistsError'`.

Service changes:
- New QuoteNumberExistsError class (mirrors OrderNumberCollisionError):
  carries existingId + attemptedNumber, mapped to 409 by the route
- CreateQuoteInput extended with quote_number, rfq_number, quote_notes,
  notes, status; imported QuoteStatus from @/lib/types
- createQuote now uses input.quote_number verbatim when supplied, else
  falls back to the existing atomic-counter generator
- createQuote's pb.create call is wrapped in try/catch: PB's UNIQUE
  constraint violation on quote_number is re-thrown as the typed error
  with the colliding row's id resolved via a follow-up getList

New route test file (9 cases) covers happy path with supplied
quote_number, the 409 collision branch, notes/quote_notes fallback
merge, auth-gate failures, missing customer/items validation, and
generic 500 fallback. Uses importOriginal so the real
QuoteNumberExistsError class survives the service-layer mock.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 04:19:51 +02:00
Dave
1717d62980 fix(approvals): 404 test coverage, exclude PDFs from outbound, drop dead pushMock
Three final-review fixes for the Approve & send from list feature:

1. The 404 branch added in 5760ef9 had no test guarding it. Add
   a test mirroring the invalid_status pattern: mock a 404 from
   review-approve, assert router.refresh() was called and
   toast.info fired with the 'no longer exists' message.

2. The list page's ensureOutboundAttachments fetched all attachments
   where direction='outbound' — including PDFs — and passed them
   straight to SendQuoteModal. The modal renders its own pdfAttachment
   above the outbound list, so an operator who re-uploaded a PDF as
   a proof would see it twice (once as always-attached, once as a
   selectable outbound). Mirror the detail page's filter
   (line 1217 of approvals/[id]/page.tsx) by adding 'kind != pdf' to
   the list page's outbound-attachments query.

3. The list page never calls router.push (it uses router.refresh
   for all branches), so the pushMock in the test scaffold was
   dead. Drop it.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 01:04:02 +02:00
Dave
5760ef9966 fix(approvals): address Task 2 code quality findings + test mock update
Four code-quality issues from review + one test-mock follow-up:

1. CRITICAL: isPlaceholderCustomer was called with a string arg but
   expects { id, customer, email } - TS error, and at runtime the
   function would always return true. Variable was dead (computed but
   never read) so remove the call, the explanatory comment, and the
   import entirely. The Option A behaviour (server handles placeholder
   via its 422) is preserved by simply not having the dead variable.

2. CRITICAL: toast(..., { type: 'info' }) doesn't compile under
   ExternalToast and silently drops the type option at runtime, so
   the invalid_status race toast renders with default styling instead
   of info (blue). Switch to toast.info(...). Updates the test
   assertion to check toast.info directly (the test mock defines
   toast.info as an independent vi.fn(), not a wrapper around toast,
   so the prior expect(toast).toHaveBeenCalledWith assertion would
   no longer match).

3. IMPORTANT: Spec asks for a dedicated 404 branch ('This quote no
   longer exists. Refreshing.') but the implementation fell through
   to the generic catch-all. Add the 404 branch between the mail-fail
   branch and the generic fallback.

4. NICE-TO-HAVE: formatCurrency was imported but never used. Drop.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 01:00:43 +02:00
Dave
911c6f7496 chore(approvals): drop dead handleApprovePartial
Task 2 collapsed the list's per-row "Approve partial" button into
the new "Approve & send" button, which routes through review-approve
on the server. The list no longer calls the legacy partial-approve
endpoint, but handleApprovePartial was left in the page file because
the plan's Step 3 only covered handleApprove replacement. Remove it.

The /api/v1/quotes/[id]/partial-approve endpoint itself is unchanged
(it's still used by other parts of the app).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 00:55:00 +02:00
Dave
a499329a4e feat(approvals): Approve & send from list rows
The pending-approvals list now offers the same one-step
approve-and-dispatch affordance as the detail page, instead of
forcing the rep through a two-step approve-then-drill-in flow.

- Replace the list's standalone 'Approve' button (direct PB write)
  and 'Approve partial' button (POST /partial-approve) with a single
  'Approve & send' button that opens SendQuoteModal.
- Lift the modal and sendApprove callback to page level, keyed by
  row id, callable per row.
- Submit posts to POST /api/v1/quotes/[id]/review-approve with
  action: 'approve' and the modal's email payload. The server
  atomically approves + dispatches.
- Same handler works for partial_approval_pending rows — server
  flips partial_approved -> sending -> emailed natively.
- Toast wording matches the detail page: 'Approved - email sent...'
  for full, 'Partial approved - email sent...' for partial, with
  invalid_status and mail-failed branches surfacing their own toasts.
- Reject path untouched.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 00:53:45 +02:00
Dave
c0eca10b51 fix(test): drop stale triggerSend comment in approvals page test
The fix commit (27f60e1) removed the triggerSend hoisted spy and
wired the modal stub's button to call props.onSend directly, but
left a two-line comment in test 3 that still referenced the deleted
symbol. Replace the comment with one that describes the actual
mechanism so future readers don't go hunting for a triggerSend
identifier that no longer exists.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 00:48:14 +02:00
Dave
27f60e14ec fix(test): wire SendQuoteModal stub to props.onSend
The SendQuoteModal test stub had a noop triggerSend placeholder that
never called props.onSend, so the click on the modal's "Submit modal"
button would silently fail. Tests 3-7 (which assert on fetch /
refresh / toast effects from sendApprove) would all hang even after
Task 2 implements the page correctly.

Fix: call props.onSend directly from the button's onClick. Drop the
unused triggerSend hoisted spy. Tighten the stub's props type to
SendQuoteModalProps so Task 2's prop-name typos surface as TS errors.

Also widen the PB mock fallback's getList to return the full envelope
(items + totalItems + totalPages) so Task 2 reading quote_attachments
or any other collection doesn't crash on undefined totalItems.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 00:46:34 +02:00
Dave
097ada09f8 test(approvals): scaffold Approve & send from list tests
Component tests cover the row-button -> SendQuoteModal ->
review-approve submit flow including success, invalid_status,
and mail-failed error branches. All 7 fail; implementation in
follow-up commits.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-27 00:39:16 +02:00
Dave
87eacdc307 fix(quotes): persist is_no_quote through quotes.create path
Closes the gap that the MCP quotes.create tool advertised is_no_quote
but the upstream POST /api/v1/quotes/create chain silently dropped the
flag. Three additions:

  - CreateQuoteInput interface gains the field
  - createQuote() forwards it to pb.collection.create(...)
  - POST handler forwards body.is_no_quote to createQuote()

Spec test case 10 ("quotes.create with is_no_quote: true persists
flag and returns quote") now passes end-to-end.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-26 23:58:43 +02:00
Dave
e51a405414 feat(quotes): enhance quote creation with new fields and error handling
Added support for additional fields in the quote creation process, including quote_number, rfq_number, quote_notes, and status. Updated the error handling to return a specific 409 response when a quote_number conflict occurs, improving the API's feedback for clients. Enhanced documentation to reflect these changes.
2026-08-26 23:55:30 +02:00
Dave
d4ff62c7b2 feat(mcp): expose is_no_quote on quotes.create and quotes.update
Lets Hermes and other external agents drive the no-quote flow without
a UI step. Hints document the gate-bypass and reversibility.

Five touch points in src/app/api/mcp/route.ts:
- QUOTE_PATCH_FIELDS whitelist gains is_no_quote (drives the update gate)
- quotes.create input shape declares the boolean
- quotes.update input shape declares the boolean
- quotes.update description text lists is_no_quote in the whitelist
- quotes.create handler forwards args.is_no_quote to the upstream body

The quotes.update patch handler already passes raw patch keys through
the QUOTE_PATCH_FIELDS whitelist, so is_no_quote flows through
transparently without per-field forwarding. quotes.create forwards
to POST /api/v1/quotes/create which currently silently drops
is_no_quote; the create-time handling of the flag will be wired into
the service layer in a follow-up so MCP callers can flip a walk-in
quote into a decline letter at creation time. The schema hint is
live today.

Smoke verification deferred to Task 11 (no-quote E2E). Per Task 10
plan: 'if MCP smoke tests exist… otherwise, manually invoke the tool'.
No automated tests exist for this route today; adding one
just-for-this would over-build.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-26 23:50:37 +02:00
Dave
6d9b6a8274 fix(quotes): address UI toggle code review
Addresses the 2 IMPORTANT and 4 NIT findings from the Task 9
code-quality review:

- Initial state bug: useState read 'quote' on first render when it
  was null, sealing isNoQuote as false. A previously-persisted
  is_no_quote: true quote would show the toggle as off. Replace
  with a useEffect that syncs once per loaded quote id.

- Dark mode: add dark: variants to the amber card border/background
  to match the project precedent (collection-pool/components.tsx:175).

- Label size: drop from text-sm to text-xs for consistency with
  every other <Label> in the file.

- Error surface: parse the PATCH 400 body and surface data.error
  rather than a generic toast, matching the lookupItem/generatePdf
  pattern in the same file.

- useCallback deps: stop recreating the callback on every toggle
  by stashing the current value in a ref.

- quote-notes-card id: add a comment so the wrapper div's purpose
  is clear.
2026-08-26 23:47:27 +02:00
Dave
0b6226cdf1 feat(quotes): add no-quote toggle to Quote Details card
Switch + Label inside the Quote Details card on the process page.
Optimistic update with rollback on error. When flipped on, scrolls
the quote_notes editor into view so the operator can type the
reason in the same flow.
2026-08-26 23:43:15 +02:00
Dave
a2e10652a4 fix(quotes): no-quote letters always flip to approval_pending
The downstream status-flip logic uses ready.length === allItems.length
to choose between approval_pending and partial_approval_pending. For
an is_no_quote quote with all-unpriced items, the unfiltered bypass
in Task 7 made ready.length === 0 while allItems.length > 0, flipping
to partial_approval_pending instead of approval_pending — contradicting
the spec ('new → approval_pending → sending → emailed' for no-quote
letters).

Add a guard so is_no_quote quotes always flip to approval_pending.
The partial-flip path remains unchanged for priced quotes.
2026-08-26 23:39:15 +02:00
Dave
cfcbbc5f10 feat(quotes): bypass PDF ready filter when is_no_quote
Lets the PDF render with all active items (regardless of price)
when the operator has marked the quote as a decline letter. The
existing PDF diagnostic still flags data issues for priced quotes.

Also adds the (unused) noQuote prop to QuoteDocumentProps so the
route compiles. Task 8 will wire the rendering.
2026-08-26 23:37:48 +02:00
Dave
bf91b601aa feat(quotes): expose is_no_quote on PATCH /api/v1/quotes/[id]
Adds the new field to the PATCHABLE_FIELDS whitelist, validates
type as boolean, and dispatches to a new setIsNoQuote service helper.
Tested via mocked route handler.
2026-08-26 23:24:21 +02:00
Dave
720531bc0d refactor(orders, quotes): improve table layout and column widths for better readability
- Updated the Orders and Quotes detail pages to enhance table layouts, ensuring that the Description column has adequate space for multi-line text.
- Removed the Driver column from the Orders table to streamline the layout and prevent content squeezing.
- Adjusted fixed widths for various columns to fit within the viewport without horizontal scrolling, improving overall user experience.
- Ensured consistent styling and layout adjustments across both pages for a unified look and feel.
2026-08-24 07:57:15 +02:00
Dave
a0afa51600 refactor(orders): replace OrdersReviewModal with OrdersReviewModalMount and streamline attachment handling
- Updated the orders page to use `OrdersReviewModalMount` for improved order review functionality.
- Removed the inline `orderAttachementsToQuotes` function to simplify the code structure and enhance maintainability.
- Introduced a review modal in the order detail page, allowing users to trigger the review process with a dedicated button.
- Ensured consistent behavior between the orders list and detail pages regarding order review interactions.
2026-08-24 07:26:18 +02:00
Dave
139a3f3bfe feat(quotes): enhance scoring logic to include price matching
- Updated the `scoreQuote` function to incorporate price scoring, allowing quotes to be evaluated based on selling and cost prices when the query is numeric.
- Introduced new weights for item selling and cost prices, ensuring they contribute to the overall score only when relevant.
- Enhanced the test suite to cover new price matching scenarios, ensuring accurate scoring behavior for quotes based on price inputs.
- Refactored related functions to support the new price scoring logic, maintaining consistency across the scoring process.
2026-08-24 07:09:39 +02:00
Dave
07a5e810e5 feat(attachments): enhance order attachment handling and retrieval
- Updated the `orders.add_attachment` and `orders.list_attachments` tools to utilize the order's own `order_attachements` field, allowing immediate visibility of attachments without requiring a matched quote.
- Enhanced the `POST` route for uploading attachments to support multiple files and improved response structure to include `collectionId` and `collectionName`.
- Merged attachment sources in the `GET` route for matched attachments, providing a unified view of order-level and quote-level files, with a new `source` field to indicate the origin of each attachment.
- Added utility functions for inferring MIME types and attachment kinds based on file extensions, ensuring consistent handling across the application.
- Improved documentation for clarity on the attachment handling process and updated response formats to reflect the new structure.
2026-08-21 20:47:00 +02:00
Dave
c3b452dbc4 feat(orders): enhance order handling with email_message_id deduplication and displayContactName utility
- Added support for deduplication of orders based on the `email_message_id`, preventing duplicate entries during email intake.
- Introduced the `displayContactName` utility to sanitize and format contact names, improving the display of order details by removing extraneous whitespace.
- Updated relevant order-related components to utilize the new utility for consistent contact name rendering.
- Enhanced tests to cover the new deduplication logic and the functionality of the `displayContactName` utility, ensuring robust behavior in various scenarios.
2026-08-21 19:18:02 +02:00
Dave
fb283765fb fix(repair-details): update file handling and type definitions
- Modified the file handling in the repair details page to use the correct repair ID and simplified the key assignment for photo files.
- Updated the API route to cast the status parameter to the new RepairStatus type for improved type safety.
- Expanded the RepairFileField type to include "delivery_note" for better file management.
- Removed unused helper functions to clean up the codebase and improve maintainability.
2026-08-21 01:13:44 +02:00
Dave
663c018a70 feat(quotes): add "Include older" toggle for searching full quote history
- Introduced a new toggle in the quotes and archive pages to allow users to include older quotes beyond the default 90-day cutoff.
- Updated the search parameters in the quotes and archive APIs to handle the new `includeOlder` flag, enabling full history searches.
- Enhanced the user interface to reflect the current search scope, providing clear feedback on whether the search is limited to recent quotes or includes the full archive.
- Adjusted the quotes search service to support the new functionality, ensuring efficient retrieval of older quotes when requested.
- Added relevant tests to verify the correct behavior of the new search feature and its integration with existing functionality.
2026-08-20 10:18:44 +02:00
Dave
88047eee33 feat(quotes): add "Completed (7 days)" tab for recent quotes
- Introduced a new tab for quotes completed within the last 7 days, enhancing the user experience by allowing quick access to recent activity.
- Updated the quotes search service to filter results based on the new "completed_recent" status, ensuring efficient retrieval of relevant quotes.
- Enhanced the quotes service to compute the cutoff date for the recent quotes dynamically, maintaining consistency across the application.
- Added tests to verify the functionality of the new filter, ensuring accurate behavior in the quotes search pipeline.
2026-08-19 13:36:41 +02:00
Dave
43bbdfc458 feat(tv-dashboard): enhance TV dashboard with driver stops and performance metrics
- Updated the TV dashboard to include a new section for driver stops, displaying active deliveries and pickups with GPS coordinates.
- Refactored the dashboard data structure to incorporate year-over-year performance metrics alongside existing monthly and weekly stats.
- Replaced the supplier map with a driver stops map for improved clarity on driver activities.
- Enhanced the user performance metrics to include detailed counts for quotes and orders processed by each representative.
- Removed the obsolete supplier map component to streamline the dashboard's functionality.
2026-08-19 10:16:49 +02:00
Dave
2f0e911563 feat(delivery-notes): standardize delivery-note letterhead across PDFs
- Introduced a centralized `deliveryNoteLetterheadCode` function to ensure all delivery-note PDFs consistently use the Westcott Group letterhead, regardless of the subsidiary.
- Updated relevant API routes and services to utilize the new function, removing previous hardcoded fallbacks.
- Enhanced the `DriverStop` interface to include timestamps for better reporting capabilities.
- Added new fields to track creation timestamps for driver stops, improving data accuracy for outstanding reporting.
2026-08-18 11:53:25 +02:00
Dave
8ed0952364 refactor(forms): update layout and structure of form components
- Adjusted the `SheetContent` width in multiple form components to improve responsiveness.
- Enhanced the `CompanyForm` layout by reorganizing fields for better clarity and usability, including the addition of coordinates input.
- Updated the `CustomerForm` and `SupplierForm` to maintain consistency in design and structure.
- Improved the `DataTable` component's card styling for better visual appeal and user interaction.
- Refined the `QuotesPage` card styling to enhance the user experience when selecting quotes.
2026-08-17 23:34:30 +02:00
Dave
0b6171b851 feat(collection-pool): enhance stock row handling and filtering logic
- Added `drv_id` to the stock row data structure to ensure proper assignment tracking.
- Implemented a new `isReadyForCollection` function to streamline the logic for determining if stock rows are ready for collection.
- Updated the `buildVisibleRows` function to utilize the new readiness check, improving the accuracy of row filtering based on the ready status.
- Added a test case to verify that stock rows are correctly included in the ready filter, ensuring expected behavior in the collection pool view.
- Enhanced coordinate parsing logic to handle various input formats, improving robustness in the driver stops API.
2026-08-17 22:33:47 +02:00
Dave
50944a3526 feat(quotes): add operator-facing notes functionality to quotes
- Introduced a new `quote_notes` field to store operator-facing free-text notes, distinct from the existing `notes` field used for rejection audit logs.
- Updated the quote processing page to include a `QuoteNoteCard` for editing and displaying the operator notes, ensuring seamless integration into the PDF generation flow.
- Enhanced the PDF generation logic to render the operator notes in a blue callout below the totals, improving visibility for end-users.
- Implemented a new service function `setQuoteNotes` to handle the saving of notes, including trimming whitespace and handling non-string inputs defensively.
- Added unit tests for the `setQuoteNotes` function to ensure correct behavior and data integrity when updating notes.
- Updated relevant types and documentation to reflect the new functionality and its intended use cases.
2026-08-17 15:22:16 +02:00
Dave
9e8bb0a1ad feat(order-splitting): enhance order item splitting functionality and error handling
- Implemented a new split mechanism for order items, allowing operators to split quantities based on in-stock and non-in-stock statuses.
- Updated the payload for creating split order items to ensure it adheres to the PocketBase schema, avoiding empty strings for relation fields and removing deprecated fields.
- Enhanced error handling in the split route to log unexpected errors for better debugging.
- Improved the UI to conditionally render split buttons based on item availability, ensuring a clearer user experience.
- Added unit tests to validate the new splitting logic and payload structure, ensuring robustness against schema changes.
2026-08-17 14:14:27 +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
fc55613070 feat(collection-confirm): add quantity editing tests for order items and stock sources
- Implemented tests for quantity editing functionality, ensuring that pressing Enter updates the quantity for order items and stock sources correctly.
- Added validation for input errors, rejecting fractions and negatives with appropriate inline error messages.
- Enhanced test coverage for the quantity handling logic, confirming correct API calls and data submissions for both customer and stock items.
2026-08-17 07:21:50 +02:00
Dave
d0fb8e81f0 feat(collection-confirm): add quantity collection handling and deduplication improvements
- Introduced a new `handleQtyChange` function to manage per-row quantity updates for both customer and stock items, ensuring correct data submission to the appropriate endpoints.
- Enhanced the `dedupeAttachments` function to handle additional document sources, including parent purchase orders, and skip empty filenames to prevent 404 errors.
- Updated the `ItemsTable` and `PoCard` components to support quantity editing, allowing users to modify collected quantities directly in the UI.
- Added tests to validate the new quantity handling logic and attachment deduplication, ensuring robust functionality across various scenarios.
2026-08-17 02:17:27 +02:00
Dave
4a59288a01 feat(deliveries): enhance delivery notes management and UI
- Introduced a new `DeliveryNoteRow` type to track delivery notes alongside items, improving the organization of delivery data.
- Updated the `DeliveryListResponse` type to include delivery notes, allowing for better integration with the existing delivery management system.
- Enhanced the `groupByCustomerOrder` function to accommodate delivery notes, ensuring they are grouped correctly with their respective orders.
- Implemented state management for delivery notes in the `DeliveriesPage` component, including loading and error handling.
- Added search functionality for delivery notes, enabling operators to filter by delivery note number, customer PO, and order details.
- Updated the UI to display delivery notes alongside items, improving the overall user experience in the deliveries management interface.
- Enhanced tests to cover new delivery notes functionality and ensure correct behavior across various scenarios.
2026-08-17 01:27:03 +02:00
Dave
1640c1e09a feat(delivery-notes): implement QuickBooks invoice auto-close functionality
- Added a new service function `markDeliveryNoteCompletedByInvoice` to auto-close delivery notes after successful QuickBooks invoice creation, updating the status to "completed" and appending an audit banner to the notes.
- Enhanced the delivery note detail page with a modal for creating invoices via QuickBooks, including customer selection and billing company options.
- Implemented state management for the invoice modal, including loading states and error handling for customer fetching.
- Updated tests to cover the new auto-close functionality and ensure correct behavior across various scenarios.
- Improved documentation for the delivery notes service to reflect the new functionality and its usage.
2026-08-16 22:24:08 +02:00
Dave
17580b5280 feat(invoicing): implement detailed view toggle for supplier and PO number
- Added a switch to toggle the visibility of supplier names and PO numbers in the invoicing page, enhancing user experience by allowing operators to view detailed information as needed.
- Updated the `RowIdentity` component to conditionally render supplier and PO details based on the toggle state.
- Introduced localStorage persistence for the detailed view preference, ensuring user settings are retained across sessions.
- Enhanced tests to verify the functionality of the detailed view toggle and its impact on displayed data.
2026-08-16 11:01:52 +02:00
Dave
1bc80dc686 feat(orders): enhance order retrieval and statistics functionality
- Updated the `GET /api/v1/orders` endpoint to support additional query parameters including `customer`, `createdAfter`, `createdBefore`, and `status`, allowing for more granular order filtering.
- Implemented logic to resolve customer IDs from names and aggregate order items by their statuses for improved data retrieval.
- Introduced new helper functions in `orders-service.ts` for fetching order statistics and daily summaries, enhancing reporting capabilities.
- Added comprehensive documentation for the new query parameters and their usage in the API documentation.
- Improved error handling for invalid customer and status inputs, ensuring clearer feedback for API consumers.
2026-08-16 00:51:24 +02:00
Dave
cf87a370cf feat(quotes): enhance quotes.submit functionality and error handling
- Updated the `quotes.submit` tool to allow submission of quotes with unpriced items by introducing a `partial: true` flag, transitioning the quote to `partial_approval_pending` instead of `approval_pending`.
- Enhanced the response structure of `submitQuote` to include `previousStatus`, `submittedAt`, and `targetStatus`, providing better context for the agent.
- Improved error handling in the `sendQuoteEmail` function to surface detailed readiness issues when a PDF cannot be generated, allowing operators to address specific gaps in quote items.
- Regenerated API documentation in `docs/api-mcp-layer.md` to reflect these changes, ensuring accuracy and clarity in tool descriptions.
- Added comprehensive tests to validate the new functionality and error handling improvements.
2026-08-15 23:57:51 +02:00
Dave
e5e31a7bdc feat(mcp): implement upstream origin resolution for internal API calls
- Added a new `upstreamOrigin` function to resolve the correct upstream API origin for internal MCP to REST calls, addressing issues with container deployments where `req.nextUrl.origin` resolves incorrectly.
- The function prioritizes environment variables for configuration, ensuring proper routing in various deployment scenarios.
- Updated multiple API fetch calls to utilize the new `upstreamOrigin` function, improving reliability and consistency in internal API interactions.
2026-08-15 23:22:19 +02:00
Dave
e7672ab7c9 feat(mcp): implement structured MCP error handling for upstream fetches
- Introduced structured error handling for MCP upstream fetches, distinguishing between network errors (`-32010`) and upstream errors (`-32011`). This allows the agent to provide more informative feedback based on the type of failure encountered.
- Updated the `fetchUpstreamJson` function to throw `McpUpstreamError` with detailed error data, including hints and next actions for specific tools.
- Enhanced the `TOOL_HINTS` mapping to provide tool-specific guidance for handling errors, improving the agent's ability to respond to various failure scenarios.
- Added comprehensive unit tests to validate the new error handling logic and ensure correct behavior across different error cases.
- Updated documentation in `docs/agents/hermes-skill.md` to include a new section on structured MCP errors, detailing the error codes and their meanings.
2026-08-15 22:32:11 +02:00
Dave
151ae8d67f feat(quotes): introduce quotes.submit tool for approval transition
- Added a new `quotes.submit` tool that transitions a quote from `new` to `approval_pending` once all line items are priced. This tool ensures that only fully priced quotes can be submitted for approval, enhancing the integrity of the approval process.
- Updated the API documentation in `docs/api-mcp-layer.md` to include the new `quotes.submit` tool and its usage details.
- Enhanced the `quotes` status enum in `docs/agents/hermes-skill.md` to reflect the new transition and its implications.
- Regenerated documentation to ensure accuracy, increasing the total tool count from 126 to 127.
- Added comprehensive tests for the `submitQuote` function to cover various scenarios, including idempotency and error handling for unpriced items.
2026-08-15 19:01:52 +02:00
Dave
c4b53bcc4b feat(mcp): add walk-in quote creation and enhance customer filtering
- Introduced a new `quotes.create` tool for creating quotes from scratch without an inbound email, allowing operators to generate quotes directly from the dashboard.
- Updated `customers.list` to support filtering by representative using either a 15-char PB user ID or a case-insensitive name/email match.
- Enhanced API documentation in `docs/api-mcp-layer.md` to reflect the addition of the new quote creation tool and updated customer filtering capabilities.
- Regenerated documentation to ensure accuracy, increasing the total tool count from 125 to 126.
2026-08-15 11:50:48 +02:00
Dave
80a08a742b feat(mcp): enhance API documentation and add new tools
- Updated `docs/api-mcp-layer.md` to reflect the current state of the API, including new endpoints and tools.
- Added `category` field to `QUOTE_ITEM_PATCH_FIELDS` and `ORDER_ITEM_PATCH_FIELDS` to support item categorization.
- Introduced 22 new MCP tools to cover previously missing REST functionalities, increasing the total tool count from 103 to 125.
- Improved descriptions for several existing tools to clarify their usage and parameters.
- Regenerated the API documentation to ensure accuracy and completeness.
2026-08-15 00:06:40 +02:00
Dave
48053a6205 feat: add support for stock purchase attachments
- update backend service to fetch stock purchase attachments
- refactor dedupe logic to avoid filename collisions between customer and stock attachments
- adjust PoCard to generate correct file URLs for stock attachments
- update test expectations for the modified service query
2026-08-14 18:25:05 +02:00
Dave
e27184245b refactor(agent scan route): remove debug logs and redundant sanity check
Remove dev-time console logging and the unnecessary stock_purchases collection probe check to clean up the route handler.
2026-08-14 17:55:58 +02:00
Dave
162e47e80e refactor: clean imports, update types and fix issues
- move CollectionConfirmValidationError imports from stock-purchase-items-service to collection-confirm-service
- update ConfirmRow type: add supplier_sku field and remove unused nested expand properties
- refactor sortRowsByField generic in orders-service to fix type access errors
- add non-null assertion to firstRow.stock_purchase_id in collection confirm page upload handler
2026-08-14 17:13:04 +02:00
Dave
59f245d798 feat(collection-confirm): add stock item edits
- Add new POST API endpoints at `/api/v1/stock-purchase-items/[id]/sku` and `/api/v1/stock-purchase-items/[id]/cost` for updating stock purchase item details
- Update ItemsTable to support cost and SKU edits for all row types, add supplier SKU column and editing for stock rows
- Extend PoCard to pass edit callbacks and add stock-specific invoice upload functionality
- Update all related test suites to cover new editing and UI features
- Adjust invoice upload messaging to differentiate between customer and stock purchase orders
2026-08-14 16:39:46 +02:00
Dave
9e74968b5e feat: add PO agent workflow and inline SKU editing
Add full purchase order agent tooling and supporting changes:
- Add server-side purchase-order-agent-service for scanning both stock and customer POs, with complete test coverage
- Add inline SKU and supplier SKU editing flows to stock order detail pages
- Update MCP API route to support new PO agent tools (purchase_orders.scan, diff, record_diff_report) and stock_purchase_items.list
- Fix flaky CollectionPoolPage tests by wrapping state-dependent assertions in waitFor
- Refactor CollectionPoolPage auto-collapse logic to avoid react-hooks lint trap and run exactly once on initial load
- Delete temporary .tmp-inspect-pool.mjs development script
2026-08-14 12:37:13 +02:00
Dave
541676dabb refactor(invoicing): remove intermediate trimmedOverride variable 2026-08-14 11:25:10 +02:00
Dave
999948861e feat: add server-side quote search & PO number format update
Implement new server-side ranked quote search endpoint at `/api/v1/quotes/search`, replacing the legacy 500-row client-side substring scan that missed older quotes and didn't rank results by relevance. The new search scores matches across quote identifiers, customer/company headers, and line item fields.
Update PO number generation and validation to use a dashless format (POYYYYMMNNNXXX) with per-company scoped sequence numbers, and ignore legacy PO formats when calculating new sequences.
Remove legacy `searchQuotesWithItems` function and its associated test file, then update the quotes page to use the new server-side search API.
Add comprehensive unit and integration tests for the new quote search service and updated PO number logic.
2026-08-14 11:15:38 +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
43de97501c feat(tv): add TV dashboard + refine workflows
Add full PIN-protected TV dashboard stack including auth, redirects, map view, APIs, and server services.

Also include these critical refinements:
- Overhaul PO number generation to use per-company monthly sequences
- Rework quote rejection to bounce quotes back to operator queues instead of marking them terminal
- Add post_processing order tab group for clearer workflow visibility
- Fix collection pool tooltip positioning, overflow clipping, and default collapsed state
- Refactor customer list API, clean up unused form props, and fix item intel popover target ID handling
- Add comprehensive tests for new components and update existing test expectations
2026-08-13 20:55:22 +02:00
Dave
dce9bfb04a feat(invoicing, dn): add surplus tracking, fix qty, update branding
- Fix resolveInvoiceLineDefaults to prioritize customer order quantity over supplier-collected quantity, ensuring invoices reflect what the customer actually ordered
- Add surplusQty utility function to calculate supplier MOQ overflow that should be booked to stock via /api/v1/stock/surplus-book
- Add surplus stock indicators in order and delivery note modals with descriptive tooltips explaining the surplus amount
- Expand supported letterhead codes to include WESTSCOTT GROUP, and configure all delivery notes to use the Westcott Group letterhead globally per business branding rules
- Improve error handling and logging in the delivery notes API POST route to include PocketBase error details and request context for easier debugging
- Add comprehensive unit tests for the updated invoice line logic and new surplusQty function to prevent regressions
2026-08-13 13:02:37 +02:00
Dave
8a224a7a24 feat(delivery-notes): add order disambiguation, fix PDF layout, update combine flow and qty resolution
- Fix delivery note PDF signature section overlapping page contents by removing fixed absolute positioning
- Add client-side picker for duplicate order numbers when merging delivery notes
- Add orderId parameter to combine delivery notes API for direct order lookup
- Update resolveDeliveryNoteLineQty to prioritize operator-edited quantity first
- Refactor combine delivery notes service and API to handle order lookup errors and candidate selection
2026-08-13 12:00:51 +02:00
Dave
617c5b3422 feat(delivery-notes): add inline qty editing and bulk combine features
Add inline quantity editing for delivery note items:
- New PATCH API endpoint and service function to update item quantities
- Optimistic UI updates on the detail page with error and loading states
- Input validation and status gating for draft/sent delivery notes

Add bulk delivery note combine functionality:
- New POST API endpoint and service to merge multiple draft/sent notes
- Row selection support in DataTable across pagination
- Confirmation modal with selected notes preview and optional order linking
- Automatic audit trail updates for merged and cancelled records

Extend OrderItem TypeScript interface with optional order expand field
Fix minor type casting issues in createAdHocDeliveryNote utility
2026-08-13 11:24:05 +02:00
Dave
4dd5898007 feat(delivery-notes): add pdf regeneration and fix ad-hoc pdf saves
Add new POST API endpoint at /api/v1/delivery-notes/[id]/regenerate-pdf to re-render and save delivery note PDFs from current line items. Add a "Regenerate PDF" button to the delivery note detail page with loading state and single-flight protection to prevent duplicate requests. Update ad-hoc delivery note creation to automatically attach PDFs to order items so print and download buttons work immediately after creation. Add helper utilities for resolving relation names and calculating line item quantities for PDF rendering.
2026-08-13 10:59:22 +02:00
Dave
18397bc9be feat(counter): add standalone counter order PWA
This commit adds a full unauthenticated PWA for in-store retail order taking:
- Dedicated `/new-order` route with complete order form, customer/company picker
- Public `/api/counter/*` endpoints for search, order creation, and photo uploads
- Photo capture and gallery selection with 10-photo per order limit
- Offline caching via dedicated service worker and app manifest
- Shortcut redirect at `/counter` for quick installation
- Comprehensive setup and usage documentation in `docs/counter-pwa.md`
- Minor cleanup to root layout metadata and service worker registration
- Small layout fix for collection pool row text truncation

Orders submitted via the PWA appear immediately in the main CRM's New tab, and the app is designed for trusted office kiosk phones without requiring user login.
2026-08-13 09:27:20 +02:00
Dave
c0ec3c99ef feat(orders,invoicing): add duplicate order cleanup and soft cancellation
Add a complete workflow for managing duplicate intake orders:
- Add new server API endpoints for single order cancellation, duplicate detection, and bulk duplicate cancellation
- Add client UI components for bulk duplicate cleanup and single order soft cancellation
- Add a "Duplicates" button to the orders dashboard for quick access to the cleanup tool
- Refactor invoicing module: rename customerNameSimilarity to nameSimilarity, update QB customer picker to use radio buttons and remove automatic preselection
- Add comprehensive test coverage for the new order service utilities
2026-08-13 09:02:27 +02:00
Dave
756597068e fix(delivery-notes): correct DN cancellation and filter issues
Update the cancel delivery note dialog description to correctly state items are marked as cancelled
Fix PocketBase expand field whitelisting for delivery items to avoid missing related entity data
Change order item status to "cancelled" instead of "completed" when cancelling a delivery note, with updated JSDoc explaining the choice of status
Update the buyer tasks PO sent filter and its regression test to cover all current ItemStatus values
2026-08-12 15:36:31 +02:00
Dave
9eea138662 feat(delivery-notes): add mark complete and cancel delivery note functionality
Add backend API routes and service functions for delivery note status updates with strict validation rules
Add UI action buttons and cancellation confirmation modal to the delivery note detail page
Update table layouts and styling across delivery notes and invoicing pages to improve responsiveness
Fix text overflow issues by adding whitespace-nowrap and break-words classes
Refactor order items table to display status inline and reduce redundant layout code
2026-08-12 08:55:56 +02:00
Dave
8856146023 fix(orders,delivery-notes): resolve build type errors after supplier removal 2026-08-12 01:42:19 +02:00