Commit graph

54 commits

Author SHA1 Message Date
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
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
fc361edfc8 fix(pos): reset stateful printer attributes per-block instead of relying on order
Root cause of the oversized/wrapping text on the printed slip: ePOS-
Print XML's <text> attributes (width, height, em) are stateful on the
printer — they persist across subsequent <text> tags until explicitly
changed back, not per-tag like CSS. The sale slip's header_title block
sets width="2" height="2" for the big title and nothing after it ever
reset back to width="1" height="1", so the entire rest of the receipt
(business info, items, totals, payment, footer) inherited double-size
text — exactly the wrapping/misalignment seen in the photo. Same
issue existed in the delivery note (header's height="2" leaking into
every line after it) and cash-up (GRAND TOTAL's em="true" leaking
into CASH RECONCILIATION's non-bold lines).

Now every <text> tag in all three XML builders explicitly declares
width/height/em rather than relying on inheritance from whatever
tag ran before it. This isn't just a display fix — since blocks are
now user-reorderable/toggleable, no block can safely assume what
state a neighboring block left behind; every block must be correct
in isolation regardless of order.

Same class of bug existed on the Bluetooth ESC/POS side: ESC/POS's
alignment command is equally persistent, and the sale slip's
header_title centered the title but relied on the next block
(business_info) to reset it back to left — broken the moment
business_info is disabled or reordered. Fixed the same way: each
block resets what it changed before returning.
2026-09-04 15:58:08 +02:00
ec711760a3 fix(pos): slip layout preview collapsed to a single squished line
Two compounding issues, both pre-existing (this exact <span>-joining
pattern and CSS was carried over unchanged from the old settings.html
preview I moved in the previous commit):

1. Preview lines are joined as adjacent <span> elements with no
   separator, so without an explicit block display they render as
   one continuous inline line instead of stacked rows.
2. .receipt-preview sits directly in body's flex-column layout and
   has overflow-x:auto — per the CSS Flexbox spec, an overflow-bearing
   flex item's automatic minimum size becomes 0 instead of its content
   size, so the flex column was free to squeeze it down to a sliver
   regardless of its 19 lines of content.

Fixes: span{display:block} for the per-line stacking, flex-shrink:0
so the flex column can't shrink it below its content height.
2026-09-04 14:07:39 +02:00
978456a807 feat(pos): add slip-layout.html block editor, move preview out of settings.html 2026-09-04 13:42:01 +02:00
238642c949 feat(pos): delivery note pricing checkbox defaults from the letterhead's saved setting 2026-09-04 13:35:49 +02:00
f07d70b976 refactor(pos): make buildEscPos/buildEscPosDelivery (Bluetooth) block-driven
Also fixes a live bug found while refactoring: buildEscPos returned a
Uint8Array, but its caller in printThermalBluetooth() ran the result
through TextEncoder.encode() (which expects a string) before writing
to the Bluetooth characteristic — encoding a Uint8Array's default
toString() ("27,64,27,...") instead of the actual bytes. Regular
(non-account) sale slips printed over Bluetooth have been sending
garbled data. Both builders now return plain strings, matching
buildEscPosDelivery's already-correct pattern.

Also preserves a pre-existing divergence rather than silently
resolving it: Bluetooth's delivery note prints letterhead business
info (name/address/phone/VAT), the network XML version never has.
Kept as-is per transport (folded into Bluetooth's header_title block)
rather than unilaterally adding or removing content — worth an
explicit decision later on which behavior is correct.
2026-09-04 13:35:19 +02:00
d47394723b refactor(pos): make buildEposCashupXml block-driven, fix broken logo image tag
Also fixes a logo-ordering bug this surfaced: slip-layout.js's sale
and cashup defaults put header_title before logo, which (once a real
logo renders instead of the always-empty placeholder used in earlier
regression tests) reversed the original image-then-title print order.
Reordered both to logo-first and added the missing logo block to
cashup's default catalog, since cashup's original code already
attempted (brokenly) to print one.
2026-09-04 13:31:13 +02:00
317b79ed29 refactor(pos): make buildEposDeliveryXml block-driven 2026-09-04 13:28:45 +02:00
81197ad641 refactor(pos): make buildEposXml block-driven 2026-09-04 13:27:13 +02:00
c56211d35e chore(pos): wire slip-layout.js into receipt/cashup/settings, bump SW cache 2026-09-04 13:25:31 +02:00
8e879d1447 feat(pos): add shared slip-layout block catalog and resolver 2026-09-04 13:24:40 +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
b20651b12d fix(pos): replace undefined toast.error() calls with a real toast
confirmSale() called toast.error(...) on checkout rejection (e.g.
insufficient stock), but no toast global was ever defined anywhere
in the app, so the ReferenceError swallowed the intended error
message and operators saw nothing. Adds a minimal self-contained
showToast() and bumps the SW cache so tills pick it up.
2026-09-04 12:52:26 +02:00
2f3007d978 feat(pos): enhance POS configuration to handle missing API key prompts gracefully and add slip preview functionality in settings 2026-09-04 11:55:38 +02:00
5c9f30dbd1 feat(pos): 'Print slip' on account sales auto-routes to delivery note (network + BT) 2026-09-04 10:40:48 +02:00
79cc7d1f0a fix(pos): keep edit-line modal from getting stuck on invalid qty; show clear 409 stock-shortage error instead of silently queuing offline 2026-09-04 10:37:00 +02:00
46d5d540a0 feat(pos): letterhead service + 2 API endpoints + vat_percent on Letterhead type 2026-09-04 09:13:53 +02:00
3b5891f0c5 chore(pos): bump SW cache to v17 so reprints work offline 2026-09-03 18:29:11 +02:00
20e460644b feat(pos): wire history reprint to redirect to receipt page 2026-09-03 18:28:55 +02:00
095a3433fa fix(pos): expose onclick-bound functions on window so buttons actually work 2026-09-03 18:24:37 +02:00
c4bf4a24ab chore(pos): cache epos-print.js in the PWA app shell 2026-09-03 17:54:48 +02:00
d5a48226ff feat(pos): print delivery notes over the network transport 2026-09-03 17:54:41 +02:00
bee80d8ec8 feat(pos): print sale receipts over the network transport 2026-09-03 17:54:21 +02:00
aec8e50f54 feat(pos): add printer type/IP settings UI to receipt page 2026-09-03 17:53:58 +02:00
06abd3c551 feat(pos): send ePOS-Print jobs over HTTPS to network printers 2026-09-03 17:53:32 +02:00
93eeea15e8 feat(pos): add ePOS-Print XML builder for sale receipts 2026-09-03 17:53:23 +02:00
4f54051b66 feat(pos): add canvas-based logo rasterization for ePOS-Print 2026-09-03 17:52:47 +02:00
2ea76f9d9a feat(pos): add printer settings persistence for network printing 2026-09-03 17:52:39 +02:00
ca6e9364a8 fix(pos): resolve duplicate const d declaration breaking receipt.html script 2026-09-03 17:52:30 +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
bdd0ac46e3 feat: implement per-route service worker selection
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.
2026-09-02 07:49:38 +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
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
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
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
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
b755ad34d6 feat(boss-dashboard): add /manifest-mobile.json PWA manifest 2026-08-28 00:15:00 +02:00
Dave
18397bc9be feat(counter): add standalone counter order PWA
This commit adds a full unauthenticated PWA for in-store retail order taking:
- Dedicated `/new-order` route with complete order form, customer/company picker
- Public `/api/counter/*` endpoints for search, order creation, and photo uploads
- Photo capture and gallery selection with 10-photo per order limit
- Offline caching via dedicated service worker and app manifest
- Shortcut redirect at `/counter` for quick installation
- Comprehensive setup and usage documentation in `docs/counter-pwa.md`
- Minor cleanup to root layout metadata and service worker registration
- Small layout fix for collection pool row text truncation

Orders submitted via the PWA appear immediately in the main CRM's New tab, and the app is designed for trusted office kiosk phones without requiring user login.
2026-08-13 09:27:20 +02:00
SJCoetz
7a68cafe35 chore: add logos, opencode config and build assets
This commit includes the temporary Next.js build cache files, public brand
logos, and project configuration added in this change.
2026-07-27 08:21:20 +02:00
SJCoetz
47b01bddfa feat: add picking slips, bulk print tracking, supplier map
- Add Supplier Map component to suppliers page and reorganize sidebar navigation
- Implement picking slip PDF generation with supplier per line item and picked-by signature footer
- Add invoice bulk print tracking API and view models to track printed invoice state
- Extend company search and form to support email/telephone fields and improve searchability
- Fix data table debounce race condition by using useRef for onSearch handler
- Fix supplier page fetch race condition with debounced timeout
- Reduce list page item counts to 10 per page for customers, companies, and suppliers
- Update company search filter to include email and telephone in search queries
- Add comprehensive test suite for new and existing components and APIs
- Add pdf-lib dependency for PDF generation and update related tooling
- Clean up unused code and fix minor linting issues
2026-07-10 10:47:11 +02:00
SJCoetz
bb4233a9f2 feat(dashboard): add multi-theme system and supporting improvements
Add complete multi-theme dashboard system including:
- 8 selectable themes with persistent per-browser preferences
- Theme bootstrap script to prevent FOUC on page load
- 12+ reusable chart components (area, bar, donut, radial gauge, etc)
- Custom checkbox UI component
- New settings page for appearance customization

Other changes:
- Optimize supplier map API payload with getMappableSuppliers helper
- Add aria-label attributes to back buttons on detail pages for better accessibility
- Update next.config.ts to include new local development IP 192.168.1.128
- Update site manifest with all new favicon and PWA assets
- Fix styling and layout spacing in rewrite quote modal
2026-06-26 17:59:40 +02:00