- Implemented TemplateCheckpointEditDialog for creating and editing inspection checkpoints. - Added VendorActions component for managing vendor actions including edit, activate/deactivate, and delete. - Created VendorContext for managing vendor state across components. - Developed VendorGeneralInfo component to display detailed vendor information. - Introduced AedSymbol and Money components for consistent currency representation. - Added PromptDialog for user input prompts throughout the application. - Implemented RelationLink component for unified related-data display in CRUD tables. - Created InspectionTemplatesClient for API interactions related to inspection templates.
214 lines
7.9 KiB
TypeScript
214 lines
7.9 KiB
TypeScript
"use client"
|
|
|
|
import React, { useEffect, useMemo, useRef, useState } from "react"
|
|
import { SearchIcon } from "lucide-react"
|
|
import { Input } from "@/shared/components/ui/input"
|
|
import { Tabs, TabsList, TabsTrigger } from "@/shared/components/ui/tabs"
|
|
import { formatEnum } from "@/shared/utils/formatters"
|
|
import { DataTable, type ActionsColumnOptions, type DataViewProps } from "@/shared/data-view/table-view"
|
|
import { useResourcePage, type UseResourcePageOptions, type ResourceItem, type ResourcePageClient } from "./use-resource-page"
|
|
import type { ColumnDef } from "@tanstack/react-table"
|
|
|
|
export type StatusFilterConfig = {
|
|
/** Allowed status values (typically the matching `<Resource>Status` enum). */
|
|
statuses: readonly string[]
|
|
/** Query-param key forwarded to the API. Defaults to "status". */
|
|
paramKey?: string
|
|
/** Initial selected tab. Defaults to "all". */
|
|
defaultValue?: string
|
|
/** Label for the catch-all tab. Defaults to "All". */
|
|
allLabel?: string
|
|
/** Per-status label transform. Defaults to formatEnum (snake_case → Title Case). */
|
|
formatLabel?: (status: string) => string
|
|
}
|
|
|
|
type LooseColumnDef<TData> = ColumnDef<TData, unknown> | (Omit<ColumnDef<TData, unknown>, "accessorKey"> & { accessorKey: string })
|
|
|
|
export type CrudResourceColumnHelpers<TClient extends ResourcePageClient> = {
|
|
actionsColumn: (options?: Partial<ActionsColumnOptions<ResourceItem<TClient>>>) => ColumnDef<ResourceItem<TClient>, unknown>
|
|
openEdit: (row: ResourceItem<TClient>) => void
|
|
deleteItem: (id: string) => Promise<unknown>
|
|
}
|
|
|
|
export type CrudResourceContext<TClient extends ResourcePageClient> = {
|
|
selectedItem: ResourceItem<TClient> | null
|
|
isDialogOpen: boolean
|
|
dialogResourceId: string | null
|
|
isLoading: boolean
|
|
data: ResourceItem<TClient>[]
|
|
openCreate: () => void
|
|
openEdit: (row: ResourceItem<TClient>) => void
|
|
closeDialog: () => void
|
|
deleteItem: (id: string) => Promise<unknown>
|
|
invalidateQuery: () => void
|
|
}
|
|
|
|
type ReactNodeOrRender<TClient extends ResourcePageClient> =
|
|
| React.ReactNode
|
|
| ((context: CrudResourceContext<TClient>) => React.ReactNode)
|
|
|
|
type ManagedTableProps = "columns" | "data" | "pagination" | "sorting" | "onChange" | "isLoading"
|
|
|
|
export type CrudResourceProps<TClient extends ResourcePageClient> = UseResourcePageOptions<TClient> & {
|
|
columns: LooseColumnDef<ResourceItem<TClient>>[] | ((helpers: CrudResourceColumnHelpers<TClient>) => LooseColumnDef<ResourceItem<TClient>>[])
|
|
onRowClick?: (row: ResourceItem<TClient>) => void
|
|
tableHeader?: ReactNodeOrRender<TClient>
|
|
tableProps?: Omit<Partial<DataViewProps<ResourceItem<TClient>>>, ManagedTableProps>
|
|
render?: (table: React.ReactElement, context: CrudResourceContext<TClient>) => React.ReactElement
|
|
/** Render a built-in search input above the table and forward `search` to the API. */
|
|
searchable?: boolean
|
|
/** Placeholder for the built-in search input. */
|
|
searchPlaceholder?: string
|
|
/** Render a built-in status tab strip above the table and forward the selected status to the API. */
|
|
statusFilter?: StatusFilterConfig
|
|
}
|
|
|
|
export function CrudResource<TClient extends ResourcePageClient>({
|
|
columns: columnsProp,
|
|
routeKey,
|
|
getClient,
|
|
queryOptions,
|
|
paramKey,
|
|
extraParams,
|
|
localState,
|
|
onRowClick,
|
|
tableHeader,
|
|
tableProps,
|
|
render,
|
|
searchable = false,
|
|
searchPlaceholder = "Search...",
|
|
statusFilter,
|
|
}: CrudResourceProps<TClient>) {
|
|
type TItem = ResourceItem<TClient>
|
|
|
|
const [searchInput, setSearchInput] = useState("")
|
|
const [search, setSearch] = useState("")
|
|
const statusParamKey = statusFilter?.paramKey ?? "status"
|
|
const statusAllValue = statusFilter?.defaultValue ?? "all"
|
|
const [statusValue, setStatusValue] = useState<string>(statusAllValue)
|
|
|
|
useEffect(() => {
|
|
const timer = setTimeout(() => setSearch(searchInput.trim()), 300)
|
|
return () => clearTimeout(timer)
|
|
}, [searchInput])
|
|
|
|
const mergedExtraParams = useMemo(() => {
|
|
const base = { ...(extraParams ?? {}) } as Record<string, unknown>
|
|
if (searchable && search) base.search = search
|
|
if (statusFilter && statusValue !== statusAllValue) {
|
|
base[statusParamKey] = statusValue
|
|
}
|
|
return base
|
|
}, [extraParams, search, searchable, statusFilter, statusValue, statusAllValue, statusParamKey])
|
|
|
|
const page = useResourcePage<TClient>({
|
|
routeKey,
|
|
getClient,
|
|
queryOptions,
|
|
paramKey,
|
|
extraParams: mergedExtraParams,
|
|
localState,
|
|
})
|
|
|
|
const prevSearchRef = useRef(search)
|
|
const prevStatusRef = useRef(statusValue)
|
|
const setParamsRef = useRef(page.setParams)
|
|
setParamsRef.current = page.setParams
|
|
useEffect(() => {
|
|
if (prevSearchRef.current !== search) {
|
|
prevSearchRef.current = search
|
|
setParamsRef.current({ page: 1 })
|
|
}
|
|
}, [search])
|
|
useEffect(() => {
|
|
if (prevStatusRef.current !== statusValue) {
|
|
prevStatusRef.current = statusValue
|
|
setParamsRef.current({ page: 1 })
|
|
}
|
|
}, [statusValue])
|
|
|
|
const columns = typeof columnsProp === "function"
|
|
? columnsProp({
|
|
actionsColumn: page.actionsColumn,
|
|
openEdit: page.openEdit,
|
|
deleteItem: page.deleteItem,
|
|
})
|
|
: columnsProp
|
|
|
|
type ListResponse = { data?: TItem[] }
|
|
const responseData = page.data as ListResponse | undefined
|
|
const items = (responseData?.data ?? []) as TItem[]
|
|
|
|
const context: CrudResourceContext<TClient> = {
|
|
selectedItem: page.selectedItem,
|
|
isDialogOpen: page.isDialogOpen,
|
|
dialogResourceId: page.dialogResourceId,
|
|
isLoading: page.isLoading,
|
|
data: items,
|
|
openCreate: page.openCreate,
|
|
openEdit: page.openEdit,
|
|
closeDialog: page.closeDialog,
|
|
deleteItem: page.deleteItem,
|
|
invalidateQuery: () => page.invalidateQuery(),
|
|
}
|
|
|
|
const searchInputEl = searchable ? (
|
|
<div className="relative w-64">
|
|
<SearchIcon className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder={searchPlaceholder}
|
|
value={searchInput}
|
|
onChange={(e) => setSearchInput(e.target.value)}
|
|
className="pl-8"
|
|
/>
|
|
</div>
|
|
) : null
|
|
|
|
const statusTabsEl = statusFilter ? (
|
|
<Tabs value={statusValue} onValueChange={setStatusValue}>
|
|
<TabsList variant="line">
|
|
<TabsTrigger value={statusAllValue}>{statusFilter.allLabel ?? "All"}</TabsTrigger>
|
|
{statusFilter.statuses.map((status) => (
|
|
<TabsTrigger key={status} value={status}>
|
|
{(statusFilter.formatLabel ?? formatEnum)(status)}
|
|
</TabsTrigger>
|
|
))}
|
|
</TabsList>
|
|
</Tabs>
|
|
) : null
|
|
|
|
const resolvedTableHeader = tableHeader
|
|
? (typeof tableHeader === "function" ? tableHeader(context) : tableHeader)
|
|
: null
|
|
|
|
const leftHeader = statusTabsEl ?? resolvedTableHeader
|
|
const extraHeaderAbove = statusTabsEl && resolvedTableHeader ? resolvedTableHeader : null
|
|
|
|
const headerRow = (searchInputEl || leftHeader) ? (
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<div className="flex-1 min-w-0">{leftHeader}</div>
|
|
{searchInputEl}
|
|
</div>
|
|
) : null
|
|
|
|
const table = (
|
|
<>
|
|
{extraHeaderAbove}
|
|
{headerRow}
|
|
<DataTable
|
|
{...tableProps}
|
|
columns={columns as ColumnDef<TItem, any>[]}
|
|
data={items}
|
|
pagination={page.pagination}
|
|
sorting={page.sorting}
|
|
onChange={page.handleChange}
|
|
isLoading={page.isLoading}
|
|
onRowClick={onRowClick ?? tableProps?.onRowClick}
|
|
/>
|
|
</>
|
|
)
|
|
|
|
if (render) return render(table, context)
|
|
return table
|
|
}
|