584 lines
22 KiB
Markdown
584 lines
22 KiB
Markdown
---
|
|
name: resource-details-page
|
|
description: "Create resource details pages with tabbed layouts, context providers, and sub-pages for the carage-erp dashboard. Use when: building a details/show page for a resource, adding tabs to a resource detail view, creating a nested [id] layout, scaffolding sub-pages (owners, documents, estimates), implementing resource context providers, or adding actions menus to detail headers."
|
|
---
|
|
|
|
# Resource Details Page
|
|
|
|
Create fully structured resource details pages with tabbed navigation, shared context, and reusable sub-page patterns. This skill covers: layout → context provider → general-info component → actions component → tab sub-pages.
|
|
|
|
## When to Use
|
|
|
|
- User asks to create a details/show page for a resource (e.g. "create a customer details page")
|
|
- User asks to add tabs to a resource (e.g. "add an invoices tab to the vehicle page")
|
|
- User asks to scaffold a `[id]` layout with nested pages
|
|
- User wants sub-pages that share parent resource data (e.g. vehicle → estimates)
|
|
- User wants an actions dropdown (edit/delete) on a resource header
|
|
|
|
## Reference Implementation
|
|
|
|
The canonical implementation is the **vehicle details page** at:
|
|
```
|
|
app/(authenticated)/sales/vehicles/[id]/
|
|
├── layout.tsx ← Server component: fetches resource, renders DashboardDetailsPage
|
|
├── page.tsx ← Details tab: server component, read-only info display
|
|
├── owners/page.tsx ← Sub-tab: client component, manual DataTable
|
|
├── documents/page.tsx ← Sub-tab: client component, manual DataTable + upload
|
|
├── estimates/page.tsx ← Sub-tab: client component, ResourcePage with extraParams
|
|
```
|
|
|
|
Module files at:
|
|
```
|
|
modules/vehicles/
|
|
├── vehicle.schema.ts
|
|
├── vehicle-form.tsx
|
|
├── vehicle-general-info.tsx ← Read-only info cards for Details tab
|
|
├── vehicle-actions.tsx ← Dropdown menu (Edit/Delete)
|
|
├── vehicle-context.tsx ← React context for sharing resource data to sub-pages
|
|
└── vehicle-document-form.tsx ← Document-specific form (optional)
|
|
```
|
|
|
|
## Procedure
|
|
|
|
### Step 1: Create the Resource Context
|
|
|
|
Create `modules/<resource>/<resource>-context.tsx`:
|
|
|
|
This context allows child tab pages to access the parent resource's identity (id + display label) without re-fetching. This is essential for sub-pages that create related records (e.g. creating an estimate pre-populated with the parent vehicle).
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import { createContext, useContext } from "react"
|
|
|
|
type <Resource>ContextValue = {
|
|
id: string
|
|
label: string
|
|
}
|
|
|
|
const <Resource>Context = createContext<<Resource>ContextValue | null>(null)
|
|
|
|
export function <Resource>Provider({
|
|
<resource>,
|
|
children,
|
|
}: {
|
|
<resource>: <Resource>ContextValue
|
|
children: React.ReactNode
|
|
}) {
|
|
return (
|
|
<<Resource>Context.Provider value={<resource>}>
|
|
{children}
|
|
</<Resource>Context.Provider>
|
|
)
|
|
}
|
|
|
|
export function use<Resource>() {
|
|
return useContext(<Resource>Context)
|
|
}
|
|
```
|
|
|
|
**Key rules:**
|
|
- Always a `"use client"` component (context requires client React)
|
|
- Keep the context value minimal — only `id` and `label`
|
|
- `label` is a human-readable display string built from the resource's key fields
|
|
- The hook returns `null` when used outside the provider (no throw — graceful fallback)
|
|
- Do NOT store the full resource object — only identity data needed by sub-pages
|
|
|
|
### Step 2: Create the Actions Component
|
|
|
|
Create `modules/<resource>/<resource>-actions.tsx`:
|
|
|
|
This is a dropdown menu rendered in the `DashboardDetailsPage` header for resource-level actions.
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import { useAuthApi } from "@/shared/useApi"
|
|
import { useRouter } from "next/navigation"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/shared/components/ui/dropdown-menu"
|
|
import { Ellipsis, Pencil, Trash2 } from "lucide-react"
|
|
|
|
type <Resource>ActionsProps = {
|
|
<resource>Id: string
|
|
}
|
|
|
|
export function <Resource>Actions({ <resource>Id }: <Resource>ActionsProps) {
|
|
const api = useAuthApi()
|
|
const router = useRouter()
|
|
|
|
const handleEdit = () => {
|
|
router.push(`/<section>/<resources>/${<resource>Id}/edit`)
|
|
}
|
|
|
|
const handleDelete = async () => {
|
|
await api.<resources>.destroy(<resource>Id)
|
|
router.push("/<section>/<resources>")
|
|
}
|
|
|
|
return (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon">
|
|
<Ellipsis className="size-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={handleEdit}>
|
|
<Pencil className="size-4" />
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
|
<Trash2 className="size-4" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Key rules:**
|
|
- Always a `"use client"` component
|
|
- Accepts `<resource>Id: string` as the only required prop
|
|
- Uses `useAuthApi()` for API calls, `useRouter()` for navigation
|
|
- `handleDelete` navigates back to the list page after deletion
|
|
- Add confirmation dialog for destructive actions when appropriate
|
|
|
|
### Step 3: Create the General Info Component
|
|
|
|
Create `modules/<resource>/<resource>-general-info.tsx`:
|
|
|
|
A read-only display component for the **Details tab** (the default `page.tsx`). Uses Card-based layout with icon-labeled fields.
|
|
|
|
```tsx
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
|
import { Badge } from "@/shared/components/ui/badge"
|
|
import { Separator } from "@/shared/components/ui/separator"
|
|
// Import relevant icons from lucide-react
|
|
|
|
type <Resource>Data = {
|
|
// Type all fields from the API response
|
|
id?: number
|
|
name?: string
|
|
// ...
|
|
}
|
|
|
|
type <Resource>GeneralInfoProps = {
|
|
<resource>: <Resource>Data
|
|
}
|
|
|
|
function InfoItem({
|
|
icon: Icon,
|
|
label,
|
|
value,
|
|
}: {
|
|
icon: React.ComponentType<{ className?: string }>
|
|
label: string
|
|
value?: string | null
|
|
}) {
|
|
return (
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
|
<Icon className="size-4" />
|
|
</div>
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="text-xs text-muted-foreground">{label}</span>
|
|
<span className="text-sm font-medium">
|
|
{value || <span className="text-muted-foreground">—</span>}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export function <Resource>GeneralInfo({ <resource> }: <Resource>GeneralInfoProps) {
|
|
return (
|
|
<div className="grid gap-6 md:grid-cols-2">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
{/* Icon */} Section Title
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="grid gap-4">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<InfoItem icon={...} label="Field Name" value={<resource>.field} />
|
|
{/* More InfoItems */}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
{/* More Cards for other field groups */}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Key rules:**
|
|
- This is a **server component** (no `"use client"`) — it receives data as props
|
|
- Group related fields into separate `Card` sections
|
|
- Use the `InfoItem` helper for consistent icon+label+value layout
|
|
- Show `"—"` dash for missing/null values
|
|
- Use `Badge` for status-like fields, `Separator` between groups
|
|
|
|
### Step 4: Create the Layout (Server Component)
|
|
|
|
Create `app/(authenticated)/<section>/<resources>/[id]/layout.tsx`:
|
|
|
|
This is the **central orchestrator** — it fetches the resource, renders the tabbed shell, and wraps children with the context provider.
|
|
|
|
```tsx
|
|
import { DashboardDetailsPage } from '@/base/components/layout/dashboard'
|
|
import { getServerApi } from '@garage/api/server'
|
|
import { <Resource>Actions } from '@/modules/<resources>/<resource>-actions'
|
|
import { <Resource>Provider } from '@/modules/<resources>/<resource>-context'
|
|
import React from 'react'
|
|
|
|
export default async function layout(props: {
|
|
params: Promise<{ id: string }>
|
|
children: React.ReactNode
|
|
}) {
|
|
const { id } = await props.params
|
|
const api = await getServerApi()
|
|
const <resource> = await api.<resources>.getById(id)
|
|
|
|
const title = /* Build display title from resource fields */ ''
|
|
const <resource>Label = /* Build label for context, e.g. combining key fields */ ''
|
|
|
|
return (
|
|
<>
|
|
<<Resource>Provider <resource>={{ id, label: <resource>Label || title }}>
|
|
<DashboardDetailsPage
|
|
className="p-0 lg:p-0"
|
|
avatarSrc={<resource>.data?.image_url || ""}
|
|
title={title}
|
|
description={/* subtitle text */}
|
|
backHref="/<section>/<resources>"
|
|
actions={<<Resource>Actions <resource>Id={id} />}
|
|
tabs={[
|
|
{ href: `/<section>/<resources>/${id}`, label: 'Details' },
|
|
{ href: `/<section>/<resources>/${id}/<sub-tab>`, label: '<Sub Tab>' },
|
|
// More tabs...
|
|
]}
|
|
>
|
|
{props.children}
|
|
</DashboardDetailsPage>
|
|
</<Resource>Provider>
|
|
</>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Key rules:**
|
|
- **Server component** (no `"use client"`) — uses `await` for data fetching
|
|
- Params are `Promise<{ id: string }>` in Next.js 15+ (use `await props.params`)
|
|
- Uses `getServerApi()` from `@garage/api/server` for server-side API calls
|
|
- `<Resource>Provider` wraps the entire `DashboardDetailsPage` so all tab children can access context
|
|
- `backHref` points to the resource list page
|
|
- `tabs[0].href` is always the base `/<section>/<resources>/${id}` (Details tab)
|
|
- `className="p-0 lg:p-0"` removes default padding — sub-pages handle their own spacing
|
|
|
|
**`DashboardDetailsPage` props:**
|
|
|
|
| Prop | Type | Purpose |
|
|
|---|---|---|
|
|
| `title` | `string` | Primary heading in the header |
|
|
| `description` | `string?` | Subtitle text below the title |
|
|
| `avatarSrc` | `string?` | Avatar image URL |
|
|
| `avatarFallback` | `string?` | Fallback text for avatar (e.g. initials) |
|
|
| `icon` | `ReactNode?` | Icon instead of avatar |
|
|
| `actions` | `ReactNode?` | Action buttons in header (right side) |
|
|
| `backHref` | `string?` | Back navigation URL |
|
|
| `tabs` | `{ href, label }[]?` | Route-based tab navigation |
|
|
| `className` | `string?` | Additional classes for content area |
|
|
| `children` | `ReactNode` | Active tab content (Next.js route children) |
|
|
|
|
### Step 5: Create the Details Tab (Default Page)
|
|
|
|
Create `app/(authenticated)/<section>/<resources>/[id]/page.tsx`:
|
|
|
|
```tsx
|
|
import { getServerApi } from '@garage/api/server'
|
|
import { <Resource>GeneralInfo } from '@/modules/<resources>/<resource>-general-info'
|
|
import DashboardPage from '@/base/components/layout/dashboard/dashboard-page'
|
|
|
|
export default async function page(props: { params: Promise<{ id: string }> }) {
|
|
const { id } = await props.params
|
|
const api = await getServerApi()
|
|
const <resource> = await api.<resources>.getById(id)
|
|
|
|
if (!<resource>.data) {
|
|
return <div className="text-muted-foreground"><Resource> not found.</div>
|
|
}
|
|
|
|
return (
|
|
<DashboardPage header={null}>
|
|
<<Resource>GeneralInfo <resource>={<resource>.data} />
|
|
</DashboardPage>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Key rules:**
|
|
- **Server component** — fetches resource data on the server
|
|
- Uses `DashboardPage` wrapper with `header={null}` (the layout already has the header)
|
|
- Renders the general-info component with the full resource data
|
|
- Shows a fallback message if the resource is not found
|
|
|
|
### Step 6: Create Sub-Tab Pages
|
|
|
|
Choose the appropriate pattern based on the sub-tab's requirements:
|
|
|
|
#### Pattern A: ResourcePage with `extraParams` (Preferred for CRUD sub-tabs)
|
|
|
|
Use when the sub-tab needs full CRUD for a **related** resource filtered by the parent ID.
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import { use } from "react"
|
|
import { ResourcePage } from '@/shared/data-view/resource-page'
|
|
import { ColumnHeader } from '@/shared/data-view/table-view'
|
|
import FormDialog from '@/shared/components/form-dialog'
|
|
import { <Related>Form } from '@/modules/<related>/<related>-form'
|
|
import { <RELATED>_ROUTES } from '@garage/api'
|
|
import type { <Related>Client } from '@garage/api'
|
|
import { use<Resource> } from '@/modules/<resource>/<resource>-context'
|
|
|
|
export default function <Resource><Related>Page({ params }: { params: Promise<{ id: string }> }) {
|
|
const { id: <resource>Id } = use(params)
|
|
const <resource> = use<Resource>()
|
|
|
|
return (
|
|
<ResourcePage<<Related>Client>
|
|
toolbar={({ invalidateQuery, selectedItem, closeDialog }) => (
|
|
<FormDialog title="<Related>">
|
|
{(resourceId) => (
|
|
<<Related>Form
|
|
resourceId={resourceId}
|
|
initialData={{
|
|
<resource>: <resource>
|
|
? { value: <resource>.id, label: <resource>.label }
|
|
: null,
|
|
}}
|
|
onSuccess={() => {
|
|
closeDialog();
|
|
invalidateQuery();
|
|
}}
|
|
/>
|
|
)}
|
|
</FormDialog>
|
|
)}
|
|
pageTitle="<Resource> <Related>s"
|
|
routeKey={<RELATED>_ROUTES.INDEX}
|
|
getClient={(api) => api.<related>s}
|
|
extraParams={{ <resource>_id: <resource>Id }}
|
|
header={null}
|
|
columns={({ actionsColumn }) => [
|
|
// Column definitions...
|
|
actionsColumn(),
|
|
]}
|
|
/>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Key rules:**
|
|
- `"use client"` — uses hooks (`use()`, `use<Resource>()`)
|
|
- Uses `use(params)` (React 19) to unwrap the params Promise
|
|
- Consumes the parent context via `use<Resource>()` to pre-populate the form's relation field
|
|
- `extraParams={{ <resource>_id: <resource>Id }}` filters the list to only show related records
|
|
- `header={null}` — the layout already provides the header
|
|
- Pass `initialData` with the parent resource as a relation field `{ value, label }`
|
|
|
|
#### Pattern B: Manual DataTable (For custom interactions)
|
|
|
|
Use when the sub-tab needs non-standard behavior (link/unlink, file uploads, custom mutations).
|
|
|
|
```tsx
|
|
"use client"
|
|
|
|
import { useParams } from "next/navigation"
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
|
import { useAuthApi } from "@/shared/useApi"
|
|
import { DataTable, ColumnHeader } from "@/shared/data-view/table-view"
|
|
import DashboardPage from "@/base/components/layout/dashboard/dashboard-page"
|
|
|
|
export default function <Resource><Tab>Page() {
|
|
const { id: <resource>Id } = useParams<{ id: string }>()
|
|
const api = useAuthApi()
|
|
const queryClient = useQueryClient()
|
|
|
|
const queryKey = ["<resource>-<tab>", <resource>Id]
|
|
|
|
const { data, isLoading } = useQuery({
|
|
queryKey,
|
|
queryFn: () => api.<resources>.get<Tab>(<resource>Id),
|
|
})
|
|
|
|
// Custom mutations (link, unlink, upload, etc.)
|
|
|
|
return (
|
|
<DashboardPage header={null}>
|
|
<DataTable
|
|
columns={columns}
|
|
data={data?.data ?? []}
|
|
isLoading={isLoading}
|
|
/>
|
|
</DashboardPage>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Key rules:**
|
|
- Use `useParams()` or `use(params)` to get the parent resource ID
|
|
- Custom query key includes the parent resource ID
|
|
- Use `DashboardPage header={null}` as wrapper
|
|
- Implement custom mutations with `useMutation` + `useQueryClient` for cache invalidation
|
|
|
|
## API Integration
|
|
|
|
### Server-Side (Layout + Details Tab)
|
|
|
|
```tsx
|
|
import { getServerApi } from '@garage/api/server'
|
|
|
|
// In an async server component:
|
|
const api = await getServerApi()
|
|
const resource = await api.<resources>.getById(id)
|
|
```
|
|
|
|
- `getServerApi()` reads auth cookies server-side — no token passing needed
|
|
- Returns typed responses based on OpenAPI schema
|
|
- Used in `layout.tsx` and the default `page.tsx` (Details tab)
|
|
|
|
### Client-Side (Sub-Tab Pages)
|
|
|
|
```tsx
|
|
import { useAuthApi } from "@/shared/useApi"
|
|
|
|
// In a client component:
|
|
const api = useAuthApi()
|
|
// Then use with React Query:
|
|
useQuery({ queryFn: () => api.<resources>.list() })
|
|
```
|
|
|
|
- `useAuthApi()` returns the API client with the auth token from the store
|
|
- Always use with React Query (`useQuery`, `useMutation`) for caching and state management
|
|
- `ResourcePage` handles this internally — only needed for manual DataTable pages
|
|
|
|
### Client Types
|
|
|
|
API clients live in `packages/api/src/clients/<resource>.ts`. Each exports:
|
|
- `<RESOURCE>_ROUTES` — route constants (`INDEX`, `BY_ID`, etc.)
|
|
- `<Resource>Client` — class with typed methods
|
|
|
|
These are re-exported from `@garage/api` for use in the dashboard.
|
|
|
|
## Component Reusability
|
|
|
|
### Reusable Across Resources
|
|
|
|
| Component | Location | Reuse Pattern |
|
|
|---|---|---|
|
|
| `DashboardDetailsPage` | `@/base/components/layout/dashboard` | Used by every `[id]/layout.tsx` |
|
|
| `DashboardPage` | `@/base/components/layout/dashboard` | Used by every tab `page.tsx` with `header={null}` |
|
|
| `ResourcePage` | `@/shared/data-view/resource-page` | Used by CRUD sub-tabs with `extraParams` |
|
|
| `FormDialog` | `@/shared/components/form-dialog` | Used for create/edit dialogs in sub-tabs |
|
|
| `DataTable` | `@/shared/data-view/table-view` | Used for custom sub-tabs (non-CRUD) |
|
|
| `ColumnHeader` | `@/shared/data-view/table-view` | Used in all column definitions |
|
|
| `InfoItem` pattern | Copy per resource | Icon+label+value display for general-info |
|
|
|
|
### Resource-Specific (Create Per Resource)
|
|
|
|
| Component | Location | Purpose |
|
|
|---|---|---|
|
|
| `<resource>-context.tsx` | `modules/<resource>/` | Context provider for parent resource identity |
|
|
| `<resource>-actions.tsx` | `modules/<resource>/` | Header dropdown menu (edit/delete) |
|
|
| `<resource>-general-info.tsx` | `modules/<resource>/` | Read-only details display for Details tab |
|
|
| `<resource>-form.tsx` | `modules/<resource>/` | CRUD form (shared with list page creation) |
|
|
| `<resource>.schema.ts` | `modules/<resource>/` | Zod schema (shared with form) |
|
|
|
|
## File Structure Convention
|
|
|
|
```
|
|
app/(authenticated)/<section>/<resources>/[id]/
|
|
├── layout.tsx ← Server: fetch + DashboardDetailsPage + Provider
|
|
├── page.tsx ← Server: Details tab (GeneralInfo)
|
|
├── <sub-tab-1>/page.tsx ← Client: ResourcePage or manual DataTable
|
|
├── <sub-tab-2>/page.tsx ← Client: ResourcePage or manual DataTable
|
|
└── ...
|
|
|
|
modules/<resources>/
|
|
├── <resource>.schema.ts ← Zod form schema
|
|
├── <resource>-form.tsx ← CRUD form component
|
|
├── <resource>-context.tsx ← Context provider (id + label)
|
|
├── <resource>-actions.tsx ← Actions dropdown
|
|
├── <resource>-general-info.tsx ← Read-only info cards
|
|
└── <optional-sub-forms>/ ← e.g. document upload forms
|
|
```
|
|
|
|
## Naming Conventions
|
|
|
|
| Item | Pattern | Example |
|
|
|---|---|---|
|
|
| Layout file | `app/.../<resources>/[id]/layout.tsx` | `vehicles/[id]/layout.tsx` |
|
|
| Details page | `app/.../<resources>/[id]/page.tsx` | `vehicles/[id]/page.tsx` |
|
|
| Sub-tab page | `app/.../<resources>/[id]/<tab>/page.tsx` | `vehicles/[id]/estimates/page.tsx` |
|
|
| Context file | `modules/<resources>/<resource>-context.tsx` | `vehicles/vehicle-context.tsx` |
|
|
| Context type | `<Resource>ContextValue` | `VehicleContextValue` |
|
|
| Provider | `<Resource>Provider` | `VehicleProvider` |
|
|
| Hook | `use<Resource>()` | `useVehicle()` |
|
|
| Actions file | `modules/<resources>/<resource>-actions.tsx` | `vehicles/vehicle-actions.tsx` |
|
|
| Actions component | `<Resource>Actions` | `VehicleActions` |
|
|
| General info file | `modules/<resources>/<resource>-general-info.tsx` | `vehicles/vehicle-general-info.tsx` |
|
|
| General info component | `<Resource>GeneralInfo` | `VehicleGeneralInfo` |
|
|
|
|
## Imports Cheat Sheet
|
|
|
|
```tsx
|
|
// Layout
|
|
import { DashboardDetailsPage } from '@/base/components/layout/dashboard'
|
|
import { getServerApi } from '@garage/api/server'
|
|
import { <Resource>Actions } from '@/modules/<resources>/<resource>-actions'
|
|
import { <Resource>Provider } from '@/modules/<resources>/<resource>-context'
|
|
|
|
// Details tab
|
|
import { getServerApi } from '@garage/api/server'
|
|
import { <Resource>GeneralInfo } from '@/modules/<resources>/<resource>-general-info'
|
|
import DashboardPage from '@/base/components/layout/dashboard/dashboard-page'
|
|
|
|
// Sub-tab (ResourcePage pattern)
|
|
import { ResourcePage } from '@/shared/data-view/resource-page'
|
|
import { ColumnHeader } from '@/shared/data-view/table-view'
|
|
import FormDialog from '@/shared/components/form-dialog'
|
|
import { use<Resource> } from '@/modules/<resources>/<resource>-context'
|
|
import type { <Related>Client } from '@garage/api'
|
|
import { <RELATED>_ROUTES } from '@garage/api'
|
|
|
|
// Sub-tab (Manual DataTable pattern)
|
|
import { useAuthApi } from "@/shared/useApi"
|
|
import { DataTable, ColumnHeader } from "@/shared/data-view/table-view"
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
|
|
import DashboardPage from '@/base/components/layout/dashboard/dashboard-page'
|
|
|
|
// Context provider
|
|
import { createContext, useContext } from "react"
|
|
|
|
// Actions
|
|
import { useAuthApi } from "@/shared/useApi"
|
|
import { useRouter } from "next/navigation"
|
|
import { Button } from "@/shared/components/ui/button"
|
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/shared/components/ui/dropdown-menu"
|
|
|
|
// General Info
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/components/ui/card"
|
|
import { Badge } from "@/shared/components/ui/badge"
|
|
import { Separator } from "@/shared/components/ui/separator"
|
|
```
|