Skip to content

Item Catalog — Frontend Plan

Module: item-catalog (BE: item_catalog) Status: Draft (plan only; Linear becomes canonical once tickets are created) Generated from: business doc, domain-model, use-cases-detailed, api-contract, data-model, conventions, OpenAPI per-module spec, ui-direction.md (approved 2026-05-21 with chosen-variant TSX baselines under src/ux-exploration/item-catalog/).

Missing Inputs

All planning inputs were available.

Source Path Status
BE business spec ../../../grinsystem-api/docs/business/modules/item_catalog.md Present — 6 use cases, 9 business rules, full actor matrix
BE domain model ../../../grinsystem-api/docs/modules/item_catalog/domain-model.md Present — 2 aggregates, state machine, 4 domain events
BE use-cases-detailed ../../../grinsystem-api/docs/modules/item_catalog/use-cases-detailed.md Present — 10 commands + 6 queries
BE api-contract ../../../grinsystem-api/docs/modules/item_catalog/api-contract.md Present — 16 endpoints with full schemas
BE data-model ../../../grinsystem-api/docs/modules/item_catalog/data-model.md Present — 3 tables, constraints, indexes, RLS
BE conventions ../../../grinsystem-api/docs/architecture/conventions.md Present
OpenAPI spec ../../../openapi/openapi.json (mirror of ../grinsystem-api/docs/api/openapi.json) Present — item-catalog slice carved out by tag filter in orval.config.ts
UX direction ./ux-exploration/ui-direction.md Present — ## Decisions locked 2026-05-21
Chosen variants ../../../src/ux-exploration/item-catalog/*-a.tsx, item-group-detail.tsx Present — all 7 chosen TSX files read structurally
FE conventions ../../standards.md, ../../architecture.md, ../../design-system.md, ../../testing.md Present

UX direction baseline

Chosen variants per ./ux-exploration/ui-direction.md § Decisions (locked 2026-05-21):

Screen Chosen variant Mockup file
item-list A src/ux-exploration/item-catalog/item-list-a.tsx
item-detail A src/ux-exploration/item-catalog/item-detail-a.tsx
item-create A src/ux-exploration/item-catalog/item-create-a.tsx
item-edit A src/ux-exploration/item-catalog/item-edit-a.tsx
item-group-list A src/ux-exploration/item-catalog/item-group-list-a.tsx
item-group-detail sole src/ux-exploration/item-catalog/item-group-detail.tsx
item-import A src/ux-exploration/item-catalog/item-import-a.tsx

Cross-cutting decisions (verbatim from ui-direction.md):

  • Pagination is split out as its own card across every list page.
  • Filter bar selects use the shared shadcn Select.
  • AppShell wraps every variant.
  • The mockups inline <table> markup with local Th / Td / ActionBtn helpers; the real implementation replaces this with the reusable <DataTable> (M0 prereq #1) composing the shadcn Table primitives.
  • MISSING_COMPONENT_DetailDrawer, MISSING_COMPONENT_DetailHeader, MISSING_COMPONENT_SectionNav, MISSING_COMPONENT_PreviewPane, MISSING_COMPONENT_VerticalStepList were only referenced by rejected B variants and are dropped from the prerequisite list.

The mockups are the visual + UX baseline. They are throwaway — hardcoded strings, no i18n, no Orval, no routing, no permission gating. Implementer tickets translate them into real architecture per ../../standards.md (Orval hooks, RHF + Zod, i18n keys, permission gates, URL-bound search params). Discrepancies against the BE OpenAPI are tracked in ./be-feedback.md — implementers do not ship unsupported controls.

Component prerequisites

Validated against the current src/components/ inventory on 2026-05-21 (refreshed 2026-05-25 to add DisabledWithReason per GRF-28 and Combobox per GRF-29 after prototype review; refreshed 2026-05-27 to add MultiSelect per GRF-119 after GRF-46 planning surfaced it as a missing primitive — pages.md § ItemListPage mandates a multi-select Type filter that none of the existing single-value primitives supports). 14 stubs are genuinely missing. Each row becomes one M0 ticket; rows are sorted by reference count (descending) so the most-used components ticket first and unblock the largest number of downstream tickets.

Component Proposed home Consumers (in this module) Cross-module reuse expected?
DataTable src/components/display/DataTable item-list, item-group-list, item-group-detail (items-in-group) Yes — every list-style screen in the app.
FilterBar src/components/common/FilterBar item-list, item-group-list Yes — all list pages.
StatsBar src/components/display/StatsBar item-list, item-group-list Yes — every list page with tenant-wide counts.
Pagination src/components/common/Pagination item-list, item-group-list, item-group-detail Yes — pairs with DataTable.
TagInput src/components/forms/TagInput item-create, item-edit Likely — any tagged entity.
FormActions src/components/common/FormActions item-create, item-edit Yes — every form.
FormDialog src/components/common/FormDialog item-group-list, item-group-detail Yes — every modal form.
DisabledWithReason src/components/common/DisabledWithReason item-group-list (Delete row action), item-group-detail (Delete header) Yes — any aggregate with a server-side deletion / state guard.
FileDropzone src/components/forms/FileDropzone item-import Yes — imports across modules.
ImportErrorList src/components/display/ImportErrorList item-import Yes — every import flow.
DownloadCard src/components/display/DownloadCard item-import Yes — template downloads + exports.
Stepper src/components/common/Stepper (horizontal) item-import Yes — any future wizard.
Combobox src/components/ui/Combobox item-create, item-edit (group field), item-list FilterBar (group filter) Yes — any long-list single-select.
MultiSelect src/components/common/MultiSelect item-list FilterBar (Type multi-select) Yes — multi-status / multi-location / multi-segment facets across manufacturing, warehouse, traceability, crm, invoicing list pages.

DataTable composes the shadcn Table primitives (Table, TableHeader, TableHead, TableBody, TableRow, TableCell from src/components/ui/table.tsx) plus a typed columns API (header label, accessor / render, optional sortKey, optional cell class), sort-header click handlers (asc → desc → unsorted per ../../standards.md § Sorting), row click + row-action slot, empty-state slot, loading / refetch overlay, and a density: 'compact' | 'comfortable' prop (default compact) per ../../design-system.md § Tables.

DisabledWithReason wraps a single clickable child with disabled visuals (opacity-50 cursor-not-allowed) + a Tooltip explaining why the action is blocked. reason is always an i18n key lookup (t('...')), never a literal — see ../../design-system.md § Disabled states for the canonical pattern, key-naming convention, and the composition rule with <PermissionGate>. Use it whenever a control is blocked by a server-side business rule the FE can pre-empt (e.g. delete-with-members → ITEM_GROUP_HAS_MEMBERS 409).

Already shipped (no ticket needed)

shadcn Table primitives (src/components/ui/table.tsx), PageHeader, PageContainer, ConfirmDialog, StatusBadge, Button, Badge, Label, EmptyState, TextField, SelectField, NumberField, TextareaField, CheckboxField, DateField, SwitchField, SaveButton, CancelButton, DeleteButton, all other shadcn primitives.

Dropped (only referenced by rejected B variants)

DetailDrawer, DetailHeader, SectionNav, PreviewPane, VerticalStepList. Per ui-direction.md § Cross-cutting decisions — none of the chosen variants reach for these.

Backend Feedback summary

See ./be-feedback.mdresolved 2026-05-25 via GRF-31. All four flagged items (BF-01 items stats, BF-02 item-groups stats, BF-03 item-groups/options endpoint, BC-01 version field on Item responses) shipped on BE and were synced into the FE Orval client in that ticket. No outstanding rows.

linear-issue-creator-fe is unblocked for this module.

Non-gap recorded for posterity

The PATCH /api/v1/items/{id} body does not accept group_id — and that is intentional. The group is a REST sub-resource with its own permission (ITEM_GROUP_ASSIGN) and its own audit semantics. The FE handles this via the composite-edit pattern (see § Decisions below). No BE change requested. Full reasoning is recorded in be-feedback.md § Non-gaps and in ../../standards.md § Composite Edits.

Scope

In scope (Phase 1):

  • IC-UC-01 Create Item.
  • IC-UC-02 Edit Item.
  • IC-UC-03 Deactivate / Reactivate Item.
  • IC-UC-04 Search and Browse Catalog.
  • IC-UC-05 Create and Manage Item Groups (full CRUD: list, detail, create, edit, delete, assign/remove items).
  • IC-UC-06 Import Items from Excel.

Out of scope:

  • Cross-module reads on the item detail page (stock summary from warehouse, open orders from manufacturing, etc.) — those wait for the respective modules' own plans. The Activity card on ItemDetailPage shows only what the item's own created_* / updated_* fields support until the audit-log endpoint exists (see ../foundation/audit-standards.md for the deferred work).
  • The deactivation guard checks (ITEM_HAS_STOCK_REMAINING, ITEM_HAS_OPEN_PRODUCTION_ORDERS, ITEM_IS_IN_PUBLISHED_BOM, ITEM_HAS_OPEN_SALES_ORDERS) are BE-side cross-module queries (Warehouse / Manufacturing / CRM ports) — the FE only receives the 409 codes and surfaces localized messages. It does not fetch cross-module counts to pre-empt.

Decisions

FE-specific decisions made while planning. Each is anchored in either ../../standards.md or this module's chosen variants.

  • Reusable DataTable composing shadcn Table primitives — The mockups inline <table> markup with local Th / Td helpers as a throwaway shortcut. The real implementation introduces a reusable <DataTable> in src/components/display/ that every list-style page in the app composes. M0 ticket #1. Rationale: dropping the abstraction would force every list page to re-implement sort headers, refetch overlays, and empty-state slots inconsistently.
  • Composite-save pattern for ItemEditPage — The Item aggregate exposes editable fields at PATCH /items/{id} and the group relation at PUT|DELETE /items/{id}/group. The FE collapses this into one user-facing form. On submit, the per-aggregate saveItem helper diffs dirty fields, dispatches the relevant sub-resource mutations in parallel via Promise.allSettled, and maps to a typed ItemSaveOutcome for partial-failure UX. Documented as a new convention in ../../standards.md § Composite Edits so future modules with sub-resource endpoints follow the same pattern.
  • Per-sub-resource permission gating on the edit formPermission.ITEM_UPDATE disables the editable fieldsets; Permission.ITEM_GROUP_ASSIGN disables only the Group select. The Save button is hidden only when both are denied. Reaches the BE design's intent (separate permissions for separate sub-resources) without splitting the form into two screens.
  • System items render as read-only — When is_system === true, the detail page hides Edit + Deactivate; the edit route redirects back to detail with a toast as defence-in-depth against direct URL access. BR-IC-04 (BE) is authoritative; the FE just doesn't surface controls that would always 403.
  • Deactivation guard errors render as inline alerts inside the ConfirmDialog — The four 409 codes get module-local copy (under i18n-keys.md § deactivateError.*) and are rendered as alerts inside the dialog body (the dialog stays open). Route-level toasts would lose the context of which item the user was trying to deactivate. The shared handleApiError parser is still used for all other 4xx codes — this is the only module-local error rendering.
  • StatusBadge known limitation deferred — The badge references CSS variables --status-{success,info,warning,neutral,muted}-{bg,text} that aren't defined in src/styles/tokens.css (which defines --status-{draft,planned,inprogress,completed,archived}-* instead). Status pills render with transparent backgrounds today. Flagged for a foundation ticket per ui-direction.md's cross-cutting note; not in scope for this module's tickets. Item-catalog uses StatusBadge as-is.
  • Pagination defaults match BE defaultspage=1, page_size=20, sort=-created_at, status="active". The URL stays clean at defaults — only non-default values are written, per ../../standards.md § URL as Source of Truth.
  • Cache invalidation uses Orval query-key helpers — Never hand-built arrays. Per-mutation feature hooks (useCreateItemMutation, useEditItemMutation, useDeactivateItemMutation, etc.) wrap the generated mutation hooks and own their invalidation matrix; pages do not call invalidateQueries directly.
  • Group <Combobox> consumes the new /options endpoint, not the paginated list — Documented as a new FE+BE convention in ../../standards.md § Options endpoints (BE side: ../../grinsystem-api/docs/architecture/conventions.md § List Resource Options Endpoints, tracked on BE as GRI-103). Pickers want { id, label }[] not full domain objects, and need an independent cache key so picker invalidation is decoupled from list-page invalidation. The paginated useListItemGroups stays in use on GroupListPage / GroupDetailPage (the actual list pages); only the three picker call sites (item-create form, item-edit form, item-list FilterBar) consume useListItemGroupOptions({ item_type }). BF-03 tracks the BE delivery.
  • Responsive contract (added 2026-05-27 via GRF-113) — Per-page bucket assignments live in ./pages.md § Responsive buckets and are inherited verbatim into each page-implementing Linear ticket's acceptance criteria, per ../../standards.md § Responsive — Implementation contract. Four pages target Excellent on phone (ItemList, ItemDetail, GroupList, GroupDetail), two target Workable on phone (ItemCreate, ItemEdit — 9-field single-column forms), and one targets Soft-gated on phone (ItemImport — Excel template + multi-row validation review needs a wider canvas, and the workflow is an admin / setup task; renders <DesktopOnly> with continue-anyway at <sm). Tablet is always Excellent (feature-parity floor at ≥md). FE pages consume the M8 RF foundation primitives (useBreakpoint, <DesktopOnly>, Sheet, <DataCardList>, <FilterBar>-as-Sheet, Dialog fullscreen-at-sm, Stepper vertical-at-md, FormActions reverse-stack, etc.) — no page-level hand-rolled matchMedia or <md breakpoint logic. The chosen-variant TSX baselines under src/ux-exploration/item-catalog/ remain desktop-only until GRF-114 ships their responsive overlays; implementing tickets still consume the RF primitives directly. Cross-links: ../../responsive-strategy.md, ../../standards.md § Responsive, ../../architecture.md § Responsive contracts, ../../design-system.md § Responsive Design.

Milestones (proposed — Linear becomes canonical once created)

Five milestones, 23 tickets. M0 component prerequisites first because every later milestone depends on them; DataTable heads the M0 list because every list page in the app composes it.

M0 — Bootstrap + Component prerequisites (15 tickets)

  1. DataTable reusable component — composes shadcn Table primitives. Typed columns API, sort-header click handlers, row click + row-action slot, empty-state slot, loading / refetch overlay, density: 'compact' | 'comfortable' prop (default compact) per docs/design-system.md § Tables. Consumers: item-list, item-group-list, item-group-detail (items-in-group), every future list page.
  2. FilterBar reusable component — search input + dropdown facets + reset.
  3. StatsBar reusable component — clickable stat items.
  4. Pagination reusable component — split card with page nav + page-size selector.
  5. TagInput RHF-aware reusable component — enforces 20-tag / 50-char caps client-side.
  6. FormActions reusable component — sticky save / cancel bar with optional left-side helper text.
  7. FormDialog reusable component — dialog wrapper for forms with submit / cancel footer.
  8. DisabledWithReason reusable component — wraps a single clickable child with opacity-50 cursor-not-allowed + a Tooltip whose text comes from reason: string | null (i18n key lookup, never a literal). Composes outside-in with <PermissionGate>. Spec: docs/design-system.md § Disabled states. Used by item-group-list Delete row action and item-group-detail Delete header (ITEM_GROUP_HAS_MEMBERS 409 pre-empt).
  9. FileDropzone reusable component — drag-drop + click, surfaces accepted MIME + max size.
  10. ImportErrorList reusable component — accepts { row, column, message }[] BE shape.
  11. DownloadCard reusable component — title + description + download button.
  12. Stepper (horizontal) reusable component — step indicators with done / current / upcoming states.
  13. Combobox reusable component (shadcn) — searchable single-select primitive (Popover + cmdk Command) for picking one value from a long list. Controlled value / onChange API matching SelectField's shape so consumers can swap with minimal churn. Used by item-create / item-edit for the group field and by the item-list FilterBar's group filter.
  14. MultiSelect reusable component — peer of <Combobox> for the multi-value case (Popover + cmdk Command + per-option Checkbox; bottom Sheet at <sm when fullscreenOnMobile). value: string[] / onChange: (next: string[]) => void; popover stays open across selections; trigger shows the single label at count===1 and a caller-provided summaryTemplate(count) for 2+. First consumer is item-list FilterBar's Type filter; broader reuse expected across multi-status / multi-location / multi-segment facets in manufacturing, warehouse, traceability, crm, invoicing list pages.
  15. Module bootstrap — verify src/generated/item-catalog.ts is current with the latest BE spec (regenerate via pnpm orval if not — the Orval block was wired by GRF-31, so this should be a no-op on a clean tree); create src/features/item-catalog/; create src/locales/{en,pl}/item-catalog.json with the seed translations from i18n-keys.md; add catalog entries to src/lib/routes.ts and NavItems to src/lib/navigation.ts; create the route shell files (empty lazyRouteComponent exports under src/routes/_auth/catalog/); add common-namespace nav labels to src/locales/{en,pl}/common.json.

M1 — Item Read (2 tickets)

  1. T1.1 — Item list page (variant A) — URL-bound search params per routes.md; FilterBar wiring with draft → commit on Apply + 300 ms debounce on search; StatsBar fed by useGetItemStats(); DataTable with sortable columns; split Pagination card; row click → detail; header buttons (New item, Import) per-permission. Empty-state two-copies (no items vs filters-match-nothing).
  2. T1.2 — Item detail page (variant A)useGetItem(itemId); header card with Edit + Deactivate/Reactivate (status-conditional); 2-col grid with three InfoCards; side rail Audit + Activity-placeholder; system-item read-only enforcement; deactivate / reactivate ConfirmDialog wiring lives in T2.3 but the buttons land here.

M2 — Item Writes (3 tickets)

  1. T2.1 — Item create page (variant A) — Zod schema from CreateItemRequest; grouped Fieldsets; TagInput integration; conditional shelf_life_days enablement; group <Combobox> fed by useListItemGroupOptions({ item_type }) (options endpoint per ../../standards.md § Options endpoints); useCreateItem mutation; success → toast + navigate to detail; field-error mapping for 409 codes.
  2. T2.2 — Item edit page (variant A)useGetItem prefill including version; locked Identity fieldset; editable fieldsets per permission; composite-save pattern via saveItem helper (docs/standards.md § Composite Edits) — diffs dirty fields, dispatches useEditItem + useAssignItemToGroup / useRemoveItemFromGroup in parallel via Promise.allSettled; partial-failure UX; 409 CONCURRENT_MODIFICATION handling.
  3. T2.3 — Deactivate / Reactivate actionsConfirmDialog triggered from detail page header buttons; POST /deactivate or /reactivate with Idempotency-Key; localized inline alerts for the four guard-error codes; cache invalidation per data-fetching.md. Permission gate ITEM_DEACTIVATE.

M3 — Item Groups (2 tickets)

  1. T3.1 — Item-group list page (variant A) + create/edit/delete dialogs — URL-bound search params; FilterBar + StatsBar fed by useGetItemGroupStats(); DataTable with Group name / Type / Items count / row Actions; split pagination; "New group" → <FormDialog> create; Edit icon → <FormDialog> edit (PATCH with version echo); Delete icon → <ConfirmDialog> with ITEM_GROUP_HAS_MEMBERS handling; reusable <ItemGroupForm> feature component for both modes.
  2. T3.2 — Item-group detail pageuseGetItemGroup(groupId); header card with Edit (reuses <ItemGroupForm> in <FormDialog>) + Delete (<ConfirmDialog>); items-in-group DataTable fed by useListItemsByGroup; split pagination; per-row Remove-from-group (<ConfirmDialog>DELETE /items/{id}/group); side rail metadata + audit cards.

M4 — Excel Import (1 ticket)

  1. T4.1 — Item import wizard (variant A) — Horizontal <Stepper> with 4 steps; Step 0 <DownloadCard> triggering binary template download; Step 1 <FileDropzone> (≤5 MB, .xlsx only); Step 2 fires POST /items/import, renders <ImportErrorList> on IMPORT_VALIDATION_FAILED (block advancing), bounces to Step 1 on file-size / row-count errors with a toast; Step 3 success state with created_count and "Go to catalog" CTA. Permission gate ITEM_BULK_IMPORT.

Permissions overview

From the BE business doc actor matrix (../../../grinsystem-api/docs/business/modules/item_catalog.md):

Use case Allowed actors
IC-UC-01 (Create Item) Admin, Manager, Technologist
IC-UC-02 (Edit Item — fields) Admin, Manager, Technologist
IC-UC-02 (Edit Item — group reassign) Admin, Manager, Technologist
IC-UC-03 (Deactivate / Reactivate Item) Admin
IC-UC-04 (Search / Browse Catalog) All roles (read-only included)
IC-UC-05 (Manage Item Groups) Admin, Manager (create / edit / delete); All roles (read)
IC-UC-06 (Import Items from Excel) Admin

FE-side Permission.X proposals (per pages.md § Permissions summary):

  • Permission.ITEM_READ — list / detail routes.
  • Permission.ITEM_CREATE — header CTAs, /items/new route.
  • Permission.ITEM_UPDATE — editable fieldsets in the edit form, Edit row / header actions.
  • Permission.ITEM_GROUP_ASSIGN — Group select on the edit form, group-detail "Add item" + per-row Remove.
  • Permission.ITEM_DEACTIVATE — detail header Deactivate / Reactivate.
  • Permission.ITEM_BULK_IMPORT — list header Import button, /import route.
  • Permission.ITEM_GROUP_READ — groups list / detail routes.
  • Permission.ITEM_GROUP_CREATE — groups list header CTA, FormDialog create.
  • Permission.ITEM_GROUP_UPDATE — group edit FormDialog (list + detail).
  • Permission.ITEM_GROUP_DELETE — group delete ConfirmDialog (list + detail).

FE gating is deferred until the BE identity module ships (per CLAUDE.md § Authentication and authorization). The plan reflects intent; per-page permission flags are wired today as comments / TODOs and become real <RequirePermission> / <PermissionGate> wrappers when identity lands. Each name above must be cross-checked against the BE permission registry when identity ships.

References

BE source docs

  • Business: ../grinsystem-api/docs/business/modules/item_catalog.md
  • Domain model: ../grinsystem-api/docs/modules/item_catalog/domain-model.md
  • Use cases: ../grinsystem-api/docs/modules/item_catalog/use-cases-detailed.md
  • API contract: ../grinsystem-api/docs/modules/item_catalog/api-contract.md
  • Data model: ../grinsystem-api/docs/modules/item_catalog/data-model.md
  • Conventions: ../grinsystem-api/docs/architecture/conventions.md

FE conventions

FE plan detail docs (this folder)

  • be-feedback.md — 3 BE items outstanding (BF-01, BF-02, BC-01); plus 1 deliberate non-gap recorded.
  • routes.md — 8 route entries under src/routes/_auth/catalog/, search-param schemas, lazy-load policy, lib/routes.ts + lib/navigation.ts entries.
  • pages.md — 7 pages with per-control permission tables and aggregated permission summary.
  • data-fetching.md — per-page Orval hook usage, mutation hook wrappers, cache-invalidation matrix, composite-save flow for ItemEditPage.
  • forms.mdItemCreateForm, ItemEditForm (composite-save), ItemGroupForm (create + edit modes).
  • i18n-keys.md — proposed src/locales/{en,pl}/item-catalog.json tree with Polish drafts; common-namespace nav labels noted for common.json.
  • prototype-refs.md — page → chosen variant mapping; lifecycle note for src/ux-exploration/item-catalog/.

UX exploration (read-only)

Next step

Review these eight docs. When approved, invoke linear-issue-creator-fe to create the Linear project + 5 milestones + 21 tickets. The skill will block until the three BE-feedback items are checked off (or explicitly acknowledged) in be-feedback.md's Resolution log.