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. AppShellwraps every variant.- The mockups inline
<table>markup with localTh/Td/ActionBtnhelpers; the real implementation replaces this with the reusable<DataTable>(M0 prereq #1) composing the shadcnTableprimitives. MISSING_COMPONENT_DetailDrawer,MISSING_COMPONENT_DetailHeader,MISSING_COMPONENT_SectionNav,MISSING_COMPONENT_PreviewPane,MISSING_COMPONENT_VerticalStepListwere 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.md — resolved 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 frommanufacturing, etc.) — those wait for the respective modules' own plans. The Activity card onItemDetailPageshows only what the item's owncreated_*/updated_*fields support until the audit-log endpoint exists (see../foundation/audit-standards.mdfor 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
DataTablecomposing shadcnTableprimitives — The mockups inline<table>markup with localTh/Tdhelpers as a throwaway shortcut. The real implementation introduces a reusable<DataTable>insrc/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 atPATCH /items/{id}and the group relation atPUT|DELETE /items/{id}/group. The FE collapses this into one user-facing form. On submit, the per-aggregatesaveItemhelper diffs dirty fields, dispatches the relevant sub-resource mutations in parallel viaPromise.allSettled, and maps to a typedItemSaveOutcomefor 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 form —
Permission.ITEM_UPDATEdisables the editable fieldsets;Permission.ITEM_GROUP_ASSIGNdisables 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 (underi18n-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 sharedhandleApiErrorparser is still used for all other 4xx codes — this is the only module-local error rendering. StatusBadgeknown limitation deferred — The badge references CSS variables--status-{success,info,warning,neutral,muted}-{bg,text}that aren't defined insrc/styles/tokens.css(which defines--status-{draft,planned,inprogress,completed,archived}-*instead). Status pills render with transparent backgrounds today. Flagged for a foundation ticket perui-direction.md's cross-cutting note; not in scope for this module's tickets. Item-catalog usesStatusBadgeas-is.- Pagination defaults match BE defaults —
page=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 callinvalidateQueriesdirectly. - Group
<Combobox>consumes the new/optionsendpoint, 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 paginateduseListItemGroupsstays in use onGroupListPage/GroupDetailPage(the actual list pages); only the three picker call sites (item-create form, item-edit form, item-list FilterBar) consumeuseListItemGroupOptions({ 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,Dialogfullscreen-at-sm,Steppervertical-at-md,FormActionsreverse-stack, etc.) — no page-level hand-rolledmatchMediaor<mdbreakpoint logic. The chosen-variant TSX baselines undersrc/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;
DataTableheads the M0 list because every list page in the app composes it.
M0 — Bootstrap + Component prerequisites (15 tickets)¶
DataTablereusable component — composes shadcnTableprimitives. TypedcolumnsAPI, sort-header click handlers, row click + row-action slot, empty-state slot, loading / refetch overlay,density: 'compact' | 'comfortable'prop (defaultcompact) perdocs/design-system.md§ Tables. Consumers: item-list, item-group-list, item-group-detail (items-in-group), every future list page.FilterBarreusable component — search input + dropdown facets + reset.StatsBarreusable component — clickable stat items.Paginationreusable component — split card with page nav + page-size selector.TagInputRHF-aware reusable component — enforces 20-tag / 50-char caps client-side.FormActionsreusable component — sticky save / cancel bar with optional left-side helper text.FormDialogreusable component — dialog wrapper for forms with submit / cancel footer.DisabledWithReasonreusable component — wraps a single clickable child withopacity-50 cursor-not-allowed+ a Tooltip whose text comes fromreason: 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_MEMBERS409 pre-empt).FileDropzonereusable component — drag-drop + click, surfaces accepted MIME + max size.ImportErrorListreusable component — accepts{ row, column, message }[]BE shape.DownloadCardreusable component — title + description + download button.Stepper(horizontal) reusable component — step indicators with done / current / upcoming states.Comboboxreusable component (shadcn) — searchable single-select primitive (Popover+cmdkCommand) for picking one value from a long list. Controlledvalue/onChangeAPI matchingSelectField'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.MultiSelectreusable component — peer of<Combobox>for the multi-value case (Popover+cmdkCommand+ per-option Checkbox; bottomSheetat<smwhenfullscreenOnMobile).value: string[]/onChange: (next: string[]) => void; popover stays open across selections; trigger shows the single label at count===1 and a caller-providedsummaryTemplate(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.- Module bootstrap — verify
src/generated/item-catalog.tsis current with the latest BE spec (regenerate viapnpm orvalif not — the Orval block was wired by GRF-31, so this should be a no-op on a clean tree); createsrc/features/item-catalog/; createsrc/locales/{en,pl}/item-catalog.jsonwith the seed translations fromi18n-keys.md; add catalog entries tosrc/lib/routes.tsand NavItems tosrc/lib/navigation.ts; create the route shell files (emptylazyRouteComponentexports undersrc/routes/_auth/catalog/); add common-namespace nav labels tosrc/locales/{en,pl}/common.json.
M1 — Item Read (2 tickets)¶
- T1.1 — Item list page (variant A) — URL-bound search params per
routes.md;FilterBarwiring with draft → commit on Apply + 300 ms debounce on search;StatsBarfed byuseGetItemStats();DataTablewith sortable columns; splitPaginationcard; row click → detail; header buttons (New item, Import) per-permission. Empty-state two-copies (no items vs filters-match-nothing). - 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)¶
- T2.1 — Item create page (variant A) — Zod schema from
CreateItemRequest; grouped Fieldsets;TagInputintegration; conditionalshelf_life_daysenablement; group<Combobox>fed byuseListItemGroupOptions({ item_type })(options endpoint per../../standards.md§ Options endpoints);useCreateItemmutation; success → toast + navigate to detail; field-error mapping for 409 codes. - T2.2 — Item edit page (variant A) —
useGetItemprefill includingversion; locked Identity fieldset; editable fieldsets per permission; composite-save pattern viasaveItemhelper (docs/standards.md§ Composite Edits) — diffs dirty fields, dispatchesuseEditItem+useAssignItemToGroup/useRemoveItemFromGroupin parallel viaPromise.allSettled; partial-failure UX; 409CONCURRENT_MODIFICATIONhandling. - T2.3 — Deactivate / Reactivate actions —
ConfirmDialogtriggered from detail page header buttons; POST/deactivateor/reactivatewith Idempotency-Key; localized inline alerts for the four guard-error codes; cache invalidation perdata-fetching.md. Permission gateITEM_DEACTIVATE.
M3 — Item Groups (2 tickets)¶
- T3.1 — Item-group list page (variant A) + create/edit/delete dialogs — URL-bound search params;
FilterBar+StatsBarfed byuseGetItemGroupStats();DataTablewith Group name / Type / Items count / row Actions; split pagination; "New group" →<FormDialog>create; Edit icon →<FormDialog>edit (PATCH withversionecho); Delete icon →<ConfirmDialog>withITEM_GROUP_HAS_MEMBERShandling; reusable<ItemGroupForm>feature component for both modes. - T3.2 — Item-group detail page —
useGetItemGroup(groupId); header card with Edit (reuses<ItemGroupForm>in<FormDialog>) + Delete (<ConfirmDialog>); items-in-groupDataTablefed byuseListItemsByGroup; split pagination; per-row Remove-from-group (<ConfirmDialog>→DELETE /items/{id}/group); side rail metadata + audit cards.
M4 — Excel Import (1 ticket)¶
- T4.1 — Item import wizard (variant A) — Horizontal
<Stepper>with 4 steps; Step 0<DownloadCard>triggering binary template download; Step 1<FileDropzone>(≤5 MB,.xlsxonly); Step 2 firesPOST /items/import, renders<ImportErrorList>onIMPORT_VALIDATION_FAILED(block advancing), bounces to Step 1 on file-size / row-count errors with a toast; Step 3 success state withcreated_countand "Go to catalog" CTA. Permission gateITEM_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/newroute.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,/importroute.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¶
- Standards (data fetching, idempotency, OCC, error envelope, lists, forms, composite edits, permissions, observability):
../../standards.md - Architecture (directory layout, three-layer model, routing):
../../architecture.md - Design system (tokens, three-layer component model):
../../design-system.md - Testing (Vitest + RTL + MSW, baseline coverage per ticket):
../../testing.md
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 undersrc/routes/_auth/catalog/, search-param schemas, lazy-load policy,lib/routes.ts+lib/navigation.tsentries.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.md—ItemCreateForm,ItemEditForm(composite-save),ItemGroupForm(create + edit modes).i18n-keys.md— proposedsrc/locales/{en,pl}/item-catalog.jsontree with Polish drafts; common-namespace nav labels noted forcommon.json.prototype-refs.md— page → chosen variant mapping; lifecycle note forsrc/ux-exploration/item-catalog/.
UX exploration (read-only)¶
./ux-exploration/ui-direction.md— UX direction doc with## Decisions.
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.