Redesign the per-item ⓘ popover on /sprint so it surfaces actionable history
(cheapest supplier, other quotes to this company, last sold, recommendation)
instead of the current decorative rainbow card.
- Deterministic PB-join service (new) computes suppliers, other quotes, last
sold, stock from existing collections.
- Existing n8n AI blob keeps running — render side by side for comparison.
- After 1 week: delete whichever data source loses on speed / freshness /
cost.
- Process orders section is a separate follow-up round.
Also ignore .superpowers/ (visual companion mockups are local-only).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Orca <help@stably.ai>
The previous commit added the Collection Pool entry using the
Package icon, but the lucide-react named-imports list in
sidebar.tsx didn't already include Package (it was left over from
a prior removal). Without it, the route served an icon-less row.
Re-runs the alpha-sorted insert of Package into the imports.
LogisticsItems had Collection Map and Collection Confirm but no
Collection Pool, mirroring the post-deletion state from WFDASH_NEXTJS.
Without an entry the /collection-pool page is reachable by URL but
invisible from the nav menu, so operators wouldn't know it exists.
The icon () wasn't in the existing lucide-react named-imports
list — checked the file first (it was) so no import change needed.
Adding the entry doesn't need the PB collection
itself to exist; the link will 404 until that migration runs, but
that's still better than the entry being absent.
Pushes on top of 811493b (the page+service commit).
Restores the /collection-pool feature in this codebase.
- src/lib/server/orders-service.ts
- Re-adds `listCollectionPool({ status, page, perPage })` which
queries the `collection_pool` PB collection via the admin
client (filter on pool_status, sort -created, expand
order_item/order/supplier/drv_id). MCP route.ts already
imported this — without it, the MCP route failed to
compile.
- src/app/(app)/collection-pool/{page,components,view-model}*.tsx
- Re-creates the page directory (5 files, 2 315 lines) carrying
the supplier-group + per-stop checkbox UI, the
`CollectionPoolRowSource = "customer" | "stock"` union, the
assignment-state badge helpers (buildProximityMarkerMap,
buildSupplierGroups, buildVisibleRows), and the
MarkReadyDialog / AssignDriverDialog wiring.
- src/app/api/mcp/route.test.ts
- Adds `const listCollectionPool = vi.fn()` and exposes it in
the vi.mock factory so the existing collection_pool.list test
cases (and the bulk_update_quote_items ones that call
`.mockReset()` on every fixture) stop crashing with
'ReferenceError: listCollectionPool is not defined'.
Not in this commit (kept narrow per request):
- The /api/v1/collection-pool route handler and its test
- The 'Collection Pool' entry in the sidebar nav
- The migration script that creates the live `collection_pool`
PB collection (it doesn't exist on the cloud PB instance
yet; the page render relies on it being created separately
or via `npm run pb:migrate:orders-v2`)
- New `handleProcess` claims new quotes inline and navigates straight to the process workspace
- Primary "Process" button with secondary "Open" for claimable quotes; other statuses show "Open" only
- Mobile card includes a full-width Process shortcut for new/claimed-by-me quotes
Customer and Company used to be two side-by-side DataTable columns,
which cost ~120 px of horizontal width and made rows feel sparse when
the operator is just eyeballing who's-who across many quotes.
Now the Customer column carries both names in one cell: customer in
the default font-medium row, company underneath in text-xs
text-muted-foreground so the visual hierarchy reads as
"Primary person > Their organisation".
Effect on row layout:
- Total table column count drops from 7 to 6.
- Row width shrinks on desktop, freeing space for the larger
Quote # column (which carries the follow-up / replied / aged
chips and was getting crowded).
- The mobile card render (used below the sm breakpoint) gets the
same stacking so the company line shows on every viewport.
Edge cases handled:
- Both names empty → '—' muted placeholder
- Customer present, no company → just the customer name
- Company present, no customer → '—' for the customer, company below
- Very long names → truncate via className='truncate'
The existing standalone Company column is removed; downstream
code (DataTable, mobileCardRender) reads from the same expanded
fields so no extra fetches.
quotes/page.tsx
- Reduce inter-tab gap from gap-1 (4px) to gap-0.5 (2px) so the
pill strip reads as one cohesive filter row instead of a row of
loose badges.
- Drop `flex flex-col sm:flex-row items-start sm:items-center` on
the page header in favour of `flex flex-row flex-wrap
items-center`. On mobile widths (375px tested), the Approvals
and "+" buttons now stay inline with the "Quotes" title instead
of dropping below it.
next.config.ts
- Add 192.168.1.42 to allowedDevOrigins. The dev server was
warning when the user hit the app from a LAN host (their iPad,
visible in ps as `192.168.1.42:newoak->ipad.home:55199`). Now
whitelisted explicitly alongside the existing 192.168.1.122 /
192.168.1.128 entries.
On mobile widths (verified at 375px viewport), the 9 status tabs
wrap to 4 rows but the TabsList DOM reported height = 32px (one
row), so space-y-6 placed the search bar at y=254 — directly
overlapping tab rows 3 and 4.
Root cause: the @base-ui/react/tabs Root has `flex gap-2
data-horizontal:flex-col` baked in. When the wrapped TabsList's
intrinsic height (4 rows of 28px = 112px+) didn't propagate to the
flex-column parent, the next sibling was positioned based on the
shorter (single-row) computed height.
Fix: add `min-h-fit` to the TabsList override so the flex item
forces its content height regardless of the column's intrinsic
shrinkage. Verified at 375px width: tabs now correctly occupy
y=198-338 (4 wrapped rows), search bar at y=362 — clear 24px gap
with no overlap.
Also tightened the override:
- `inline-flex` -> `flex` (so the container is a full-width block
flex item rather than inline fit-content)
- `items-center` -> `items-start` (so wrapped children don't
cluster in the middle of an under-sized box)
- added `w-full` and `min-h-fit` to lock the height floor
`pb-4` from the earlier 'some space between tabs and search' tweak
is kept — now correctly visible as 16px additional breathing room
above the search bar's 40px input.
Scoped to the Quotes page only (one-line override) so other pages
that use the default TabsList (login grid, pricelist, customers)
are unaffected.
Two small visual tweaks on the Quotes screen:
1. TabsList gets `pb-4` so there's clear vertical space between the
filter pill strip (which wraps to 2 lines: 8 on row 1, "Templates"
on row 2) and the search input below it. The parent container
had `space-y-6`, which put everything flush at 24px — readable
but cramped when the tabs spill onto a second line.
2. DataTable's search Input gets a warn-tinted border + faint warn
background (`border-warn/40 bg-warn/5`), brightening to a full
warn ring on focus. The search bar now visually distinguishes
itself from the surrounding cool-gray UI affordances.
Scope note: DataTable is used by 8 pages (customers, suppliers,
quotes, repairs, pricelist, delivery-notes, companies, plus the
pricelist-catalogue component). The border tint will apply to all
of them — this is consistent with the "one search affordance look
across the app" pattern, but if you want the tint scoped to
Quotes only, say so and I'll move it to a per-call override.
The Quotes page filter strip showed 9 tabs with 'Templates 0' pushed
far to the right with a big visible gap — caused by `flex-1` on every
TabsTrigger combined with `inline-flex w-fit` on the parent
TabsList. `flex-1` forced every trigger into an equal-width cell,
so the narrower 'Templates' tab left a wide empty right margin.
Tested: min/max width across the 9 tabs dropped from a 1:1 equal
distribution (every cell = container-width/9 ≈ 130px) to a content-
proportional 70–182px range; the strip wraps tightly on one row
with 'Templates' falling to line 2 because there isn't room. The
visible gap is gone.
Other consumers of TabsList (`customers/[id]`, `pricelist`,
`login`) are unaffected:
- pricelist has 2 tabs, content-sized is the same or better.
- customers/[id] has 3 tabs — equal-width was visually fine,
content-sized still reads cleanly.
- login uses `grid w-full grid-cols-3`, so flex-1 on the children
had no effect to begin with.
Also bumped horizontal padding from `px-1.5` to `px-3` for
breathing room now that pills aren't width-stretched.
The form's deprecated generateQuoteNumber() was swapped for
fetch('/api/v1/quotes/next-number'). Three tests' setups pointed
fetchMock only at the upload response — and one asserted
fetchMock was never called (it now always is, for next-number).
- 'uploads per-item files' + 'shows a partial failure': chain two
mockResolvedValueOnce responses (next-number, then upload).
- 'allows a company-only selection': one mockResolvedValueOnce for
next-number.
- 'stops later uploads when a quote item create fails': add the
next-number mock and replace 'not.toHaveBeenCalled()' with
'toHaveBeenCalledTimes(1)' plus an explicit assertion on the
URL — item-create branch throws before the upload fetch, so
the original negative assertion was always-true and is now wrong.
The generateQuoteNumberMock in the hoisted block is now dead (the
form no longer imports it). Left in place to keep this commit
narrow; safe to delete in a future PR alongside the deprecated
function in src/lib/pb-helpers.ts (which now has zero callers).
src/app/api/v1/quotes/next-number/route.ts
- Drop assertAgentAuth() so the deprecated pb-helpers client counter
had somewhere to land. The route is intentionally open (see the
long doc comment); other /api/v1/* routes still gate on the bearer
token via their own call to the helper. Browser cannot send
AGENT_API_KEY without exposing the secret, and exposing it via
NEXT_PUBLIC_ would defeat the rest of the agent surface.
src/components/quote-form.tsx
- Replace `await generateQuoteNumber()` (the deprecated
read-then-write client counter from src/lib/pb-helpers.ts) with
a `fetch('/api/v1/quotes/next-number')`. Falls through to the
existing Math.random fallback if the endpoint errors. Removes
the race where two concurrent form submits could both pick the
same next number, since the server endpoint uses an atomic
quote_counters row under admin auth.
- Drop `generateQuoteNumber` from the @/lib/pb-helpers import.
Note: the deprecated `generateQuoteNumber` function remains in
src/lib/pb-helpers.ts — it has zero callers now. Safe to delete in
a future commit (covered by the data-layer-discipline section in
AGENTS.md).
src/app/api/v1/sprint/attachment-counts/route.ts
- Chunk itemIds into batches of 80 before building the PocketBase filter;
the live cap was hit at ~115 IDs × 15 chars and surfaced as a
generic Next 500 ("Something went wrong while processing your
request.") that hid the real cause. 7 chunks for the sprint
page's max of 500 items.
src/app/layout.tsx
- Add metadataBase derived from NEXT_PUBLIC_SITE_URL /
NEXT_PUBLIC_POCKETBASE_URL. Silences the per-page
"metadataBase not set" warning during dev renders and gives
OG / Twitter cards a stable canonical base.
src/app/api/v1/{quotes,order-items,purchase-orders,stock,
stock-purchases,users,sprint,driver,ai-insights}/route.ts (NEW)
- Add the missing top-level collection routes the agent API
needs. seven are straight passthroughs to their PB collection;
/sprint proxies to quote_items with optional claimStatus and
category query params; /driver lists PB users (no role field
in PB so callers narrow with ?filter=); /ai-insights returns
a roll-up count + most-recent regenerate timestamp across the
four collections that carry ai_* JSON fields.
src/app/api/v1/collection-pool/{route.ts,route.test.ts},
src/app/(app)/collection-pool/{page.tsx,page.test.tsx,
view-model.ts,view-model.test.ts,components.tsx} (DELETED)
- Drop the collection_pool feature entirely: PB has no
collection_pool collection on the live instance
(pb:migrate:orders-v2 was never run), the codebase comment
in purchase-orders-service.ts says "now-removed", and the
sidebar entrypoint has been migrated to /sprint + /deliveries.
The replacement operational status is the `item_status =
"collection_pool"` enum value on order_items, which stays.
src/lib/server/orders-service.ts (+ test), src/lib/pb-helpers.ts,
src/components/sidebar.tsx, src/components/ui/assign-driver-dialog.tsx,
src/app/(app)/collection-map/sidebar.tsx,
src/app/api/mcp/route.ts (+ test) (MODIFIED)
- Remove listCollectionPool + getCollectionPool +
updateCollectionPoolStatus, the /collection-pool sidebar entry,
the deep-link in assign-driver-dialog and collection-map
sidebar, and the collection_pool.list MCP tool (the tool,
handler, and test mock).
scripts/migrations/2026-07-26-fix-quote-counter-and-reassign-numbers.ts (NEW)
- One-shot migration script that:
1. Reads every quote, sorts by created ASC.
2. Reassigns clean Q202607001 … Q202607{N} over the whole
table (including the e+22 garbage and the legacy
Qnnnnnn rows).
3. Sets quote_counters.(year=2026, month=7).last_seq = N
so the next /api/v1/quotes/next-number returns a clean,
atomic, unique number.
Default mode = dry-run; pass --apply to write.
Already executed against the live cloud PB during this session
(verified: 200/200 rows updated, counter at 200, next-number
returns Q202607201, Q202607202, Q202607203 on consecutive calls).
AGENTS.md (MODIFIED)
- Add data-layer-discipline block matching the style of the
three existing blocks. Documents which layer is legacy (pb-helpers)
vs current (lib/server/*-service.ts via /api/v1/*), gives the
migration order (customers → orders → quotes → suppliers →
companies → rest), and includes the grep one-liner future AI
agents should run before extending pb-helpers.
Not in this commit (your WIP, untouched):
src/app/(app)/{dashboard,delivery-notes,layout,orders/[id],
quotes/[id],quotes/approvals,quotes,sprint}/page.tsx,
src/app/(auth)/login/page.tsx, src/components/charts/*,
src/components/{company-form,customer-form,quote-email-card,
sprint-item-pricing-modal,supplier-form,theme-bootstrap,
theme-picker}*.{tsx,test.tsx}, src/app/api/invoicing/delivery-note/*
src/app/api/v1/quotes/[id]/pdf/route.ts, src/app/globals.css,
src/lib/{item-status-ui,pdf/quote-document,quote-status*,themes,
types}.ts, src/lib/server/{order-item-status*,quotes-service}.ts,
src/styles/themes.css, APP_CAPABILITIES.md, README.md,
docs/SHIPPED.md, scripts/smoke/screenshot-pages.mjs,
and the new untracked files under docs/, scripts/migrations/
2026-07-19…25, src/app/{(app)/deliveries,(app)/delivery-notes,
api/v1/ai-insights/rerun,api/v1/deliveries,api/v1/delivery-notes,
api/v1/quotes/[id]/partial-approve,api/v1/quotes/[id]/split},
src/components/{ai-insights-panel,ai-insights-renderer,
ai-item-history-trigger,brand-stripe,delivery-notes,ui/stock-item-search,
ui/duplicate-warning,ui/duplicate-warning.test}.tsx, supplier-form.test.tsx,
src/lib/{pb-helpers-duplicate.test,server/deliveries-service*,
server/delivery-notes-service*}, public/wfdash-logo*.png,
opencode.jsonc.
Add complete stock purchase order workflow including:
- Status tracking with sending/mail_failed states and UI labels
- Server-side PDF rendering and document storage
- File proxy API support for stock PO attachments
- Send outcome webhook callback for email notifications
Add driver app support:
- Unified stop list API for both customer deliveries and stock pickups
- Status update endpoint for collected quantities and problem reporting
- Collection map page support for stock pickup locations
Include matching PocketBase migrations, type definitions, and stock inventory fulfillment logic.
Add full integration for stock supplier pickup lines into the collection pool workflow:
- Add two idempotent database migrations for stock line drv_id and state tracking
- Update stock purchase number format to POyyMMddNNSTK convention
- Extend collection pool to load both customer order items and stock purchase items
- Add stock-specific UI, driver assignment flow, and mark collected action
- Update type definitions and service logic for stock purchases and line items
- Add comprehensive test coverage for all new workflows
This commit adds the complete v2/v3 stock management functionality:
- add new stock-specific item statuses and update the status visual map
- implement full stock purchase API suite (create, list, retrieve, send, receive)
- reorganize sidebar navigation to group stock-related pages together
- add stock item CRUD APIs, status utilities, low stock filtering, and surplus booking logic
- add database migration for the `stock_items.target_qty` field
- update type definitions and add comprehensive tests for all new stock components
This commit adds the full end-to-end delivery note workflow for the invoicing system:
- Added `letterheadForCompany` utility to infer the correct letterhead from a company name
- Created the DeliveryNoteDocument PDF component and server-side render function
- Added API endpoints for delivery note creation, PDF retrieval, printing, and print tracking
- Updated the PickingSlip component to support both invoice and delivery note headers
- Added view models for grouping, filtering, and displaying delivery note items
- Updated AGENTS.md with PocketBase schema and attachment best practices for new workflows
- Added comprehensive test coverage for all new components and API routes
- Lift customer query state to parent page to maintain consistent cache keys for QB customer lookups
- Restructure SingleOrderQbModal UI into grouped sections for better organization
- Update modal footer padding and add mobile-responsive layout
- Revise line item input labels and add accessible aria labels matching test selectors
- Refactor QB customer lookup effects and callbacks to support edited search queries
- Fix test selectors for invoice quantity and price fields to match updated labels
- add `costPrice` field to invoicing row types, plus ZAR currency formatting and profit margin calculations
- update invoice row displays to show ordered/received quantity, unit cost, selling price and margin metrics
- add per-row selection in QuickBooks send modals, letting users filter which order lines to include on invoices
- update ModalShell component to support responsive breakpoint-based sizing instead of fixed sm-only widths
- refine UI styling across the invoicing page to use consistent theme tokens (bg-card, muted-foreground)
- update tests to use dynamic yesterday dates instead of hardcoded values for stable, time-agnostic assertions
- improve button states, labels and text for QuickBooks invoice actions, including showing selected item counts
Add full Tangerine brand theme with light and dark mode variants to the app theme system.
Add four new design and integration documents for collection pool, map and driver app workflows.
Add initial scaffolding for three new plugin source directories for the collection pool bulk assignment feature.
Two-pin model per supplier bucket: blue circle + letter per driver,
amber circle + ✓/✕ for unassigned. Filters (status, readiness, driver
multi-select, search) all leave; date picker stays. No backend or
schema changes. Awaiting user review before plan + implementation.
Both the per-row Assign Driver button and the bulk-assign button in each
supplier section header now disable when the target row(s) have not been
marked ready for collection. Tooltips explain the reason.
This enforces the rule that a stop must be ready before it can be
assigned to a driver — same gate for single-row and bulk flows.
The supplier name is already shown in the SupplierGroup section header,
so repeating it on each row inside the section adds visual weight
without information. Keep it on the checkbox aria-label for screen
readers only.