- Header (title, description, Reorder, + New stock order) now
renders unconditionally, so operators can navigate away even when
the list query throws.
- Full error is logged server-side via console.error; the user
sees a generic 'Failed to load stock orders. Please try again.'
message that doesn't leak PB URLs, admin env var names, or
filter syntax.
Co-authored-by: Orca <help@stably.ai>
Reads searchParams (status, page), validates both, calls
listStockPurchases via the admin PB, and renders the new
StockOrdersTable client component. Drops the legacy direct-PB
read path per AGENTS.md data-access discipline.
The old page.test.tsx is removed; its assertions are mirrored
in the new StockOrdersTable.test.tsx (next task).
Co-authored-by: Orca <help@stably.ai>
- Disabled pagination links (Previous at page 1, Next at last page)
now also set tabIndex=-1 and preventDefault on click, so keyboard
users can't activate them when at the boundary. aria-disabled +
pointer-events-none blocked only mouse clicks.
- Row click uses useRouter().push() instead of window.location.href
for a soft navigation that preserves client state (consistent
with the original /stock-orders page).
Co-authored-by: Orca <help@stably.ai>
Holds the QuickDraftModal open state, renders status filter tabs as <Link>s to ?status=..., and adds two new columns (Ordered R, Received R) showing per-row aggregates from the service layer.\n\nReplaces <Button asChild><Link/></Button> with <Link className={cn(buttonVariants(...))}> because the project's Button (built on @base-ui/react/button) doesn't support asChild. Visually identical; preserves navigation semantics.
Co-authored-by: Orca <help@stably.ai>
- Catch block now preserves error.status so assertAgentAuth's 401
surfaces as 401, not 500. Matches sibling /api/v1/stock-purchases
pattern.
- page/perPage parsing falls back to documented defaults on
non-numeric or zero input rather than passing NaN to the service.
- Add 2 tests covering the auth-status and pagination boundaries.
Co-authored-by: Orca <help@stably.ai>
Thin wrapper around listStockPurchases that validates the status
param against STOCK_PURCHASE_STATUS_VALUES (400 on bad input) and
caps perPage at 200. Mirrors the agent-auth pattern used by the
other v1 list routes.
Co-authored-by: Orca <help@stably.ai>
Add ordered_value and received_value to StockPurchaseListRow. Each is
Σ line.{quantity|quantity_received} × line.cost_price, rounded to 2dp.
Replaces countItemsForPurchase with summarizeItemsForPurchase so the
per-row N+1 returns all three numbers in one fetch (no extra round
trip).
Co-authored-by: Orca <help@stably.ai>
Six-task TDD plan to widen listStockPurchases with ordered_value +
received_value per row, expose it via a new v1 list route, migrate
the /stock-orders page from a client component reading PB directly
to a Server Component + sibling client table component, and move the
existing 7 page tests + add an 8th.
Co-authored-by: Orca <help@stably.ai>
Previously, when recordPurchaseOrderSendOutcome received a "success"
outcome from the n8n workflow, it auto-flipped every linked
order_items.item_status from "po_generated" to "collection_pool".
This dropped items off the buyer-tasks PO Sent tab immediately on
send — before the supplier had even confirmed receipt. The operator
had no chance to mark items ready-to-collect manually.
New flow: send mail → items stay "po_generated" and remain visible
in buyer-tasks PO Sent → operator manually marks items ready →
items drop off buyer-tasks.
The back-link sync (purchase_orders.order_item) still runs and now
reads all items with purchase_order = poId regardless of status, so
the array stays accurate as items progress through their lifecycle.
Adds src/lib/server/purchase-orders-service.test.ts with 4 cases:
- success does NOT auto-flip linked items
- success flips PO to sent + stamps email_message_id
- success keeps the PO's order_item back-link in sync
- failure flips PO to mail_failed + stamps error (also no item writes)
Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
Add an Approved design doc for the /stock-orders list table: two new
currency columns (Ordered, Received) showing sum(line.quantity *
line.cost_price) and sum(line.quantity_received * line.cost_price) per
stock purchase. Migrates the page from client-side direct PB read to
the service layer per AGENTS.md.
Co-authored-by: Orca <help@stably.ai>
The n8n Code node that posts the SMTP outcome back to the
/api/v1/purchase-orders/[id]/send-email/outcome route couldn't
read env vars at runtime (N8N_BLOCK_ENV_ACCESS_IN_NODE=true in
docker-compose), so it composed the callback URL from fields on
the webhook body. The route never pushed dashboardBaseUrl or
outcomeUrl, so n8n fell back to a relative URL, the request
died, n8n returned HTTP 500, and the route flipped the PO to
mail_failed with a misleading '[webhook] Webhook returned HTTP
500' message.
DASHBOARD_URL is already defined in .env (labelled 'used by n8n
workflows to call back into this app') — the route just never
read it. Now the route pushes both the dashboard base and a
pre-built absolute outcome URL, trimmed of trailing slashes, and
omitted (not empty) when DASHBOARD_URL is unset so the existing
'missing' branch in n8n fires clearly.
A new test file asserts all three cases: with DASHBOARD_URL,
with a trailing slash, and without the env var.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
The route is a pass-through, but the smoke test guards against a
future refactor silently dropping the new emailError field — the
operator's retry affordance depends on it.
Co-authored-by: Orca <help@stably.ai>
The SendPoModal renders its 'Resend' label and inline failure
banner from purchaseOrder.status / email_error, but the buyer-tasks
page stub didn't include either field. As a result, the modal
showed 'Send' and an empty banner even when the underlying PO was
mail_failed. Now the stub includes both so the operator sees the
correct affordance.
Co-authored-by: Orca <help@stably.ai>
The BuyerTaskGroup type gained an emailError field in the previous
commit. This pre-existing fixture constructs a BuyerTaskGroup
literal and now needs the field to compile. No behaviour change
in the test — emailError: null matches a clean draft.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
When a card's poStatus is 'mail_failed', the card now renders a
red warning row with the email_error reason and switches the
action button label to 'Resend' with the destructive colour. A
new data-po-status attribute on the button lets tests target the
state without depending on the human-readable label.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
The po_draft filter previously required po.status === 'draft', so
a failed send (status: 'mail_failed') caused the group to vanish
from buyer-tasks entirely. The only recovery path was the per-PO
Resend button on the order detail page, which is friction. Now
mail_failed groups stay in place so the operator can re-open the
SendPoModal and retry from the same list. po_sent is unchanged— mail_failed must not appear there.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
Adds an emailError field on BuyerTaskGroup so the UI can render
the send-pipeline failure reason. The value is truncated to 200
chars at the service boundary to keep the card layout bounded.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
Six tasks: service field, service filter, card visual, page wiring,
route smoke test, full verification. All TDD with frequent commits.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
Mail-failed POs currently vanish from every buyer-tasks tab because
the po_draft and po_sent filters both exclude the 'mail_failed' status.
This adds a focused design that surfaces them in place inside PO Draft
with a red warning row + 'Resend' button that re-opens the shared
SendPoModal.
Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
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>
Wraps a single PO group on the collection-confirm review page.
Renders the photo strip + items table + confirm button, with the
PO number and supplier name in the header. The cost edit/read
flow stays inside ItemsTable; the parent owns onCostChange
optimistic updates and the actual confirm mutation.
Co-authored-by: Orca <help@stably.ai>
Removes the Adjust Quantity modal, surfaces invoice photos in a per-PO
strip, exposes inline-editable cost prices, and migrates the page to a
thin service layer (listCollectedItems, updateOrderItemCost,
confirmOrderItemsForInvoicing) with three new /api/v1 routes. Cost edits
are audited in the existing notes feed as category=cost entries; confirm
is one PocketBase batch.
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>