--- name: crud-page description: "Create CRUD resource pages, forms, schemas, and API clients for the carage-erp dashboard. Use when: adding a new resource page, creating a CRUD feature, building a list/create/edit/delete page, scaffolding a new module, adding a new entity to the dashboard. Covers API client, Zod schema, form component, and page component creation." --- # CRUD Page Generator Create fully functional CRUD resource pages following the established codebase patterns. This skill covers the full stack: API client → Zod schema → form component → page component. ## When to Use - User asks to create a new resource/entity page (e.g. "create a vendors page", "add invoices CRUD") - User asks to add list/create/edit/delete functionality for a domain entity - User asks to scaffold a new module or feature page - User wants to extend the dashboard with a new data management page ## Decision: ResourcePage vs Manual DataTable **Use `ResourcePage` (preferred)** when the resource needs full CRUD (list + create + edit + delete in a dialog). This is the standard pattern. **Use manual `DataTable` + `useDataTableQuery`** only when the page is read-only or has highly custom layout needs. Always prefer `ResourcePage` unless the user explicitly needs something different. ## Procedure Follow these steps **in order**. Each step produces one file. Check the [reference files](./references/) for complete templates and patterns. ### Step 1: Check if API Client Exists Look in `packages/api/src/clients/` for an existing client. Also check `packages/api/src/clients/index.ts` for all registered clients, and `packages/api/src/api.ts` for the factory. - If client exists → skip to Step 3 - If client doesn't exist → continue to Step 2 ### Step 2: Create API Client Read the [API Client Reference](./references/api-client.md) for patterns and template. Create the domain client file at `packages/api/src/clients/.ts`: 1. Define `RESOURCE_ROUTES` const with `INDEX` and `BY_ID` routes (and any extras) 2. Create a class extending `CrudClient` with the route types 3. Add any domain-specific methods beyond standard CRUD 4. Register in `packages/api/src/clients/index.ts` (export class + routes) 5. Register in `packages/api/src/api.ts` (import + add to `createApi()`) **Route pattern**: `"/api/"` for INDEX, `"/api//{id}"` for BY_ID. **IMPORTANT**: Routes must exist in the OpenAPI schema (`packages/api/types/index.ts`) for type safety. If the route doesn't exist in the schema yet, inform the user and ask if they want to proceed with `any` types or wait for schema update. ### Step 3: Create Zod Schema Read the [Schema Reference](./references/schema.md) for patterns and template. Create `apps/dashboard/modules//.schema.ts`: 1. Define `relationFieldSchema` (reuse if already exported) for foreign-key fields 2. Build the Zod object schema with all form fields 3. Use `.optional()` for non-required fields, `.min(1, "...")` for required strings 4. Use `z.union([z.string().email(...), z.literal("")]).optional()` for optional emails 5. Export the schema, the inferred type, and `relationFieldSchema` if new ### Step 4: Create Form Component Read the [Form Reference](./references/form.md) for the complete template. Create `apps/dashboard/modules//-form.tsx`: 1. Define default values matching the schema 2. Create `mapToFormValues(data)` — transforms API shape → form shape using `toRelation()` 3. Create `mapFormToPayload(values)` — transforms form shape → API shape using `toId()` 4. Use `useResourceForm()` for form initialization + edit pre-filling 5. Use `useFormMutation()` for submit with automatic validation error mapping 6. Render with `Rhform` + `RhfTextField` / `RhfSelectField` / `RhfAsyncSelectField` etc. 7. Include error alert, submit button with loading/edit states ### Step 5: Create Page Component Read the [Page Reference](./references/page.md) for the complete template. Create `apps/dashboard/app/(authenticated)/
//page.tsx`: 1. Add `"use client"` directive 2. Import `ResourcePage`, `ColumnHeader`, the form, client type, and routes 3. Configure: `pageTitle`, `title`, `routeKey`, `getClient`, `columns`, `renderForm` 4. Use `columns` callback to receive `actionsColumn` helper 5. Add sortable column headers with `` 6. Include `actionsColumn()` as last column ### Step 6: Verify - Ensure all imports resolve - Check that route constants match OpenAPI paths - Confirm the client is registered in both `clients/index.ts` and `api.ts` ## Key Conventions ### Naming | Item | Pattern | Example | |---|---|---| | Client file | `packages/api/src/clients/.ts` | `job-cards.ts` | | Client class | `Client` | `JobCardsClient` | | Routes const | `_ROUTES` | `JOB_CARD_ROUTES` | | Schema file | `modules//.schema.ts` | `job-card.schema.ts` | | Form file | `modules//-form.tsx` | `job-card-form.tsx` | | Page file | `app/(authenticated)/
//page.tsx` | `sales/job-cards/page.tsx` | | Zod schema | `FormSchema` | `jobCardFormSchema` | | Form values type | `FormValues` | `JobCardFormValues` | | Form component | `Form` | `JobCardForm` | | Page component | `Page` (default export) | `JobCardsPage` | ### Relation Fields (Foreign Keys) - Stored in form as `{ value: string, label: string } | null` - Use `toRelation(id, name)` to convert API data → form value - Use `toId(relation)` to convert form value → API payload - Schema uses `relationFieldSchema = z.object({ value: z.string(), label: z.string() }).nullable()` - Rendered with `` (fetches options via React Query) ### Async Select Pattern ```tsx const mapLookupOption = (item: any) => ({ value: String(item.id), label: item.name }) const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label } api.resource.listSomething()} mapOption={mapLookupOption} {...STORE_OBJECT} /> ``` ### Available Form Field Components | Component | Use For | |---|---| | `RhfTextField` | Text, email, phone, URL inputs | | `RhfTextareaField` | Multi-line text | | `RhfCheckboxField` | Boolean toggles | | `RhfSelectField` | Static option dropdowns | | `RhfAsyncSelectField` | Server-fetched single-select combobox | | `RhfAsyncMultiSelectField` | Server-fetched multi-select combobox | ### Imports Cheat Sheet ```tsx // Page import { ResourcePage } from '@/shared/data-view/resource-page' import { ColumnHeader } from '@/shared/data-view/table-view' import type { Client } from '@repo/api' import { _ROUTES } from '@repo/api' // Form import { Rhform, RhfTextField, RhfSelectField, RhfAsyncSelectField } from "@/shared/components/form" import { useResourceForm } from "@/shared/hooks/use-resource-form" import { useFormMutation } from "@/shared/hooks/use-form-mutation" import { useAuthApi } from "@/shared/useApi" import { toRelation, toId } from "@/shared/lib/utils" import { Button } from "@/shared/components/ui/button" import { Alert, AlertTitle } from "@/shared/components/ui/alert" import { FieldGroup } from "@/shared/components/ui/field" import { toast } from "sonner" // Schema import { z } from "zod" ``` ## Extending the CRUD Codebase If a feature requires functionality not covered by existing utilities (e.g. inline editing, tab-based forms, file uploads, nested resources), you are encouraged to extend the shared infrastructure: - Add new form field components in `shared/components/form/controls/` and `shared/components/form/fields/` - Add new hooks in `shared/hooks/` - Extend `ResourcePage` props if needed - Add new column helper factories in `shared/data-view/table-view/` - Keep extensions generic and reusable — follow the same patterns as existing code