update page to using same tamplet in all project not using inline code
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { Alert, Spinner } from "../UI";
|
||||
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";
|
||||
@@ -16,37 +16,8 @@ import type {
|
||||
DriverStatus,
|
||||
} from "@/src/types/driver";
|
||||
import type { Branch } from "@/src/types/branch";
|
||||
import { FileInput } from "@/src/Components/Driver/DriverFormModalHelper";
|
||||
|
||||
// ── 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,
|
||||
};
|
||||
// ── Shared bits ──────────────────────────────────────────────────────────────
|
||||
|
||||
const sectionHeadingStyle: React.CSSProperties = {
|
||||
fontSize: 11,
|
||||
@@ -57,12 +28,7 @@ const sectionHeadingStyle: React.CSSProperties = {
|
||||
margin: "0.5rem 0 0",
|
||||
};
|
||||
|
||||
const optionalLabelStyle: React.CSSProperties = {
|
||||
fontSize: 10,
|
||||
fontWeight: 500,
|
||||
color: "var(--color-text-hint)",
|
||||
marginRight: 4,
|
||||
};
|
||||
const FORM_ID = "driver-form";
|
||||
|
||||
// ── Date helper ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -144,9 +110,12 @@ export function DriverFormModal({
|
||||
handleSubmit,
|
||||
control,
|
||||
setError,
|
||||
setFocus,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<DriverFormValues>({
|
||||
resolver: yupResolver(isNew ? createDriverSchema : updateDriverSchema) as never,
|
||||
// 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 ?? "",
|
||||
@@ -174,23 +143,12 @@ export function DriverFormModal({
|
||||
});
|
||||
|
||||
const [apiError, setApiError] = useState("");
|
||||
const firstRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// register("name") already attaches its own ref — focusing via setFocus
|
||||
// avoids the ref collision that a separate `ref={firstRef}` would cause.
|
||||
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 withError = (hasError: boolean): React.CSSProperties => ({
|
||||
...inputBase,
|
||||
...(hasError ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}),
|
||||
});
|
||||
setFocus("name");
|
||||
}, [setFocus]);
|
||||
|
||||
// ── Submit ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -240,377 +198,250 @@ export function DriverFormModal({
|
||||
// ── Render ────────────────────────────────────────────────────────────────
|
||||
|
||||
return (
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="driver-modal-title"
|
||||
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)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
padding: "1rem", overflowY: "auto",
|
||||
}}
|
||||
<Modal
|
||||
open
|
||||
onClose={onClose}
|
||||
size="lg"
|
||||
subtitle={isNew ? "إضافة سائق" : "تعديل سائق"}
|
||||
title={isNew ? "سائق جديد" : editDriver?.name ?? ""}
|
||||
footer={
|
||||
<>
|
||||
<Button variant="secondary" type="button" onClick={onClose} disabled={isSubmitting}>
|
||||
إلغاء
|
||||
</Button>
|
||||
<Button type="submit" form={FORM_ID} loading={isSubmitting}>
|
||||
{isNew ? "إضافة السائق" : "حفظ التغييرات"}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
style={{
|
||||
width: "100%", maxWidth: 640,
|
||||
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",
|
||||
}}
|
||||
<form
|
||||
id={FORM_ID}
|
||||
onSubmit={handleSubmit(submitHandler)}
|
||||
noValidate
|
||||
dir="rtl"
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||
padding: "1.25rem 1.5rem",
|
||||
borderBottom: "1px solid var(--color-border)",
|
||||
background: "var(--color-surface-muted)",
|
||||
}}>
|
||||
<div>
|
||||
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600, margin: 0 }}>
|
||||
{isNew ? "إضافة سائق" : "تعديل سائق"}
|
||||
</p>
|
||||
<h2 id="driver-modal-title" style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}>
|
||||
{isNew ? "سائق جديد" : editDriver?.name}
|
||||
</h2>
|
||||
</div>
|
||||
<button type="button" onClick={onClose} aria-label="إغلاق"
|
||||
style={{
|
||||
width: 34, height: 34, borderRadius: "var(--radius-md)",
|
||||
border: "1px solid var(--color-border)", background: "var(--color-surface)",
|
||||
cursor: "pointer", fontSize: 18, color: "var(--color-text-muted)",
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
}}>
|
||||
×
|
||||
</button>
|
||||
{errors.name?.type === "manual" && (
|
||||
<Alert type="error" message={errors.name.message ?? ""} onClose={() => setApiError("")} />
|
||||
)}
|
||||
|
||||
{/* ── Section: Personal Info ── */}
|
||||
<p style={sectionHeadingStyle}>البيانات الشخصية</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<Input
|
||||
label="الاسم الكامل *"
|
||||
placeholder="محمد عبدالله"
|
||||
dir="rtl"
|
||||
autoComplete="off"
|
||||
error={errors.name?.type !== "manual" ? errors.name?.message : undefined}
|
||||
{...register("name")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="رقم الجوال *"
|
||||
placeholder="05XXXXXXXX"
|
||||
dir="ltr"
|
||||
error={errors.phone?.message}
|
||||
{...register("phone")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="البريد الإلكتروني (اختياري)"
|
||||
type="email"
|
||||
placeholder="example@mail.com"
|
||||
dir="ltr"
|
||||
error={errors.email?.message}
|
||||
{...register("email")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="الجنسية (اختياري)"
|
||||
placeholder="سعودي"
|
||||
dir="rtl"
|
||||
error={errors.nationality?.message}
|
||||
{...register("nationality")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="sm:col-span-2"
|
||||
label="العنوان (اختياري)"
|
||||
placeholder="الرياض، حي..."
|
||||
dir="rtl"
|
||||
error={errors.address?.message}
|
||||
{...register("address")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<form
|
||||
onSubmit={handleSubmit(submitHandler)}
|
||||
noValidate
|
||||
style={{
|
||||
padding: "1.5rem",
|
||||
display: "flex", flexDirection: "column", gap: "1rem",
|
||||
maxHeight: "75vh", overflowY: "auto",
|
||||
}}
|
||||
dir="rtl"
|
||||
>
|
||||
{errors.name?.type === "manual" && (
|
||||
<Alert type="error" message={errors.name.message ?? ""} onClose={() => setApiError("")} />
|
||||
{/* ── Section: ID & GOSI ── */}
|
||||
<p style={sectionHeadingStyle}>الهوية والتأمينات</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<Select
|
||||
label="نوع الهوية (اختياري)"
|
||||
dir="rtl"
|
||||
error={errors.nationalIdType?.message}
|
||||
{...register("nationalIdType")}
|
||||
>
|
||||
<option value="">اختر النوع</option>
|
||||
<option value="NationalID">هوية وطنية</option>
|
||||
<option value="Iqama">إقامة</option>
|
||||
<option value="Passport">جواز سفر</option>
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
label="رقم الهوية (اختياري)"
|
||||
placeholder="1XXXXXXXXX"
|
||||
dir="ltr"
|
||||
error={errors.nationalId?.message}
|
||||
{...register("nationalId")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="انتهاء الهوية (اختياري)"
|
||||
type="date"
|
||||
error={errors.nationalIdExpiry?.message}
|
||||
{...register("nationalIdExpiry")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="رقم GOSI (اختياري)"
|
||||
placeholder="GOSI-XXXX"
|
||||
dir="ltr"
|
||||
error={errors.gosiNumber?.message}
|
||||
{...register("gosiNumber")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Section: License ── */}
|
||||
<p style={sectionHeadingStyle}>رخصة القيادة</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<Input
|
||||
label="رقم الرخصة (اختياري)"
|
||||
placeholder="LIC-XXXX"
|
||||
dir="ltr"
|
||||
error={errors.licenseNumber?.message}
|
||||
{...register("licenseNumber")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="نوع الرخصة (اختياري)"
|
||||
placeholder="خاص / عام"
|
||||
dir="rtl"
|
||||
error={errors.licenseType?.message}
|
||||
{...register("licenseType")}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="انتهاء الرخصة (اختياري)"
|
||||
type="date"
|
||||
error={errors.licenseExpiry?.message}
|
||||
{...register("licenseExpiry")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Section: Driver Card ── */}
|
||||
<p style={sectionHeadingStyle}>بطاقة السائق</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<Input
|
||||
label="رقم البطاقة (اختياري)"
|
||||
placeholder="CARD-XXXX"
|
||||
dir="ltr"
|
||||
error={errors.driverCardNumber?.message}
|
||||
{...register("driverCardNumber")}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="نوع البطاقة (اختياري)"
|
||||
dir="rtl"
|
||||
error={errors.driverCardType?.message}
|
||||
{...register("driverCardType")}
|
||||
>
|
||||
<option value="">اختر النوع</option>
|
||||
<option value="Temporary">مؤقتة</option>
|
||||
<option value="Seasonal">موسمية</option>
|
||||
<option value="Annual">سنوية</option>
|
||||
<option value="Restricted">مقيدة</option>
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
label="انتهاء البطاقة (اختياري)"
|
||||
type="date"
|
||||
error={errors.driverCardExpiry?.message}
|
||||
{...register("driverCardExpiry")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* ── Section: Operational ── */}
|
||||
<p style={sectionHeadingStyle}>بيانات تشغيلية</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<Select
|
||||
label="الفرع (اختياري)"
|
||||
dir="rtl"
|
||||
error={errors.branchId?.message}
|
||||
{...register("branchId")}
|
||||
>
|
||||
<option value="">اختر الفرع</option>
|
||||
{branches.map((b) => (
|
||||
<option key={b.id} value={b.id}>{b.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
label="نوع السائق (اختياري)"
|
||||
placeholder="رئيسي / احتياطي"
|
||||
dir="rtl"
|
||||
error={errors.driverType?.message}
|
||||
{...register("driverType")}
|
||||
/>
|
||||
|
||||
{/* Status — edit only */}
|
||||
{!isNew && (
|
||||
<Select label="الحالة" dir="rtl" {...register("status")}>
|
||||
<option value="Active">نشط</option>
|
||||
<option value="InTrip">في رحلة</option>
|
||||
<option value="Inactive">غير نشط</option>
|
||||
<option value="Suspended">موقوف</option>
|
||||
</Select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Section: Personal Info ── */}
|
||||
<p style={sectionHeadingStyle}>البيانات الشخصية</p>
|
||||
{/* ── Section: Photos ── */}
|
||||
<p style={sectionHeadingStyle}>الصور والمستندات</p>
|
||||
<p style={{ fontSize: 11, color: "var(--color-text-hint)", margin: "0.25rem 0 0", fontWeight: 400 }}>
|
||||
جميع حقول الصور اختيارية
|
||||
</p>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
|
||||
{/* Name — required */}
|
||||
<label style={labelStyle}>
|
||||
الاسم الكامل *
|
||||
<input
|
||||
ref={firstRef}
|
||||
style={withError(!!errors.name && errors.name.type !== "manual")}
|
||||
{...register("name")}
|
||||
placeholder="محمد عبدالله"
|
||||
dir="rtl"
|
||||
autoComplete="off"
|
||||
/>
|
||||
{errors.name && errors.name.type !== "manual" && (
|
||||
<span style={errorTextStyle}>{errors.name.message}</span>
|
||||
)}
|
||||
</label>
|
||||
|
||||
{/* Phone — required */}
|
||||
<label style={labelStyle}>
|
||||
رقم الجوال *
|
||||
<input
|
||||
style={withError(!!errors.phone)}
|
||||
{...register("phone")}
|
||||
placeholder="05XXXXXXXX"
|
||||
dir="ltr"
|
||||
/>
|
||||
{errors.phone && <span style={errorTextStyle}>{errors.phone.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* Email — optional */}
|
||||
<label style={labelStyle}>
|
||||
البريد الإلكتروني
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={withError(!!errors.email)}
|
||||
type="email"
|
||||
{...register("email")}
|
||||
placeholder="example@mail.com"
|
||||
dir="ltr"
|
||||
/>
|
||||
{errors.email && <span style={errorTextStyle}>{errors.email.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* Nationality — optional */}
|
||||
<label style={labelStyle}>
|
||||
الجنسية
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={withError(!!errors.nationality)}
|
||||
{...register("nationality")}
|
||||
placeholder="سعودي"
|
||||
dir="rtl"
|
||||
/>
|
||||
{errors.nationality && <span style={errorTextStyle}>{errors.nationality.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* Address — optional, full width */}
|
||||
<label style={{ ...labelStyle, gridColumn: "1 / -1" }}>
|
||||
العنوان
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={withError(!!errors.address)}
|
||||
{...register("address")}
|
||||
placeholder="الرياض، حي..."
|
||||
dir="rtl"
|
||||
/>
|
||||
{errors.address && <span style={errorTextStyle}>{errors.address.message}</span>}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* ── Section: ID & GOSI ── */}
|
||||
<p style={sectionHeadingStyle}>الهوية والتأمينات</p>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
|
||||
{/* National ID Type — optional */}
|
||||
<label style={labelStyle}>
|
||||
نوع الهوية
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<select
|
||||
style={{ ...withError(!!errors.nationalIdType), cursor: "pointer" }}
|
||||
{...register("nationalIdType")}
|
||||
dir="rtl"
|
||||
>
|
||||
<option value="">اختر النوع</option>
|
||||
<option value="NationalID">هوية وطنية</option>
|
||||
<option value="Iqama">إقامة</option>
|
||||
<option value="Passport">جواز سفر</option>
|
||||
</select>
|
||||
{errors.nationalIdType && <span style={errorTextStyle}>{errors.nationalIdType.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* National ID number */}
|
||||
<label style={labelStyle}>
|
||||
رقم الهوية
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<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" {...register("nationalIdExpiry")} />
|
||||
</label>
|
||||
|
||||
{/* GOSI — optional */}
|
||||
<label style={labelStyle}>
|
||||
رقم GOSI
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input style={inputBase} {...register("gosiNumber")} placeholder="GOSI-XXXX" dir="ltr" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* ── Section: License ── */}
|
||||
<p style={sectionHeadingStyle}>رخصة القيادة</p>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
|
||||
{/* License number — optional */}
|
||||
<label style={labelStyle}>
|
||||
رقم الرخصة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={withError(!!errors.licenseNumber)}
|
||||
{...register("licenseNumber")}
|
||||
placeholder="LIC-XXXX"
|
||||
dir="ltr"
|
||||
/>
|
||||
{errors.licenseNumber && <span style={errorTextStyle}>{errors.licenseNumber.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* License type — optional */}
|
||||
<label style={labelStyle}>
|
||||
نوع الرخصة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input style={inputBase} {...register("licenseType")} placeholder="خاص / عام" dir="rtl" />
|
||||
</label>
|
||||
|
||||
{/* License expiry — optional, validated if provided */}
|
||||
<label style={labelStyle}>
|
||||
انتهاء الرخصة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input style={withError(!!errors.licenseExpiry)} type="date" {...register("licenseExpiry")} />
|
||||
{errors.licenseExpiry && <span style={errorTextStyle}>{errors.licenseExpiry.message}</span>}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* ── Section: Driver Card ── */}
|
||||
<p style={sectionHeadingStyle}>بطاقة السائق</p>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
|
||||
{/* Card number — optional */}
|
||||
<label style={labelStyle}>
|
||||
رقم البطاقة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input style={inputBase} {...register("driverCardNumber")} placeholder="CARD-XXXX" dir="ltr" />
|
||||
</label>
|
||||
|
||||
{/* Card type — optional */}
|
||||
<label style={labelStyle}>
|
||||
نوع البطاقة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<select
|
||||
style={{ ...withError(!!errors.driverCardType), cursor: "pointer" }}
|
||||
{...register("driverCardType")}
|
||||
dir="rtl"
|
||||
>
|
||||
<option value="">اختر النوع</option>
|
||||
<option value="Temporary">مؤقتة</option>
|
||||
<option value="Seasonal">موسمية</option>
|
||||
<option value="Annual">سنوية</option>
|
||||
<option value="Restricted">مقيدة</option>
|
||||
</select>
|
||||
{errors.driverCardType && <span style={errorTextStyle}>{errors.driverCardType.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* Card expiry — optional, validated if provided */}
|
||||
<label style={labelStyle}>
|
||||
انتهاء البطاقة
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input
|
||||
style={withError(!!errors.driverCardExpiry)}
|
||||
type="date"
|
||||
{...register("driverCardExpiry")}
|
||||
/>
|
||||
{errors.driverCardExpiry && (
|
||||
<span style={errorTextStyle}>{errors.driverCardExpiry.message}</span>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* ── Section: Operational ── */}
|
||||
<p style={sectionHeadingStyle}>بيانات تشغيلية</p>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
|
||||
{/* Branch — optional */}
|
||||
<label style={labelStyle}>
|
||||
الفرع
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<select
|
||||
style={{ ...withError(!!errors.branchId), cursor: "pointer" }}
|
||||
{...register("branchId")}
|
||||
dir="rtl"
|
||||
>
|
||||
<option value="">اختر الفرع</option>
|
||||
{branches.map((b) => (
|
||||
<option key={b.id} value={b.id}>{b.name}</option>
|
||||
))}
|
||||
</select>
|
||||
{errors.branchId && <span style={errorTextStyle}>{errors.branchId.message}</span>}
|
||||
</label>
|
||||
|
||||
{/* Driver type — optional */}
|
||||
<label style={labelStyle}>
|
||||
نوع السائق
|
||||
<span style={optionalLabelStyle}>(اختياري)</span>
|
||||
<input style={inputBase} {...register("driverType")} placeholder="رئيسي / احتياطي" dir="rtl" />
|
||||
</label>
|
||||
|
||||
{/* Status — edit only */}
|
||||
{!isNew && (
|
||||
<label style={labelStyle}>
|
||||
الحالة
|
||||
<select style={{ ...inputBase, cursor: "pointer" }} {...register("status")} dir="rtl">
|
||||
<option value="Active">نشط</option>
|
||||
<option value="InTrip">في رحلة</option>
|
||||
<option value="Inactive">غير نشط</option>
|
||||
<option value="Suspended">موقوف</option>
|
||||
</select>
|
||||
</label>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
{/* 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} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Section: Photos ── */}
|
||||
<p style={sectionHeadingStyle}>الصور والمستندات</p>
|
||||
<p style={{ fontSize: 11, color: "var(--color-text-hint)", margin: "0.25rem 0 0", fontWeight: 400 }}>
|
||||
جميع حقول الصور اختيارية
|
||||
</p>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
|
||||
{/* 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 ── */}
|
||||
<div style={{ display: "flex", gap: "0.5rem", justifyContent: "flex-end", paddingTop: "0.5rem" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
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: isSubmitting ? "not-allowed" : "pointer",
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
إلغاء
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
style={{
|
||||
height: 40, padding: "0 1.5rem",
|
||||
borderRadius: "var(--radius-md)",
|
||||
border: "none",
|
||||
background: isSubmitting ? "var(--color-brand-400)" : "var(--color-brand-600)",
|
||||
fontSize: 13, fontWeight: 700, color: "#FFF",
|
||||
cursor: isSubmitting ? "not-allowed" : "pointer",
|
||||
display: "flex", alignItems: "center", gap: 8,
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
{isSubmitting && <Spinner size="sm" className="text-white" />}
|
||||
{isSubmitting ? "جارٍ الحفظ…" : isNew ? "إضافة السائق" : "حفظ التغييرات"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
<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>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user