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