"use client"; import { useCallback, useEffect, useState } from "react"; import { useForm, Controller } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; import { Alert, Button, FileInput, Input, Modal, Select } from "../UI"; import { get } from "@/src/services/api"; import { getStoredToken } from "@/src/lib/auth"; import { createDriverSchema, updateDriverSchema } from "@/src/validations/driver.validator"; import type { Driver, CreateDriverPayload, UpdateDriverPayload, NationalIdType, DriverCardType, DriverStatus, } from "@/src/types/driver"; import type { Branch } from "@/src/types/branch"; // ── Shared bits ────────────────────────────────────────────────────────────── 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 = "driver-form"; // ── Date helper ────────────────────────────────────────────────────────────── function toIsoDateTime(val: string): string { if (!val) return val; if (val.includes("T")) return val; return `${val}T00:00:00.000Z`; } // ── Form values shape ──────────────────────────────────────────────────────── interface DriverFormValues { name: string; phone: string; email: string; address: string; nationality: string; nationalIdType: NationalIdType | ""; nationalId: string; nationalIdExpiry: string; gosiNumber: string; licenseNumber: string; licenseType: string; licenseExpiry: string; driverCardNumber: string; driverCardType: DriverCardType | ""; driverCardExpiry: string; driverType: string; branchId: string; status: DriverStatus; photo: File | null; nationalPhoto: File | null; driverCardPhoto: File | null; } // ── Props ──────────────────────────────────────────────────────────────────── interface DriverFormModalProps { editDriver: Driver | null; branches: Branch[]; onClose: () => void; onSubmit: ( payload: CreateDriverPayload | UpdateDriverPayload, isNew: boolean, ) => Promise; } // ── Component ──────────────────────────────────────────────────────────────── export function DriverFormModal({ editDriver, branches: branchesProp, onClose, onSubmit, }: DriverFormModalProps) { const isNew = editDriver === null; // ── Local branches (auto-fetched if prop is empty) ───────────────────────── const [branches, setBranches] = useState(branchesProp); useEffect(() => { if (branchesProp.length > 0) { queueMicrotask(() => 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 */ }); }, [branchesProp]); // ── react-hook-form ──────────────────────────────────────────────────────── const { register, handleSubmit, control, setError, setFocus, formState: { errors, isSubmitting }, } = useForm({ // Cast the schema itself — the create/update schemas have structurally // different required fields, so yup can't unify them into one type. resolver: yupResolver((isNew ? createDriverSchema : updateDriverSchema) as any), defaultValues: { name: editDriver?.name ?? "", phone: editDriver?.phone ?? "", email: editDriver?.email ?? "", address: editDriver?.address ?? "", nationality: editDriver?.nationality ?? "", nationalIdType: editDriver?.nationalIdType ?? "", nationalId: (editDriver as Driver & { nationalId?: string })?.nationalId ?? "", nationalIdExpiry: (editDriver as Driver & { nationalIdExpiry?: string })?.nationalIdExpiry?.slice(0, 10) ?? "", gosiNumber: editDriver?.gosiNumber ?? "", licenseNumber: editDriver?.licenseNumber ?? "", licenseType: editDriver?.licenseType ?? "", licenseExpiry: editDriver?.licenseExpiry?.slice(0, 10) ?? "", driverCardNumber: editDriver?.driverCardNumber ?? "", driverCardType: editDriver?.driverCardType ?? "", driverCardExpiry: editDriver?.driverCardExpiry?.slice(0, 10) ?? "", driverType: editDriver?.driverType ?? "", branchId: (editDriver as Driver & { branchId?: string })?.branchId ?? "", status: editDriver?.status ?? "Active", photo: null, nationalPhoto: null, driverCardPhoto: null, }, }); const [apiError, setApiError] = useState(""); // register("name") already attaches its own ref — focusing via setFocus // avoids the ref collision that a separate `ref={firstRef}` would cause. useEffect(() => { setFocus("name"); }, [setFocus]); // ── Submit ──────────────────────────────────────────────────────────────── const submitHandler = useCallback( async (data: DriverFormValues) => { const payload: Record = { name: data.name, phone: data.phone }; if (data.email) payload.email = data.email; if (data.address) payload.address = data.address; if (data.nationality) payload.nationality = data.nationality; if (data.nationalIdType) payload.nationalIdType = data.nationalIdType; if (data.nationalId) payload.nationalId = data.nationalId; if (data.nationalIdExpiry) payload.nationalIdExpiry = toIsoDateTime(data.nationalIdExpiry); if (data.gosiNumber) payload.gosiNumber = data.gosiNumber; if (data.licenseNumber) payload.licenseNumber = data.licenseNumber; if (data.licenseType) payload.licenseType = data.licenseType; if (data.licenseExpiry) payload.licenseExpiry = toIsoDateTime(data.licenseExpiry); if (data.driverCardNumber) payload.driverCardNumber = data.driverCardNumber; if (data.driverCardType) payload.driverCardType = data.driverCardType; if (data.driverCardExpiry) payload.driverCardExpiry = toIsoDateTime(data.driverCardExpiry); if (data.driverType) payload.driverType = data.driverType; if (data.branchId) payload.branchId = data.branchId; if (!isNew) payload.status = data.status; // File fields are excluded from the JSON diff unless actually chosen — // matches the pre-refactor behavior exactly. if (data.photo) payload.photo = data.photo; if (data.nationalPhoto) payload.nationalPhoto = data.nationalPhoto; if (data.driverCardPhoto) payload.driverCardPhoto = data.driverCardPhoto; setApiError(""); try { const ok = await onSubmit(payload as unknown as CreateDriverPayload, isNew); if (ok) { onClose(); } else { setError("name", { message: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً." }); setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."); } } catch (err) { const message = err instanceof Error ? err.message : "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."; setError("name", { message }); setApiError(message); } }, [isNew, onSubmit, onClose, setError], ); // ── Render ──────────────────────────────────────────────────────────────── return ( } >
{errors.name?.type === "manual" && ( setApiError("")} /> )} {/* ── Section: Personal Info ── */}

البيانات الشخصية

{/* ── Section: ID & GOSI ── */}

الهوية والتأمينات

{/* ── Section: License ── */}

رخصة القيادة

{/* ── Section: Driver Card ── */}

بطاقة السائق

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

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

{/* Status — edit only */} {!isNew && ( )}
{/* ── Section: Photos ── */}

الصور والمستندات

جميع حقول الصور اختيارية

{/* File inputs cannot use register() directly — bind via Controller to the existing FileInput component's current/onChange props. */} ( )} /> ( )} /> ( )} />
); }