garage-erp/apps/dashboard/modules/inspections/inspection-from-template-form.tsx
humam kerdiah 4bfd8c84a9 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.
2026-05-18 12:08:42 +04:00

263 lines
12 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { toast } from "sonner"
import { ClipboardList, Plus } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
import { FieldGroup } from "@/shared/components/ui/field"
import {
Rhform,
RhfTextField,
RhfTextareaField,
RhfDateField,
RhfTimeField,
} from "@/shared/components/form"
import { useAuthApi } from "@/shared/useApi"
import type { InspectionTemplate } from "@garage/api"
import { RhfCustomerSelectField } from "@/modules/customers/rhf-customer-select-field"
import { RhfVehicleSelectField } from "@/modules/vehicles/rhf-vehicle-select-field"
import { RhfEmployeeSelectField } from "@/modules/employees/rhf-employee-select-field"
import { RhfAsyncSelectField } from "@/shared/components/form"
import { DEPARTMENT_ROUTES } from "@garage/api"
import { useFormContext, useWatch } from "react-hook-form"
import { useEffect as useReactEffect, useRef } from "react"
const relationFieldSchema = z
.object({ value: z.string(), label: z.string() })
.nullable()
const schema = z.object({
customer: relationFieldSchema.refine((v) => !!v?.value, "Customer is required"),
vehicle: relationFieldSchema.refine((v) => !!v?.value, "Vehicle is required"),
department: relationFieldSchema.refine((v) => !!v?.value, "Department is required"),
employee: relationFieldSchema.refine((v) => !!v?.value, "Employee is required"),
title: z.string().min(1, "Title is required").max(100),
order_number: z.string().min(1, "Order number is required").max(100),
date: z.string().min(1, "Date is required"),
time: z.string().min(1, "Time is required"),
odometer: z.coerce.number().int().min(0).optional().or(z.literal("")).transform(v => v === "" ? undefined : v),
note: z.string().optional(),
})
type FormValues = z.infer<typeof schema>
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
const mapLookupOption = (item: any) => ({ value: String(item.id), label: item.name ?? item.title ?? String(item.id) })
// Scope vehicle picker to selected customer
function CustomerScopedVehicleField() {
const { setValue, control, getValues } = useFormContext<FormValues>()
const customer = useWatch({ control, name: "customer" })
const customerId = customer?.value ?? null
const lastRef = useRef<string | null>(customerId)
useReactEffect(() => {
if (lastRef.current !== customerId) {
if (getValues("vehicle")) {
setValue("vehicle", null, { shouldDirty: true, shouldValidate: false })
}
lastRef.current = customerId
}
}, [customerId, setValue, getValues])
return (
<RhfVehicleSelectField
name="vehicle"
customer_id={customerId}
disabled={!customerId}
placeholder={customerId ? "Search this customer's vehicles…" : "Select a customer first"}
/>
)
}
export function InspectionFromTemplateForm({ onSuccess }: { onSuccess?: (inspectionId: number) => void }) {
const api = useAuthApi()
const [step, setStep] = useState<"pick-template" | "fill-details">("pick-template")
const [templates, setTemplates] = useState<InspectionTemplate[]>([])
const [selectedTemplate, setSelectedTemplate] = useState<InspectionTemplate | null>(null)
const [loadingTemplates, setLoadingTemplates] = useState(true)
const form = useForm<FormValues>({
resolver: zodResolver(schema) as any,
defaultValues: {
customer: null,
vehicle: null,
department: null,
employee: null,
title: "",
order_number: "",
date: new Date().toISOString().slice(0, 10),
time: new Date().toTimeString().slice(0, 8),
odometer: undefined,
note: "",
},
})
useEffect(() => {
let cancelled = false
setLoadingTemplates(true)
api.inspectionTemplates.list({ is_active: true }).then((res) => {
if (!cancelled) {
setTemplates(res.data ?? [])
setLoadingTemplates(false)
}
}).catch((e: any) => {
if (!cancelled) {
toast.error(e?.message ?? "Failed to load templates")
setLoadingTemplates(false)
}
})
return () => { cancelled = true }
}, [])
const pickTemplate = (t: InspectionTemplate) => {
setSelectedTemplate(t)
form.setValue("title", t.name)
setStep("fill-details")
}
const handleSubmit = async (values: FormValues) => {
if (!selectedTemplate) return
try {
const res = await api.inspections.createFromTemplate({
template_id: selectedTemplate.id,
title: values.title,
customer_id: values.customer!.value,
vehicle_id: values.vehicle!.value,
department_id: values.department!.value,
employee_id: values.employee!.value,
order_number: values.order_number,
date: values.date,
time: values.time,
odometer: values.odometer as any,
note: values.note || undefined,
})
const created = (res as any)?.data
toast.success("Inspection created with checkpoints from template")
onSuccess?.(created?.id)
} catch (e: any) {
const errors = e?.payload?.errors
const firstError = errors && typeof errors === "object"
? (Object.values(errors)[0] as string[])?.[0]
: null
toast.error(firstError ?? e?.message ?? "Failed to create inspection")
}
}
if (step === "pick-template") {
return (
<div className="space-y-3">
<div>
<h3 className="text-sm font-medium">Choose an inspection template</h3>
<p className="text-xs text-muted-foreground">
The selected template's sections and checkpoints will be cloned onto the new inspection.
</p>
</div>
{loadingTemplates && (
<div className="rounded border-dashed border p-6 text-center text-sm text-muted-foreground">Loading templates</div>
)}
{!loadingTemplates && templates.length === 0 && (
<div className="rounded border-dashed border p-6 text-center text-sm text-muted-foreground">
No active templates. Create one under Settings Inspection Templates.
</div>
)}
{!loadingTemplates && templates.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 max-h-[60vh] overflow-y-auto">
{templates.map((t) => {
const sectionCount = t.sections?.length ?? 0
const cpCount = (t.sections ?? []).reduce((s, sec) => s + (sec.check_points?.length ?? 0), 0)
const empty = cpCount === 0
return (
<button
type="button"
key={t.id}
onClick={() => !empty && pickTemplate(t)}
disabled={empty}
title={empty ? "This template has no checkpoints yet — add some in Settings → Inspection Templates" : undefined}
className={`rounded border p-3 text-left transition ${
empty
? "opacity-50 cursor-not-allowed border-dashed"
: "hover:border-primary hover:bg-primary/5"
}`}
>
<div className="flex items-start gap-2">
<ClipboardList className="size-4 mt-0.5 text-muted-foreground" />
<div className="flex-1 min-w-0">
<div className="font-medium text-sm flex items-center gap-2">
{t.name}
{empty && (
<span className="text-[10px] uppercase tracking-wide bg-amber-100 text-amber-800 px-1.5 py-0.5 rounded">
Empty
</span>
)}
</div>
{t.description && (
<div className="text-xs text-muted-foreground line-clamp-2 mt-0.5">
{t.description}
</div>
)}
<div className="text-xs text-muted-foreground mt-1">
{sectionCount} sections · {cpCount} checkpoints
</div>
</div>
</div>
</button>
)
})}
</div>
)}
</div>
)
}
return (
<Rhform form={form} onSubmit={handleSubmit}>
<div className="flex items-center justify-between mb-3">
<div className="text-sm">
<span className="text-muted-foreground">Template:</span>{" "}
<span className="font-medium">{selectedTemplate?.name}</span>
</div>
<Button type="button" variant="ghost" size="sm" onClick={() => setStep("pick-template")}>
Change template
</Button>
</div>
<FieldGroup>
<RhfTextField name="title" label="Title" placeholder="e.g. Pre-purchase" required />
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfCustomerSelectField name="customer" />
<CustomerScopedVehicleField />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="department"
label="Department"
placeholder="Select department"
queryKey={[DEPARTMENT_ROUTES.INDEX]}
listFn={() => api.departments.list()}
mapOption={mapLookupOption}
{...STORE_OBJECT}
/>
<RhfEmployeeSelectField name="employee" />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<RhfTextField name="order_number" label="Order number" placeholder="e.g. ORD-001" required />
<RhfDateField name="date" label="Date" />
<RhfTimeField name="time" label="Time" withSeconds />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="odometer" label="Odometer" type="number" placeholder="km" />
</div>
<RhfTextareaField name="note" label="Note" placeholder="Internal notes…" />
<Button type="submit" variant="default" disabled={form.formState.isSubmitting}>
<Plus className="size-4" />
{form.formState.isSubmitting ? "Creating…" : "Create inspection"}
</Button>
</FieldGroup>
</Rhform>
)
}