12-task TDD plan covering the service module, four API routes,
the modal component (built incrementally across 4 tasks), and the
page-hook. Each task ends with a commit; spec gaps surfaced during
self-review already merged into task content.
Co-authored-by: Orca <help@stably.ai>
Adds spec for the new item-first quick-draft modal on /stock-orders:
item-first stock-order entry, accumulates per supplier into draft POs,
reuses existing Send/Receive flow. No schema changes.
Co-authored-by: Orca <help@stably.ai>
The Process Order Item modal already fetched order_items history with
the supplier id, but the rendered row only showed description, order
number, cost, selling, and markup — leaving the operator to guess
which supplier fulfilled each past order.
Now each Recent Orders row also shows the resolved supplier name,
tinted with the same deterministic supplier colour used by the
buyer-tasks cards (djb2 → 18-swatch palette). The id is looked up
locally against the modal's existing `suppliers` prop — no extra PB
expand needed.
Unknown supplier ids (e.g. supplier deleted since the past order) are
silently omitted rather than surfaced as a stale id. Two new tests
cover the resolved-name and missing-id cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
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>
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>
When the operator clicks "Mark as ready" on a buyer-tasks po_sent card
and fills in the note, the item should land in the collection pool
(`item_status: "collection_pool"` + `ready_for_collection: true`) so
a driver can be assigned. Previously the service set
`item_status: "ready_delivery"`, which was rendered as the word
"Collected" on /orders and on /orders tabs — operators reasonably
read that as "driver already picked it up", and the item never
appeared on /collection-pool where they expected to assign a driver.
Now:
- markGroupReady writes a single PB update with the same shape as
/api/v1/collection-map/items/[id]/assign (collection_pool +
ready_for_collection: true + notes)
- po_sent buyer-tasks filter excludes collection_pool and assigned,
so marked-ready items drop out of buyer-tasks and surface on
/collection-pool instead
- ITEM_STATUS_VISUAL.ready_delivery is labelled "Ready for
Collection" (kept distinct from the actual `collected` state) for
any other flow that legitimately sets this status (e.g. stock-out)
- new item-status-ui.test.ts locks down the label/tab mapping so
the regression can't return
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
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>
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>