One resolution per line, shared by product_matched, units_convertible and
promote, so a note cannot pass its check and then be linked elsewhere.
Co-Authored-By: MiniMax <noreply@minimax.io>
matched / ambiguous / none. The dimension set must match exactly, so
U PIPES-600 can no longer resolve to S&S Pipes-1050 on shared tokens.
Includes a regression test asserting no known input returns a wrong match.
Co-Authored-By: MiniMax <noreply@minimax.io>
Found while reviewing the tasks 2-5 work.
promoteNote was unguarded, so a write failure on one note threw out of
runBatch. The notes promoted before it stayed in delivery_notes while the
final update -- the one that writes notes_promoted -- never ran, so the
board reported "0 of 5" for a ledger that already held two. The notes
after it were never attempted at all.
Now the failure is caught, attributed to that note as a failing check,
and the remaining notes still process. Verified by mutation: the new test
fails when the catch re-throws.
Also replaces two run.test.ts tests that injected a notes_reconciled
check the real runBatchChecks can no longer emit -- they passed while
describing a world that no longer exists. The allNotesPromoted gate is
documented as belt-and-braces: it cannot currently be the deciding factor,
because a skipped note always coincides with a failing check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
61 of 148 purchase_order_items have an empty unit. Blocking every
delivery against them rejects 41% of lines over a gap on the order.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Delivery notes weigh in kg while the same supplier's invoices weigh in
tons, so the check was comparing 29980 kg against 30.275 tons. Also stops
failing documents that simply have no weigh section.
Also updates promote.test.ts, run.test.ts and segment.test.ts fixtures
for the new required mass_unit field on PageRead/NoteSegment (not named
in the plan's file list for this task, but required for typecheck).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PocketBase's created is empty on every dn_batches record, making the
board's -created sort a no-op and the tick's oldest-first meaningless.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Operators can now drop a folder of slips at once instead of uploading
them one at a time. Each file becomes its own inbox batch; the route
processes all of them in one request and returns per-file outcomes
(created / duplicate / error).
Backward compatible: single-file requests still get the legacy
response shape. New multi-file response shape:
201 {
results: Array<{ ok: true, batch_id, ... } |
{ ok: true, duplicate: true, ... } |
{ ok: false, error, filename }>,
summary: { total, created, duplicates, errors }
}
The UI input accepts multiple files; the result panel shows one row
per file with a colour-coded icon.
8 new tests cover: per-batch outcomes, >50 files rejected,
mixed MIME types, byte-identical duplicates within one request,
site lookup short-circuits before per-file work.
Fetches the PO item unit and delegates row construction to
buildDeliveryNoteItemRows, which also removes the matched-id off-by-one
that linked lines to the wrong PO item after a zero-quantity line.
Filters once so matched PO item ids cannot drift when a zero-quantity
line is dropped, and converts the slip quantity into the PO line's unit.
An unconvertible line contributes zero rather than entering the maths at
the wrong magnitude.
The kiosk phone needs the app installed for the full-screen
standalone experience, but no browser auto-prompts on day one —
Chrome's mini-infobar waits for an engagement heuristic that's
too slow for the kiosk use case, and iOS Safari never fires
beforeinstallprompt at all.
New InstallPrompt component mounted in the clock layout (so it
only appears on /clock, not the dashboard). Three branches:
* Chrome / Edge / Android: defer beforeinstallprompt, surface
our own Install button that calls prompt() on tap.
* iOS Safari: detect iOS user agent and show 'Tap Share then
Add to Home Screen' (Apple doesn't fire the install event).
* Already installed: detect display-mode:standalone +
navigator.standalone and render nothing — silent on the
kiosk itself.
Dismissal is per-session (sessionStorage), so a fresh visit on
a new device re-shows the prompt. The banner sits above the
clock footer (bottom: 6rem) so it doesn't fight the Person /
Fleet tabs for taps.
The kiosk phone is mounted on a wall with no physical UI to confirm
'this is the scanner' — without the overlay the camera feed alone
is a confusing blank screen. Now the scanner paints:
* a centred orange-cornered viewfinder (the QR alignment frame)
* a black vignette dimming everything outside that frame
(achieved via a 9999px box-shadow on the viewfinder, the
cleanest way to punch a rounded hole in an overlay without SVG
mask math)
* an orange scan line sweeping top-to-bottom inside the frame
(transform-based keyframes, pauses under prefers-reduced-motion)
* a pulsing 'Scanning for badge' status pill at the top
* a one-line 'Hold the QR badge steady inside the frame' hint
at the bottom
Everything is pointer-events-none so touches still reach the
camera. The overlay hides during the green/blue/red result banners
so it doesn't compete with the success message.
The new PWA icon matches the supplied reference: dark background, a
clock face with an orange upper-right arc and hands at 12 and 4
o'clock, sitting on an isometric cube whose two visible faces each
have the MEGA 'M' carved into the background colour, with two small
orange base wedges under the cube.
When the /clock URL is shared (Slack, iMessage, etc.) the static
1200x630 opengraph-image.png is picked up automatically and renders
the icon alongside the 'MEGA Clock' wordmark and tagline, via
openGraph + twitter metadata added to the route.
A supervisor-facing view of who's currently clocked in, per site — a
live muster list built entirely from the existing clock_events data
(no schema change). Lives in the dashboard next to Workers/Timesheets,
not on the kiosk phone, since it's a manager tool rather than
something a worker scanning their own badge needs to see.
The per-employee "latest event" reduction is shared via
src/lib/clock-events.ts so the on-site page and the Workforce
landing page's new "On site now" KPI tile stay in sync by
construction rather than by copy-pasted logic.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Footer buttons gain icons and more padding for easier tapping on a
wall-mounted kiosk phone. The header now shows a running count of
badges scanned since the app was opened. PersonScanner also requests a
screen wake lock while its camera is active (re-acquired on
visibilitychange, since the OS revokes it when backgrounded) and adds
vibration feedback per scan outcome, so a kiosk phone doesn't need to
be watched closely to confirm a scan landed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>