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

265 lines
12 KiB
TypeScript
Raw Permalink 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 { useCallback, useEffect, useState } from "react"
import Link from "next/link"
import { useParams } from "next/navigation"
import { ChevronLeft, Pencil, Plus, Trash2 } from "lucide-react"
import { toast } from "sonner"
import { Button } from "@/shared/components/ui/button"
import { Input } from "@/shared/components/ui/input"
import { Textarea } from "@/shared/components/ui/textarea"
import { Switch } from "@/shared/components/ui/switch"
import { useAuthApi } from "@/shared/useApi"
import { confirm } from "@/shared/components/confirm-dialog"
import { prompt } from "@/shared/components/prompt-dialog"
import { TemplateCheckpointEditDialog } from "@/modules/inspections/template-checkpoint-edit-dialog"
import type {
InspectionTemplate,
InspectionTemplateSection,
InspectionTemplateCheckPoint,
InspectionSeverity,
} from "@garage/api"
const SEVERITY_BADGE: Record<InspectionSeverity, { label: string; cls: string }> = {
good: { label: "Good", cls: "bg-emerald-100 text-emerald-800" },
attention: { label: "Attention", cls: "bg-amber-100 text-amber-800" },
critical: { label: "Critical", cls: "bg-rose-100 text-rose-800" },
na: { label: "N/A", cls: "bg-slate-100 text-slate-700" },
not_inspected: { label: "Not inspected", cls: "bg-gray-100 text-gray-700" },
}
const RECORD_TYPE_LABEL: Record<string, string> = {
capture_photo: "Photo",
record_video: "Video",
record_audio: "Audio",
record_conditions: "0100",
wire_frame: "Wireframe",
none: "No media",
}
export default function InspectionTemplateEditorPage() {
const params = useParams<{ id: string }>()
const id = params.id
const api = useAuthApi()
const [template, setTemplate] = useState<InspectionTemplate | null>(null)
const [loading, setLoading] = useState(true)
const [editing, setEditing] = useState<{
section: InspectionTemplateSection
checkpoint: InspectionTemplateCheckPoint | null
} | null>(null)
const load = useCallback(async () => {
setLoading(true)
try {
const res = await api.inspectionTemplates.show(id)
setTemplate(res.data)
} catch (e: any) {
toast.error(e?.message ?? "Failed to load template")
} finally {
setLoading(false)
}
}, [id])
useEffect(() => { load() }, [load])
const updateMeta = async (partial: Partial<InspectionTemplate>) => {
if (!template) return
try {
const res = await api.inspectionTemplates.update(template.id, partial)
setTemplate(res.data)
} catch (e: any) {
toast.error(e?.message ?? "Failed to save")
}
}
const addSection = async () => {
if (!template) return
const name = await prompt({
title: "New section",
label: "Section name",
placeholder: "e.g. Brakes",
confirmLabel: "Add section",
})
if (!name) return
const nextOrder = (template.sections?.length ?? 0) + 1
try {
await api.inspectionTemplates.createSection(template.id, { name, sort_order: nextOrder })
await load()
} catch (e: any) {
toast.error(e?.message ?? "Failed to add section")
}
}
const deleteSection = async (section: InspectionTemplateSection) => {
if (!template) return
const confirmed = await confirm({
title: `Delete section "${section.name}"?`,
description: "This will remove the section and all its checkpoints.",
confirmLabel: "Delete",
variant: "destructive",
})
if (!confirmed) return
try {
await api.inspectionTemplates.destroySection(template.id, section.id)
await load()
} catch (e: any) {
toast.error(e?.message ?? "Failed to delete section")
}
}
const renameSection = async (section: InspectionTemplateSection, name: string) => {
if (!template || name === section.name) return
try {
await api.inspectionTemplates.updateSection(template.id, section.id, { name })
await load()
} catch (e: any) {
toast.error(e?.message ?? "Failed to rename section")
}
}
const deleteCheckpoint = async (section: InspectionTemplateSection, cp: InspectionTemplateCheckPoint, e: React.MouseEvent) => {
e.stopPropagation()
if (!template) return
const confirmed = await confirm({
title: `Delete "${cp.name}"?`,
description: "This checkpoint will be removed from the template.",
confirmLabel: "Delete",
variant: "destructive",
})
if (!confirmed) return
try {
await api.inspectionTemplates.destroyCheckpoint(template.id, section.id, cp.id)
await load()
} catch (e: any) {
toast.error(e?.message ?? "Failed to delete checkpoint")
}
}
if (loading) return <div className="p-6 text-muted-foreground">Loading</div>
if (!template) return <div className="p-6 text-muted-foreground">Template not found</div>
return (
<div className="p-6 space-y-6 max-w-4xl">
<div className="flex items-center gap-2">
<Link href="/settings/inspection-templates">
<Button variant="ghost" size="sm"><ChevronLeft className="size-4 mr-1" /> Back</Button>
</Link>
</div>
<div className="rounded border p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
<div>
<label className="text-xs text-muted-foreground">Name</label>
<Input
defaultValue={template.name}
onBlur={(e) => e.currentTarget.value !== template.name && updateMeta({ name: e.currentTarget.value })}
/>
</div>
<div className="flex items-center gap-3 mt-5">
<Switch
checked={template.is_active}
onCheckedChange={(checked) => updateMeta({ is_active: checked })}
/>
<span className="text-sm">{template.is_active ? "Active" : "Inactive"}</span>
</div>
</div>
<div>
<label className="text-xs text-muted-foreground">Description</label>
<Textarea
defaultValue={template.description ?? ""}
rows={2}
onBlur={(e) => e.currentTarget.value !== (template.description ?? "") && updateMeta({ description: e.currentTarget.value })}
/>
</div>
</div>
<div className="flex items-center justify-between">
<h2 className="text-lg font-medium">Sections & Checkpoints</h2>
<Button onClick={addSection} size="sm"><Plus className="size-4 mr-1" /> Add section</Button>
</div>
<div className="space-y-4">
{(template.sections ?? []).length === 0 && (
<div className="rounded border-dashed border p-6 text-center text-sm text-muted-foreground">
No sections yet. Add one to start building the checklist.
</div>
)}
{(template.sections ?? []).map((section) => (
<div key={section.id} className="rounded border">
<div className="flex items-center gap-2 px-3 py-2 bg-muted/40">
<Input
defaultValue={section.name}
onBlur={(e) => renameSection(section, e.currentTarget.value)}
className="max-w-md h-8"
/>
<span className="text-xs text-muted-foreground">
{(section.check_points ?? []).length} checkpoint{(section.check_points ?? []).length === 1 ? "" : "s"}
</span>
<div className="ml-auto flex items-center gap-1">
<Button size="sm" variant="ghost" onClick={() => setEditing({ section, checkpoint: null })}>
<Plus className="size-4 mr-1" /> Checkpoint
</Button>
<Button size="sm" variant="ghost" onClick={() => deleteSection(section)}>
<Trash2 className="size-4 text-red-600" />
</Button>
</div>
</div>
<div className="divide-y">
{(section.check_points ?? []).length === 0 && (
<div className="p-3 text-sm text-muted-foreground">No checkpoints in this section.</div>
)}
{(section.check_points ?? []).map((cp) => {
const sev = SEVERITY_BADGE[cp.severity_default] ?? SEVERITY_BADGE.not_inspected
return (
<button
type="button"
key={cp.id}
onClick={() => setEditing({ section, checkpoint: cp })}
className="w-full text-left flex items-center gap-3 px-3 py-2 hover:bg-muted/30 transition"
>
<span className="text-xs text-muted-foreground w-6 text-right">
{cp.sort_order}
</span>
<span className="flex-1 min-w-0 truncate text-sm font-medium">
{cp.name}
</span>
<span className="text-xs text-muted-foreground hidden sm:inline-block">
{RECORD_TYPE_LABEL[cp.record_type] ?? cp.record_type}
</span>
<span className={`text-xs rounded-full px-2 py-0.5 ${sev.cls}`}>
{sev.label}
</span>
<Pencil className="size-3.5 text-muted-foreground" />
<button
type="button"
onClick={(e) => deleteCheckpoint(section, cp, e)}
className="text-red-600 hover:text-red-700"
aria-label="Delete checkpoint"
>
<Trash2 className="size-4" />
</button>
</button>
)
})}
</div>
</div>
))}
</div>
{editing && (
<TemplateCheckpointEditDialog
open={!!editing}
onOpenChange={(o) => { if (!o) setEditing(null) }}
templateId={template.id}
sectionId={editing.section.id}
checkpoint={editing.checkpoint}
nextSortOrder={(editing.section.check_points?.length ?? 0) + 1}
onSaved={load}
/>
)}
</div>
)
}