[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:

  • T02 per-entry "Batch #N" chip in the audit-log panel.

  • T03 Done-stage "View batch" link + Review-missing card.

  • T04 dialog "Done" view → "View batch" link.

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). openedBy / closedBy are display-only until the criteria widens.

Table

Columns: # · Name · Type · Status · Full · Opened on · Opened by · Closed on · Closed by. Sort: any sortable column. Default sort: openedOn DESC. Pagination: server-driven, page size 50 (default), 20/50/100 options. Row click → detail screen.

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

← Back to stock activity returns to the list (history-preserving — the list’s filter state is reapplied via its URL).

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 status = OPEN, a "Close batch" button. Routes mode by batch shape — mode=full-stock-take when type=STOCK_TAKE AND fullMode=true, otherwise mode=normal. Hover copy explains the mode the operator is about to invoke.

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 ?focused=N) · state-pill transition · reason · note · actor · timestamp.

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

  1. Operator navigates to Inventory ▸ Stock activity (or follows a deep-link from T02 / T03 / T04).

  2. List renders sorted by openedOn DESC. Operator filters by type / status / date as needed.

  3. Operator clicks a row → detail screen.

  4. Detail header card renders immediately from GET /api/operational-batches/{id}.

  5. Body renders the categorised drill-down from GET /api/operational-batches/{id}/state-log (pending — see Backend follow-ups).

  6. (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-activity filterable 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/:batchId renders the header card from GET /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_SERVICE appears 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

GET /api/operational-batches

List screen. Paginated (X-Total-Count + Link), JHipster criteria filtering.

GET /api/operational-batches/{id}

Detail header card.

POST /api/operational-batches/{id}/close?mode=normal|full-stock-take

Close-batch action (header).

GET /api/operational-batches/{id}/state-log

Drill-down body for STOCK_TAKE; chronological list for other types.

GET /api/operational-batches/{id}/members

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.

Out of Scope

  • Stock browsing — T02.

  • Stock return — T03.

  • Bulk actions — T04.

  • Print-batch manifest download — already exposed at GET /api/operational-batches/{id}/manifest; surfacing it from this screen is a Track 3 follow-up.

Design Anchors

  • T02 — Number Stock View

  • T03 — Stock Return

  • T04 — Bulk Flag UNFIT / Dispose

  • 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