Skip to content

design

Persona surface for the Supply Chain & Parts, Vehicle Operations and Production — Product Specification. Web flow, canonical Atlas shell (1440).

The Inventory Manager owns stock levels, labelling, and the receiving/returns desk. Today the job runs on a paper Inventory Requisition & Return Form, a Stock Received spreadsheet, and a MocX Delivery Note pad — plus a manual daily count and a low-stock email to Procurement. This surface replaces all of that.

  • Low stock is noticed by eye and chased over email; no in-system trigger and no notification when a threshold is crossed.
  • Labelling is ad hoc, no controlled vocabulary, no unit of measure, no serialized/consumable distinction.
  • Faulty parts pulled during maintenance have nowhere to go — they get mixed back into on-hand or lost.
  • Receiving does not compare expected vs received, and marking an item received has not been decrementing stock (the Atlas/Polaris mismatch).
  • No quantity-change history for month-end reconciliation; no item photos; no defined issuance window so requesters can plan.

The inventory store is always the warehouse (Sango). Every requester is a facility — Ikeja / Abuja / Ibadan, plus the on-site Sango production and retrofit facilities. “Cluster” has been retired from all user-visible copy globally: Transfers, the Delivery notes document (titles, Deliver-to, recent-notes table + filters, tooltips, prose, toasts), the retrieved-bundles (Returns) surface, dashboard events, cross-facility usage, and stock-history rows all read “facility” now (a facility request → fulfilled → facility delivery note). Only internal code identifiers stay legacy-named to avoid rewiring — the data-cluster / dataset.cluster attributes, the CLUSTER_DIR directory, #*-cluster element ids, and BUNDLES cluster: seed keys — and a code comment on CLUSTER_DIR notes the key is legacy-named but its values now hold facility names.

UoM labels are canonical lowercase singular: pcs · each · roll · metre · litre (both pcs and each retained). The unit token — the UoM column value, data-uom, and the UoM/item-create option lists — always stays singular. But an inline count phrase (a quantity followed by its unit) pluralizes when the quantity is not 1: “24 roll” → “24 rolls”, “180 roll covers 14 days” → “180 rolls covers…”, “6 roll to accept” → “6 rolls”, while “1 roll” and sub-1 fractions (“0.5 litre”) stay singular and “18.5 litre” → “18.5 litres”. Static seed copy (item sub-lines, dashboard/return count phrases, delivery-note line qtys, REQ/receipt text) was hand-fixed; dynamic phrases (Transfers detail qty, the fulfil-review “requested …”, fulfilled-vs-requested summaries, delivery-note lines, production-receipt qty/toast) run through a pluralizeQty / unitPlural helper keyed off UNIT_PLURAL = {roll,metre,litre} (already-plural pcs/sets/sheets/pairs and invariant each/ea pass through unchanged). Genuine prose (a return remark “Unused rolls left over…”, “centimetres”) is untouched.

Sidebar: Dashboard, Inventory, Items, Item Categories, Batches, Transfers, Delivery notes, Receiving, Production output, Returns & faulty. Dashboard is the landing view. Breadcrumbs in the topbar; every drill-down carries a back path.

Slug What it does
dashboard KPIs (on-hand, low, critical, open requests to Procurement, pending returns). ATP days-of-cover table (On-hand / Assigned / Available / Burn·day / Days of cover, each with a glossary tooltip; no Held column). Issuance window card — the timeslot the IM sets, visible to requesters.
alerts Low/critical alerts with multi-select + a bulk “Request from Procurement” action (per-line quantity editable). Per-row request also available. Filter bar + dates. The request composer (#dlg-newreq) sends the three things Procurement needs to buy correctly: the requesting location (#newreq-loc), the requester (read-only) and each line’s unit of measure — a line whose item is not in the catalog carries no unit, so reviewNewReq blocks the send until every line resolves to a catalog item.
items Catalog stewardship + KPI row (catalog items, serialized, consumable, faulty held). Category, serialized/consumable type, UOM, faulty-held-separate. Includes finished-good kit / bundle items — Retrofit Kit (v1), Telemetry Bundle, Main Retrofit Harness (Bundle) — carried as Serialized items under a Kits category (closes the loop: Production assembles a kit from inventory items and sends it back as a stockable finished good the Retrofit facility can then request). New item and edit both capture an editable quantity + required reason and show a confirm-with-summary dialog. The item name is composed, not free-typed (both New and Edit): a searchable Item type combobox over a controlled list with search-only aliases (typing “loom” finds Harness, “loctite” finds Threadlocker) — a strict controlled pick with no add-a-type affordance, an unmatched query shows “No matching type” (a type is added on the Item types tab of Item Categories, never inline here, and a disabled type is not offered) — plus Describe it, a single Key specs field, and an optional Add a version toggle (Version + required How-it-differs). A live formed-name preview composes [Describe] [Type] [Version] (Key specs · version basis); a duplicate guardrail blocks an exact catalog match (red) and warns on a similar name (amber, but still allows Create). The composed name flows to the confirm summary, the toast, a real catalog row, and the item detail; the composed parts are stored per item (ITEM_PARTS, a few seed items seeded) so Edit re-opens the slots — items without stored parts keep their current name unless the composer is touched. The composer uses the canonical .fld field block (never the login’s .inp). Row detail: image upload + quantity change history (date-time / change / actor / reason — no source-doc column — CSV export). Column tooltips throughout.
receiving .pseg tabs: Receipts (default), Request variance, Quarantine and Variance cases. The first was named Requests to procurement and argued for it: every row began as a request, so the tab was named for the request rather than the receipt. TH-234 ends that. A delivery another team pushes (Telpo straight to Telematics) was never requested, and the one noun that covers every row is the receipt, so the tab is Receipts and the table names who sent each one in a Source column (see Receipts from other teams below). The low-stock request tab and its bulk-request dialog were removed: requesting from Procurement is a button on the page head and on the dashboard. What the warehouse receives from Procurement and from any team that pushes a delivery. KPI row + a Receipts table with Pending → Received status (Pending populates from requests already sent and from team pushes; opening a receipt shows what was sent, takes the quantity received, and verifies to populate the stock count, whether or not quantities match — no three-way-match strip). Serialized items capture serials on receipt — one serial per unit, entered by type + Enter (removable chips) or CSV upload, validated against the quantity received before it commits; this works on both single receive and bulk receive (each serialized line captures its own serials). A serialized receipt then auto-creates its batch (bulk receive makes a separate batch per item) — or, on single receive only, is received into an existing batch (TH-238): a Receive as .fld select (New batch by default · Existing batch) appears only when at least one candidate exists (same item, same destination warehouse, not tied to any receipt), the batch is picked in a .combo whose options read batch number · item · quantity · serials · date created, and the chosen batch’s facts sit as .kv rows above the upload. Damaged/quarantine is hidden in that mode and an Arrival date (defaulting to the batch’s created date) is added. The receipt row and record then show “84 of 100 serials” with Upload remaining serials until every unit carries one (see Receiving into an existing batch below). Raw serialized components get their warehouse serial here. A receiving-against-expectation table (expected vs received, short/over) is kept below with no row action. Production output is no longer a sub-tab here — it was promoted to its own Production output view (a quick-link button sits in the Receiving page header).
production Production output — finished-goods batches from closed work orders (own nav item under Movement · #nav-production). Closing a work order creates a batch here automatically; the warehouse then accepts the receipt into stock. KPI row (To receive · Units built this week · Received this week) + a #prod-receipts-tbl table (Batch + WO · Kit · Qty built · From · Tracking · Status · Action). Units are serialized on the line when they pass QA, so those production serials carry through on receipt — no new serials are minted here (openReceiveProdKit/confirmReceiveProdKit, #dlg-receive-prod “Accept production batch”): accepting restocks the kit’s on-hand in Items and records the production serials against stock, and the To-receive KPI decrements. A quick-link back to Receiving sits in the header.
batches Serialized batches accepted into the warehouse. Batches are created automatically on acceptance — from a Procurement receipt or from Production output. There is no manual “log batch” button and no per-batch confirm-receipt step: a batch exists only once accepted, so its units are already received. KPI row by source: Total batches · From procurement · From production output · Units received. #batches-tbl columns: Batch number · Item · Category · Qty received · Delivery · Date created · Source (Procurement / Production output, on data-src). Row-click opens #dlg-batch: batch info (incl. Source), and — for a batch that came from Procurement — what it cost: unit cost (what Procurement paid the vendor), the logistics carried in with it and over how many units, the actual unit cost those two make, and the batch total (BATCH_UNIT_COST, BATCH_LOGISTICS, batchCosting). The actual unit cost is the figure the order already computed, unit cost plus that line’s share of the freight, so the warehouse and Procurement state the same number rather than each doing their own sum. Production output is built, not bought, so it carries no purchase cost and those rows do not show. Then a batch-items table Serial Number (the unit’s serial, with any component serial beneath it) · Item (category beneath) · Custodian · Status · Transfer · and the action. Seven columns and seven cells: the row used to build ten against an eight-column header, so every value after the serial sat one place right of its heading. Location is not a column because every unit in a batch is at the warehouse the block above already names, and the last column holds only what can still be done to the unit · what has already happened to it is the Status column’s job. Component serials (e.g. a Battery Pack’s BMS ID) appear only on production-output batches — captured on the line at assembly QA and carried through, never captured at inventory; the #bd-comp-th header takes the component’s name (BATCH_UNIT_SERIALS) and a unit can list several. Procurement batches carry the item’s own serial only. Custodian / Received-by / Received-at populate from the accepting user (BATCH_RECEIVER) and the location tied to them (BATCH_RECEIVE_ADDR). Send-selected-to-quarantine is retained for faulty units. A receipt-less batch (minted by ops before Procurement raised the order, Source No receipt) becomes a receipt’s batch when a delivery is received into it: the record then names the Receipt and arrival date, Serials on batch (“84 of 100 serials”, with an Upload remaining serials .link-add on that row while any are missing · not a fourth footer button), the one Reconciliation event (units before → after, added, removed, serials attached), and, when fewer were received than the batch holds and some of the extra units had already moved, an Over by N · moved units kept list naming each kept serial and where it went. A unit still waiting for a serial reads Serial pending in the Serial Number column; a moved unit names its holder as custodian and reads Issued / Transferred.
returns Two sections under one view. Returns to warehouse holds loose stock coming back from the facilities, production and the retrofit station: delivered-but-unused restock, facility push-back of good surplus stock, faulty-component returns (fault, batch/lot, returned-by, remarks), and components stripped out of a kit, each naming the kit it came out of. Returned retrievals holds whole kits and bundles (BND-####) sent back as the catalog item they are — Retrofit Kit (v1), Telemetry Bundle, Main Retrofit Harness (Bundle): category Kits, Serialized, unit each — and the row says how many items that kit is built from, for reading. Only the kit’s own serial is reconciled, against the serial issued; the warehouse does not scan a kit’s parts because it is not receiving parts (the split is the retrofit station’s act). Both sections read one status vocabulary: Awaiting acceptance on arrival, naming no receiver, then Restocked · Held for repair · Sent to Procurement · Faulty stock · Scrapped · Declined. Received by and the received date/time are system-set, not typed — stamped with the acting Inventory persona at the moment custody passes, which is a different date from the day the stock came back. Faulty units are held in a separate faulty stock count, shown in the KPI row, never counted as available.

| transfers (Requests) | Warehouse → facilities. The surface is labelled Requests — it holds the requests coming in from the facilities (the slug, ids and functions keep the transfers name). KPI row + a table of incoming facility requests (urgency badge kept). The Reason is not a table column — a long field like reason does not belong in the table, so it lives only in the row-click detail (openTrfDetail / #dlg-trf-detail); the reason still rides each row as a data-reason attribute the detail reads. Every row is clickable to a centred detail dialog showing the full request as the facility sent it — facility, requested by, approved by (facility manager), item, qty + unit, reason / purpose, urgency, date required, date requested, status — with Fulfil and Reject actions. Fulfil raises a delivery note and decrements warehouse on-hand. Fulfilment is capped at available on-hand — the review dialog shows requested vs available per line, prefills the quantity at available, highlights any line pushed over, and blocks the delivery note until it is within stock. Seeded with Sango production and retrofit facility requests so the Production / Retrofit → Inventory link is visible. Lifecycle aligns to Production / Retrofit: Open → Fulfilled, plus Rejected (required-reason dialog; badge b-red; the reason is what the facility sees) and Partially fulfilled (issuing less than requested needs a short reason; badge b-amber; the detail shows fulfilled-vs-requested, e.g. “4 of 24 rolls issued”, with the reason — mirroring the Procurement partial-fulfilment pattern). The status filter and KPI row both carry Partially fulfilled + Rejected. | | delivery-note | Generates the facility Delivery Note (deliver-to, delivery no., date, line items chosen from the catalog via a picker, logistics/vehicle/plate). Recent-notes rows are clickable to a detail view with a Print action. | | usage | Cross-facility usage/consumption trend per item with data-suggested reorder points. Filters + dates. | | item-categories | Catalog governance under one .pseg tab strip: Categories, Units of measure, Item types, Category requests (dual approval by Production + Retrofit) and Item requests. The Item types tab (#cat-tab-itemtypes, #itype-tbl) manages the governed vocabulary the item name is composed from, and manages it the way Units of measure are managed — Add item type, Edit, Disable / Enable and Delete, with search, a status .filt-select and Export (filterItemTypes). Columns: Item type · Also found by · Items · Status · Action. The rows are rendered from ITEM_TYPES itself rather than authored as static markup, so the register a steward manages and the combobox an item is named from can never hold two different vocabularies; the search matches a type’s aliases as well as its name (“loom” finds Harness), and a disabled type (seeded: Adapter) drops out of the item form’s combobox while staying on the items already named with it. Items is the count of catalog items whose stored ITEM_PARTS name that type, so the column, the in-use read and the rename all join one way. A rename carries through: the Edit dialog says how many items are named with the type before it is saved, and saving composes each of those names again through composeParts — the one composition rule the live preview also reads — then renameCatalogItem walks the catalog row, the pickers and the records keyed by the old name so a renamed item never reads as two. A type in use cannot be deleted: Delete on such a type opens the shared confirm carrying the list of items named with it and offers Disable item type instead; Delete on a type no item uses removes it from the vocabulary. The Item requests tab (#cat-tab-itemreqs, #itemreq-tbl) is the arrival surface for items a Production Manager or Retrofit Station Manager asked for that the catalog did not have, and says so in one line under its title (“Items the catalog did not carry, approved and now on their way”). Columns: Request · Item · Category · Unit · Qty · Requester · Location · Requested · Status · Action, with search plus status and location .filt-selects (filterItemReq). Statuses are Pending approval / Approved / Rejected — the decision belongs to the requester’s line manager (Head of Production, Head of Retrofit), so Inventory has no approve or reject control here. Row click opens #dlg-itemreq (openItemReq) carrying the whole case: item, category, unit, quantity, why it is needed, industry standard, location, requester and role, need-by, urgency, date requested, and on an approved record the item code, who approved it and when, and the inventory request raised for the quantity. An approved request goes straight to Procurement — the warehouse has no stock of an item that did not exist until now — so the record carries the Procurement request reference alongside the item code, and the status runs With Procurement → Issue to requester → Issued. The seeds are the same requests the Production and Retrofit consoles raised, so one request reads identically at both ends and the issue step can be exercised: ITR-0041 Torque-limiting hub nut M16 (Tobi Oyeshile, procurement request REQ-2071) is received and sits on Issue to requester; ITR-0043 Pre-charge resistor 72V 50W (Kelechi Nwosu, REQ-2073) is still With Procurement; ITR-0035 Anti-static assembly mat roll is already Issued. All three exist in the Items catalog. Issue to requester (issueItemReq) opens the delivery-note builder prefilled with the requester’s facility, the item, the quantity and the procurement request as the linked reference; Open in Items (findItem) jumps to the catalog row with the search set. |

Deep-linkable via ?scenario=<slug> (dashboard, alerts, items, item-categories, receiving, transfers, returns, delivery-note, usage).

Every persona console carries the same Send feedback action: a topbar .icon-action (message icon) opens #dlg-feedback — Type (Bug / Improvement) · Impact (Blocking / Slows me down / Minor) · Where (this persona’s own pages, preselected to the page it was raised from) · Details · optional Screenshot. sendFeedback() validates the required fields and confirms with a toast. Submissions land on the Product Manager · Tricycle Hardware console’s Tech feedback queue, which triages them by type, impact and area.

FR-1 (in-system request trigger), FR-2/FR-18/FR-19 (serialized, UOM, tags), FR-8 (labelling), FR-12 (dashboard), FR-14 (returns increment on-hand), FR-21 (low-stock alerts), FR-23 (decrement/populate-on-receive), FR-25 (reorder from cross-facility usage), FR-26 (faulty logging + disposition, separate faulty stock), FR-27 (item photos), FR-28 (quantity audit log + export), FR-31 (issuance window), FR-32 (delivery note with catalog picker + printable detail), FR-33 (expected vs received). FR-34 (issue-blocking rule) and FR-16 (Atlas·Polaris discrepancy flags) descoped. Conventions: NFR-6/7/8/9; every table column carries a glossary tooltip.

Links out to Battery Identity Admin for an end-to-end demo. No Polaris link — the Inventory Manager requests through Procurement, and Procurement owns the Polaris side.

Per PRD §11 — a Store Officer (issues items, receives faulty returns, produces the delivery note) and a Line Manager (team-request visibility) sit around this surface and are not yet built out.

The Product Manager (Tricycle Hardware) raises investigations off their own analysis and assigns each one to a named owner. This console is where the assignee works it. Assigned to me lists ref, subject, who raised it, severity, how long it has been running (flagged red once it passes the limit the PM set) and status. A row opens the record: scope, the full activity log naming who did what and when, and the actions Start work, Add finding and Close investigation - which will not resolve without both a root cause and the action taken. Findings and the closure are what the PM reads back. Nothing on this page operates a work order.

Stock for an item is held in three buckets: New, Refurbished, Used. One item, one name — a condition is never a separate catalog item and never enters the item’s name, so the same part does not fork into “Buck converter” and “Buck converter (refurb)”.

Every bucket is usable stock. On-hand, ATP, days of cover and the reorder-threshold alerts all run on the total across buckets, because a refurbished unit is stock a facility can be issued. Faulty is not a fourth bucket: faulty units are held apart and never count as available.

The catalog shows composition at a glance. The table carries On-hand, then New / Refurb / Used as their own columns, and the item record repeats the breakdown with each bucket’s share of on-hand. The table stays inside the eight-column rule by folding what only qualifies another cell into it: unit of measure and tracking read under the item they describe, and the count date reads under the status it explains.

Fulfilment assigns the condition; the requester never picks it. A facility requests an item and a quantity, exactly as before — no condition field, no new step. In the fulfil dialog the manager gets one small input per bucket, each capped by what that bucket actually holds and each labelled with it, plus the line’s running total. They can split across buckets (2 new + 1 refurbished), take it all from one, or issue less than requested; the shortfall stays outstanding on the request and still asks for the partial reason. The line’s issued figure is never typed — it is what the buckets add up to.

The split reads on both sides. A fulfilled request states issued: 2 new, 1 refurbished on the row and on the record (Issued from), and the same words go to the item’s movement history. The requester never chose a condition and is still told what they were given.

Returns are received in the bucket vocabulary (RET_BUCKET): the condition choices are exactly the buckets — Unused → New, Refurbished → Refurbished, Used → Used — so a return cannot be described in words the stock model does not hold. Faulty is not in the map at all, so they can never silently become available stock. A faulty return is not automatically Procurement’s problem either, and it is not automatically a repair: the manager chooses the disposition, and the default is simply to accept it as faulty stock — counted on the item’s faulty column and on the faulty KPI, held apart, never available, with what happens to it left open. The other two are hold it for repair, where it counts as faulty (on the shelf, never available) until the fix lands and Accept repaired restocks it into the Refurbished bucket, and return it to Procurement’s quarantine. A row accepted as plain faulty stock keeps its stage hook, so it can still be sent to Procurement later. The disposition applies to loose-stock returns and retrieved kits and bundles alike.

The action column is one three-dot menu. On both desks a pending row’s actions - Accept return / Receive return, and Decline below a divider as the danger option - sit behind the same kebab (.dd row-menu as pm-maintenance ships it), so the column reads uniformly and neither action can be hit by a stray click. The receive dialogs keep their own Decline button, and every decline path lands on one decline dialog (reference, item, sender, required reason). Declining refuses custody: nothing restocks, the stock stays the sender’s, and the row resolves to Declined with the reason travelling back on both records. Choosing hold for repair on a faulty intake reads as Held for repair - the repair has not started, the unit is held apart intending one - until Accept repaired restocks it.

Serialized items carry condition per unit, and the buckets are the rollup — the item record says so where the tracking is serialized.

A stock count counts per condition. The count dialog takes one counted quantity per bucket — New, Refurbished, Used — and Faulty beside them, since faulty units sit on the same shelf without being stock. Every line must be counted (a count that skips a condition is not a count), on-hand reconciles as the sum of the counted buckets, ATP shifts by the same delta, faulty reconciles separately, and the history logs the split. The counted date reads under Status, where the column fold put it.

One place holds the numbers. bkRead / bkWrite / bkAdjust read and move the buckets on the catalog row, and bkAdjust rewrites on-hand as the sum, so on-hand can never drift from its own composition. Everything else — the record, the fulfil dialog, returns — reads through those.

A retrieval comes back from a station as a record, but kits and bundles are already individual items in inventory, so there is nothing to break down: the flow no longer offers a contents view, an item-by-item classification, or a keep-it-together-as-a-reusable-bundle outcome, and the “Reusable bundles” tile is gone.

Receiving and inspection are a single step, and the step is per item. One dialog takes warehouse custody (who received it, stamped when) and settles every item on the retrieval.

What arrives as a kit is received as a kit. Breaking a kit into the parts it is built from is the retrofit station’s act, and it is done on that station’s own Kit reassignment record (atlas/flows/retrofit-manager). What reaches the warehouse is whatever the station actually sent: a whole kit arrives here as that kit, one catalog item with its serial, and components stripped out of a kit arrive as individual items on the returns tab, where every other loose item comes back, each naming the kit it was taken out of. Each line carries its own serial, reconciled against the serial that was issued, its own condition, and where it goes.

A retrieval is one kit or bundle, and only its own serial is reconciled. Every retrieval carries the kit it is; the table names it and says how many items that kit is built from, for reading. The warehouse does not scan a kit’s parts, because it is not receiving parts.

A serial never appears in a list cell. There can be many of them, and a column cannot hold them. The row names the return, the record holds the units, and the movement history follows where each one went.

Both tables carry the same four facts. Who returned it and when it left them; who accepted it and when custody passed (a different date, blank until someone accepts or declines); what came back; and the status. Every row offers its actions the same way, through a row menu, which is also what the record lifts to show the same actions on the detail.

A serial is captured in one field, not one field per unit. Twenty units meant twenty empty boxes down the dialog. Both returns tabs now take the serial however it is read — typed, off a scanner, or out of a CSV packing list — and each lands as a chip in a list that counts itself against what was issued. Reconciliation is unchanged in substance: a unit that was issued and did not come back reads Not returned, and one that was never issued on that note reads Serial mismatch.

Some stock reached the warehouse before Procurement sent it, and ops minted its batch with real serials. When the delivery note for it arrives, the receipt lands in that batch rather than minting a second one. The choice is a .fld select (#rcv-mode), not a .pseg (a tab strip is navigation, never a form answer inside a .dlg) and never .upload-option radio rows (the onboarding wizard’s own atom, defined in no other flow, where PATTERNS names .fld as the form field). It always shows on single receive, serialized or not (round 2, below): with no candidate the Existing batch option stays in the list but is disabled, and the .f-hint under the field says why (No receipt-less Cable fuse 15A batch in Sango warehouse). An unavailable option is shown dim, never hidden, so the IM can see the path exists and what it is waiting for. A non-serialized receipt never mints a batch, so its default option reads On-hand · no batch rather than New batch.

  • The batch picker is a .combo (#rcv-batch-combo). Inside the dialog its panel goes position:fixed off the trigger’s rect, is the trigger’s width and flips above when there is no room, because .dlg-body scrolls and clips. Options are two lines: the batch number, then item · quantity · serials · date created as the .combo-alias.
  • Quantity received resets the batch. More than the batch holds adds units that wait for a serial; fewer removes units awaiting a serial first, then the newest unmoved unit. A unit that has moved (issued, transferred, fitted) is never removed; when they leave the batch above the quantity received, the hint under the quantity says over by N before the commit and the batch record lists them after it.
  • Every serial in the upload is named, with what happens to it (.ser-line rows: the serial in .ser-exp b, the detail under it, a .badge): Repeated in file with its rows, or Captured twice for scans (b-red, blocks); In use elsewhere naming the batch and holder or the pack/VCU/kit (b-red, blocks); Already on this batch (b-grey, skipped); New (b-green, accepted). Checks are trim + case-insensitive and blanks are dropped.
  • Capacity blocks before anything commits. Serials already on the batch plus the new ones may not exceed the quantity received; the .dlg-note.n-amber states the three numbers and how many to remove.
  • Fewer serials than received is allowed. The receipt row shows “84 of 100 serials” and Upload remaining serials, as do the receipt record and the batch record; each reopens the same serial section with the same checks, against the quantity received, until it is complete.
  • No new batch, no second added-to-inventory. The confirm reads Receive into batch, the toast says which batch it was received into, autoBatchFromReceipt is not called, on-hand moves only by the units added or removed, and the batch carries exactly one reconciliation event.

Round 2 (2026-10-09) · non-serialized, the production pool, and the batch-side entry

Section titled “Round 2 (2026-10-09) · non-serialized, the production pool, and the batch-side entry”

UAT showed no Existing batch anywhere: the one demo receipt was non-serialized, the only receipt-less batch had every unit in the production pool, and nothing pointed from a batch to its delivery. Four rules change, and the first one supersedes the ticket’s “the option does not show”:

  • Disabled with its reason, never hidden. #rcv-mode-wrap is always on the single receive dialog (#rcv-mode-ex is disabled when nothing matches, with the reason in #rcv-mode-hint). Bulk receive still offers no existing batch.

  • Non-serialized is batch only. Same batch .combo, same “On this batch now” facts (without the serials row), same quantity and arrival date, but no serial section at all: no upload, no scan, no .ser-line check. The outcome hint says what the resize does: units added get a system serial (never pending, never uploaded); a shrink removes units at rest newest first, and a moved unit is kept and counted as over by N. The receipt is linked, the order set, units at rest repriced, one reconciliation event written, and nothing is posted to the quantity ledger (the units were counted when ops minted the batch), so on-hand does not move. The batch record and its reconciliation line say batch only, quantity ledger not posted.

  • The production pool is at the warehouse. A unit in the warehouse’s own production pool keeps the batch a candidate (B-261009-KAAG0001, every unit pooled), but it is moved: never removed by a shrink, shown as In pool on the batch record, counted in “Moved off the shelf” and listed as over.

  • Batch-side entry. A receipt-less batch’s record carries Receive a delivery into this batch as a .link-add on its Source fact (the foot already has three buttons, and a fourth would make it a surface asking several questions). It opens #dlg-batch-rcv: the pending delivery receipts of the same item for that warehouse as a .tbl (receipt · delivery note · quantity expected · requested), the whole row clickable, or a .dlg-note reading No pending delivery for this item at Ikeja facility. Choosing one opens the same #dlg-receive with Existing batch chosen and this batch picked; the two candidate rules are the same rule read from either end, so a receipt is never forced onto a batch its own dialog would not offer.

  • Used (the default), Refurbished or Unused puts that part back into the bucket its condition names, through the same restockItem to bkAdjust path everything else uses.

  • Faulty swaps the shelf for a disposition, per part: accept as faulty stock, hold for repair, or return to Procurement. Only the parts actually held for repair come back when Accept repaired is pressed, and they come back Refurbished.

The warehouse receives, it does not fit. Sending stock straight out to a rider is the station’s act, not the warehouse’s. What is decided here is the condition each item came back in and, where it is faulty, its disposition; a usable item goes back on the shelf.

Arrived is not accepted, and both tabs say so the same way. A return that had only turned up at the gate read Received, and named a receiver, before anyone had looked at it; the retrievals tab called the same state In transit. Both now read Awaiting acceptance, name nobody until someone accepts or declines, and close into the same words (Restocked, Held for repair, Sent to Procurement, Faulty stock, Scrapped). Opening a record no longer means closing it again to act: the record offers whatever its row offers, as buttons, lifted from the row so the stage logic is not written twice.

The cards are the queue, not a stock report. Awaiting acceptance (counted off both tables, so a card and its rows cannot disagree), how long the oldest one has waited, units restocked in the last seven days, and what is being held out of stock.

One retrieval can therefore end several ways at once, and the row says so (“5 restocked, 2 faulty (1 held for repair)”). Reopening it lists what happened to each item. Discrepancy notes are required on a faulty retrieval, and everything sits behind a sign-off; the status runs in transit -> closed with no pending-inspection stage in between. Reopening a closed retrieval shows the recorded outcome and notes, read-only.

The table carries the full filter set - search, status, severity, a from/to date on when it was raised, and a clear - and its summary cards (active, not started, in progress, running long) sit clear of it. The owner can export a findings pack from here, not only from the PM console. When the PM raises one and assigns it, an assignment notification lands in this console’s own notifications - naming the ref, who raised it and what it is - so a flagged investigation is not missed among the personas who share it.

The PM sets the green and amber bands on their own Analytics — Health targets tab. Until now the people measured against them could not see them, so “we are behind” arrived as an opinion rather than a number. The dashboard carries a Targets card: the measure, where this console stands, the target, and the size of the gap when it is off — over by 5.4h, under by 3 pts — coloured by the PM’s own bands, amber inside the amber band and red past it. The card head counts how many are off, so a miss is visible without reading the rows. A gap between two percentages is stated in points, not per cent.

The card is read-only here. PMTGT holds the same band edges the PM set; tgBand, tgGap and tgStatus derive everything else, so there is one definition of “on target” and no second opinion to keep in step.

A dashboard is where someone lands, so that is where a breach has to reach them; a five-column reference table is not what a dashboard is for. So the two were split the way Monitor and Analyse were split, and neither restates the other.

On the dashboard, one line. #pmg-tgt holds a single .tbl-card head: whether anything is off, and which one is worst. Nothing else. The whole strip is the click target.

Worst means furthest through its own band. Comparing a gap of 5.4h against one of 3 points is not a comparison, so tgFrac expresses each gap against the width of its own band and ranks on that, after ranking on band severity. Before this, “worst” was whichever off-target measure happened to come first, which named Fleet-ready when Immobilized was four times further gone.

On the page, what a strip cannot hold. Each measure against the red/amber/green bands as a bar with a marker where the value sits (tgBar), the full band definitions, and every change the PM has made to that target with the date and who made it. A row opens that measure on its own.

Two things came out of driving the runtime probe to zero on this console.

Wide tables fold, they do not shrink. A qualifying field goes under the field it qualifies as a .td-sub: the category under the item it classifies, the serial under the tracking mode that produced it, the location under the person at it, the date a return was actioned under the outcome. Nothing was dropped to make the count.

A table whose rows have nothing behind them says so. A breakdown by line, a log of what changed, the line items on one note, an admin list whose rows carry their own Edit and Disable links — none of those has a record to open, and demanding a row handler would only produce one that goes nowhere. Those tables carry data-canon="allow-static-rows" with the reason written beside them. Where a record did exist, the row opens it instead.

The page spaces its own sections. Each section used to carry whatever margin its own atom happened to have, which left the summary strip 4px from the section under it here and 0px on the head consoles. #tgt-page now zeroes its children’s bottom margin and put a single 20px between them, so the gap does not depend on which atom comes next. The two tables also stopped declaring an inline margin-top, which would have won against the page rule.

The Targets card used to score Days of cover as a median across all parts. Cover is a per-item measure, so a median of it says nothing about the items that are about to run out. The row is now Parts below cover — how many items sit under the PM’s per-item rule, with the rule named in the basis (of 182 items · each under the 21-day rule).

The requests desk carries two tabs. Incoming is the work queue - open and partially fulfilled, the ones still waiting on the warehouse. All requests is the record, actioned ones included, so a fulfilled, declined or partially fulfilled request stays findable instead of falling off the desk. The facility and status filters run inside either tab, and the seeds carry one of each terminal state so the record has something to hold.

A retrieval says whose keke it came off, and its serials reconcile

Section titled “A retrieval says whose keke it came off, and its serials reconcile”

The retrievals table carries Retrieved from - the rider and vehicle the kit was taken off, or the facility where no rider holds it - beside the reference. The receive dialog reconciles every serialized line against the serial that was issued, the same way a return does: a serial that does not match, or one not returned, requires notes before the retrieval can be received and lands on the record as Restocked · flagged with the discrepancy logged.

Opening an item used to land on the name composer, with the stock buried under three cards of form fields. The record now opens on a KPI row - on-hand with its bucket split, available to promise, faulty held, and how many units are out with holders - and three tabs carry the rest:

  • Overview · stock by condition, reorder thresholds, stock adjustment, quantity history, and the item’s own details: the name composer, photos and classification. Details was its own tab and is not any more, because what an item is and what there is of it are read together.
  • Serials · the units the warehouse has on hand, with search, a condition filter and export. A unit fitted to a keke, out with a team or in Procurement’s quarantine is not stock, and where it went is what the movement history is for. Serials live here and never in a list cell, because there can be many of them.
  • Movement history · what inventory has issued out, newest first. Every line is one issue against a request: the team it went to, where they are, what they asked for, what they were given (with the shortfall where there is one), the units, when it was fulfilled and the request it went against. There is no movement column because there is only one kind of line. It names who fulfilled it beside the date, and filters by team and date range as every other table here does. The tab also carries stock out with teams and sent by inventory to date, both computed off the same data.
  • The record’s table follows the same rules. Filter (team and date range), search, export, and a By column naming who issued the movement and who carried each onward line.

Faulty is the only word for stock that cannot be used

Section titled “Faulty is the only word for stock that cannot be used”

Damaged and Incomplete are gone from the received-condition list: a damaged unit and a unit missing parts are both faulty units, and splitting them bought three words for one state and one disposition path. The list is now the three buckets plus Faulty - if it can be used it names the bucket it restocks into, and if it cannot it is faulty. What was actually wrong with it belongs in the notes, which a faulty return already requires.

The 20px between stacked cards comes from .view>.card+.card, a direct-child rule. Cards moved into the item record’s tab panes stopped matching it and sat flush against each other, which reads as the sections intersecting. .it-tab>.card+.card restores it. Worth knowing before nesting cards anywhere else: the tab panes that already existed hold .tbl-cards, so nothing had hit this before.

Verifying a count decides whether anybody hears about it

Section titled “Verifying a count decides whether anybody hears about it”

A receipt whose count disagrees with the delivery note used to read back as a note on the row and stop there. Now any difference, by any amount, opens a variance case the moment the receipt is verified (raiseVarCase), and Procurement and the requester who raised the request are notified, in-app and by email. There is no tolerance and no setting: the Inventory Manager is the only one who counts, and nothing decides that a difference does not count.

The receive dialog says a case will open before the count is committed (rcvTolHint), so the manager is not told afterwards what they had just caused.

Receiving · Variance cases reads the cases this warehouse raised: case, delivery note leg, item, qty sent, qty received, variance and status, with a record carrying the counts, the vendor, the contractor and vehicle, the request, and the outcome once there is one. It is read-only here. Chasing the vendor and the haulier, on the phone, by email or in person, and the outcome, belong to Procurement; this warehouse supplies the count and the photo.

The value on a case is the landed unit cost times the quantity missing, the same figure Procurement works from, so neither side argues about what the shortfall was worth.

Some items need a manager before the warehouse will fulfil them

Section titled “Some items need a manager before the warehouse will fulfil them”

Most of the catalogue is handed over on the request alone. A few lines are not — the expensive ones, the ones a unit can only justify case by case — and Inventory is the surface that knows which. The item record carries Needs a manager’s approval, and the unit heads’ Item requests pages read it to decide whether a request for an item the catalogue already holds takes that step first.

Inventory sets the flag; Inventory never gives the approval. The approver is the requester’s own line manager, because a warehouse cannot be the one to authorise a request made on it. The field says so in its own hint rather than leaving it to be inferred.

A stock count writes a variance against an item, and the history recorded the split by condition but never the reason, so a reader six weeks later could see the figure move and not what prompted it. The field is optional — most counts are the routine cycle and have nothing to add — and what is typed reads on the item’s history beside the figure: “Stock count · New 12, Refurbished 3, Used 0 · recount after the shelf move”.

The flag Inventory sets is visible on Inventory

Section titled “The flag Inventory sets is visible on Inventory”

Inventory authors the rule that some catalogue items need a manager’s approval before the warehouse will fulfil them, and the rule was invisible on its own screens. Worse, it was held twice: a list of flagged names in one place and an attribute on the catalogue rows in another, and the two had already drifted — the list named five items, three of which this catalogue does not carry, while two rows carried the attribute.

One list now drives both. The catalogue row reads it and carries Needs approval under the item name, the item record’s toggle writes to it, and changing the toggle repaints every surface that reads it. The flagged four are the motor, the controller, the pack BMS and the inverter: the expensive ones.

The requests table names the approver. It was on the record only, so a warehouse clerk could not see who had authorised a queue of requests without opening each one. The column names the requester’s line manager — not a facility manager, and never Inventory, which fulfils a request rather than authorising one made on it — and a request for a flagged item says that the flag is why it waited.

Print put the whole console on the paper. The sheet is now the note alone: no sidebar, no topbar, no page behind it, and no buttons that mean nothing to a driver.

The column answers whether it was approved, not only who signed

Section titled “The column answers whether it was approved, not only who signed”

Three things were wrong with the approver column. A name with no role does not tell a clerk what authority the request travelled on. A request that needed no approval still carried a name, which is the screen asserting what the model does not: only an item Inventory has flagged takes an approval, and everything else is fulfilled on the request alone. And the thing the column was named for was nowhere, because a name was being asked to carry it.

The column is Approval and it states the answer: Approved with the person and Line manager under it, or Not required. The name that was never an approval came off the seven requests that never took one, so the data says what the screen says. The record reads the same way: who approved it and that the flag is why, or Not required - the warehouse fulfils this item on the request alone.

An item request arrives against an item that exists

Section titled “An item request arrives against an item that exists”

The approval mints the catalogue record off what the requester composed, so a request reaching the warehouse is a request for an item already on the catalogue. There is no state here for creating it and no step to wait in.

The warehouse reads the unit cost and only reads it

Section titled “The warehouse reads the unit cost and only reads it”

Inventory does not buy, so it does not set a price. It still has to know what a unit costs: a stock adjustment, a write-off and a count variance are all worth money, and this was the one surface with no figure on it.

The item record’s summary tiles carry it, as a fifth figure beside On-hand, Available to promise, Faulty held and Out with holders: Unit cost, with the vendor and date it came from underneath. It belongs there rather than in a card of its own halfway down the page, which put a number nobody edits among the fields that are edited. There is no history, no landed cost and nothing editable. Procurement owns the price and its record, on Item pricing; this states the one figure the warehouse needs.

A cost that nobody has bought at yet is a quote, and the line beneath says so. A quote that reads like a price is the same mistake on any console. An item with neither reads a dash over “No order yet”, because a bundle worth its parts and an item never ordered are not worth nothing.

A vendor sometimes delivers straight to a team: Telpo drops spare VCUs and display units at Telematics, and Procurement never raised an order. The team pushes that delivery to Inventory from its own console (technical-specialist-kaag · Deliveries to Inventory), and each line lands here as a Pending receipt beside the ones from Procurement. Receiving has no create action and keeps none: a warehouse that could invent its own expected deliveries could receive goods nobody sent, so the push belongs to the sending team, under its own permission.

The tab is Receipts, and the Source column says who sent it. Source reads Procurement or the team’s name (Telematics today, any team that adopts the push tomorrow). Beneath it sits what came with it and when: Procurement’s delivery note leg (DN-0241 · 15 Jul), or the vendor and the day the vendor delivered to the team (Telpo · 16 Jul). The sent date lives there rather than in its own column because a ninth column broke the width budget, and the date belongs to the sender anyway. The Receipt column’s sub-line says what the receipt came from, from REQ-1044 or from TDL-0012. A Source .filt-select sits in the Receipts filter dialog beside status, and status, source and search now narrow together (applyReceipts) where each used to overwrite the others.

The count does not change. Opening a team receipt opens the same #dlg-receive, takes the same quantity received, damaged split, dimension, serials and photo, and verifies the same way. What is added is read-only and sits above the count: Sent by (#rcv-dlg-req, which replaced “Sent from request”: Procurement · REQ-1044 or Telematics · TDL-0012 · Telpo), Received by the team (#rcv-dlg-tm-row: who took it from the vendor, and when) and Proof of delivery (#rcv-dlg-proof-row names the paperwork, Waybill · TLP-WB-55821 · 2 files, and #rcv-dlg-proof shows the files as .thumb tiles that open in the existing #img-view lightbox). The receipt detail (#dlg-rcv-detail) carries the same three rows (#rcd-req, #rcd-tm-row, #rcd-proof-row, #rcd-proof), because a receipt someone reads after the fact needs the proof more than the person counting it in.

A serial the team did not push is refused at the count. The pushed serials are never shown for copying (TH-221 holds: the warehouse keys or scans the unit in hand), but they decide what is accepted. A typed, scanned or CSV serial that is not on the push is refused by name (rcvOnPush, rcvRefuse, rcvKeepPushed), on single and bulk receive alike, and the unit goes back to the team to push again or cancel. Accepting it and recording a variance would have changed the count flow the ticket says must not change.

No variance case for a team receipt. A variance case is Procurement’s dispute with its vendor over a delivery note leg, and a team receipt has neither. A short or over count is recorded on the receipt and on the team’s delivery record, and the hint under the quantity says so before the commit (rcvTolHint). Damaged units still go to quarantine, carrying the vendor the team named. A serialized team receipt mints its batch with the team as its Source, so it never reads as a Procurement batch with no cost behind it.

Demo: TDL-0012, Telematics, Telpo, 6 VCUs (RCV-0048) and 4 display units (RCV-0047), both serialized, both Pending, read from TEAM_DELIVERIES.