Introduced a new link in the dashboard that allows users to access the mobile view of the application. The link features a smartphone icon and is styled for better visibility, enhancing user navigation options.
Updated the service worker registration logic to allow for specific service workers based on the route. Added support for a mobile service worker at `/m`, while maintaining existing functionality for `/counter` and `/new-order` routes. This enhances the PWA experience by serving optimized service workers for different app sections.
The pricing modal saves quote_description correctly, but the PDF
template rendered item.description unconditionally, so the
operator's edit never showed up on the customer-facing quote.
Auth gates redirected to a bare /login with no memory of where the
user was headed, and the login page always sent them to /dashboard
afterward — so opening the /m PWA while signed out landed you on the
main dashboard instead of back on /m. Both gates now pass ?next=,
and login honors it (falling back to /dashboard if missing/unsafe).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a chevron button at the left edge of every delivery-note row on
/deliveries. Clicking reveals an inline items panel (description +
qty) so the operator can match against the printed DN paper in their
hand before loading.
Items come from a client-side filter of the order group's items array
(items.filter(i => i.delivery_note === dn.id)). No new server endpoint.
DNs whose items aren't in the order group's slice show a 'See full DN →'
link to /delivery-notes/[id].
Expand state is held at the OrderGroupView level as a Set<string> of
expanded DN ids — independent per DN, resets on refresh. (State lifted
to OrderGroupView rather than DeliveriesPage because order.items is
already in scope there; avoids prop-drilling through CustomerGroupView.)
Service change: deliveries-service.listDeliveryItems now fetches the
delivery_note (relation id) + unit fields on each item, so the page
can filter and render units without an extra round-trip.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The 'Total {sumQty}{unitSuffix}' header line now explicitly documents
how mixed / missing units are handled: suffix is omitted when all
items have empty/null units (operator defaults to 'EA'); when units
are mixed, no suffix is shown; when all items share a non-EA unit
(e.g. BOX), the suffix is shown.
Self-review pass; no other changes.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Click-to-expand on each delivery-note row reveals an inline items
panel (description + qty) below the row. Operator can match the items
against the printed DN paper in their hand before loading. Pure
client-side render — items already loaded by /api/v1/deliveries are
filtered by delivery_note == dn.id. Empty state with fallback link to
/delivery-notes/[id] for DNs whose items aren't in the page slice.
Single-file edit on src/app/(app)/deliveries/page.tsx. No server,
schema, API, or MCP changes.
Co-Authored-By: Claude Code <noreply@anthropic.com>
The plan + summary landed locally during the implementation but were
not committed alongside the code. Both documents describe the
rep-scoped Orders / Quotes / Invoicing / Delivery Notes list filter
work that already shipped (commits 8faa443, ed07617, 29eb003, 9cf8e39
etc).
- 2026-08-31-rep-scoped-list-filter.md — the implementation plan
- 2026-09-01-rep-scoped-list-filter-SUMMARY.md — post-implementation
summary with the commit log + verification notes + the known
limitation (Orders/Quotes tab counts are unfiltered for reps)
Co-Authored-By: Claude Code <noreply@anthropic.com>
The /login page previously had three tabs (Login / Register / Reset
Password) wired through useAuth(). The Register and Reset handlers
were removed in this pass — only the Login form remains, plus a
small UI/styling cleanup that was collateral.
- Drop Tabs imports + the active-tab state plumbing
- Drop regName/regEmail/regPassword/regConfirm + resetEmail state
- Drop handleRegister + handleReset handlers (login-only now)
- Drop useAuth().register / useAuth().requestPasswordReset destructuring
- Trim the local stylesheet comments that referenced the Base UI
tabs triggers (no longer applicable)
Co-Authored-By: Claude Code <noreply@anthropic.com>
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>
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.
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>
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.
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.
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>
- 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
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>
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>
- 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
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.
- 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
"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>
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>
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>
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>
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.
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>
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).