Commit graph

912 commits

Author SHA1 Message Date
35bfb1a0c4 feat(orders): carry breakdown priority through the workflow 2026-11-11 14:34:12 +02:00
a503bd55ff feat(driver): serve proof of payment for assigned POs 2026-11-11 13:57:19 +02:00
fc497fad96 feat(buyer-tasks): add shared notes to sent POs 2026-11-11 12:27:02 +02:00
694c2a2292 Improve book-in form field clarity
Restyle booking fields as high-contrast cards, add visible required/optional indicators and ARIA requirements, and make lookup and form errors more prominent.
2026-11-09 16:07:42 +02:00
3113830a42 docs(whats-new): release 9, Repairs section and sidebar scrolling
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 12:53:15 +02:00
021af24d37 feat(collection-confirm): wrap descriptions, and make the POS price a decision
Two things on the collection pages.

Descriptions were clamped to two lines (`line-clamp-2`) on the
Collection Confirm items table and the Collection Pool card, so a long
description lost everything after the second line - exactly the part
that distinguishes one line from another (size, spec, colour). Both now
wrap to full height, with `break-words` so a long unbroken part number
cannot widen the table.

And a POS item's price was never a decision. `stock_items.selling_price`
is what the till charges, and it could move on its own: a bulk import
wrote a spreadsheet's selling price straight over the catalogue, and
the /stock pricing grid recomputed the selling price whenever its cost
cell was edited. Nothing asked.

Now the operator decides, per item, when they confirm a PO card. A
price confirmation dialog opens BEFORE the invoice/stock confirm - once
per card, and only when confirming would move a till price. Every row
shows the old price and the new price; the default is Keep price, so
moving a till price is always deliberate. Where the line cost and the
catalogue cost differ, both are shown and the operator picks which one
prices the till.

The price is derived, never accepted from the client: the route reads
the item's own markup and computes it, so the number the operator saw is
the number that lands. Update writes cost AND price together (a price
with a stale cost behind it would mis-price the next markup run), plus
a zero-quantity cost_history entry and a stock_ledger adjustment row. The
markup field is never written - the operator's rate is theirs. A failed
price write does not block the confirm: goods stuck unconfirmed is a
bigger problem than a till price that did not move.

Also fixes a live bug found on the way: `listCollectedItems` expanded
`purchase.stock_item`, but `stock_purchases` has no `stock_item`
relation - it lives on `stock_purchase_items` - so `expand.stock_item`
came back undefined on every row and the POS pricing was never
available. Expanding `stock_item` directly takes the stock leg from
0/27 rows carrying POS pricing to 27/27 (verified on live PB).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-11-09 09:56:16 +02:00
9232063ca5 feat(sidebar): Delivery Pool under Logistics, own Repairs section with dashboard, fix nav scrolling
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 08:13:19 +02:00
569e0121a8 Move invoicing links to Operations
Reorganize the sidebar navigation by moving Invoicing and Delivery Notes from the Logistics group to Operations, while keeping Delivery Pool under Logistics.
2026-11-09 07:48:25 +02:00
500fc51270 fix(repairs-app): give the board its screen back, and fix search with a status
Board: chips scroll in one row (44px, was 136px over three rows), the
board opens on a new "Open" chip (not Done, not Cancelled; derived from
the same counts so Open + Done + Cancelled = All), cards say how long
the machine has waited, the badge drops when one status is chosen, and
the loading line shows a real ellipsis instead of "…". The one-tab
bar is hidden until a second tab exists.

Repair page: a way back, labels above values, the customer quote and
order as plain text (the office pages show prices), the quote listed
once, and Cancelled no longer drawn as the step after Done.

Book-in: a way back, photos first, placeholders marked "e.g.".

listRepairs: brackets each filter clause. The search is an OR chain, so
a status filter used to bind to serial_no only and a search returned
repairs of any status. Adds status "open".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 13:55:13 +02:00
38bc370f74 fix(repairs-app): honest next-step labels, an honest count, and the missing guards 2026-11-06 19:34:46 +02:00
63ef01f604 feat(repairs-app): the repair detail with one obvious next step 2026-11-06 18:50:55 +02:00
c4e97c3cd5 test(repairs-app): pin the book-in session header and the two-step photo contract 2026-11-06 18:33:29 +02:00
0d9848e90e feat(repairs-app): book a machine in, with photos 2026-11-06 17:53:42 +02:00
13d887eae8 fix(repairs-app): stop the book-in button covering the last card, and cover the session header 2026-11-06 17:32:33 +02:00
e2764506c7 feat(repairs-app): the status board with reconciling counts 2026-11-06 16:52:17 +02:00
43aa1a96bb fix(repairs-app): make /offline a real route so the service worker can install 2026-11-06 16:37:22 +02:00
10810142b0 feat(repairs-app): the phone app shell, manifest and service worker 2026-11-06 15:59:39 +02:00
745593ee35 fix(repairs): send the session on the detail photo upload, and cover every handler 2026-11-06 15:43:12 +02:00
20ef99264d fix(repairs): require a browser session on the repairs API and its files 2026-11-06 15:12:02 +02:00
6e96d823b0 fix(stock-counts): generate POS count PDFs on demand without storage 2026-11-06 12:50:28 +02:00
b929183ffa docs(release): release 8 stock count workflow 2026-11-06 11:44:21 +02:00
95fd09b1a2 feat(giellie): conversational stock counts with split PDFs and photo review 2026-11-06 11:40:13 +02:00
9358b379ff Merge feature/repairs-phase-1 into main 2026-11-06 09:30:44 +02:00
7bc2ea55a5 docs(release): release 7 — Giellie can update stock quantities
Release number and the commit it went out with. No dates.
2026-11-06 09:06:11 +02:00
2a0f0a97f7 docs(mcp): correct the stock adjust justification and document create's actor
The system prompt justified avoiding the stock_items.list +
stock_items.adjust loop by claiming that path "writes a ledger row with no
actor". That stopped being true when the branch routed adjust through the
audited write service: adjust now requires actor.id and its ledger rows
carry created_by. The instruction itself is unchanged and not weakened —
the signed delta and the missing batch call are still the reasons to
avoid the loop, and the new tool's own advantages are now named.

stock_items.create now also requires actor.id, so an external MCP client
that omits it gets a 500 JSON-RPC error. The generated catalogue row is
truncated at 240 chars before that sentence, so both authoritative docs
get a hand-written row and an input-shape block instead. The generator is
not re-run; the catalogue is still 148 tools.
2026-11-06 09:01:58 +02:00
0ff6a2a3a6 fix(repairs): pin the transition guard's wiring, test the office pages, and clear the review findings 2026-11-06 08:46:21 +02:00
887b56ea26 test(giellie): pin the negative-quantity safety cases
The ledger asked the final review to close this gap, and it was still
open: the negative-quantity test asserted the ledger row (the delta and
the note) but never the persisted `stock_items` patch, and nothing pinned
a negative `targetQty` under `allowNegativeQty` — where the code applies
the negative allowance only to `currentQty`.

- The negative-quantity test now also asserts the persisted patch
  carries `current_qty: -3`.
- A new test asserts a negative `targetQty` is still rejected while
  `allowNegativeQty` is set, and that nothing is written.
2026-11-06 08:07:20 +02:00
7031499bf0 fix(giellie): attribute the stock_items.create opening balance
Creating a stock item with `current_qty > 0` wrote the opening-balance
`stock_ledger` row with `created_by: ""`, so the row landed in the audit
trail unattributed — and it was written after the quantity, so a failure
in between left the two out of step.

- The create now requires `actor.id` (the same 15-char check and error
  style as `stock_items.set_quantities`) and stamps the opening-balance
  row with it.
- The opening balance is booked through the audited write path: the item
  is created with no quantity, the ledger row is written first, and the
  quantity second. If the quantity write fails, the service reverses
  the ledger row, so `SUM(change_qty)` still rebuilds `current_qty` and a
  retry is safe.
- A zero or negative opening balance is written on the create itself,
  exactly as before.
2026-11-06 08:07:20 +02:00
ab16404003 fix(giellie): route stock_items.adjust through the audited write path
`stock_items.adjust` wrote its `stock_ledger` row with no acting user
(`created_by: ""`), and it updated `current_qty` with no compensation —
the same orphan-ledger shape the new quantity tool removed. The docs
advertise this tool, so it is a load-bearing write path.

- `updateStockItemsQuantities` takes optional `reason`, `referenceType`,
  `referenceId` and `notes` overrides. The defaults are unchanged
  ("adjustment", "manual", the service's own note), so the /stock page
  and the agent write exactly what they wrote before.
- The adjust handler now requires `actor.id` (the same 15-char check and
  error style as `stock_items.set_quantities`), reads the row's current
  quantity, turns the signed delta into an absolute target and calls the
  service. Attribution, ledger-first ordering and the compensating
  reversal row come from the service instead of being duplicated here.
- The caller's `reason` and `notes` survive the conversion, and a
  negative resulting quantity is allowed — the till oversells on purpose.
- The tool description marks the tool as superseded for quantity work
  and points at `stock_items.set_quantities`.
2026-11-06 08:01:10 +02:00
283d970cb0 refactor(repairs): one status vocabulary, and the supplier scope input 2026-11-06 08:00:14 +02:00
c4936613e1 fix(mcp): tell the agent what repairs.upload_file does, and test the rest of the repair tools 2026-11-06 07:39:58 +02:00
9146fd46f0 feat(mcp): eight repair tools for the agent 2026-11-06 07:17:01 +02:00
adb019061a feat(repairs): expose supplier scope, signed copies and status counts 2026-11-06 06:52:23 +02:00
d178309236 feat(repairs): supplier scope, signed copy, status counts, order link 2026-11-06 06:17:56 +02:00
4c0256baf4 test(repairs): pin the status labels, variants and next steps 2026-11-06 05:57:17 +02:00
a3b5996d15 fix(giellie): correct the stock tool actor, audit and quantity pointers 2026-11-06 05:35:26 +02:00
2ad066045a feat(repairs): one shared status vocabulary 2026-11-06 04:56:13 +02:00
36704bd48b fix(pos): put the stock quantity under the price, not in the sub line
The quantity went into the .sub line, which is nowrap + ellipsis: a
28- or 38-character SKU truncated it, so the number fell off the right
edge and pushed the in-cart count out with it. The sub line is back to
what it was — sku, brand, unit, N in cart — and the quantity now sits
under the price, right-aligned, where nothing truncates it.

The POS tests asserted the file's prose rather than its expressions, so
five mutations of the implementation all passed them: a min_qty cue, a
lost zero boundary, a clamp, a round and the segment deleted. They now
pin the expressions, the interpolation, the class wiring and the dark
rule's colour value. All seven mutations tried are killed.

A non-numeric current_qty rendered "NaN in stock" and was not badged —
NaN <= 0 is false. It now falls back to 0 and badges.

Bumped the service-worker cache: pos-app.js and pos.html are both
served cache-first out of the shell, so without a new CACHE name the
tills keep the old ones.
2026-11-06 04:53:37 +02:00
e697f66e98 feat(pos): show the stock quantity on the till tile
The catalog endpoint already sends current_qty and the till already
receives it — pos-app.js just never drew it. The number is shown as it
is, negatives included, and the tile is styled as out of stock when
the quantity is not positive.

The cue is current_qty <= 0, not current_qty <= min_qty: only 48 of
the 4,083 sellable items have a reorder point set, so a min-based rule
would badge every tile and mean nothing.

Nothing is blocked. The cashier can still sell the item and the
quantity still goes into minus.
2026-11-06 02:18:34 +02:00
1b4534a9fc fix(giellie): label the Stock update shortcut and cap the paste box
The shortcut was an unlabelled icon sitting between the paperclip and the
camera, so on a touch device it read as a third kind of attachment. It now
carries a visible "Stock update" label and sits on its own row under the
input row, which also gives the textbox back the width the third icon took.

The paste box gets an accessible name, a max height so a long list cannot
take over the screen, and the trigger reports whether the panel is open.
2026-11-06 02:03:32 +02:00
e08d28f365 feat(giellie): a Stock update shortcut in the composer
Opens a paste panel for the SKU and quantity lines and sends them to
the chat. It carries no set/add intent — the operator is asked that
every time — so it is only a shortcut into the conversation.
2026-11-06 01:39:32 +02:00
c943118300 fix(giellie): a bad stock line must not take the approval card down
The card is fed the model's raw tool input, so a malformed line (a
null, or a qty that is not a number) reached the renderer unchecked
and null.sku threw during render — taking the card, and its
Approve/Reject buttons, with it.

Lines are now read the way the MCP route reads them: an unreadable
line is reported in place, the good lines beside it still render, and
the operator is told that approving rejects the whole batch.

The headings stay put as well. The table now rides inside the card's
own scroller instead of a second scroll region, the header row is
sticky on an opaque background, and the set/add note rides with it —
so every row of a 40-line stocktake is read with its labels and its
mode in view.
2026-11-06 01:26:55 +02:00
9813732e5f fix(giellie): say what "applied" means on a dry run
On a dry run nothing is written, so a row the engine reports as
applied only means it would be applied. The tool description said
none of that, and a reader of the description alone still got the
old ambiguity.
2026-11-06 00:52:35 +02:00
0db696c898 feat(giellie): show stock quantity approvals as a table
A 40-line stocktake in raw JSON is unreviewable. The stock quantity
tool gets a table with the mode, SKU, description and quantity.
Every other tool keeps its JSON exactly as it was.
2026-11-06 00:52:35 +02:00
5c106cf033 fix(giellie): a dry run is a plan, not an outcome
The engine reports a row as applied on a dry run when it only
would be applied, so the preview now says so and states that nothing
has changed yet. The prompt also maps the answer to the mode value
and names the fields the catalogue and the till each keep.
2026-11-06 00:43:15 +02:00
4858717002 feat(giellie): teach the agent the stock quantity workflow
The dry run is a read, so the preview never raises an approval card.
The prompt says to ask whether the numbers are the new quantity on
hand or an addition, every time, before writing anything.
2026-11-06 00:30:04 +02:00
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
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