Commit graph

1050 commits

Author SHA1 Message Date
38cd015e28 fix(mcp): a stock quantity write needs an acting user
The batch ran with an empty actor.id when the caller sent none, so
the ledger row went out unattributed — against the plan's own rule
that the acting user is stamped on every ledger row. The tool now
requires a real 15-char user id, exactly as stock_items.set_pricing
does, and declares the actor in its schema so a schema-following
client can supply one.

The other fixes follow from the same review:

- A numeric SKU is a SKU, so it is coerced to its string form; a
  boolean or an object is malformed input and is refused rather
  than dropped silently.
- The duplicate-SKU error names both spellings and the lines they
  are on, so the operator can see which two rows collided.
- dry_run also accepts the string "true". It is the one flag that
  decides whether data is written at all.
- The description now says the batch is rejected as a whole on a
  repeated SKU, and that quantities are truncated to whole units.
2026-11-05 23:53:54 +02:00
2c415d82f1 feat(mcp): stock_items.set_quantities
One call sets the stock quantity on up to 100 pasted lines. Each line
is resolved by exact SKU, then by description, and reports what it
could not resolve instead of guessing.

The batch is validated up front — a duplicate SKU is a malformed
paste, not a per-row failure — while a line that simply does not match
is reported and the rest of the batch still applies.
2026-11-05 23:24:50 +02:00
62faa390d5 docs(spec,plan): a failed write is compensated, so a retry is safe
The ledger row is written first on purpose, so a crash cannot leave the
audit trail behind the cached total. But when the quantity write then
fails, the ledger sat ahead of the quantity - and the operator was told
to retry, which wrote a second ledger row for the same change and
double-counted it.

A failed quantity write now reverses its own ledger row, so SUM(change_qty)
still reconstructs current_qty and a retry is safe.
2026-11-05 22:36:37 +02:00
7dd70577a7 fix(stock): a catalogue mirror error no longer aborts the batch
The price_list mirror could reject — a bad filter, a 400, a network
blip — and the rejection escaped setStockQuantitiesBulk. The
quantities had already been written and their ledger rows were in
place, so the caller got an exception instead of a summary: the
remaining lines were never reported, and a retry re-applied the
quantities and wrote a second set of ledger rows, double-counting the
audit trail.

The mirror is now guarded. The quantity landed, so the row stays
"applied" — reporting it as "failed" would say the quantity did not
change, which is false. The row carries price_list_rows_touched: 0
and a message saying the catalogue snapshot failed and why. The batch
always returns its summary and results.

Also: "ok" now means what it says — summary.failed === 0 — instead of
always being true. The price-list page is unbounded by design at this
data volume (about 2,900 catalogue rows over about 2,700 SKUs), and
the comment now says so, with the caveat that the filter's raw-SKU
LIKE clause treats % and _ as wildcards, so the page is sized by
substring noise rather than by the SKU's own rows.
2026-11-05 22:28:18 +02:00
acaec3f5c7 fix(stock): compensate the ledger row when the quantity write fails
The write service puts the stock_ledger row first and then patches
current_qty. When that second write failed, the ledger row was left
behind: the ledger sat one change ahead of the quantity, and a retry
wrote a second row for the same change, so SUM(change_qty) no longer
rebuilt current_qty — the premise a drift-repair job rests on.

The failed write is now compensated with a ledger row for the same
item carrying the opposite change_qty, stamped with the same actor
and noted as a reversal of the adjustment. If the compensation itself
fails, the row is still reported as failed with a message that says
the ledger could not be reversed. The failed row's message says when
the ledger was compensated, so a retry is safe.

The ledger-first ordering is unchanged — it is what makes a crash
between the two writes leave the audit trail ahead of the cached
total.
2026-11-05 22:25:03 +02:00
4362eb4201 docs(spec,plan): a failed write is its own status
Reporting a failed write as an unmatched line tells the operator they
typed the SKU wrong when the cause was a transient database error. That
sends them hunting for a typo instead of retrying - different problems,
different responses.

Also records the dry-run fix: on a dry run a matched line is reported
applied with its before and after numbers, and nothing is written.
2026-11-05 22:00:29 +02:00
23725f10c3 feat(stock): tell a failed write apart from an unmatched SKU
A write that did not complete now has its own status, "failed", and
its own summary count. Reporting it as "unmatched" sent the operator
looking for a typo in the SKU when the real cause was a PocketBase
error — two different problems with two different responses, so they
no longer share a status. "unmatched" keeps its meaning: the line
never resolved to an active stock item.

The five counts still add up to "total".

The price-list mirror reads one 200-row page. The catalogue holds
about 2,900 rows over about 2,700 SKUs, so a SKU has one row per
supplier and a handful at most: the page is unbounded by design at
this data volume, not an oversight.
2026-11-05 21:59:06 +02:00
22305645e7 feat(stock): bulk quantity writes with a price-list mirror
Resolves pasted lines, computes the signed delta, and hands the write
to updateStockItemsQuantities so the ledger row is written first and
stamped with the acting user. A negative result is a normal outcome,
not an error — the till oversells on purpose.

Mirrors the new quantity onto price_list.stock_qty, which is a
denormalised snapshot of our own quantity. The catalogue holds one row
per SKU per supplier, so one item can touch several rows; the count is
reported per line.
2026-11-05 21:54:20 +02:00
36894a2908 Merge main into feature/giellie-stock-quantities 2026-11-05 21:33:09 +02:00
a85a6d9a9a feat(stock): match the SKU text too, and name inactive best matches
Three changes from the Task 2 review.

The SKU string gets a second chance at matching by how the item is
named. When the exact SKU misses, the line's description AND its SKU
are joined and scored as text, so a line carrying only a SKU still
finds its item. The match method stays "description_fuzzy" - it is a
text match either way, and the threshold and the top-unless-tied rule
are unchanged.

An inactive best match now names itself. The text step scores every
candidate, active or inactive, exactly as the SKU step does, so an
inactive row is no longer filtered out before scoring - it is reported
as unmatched with its name and the reason, instead of vanishing and
leaving a bare "no match" behind.

The resolver truncates the quantity. Quantities are whole units, so
the dry-run plan and the stored value now agree instead of the plan
reporting 12.7 for a write that stores 12.
2026-11-05 21:31:22 +02:00
b23626b10c docs(spec,plan): the SKU text also gets matched as text
The operator decided that when a pasted SKU matches nothing exactly,
the SKU string should get a second chance at matching by how the item
is named - pasting "M16 Flat Washer" into the SKU box should still find
the flat washer.

The line's match text is now the description and the SKU joined, so a
line carrying only a SKU still gets a text match. Everything else about
the text step is unchanged: the same 0.6 threshold, the same "top
candidate wins unless two are tied at the top" rule.

Also from the Task 2 review: an inactive best match now names itself
instead of vanishing, the resolver truncates the quantity so the dry-run
plan and the stored value agree, and the test list picks up the
fall-through path that had no coverage.
2026-11-05 21:17:00 +02:00
1ff82c2f7f feat(stock): resolve pasted stock lines to stock items
Pure matching, no PocketBase, so the rules can be tested on their own.
Exact SKU first (normalised), then a tokenised Jaccard description
match against the item's name and description.

The description match takes the top candidate unless two are tied at
the top — a single threshold alone would report 'ambiguous' for almost
every description and defeat the feature.
2026-11-05 08:05:21 +02:00
00fbada312 feat(stock): allow the agent to write a negative current_qty
The /stock page keeps its non-negative floor — the operator types a
physical count there. The agent has to be able to write an absolute
count that sits below zero, because the till oversells on purpose and
the stocktake is what corrects it.

Also make the ledger note say where the edit came from, so an agent
write is not mislabelled as a /stock edit.
2026-11-05 07:38:17 +02:00
a8958abc70 docs(plan): correct three edit instructions against the real source
The approval card renders the JSON inside a <pre> with max-h-48 classes,
the composer already imports Textarea, and the POS tile is built in a
template string rather than a class-join expression. Each instruction
now names the code that is actually there.
2026-11-05 07:16:03 +02:00
9254eecc9a docs(spec,plan): giellie stock quantity update
The spec settles what was decided; the plan breaks it into 9 tasks
with the code each one needs.

Task order is the dependency order: the service learns to write a
negative quantity, a pure resolver learns to map a pasted line to a
stock item, a bulk service learns to apply and mirror, and only then
does the MCP tool, the agent prompt, the approval card, the composer
shortcut and the till tile hang off it.

Two corrections to the spec while writing the plan: the approval card
receives dotted MCP names, not model-facing ones, and the description
match takes the top candidate unless two are tied at the top - a bare
threshold would report 'ambiguous' for almost every description and
defeat the feature.
2026-11-05 07:03:30 +02:00
4babb893f4 docs(spec): giellie stock quantity update
Giellie can already reach stock_items, but only through ad-hoc
list+adjust loops: a signed delta instead of an absolute count, no
batch call, fuzzy SKU picks the model cannot see are wrong, and
stock_items.adjust throws the actor away so the ledger row has no
attribution.

Adds a stock_items.set_quantities MCP tool that resolves each pasted
line (exact SKU, then description fuzzy), computes the delta, and
writes through updateStockItemsQuantities so the ledger is audited
against the signed-in user. Also mirrors price_list.stock_qty, which
price-list-service.ts documents as a denormalised snapshot of
current_qty.

The live data makes this matter: 522 of the 4,083 sellable items carry
a negative tracked quantity and 2,901 sit at zero. That drift is by
design - the till oversells on purpose (pos-service.ts:603) and is
corrected at the next stocktake, which is what this feature is.

Nothing blocks on low stock. One service flag (allowNegativeQty)
lifts the non-negative floor for the agent's writes only, leaving the
/stock page untouched.
2026-11-05 06:46:19 +02:00
a05330e0cd docs(whats-new): reword the release 6 bullet
It read like leftover instruction text — "and no dates" in a popup that
carries no dates — instead of something an operator would want to read.
2026-11-05 01:34:42 +02:00
3168b9a906 docs: how to talk to the live PocketBase directly
Records live under /api/collections/<name>/records, and the admin client
authenticates with PB_ADMIN_EMAIL/PB_ADMIN_PASSWORD — the PB_ADMIN_TOKEN
in .env is not always current.
2026-11-05 01:23:25 +02:00
e12a5ccb32 refactor(release-notes): drop the date field from the release registry
The date was kept in the registry as an authoring reference and never
rendered, but it was still a date sitting in the notes file, and a
date-shaped value can always find its way to a screen. The ReleaseNote
type has no date field at all now, so one cannot be added without a
deliberate change to the contract.

A test walks the registry and fails on any date — a stray property, a
headline or a line of copy — so the popup carries a release number and
a commit and nothing else.

Release 6 also says what it is: Payments and Approvals is in beta. The
schema and the enforcement hooks are live on the PocketBase, but the
backend behind the queue is still being worked on.
2026-11-05 01:19:50 +02:00
de6de3340f feat(whats-new): release 6 covers supplier payments and the theme picker
The payments and approvals queue, the theme rework and the release
number on the popup all landed since release 5, so the office is told
about them in one place.

The entry is the release number and the commit, never a date, and the
popup still renders no date of any kind.
2026-11-05 00:52:59 +02:00
4eb6ebf838 Merge codex/order-item-payments into main
The item supplier payments and approvals queue: a payment hold on order
items, the Payments and Approvals page, supporting documents and proof
of payment, and the PB hook that stops an unpaid item becoming ready for
collection.
2026-11-05 00:43:06 +02:00
fc7043b364 fix(pdf): add the missing email_to to the quote fixtures
Quote requires email_to, so both PDF fixtures failed to type check.
Add the field and the two long-standing errors in tsc --noEmit are gone.
2026-11-05 00:40:47 +02:00
9ce6c97772 feat(theme): the picker reads the real palette, not a hand-kept copy
The theme registry carried a hand-maintained set of hex swatches for
every palette, and they had drifted away from the CSS the app actually
paints. The picker previewed colours the product never showed.

The registry now holds only what cannot be read from CSS — id, label,
tagline, fonts, radius — and the picker, the Settings swatches and the
preview cards resolve --background, --primary and --foreground off the
theme itself. A preview can no longer disagree with the screen it
previews.

themes.css is rewritten so every palette carries a real dark surface,
and the palettes that are dark by nature stay dark whichever way the
mode switch is set. The pre-hydration script validates the stored theme
id against the registry, so a retired id falls back to WFDASH instead of
painting an unstyled page, and the old "warm" mode migrates to Light.

The legacy text-{warn,success,info}-foreground classes now resolve to
the semantic text colour and switch to the filled ink only on an exact
solid fill, so one class reads correctly on both a tinted badge and a
filled button. A solid focus outline replaces the faint ring, and the
charts and quote status badges take their colours from tokens rather
than hard-coded dark strokes.

Tests: theme-bootstrap.test.tsx covers saved-theme recovery and the
storage-blocked path; e2e/themes.spec.ts walks every palette in both
modes checking surface contrast, tinted and solid semantic pairs, focus
colour, chart tokens and the destructive hover, and asserts that the
picker preview matches the applied theme.
2026-11-05 00:40:40 +02:00
97603dce71 refactor: simplify supplier payments to document checklist
Remove payment amount, reference, notes and manual combining forms. Link supplier POs automatically and record paid from supplier evidence plus proof, keeping readiness and attachment safeguards.

Keep active supplier/company views current and retire stale supplier evidence on routing changes. Verified 5167 tests with only the two existing script-import errors.
2026-11-03 07:18:56 +02:00
cb0113f9f4 feat: add item supplier payments and approvals queue
Keep payment holds independent of workflow and margin approval. Require proof before collection readiness; deduplicate uploads transactionally in PocketBase and preserve payment history.

Includes schema migration, deployment runbook, browser checks and isolated PocketBase regression coverage. Full suite has 5156 passing tests and retains two pre-existing script-import errors.
2026-11-03 01:27:07 +02:00
34b42867f8 docs: the What's New popup carries release numbers, never dates 2026-11-02 20:44:07 +02:00
a9e9efecba feat(whats-new): show the release number and commit instead of the date
The popup rendered the day each release went out, which gave the office
a way to ask what was done on a given day. Each release now shows its
number and the short SHA it shipped with, and the date-shaped version
string is gone from the registry.

The date formatter is deleted rather than hidden, so no code path can
put a date on screen, and a test fails the build if one comes back.

The 'seen' marker moves from the version string to the release number,
so the badge lights up once for everyone on this deploy and then
settles.
2026-11-02 20:41:17 +02:00
fd5bb11325 refactor(release-notes): give every release a number and a commit
The version strings are dates ("2026.11.01"), so a release has no
identity that is not a date. Every entry now also carries a release
number and the short SHA of the commit it went out with.

The date-shaped version string is left in place for now — the popup
switches over and drops it in the next commit, so this one stays
green on its own.

A test holds the numbering unique, contiguous and newest-first, and
holds every commit to a real SHA.
2026-11-02 20:35:57 +02:00
a0e02759d2 docs: plan for What's New release numbers and commit IDs
Three tasks, each ending with the suite green: give every release a
number and a commit, switch the popup over and drop the date-shaped
version string, then document the convention and verify.

The order matters — the new fields land first so the component never
sits on a broken type mid-change.
2026-11-02 20:25:25 +02:00
ebfee9eeaa docs: What's New release numbers and commit IDs instead of dates
The What's New popup shows the date each release went out, and the
version strings are dates too. That gives the office a way to ask
"what did you do on the 30th?".

Design: each release is identified by a release number and the short
commit SHA it shipped with. The date stays in the data file as an
authoring reference but is never rendered, and the date formatter is
deleted so no date can reach the screen.

Five existing entries are renumbered 1-5, each anchored to the last
commit that landed in it. A guard test fails the build if a date ever
comes back into the popup.
2026-11-02 20:17:08 +02:00
3ca66ef19a Expose public build-time environment variables
Declare and pass PocketBase, TomTom, and QuickBooks webhook public variables into the Docker build environment so Next.js can bake Ship-provided values into the browser bundle.
2026-11-02 07:18:07 +02:00
139ea259f4 feat(sidebar): What's New button and a Dark Mode section
The sidebar's bottom block gets two new controls:

- A "What's New" button that opens the release-note popup, with a badge
  until the operator has read the latest release.
- A Dark Mode section that replaces the old one-click toggle with an
  explicit Light/Dark switch, so the current mode is always visible
  instead of being inferred from the button's label.

The theme colour picker stays in Settings -> Appearance; the sidebar
carries the mode switch only, so the nav rail is not a strip of 16
coloured squares.
2026-11-02 07:13:51 +02:00
61fb2669e0 feat(sidebar): hide Deliveries and DN Scanner; Delivery Pool replaces them
Both pages still open by URL (/deliveries, /scan).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:26:35 +02:00
65036a8a6a feat(delivery-pool): company sections start collapsed
Like the Collection Pool, the page opens as a list of companies; click
one to see its invoice cards. A search opens the matching companies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:25:20 +02:00
bae4f9e795 Lock quote letterheads to customer companies
Add a per-company quote letterhead lock backed by `companies.confirmed_letterhead`. The quote process flow prompts for a default when unset, permits only the confirmed letterhead with a triple-click per-quote override, and enforces the rule during PDF generation and email sending. Also add company settings controls and API, email-ingest defaults, migration and backfill tooling, and unit coverage.
2026-10-31 10:20:23 +02:00
afdfbce71d fix(pos): never sell or list an item at R0; customer lines go to the price list
Owner's rule: the POS counter only carries priced stock.

- POS catalogue lists only active stock items with selling_price > 0
  (POS_CATALOG_FILTER); checkout refuses any line priced at R0 or less.
- New stock order now gives a new stock item the PO line's cost and the
  default 80% markup. It used to drop the cost, which is how 157 stock
  items reached the till at R0.
- Collection Confirm "Add to POS" becomes "Add to price list": customer
  lines go to price_list at cost + 80% (needs SKU, cost and supplier),
  never into stock_items. Route renamed to add-to-price-list.
- scripts/2026-10-30-pos-zero-price-cleanup.ts: priced 79 items from their
  PO line cost, switched off 48 with no cost and no stock, left 30 stocked
  ones active for costing (already applied on live PB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:56:03 +02:00
45529731a7 feat(delivery-pool): assign invoiced deliveries to drivers, close with the signed copy
New /delivery-pool page, a clone of the Collection Pool for deliveries
with no pending stage. Invoiced lines are grouped per company, one card
per customer invoice; a card expands to its lines. Assign a driver and
day per invoice (or bulk per company); uploading the signed copy adds it
to every line, marks the lines completed + delivered_at, and closes a
delivery note once none of its lines is still open.

- Driver lives in new order_items.delivery_driver / delivery_assigned_at
  (npm run pb:migrate:delivery-pool), never drv_id (collection driver).
- Pool starts at DELIVERY_POOL_START (2026-10-30); lines whose DN is
  already completed/cancelled (POS counter sales) are left out.
- Service + /api/v1/delivery-pool routes (browser session); the service
  503s until the migration has run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:55:35 +02:00
4ae45cb3aa fix(pos): accept a comma as the decimal point in custom item price and cash given
SA decimal keypads type ',' and the sanitizer stripped it, so R12,50 became R1250.
Bump service worker cache to pos-v37 so tills pick up the new pos-app.js.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 23:33:11 +02:00
83aec143d4 feat(quotes): record who a quote is sent to (email_to)
sendQuoteEmail now stamps the resolved To address on quotes.email_to.
The n8n send workflow mails it (falling back to customer.email), so the
rep's To override and the sender fallback are honoured, and the audit
hook shows the recipient in the quote history. Needs the live step
`npm run pb:migrate:quote-email-to`.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 23:32:39 +02:00
6883971bb7 feat(collection-pool): pending stops become ready on their expected date
A scheduled check (n8n, every 2 hours, POST /api/v1/collection-pool/ready-dates/tick)
marks pending customer stops ready once their expected-ready date is today or
earlier (South African time). The stop stays unassigned, keeps its pool age,
and gets an audit note. Stops with a driver problem flag are left alone.
Stock lines are not covered: they have no date field.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 22:08:19 +02:00
91173edbcd feat(collection-pool): stock lines can go back to pending and be marked ready
Back to pending on a stock line used to clear only the driver, so it stayed
in the pool as ready. Stock lines now carry a pool_pending flag (the inverse
of ready, so every existing line stays ready) and behave like customer stops:
pending badge, Mark Ready (new stock-ready route), no driver assignment while
pending. The back-to-pending route refuses to claim success when PocketBase
has no pool_pending field yet.

Needs the manual live step npm run pb:migrate:pool-pending (already run).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 22:08:19 +02:00
0e9d85d536 feat(collection-confirm): AI notes button on each PO card
A robot button beside each PO opens the notes the AI made: card findings,
reading notes, and per line its reason, any discount it read (with the
arithmetic), price notes and a filled-in SKU. An amber dot shows when the
AI raised a finding or read a discount.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 13:56:16 +02:00
25466d4e64 fix(collection-ai): the AI never changes a price
On PO202610005TEL the AI read a 15% discount that was not on the invoice
and cut a line's cost from R146.47 to R124.50. It no longer writes a cost
to order or stock lines. A cost that differs from the invoice's discounted
price shows as a red Price tick, which also keeps the card out of
auto-push, so a person settles it. Earlier cost changes stay visible on
the line. Tests now assert no cost is ever written; runbook and AGENTS.md
updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 13:56:16 +02:00
18cdc2976e feat: show the build commit as a faint corner mark
Declares ARG NEXT_PUBLIC_APP_COMMIT, supplied by Ship, and renders it as a
nearly transparent, click-through stamp. Renders nothing when unset.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 01:27:22 +02:00
3eea87ec03 feat(collection-confirm): auto-push purple cards on to invoicing, with a 2-day log
After each AI tick, every Collection Confirm card the page shows purple
(cardAiState verified) is confirmed without a person. Customer lines go
to ready for invoicing with the invoice number and date the AI read; stock
orders go to stock booking once every line is collected. Cards with a
problem flag, a photo for another PO, or no readable invoice number/date
stay for review.

Each push is recorded in ai_settings.collection_auto_push_log and shown at
the top of the page for today and yesterday (PO, supplier, items, value).
Off by default behind a Settings > AI switch; needs the manual live step
npm run pb:migrate:collection-auto-push before it will run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:36:23 +02:00
6c83282888 feat(settings): link to AI settings from the Settings page
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 11:36:23 +02:00
66f23a34e9 fix(orders): quote shortlist no longer requires the same customer contact
The AI order assistant's shortlist filtered candidate quotes to the order's
customer (or blank). A different contact at the same company is common, so
the right quote was dropped and the AI was never asked (M24 nylock order
74288 vs Q202610684). The scorer still rewards a customer match.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 10:38:36 +02:00
16743f87ed docs: driver invoice photo handover - widen the filter, use confirm_collected
Tested the handover against live PB on 2026-10-27. The read path works as
written; four defects fixed:

- The filter only caught item_status = 'assigned'. The collection_pool
  tab group is [collection_pool, assigned, ready_delivery] and
  order_items.confirm_collected accepts collection_pool or assigned, so
  a line stuck in either was invisible to the sweep.
- Stock POs were not covered at all. Stock lines live in
  stock_purchase_items and their photos on the parent
  stock_purchases.additional_attachments.
- The write path hand-rolled order_items.update + update_status, which
  skips the eligibility check, the over-collect guard, drv_collected_at
  and the actor-stamped audit note. Now uses
  order_items.confirm_collected / stock_purchase_items.mark_collected.
- No de-duplication: the NSS M24 line is still 'assigned' with a photo,
  so every run would re-report it. Added a 'Waiting on a person' list.

Also noted that no cron runs this yet, that order_items_attachements is
multi-file (read every filename), and added a supplier check.
2026-10-27 07:07:40 +02:00
b212d0fcd8 feat(manager): a mobile orders board for the managers at /manager
A hidden, installable PWA: one link, opened on a phone, showing where
every order is. New / Processing / Completed, with a tap-to-preview
popup and the invoice/line detail Gary needs. Read-only, and NOT in
the sidebar — the route lives outside `(app)`, so `<Sidebar />` never
renders there.

The preview popup is reused, not rebuilt: the service emits
`UniversalHit`s and the page hands them to the existing
`SearchPreviewDialog`, so the manager gets the desk's quick-overview
verbatim.

Also fixes the /orders Completed tab, which was matching ZERO orders.
`delivered_at` is stamped on only 2 of 12,957 delivered lines and the
business stopped writing `delivered` on 2026-08-14 (the live pipeline
now ends at `invoiced` / `checked_in`), so the old filter never
matched. "Settled" is now defined once — `SETTLED_STATUSES` +
`SETTLED_DATE_FIELDS` — and dated per status: `invoiced_date` for
invoiced (the only properly stamped field, 2,199 lines), `delivered_at`
for delivered, `updated` only where no dedicated column exists.

`delivery_note` and `checked_in` move into `completed` as instructed,
and the standalone `post_processing` tab is retired (a DN is not a
stage of its own). `invoiced` stays in `collected` as well, so
invoiced orders don't vanish from the workflow tabs.

Completed tab: 0 -> 341 orders over 30 days, 112 over 7.
2026-10-26 23:09:01 +02:00
55cc7fa2f6 Merge branch 'feat/ai-only-quote-match': AI-only quote matching, order-card tint, blue price check, size-aware POS matcher
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 01:46:46 +02:00