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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>