"use client"; import { useEffect, useRef, useState } from "react"; import { Alert, Spinner } from "../UI"; import { get } from "../../services/api"; import { getStoredToken } from "../../lib/auth"; import type { Car, CarFormErrors, CreateCarPayload, UpdateCarPayload, } from "../../types/car"; import type { Branch } from "../../types/branch"; // ── Shared input style ─────────────────────────────────────────────────────── const inputBase: React.CSSProperties = { width: "100%", height: 40, padding: "0 0.75rem", borderRadius: "var(--radius-md)", border: "1px solid var(--color-border)", background: "var(--color-surface)", fontSize: 13, color: "var(--color-text-primary)", outline: "none", fontFamily: "var(--font-sans)", }; const labelStyle: React.CSSProperties = { display: "flex", flexDirection: "column", gap: 6, fontSize: 12, fontWeight: 600, color: "var(--color-text-secondary)", }; const errorTextStyle: React.CSSProperties = { fontSize: 11, color: "var(--color-danger)", fontWeight: 500, }; // ── Validation ─────────────────────────────────────────────────────────────── function validate(form: Partial): CarFormErrors { const e: CarFormErrors = {}; if (!form.manufacturer?.trim()) e.manufacturer = "الشركة المصنعة مطلوبة"; if (!form.model?.trim()) e.model = "الموديل مطلوب"; if ( !form.year || form.year < 1900 || form.year > new Date().getFullYear() + 1 ) e.year = "سنة غير صالحة"; if (!form.plateNumber?.trim()) e.plateNumber = "رقم اللوحة مطلوب"; if (!form.plateLetters?.trim()) e.plateLetters = "حروف اللوحة مطلوبة"; return e; } // ── Date helper ────────────────────────────────────────────────────────────── // gives "YYYY-MM-DD"; the backend z.string().datetime() // requires a full ISO-8601 string ("YYYY-MM-DDTHH:mm:ss.sssZ"). // We keep state as the short form (so the input renders correctly) and convert // only at submit time. function toIsoDateTime(val: string): string { if (!val) return val; if (val.includes("T")) return val; // Already in ISO format, likely from an existing record — just return as-is. return `${val}T00:00:00.000Z`; // transform "2026-12-31" → "2026-12-31T00:00:00.000Z" } // ── 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; // ── Local branches (auto-fetched if prop is empty) ───────────────────────── const [branches, setBranches] = useState(branchesProp); useEffect(() => { if (branchesProp.length > 0) { setBranches(branchesProp); return; } const token = getStoredToken(); get<{ data: { data: Branch[] } }>("branches?limit=100", token) .then((res) => { const list = (res as unknown as { data: { data: Branch[] } }).data?.data ?? []; setBranches(list); }) .catch(() => { /* silently ignore — user can still submit */ }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // ── Form state — each field correctly typed ──────────────────────────────── // String fields const [manufacturer, setManufacturer] = useState(editCar?.manufacturer ?? ""); const [model, setModel] = useState(editCar?.model ?? ""); const [color, setColor] = useState(editCar?.color ?? ""); const [plateNumber, setPlateNumber] = useState(editCar?.plateNumber ?? ""); const [plateLetters, setPlateLetters] = useState(editCar?.plateLetters ?? ""); const [plateType, setPlateType] = useState(editCar?.plateType ?? ""); const [registrationNumber, setRegistrationNumber] = useState( editCar?.registrationNumber ?? "", ); const [vinNumber, setVinNumber] = useState(editCar?.vinNumber ?? ""); const [branchId, setBranchId] = useState(editCar?.branch?.id ?? ""); const [currentStatus, setCurrentStatus] = useState( editCar?.currentStatus ?? "Active", ); const [insuranceStatus, setInsuranceStatus] = useState( editCar?.insuranceStatus ?? "Valid", ); const [registrationExpiryDate, setRegistrationExpiryDate] = useState( editCar?.registrationExpiryDate?.slice(0, 10) ?? "", ); const [insuranceExpiryDate, setInsuranceExpiryDate] = useState( editCar?.insuranceExpiryDate?.slice(0, 10) ?? "", ); const [inspectionExpiryDate, setInspectionExpiryDate] = useState( editCar?.inspectionExpiryDate?.slice(0, 10) ?? "", ); const [gpsDeviceId, setGpsDeviceId] = useState(editCar?.gpsDeviceId ?? ""); // Numeric fields — undefined when blank const [year, setYear] = useState( editCar?.year ?? new Date().getFullYear(), ); const [capacity, setCapacity] = useState( editCar?.capacity ?? undefined, ); const [weight, setWeight] = useState( editCar?.weight ?? undefined, ); const [errors, setErrors] = useState({}); const [saving, setSaving] = useState(false); const [apiError, setApiError] = useState(""); const firstRef = useRef(null); useEffect(() => { firstRef.current?.focus(); }, []); useEffect(() => { const h = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", h); return () => window.removeEventListener("keydown", h); }, [onClose]); // ── Number input helper ─────────────────────────────────────────────────── const parseNum = (v: string): number | undefined => v.trim() === "" ? undefined : Number(v); // ── Input error style ───────────────────────────────────────────────────── const inputStyle = (field: keyof CarFormErrors): React.CSSProperties => ({ ...inputBase, ...(errors[field] ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}), }); const clearFieldError = (field: keyof CarFormErrors) => setErrors((p) => ({ ...p, [field]: undefined })); // ── Submit ──────────────────────────────────────────────────────────────── const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); // Build a partial payload for validation (only required fields matter here) const forValidation: Partial = { manufacturer, model, year, plateNumber, plateLetters, }; const errs = validate(forValidation); if (Object.keys(errs).length) { setErrors(errs); return; } // Build the full payload — strip empty strings to avoid schema rejection. // We use `unknown` cast here to satisfy TypeScript while we construct the // object dynamically; the structure still fully matches CreateCarPayload. const raw: Record = { manufacturer, model, year, plateNumber, plateLetters, currentStatus, }; // Optional string fields — only include if non-empty if (color) raw.color = color; if (plateType) raw.plateType = plateType; if (registrationNumber) raw.registrationNumber = registrationNumber; if (vinNumber) raw.vinNumber = vinNumber; if (branchId) raw.branchId = branchId; if (insuranceStatus) raw.insuranceStatus = insuranceStatus; if (registrationExpiryDate) raw.registrationExpiryDate = toIsoDateTime(registrationExpiryDate); if (insuranceExpiryDate) raw.insuranceExpiryDate = toIsoDateTime(insuranceExpiryDate); if (inspectionExpiryDate) raw.inspectionExpiryDate = toIsoDateTime(inspectionExpiryDate); if (gpsDeviceId) raw.gpsDeviceId = gpsDeviceId; // Numeric optional fields if (capacity !== undefined) raw.capacity = capacity; if (weight !== undefined) raw.weight = weight; // Cast to CreateCarPayload — structure is identical, we just built it dynamically const payload = raw as unknown as CreateCarPayload; setSaving(true); setApiError(""); const ok = await onSubmit(payload, isNew); setSaving(false); if (ok) onClose(); else setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."); }; // ── Render ──────────────────────────────────────────────────────────────── return (
{ if (e.target === e.currentTarget) onClose(); }} style={{ position: "fixed", inset: 0, zIndex: 50, background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", overflowY: "auto", }} >
e.stopPropagation()} style={{ width: "100%", maxWidth: 620, background: "var(--color-surface)", borderRadius: "var(--radius-2xl)", border: "1px solid var(--color-border)", boxShadow: "0 24px 64px rgba(0,0,0,.18)", overflow: "hidden", margin: "auto", }} > {/* Header */}

{isNew ? "إضافة مركبة" : "تعديل مركبة"}

{isNew ? "مركبة جديدة" : `${editCar?.manufacturer} ${editCar?.model}`}

{/* Form */}
{apiError && ( setApiError("")} /> )} {/* Section: Basic Info */}

بيانات أساسية

{/* Manufacturer — directly mapped to CreateCarPayload.manufacturer */} {/* Model — directly mapped to CreateCarPayload.model */}
{/* Year — maps to CreateCarPayload.year (number) */}
{/* Section: Plate */}

بيانات اللوحة

{/* plateNumber — directly mapped to CreateCarPayload.plateNumber */} {/* plateLetters — directly mapped to CreateCarPayload.plateLetters */}
{/* Section: Registration & Legal */}

الترخيص والتأمين

{/* Section: Operational */}

بيانات تشغيلية

{/* capacity — maps to CreateCarPayload.capacity (number | undefined) */} {/* weight — maps to CreateCarPayload.weight (number | undefined) */}
{/* Actions */}
); }