garage-erp/apps/dashboard/modules/inspections/template-checkpoint-edit-dialog.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

220 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/components/ui/dialog"
import { Button } from "@/shared/components/ui/button"
import { Input } from "@/shared/components/ui/input"
import { Textarea } from "@/shared/components/ui/textarea"
import { Label } from "@/shared/components/ui/label"
import { useAuthApi } from "@/shared/useApi"
import type {
InspectionSeverity,
InspectionTemplateCheckPoint,
InspectionTemplateCheckPointRecordType,
} from "@garage/api"
const RECORD_TYPE_OPTIONS: { value: InspectionTemplateCheckPointRecordType; label: string }[] = [
{ value: "none", label: "No media" },
{ value: "capture_photo", label: "Capture photo" },
{ value: "record_video", label: "Record video" },
{ value: "record_audio", label: "Record audio" },
{ value: "record_conditions", label: "Condition rating (0100)" },
{ value: "wire_frame", label: "Wireframe diagram" },
]
const SEVERITY_OPTIONS: { value: InspectionSeverity; label: string; cls: string }[] = [
{ value: "not_inspected", label: "Not inspected", cls: "bg-gray-100 text-gray-700" },
{ value: "good", label: "Good", cls: "bg-emerald-100 text-emerald-800" },
{ value: "attention", label: "Needs attention", cls: "bg-amber-100 text-amber-800" },
{ value: "critical", label: "Critical", cls: "bg-rose-100 text-rose-800" },
{ value: "na", label: "Not applicable", cls: "bg-slate-100 text-slate-700" },
]
export type CheckpointDraft = {
name: string
description: string
record_type: InspectionTemplateCheckPointRecordType
severity_default: InspectionSeverity
sort_order: number
}
export function TemplateCheckpointEditDialog({
open,
onOpenChange,
templateId,
sectionId,
checkpoint,
nextSortOrder,
onSaved,
}: {
open: boolean
onOpenChange: (open: boolean) => void
templateId: number
sectionId: number
/** When set, edit this checkpoint. When null, create new. */
checkpoint: InspectionTemplateCheckPoint | null
nextSortOrder?: number
onSaved: () => void
}) {
const api = useAuthApi()
const isEdit = !!checkpoint
const [name, setName] = useState("")
const [description, setDescription] = useState("")
const [recordType, setRecordType] = useState<InspectionTemplateCheckPointRecordType>("capture_photo")
const [severity, setSeverity] = useState<InspectionSeverity>("not_inspected")
const [sortOrder, setSortOrder] = useState<number>(1)
const [saving, setSaving] = useState(false)
useEffect(() => {
if (!open) return
if (checkpoint) {
setName(checkpoint.name)
setDescription(checkpoint.description ?? "")
setRecordType(checkpoint.record_type)
setSeverity(checkpoint.severity_default)
setSortOrder(checkpoint.sort_order)
} else {
setName("")
setDescription("")
setRecordType("capture_photo")
setSeverity("not_inspected")
setSortOrder(nextSortOrder ?? 1)
}
}, [open, checkpoint, nextSortOrder])
const submit = async () => {
if (!name.trim()) {
toast.error("Name is required")
return
}
setSaving(true)
try {
const payload = {
name: name.trim(),
description: description.trim() || undefined,
record_type: recordType,
severity_default: severity,
sort_order: sortOrder,
}
if (isEdit && checkpoint) {
await api.inspectionTemplates.updateCheckpoint(templateId, sectionId, checkpoint.id, payload)
toast.success("Checkpoint updated")
} else {
await api.inspectionTemplates.createCheckpoint(templateId, sectionId, payload)
toast.success("Checkpoint added")
}
onSaved()
onOpenChange(false)
} 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 ?? (isEdit ? "Failed to update checkpoint" : "Failed to add checkpoint"))
} finally {
setSaving(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>{isEdit ? "Edit checkpoint" : "Add checkpoint"}</DialogTitle>
<DialogDescription>
Define what the technician will check and how the finding is recorded.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<div className="grid gap-1.5">
<Label htmlFor="cp-name">Name</Label>
<Input
id="cp-name"
autoFocus
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g. Front-left brake pad thickness"
/>
</div>
<div className="grid gap-1.5">
<Label htmlFor="cp-description">Description (optional)</Label>
<Textarea
id="cp-description"
value={description}
rows={2}
onChange={(e) => setDescription(e.target.value)}
placeholder="Brief guidance for the technician"
/>
</div>
<div className="grid gap-1.5">
<Label>How it's recorded</Label>
<div className="grid grid-cols-2 gap-1.5">
{RECORD_TYPE_OPTIONS.map((opt) => (
<button
type="button"
key={opt.value}
onClick={() => setRecordType(opt.value)}
className={`rounded border px-2 py-1.5 text-xs text-left transition ${
recordType === opt.value ? "border-primary bg-primary/5 text-foreground" : "text-muted-foreground hover:border-foreground/30"
}`}
>
{opt.label}
</button>
))}
</div>
</div>
<div className="grid gap-1.5">
<Label>Default severity (when not yet inspected)</Label>
<div className="flex flex-wrap gap-1.5">
{SEVERITY_OPTIONS.map((opt) => (
<button
type="button"
key={opt.value}
onClick={() => setSeverity(opt.value)}
className={`rounded-full px-3 py-1 text-xs font-medium transition border ${
severity === opt.value ? "border-primary ring-1 ring-primary " + opt.cls : opt.cls + " border-transparent opacity-70 hover:opacity-100"
}`}
>
{opt.label}
</button>
))}
</div>
</div>
<div className="grid gap-1.5 max-w-[160px]">
<Label htmlFor="cp-order">Sort order</Label>
<Input
id="cp-order"
type="number"
min={0}
value={sortOrder}
onChange={(e) => setSortOrder(parseInt(e.target.value || "0", 10))}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={saving}>Cancel</Button>
<Button onClick={submit} disabled={saving || !name.trim()}>
{saving ? "Saving…" : isEdit ? "Save changes" : "Add checkpoint"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}