diff --git a/src/Components/Branch/BranchFormModal.tsx b/src/Components/Branch/BranchFormModal.tsx index 213109d..6f58d05 100644 --- a/src/Components/Branch/BranchFormModal.tsx +++ b/src/Components/Branch/BranchFormModal.tsx @@ -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 { - const schema = isNew ? createBranchSchema : updateBranchSchema; - try { - await schema.validate(data, { abortEarly: false }); - return {}; - } catch (err) { - if (err instanceof yup.ValidationError) { - return err.inner.reduce((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({ - 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({ + // 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((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({}); - const [saving, setSaving] = useState(false); - const [apiError, setApiError] = useState(""); - const firstInputRef = useRef(null); - useEffect(() => { firstInputRef.current?.focus(); }, []); - - const set = (field: keyof BranchFormData) => - (e: React.ChangeEvent) => { - 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={ <> - - @@ -98,21 +76,22 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod >
- {apiError && setApiError("")} />} + {errors.name?.type === "manual" && ( + {}} /> + )} {/* name */} @@ -121,9 +100,8 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod @@ -164,17 +139,15 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
@@ -184,25 +157,22 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
@@ -211,9 +181,8 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod {/* country */} @@ -222,18 +191,16 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod
` onChange handler, it -// stays a string and fails the backend's strict `z.number()` check. Coerce -// defensively both when hydrating the form AND right before building the -// submit payload, so this can never happen regardless of the source. +// `number`. Coerce defensively both when hydrating the form AND right before +// building the submit payload, so this can never slip through as a string. function toNumberOrUndefined(v: unknown): number | undefined { if (v === undefined || v === null || v === "") return undefined; @@ -31,30 +27,6 @@ function toNumberOrUndefined(v: unknown): number | undefined { return Number.isNaN(n) ? undefined : n; } -// ── yup validation ──────────────────────────────────────────────────────────── -// Checks the form values against the right schema and turns any problems -// into a simple field -> message map the inputs below can read from. - -async function validate( - form: Partial, - isNew: boolean, -): Promise { - const schema = isNew ? createMaintenanceSchema : updateMaintenanceSchema; - try { - await schema.validate(form, { abortEarly: false }); - return {}; - } catch (err) { - if (err instanceof yup.ValidationError) { - return err.inner.reduce((acc, e) => { - const field = e.path as keyof MaintenanceFormErrors; - if (field && !acc[field]) acc[field] = e.message; - return acc; - }, {}); - } - return {}; - } -} - // ── Date helper ─────────────────────────────────────────────────────────────── // gives "YYYY-MM-DD"; the backend expects full ISO-8601. @@ -64,6 +36,19 @@ function toIsoDateTime(val: string): string { return `${val}T00:00:00.000Z`; } +// ── Form values shape ──────────────────────────────────────────────────────── +// `cost` stays number | undefined (via setValueAs) so an empty input maps to +// `undefined` rather than NaN; startAt/endAt stay as raw "YYYY-MM-DD" strings +// while typed — yup's dateString test accepts that format directly — and are +// only converted to full ISO-8601 right before the payload is built. + +interface MaintenanceFormValues { + reason: string; + cost: number | undefined; + startAt: string; + endAt: string; +} + // ── Props ───────────────────────────────────────────────────────────────────── interface CarMaintenanceFormModalProps { @@ -88,64 +73,52 @@ export function CarMaintenanceFormModal({ }: CarMaintenanceFormModalProps) { const isNew = editRecord === null; - // ── Form state ───────────────────────────────────────────────────────────── - const [reason, setReason] = useState(editRecord?.reason ?? ""); - // Coerced defensively: editRecord.cost may arrive as a numeric string from - // the backend (e.g. a Decimal column serialized to JSON as "150"). - const [cost, setCost] = useState(toNumberOrUndefined(editRecord?.cost)); - const [startAt, setStartAt] = useState(editRecord?.startAt?.slice(0, 10) ?? ""); - const [endAt, setEndAt] = useState(editRecord?.endAt?.slice(0, 10) ?? ""); + const { + register, + handleSubmit, + setError, + formState: { errors, isSubmitting }, + } = useForm({ + // Cast the schema itself and pin the generic explicitly — create/update + // schemas differ structurally in which fields are required, so yup can't + // unify them into MaintenanceFormValues on its own. + resolver: yupResolver( + (isNew ? createMaintenanceSchema : updateMaintenanceSchema) as any, + ), + defaultValues: { + reason: editRecord?.reason ?? "", + // Coerced defensively: editRecord.cost may arrive as a numeric string + // from the backend (e.g. a Decimal column serialized to JSON as "150"). + cost: toNumberOrUndefined(editRecord?.cost), + startAt: editRecord?.startAt?.slice(0, 10) ?? "", + endAt: editRecord?.endAt?.slice(0, 10) ?? "", + }, + }); - const [errors, setErrors] = useState({}); - const [saving, setSaving] = useState(false); - const [apiError, setApiError] = useState(""); - const firstRef = useRef(null); - - useEffect(() => { firstRef.current?.focus(); }, []); - - const parseNum = (v: string): number | undefined => - v.trim() === "" ? undefined : Number(v); - - const clearFieldError = (field: keyof MaintenanceFormErrors) => - setErrors((p) => ({ ...p, [field]: undefined })); + const costField = register("cost", { + setValueAs: (v) => (v === "" || v === null ? undefined : Number(v)), + }); // ── Submit ──────────────────────────────────────────────────────────────── - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - + const submitHandler = async (data: MaintenanceFormValues) => { // Belt-and-braces: re-coerce cost right before it's used, in case it - // somehow slipped back into a string (e.g. untouched value hydrated - // from a record whose `cost` came back as a numeric string). - const safeCost = toNumberOrUndefined(cost); + // somehow slipped back into a string. + const safeCost = toNumberOrUndefined(data.cost); - // Build a snapshot for yup — include everything so optional rules also run. - const formSnapshot: Partial = { - reason, - cost: safeCost, - ...(startAt && { startAt }), - ...(endAt && { endAt }), - }; - - const errs = await validate(formSnapshot, isNew); - if (Object.keys(errs).length) { - setErrors(errs); - return; - } - - // Build the final payload — dates go out as full ISO-8601 strings, and - // cost always goes out as a real number, never a string. - const raw: Record = { reason, cost: safeCost }; - if (startAt) raw.startAt = toIsoDateTime(startAt); - if (endAt) raw.endAt = toIsoDateTime(endAt); + // Dates go out as full ISO-8601 strings, and cost always goes out as a + // real number, never a string. + const raw: Record = { reason: data.reason, cost: safeCost }; + if (data.startAt) raw.startAt = toIsoDateTime(data.startAt); + if (data.endAt) raw.endAt = toIsoDateTime(data.endAt); const payload = raw as unknown as CreateMaintenancePayload; - setSaving(true); - setApiError(""); const ok = await onSubmit(payload, isNew); - setSaving(false); - if (ok) onClose(); - else setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."); + if (ok) { + onClose(); + } else { + setError("reason", { message: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً." }); + } }; // ── Render ──────────────────────────────────────────────────────────────── @@ -159,10 +132,10 @@ export function CarMaintenanceFormModal({ title={carLabel ?? (isNew ? "سجل صيانة جديد" : "تعديل السجل")} footer={ <> - - @@ -170,21 +143,22 @@ export function CarMaintenanceFormModal({ > - {apiError && setApiError("")} />} + {errors.reason?.type === "manual" && ( + {}} /> + )} { setReason(e.target.value); clearFieldError("reason"); }} + {...register("reason")} + error={errors.reason && errors.reason.type !== "manual" ? errors.reason.message : undefined} placeholder="تغيير زيت المحرك" dir="rtl" - error={errors.reason} + autoFocus />
@@ -192,28 +166,25 @@ export function CarMaintenanceFormModal({ label="التكلفة (ر.س) *" type="number" min={0} - value={cost ?? ""} - onChange={(e) => { setCost(parseNum(e.target.value)); clearFieldError("cost"); }} + {...costField} + error={errors.cost?.message} placeholder="0" dir="ltr" - error={errors.cost} /> { setStartAt(e.target.value); clearFieldError("startAt"); }} - error={errors.startAt} + {...register("startAt")} + error={errors.startAt?.message} />
{ setEndAt(e.target.value); clearFieldError("endAt"); }} - error={errors.endAt} + {...register("endAt")} + error={errors.endAt?.message} hint="اتركه فارغاً إذا كانت الصيانة ما زالت جارية." /> diff --git a/src/Components/Trip_Report/Tripreportpanel.tsx b/src/Components/Trip_Report/Tripreportpanel.tsx index 05f6240..8280470 100644 --- a/src/Components/Trip_Report/Tripreportpanel.tsx +++ b/src/Components/Trip_Report/Tripreportpanel.tsx @@ -1,7 +1,7 @@ "use client"; import { useRef, useState } from "react"; -import { Spinner ,Toast} from "../UI"; +import { Button, Toast } from "../UI"; import { tripService } from "@/src/services/trip.service"; import { getStoredToken } from "@/src/lib/auth"; import type { TripNotification } from "@/src/hooks/useTrip"; @@ -78,30 +78,9 @@ export function TripReportPanel({ tripId }: TripReportPanelProps) { إنشاء تقرير الرحلة

- +
: Badge only renders a label +// pill (no dot indicator), and the pulse-dot is the whole point of this status +// chip's design. function StatusBadge({ active }: { active: boolean }) { return ( (null); - // close on Escape - useEffect(() => { - const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [onClose]); - // fetch user details on mount useEffect(() => { - let cancelled = false; - (async () => { - try { - const token = getStoredToken(); - const data = await userService.getById(userId, token); - if (!cancelled) setUser(data); - } catch { - if (!cancelled) setError("تعذّر تحميل بيانات المستخدم. يرجى المحاولة لاحقاً."); - } finally { - if (!cancelled) setLoading(false); - } - })(); - return () => { cancelled = true; }; -}, [userId]); + let cancelled = false; + (async () => { + try { + const token = getStoredToken(); + const data = await userService.getById(userId, token); + if (!cancelled) setUser(data); + } catch { + if (!cancelled) setError("تعذّر تحميل بيانات المستخدم. يرجى المحاولة لاحقاً."); + } finally { + if (!cancelled) setLoading(false); + } + })(); + return () => { cancelled = true; }; + }, [userId]); // ── helpers ─────────────────────────────────────────────────────────────── const fmt = (iso?: string | null) => @@ -98,144 +96,67 @@ export function UserDetailModal({ userId, onClose }: UserDetailModalProps) { // ── render ──────────────────────────────────────────────────────────────── return ( -
{ if (e.target === e.currentTarget) onClose(); }} - style={{ - position: "fixed", inset: 0, zIndex: 55, - background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", - display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", - }} + + إغلاق + + } > -
e.stopPropagation()} - style={{ - width: "100%", maxWidth: 480, - 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", - display: "flex", flexDirection: "column", - }} - > - {/* ── header ── */} -
-
-

- بيانات المستخدم -

-

- {user?.name ?? "عرض المستخدم"} -

-
- + {/* loading */} + {loading && ( +
+ + جارٍ التحميل…
+ )} - {/* ── body ── */} -
+ {/* error */} + {!loading && error && } - {/* loading */} - {loading && ( -
- - جارٍ التحميل… -
- )} + {/* content */} + {!loading && user && ( +
- {/* error */} - {!loading && error && ( -
- {error} -
- )} - - {/* content */} - {!loading && user && ( -
- - {/* avatar + name + status row */} -
- -
-

{user.name}

- {user.userName && ( -

- @{user.userName} -

- )} -
- -
-
-
- - {/* detail rows */} - - - - - - - - {user.passwordChangedAt && ( - + {/* avatar + name + status row */} +
+ +
+

{user.name}

+ {user.userName && ( +

+ @{user.userName} +

)} +
+ +
+
+ + {/* detail rows */} + + + + + + + + {user.passwordChangedAt && ( + )}
- - {/* ── footer ── */} -
- -
-
-
+ )} + ); } \ No newline at end of file diff --git a/src/Components/User/UserFormModal.tsx b/src/Components/User/UserFormModal.tsx index 9f564db..d89a64c 100644 --- a/src/Components/User/UserFormModal.tsx +++ b/src/Components/User/UserFormModal.tsx @@ -1,30 +1,13 @@ "use client"; -import { useEffect, useRef, useState } from "react"; -import * as yup from "yup"; -import { Alert, Button, Input, Select, Spinner } from "../UI"; +import { useForm } from "react-hook-form"; +import type { Resolver } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { Alert, Button, Input, Select } from "../UI"; import { createUserSchema, updateUserSchema } from "@/src/validations/user.validator"; import type { Branch } from "@/src/types/branch"; import type { Role } from "@/src/types/role"; -import type { FormErrors, User, UserFormData } from "@/src/types/user"; - -// ── yup validation ──────────────────────────────────────────────────────────── -async function validate(data: UserFormData, isNew: boolean): Promise { - const schema = isNew ? createUserSchema : updateUserSchema; - try { - await schema.validate(data, { abortEarly: false }); - return {}; - } catch (err) { - if (err instanceof yup.ValidationError) { - return err.inner.reduce((acc, e) => { - const field = e.path as keyof FormErrors; - if (field && !acc[field]) acc[field] = e.message; - return acc; - }, {}); - } - return {}; - } -} +import type { User, UserFormData } from "@/src/types/user"; // ── Props ───────────────────────────────────────────────────────────────────── interface UserFormModalProps { @@ -39,50 +22,43 @@ interface UserFormModalProps { export function UserFormModal({ editUser, roles, branches, onClose, onSubmit }: UserFormModalProps) { const isNew = editUser === null; - const [form, setForm] = useState({ - name: editUser?.name ?? "", - email: editUser?.email ?? "", - phone: editUser?.phone ?? "", - password: "", - roleId: editUser?.role?.id ?? "", - branchId: editUser?.branch?.id ?? "", + // Custom resolver wrapper — on edit, an empty password field must be + // treated as "not provided" (skip the min(8) rule entirely) rather than + // validated as an empty string, exactly like the old validate() call did + // by stripping `password` from the object before running the schema. + const resolver: Resolver = async (values, context, options) => { + const schema = isNew ? createUserSchema : updateUserSchema; + const data = !isNew && !values.password ? { ...values, password: undefined } : values; + return yupResolver(schema as any)(data as UserFormData, context, options); + }; + + const { + register, + handleSubmit, + setError, + formState: { errors, isSubmitting }, + } = useForm({ + resolver, + defaultValues: { + name: editUser?.name ?? "", + email: editUser?.email ?? "", + phone: editUser?.phone ?? "", + password: "", + roleId: editUser?.role?.id ?? "", + branchId: editUser?.branch?.id ?? "", + }, }); - const [errors, setErrors] = useState({}); - const [saving, setSaving] = useState(false); - const [apiError, setApiError] = useState(""); - const firstInputRef = useRef(null); - useEffect(() => { firstInputRef.current?.focus(); }, []); - useEffect(() => { - const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [onClose]); - - const set = (field: keyof UserFormData) => - (e: React.ChangeEvent) => { - 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( - !isNew && !form.password - ? { ...form, password: undefined as unknown as string } - : form, - isNew, - ); - if (Object.keys(errs).length) { setErrors(errs); return; } - setSaving(true); - setApiError(""); - const payload: Partial = { ...form }; + const submitHandler = async (data: UserFormData) => { + const payload: Partial = { ...data }; if (!isNew && !payload.password) delete payload.password; + const ok = await onSubmit(payload as UserFormData, isNew); - setSaving(false); - if (ok) onClose(); - else setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."); + if (ok) { + onClose(); + } else { + setError("name", { message: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً." }); + } }; return ( @@ -133,18 +109,19 @@ export function UserFormModal({ editUser, roles, branches, onClose, onSubmit }:
{/* body */} -
- {apiError && setApiError("")} />} + + {errors.name?.type === "manual" && ( + {}} /> + )} {/* name */} @@ -153,9 +130,8 @@ export function UserFormModal({ editUser, roles, branches, onClose, onSubmit }: setPendingPermId(e.target.value)} - disabled={mutating} - style={{ - flex: 1, height: 36, borderRadius: "var(--radius-md)", - border: "1px solid var(--color-border)", fontSize: 12, - padding: "0 0.5rem", fontFamily: "var(--font-sans)", - color: "var(--color-text-primary)", background: "var(--color-surface)", - }} - > - - {assignablePermissions.map((p) => ( - - ))} - - -
- )} - - {role.permissions && role.permissions.length > 0 ? ( -
- {role.permissions.map(({ permission }) => ( - - {permission.name} - - + {assignablePermissions.length > 0 && ( +
+
+ +
+
-
- )} -
+ )} -
- + {role.permissions && role.permissions.length > 0 ? ( +
+ {role.permissions.map(({ permission }) => ( + + {permission.name} + + + ))} +
+ ) : ( +

لا توجد صلاحيات مسندة لهذا الدور

+ )} +
-
- + )} + ); } \ No newline at end of file diff --git a/src/Components/role/RoleFormModal.tsx b/src/Components/role/RoleFormModal.tsx index f9fc85f..054e16a 100644 --- a/src/Components/role/RoleFormModal.tsx +++ b/src/Components/role/RoleFormModal.tsx @@ -1,28 +1,12 @@ "use client"; -import { useEffect, useState } from "react"; -import * as yup from "yup"; +import { useForm, Controller } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; import { Alert, Button, Input, Modal, Textarea } from "../UI"; -import { createRoleSchema, updateRoleSchema, type RoleFormErrors } from "@/src/validations/role.validator"; +import { createRoleSchema, updateRoleSchema } from "@/src/validations/role.validator"; import { Permission, Role, RoleFormData } from "@/src/types/role"; -// ── Validation ──────────────────────────────────────────────────────────────── -async function validate(data: RoleFormData, isNew: boolean): Promise { - const schema = isNew ? createRoleSchema : updateRoleSchema; - try { - await schema.validate(data, { abortEarly: false }); - return {}; - } catch (err) { - if (err instanceof yup.ValidationError) { - return err.inner.reduce((acc, e) => { - const field = e.path as keyof RoleFormErrors; - if (field && !acc[field]) acc[field] = e.message; - return acc; - }, {}); - } - return {}; - } -} +const FORM_ID = "role-form"; // ── Permission checkbox group ───────────────────────────────────────────────── function PermissionGroup({ @@ -82,46 +66,32 @@ export function RoleFormModal({ editRole, permissions, onClose, onSubmit }: Role const currentPermIds = editRole?.permissions?.map(p => p.permission.id) ?? []; - const [form, setForm] = useState({ - name: editRole?.name ?? "", - description: editRole?.description ?? "", - permissionIds: currentPermIds, + const { + register, + handleSubmit, + control, + setError, + setValue, + formState: { errors, isSubmitting }, + } = useForm({ + // Cast the schema itself and pin the generic explicitly — create/update + // schemas differ structurally in which fields are required, so yup can't + // unify them into RoleFormData on its own. + resolver: yupResolver((isNew ? createRoleSchema : updateRoleSchema) as any), + defaultValues: { + name: editRole?.name ?? "", + description: editRole?.description ?? "", + permissionIds: currentPermIds, + }, }); - const [errors, setErrors] = useState({}); - const [saving, setSaving] = useState(false); - const [apiError, setApiError] = useState(""); - useEffect(() => { - const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [onClose]); - - const set = (field: keyof Pick) => - (e: React.ChangeEvent) => { - setForm(p => ({ ...p, [field]: e.target.value })); - if (errors[field]) setErrors(p => ({ ...p, [field]: undefined })); - }; - - const togglePerm = (id: string) => { - setForm(p => ({ - ...p, - permissionIds: p.permissionIds.includes(id) - ? p.permissionIds.filter(x => x !== id) - : [...p.permissionIds, id], - })); - }; - - 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: RoleFormData) => { + const ok = await onSubmit(data, isNew); + if (ok) { + onClose(); + } else { + setError("name", { message: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً." }); + } }; return ( @@ -133,30 +103,31 @@ export function RoleFormModal({ editRole, permissions, onClose, onSubmit }: Role size="md" footer={ <> - - } > - {apiError && setApiError("")} />} + {errors.name?.type === "manual" && ( + {}} /> + )}
@@ -165,49 +136,70 @@ export function RoleFormModal({ editRole, permissions, onClose, onSubmit }: Role