update page to using same tamplet in all project not using inline code
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user