Commit graph

182 commits

Author SHA1 Message Date
SJCoetz
172d562f06 fix(buyer-tasks): generate PO succeeds + supplier stripe + PO badge
generatePoForGroup was creating purchase_orders records without
`po_number`, which is required by the PB schema. PB rejected the
create with 500 ("Failed to create record.") and the operator never
got a PO. Fix:

- New src/lib/server/po-number.ts — server-side generatePONumber()
  that mirrors the client version in pb-helpers.ts (PO-YYYY-MM-NNN
  format) but uses getAdminPocketBase(). Per the data-access rules,
  don't extend pb-helpers.ts; server business logic lives under
  lib/server/.
- generatePoForGroup now generates the PO number, includes
  total_amount (sum of costPrice * quantity) and order_item
  (back-link to the items) in the create payload — mirrors the
  canonical /orders/[id]/process PO-creation flow so the PB admin
  row shows related lines without an extra getList call.
- Test updated to assert the new payload (po_number regex,
  total_amount computed, order_item back-link).

UI while here:
- BuyerTaskGroupCard now renders a deterministic supplier colour
  stripe on the left edge. New src/lib/supplier-color.ts hashes the
  supplierId into a small Tailwind 600 palette (18 colours) so the
  same supplier always shows the same colour, and the supplier name
  gets tinted with the same colour for quick scanning.
- PO number is now shown prominently at the top of the card (in a
  warm-toned badge that matches the AI Insights theme) when one
  exists — po_draft and po_sent tabs. Needs PO tab omits it.

Co-authored-by: Orca <help@stably.ai>
2026-07-30 15:00:18 +02:00
SJCoetz
20b57ce2bb feat(sprint-modal): show markup + order desc hint + explicit Show/Hide
Three small additions to the process modal:

- Markup cell added to the pricing grid (4 cols now: PO Qty, Cost,
  Selling, Markup). Read-only and dashed like Selling — derived from
  selling/cost so the operator sees the margin they're pricing at.
  Green when selling ≥ cost, destructive when selling < cost.

- Above the PO description textarea, the order's original item
  description is now shown as a read-only reference hint. Lets the
  operator see what the customer asked for vs what they're sending
  to the supplier without having to scroll back to the modal header.

- AI Insights toggle now has an explicit "Show" / "Hide" label next
  to the chevron so the affordance is obvious. The chevron direction
  was the only signal before; the label makes the toggle
  self-documenting.

Co-authored-by: Orca <help@stably.ai>
2026-07-30 14:54:02 +02:00
SJCoetz
b9dfec8d28 style(modal): re-tint AI Insights section from info (blue) to warn (yellow)
The AI Insights section was info-blue while the modal's outer border
and the popover's AI card are warn-yellow. The mixed cool/warm
palette felt disjointed. Switching the section to warm warn tones
makes the yellow border feel intentional rather than decorative.

- border-info/20 → border-warn/40 (heavier, with shadow-warn/10 so
  the section has a soft glow)
- bg-info/5 → bg-warn/5
- text-info → text-warn-foreground (Sparkles icon, "AI Insights"
  label, chevrons, suggestion-available pill, Use AI button)
- Use AI button: border-info/30 → border-warn/40, hover bg-info/10
  → hover bg-warn/10

Co-authored-by: Orca <help@stably.ai>
2026-07-30 14:14:45 +02:00
SJCoetz
0007408f14 style(popover,modal): warn-toned AI card + warning-bordered modal
Popover (the ⓘ AI snapshot card):
- Switch from raw amber-200/amber-50 to the project's warn theme
  tokens so it matches the rest of the design system and respects
  dark mode.
- Subtle gradient backgrounds (from-warn/10 to-warn/5) so each card
  reads as a distinct section without screaming for attention.
- Icons now sit in coloured chips (rounded-md bg-warn/20 etc.) for
  better visual hierarchy.
- Freshness badges are pill-shaped with an inset ring instead of a
  flat square — softer.
- Fuzzy-match warning box uses warn tokens too so it doesn't clash
  with the parent card.
- Supplier / quote rows get a faint hover background and tighter
  spacing for easier scanning.

Process modal:
- Outer Card border switched from border-border/60 to
  border-2 border-warn/40 with a warm shadow-warn/15. The whole
  modal now pops on the dimmed background — fits the "stand out
  orange/yellow" ask.

Co-authored-by: Orca <help@stably.ai>
2026-07-30 14:12:59 +02:00
SJCoetz
5f039ea8cd feat(sprint-modal): lock selling, add PO qty, wire AI suggestion
The OrderSprintProcessModal was trying to do too much and made
operators do too much. The buying flow on this screen is now:

- Selling comes from the order and is fixed. Rendered as a read-only
  dashed-bordered cell with a "From order · not editable" hint — the
  price the customer was quoted is still visible so the operator can
  see the margin they're pricing at.
- Markup field removed (it only existed to drive the cost↔selling
  recalc). Markup is now computed from selling/cost at save time so
  downstream margin reports stay consistent.
- New PO Qty input (defaults to order_qty, falls back to quantity)
  written to order_qty. Lets buyers override for partial fills.
- "Use AI suggestion" button next to the AI Insights header reads
  ai_item_insights.top_historical_matches[0] and applies supplier_id
  (resolved to the actual supplier name in the dropdown) + cost_price.
  Disabled when no AI matches; warns if the AI's supplier_id isn't in
  the suppliers list instead of silently dropping it.

Visual refresh while here: backdrop-blur overlay, info-tinted AI
section so it reads as distinct from the deterministic history, soft
borders (border-border/60), success-coloured Process button with
shadow, gradient header, hover transitions, sticky footer with
status hint.

Co-authored-by: Orca <help@stably.ai>
2026-07-30 14:07:58 +02:00
SJCoetz
6093a749eb feat(ai-history): render item-history blob shape-aware
The AI agent returns `procurement_summary` + `top_historical_matches`
arrays but the popover was dumping both as raw JSON. Now:

- AiInsightsCard promotes the first *_summary/*_description key (or any
  long string) into a prose headline above the structured sections.
- isNamedEntry recognises matched_description / match_description /
  suppliername, so the renderer turns n8n's historical-match rows into
  structured list rows instead of an object-dump.
- AiCard inside the popover delegates body rendering to AiInsightsCard;
  the amber framing + freshness badge + raw-data fallback are kept.

Tests cover headline promotion, named-list rendering, empty blob,
description-keyed entries, and the popover's end-to-end shape.

Co-authored-by: Orca <help@stably.ai>
2026-07-30 13:56:39 +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
bca6a32c0d feat(buyer-tasks): page body with tabs, cards, mark-ready flow
Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:18:57 +02:00
SJCoetz
9d40109b81 feat(buyer-tasks): mark-ready confirmation modal
Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:14:11 +02:00
SJCoetz
10dd4aff81 feat(buyer-tasks): group card with color-coded action button
Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:11:39 +02:00
SJCoetz
6fa44c55ff feat(buyer-tasks): collapsible items-list component
Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:06:10 +02:00
SJCoetz
5a08d3651e feat(buyer-tasks): tab strip component
Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:05:17 +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
05242f09f4 feat(buyer-tasks): markGroupReady with cross-order check
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-07-29 20:00:45 +02:00
SJCoetz
98a0bc4143 feat(buyer-tasks): generatePoForGroup creates PO and updates items
Co-authored-by: Orca <help@stably.ai>
2026-07-29 19:58:07 +02:00
SJCoetz
b4c9f2d2ce fix(buyer-tasks): use getFullList and tighten item_status filter for po_draft/po_sent
- Switch from getList(1, 500) to getFullList so candidate sets
  aren't silently truncated at 500 rows (matches orders-service
  and quotes-service conventions).
- Tighten the po_draft filter to item_status = po_generated so
  items in downstream statuses (collection_pool, delivered, etc.)
  don't get pulled over the wire.
- Tighten the po_sent filter to exclude item_status in
  {ready_delivery, invoiced, delivered, cancelled} for the same
  reason.
- Drop the redundant ready_delivery post-filter on po_sent now
  that the PB layer handles it.
- Tests capture the filter string passed to getFullList and assert
  its clauses so regressions are caught.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-29 19:56:53 +02:00
SJCoetz
49acc04fd1 fix(buyer-tasks): type-safe expand traversal for customer/company
Co-authored-by: Orca <help@stably.ai>
2026-07-29 19:53:09 +02:00
SJCoetz
463917d755 feat(buyer-tasks): listBuyerTasks service with grouping
Co-authored-by: Orca <help@stably.ai>
2026-07-29 19:52:41 +02:00
SJCoetz
d307e8d400 feat(orders): hide cost and markup from review modal items table
Co-authored-by: Orca <help@stably.ai>
2026-07-29 19:02:49 +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
c9e5362cf1 fix(types): restore ItemHistorySupplier + ItemHistoryOtherQuote (dropped during merge resolution)
The merge of feature/order-sprint-item-history into main concatenated
src/lib/types.ts by appending the last 50 lines of the feature branch's
version. The two helper interfaces ItemHistorySupplier and
ItemHistoryOtherQuote were defined earlier in the file (lines 1416-1433)
and were dropped, breaking the references in ItemHistory and the import
in item-history-service.ts. This commit restores them.

Co-authored-by: Orca <help@stably.ai>
2026-07-29 17:45:15 +02:00
SJCoetz
43011d4d5e feat(item-history): wire description-fuzzy matcher into getItemHistory
Co-authored-by: Orca <help@stably.ai>
2026-07-29 17:43:59 +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
7801b602f5 feat(orders): add edit-mode confirmation for items table
Items in the orders review modal are read-only by default. A new
'Edit items' button (Pencil icon, top right of the items list)
opens a confirmation dialog before the inputs become editable.

- State: editingItems (default false), confirmingEdit (default false).
- Both reset to false whenever the modal re-opens (useEffect on
  [open, order.id]) so re-opening always starts read-only.
- All 4 per-row inputs (qty, cost, markup, selling) carry
  disabled={!editingItems || saving}.
- Visual hint: when editing, the items list gets a subtle warn
  ring + tint (ring-1 ring-warn/40 bg-warn/5).
- data-testid hooks: review-edit-items-btn on the button,
  review-edit-confirm-dialog on the dialog, review-items-list on
  the ul with data-editing attribute.
- 4 new tests cover: read-only default, button opens dialog,
  confirm enables editing, cancel keeps read-only.

Also adds order_attachements?: string[] to the Order type — the
PB file field on the orders collection used by the modal's source-
of-truth panel (separate commit wires the data).

Co-authored-by: Orca <help@stably.ai>
2026-07-29 17:41:21 +02:00
SJCoetz
22df511293 merge
Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:49:37 +02:00
SJCoetz
96e071330a test(orders): reconcile duplicate test file state
A post-commit re-edit of the test file introduced a duplicate
updateOrderItemsPrices describe block at the tail. All tests still
pass (the duplicates run independently). Keep the file aligned
with HEAD.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:24:03 +02:00
SJCoetz
940475fc5b fix(orders): modal reflects selected customer + rebase on partial failure
Fix #4: The customer label rendered below the combobox read the
original `customer` prop, so switching the selection left a stale
name on screen. Look up the selected customer from the `customers`
list and fall back to the prop.

Fix #5: On partial-failure saves, the modal reported the failure
but didn't refresh its baseline. A retry would resend already-
successful rows. Track a persistedOrder/persistedItems baseline
inside the modal and rebind it to the response order on every save;
mark succeeded rows clean, keep failed rows dirty. Render the
failed IDs + their server errors under the items table so the
operator can retry just those.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:22:32 +02:00
SJCoetz
deb224ac10 test(orders): add tests for cross-order, filter escape, unique-constraint
Cross-order rejection (Fix #3), PB filter quote escape (Fix #6), and
mapping of PB unique-index write errors to OrderNumberCollisionError
(Fix #7) get full coverage.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:20:48 +02:00
SJCoetz
53b778691e fix(orders): validate item order_id in updateOrderItemsPrices
A request to order A could include an item ID from order B and
silently modify it. Read the current row and reject with failed[]
when current.order does not match orderId. Test covers the cross-
order case.

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

Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:20:41 +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
3f8836d981 fix(item-history): query pos collection for stock_qty (was stock_purchase_items, which doesn't exist)
Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:02:13 +02:00
SJCoetz
f8784e3ba9 fix(orders): review modal preview review fixes (mixed tests, decorative chip comment)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-07-29 16:00:16 +02:00
SJCoetz
32237f52c7 feat(orders): inline attachment preview chain + email fallback
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-07-29 15:16:47 +02:00
SJCoetz
cafc4c7962 refactor(item-history): trigger fetches new endpoint instead of inline read
Co-authored-by: Orca <help@stably.ai>
2026-07-29 15:13:15 +02:00
SJCoetz
7bc7208f8a chore(item-history): pin Fastener Co duplicate rendering in popover tests
Co-authored-by: Orca <help@stably.ai>
2026-07-29 15:09:35 +02:00
SJCoetz
bce66b135a feat(item-history): tight popover renderer with 4 render states
Co-authored-by: Orca <help@stably.ai>
2026-07-29 15:08:03 +02:00
SJCoetz
c572d08e53 fix(orders): review modal review fixes (drop dead markup check, lock tests, link assertion)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Orca <help@stably.ai>
2026-07-29 15:07:11 +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