Commit graph

411 commits

Author SHA1 Message Date
6dc7204477 fix(invoicing): restore expand data on delivery-note PDF generation
PocketBase silently drops `expand` from the response once a `fields`
projection is present, unless the expand sub-paths are explicitly
listed in that same `fields` string. The `exclude_from_invoice`
projection added on 2026-08-30 didn't list any `expand.*` paths, so
order/company/customer all rendered blank on the delivery note PDF
(e.g. DN26090702).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 10:22:17 +02:00
95d93791f9 fix(rep): refresh the customer snapshot on reopen, not just on first load
Reported live: a customer (Benjamin) was pinned this morning, but the
rep check-in screen never showed him nearby no matter how far the
search radius was widened — even though the server-side query for him
was confirmed correct and fast.

Root cause: the customer snapshot only ever fetched once, on mount
(empty-deps useEffect). A PWA is rarely actually closed — it gets
backgrounded and resumed — so reopening the app after someone else's
pin landed didn't count as "loading" it again; the phone kept working
off whatever list it cached before that write happened. Widening the
radius only changes which of the *already-loaded* customers match; it
can't find someone who was never in the list to begin with.

This was invisible because the freshness label only showed the
calendar date ("updated 9/7/2026"), so a snapshot from three hours
earlier read identically to one from thirty seconds ago for the rest
of that day.

Fixes, in snapshot-freshness.ts (pure, 9 tests) + check-in-view.tsx:
- Label now shows relative age ("5 min ago", "3h ago"), ticking forward
  every 30s while the screen sits open.
- Snapshot older than 15 min is flagged with an inline "refresh" link.
- Refetches automatically when the app regains foreground focus AND
  the cached snapshot has gone stale (visibilitychange + staleness
  check — not on every glance, to avoid needless refetching).
- Always-available manual refresh icon in the header, independent of
  staleness, for a rep who has out-of-band reason to believe the list
  changed (e.g. told a colleague just pinned someone).

Separately, on the same report: "Henny" showing as nearby instead of
Benjamin was NOT a bug — there are 6 different "Hennie"-named customers
in the database, 5 unpinned and one (HENNIE, all caps) with real
coordinates 552m from Benjamin's, genuinely in the Driefontein area,
not Fochville. The app matched correctly; the ambiguous duplicate name
is a data-quality issue for the batch pin tool to help sort out
separately, not a code fix.

27 tests pass in src/app/(rep). Verified the server-side customer query
directly against PocketBase during diagnosis — confirmed correct and
not the source of the bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 09:10:18 +02:00
7ba0b9e71f feat(visits): batch pin tool — tag every customer on a shaft at once
The single-pin tool (pick one customer, click the map, save) doesn't
scale to how the customer base actually clusters: dozens of contacts
sit at Harmony Kusasalethu, at South Deep Shaft 3, and so on. This adds
a batch counterpart at /visits/pin-tool/batch: type a site name, tick
every customer there, click the map once, and all of them get pinned.

Site is a plain free-text field on customers (Customer.site in
lib/types.ts), not a separate sites collection — simpler to add and
maintain, at the cost of relying on the office typing the name the same
way each time. The batch tool's autocomplete (backed by
listDistinctSites) and its "select everyone already tagged X" shortcut
exist specifically to curb that drift without a second collection to
manage.

- jitterCoordinates (lib/geo.ts) scatters each customer 30-80m from the
  clicked point so forty pins on one shaft render as distinct dots on
  the office map instead of one marker stacked forty deep. Bounded by
  distanceKm in tests, which is the same great-circle maths the
  check-in radius match uses.
- assignCustomersToSite / listDistinctSites (customers-service.ts) —
  the batch write and its autocomplete source. Each customer is written
  independently, mirroring recordVisits: a stale id in a forty-customer
  batch shouldn't cost the other thirty-nine.
- use-customer-list.ts extracted from pin-tool-view.tsx (verbatim, no
  behaviour change) so the batch view doesn't duplicate the fetch.
- batch-pin-helpers.ts holds the pure decisions (save-readiness order,
  filter) the same way pin-tool-helpers.ts does for the single-pin tool
  — the Leaflet-dependent view is hard to unit test directly.

Applied the "use client" + dynamic(ssr:false) lesson from the single-pin
tool's earlier 500 up front in batch/page.tsx, with a comment explaining
why it's load-bearing rather than decorative.

99 new/updated tests. Scoped verification only (geo, customers-service,
visits/*) — the wider suite currently shows unrelated failures in
quotes-service/quote-pdf-service from a change on main I did not make
and did not investigate further; none of it touches customers, geo, or
visits.

BLOCKED until one PB admin change: customers.site does not exist yet
(text field, optional). Nothing in this commit writes to it until then.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 08:30:23 +02:00
66559af422 Add quote PDF generation and stale PDF handling 2026-09-07 07:08:27 +02:00
6bedf27617 fix(pos): print the letterhead's own logo, and match delivery note to the sale slip
Logo — three separate faults, all of which had to be fixed for the
letterhead logo to print at all:

1. receipt.html hardcoded '/wfdash-logo.png' for the sale slip, so it
   printed the app's bundled mark (the solid black W block) and never
   looked at the letterhead at all.
2. The paths that DID use the letterhead (printTest, cashup) passed
   PocketBase's fully-qualified cross-origin URL into imageToRaster(),
   which draws to a <canvas> — getImageData() taints and throws on a
   cross-origin image, so the .catch(() => null) silently dropped the
   logo. pos-letterhead-service now returns a same-origin
   /api/v1/files/... URL (per AGENTS.md's file-URL rule).
3. letterhead_settings wasn't whitelisted in that file proxy, so the
   same-origin URL would have 403'd.

Verified against the live WESTSCOTT letterhead: its SVG rasterizes to
384x137 at ~15% ink coverage (line art), no tainted canvas.

Also fixes the company name never printing: every builder read
`letterhead.name`, but the POS letterhead record exposes
`display_name` — so the name line silently rendered empty on every
slip, on screen and on paper.

Delivery note now matches the sale slip, per request: gains the logo
and business_info blocks and the same 2x-size title, instead of being
a stripped-down variant. This also reconciles the transport
divergence flagged earlier (Bluetooth baked business info into its
header; the network builder omitted it entirely) — it is now one
reorderable business_info block on both.

resolveBlocks() now slots a newly-shipped block in at its intended
position rather than appending it to the end. Without this, any
letterhead with an already-saved layout would have printed the new
logo/business_info blocks BELOW the signature line.
2026-09-06 23:03:24 +02:00
a7341397a6 fix(pwa): restore apple-mobile-web-app-capable so iOS installs standalone
Adding /rep to an iPad home screen and tapping it opened the main app,
not the rep app — while the shortcut URL was still /rep.

Cause: Next 16 renders `appleWebApp.capable: true` as the standardised
<meta name="mobile-web-app-capable"> and no longer emits the
apple-prefixed name. iOS only reads the apple-prefixed one. Without it
the icon is a plain Safari bookmark rather than a standalone app, so
tapping it foregrounds Safari on whatever tab was already open — which
presents exactly as "it opened the main app instead".

This was not specific to /rep. Verified against the running app that
/m (Boss Phone) had the same gap, so it has never actually launched
standalone on iOS either; /counter likewise. The comment in
(counter)/layout.tsx asserting the root layout already supplied this
tag was wrong — the root also only set `appleWebApp.capable`.

Declared in the root layout so all four surfaces inherit it, and
repeated in the (rep) and (mobile) layouts because a child's
`metadata.other` replaces the parent's rather than merging into it.
Corrected the misleading counter comment.

Verified by fetching each route and asserting the rendered head:
/rep, /m, /new-order and /dashboard now all emit
apple-mobile-web-app-capable="yes" while still resolving their own
manifest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 21:56:01 +02:00
c97b2cf018 fix(visits): surface batch write failures instead of a silent 200
Found by driving /rep in a real browser session, not by a unit test:
submitting a check-in returned 200 OK with body {"created":0,"pinned":1},
and the visit vanished with no trace while the customer coordinate pin
still landed. Reproduced against the live database (confirmed via
direct PocketBase query) and again after the fix, with the response
now 502 and the entry correctly retained in the outbox.

Root cause was two-layered:

1. visits-service.ts caught every per-visit write error and continued
   silently, discarding the message. A per-item failure (one stale
   customer id in a batch) is meant to be swallowed so it doesn't cost
   the other visits — but the same catch also swallowed a systemic
   failure (every item in the batch hitting an identical PB validation
   error), which looks completely different: a bare `created: 0` gives
   the caller no way to tell "nothing to do" from "everything failed".
   recordVisits now returns an `errors: {customerId, message}[]` array
   instead of discarding the exception.

2. POST /api/v1/visits returned HTTP 200 whenever the request was
   well-formed, regardless of whether anything was actually written.
   The offline outbox treats any 2xx as delivered and discards the
   entry — so a total write failure was indistinguishable from success
   at the one layer that decides whether to keep retrying. The route
   now returns 502 when `created === 0`, which the outbox already
   retries (see the existing 5xx-keeps-the-entry test), rather than a
   4xx, which it treats as permanent and drops.

The concrete trigger was live: rep_visits.position_source doesn't yet
allow "phone" (flagged in the previous commit). That specific gap will
close once it's added to the PB select, but this fix stands on its own
— any future rejected field would have hit the identical silent-loss
path.

12 new tests (service: captured errors on per-item and total failure;
route: 502 vs 200 boundary, repId taken from the session not the body;
outbox: a 502 keeps the entry queued rather than treating it as sent).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 21:25:52 +02:00
c8b7e1f51b feat(rep): visit check-in PWA — phone GPS, offline outbox
The rep app: open it on site, it finds who's around you, tick who you
saw, done. Third instance of the per-route-group PWA pattern already
running here (/m boss phone, /counter), so it reuses MobileAuthGate and
gets its own manifest + service worker.

Phone GPS is the primary position source, not the vehicle tracker. The
tracker knows where the bakkie is; the phone knows where the rep is,
and those diverge by a few hundred metres once the rep walks into the
plant. GPS also needs no network, which matters because mine shafts
have poor signal. The tracker is the secondary path
(GET /api/v1/visits/position) for when location is denied or
unavailable. The independently-collected vehicle trail corroborates
check-ins afterwards, so honesty doesn't depend on the phone.

Matching runs on the phone, not the server. The app caches a compact
customer snapshot and runs rankNearbyCustomers locally, so there's one
code path that behaves identically online and off — rather than a
server path plus an offline fallback that only gets exercised in the
places it's hardest to debug.

- (rep)/rep/            layout, page, check-in view, outbox
- visits-service.ts     getCustomerSnapshot, recordVisits
- POST /api/v1/visits           record a check-in
- GET  /api/v1/visits/customers snapshot for the offline cache
- GET  /api/v1/visits/position  tracker fallback
- public/manifest-rep.json, public/sw-rep.js, pickSwPath() branch

These routes authenticate with the browser session, not AGENT_API_KEY —
the caller is a logged-in rep. The rep id comes from the validated
session rather than the request body, so a client can't log a visit as
someone else.

The outbox uses localStorage rather than IndexedDB deliberately: the
payloads are tiny and localStorage is synchronous, which matters when
the rep hits Save and immediately pockets the phone. Entries are only
removed once the server acknowledges them, so a failed flush retries
instead of dropping a visit — duplicates are preferred over loss, and
surfaced rather than merged. Permanently-rejected entries (4xx other
than 401/408/429) are dropped so one bad row can't wedge the queue.

recordVisits writes each visit independently: a stale customer id
shouldn't cost the other four visits in a queued batch.

BLOCKED until one PB admin change: rep_visits.position_source does not
yet allow "phone" — verified against the live instance, PB returns
400 "Invalid value phone". Add it to the select. Separately,
accuracy_m is silently accepted and discarded (PB ignores unknown
fields), so GPS accuracy is thrown away until that number field is
added; not blocking.

47 new tests. Verified on :4001: /rep serves 200 with the right
metadata, manifest-rep.json and sw-rep.js are served.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 21:09:27 +02:00
a551a71277 feat(tracker): breadcrumb poller — WFDASH's own vehicle position history
FleetAI has no position-history endpoint, so to answer "where was this
rep's vehicle at 10:20 last Tuesday" we have to record it ourselves.
An n8n Schedule Trigger calls POST /api/v1/tracker/poll every 5 minutes
during working hours; a nightly run derives stops and prunes.

  poll:     */5 6-18 * * 1-5     (the working-hours window lives in the
                                  cron, not in app settings — disabling
                                  the workflow is the off switch)
  nightly:  30 2 * * *

One getTokenBaseLiveData call covers the whole fleet per cycle, and only
genuinely new GPS fixes are written: dedupe keys on (vehicle_no,
fixed_at) against the newest fix already held. A parked tracker keeps
re-reporting the same GPSActualTime, so without this a 5-minute cycle
would store the same row a dozen times an hour.

Raw breadcrumbs are pruned at 35 days; derived rep_stops are kept
indefinitely, since a stop is small and is what reports actually ask
about.

Two bugs found by running this against the real stack, neither of which
the unit tests could have caught:

- FakeTrackerAdapter derived its fix time from Date.now(), so every poll
  looked like new movement. It could never reproduce a parked vehicle —
  the exact case dedupe exists for — and made the poller look correct
  while storing a duplicate every cycle. The fake now quantises its fix
  time to a bucket, so repeated polls inside one window return an
  identical fix.
- PocketBase's filter parser silently matches NOTHING against an
  ISO-8601 `T` separator. `fixed_at >= "2026-09-06T00:00:00.000Z"`
  returned 0 rows where the space-separated form returned 2, with no
  error either way — the rollup would have derived no stops and the
  prune deleted no rows, both reporting success, forever. Fixed with a
  `pbDate()` helper matching the existing convention in
  boss-dashboard-service.ts and order-delivery-note-link-service.ts.

Verified end to end on :4001 against the live PocketBase: three
consecutive polls returned stored=0/duplicates=1, and the nightly rollup
went from vehicles=0 to vehicles=1 once the date format was fixed. Test
rows were removed afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 20:08:20 +02:00
b09a7e2e56 fix(visits): pin tool page 500s without the "use client" directive
`GET /visits/pin-tool` returned 500 on every request:

  x `ssr: false` is not allowed with `next/dynamic` in Server
    Components. Please move it into a Client Component.

The page uses `dynamic(..., { ssr: false })` to keep Leaflet off the
server (it touches `window` on import) but was missing the
`"use client"` directive that `supplier-map/page.tsx` carries for
exactly the same reason.

Worth noting how this got through: it fails at request time, not build
time, so `tsc --noEmit` was clean and all 32 unit tests passed. Nothing
renders the page, so only loading it in a browser surfaces it. Verified
by hand — 500 before, 200 after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 17:00:19 +02:00
61c5d8c906 feat(visits): office pin tool for seeding customer coordinates
Radius-based rep check-in has nothing to match against until customers
carry real positions: 0 of 616 had any — every `coordinates` geoPoint
was PocketBase's `{lon: 0, lat: 0}` default, and most company addresses
are P.O. Boxes, so geocoding can't seed them either. This is the tool
that seeds the ~20 main West Rand shafts by hand.

- /visits            landing page; a launcher until the visit-history
                     view has data to show
- /visits/pin-tool   pick a customer, click the map, save
- setCustomerCoordinates() in customers-service.ts, with lat/lng range
  validation so a string off a form input can't write a poisoned row
- PATCH /api/v1/customers/[id]/coordinates for agent/MCP callers
- Visits entry in the sidebar's Management group, plus MOBILE_TITLE_MAP

The browser writes through a Server Action, not the /api/v1/ route:
that route sits behind `assertAgentAuth` and a browser has no
AGENT_API_KEY. Both paths share `setCustomerCoordinates`. Mirrors the
existing saveAiSettingsAction split.

Pure view logic lives in pin-tool-helpers.ts so it can be tested away
from the Leaflet runtime.

Also refreshes the PB schema export — `rep_visits`, `rep_stops`,
`rep_vehicle_positions` and `users.tracker_vehicle_id` now exist on the
live instance.

32 tests. The suite's 9 pre-existing failing files are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 16:58:07 +02:00
f336404f71 fix(invoicing): refresh a cached Invoiced tab after stock is booked
Booking stock from the To Invoice tab did not show up on the Invoiced
tab when that tab had already been visited once. The new-booking event
was ignored unless the operator happened to be on Invoiced at the time,
and switching back afterwards found a non-null `stockBookings` cache and
skipped the fetch — so the freshly booked PO stayed invisible until a
full reload.

The event handler now invalidates the cache first and only then decides
whether to re-fetch, so the tab always picks up the new row. The initial
load is deferred by a zero-delay timeout to keep the fetch out of the
tab-switch render pass.

Also corrects the `loadStockBookings` comment, which referred to a
`recentStockBookings` binding that no longer exists, and notes that rows
sort by `booked_at` rather than the stock PO's own dates.

Covered by a new test that visits Invoiced, switches to To Invoice,
fires the booking event, and asserts the second fetch happens.

This change was already present in the working tree at the start of the
session that committed it; committed here on request. The one unrelated
failure in this file ("shows a Printed badge in the Invoiced tab") is
pre-existing and was verified identical on a stashed tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 14:30:50 +02:00
102673d00b feat(pos): add GET/PUT /api/v1/pos/slip-layout/[letterheadId] 2026-09-04 13:24:00 +02:00
7d2132e000 fix(pos): fix uncaught 500 on checkout, allow overselling into negative stock
- checkoutSale's stock-availability loop called pb.getOne() with no
  try/catch, so a stale/deleted stock_item_id in a persisted cart
  threw all the way up to a bare 500 with no useful message. Now
  caught and returned as a clear pb_error the till can display.
- Removed the insufficient-stock block entirely: the till is used
  ahead of a physical stock take, so a sale must never be blocked
  for lack of stock — current_qty is now allowed to go negative and
  gets corrected at the next stock take. Drops the now-dead
  insufficient_stock/shortages branches from the route, service
  types, and client error handling.
2026-09-04 13:00:12 +02:00
85761a8698 fix(orders): improve error handling for order creation and enforce email body length limit
Enhanced error messages to include specific field validation details from PocketBase, allowing callers to identify issues more easily. Additionally, implemented a safeguard to truncate the email body to a maximum of 5000 characters to prevent validation errors from surfacing as generic messages.
2026-09-04 12:55:53 +02:00
46d5d540a0 feat(pos): letterhead service + 2 API endpoints + vat_percent on Letterhead type 2026-09-04 09:13:53 +02:00
aed3f5c861 feat: implement discount functionality in POS system
Added support for discounts in the POS checkout process, allowing operators to apply either a fixed amount or a percentage off the gross total. Updated the state management to include discount details and modified the receipt and cart rendering to display pre- and post-discount values. Enhanced the checkout API to handle discount inputs and ensure proper calculations. Updated relevant tests to cover new discount logic.
2026-09-03 12:19:21 +02:00
c64e5dc2ab feat: expand customer relation in PDF generation
Enhanced the PDF generation for quotes by expanding the customer relation to include the associated company information. This allows the company name to be displayed prominently in the "TO:" block of the PDF, improving clarity for users. Updated test data to reflect the new structure and ensure proper rendering of the company name when available.
2026-09-02 11:40:50 +02:00
ccb27db82f feat: add phone view link to dashboard
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.
2026-09-02 11:06:15 +02:00
23d4e8dfe1 chore: update page.tsx 2026-09-02 01:38:33 +02:00
058803d90f chore: update AGENTS.md 2026-09-02 01:17:05 +02:00
ac201cb0a1 fix: preserve destination through the login redirect
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>
2026-09-01 15:47:27 +02:00
2014814c84 feat(deliveries): click-to-expand DN items panel
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>
2026-09-01 12:15:35 +02:00
742010a9a9 chore(login): simplify to login-only, remove register/reset tabs
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>
2026-09-01 02:11:28 +02:00
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
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
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
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
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
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
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