updates
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"use client"
|
||||
|
||||
import { useAuthApi } from "@/shared/useApi"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/components/ui/dropdown-menu"
|
||||
import { Ellipsis, Pencil, Trash2, Play, CheckCircle2 } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
type InspectionActionsProps = {
|
||||
inspectionId: string
|
||||
status?: string
|
||||
onStatusChange?: () => void
|
||||
}
|
||||
|
||||
const STATUS_TRANSITIONS: Record<string, { next: string; label: string; icon: typeof Play }> = {
|
||||
open: { next: "in_progress", label: "Start Inspection", icon: Play },
|
||||
in_progress: { next: "completed", label: "Mark Completed", icon: CheckCircle2 },
|
||||
}
|
||||
|
||||
export function InspectionActions({ inspectionId, status, onStatusChange }: InspectionActionsProps) {
|
||||
const api = useAuthApi()
|
||||
const router = useRouter()
|
||||
|
||||
const handleEdit = () => {
|
||||
router.push(`/sales/inspections/${inspectionId}/edit`)
|
||||
}
|
||||
|
||||
const handleDelete = async () => {
|
||||
const promise = api.inspections.destroy(inspectionId)
|
||||
toast.promise(promise, {
|
||||
loading: "Deleting inspection...",
|
||||
success: "Inspection deleted successfully",
|
||||
error: "Failed to delete inspection",
|
||||
})
|
||||
await promise
|
||||
router.push("/sales/inspections")
|
||||
}
|
||||
|
||||
const handleStatusChange = async (newStatus: string) => {
|
||||
const promise = api.inspections.changeStatus({
|
||||
status: newStatus,
|
||||
} as never)
|
||||
toast.promise(promise, {
|
||||
loading: "Updating status...",
|
||||
success: "Status updated successfully",
|
||||
error: "Failed to update status",
|
||||
})
|
||||
await promise
|
||||
onStatusChange?.()
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
const transition = status ? STATUS_TRANSITIONS[status] : undefined
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
<Ellipsis className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={handleEdit}>
|
||||
<Pencil className="size-4" />
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
{transition && (
|
||||
<DropdownMenuItem onClick={() => handleStatusChange(transition.next)}>
|
||||
<transition.icon className="size-4" />
|
||||
{transition.label}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
|
||||
<Trash2 className="size-4" />
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import { createContext, useContext } from "react"
|
||||
|
||||
type InspectionContextValue = {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
const InspectionContext = createContext<InspectionContextValue | null>(null)
|
||||
|
||||
export function InspectionProvider({
|
||||
inspection,
|
||||
children,
|
||||
}: {
|
||||
inspection: InspectionContextValue
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<InspectionContext.Provider value={inspection}>
|
||||
{children}
|
||||
</InspectionContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useInspection() {
|
||||
return useContext(InspectionContext)
|
||||
}
|
||||
@@ -24,11 +24,11 @@ import {
|
||||
} from "./inspection.schema"
|
||||
import {
|
||||
INSPECTION_ROUTES,
|
||||
CUSTOMER_ROUTES,
|
||||
VEHICLE_ROUTES,
|
||||
DEPARTMENT_ROUTES,
|
||||
EMPLOYEE_ROUTES,
|
||||
} from "@garage/api"
|
||||
import { RhfVehicleSelectField } from "@/modules/vehicles/rhf-vehicle-select-field"
|
||||
import { RhfCustomerSelectField } from "@/modules/customers/rhf-customer-select-field"
|
||||
import { RhfEmployeeSelectField } from "@/modules/employees/rhf-employee-select-field"
|
||||
|
||||
// ── Props ──
|
||||
|
||||
@@ -91,21 +91,6 @@ const mapLookupOption = (item: any) => ({
|
||||
label: item.name ?? item.title ?? String(item.id),
|
||||
})
|
||||
|
||||
const mapCustomerOption = (item: any) => ({
|
||||
value: String(item.id),
|
||||
label: `${item.first_name ?? ""} ${item.last_name ?? ""}`.trim() || String(item.id),
|
||||
})
|
||||
|
||||
const mapVehicleOption = (item: any) => ({
|
||||
value: String(item.id),
|
||||
label: `${item.make ?? ""} ${item.model ?? ""} ${item.year ?? ""}`.trim() || String(item.id),
|
||||
})
|
||||
|
||||
const mapEmployeeOption = (item: any) => ({
|
||||
value: String(item.id),
|
||||
label: `${item.first_name ?? ""} ${item.last_name ?? ""}`.trim() || String(item.id),
|
||||
})
|
||||
|
||||
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
|
||||
|
||||
// ── Component ──
|
||||
@@ -157,24 +142,8 @@ export function InspectionForm({ resourceId, initialData, onSuccess }: Inspectio
|
||||
<RhfTextField name="title" label="Title" placeholder="e.g. Pre-purchase" required />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfAsyncSelectField
|
||||
name="customer"
|
||||
label="Customer"
|
||||
placeholder="Select customer"
|
||||
queryKey={[CUSTOMER_ROUTES.INDEX]}
|
||||
listFn={() => api.customers.list()}
|
||||
mapOption={mapCustomerOption}
|
||||
{...STORE_OBJECT}
|
||||
/>
|
||||
<RhfAsyncSelectField
|
||||
name="vehicle"
|
||||
label="Vehicle"
|
||||
placeholder="Select vehicle"
|
||||
queryKey={[VEHICLE_ROUTES.INDEX]}
|
||||
listFn={() => api.vehicles.list()}
|
||||
mapOption={mapVehicleOption}
|
||||
{...STORE_OBJECT}
|
||||
/>
|
||||
<RhfCustomerSelectField name="customer" />
|
||||
<RhfVehicleSelectField name="vehicle" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
@@ -202,21 +171,13 @@ export function InspectionForm({ resourceId, initialData, onSuccess }: Inspectio
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RhfAsyncSelectField
|
||||
name="employee"
|
||||
label="Employee"
|
||||
placeholder="Select employee"
|
||||
queryKey={[EMPLOYEE_ROUTES.INDEX]}
|
||||
listFn={() => api.employees.list()}
|
||||
mapOption={mapEmployeeOption}
|
||||
{...STORE_OBJECT}
|
||||
/>
|
||||
<RhfEmployeeSelectField name="employee" />
|
||||
|
||||
<RhfTextField name="order_number" label="Order Number" placeholder="e.g. ORD-001" />
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<RhfTextField name="date" label="Date" placeholder="YYYY-MM-DD" type="date" />
|
||||
<RhfTextField name="time" label="Time" placeholder="HH:MM:SS" type="time" />
|
||||
<RhfTextField name="time" label="Time" placeholder="HH:MM:SS" type="time" step={1} />
|
||||
</div>
|
||||
|
||||
<Button type="submit" variant="default" disabled={isPending}>
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import {
|
||||
ClipboardCheck,
|
||||
Calendar,
|
||||
Clock,
|
||||
Hash,
|
||||
FileText,
|
||||
User,
|
||||
Car,
|
||||
Building2,
|
||||
FolderOpen,
|
||||
} from "lucide-react"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/shared/components/ui/card"
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { Separator } from "@/shared/components/ui/separator"
|
||||
|
||||
type InspectionData = {
|
||||
id?: number
|
||||
title?: string
|
||||
customer_id?: number
|
||||
vehicle_id?: number
|
||||
department_id?: number
|
||||
inspection_category_id?: number
|
||||
employee_id?: number
|
||||
order_number?: string
|
||||
date?: string
|
||||
time?: string
|
||||
note?: string
|
||||
status?: string
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
customer?: { id?: number; first_name?: string; last_name?: string }
|
||||
vehicle?: { id?: number; make?: string; model?: string; license_plate?: string }
|
||||
department?: { id?: number; name?: string }
|
||||
inspection_category?: { id?: number; name?: string }
|
||||
employee?: { id?: number; first_name?: string; last_name?: string }
|
||||
}
|
||||
|
||||
type InspectionGeneralInfoProps = {
|
||||
inspection: InspectionData
|
||||
}
|
||||
|
||||
function InfoItem({
|
||||
icon: Icon,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
icon: React.ComponentType<{ className?: string }>
|
||||
label: string
|
||||
value?: string | null
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
||||
<Icon className="size-4" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-xs text-muted-foreground">{label}</span>
|
||||
<span className="text-sm font-medium">
|
||||
{value || <span className="text-muted-foreground">—</span>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
open: "bg-blue-100 text-blue-800",
|
||||
in_progress: "bg-yellow-100 text-yellow-800",
|
||||
completed: "bg-green-100 text-green-800",
|
||||
}
|
||||
|
||||
function formatStatus(status?: string) {
|
||||
if (!status) return "—"
|
||||
return status.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
|
||||
}
|
||||
|
||||
export function InspectionGeneralInfo({ inspection }: InspectionGeneralInfoProps) {
|
||||
const customerName = inspection.customer
|
||||
? `${inspection.customer.first_name ?? ""} ${inspection.customer.last_name ?? ""}`.trim()
|
||||
: undefined
|
||||
|
||||
const vehicleName = inspection.vehicle
|
||||
? `${inspection.vehicle.make ?? ""} ${inspection.vehicle.model ?? ""}`.trim()
|
||||
: undefined
|
||||
|
||||
const employeeName = inspection.employee
|
||||
? `${inspection.employee.first_name ?? ""} ${inspection.employee.last_name ?? ""}`.trim()
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Inspection Details */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ClipboardCheck className="size-4" />
|
||||
Inspection Details
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{inspection.title && (
|
||||
<Badge variant="secondary">{inspection.title}</Badge>
|
||||
)}
|
||||
{inspection.status && (
|
||||
<Badge className={STATUS_COLORS[inspection.status] || ""}>
|
||||
{formatStatus(inspection.status)}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<InfoItem
|
||||
icon={Hash}
|
||||
label="Order Number"
|
||||
value={inspection.order_number}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={FolderOpen}
|
||||
label="Category"
|
||||
value={inspection.inspection_category?.name}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Calendar}
|
||||
label="Date"
|
||||
value={inspection.date ? new Date(inspection.date).toLocaleDateString() : null}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Clock}
|
||||
label="Time"
|
||||
value={inspection.time}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Assignments */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<User className="size-4" />
|
||||
Assignments
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 sm:grid-cols-2">
|
||||
<InfoItem
|
||||
icon={User}
|
||||
label="Customer"
|
||||
value={customerName}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Car}
|
||||
label="Vehicle"
|
||||
value={vehicleName}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={User}
|
||||
label="Employee"
|
||||
value={employeeName}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Building2}
|
||||
label="Department"
|
||||
value={inspection.department?.name}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Notes & Timestamps */}
|
||||
{(inspection.note || inspection.created_at) && (
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FileText className="size-4" />
|
||||
Notes & Timestamps
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<InfoItem
|
||||
icon={Calendar}
|
||||
label="Created At"
|
||||
value={inspection.created_at ? new Date(inspection.created_at).toLocaleString() : null}
|
||||
/>
|
||||
<InfoItem
|
||||
icon={Calendar}
|
||||
label="Updated At"
|
||||
value={inspection.updated_at ? new Date(inspection.updated_at).toLocaleString() : null}
|
||||
/>
|
||||
</div>
|
||||
{inspection.note && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
||||
<FileText className="size-4" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-xs text-muted-foreground">Note</span>
|
||||
<p className="text-sm whitespace-pre-wrap">{inspection.note}</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user