- 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.
158 lines
6.8 KiB
TypeScript
158 lines
6.8 KiB
TypeScript
"use client"
|
|
|
|
import { useEffect, useState } from "react"
|
|
import Link from "next/link"
|
|
import { Copy, 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 { useAuthApi } from "@/shared/useApi"
|
|
import { confirm } from "@/shared/components/confirm-dialog"
|
|
import type { InspectionTemplate } from "@garage/api"
|
|
|
|
export default function InspectionTemplatesPage() {
|
|
const api = useAuthApi()
|
|
const [items, setItems] = useState<InspectionTemplate[]>([])
|
|
const [search, setSearch] = useState("")
|
|
const [loading, setLoading] = useState(true)
|
|
const [creating, setCreating] = useState(false)
|
|
const [newName, setNewName] = useState("")
|
|
|
|
const load = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const res = await api.inspectionTemplates.list(search ? { search } : undefined)
|
|
setItems(res.data ?? [])
|
|
} catch (e: any) {
|
|
toast.error(e?.message ?? "Failed to load templates")
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
load()
|
|
}, [])
|
|
|
|
const handleCreate = async () => {
|
|
if (!newName.trim()) return
|
|
try {
|
|
await api.inspectionTemplates.create({ name: newName.trim(), is_active: true })
|
|
setNewName("")
|
|
setCreating(false)
|
|
toast.success("Template created")
|
|
load()
|
|
} catch (e: any) {
|
|
toast.error(e?.message ?? "Failed to create template")
|
|
}
|
|
}
|
|
|
|
const handleDuplicate = async (id: number) => {
|
|
try {
|
|
await api.inspectionTemplates.duplicate(id)
|
|
toast.success("Template duplicated")
|
|
load()
|
|
} catch (e: any) {
|
|
toast.error(e?.message ?? "Failed to duplicate template")
|
|
}
|
|
}
|
|
|
|
const handleDelete = async (id: number, name: string) => {
|
|
const confirmed = await confirm({
|
|
title: `Delete "${name}"?`,
|
|
description: "This will remove the template and all its sections / checkpoints.",
|
|
confirmLabel: "Delete",
|
|
variant: "destructive",
|
|
})
|
|
if (!confirmed) return
|
|
try {
|
|
await api.inspectionTemplates.destroy(id)
|
|
toast.success("Template deleted")
|
|
load()
|
|
} catch (e: any) {
|
|
toast.error(e?.message ?? "Failed to delete template")
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 space-y-4">
|
|
<div className="flex items-center justify-between gap-2">
|
|
<h1 className="text-2xl font-semibold">Inspection Templates</h1>
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
placeholder="Search…"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && load()}
|
|
className="w-56"
|
|
/>
|
|
<Button onClick={() => setCreating((c) => !c)}>
|
|
<Plus className="size-4 mr-1" /> New template
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{creating && (
|
|
<div className="flex items-center gap-2 rounded border p-3">
|
|
<Input
|
|
autoFocus
|
|
placeholder="Template name (e.g. Annual Safety)"
|
|
value={newName}
|
|
onChange={(e) => setNewName(e.target.value)}
|
|
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
|
|
className="max-w-md"
|
|
/>
|
|
<Button onClick={handleCreate}>Create</Button>
|
|
<Button variant="ghost" onClick={() => { setCreating(false); setNewName("") }}>Cancel</Button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="rounded border">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-muted">
|
|
<tr>
|
|
<th className="text-left p-3">Name</th>
|
|
<th className="text-left p-3">Sections</th>
|
|
<th className="text-left p-3">Checkpoints</th>
|
|
<th className="text-left p-3">Status</th>
|
|
<th className="text-right p-3">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{loading && (
|
|
<tr><td colSpan={5} className="p-6 text-center text-muted-foreground">Loading…</td></tr>
|
|
)}
|
|
{!loading && items.length === 0 && (
|
|
<tr><td colSpan={5} className="p-6 text-center text-muted-foreground">No templates yet</td></tr>
|
|
)}
|
|
{!loading && items.map((t) => {
|
|
const sectionCount = t.sections?.length ?? 0
|
|
const cpCount = (t.sections ?? []).reduce((sum, s) => sum + (s.check_points?.length ?? 0), 0)
|
|
return (
|
|
<tr key={t.id} className="border-t hover:bg-muted/40">
|
|
<td className="p-3 font-medium">{t.name}</td>
|
|
<td className="p-3">{sectionCount}</td>
|
|
<td className="p-3">{cpCount}</td>
|
|
<td className="p-3">
|
|
<span className={`inline-block rounded-full px-2 py-0.5 text-xs ${t.is_active ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-600"}`}>
|
|
{t.is_active ? "Active" : "Inactive"}
|
|
</span>
|
|
</td>
|
|
<td className="p-3 text-right space-x-1">
|
|
<Link href={`/settings/inspection-templates/${t.id}`}>
|
|
<Button size="sm" variant="ghost"><Pencil className="size-4" /></Button>
|
|
</Link>
|
|
<Button size="sm" variant="ghost" onClick={() => handleDuplicate(t.id)}><Copy className="size-4" /></Button>
|
|
<Button size="sm" variant="ghost" onClick={() => handleDelete(t.id, t.name)}><Trash2 className="size-4 text-red-600" /></Button>
|
|
</td>
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|