The trip files have been reorganized. Errors related to vehicle and driver editing have been resolved, archive display issues fixed, and model data display standardized across the project; order and trip-related issues have also been addressed.

This commit is contained in:
m7amedez5511
2026-07-22 14:19:32 +03:00
parent f1a521dd5a
commit b87d81100f
59 changed files with 2261 additions and 3261 deletions

View File

@@ -1,9 +1,21 @@
"use client";
// src/Components/Trip/TripFormModal.tsx
// CHANGE: renamed from Tripformmodal.tsx (proper PascalCase, matches
// UserFormModal.tsx / DriverFormModal.tsx). Split out of the old single-file
// Trip module into TripFormModal.tsx / TripTable.tsx / TripDetailModal.tsx.
// FIX 1: notes/endReason schema — empty string now transforms to undefined
// before .min() runs, so leaving them blank on create no longer fails
// validation despite .optional() (see trip.validator.ts).
// FIX 2: edit-mode defaultValues now fall back to the nested driver/car/branch
// ids, and setValue() re-applies the saved id once each dropdown's options
// actually load — previously the <select> had no matching <option> yet when
// defaultValues were first applied, so the old selection silently reset to "".
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { Alert, Button, Input, Select, Textarea, Spinner } from "../UI";
import { Alert, Button, Input, Modal, Select, Textarea } from "../UI";
import { getStoredToken } from "@/src/lib/auth";
import {
createTripSchema,
@@ -21,6 +33,8 @@ import type { DriverOption } from "@/src/types/driver";
import type { CarOption } from "@/src/types/car";
import type { BranchOption } from "@/src/types/branch";
const FORM_ID = "trip-form";
// ── Shared styles ────────────────────────────────────────────────────────────
// Only for section headings — everything field-level now comes from the
// Input / Select / Textarea components themselves.
@@ -82,29 +96,34 @@ export function TripFormModal({
const [cars, setCars] = useState<CarOption[]>([]);
const [branches, setBranches] = useState<BranchOption[]>([]);
useEffect(() => {
const token = getStoredToken();
driverService.getActiveOptions(token).then(setDrivers).catch(() => {});
carService.getActiveOptions(token).then(setCars).catch(() => {});
branchService.getOptions(token).then(setBranches).catch(() => {});
}, []);
// ── react-hook-form ────────────────────────────────────────────────────────
// Moved above the dropdown-loading effect below: that effect calls
// setValue(), so useForm() (which defines it) must run first — otherwise
// setValue is referenced before its own initialization (TDZ ReferenceError).
const {
register,
handleSubmit,
setError,
setFocus,
setValue,
formState: { errors, isSubmitting },
} = useForm<TripFormValues>({
// 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),
// Cast both the schema argument and the resolver's result:
// - argument: createTripSchema/updateTripSchema have structurally
// different shapes (create requires title/driverId/etc., update makes
// everything optional), so the ternary's type is a union yupResolver's
// single-schema signature won't accept.
// - result: yup's inferred type for .optional() fields
// (`collectedCount?: number`) is structurally incompatible with
// TripFormValues's `collectedCount: number | undefined` — letting
// useForm<TripFormValues> alone establish the field-values type avoids
// that mismatch.
resolver: yupResolver((isNew ? createTripSchema : updateTripSchema) as any) as any,
defaultValues: {
title: editTrip?.title ?? "",
driverId: editTrip?.driverId ?? "",
carId: editTrip?.carId ?? "",
branchId: editTrip?.branchId ?? "",
driverId: editTrip?.driverId ?? editTrip?.driver?.id ?? "",
carId: editTrip?.carId ?? editTrip?.car?.id ?? "",
branchId: editTrip?.branchId ?? editTrip?.branch?.id ?? "",
status: editTrip?.status ?? "Scheduled",
startTime: editTrip?.startTime?.slice(0, 16) ?? "",
endTime: editTrip?.endTime?.slice(0, 16) ?? "",
@@ -119,6 +138,31 @@ export function TripFormModal({
},
});
useEffect(() => {
const token = getStoredToken();
driverService.getActiveOptions(token).then((list) => {
setDrivers(list);
// defaultValues were applied before this list existed, so the <select>
// had no matching <option> yet and silently fell back to "" — reapply
// the saved id now that the option actually exists in the DOM.
const savedDriverId = editTrip?.driverId ?? editTrip?.driver?.id;
if (savedDriverId) setValue("driverId", savedDriverId);
}).catch(() => {});
carService.getActiveOptions(token).then((list) => {
setCars(list);
const savedCarId = editTrip?.carId ?? editTrip?.car?.id;
if (savedCarId) setValue("carId", savedCarId);
}).catch(() => {});
branchService.getOptions(token).then((list) => {
setBranches(list);
const savedBranchId = editTrip?.branchId ?? editTrip?.branch?.id;
if (savedBranchId) setValue("branchId", savedBranchId);
}).catch(() => {});
}, [editTrip, setValue]);
const [apiError, setApiError] = useState("");
// register("title") already attaches its own ref — Input forwards it
@@ -127,13 +171,6 @@ export function TripFormModal({
useEffect(() => {
setFocus("title");
}, [setFocus]);
useEffect(() => {
const h = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", h);
return () => window.removeEventListener("keydown", h);
}, [onClose]);
const numberField = (
field: "collectedCount" | "deliveredCount" | "returnedCount" | "totalCashCollected",
@@ -189,273 +226,205 @@ export function TripFormModal({
};
return (
<div
role="dialog"
aria-modal="true"
aria-labelledby="trip-form-title"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
style={{
position: "fixed",
inset: 0,
zIndex: 60,
background: "rgba(15,23,42,0.55)",
backdropFilter: "blur(4px)",
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
padding: "2rem 1rem",
overflowY: "auto",
}}
>
<div
onClick={(e) => e.stopPropagation()}
style={{
width: "100%",
maxWidth: 700,
background: "var(--color-surface)",
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
boxShadow: "0 24px 56px rgba(0,0,0,.2)",
overflow: "hidden",
}}
>
{/* Header */}
<div
style={{
padding: "1.25rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<h2
id="trip-form-title"
style={{
fontSize: 16,
fontWeight: 700,
color: "var(--color-text-primary)",
margin: 0,
}}
>
{isNew ? "إضافة رحلة جديدة" : "تعديل بيانات الرحلة"}
</h2>
<Button
type="button"
variant="ghost"
size="sm"
onClick={onClose}
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>
<Modal
open
onClose={onClose}
size="lg"
title={isNew ? "إضافة رحلة جديدة" : "تعديل بيانات الرحلة"}
footer={
<>
<Button type="button" variant="secondary" onClick={onClose} disabled={isSubmitting}>
إلغاء
</Button>
<Button type="submit" form={FORM_ID} variant="primary" loading={isSubmitting}>
{isNew ? "إضافة الرحلة" : "حفظ التغييرات"}
</Button>
</>
}
>
<form
id={FORM_ID}
onSubmit={handleSubmit(submitHandler)}
noValidate
dir="rtl"
className="flex flex-col gap-4"
>
{errors.title?.type === "manual" && (
<Alert type="error" message={errors.title.message ?? ""} onClose={() => setApiError("")} />
)}
{/* ── Section: trip management ── */}
<p style={sectionHeadingStyle}>أساسيات الرحلة</p>
{/* Title */}
<Input
label="عنوان الرحلة *"
error={titleError}
{...register("title")}
placeholder="مثال: توزيع الرياض الشمالي"
dir="rtl"
/>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
{/* Driver */}
<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 */}
<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>
{/* Form */}
<form
onSubmit={handleSubmit(submitHandler)}
noValidate
style={{
padding: "1.5rem",
display: "flex",
flexDirection: "column",
gap: "1rem",
}}
>
{errors.title?.type === "manual" && (
<Alert type="error" message={errors.title.message ?? ""} onClose={() => setApiError("")} />
)}
{/* ── Section: trip management ── */}
<p style={sectionHeadingStyle}>أساسيات الرحلة</p>
{/* Title */}
<Input
label="عنوان الرحلة *"
error={titleError}
{...register("title")}
placeholder="مثال: توزيع الرياض الشمالي"
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
{/* Branch */}
<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 */}
<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" }}>
<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>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
{/* Driver */}
<Select
label="السائق *"
error={errors.driverId?.message}
{...register("driverId")}
{/* ── Section: الأعداد والمبالغ ── */}
<p style={sectionHeadingStyle}>الأعداد والمبالغ</p>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr 1fr", gap: "0.75rem" }}>
<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" }}>
<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>
<Input
label="سبب التعديل"
hint="مطلوب عند تغيير السائق أو السيارة"
{...register("reason")}
placeholder="مثال: تغيير السائق بسبب إجازة طارئة"
dir="rtl"
>
<option value="">اختر السائق</option>
{drivers.map((d) => (
<option key={d.id} value={d.id}>
{d.name} {d.phone}
</option>
))}
</Select>
{/* Car */}
<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 */}
<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 */}
<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" }}>
<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" }}>
<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" }}>
<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>
<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" variant="secondary" onClick={onClose} disabled={isSubmitting}>
إلغاء
</Button>
<Button type="submit" variant="primary" loading={isSubmitting}>
{isSubmitting ? "جارٍ الحفظ…" : isNew ? "إضافة الرحلة" : "حفظ التغييرات"}
</Button>
</div>
</form>
</div>
</div>
</>
)}
</form>
</Modal>
);
}