refactor(forms): migrate form modals to react-hook-form + yupResolver, and adopt shared UI kit components across Role/Order/User/Trip modals and tables

Replace manual useState + validate() wiring with react-hook-form and
yupResolver across CarFormModal, DriverFormModal, Tripformmodal,
OrderFormModal, BranchFormModal, CarMaintananceFormModal, RoleFormModal,
and UserFormModal, following the existing pattern in Addressformmodal.

- Pin yupResolver's generic explicitly (yupResolver<FormValues>(schema as any))
  since create/update schemas differ structurally and yup can't unify them
  into a single inferred type on its own.
- Replace manual ref-based autofocus (useRef + ref={firstRef}) with
  autoFocus/setFocus to avoid ref collisions with register()'s own ref.
- DriverFormModal: bind photo/nationalPhoto/driverCardPhoto file fields via
  Controller instead of register.
- Tripformmodal: convert startTime/endTime to ISO-8601 via setValueAs while
  keeping the datetime-local input display unaffected.
- OrderFormModal: register nested deliveryAddress/pickupAddress fields via
  dot-path syntax; keep pickupMode as local state; preserve conditional
  address payload assembly.
- CarMaintananceFormModal: keep toNumberOrUndefined cost coercion and
  ISO-8601 date conversion at submit time.
- RoleFormModal: bind permissionIds checkbox group via Controller.
- UserFormModal: add a custom resolver wrapper to preserve the
  empty-password-on-edit behavior (skip password validation when editing
  with a blank password field).

Also replace raw HTML elements with the shared UI kit components
(Modal, Input, Textarea, Select, Button, Alert, Badge, EmptyState) across
form modals, detail modals, and tables:

- RoleFormModal: Modal, Input, Textarea, Button
- OrderFormModal: Modal, Input, Select, Button
- UserDetailModal / Archiveduserdetailmodal: Modal, Alert, Button
- UserTable / Archivedusertable: Badge, Button, EmptyState
- RoleDetailModal / ArchivedRoleDetailModal: Modal, Alert, Select, Button
- RoleTable / ArchivedRoleTable: Button, EmptyState
- Tripreportpanel: Button with built-in loading state instead of manual
  Spinner/disabled/color wiring

StatusBadge, IconBtn, and other per-action custom chips/checkboxes are kept
as-is in every file — the shared Badge/Button components don't support dot
indicators or per-action color-coding, so forcing them in would misshape or
strip semantics from these controls.

Archivedusersmodal, ArchivedRolesModal, and ArchivedTripList required no
changes — already fully compliant with the shared UI kit.

No changes to visual layout, CSS, or Arabic labels/section headings beyond
what's required by the component swaps above.
This commit is contained in:
m7amedez5511
2026-07-21 13:15:22 +03:00
parent 55f76005c9
commit f1a521dd5a
13 changed files with 734 additions and 1099 deletions

View File

@@ -1,31 +1,13 @@
"use client";
import { useEffect, useRef, useState } from "react";
import * as yup from "yup";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { Alert, Button, Input, Modal } from "../UI";
import { createBranchSchema, updateBranchSchema } from "@/src/validations/branch.validator";
import type { Branch, BranchFormData, FormErrors } from "@/src/types/branch";
import type { Branch, BranchFormData } from "@/src/types/branch";
const FORM_ID = "branch-form";
// ── yup validation ────────────────────────────────────────────────────────────
async function validate(data: BranchFormData, isNew: boolean): Promise<FormErrors> {
const schema = isNew ? createBranchSchema : updateBranchSchema;
try {
await schema.validate(data, { abortEarly: false });
return {};
} catch (err) {
if (err instanceof yup.ValidationError) {
return err.inner.reduce<FormErrors>((acc, e) => {
const field = e.path as keyof FormErrors;
if (field && !acc[field]) acc[field] = e.message;
return acc;
}, {});
}
return {};
}
}
// ── Props ─────────────────────────────────────────────────────────────────────
interface BranchFormModalProps {
editBranch: Branch | null;
@@ -37,45 +19,41 @@ interface BranchFormModalProps {
export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormModalProps) {
const isNew = editBranch === null;
const [form, setForm] = useState<BranchFormData>({
name: editBranch?.name ?? "",
email: editBranch?.email ?? "",
phone: editBranch?.phone ?? "",
country: editBranch?.country ?? "SA",
city: editBranch?.city ?? "",
state: editBranch?.state ?? "",
district: editBranch?.district ?? "",
street: editBranch?.street ?? "",
buildingNo: editBranch?.buildingNo ?? "",
unitNo: editBranch?.unitNo ?? "",
zipCode: editBranch?.zipCode ?? "",
latitude: editBranch?.latitude != null ? String(editBranch.latitude) : "",
longitude: editBranch?.longitude != null ? String(editBranch.longitude) : "",
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm<BranchFormData>({
// Cast the schema itself and pin the generic explicitly on yupResolver —
// create/update schemas differ structurally in which fields are required
// (e.g. city/street are optional on update), so yup can't unify them
// into the single flat BranchFormData shape on its own.
resolver: yupResolver<BranchFormData>((isNew ? createBranchSchema : updateBranchSchema) as any),
defaultValues: {
name: editBranch?.name ?? "",
email: editBranch?.email ?? "",
phone: editBranch?.phone ?? "",
country: editBranch?.country ?? "SA",
city: editBranch?.city ?? "",
state: editBranch?.state ?? "",
district: editBranch?.district ?? "",
street: editBranch?.street ?? "",
buildingNo: editBranch?.buildingNo ?? "",
unitNo: editBranch?.unitNo ?? "",
zipCode: editBranch?.zipCode ?? "",
latitude: editBranch?.latitude != null ? String(editBranch.latitude) : "",
longitude: editBranch?.longitude != null ? String(editBranch.longitude) : "",
},
});
const [errors, setErrors] = useState<FormErrors>({});
const [saving, setSaving] = useState(false);
const [apiError, setApiError] = useState("");
const firstInputRef = useRef<HTMLInputElement>(null);
useEffect(() => { firstInputRef.current?.focus(); }, []);
const set = (field: keyof BranchFormData) =>
(e: React.ChangeEvent<HTMLInputElement>) => {
setForm(p => ({ ...p, [field]: e.target.value }));
// clear field error on change
if (errors[field]) setErrors(p => ({ ...p, [field]: undefined }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const errs = await validate(form, isNew);
if (Object.keys(errs).length) { setErrors(errs); return; }
setSaving(true);
setApiError("");
const ok = await onSubmit(form, isNew);
setSaving(false);
if (ok) onClose();
else setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.");
const submitHandler = async (data: BranchFormData) => {
const ok = await onSubmit(data, isNew);
if (ok) {
onClose();
} else {
setError("name", { message: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً." });
}
};
return (
@@ -87,10 +65,10 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
size="md"
footer={
<>
<Button variant="secondary" onClick={onClose} disabled={saving}>
<Button variant="secondary" onClick={onClose} disabled={isSubmitting}>
إلغاء
</Button>
<Button type="submit" form={FORM_ID} loading={saving}>
<Button type="submit" form={FORM_ID} loading={isSubmitting}>
{isNew ? "إضافة الفرع" : "حفظ التغييرات"}
</Button>
</>
@@ -98,21 +76,22 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
>
<form
id={FORM_ID}
onSubmit={handleSubmit}
onSubmit={handleSubmit(submitHandler)}
noValidate
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
>
{apiError && <Alert type="error" message={apiError} onClose={() => setApiError("")} />}
{errors.name?.type === "manual" && (
<Alert type="error" message={errors.name.message ?? ""} onClose={() => {}} />
)}
{/* name */}
<Input
ref={firstInputRef}
label="اسم الفرع *"
value={form.name}
onChange={set("name")}
error={errors.name}
{...register("name")}
error={errors.name && errors.name.type !== "manual" ? errors.name.message : undefined}
placeholder="فرع الرياض"
autoComplete="organization"
autoFocus
dir="rtl"
/>
@@ -121,9 +100,8 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
<Input
label="البريد الإلكتروني"
type="email"
value={form.email}
onChange={set("email")}
error={errors.email}
{...register("email")}
error={errors.email?.message}
placeholder="branch@co.sa"
autoComplete="email"
dir="ltr"
@@ -131,9 +109,8 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
<Input
label="رقم الهاتف"
type="tel"
value={form.phone}
onChange={set("phone")}
error={errors.phone}
{...register("phone")}
error={errors.phone?.message}
placeholder="+966 5x xxx xxxx"
autoComplete="tel"
dir="ltr"
@@ -144,17 +121,15 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<Input
label="المدينة *"
value={form.city}
onChange={set("city")}
error={errors.city}
{...register("city")}
error={errors.city?.message}
placeholder="الرياض"
dir="rtl"
/>
<Input
label="الشارع *"
value={form.street}
onChange={set("street")}
error={errors.street}
{...register("street")}
error={errors.street?.message}
placeholder="شارع الملك فهد"
dir="rtl"
/>
@@ -164,17 +139,15 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<Input
label="المنطقة"
value={form.state}
onChange={set("state")}
error={errors.state}
{...register("state")}
error={errors.state?.message}
placeholder="منطقة الرياض"
dir="rtl"
/>
<Input
label="الحي"
value={form.district}
onChange={set("district")}
error={errors.district}
{...register("district")}
error={errors.district?.message}
placeholder="حي العليا"
dir="rtl"
/>
@@ -184,25 +157,22 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
<Input
label="رقم المبنى"
value={form.buildingNo}
onChange={set("buildingNo")}
error={errors.buildingNo}
{...register("buildingNo")}
error={errors.buildingNo?.message}
placeholder="1234"
dir="ltr"
/>
<Input
label="رقم الوحدة"
value={form.unitNo}
onChange={set("unitNo")}
error={errors.unitNo}
{...register("unitNo")}
error={errors.unitNo?.message}
placeholder="5"
dir="ltr"
/>
<Input
label="الرمز البريدي"
value={form.zipCode}
onChange={set("zipCode")}
error={errors.zipCode}
{...register("zipCode")}
error={errors.zipCode?.message}
placeholder="12345"
dir="ltr"
/>
@@ -211,9 +181,8 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
{/* country */}
<Input
label="الدولة"
value={form.country}
onChange={set("country")}
error={errors.country}
{...register("country")}
error={errors.country?.message}
placeholder="SA"
dir="ltr"
/>
@@ -222,18 +191,16 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<Input
label="خط العرض (اختياري)"
value={form.latitude}
onChange={set("latitude")}
error={errors.latitude}
{...register("latitude")}
error={errors.latitude?.message}
placeholder="24.7136"
dir="ltr"
inputMode="decimal"
/>
<Input
label="خط الطول (اختياري)"
value={form.longitude}
onChange={set("longitude")}
error={errors.longitude}
{...register("longitude")}
error={errors.longitude?.message}
placeholder="46.6753"
dir="ltr"
inputMode="decimal"