Commit graph

150 commits

Author SHA1 Message Date
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