2026-03-27 16:20:46 +03:00

226 lines
7.1 KiB
Markdown

# Page Reference
## File Location
`apps/dashboard/app/(authenticated)/<section>/<feature>/page.tsx`
Where `<section>` is the navigation section (e.g. `sales`, `inventory`, `hr`) and `<feature>` is the resource in kebab-case plural.
## Complete Template (ResourcePage Pattern)
```tsx
"use client"
import { ResourcePage } from '@/shared/data-view/resource-page'
import { ColumnHeader } from '@/shared/data-view/table-view'
import { <Feature>Form } from '@/modules/<feature>/<feature>-form'
import { <RESOURCE>_ROUTES } from '@garage/api'
import type { <Resource>Client } from '@garage/api'
export default function <Features>Page() {
return (
<ResourcePage<<Resource>Client>
pageTitle="<Features>"
title="<Feature>"
routeKey={<RESOURCE>_ROUTES.INDEX}
getClient={(api) => api.<camelResource>}
columns={({ actionsColumn }) => [
{
accessorKey: "<primary_field>",
header: ({ column }) => <ColumnHeader column={column} title="<Primary Field>" />,
},
{
accessorKey: "<field_2>",
header: ({ column }) => <ColumnHeader column={column} title="<Field 2>" />,
},
// Add more columns as needed...
actionsColumn(),
]}
renderForm={({ resourceId, initialData, onSuccess }) => (
<<Feature>Form
resourceId={resourceId}
initialData={initialData}
onSuccess={onSuccess}
/>
)}
/>
)
}
```
## ResourcePage Props
| Prop | Required | Description |
|---|---|---|
| `pageTitle` | No | Page heading text (e.g. "Customers") |
| `title` | Yes | Singular noun for button/dialog (e.g. "Customer" → "Add Customer") |
| `routeKey` | Yes | React Query cache key, use `ROUTES.INDEX` |
| `getClient` | Yes | Selects the domain client from the authenticated API |
| `columns` | Yes | Column definitions — use callback form to get `actionsColumn` helper |
| `renderForm` | Yes | Renders the form component inside the dialog |
| `queryOptions` | No | React Query overrides (`staleTime`, etc.) |
## Column Patterns
### Simple text column (sortable)
```tsx
{
accessorKey: "name",
header: ({ column }) => <ColumnHeader column={column} title="Name" />,
},
```
### Custom cell renderer
```tsx
{
accessorKey: "status",
header: ({ column }) => <ColumnHeader column={column} title="Status" />,
cell: ({ row }) => (
<span className={row.original.status === "active" ? "text-green-600" : "text-red-600"}>
{row.original.status}
</span>
),
},
```
### Column with icon
```tsx
{
accessorKey: "name",
header: ({ column }) => <ColumnHeader column={column} title="Name" />,
cell: ({ row }) => (
<div className="flex items-center gap-2">
<UserIcon className="text-muted-foreground" />
<span>{row.original.name}</span>
</div>
),
},
```
### Non-sortable column
```tsx
{
accessorKey: "notes",
header: () => <span>Notes</span>,
enableSorting: false,
},
```
### Actions column (always last)
```tsx
actionsColumn(),
```
## Real Example: Customers Page
```tsx
"use client"
import { ResourcePage } from '@/shared/data-view/resource-page'
import { ColumnHeader } from '@/shared/data-view/table-view'
import { CustomerForm } from '@/modules/customers/customer-form'
import { CUSTOMER_ROUTES } from '@garage/api'
import type { CustomersClient } from '@garage/api'
import { Building2Icon, UserIcon } from 'lucide-react'
export default function CustomersPage() {
return (
<ResourcePage<CustomersClient>
pageTitle='Customers'
title="Customer"
routeKey={CUSTOMER_ROUTES.INDEX}
getClient={(api) => api.customers}
columns={({ actionsColumn }) => [
{
accessorKey: "first_name",
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
cell: ({ row }) => {
const customerName = row.original.first_name
const isCompany = row.original.customer_type?.name?.toLocaleLowerCase() === "company";
const companyName = row.original.company_name
const name = isCompany && companyName
? `${customerName} (${row.original.last_name})`
: customerName
return (
<div className="flex items-center gap-2">
{isCompany
? <Building2Icon className="text-muted-foreground" />
: <UserIcon className="text-muted-foreground" />}
<span>{name}</span>
</div>
)
},
},
{
accessorKey: "email",
header: ({ column }) => <ColumnHeader column={column} title="Email" />,
},
{
accessorKey: "phone",
header: ({ column }) => <ColumnHeader column={column} title="Phone" />,
},
actionsColumn(),
]}
renderForm={({ resourceId, initialData, onSuccess }) => (
<CustomerForm
resourceId={resourceId}
initialData={initialData}
onSuccess={onSuccess}
/>
)}
/>
)
}
```
## Alternative: Manual DataTable Pattern (Read-Only or Custom Layout)
Use only when you don't need create/edit/delete in a dialog:
```tsx
"use client"
import { DashboardHeader } from '@/base/components/layout/dashboard'
import DashboardPage from '@/base/components/layout/dashboard/dashboard-page'
import { ColumnHeader, DataTable, useDataTableQuery } from '@/shared/data-view/table-view'
import { useAuthApi } from '@/shared/useApi'
import { <RESOURCE>_ROUTES } from '@garage/api'
import type { ColumnDef } from '@tanstack/react-table'
const columns: ColumnDef<any>[] = [
{
accessorKey: "name",
header: ({ column }) => <ColumnHeader column={column} title="Name" />,
},
// ... more columns
]
export default function <Features>Page() {
const api = useAuthApi()
const { data, isLoading, pagination, sorting, handleChange } = useDataTableQuery({
queryKey: [<RESOURCE>_ROUTES.INDEX],
client: api.<camelResource>,
})
const response = data as any
return (
<DashboardPage header={<DashboardHeader />}>
<DataTable
columns={columns}
data={response?.data ?? []}
pagination={{
...pagination,
pageCount: response?.meta?.last_page ?? 1,
total: response?.meta?.total ?? 0,
}}
sorting={sorting}
onChange={handleChange}
isLoading={isLoading}
/>
</DashboardPage>
)
}
```