"use client"; // Components/Car/CarFormModal.tsx import { useEffect, useRef, useState } from "react"; import * as yup from "yup"; import { Alert, Spinner } from "../UI"; import { get } from "../../services/api"; import { getStoredToken } from "../../lib/auth"; import { createCarSchema, updateCarSchema } from "@/validations/car.validator"; import type { Car, CarFormErrors, CreateCarPayload, InsuranceStatus, 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, }; // ── yup validation ──────────────────────────────────────────────────────────── async function validate( form: Partial, isNew: boolean, ): Promise { const schema = isNew ? createCarSchema : updateCarSchema; try { await schema.validate(form, { abortEarly: false }); return {}; } catch (err) { if (err instanceof yup.ValidationError) { return err.inner.reduce((acc, e) => { const field = e.path as keyof CarFormErrors; if (field && !acc[field]) acc[field] = e.message; return acc; }, {}); } return {}; } } // ── 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`; } // ── 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 */ }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // ── Form state ───────────────────────────────────────────────────────────── 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 as 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 ?? ""); 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]); const parseNum = (v: string): number | undefined => v.trim() === "" ? undefined : Number(v); 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 snapshot for yup — include all fields so optional rules also run const formSnapshot: Partial = { manufacturer, model, year, plateNumber, plateLetters, currentStatus, insuranceStatus, registrationNumber, vinNumber, ...(color && { color }), ...(plateType && { plateType }), ...(branchId && { branchId }), ...(registrationExpiryDate && { registrationExpiryDate }), ...(insuranceExpiryDate && { insuranceExpiryDate }), ...(inspectionExpiryDate && { inspectionExpiryDate }), ...(gpsDeviceId && { gpsDeviceId }), ...(capacity !== undefined && { capacity }), ...(weight !== undefined && { weight }), }; const errs = await validate(formSnapshot, isNew); if (Object.keys(errs).length) { setErrors(errs); return; } // Build final payload — convert date strings to ISO-8601 for the backend const raw: Record = { manufacturer, model, year, plateNumber, plateLetters, currentStatus, }; 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; if (capacity !== undefined) raw.capacity = capacity; if (weight !== undefined) raw.weight = weight; 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 ── */}

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

{/* ── Section: Plate ── */}

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

{/* ── Section: Registration & Legal ── */}

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

{/* ── Section: Operational ── */}

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

{/* ── Actions ── */}
); }