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 { 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 <input type="datetime-local"> DOM element itself keeps showing
@@ -127,9 +94,12 @@ export function TripFormModal({
register,
handleSubmit,
setError,
setFocus,
formState: { errors, isSubmitting },
} = useForm<TripFormValues>({
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<TripFormValues>((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<HTMLInputElement>(null);
// register("title") already attaches its own ref — Input forwards it
// through to the underlying <input>, 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 ? "إضافة رحلة جديدة" : "تعديل بيانات الرحلة"}
</h2>
<button
<Button
type="button"
variant="ghost"
size="sm"
onClick={onClose}
style={{
background: "none",
border: "none",
cursor: "pointer",
color: "var(--color-text-muted)",
padding: 4,
}}
aria-label="إغلاق"
className="!px-1"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</Button>
</div>
{/* Form */}
@@ -306,212 +273,186 @@ export function TripFormModal({
<Alert type="error" message={errors.title.message ?? ""} onClose={() => setApiError("")} />
)}
{/* ── Section:trip management── */}
{/* ── Section: trip management ── */}
<p style={sectionHeadingStyle}>أساسيات الرحلة</p>
{/* Title */}
<label style={labelStyle}>
عنوان الرحلة <span style={{ color: "var(--color-danger)" }}>*</span>
<input
ref={firstRef}
style={withError(!!errors.title && errors.title.type !== "manual")}
{...register("title")}
placeholder="مثال: توزيع الرياض الشمالي"
dir="rtl"
/>
{errors.title && errors.title.type !== "manual" && (
<span style={errorTextStyle}>{errors.title.message}</span>
)}
</label>
<Input
label="عنوان الرحلة *"
error={titleError}
{...register("title")}
placeholder="مثال: توزيع الرياض الشمالي"
dir="rtl"
/>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
{/* Driver */}
<label style={labelStyle}>
السائق <span style={{ color: "var(--color-danger)" }}>*</span>
<select style={{ ...withError(!!errors.driverId), cursor: "pointer" }} {...register("driverId")} dir="rtl">
<option value="">اختر السائق</option>
{drivers.map((d) => (
<option key={d.id} value={d.id}>
{d.name} {d.phone}
</option>
))}
</select>
{errors.driverId && <span style={errorTextStyle}>{errors.driverId.message}</span>}
</label>
<Select
label="السائق *"
error={errors.driverId?.message}
{...register("driverId")}
dir="rtl"
>
<option value="">اختر السائق</option>
{drivers.map((d) => (
<option key={d.id} value={d.id}>
{d.name} {d.phone}
</option>
))}
</Select>
{/* Car */}
<label style={labelStyle}>
السيارة <span style={{ color: "var(--color-danger)" }}>*</span>
<select style={{ ...withError(!!errors.carId), cursor: "pointer" }} {...register("carId")} dir="rtl">
<option value="">اختر السيارة</option>
{cars.map((c) => (
<option key={c.id} value={c.id}>
{c.manufacturer} {c.model} {c.plateNumber}
</option>
))}
</select>
{errors.carId && <span style={errorTextStyle}>{errors.carId.message}</span>}
</label>
<Select
label="السيارة *"
error={errors.carId?.message}
{...register("carId")}
dir="rtl"
>
<option value="">اختر السيارة</option>
{cars.map((c) => (
<option key={c.id} value={c.id}>
{c.manufacturer} {c.model} {c.plateNumber}
</option>
))}
</Select>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
{/* Branch */}
<label style={labelStyle}>
الفرع <span style={{ color: "var(--color-danger)" }}>*</span>
<select style={{ ...withError(!!errors.branchId), cursor: "pointer" }} {...register("branchId")} dir="rtl">
<option value="">اختر الفرع</option>
{branches.map((b) => (
<option key={b.id} value={b.id}>{b.name}</option>
))}
</select>
{errors.branchId && <span style={errorTextStyle}>{errors.branchId.message}</span>}
</label>
<Select
label="الفرع *"
error={errors.branchId?.message}
{...register("branchId")}
dir="rtl"
>
<option value="">اختر الفرع</option>
{branches.map((b) => (
<option key={b.id} value={b.id}>{b.name}</option>
))}
</Select>
{/* Status */}
<label style={labelStyle}>
الحالة
<select style={{ ...inputBase, cursor: "pointer" }} {...register("status")} dir="rtl">
<option value="Scheduled">مجدولة</option>
<option value="InProgress">جارية</option>
<option value="Completed">مكتملة</option>
<option value="Cancelled">ملغاة</option>
</select>
</label>
<Select label="الحالة" {...register("status")} dir="rtl">
<option value="Scheduled">مجدولة</option>
<option value="InProgress">جارية</option>
<option value="Completed">مكتملة</option>
<option value="Cancelled">ملغاة</option>
</Select>
</div>
{/* ── Section: التوقيت ── */}
<p style={sectionHeadingStyle}>التوقيت</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<label style={labelStyle}>
وقت البدء <span style={optionalLabelStyle}>(اختياري)</span>
<input type="datetime-local" style={withError(!!errors.startTime)} {...dateTimeField("startTime")} />
{errors.startTime && <span style={errorTextStyle}>{errors.startTime.message}</span>}
</label>
<label style={labelStyle}>
وقت الانتهاء <span style={optionalLabelStyle}>(اختياري)</span>
<input type="datetime-local" style={withError(!!errors.endTime)} {...dateTimeField("endTime")} />
{errors.endTime && <span style={errorTextStyle}>{errors.endTime.message}</span>}
</label>
<Input
label="وقت البدء"
hint="اختياري"
type="datetime-local"
error={errors.startTime?.message}
{...dateTimeField("startTime")}
/>
<Input
label="وقت الانتهاء"
hint="اختياري"
type="datetime-local"
error={errors.endTime?.message}
{...dateTimeField("endTime")}
/>
</div>
{/* ── Section: الأعداد والمبالغ ── */}
<p style={sectionHeadingStyle}>الأعداد والمبالغ</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr 1fr", gap: "0.75rem" }}>
<label style={labelStyle}>
المجمّع <span style={optionalLabelStyle}>(اختياري)</span>
<input type="number" min="0" style={withError(!!errors.collectedCount)} {...numberField("collectedCount")} placeholder="0" dir="ltr" />
{errors.collectedCount && <span style={errorTextStyle}>{errors.collectedCount.message}</span>}
</label>
<label style={labelStyle}>
المُسلَّم <span style={optionalLabelStyle}>(اختياري)</span>
<input type="number" min="0" style={withError(!!errors.deliveredCount)} {...numberField("deliveredCount")} placeholder="0" dir="ltr" />
{errors.deliveredCount && <span style={errorTextStyle}>{errors.deliveredCount.message}</span>}
</label>
<label style={labelStyle}>
المُرتجع <span style={optionalLabelStyle}>(اختياري)</span>
<input type="number" min="0" style={withError(!!errors.returnedCount)} {...numberField("returnedCount")} placeholder="0" dir="ltr" />
{errors.returnedCount && <span style={errorTextStyle}>{errors.returnedCount.message}</span>}
</label>
<label style={labelStyle}>
النقد المحصّل <span style={optionalLabelStyle}>(اختياري)</span>
<input type="number" min="0" step="0.01" style={withError(!!errors.totalCashCollected)} {...numberField("totalCashCollected")} placeholder="0.00" dir="ltr" />
{errors.totalCashCollected && <span style={errorTextStyle}>{errors.totalCashCollected.message}</span>}
</label>
<Input
label="المجمّع"
hint="اختياري"
type="number"
min="0"
error={errors.collectedCount?.message}
{...numberField("collectedCount")}
placeholder="0"
dir="ltr"
/>
<Input
label="المُسلَّم"
hint="اختياري"
type="number"
min="0"
error={errors.deliveredCount?.message}
{...numberField("deliveredCount")}
placeholder="0"
dir="ltr"
/>
<Input
label="المُرتجع"
hint="اختياري"
type="number"
min="0"
error={errors.returnedCount?.message}
{...numberField("returnedCount")}
placeholder="0"
dir="ltr"
/>
<Input
label="النقد المحصّل"
hint="اختياري"
type="number"
min="0"
step="0.01"
error={errors.totalCashCollected?.message}
{...numberField("totalCashCollected")}
placeholder="0.00"
dir="ltr"
/>
</div>
{/* ── Section: ملاحظات ── */}
<p style={sectionHeadingStyle}>ملاحظات</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<label style={labelStyle}>
ملاحظات <span style={optionalLabelStyle}>(اختياري)</span>
<textarea
style={{ ...withError(!!errors.notes), height: 72, padding: "0.5rem 0.75rem", resize: "vertical" }}
{...register("notes")}
placeholder="أي ملاحظات إضافية…"
dir="rtl"
/>
{errors.notes && <span style={errorTextStyle}>{errors.notes.message}</span>}
</label>
<label style={labelStyle}>
سبب الإنهاء <span style={optionalLabelStyle}>(اختياري)</span>
<textarea
style={{ ...withError(!!errors.endReason), height: 72, padding: "0.5rem 0.75rem", resize: "vertical" }}
{...register("endReason")}
placeholder="سبب إنهاء أو إلغاء الرحلة…"
dir="rtl"
/>
{errors.endReason && <span style={errorTextStyle}>{errors.endReason.message}</span>}
</label>
<Textarea
label="ملاحظات"
hint="اختياري"
error={errors.notes?.message}
{...register("notes")}
placeholder="أي ملاحظات إضافية…"
dir="rtl"
className="h-[72px]"
/>
<Textarea
label="سبب الإنهاء"
hint="اختياري"
error={errors.endReason?.message}
{...register("endReason")}
placeholder="سبب إنهاء أو إلغاء الرحلة…"
dir="rtl"
className="h-[72px]"
/>
</div>
{/* reason — edit only (for reassigning driver/car) */}
{!isNew && (
<>
<p style={sectionHeadingStyle}>سجل التغيير</p>
<label style={labelStyle}>
سبب التعديل{" "}
<span style={optionalLabelStyle}>(مطلوب عند تغيير السائق أو السيارة)</span>
<input
style={inputBase}
{...register("reason")}
placeholder="مثال: تغيير السائق بسبب إجازة طارئة"
dir="rtl"
/>
</label>
<Input
label="سبب التعديل"
hint="مطلوب عند تغيير السائق أو السيارة"
{...register("reason")}
placeholder="مثال: تغيير السائق بسبب إجازة طارئة"
dir="rtl"
/>
</>
)}
{/* ── 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 type="button" variant="secondary" onClick={onClose} disabled={isSubmitting}>
إلغاء
</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" />}
</Button>
<Button type="submit" variant="primary" loading={isSubmitting}>
{isSubmitting ? "جارٍ الحفظ…" : isNew ? "إضافة الرحلة" : "حفظ التغييرات"}
</button>
</Button>
</div>
</form>
</div>