[T09] Stock Activity
Summary
Tenant-scope list of all operational_batch rows + per-batch drill-down. The destination for every Round A routerLink="['/inventory/stock-activity', batchId]" dead-end:
For a full-mode STOCK_TAKE batch the drill-down renders five report categories: Expected - Found (member + scanned), Expected - Missing (member + NOT scanned + flipped to ISSUED via STOCK_TAKE_MISSED, keeping or restoring its last holder), Returned (scanned, not a member, not unfit — the register had the number out, and the scan puts it back in stock with the holder cleared), Unfit (scanned while UNFIT_FOR_SERVICE — the number was physically in front of the operator, and is to be pulled off the shelf for disposal rather than reissued; the tab is hidden when the take scanned none), Unmatched (NOT_FOUND; likewise hidden at zero). A STOCK_TAKE that is not full-mode has no expected set, so it renders a single Scanned list instead.
T09 v1 ships with a graceful fallback for the body of the report categories — the per-batch state-log endpoint (GET /api/operational-batches/:id/state-log) is not yet exposed on develop. Until it lands the screen renders the header card + close action fully (so the dead-end links resolve) and surfaces an info banner explaining the missing endpoint in the body. See Backend follow-ups below.
|
Actor & Context
Actor: tenant admin, stock operator, finance / audit reviewer.
Frequency: ad-hoc during incident review; weekly during event seasons.
Precondition: user has TENANT_ADMIN permission; sidebar tenant scope active (see C01 § sidebar).
Entry point: the Activity tab of the Numbers & Tags section (C01), at numbers/activity and numbers/activity/:batchId. Deep links from T02, T03 and T04 also lead here. The earlier stock-activity routes redirect.
Layout
T09 renders inside C01’s shell — sidebar (tenant scope) + topbar are C01’s. T09 owns the main content area.
List screen — /inventory/stock-activity
| Region | Content |
|---|---|
Header |
No title. The breadcrumb ends at Activity, and the record count sits in the pagination bar (list screen affordances). |
Filter strip |
Active filter chips (per the JHipster Tanstack substrate). Per-column popovers expose facets where the back-end criteria carries a matching filter — id (range), name (specified), type (specified), status (specified), opened-on (date range), closed-on (date range). |
Table |
Columns: |
Empty state |
"No batches match these filters" + a Clear-filters affordance when any filter is active. |
Detail screen — /inventory/stock-activity/:batchId
| Region | Content |
|---|---|
Back link |
|
Header card |
Batch eyebrow (id + type label) · name · description · status badge · full-mode badge (STOCK_TAKE only) · grid of opened-on / opened-by / closed-on / closed-by / event (when scoped) / Log entries. A full-mode STOCK_TAKE adds Expected, Scanned and Missing. Every figure carries an info affordance, because the three totals answer different questions and read as contradictory without one: Expected is the snapshot taken when the batch opened, Scanned is what the operator actually read (found + returned + unfit), Missing is expected minus found, and Log entries counts state-change rows rather than numbers — one per scan plus one per number marked missing at close, so it matches neither of the others. |
Close action |
When |
Body — non-STOCK_TAKE |
Single chronological list of attached state-log entries. Each row: bib link (out to T02 with the per-number audit panel auto-opened via |
Body — STOCK_TAKE |
Full mode: tab strip — Expected - Found / Expected - Missing / Returned / Unfit / Unmatched. Each tab shows a count badge + inline explanation of the category + the matching list of state-log entries. Unfit and Unmatched are rendered only when they hold something — both are exceptions, and an empty exception tab is noise. The three counting tabs are always present, because a zero on them is itself an answer. Not full mode: a single Scanned list, since there is no expected set to compare against. |
Main Flow
-
Operator navigates to
Inventory ▸ Stock activity(or follows a deep-link from T02 / T03 / T04). -
List renders sorted by
openedOn DESC. Operator filters by type / status / date as needed. -
Operator clicks a row → detail screen.
-
Detail header card renders immediately from
GET /api/operational-batches/{id}. -
Body renders the categorised drill-down from
GET /api/operational-batches/{id}/state-log(pending — see Backend follow-ups). -
(Optional) Operator closes an OPEN batch via the Close-batch button — mode chosen by batch shape; success refreshes the header card with the closed-state DTO.
Alternative Flows
-
AF-1: Batch id in URL does not exist → 404-style alert "Couldn’t load this batch." Header card stays hidden; the Back link remains active.
-
AF-2: STOCK_TAKE state-log endpoint returns 404 → info banner explaining the backend follow-up; header card + close action remain operational.
-
AF-3: STOCK_TAKE state-log endpoint returns 5xx → "Couldn’t load batch activity. Try refreshing." alert; header card unaffected.
-
AF-4: Close action returns 403 → inline "You don’t have permission to close this batch." next to the button; batch stays OPEN.
Acceptance Criteria
-
List rendered at
/inventory/stock-activityfilterable by id / name / type / status / opened-on / closed-on. -
Default sort =
openedOn DESC; default page size = 50; both pinned on the URL on first paint. -
Sidebar entry "Stock activity" between Return and Onboard, admin-role gated.
-
Detail screen at
/inventory/stock-activity/:batchIdrenders the header card fromGET /api/operational-batches/{id}. -
Detail screen renders the close-batch action for OPEN batches; full-mode STOCK_TAKE batches route
mode=full-stock-take. -
Detail screen renders the report categories for STOCK_TAKE batches with the bucketing predicates documented above.
-
Non-STOCK_TAKE batches render the single chronological list of state-log entries.
-
Round A dead-end links from T02 / T03 / T04 now resolve to a real screen.
-
Header card of a full-mode STOCK_TAKE shows Expected, Scanned and Missing, and every header figure carries an info affordance describing what it counts.
-
Tabs read Expected - Found / Expected - Missing / Returned / Unfit / Unmatched.
-
A number scanned while
UNFIT_FOR_SERVICEappears under Unfit and is excluded from Returned, so the tab is the pull-for-disposal list for this take. -
Unfit lists only numbers scanned in this take — never the unfit register at large.
-
Unfit and Unmatched are hidden at zero; Expected - Found, Expected - Missing and Returned render at zero.
-
A STOCK_TAKE batch that is not full-mode renders a single Scanned list rather than the tab strip.
API Surface
| Call | Purpose |
|---|---|
|
List screen. Paginated ( |
|
Detail header card. |
|
Close-batch action (header). |
|
Drill-down body for STOCK_TAKE; chronological list for other types. |
|
The open-time snapshot of a full-mode STOCK_TAKE — what makes Expected, Expected - Found and Returned computable. |
Backend follow-ups
Both original follow-ups have landed: GET /api/operational-batches/{id}/state-log and GET /api/operational-batches/{id}/members are exposed, so the detail body no longer needs its "endpoint pending" fallback.
The Unfit category needs no new endpoint either — a STOCK_TAKE_SCAN row records the state the number was in when it was scanned, so oldState = UNFIT_FOR_SERVICE identifies it.
Decisions
2026-09-23 — category names, header totals, and a fifth tab. The original labels described the code’s bucketing predicate rather than what the operator sees. A take that put 438 boards over the reader reported "In-stock 337", and "Outsider" named boards that are plainly ours — the register simply had them out. The categories are now Expected - Found, Expected - Missing and Returned.
The scanned total was not on the screen at all: it is the sum of two tabs. The header card now carries Expected, Scanned and Missing, and "Attached rows" becomes Log entries, with an info affordance on each, since that figure counts state-change rows rather than numbers and therefore reconciles with neither total.
Unfit is split out of Returned because it drives a different action: those numbers are on the shelf but must be pulled for disposal rather than reissued, and an operator working from the Returned list would put them straight back into circulation. It is scoped to this take — a number is listed only if it was scanned — so the tab is a pull list for the boxes in front of the operator, not a view of the unfit register. Most takes will scan none, so the tab is hidden when its count is zero, as Unmatched is: both report an exception, and an exception tab reading zero is noise on every ordinary take. The three counting tabs stay visible at zero, because a zero on those is itself an answer — nothing missing, or nothing returned.
A STOCK_TAKE that is not full-mode has no snapshot, so every scan would fall under Returned and Expected - Found would always be empty. Those batches render a single Scanned list.
Raised by the Afrimat board stock take, where the four original labels could not be reconciled with the number of boards actually scanned.
Design Anchors
-
design-journal/2026-02/stock-take-tag-number-grouping.adoc— original four-category framing -
design-journal/2026-03/number-tag-management.adoc§ session 11 — full-mode + missing semantics -
design-journal/2026-05/number-tag-ui-orchestration.adoc§ Feature #742 — Round B wire-up -
design-journal/2026-05/stock-activity-screen-design.adoc— this screen’s decisions log