Commit graph

682 commits

Author SHA1 Message Date
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
8ae4592108 feat(orders): add since=YYYY-MM-DD to backfill endpoint for scoped re-scans
Lets the operator scope a backfill to recent orders so off-peak
sanity passes don't disturb the active workflow. e.g.:

  POST /api/v1/orders/backfill-duplicate-checks?limit=200&since=2026-08-24

rescans the last 7 days up to 200 rows. Combines with limit/skip;
rejects malformed dates with 400 instead of silently no-op'ing.
2026-08-31 11:34:46 +02:00
b4855a9c9f feat(orders): add Duplicates tab surfacing flagged duplicate orders
Cross-cutting review tab at the end of the strip — lists every order
with any of the three duplicate-detection flags set:
  - order_number_duplicate
  - potential_duplicate_order
  - potential_delivery_note

Implementation:
  - src/lib/item-status-ui.ts: add 'duplicates' to TabGroup + visual +
    TAB_GROUP_ORDER (placed last so it doesn't compete with
    active-workflow tabs). Empty array in TAB_GROUP_ITEM_STATUSES
    (it's not an item-status group — the duplicates tab is
    cross-cutting on order-header fields).
  - src/lib/server/orders-service.ts: add duplicateOrderFilter() +
    orderHasAnyDuplicateFlag() helpers. listOrdersWithItems routes
    tabGroup='duplicates' to a PB filter on the orders collection
    (separate from the item-level tabGroupFilter the other tabs use).
    getOrderItemStatusTabCounts counts duplicates independently so
    the badge stays accurate even when the active-workflow counts
    are filtered out.
  - src/app/(app)/orders/page.tsx: add the tab to the strip + an
    empty state with the purple #a855f7 visual.
2026-08-31 11:30:09 +02:00
f1d23da685 Merge feat/wfsales-pos: WFSALES POS 2026-08-31 10:16:11 +02:00
63c943d05b feat(orders): include selling_price in duplicate-detection line match 2026-08-31 09:07:59 +02:00
ff150bdf6e feat(orders): include selling_price in duplicate-detection line match 2026-08-31 09:03:00 +02:00
bef2a08299 docs(pos): record created PB collections, live test results, catalog paging
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 02:25:28 +02:00
a956b70137 feat(pos): default catalog to recently added, render in chunks of 50
The till was painting all 373 active stock items at once, which is slow
and buries the stock staff actually reach for. The catalog now comes back
newest-first (PosCatalogItem carries created) and renders 50 at a time,
with a Show-more button that also auto-loads as it scrolls into view.

Header reads 'Recently added' when browsing and 'Results' once a search
or category is applied; changing either resets paging to the first chunk.
Scroll position is preserved across re-renders so adding to the cart no
longer throws the operator back to the top of the list.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 02:25:05 +02:00
8e6a6deac9 docs(pos): record implementation deviations and rollout blockers
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:51:56 +02:00
c56961d7d9 chore(pos): bump SW cache to v13, skip /api/ caching, scope manifest to /pos/
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:48:49 +02:00
c6e35ad5d7 feat(pos): delivery-note slip shows company name and optional prices
The Include-prices toggle drives both the on-screen slip and the
Bluetooth thermal print, so what prints matches what the operator saw.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:48:22 +02:00
58f06175e1 feat(pos): rewire cashup.html to /api/v1/pos/sales
Method split now reads pos_sales.payment_method directly. The four
method rows cashup already renders (cash/card/eft/account) would have
been permanently R0 for eft and account under a cash/card-only backend.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:46:26 +02:00
fac417a697 feat(pos): rewire history.html to /api/v1/pos/sales
Payment method now read from pos_sales.payment_method rather than
sniffed out of an order's notes with regexes.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:45:43 +02:00
600b3ee3bf feat(pos): rewire index.html product count to /api/v1/pos/catalog
Also surfaces a rejected API key explicitly instead of letting it read
as an offline blip.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:44:55 +02:00
e8e65aabb7 feat(pos): checkout via /api/v1/pos/checkout, drop orders/order_items/delivery_notes writes
Sends client_ref (the till's localId) so the offline queue can retry a
sale that already landed without recording it twice. Customer search
moves to /api/v1/pos/customers and now carries the company name for the
delivery-note slip.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:44:31 +02:00
47f4c73f18 feat(pos): load catalog from /api/v1/pos/catalog instead of PB directly
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:42:42 +02:00
7d0d50408c feat(pos): replace hardcoded PB superuser creds with POS_API_KEY config
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:42:24 +02:00
78f357de69 chore(pos): copy WFSALES POS mockup into public/pos/ (unwired)
pb-config.js deliberately not copied — it hardcoded a PocketBase
superuser email/password in client-shipped JS.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:41:54 +02:00
4d58bde6ff fix(pos): type checkout test stub as PosPb to satisfy tsc
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:41:26 +02:00