Skip to content

Item Catalog — Pages

One section per page. Cross-referenced from routes.md, data-fetching.md, forms.md. Permission tables follow docs/standards.md § Permission-Gated UI — default is disabled + tooltip; hidden only when justified inline.

The chosen variants live under src/ux-exploration/item-catalog/ (per ui-direction.md § Decisions, locked 2026-05-21). Mockups are throwaway visual baselines — implementer tickets translate them into proper architecture per docs/architecture.md and docs/standards.md. See prototype-refs.md for the list of mockup shortcuts to ignore.


ItemListPage

  • Route: /catalog/items
  • Route gate: <RequirePermission permission={Permission.ITEM_READ}> (deferred until identity ships).
  • Purpose: Browse the item catalog with type / status / group filters, full-text search across name + code + description + group name + tags, sortable columns, paginated. Drives IC-UC-04. Primary entry to IC-UC-01 (Create) and IC-UC-06 (Import).
  • UX baseline: Variant A — src/ux-exploration/item-catalog/item-list-a.tsx. StatsBar above labelled FilterBar above DataTable above split-pagination card. Native English strings, hardcoded example rows, and the inline <table> + local Th/Td helpers in the mockup are throwaway — replaced by i18n keys, Orval data, and the reusable DataTable respectively.
  • Composing components:
  • <PageContainer variant="list"> + <PageHeader> (display, exists) — title from t('item-catalog:page.itemList.title'), description, right-aligned action slot.
  • <StatsBar> (display, M0 prereq) — fed by useGetItemStats. Surfaces Total + Active + Archived only; clicking Active / Archived sets the status URL filter. Per-item_type counts are intentionally not shown — type filtering lives in the FilterBar and a 10-cell banner is noise (decision taken during GRF-46 review; the BE by_item_type breakdown is still returned, just not rendered here).
  • <FilterBar> (common, M0 prereq) — children: search input (debounced 300 ms onto URL q), Type multi-select (Radix Select from ui/), Status tri-state (active / inactive / all), Group <Combobox> (the same searchable single-select primitive used by the item create / edit forms) fed by useListItemGroupOptions({ item_type }) — the BE options endpoint per ../../standards.md § Options endpoints (NOT the paginated useListItemGroups); disabled until exactly one Type is picked (matches BR-IC-05's type-scoped groups), "Reset all" button.
  • <DataTable> (display, M0 prereq) — columns: Code (monospace, narrow), Name (with is_system badge inline), Type (Badge secondary, translated via t('item-catalog:itemType.${row.item_type}')), Group name (or em-dash), Unit (centred, t('item-catalog:storageUnit.${row.storage_unit}')), Status (StatusBadge mapping active → success, inactive → muted). Sortable headers tied to URL sort (asc → desc → unsorted, per standards). Row click → routes.itemCatalogRoutes.itemDetail(row.item_id). Row actions slot: View + Edit icon buttons. Density: uses the default compact (py-2 / h-9) per ../../design-system.md § Tables.
  • <Pagination> (common, M0 prereq) — its own card below the table; binds to URL page + page_size; preserves snapshot_at on page changes and drops it on filter / sort changes (per docs/standards.md § Pagination).
  • Data: data-fetching.md § ItemListPage.
  • Permission gates (per control):
Control Required permission If denied
Header: "New item" Permission.ITEM_CREATE disabled + tooltip
Header: "Import" Permission.ITEM_BULK_IMPORT disabled + tooltip
Row action: View (eye icon) always allowed
Row action: Edit (pencil icon) Permission.ITEM_UPDATE OR Permission.ITEM_GROUP_ASSIGN disabled + tooltip
  • Modals: none (create / edit / import all have their own routes).
  • Empty state: <EmptyState> with Package icon. Two copies — one for "no items at all" (CTA "Add first item" if ITEM_CREATE, otherwise plain message), one for "filters match nothing" (CTA "Reset filters"). Distinguish on whether any filter is active in URL search params.
  • Prototype reference: see prototype-refs.mditem-list-a.tsx synthesises 02-production-list.html + 05-product-catalog.html.

ItemDetailPage

  • Route: /catalog/items/$itemId
  • Route gate: <RequirePermission permission={Permission.ITEM_READ}>.
  • Purpose: Read-only view of one item with header actions for edit and (de)activate. Drives IC-UC-04 (browse) + entry point to IC-UC-02 (edit) and IC-UC-03 (deactivate/reactivate).
  • UX baseline: Variant A — src/ux-exploration/item-catalog/item-detail-a.tsx. Header card + 2-column grid (1fr main column + 380 px side rail). Main column: three InfoCards (Item information, Inventory parameters, Description & tags). Side rail: Audit card + Activity-placeholder card.
  • Composing components:
  • <PageContainer variant="detail"> + back-link (ghost button).
  • Header card (feature-local in features/item-catalog/components/ItemDetailHeader.tsx): mono code eyebrow, large name, secondary metadata line (type · unit · group), right-aligned <Badge> cluster + action buttons.
  • Main column InfoCard + InfoRow (feature-local; small enough to stay in-module — promote to components/display/ only if the second module that needs them shows up).
  • Side rail: AuditCard (Created by/at, Updated by/at — straight from created_by / created_at / updated_by / updated_at), ActivityCard placeholder that renders two entries derived from those same fields until the audit-log endpoint exists (per ui-direction.md § Decisions and the foundation audit-standards proposal).
  • <ConfirmDialog> (common, exists) — used by Deactivate/Reactivate actions.
  • System-item check: when is_system === true, hide Edit + Deactivate/Reactivate buttons entirely. System items are explicitly read-only to tenant actors (BR-IC-04, BE returns SYSTEM_ITEM_IS_READ_ONLY 403 on writes anyway, but the FE doesn't even render the controls).
  • Data: data-fetching.md § ItemDetailPage.
  • Permission gates (per control):
Control Required permission If denied
Header: Edit Permission.ITEM_UPDATE OR Permission.ITEM_GROUP_ASSIGN disabled + tooltip
Header: Deactivate (when status === 'active') Permission.ITEM_DEACTIVATE disabled + tooltip
Header: Reactivate (when status === 'inactive') Permission.ITEM_DEACTIVATE disabled + tooltip
  • Modals: <ConfirmDialog> for both Deactivate and Reactivate. Deactivate dialog body lists the four preconditions (zero stock, no open ZPs, item not in published BOM, no open ZSs) so the user understands what BE will check; specific 409 codes from the BE map to dialog-body alerts (see i18n-keys.md § deactivateError.*).
  • Empty state: N/A (single-aggregate view).
  • Prototype reference: see prototype-refs.mditem-detail-a.tsx synthesises 11-product-detail.html + 03-production-order-detail.html.

ItemCreatePage

  • Route: /catalog/items/new
  • Route gate: <RequirePermission permission={Permission.ITEM_CREATE}>.
  • Purpose: Create one item. Drives IC-UC-01.
  • UX baseline: Variant A — src/ux-exploration/item-catalog/item-create-a.tsx. Single-column form with grouped Fieldset cards (Identity / Classification / Inventory / Description & tags) + sticky <FormActions> bar.
  • Composing components:
  • <PageContainer variant="detail"> + back-link + <PageHeader>.
  • <ItemForm mode="create"> (feature component, features/item-catalog/components/ItemForm.tsx) — the same component renders in create + edit modes (per docs/standards.md § Forms "Feature forms work both standalone and inside FormDialog — same component"). Mode-specific differences (locked fields, FormActions copy) are driven by props.
  • Fieldsets compose: <TextField>, <SelectField>, <NumberField>, <TextareaField> (all forms, exist) + <TagInput> (forms, M0 prereq).
  • <FormActions> (common, M0 prereq) — left-side helper text t('item-catalog:page.itemCreate.helperImmutable') ("Code, type and storage unit cannot be changed after creation."), right-side <CancelButton> + <SaveButton>.
  • Data: data-fetching.md § ItemCreatePage.
  • Permission gates: Route gate covers it. No per-control gating inside the form for create — if the user reaches the page, they can fill every field.
  • Modals: none.
  • Empty state: N/A.
  • Prototype reference: see prototype-refs.mditem-create-a.tsx synthesises 12-product-edit.html.

ItemEditPage

  • Route: /catalog/items/$itemId/edit
  • Route gate: <RequirePermission anyOf={[Permission.ITEM_UPDATE, Permission.ITEM_GROUP_ASSIGN]}>. Both permissions enter through the same page (the user might be able to reassign group only, or edit fields only, or both). Per-control gates inside the form make the actual capability surface.
  • Purpose: Edit one item using the composite-save pattern (docs/standards.md § Composite Edits). Drives IC-UC-02 (fields) + IC-UC-05 / IC-UC-06 (group assign / remove) via a single form.
  • UX baseline: Variant A — src/ux-exploration/item-catalog/item-edit-a.tsx. Same layout as create-a, with the Identity fieldset showing locked immutable inputs and a "Locked" badge.
  • Composing components:
  • Same <PageContainer> + back-link + <PageHeader> shell as create.
  • <ItemForm mode="edit"> — reuses the create form. In edit mode:
    • Locked (rendered as disabled <TextField>s in the Identity fieldset): code, storage_unit, item_type. They are not included in the form's Zod schema for submission — they exist purely as visual context. Hint copy from t('item-catalog:page.itemEdit.lockedHint').
    • Editable, ITEM_UPDATE-gated: name, description, tags, shelf_life_days, ean.
    • Editable, ITEM_GROUP_ASSIGN-gated: group_id (the only field in the Classification fieldset that's editable on edit-mode).
  • Submit handler calls saveItem(itemId, initial, next, mutators) (features/item-catalog/hooks/saveItem.ts) — diffs and dispatches useEditItem and/or useAssignItemToGroup / useRemoveItemFromGroup in parallel, returns a typed ItemSaveOutcome. See forms.md § ItemEditForm and data-fetching.md § ItemEditPage.
  • Data: data-fetching.md § ItemEditPage.
  • System-item check: if the loaded item has is_system === true, redirect back to the detail page with a toast — system items are read-only (BR-IC-04). The Edit button on the detail page should already be hidden, so this is a defence-in-depth check for direct URL access.
  • Permission gates (per control inside the form):
Control Required permission If denied
Identity / Description / Inventory fieldset inputs Permission.ITEM_UPDATE disabled + tooltip
Classification fieldset > Group select Permission.ITEM_GROUP_ASSIGN disabled + tooltip
Save button ITEM_UPDATE OR ITEM_GROUP_ASSIGN hidden when both are denied (the user would have never reached the page in that case — hideWhenDenied is justified because the button has no meaningful action)
  • Modals: none on save success (toast + navigate). On 409 CONCURRENT_MODIFICATION from the field PATCH (pending BC-01), an inline alert above FormActions explains the conflict and provides a "Reload latest" button that re-fetches the item and resets the form.
  • Partial-failure UX: per docs/standards.md § Composite Edits. Field-PATCH errors render as field-level errors inside the relevant fieldset (e.g. EAN_ALREADY_IN_USE highlights the EAN input). Group-call errors render as a section-level alert banner on the Classification fieldset. Toast copy distinguishes "Saved", "Saved with errors — review highlighted sections", "Failed to save".
  • Empty state: N/A.
  • Prototype reference: see prototype-refs.mditem-edit-a.tsx synthesises 12-product-edit.html.

GroupListPage

  • Route: /catalog/groups
  • Route gate: <RequirePermission permission={Permission.ITEM_GROUP_READ}>.
  • Purpose: Browse item-groups. Inline create / edit / delete via FormDialog + ConfirmDialog. Drives IC-UC-05.
  • UX baseline: Variant A — src/ux-exploration/item-catalog/item-group-list-a.tsx. Same StatsBar + FilterBar + DataTable + split pagination pattern as the item list. Row click → dedicated detail page.
  • Composing components:
  • <PageContainer variant="list"> + <PageHeader> with "New group" button.
  • <StatsBar> fed by useGetItemGroupStats.
  • <FilterBar> — search input + Type single-select (Radix Select).
  • <DataTable> — columns: Group name, Type (translated badge), Items count (right-aligned), row Actions (View, Edit, Delete).
  • <Pagination> in its own card below.
  • <FormDialog> (common, M0 prereq) wrapping <ItemGroupForm> (feature component, used by both create and edit flows here and by GroupDetailPage's edit button).
  • <ConfirmDialog> for delete with ITEM_GROUP_HAS_MEMBERS handling.
  • Data: data-fetching.md § GroupListPage.
  • Permission gates (per control):
Control Required permission If denied
Header: "New group" Permission.ITEM_GROUP_CREATE disabled + tooltip
Row action: View always allowed
Row action: Edit Permission.ITEM_GROUP_UPDATE disabled + tooltip
Row action: Delete Permission.ITEM_GROUP_DELETE disabled + tooltip
  • Modals: <FormDialog> for create + edit (same <ItemGroupForm> component, props decide which mode); <ConfirmDialog> for delete. Delete dialog renders an inline alert and disables Confirm when member_count > 0 — BE will reject with ITEM_GROUP_HAS_MEMBERS anyway but the FE pre-empts the round-trip.
  • Empty state: <EmptyState> with FolderTree icon; CTA "New group" if ITEM_GROUP_CREATE.
  • Prototype reference: see prototype-refs.mditem-group-list-a.tsx synthesises 13-ingredient-groups.html.

GroupDetailPage

  • Route: /catalog/groups/$groupId
  • Route gate: <RequirePermission permission={Permission.ITEM_GROUP_READ}>.
  • Purpose: View one item-group and the items belonging to it. Edit / delete from header. Remove individual items from the group inline. Drives IC-UC-05 + entry point to IC-UC-06 (remove-from-group).
  • UX baseline: Sole variant — src/ux-exploration/item-catalog/item-group-detail.tsx. Header card + 2-column grid (1fr main + 380 px side rail). Main column: items-in-group DataTable + split pagination. Side rail: Group metadata card + Audit card.
  • Composing components:
  • <PageContainer variant="detail"> + back-link.
  • Group header card (feature-local): uppercase "Item group" eyebrow, name, metadata line (type · item count), right-aligned badges + Edit / Delete buttons. The Delete button is wrapped in <DisabledWithReason reason={hasMembers ? t('item-catalog:groupDetail.deleteBlockedReason') : null}> so a group with assigned items shows a disabled control + tooltip explaining why, instead of opening the dialog only to be blocked by ITEM_GROUP_HAS_MEMBERS 409. See ../../design-system.md § Disabled states for the canonical pattern and the <actionVerb>BlockedReason key-naming convention. The i18n key is added by the implementing item-catalog ticket.
  • Main column items table: same <DataTable> component as ItemListPage, with column set scoped to (Code, Name + is_system badge, Unit, Status, row Actions — View → item detail, Remove from group). Inline header bar above the table: "Items in this group · N" left, "Manage items" / "Add item" buttons right. Density compact per ../../design-system.md § Tables.
  • <Pagination> below the items table.
  • Side rail InfoCards: Group metadata (Name, Type, Item count, Deletable: derived from member_count === 0), Audit (same shape as ItemDetailPage's Audit card).
  • <FormDialog> + <ItemGroupForm> for the header Edit button (reuses the form component from GroupListPage).
  • <ConfirmDialog> for header Delete — only opens when <DisabledWithReason> evaluates reason to null (no members). Still surfaces a server-side ITEM_GROUP_HAS_MEMBERS 409 inline (race condition where another user reassigned items between page load and click).
  • <ConfirmDialog> for per-row Remove-from-group (small confirmation — the action is reversible but the user should know what they did).
  • Data: data-fetching.md § GroupDetailPage.
  • Permission gates (per control):
Control Required permission If denied
Header: Edit Permission.ITEM_GROUP_UPDATE disabled + tooltip
Header: Delete Permission.ITEM_GROUP_DELETE disabled + tooltip
Above table: "Add item" Permission.ITEM_GROUP_ASSIGN disabled + tooltip
Row action: Remove from group Permission.ITEM_GROUP_ASSIGN disabled + tooltip
  • Modals: Edit <FormDialog>, Delete <ConfirmDialog>, Remove-from-group <ConfirmDialog> (per-row).
  • Empty state: items table empty state — <EmptyState> with Package icon and CTA "Add item to group" (gated by ITEM_GROUP_ASSIGN).
  • Prototype reference: see prototype-refs.mditem-group-detail.tsx has no direct HTML prototype; layout is adapted from item-detail-a.tsx.

ItemImportPage

  • Route: /catalog/import
  • Route gate: <RequirePermission permission={Permission.ITEM_BULK_IMPORT}>.
  • Purpose: Bulk-create items from an .xlsx upload using all-or-nothing semantics. Drives IC-UC-06.
  • UX baseline: Variant A — src/ux-exploration/item-catalog/item-import-a.tsx. Horizontal <Stepper> with 4 steps + single panel per step + footer nav.
  • Composing components:
  • <PageContainer variant="detail"> + back-link + <PageHeader> (title t('item-catalog:page.itemImport.title'), description explaining all-or-nothing).
  • <Stepper> (common, M0 prereq) — 4 steps: Download template / Upload file / Review / Confirm.
  • Step 0 panel: <DownloadCard> (display, M0 prereq) — triggers GET /api/v1/items/import/template (binary .xlsx); filename surfaced from the response's Content-Disposition.
  • Step 1 panel: <FileDropzone> (forms, M0 prereq) — accepts .xlsx only, max 5 MB client-side. Block "Continue" until a file is queued.
  • Step 2 panel: on entry, fire POST /api/v1/items/import with the multipart file + Idempotency-Key. While isLoading, show a spinner and disable Back / Continue. On 400 IMPORT_VALIDATION_FAILED, render <ImportErrorList> (display, M0 prereq) with the details[] rows and block "Continue". On 400 IMPORT_FILE_TOO_LARGE / IMPORT_TOO_MANY_ROWS, show a toast and route the user back to Step 1. On 200/201 (after BE shape clarification — see data-fetching.md), surface the created_count and unlock "Continue".
  • Step 3 panel: success state with created_count and "Go to catalog" CTA — navigates to ItemListPage and invalidates getListItemsQueryKey() + getGetItemStatsQueryKey().
  • Footer nav bar: Back (disabled on step 0) + Continue / Confirm (disabled per the step's gating).
  • Data: data-fetching.md § ItemImportPage.
  • Permission gates (per control): route gate covers it. The page is admin-only by business rule.
  • Modals: none.
  • Empty state: N/A.
  • Prototype reference: no direct HTML prototype — derived from ui-direction.md synthesis of IC-UC-06.

Responsive buckets

Per-page bucket assignment per the rubric in ../../standards.md § Responsive and the policy in ../../responsive-strategy.md. Tablet column is always Excellent by the standards.md § Tablet rule (feature-parity floor at ≥md). Phone column reflects how the screen behaves at <sm. Notes call out the M8 RF foundation primitives the implementing ticket leans on; the full RF index lives in fe-plan.md § Decisions → Responsive contract.

Page Phone bucket Tablet bucket Notes
ItemListPage Excellent on phone Excellent <DataTable> swaps to <DataCardList> at <md (RF12); <FilterBar> collapses into a Sheet trigger at <md (RF13); <StatsBar> scroll-snaps (RF14); <Pagination> compact mode at <md (RF15).
ItemDetailPage Excellent on phone Excellent 2-col grid collapses to single column at <md; side rail (Audit + Activity-placeholder) stacks below the main column. <ConfirmDialog> for Deactivate/Reactivate goes fullscreen at <sm (RF8 + RF9 button stack).
ItemCreatePage Workable on phone Excellent 9-field single-column grouped form — owners can complete the flow on phone but it's scroll-heavy. <FormActions> reverse-stacks with primary on top + full-width at <sm (RF17). <TagInput> chips wrap with 44 px touch targets (RF19). Group <Combobox> falls back to a wider popover at <sm (RF18).
ItemEditPage Workable on phone Excellent Same shell as create. Locked Identity fieldset still renders disabled inputs; the "Locked" hint stays at the top of the card. Composite-save partial-failure alerts render inside their fieldset card so they remain visible after stacking. <DisabledWithReason> tooltips use tap-to-show on touch (RF25).
GroupListPage Excellent on phone Excellent Same list pattern as ItemListPage (RF12–RF15). <FormDialog> for create/edit goes fullscreen + sticky footer at <sm (RF10); Delete <ConfirmDialog> stacks buttons (RF9). The <DisabledWithReason> wrapping the Delete row action when member_count > 0 taps to show its reason (RF25).
GroupDetailPage Excellent on phone Excellent Header card collapses actions into ··· overflow at <md (RF11); items-in-group <DataTable> swaps to <DataCardList> at <md (RF12); side rail (Group metadata + Audit) stacks below the items list. Header Delete <DisabledWithReason> taps to show its reason on touch (RF25).
ItemImportPage Soft-gated on phone Excellent Renders <DesktopOnly> at <sm (RF1) — a polite "use a larger device" card with a continue-anyway link. Reason: Excel-template download + multi-row validation review needs a wider canvas to honestly compare error rows against their source cells, and the workflow is an admin / setup task — owners do not initiate bulk imports from a phone in the field. If continued anyway, the page degrades but does not break: <Stepper> renders vertical at <md (RF16); <FileDropzone> swaps to tap-to-browse on touch (RF22); <ImportErrorList> stacks Row / Column / Message vertically per error at <md (RF23); <DownloadCard> collapses to a single column at <sm (RF24). All-or-nothing semantics are preserved.

Every page-implementing Linear ticket carries the AC line - [ ] Responsive bucket: {bucket} on phone. Tested at <sm and ≥md per docs/responsive-strategy.md. verbatim, per ../../standards.md § Implementation contract.


Permissions summary

Aggregated list of every Permission.X value referenced above. These are FE-side proposals to be validated against the BE permission registry once the identity module ships. Names follow the MODULE_RESOURCE_ACTION convention.

Permission Used at BE actor matrix (from ../../../grinsystem-api/docs/business/modules/item_catalog.md)
Permission.ITEM_READ List / detail routes All roles (read-only included)
Permission.ITEM_CREATE Header CTAs, route gate on /items/new Admin, Manager, Technologist
Permission.ITEM_UPDATE Edit-form fieldsets (fields), edit-page route, Edit row/header actions Admin, Manager, Technologist
Permission.ITEM_GROUP_ASSIGN Edit-form Group select, edit-page route, group-detail "Add item" + "Remove from group" Admin, Manager, Technologist
Permission.ITEM_DEACTIVATE Detail header Deactivate / Reactivate Admin
Permission.ITEM_BULK_IMPORT List header Import button, /import route Admin
Permission.ITEM_GROUP_READ Groups list / detail routes All roles
Permission.ITEM_GROUP_CREATE Groups list header CTA, FormDialog create Admin, Manager
Permission.ITEM_GROUP_UPDATE Group edit FormDialog (list + detail) Admin, Manager
Permission.ITEM_GROUP_DELETE Group delete ConfirmDialog (list + detail) Admin, Manager

ITEM_GROUP_ASSIGN is intentionally separate from ITEM_UPDATE (per the BE design captured in docs/standards.md § Composite Edits and in be-feedback.md § Non-gaps). All other names are direct translations of UC actor entries. None of these names had to be invented during planning — they fall out of the BE business spec — but each one must be cross-checked against the BE permission registry when identity ships, and any rename round-trips through this table.