update page to using same tamplet in all project not using inline code

This commit is contained in:
m7amedez5511
2026-07-20 16:03:07 +03:00
parent d23866d7fc
commit 55f76005c9
24 changed files with 1832 additions and 3037 deletions

View File

@@ -1,9 +1,9 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, 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, Modal, Select } from "../UI";
import { getStoredToken } from "@/src/lib/auth";
import {
createCarSchema,
@@ -18,35 +18,9 @@ import type {
import type { Branch } from "@/src/types/branch";
import { branchService } from "@/src/services/branch.service";
// ── 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 styles ─────────────────────────────────────────────────────────────
// Only section-heading styling is left here — every input/select/label/error
// visual is now owned by the shared <Input /> / <Select /> components.
const sectionHeadingStyle: React.CSSProperties = {
fontSize: 11,
@@ -57,6 +31,8 @@ const sectionHeadingStyle: React.CSSProperties = {
margin: "0.5rem 0 0",
};
const FORM_ID = "car-form";
// ── Date helper ───────────────────────────────────────────────────────────────
// <input type="date"> gives "YYYY-MM-DD"; backend expects full ISO-8601.
@@ -140,9 +116,13 @@ export function CarFormModal({
register,
handleSubmit,
setError,
setFocus,
formState: { errors, isSubmitting },
} = useForm<CarFormValues>({
resolver: yupResolver(isNew ? createCarSchema : updateCarSchema) as never,
// The create/update schemas have structurally different required fields,
// so yup's inferred types don't unify — cast the schema itself (not the
// yupResolver() call) to sidestep the mismatch.
resolver: yupResolver<CarFormValues>((isNew ? createCarSchema : updateCarSchema) as any),
defaultValues: {
manufacturer: editCar?.manufacturer ?? "",
model: editCar?.model ?? "",
@@ -169,18 +149,14 @@ export function CarFormModal({
});
const [apiError, setApiError] = useState("");
const firstRef = useRef<HTMLInputElement>(null);
// register("manufacturer") already provides a ref for this input, so we
// focus it via RHF's setFocus instead of a separate useRef — assigning
// both `ref={firstRef}` and `{...register(...)}` on the same element
// causes the second ref to silently overwrite the first.
useEffect(() => {
firstRef.current?.focus();
}, []);
useEffect(() => {
const h = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", h);
return () => window.removeEventListener("keydown", h);
}, [onClose]);
setFocus("manufacturer");
}, [setFocus]);
// Numeric fields: empty string -> undefined (never NaN), matching the
// behavior of the old parseNum() helper.
@@ -191,11 +167,6 @@ export function CarFormModal({
setValueAs: (v) => (v === "" || v === null ? undefined : Number(v)),
});
const withError = (hasError: boolean): React.CSSProperties => ({
...inputBase,
...(hasError ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}),
});
// ── Submit ────────────────────────────────────────────────────────────────
const submitHandler = async (data: CarFormValues) => {
@@ -244,351 +215,173 @@ export function CarFormModal({
// ── Render ────────────────────────────────────────────────────────────────
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="car-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
title={isNew ? "مركبة جديدة" : `${editCar?.manufacturer} ${editCar?.model}`}
subtitle={isNew ? "إضافة مركبة" : "تعديل مركبة"}
onClose={onClose}
size="lg"
footer={
<>
<Button variant="secondary" onClick={onClose} disabled={isSubmitting}>
إلغاء
</Button>
<Button type="submit" form={FORM_ID} loading={isSubmitting}>
{isNew ? "إضافة المركبة" : "حفظ التغييرات"}
</Button>
</>
}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
width: "100%",
maxWidth: 620,
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
style={{ display: "flex", flexDirection: "column", gap: "1rem" }}
dir="rtl"
>
{/* 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="car-modal-title"
style={{
fontSize: 17,
fontWeight: 700,
color: "var(--color-text-primary)",
margin: "4px 0 0",
}}
>
{isNew ? "مركبة جديدة" : `${editCar?.manufacturer} ${editCar?.model}`}
</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.manufacturer?.type === "manual" && (
<Alert
type="error"
message={errors.manufacturer.message ?? ""}
onClose={() => setApiError("")}
/>
)}
{/* ── Section: Basic Info ── */}
<p style={sectionHeadingStyle}>بيانات أساسية</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<Input
label="الشركة المصنعة *"
{...register("manufacturer")}
error={
errors.manufacturer && errors.manufacturer.type !== "manual"
? errors.manufacturer.message
: undefined
}
placeholder="تويوتا"
dir="rtl"
autoComplete="off"
/>
<Input
label="الموديل *"
{...register("model")}
error={errors.model?.message}
placeholder="لاند كروزر"
dir="rtl"
/>
</div>
{/* Form */}
<form
onSubmit={handleSubmit(submitHandler)}
noValidate
style={{
padding: "1.5rem",
display: "flex",
flexDirection: "column",
gap: "1rem",
maxHeight: "75vh",
overflowY: "auto",
}}
dir="rtl"
>
{errors.manufacturer?.type === "manual" && (
<Alert
type="error"
message={errors.manufacturer.message ?? ""}
onClose={() => setApiError("")}
/>
)}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
<Input
label="سنة الصنع *"
type="number"
min={1900}
max={new Date().getFullYear() + 1}
{...numberField("year")}
error={errors.year?.message}
dir="ltr"
/>
<Input label="اللون" {...register("color")} placeholder="أبيض" dir="rtl" />
<Input label="نوع اللوحة" {...register("plateType")} placeholder="خاص" dir="rtl" />
</div>
{/* ── Section: Basic Info ── */}
<p style={sectionHeadingStyle}>بيانات أساسية</p>
{/* ── Section: Plate ── */}
<p style={sectionHeadingStyle}>بيانات اللوحة</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<Input
label="رقم اللوحة *"
{...register("plateNumber")}
error={errors.plateNumber?.message}
placeholder="1234"
dir="ltr"
/>
<Input
label="حروف اللوحة *"
{...register("plateLetters")}
error={errors.plateLetters?.message}
placeholder="أ ب ج"
dir="rtl"
/>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<label style={labelStyle}>
الشركة المصنعة *
<input
ref={firstRef}
style={withError(!!errors.manufacturer && errors.manufacturer.type !== "manual")}
{...register("manufacturer")}
placeholder="تويوتا"
dir="rtl"
autoComplete="off"
/>
{errors.manufacturer && errors.manufacturer.type !== "manual" && (
<span style={errorTextStyle}>{errors.manufacturer.message}</span>
)}
</label>
<label style={labelStyle}>
الموديل *
<input
style={withError(!!errors.model)}
{...register("model")}
placeholder="لاند كروزر"
dir="rtl"
/>
{errors.model && <span style={errorTextStyle}>{errors.model.message}</span>}
</label>
</div>
{/* ── Section: Registration & Legal ── */}
<p style={sectionHeadingStyle}>الترخيص والتأمين</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<Input
label="رقم الاستمارة *"
{...register("registrationNumber")}
error={errors.registrationNumber?.message}
placeholder="SA-001234"
dir="ltr"
/>
<Input
label="رقم الهيكل (VIN)"
{...register("vinNumber")}
error={errors.vinNumber?.message}
placeholder="1HGBH41JXMN109186"
dir="ltr"
/>
<Input
label="انتهاء الاستمارة"
type="date"
{...register("registrationExpiryDate")}
/>
<Select label="حالة التأمين" {...register("insuranceStatus")} dir="rtl">
<option value="Valid">سارٍ</option>
<option value="Expired">منتهي</option>
<option value="NotInsured">غير مؤمَّن</option>
</Select>
<Input
label="انتهاء التأمين"
type="date"
{...register("insuranceExpiryDate")}
/>
<Input
label="انتهاء الفحص الدوري"
type="date"
{...register("inspectionExpiryDate")}
/>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
<label style={labelStyle}>
سنة الصنع *
<input
style={withError(!!errors.year)}
type="number"
min={1900}
max={new Date().getFullYear() + 1}
{...numberField("year")}
dir="ltr"
/>
{errors.year && <span style={errorTextStyle}>{errors.year.message}</span>}
</label>
<label style={labelStyle}>
اللون
<input style={inputBase} {...register("color")} placeholder="أبيض" dir="rtl" />
</label>
<label style={labelStyle}>
نوع اللوحة
<input style={inputBase} {...register("plateType")} placeholder="خاص" dir="rtl" />
</label>
</div>
{/* ── Section: Plate ── */}
<p style={sectionHeadingStyle}>بيانات اللوحة</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<label style={labelStyle}>
رقم اللوحة *
<input
style={withError(!!errors.plateNumber)}
{...register("plateNumber")}
placeholder="1234"
dir="ltr"
/>
{errors.plateNumber && (
<span style={errorTextStyle}>{errors.plateNumber.message}</span>
)}
</label>
<label style={labelStyle}>
حروف اللوحة *
<input
style={withError(!!errors.plateLetters)}
{...register("plateLetters")}
placeholder="أ ب ج"
dir="rtl"
/>
{errors.plateLetters && (
<span style={errorTextStyle}>{errors.plateLetters.message}</span>
)}
</label>
</div>
{/* ── Section: Registration & Legal ── */}
<p style={sectionHeadingStyle}>الترخيص والتأمين</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<label style={labelStyle}>
رقم الاستمارة *
<input
style={withError(!!errors.registrationNumber)}
{...register("registrationNumber")}
placeholder="SA-001234"
dir="ltr"
/>
{errors.registrationNumber && (
<span style={errorTextStyle}>{errors.registrationNumber.message}</span>
)}
</label>
<label style={labelStyle}>
رقم الهيكل (VIN)
<input
style={withError(!!errors.vinNumber)}
{...register("vinNumber")}
placeholder="1HGBH41JXMN109186"
dir="ltr"
/>
{errors.vinNumber && (
<span style={errorTextStyle}>{errors.vinNumber.message}</span>
)}
</label>
<label style={labelStyle}>
انتهاء الاستمارة
<input style={inputBase} type="date" {...register("registrationExpiryDate")} />
</label>
<label style={labelStyle}>
حالة التأمين
<select style={{ ...inputBase, cursor: "pointer" }} {...register("insuranceStatus")} dir="rtl">
<option value="Valid">سارٍ</option>
<option value="Expired">منتهي</option>
<option value="NotInsured">غير مؤمَّن</option>
</select>
</label>
<label style={labelStyle}>
انتهاء التأمين
<input style={inputBase} type="date" {...register("insuranceExpiryDate")} />
</label>
<label style={labelStyle}>
انتهاء الفحص الدوري
<input style={inputBase} type="date" {...register("inspectionExpiryDate")} />
</label>
</div>
{/* ── Section: Operational ── */}
<p style={sectionHeadingStyle}>بيانات تشغيلية</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
<label style={labelStyle}>
الفرع
<select style={{ ...inputBase, cursor: "pointer" }} {...register("branchId")} dir="rtl">
<option value="">اختر الفرع</option>
{branches.map((b) => (
<option key={b.id} value={b.id}>
{b.name}
</option>
))}
</select>
</label>
<label style={labelStyle}>
الحالة
<select style={{ ...inputBase, cursor: "pointer" }} {...register("currentStatus")} dir="rtl">
<option value="Active">نشط</option>
<option value="InMaintenance">صيانة</option>
<option value="InTrip">في رحلة</option>
<option value="Inactive">غير نشط</option>
</select>
</label>
<label style={labelStyle}>
رقم GPS
<input style={inputBase} {...register("gpsDeviceId")} placeholder="GPS-001" dir="ltr" />
</label>
<label style={labelStyle}>
الطاقة الاستيعابية
<input
style={withError(!!errors.capacity)}
type="number"
min={0}
{...numberField("capacity")}
placeholder="0"
dir="ltr"
/>
{errors.capacity && (
<span style={errorTextStyle}>{errors.capacity.message}</span>
)}
</label>
<label style={labelStyle}>
الوزن (كجم)
<input
style={withError(!!errors.weight)}
type="number"
min={0}
{...numberField("weight")}
placeholder="0"
dir="ltr"
/>
{errors.weight && <span style={errorTextStyle}>{errors.weight.message}</span>}
</label>
</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>
{/* ── Section: Operational ── */}
<p style={sectionHeadingStyle}>بيانات تشغيلية</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: "0.75rem" }}>
<Select label="الفرع" {...register("branchId")} dir="rtl">
<option value="">اختر الفرع</option>
{branches.map((b) => (
<option key={b.id} value={b.id}>
{b.name}
</option>
))}
</Select>
<Select label="الحالة" {...register("currentStatus")} dir="rtl">
<option value="Active">نشط</option>
<option value="InMaintenance">صيانة</option>
<option value="InTrip">في رحلة</option>
<option value="Inactive">غير نشط</option>
</Select>
<Input label="رقم GPS" {...register("gpsDeviceId")} placeholder="GPS-001" dir="ltr" />
<Input
label="الطاقة الاستيعابية"
type="number"
min={0}
{...numberField("capacity")}
error={errors.capacity?.message}
placeholder="0"
dir="ltr"
/>
<Input
label="الوزن (كجم)"
type="number"
min={0}
{...numberField("weight")}
error={errors.weight?.message}
placeholder="0"
dir="ltr"
/>
</div>
</form>
</Modal>
);
}