Skip to content

Item Catalog — Routes

URL structure under src/routes/_auth/catalog/. All routes are lazy-loaded per docs/standards.md § Code-Splitting and Lazy Routes. Route files are entry points only (≤20 lines) — the page implementation lives in features/item-catalog/pages/ and features/item-catalog/components/. Every path string belongs in src/lib/routes.ts; route files and components read from that module rather than hard-coding literals.

Route tree

URL File Search-param schema Page (from pages.md) Route gate
/catalog src/routes/_auth/catalog/index.tsx redirects to /catalog/items Permission.ITEM_READ
/catalog/items src/routes/_auth/catalog/items/index.tsx itemListSearchSchema ItemListPage Permission.ITEM_READ
/catalog/items/$itemId src/routes/_auth/catalog/items/$itemId/index.tsx ItemDetailPage Permission.ITEM_READ
/catalog/items/new src/routes/_auth/catalog/items/new.tsx ItemCreatePage Permission.ITEM_CREATE
/catalog/items/$itemId/edit src/routes/_auth/catalog/items/$itemId/edit.tsx ItemEditPage Permission.ITEM_UPDATE (composite — see pages.md)
/catalog/groups src/routes/_auth/catalog/groups/index.tsx groupListSearchSchema GroupListPage Permission.ITEM_GROUP_READ
/catalog/groups/$groupId src/routes/_auth/catalog/groups/$groupId.tsx groupDetailItemsSchema GroupDetailPage Permission.ITEM_GROUP_READ
/catalog/import src/routes/_auth/catalog/import.tsx ItemImportPage Permission.ITEM_BULK_IMPORT

Permission.X values are deferred until the identity module ships. Until then routes render unconditionally (per CLAUDE.md § Authentication and authorization). The intent above is wired today via comments / TODOs so the eventual gate is unambiguous.

Search-param schemas

Schemas live next to the route file that uses them (features/item-catalog/schemas.ts if multiple routes share a shape, otherwise inline at the route file). Defaults mirror BE defaults so the URL stays clean — non-default values only are written to the URL per docs/standards.md § URL as Source of Truth (lists).

itemListSearchSchema

import { z } from 'zod'
import {
  itemTypeSchema,         // generated enum: ingredient | product | intermediate | packaging | consumable | service | resale
  itemStatusFilterSchema, // generated enum: active | inactive | all
  itemListSortSchema,     // generated enum: name | -name | code | -code | created_at | -created_at | updated_at | -updated_at | item_type | -item_type | status | -status | storage_unit | -storage_unit | shelf_life_days | -shelf_life_days
} from '@/generated/schemas/item-catalog'

export const itemListSearchSchema = z.object({
  q:           z.string().max(200).optional(),
  item_type:   z.array(itemTypeSchema).optional(),                 // repeated query param
  status:      itemStatusFilterSchema.default('active'),
  group_id:    z.string().uuid().optional(),
  page:        z.coerce.number().int().min(1).default(1),
  page_size:   z.coerce.number().int().min(1).max(100).default(20),
  sort:        itemListSortSchema.default('-created_at'),
  snapshot_at: z.string().datetime({ offset: true }).optional(),
})

export type ItemListSearch = z.infer<typeof itemListSearchSchema>

groupListSearchSchema

export const groupListSearchSchema = z.object({
  q:         z.string().max(200).optional(),
  item_type: itemTypeSchema.optional(),
  page:      z.coerce.number().int().min(1).default(1),
  page_size: z.coerce.number().int().min(1).max(100).default(20),
})

groupDetailItemsSchema (items-in-group pagination on the group-detail page)

export const groupDetailItemsSchema = z.object({
  page:      z.coerce.number().int().min(1).default(1),
  page_size: z.coerce.number().int().min(1).max(100).default(20),
})

src/lib/routes.ts entries (added by M0 ticket 12)

export const itemCatalogRoutes = {
  root:        '/catalog',
  itemList:    '/catalog/items',
  itemDetail:  (itemId: string) => `/catalog/items/${itemId}`,
  itemCreate:  '/catalog/items/new',
  itemEdit:    (itemId: string) => `/catalog/items/${itemId}/edit`,
  groupList:   '/catalog/groups',
  groupDetail: (groupId: string) => `/catalog/groups/${groupId}`,
  import:      '/catalog/import',
} as const

src/lib/navigation.ts entry (added by M0 ticket 12)

One top-level NavItem with two children, sorted so "Items" is the default landing under "Catalog":

{
  i18nKey: 'common:nav.catalog',
  icon: PackageIcon,         // lucide-react
  path: itemCatalogRoutes.itemList,
  requiredPermission: Permission.ITEM_READ,
  children: [
    { i18nKey: 'common:nav.items',  path: itemCatalogRoutes.itemList,  requiredPermission: Permission.ITEM_READ },
    { i18nKey: 'common:nav.groups', path: itemCatalogRoutes.groupList, requiredPermission: Permission.ITEM_GROUP_READ },
  ],
}

The Import flow is not in the sidebar — it's reached from the Item list page header.

Lazy-load policy

All routes use lazyRouteComponent. None of this module's pages render on first paint (sidebar nav, dashboard, and login all live elsewhere), so no exceptions are needed.