From b87d81100fa9ded3b53a664634e1f15fc2d5f9b2 Mon Sep 17 00:00:00 2001 From: m7amedez5511 Date: Wed, 22 Jul 2026 14:19:32 +0300 Subject: [PATCH] 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. --- app/components/layout/Sidebar.tsx | 18 +- app/dashboard/drivers/page.tsx | 6 +- app/dashboard/trips/[tripId]/page.tsx | 71 ++- .../trips/archived/[tripId]/page.tsx | 114 +--- app/dashboard/trips/page.tsx | 500 ++--------------- src/Components/Branch/BranchDetailModal.tsx | 248 +++----- src/Components/Branch/BranchTable.tsx | 51 +- .../archive/ArchivedBranchDetailModal.tsx | 183 ++---- .../Branch/archive/ArchivedBranchTable.tsx | 49 +- .../Branch/archive/ArchivedBranchesModal.tsx | 98 +--- .../ArchivedCarMaintananceDetailpanel.tsx | 145 ++--- .../archive/ArchivedCarMaintananceTable.tsx | 27 +- .../archive/ArchivedCarsMaintananceModal.tsx | 116 ++-- src/Components/Client/Clienttable.tsx | 139 +---- .../archive/ArchivedClientDetailModal.tsx | 386 +++++-------- .../Client/archive/ArchivedClientTable.tsx | 38 +- .../Client/archive/ArchivedClientsModal.tsx | 115 ++-- .../ArchivedClientAddressesDetailModal.tsx | 168 ++---- .../archive/ArchivedClientAddressesTable.tsx | 36 +- .../archive/ArchivedClientsAddressesModal.tsx | 109 ++-- src/Components/Driver/DriverDetailPanel.tsx | 61 +- src/Components/Driver/DriverFormModal.tsx | 59 +- .../archive/ArchivedDriverDetailModal.tsx | 351 +++++------- .../Driver/archive/ArchivedDriverTable.tsx | 51 +- .../Driver/archive/ArchivedDrivers.tsx | 8 +- src/Components/Order/OrderDetailBanel.tsx | 103 +--- src/Components/Order/OrderFormModal.tsx | 8 - .../archive/ArchivedOrderDetailModal.tsx | 93 ++- .../Order/archive/ArchivedOrderTable.tsx | 44 +- src/Components/Trip/TripDetailModal.tsx | 155 +++++ src/Components/Trip/TripTable.tsx | 172 ++++++ src/Components/Trip/Tripformmodal.tsx | 531 +++++++++--------- .../Trip/archive/ArchivedTripList.tsx | 179 ------ .../Trip/archive/ArchivedTripModal.tsx | 87 +++ .../Trip/archive/ArchivedTripTable.tsx | 135 +++++ .../Trip/archive/ArchivedtripDetailModal.tsx | 155 +++++ src/Components/Trip/index.ts | 6 +- src/Components/UI/IconBtn.tsx | 22 + src/Components/UI/ModalProps.tsx | 5 +- src/Components/UI/index.ts | 3 +- src/Components/User/UserFormModal.tsx | 29 +- src/Components/User/UserTable.tsx | 17 +- .../User/archive/Archiveduserdetailmodal.tsx | 1 + .../User/archive/Archivedusertable.tsx | 17 +- src/Components/car/CarDetailPanel.tsx | 60 +- src/Components/car/CarFormModal.tsx | 71 ++- src/Components/car/CarImageGallery.tsx | 90 +-- .../car/archive/Archivedcardetailpanel.tsx | 164 ++---- .../car/archive/Archivedcarsmodal.tsx | 115 ++-- .../car/archive/Archivedcartable.tsx | 33 +- src/Components/role/RoleTable.tsx | 25 +- .../role/archive/ArchivedRoleDetailModal.tsx | 1 + .../role/archive/ArchivedRoleTable.tsx | 15 +- src/hooks/archive/useArchivedDrivers.ts | 18 +- src/hooks/useDriver.ts | 1 + src/hooks/useUser.ts | 2 +- .../archive/archivedDriver.service.ts | 13 +- src/types/driver.ts | 1 + src/validations/trip.validator.ts | 4 + 59 files changed, 2261 insertions(+), 3261 deletions(-) create mode 100644 src/Components/Trip/TripDetailModal.tsx create mode 100644 src/Components/Trip/TripTable.tsx delete mode 100644 src/Components/Trip/archive/ArchivedTripList.tsx create mode 100644 src/Components/Trip/archive/ArchivedTripModal.tsx create mode 100644 src/Components/Trip/archive/ArchivedTripTable.tsx create mode 100644 src/Components/Trip/archive/ArchivedtripDetailModal.tsx create mode 100644 src/Components/UI/IconBtn.tsx diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index d498dc0..cf93166 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -146,7 +146,20 @@ const GRAD = "linear-gradient(175deg, #1E3A8A 0%, #1D4ED8 60%, #1565C0 100%)"; ══════════════════════════════════════════ */ export function Sidebar() { const pathname = usePathname(); - const user = getStoredUser(); + + // ⚠️ التقاط اليوزر بعد الـ mount بس، عشان أول render على الكلاينت + // يتطابق مع السيرفر (فاضي) ويتجنب hydration mismatch + const [user, setUser] = useState>(null); + + useEffect(() => { + setUser(getStoredUser()); + }, []); + + // إعادة القراءة بعد أي navigation (زي بعد الحفظ لو الصلاحيات اتغيرت) + useEffect(() => { + setUser(getStoredUser()); + }, [pathname]); + const permissions = user?.permissions ?? []; const { open, setOpen } = useSidebarDrawer(); @@ -363,7 +376,6 @@ function SidebarContent({ pathname.startsWith(item.href + "/")); return ( ); -} +} \ No newline at end of file diff --git a/app/dashboard/drivers/page.tsx b/app/dashboard/drivers/page.tsx index 1377c25..d9a24a2 100644 --- a/app/dashboard/drivers/page.tsx +++ b/app/dashboard/drivers/page.tsx @@ -6,6 +6,7 @@ import { Spinner, ArchiveButton, ConfirmDialog, + Toast, } from "@/src/Components/UI"; import { ArchivedDrivers } from "@/src/Components/Driver/archive/ArchivedDrivers"; import { useDrivers } from "@/src/hooks/useDriver"; @@ -87,6 +88,7 @@ export default function DriversPage() { updateDriver, deleteDriver, notification, + clearNotification, } = useDrivers(); // ── Panel / modal state ─────────────────────────────────────────────────── @@ -174,9 +176,7 @@ export default function DriversPage() { /> {/* ── Notifications ── */} - {notification && ( - - )} + {error && } {/* ── Table ── */} diff --git a/app/dashboard/trips/[tripId]/page.tsx b/app/dashboard/trips/[tripId]/page.tsx index 0ea45ac..7df8f52 100644 --- a/app/dashboard/trips/[tripId]/page.tsx +++ b/app/dashboard/trips/[tripId]/page.tsx @@ -1,9 +1,18 @@ "use client"; +// app/dashboard/trips/[tripId]/page.tsx +// CHANGE: TripFormModal now imported from the barrel (@/src/Components/Trip) +// instead of a direct path — avoids the Tripformmodal.tsx case-sensitivity +// mismatch that breaks on case-sensitive filesystems (Linux/Docker builds). +// CHANGE: loadTrip's catch block now uses the same extractApiMessage helper +// already defined in this file (and used by handleEditSubmit/handleConfirmDelete) +// instead of `err instanceof Error ? err.message : ...` — so backend +// validation messages surface correctly here too, not just on edit/delete. + import { useCallback, useEffect, useRef, useState } from "react"; import { useParams, useRouter } from "next/navigation"; import { ConfirmDialog, Spinner } from "@/src/Components/UI"; -import { TripFormModal } from "@/src/Components/Trip/Tripformmodal"; +import { TripFormModal } from "@/src/Components/Trip"; import { TripReportPanel } from "@/src/Components/Trip_Report/Tripreportpanel"; import { tripService } from "@/src/services/trip.service"; import { getStoredToken } from "@/src/lib/auth"; @@ -205,42 +214,42 @@ export default function TripDetailPage() { // ── Load trip ───────────────────────────────────────────────────────────── const loadTrip = useCallback(async () => { - if (!tripId) return; - setLoading(true); - setError(null); - try { - const token = getStoredToken(); - const data = await tripService.getById(tripId, token); - setTrip(data); - } catch (err: unknown) { - setError(err instanceof Error ? err.message : "تعذّر تحميل بيانات الرحلة."); - } finally { - setLoading(false); - } -}, [tripId]); + if (!tripId) return; + setLoading(true); + setError(null); + try { + const token = getStoredToken(); + const data = await tripService.getById(tripId, token); + setTrip(data); + } catch (err: unknown) { + setError(extractApiMessage(err, "تعذّر تحميل بيانات الرحلة.")); + } finally { + setLoading(false); + } + }, [tripId]); useEffect(() => { queueMicrotask(loadTrip); }, [loadTrip]); // ── Edit submit ─────────────────────────────────────────────────────────── const handleEditSubmit = useCallback( - async ( - payload: CreateTripPayload | UpdateTripPayload, - ): Promise => { - if (!trip) return false; - try { - const token = getStoredToken(); - const updated = await tripService.update(trip.id, payload as UpdateTripPayload, token); - setTrip(updated); - notify({ type: "success", message: "تم تحديث بيانات الرحلة بنجاح." }); - return true; - } catch (err) { - notify({ type: "error", message: extractApiMessage(err, "تعذّر تحديث الرحلة.") }); - return false; - } - }, - [trip, notify], -); + async ( + payload: CreateTripPayload | UpdateTripPayload, + ): Promise => { + if (!trip) return false; + try { + const token = getStoredToken(); + const updated = await tripService.update(trip.id, payload as UpdateTripPayload, token); + setTrip(updated); + notify({ type: "success", message: "تم تحديث بيانات الرحلة بنجاح." }); + return true; + } catch (err) { + notify({ type: "error", message: extractApiMessage(err, "تعذّر تحديث الرحلة.") }); + return false; + } + }, + [trip, notify], + ); // ── Delete confirm ──────────────────────────────────────────────────────── const handleConfirmDelete = useCallback(async () => { diff --git a/app/dashboard/trips/archived/[tripId]/page.tsx b/app/dashboard/trips/archived/[tripId]/page.tsx index 5837b83..5134eab 100644 --- a/app/dashboard/trips/archived/[tripId]/page.tsx +++ b/app/dashboard/trips/archived/[tripId]/page.tsx @@ -1,110 +1,28 @@ "use client"; -import { useEffect, useState } from "react"; -import { useParams, useRouter } from "next/navigation"; -import { Spinner, EmptyState, Badge, PageHeader } from "@/src/Components/UI"; -import { tripService } from "@/src/services/trip.service"; -import { getStoredToken } from "@/src/lib/auth"; -import type { Trip } from "@/src/types/trip"; -import { TRIP_STATUS_MAP } from "@/src/types/trip"; +import { ArchivedTripDetailModal } from "@/src/Components/Trip/archive/ArchivedtripDetailModal"; +// app/dashboard/trips/archived/[tripId]/page.tsx +// CHANGE: rebuilt as a thin route wrapper around the shared +// ArchivedTripDetailModal instead of its own independent fetch/loading/error/ +// render logic. This route now shows the exact same full detail view as the +// in-app archive browser (driver + car + counts + cash + notes + endReason, +// not just the handful of fields this page used to render on its own), and +// stays in sync automatically with any future changes to that component. +// `onClose` navigates back to the trips list (via router.push) instead of +// closing an in-place modal, since this route has no page behind it to +// reveal. -function fmtDateTime(iso?: string | null): string { - if (!iso) return "—"; - return new Date(iso).toLocaleString("ar-SA", { dateStyle: "medium", timeStyle: "short" }); -} +import { useParams, useRouter } from "next/navigation"; export default function ArchivedTripDetailPage() { const params = useParams(); const router = useRouter(); const tripId = params?.tripId as string; - const [trip, setTrip] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - if (!tripId) return; - - let cancelled = false; - (async () => { - setLoading(true); - setError(null); - try { - const token = getStoredToken(); - const data = await tripService.getArchivedById(tripId, token); - if (!cancelled) setTrip(data); - } catch { - if (!cancelled) setError("لم يتم العثور على هذه الرحلة في الأرشيف."); - } finally { - if (!cancelled) setLoading(false); - } - })(); - - return () => { cancelled = true; }; - }, [tripId]); - - if (loading) { - return ( -
- - جارٍ التحميل… -
- ); - } - - if (error || !trip) { - return ( - router.push("/dashboard/trips")} - className="text-sm font-semibold text-[var(--color-brand-600)] underline" - > - العودة إلى قائمة الرحلات - - } - /> - ); - } - - const statusConfig = TRIP_STATUS_MAP[trip.status]; - return ( -
- } - /> - -
- -
-
-
وقت البدء
-
{fmtDateTime(trip.startTime)}
-
-
-
وقت الانتهاء
-
{fmtDateTime(trip.endTime)}
-
-
-
تاريخ الأرشفة
-
{fmtDateTime(trip.deletedAt)}
-
- {trip.driver && ( -
-
السائق
-
{trip.driver.name}
-
- )} -
-
-
+ router.push("/dashboard/trips")} + /> ); } \ No newline at end of file diff --git a/app/dashboard/trips/page.tsx b/app/dashboard/trips/page.tsx index 0138b8c..2d6b1f5 100644 --- a/app/dashboard/trips/page.tsx +++ b/app/dashboard/trips/page.tsx @@ -1,164 +1,31 @@ "use client"; +// app/dashboard/trips/page.tsx +// CHANGE: replaced the old hand-rolled inline table with . +// CHANGE: TripFormModal now imported from the barrel (@/src/Components/Trip) +// instead of a direct path — avoids the Tripformmodal.tsx case-sensitivity +// mismatch that breaks on case-sensitive filesystems (Linux/Docker builds). +// CHANGE: removed the local ArchivedTripsModal wrapper (it duplicated what +// the new ArchivedTripModal already does — search bar + table + detail) and +// removed the ArchivedTripList import; now renders +// directly. + import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { useTrips } from "@/src/hooks/useTrip"; -import { TripFormModal } from "@/src/Components/Trip/Tripformmodal"; -import { ArchivedTripList } from "@/src/Components/Trip/archive/ArchivedTripList"; -import { Alert, Spinner, ArchiveButton, ConfirmDialog , Toast } from "@/src/Components/UI"; -import type { - Trip, - TripStatus, - CreateTripPayload, - UpdateTripPayload, -} from "@/src/types/trip"; -import { TRIP_STATUS_MAP } from "@/src/types/trip"; +import { TripFormModal, TripTable } from "@/src/Components/Trip"; +import { ArchivedTripModal } from "@/src/Components/Trip/archive/ArchivedTripModal"; +import { Alert, ArchiveButton, ConfirmDialog, Toast } from "@/src/Components/UI"; +import type { Trip, CreateTripPayload, UpdateTripPayload } from "@/src/types/trip"; import Header from "@/src/Components/UI/Header"; -// ── Status badge — same visual pattern as Driver's status badge ───────────── - -function StatusBadge({ status }: { status: TripStatus }) { - const s = TRIP_STATUS_MAP[status]; - return ( - - - {s.label} - - ); -} - -// ── Archived trips modal — unchanged wrapper, kept as-is ──────────────────── - -function ArchivedTripsModal({ onClose }: { onClose: () => void }) { - const router = useRouter(); - - useEffect(() => { - const h = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - window.addEventListener("keydown", h); - return () => window.removeEventListener("keydown", h); - }, [onClose]); - - 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", - }} - > -
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", - }} - > -
-

- أرشيف الرحلات -

- -
- -
- {/* Was pointing at the normal trip page — send it to the archived route instead */} - - router.push(`/dashboard/trips/archived/${trip.id}`) - } - /> -
-
-
- ); -} - // ── Page ────────────────────────────────────────────────────────────────────── export default function TripsPage() { const router = useRouter(); - // All list state + mutations now come from the hook, same as useDrivers - // is used on the Driver page. No direct tripService calls in this file. + // All list state + mutations come from the hook, same as useDrivers is + // used on the Driver page. No direct tripService calls in this file. const { trips, total, @@ -169,8 +36,6 @@ export default function TripsPage() { setPage, search, handleSearch, - status, - handleStatusFilter, createTrip, updateTrip, deleteTrip, @@ -202,6 +67,19 @@ export default function TripsPage() { setDeleteTarget(trip); }, []); + // TripTable exposes a dedicated "view" action (icon button) instead of a + // row click — wired to the trip detail page, same destination the old + // row-click handler used to navigate to. + const handleView = useCallback( + (trip: Trip) => router.push(`/dashboard/trips/${trip.id}`), + [router], + ); + + const handleAdd = useCallback(() => { + setEditTrip(null); + setShowForm(true); + }, []); + // ── Create / update — delegates to the hook (hook fires the toast) ─────── const handleSubmit = async ( payload: CreateTripPayload | UpdateTripPayload, @@ -233,298 +111,38 @@ export default function TripsPage() { }} > {/* ── Header ── */} -
{}} - setPage={setPage} - isAudit={false} - onAdd={() => { setEditTrip(null); setShowForm(true); }}/> +
{}} + setPage={setPage} + isAudit={false} + onAdd={handleAdd} + /> + {/* ── Error handling: Alert banner for list/API errors ── Same pattern as the Driver page: a persistent banner tied to the hook's `error` state, dismissible via `clearError`. */} {error && } {/* ── Table ── */} -
-
- رقم الرحلة - العنوان - السائق - السيارة - الحالة - البدء - الانتهاء - إجراءات -
- - {loading ? ( -
- - جارٍ التحميل… -
- ) : trips.length === 0 ? ( -

- لا توجد رحلات مطابقة {search && `لـ "${search}"`} -

- ) : ( -
    - {trips.map((trip, i) => ( -
  • router.push(`/dashboard/trips/${trip.id}`)} - style={{ - display: "grid", - gridTemplateColumns: - "1.5fr 2fr 1.3fr 1.3fr 1fr 1.1fr 1.1fr 0.8fr", - alignItems: "center", - gap: "0.5rem", - padding: "0.875rem 1.5rem", - borderBottom: "1px solid var(--color-border)", - background: - i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent", - fontSize: 13, - cursor: "pointer", - transition: "background 0.15s", - }} - onMouseEnter={(e) => - (e.currentTarget.style.background = - "var(--color-surface-hover, #F8FAFC)") - } - onMouseLeave={(e) => - (e.currentTarget.style.background = - i % 2 !== 0 ? "var(--color-surface-muted)" : "transparent") - } - > - - {trip.tripNumber} - - - {trip.title} - - - {trip.driver?.name ?? "—"} - - - {trip.car - ? `${trip.car.manufacturer} ${trip.car.model}` - : "—"} - - - - {trip.startTime - ? new Date(trip.startTime).toLocaleString("ar-SA", { - dateStyle: "short", - timeStyle: "short", - }) - : "—"} - - - {trip.endTime - ? new Date(trip.endTime).toLocaleString("ar-SA", { - dateStyle: "short", - timeStyle: "short", - }) - : "—"} - - - {/* Inline row actions — stopPropagation so the click does not - also trigger navigation to the detail page. */} -
    e.stopPropagation()} - > - - - -
    -
  • - ))} -
- )} - - {/* ── Pagination — same pattern as Driver page ── */} - {pages > 1 && ( -
- - صفحة{" "} - - {page} - {" "} - من{" "} - - {pages} - - {" — "} - {total} رحلة - -
- {[ - { - label: "السابق", - action: () => setPage((p) => Math.max(1, p - 1)), - disabled: page === 1, - }, - { - label: "التالي", - action: () => setPage((p) => Math.min(pages, p + 1)), - disabled: page === pages, - }, - ].map((btn) => ( - - ))} -
-
- )} -
+ {/* ── Form modal ── */} {showForm && ( @@ -549,10 +167,10 @@ export default function TripsPage() { description={`هل أنت متأكد من حذف رحلة ${deleteTarget?.title ?? ""} (${deleteTarget?.tripNumber ?? ""})؟ لا يمكن التراجع عن هذا الإجراء.`} /> - {/* ── Archive browser modal ── */} - {archiveOpen && ( - setArchiveOpen(false)} /> - )} + {/* ── Archive browser modal — now the split ArchivedTripModal directly, + no local wrapper needed since it already ships its own search bar, + table, and detail-view wiring. ── */} + {archiveOpen && setArchiveOpen(false)} />} {/* ── Floating archive button — same component Driver page uses ── */} setArchiveOpen(true)} label="الأرشيف" /> diff --git a/src/Components/Branch/BranchDetailModal.tsx b/src/Components/Branch/BranchDetailModal.tsx index b7fd5c3..87b3cfa 100644 --- a/src/Components/Branch/BranchDetailModal.tsx +++ b/src/Components/Branch/BranchDetailModal.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useState } from "react"; -import { Spinner } from "../UI"; +import { Alert, Badge, Button, Modal, Spinner } from "../UI"; import { getStoredToken } from "@/src/lib/auth"; import { branchService } from "@/src/services/branch.service"; import type { BranchDetail } from "@/src/types/branch"; @@ -11,7 +11,7 @@ interface BranchDetailModalProps { onClose: () => void; } -// ── small helper components ─────────────────────────────────────────────────── +// ── small helper components (no template exists for a plain label/value row) ── function DetailRow({ label, value }: { label: string; value?: string | null }) { return (
- - {active ? "نشط" : "معطل"} - - ); -} - +// Branch icon badge has no equivalent in the shared UI kit — kept custom. function BranchIcon() { return (
(null); - // close on Escape - useEffect(() => { - const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [onClose]); - // fetch branch details on mount useEffect(() => { - let cancelled = false; - (async () => { - try { - const token = getStoredToken(); - const data = await branchService.getById(branchId, token); - if (!cancelled) setBranch(data); - } catch { - if (!cancelled) setError("تعذّر تحميل بيانات الفرع. يرجى المحاولة لاحقاً."); - } finally { - if (!cancelled) setLoading(false); - } - })(); - return () => { cancelled = true; }; -}, [branchId]); + let cancelled = false; + (async () => { + try { + const token = getStoredToken(); + const data = await branchService.getById(branchId, token); + if (!cancelled) setBranch(data); + } catch { + if (!cancelled) setError("تعذّر تحميل بيانات الفرع. يرجى المحاولة لاحقاً."); + } finally { + if (!cancelled) setLoading(false); + } + })(); + return () => { cancelled = true; }; + }, [branchId]); // ── helpers ─────────────────────────────────────────────────────────────── const fmt = (iso?: string | null) => @@ -102,148 +79,71 @@ export function BranchDetailModal({ branchId, onClose }: BranchDetailModalProps) // ── render ──────────────────────────────────────────────────────────────── return ( -
{ if (e.target === e.currentTarget) onClose(); }} - style={{ - position: "fixed", inset: 0, zIndex: 55, - background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", - display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", - }} + + إغلاق + + } > -
e.stopPropagation()} - style={{ - width: "100%", maxWidth: 480, - 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 ── */} -
-
-

- بيانات الفرع -

-

- {branch?.name ?? "عرض الفرع"} -

-
- + {/* loading */} + {loading && ( +
+ + جارٍ التحميل…
+ )} - {/* ── body ── */} -
+ {/* error */} + {!loading && error && } - {/* loading */} - {loading && ( -
- - جارٍ التحميل… -
- )} + {/* content */} + {!loading && branch && ( +
- {/* error */} - {!loading && error && ( -
- {error} -
- )} - - {/* content */} - {!loading && branch && ( -
- - {/* icon + name + status row */} -
- -
-

{branch.name}

- {branch.city && ( -

- {branch.city} -

- )} -
- -
-
+ {/* icon + name + status row */} +
+ +
+

{branch.name}

+ {branch.city && ( +

+ {branch.city} +

+ )} +
+
- - {/* detail rows */} - - - - - - - - -
- )} -
+
- {/* ── footer ── */} -
- + {/* detail rows */} + + + + + + + + +
-
-
+ )} + ); } \ No newline at end of file diff --git a/src/Components/Branch/BranchTable.tsx b/src/Components/Branch/BranchTable.tsx index b6514c7..17f4682 100644 --- a/src/Components/Branch/BranchTable.tsx +++ b/src/Components/Branch/BranchTable.tsx @@ -1,28 +1,8 @@ "use client"; -import { Spinner } from "../UI"; +import { Badge, Button, EmptyState, IconBtn, Spinner } from "../UI"; import type { Branch } from "@/src/types/branch"; -// ── status badge ───────────────────────────────────────────────────────────── -function StatusBadge({ active }: { active: boolean }) { - return ( - - - {active ? "نشط" : "معطل"} - - ); -} - -// ── icon button ────────────────────────────────────────────────────────────── -function IconBtn({ onClick, title, color, bg, borderColor, children }: { onClick: () => void; title: string; color: string; bg: string; borderColor: string; children: React.ReactNode }) { - return ( - - ); -} - // ── card / header styles ───────────────────────────────────────────────────── const cardStyle: React.CSSProperties = { borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)", @@ -71,17 +51,17 @@ export function BranchTable({ branches, loading, search, page, pages, onEdit, on
) : branches.length === 0 ? ( /* empty state */ -
-

- {search ? `لا توجد نتائج لـ "${search}"` : "لا توجد فروع لعرضها."} -

- {!search && ( - - )} -
+ + أضف أول فرع + + ) + } + /> ) : ( /* data rows */
    @@ -99,7 +79,7 @@ export function BranchTable({ branches, loading, search, page, pages, onEdit, on
{b.city || "—"} {b.phone ?? "—"} - + {new Date(b.createdAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })} @@ -142,10 +122,9 @@ export function BranchTable({ branches, loading, search, page, pages, onEdit, on { label: "السابق", action: () => onPageChange(Math.max(1, page - 1)), disabled: page === 1 }, { label: "التالي", action: () => onPageChange(Math.min(pages, page + 1)), disabled: page === pages }, ].map(btn => ( - + ))}
diff --git a/src/Components/Branch/archive/ArchivedBranchDetailModal.tsx b/src/Components/Branch/archive/ArchivedBranchDetailModal.tsx index 95c11e2..bf7c549 100644 --- a/src/Components/Branch/archive/ArchivedBranchDetailModal.tsx +++ b/src/Components/Branch/archive/ArchivedBranchDetailModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect } from "react"; +import { Badge, Button, Modal } from "../../UI"; import type { ArchivedBranch } from "@/src/types/branch"; interface ArchivedBranchDetailModalProps { @@ -26,23 +26,6 @@ function DetailRow({ label, value }: { label: string; value?: string | null }) { ); } -function StatusBadge({ active }: { active: boolean }) { - return ( - - - {active ? "نشط" : "معطل"} - - ); -} - function BranchIcon() { return (
{ - const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [onClose]); - // ── helpers ─────────────────────────────────────────────────────────────── const fmt = (iso?: string | null) => iso ? new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "long", day: "numeric" }) : null; @@ -81,126 +57,61 @@ export function ArchivedBranchDetailModal({ branch, onClose }: ArchivedBranchDet // ── render ──────────────────────────────────────────────────────────────── return ( -
{ if (e.target === e.currentTarget) onClose(); }} - style={{ - position: "fixed", inset: 0, zIndex: 55, - background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", - display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", - }} + + إغلاق + + } > -
e.stopPropagation()} - style={{ - width: "100%", maxWidth: 480, - 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 ── */} +
+ + {/* icon + name + status row */}
-
-

- فرع مؤرشف + +

+

{branch.name}

+

+ #{branch.id}

-

- {branch.name} -

-
- -
- - {/* ── body ── */} -
-
- - {/* icon + name + status row */} -
- -
-

{branch.name}

-

- #{branch.id} -

-
- -
-
+
+
- - {/* detail rows — every field requested */} - - - - - - - - - - - - - -
- {/* ── footer ── */} -
- -
+ {/* detail rows — every field requested */} + + + + + + + + + + + + + +
-
+ ); } \ No newline at end of file diff --git a/src/Components/Branch/archive/ArchivedBranchTable.tsx b/src/Components/Branch/archive/ArchivedBranchTable.tsx index e16e142..ded557e 100644 --- a/src/Components/Branch/archive/ArchivedBranchTable.tsx +++ b/src/Components/Branch/archive/ArchivedBranchTable.tsx @@ -1,28 +1,8 @@ "use client"; -import { Spinner } from "../../UI"; +import { Badge, Button, IconBtn, Spinner } from "../../UI"; import type { ArchivedBranch } from "@/src/types/branch"; -// ── status badge ───────────────────────────────────────────────────────────── -function StatusBadge({ active }: { active: boolean }) { - return ( - - - {active ? "نشط" : "معطل"} - - ); -} - -// ── icon button ────────────────────────────────────────────────────────────── -function IconBtn({ onClick, title, color, bg, borderColor, children }: { onClick: () => void; title: string; color: string; bg: string; borderColor: string; children: React.ReactNode }) { - return ( - - ); -} - // ── card / header styles ───────────────────────────────────────────────────── const cardStyle: React.CSSProperties = { borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)", @@ -89,7 +69,7 @@ export function ArchivedBranchTable({ branches, loading, search, page, pages, on

{b.street}

{b.city || "—"} - + {new Date(b.createdAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })} @@ -117,15 +97,22 @@ export function ArchivedBranchTable({ branches, loading, search, page, pages, on صفحة {page} من {pages}
- {[ - { label: "السابق", action: () => onPageChange(Math.max(1, page - 1)), disabled: page === 1 }, - { label: "التالي", action: () => onPageChange(Math.min(pages, page + 1)), disabled: page === pages }, - ].map(btn => ( - - ))} + +
)} diff --git a/src/Components/Branch/archive/ArchivedBranchesModal.tsx b/src/Components/Branch/archive/ArchivedBranchesModal.tsx index e7f8d71..8abaec8 100644 --- a/src/Components/Branch/archive/ArchivedBranchesModal.tsx +++ b/src/Components/Branch/archive/ArchivedBranchesModal.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Alert } from "../../UI"; +import { Alert, Input, Modal } from "../../UI"; import { ArchivedBranchTable } from "./ArchivedBranchTable"; import { ArchivedBranchDetailModal } from "./ArchivedBranchDetailModal"; import { useArchivedBranches } from "@/src/hooks/archive/useArchiveBranch"; @@ -11,6 +11,12 @@ interface ArchivedBranchesModalProps { onClose: () => void; } +const SearchIcon = ( + + + +); + export function ArchivedBranchesModal({ onClose }: ArchivedBranchesModalProps) { // hold the full object, not just the id — no GET /branches/archived/{id} route exists const [viewBranch, setViewBranch] = useState(null); @@ -22,79 +28,24 @@ export function ArchivedBranchesModal({ onClose }: ArchivedBranchesModalProps) { } = useArchivedBranches(); 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", - }} - > - {viewBranch && ( - setViewBranch(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)} 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={SearchIcon} />
@@ -111,7 +62,14 @@ export function ArchivedBranchesModal({ onClose }: ArchivedBranchesModalProps) { onPageChange={setPage} />
-
-
+ + + {/* Rendered after the list modal so it stacks visually on top of it + (both use the shared Modal's fixed full-screen overlay at the same + z-index — later in the DOM wins the paint order). */} + {viewBranch && ( + setViewBranch(null)} /> + )} + ); } \ No newline at end of file diff --git a/src/Components/Car_Maintanance/archive/ArchivedCarMaintananceDetailpanel.tsx b/src/Components/Car_Maintanance/archive/ArchivedCarMaintananceDetailpanel.tsx index eb1dc86..6897b41 100644 --- a/src/Components/Car_Maintanance/archive/ArchivedCarMaintananceDetailpanel.tsx +++ b/src/Components/Car_Maintanance/archive/ArchivedCarMaintananceDetailpanel.tsx @@ -1,7 +1,6 @@ "use client"; -import { useEffect } from "react"; -import { Spinner } from "../../UI"; +import { Alert, Badge, Button, Modal, Spinner } from "../../UI"; import { MAINTENANCE_STATUS_MAP, fmtDate, @@ -33,106 +32,60 @@ function DetailRow({ label, value, mono = false }: { label: string; value: strin } export function ArchivedCarMaintenanceDetailPanel({ record, loading, error, onClose }: ArchivedCarMaintenanceDetailPanelProps) { - useEffect(() => { - const h = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", h); - return () => window.removeEventListener("keydown", h); - }, [onClose]); - return ( -
{ if (e.target === e.currentTarget) onClose(); }} - style={{ - position: "fixed", inset: 0, zIndex: 80, - background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", - display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", - }} + + إغلاق + + } > -
e.stopPropagation()} - style={{ - width: "100%", maxWidth: 480, - 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", - }} - > -
-
-

- سجل صيانة مؤرشف -

-

- {record ? record.reason : "عرض السجل"} -

+
+ {loading && ( +
+ + جارٍ التحميل…
- -
+ )} -
- {loading && ( -
- - جارٍ التحميل… -
- )} + {!loading && error && } - {!loading && error && ( -
- {error} -
- )} + {!loading && record && (() => { + const status = MAINTENANCE_STATUS_MAP[getMaintenanceStatus(record)]; + return ( + <> +
+ {/* Dynamic per-status colors come from MAINTENANCE_STATUS_MAP, which + doesn't map cleanly onto the fixed Badge color palette — kept custom. */} + + {status.label} + + +
- {!loading && record && (() => { - const status = MAINTENANCE_STATUS_MAP[getMaintenanceStatus(record)]; - return ( - <> -
- - {status.label} - - - مؤرشف - -
- - {record.car && ( - - )} - - - - - - - - ); - })()} -
- -
- -
+ {record.car && ( + + )} + + + + + + + + ); + })()}
-
+ ); } \ No newline at end of file diff --git a/src/Components/Car_Maintanance/archive/ArchivedCarMaintananceTable.tsx b/src/Components/Car_Maintanance/archive/ArchivedCarMaintananceTable.tsx index bef3f3f..db9364d 100644 --- a/src/Components/Car_Maintanance/archive/ArchivedCarMaintananceTable.tsx +++ b/src/Components/Car_Maintanance/archive/ArchivedCarMaintananceTable.tsx @@ -1,6 +1,6 @@ "use client"; -import { Spinner } from "../../UI"; +import { Button, EmptyState, IconBtn, Spinner } from "../../UI"; import { fmtDateShort, fmtCost } from "@/src/types/carMaintanance"; import type { CarMaintenance } from "@/src/types/carMaintanance"; @@ -15,17 +15,6 @@ const thStyle: React.CSSProperties = { borderBottom: "1px solid var(--color-border)", }; -function IconBtn({ onClick, title, color, bg, borderColor, children }: { - onClick: () => void; title: string; color: string; bg: string; borderColor: string; children: React.ReactNode; -}) { - return ( - - ); -} - interface ArchivedCarMaintenanceTableProps { records: CarMaintenance[]; loading: boolean; @@ -60,11 +49,10 @@ export function ArchivedCarMaintenanceTable({ جارٍ التحميل…
) : records.length === 0 ? ( -
-

- {search ? `لا توجد نتائج لـ "${search}"` : "لا توجد سجلات صيانة في الأرشيف."} -

-
+ ) : (
    {records.map((r, i) => ( @@ -114,10 +102,9 @@ export function ArchivedCarMaintenanceTable({ { label: "السابق", action: () => onPageChange(Math.max(1, page - 1)), disabled: page === 1 }, { label: "التالي", action: () => onPageChange(Math.min(pages, page + 1)), disabled: page === pages }, ].map(btn => ( - + ))}
diff --git a/src/Components/Car_Maintanance/archive/ArchivedCarsMaintananceModal.tsx b/src/Components/Car_Maintanance/archive/ArchivedCarsMaintananceModal.tsx index e620e32..69b00e2 100644 --- a/src/Components/Car_Maintanance/archive/ArchivedCarsMaintananceModal.tsx +++ b/src/Components/Car_Maintanance/archive/ArchivedCarsMaintananceModal.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Alert } from "../../UI"; +import { Alert, Input, Modal } from "../../UI"; import { ArchivedCarMaintenanceTable } from "./ArchivedCarMaintananceTable"; import { ArchivedCarMaintenanceDetailPanel } from "./ArchivedCarMaintananceDetailpanel"; import { useArchivedCarMaintenance } from "@/src/hooks/archive/UseArchivedCarsMaintanance"; @@ -25,15 +25,12 @@ export function ArchivedCarsMaintenanceModal({ carId, carLabel, onClose }: Archi } = useArchivedCarMaintenance(carId); 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", - }} + {viewRecord && ( )} -
e.stopPropagation()} - style={{ - width: "100%", maxWidth: 980, - 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", - }} - > -
-
-

- الأرشيف -

-

- سجلات الصيانة المؤرشفة{carLabel ? ` — ${carLabel}` : ""} - - ({total}) - -

-
- -
- -
-
- - - - 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)", - }} - /> -
- - {error && setError(null)} />} - - setViewRecord(record)} - onPageChange={setPage} +
+
+ handleSearch(e.target.value)} + dir="rtl" + icon={ + + + + } />
+ + {error && setError(null)} />} + + setViewRecord(record)} + onPageChange={setPage} + />
-
+ ); } \ No newline at end of file diff --git a/src/Components/Client/Clienttable.tsx b/src/Components/Client/Clienttable.tsx index cd9f6be..fd198a4 100644 --- a/src/Components/Client/Clienttable.tsx +++ b/src/Components/Client/Clienttable.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; -import { Spinner } from "../UI"; +import { Button, EmptyState, IconBtn, Spinner } from "../UI"; import type { Client } from "@/src/types/client"; // ── Address count badge ──────────────────────────────────────────────────── @@ -38,47 +38,6 @@ function AddressBadge({ count, onClick }: { count: number; onClick: () => void } ); } -// ── Icon button ──────────────────────────────────────────────────────────── -function IconBtn({ - onClick, - title, - color, - bg, - borderColor, - children, -}: { - onClick: () => void; - title: string; - color: string; - bg: string; - borderColor: string; - children: React.ReactNode; -}) { - return ( - - ); -} - // ── Client detail panel (inline expandable) ──────────────────────────────── /** * ClientDetailPanel — shown below the clicked row. @@ -182,55 +141,28 @@ function ClientDetailPanel({ {/* Action buttons */}
{/* Edit client */} - + {/* * "Addresses" button — KEY FEATURE: * Navigates to /dashboard/clients/[clientId]/addresses */} - +
@@ -401,31 +333,17 @@ export function ClientTable({
) : clients.length === 0 ? ( /* Empty state */ -
-

- {search - ? `لا توجد نتائج لـ "${search}"` - : "لا يوجد عملاء لعرضهم."} -

- {!search && ( - - )} -
+ + أضف أول عميل + + ) + } + /> ) : ( /* Data rows */
    @@ -659,25 +577,16 @@ export function ClientTable({ disabled: page === pages, }, ].map((btn) => ( - + ))}
diff --git a/src/Components/Client/archive/ArchivedClientDetailModal.tsx b/src/Components/Client/archive/ArchivedClientDetailModal.tsx index acb3bce..690c6af 100644 --- a/src/Components/Client/archive/ArchivedClientDetailModal.tsx +++ b/src/Components/Client/archive/ArchivedClientDetailModal.tsx @@ -1,7 +1,6 @@ "use client"; -import { useEffect } from "react"; -import { Spinner } from "../../UI"; +import { Alert, Badge, Button, Modal, Spinner } from "../../UI"; import { useArchivedClient } from "@/src/hooks/archive/useArchiveClient"; interface ArchivedClientDetailModalProps { @@ -9,7 +8,7 @@ interface ArchivedClientDetailModalProps { onClose: () => void; } -// ── small helper components ─────────────────────────────────────────────────── +// ── small helper components (no template exists for a plain label/value row) ── function DetailRow({ label, value }: { label: string; value?: string | null }) { return (
- - {active ? "نشط" : "معطل"} - - ); -} - +// Avatar has no equivalent in the shared UI kit — kept custom. function Avatar({ name }: { name: string }) { const initials = name.trim().split(" ").slice(0, 2).map(w => w[0]).join("").toUpperCase(); return ( @@ -60,22 +43,6 @@ function Avatar({ name }: { name: string }) { ); } -// Small badge for an archived order row's status -function OrderStatusBadge({ status }: { status: string }) { - return ( - - {status} - - ); -} - // ── main component ──────────────────────────────────────────────────────────── export function ArchivedClientDetailModal({ clientId, onClose }: ArchivedClientDetailModalProps) { const { @@ -85,233 +52,158 @@ export function ArchivedClientDetailModal({ clientId, onClose }: ArchivedClientD setOrdersPage, } = useArchivedClient(clientId); - // close on Escape - useEffect(() => { - const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [onClose]); - const fmt = (iso?: string | null) => iso ? new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "long", day: "numeric" }) : null; // ── render ──────────────────────────────────────────────────────────────── return ( -
{ if (e.target === e.currentTarget) onClose(); }} - style={{ - position: "fixed", inset: 0, zIndex: 55, - 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", - }} - > - {/* ── header ── */} -
-
-

- عميل مؤرشف -

-

- {client?.name ?? "عرض العميل"} -

-
- + {/* loading */} + {loading && ( +
+ + جارٍ التحميل…
+ )} - {/* ── body ── */} -
+ {/* error */} + {!loading && error && } - {/* loading */} - {loading && ( -
+ {/* content */} + {!loading && client && ( +
+ {/* avatar + name + status row */} +
+ +
+

{client.name}

+
+ +
+
+
+ + {/* detail rows */} + + + + + + + {client.deletedAt && ( + + )} + + {/* ── addresses ── */} +

+ العناوين ({client.addresses?.length ?? 0}) +

+ {!client.addresses || client.addresses.length === 0 ? ( +

+ لا توجد عناوين مرتبطة بهذا العميل. +

+ ) : ( +
+ {client.addresses.map(addr => ( +
+ {addr.label} + {" — "}{addr.street}، {addr.city} +
+ ))} +
+ )} + + {/* ── archived orders ── */} +

+ الطلبات المؤرشفة ({ordersTotal}) +

+ + {ordersLoading ? ( +
- جارٍ التحميل… + جارٍ تحميل الطلبات…
- )} + ) : ordersError ? ( +
{ordersError}
+ ) : orders.length === 0 ? ( +

+ لا توجد طلبات مؤرشفة لهذا العميل. +

+ ) : ( + <> +
    + {orders.map(o => ( +
  • + + {o.orderNumber ?? o.id} + + + + {new Date(o.createdAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })} + +
  • + ))} +
- {/* error */} - {!loading && error && ( -
- {error} -
- )} - - {/* content */} - {!loading && client && ( -
- {/* avatar + name + status row */} -
- -
-

{client.name}

-
- + {ordersPages > 1 && ( +
+ + صفحة {ordersPage} من {ordersPages} + +
+ +
-
- - {/* detail rows */} - - - - - - - {client.deletedAt && ( - )} - - {/* ── addresses ── */} -

- العناوين ({client.addresses?.length ?? 0}) -

- {!client.addresses || client.addresses.length === 0 ? ( -

- لا توجد عناوين مرتبطة بهذا العميل. -

- ) : ( -
- {client.addresses.map(addr => ( -
- {addr.label} - {" — "}{addr.street}، {addr.city} -
- ))} -
- )} - - {/* ── archived orders ── */} -

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

- - {ordersLoading ? ( -
- - جارٍ تحميل الطلبات… -
- ) : ordersError ? ( -
{ordersError}
- ) : orders.length === 0 ? ( -

- لا توجد طلبات مؤرشفة لهذا العميل. -

- ) : ( - <> -
    - {orders.map(o => ( -
  • - - {o.orderNumber ?? o.id} - - - - {new Date(o.createdAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })} - -
  • - ))} -
- - {ordersPages > 1 && ( -
- - صفحة {ordersPage} من {ordersPages} - -
- - -
-
- )} - - )} -
+ )}
- - {/* ── footer ── */} -
- -
-
-
+ )} + ); } \ No newline at end of file diff --git a/src/Components/Client/archive/ArchivedClientTable.tsx b/src/Components/Client/archive/ArchivedClientTable.tsx index a69ec07..8b7be5a 100644 --- a/src/Components/Client/archive/ArchivedClientTable.tsx +++ b/src/Components/Client/archive/ArchivedClientTable.tsx @@ -1,28 +1,8 @@ "use client"; -import { Spinner } from "../../UI"; +import { Badge, Button, EmptyState, IconBtn, Spinner } from "../../UI"; import type { ArchivedClient } from "@/src/types/client"; -// ── status badge ───────────────────────────────────────────────────────────── -function StatusBadge({ active }: { active: boolean }) { - return ( - - - {active ? "نشط" : "معطل"} - - ); -} - -// ── icon button ────────────────────────────────────────────────────────────── -function IconBtn({ onClick, title, color, bg, borderColor, children }: { onClick: () => void; title: string; color: string; bg: string; borderColor: string; children: React.ReactNode }) { - return ( - - ); -} - // ── card / header styles ───────────────────────────────────────────────────── const cardStyle: React.CSSProperties = { borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)", @@ -68,11 +48,10 @@ export function ArchivedClientTable({ clients, loading, search, page, pages, onV
) : clients.length === 0 ? ( /* empty state */ -
-

- {search ? `لا توجد نتائج لـ "${search}"` : "لا يوجد عملاء في الأرشيف."} -

-
+ ) : ( /* data rows */
    @@ -89,7 +68,7 @@ export function ArchivedClientTable({ clients, loading, search, page, pages, onV

    {c.phone}

{c.email} - + {new Date(c.createdAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })} @@ -121,10 +100,9 @@ export function ArchivedClientTable({ clients, loading, search, page, pages, onV { label: "السابق", action: () => onPageChange(Math.max(1, page - 1)), disabled: page === 1 }, { label: "التالي", action: () => onPageChange(Math.min(pages, page + 1)), disabled: page === pages }, ].map(btn => ( - + ))}
diff --git a/src/Components/Client/archive/ArchivedClientsModal.tsx b/src/Components/Client/archive/ArchivedClientsModal.tsx index 5cce8bd..f416585 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, Input, Button } from "../../UI"; +import { Alert, Input, Modal } from "../../UI"; import { ArchivedClientTable } from "./ArchivedClientTable"; import { ArchivedClientDetailModal } from "./ArchivedClientDetailModal"; import { useArchivedClients } from "@/src/hooks/archive/useArchivedClients"; @@ -20,92 +20,47 @@ export function ArchivedClientsModal({ onClose }: ArchivedClientsModalProps) { } = useArchivedClients(); 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", - }} + {viewClientId && ( setViewClientId(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)} - dir="rtl" - icon={ - - - - } - /> -
- - {error && } - - setViewClientId(client.id)} - onPageChange={setPage} +
+ {/* search */} +
+ handleSearch(e.target.value)} + dir="rtl" + icon={ + + + + } />
+ + {error && } + + setViewClientId(client.id)} + onPageChange={setPage} + />
-
+ ); } \ No newline at end of file diff --git a/src/Components/Client_Adress/archive/ArchivedClientAddressesDetailModal.tsx b/src/Components/Client_Adress/archive/ArchivedClientAddressesDetailModal.tsx index 3547037..a011645 100644 --- a/src/Components/Client_Adress/archive/ArchivedClientAddressesDetailModal.tsx +++ b/src/Components/Client_Adress/archive/ArchivedClientAddressesDetailModal.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect } from "react"; +import { Button, Modal } from "../../UI"; import type { ArchivedClientAddress } from "@/src/types/client_adresses"; interface ArchivedClientAddressesDetailModalProps { @@ -8,7 +8,7 @@ interface ArchivedClientAddressesDetailModalProps { onClose: () => void; } -// ── small helper components ─────────────────────────────────────────────────── +// ── small helper components (no template exists for a plain label/value row) ── function DetailRow({ label, value }: { label: string; value?: string | null }) { if (!value) return null; return ( @@ -42,131 +42,61 @@ function SectionHeader({ title }: { title: string }) { // ── main component ──────────────────────────────────────────────────────────── export function ArchivedClientAddressesDetailModal({ address, onClose }: ArchivedClientAddressesDetailModalProps) { - // close on Escape - useEffect(() => { - const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; - window.addEventListener("keydown", handler); - return () => window.removeEventListener("keydown", handler); - }, [onClose]); - const { details, contactPerson, location } = address; return ( -
{ if (e.target === e.currentTarget) onClose(); }} - style={{ - position: "fixed", inset: 0, zIndex: 80, - background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", - display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem", - }} + + إغلاق + + } > -
e.stopPropagation()} - style={{ - width: "100%", maxWidth: 480, - 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 ── */} -
-
-

- عنوان مؤرشف -

-

- {address.label} -

-
- -
+
+ + - {/* ── body ── */} -
- - + + + + + + + + + + - - - - - - - - - - + {(contactPerson?.name || contactPerson?.phone) && ( + <> + + + + + )} - {(contactPerson?.name || contactPerson?.phone) && ( - <> - - - - - )} + {location?.coordinates && ( + <> + + + + + )} - {location?.coordinates && ( - <> - - - - - )} - - - - - - {address.deletedAt && ( - - )} -
- - {/* ── footer ── */} -
- -
+ + + + + {address.deletedAt && ( + + )}
-
+
); } \ No newline at end of file diff --git a/src/Components/Client_Adress/archive/ArchivedClientAddressesTable.tsx b/src/Components/Client_Adress/archive/ArchivedClientAddressesTable.tsx index 0bed6a3..1595245 100644 --- a/src/Components/Client_Adress/archive/ArchivedClientAddressesTable.tsx +++ b/src/Components/Client_Adress/archive/ArchivedClientAddressesTable.tsx @@ -1,28 +1,8 @@ "use client"; -import { Spinner } from "../../UI"; +import { Badge, EmptyState, IconBtn, Spinner } from "../../UI"; import type { ArchivedClientAddress } from "@/src/types/client_adresses"; -// ── validation badge ───────────────────────────────────────────────────────── -function ValidatedBadge({ validated }: { validated: boolean }) { - return ( - - - {validated ? "موثّق" : "غير موثّق"} - - ); -} - -// ── icon button ────────────────────────────────────────────────────────────── -function IconBtn({ onClick, title, color, bg, borderColor, children }: { onClick: () => void; title: string; color: string; bg: string; borderColor: string; children: React.ReactNode }) { - return ( - - ); -} - // ── card / header styles ───────────────────────────────────────────────────── const cardStyle: React.CSSProperties = { borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)", @@ -65,11 +45,10 @@ export function ArchivedClientAddressesTable({ addresses, loading, search, onVie
) : addresses.length === 0 ? ( /* empty state */ -
-

- {search ? `لا توجد نتائج لـ "${search}"` : "لا توجد عناوين في الأرشيف."} -

-
+ ) : ( /* data rows */
    @@ -93,7 +72,10 @@ export function ArchivedClientAddressesTable({ addresses, loading, search, onVie {a.contactPerson?.name ?? "—"} - + {new Date(a.createdAt).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" })} diff --git a/src/Components/Client_Adress/archive/ArchivedClientsAddressesModal.tsx b/src/Components/Client_Adress/archive/ArchivedClientsAddressesModal.tsx index 62f603f..362b3be 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, Input, Button } from "../../UI"; +import { Alert, Input, Modal } from "../../UI"; import { ArchivedClientAddressesTable } from "./ArchivedClientAddressesTable"; import { ArchivedClientAddressesDetailModal } from "./ArchivedClientAddressesDetailModal"; import { useArchivedClientAddresses } from "@/src/hooks/archive/useArchiveClientAdresses"; @@ -24,89 +24,44 @@ export function ArchivedClientsAddressesModal({ onClose, clientId }: ArchivedCli } = useArchivedClientAddresses(clientId); 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", - }} + {viewAddress && ( setViewAddress(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 — client-side only: this endpoint has no server-side search/pagination */} -
    - handleSearch(e.target.value)} - dir="rtl" - icon={ - - - - } - /> -
    - - {error && } - - + {/* search — client-side only: this endpoint has no server-side search/pagination */} +
    + handleSearch(e.target.value)} + dir="rtl" + icon={ + + + + } />
    + + {error && } + +
    -
    +
    ); } \ No newline at end of file diff --git a/src/Components/Driver/DriverDetailPanel.tsx b/src/Components/Driver/DriverDetailPanel.tsx index a6df45f..2f0164e 100644 --- a/src/Components/Driver/DriverDetailPanel.tsx +++ b/src/Components/Driver/DriverDetailPanel.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useState } from "react"; -import { Spinner } from "../UI"; +import { Alert, Button, Spinner } from "../UI"; import { driverService } from "@/src/services/driver.service"; import { getStoredToken } from "@/src/lib/auth"; import { DriverReportPanel } from "../Driver_Report/driverReport"; @@ -306,18 +306,7 @@ export function DriverDetailPanel({
    )} - {error && ( -
    - ⚠ {error} -
    - )} + {error && } {driver && !loading && ( <> @@ -462,55 +451,21 @@ export function DriverDetailPanel({ flexShrink: 0, }} > - + - + - +
)} diff --git a/src/Components/Driver/DriverFormModal.tsx b/src/Components/Driver/DriverFormModal.tsx index 1946171..fde09e8 100644 --- a/src/Components/Driver/DriverFormModal.tsx +++ b/src/Components/Driver/DriverFormModal.tsx @@ -86,36 +86,26 @@ export function DriverFormModal({ }: DriverFormModalProps) { const isNew = editDriver === null; - // ── Local branches (auto-fetched if prop is empty) ───────────────────────── - const [branches, setBranches] = useState(branchesProp); - useEffect(() => { - if (branchesProp.length > 0) { - queueMicrotask(() => setBranches(branchesProp)); - return; - } - const token = getStoredToken(); - get<{ data: { data: Branch[] } }>("branches?limit=100", token) - .then((res) => { - const list = (res as unknown as { data: { data: Branch[] } }).data?.data ?? []; - setBranches(list); - }) - .catch(() => { - /* silently ignore */ - }); - }, [branchesProp]); - // ── react-hook-form ──────────────────────────────────────────────────────── + // Moved above the branches-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), + // same issue already fixed in TripFormModal. const { register, handleSubmit, control, setError, setFocus, + setValue, formState: { errors, isSubmitting }, } = useForm({ - // 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), + // Cast both the schema argument and the resolver's result — same fix + // already applied in TripFormModal: createDriverSchema/updateDriverSchema + // have structurally different required fields (union type yupResolver's + // single-schema signature won't accept), and yup's inferred type for + // .optional() fields is structurally incompatible with DriverFormValues. + resolver: yupResolver((isNew ? createDriverSchema : updateDriverSchema) as any) as any, defaultValues: { name: editDriver?.name ?? "", phone: editDriver?.phone ?? "", @@ -142,6 +132,33 @@ export function DriverFormModal({ }, }); + // ── Local branches (auto-fetched if prop is empty) ───────────────────────── + const [branches, setBranches] = useState(branchesProp); + useEffect(() => { + // defaultValues.branchId was applied before this list existed, so the + // setStatusDraft(e.target.value as OrderStatus)} dir="rtl" - style={{ - 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)", - fontFamily: "var(--font-sans)", cursor: "pointer", - }} > {Object.entries(ORDER_STATUS_MAP).map(([k, v]) => ( ))} - - + setStatusReason(e.target.value)} placeholder="سبب التغيير (اختياري)" dir="rtl" - style={{ - 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)", - fontFamily: "var(--font-sans)", - }} /> - +
{order.statusHistory && order.statusHistory.length > 0 && ( @@ -492,55 +455,21 @@ export function OrderDetailPanel({ flexShrink: 0, }} > - + - + - +
)} diff --git a/src/Components/Order/OrderFormModal.tsx b/src/Components/Order/OrderFormModal.tsx index 1dc1f51..b4cdd0d 100644 --- a/src/Components/Order/OrderFormModal.tsx +++ b/src/Components/Order/OrderFormModal.tsx @@ -200,14 +200,6 @@ export function OrderFormModal({ }; }, []); - useEffect(() => { - const h = (e: KeyboardEvent) => { - if (e.key === "Escape") onClose(); - }; - window.addEventListener("keydown", h); - return () => window.removeEventListener("keydown", h); - }, [onClose]); - const numberField = ( field: | "quantity" diff --git a/src/Components/Order/archive/ArchivedOrderDetailModal.tsx b/src/Components/Order/archive/ArchivedOrderDetailModal.tsx index ea9d69f..f965be2 100644 --- a/src/Components/Order/archive/ArchivedOrderDetailModal.tsx +++ b/src/Components/Order/archive/ArchivedOrderDetailModal.tsx @@ -1,5 +1,6 @@ "use client"; +import { Button, Modal } from "../../UI"; import { ORDER_STATUS_MAP } from "../OrderDetailBanel"; import type { ArchivedOrder } from "@/src/types/order"; @@ -34,60 +35,46 @@ export function ArchivedOrderDetailModal({ order, onClose }: ArchivedOrderDetail const s = ORDER_STATUS_MAP[order.currentStatus] ?? ORDER_STATUS_MAP.Created; return ( -
{ if (e.target === e.currentTarget) onClose(); }} - style={{ position: "fixed", inset: 0, zIndex: 80, background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem" }}> -
e.stopPropagation()} - style={{ width: "100%", maxWidth: 480, 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" }}> -
-
-

طلب مؤرشف

-

- {order.shipmentNumber} -

+ + إغلاق + + } + > +
+ + + {s.label} + + + + + + + + + + {order.statusHistory && order.statusHistory.length > 0 && ( +
+

سجل الحالات

+ {order.statusHistory.map(h => { + const hs = ORDER_STATUS_MAP[h.status] ?? ORDER_STATUS_MAP.Created; + return ( +
+ {hs.label}{h.reason ? ` — ${h.reason}` : ""} + {fmtDate(h.createdAt)} +
+ ); + })}
- -
- -
- - - {s.label} - - - - - - - - - - {order.statusHistory && order.statusHistory.length > 0 && ( -
-

سجل الحالات

- {order.statusHistory.map(h => { - const hs = ORDER_STATUS_MAP[h.status] ?? ORDER_STATUS_MAP.Created; - return ( -
- {hs.label}{h.reason ? ` — ${h.reason}` : ""} - {fmtDate(h.createdAt)} -
- ); - })} -
- )} -
- -
- -
+ )}
-
+ ); } \ No newline at end of file diff --git a/src/Components/Order/archive/ArchivedOrderTable.tsx b/src/Components/Order/archive/ArchivedOrderTable.tsx index db58a6c..88c8780 100644 --- a/src/Components/Order/archive/ArchivedOrderTable.tsx +++ b/src/Components/Order/archive/ArchivedOrderTable.tsx @@ -1,6 +1,6 @@ "use client"; -import { Spinner } from "../../UI"; +import { Button, EmptyState, IconBtn, Spinner } from "../../UI"; import { ORDER_STATUS_MAP } from "../OrderDetailBanel"; import type { ArchivedOrder } from "@/src/types/order"; @@ -51,11 +51,10 @@ export function ArchivedOrderTable({ orders, loading, search, page, pages, onVie جارٍ التحميل…
) : orders.length === 0 ? ( -
-

- {search ? `لا توجد نتائج لـ "${search}"` : "لا توجد طلبات في الأرشيف."} -

-
+ ) : (
    {orders.map((o, i) => { @@ -88,14 +87,12 @@ export function ArchivedOrderTable({ orders, loading, search, page, pages, onVie {s.label}
    - +
    ); @@ -109,15 +106,24 @@ export function ArchivedOrderTable({ orders, loading, search, page, pages, onVie صفحة {page} من {pages}
    - {[ - { label: "السابق", action: () => onPageChange(Math.max(1, page - 1)), disabled: page === 1 }, - { label: "التالي", action: () => onPageChange(Math.min(pages, page + 1)), disabled: page === pages }, - ].map(btn => ( - - ))} + +
)} diff --git a/src/Components/Trip/TripDetailModal.tsx b/src/Components/Trip/TripDetailModal.tsx new file mode 100644 index 0000000..0db5fab --- /dev/null +++ b/src/Components/Trip/TripDetailModal.tsx @@ -0,0 +1,155 @@ +"use client"; + +// src/Components/Trip/TripDetailModal.tsx +// New — split out of the old single-file Trip module, mirroring +// UserDetailModal.tsx / ArchivedDriverDetailModal.tsx for consistency. + +import { useEffect, useState } from "react"; +import { Alert, Button, Modal, Spinner } from "../UI"; +import { getStoredToken } from "@/src/lib/auth"; +import { tripService } from "@/src/services/trip.service"; +import { TRIP_STATUS_MAP } from "@/src/types/trip"; +import type { Trip } from "@/src/types/trip"; + +interface TripDetailModalProps { + tripId: string; + onClose: () => void; +} + +// ── small helper components ─────────────────────────────────────────────────── +// (No shared-UI equivalents — purpose-built layouts, not generic controls.) +function DetailRow({ label, value }: { label: string; value?: string | null }) { + return ( +
+ + {label} + + + {value || "—"} + +
+ ); +} + +function StatusBadge({ status }: { status: Trip["status"] }) { + const s = TRIP_STATUS_MAP[status]; + return ( + + + {s.label} + + ); +} + +// ── main component ──────────────────────────────────────────────────────────── +export function TripDetailModal({ tripId, onClose }: TripDetailModalProps) { + const [trip, setTrip] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // fetch trip details on mount + useEffect(() => { + let cancelled = false; + (async () => { + try { + const token = getStoredToken(); + const data = await tripService.getById(tripId, token); + if (!cancelled) setTrip(data); + } catch { + if (!cancelled) setError("تعذّر تحميل بيانات الرحلة. يرجى المحاولة لاحقاً."); + } finally { + if (!cancelled) setLoading(false); + } + })(); + return () => { cancelled = true; }; + }, [tripId]); + + // ── helpers ─────────────────────────────────────────────────────────────── + const fmt = (iso?: string | null) => + iso ? new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "long", day: "numeric", hour: "2-digit", minute: "2-digit" }) : null; + + const cash = (v?: number | string | null) => + v != null ? `${Number(v).toLocaleString("ar-SA")} ر.س` : null; + + // ── render ──────────────────────────────────────────────────────────────── + return ( + + إغلاق + + } + > + {/* loading */} + {loading && ( +
+ + جارٍ التحميل… +
+ )} + + {/* error */} + {!loading && error && } + + {/* content */} + {!loading && trip && ( +
+ + {/* title + trip number + status row */} +
+

{trip.title}

+

+ {trip.tripNumber} +

+ +
+ + {/* driver / car / branch */} + + + + + {/* timing */} + + + + {/* counts + cash */} + + + + + + {/* notes */} + + + + {/* timestamps */} + + +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/Components/Trip/TripTable.tsx b/src/Components/Trip/TripTable.tsx new file mode 100644 index 0000000..2c4ea9b --- /dev/null +++ b/src/Components/Trip/TripTable.tsx @@ -0,0 +1,172 @@ +"use client"; + +// src/Components/Trip/TripTable.tsx +// New — split out of the old single-file Trip module, mirroring +// UserTable.tsx for consistency. + +import { Button, EmptyState, IconBtn, Spinner } from "../UI"; +import { TRIP_STATUS_MAP } from "@/src/types/trip"; +import type { Trip } from "@/src/types/trip"; + +// ── status badge ───────────────────────────────────────────────────────────── +// Kept custom rather than swapped for : colors come dynamically from +// TRIP_STATUS_MAP (per-status color+dot), which Badge's fixed palette +// doesn't cover — mirrors DriverTable's StatusBadge. +function StatusBadge({ status }: { status: Trip["status"] }) { + const s = TRIP_STATUS_MAP[status]; + return ( + + + {s.label} + + ); +} + +// ── card / header styles ───────────────────────────────────────────────────── +const cardStyle: React.CSSProperties = { + borderRadius: "var(--radius-xl)", border: "1px solid var(--color-border)", + background: "var(--color-surface)", overflow: "hidden", boxShadow: "var(--shadow-card)", +}; +const thStyle: React.CSSProperties = { + padding: "0.75rem 1.5rem", fontSize: 11, fontWeight: 700, + textTransform: "uppercase", letterSpacing: "0.2em", + color: "var(--color-text-muted)", background: "var(--color-surface-muted)", + borderBottom: "1px solid var(--color-border)", +}; + +const ROW_GRID_COLUMNS = "2fr 1.3fr 1.3fr 1fr 1fr 1fr 100px"; + +function fmtDate(iso?: string | null): string { + if (!iso) return "—"; + return new Date(iso).toLocaleDateString("ar-SA", { year: "numeric", month: "short", day: "numeric" }); +} + +// ── Props ──────────────────────────────────────────────────────────────────── +interface TripTableProps { + trips: Trip[]; + loading: boolean; + search: string; + page: number; + pages: number; + onEdit: (trip: Trip) => void; + onDelete: (trip: Trip) => void; + onView: (trip: Trip) => void; + onAddFirst: () => void; + onPageChange: (p: number) => void; +} + +// ── main table ─────────────────────────────────────────────────────────────── +export function TripTable({ trips, loading, search, page, pages, onEdit, onDelete, onView, onAddFirst, onPageChange }: TripTableProps) { + return ( +
+ {/* column headers */} +
+ الرحلة + السائق + السيارة + الفرع + الحالة + وقت البدء + إجراءات +
+ + {/* loading state */} + {loading ? ( +
+ + جارٍ التحميل… +
+ ) : trips.length === 0 ? ( + /* empty state */ + + أضف أول رحلة + + ) + } + /> + ) : ( + /* data rows */ +
    + {trips.map((t, i) => ( +
  • +
    +

    {t.title}

    +

    {t.tripNumber}

    +
    + {t.driver?.name ?? "—"} + {t.car ? `${t.car.manufacturer} ${t.car.model}` : "—"} + {t.branch?.name ?? "—"} + + + {fmtDate(t.startTime)} + +
    + {/* view */} + onView(t)}> + + + + + + {/* edit */} + onEdit(t)}> + + + + + + {/* delete */} + onDelete(t)}> + + + + + +
    +
  • + ))} +
+ )} + + {/* pagination */} + {pages > 1 && ( +
+ + صفحة {page} من {pages} + +
+ + +
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/Components/Trip/Tripformmodal.tsx b/src/Components/Trip/Tripformmodal.tsx index 5279465..df5ede1 100644 --- a/src/Components/Trip/Tripformmodal.tsx +++ b/src/Components/Trip/Tripformmodal.tsx @@ -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 + // had no matching