feat: add template checkpoint edit dialog and vendor management components
- 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.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
import { getServerApi } from '@garage/api/server'
|
||||
import { InspectionGeneralInfo } from '@/modules/inspections/inspection-general-info'
|
||||
import { InspectionDetailHeader } from '@/modules/inspections/inspection-detail-header'
|
||||
import DashboardPage from '@/base/components/layout/dashboard/dashboard-page'
|
||||
|
||||
export default async function page(props: { params: Promise<{ id: string }> }) {
|
||||
@@ -11,8 +12,11 @@ export default async function page(props: { params: Promise<{ id: string }> }) {
|
||||
return <div className="text-muted-foreground">Inspection not found.</div>
|
||||
}
|
||||
|
||||
const data = inspection.data as { title?: string }
|
||||
|
||||
return (
|
||||
<DashboardPage header={null}>
|
||||
<InspectionDetailHeader inspectionId={id} title={data.title} />
|
||||
<InspectionGeneralInfo inspection={inspection.data} />
|
||||
</DashboardPage>
|
||||
)
|
||||
|
||||
@@ -1,46 +1,125 @@
|
||||
"use client"
|
||||
|
||||
import { useState } 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 { InspectionForm } from "@/modules/inspections/inspection-form"
|
||||
import { INSPECTION_ROUTES } from "@garage/api"
|
||||
import { InspectionFromTemplateForm } from "@/modules/inspections/inspection-from-template-form"
|
||||
import { InspectionRowActions } from "@/modules/inspections/inspection-row-actions"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog"
|
||||
import { ScrollArea } from "@/shared/components/ui/scroll-area"
|
||||
import { INSPECTION_ROUTES, InspectionStatus } from "@garage/api"
|
||||
import type { InspectionsClient } from "@garage/api"
|
||||
import Link from "next/link"
|
||||
import { Car, ClipboardList, ListChecks, UserIcon } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { RelationLink } from "@/shared/components/relation-link"
|
||||
import { getFullName } from "@/shared/utils/getFullName"
|
||||
import { getVehicleLabel } from "@/modules/vehicles/utils/getVehicleLabel"
|
||||
|
||||
const STATUS_BADGE_CLASS: Record<string, string> = {
|
||||
in_progress: "bg-amber-100 text-amber-800",
|
||||
completed: "bg-emerald-100 text-emerald-800",
|
||||
cancelled: "bg-rose-100 text-rose-800",
|
||||
}
|
||||
|
||||
const STATUS_LABEL: Record<string, string> = {
|
||||
in_progress: "In Progress",
|
||||
completed: "Completed",
|
||||
cancelled: "Cancelled",
|
||||
}
|
||||
|
||||
export default function InspectionsPage() {
|
||||
const router = useRouter()
|
||||
const [fromTemplateOpen, setFromTemplateOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<ResourcePage<InspectionsClient>
|
||||
pageTitle="Inspections"
|
||||
routeKey={INSPECTION_ROUTES.INDEX}
|
||||
searchable
|
||||
searchPlaceholder="Search inspections..."
|
||||
statusFilter={{ statuses: InspectionStatus }}
|
||||
getClient={(api) => api.inspections}
|
||||
onRowClick={(row) => router.push(`/sales/inspections/${(row as any).id}`)}
|
||||
headerProps={({ selectedItem, invalidateQuery }) => ({
|
||||
actions: (
|
||||
<FormDialog title="Inspection">
|
||||
{(resourceId) => (
|
||||
<InspectionForm
|
||||
resourceId={resourceId}
|
||||
initialData={selectedItem}
|
||||
onSuccess={invalidateQuery}
|
||||
/>
|
||||
)}
|
||||
</FormDialog>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<Link href="/settings/inspection-templates">
|
||||
<ListChecks className="size-4" />
|
||||
Manage templates
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="sm" variant="default" onClick={() => setFromTemplateOpen(true)}>
|
||||
<ClipboardList className="size-4" />
|
||||
New from template
|
||||
</Button>
|
||||
<FormDialog title="Inspection" classNames={{ trigger: "" }}>
|
||||
{(resourceId) => (
|
||||
<InspectionForm
|
||||
resourceId={resourceId}
|
||||
initialData={selectedItem}
|
||||
onSuccess={invalidateQuery}
|
||||
/>
|
||||
)}
|
||||
</FormDialog>
|
||||
<Dialog open={fromTemplateOpen} onOpenChange={setFromTemplateOpen}>
|
||||
<DialogContent className="min-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
New inspection from template
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<ScrollArea className="max-h-[80vh] px-4">
|
||||
<InspectionFromTemplateForm
|
||||
onSuccess={(id) => {
|
||||
setFromTemplateOpen(false)
|
||||
invalidateQuery()
|
||||
if (id) router.push(`/sales/inspections/${id}/checkpoints`)
|
||||
}}
|
||||
/>
|
||||
</ScrollArea>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
),
|
||||
})}
|
||||
columns={({ actionsColumn }) => [
|
||||
columns={({ openEdit, deleteItem }) => [
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Title" />,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original as any
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{r.title ?? "—"}</span>
|
||||
{r.order_number && (
|
||||
<span className="text-xs text-muted-foreground">{r.order_number}</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "customer",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
|
||||
cell: ({ row }) => {
|
||||
const c = (row.original as any).customer
|
||||
return c ? `${c.first_name ?? ""} ${c.last_name ?? ""}`.trim() : "—"
|
||||
return (
|
||||
<RelationLink
|
||||
href={c?.id ? `/sales/customers/${c.id}` : null}
|
||||
icon={UserIcon}
|
||||
label={getFullName(c)}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -48,27 +127,51 @@ export default function InspectionsPage() {
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Vehicle" />,
|
||||
cell: ({ row }) => {
|
||||
const v = (row.original as any).vehicle
|
||||
return v ? `${v.make ?? ""} ${v.model ?? ""}`.trim() : "—"
|
||||
return (
|
||||
<RelationLink
|
||||
href={v?.id ? `/sales/vehicles/${v.id}` : null}
|
||||
icon={Car}
|
||||
label={getVehicleLabel(v as any)}
|
||||
meta={v?.license_plate}
|
||||
/>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "inspection_category",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Category" />,
|
||||
cell: ({ row }) => (row.original as any).inspection_category?.name ?? "—",
|
||||
cell: ({ row }) => {
|
||||
const ic = (row.original as any).inspection_category
|
||||
return ic?.inspection_name ?? ic?.name ?? "—"
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "status",
|
||||
header: ({ column }) => <ColumnHeader column={column} title="Status" />,
|
||||
cell: ({ row }) => {
|
||||
const status = (row.original as any).status
|
||||
const status = (row.original as any).status as string | undefined
|
||||
if (!status) return "—"
|
||||
const cls = STATUS_BADGE_CLASS[status] ?? "bg-gray-100 text-gray-700"
|
||||
return (
|
||||
<span className={status === "completed" ? "text-green-600" : "text-yellow-600"}>
|
||||
{status ?? "—"}
|
||||
<span className={`inline-flex rounded-full px-2 py-0.5 text-xs font-medium ${cls}`}>
|
||||
{STATUS_LABEL[status] ?? status}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
},
|
||||
actionsColumn(),
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="sr-only">Actions</span>,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => (
|
||||
<InspectionRowActions
|
||||
inspection={row.original as any}
|
||||
onEdit={() => openEdit(row.original as any)}
|
||||
onDelete={() => deleteItem(String((row.original as any).id))}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user