"use client"; import { useCallback, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import { Alert, Button, Input, Modal, Select } from "../UI"; import { getStoredToken } from "@/src/lib/auth"; import { useEditFormSync } from "@/src/hooks/useEditFormSync"; import { createCarSchema, updateCarSchema, } from "@/src/validations/car.validator"; import type { Car, CreateCarPayload, InsuranceStatus, UpdateCarPayload, } from "@/src/types/car"; import type { Branch } from "@/src/types/branch"; import { branchService } from "@/src/services/branch.service"; // ── Shared styles ───────────────────────────────────────────────────────────── // Only section-heading styling is left here — every input/select/label/error // visual is now owned by the shared / components. const sectionHeadingStyle: React.CSSProperties = { fontSize: 11, letterSpacing: "0.25em", textTransform: "uppercase", color: "var(--color-text-hint)", fontWeight: 700, margin: "0.5rem 0 0", }; const FORM_ID = "car-form"; // ── Date helper ─────────────────────────────────────────────────────────────── // gives "YYYY-MM-DD"; backend expects full ISO-8601. function toIsoDateTime(val: string): string { if (!val) return val; if (val.includes("T")) return val; return `${val}T00:00:00.000Z`; } // ── Form values shape ───────────────────────────────────────────────────────── // Mirrors the subset of CreateCarPayload/UpdateCarPayload actually edited here. interface CarFormValues { manufacturer: string; model: string; color: string; plateNumber: string; plateLetters: string; plateType: string; registrationNumber: string; vinNumber: string; branchId: string; currentStatus: Car["currentStatus"]; insuranceStatus: InsuranceStatus; registrationExpiryDate: string; insuranceExpiryDate: string; inspectionExpiryDate: string; gpsDeviceId: string; year: number | undefined; capacity: number | undefined; weight: number | undefined; } // ── Props ───────────────────────────────────────────────────────────────────── interface CarFormModalProps { editCar: Car | null; /** Pass pre-loaded branches or an empty array — the modal will auto-fetch if empty. */ branches: Branch[]; onClose: () => void; onSubmit: ( payload: CreateCarPayload | UpdateCarPayload, isNew: boolean, ) => Promise; } // ── Component ───────────────────────────────────────────────────────────────── export function CarFormModal({ editCar, branches: branchesProp, onClose, onSubmit, }: CarFormModalProps) { const isNew = editCar === null; // ── react-hook-form ──────────────────────────────────────────────────────── // Moved above the branches-loading effect below: that effect calls // setValue(), so useForm() (which defines it) must run first — otherwise // setValue is referenced before its own initialization (TDZ ReferenceError), // same issue already fixed in TripFormModal / DriverFormModal. const { register, handleSubmit, setError, setFocus, setValue, formState: { errors, isSubmitting }, } = useForm({ // Cast both the schema argument and the resolver's result — same fix // already applied in TripFormModal/DriverFormModal: createCarSchema/ // updateCarSchema have structurally different required fields (union // type yupResolver's single-schema signature won't accept), and yup's // inferred type for .optional() fields is structurally incompatible // with CarFormValues. resolver: yupResolver((isNew ? createCarSchema : updateCarSchema) as any) as any, defaultValues: { manufacturer: editCar?.manufacturer ?? "", model: editCar?.model ?? "", color: editCar?.color ?? "", plateNumber: editCar?.plateNumber ?? "", plateLetters: editCar?.plateLetters ?? "", plateType: editCar?.plateType ?? "", registrationNumber: editCar?.registrationNumber ?? "", vinNumber: editCar?.vinNumber ?? "", branchId: editCar?.branch?.id ?? "", currentStatus: editCar?.currentStatus ?? "Active", insuranceStatus: (editCar?.insuranceStatus as InsuranceStatus) ?? "Valid", registrationExpiryDate: editCar?.registrationExpiryDate?.slice(0, 10) ?? "", insuranceExpiryDate: editCar?.insuranceExpiryDate?.slice(0, 10) ?? "", inspectionExpiryDate: editCar?.inspectionExpiryDate?.slice(0, 10) ?? "", gpsDeviceId: editCar?.gpsDeviceId ?? "", // NOTE: kept as number | undefined instead of RHF's valueAsNumber so an // empty input maps to `undefined` (matches the old parseNum() guard) // rather than NaN, which would otherwise trip the yup .typeError() rule. year: editCar?.year ?? new Date().getFullYear(), capacity: editCar?.capacity ?? undefined, weight: editCar?.weight ?? undefined, }, }); // ── Local branches (auto-fetched if prop is empty) ───────────────────────── const [branches, setBranches] = useState(branchesProp); const loadBranches = useCallback(() => { if (branchesProp.length > 0) { queueMicrotask(() => setBranches(branchesProp)); return; } const token = getStoredToken(); branchService .getOptions(token) .then((list) => { queueMicrotask(() => setBranches(list as unknown as Branch[])); }) .catch(() => { /* silently ignore */ }); }, [branchesProp]); useEffect(() => { loadBranches(); }, [loadBranches]); // CHANGE: replaced the manual `if (savedBranchId) setValue("branchId", ...)` // calls above with useEditFormSync. useEditFormSync(setValue, "branchId", editCar?.branch?.id, branches); const [apiError, setApiError] = useState(""); // register("manufacturer") already provides a ref for this input, so we // focus it via RHF's setFocus instead of a separate useRef — assigning // both `ref={firstRef}` and `{...register(...)}` on the same element // causes the second ref to silently overwrite the first. useEffect(() => { setFocus("manufacturer"); }, [setFocus]); // Numeric fields: empty string -> undefined (never NaN), matching the // behavior of the old parseNum() helper. const numberField = ( field: "year" | "capacity" | "weight", ) => register(field, { setValueAs: (v) => (v === "" || v === null ? undefined : Number(v)), }); // ── Submit ──────────────────────────────────────────────────────────────── const submitHandler = async (data: CarFormValues) => { // Build final payload — convert date strings to ISO-8601 for the backend, // and drop empty-optional fields exactly like the pre-refactor version did. const raw: Record = { manufacturer: data.manufacturer, model: data.model, year: data.year, plateNumber: data.plateNumber, plateLetters: data.plateLetters, currentStatus: data.currentStatus, }; if (data.color) raw.color = data.color; if (data.plateType) raw.plateType = data.plateType; if (data.registrationNumber) raw.registrationNumber = data.registrationNumber; if (data.vinNumber) raw.vinNumber = data.vinNumber; if (data.branchId) raw.branchId = data.branchId; if (data.insuranceStatus) raw.insuranceStatus = data.insuranceStatus; if (data.registrationExpiryDate) raw.registrationExpiryDate = toIsoDateTime(data.registrationExpiryDate); if (data.insuranceExpiryDate) raw.insuranceExpiryDate = toIsoDateTime(data.insuranceExpiryDate); if (data.inspectionExpiryDate) raw.inspectionExpiryDate = toIsoDateTime(data.inspectionExpiryDate); if (data.gpsDeviceId) raw.gpsDeviceId = data.gpsDeviceId; // Defensive re-coercion: mirrors CarMaintenanceFormModal's // toNumberOrUndefined() safeguard, in case capacity/weight ever round-trip // as numeric strings from a Decimal-backed field on the backend. if (data.capacity !== undefined && data.capacity !== null && !Number.isNaN(Number(data.capacity))) raw.capacity = Number(data.capacity); if (data.weight !== undefined && data.weight !== null && !Number.isNaN(Number(data.weight))) raw.weight = Number(data.weight); const payload = raw as unknown as CreateCarPayload; setApiError(""); const ok = await onSubmit(payload, isNew); if (ok) { onClose(); } else { setError("manufacturer", { message: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً." }); setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."); } }; // ── Render ──────────────────────────────────────────────────────────────── return ( إلغاء {isNew ? "إضافة المركبة" : "حفظ التغييرات"} > } > {errors.manufacturer?.type === "manual" && ( setApiError("")} /> )} {/* ── Section: Basic Info ── */} بيانات أساسية {/* ── Section: Plate ── */} بيانات اللوحة {/* ── Section: Registration & Legal ── */} الترخيص والتأمين سارٍ منتهي غير مؤمَّن {/* ── Section: Operational ── */} بيانات تشغيلية اختر الفرع {branches.map((b) => ( {b.name} ))} نشط صيانة في رحلة غير نشط ); }
بيانات أساسية
بيانات اللوحة
الترخيص والتأمين
بيانات تشغيلية