`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>
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>
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>
Groundwork for the rep visit check-in feature: a rep taps "I'm on site",
the vehicle's tracker position surfaces which customers are there, and
the visit is logged against them.
Pure/unit-testable layers only — no PocketBase writes and no UI yet,
both of which are blocked on schema that has to be added in the PB
admin first. Design doc: docs/superpowers/specs/2026-09-05-rep-visit-check-in-design.md
- lib/visit-matching.ts rankNearbyCustomers: radius match around a
point, rep's own customers first
- lib/visit-stops.ts detectStops: breadcrumb trail -> stops.
Anchor-based clustering rather than a running
centroid, which would let a slow crawl chain
into one bogus stationary stop
- lib/tracker-parsing.ts Netstar FleetAI response decoding
- lib/server/tracker-service.ts
FleetAiAdapter + FakeTrackerAdapter behind a
TrackerAdapter seam. The fake is auto-selected
when no credentials are configured, so the rest
of the feature builds and tests without vendor
access.
- lib/geo.ts isRealGeoPoint, lifting the Null-Island guard
that was inlined identically in customers/[id]
and companies/[id]
Three FleetAI conventions drive real code here and are each pinned by a
test: errors arrive as HTTP 200 with a failure envelope; "--" means
"unavailable" and is not null (and for IGN, not false); timestamps are
platform-local UTC+2 with no offset marker, so parsing them naively puts
every fresh fix two hours in the past.
FleetAI has no position-history endpoint, so the design polls
getTokenBaseLiveData (whole fleet in one call) and keeps its own trail.
scripts/fleetai-probe.mjs is a throwaway read-only probe to confirm the
three account-shaped assumptions before more is built on top.
102 tests pass. The 9 pre-existing failing test files elsewhere in the
suite were verified identical on a stashed tree and are unrelated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
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.
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.
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.
- 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.
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.
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.
One row per letterhead, holding the reorderable/toggleable block
list for each thermal slip (sale, delivery note, cash-up) plus the
delivery note's default pricing toggle. Migration already run
against the live PocketBase instance.
Covers the reorderable block-list editor for sale/delivery/cash-up
slips, per-letterhead persistence, and the delivery-note pricing
default, worked out via brainstorming with the visual companion.
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.
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.
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>