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.
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.
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>
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>
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>
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>
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>
pb-config.js deliberately not copied — it hardcoded a PocketBase
superuser email/password in client-shipped JS.
Co-Authored-By: Claude <noreply@anthropic.com>
All three gated on POS_API_KEY. Checkout accepts all four tender types
and forwards client_ref for idempotent offline-queue replays.
Co-Authored-By: Claude <noreply@anthropic.com>
Records all four tender types. Only cash sets cash_given/change_given;
card, EFT and account settle outside the till, so cashup can reconcile
the physical drawer. Stock decrements for every tender including
account, since the goods leave on a delivery note.
client_ref makes checkout idempotent: an offline-queued sale that was
already committed server-side replays to the original record instead of
decrementing stock twice.
Co-Authored-By: Claude <noreply@anthropic.com>
Supports all four POS tender types (cash/card/eft/account) rather than
just cash/card: only cash puts money in the drawer, so cashup needs the
distinction. PosSale.client_ref carries the till's local sale id as an
idempotency key for the offline queue.
Co-Authored-By: Claude <noreply@anthropic.com>
Refine the handling of order_number values in the MCP API. Custom free-text order_numbers are now allowed to repeat across orders without triggering collision checks, while only values formatted as ORD-YYYY-MM-NNN will be subject to collision verification. Update tests to reflect this new behavior and ensure proper functionality.
The route accepted ?skip=N but always called getList(1, ...) — every
chunk re-processed the FIRST limit orders, leaving offsets > limit
unprocessed. Convert skip to page (floor(skip/limit) + 1) and add a
regression test asserting getList is called with the right page.
Backend-first TDD plan (pos-auth, pos-service, /api/v1/pos/* routes)
followed by relocating and rewiring the existing static POS mockup
into public/pos/, removing the hardcoded PB superuser credentials.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scopes hardening the existing POS mockup (superuser creds removed,
catalog moved to stock_items, checkout wired to stock_ledger) as a
route inside WFDASH-WEBAPP rather than a separate service.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The backfill is N+1-bounded (each order = 1 + N candidate queries),
so a 5,300-order table takes ~3 hours at ~25 orders/min on a busy
PB instance. Splitting into 200-order chunks keeps each call under
Next.js's maxDuration (10 min) and gives the operator a clear
progress signal: every chunk returns nextSkip + done.