Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Mohammad Khyata 2026-05-07 11:30:23 +03:00
parent cdd1cbc31a
commit 1fda8d8d7b
5 changed files with 65 additions and 11 deletions

View File

@ -3,11 +3,13 @@
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { ResourcePage } from '@/shared/data-view/resource-page' import { ResourcePage } from '@/shared/data-view/resource-page'
import { ColumnHeader } from '@/shared/data-view/table-view' import { ColumnHeader } from '@/shared/data-view/table-view'
import type { ColumnDef } from '@tanstack/react-table'
import FormDialog from '@/shared/components/form-dialog' import FormDialog from '@/shared/components/form-dialog'
import { ImportDataButton } from '@/shared/components/import-data-button' import { ImportDataButton } from '@/shared/components/import-data-button'
import { ExportDataButton } from '@/shared/components/export-data-button' import { ExportDataButton } from '@/shared/components/export-data-button'
import { DownloadSampleButton } from '@/shared/components/download-sample-button' import { DownloadSampleButton } from '@/shared/components/download-sample-button'
import { useAuthApi } from '@/shared/useApi' import { useAuthApi } from '@/shared/useApi'
import type { CrudResourceColumnHelpers, ResourceItem } from '@/shared/data-view/resource-page'
import { VehicleForm } from '@/modules/vehicles/vehicle-form' import { VehicleForm } from '@/modules/vehicles/vehicle-form'
import { VEHICLE_ROUTES } from '@garage/api' import { VEHICLE_ROUTES } from '@garage/api'
import type { VehiclesClient } from '@garage/api' import type { VehiclesClient } from '@garage/api'
@ -49,9 +51,11 @@ export default function VehiclesPage() {
</div> </div>
), ),
})} })}
columns={({ actionsColumn })=> [ columns={({ actionsColumn })=> [
{ {
accessorKey: "name", id: "name",
header: ({ column }) => <ColumnHeader column={column} title="Vehicle" />, header: ({ column }) => <ColumnHeader column={column} title="Vehicle" />,
cell: ({ row }) => { cell: ({ row }) => {
const r = row.original as any const r = row.original as any
@ -117,6 +121,14 @@ export default function VehiclesPage() {
return val ? new Date(val).toLocaleDateString() : "—" return val ? new Date(val).toLocaleDateString() : "—"
}, },
}, },
{
accessorKey: "customer",
header: ({ column }) => <ColumnHeader column={column} title="Customer" />,
cell: ({ row }) => {
const val = (row.original as any).customer
return val ? val.name : "—"
},
},
actionsColumn(), actionsColumn(),
]} ]}
/> />

View File

@ -252,7 +252,7 @@ export function JobCardForm({ resourceId, initialData, onSuccess }: JobCardFormP
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfCustomerSelectField name="customer" /> <RhfCustomerSelectField name="customer" />
<RhfVehicleSelectField name="vehicle" customer_id={customer?.value} /> <RhfVehicleSelectField name="vehicle" disabled={!customer?.value} customer_id={customer?.value} />
</div> </div>
<RhfCheckboxField name="has_insurance" label="Has Insurance Work?" /> <RhfCheckboxField name="has_insurance" label="Has Insurance Work?" />

View File

@ -18,8 +18,14 @@ import type { VehicleFormValues } from "./vehicle.schema"
// ── Helpers ── // ── Helpers ──
function unique(values: (string | undefined)[]): string[] { function toOptionalString(value: unknown): string | undefined {
return [...new Set(values.filter((v): v is string => !!v))].sort() if (typeof value === "string") return value || undefined
if (typeof value === "number") return String(value)
return undefined
}
function unique(values: unknown[]): string[] {
return [...new Set(values.map(toOptionalString).filter((v): v is string => !!v))].sort()
} }
// ── Internal combobox for a single free-text + suggestion field ── // ── Internal combobox for a single free-text + suggestion field ──
@ -60,8 +66,9 @@ function VehicleCombobox({
}, [value]) }, [value])
// Client-side filtering of suggestions based on what's been typed // Client-side filtering of suggestions based on what's been typed
const normalizedInput = inputText.toLowerCase()
const filtered = options.filter((opt) => const filtered = options.filter((opt) =>
!inputText || opt.toLowerCase().includes(inputText.toLowerCase()), !normalizedInput || opt.toLowerCase().includes(normalizedInput),
) )
return ( return (
@ -130,7 +137,7 @@ export function RhfVehicleIdentityField() {
const { control, setValue } = useFormContext<VehicleFormValues>() const { control, setValue } = useFormContext<VehicleFormValues>()
// Read shop_type to pass as shop_type_id filter to the API // Read shop_type to pass as shop_type_id filter to the API
const shopType = useWatch({ control, name: "shop_type" }) const shopType = useWatch({ control, name: "shop_type_id" })
const shopTypeId = shopType?.value const shopTypeId = shopType?.value
const makeCtrl = useController({ name: "make", control }) const makeCtrl = useController({ name: "make", control })
@ -158,11 +165,26 @@ export function RhfVehicleIdentityField() {
drivetrain?: string drivetrain?: string
} }
function normalizeRecord(record: unknown): MakeAndModelRecord {
const value = record as Record<string, unknown>
return {
id: typeof value.id === "number" ? value.id : undefined,
shop_type_id: typeof value.shop_type_id === "number" ? value.shop_type_id : undefined,
make: toOptionalString(value.make),
model: toOptionalString(value.model),
year: toOptionalString(value.year),
sub_model: toOptionalString(value.sub_model),
engine_size: toOptionalString(value.engine_size),
drivetrain: toOptionalString(value.drivetrain),
}
}
function extractRecords(response: unknown): MakeAndModelRecord[] { function extractRecords(response: unknown): MakeAndModelRecord[] {
const obj = response as any const obj = response as any
if (Array.isArray(obj?.data)) return obj.data if (Array.isArray(obj?.data)) return obj.data.map(normalizeRecord)
if (Array.isArray(obj?.data?.data)) return obj.data.data if (Array.isArray(obj?.data?.data)) return obj.data.data.map(normalizeRecord)
if (Array.isArray(obj)) return obj if (Array.isArray(obj)) return obj.map(normalizeRecord)
return [] return []
} }

View File

@ -26,6 +26,8 @@ import { toRelation, toId } from "@/shared/lib/utils"
import { formatUppercase } from "@/shared/utils/formatters" import { formatUppercase } from "@/shared/utils/formatters"
import { vehicleFormSchema, type VehicleFormValues } from "./vehicle.schema" import { vehicleFormSchema, type VehicleFormValues } from "./vehicle.schema"
import { CustomerForm } from "../customers/customer-form"
import { getFullName } from "@/shared/utils/getFullName"
// ── Props ── // ── Props ──
@ -43,6 +45,7 @@ const DEFAULT_VALUES: VehicleFormValues = {
vehicle_fuel_type_id: null, vehicle_fuel_type_id: null,
vehicle_transmission_id: null, vehicle_transmission_id: null,
vehicle_color_id: null, vehicle_color_id: null,
customer_id: null,
make: "", make: "",
model: "", model: "",
year: "", year: "",
@ -74,6 +77,7 @@ function mapToFormValues(data: unknown): VehicleFormValues {
vehicle_fuel_type_id: toRelation(d.vehicle_fuel_type_id, d.vehicle_fuel_type?.title), vehicle_fuel_type_id: toRelation(d.vehicle_fuel_type_id, d.vehicle_fuel_type?.title),
vehicle_transmission_id: toRelation(d.vehicle_transmission_id, d.vehicle_transmission?.title), vehicle_transmission_id: toRelation(d.vehicle_transmission_id, d.vehicle_transmission?.title),
vehicle_color_id: toRelation(d.vehicle_color_id, d.vehicle_color?.title), vehicle_color_id: toRelation(d.vehicle_color_id, d.vehicle_color?.title),
customer_id: toRelation(d.customer_id, d.customer?.name),
make: d.make || "", make: d.make || "",
model: d.model || "", model: d.model || "",
year: d.year || "", year: d.year || "",
@ -95,6 +99,8 @@ function mapToPayload(values: VehicleFormValues) {
vehicle_fuel_type_id: toId(values.vehicle_fuel_type_id), vehicle_fuel_type_id: toId(values.vehicle_fuel_type_id),
vehicle_transmission_id: toId(values.vehicle_transmission_id), vehicle_transmission_id: toId(values.vehicle_transmission_id),
vehicle_color_id: toId(values.vehicle_color_id), vehicle_color_id: toId(values.vehicle_color_id),
customer_id: toId(values.customer_id),
make: values.make, make: values.make,
model: values.model, model: values.model,
year: values.year, year: values.year,
@ -222,6 +228,20 @@ export function VehicleForm({ resourceId, initialData, onSuccess }: VehicleFormP
/> />
<RhfTextField name="vin_number" label="VIN Number" placeholder="e.g. 1HGBH41JXMN109186" /> <RhfTextField name="vin_number" label="VIN Number" placeholder="e.g. 1HGBH41JXMN109186" />
</div> </div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<RhfAsyncSelectField
name="customer_id"
label="Customer"
placeholder="Select customer"
queryKey={["customers"]}
listFn={() => api.customers.list()}
mapOption={(op:any)=> ({ value: String(op.id), label: getFullName(op) })}
createForm={(props) => <CustomerForm {...props} />}
createLabel="Customer"
{...STORE_OBJECT}
/>
</div>
{/* License & identifiers */} {/* License & identifiers */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">

View File

@ -11,7 +11,7 @@ export const vehicleFormSchema = z.object({
vehicle_fuel_type_id: relationFieldSchema, vehicle_fuel_type_id: relationFieldSchema,
vehicle_transmission_id: relationFieldSchema, vehicle_transmission_id: relationFieldSchema,
vehicle_color_id: relationFieldSchema, vehicle_color_id: relationFieldSchema,
customer_id: relationFieldSchema,
// ── Vehicle identity ── // ── Vehicle identity ──
make: z.string().optional(), make: z.string().optional(),
model: z.string().optional(), model: z.string().optional(),