This commit is contained in:
2026-04-01 15:34:50 +03:00
parent 13b56d4960
commit 5f3d208158
120 changed files with 36338 additions and 21265 deletions
@@ -0,0 +1,55 @@
"use client"
import { z } from "zod"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { Plus } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
import { FieldGroup } from "@/shared/components/ui/field"
import { Rhform, RhfTextField, type InlineCreateFormProps } from "@/shared/components/form"
import { toast } from "sonner"
import { useAuthApi } from "@/shared/useApi"
const schema = z.object({
title: z.string().min(1, "Title is required"),
})
type FormValues = z.infer<typeof schema>
export function DocumentTypeInlineForm({ onSuccess }: InlineCreateFormProps) {
const api = useAuthApi()
const form = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: { title: "" },
})
const handleSubmit = async (values: FormValues) => {
try {
const result = await api.vehicleDocuments.createDocumentType({ title: values.title })
toast.success("Document type created")
form.reset()
const item = (result as any)?.data ?? result
onSuccess({ value: String(item.id), label: item.title ?? item.name ?? String(item.id) })
} catch {
toast.error("Failed to create document type")
}
}
return (
<Rhform form={form} onSubmit={handleSubmit}>
<FieldGroup>
<RhfTextField
name="title"
label="Title"
placeholder="e.g. Registration Certificate"
required
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
<Plus />
{form.formState.isSubmitting ? "Creating..." : "Create Document Type"}
</Button>
</FieldGroup>
</Rhform>
)
}
@@ -0,0 +1,119 @@
"use client"
import { AlertTriangle, Plus, Save } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
import { Alert, AlertTitle } from "@/shared/components/ui/alert"
import { FieldGroup } from "@/shared/components/ui/field"
import { Rhform, RhfTextField } from "@/shared/components/form"
import { toast } from "sonner"
import { useAuthApi } from "@/shared/useApi"
import { useResourceForm } from "@/shared/hooks/use-resource-form"
import { useFormMutation } from "@/shared/hooks/use-form-mutation"
import { mileageFormSchema, type MileageFormValues } from "./mileage.schema"
// ── Props ──
export type MileageFormProps = {
vehicleId: string
resourceId?: string | null
initialData?: unknown
onSuccess?: () => void
}
// ── Default values ──
const DEFAULT_VALUES: MileageFormValues = {
mileage: 0,
date: "",
time: "",
}
// ── Mapping helpers ──
function mapToFormValues(data: unknown): MileageFormValues {
const d = (data as any)?.data ?? data ?? {}
return {
mileage: d.mileage ?? 0,
date: d.date || "",
time: d.time || "",
}
}
// ── Component ──
export function MileageForm({ vehicleId, resourceId, initialData, onSuccess }: MileageFormProps) {
const api = useAuthApi()
const { form, isEditing } = useResourceForm<MileageFormValues, any>({
schema: mileageFormSchema,
defaultValues: DEFAULT_VALUES,
resourceId,
initialData,
mapToFormValues,
})
const { mutate, error, isPending } = useFormMutation(form, {
mutationFn: (values: MileageFormValues) => {
const payload = { mileage: values.mileage, date: values.date, time: values.time }
const promise = isEditing && resourceId
? api.vehicleDocuments.updateMileage(resourceId, payload as any)
: api.vehicleDocuments.createMileage({ vehicle_id: Number(vehicleId), ...payload } as any)
toast.promise(promise, {
loading: isEditing ? "Updating mileage..." : "Adding mileage...",
success: isEditing ? "Mileage updated successfully" : "Mileage added successfully",
error: isEditing ? "Failed to update mileage" : "Failed to add mileage",
})
return promise
},
onSuccess: () => {
form.reset()
onSuccess?.()
},
})
return (
<Rhform form={form} onSubmit={(values) => mutate(values)}>
{error && (
<Alert variant="destructive" className="mb-4">
<AlertTriangle className="me-2 h-4 w-4" />
<AlertTitle>
{isEditing ? "Failed to update mileage" : "Failed to add mileage"}
</AlertTitle>
{error.message}
</Alert>
)}
<FieldGroup>
<RhfTextField
name="mileage"
label="Mileage"
placeholder="e.g. 50000"
type="number"
required
/>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField
name="date"
label="Date"
type="date"
required
/>
<RhfTextField
name="time"
label="Time"
type="time"
required
/>
</div>
<Button type="submit" disabled={isPending} className="w-full">
{isPending ? null : isEditing ? <Save /> : <Plus />}
{isPending ? "Saving..." : isEditing ? "Update Mileage" : "Add Mileage"}
</Button>
</FieldGroup>
</Rhform>
)
}
@@ -0,0 +1,9 @@
import { z } from "zod"
export const mileageFormSchema = z.object({
mileage: z.coerce.number({ message: "Mileage is required" }).min(0, "Mileage must be 0 or greater"),
date: z.string().min(1, "Date is required"),
time: z.string().min(1, "Time is required"),
})
export type MileageFormValues = z.infer<typeof mileageFormSchema>
@@ -0,0 +1,50 @@
"use client"
import { useAuthApi } from "@/shared/useApi"
import { useRouter } from "next/navigation"
import { Button } from "@/shared/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/shared/components/ui/dropdown-menu"
import { Ellipsis, Pencil, Trash2 } from "lucide-react"
type VehicleActionsProps = {
vehicleId: string
}
export function VehicleActions({ vehicleId }: VehicleActionsProps) {
const api = useAuthApi()
const router = useRouter()
const handleEdit = () => {
router.push(`/sales/vehicles/${vehicleId}/edit`)
}
const handleDelete = async () => {
await api.vehicles.destroy(vehicleId)
router.push("/sales/vehicles")
}
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>
<DropdownMenuItem variant="destructive" onClick={handleDelete}>
<Trash2 className="size-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -0,0 +1,18 @@
"use client"
import { createContext, useContext } from "react"
type VehicleContextValue = {
id: string
label: string
}
const VehicleContext = createContext<VehicleContextValue | null>(null)
export function VehicleProvider({ vehicle, children }: { vehicle: VehicleContextValue; children: React.ReactNode }) {
return <VehicleContext.Provider value={vehicle}>{children}</VehicleContext.Provider>
}
export function useVehicle() {
return useContext(VehicleContext)
}
@@ -0,0 +1,144 @@
"use client"
import { AlertTriangle, Plus, Save } from "lucide-react"
import { Button } from "@/shared/components/ui/button"
import { Alert, AlertTitle } from "@/shared/components/ui/alert"
import { FieldGroup } from "@/shared/components/ui/field"
import {
Rhform,
RhfTextareaField,
RhfAsyncSelectField,
RhfDocumentField,
} from "@/shared/components/form"
import { DocumentTypeInlineForm } from "./inline-forms/document-type-inline-form"
import { toast } from "sonner"
import { useAuthApi } from "@/shared/useApi"
import { useResourceForm } from "@/shared/hooks/use-resource-form"
import { useFormMutation } from "@/shared/hooks/use-form-mutation"
import { toRelation, toId } from "@/shared/lib/utils"
import { vehicleDocumentFormSchema, type VehicleDocumentFormValues } from "./vehicle-document.schema"
// ── Props ──
export type VehicleDocumentFormProps = {
vehicleId: string
resourceId?: string | null
initialData?: unknown
onSuccess?: () => void
}
// ── Default values ──
const DEFAULT_VALUES: VehicleDocumentFormValues = {
document_type: null,
file: null,
note: "",
}
// ── Mapping helpers ──
const mapLookupOption = (item: any) => ({
value: String(item.id),
label: item.name ?? item.title ?? String(item.id),
})
const STORE_OBJECT = { getOptionValue: (o: any) => o, getOptionLabel: (o: any) => o.label }
function mapToFormValues(data: unknown): VehicleDocumentFormValues {
const d = (data as any)?.data ?? data ?? {}
return {
document_type: toRelation(d.document_type_id, d.document_type?.name ?? d.document_type?.title),
file: null,
note: d.note || "",
}
}
function mapToPayload(values: VehicleDocumentFormValues, vehicleId: string) {
return {
vehicle_id: Number(vehicleId),
document_type_id: toId(values.document_type),
file: values.file instanceof File ? values.file : undefined,
note: values.note || undefined,
}
}
// ── Component ──
export function VehicleDocumentForm({ vehicleId, resourceId, initialData, onSuccess }: VehicleDocumentFormProps) {
const api = useAuthApi()
const { form, isEditing } = useResourceForm<VehicleDocumentFormValues, any>({
schema: vehicleDocumentFormSchema,
defaultValues: DEFAULT_VALUES,
resourceId,
initialData,
mapToFormValues,
})
const { mutate, error, isPending } = useFormMutation(form, {
mutationFn: (values: VehicleDocumentFormValues) => {
const payload = mapToPayload(values, vehicleId)
const promise = isEditing && resourceId
? api.vehicleDocuments.updateDocument(resourceId, payload)
: api.vehicleDocuments.createDocument(payload)
toast.promise(promise, {
loading: isEditing ? "Updating document..." : "Uploading document...",
success: isEditing ? "Document updated successfully" : "Document uploaded successfully",
error: isEditing ? "Failed to update document" : "Failed to upload document",
})
return promise
},
onSuccess: () => {
form.reset()
onSuccess?.()
},
})
return (
<Rhform form={form} onSubmit={(values) => mutate(values)}>
{error && (
<Alert variant="destructive" className="mb-4">
<AlertTriangle className="me-2 h-4 w-4" />
<AlertTitle>
{isEditing ? "Failed to update document" : "Failed to upload document"}
</AlertTitle>
{error.message}
</Alert>
)}
<FieldGroup>
<RhfAsyncSelectField
name="document_type"
label="Document Type"
placeholder="Select document type"
queryKey={["document-types"]}
listFn={() => api.vehicleDocuments.listDocumentTypes()}
mapOption={mapLookupOption}
createForm={(props) => <DocumentTypeInlineForm {...props} />}
createLabel="Document Type"
{...STORE_OBJECT}
/>
<RhfDocumentField
name="file"
label="Document File"
required
/>
<RhfTextareaField
name="note"
label="Note"
placeholder="Optional notes about this document"
/>
<Button type="submit" disabled={isPending} className="w-full">
{isPending ? null : isEditing ? <Save /> : <Plus />}
{isPending ? "Saving..." : isEditing ? "Update Document" : "Upload Document"}
</Button>
</FieldGroup>
</Rhform>
)
}
@@ -0,0 +1,11 @@
import { z } from "zod"
const relationFieldSchema = z.object({ value: z.string(), label: z.string() }).nullable()
export const vehicleDocumentFormSchema = z.object({
document_type: relationFieldSchema,
file: z.instanceof(File, { message: "File is required" }).nullable(),
note: z.string().optional(),
})
export type VehicleDocumentFormValues = z.infer<typeof vehicleDocumentFormSchema>
@@ -10,6 +10,7 @@ import {
RhfTextField,
RhfTextareaField,
RhfAsyncSelectField,
RhfImageField,
} from "@/shared/components/form"
import { ShopTypeInlineForm } from "./inline-forms/shop-type-inline-form"
import { BodyTypeInlineForm } from "./inline-forms/body-type-inline-form"
@@ -52,6 +53,7 @@ const DEFAULT_VALUES: VehicleFormValues = {
mileage: "",
owners_number: "",
note: "",
image: null,
}
// ── Mapping helpers ──
@@ -83,10 +85,11 @@ function mapToFormValues(data: unknown): VehicleFormValues {
mileage: d.mileage || "",
owners_number: d.owners_number || "",
note: d.note || "",
image: null,
}
}
function mapCreatePayload(values: VehicleFormValues) {
function mapToPayload(values: VehicleFormValues) {
return {
shop_type_id: toId(values.shop_type),
vehicle_body_type_id: toId(values.vehicle_body_type),
@@ -104,13 +107,7 @@ function mapCreatePayload(values: VehicleFormValues) {
mileage: values.mileage || undefined,
owners_number: values.owners_number || undefined,
note: values.note || undefined,
}
}
function mapUpdatePayload(values: VehicleFormValues) {
return {
mileage: values.mileage || undefined,
license_plate: values.license_plate || undefined,
image: values.image instanceof File ? values.image : undefined,
}
}
@@ -129,9 +126,10 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
const { mutate, error, isPending } = useFormMutation(form, {
mutationFn: (values: VehicleFormValues) => {
const payload = mapToPayload(values)
const promise = isEditing && resourceId
? api.vehicles.update(resourceId, mapUpdatePayload(values))
: api.vehicles.create(mapCreatePayload(values))
? api.vehicles.update(resourceId, payload)
: api.vehicles.create(payload)
toast.promise(promise, {
loading: isEditing ? "Updating vehicle..." : "Creating vehicle...",
success: isEditing ? "Vehicle updated successfully" : "Vehicle created successfully",
@@ -158,102 +156,97 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
)}
<FieldGroup>
{!isEditing && (
<>
{/* Vehicle identity */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<RhfTextField name="make" label="Make" placeholder="e.g. Toyota" required />
<RhfTextField name="model" label="Model" placeholder="e.g. Camry" required />
<RhfTextField name="year" label="Year" placeholder="e.g. 2024" required />
</div>
{/* Vehicle identity */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<RhfTextField name="make" label="Make" placeholder="e.g. Toyota" required />
<RhfTextField name="model" label="Model" placeholder="e.g. Camry" required />
<RhfTextField name="year" label="Year" placeholder="e.g. 2024" required />
</div>
<RhfTextField name="sub_model" label="Sub Model" placeholder="e.g. LE" />
<RhfTextField name="sub_model" label="Sub Model" placeholder="e.g. LE" />
{/* Associations */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="shop_type"
label="Shop Type"
placeholder="Select shop type"
queryKey={["shop-types"]}
listFn={() => api.shopTypes.list()}
mapOption={mapLookupOption}
createForm={(props) => <ShopTypeInlineForm {...props} />}
createLabel="Shop Type"
{...STORE_OBJECT}
/>
<RhfAsyncSelectField
name="vehicle_body_type"
label="Body Type"
placeholder="Select body type"
queryKey={["vehicle-body-types"]}
listFn={() => api.vehicleAttributes.listBodyTypes()}
mapOption={mapLookupOption}
createForm={(props) => <BodyTypeInlineForm {...props} />}
createLabel="Body Type"
{...STORE_OBJECT}
/>
</div>
{/* Associations */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="shop_type"
label="Shop Type"
placeholder="Select shop type"
queryKey={["shop-types"]}
listFn={() => api.shopTypes.list()}
mapOption={mapLookupOption}
createForm={(props) => <ShopTypeInlineForm {...props} />}
createLabel="Shop Type"
{...STORE_OBJECT}
/>
<RhfAsyncSelectField
name="vehicle_body_type"
label="Body Type"
placeholder="Select body type"
queryKey={["vehicle-body-types"]}
listFn={() => api.vehicleAttributes.listBodyTypes()}
mapOption={mapLookupOption}
createForm={(props) => <BodyTypeInlineForm {...props} />}
createLabel="Body Type"
{...STORE_OBJECT}
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="vehicle_fuel_type"
label="Fuel Type"
placeholder="Select fuel type"
queryKey={["vehicle-fuel-types"]}
listFn={() => api.vehicleAttributes.listFuelTypes()}
mapOption={mapLookupOption}
createForm={(props) => <FuelTypeInlineForm {...props} />}
createLabel="Fuel Type"
{...STORE_OBJECT}
/>
<RhfAsyncSelectField
name="vehicle_transmission"
label="Transmission"
placeholder="Select transmission"
queryKey={["vehicle-transmissions"]}
listFn={() => api.vehicleAttributes.listTransmissions()}
mapOption={mapLookupOption}
createForm={(props) => <TransmissionInlineForm {...props} />}
createLabel="Transmission"
{...STORE_OBJECT}
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="vehicle_fuel_type"
label="Fuel Type"
placeholder="Select fuel type"
queryKey={["vehicle-fuel-types"]}
listFn={() => api.vehicleAttributes.listFuelTypes()}
mapOption={mapLookupOption}
createForm={(props) => <FuelTypeInlineForm {...props} />}
createLabel="Fuel Type"
{...STORE_OBJECT}
/>
<RhfAsyncSelectField
name="vehicle_transmission"
label="Transmission"
placeholder="Select transmission"
queryKey={["vehicle-transmissions"]}
listFn={() => api.vehicleAttributes.listTransmissions()}
mapOption={mapLookupOption}
createForm={(props) => <TransmissionInlineForm {...props} />}
createLabel="Transmission"
{...STORE_OBJECT}
/>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="vehicle_color"
label="Color"
placeholder="Select color"
queryKey={["vehicle-colors"]}
listFn={() => api.vehicleAttributes.listColors()}
mapOption={mapLookupOption}
createForm={(props) => <ColorInlineForm {...props} />}
createLabel="Color"
{...STORE_OBJECT}
/>
<RhfTextField name="vin_number" label="VIN Number" placeholder="e.g. 1HGBH41JXMN109186" />
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="vehicle_color"
label="Color"
placeholder="Select color"
queryKey={["vehicle-colors"]}
listFn={() => api.vehicleAttributes.listColors()}
mapOption={mapLookupOption}
createForm={(props) => <ColorInlineForm {...props} />}
createLabel="Color"
{...STORE_OBJECT}
/>
<RhfTextField name="vin_number" label="VIN Number" placeholder="e.g. 1HGBH41JXMN109186" />
</div>
{/* Technical specs */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="engine_size" label="Engine Size" placeholder="e.g. 2.5L" />
<RhfTextField name="drivetrain" label="Drivetrain" placeholder="e.g. FWD" />
</div>
{/* Technical specs */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="engine_size" label="Engine Size" placeholder="e.g. 2.5L" />
<RhfTextField name="drivetrain" label="Drivetrain" placeholder="e.g. FWD" />
</div>
<RhfTextField name="owners_number" label="Number of Owners" placeholder="e.g. 1" />
</>
)}
<RhfTextField name="owners_number" label="Number of Owners" placeholder="e.g. 1" />
{/* Editable in both create and update */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfTextField name="license_plate" label="License Plate" placeholder="e.g. ABC-123" />
<RhfTextField name="mileage" label="Mileage" placeholder="e.g. 10000" />
</div>
{!isEditing && (
<RhfTextareaField name="note" label="Notes" rows={3} />
)}
<RhfImageField name="image" label="Image" />
<RhfTextareaField name="note" label="Notes" rows={3} />
<Button type="submit" variant="default" disabled={isPending}>
{isEditing ? <Save /> : <Plus />}
@@ -0,0 +1,244 @@
import {
Car,
Calendar,
Gauge,
Fuel,
Cog,
Palette,
Hash,
FileText,
Users,
Shield,
Wrench,
CircleDot,
} 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 VehicleData = {
id?: number
make?: string
model?: string
year?: string
sub_model?: string
license_plate?: string
vin_number?: string
engine_number?: string | null
engine_size?: string
drivetrain?: string
mileage?: string
owners_number?: string
front_tire_size?: string | null
rear_tire_size?: string | null
note?: string
image_url?: string | null
reg_date?: string | null
mfg_date?: string | null
created_at?: string
updated_at?: string
shop_type?: { id?: number; title?: string }
vehicle_body_type?: { id?: number; title?: string }
vehicle_fuel_type?: { id?: number; title?: string }
vehicle_transmission?: { id?: number; title?: string }
vehicle_color?: { id?: number; title?: string; code?: string }
}
type VehicleGeneralInfoProps = {
vehicle: VehicleData
}
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>
)
}
export function VehicleGeneralInfo({ vehicle }: VehicleGeneralInfoProps) {
return (
<div className="grid gap-6 md:grid-cols-2">
{/* Vehicle Identity */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Car className="size-4" />
Vehicle Identity
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<div className="flex flex-wrap items-center gap-2">
<Badge variant="secondary">
{[vehicle.make, vehicle.model].filter(Boolean).join(" ") || "Unknown"}
</Badge>
{vehicle.year && <Badge variant="outline">{vehicle.year}</Badge>}
{vehicle.sub_model && (
<Badge variant="outline">{vehicle.sub_model}</Badge>
)}
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem
icon={Hash}
label="License Plate"
value={vehicle.license_plate}
/>
<InfoItem
icon={Shield}
label="VIN Number"
value={vehicle.vin_number}
/>
<InfoItem
icon={FileText}
label="Engine Number"
value={vehicle.engine_number}
/>
<InfoItem
icon={Users}
label="Number of Owners"
value={vehicle.owners_number}
/>
</div>
</CardContent>
</Card>
{/* Technical Specifications */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Wrench className="size-4" />
Technical Specifications
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<div className="flex flex-wrap items-center gap-2">
{vehicle.vehicle_body_type?.title && (
<Badge variant="secondary">{vehicle.vehicle_body_type.title}</Badge>
)}
{vehicle.vehicle_transmission?.title && (
<Badge variant="outline">{vehicle.vehicle_transmission.title}</Badge>
)}
{vehicle.vehicle_fuel_type?.title && (
<Badge variant="outline">{vehicle.vehicle_fuel_type.title}</Badge>
)}
</div>
<Separator />
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem
icon={Cog}
label="Engine Size"
value={vehicle.engine_size}
/>
<InfoItem
icon={CircleDot}
label="Drivetrain"
value={vehicle.drivetrain}
/>
<InfoItem
icon={Gauge}
label="Mileage"
value={vehicle.mileage ? `${Number(vehicle.mileage).toLocaleString()} km` : null}
/>
<InfoItem
icon={Fuel}
label="Fuel Type"
value={vehicle.vehicle_fuel_type?.title}
/>
</div>
</CardContent>
</Card>
{/* Appearance & Shop */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Palette className="size-4" />
Appearance
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4 sm:grid-cols-2">
<InfoItem
icon={Palette}
label="Color"
value={vehicle.vehicle_color?.title}
/>
<InfoItem
icon={Wrench}
label="Shop Type"
value={vehicle.shop_type?.title}
/>
<InfoItem
icon={CircleDot}
label="Front Tire Size"
value={vehicle.front_tire_size}
/>
<InfoItem
icon={CircleDot}
label="Rear Tire Size"
value={vehicle.rear_tire_size}
/>
</CardContent>
</Card>
{/* Dates & Notes */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="size-4" />
Dates & Notes
</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid gap-4 sm:grid-cols-2">
<InfoItem
icon={Calendar}
label="Registration Date"
value={vehicle.reg_date ? new Date(vehicle.reg_date).toLocaleDateString() : null}
/>
<InfoItem
icon={Calendar}
label="Manufacturing Date"
value={vehicle.mfg_date ? new Date(vehicle.mfg_date).toLocaleDateString() : null}
/>
</div>
{vehicle.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">{vehicle.note}</p>
</div>
</div>
</>
)}
</CardContent>
</Card>
</div>
)
}
@@ -30,6 +30,9 @@ export const vehicleFormSchema = z.object({
// ── Notes ──
note: z.string().optional(),
// ── Image ──
image: z.any().optional(),
})
export type VehicleFormValues = z.infer<typeof vehicleFormSchema>