"use client" import { useRef, useState, useEffect } from "react" import { useFormContext, useController, useWatch } from "react-hook-form" import { useQuery } from "@tanstack/react-query" import { Loader2 } from "lucide-react" import { useAuthApi } from "@/shared/useApi" import { Field, FieldLabel, FieldError } from "@/shared/components/ui/field" import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty, } from "@/shared/components/ui/combobox" import type { VehicleFormValues } from "./vehicle.schema" // ── Helpers ── function unique(values: (string | undefined)[]): string[] { return [...new Set(values.filter((v): v is string => !!v))].sort() } // ── Internal combobox for a single free-text + suggestion field ── type VehicleComboboxProps = { value: string onChange: (value: string) => void onBlur?: () => void options: string[] loading?: boolean placeholder: string label: string error?: string required?: boolean disabled?: boolean } function VehicleCombobox({ value, onChange, onBlur, options, loading, placeholder, label, error, required, disabled, }: VehicleComboboxProps) { const anchorRef = useRef(null) // Local state keeps the input text in sync even when the form value // is changed externally (e.g., cascade reset or edit-mode population). const [inputText, setInputText] = useState(value ?? "") useEffect(() => { setInputText(value ?? "") }, [value]) // Client-side filtering of suggestions based on what's been typed const filtered = options.filter((opt) => !inputText || opt.toLowerCase().includes(inputText.toLowerCase()), ) return ( {label} {required && *}
{ const str = val !== null ? String(val) : "" setInputText(str) onChange(str) }} disabled={disabled} onInputValueChange={(text, { reason }) => { if (reason === "input-change") { setInputText(text) onChange(text) } }} > {loading && (
)} {!loading && filtered.map((opt) => ( {opt} ))} {!loading && filtered.length === 0 && ( {inputText ? `No suggestions for "${inputText}"` : "Start typing to see suggestions"} )}
{error && {error}}
) } // ── Main composite field ── /** * Manages the four vehicle identity fields (make, model, year, sub_model) as * a single coordinated component. Each field shows autocomplete suggestions * fetched from the make-and-models API, filtered by the values of the * preceding fields. All four fields accept free-text — the user is never * forced to pick from the suggestion list. * * Cascade behaviour: changing make resets model → year → sub_model; * changing model resets year → sub_model; changing year resets sub_model. */ export function RhfVehicleIdentityField() { const api = useAuthApi() const { control, setValue } = useFormContext() // Read shop_type to pass as shop_type_id filter to the API const shopType = useWatch({ control, name: "shop_type" }) const shopTypeId = shopType?.value const makeCtrl = useController({ name: "make", control }) const modelCtrl = useController({ name: "model", control }) const yearCtrl = useController({ name: "year", control }) const subModelCtrl = useController({ name: "sub_model", control }) const makeValue = makeCtrl.field.value ?? "" const modelValue = modelCtrl.field.value ?? "" const yearValue = yearCtrl.field.value ?? "" // ── Fetch all records once (large per_page), then derive unique suggestion // lists client-side so we don't invent API endpoints. ── type MakeAndModelRecord = { id?: number shop_type_id?: number make?: string model?: string year?: string sub_model?: string } function extractRecords(response: unknown): MakeAndModelRecord[] { const obj = response as any if (Array.isArray(obj?.data)) return obj.data if (Array.isArray(obj?.data?.data)) return obj.data.data if (Array.isArray(obj)) return obj return [] } const { data: allRecords = [], isLoading: recordsLoading } = useQuery({ queryKey: ["make-and-models-all", shopTypeId], queryFn: async () => { const res = await api.makeAndModels.list({ per_page: 9999, ...(shopTypeId ? { shop_type_id: shopTypeId } : {}) }) return extractRecords(res) }, staleTime: 5 * 60 * 1000, }) // Derive unique sorted lists from the full record set, filtered by upstream selections. const makes = unique(allRecords.map((r) => r.make)) const models = unique( allRecords .filter((r) => !makeValue || r.make === makeValue) .map((r) => r.model), ) const years = unique( allRecords .filter((r) => (!makeValue || r.make === makeValue) && (!modelValue || r.model === modelValue)) .map((r) => r.year), ) const subModels = unique( allRecords .filter( (r) => (!makeValue || r.make === makeValue) && (!modelValue || r.model === modelValue) && (!yearValue || r.year === yearValue), ) .map((r) => r.sub_model), ) const makesLoading = recordsLoading const modelsLoading = false const yearsLoading = false const subModelsLoading = false // ── Cascading onChange handlers ── function handleMakeChange(val: string) { makeCtrl.field.onChange(val) // Reset all downstream fields whenever make changes setValue("model", "") setValue("year", "") setValue("sub_model", "") } function handleModelChange(val: string) { modelCtrl.field.onChange(val) setValue("year", "") setValue("sub_model", "") } function handleYearChange(val: string) { yearCtrl.field.onChange(val) setValue("sub_model", "") } return (
subModelCtrl.field.onChange(val)} onBlur={subModelCtrl.field.onBlur} options={subModels} loading={subModelsLoading} error={subModelCtrl.fieldState.error?.message} />
) }