diff --git a/src/Components/Branch/BranchFormModal.tsx b/src/Components/Branch/BranchFormModal.tsx index a48116f..213109d 100644 --- a/src/Components/Branch/BranchFormModal.tsx +++ b/src/Components/Branch/BranchFormModal.tsx @@ -2,27 +2,11 @@ import { useEffect, useRef, useState } from "react"; import * as yup from "yup"; -import { Alert, Spinner } from "../UI"; +import { Alert, Button, Input, Modal } from "../UI"; import { createBranchSchema, updateBranchSchema } from "@/src/validations/branch.validator"; import type { Branch, BranchFormData, FormErrors } from "@/src/types/branch"; -// ── fixed styles ───────────────────────────────────────── -const S = { - input: { - 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)", - } as React.CSSProperties, - label: { - display: "flex", flexDirection: "column" as const, - gap: 6, fontSize: 12, fontWeight: 600, - color: "var(--color-text-secondary)", - } as React.CSSProperties, - errorText: { fontSize: 11, color: "var(--color-danger)", fontWeight: 500 } as React.CSSProperties, -}; +const FORM_ID = "branch-form"; // ── yup validation ──────────────────────────────────────────────────────────── async function validate(data: BranchFormData, isNew: boolean): Promise { @@ -74,11 +58,6 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod 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 BranchFormData) => (e: React.ChangeEvent) => { @@ -99,163 +78,168 @@ export function BranchFormModal({ editBranch, onClose, onSubmit }: BranchFormMod else setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."); }; - // dynamic styles for inputs with errors - const inputStyle = (field: keyof FormErrors): React.CSSProperties => ({ - ...S.input, - ...(errors[field] ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}), - }); - return ( -
{ 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", - }} + + + + + } > -
e.stopPropagation()} - style={{ - width: "100%", maxWidth: 560, - 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", - maxHeight: "90vh", - }} +
- {/* header */} -
-
-

- {isNew ? "إضافة فرع" : "تعديل فرع"} -

-

- {isNew ? "فرع جديد" : editBranch?.name} -

-
- + {apiError && setApiError("")} />} + + {/* name */} + + + {/* email + phone */} +
+ +
- {/* body */} - - {apiError && setApiError("")} />} + {/* city + street */} +
+ + +
- {/* name */} - + {/* state + district */} +
+ + +
- {/* email + phone */} -
- - -
+ {/* buildingNo + unitNo + zipCode */} +
+ + + +
- {/* city + street */} -
- - -
+ {/* country */} + - {/* state + district */} -
- - -
- - {/* buildingNo + unitNo + zipCode */} -
- - - -
- - {/* country */} - - - {/* latitude + longitude */} -
- - -
- - {/* actions */} -
- - -
- -
-
+ {/* latitude + longitude */} +
+ + +
+ +
); } \ No newline at end of file diff --git a/src/Components/Car_Maintanance/CarMaintananceFormModal.tsx b/src/Components/Car_Maintanance/CarMaintananceFormModal.tsx index 9b27dce..6db2156 100644 --- a/src/Components/Car_Maintanance/CarMaintananceFormModal.tsx +++ b/src/Components/Car_Maintanance/CarMaintananceFormModal.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react"; import * as yup from "yup"; -import { Alert, Spinner } from "../UI"; +import { Alert, Button, Input, Modal } from "../UI"; import { createMaintenanceSchema, updateMaintenanceSchema, @@ -14,35 +14,7 @@ import type { UpdateMaintenancePayload, } from "@/src/types/carMaintanance"; -// ── 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, -}; +const FORM_ID = "maintenance-form"; // ── Number coercion helper ──────────────────────────────────────────────────── // The backend can return `cost` as a numeric string (common with Decimal @@ -130,20 +102,10 @@ export function CarMaintenanceFormModal({ const firstRef = useRef(null); 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 parseNum = (v: string): number | undefined => v.trim() === "" ? undefined : Number(v); - const inputStyle = (field: keyof MaintenanceFormErrors): React.CSSProperties => ({ - ...inputBase, - ...(errors[field] ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}), - }); - const clearFieldError = (field: keyof MaintenanceFormErrors) => setErrors((p) => ({ ...p, [field]: undefined })); @@ -189,135 +151,72 @@ export function CarMaintenanceFormModal({ // ── Render ──────────────────────────────────────────────────────────────── return ( -
{ 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", - }} + + + + + } > -
e.stopPropagation()} - style={{ - width: "100%", maxWidth: 520, - 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", - }} +
- {/* Header */} -
-
-

- {isNew ? "إضافة سجل صيانة" : "تعديل سجل صيانة"} -

-

- {carLabel ?? (isNew ? "سجل صيانة جديد" : "تعديل السجل")} -

-
- + {apiError && setApiError("")} />} + + { setReason(e.target.value); clearFieldError("reason"); }} + placeholder="تغيير زيت المحرك" + dir="rtl" + error={errors.reason} + /> + +
+ { setCost(parseNum(e.target.value)); clearFieldError("cost"); }} + placeholder="0" + dir="ltr" + error={errors.cost} + /> + + { setStartAt(e.target.value); clearFieldError("startAt"); }} + error={errors.startAt} + />
- {/* Form */} - - {apiError && setApiError("")} />} - - - -
- - - -
- - - - {/* Actions */} -
- - -
- -
-
+ { setEndAt(e.target.value); clearFieldError("endAt"); }} + error={errors.endAt} + hint="اتركه فارغاً إذا كانت الصيانة ما زالت جارية." + /> + +
); } \ No newline at end of file diff --git a/src/Components/Client/Clientformmodal.tsx b/src/Components/Client/Clientformmodal.tsx index ed6f8bd..e6ace73 100644 --- a/src/Components/Client/Clientformmodal.tsx +++ b/src/Components/Client/Clientformmodal.tsx @@ -1,9 +1,8 @@ "use client"; -import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; -import { Alert, Spinner } from "../UI"; +import { Alert, Button, Input, Modal, Select } from "../UI"; import { createClientSchema, updateClientSchema, @@ -11,39 +10,7 @@ import { } from "@/src/validations/client.validator"; import type { Client, ClientFormData } from "@/src/types/client"; -// ── Styles ───────────────────────────────────────────────────────────────── -const S = { - input: { - 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)", - } as React.CSSProperties, - label: { - display: "flex", - flexDirection: "column" as const, - gap: 6, - fontSize: 12, - fontWeight: 600, - color: "var(--color-text-secondary)", - } as React.CSSProperties, - errorText: { - fontSize: 11, - color: "var(--color-danger)", - fontWeight: 500, - } as React.CSSProperties, -}; - -const withError = (hasError: boolean): React.CSSProperties => ({ - ...S.input, - ...(hasError ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}), -}); +const FORM_ID = "client-form"; // ── Props ────────────────────────────────────────────────────────────────── interface ClientFormModalProps { @@ -59,7 +26,6 @@ interface ClientFormModalProps { export function ClientFormModal({ editClient, onClose, onSubmit }: ClientFormModalProps) { const isNew = editClient === null; - // FIX 1: was `Schema` (capital S) — now correctly `schema` const { register, handleSubmit, @@ -76,12 +42,6 @@ export function ClientFormModal({ editClient, onClose, onSubmit }: ClientFormMod }, }); - useEffect(() => { - const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [onClose]); - const submitHandler = async (data: ClientFormData) => { const ok = await onSubmit(data, isNew); if (ok) { @@ -92,255 +52,91 @@ export function ClientFormModal({ editClient, onClose, onSubmit }: ClientFormMod }; return ( -
{ 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", - }} + + + + + } > -
e.stopPropagation()} - style={{ - width: "100%", - maxWidth: 520, - 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", - }} +
- {/* Header */} -
-
-

- {isNew ? "إضافة عميل" : "تعديل عميل"} -

-

- {isNew ? "عميل جديد" : editClient?.name} -

-
- + {errors.name?.type === "manual" && ( + {}} /> + )} + + + +
+ + +
- {/* Body */} - - {errors.name?.type === "manual" && ( - {}} /> - )} + + {CLIENT_TYPES.map((t) => ( + + ))} + -
-
+ )} + +
); } \ No newline at end of file diff --git a/src/Components/Client/archive/ArchivedClientsModal.tsx b/src/Components/Client/archive/ArchivedClientsModal.tsx index c198bab..5cce8bd 100644 --- a/src/Components/Client/archive/ArchivedClientsModal.tsx +++ b/src/Components/Client/archive/ArchivedClientsModal.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Alert } from "../../UI"; +import { Alert, Input, Button } from "../../UI"; import { ArchivedClientTable } from "./ArchivedClientTable"; import { ArchivedClientDetailModal } from "./ArchivedClientDetailModal"; import { useArchivedClients } from "@/src/hooks/archive/useArchivedClients"; @@ -63,36 +63,33 @@ export function ArchivedClientsModal({ onClose }: ArchivedClientsModalProps) {
- +
{/* body */}
{/* search */} -
- - - - + handleSearch(e.target.value)} dir="rtl" - style={{ - width: "100%", height: 40, - paddingRight: 36, paddingLeft: 12, - borderRadius: "var(--radius-lg)", - border: "1px solid var(--color-border)", - background: "var(--color-surface)", - fontSize: 13, outline: "none", - fontFamily: "var(--font-sans)", - color: "var(--color-text-primary)", - }} + icon={ + + + + } />
diff --git a/src/Components/Client_Adress/Addressformmodal.tsx b/src/Components/Client_Adress/Addressformmodal.tsx index ce17be7..66f74e8 100644 --- a/src/Components/Client_Adress/Addressformmodal.tsx +++ b/src/Components/Client_Adress/Addressformmodal.tsx @@ -1,9 +1,8 @@ "use client"; -import { useEffect } from "react"; import { useForm, type Resolver } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; -import { Alert, Spinner } from "../UI"; +import { Alert, Button, Input, Modal } from "../UI"; import { createAddressSchema, updateAddressSchema, @@ -13,32 +12,9 @@ import { import type { ClientAddress } from "@/src/types/client_adresses"; // ── Styles ───────────────────────────────────────────────────────────────── +// Only section-title styling is left here — every input/label/error visual +// is now owned by the shared component. const S = { - input: { - 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)", - } as React.CSSProperties, - label: { - display: "flex", - flexDirection: "column" as const, - gap: 6, - fontSize: 12, - fontWeight: 600, - color: "var(--color-text-secondary)", - } as React.CSSProperties, - errorText: { - fontSize: 11, - color: "var(--color-danger)", - fontWeight: 500, - } as React.CSSProperties, sectionTitle: { fontSize: 11, fontWeight: 700, @@ -50,11 +26,7 @@ const S = { } as React.CSSProperties, }; -const withError = (hasError: boolean): React.CSSProperties => ({ - ...S.input, - border: hasError ? "1px solid var(--color-danger)" : "1px solid var(--color-border)", - background: hasError ? "#FEF2F2" : S.input.background, -}); +const FORM_ID = "address-form"; // ── Props ────────────────────────────────────────────────────────────────── interface AddressFormModalProps { @@ -138,12 +110,6 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm location: locationErr = {}, } = errors as AddressErrors; - useEffect(() => { - const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [onClose]); - const submitHandler = async (data: CreateAddressFormValues | UpdateAddressFormValues) => { const ok = await onSubmit(data); if (ok) { @@ -154,260 +120,178 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm }; return ( -
{ 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", - }} + + + + + } > -
e.stopPropagation()} - style={{ - width: "100%", - maxWidth: 560, - maxHeight: "90vh", - 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 */} -
-
-

- {isNew ? "إضافة عنوان" : "تعديل عنوان"} -

-

- {isNew ? "عنوان جديد" : editAddress?.label} -

-
- + {errors.label?.type === "manual" && ( + {}} /> + )} + + {/* Address Meta */} +

بيانات العنوان

+ + + + + + {/* Address Details */} +

تفاصيل العنوان

+ + + +
+ +
- {/* Scrollable body */} -
- - {errors.label?.type === "manual" && ( - {}} /> - )} - - {/* Address Meta */} -

بيانات العنوان

- - - - - - {/* Address Details */} -

تفاصيل العنوان

- - - -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - {/* Contact Person */} -

جهة الاتصال (اختياري)

- -
- - -
- - {/* Coordinates */} -

الإحداثيات الجغرافية {isNew && "*"}

- -
- - -
- - {/* Actions */} -
- - -
- +
+ +
-
-
+ +
+ + +
+ +
+ + +
+ + + + {/* Contact Person */} +

جهة الاتصال (اختياري)

+ +
+ + +
+ + {/* Coordinates */} +

الإحداثيات الجغرافية{isNew ? " *" : ""}

+ +
+ + +
+ + ); } \ No newline at end of file diff --git a/src/Components/Client_Adress/archive/ArchivedClientsAddressesModal.tsx b/src/Components/Client_Adress/archive/ArchivedClientsAddressesModal.tsx index 855188a..62f603f 100644 --- a/src/Components/Client_Adress/archive/ArchivedClientsAddressesModal.tsx +++ b/src/Components/Client_Adress/archive/ArchivedClientsAddressesModal.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Alert } from "../../UI"; +import { Alert, Input, Button } from "../../UI"; import { ArchivedClientAddressesTable } from "./ArchivedClientAddressesTable"; import { ArchivedClientAddressesDetailModal } from "./ArchivedClientAddressesDetailModal"; import { useArchivedClientAddresses } from "@/src/hooks/archive/useArchiveClientAdresses"; @@ -67,36 +67,33 @@ export function ArchivedClientsAddressesModal({ onClose, clientId }: ArchivedCli
- +
{/* body */}
{/* search — client-side only: this endpoint has no server-side search/pagination */} -
- - - - + handleSearch(e.target.value)} dir="rtl" - style={{ - width: "100%", height: 40, - paddingRight: 36, paddingLeft: 12, - borderRadius: "var(--radius-lg)", - border: "1px solid var(--color-border)", - background: "var(--color-surface)", - fontSize: 13, outline: "none", - fontFamily: "var(--font-sans)", - color: "var(--color-text-primary)", - }} + icon={ + + + + } />
diff --git a/src/Components/Driver/DriverFormModal.tsx b/src/Components/Driver/DriverFormModal.tsx index c5c0367..1946171 100644 --- a/src/Components/Driver/DriverFormModal.tsx +++ b/src/Components/Driver/DriverFormModal.tsx @@ -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({ - 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(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 ( -
{ - 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", - }} + + + + + } > -
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", - }} +
- {/* Header */} -
-
-

- {isNew ? "إضافة سائق" : "تعديل سائق"} -

-

- {isNew ? "سائق جديد" : editDriver?.name} -

-
- + {errors.name?.type === "manual" && ( + setApiError("")} /> + )} + + {/* ── Section: Personal Info ── */} +

البيانات الشخصية

+ +
+ + + + + + + + +
- {/* Form */} - - {errors.name?.type === "manual" && ( - setApiError("")} /> + {/* ── Section: ID & GOSI ── */} +

الهوية والتأمينات

+ +
+ + + + + + + +
+ + {/* ── Section: License ── */} +

رخصة القيادة

+ +
+ + + + + +
+ + {/* ── Section: Driver Card ── */} +

بطاقة السائق

+ +
+ + + + + +
+ + {/* ── Section: Operational ── */} +

بيانات تشغيلية

+ +
+ + + + + {/* Status — edit only */} + {!isNew && ( + )} +
- {/* ── Section: Personal Info ── */} -

البيانات الشخصية

+ {/* ── Section: Photos ── */} +

الصور والمستندات

+

+ جميع حقول الصور اختيارية +

-
- {/* Name — required */} - - - {/* Phone — required */} - - - {/* Email — optional */} - - - {/* Nationality — optional */} - - - {/* Address — optional, full width */} - -
- - {/* ── Section: ID & GOSI ── */} -

الهوية والتأمينات

- -
- {/* National ID Type — optional */} - - - {/* National ID number */} - - - {/* National ID expiry */} - - - {/* GOSI — optional */} - -
- - {/* ── Section: License ── */} -

رخصة القيادة

- -
- {/* License number — optional */} - - - {/* License type — optional */} - - - {/* License expiry — optional, validated if provided */} - -
- - {/* ── Section: Driver Card ── */} -

بطاقة السائق

- -
- {/* Card number — optional */} - - - {/* Card type — optional */} - - - {/* Card expiry — optional, validated if provided */} - -
- - {/* ── Section: Operational ── */} -

بيانات تشغيلية

- -
- {/* Branch — optional */} - - - {/* Driver type — optional */} - - - {/* Status — edit only */} - {!isNew && ( - +
+ {/* File inputs cannot use register() directly — bind via Controller + to the existing FileInput component's current/onChange props. */} + ( + )} -
- - {/* ── Section: Photos ── */} -

الصور والمستندات

-

- جميع حقول الصور اختيارية -

- -
- {/* File inputs cannot use register() directly — bind via Controller - to the existing FileInput component's current/onChange props. */} - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> -
- - {/* ── Actions ── */} -
- - -
- -
-
+ /> + ( + + )} + /> + ( + + )} + /> +
+ +
); } \ No newline at end of file diff --git a/src/Components/Driver/archive/ArchivedDrivers.tsx b/src/Components/Driver/archive/ArchivedDrivers.tsx index e893a7a..57099bf 100644 --- a/src/Components/Driver/archive/ArchivedDrivers.tsx +++ b/src/Components/Driver/archive/ArchivedDrivers.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Alert } from "../../UI"; +import { Alert, Input, Modal } from "../../UI"; import { ArchivedDriverTable } from "./ArchivedDriverTable"; import { ArchivedDriverDetailModal } from "./ArchivedDriverDetailModal"; import { useArchivedDrivers } from "@/src/hooks/archive/useArchivedDrivers"; @@ -10,6 +10,21 @@ interface ArchivedDriversProps { onClose: () => void; } +const searchIcon = ( + +); + /** * Top-level modal for browsing archived drivers. Wires the * useArchivedDrivers hook to the table and detail modal, mirroring @@ -25,79 +40,28 @@ export function ArchivedDrivers({ onClose }: ArchivedDriversProps) { } = useArchivedDrivers(); return ( -
{ if (e.target === e.currentTarget) onClose(); }} - style={{ - position: "fixed", inset: 0, zIndex: 70, - background: "rgba(15,23,42,0.6)", backdropFilter: "blur(4px)", - display: "flex", alignItems: "flex-start", justifyContent: "center", - padding: "2rem 1rem", overflowY: "auto", - }} - > + <> {viewDriverId && ( setViewDriverId(null)} /> )} -
e.stopPropagation()} - style={{ - width: "100%", maxWidth: 920, - background: "var(--color-surface-sunken)", - borderRadius: "var(--radius-xl)", - border: "1px solid var(--color-border)", - boxShadow: "0 24px 64px rgba(0,0,0,.2)", - overflow: "hidden", - }} + - {/* header */} -
-
-

- الأرشيف -

-

- السائقون المؤرشفون - - ({total}) - -

-
- -
- - {/* body */} -
+
{/* search */} -
- - - - + handleSearch(e.target.value)} + icon={searchIcon} dir="rtl" - style={{ - width: "100%", height: 40, - paddingRight: 36, paddingLeft: 12, - borderRadius: "var(--radius-lg)", - border: "1px solid var(--color-border)", - background: "var(--color-surface)", - fontSize: 13, outline: "none", - fontFamily: "var(--font-sans)", - color: "var(--color-text-primary)", - }} />
@@ -113,7 +77,7 @@ export function ArchivedDrivers({ onClose }: ArchivedDriversProps) { onPageChange={setPage} />
-
-
+ + ); } \ No newline at end of file diff --git a/src/Components/Driver_Report/driverReport.tsx b/src/Components/Driver_Report/driverReport.tsx index 93de4ca..ae4eb7a 100644 --- a/src/Components/Driver_Report/driverReport.tsx +++ b/src/Components/Driver_Report/driverReport.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Spinner } from "../UI"; +import { Alert, Button, Input } from "../UI"; import { driverService } from "@/src/services/driver.service"; import { getStoredToken } from "@/src/lib/auth"; @@ -65,20 +65,8 @@ export function DriverReportPanel({ driverId }: DriverReportPanelProps) { }} >
- -
- +
{error && ( -
- ⚠ {error} -
+ setError(null)} + className="mt-3" + /> )}
); diff --git a/src/Components/Order/OrderFormModal.tsx b/src/Components/Order/OrderFormModal.tsx index 3e7e104..1dc1f51 100644 --- a/src/Components/Order/OrderFormModal.tsx +++ b/src/Components/Order/OrderFormModal.tsx @@ -1,10 +1,10 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import Link from "next/link"; import { useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; -import { Alert, Spinner } from "../UI"; +import { Alert, Button, Input, Modal, Select } from "../UI"; import { getStoredToken } from "@/src/lib/auth"; import { clientService } from "@/src/services/client.service"; import { tripService } from "@/src/services/trip.service"; @@ -22,34 +22,6 @@ import type { import type { Client } from "@/src/types/client"; import type { Trip } from "@/src/types/trip"; -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, -}; - const sectionHeadingStyle: React.CSSProperties = { fontSize: 11, letterSpacing: "0.25em", @@ -59,19 +31,12 @@ 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 createLinkStyle: React.CSSProperties = { fontSize: 11, color: "var(--color-brand-600)", textDecoration: "none", fontWeight: 500, - marginTop: 2, + marginTop: 6, display: "inline-flex", alignItems: "center", gap: 3, @@ -147,9 +112,12 @@ export function OrderFormModal({ register, handleSubmit, setError, + setFocus, formState: { errors, isSubmitting }, } = useForm({ - resolver: yupResolver(isNew ? createOrderSchema : updateOrderSchema) as never, + // Cast the schema itself — create/update schemas differ structurally in + // which fields are required, so yup can't unify them into one type. + resolver: yupResolver((isNew ? createOrderSchema : updateOrderSchema) as any), defaultValues: { shipmentNumber: editOrder?.shipmentNumber ?? "", recipientName: editOrder?.recipientName ?? "", @@ -200,7 +168,12 @@ export function OrderFormModal({ }); const [apiError, setApiError] = useState(""); - const firstRef = useRef(null); + + // register("shipmentNumber") already attaches its own ref — focusing via + // setFocus avoids the ref collision a separate `ref={firstRef}` would cause. + useEffect(() => { + setFocus("shipmentNumber"); + }, [setFocus]); useEffect(() => { let cancelled = false; @@ -227,9 +200,6 @@ export function OrderFormModal({ }; }, []); - useEffect(() => { - firstRef.current?.focus(); - }, []); useEffect(() => { const h = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); @@ -238,11 +208,6 @@ export function OrderFormModal({ return () => window.removeEventListener("keydown", h); }, [onClose]); - const withError = (hasError: boolean): React.CSSProperties => ({ - ...inputBase, - ...(hasError ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}), - }); - const numberField = ( field: | "quantity" @@ -340,438 +305,371 @@ export function OrderFormModal({ const pickupErr = errors.pickupAddress; return ( -
{ - 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", - }} + + + + + } > -
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", - }} +
-
+ {errors.shipmentNumber?.type === "manual" && ( + setApiError("")} /> + )} + +

بيانات الشحنة والمستلم

+ +
+ +
-

- {isNew ? "إنشاء طلب" : "تعديل طلب"} -

-

- {isNew ? "طلب جديد" : editOrder?.shipmentNumber} -

+ + {clients.map((c) => ( + + ))} + + + + + + + إنشاء عميل جديد + +
+ + + + + +
+ + + + + + + إنشاء رحلة جديدة +
-
- - {errors.shipmentNumber?.type === "manual" && ( - setApiError("")} /> +

تفاصيل الشحنة

+ +
+ + + + + +
+ +

بيانات الدفع

+ +
+ + + + + + + {!isNew && ( + )} +
-

بيانات الشحنة والمستلم

+

عنوان التسليم

+
+ + + + + + + + +
-
- +

عنوان الاستلام

- - - - - - - -
- -

تفاصيل الشحنة

- -
- - - - - -
- -

بيانات الدفع

- -
- - - - - - - {!isNew && ( - - )} -
- -

عنوان التسليم

-
- - - - - - - - -
- -

عنوان الاستلام

- -
- {(["id", "new"] as const).map((m) => ( - - ))} -
- - {pickupMode === "id" ? ( - - ) : ( -
- - - - - - - - -
- )} - -
- - + {m === "id" ? "عنوان موجود (ID)" : "عنوان جديد"} + + ))} +
+ + {pickupMode === "id" ? ( + + ) : ( +
+ + + + + + + +
- -
-
+ )} + +
); } diff --git a/src/Components/Order/archive/ArchivedOrdersModal.tsx b/src/Components/Order/archive/ArchivedOrdersModal.tsx index 0f85c1d..69c3ce2 100644 --- a/src/Components/Order/archive/ArchivedOrdersModal.tsx +++ b/src/Components/Order/archive/ArchivedOrdersModal.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Alert } from "../../UI"; +import { Alert, Input, Modal } from "../../UI"; import { ArchivedOrderTable } from "./ArchivedOrderTable"; import { ArchivedOrderDetailModal } from "./ArchivedOrderDetailModal"; import { useArchivedOrders } from "@/src/hooks/archive/useArchivedOrders"; @@ -11,6 +11,21 @@ interface ArchivedOrdersModalProps { onClose: () => void; } +const searchIcon = ( + +); + export function ArchivedOrdersModal({ onClose }: ArchivedOrdersModalProps) { const [viewOrder, setViewOrder] = useState(null); @@ -21,79 +36,28 @@ export function ArchivedOrdersModal({ onClose }: ArchivedOrdersModalProps) { } = useArchivedOrders(); return ( -
{ if (e.target === e.currentTarget) onClose(); }} - style={{ - position: "fixed", inset: 0, zIndex: 70, - background: "rgba(15,23,42,0.6)", backdropFilter: "blur(4px)", - display: "flex", alignItems: "flex-start", justifyContent: "center", - padding: "2rem 1rem", overflowY: "auto", - }} - > + <> {viewOrder && ( setViewOrder(null)} /> )} -
e.stopPropagation()} - style={{ - width: "100%", maxWidth: 920, - background: "var(--color-surface-sunken)", - borderRadius: "var(--radius-xl)", - border: "1px solid var(--color-border)", - boxShadow: "0 24px 64px rgba(0,0,0,.2)", - overflow: "hidden", - }} + - {/* header */} -
-
-

- الأرشيف -

-

- الطلبات المؤرشفة - - ({total}) - -

-
- -
- - {/* body */} -
+
{/* search */} -
- - - - + handleSearch(e.target.value)} + icon={searchIcon} dir="rtl" - style={{ - width: "100%", height: 40, - paddingRight: 36, paddingLeft: 12, - borderRadius: "var(--radius-lg)", - border: "1px solid var(--color-border)", - background: "var(--color-surface)", - fontSize: 13, outline: "none", - fontFamily: "var(--font-sans)", - color: "var(--color-text-primary)", - }} />
@@ -109,7 +73,7 @@ export function ArchivedOrdersModal({ onClose }: ArchivedOrdersModalProps) { onPageChange={setPage} />
-
-
+ + ); } \ No newline at end of file diff --git a/src/Components/Trip/Tripformmodal.tsx b/src/Components/Trip/Tripformmodal.tsx index b9f0108..5279465 100644 --- a/src/Components/Trip/Tripformmodal.tsx +++ b/src/Components/Trip/Tripformmodal.tsx @@ -1,9 +1,9 @@ "use client"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { yupResolver } from "@hookform/resolvers/yup"; -import { Alert, Spinner } from "../UI"; +import { Alert, Button, Input, Select, Textarea, Spinner } from "../UI"; import { getStoredToken } from "@/src/lib/auth"; import { createTripSchema, @@ -22,34 +22,8 @@ import type { CarOption } from "@/src/types/car"; import type { BranchOption } from "@/src/types/branch"; // ── Shared styles ──────────────────────────────────────────────────────────── - -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, -}; +// Only for section headings — everything field-level now comes from the +// Input / Select / Textarea components themselves. const sectionHeadingStyle: React.CSSProperties = { fontSize: 11, @@ -60,13 +34,6 @@ const sectionHeadingStyle: React.CSSProperties = { margin: "0.5rem 0 0", }; -const optionalLabelStyle: React.CSSProperties = { - fontSize: 10, - fontWeight: 500, - color: "var(--color-text-hint)", - marginRight: 4, -}; - // ── Form values shape ──────────────────────────────────────────────────────── // startTime/endTime store the ISO-8601 string (via setValueAs on register), // while the DOM element itself keeps showing @@ -127,9 +94,12 @@ export function TripFormModal({ register, handleSubmit, setError, + setFocus, formState: { errors, isSubmitting }, } = useForm({ - resolver: yupResolver(isNew ? createTripSchema : updateTripSchema) as never, + // Cast the schema itself — create/update schemas differ structurally in + // which fields are required, so yup can't unify them into one type. + resolver: yupResolver((isNew ? createTripSchema : updateTripSchema) as any), defaultValues: { title: editTrip?.title ?? "", driverId: editTrip?.driverId ?? "", @@ -150,11 +120,13 @@ export function TripFormModal({ }); const [apiError, setApiError] = useState(""); - const firstRef = useRef(null); + // register("title") already attaches its own ref — Input forwards it + // through to the underlying , so setFocus works with no separate + // ref needed here. useEffect(() => { - firstRef.current?.focus(); - }, []); + setFocus("title"); + }, [setFocus]); useEffect(() => { const h = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); @@ -163,11 +135,6 @@ export function TripFormModal({ return () => window.removeEventListener("keydown", h); }, [onClose]); - const withError = (hasError: boolean): React.CSSProperties => ({ - ...inputBase, - ...(hasError ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}), - }); - const numberField = ( field: "collectedCount" | "deliveredCount" | "returnedCount" | "totalCashCollected", ) => @@ -182,6 +149,9 @@ export function TripFormModal({ setValueAs: (v) => (v ? `${v}:00.000Z` : ""), }); + const titleError = + errors.title && errors.title.type !== "manual" ? errors.title.message : undefined; + // ── Submit ──────────────────────────────────────────────────────────────── const submitHandler = async (data: TripFormValues) => { @@ -273,22 +243,19 @@ export function TripFormModal({ > {isNew ? "إضافة رحلة جديدة" : "تعديل بيانات الرحلة"} - +
{/* Form */} @@ -306,212 +273,186 @@ export function TripFormModal({ setApiError("")} /> )} - {/* ── Section:trip management── */} + {/* ── Section: trip management ── */}

أساسيات الرحلة

{/* Title */} - +
{/* Driver */} - + {/* Car */} - +
{/* Branch */} - + {/* Status */} - +
{/* ── Section: التوقيت ── */}

التوقيت

- - - + +
{/* ── Section: الأعداد والمبالغ ── */}

الأعداد والمبالغ

- - - - - - - + + + +
{/* ── Section: ملاحظات ── */}

ملاحظات

-