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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user