Commit graph

392 commits

Author SHA1 Message Date
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
f819478bc0 feat(order-review): edit item description, show email body, fix duplicate key
Three review-modal polish items rolled up:

1. Persisted line items now expose a description input in edit mode
   (mirroring the existing qty / selling inputs). The save route +
   the orders service's `updateOrderItemsPrices` accept the new
   `description` field and write `description` + `po_description`
   together (rejecting a blank description per row, with a
   per-row failure entry so the operator can retry just those).

2. The source-of-truth panel now ALWAYS renders the email body, not
   only when there are no attachments. The body is collapsed by
   default and capped at `max-h-40` so the email stays a secondary
   audit block instead of pushing the operator's real work
   (line items + attachments) down the page. The toggle button is
   unchanged.

3. The chip strip and file list inside the source-of-truth panel
   were using `key={a.id}`. `orderAttachementsToQuotes` synthesizes
   every attachment with `id: order.id` (PB file URLs need
   `/files/{collectionId}/{recordId}/{filename}`), so a multi-file
   order was throwing 'Encountered two children with the same key'.
   Switched to `key={a.filename}` — unique per file in the
   multi-file field, leaves the file URL untouched.

The Order type grew `email_subject`, `email_sender`,
`email_body`, and `email_message_id`. These columns exist in the
live PB (the local `pb_schema-2.json` snapshot is reference-only
per AGENTS.md, so the drift is expected). `getOrderReviewData`
prefers the order's own `email_body` and falls back to the
related `quote.email_body` for legacy orders that pre-date the
snapshot columns — quoting the operator's outbound is better than
showing '(no body)'.

Co-authored-by: Orca <help@stably.ai>
2026-08-04 08:51:22 +02:00
SJCoetz
69175e493d feat(orders): show resolved sales rep name on order list and sprint
The order list and order sprint pages were rendering the rep column
with a raw PocketBase record id, which the operator could not
recognize. The data layer now resolves the rep through the user's
name:

- New `resolveRepDisplayName` helper (`src/lib/order-rep.ts`) takes
  the customer record's `expand.rep`, the `customer.rep` relation
  id, the legacy `orders.rep` text snapshot, and an admin-resolved
  user map, and returns a name (or empty when only an unresolved PB
  id is present).
- The orders service now fetches the user list alongside the order
  page/counts query and resolves the rep on the way out, so the
  client receives a `repName` per row. New endpoints
  `/api/v1/orders/list` and `/api/v1/orders/[id]/review` expose
  the read model; the page and sprint calls go through them.
- The list card and the sprint table + mobile card render the
  resolved name. An opaque PB id is never displayed.

Migrates `/orders` off `pb-helpers.ts` per the data-layer
discipline, and adds a shared browser-session helper for
`Authorization: Bearer <PB token>` validation in route handlers
that serve the dashboard UI (vs. the agent API).

Co-authored-by: Orca <help@stably.ai>
2026-08-04 08:50:54 +02:00
SJCoetz
b66e1b6b6b feat(webview): probe upstream + show status badge in header
The /webview page embeds supplier portals in an iframe. When the
target sets X-Frame-Options: DENY or frame-ancestors 'none' the
browser silently renders a blank iframe with no visible signal
to the operator that the load failed — the page just looks empty
and it's not obvious whether the portal is down or whether the
operator is hitting an embed restriction.

Surface the most common cause up-front:

- Server-side HEAD probe before render: fetches the target URL
  with `redirect: "manual"` and a 5s timeout. The probe bypasses
  browser frame restrictions (it's a server-to-server call) and
  reports the raw HTTP status, including 302 redirects (counted
  as `ok` since suppliers often gate their portals behind a
  login redirect).
- Status badge in the header next to the URL: green dot +
  `up (200)` when reachable, amber dot + `HTTP <status>` for
  non-2xx, amber dot + `unreachable` on network / timeout
  failures. Title attribute carries the full probe detail.
- Banner placeholder inside the iframe container when JS is
  disabled (defensive — the existing iframe is JS-required).

The badge gives the operator an immediate "is this a portal
problem or an embed problem?" answer before they spend time
clicking around inside the iframe.

Co-authored-by: Orca <help@stably.ai>
2026-08-04 01:42:31 +02:00
SJCoetz
5291a044ed feat(collection-confirm): in-app invoice viewer with zoom + mobile layout
The photo-strip chips on the collection-confirm page used to call
`window.open(url, "_blank")` for the supplier invoice, kicking the
operator out to a new tab to verify the invoice against the items
table. That tab switch broke the side-by-side comparison the
operator actually wants to do — look at a line on the invoice,
look at the same line in the table, look back.

Replace that flow with a floating, draggable invoice viewer
that lives on the page:

- New `InvoiceViewer` component renders the PDF in an `<iframe>`
  against the existing `/api/v1/files/order_items/<id>/` route,
  which already serves PDFs with `Content-Disposition: inline`.
- Desktop: floating box anchored to the bottom-right (720×560),
  header is draggable. Position is clamped to the viewport so
  the close button can't be dragged off-screen. State lives in
  the component; not persisted to localStorage.
- Mobile (≤ 640px): full-screen panel (`100vw × 100dvh`,
  border-radius 0). Drag affordance, grip icon, and "Viewing"
  badge are dropped — they're meaningless on touch or when the
  panel fills the screen. Layout flips live on viewport change
  via a small `useMediaQuery` hook.
- Zoom controls: + / − / Reset with discrete levels
  (50 / 75 / 100 / 125 / 150 / 200 / 300%). The % display is
  itself a click-to-reset button. Implemented as
  `transform: scale()` on the iframe with a pre-shrunk CSS box
  (`${100 / zoom}%`) and a `${zoom * 100}%` scrollable wrapper
  so scrollbars appear at higher zoom levels. CSS-resize-only
  does not work for PDF iframes — the browser's native PDF
  viewer caches the initial render size and ignores post-load
  CSS resizes, so the page content never actually re-renders.
  `transform: scale()` works around that across Chrome / Safari
  / Firefox.
- "Open in new tab" header button is preserved as an opt-in
  escape hatch (landscape invoices, full-page viewing).

The PhotoStrip no longer opens a new tab by default. Clicking a
chip calls a page-level `onOpen` handler that toggles the viewer
(open / swap / close); the matching chip is highlighted with
`aria-pressed="true"` + a primary variant so the operator knows
which invoice is open. Clicking the active chip closes the
viewer. The chip falls back to `window.open(...)` only when no
`onOpen` is passed, so the strip stays usable in isolation.

Tests: 126 pass across the collection-confirm suite. New tests
cover viewer open / close (X + Esc), drag math + drag-bail-out
on header buttons, the active-state highlight, swap-on-different
chip, the in-viewer new-tab escape hatch, every zoom level +
clamping + reset, the PDF wrapper / iframe transform math at
multiple zooms, and the desktop ↔ mobile layout flip on a
mocked viewport change.

Co-authored-by: Orca <help@stably.ai>
2026-08-04 01:38:25 +02:00
SJCoetz
db2d43ae84 feat: web view page + sidebar link
Lightweight dashboard view surfaced under the 'Other' sidebar group
for client-facing use cases that need a stripped-down WFDash
surface (e.g. embed iframe, kiosk mode).

Co-authored-by: Orca <help@stably.ai>
2026-08-04 00:58:40 +02:00
SJCoetz
e409e28ff3 feat(order-items): below-22% margin approval workflow
Blocks order-item edits when the operator saves below the 22% markup
threshold and routes them through a manager-approval gate before the
item can be processed.

- new `order-item-margin-approval-service` saves the submitted draft,
  stamps `item_status='approval_needed'`, then fires the configured
  n8n webhook; webhook failure leaves the item blocked and returns
  `webhookFired=false`
- new POST /api/v1/order-items/[id]/request-margin-approval browser
  endpoint gates the same flow with source='order_process' |
  'order_sprint'
- new `MarginApprovalDialog` shared by both order-process and
  order-sprint process surfaces — single source of truth for the
  submit/audit-note flow
- new `approval_needed` / `approval_rejected` item statuses; the
  order-process save path now returns 'blocked' when an item is in
  approval_needed so the operator can't re-edit past the gate
- order-sprint-process-modal now uses the same dialog so the
  below-threshold flow is consistent across both entry points

Tests: 21 new (service + route + sprint modal), 1231 → 1242 passing.

Co-authored-by: Orca <help@stably.ai>
2026-08-04 00:58:33 +02:00
SJCoetz
76947a9711 chore(stock): remove Location from PDF and UI
The single office bin is the only location, so the field is
unused. Strip it from everywhere the operator sees it:

PDF (stock-item-stocktake-document.tsx):
- Drop Location column + gapMaxLocation from styles, header, rows
- Re-balance widths: Name 23% → 28%, Status 12% → 18%
  (the freed space makes Name properly readable again)

Stock list page (src/app/(app)/stock/page.tsx):
- Drop Location TableHead + TableCell
- Drop location state + input + submit-handler field from the
  New stock item modal
- Drop 'location' from the modal JSDoc field list

Stock detail page (src/app/(app)/stock/[id]/page.tsx):
- Drop the MapPin + item.location display in the header
- Drop the now-unused MapPin import from lucide-react

Fulfill-from-stock dialog (src/components/stock/fulfill-from-stock-dialog.tsx):
- Drop location from the search predicate
- Drop location from the placeholder text
- Drop the '· {item.location}' display in the row

Note: the location field stays on the StockItem type and PB
schema for backward compatibility — removing it from the type
would break any code that still references it, and removing it
from the PB schema is a backend migration. The field is just
no longer surfaced anywhere.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 19:34:56 +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
6033aba18d fix(stocktake-pdf): match /stock columns + add button on /stock page
The original Stocktake PDF columns (Name, SKU, Location, Unit,
Current qty, blank Actual, Supplier) didn't match what the
operator sees on /stock. They pointed at the wrong list — they
worked for a blank-column walk-through stocktake form, but the
user wants to print the /stock list as it appears on screen.

This rewrite makes the Stocktake PDF mirror the /stock table
column-for-column:
- SKU, Name, Supplier, Current, Min, Target, Location, Status
- Status derived via getStockStatus + STOCK_STATUS_STYLES so
  it matches the on-screen badge label
- Current qty now renders with the unit suffix (matching the
  on-screen cell)
- Target renders '—' when null/undefined/0, matching /stock

Also:
- Switched to landscape A4 to give 8 columns room (was
  portrait).
- Added a copy of the 'Download Stocktake PDF' button on the
  /stock page next to 'New stock item' — both /stock and
  /stock-orders now expose the same endpoint. Button uses the
  same plain <a target='_blank' rel='noopener'> pattern as
  the existing stock-orders buttons, with aria-hidden on the
  Download icon.
- Added a test on /stock for the new link (href, target,
  rel). 33/33 tests pass.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 18:18:58 +02:00
SJCoetz
b1c0c6030f fix(api): make list-pdf routes browser-accessible
Both new routes — GET /api/v1/stock-purchases/list-pdf and
GET /api/v1/stock-items/stocktake-pdf — were gated on
`assertAgentAuth`, which requires an `Authorization: Bearer
<AGENT_API_KEY>` header. Browser <a target='_blank'> clicks
don't send that header, so the buttons returned 401
'Unauthorized'.

The existing single-PO PDF route at
/api/v1/stock-purchases/[id]/pdf doesn't gate on
`assertAgentAuth` — it relies on the in-app session and uses
the admin PB instance. The two list PDFs are the same shape
(throwing reports, in-app origin, admin-scoped reads) so the
fix is to drop the gate. Auth now matches the single-PO PDF
and the invoicing delivery-note PDFs.

- Removed `assertAgentAuth(req)` from both routes; dropped the
  `agent-auth` import and the unused `req` parameter.
- Updated route JSDoc to describe the new auth contract and
  reference the single-PO PDF route as the precedent.
- Updated both route tests: dropped the 'returns 401 when auth
  is missing' case (no longer applicable) and the
  `AGENT_API_KEY` env-setup boilerplate. Test names updated
  to reflect the new contract — 'no auth required'.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 18:12:38 +02:00
SJCoetz
6bf8310ef6 fix(stock-purchase-send): use DASHBOARD_URL for outcomeUrl (was req.nextUrl.origin)
\`req.nextUrl.origin\` inside the Next.js process resolves to the
container bind address (e.g. \`0.0.0.0:3000\`), which the n8n
container can't reach — the v4 workflow's outcome callback
fails with \`ECONNREFUSED 0.0.0.0:3000\` and the stock PO gets
stuck on \`sending\` because the dashboard never receives the
status-flip callback.

Mirror the customer-PO route: build the callback URL from
\`process.env.DASHBOARD_URL\` (with trailing-slash trim). When
\`DASHBOARD_URL\` is unset the outcomeUrl is empty, and the
workflow's "outcomeUrl missing" branch fires cleanly.

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

Co-authored-by: Orca <help@stably.ai>
2026-08-03 17:44:38 +02:00
SJCoetz
91d8363380 fix(stock-purchase-send): render auto-PDF in-process instead of HTTP self-call
The send-email route's auto-PDF block did an internal
\`fetch(/api/v1/stock-purchases/[id]/pdf)\` to render a PDF when
the operator hit Send on a stock PO without one. Cloudflare's
loopback filter rejects that self-call with a network-level
error before Next.js sees it, surfacing as 'fetch failed' in the
modal (status rolled to draft, route returned 500 with code
'pdf_failed').

Extract the PDF generation into \`generateStockPurchasePdf\` on
the service layer so the send-email route can render the PDF
in-process. Same pattern as the customer-PO flow's
\`generatePurchaseOrderPdf\`. The route stays a thin shim that
maps \`StockPurchasePdfValidationError\` to 422.

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

Co-authored-by: Orca <help@stably.ai>
2026-08-03 16:51:07 +02:00
SJCoetz
86534f738a test(stock-orders): tighten download-button assertions + mark icons decorative
Two review follow-ups from the code-quality reviewer:
- Switch the two new PDF-button tests from getByTestId to
  getByRole('link', { name }) so visible-label regressions fail.
  Add a rel='noopener' assertion so the security contract on
  target='_blank' is locked down.
- Add aria-hidden='true' to both <Download> icons. The visible
  text already supplies each link's accessible name, but marking
  the icon decorative explicitly is more robust to future
  library behavior changes.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 16:43:59 +02:00
SJCoetz
1b2f4a4f52 feat(stock-orders): wrap cells + add download PDF buttons
Two UX improvements on the /stock-orders list:
- Number + Supplier cells now wrap long text instead of
  clipping. Uses whitespace-normal + break-words + max-w-[200px]
  on the cell, plus a row-level py-3 bump so wrapped cells look
  balanced with single-line rows.
- Two new outline buttons next to 'New Stock Item':
  'Download Stocktake PDF' -> /api/v1/stock-items/stocktake-pdf
  'Download Orders PDF' -> /api/v1/stock-purchases/list-pdf
  Both open the PDF in a new tab via target=_blank; the browser
  handles the download.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 16:40:24 +02:00
SJCoetz
e025b340b8 fix(api): use SAST date for PDF filenames + tighten list-pdf tests
Two changes:
- Filename dates now use the Africa/Johannesburg timezone rather than
  UTC, so a download generated at 00:30 SAST gets today's date
  (not yesterday's). The renderer call still uses ISO UTC for
  generatedAtIso — that's an audit timestamp, not a human-readable
  date. One 'now' is captured at the top so the two values can't
  drift across a millisecond boundary.
- The 200 tests now assert Content-Disposition (including the
  filename), Cache-Control, the service arguments, and the
  renderer arguments. vi.useFakeTimers pins the clock so the
  filename is deterministic.

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

Co-authored-by: Orca <help@stably.ai>
2026-08-03 16:37:12 +02:00
SJCoetz
bbe3e1bd1f feat(api): GET /api/v1/stock-purchases/list-pdf + stocktake-pdf
Two list-report endpoints wired to the new PDF renderers. Both
auth via assertAgentAuth, ignore query params (always all rows),
and return application/pdf with attachment Content-Disposition.
The Stocktake PDF is sourced from the new listStockItemsForStocktake
service helper; the Orders PDF reuses the existing listStockPurchases
with perPage=200.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 16:29:57 +02:00
SJCoetz
18e1d97b6c test(stock-orders): tighten total-value + edit-reset assertions
The total-value case now locates each cell by its column header so a
regression that swapped Ordered (R) and Received (R) would fail. The
edit-draft case now closes the modal after Edit and asserts
editPurchaseId resets to '', so a regression that leaked the id into
a subsequent 'New' click would fail.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 15:18:39 +02:00
SJCoetz
760f088ec9 test(stock-orders): move page tests to StockOrdersTable + add total-value case
The seven pre-existing assertions on the quick-draft modal and edit
button now exercise the new client component. New case confirms
ordered_value/received_value render in the row. Uses Intl-format
substring assertions because the currency glyph/spacing varies by
ICU version.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 15:13:12 +02:00
SJCoetz
b89a4a7fa3 fix(stock-orders): preserve header nav + sanitize error message
- Header (title, description, Reorder, + New stock order) now
  renders unconditionally, so operators can navigate away even when
  the list query throws.
- Full error is logged server-side via console.error; the user
  sees a generic 'Failed to load stock orders. Please try again.'
  message that doesn't leak PB URLs, admin env var names, or
  filter syntax.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 15:07:46 +02:00
SJCoetz
dedd5013b9 refactor(stock-orders): migrate list page to Server Component
Reads searchParams (status, page), validates both, calls
listStockPurchases via the admin PB, and renders the new
StockOrdersTable client component. Drops the legacy direct-PB
read path per AGENTS.md data-access discipline.

The old page.test.tsx is removed; its assertions are mirrored
in the new StockOrdersTable.test.tsx (next task).

Co-authored-by: Orca <help@stably.ai>
2026-08-03 15:02:59 +02:00
SJCoetz
fcb15a20e2 fix(stock-orders): keyboard a11y on pagination + soft row nav
- Disabled pagination links (Previous at page 1, Next at last page)
  now also set tabIndex=-1 and preventDefault on click, so keyboard
  users can't activate them when at the boundary. aria-disabled +
  pointer-events-none blocked only mouse clicks.
- Row click uses useRouter().push() instead of window.location.href
  for a soft navigation that preserves client state (consistent
  with the original /stock-orders page).

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:51:41 +02:00
SJCoetz
0c556a0513 feat(stock-orders): extract StockOrdersTable client component
Holds the QuickDraftModal open state, renders status filter tabs as <Link>s to ?status=..., and adds two new columns (Ordered R, Received R) showing per-row aggregates from the service layer.\n\nReplaces <Button asChild><Link/></Button> with <Link className={cn(buttonVariants(...))}> because the project's Button (built on @base-ui/react/button) doesn't support asChild. Visually identical; preserves navigation semantics.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:46:50 +02:00
SJCoetz
469b2c0460 fix(api): preserve auth status + guard pagination against NaN
- Catch block now preserves error.status so assertAgentAuth's 401
  surfaces as 401, not 500. Matches sibling /api/v1/stock-purchases
  pattern.
- page/perPage parsing falls back to documented defaults on
  non-numeric or zero input rather than passing NaN to the service.
- Add 2 tests covering the auth-status and pagination boundaries.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:38:51 +02:00
SJCoetz
8cb4e9a60c feat(api): GET /api/v1/stock-purchases/list with value aggregation
Thin wrapper around listStockPurchases that validates the status
param against STOCK_PURCHASE_STATUS_VALUES (400 on bad input) and
caps perPage at 200. Mirrors the agent-auth pattern used by the
other v1 list routes.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 14:31:22 +02:00
SJCoetz
aea1ab67f3 fix(po-send): push dashboardBaseUrl + outcomeUrl on n8n webhook body
The n8n Code node that posts the SMTP outcome back to the
/api/v1/purchase-orders/[id]/send-email/outcome route couldn't
read env vars at runtime (N8N_BLOCK_ENV_ACCESS_IN_NODE=true in
docker-compose), so it composed the callback URL from fields on
the webhook body. The route never pushed dashboardBaseUrl or
outcomeUrl, so n8n fell back to a relative URL, the request
died, n8n returned HTTP 500, and the route flipped the PO to
mail_failed with a misleading '[webhook] Webhook returned HTTP
500' message.

DASHBOARD_URL is already defined in .env (labelled 'used by n8n
workflows to call back into this app') — the route just never
read it. Now the route pushes both the dashboard base and a
pre-built absolute outcome URL, trimmed of trailing slashes, and
omitted (not empty) when DASHBOARD_URL is unset so the existing
'missing' branch in n8n fires clearly.

A new test file asserts all three cases: with DASHBOARD_URL,
with a trailing slash, and without the env var.

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

Co-authored-by: Orca <help@stably.ai>
2026-08-03 11:23:40 +02:00
SJCoetz
faaa47a4fe test(buyer-tasks): lock wire shape of mail_failed group in po_draft
The route is a pass-through, but the smoke test guards against a
future refactor silently dropping the new emailError field — the
operator's retry affordance depends on it.

Co-authored-by: Orca <help@stably.ai>
2026-08-03 10:55:10 +02:00
SJCoetz
8616d5dbef fix(buyer-tasks): add emailError to route test fixture
The BuyerTaskGroup type gained an emailError field in the previous
commit. This pre-existing fixture constructs a BuyerTaskGroup
literal and now needs the field to compile. No behaviour change
in the test — emailError: null matches a clean draft.

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

Co-authored-by: Orca <help@stably.ai>
2026-08-03 10:52:58 +02:00
SJCoetz
4444842ef5 feat: collection-map updates, todays-stops, quote/approval rework, buyer-tasks, e2e
- Add todays-stops test and component to collection-map
- Expand collection-map sidebar and pin-model
- Rework orders/[id]/process page
- Add quotes/[id]/page and process pages
- Add quotes/approvals/[id] page
- Add buyer-tasks generate-po route
- Add quotes partial-approve and pdf routes
- Add purchase-orders [id]/pdf route
- Add e2e create-flows spec
- Add superpowers plan doc
- Update pb_schema-2.json
- Update form components (company, customer, order, quote, supplier)
- Update UI components (button, command, dialog)
- Add collection-pool test components

Co-authored-by: Orca <help@stably.ai>
2026-08-03 09:33:45 +02:00
SJCoetz
0064555f22 chore(collection-confirm): cleanup nice-to-haves from final review
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 03:04:23 +02:00
SJCoetz
d6af21193c fix(collection-confirm): scope costOverrides to current PO in dialog
Co-authored-by: Orca <help@stably.ai>
2026-08-03 02:48:17 +02:00
SJCoetz
fea67b15ba fix(collection-confirm): hoist onCostChange out of setEdit updater + preserve null rollback
Co-authored-by: Orca <help@stably.ai>
2026-08-03 02:46:28 +02:00
SJCoetz
e2896264f4 fix(collection-confirm): add route validation + server-side actor derivation
Co-authored-by: Orca <help@stably.ai>
2026-08-03 02:35:58 +02:00
SJCoetz
dbf02e2511 fix(collection-confirm): restore Book to stock for stock rows + cost guard
Restores the 'Book to stock' action that the page rewrite in f7d3032 dropped from collection-confirm/page.tsx, and routes the cost pencil away from stock rows so the existing cost-edit endpoint (which queries order_items) can't 404 on a stock_purchase_items id.

- New POST /api/v1/collection-confirm/book-stock endpoint + route tests.
- New bookStockRows(rowIds, { actor }) service that mirrors the pre-rewire bookStockLineForRow flow per-row: ledger → cache increment → line update → auto-derive parent send→received.
- PoCard footer now switches on the row mix: stock rows → 'Book to stock'; customer rows → 'Confirm for invoicing'; mixed → both.
- ItemsTable hides the cost pencil when row.source === 'stock' (cost cell stays readable).
- Page-level handleBookStock wired through to the new endpoint with a reload on success.

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

Co-authored-by: Orca <help@stably.ai>
2026-08-03 02:29:20 +02:00
SJCoetz
1edaf36023 test(collection-confirm): update page tests for PoCard layout
Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:33:31 +02:00
SJCoetz
f7d3032438 feat(collection-confirm): rewire page to use PoCard + remove Adjust Quantity
- Replace inline row rendering (ConfirmCard, QuantityDialog, NoteDialog,
  StockItemPickerDialog) with <PoCard> composition: PhotoStrip +
  ItemsTable + Confirm button.
- Cost edits dispatch POST /api/v1/order-items/{id}/cost with optimistic
  UI + rollback on failure.
- Confirm click opens ConfirmInvoiceDialog and POSTs to
  /api/v1/order-items/confirm with itemIds, invoice, costOverrides, actor.
- Keep AttachDialog + ReturnToPoolDialog component definitions per spec;
  render with open=false (no per-row buttons in PoCard layout).
- Switch load() to /api/v1/collection-confirm via fetch().
- Stock-source Book-to-stock, surplus chip, Return-to-pool, Adjust
  Quantity per-row actions are removed from the page body; PoCard is
  the primary grouping. Tests for those actions will fail and are
  scheduled for task 14.

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

Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:25:31 +02:00
SJCoetz
7360e14c58 refactor(collection-confirm): extract ConfirmInvoiceDialog component
Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:09:37 +02:00
SJCoetz
3f228808ea feat(collection-confirm): add POST /api/v1/order-items/confirm route
Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:03:52 +02:00
SJCoetz
fada6f155f feat(collection-confirm): add POST /api/v1/order-items/[id]/cost route
Co-authored-by: Orca <help@stably.ai>
2026-08-03 01:01:30 +02:00
SJCoetz
e860611980 fix(collection-confirm): log server errors and return generic message to client
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:59:02 +02:00
SJCoetz
bb10f1350b feat(collection-confirm): add GET /api/v1/collection-confirm route
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:51:37 +02:00
SJCoetz
c856db1617 fix(collection-confirm): thread null cost through appendCostNote to preserve audit on null→0
Co-authored-by: Orca <help@stably.ai>
2026-08-03 00:46:50 +02:00
SJCoetz
06d8294c77 refactor(collection-confirm): make buildConfirmPatch take options bag with costOverride
Co-authored-by: Orca <help@stably.ai>
2026-08-02 23:59:22 +02:00
SJCoetz
7c807f3c18 fix(collection-confirm): render cost category label correctly
Co-authored-by: Orca <help@stably.ai>
2026-08-02 23:57:06 +02:00
SJCoetz
529e08eb2a fix(collection-confirm): widen NoteEntry.body and dedup attachments within row
Co-authored-by: Orca <help@stably.ai>
2026-08-02 23:52:21 +02:00
SJCoetz
7e1f22f363 feat(collection-confirm): add appendCostNote and dedupeAttachments helpers
Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-08-02 23:48:51 +02:00
SJCoetz
7136623331 feat(orders): expand rep on customers fetch for review modal
The customers getFullList in OrdersReviewModalMount now expands
both 'company' and 'rep'. The Review Order modal's customer
cascade reads selectedCustomer.expand.rep.name to populate the
Rep label and writes it to orders.rep on save.

No other consumer of the customer list reads c.expand.* — the
scout confirmed this is safe.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-31 15:31:21 +02:00
SJCoetz
d3c3e3908d feat(orders): forward company and rep in review-save route
Destructure company and rep from the request body and forward them
through updateOrderReviewFields' headerPatch. The service layer
(Task 1) already accepts and validates these fields; the route now
just plumbs them.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-31 15:29:43 +02:00
SJCoetz
2cb6491654 feat(stock-orders): show SKU in line table on stock-order detail
Adds a 'SKU' column between Description and Ordered in the lines
table on /stock-orders/[id]. The column reads from
line.expand.stock_item.sku (the relation was already accessible via
the type, but the page was not expanding it on the fetch).

Falls back to an em-dash when expand is missing (e.g. an item whose
stock_item was deleted) so the row never renders an empty cell.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-31 14:20:17 +02:00
SJCoetz
7b3601eab2 test(stock-orders): Edit button row coverage
Asserts the Edit button appears on draft rows only, and that clicking
it opens the modal with the right editPurchaseId. The mock stops
forwarding the click to the row's onClick via the existing
e.stopPropagation() pattern in the implementation.

Co-authored-by: Orca <help@stably.ai>
2026-07-31 14:01:40 +02:00
SJCoetz
1d2499a17a feat(stock-orders): Edit button on draft PO rows
Adds a Pencil-icon ghost button to the trailing cell of every draft
row. Clicking it opens the QuickDraftModal in edit mode for that
purchase, scoped to that draft's lines. e.stopPropagation prevents
the row's detail-page navigation from also firing.

Co-authored-by: Orca <help@stably.ai>
2026-07-31 14:00:03 +02:00
SJCoetz
811b2dca1c feat(stock-quick-draft): PATCH /api/v1/stock-quick-draft/line/[lineId]
Adds a PATCH handler next to the existing DELETE handler. Validates
JSON body, maps service result reasons to HTTP status codes, and uses
admin PB to bypass user-side collection rules.

Co-authored-by: Orca <help@stably.ai>
2026-07-31 13:40:06 +02:00
SJCoetz
f9a0d08dab fix(tests): mock scaffold destructure + collection-pool + pocketbase startup
- orders-service.test.ts: destructure capture-group bug on regex match
  (was assigning m[0] to 'field', so filter clauses never matched).
  Two pre-seed tests were masked by the bug.
- quotes-service.test.ts: createQuoteFromEmail now returns 0
  attachments; the n8n flow uploads them in a follow-up step. Update
  the assertion that still expected the old embedded-base64 count.
- collection-pool/page.test.tsx: tests assume ONE supplier group with
  multiple rows; supply that shape and scope the selectors. Also wait
  for data load in the chevron test and gather list items across all
  <ul> elements in the sort control helper.
- pocketbase.ts: wrap authStore.onChange registration defensively so
  the test mock (no authStore) doesn't throw at startup.
- stock-orders/page.tsx: drop the re-export of
  STOCK_PURCHASE_STATUS_VALUES on the page module — Next.js page
  route files only allow the default export.
- collection-confirm/view-model.test.ts: buildConfirmNote input is
  Pick<InvoiceDetails, 'date' | 'number'>; drop the extra setBy fields.

Co-authored-by: Orca <help@stably.ai>
2026-07-31 00:01:24 +02:00
SJCoetz
4cf9c782e6 feat(stock-quick-draft): mount modal on /stock-orders via New Stock Item button
Co-authored-by: Orca <help@stably.ai>
2026-07-30 23:33:12 +02:00
SJCoetz
6329a100e7 fix(stock-quick-draft): isolate new-SKU suppliers, fix empty-string pick, service layer
Co-authored-by: Orca <help@stably.ai>
2026-07-30 23:31:21 +02:00
SJCoetz
dfae2d0c8b feat(stock-quick-draft): no-history supplier dropdown fallback
Co-authored-by: Orca <help@stably.ai>
2026-07-30 23:18:48 +02:00
SJCoetz
8acb6dcf95 fix(stock-quick-draft): dedicated /drafts endpoint with nested shape, browser-auth
Co-authored-by: Orca <help@stably.ai>
2026-07-30 23:11:27 +02:00
SJCoetz
921ea4e63d fix(stock-quick-draft): parseInt limit + exhaustive status map
Co-authored-by: Orca <help@stably.ai>
2026-07-30 22:11:55 +02:00
SJCoetz
0515b1ce7c feat(stock-quick-draft): API routes (search, history, add-line, delete-line)
Co-authored-by: Orca <help@stably.ai>
2026-07-30 22:07:37 +02:00
SJCoetz
3b8a299b15 fix(orders): use text-warn (not text-warn-foreground) for icon buttons
text-warn-foreground is the dark contrasting text colour meant to
sit ON TOP of a warn-tinted background (e.g. an alert panel).
Used as the icon colour on a plain card it renders nearly black.

text-warn is the actual amber/orange hue — what we want for the
Review + Open icons on the order list.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-30 15:34:12 +02:00
SJCoetz
f17f4d08b2 style(orders): tint Open button warn-amber to match Review
Both right-hand action buttons on the order list card now share the
warn-amber treatment, so they pop against the muted card background
and the operator can spot them at a glance. The icons (AlertTriangle
for Review, ExternalLink for Open) keep the actions distinguishable.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-30 15:32:33 +02:00
SJCoetz
d3f8242e4f fix(orders-sprint): transition pending→processed, merge Unknown tabs, polish layout
Three issues on /orders/sprint, fixed together:

1. 'Process' was a no-op for the buyer queue. The modal saved
   supplier + cost + selling + markup + notes, but never wrote
   item_status or po_description, so items stayed 'pending' and
   reappeared on the next refresh — never reaching buyer-tasks.
   Now mirrors the canonical /orders/[id]/process saveItem gate:
   pending → processed only when supplier + cost + po_description
   are all set, and items already past 'processed' (stock_booked,
   po_generated, etc.) are never downgraded.

2. Two 'Unknown' tabs side-by-side: a hardcoded 'Unknown' tab for
   blank categories and a PB-managed 'Unknown / Other' tab for
   category id zgr50fji47r36wu. They had the same meaning and
   different colours. Now resolved by slug ('cat_unknown') and
   bucketing both into a single tab, matching how /sprint already
   tries to.

3. Layout polish: dark-mode-safe category pills (withAlpha helper),
   hidden redundant Category column when filtered to one tab,
   empty <th> given an aria-label, supplier-less cells no longer
   render an empty Badge box, and client-side bucketing so tab
   switches no longer trigger a full refetch + layout shift.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-29 23:06:11 +02:00
SJCoetz
6fa658df62 feat(buyer-tasks): require audit note when marking ready
The Mark ready action on the buyer-tasks page now mirrors the
collection-pool flow: the operator is asked for a free-text note
(name of who they spoke to, collection number, etc.) before the
items transition to ready_delivery. The note is required, is trimmed,
and is written to order_items.notes alongside the status change so
there's an audit trail.

- Service: markGroupReady now takes a required note param, trims it,
  rejects empty values, and persists the trimmed text to notes.
- Route: /api/v1/buyer-tasks/mark-ready returns 400 when note is
  missing or whitespace-only, and forwards the value to the service.
- Page: BuyerTasksPage swaps the no-input MarkReadyModal for the
  shared MarkReadyDialog, threads the note through the POST body,
  and surfaces server-side validation errors back into the dialog
  rather than only as a toast.
- The old MarkReadyModal is removed.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-29 22:38:00 +02:00
SJCoetz
6259ed8407 fix(orders): transition pending → processed on save when priced
The buyer-tasks Needs PO tab filters by
  item_status = 'processed' && purchase_order = ''
but the order process page's saveItem() never updated item_status,
so priced items stayed pending and never surfaced on the buyer
board.

Save now promotes pending → processed when the operator has filled
supplier + cost_price + po_description. Items already past processed
(stock_booked, po_generated, ready_delivery, …) are never downgraded.
The stock-fulfil branch writes processed too as a no-op for the
case where the subsequent stock/fulfil call fails — it will be
overwritten with stock_booked on success.

Co-authored-by: Orca <help@stably.ai>
2026-07-29 22:02:33 +02:00
SJCoetz
76f1d79082 fix(buyer-tasks): prefill SendPoModal fields from buyer-tasks page
Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:45:12 +02:00
SJCoetz
3cfb2ac00c feat(buyer-tasks): extract send-po modal to shared component
Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:40:50 +02:00
SJCoetz
1486e10bbe feat(buyer-tasks): page route + sidebar entry
Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:19:33 +02:00
SJCoetz
bcda00ac46 feat(buyer-tasks): POST mark-ready route
Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:04:32 +02:00
SJCoetz
4f0be21732 feat(buyer-tasks): POST generate-po route
Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:03:26 +02:00
SJCoetz
275a47e28f feat(buyer-tasks): GET /api/v1/buyer-tasks route
Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:02:04 +02:00
SJCoetz
74c73e9f17 feat(orders): totals + add/remove items in review modal
Two related enhancements to the orders review modal:

* Tier 1 — header totals: the "Order items" header now shows the
  item count plus the cost and selling totals (qty*price summed
  across the live draft state). Uses formatCurrency from
  pb-helpers.ts for the ZAR formatting.

* Tier 2 — add/remove items: the edit-mode items list now has an
  "Add item" button (Plus icon) and a per-row Trash icon. New rows
  get a sentinel id like "__new_1" and a left-edge accent stripe;
  rows flagged for removal flip to a strikethrough + reduced opacity
  with disabled inputs (toggle button flips to an Undo2 icon).

  On save, the route posts to /api/v1/orders/[id]/review-save with
  new itemsToAdd and itemsToRemove arrays. The service layer adds
  createOrderItems + softFlagOrderItems handlers (set
  item_status="removed" — operator confirmed the value is in the
  PB admin schema). The modal re-keys the new drafts with the
  returned PB ids so a follow-up save treats them as existing
  items.

  Partial-failure UI now has three sections (updates / adds /
  removes) sharing a labelled FailureSection helper. Response
  envelope now includes itemsAdded and itemsRemoved alongside the
  existing items block, plus a flat partialFailures array for
  the toast summary.

  Type cleanup: "removed" added to ItemStatus and the
  ITEM_STATUS_VISUAL map.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-29 18:54:49 +02:00
SJCoetz
70eb59e17a feat(orders): add Order Sprint page + process modal + sidebar entry
Wires a new /orders/sprint surface that lists pending order items
(purchase_order = '' && item_status = 'pending') grouped by category,
mirrors the quote-sprint workflow. Click a row to open
OrderSprintProcessModal for assigning a supplier, cost, and emitting
a purchase order.

Sidebar gets an "Order Sprint" entry under the Orders group.

Notes:
- No automated tests yet (manual smoke test recommended).
- OrderSprintItem type is defined locally in both page.tsx and the
  modal — duplicated but consistent; extract to types.ts in a follow-up.
- Page + modal use client-side getPocketBase (Pb is filtered to the
  user); the counts route is server-side.

Co-authored-by: Orca <help@stably.ai>
2026-07-29 17:50:21 +02:00
SJCoetz
d6570dca71 fix(orders): source attachments from order_attachements field
The orders review modal's source-of-truth panel now reads from the
order_attachements multi-file field on the orders collection
(pbc_3527180448) instead of the quote_attachments collection rows
on the parent quote. Older orders were migrated on 2026-07-29; the
order-level field is authoritative going forward.

- Added orderAttachementsToQuotes() helper in
  src/app/(app)/orders/page.tsx that synthesises the
  QuoteAttachment[] shape from the order's file-field array, deriving
  kind (pdf/image/doc) and content_type from the filename extension.
  PB file fields don't carry a content-type from the wire, so we
  infer from extension (png/jpg/gif/webp/svg/bmp/pdf/docx/txt).
- Replaced the quote_attachments collection query in
  OrdersReviewModalMount with the helper call.
- Added orders + pbc_3527180448 to the ALLOWED_COLLECTIONS whitelist
  in the file proxy route so the review modal's PDF/image preview
  iframes can stream through /api/v1/files/... .

Co-authored-by: Orca <help@stably.ai>
2026-07-29 17:41:29 +02:00
SJCoetz
22df511293 merge
Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:49:37 +02:00
SJCoetz
064f13a7ca fix(orders): use client PB for customer list in modal mount
getAdminPocketBase reads PB_ADMIN_EMAIL/PB_ADMIN_PASSWORD which are
server-only env vars. The modal mount is a client component, so the
admin client would fail to authenticate at runtime. The users PB
session already has read access; switch to getPocketBase.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:19:40 +02:00
SJCoetz
8b6b10d64b fix(orders): remove assertAgentAuth from review-save route (browser-callable)
This route is invoked from the in-app review modal, not from an external
agent integration. The browser session is already authenticated via PB;
calling assertAgentAuth here caused every modal save to 401.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:19:27 +02:00
SJCoetz
a80721c6e0 fix(orders): review modal mount review fixes (customer fallback, dead ai wiring)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:11:11 +02:00
SJCoetz
de87eb312e feat(orders): warn-amber review button + modal mount on orders list
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:04:00 +02:00
SJCoetz
f4d9601881 chore(item-history): fix auth docstring, log AI read failures
Co-authored-by: Orca <help@stably.ai>
2026-07-29 15:03:24 +02:00
SJCoetz
e5a9221c3e feat(api): add /quote_items/[id]/item-history endpoint
Co-authored-by: Orca <help@stably.ai>
2026-07-29 14:59:50 +02:00
SJCoetz
7df7a8850f feat(item-history): add getItemHistory service with 5 PB queries
Co-authored-by: Orca <help@stably.ai>
2026-07-29 14:51:58 +02:00
SJCoetz
cd33e425fc fix(orders): review-save route review fixes (auth, 404, customer, fixture)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-07-29 14:49:54 +02:00
SJCoetz
3d8d39af2a feat(orders): POST /api/v1/orders/[id]/review-save endpoint
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-07-29 14:40:01 +02:00
SJCoetz
811493bedb feat(collection-pool): add page and service function
Restores the /collection-pool feature in this codebase.

- src/lib/server/orders-service.ts
  - Re-adds `listCollectionPool({ status, page, perPage })` which
    queries the `collection_pool` PB collection via the admin
    client (filter on pool_status, sort -created, expand
    order_item/order/supplier/drv_id). MCP route.ts already
    imported this — without it, the MCP route failed to
    compile.

- src/app/(app)/collection-pool/{page,components,view-model}*.tsx
  - Re-creates the page directory (5 files, 2 315 lines) carrying
    the supplier-group + per-stop checkbox UI, the
    `CollectionPoolRowSource = "customer" | "stock"` union, the
    assignment-state badge helpers (buildProximityMarkerMap,
    buildSupplierGroups, buildVisibleRows), and the
    MarkReadyDialog / AssignDriverDialog wiring.

- src/app/api/mcp/route.test.ts
  - Adds `const listCollectionPool = vi.fn()` and exposes it in
    the vi.mock factory so the existing collection_pool.list test
    cases (and the bulk_update_quote_items ones that call
    `.mockReset()` on every fixture) stop crashing with
    'ReferenceError: listCollectionPool is not defined'.

Not in this commit (kept narrow per request):
- The /api/v1/collection-pool route handler and its test
- The 'Collection Pool' entry in the sidebar nav
- The migration script that creates the live `collection_pool`
  PB collection (it doesn't exist on the cloud PB instance
  yet; the page render relies on it being created separately
  or via `npm run pb:migrate:orders-v2`)
2026-07-27 15:04:30 +02:00
SJCoetz
72fede1891 Add one-click "Process" action to quotes list skipping detail page
- New `handleProcess` claims new quotes inline and navigates straight to the process workspace
- Primary "Process" button with secondary "Open" for claimable quotes; other statuses show "Open" only
- Mobile card includes a full-width Process shortcut for new/claimed-by-me quotes
2026-07-27 10:43:24 +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
0ad9c42df4 style(quotes): stack company under customer in the same column
Customer and Company used to be two side-by-side DataTable columns,
which cost ~120 px of horizontal width and made rows feel sparse when
the operator is just eyeballing who's-who across many quotes.

Now the Customer column carries both names in one cell: customer in
the default font-medium row, company underneath in text-xs
text-muted-foreground so the visual hierarchy reads as
"Primary person  >  Their organisation".

Effect on row layout:
- Total table column count drops from 7 to 6.
- Row width shrinks on desktop, freeing space for the larger
  Quote # column (which carries the follow-up / replied / aged
  chips and was getting crowded).
- The mobile card render (used below the sm breakpoint) gets the
  same stacking so the company line shows on every viewport.

Edge cases handled:
- Both names empty             → '—' muted placeholder
- Customer present, no company → just the customer name
- Company present, no customer → '—' for the customer, company below
- Very long names              → truncate via className='truncate'

The existing standalone Company column is removed; downstream
code (DataTable, mobileCardRender) reads from the same expanded
fields so no extra fetches.
2026-07-27 00:04:03 +02:00
SJCoetz
e626292b04 style(quotes): tighter tab gap; header buttons always inline + dev: whitelist 192.168.1.42
quotes/page.tsx
- Reduce inter-tab gap from gap-1 (4px) to gap-0.5 (2px) so the
  pill strip reads as one cohesive filter row instead of a row of
  loose badges.
- Drop `flex flex-col sm:flex-row items-start sm:items-center` on
  the page header in favour of `flex flex-row flex-wrap
  items-center`. On mobile widths (375px tested), the Approvals
  and "+" buttons now stay inline with the "Quotes" title instead
  of dropping below it.

next.config.ts
- Add 192.168.1.42 to allowedDevOrigins. The dev server was
  warning when the user hit the app from a LAN host (their iPad,
  visible in ps as `192.168.1.42:newoak->ipad.home:55199`). Now
  whitelisted explicitly alongside the existing 192.168.1.122 /
  192.168.1.128 entries.
2026-07-26 23:56:23 +02:00
SJCoetz
6d63da70d0 fix(quotes): TabsList height propagates correctly on mobile, search bar no longer overlaps tabs
On mobile widths (verified at 375px viewport), the 9 status tabs
wrap to 4 rows but the TabsList DOM reported height = 32px (one
row), so space-y-6 placed the search bar at y=254 — directly
overlapping tab rows 3 and 4.

Root cause: the @base-ui/react/tabs Root has `flex gap-2
data-horizontal:flex-col` baked in. When the wrapped TabsList's
intrinsic height (4 rows of 28px = 112px+) didn't propagate to the
flex-column parent, the next sibling was positioned based on the
shorter (single-row) computed height.

Fix: add `min-h-fit` to the TabsList override so the flex item
forces its content height regardless of the column's intrinsic
shrinkage. Verified at 375px width: tabs now correctly occupy
y=198-338 (4 wrapped rows), search bar at y=362 — clear 24px gap
with no overlap.

Also tightened the override:
- `inline-flex` -> `flex` (so the container is a full-width block
  flex item rather than inline fit-content)
- `items-center` -> `items-start` (so wrapped children don't
  cluster in the middle of an under-sized box)
- added `w-full` and `min-h-fit` to lock the height floor

`pb-4` from the earlier 'some space between tabs and search' tweak
is kept — now correctly visible as 16px additional breathing room
above the search bar's 40px input.

Scoped to the Quotes page only (one-line override) so other pages
that use the default TabsList (login grid, pricelist, customers)
are unaffected.
2026-07-26 23:50:48 +02:00
SJCoetz
d33b61fb52 style(quotes): add breathing room above search bar; tint search bar warn
Two small visual tweaks on the Quotes screen:

1. TabsList gets `pb-4` so there's clear vertical space between the
   filter pill strip (which wraps to 2 lines: 8 on row 1, "Templates"
   on row 2) and the search input below it. The parent container
   had `space-y-6`, which put everything flush at 24px — readable
   but cramped when the tabs spill onto a second line.

2. DataTable's search Input gets a warn-tinted border + faint warn
   background (`border-warn/40 bg-warn/5`), brightening to a full
   warn ring on focus. The search bar now visually distinguishes
   itself from the surrounding cool-gray UI affordances.

Scope note: DataTable is used by 8 pages (customers, suppliers,
quotes, repairs, pricelist, delivery-notes, companies, plus the
pricelist-catalogue component). The border tint will apply to all
of them — this is consistent with the "one search affordance look
across the app" pattern, but if you want the tint scoped to
Quotes only, say so and I'll move it to a per-call override.
2026-07-26 23:40:43 +02:00
SJCoetz
bd2a56ce9f chore(quotes): route next-number auth-free + replace deprecated client counter call in quote-form
src/app/api/v1/quotes/next-number/route.ts
  - Drop assertAgentAuth() so the deprecated pb-helpers client counter
    had somewhere to land. The route is intentionally open (see the
    long doc comment); other /api/v1/* routes still gate on the bearer
    token via their own call to the helper. Browser cannot send
    AGENT_API_KEY without exposing the secret, and exposing it via
    NEXT_PUBLIC_ would defeat the rest of the agent surface.

src/components/quote-form.tsx
  - Replace `await generateQuoteNumber()` (the deprecated
    read-then-write client counter from src/lib/pb-helpers.ts) with
    a `fetch('/api/v1/quotes/next-number')`. Falls through to the
    existing Math.random fallback if the endpoint errors. Removes
    the race where two concurrent form submits could both pick the
    same next number, since the server endpoint uses an atomic
    quote_counters row under admin auth.
  - Drop `generateQuoteNumber` from the @/lib/pb-helpers import.

Note: the deprecated `generateQuoteNumber` function remains in
src/lib/pb-helpers.ts — it has zero callers now. Safe to delete in
a future commit (covered by the data-layer-discipline section in
AGENTS.md).
2026-07-26 22:30:44 +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
ae88a2f3bc feat(invoicing): add complete delivery note workflow
This commit adds the full end-to-end delivery note workflow for the invoicing system:
- Added `letterheadForCompany` utility to infer the correct letterhead from a company name
- Created the DeliveryNoteDocument PDF component and server-side render function
- Added API endpoints for delivery note creation, PDF retrieval, printing, and print tracking
- Updated the PickingSlip component to support both invoice and delivery note headers
- Added view models for grouping, filtering, and displaying delivery note items
- Updated AGENTS.md with PocketBase schema and attachment best practices for new workflows
- Added comprehensive test coverage for all new components and API routes
2026-07-14 17:35:36 +02:00
SJCoetz
a6750b09e1 feat(invoicing): add editable company search to QB modal
- Lift customer query state to parent page to maintain consistent cache keys for QB customer lookups
- Restructure SingleOrderQbModal UI into grouped sections for better organization
- Update modal footer padding and add mobile-responsive layout
- Revise line item input labels and add accessible aria labels matching test selectors
- Refactor QB customer lookup effects and callbacks to support edited search queries
- Fix test selectors for invoice quantity and price fields to match updated labels
2026-07-14 13:00:12 +02:00
SJCoetz
7ebfb8b86b feat: add row selection and cost pricing
- add `costPrice` field to invoicing row types, plus ZAR currency formatting and profit margin calculations
- update invoice row displays to show ordered/received quantity, unit cost, selling price and margin metrics
- add per-row selection in QuickBooks send modals, letting users filter which order lines to include on invoices
- update ModalShell component to support responsive breakpoint-based sizing instead of fixed sm-only widths
- refine UI styling across the invoicing page to use consistent theme tokens (bg-card, muted-foreground)
- update tests to use dynamic yesterday dates instead of hardcoded values for stable, time-agnostic assertions
- improve button states, labels and text for QuickBooks invoice actions, including showing selected item counts
2026-07-14 12:34:47 +02:00
SJCoetz
6af4dcf53e test(collection-map): sidebar + popup + no-green-pin contracts (v3) 2026-07-12 12:34:10 +02:00
SJCoetz
21a2d7728b chore(collection-map): drop bottom-list placeholder
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 12:30:20 +02:00
SJCoetz
fdd95add30 feat(collection-map): 'X of Y collected' line in supplier popup 2026-07-12 12:26:23 +02:00
SJCoetz
9769dbb5ae feat(collection-map): sidebar with legend, driver cards, unassigned compact (page split) 2026-07-12 12:23:13 +02:00
SJCoetz
0d63061ea0 refactor(collection-map): flex column root + map flex-1, drop bottom list shell 2026-07-12 12:17:03 +02:00
SJCoetz
56626c8270 feat(pin-model): add aggregateDriverStatus helper (TDD) 2026-07-12 12:12:41 +02:00
SJCoetz
8c21347ecc feat(collection-map): drop date filter; thread ready-gate into assign dialog 2026-07-12 00:46:44 +02:00
SJCoetz
d78d479091 feat(assign-api): hard-gate on ready_for_collection, drop auto-flip 2026-07-12 00:33:00 +02:00
SJCoetz
ee9a0e1001 test(collection-map): pin toolbar shape to prevent filter-chip regression 2026-07-11 23:46:46 +02:00
SJCoetz
adfde8a0fa test(collection-map): pin-bucket split; absorb orphaned assigned items into pool 2026-07-11 23:45:42 +02:00
SJCoetz
d4c72057bf feat(collection-map): two-pin model per supplier (letter + ✓/✕) 2026-07-11 23:45:07 +02:00
SJCoetz
dcfe662c9f refactor(collection-map): drop Drivers sidebar card + driver multi-select 2026-07-11 23:43:10 +02:00
SJCoetz
d876455e41 refactor(collection-map): drop status / readiness / search filters 2026-07-11 23:42:13 +02:00
SJCoetz
ed9503706d feat(collection-pool): gate assign UI on ready_for_collection
Both the per-row Assign Driver button and the bulk-assign button in each
supplier section header now disable when the target row(s) have not been
marked ready for collection. Tooltips explain the reason.

This enforces the rule that a stop must be ready before it can be
assigned to a driver — same gate for single-row and bulk flows.
2026-07-11 22:56:42 +02:00
SJCoetz
c88a52cf2d refactor(collection-pool): drop redundant supplier name from row body
The supplier name is already shown in the SupplierGroup section header,
so repeating it on each row inside the section adds visual weight
without information. Keep it on the checkbox aria-label for screen
readers only.
2026-07-11 22:52:39 +02:00
SJCoetz
da9b3e8b58 refactor(collection-pool): split PoolRow + SupplierGroup into components.tsx 2026-07-11 00:13:20 +02:00
SJCoetz
a5eaae6f7b test(collection-pool): cover groups, bulk assign, notes tooltip 2026-07-11 00:07:13 +02:00
SJCoetz
c5731a1206 feat(collection-pool): sequential bulk-assign PATCH 2026-07-11 00:04:27 +02:00
SJCoetz
75a111a334 feat(collection-pool): wire bulk-assign dialog mount 2026-07-11 00:01:25 +02:00
SJCoetz
5984f2f814 feat(collection-pool): render supplier-grouped sections 2026-07-10 23:58:47 +02:00
SJCoetz
8e4289b104 fix(collection-pool): align SupplierGroup with spec (a11y, chevron, assign) 2026-07-10 23:55:23 +02:00
SJCoetz
c17f7c662e feat(collection-pool): add SupplierGroup component 2026-07-10 23:52:50 +02:00
SJCoetz
b6ed9f2685 feat(collection-pool): add per-row selection state and checkbox
- selectedIds Set state plus toggleRowSelected / toggleSectionSelected callbacks
- PoolRow signature extended with selected + onToggleSelected
- New flex wrapper around supplier/description block with leading checkbox input
- bg-primary/5 row tint applied when selected
- aria-label composes supplier + description for screen readers
2026-07-10 23:50:17 +02:00
SJCoetz
7ce4445865 feat(collection-pool): replace notes title with hover tooltip 2026-07-10 23:46:59 +02:00
SJCoetz
0b03a6cb81 style(collection-pool): promote time-in-list to text-primary 2026-07-10 23:44:27 +02:00
SJCoetz
c989d0d12e feat(collection-pool): sort by supplier name first 2026-07-10 23:41:45 +02:00
SJCoetz
4662347ca0 feat(collection-pool): add buildSupplierGroups helper 2026-07-10 23:39:05 +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
47b01bddfa feat: add picking slips, bulk print tracking, supplier map
- Add Supplier Map component to suppliers page and reorganize sidebar navigation
- Implement picking slip PDF generation with supplier per line item and picked-by signature footer
- Add invoice bulk print tracking API and view models to track printed invoice state
- Extend company search and form to support email/telephone fields and improve searchability
- Fix data table debounce race condition by using useRef for onSearch handler
- Fix supplier page fetch race condition with debounced timeout
- Reduce list page item counts to 10 per page for customers, companies, and suppliers
- Update company search filter to include email and telephone in search queries
- Add comprehensive test suite for new and existing components and APIs
- Add pdf-lib dependency for PDF generation and update related tooling
- Clean up unused code and fix minor linting issues
2026-07-10 10:47:11 +02:00
SJCoetz
ae66762b76 feat(invoicing): add per-order color stripe on group sections 2026-07-10 02:09:29 +02:00
SJCoetz
bbd1e1df7c feat(collection-confirm): add per-order color stripe on group sections 2026-07-10 02:07:25 +02:00
SJCoetz
6d03be2ebc feat(collection-confirm): rewrite row card as stacked vertical sections 2026-07-10 01:07:29 +02:00
SJCoetz
9ab4be2666 feat(collection-confirm): show signed-in-as pill in the page header 2026-07-10 01:02:59 +02:00
SJCoetz
6074e79be0 fix(collection-confirm): hydrate actor from dev-identity cookie, not localStorage 2026-07-10 01:01:24 +02:00
SJCoetz
961d09a798 fix(collection-map): use ISO 8601 UTC bounds for assigned_at day filter 2026-07-10 00:51:17 +02:00
SJCoetz
1846e86b9e test(collection-map): cover assigned_at clear on unassign 2026-07-10 00:49:36 +02:00
SJCoetz
5ce3ef1000 fix(collection-map): clear assigned_at on unassign so item returns to pool 2026-07-10 00:48:23 +02:00
SJCoetz
72b2ec09e2 test(collection-pool): simplify operational sort test + correct spec reasoning 2026-07-10 00:38:31 +02:00
SJCoetz
363cdebe36 test(collection-pool): pin nearby-sort behaviour for missing coords 2026-07-10 00:36:28 +02:00
SJCoetz
e5285e67df chore(collection-pool): rename labels Ready/Pending instead of Ready/Awaiting 2026-07-10 00:24:43 +02:00
SJCoetz
32194e8bb3 feat(collection-pool): require note on Mark Ready 2026-07-10 00:15:24 +02:00
SJCoetz
00fb9a38f0 test(collection-map): cover ready_notes required-on-ready 2026-07-10 00:05:54 +02:00
SJCoetz
13165bfb8c feat(collection-map): require ready_notes when marking ready 2026-07-10 00:04:01 +02:00
SJCoetz
0eef0abd92 feat(orders): make Send PO modal wider and two-column, scrollable body 2026-07-09 23:48:26 +02:00
SJCoetz
f44f090351 test(po-attachments): use node env so the 25 MB cap assertion works 2026-07-09 23:39:12 +02:00
SJCoetz
0e59d83107 feat(mcp): add purchase_orders.add_attachment tool 2026-07-09 23:37:42 +02:00
SJCoetz
a6139b7c8a chore(orders): drop unused attachmentsLoading state from Send PO panel 2026-07-09 23:34:51 +02:00
SJCoetz
14e1a12cfe feat(orders): add additional attachments panel to Send PO modal 2026-07-09 23:33:56 +02:00
SJCoetz
2b3b50596c feat(po): include additional_attachments in send-email webhook 2026-07-09 23:25:05 +02:00
SJCoetz
9bc9d389ce test(po-attachments): use NextRequest instead of any cast 2026-07-09 23:20:10 +02:00
SJCoetz
1477265db9 feat(po-attachments): add delete route for PO additional attachments 2026-07-09 23:18:51 +02:00
SJCoetz
fff095caae test(po-attachments): cover delete route behaviour 2026-07-09 23:16:53 +02:00
SJCoetz
b362d92256 feat(po-attachments): add upload route for PO additional attachments 2026-07-09 23:15:08 +02:00
SJCoetz
a16b8fcdcb test(po-attachments): cover upload route behaviour 2026-07-09 23:01:55 +02:00
SJCoetz
dfe52a62d9 fix(orders): seed Send PO body with this PO's line count, not the order's 2026-07-09 22:43:52 +02:00
SJCoetz
44db71e6d7 feat(orders): add n8n shortcut icon to Supplier POs card 2026-07-09 22:34:04 +02:00
SJCoetz
f146e42ed0 test(po-pdf): cover token capture + proxy routing 2026-07-09 22:23:49 +02:00
SJCoetz
6a7dcc0af2 fix(po-pdf): capture PB token on upload and route GET through proxy 2026-07-09 22:18:53 +02:00
SJCoetz
1e99d23e8b feat(po-pdf): add token field to PurchaseOrderDocumentEntry 2026-07-09 22:17:21 +02:00
SJCoetz
2bee7aa4be feat(quotes/process): load customers with company expanded for picker 2026-07-09 21:45:00 +02:00
SJCoetz
18334721cb feat(invoicing): add QuickBooks invoicing flow and print functionality
Add complete end-to-end QuickBooks invoicing workflow for the invoicing page:
- Add `jspdf` dependency for client-side PDF generation
- Implement updated item status handling with fallback for new invoice statuses
- Add server-side proxy API for secure QuickBooks customer lookup
- Build utilities to combine QuickBooks invoices and picking slips into a single PDF
- Add invoicing view models for row grouping, payload construction, and status checks
- Add full test coverage for all new components, APIs, and utilities
- Include feature design documentation and sample n8n workflow config
2026-07-09 09:44:55 +02:00
SJCoetz
9378685164 feat(collection-confirm): add full collection confirm workflow
Implement the complete collection confirm feature set:
- Add new `/collection-confirm` operational page for reviewing and confirming collected order items
- Add sidebar navigation entries for Collection Confirm and Invoicing
- Add `order_items` to the file proxy API whitelist for attachment access
- Install required testing dependencies and configure Vitest test setup
- Add full design documentation, test suite, and page overview screenshot
- Update package.json and build configuration files
2026-07-08 01:35:00 +02:00
SJCoetz
5f1838051f feat(collection-pool): add nearby supplier cluster sort mode
Add optional nearby-supplier cluster sort mode to the collection pool table, keeping operational sort as the default. Also includes:
- Update UserSearch component to support fallback selected label for pre-selected values
- Refactor AssignDriverDialog into a separate inner component with intent and pre-filled driver label support
- Add assignment badge helper types and functions
- Add full test coverage for new sort logic and component behaviors
- Update implementation plans and documentation files
- Fix minor code consistency and lint issues across touched files
2026-07-07 21:56:29 +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
235c0ad39b fix(attachment-counts api): surface real error details instead of generic PB admin message
capture the thrown error, extract its message if available, and append it to the base "PB admin unavailable" error response. fall back to a generic unknown error message for non-Error failures, revealing actionable debugging details like missing env vars or invalid PB credentials.
2026-07-03 22:52:39 +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
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
a0332c4159 fix(approve modal): break infinite loop from openNonce in effect deps
The ApproveAndSendDialog had a useEffect whose deps array included
`openNonce` — a state counter the effect itself incremented:

    useEffect(() => {
      if (!open) return;
      setOpenNonce((n) => n + 1);   // updates openNonce
      // ... seed defaults ...
    }, [open, openNonce, ...]);    // openNonce is in deps

This produced Maximum update depth exceeded: the effect fires → state
updates → deps change → effect re-fires → loop, until React pulls the
plug.

The intent was 'seed once per open cycle, not on every claimerName /
repEmail update' (so async lookups arriving after the modal opened
didn't clobber operator edits). The state-counter approach was the
wrong mechanism for that — mutating a dep from inside the effect
*is* the loop.

Fix: replace openNonce (state) with seededFor (ref). A ref mutation
doesn't trigger re-render, so the loop is impossible. We still depend
on [open, quote.id] in the effect, and the ref's purpose is purely
'has this (open, quote.id) cycle been seeded yet?'. Reset to null on
open=false so the next open seeds fresh.

Dropped claimerName / repEmail from the deps too — they were the
reason openNonce existed in the first place (don't re-seed when they
update), and the ref now enforces that without the side-effect trap.

Tests: 229/230 vitest (the 1 failure is the pre-existing attachments
off-by-one, unrelated to this change).
2026-06-29 23:20:16 +02:00
SJCoetz
9b2b0897a1 refactor(quotes detail): move Quote Settings into kebab menu as dialog
The Quote Settings card sat in the right column as a collapsed accordion
alongside History and Email Status — three near-identical cards the
operator had to scan past to find the data they actually came for.
Quote Settings is a low-frequency config action; it belongs with the
other low-frequency actions (Save as Template, Clone, Delete).

  ⋮ → View Quote
     View Order
     Save as Template
     Clone
     ─────────────
     Quote Settings…    ← new
     ─────────────
     Delete Quote (red)

The Settings card is removed from the right column. Its contents now
live in QuoteSettingsDialog — same three fields (Quote date, Valid for
days, Follow up by), same Save handler, but rendered as a focused
modal so the operator sees exactly what they're editing.

The page header's FollowUpChip (the yellow / red bell that surfaces a
follow-up date on a quote) still opens the same dialog via the renamed
`settingsDialogOpen` state. Auto-open on follow_up / problem status
is preserved — same effect, just an opened modal instead of an expanded
accordion.

Template mode (no customer-facing fields) still works: the dialog
shows only the Follow up by field plus the explanatory paragraph about
why date / validity don't apply to templates.

Tests: 229/230 vitest (the 1 failure is the pre-existing attachments
off-by-one, unrelated to this change).
2026-06-29 23:04:03 +02:00
SJCoetz
4e085a3f69 refactor(quotes detail): two-column layout with hero Items card
The quote detail page had grown to eight stacked cards, all with
identical Card chrome — Items, Outbound, Linked Orders, Notes, Settings,
History, Email Status, and a TODO 'Generate PDF (coming soon)' card. The
Items card (the page's hero) had the same visual weight as the
admin-panel cards, so the eye had nowhere to land.

Two-column layout on desktop (lg:grid-cols-3, primary 2/3, secondary 1/3):

  Left column (primary)
    - Description card (conditional)
    - Items card — hero treatment:
        * Left primary-blue accent border (border-l-4 border-l-primary)
        * Tinted primary header background (bg-primary/5)
        * Larger title (text-base font-semibold)
        * Rewrite with AI demoted from loud amber pill to outline variant
          (matches the accent trap from MEMORY.md)
    - Notes card (conditional)

  Right column (secondary)
    - Customer meta card — replaces the old 5-card info strip with
      a single compact card: customer name, company, letterhead,
      created date, quote date (if set), and claimed-by with avatar.
    - Outbound attachments card
    - Linked orders card
    - Quote Settings accordion (collapsed by default)
    - History accordion (collapsed by default)
    - Email Status card — tightened to 2-col grid

Mobile (below lg) collapses to single column with content-then-meta
order: Description → Items → Notes → Customer meta → Outbound → Linked
→ Settings → History → Email Status.

The 'Generate PDF (coming soon)' placeholder card is removed — that was
a TODO shipped to users. The actual Generate PDF flow lives in the
'View Quote' menu item (QuotePdfViewer) and the 'Approve' modal, neither
of which need this stub.

Items card header is flex-col on mobile so the title, totals, and
Rewrite button each get their own line instead of getting clipped.

Tests: 229/230 vitest (the 1 failure is the pre-existing attachments
off-by-one, unrelated to this change).
2026-06-29 22:23:44 +02:00
SJCoetz
5c46dfed88 refactor(quotes): collapse 10 workflow tabs into 8, add follow-up + replied chips
The list page had grown 10 colour-coded tabs (one per legacy PB status)
that fragmented the workflow rather than grouping it. Merging the
overlapping concepts into workflow buckets per the operator's flow:

  - new              ← new + follow_up  (follow-up rows show a bell chip)
  - claimed          ← claimed
  - pending_approval ← processed + approval_pending + sending
  - completed        ← approved + complete + emailed + cancelled + rejected
                       (emailed renders as 'Awaiting reply', cancelled + rejected
                        are substatus pills inside the Completed tab)
  - email_failed     ← email_failed + mail_failed + unmatched_bounce  (new tab)
  - problem          ← problem
  - templates        ← is_template = true
  - all              ← catch-all

Two row-level indicators added next to the quote number:

  - Bell chip for follow-up rows (status=follow_up OR follow_up_due set
    on an actionable status). Colour flips to red when overdue.
  - Reply-bubble chip for rows where last_reply_at is set, so the
    operator can distinguish 'fresh new' from 'replied new' at a glance.

Implementation notes:

  - getQuotes now accepts string[] statuses and OR-chains them in the
    PB filter (PB 0.26 has no IN operator). getQuoteStatusCounts sums
    per-status totals into the new tab buckets in one batch.
  - formatQuoteStatus gains workflow-friendly label overrides
    ('Awaiting reply', 'Sending…', 'Pending approval', etc.) shared by
    the list, archived, and process pages.
  - The detail page's local statusLabels map is kept in sync so toast
    + history-row interpolations read correctly.

Tests: 229/230 vitest (the 1 failure is the pre-existing attachments
off-by-one, unrelated to this change). Updated quote-status.test.ts
for the new label overrides.
2026-06-29 22:15:32 +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
bb4233a9f2 feat(dashboard): add multi-theme system and supporting improvements
Add complete multi-theme dashboard system including:
- 8 selectable themes with persistent per-browser preferences
- Theme bootstrap script to prevent FOUC on page load
- 12+ reusable chart components (area, bar, donut, radial gauge, etc)
- Custom checkbox UI component
- New settings page for appearance customization

Other changes:
- Optimize supplier map API payload with getMappableSuppliers helper
- Add aria-label attributes to back buttons on detail pages for better accessibility
- Update next.config.ts to include new local development IP 192.168.1.128
- Update site manifest with all new favicon and PWA assets
- Fix styling and layout spacing in rewrite quote modal
2026-06-26 17:59:40 +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
714e910949 feat: add branded quote PDF generation and in-app viewer
This commit adds full quote PDF generation functionality with per-company letterhead branding:
- Server-side PDF rendering using @react-pdf/renderer matching the sample quote layout
- In-app modal PDF viewer with print, download, and new-tab controls
- Letterhead service to fetch and apply company-specific branding (CNL/ISHERWOOD)
- ZAR currency and date formatting utilities for South African quotes
- Updated QuoteAttachment type to handle PocketBase's array-based file tokens
- Fixed PocketBase attachment URL generation in quote email card
- Removed deprecated `included_in_quote` field from QuoteItem and quotes service
- Updated quote item creation test to validate draft item logic
- Added comprehensive unit tests for PDF rendering and formatting helpers
- Added dev scripts for debugging PDF and image rendering issues
- Added sample generated PDF files for visual verification
- Updated tsconfig to exclude development scripts directory
- Added @react-pdf/renderer production dependency
- Removed JEI as a valid letterhead option per requirements
2026-06-23 08:30:38 +02:00
SJCoetz
4c0b6b0163 feat: enhance toasts and add quote item toggle
- completely rewrite the Sonner toaster component with theme-aligned styling, default top-center position, brand-colored toast states, and improved spacing/sizing
- update root layout to remove hardcoded Toaster position prop since the component now handles its own positioning
- add toggle to show/hide completed quote items on the quote process page
- add empty state message when all quote items are fully processed
- memoize displayed quote items for stable sorting and filtering
2026-06-22 23:50:33 +02:00
SJCoetz
86c942a466 feat: add locked pricing and overhaul quote rewrite
- Bump quote rewrite API timeout to 180s for longer AI batch processing
- Extend structured quote item fields to include model and size, remove old type field
- Overhaul n8n quote rewriter workflow with updated prompt and null-safe output handling
- Add locked source-of-truth pricing controls to auto-calculate cost/selling prices
- Remove inline cost/selling inputs from quote items table and refine table layout
- Improve rewrite modal UI with sticky headers, better empty chip handling, and styling
2026-06-22 23:07:53 +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
a18ab0f19f feat: add quote email tracking, user color coding, and improved attachments
Add comprehensive quote management and UI improvements:
- Add `email_received_at` field to quotes collection, TypeScript types, and setup script
- Update quote attachments schema to mark `kind` optional for legacy attachments, add `uploaded_at` field, and improve documentation comments
- Create `/api/v1/users/lookup` server endpoint to resolve user IDs to display names
- Add stable user ID → color mapping for visual grouping of claimed quotes
- Enhance quotes list page: reorder tab filters, show claimer names, color-code rows per claimant
- Update quote detail page to display claimer info with user icon
- Expand DataTable component to support per-row custom class and style props
- Overhaul QuoteEmailCard: add proper HTML/plain text body sanitization/linkification, inline PDF/image previews, improved metadata display, and fixed auth-aware attachment previews via blob URLs
2026-06-22 15:15:00 +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
83935c990b Add dark map, marker editing and fullscreen
Add dark-theme styles and Leaflet z-index tweaks; switch tile layer URL/attribution based on Next.js theme. Introduce theme hook, PocketBase helper import and tile constants; lower label zoom breakpoint. Replace ZoomWatcher with MapWatcher that invalidates map size when toggling fullscreen. Add fullscreen UI, edit mode toggle, draggable markers when editing, and saveSupplierLocation to persist coordinate updates to PocketBase with optimistic UI and toast feedback. Misc: simplify cluster marker popup handling, ensure popups close before cluster flyTo, and tweak cluster distance label to reflect current zoom.
2026-05-27 17:57:24 +02:00
SJCoetz
54644c7ce8 Improve map clustering and labels
Enhance supplier map UX by introducing zoom-aware clustering and refined marker visuals. Adds clusterIcon, clusterDistanceForZoom, ZoomWatcher, ClusterMarker and SupplierPopup components; switches to dynamic cluster sizes and distances based on zoom, and flies into clusters on click. Shows individual supplier markers and optional permanent labels above configurable zoom breakpoints, and reduces marker sizes/shadows for a cleaner look. Also adds .supplier-map-label CSS for tooltip-style permanent labels.
2026-05-27 14:53:54 +02:00
SJCoetz
565b1a6e2b Add supplier map feature with Leaflet UI
Introduce a new SupplierMap component and page to visualize suppliers on a Leaflet map (client-side, dynamic import). Wire the feature into the app by importing Leaflet CSS in the global layout and adding a Supplier Map entry to the sidebar. The map fetches suppliers, parses coordinates, groups nearby suppliers into proximity clusters, and shows markers, tooltips, and lists of missing coordinates. Update package.json to include leaflet, react-leaflet and types; remove a Windows-specific lightningcss package. Also normalize Dockerfile ENV formatting (no behavior change).
2026-05-27 14:39:00 +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
SJCoetz
954dc266e4 feat: add service worker, offline page, and manifest for PWA support 2026-05-24 17:02:07 +02:00
SJCoetz
a828432540 fix: improve responsiveness and accessibility of dashboard and quotes pages 2026-05-24 16:51:12 +02:00
SJCoetz
363786ea36 feat: add UI components for separator, sheet, sonner, switch, table, tabs, textarea, tooltip
- Introduced a new Separator component for visual separation in UI.
- Added Sheet component for modal-like functionality with triggers and overlays.
- Implemented Toaster component for notifications using Sonner library.
- Created Switch component for toggle functionality.
- Developed Table component with headers, body, footer, and rows for structured data display.
- Added Tabs component for organizing content into tabbed interfaces.
- Implemented Textarea component for multi-line text input.
- Created Tooltip component for displaying additional information on hover.

feat: add PocketBase helpers and types

- Implemented various helper functions for interacting with PocketBase collections (customers, companies, suppliers, quotes, orders).
- Added type definitions for entities such as Customer, Company, Supplier, Quote, Order, and their respective items.
- Included utility functions for generating unique identifiers for quotes, orders, and purchase orders.
- Added functions for calculating totals and formatting currency and dates.

chore: add TypeScript configuration

- Added tsconfig.json for TypeScript support with strict type checking and module resolution.
2026-05-24 16:31:01 +02:00