make header one in all page
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import * as yup from "yup";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { Alert, Spinner } from "../UI";
|
||||
import { get } from "@/src/services/api";
|
||||
import { getStoredToken } from "@/src/lib/auth";
|
||||
import { createDriverSchema, updateDriverSchema } from "@/src/validations/driver.validator";
|
||||
import type { DriverSchemaErrors } from "@/src/validations/driver.validator";
|
||||
import type {
|
||||
Driver,
|
||||
CreateDriverPayload,
|
||||
@@ -72,6 +72,32 @@ function toIsoDateTime(val: string): string {
|
||||
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 {
|
||||
@@ -104,161 +130,123 @@ export function DriverFormModal({
|
||||
const token = getStoredToken();
|
||||
get<{ data: { data: Branch[] } }>("branches?limit=100", token)
|
||||
.then((res) => {
|
||||
const list =
|
||||
(res as unknown as { data: { data: Branch[] } }).data?.data ?? [];
|
||||
const list = (res as unknown as { data: { data: Branch[] } }).data?.data ?? [];
|
||||
setBranches(list);
|
||||
})
|
||||
.catch(() => { /* silently ignore */ });
|
||||
.catch(() => {
|
||||
/* silently ignore */
|
||||
});
|
||||
}, [branchesProp]);
|
||||
|
||||
// ── Form state ────────────────────────────────────────────────────────────
|
||||
const [name, setName] = useState(editDriver?.name ?? "");
|
||||
const [phone, setPhone] = useState(editDriver?.phone ?? "");
|
||||
const [email, setEmail] = useState(editDriver?.email ?? "");
|
||||
const [address, setAddress] = useState(editDriver?.address ?? "");
|
||||
const [nationality, setNationality] = useState(editDriver?.nationality ?? "");
|
||||
const [nationalIdType, setNationalIdType] = useState<NationalIdType | "">(
|
||||
editDriver?.nationalIdType ?? "",
|
||||
);
|
||||
const [nationalId, setNationalId] = useState(
|
||||
(editDriver as Driver & { nationalId?: string })?.nationalId ?? "",
|
||||
);
|
||||
const [nationalIdExpiry, setNationalIdExpiry] = useState(
|
||||
(editDriver as Driver & { nationalIdExpiry?: string })?.nationalIdExpiry?.slice(0, 10) ?? "",
|
||||
);
|
||||
const [gosiNumber, setGosiNumber] = useState(editDriver?.gosiNumber ?? "");
|
||||
const [licenseNumber, setLicenseNumber] = useState(editDriver?.licenseNumber ?? "");
|
||||
const [licenseType, setLicenseType] = useState(editDriver?.licenseType ?? "");
|
||||
const [licenseExpiry, setLicenseExpiry] = useState(
|
||||
editDriver?.licenseExpiry?.slice(0, 10) ?? "",
|
||||
);
|
||||
const [driverCardNumber, setDriverCardNumber] = useState(editDriver?.driverCardNumber ?? "");
|
||||
const [driverCardType, setDriverCardType] = useState<DriverCardType | "">(
|
||||
editDriver?.driverCardType ?? "",
|
||||
);
|
||||
const [driverCardExpiry, setDriverCardExpiry] = useState(
|
||||
editDriver?.driverCardExpiry?.slice(0, 10) ?? "",
|
||||
);
|
||||
const [driverType, setDriverType] = useState(editDriver?.driverType ?? "");
|
||||
const [branchId, setBranchId] = useState(
|
||||
(editDriver as Driver & { branchId?: string })?.branchId ?? "",
|
||||
);
|
||||
const [status, setStatus] = useState<DriverStatus>(editDriver?.status ?? "Active");
|
||||
// ── react-hook-form ────────────────────────────────────────────────────────
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
control,
|
||||
setError,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<DriverFormValues>({
|
||||
resolver: yupResolver(isNew ? createDriverSchema : updateDriverSchema) as never,
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
// Photo state (new uploads only)
|
||||
const [photo, setPhoto] = useState<File | null>(null);
|
||||
const [nationalPhoto, setNationalPhoto] = useState<File | null>(null);
|
||||
const [driverCardPhoto, setDriverCardPhoto] = useState<File | null>(null);
|
||||
|
||||
const [errors, setErrors] = useState<DriverSchemaErrors>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [apiError, setApiError] = useState("");
|
||||
const firstRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => { firstRef.current?.focus(); }, []);
|
||||
useEffect(() => {
|
||||
const h = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
|
||||
firstRef.current?.focus();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const h = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") onClose();
|
||||
};
|
||||
window.addEventListener("keydown", h);
|
||||
return () => window.removeEventListener("keydown", h);
|
||||
}, [onClose]);
|
||||
|
||||
// ── Input error style ──────────────────────────────────────────────────────
|
||||
const inputStyle = (field: keyof DriverSchemaErrors): React.CSSProperties => ({
|
||||
const withError = (hasError: boolean): React.CSSProperties => ({
|
||||
...inputBase,
|
||||
...(errors[field] ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}),
|
||||
...(hasError ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}),
|
||||
});
|
||||
|
||||
const clearFieldError = (field: keyof DriverSchemaErrors) =>
|
||||
setErrors((p) => ({ ...p, [field]: undefined }));
|
||||
// ── Submit ────────────────────────────────────────────────────────────────
|
||||
|
||||
// ── Submit with yup validation ────────────────────────────────────────────
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const submitHandler = useCallback(
|
||||
async (data: DriverFormValues) => {
|
||||
const payload: Record<string, unknown> = { 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;
|
||||
|
||||
// Build the raw object to validate
|
||||
const raw: Record<string, unknown> = {
|
||||
name: name || undefined,
|
||||
phone: phone || undefined,
|
||||
email: email || undefined,
|
||||
address: address || undefined,
|
||||
nationality: nationality || undefined,
|
||||
nationalIdType: nationalIdType || undefined,
|
||||
gosiNumber: gosiNumber || undefined,
|
||||
licenseNumber: licenseNumber || undefined,
|
||||
licenseType: licenseType || undefined,
|
||||
licenseExpiry: licenseExpiry || undefined,
|
||||
driverCardNumber: driverCardNumber || undefined,
|
||||
driverCardType: driverCardType || undefined,
|
||||
driverCardExpiry: driverCardExpiry || undefined,
|
||||
driverType: driverType || undefined,
|
||||
branchId: branchId || undefined,
|
||||
...(!isNew ? { status } : {}),
|
||||
};
|
||||
|
||||
try {
|
||||
const schema = isNew ? createDriverSchema : updateDriverSchema;
|
||||
await schema.validate(raw, { abortEarly: false });
|
||||
} catch (err) {
|
||||
if (err instanceof yup.ValidationError) {
|
||||
const fieldErrors: DriverSchemaErrors = {};
|
||||
err.inner.forEach((e) => {
|
||||
if (e.path) {
|
||||
fieldErrors[e.path as keyof DriverSchemaErrors] = e.message;
|
||||
}
|
||||
});
|
||||
setErrors(fieldErrors);
|
||||
return;
|
||||
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],
|
||||
);
|
||||
|
||||
const payload: Record<string, unknown> = { name, phone };
|
||||
if (email) payload.email = email;
|
||||
if (address) payload.address = address;
|
||||
if (nationality) payload.nationality = nationality;
|
||||
if (nationalIdType) payload.nationalIdType = nationalIdType;
|
||||
if (nationalId) payload.nationalId = nationalId;
|
||||
if (nationalIdExpiry) payload.nationalIdExpiry = toIsoDateTime(nationalIdExpiry);
|
||||
if (gosiNumber) payload.gosiNumber = gosiNumber;
|
||||
if (licenseNumber) payload.licenseNumber = licenseNumber;
|
||||
if (licenseType) payload.licenseType = licenseType;
|
||||
if (licenseExpiry) payload.licenseExpiry = toIsoDateTime(licenseExpiry);
|
||||
if (driverCardNumber) payload.driverCardNumber = driverCardNumber;
|
||||
if (driverCardType) payload.driverCardType = driverCardType;
|
||||
if (driverCardExpiry) payload.driverCardExpiry = toIsoDateTime(driverCardExpiry);
|
||||
if (driverType) payload.driverType = driverType;
|
||||
if (branchId) payload.branchId = branchId;
|
||||
if (!isNew) payload.status = status;
|
||||
if (photo) payload.photo = photo;
|
||||
if (nationalPhoto) payload.nationalPhoto = nationalPhoto;
|
||||
if (driverCardPhoto) payload.driverCardPhoto = driverCardPhoto;
|
||||
// ── Render ────────────────────────────────────────────────────────────────
|
||||
|
||||
setSaving(true);
|
||||
setApiError("");
|
||||
|
||||
try {
|
||||
const ok = await onSubmit(payload as unknown as CreateDriverPayload, isNew);
|
||||
if (ok) {
|
||||
onClose();
|
||||
} else {
|
||||
// onSubmit returned false without throwing — show generic fallback
|
||||
setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.");
|
||||
}
|
||||
} catch (err) {
|
||||
// onSubmit threw — surface the real API error message
|
||||
const message = err instanceof Error ? err.message : "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.";
|
||||
setApiError(message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// ── Render ─────────────────────────────────────────────────────────────────
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="driver-modal-title"
|
||||
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
style={{
|
||||
position: "fixed", inset: 0, zIndex: 50,
|
||||
background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
|
||||
@@ -305,7 +293,7 @@ export function DriverFormModal({
|
||||
|
||||
{/* Form */}
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
onSubmit={handleSubmit(submitHandler)}
|
||||
noValidate
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
@@ -314,8 +302,8 @@ export function DriverFormModal({
|
||||
}}
|
||||
dir="rtl"
|
||||
>
|
||||
{apiError && (
|
||||
<Alert type="error" message={apiError} onClose={() => setApiError("")} />
|
||||
{errors.name?.type === "manual" && (
|
||||
<Alert type="error" message={errors.name.message ?? ""} onClose={() => setApiError("")} />
|
||||
)}
|
||||
|
||||
{/* ── Section: Personal Info ── */}
|
||||
@@ -327,27 +315,27 @@ export function DriverFormModal({
|
||||
الاسم الكامل *
|
||||
<input
|
||||
ref={firstRef}
|
||||
style={inputStyle("name")}
|
||||
value={name}
|
||||
onChange={(e) => { setName(e.target.value); clearFieldError("name"); }}
|
||||
style={withError(!!errors.name && errors.name.type !== "manual")}
|
||||
{...register("name")}
|
||||
placeholder="محمد عبدالله"
|
||||
dir="rtl"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{errors.name && <span style={errorTextStyle}>{errors.name}</span>}
|
||||
{errors.name && errors.name.type !== "manual" && (
|
||||
<span style={errorTextStyle}>{errors.name.message}</span>
|
||||
)}
|
||||
</label>
|
||||
|
||||
{/* Phone — required */}
|
||||
<label style={labelStyle}>
|
||||
رقم الجوال *
|
||||
<input
|
||||
style={inputStyle("phone")}
|
||||
value={phone}
|
||||
onChange={(e) => { setPhone(e.target.value); clearFieldError("phone"); }}
|
||||
style={withError(!!errors.phone)}
|
||||
{...register("phone")}
|
||||
placeholder="05XXXXXXXX"
|
||||
dir="ltr"
|
||||
/>
|
||||
{errors.phone && <span style={errorTextStyle}>{errors.phone}</span>}
|
||||
{errors.phone && <span style={errorTextStyle}>{errors.phone.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* Email — optional */}
|
||||
@@ -355,14 +343,13 @@ export function DriverFormModal({
|
||||
البريد الإلكتروني
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputStyle("email")}
|
||||
style={withError(!!errors.email)}
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => { setEmail(e.target.value); clearFieldError("email"); }}
|
||||
{...register("email")}
|
||||
placeholder="example@mail.com"
|
||||
dir="ltr"
|
||||
/>
|
||||
{errors.email && <span style={errorTextStyle}>{errors.email}</span>}
|
||||
{errors.email && <span style={errorTextStyle}>{errors.email.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* Nationality — optional */}
|
||||
@@ -370,13 +357,12 @@ export function DriverFormModal({
|
||||
الجنسية
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputBase}
|
||||
value={nationality}
|
||||
onChange={(e) => { setNationality(e.target.value); clearFieldError("nationality"); }}
|
||||
style={withError(!!errors.nationality)}
|
||||
{...register("nationality")}
|
||||
placeholder="سعودي"
|
||||
dir="rtl"
|
||||
/>
|
||||
{errors.nationality && <span style={errorTextStyle}>{errors.nationality}</span>}
|
||||
{errors.nationality && <span style={errorTextStyle}>{errors.nationality.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* Address — optional, full width */}
|
||||
@@ -384,13 +370,12 @@ export function DriverFormModal({
|
||||
العنوان
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputStyle("address")}
|
||||
value={address}
|
||||
onChange={(e) => { setAddress(e.target.value); clearFieldError("address"); }}
|
||||
style={withError(!!errors.address)}
|
||||
{...register("address")}
|
||||
placeholder="الرياض، حي..."
|
||||
dir="rtl"
|
||||
/>
|
||||
{errors.address && <span style={errorTextStyle}>{errors.address}</span>}
|
||||
{errors.address && <span style={errorTextStyle}>{errors.address.message}</span>}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -403,9 +388,8 @@ export function DriverFormModal({
|
||||
نوع الهوية
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<select
|
||||
style={{ ...inputStyle("nationalIdType"), cursor: "pointer" }}
|
||||
value={nationalIdType}
|
||||
onChange={(e) => { setNationalIdType(e.target.value as NationalIdType | ""); clearFieldError("nationalIdType"); }}
|
||||
style={{ ...withError(!!errors.nationalIdType), cursor: "pointer" }}
|
||||
{...register("nationalIdType")}
|
||||
dir="rtl"
|
||||
>
|
||||
<option value="">اختر النوع</option>
|
||||
@@ -413,45 +397,28 @@ export function DriverFormModal({
|
||||
<option value="Iqama">إقامة</option>
|
||||
<option value="Passport">جواز سفر</option>
|
||||
</select>
|
||||
{errors.nationalIdType && <span style={errorTextStyle}>{errors.nationalIdType}</span>}
|
||||
{errors.nationalIdType && <span style={errorTextStyle}>{errors.nationalIdType.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* National ID number */}
|
||||
<label style={labelStyle}>
|
||||
رقم الهوية
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputBase}
|
||||
value={nationalId}
|
||||
onChange={(e) => setNationalId(e.target.value)}
|
||||
placeholder="1XXXXXXXXX"
|
||||
dir="ltr"
|
||||
/>
|
||||
<input style={inputBase} {...register("nationalId")} placeholder="1XXXXXXXXX" dir="ltr" />
|
||||
</label>
|
||||
|
||||
{/* National ID expiry */}
|
||||
<label style={labelStyle}>
|
||||
انتهاء الهوية
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputBase}
|
||||
type="date"
|
||||
value={nationalIdExpiry}
|
||||
onChange={(e) => setNationalIdExpiry(e.target.value)}
|
||||
/>
|
||||
<input style={inputBase} type="date" {...register("nationalIdExpiry")} />
|
||||
</label>
|
||||
|
||||
{/* GOSI — optional */}
|
||||
<label style={labelStyle}>
|
||||
رقم GOSI
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputBase}
|
||||
value={gosiNumber}
|
||||
onChange={(e) => setGosiNumber(e.target.value)}
|
||||
placeholder="GOSI-XXXX"
|
||||
dir="ltr"
|
||||
/>
|
||||
<input style={inputBase} {...register("gosiNumber")} placeholder="GOSI-XXXX" dir="ltr" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -464,39 +431,27 @@ export function DriverFormModal({
|
||||
رقم الرخصة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputStyle("licenseNumber")}
|
||||
value={licenseNumber}
|
||||
onChange={(e) => { setLicenseNumber(e.target.value); clearFieldError("licenseNumber"); }}
|
||||
style={withError(!!errors.licenseNumber)}
|
||||
{...register("licenseNumber")}
|
||||
placeholder="LIC-XXXX"
|
||||
dir="ltr"
|
||||
/>
|
||||
{errors.licenseNumber && <span style={errorTextStyle}>{errors.licenseNumber}</span>}
|
||||
{errors.licenseNumber && <span style={errorTextStyle}>{errors.licenseNumber.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* License type — optional */}
|
||||
<label style={labelStyle}>
|
||||
نوع الرخصة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputBase}
|
||||
value={licenseType}
|
||||
onChange={(e) => setLicenseType(e.target.value)}
|
||||
placeholder="خاص / عام"
|
||||
dir="rtl"
|
||||
/>
|
||||
<input style={inputBase} {...register("licenseType")} placeholder="خاص / عام" dir="rtl" />
|
||||
</label>
|
||||
|
||||
{/* License expiry — optional, validated if provided */}
|
||||
<label style={labelStyle}>
|
||||
انتهاء الرخصة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputStyle("licenseExpiry")}
|
||||
type="date"
|
||||
value={licenseExpiry}
|
||||
onChange={(e) => { setLicenseExpiry(e.target.value); clearFieldError("licenseExpiry"); }}
|
||||
/>
|
||||
{errors.licenseExpiry && <span style={errorTextStyle}>{errors.licenseExpiry}</span>}
|
||||
<input style={withError(!!errors.licenseExpiry)} type="date" {...register("licenseExpiry")} />
|
||||
{errors.licenseExpiry && <span style={errorTextStyle}>{errors.licenseExpiry.message}</span>}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -508,13 +463,7 @@ export function DriverFormModal({
|
||||
<label style={labelStyle}>
|
||||
رقم البطاقة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputBase}
|
||||
value={driverCardNumber}
|
||||
onChange={(e) => setDriverCardNumber(e.target.value)}
|
||||
placeholder="CARD-XXXX"
|
||||
dir="ltr"
|
||||
/>
|
||||
<input style={inputBase} {...register("driverCardNumber")} placeholder="CARD-XXXX" dir="ltr" />
|
||||
</label>
|
||||
|
||||
{/* Card type — optional */}
|
||||
@@ -522,9 +471,8 @@ export function DriverFormModal({
|
||||
نوع البطاقة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<select
|
||||
style={{ ...inputStyle("driverCardType"), cursor: "pointer" }}
|
||||
value={driverCardType}
|
||||
onChange={(e) => { setDriverCardType(e.target.value as DriverCardType | ""); clearFieldError("driverCardType"); }}
|
||||
style={{ ...withError(!!errors.driverCardType), cursor: "pointer" }}
|
||||
{...register("driverCardType")}
|
||||
dir="rtl"
|
||||
>
|
||||
<option value="">اختر النوع</option>
|
||||
@@ -533,7 +481,7 @@ export function DriverFormModal({
|
||||
<option value="Annual">سنوية</option>
|
||||
<option value="Restricted">مقيدة</option>
|
||||
</select>
|
||||
{errors.driverCardType && <span style={errorTextStyle}>{errors.driverCardType}</span>}
|
||||
{errors.driverCardType && <span style={errorTextStyle}>{errors.driverCardType.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* Card expiry — optional, validated if provided */}
|
||||
@@ -541,12 +489,13 @@ export function DriverFormModal({
|
||||
انتهاء البطاقة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputStyle("driverCardExpiry")}
|
||||
style={withError(!!errors.driverCardExpiry)}
|
||||
type="date"
|
||||
value={driverCardExpiry}
|
||||
onChange={(e) => { setDriverCardExpiry(e.target.value); clearFieldError("driverCardExpiry"); }}
|
||||
{...register("driverCardExpiry")}
|
||||
/>
|
||||
{errors.driverCardExpiry && <span style={errorTextStyle}>{errors.driverCardExpiry}</span>}
|
||||
{errors.driverCardExpiry && (
|
||||
<span style={errorTextStyle}>{errors.driverCardExpiry.message}</span>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -559,9 +508,8 @@ export function DriverFormModal({
|
||||
الفرع
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<select
|
||||
style={{ ...inputStyle("branchId"), cursor: "pointer" }}
|
||||
value={branchId}
|
||||
onChange={(e) => { setBranchId(e.target.value); clearFieldError("branchId"); }}
|
||||
style={{ ...withError(!!errors.branchId), cursor: "pointer" }}
|
||||
{...register("branchId")}
|
||||
dir="rtl"
|
||||
>
|
||||
<option value="">اختر الفرع</option>
|
||||
@@ -569,32 +517,21 @@ export function DriverFormModal({
|
||||
<option key={b.id} value={b.id}>{b.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.branchId && <span style={errorTextStyle}>{errors.branchId}</span>}
|
||||
{errors.branchId && <span style={errorTextStyle}>{errors.branchId.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* Driver type — optional */}
|
||||
<label style={labelStyle}>
|
||||
نوع السائق
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={inputBase}
|
||||
value={driverType}
|
||||
onChange={(e) => setDriverType(e.target.value)}
|
||||
placeholder="رئيسي / احتياطي"
|
||||
dir="rtl"
|
||||
/>
|
||||
<input style={inputBase} {...register("driverType")} placeholder="رئيسي / احتياطي" dir="rtl" />
|
||||
</label>
|
||||
|
||||
{/* Status — edit only */}
|
||||
{!isNew && (
|
||||
<label style={labelStyle}>
|
||||
الحالة
|
||||
<select
|
||||
style={{ ...inputBase, cursor: "pointer" }}
|
||||
value={status}
|
||||
onChange={(e) => setStatus(e.target.value as DriverStatus)}
|
||||
dir="rtl"
|
||||
>
|
||||
<select style={{ ...inputBase, cursor: "pointer" }} {...register("status")} dir="rtl">
|
||||
<option value="Active">نشط</option>
|
||||
<option value="InTrip">في رحلة</option>
|
||||
<option value="Inactive">غير نشط</option>
|
||||
@@ -611,9 +548,29 @@ export function DriverFormModal({
|
||||
</p>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
|
||||
<FileInput label="صورة السائق" current={photo} onChange={setPhoto} />
|
||||
<FileInput label="صورة الهوية" current={nationalPhoto} onChange={setNationalPhoto} />
|
||||
<FileInput label="صورة البطاقة" current={driverCardPhoto} onChange={setDriverCardPhoto} />
|
||||
{/* File inputs cannot use register() directly — bind via Controller
|
||||
to the existing FileInput component's current/onChange props. */}
|
||||
<Controller
|
||||
name="photo"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FileInput label="صورة السائق" current={field.value} onChange={field.onChange} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="nationalPhoto"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FileInput label="صورة الهوية" current={field.value} onChange={field.onChange} />
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="driverCardPhoto"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FileInput label="صورة البطاقة" current={field.value} onChange={field.onChange} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Actions ── */}
|
||||
@@ -621,14 +578,14 @@ export function DriverFormModal({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
disabled={saving}
|
||||
disabled={isSubmitting}
|
||||
style={{
|
||||
height: 40, padding: "0 1.25rem",
|
||||
borderRadius: "var(--radius-md)",
|
||||
border: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface)",
|
||||
fontSize: 13, fontWeight: 600, color: "var(--color-text-secondary)",
|
||||
cursor: saving ? "not-allowed" : "pointer",
|
||||
cursor: isSubmitting ? "not-allowed" : "pointer",
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
@@ -636,20 +593,20 @@ export function DriverFormModal({
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
disabled={isSubmitting}
|
||||
style={{
|
||||
height: 40, padding: "0 1.5rem",
|
||||
borderRadius: "var(--radius-md)",
|
||||
border: "none",
|
||||
background: saving ? "var(--color-brand-400)" : "var(--color-brand-600)",
|
||||
background: isSubmitting ? "var(--color-brand-400)" : "var(--color-brand-600)",
|
||||
fontSize: 13, fontWeight: 700, color: "#FFF",
|
||||
cursor: saving ? "not-allowed" : "pointer",
|
||||
cursor: isSubmitting ? "not-allowed" : "pointer",
|
||||
display: "flex", alignItems: "center", gap: 8,
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
{saving && <Spinner size="sm" className="text-white" />}
|
||||
{saving ? "جارٍ الحفظ…" : isNew ? "إضافة السائق" : "حفظ التغييرات"}
|
||||
{isSubmitting && <Spinner size="sm" className="text-white" />}
|
||||
{isSubmitting ? "جارٍ الحفظ…" : isNew ? "إضافة السائق" : "حفظ التغييرات"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user