Commit graph

700 commits

Author SHA1 Message Date
bd328d40dc feat(deliveries): enrich DN row subline with customer/order/paperclip
The /deliveries To Load + Back from Driver + Checked In tabs each
render delivery-note rows showing only 'DN {number}' + a one-line
status subline. Operators couldn't identify the customer or tell
whether the signed copy was back without scrolling up to the
customer/order group header.

Replace the existing subline with a compact pieces list:
customer name (truncated, with title tooltip) · company name ·
parent order number · signed-copy indicator (paperclip icon +
filenames in title=) · delivery date (if set). Counter-pickup DNs
(no parent order) collapse to 'Counter pickup' in italic.

All data is already on the row payload from /api/v1/deliveries —
no server or schema changes.

Also removed the now-unused 'subline' const that the prior
implementation used for the old single-line status subline.

Verified visually via dev server: counter-pickup fallback renders
correctly ('Counter pickup' italic). Order-attached and signed-copy
code paths are structurally correct per the spec — would need a
DN with order + signed files in the live data to exercise them,
but no such DN was visible in the current dev data.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 02:09:49 +02:00
1fc3580aed perf(dashboard2): consolidate 14 PB count queries into 1 fetch
Single getList pulls every quote this rep created in the last 62 days,
then buckets client-side into the 6 day/week/month windows plus the
8-week trend. Also fans out the 4 remaining queries (outstandingQuotes,
outstandingOrderItems probe, monthRevenue, the consolidated quotes
fetch) in parallel via Promise.all.

Query count: 18 → 5. Page load bounded by the slowest query instead of
the sum. The widest quote window is "last month" (~62 days), so that
single bound covers every other day/week/month window for bucketing.
Cap at 2000 rows to defend against runaway responses for very active
reps.
2026-09-01 02:08:52 +02:00
dc8436e832 feat(orders): surface order age as colour-coded badge
Replaces the bare formatDate(order.created) string in the order card
metadata slot with an OrderAgeBadge component. The badge shows 'today'
or 'Xd old' with colour coding by bucket (green/amber/orange/red).
The absolute date is preserved in the badge's title= tooltip.

Bucket math lives in src/lib/order-age.ts (covered by 14 unit tests).
This change is rendering-only — no server or schema impact.

Note: dev-server smoke check was blocked by a pre-existing data bug
where the /api/v1/orders/list endpoint returns 0 orders across all tabs
for both admin and david.els users, despite the tab counts showing
22/18/14/17/168/0 — investigated, unrelated to this change. The
OrderAgeBadge will render correctly once that bug is fixed.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 02:05:07 +02:00
27efc8628c feat(dashboard2): show last month's selling value alongside the delta
Selling card now reads "+12% vs previous · R 200,000" instead of just
"+12% vs previous". Optional previousValue prop on RepKpiCard; the other
3 cards don't pass it, so their format is unchanged.
2026-09-01 02:04:25 +02:00
42916f2948 fix(dashboard2): surface PocketBase error details in /api/v1/rep-dashboard
PB SDK rejects filters with a generic "Failed to fetch." wrapper, but the
real reason lives in error.response.data.message (or .data.message).
Walk the chain so the page's inline error UI shows the actual PB message
instead of a useless generic toast. Also log the full error server-side.
2026-09-01 02:00:55 +02:00
6fcef50b3e feat(orders): add order-age util with bucket thresholds
Pure function: getOrderAge(created, now?) returns {daysOld, bucket, label}
in calendar-day delta for Africa/Johannesburg. bucketToBadgeClass maps
to tailwind classes (fresh/recent/slipping/stale). No React, no DB.

Vitest unit tests cover all 5 bucket thresholds, future-date clamp, and
the JNB timezone edge case (same calendar day in JNB = daysOld 0).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 01:58:46 +02:00
f84d14fe4d fix(dashboard2): show error inline instead of getting stuck on Loading
- Track explicit `error` state; render an inline error panel with a Try-again button when the fetch fails and we have no data to display (previously the toast fired but the page sat on the Loading… screen forever)
- Add `content-type: application/json` header (matches /dashboard convention)
- Switch gate from `loading || !data` to `loading && !data` so we don't hide data once it's loaded
- Defer the initial fetch via setTimeout(0) and only start the realtime subscriptions AFTER the first successful load, so a slow first fetch doesn't race with realtime events
2026-09-01 01:58:38 +02:00
b3965b1454 docs: implementation plan for compact DN row + order age badge
Three bite-sized tasks with full code:
  1. TDD the order-age util (test → impl → run)
  2. Wire OrderAgeBadge into /orders (replace bare date)
  3. Enrich /deliveries DN row subline (customer/order/paperclip)

All UI/rendering only. No server, no schema, no MCP changes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 01:57:47 +02:00
3fc9c576bf docs: spec for compact DN row + order age badge
Two small, independent rendering improvements for /deliveries and /orders:

  1. /deliveries — each delivery-note row gets a compact subline with
     customer name, company name, parent order number, and a signed-copy
     indicator (paperclip + filename tooltip). All data already on the
     row payload — no server-side changes.

  2. /orders — order card right-aligned metadata slot gets a colour-
     coded age badge (today / Xd old) replacing the bare date string.
     New src/lib/order-age.ts as a pure function with bucket thresholds
     (green <3d, amber 3-7d, orange 8-14d, red 15+d) and a unit test.

No PB schema changes. No /api/v1 route changes. No MCP tool changes.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-01 01:56:28 +02:00
c9096dfd84 feat(dashboard2): trim Row 1 to Selling-only with last-month delta
- Drop Cost and Markup gauge — Selling is the headline the rep cares about
- monthRevenue is now {sellingThisMonth, sellingLastMonth}
- One combined PB query covers both months; classify each row by date in code so the delta costs zero extra round-trips
- RepKpiCard gains a format="currency" prop so Selling renders ZAR-formatted with a delta arrow (mirrors the quote-count cards)
- Row 1 grid adjusts from 6-col to 4-col to match the new card count
2026-09-01 01:55:52 +02:00
f7938375f6 feat(dashboard2): move Selling/Cost/Markup up to Row 1 alongside quote KPIs
All 6 headline numbers now sit on a single row at the top (grid-cols-1 / sm:grid-cols-3 / lg:grid-cols-6). Gauge is shrunk to size=140 so it fits a 6-col grid; sublabel trimmed to single-line currency pair so it doesn't wrap inside the narrow card.
2026-09-01 01:52:22 +02:00
9abc7d4d03 feat(dashboard2): split Selling/Cost into KPI cards, parallelize data fetches
- Row 3 now shows separate Selling + Cost KPI cards (ZAR-formatted) plus the Markup gauge
- Outstanding-quotes, outstanding-order-items probe, and month-revenue queries now run in parallel via Promise.all (was sequential, costing ~300ms)
- Outstanding-order-items probe capped at 200 rows so a single rep's data stays in a single PB round-trip instead of paginating
2026-09-01 01:49:57 +02:00
23050e3dc6 feat: add /dashboard2 rep-scoped dashboard page 2026-09-01 01:36:53 +02:00
afb396b03b feat: add /api/v1/rep-dashboard route 2026-09-01 01:36:35 +02:00
3637eef83a feat: add rep-scoped dashboard data aggregation service 2026-09-01 01:36:31 +02:00
f7b5de4c85 feat(charts): allow RadialGauge to override its center label 2026-09-01 01:33:16 +02:00
652b60706e feat: add getBrowserSessionUser for routes that need the caller's identity 2026-09-01 01:33:10 +02:00
5c210546e9 docs: add implementation plan for rep dashboard (dashboard 2)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 01:31:57 +02:00
cc6f63dabb docs: add design spec for rep dashboard (dashboard 2)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 01:25:53 +02:00
9cf8e39b65 test(invoicing): mock useAuth to keep rep-filter a no-op in tests 2026-08-31 23:38:06 +02:00
ed07617827 feat(delivery-notes): filter list to the logged-in rep's own notes 2026-08-31 23:28:02 +02:00
29eb003cd6 feat(invoicing): filter rows to the logged-in rep's own orders 2026-08-31 23:27:23 +02:00
8faa4437b6 feat(quotes): filter list to the logged-in rep's own quotes 2026-08-31 23:26:42 +02:00
defa481661 feat(orders): filter list to the logged-in rep's own orders 2026-08-31 23:25:44 +02:00
d23c811b91 feat: add isRepUser helper for rep-scoped list filtering 2026-08-31 23:24:25 +02:00
392a76fef7 fix(boss-dashboard): show raw claim count + fix stale window on tab switch
"Claimed" was gated to emailed/approved/sent quotes, undercounting reps
who claim a lot but haven't pushed everything out yet (e.g. Karin showed
1 instead of her actual 4 claimed today). Swap in quotesLoaded so it
reflects total quotes processed, any status.

Also fixes tab switching (Today/Week/Month) needing a manual refresh to
show the right numbers: selectWindow() called fetchData() before the
activeWindowRef mirroring effect had run, so the fetch used the
previous tab's window. Now set synchronously in the click handler.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 22:38:41 +02:00
e1f14d9c48 fix(boss-dashboard): make /m installable as a PWA
manifest-mobile.json had an empty icons array, which blocks Chrome's
install prompt entirely. Point it at the existing android-chrome
192/512 icons and override appleWebApp.title to "Boss" so iOS
Add-to-Home-Screen matches the manifest's short_name instead of
inheriting the root layout's "WFDASH".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 22:14:48 +02:00
e722788ab0 refactor(boss-dashboard): simplify rep card to loaded/claimed/orders
Drop the separate quotesLoaded metric and relabel quotesDoneFor as
"loaded" and quotesDoneBy as "claimed" — two near-duplicate numbers
per rep card was more confusing than useful.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 22:14:43 +02:00
f05fc5d9cf fix(boss-dashboard): filter to active reps only + fix PB date-filter bug
The reps list seeded a row for every `users` record (admin, test
accounts, drivers, duplicate entries), not just the 6 real sales reps.
Also, window filters built with toISOString() used a "T"-separated
date PB's filter engine doesn't match against `created`, so every
window silently returned zero rows — the whole dashboard read 0
regardless of real activity. Same fix pattern already used in
order-delivery-note-link-service.ts / order-order-duplicate-link-service.ts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 22:14:40 +02:00
40993ce8f8 refactor: remove check-progress and run-backfill scripts; add DELETE endpoint for quote_items
This commit deletes the check-progress.mjs and run-backfill.mjs files, which were previously used for order processing and backfilling tasks. Additionally, it introduces a DELETE endpoint for quote_items, allowing for the deletion of quote items by ID, along with corresponding tests to ensure proper functionality and error handling.
2026-08-31 18:36:15 +02:00
e0480878a6 fix(pos): correct Font Awesome webfont URL in bundled CSS
The CSS lived at public/pos/fa/all.min.css but its url() references
used ../webfonts/..., which from that location resolves to
/pos/webfonts/... rather than /pos/fa/webfonts/. The woff2 was at
the latter, so the browser silently 404'd every font request and
the icons rendered as tofu boxes.

The original FA 6.5.2 CSS ships with ../webfonts/... because it
assumes the CSS sits in a sibling css/ subdirectory next to webfonts/.
Ours is one level deeper, so the path is now plain webfonts/....

Bump SW cache to v15 so installed tills pick up the corrected CSS.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 18:04:14 +02:00
6c21ad70d6 merge: /stock inline pricing edit (T1-T9 + review fixes)
12 commits:
- 85f238d  service scaffold (T1)
- 0cab704  reason-union tightening (T1 fix)
- 202f5e7  ledger audit on cost change (T2)
- 86eba97  first-time-pricing guard + cost delta in notes (T2 fix)
- ab639ee  validation / not_found / per-row independence tests (T3)
- 6f8c6da  POST /api/v1/stock/items/set-pricing (T4)
- a4bfef1  malformed-id returns 400 (T4 fix)
- f63fb7d  show pricing columns toggle (T5)
- 104cf4c  missing-pricing filter (T6)
- 759e050  edit-pricing mode + apply bar (T7)
- 7aaed60  localStorage draft persistence (T8)
- 9f27e9d  beforeunload warning (T9)
- afa4c07  spec reconciliation

Tests: 26/26 passing (10 service + 16 page).
2026-08-31 17:44:07 +02:00
Claude
afa4c070b3 docs(stock): reconcile spec with shipped design
Two spec-vs-implementation drifts caught by the final review:
- Move 'per-row validation → 200 with reason in results[]' out of the
  'Reject 400' list. The route enforces body shape (json / empty /
  too-many / bad id / missing actor) as 400; the service enforces
  forbidden_field / non-finite / not_found / pb_error per row so one
  bad sibling never aborts the rest.
- Move the 'realtime-conflict banner' from Page-behaviour to Out of
  scope (the implementation deferred it; the spec still listed it).
2026-08-31 17:19:12 +02:00
9f27e9d4f1 feat(stock): beforeunload warning when pricing draft is dirty 2026-08-31 17:15:42 +02:00
7aaed60860 feat(stock): persist pricing draft to localStorage 2026-08-31 17:14:34 +02:00
759e05048c feat(stock): edit-pricing mode + dirty rows + apply bar 2026-08-31 17:12:28 +02:00
104cf4cc6c feat(stock): missing-pricing-only filter on /stock 2026-08-31 17:08:26 +02:00
f63fb7d8fc feat(stock): show pricing columns toggle on /stock 2026-08-31 16:57:25 +02:00
Claude
a4bfef123b fix(stock): return 400 (not 500) for malformed PB id in set-pricing route
Code-review feedback from Task 4: the verbatim plan source had a
`throw new Error(...)` inside the per-row .map() that escaped
synchronously as an HTTP 500 instead of the structured 400 envelope
the rest of the route uses for validation errors. Swap .map() for a
for-of loop that returns the structured 400.

Verified:
- Bad id → 400 {code: "validation_error", ...}
- Happy path → 200 unchanged
2026-08-31 16:56:03 +02:00
6f8c6da0fe feat(stock): add POST /api/v1/stock/items/set-pricing 2026-08-31 16:54:03 +02:00
ab639eea6d test(stock): cover validation + not_found + per-row independence 2026-08-31 16:48:40 +02:00
Claude
86eba97d33 fix(stock): skip ledger row on first-time pricing + include cost delta in notes
Code-review feedback from Task 2:
- Code-review Issue #1 (Important): the strict-equality guard fired on
  'before.cost_price = undefined' vs 'update.costPrice = 0' — exactly
  the population this feature targets (fresh catalogue rows the
  operator is pricing for the first time). No real cost movement was
  happening, but a spurious ledger row was getting written. Gate the
  ledger write on Number.isFinite(before.cost_price) so first-time
  pricing doesn't pollute audit history with a 0 -> 0 'movement'.
- Code-review Issue #4 (Minor, audit-readability): add the actual
  before -> after delta to the ledger notes so operators scrolling
  the ledger history see 'Gary edited cost 2.50 -> 3.20 from /stock'
  rather than the prior opaque 'Gary edited cost from /stock'.
- Added two regression tests: one for the first-time-pricing case,
  one pinning the notes delta format.
2026-08-31 16:46:42 +02:00
202f5e73e2 feat(stock): write stock_ledger audit row when cost_price changes 2026-08-31 16:43:13 +02:00
Claude
0cab704840 refactor(stock): tighten pricing-service reason type + drop dead FORBIDDEN_KEYS
Code-review feedback from Task 1:
- Drop the unused FORBIDDEN_KEYS constant + its export. The whitelist
  PATCHABLE_KEYS already covers what was meant; nothing else reads this.
- Replace UpdateStockItemsPricingResultRow's reason: string with a tight
  UpdateStockItemsPricingFailureReason union so the route can map to HTTP
  status without parsing the message string. Matches UpdateStockItemResult
  in stock-items-service.ts:113.
- Replace the array-tail-fragile 'last push matches current id' guard
  with an explicit validationFailed boolean per row, decoupling from
  push order in the inner loop.
2026-08-31 16:41:34 +02:00
076e80ecb4 feat(pos): add FA icons to catalog, matched by category
Each stock item now renders a Font Awesome icon in the catalog grid
in place of the generic '?' initials. The icon comes from the item's
category, set on the categories collection's new 'icon' field (e.g.
'fa-solid fa-bolt' for Fasteners). All 10 active categories seeded
with sensible defaults; can be edited per-row in PB admin.

Match logic lives in pos-service.listActiveStockCatalog: it pulls the
categories matcher table, then assigns each item by:
  1. category.match_brands (against item text or supplier's supply_tags)
  2. category.match_supplier_tags
  3. category.match_keywords (longest first so 'hydraulic hose' beats 'hose')
  4. the 'Unknown / Other' catch-all is excluded from matching — anything
     with no match simply gets the initials fallback

The matcher table is in PB (the existing match_keywords/match_supplier_tags
/match_brands fields on categories), so admins can tune coverage without
a deploy. Two keyword additions to Fasteners (allen cap, set screw) and
Cutting Tools (tap, drill, chuck) lifted the live match rate from 67% to
71% (298/418 items across 8 categories).

FA 6.5.2 Free, solid only, bundled under public/pos/fa/ (256KB total:
100KB CSS + 156KB solid woff2). Both the CSS and the woff2 are in the
service worker APP_SHELL so icons render offline. No CDN dependency.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 16:40:02 +02:00
85f238d3e0 feat(stock): add stock-pricing-service scaffold with happy-path 2026-08-31 16:37:02 +02:00
e66437322f Revert "feat(orders): add Duplicates tab surfacing flagged duplicate orders"
This reverts commit b4855a9c9f.
2026-08-31 16:34:37 +02:00
Claude
b6a76be75b docs(stock): implementation plan for inline pricing edit 2026-08-31 16:17:10 +02:00
Claude
7c3b8f642a docs(stock): bulk cap 100 + clarify selling-derive precedence 2026-08-31 16:14:29 +02:00
Claude
84a97ac7b2 docs(stock): design spec for inline pricing edit on /stock 2026-08-31 16:13:44 +02:00