-
+
+
إدارة الأسطول
-
+
-
+
المركبات
-
- إجمالي {total} مركبة في الأسطول
+
+ إجمالي{" "}
+
+ {total}
+ {" "}
+ مركبة في الأسطول
-
+
{/* Search */}
{ setSearch(e.target.value); setPage(1); }}
+ onChange={(e) => {
+ setSearch(e.target.value);
+ setPage(1);
+ }}
dir="rtl"
style={{
- width: "100%", height: 40, paddingRight: 36, paddingLeft: 12,
+ width: "100%",
+ height: 40,
+ paddingRight: 36,
+ paddingLeft: 12,
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
- fontSize: 13, color: "var(--color-text-primary)",
- outline: "none", fontFamily: "var(--font-sans)",
+ fontSize: 13,
+ color: "var(--color-text-primary)",
+ outline: "none",
+ fontFamily: "var(--font-sans)",
}}
/>
@@ -344,19 +607,28 @@ export default function CarsPage() {
type="button"
onClick={() => setFormTarget(null)}
style={{
- height: 40, padding: "0 1.125rem",
+ height: 40,
+ padding: "0 1.125rem",
borderRadius: "var(--radius-lg)",
border: "none",
background: "var(--color-brand-600)",
- fontSize: 13, fontWeight: 700, color: "#FFF",
+ fontSize: 13,
+ fontWeight: 700,
+ color: "#FFF",
cursor: "pointer",
- display: "inline-flex", alignItems: "center", gap: 7,
+ display: "inline-flex",
+ alignItems: "center",
+ gap: 7,
fontFamily: "var(--font-sans)",
boxShadow: "0 1px 4px rgba(37,99,235,.35)",
whiteSpace: "nowrap",
}}
>
-
+
إضافة مركبة
@@ -364,27 +636,54 @@ export default function CarsPage() {
{/* Error alert */}
- {error &&
setError(null)} />}
+ {error && (
+ setError(null)} />
+ )}
{/* ── Loading ── */}
{loading ? (
-
+
جارٍ تحميل المركبات…
) : cars.length === 0 ? (
-
+
🚗
-
- {search ? `لا توجد مركبات تطابق "${search}"` : "لا توجد مركبات بعد"}
+
+ {search
+ ? `لا توجد مركبات تطابق "${search}"`
+ : "لا توجد مركبات بعد"}
-
+
اضغط على "إضافة مركبة" لإضافة أول مركبة في الأسطول.
{!search && (
@@ -392,11 +691,17 @@ export default function CarsPage() {
type="button"
onClick={() => setFormTarget(null)}
style={{
- marginTop: 16, height: 40, padding: "0 1.5rem",
+ marginTop: 16,
+ height: 40,
+ padding: "0 1.5rem",
borderRadius: "var(--radius-lg)",
- border: "none", background: "var(--color-brand-600)",
- fontSize: 13, fontWeight: 700, color: "#FFF",
- cursor: "pointer", fontFamily: "var(--font-sans)",
+ border: "none",
+ background: "var(--color-brand-600)",
+ fontSize: 13,
+ fontWeight: 700,
+ color: "#FFF",
+ cursor: "pointer",
+ fontFamily: "var(--font-sans)",
}}
>
إضافة مركبة
@@ -405,11 +710,13 @@ export default function CarsPage() {
) : (
/* ── Card grid ── */
-
+
{/* PERF NOTE: Cards are rendered inline via .map() below. At the
current pagination size (10-12 items) this is not a bottleneck.
If page size increases (e.g. "show all" mode, a larger
@@ -431,24 +738,44 @@ export default function CarsPage() {
{/* ── Pagination ── */}
{pages > 1 && (
-
+
- صفحة {page} من{" "}
- {pages}
+ صفحة{" "}
+
+ {page}
+ {" "}
+ من{" "}
+
+ {pages}
+
{" · "}
- {total} مركبة
+
+ {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 },
+ {
+ 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) => (
setArchiveOpen(true)} />
>
);
-}
\ No newline at end of file
+}
diff --git a/app/dashboard/drivers/[driverId]/page.tsx b/app/dashboard/drivers/[driverId]/page.tsx
index ba3ff46..340b746 100644
--- a/app/dashboard/drivers/[driverId]/page.tsx
+++ b/app/dashboard/drivers/[driverId]/page.tsx
@@ -3,40 +3,69 @@
import { useCallback, useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { ConfirmDialog, Spinner, Button, Toast } from "@/src/Components/UI";
-import { DriverFormModal , PhotoCard } from "@/src/Components/Driver";
+import { DriverFormModal, PhotoCard } from "@/src/Components/Driver";
import { DriverReportPanel } from "@/src/Components/Driver_Report/driverReport";
-import type { Driver, CreateDriverPayload, UpdateDriverPayload } from "@/src/types/driver";
-import { DRIVER_STATUS_MAP, DRIVER_CARD_TYPE_MAP, NATIONAL_ID_TYPE_MAP } from "@/src/types/driver";
+import type {
+ Driver,
+ CreateDriverPayload,
+ UpdateDriverPayload,
+} from "@/src/types/driver";
+import {
+ DRIVER_STATUS_MAP,
+ DRIVER_CARD_TYPE_MAP,
+ NATIONAL_ID_TYPE_MAP,
+} from "@/src/types/driver";
import { driverService } from "@/src/services";
import { getStoredToken } from "@/src/lib/auth";
function fmtDate(iso?: string | null): string {
if (!iso) return "—";
return new Date(iso).toLocaleDateString("ar-SA", {
- year: "numeric", month: "long", day: "numeric",
+ year: "numeric",
+ month: "long",
+ day: "numeric",
});
}
function isExpiringSoon(iso?: string | null): boolean {
if (!iso) return false;
- return (new Date(iso).getTime() - Date.now()) <= 90 * 86_400_000;
+ return new Date(iso).getTime() - Date.now() <= 90 * 86_400_000;
}
-function SectionCard({ title, children }: { title: string; children: React.ReactNode }) {
+function SectionCard({
+ title,
+ children,
+}: {
+ title: string;
+ children: React.ReactNode;
+}) {
return (
-
-
-
+
+
@@ -45,50 +74,82 @@ function SectionCard({ title, children }: { title: string; children: React.React
);
}
-function DetailRow({ label, value, mono = false, warn = false }: {
- label: string; value: string; mono?: boolean; warn?: boolean;
+function DetailRow({
+ label,
+ value,
+ mono = false,
+ warn = false,
+}: {
+ label: string;
+ value: string;
+ mono?: boolean;
+ warn?: boolean;
}) {
return (
-
-
{label}
-
- {warn && value !== "—" ? "⚠ " : ""}{value}
+
+
+ {label}
+
+
+ {warn && value !== "—" ? "⚠ " : ""}
+ {value}
);
}
export default function DriverDetailPage() {
- const params = useParams();
- const router = useRouter();
+ const params = useParams();
+ const router = useRouter();
const driverId = params?.driverId as string;
- const [driver, setDriver] = useState(null);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
+ const [driver, setDriver] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
const [avatarError, setAvatarError] = useState(false);
useEffect(() => {
queueMicrotask(() => setAvatarError(false));
}, [driver?.photoUrl]);
- const [toast, setToast] = useState<{ type: "success" | "error"; message: string } | null>(null);
+ const [toast, setToast] = useState<{
+ type: "success" | "error";
+ message: string;
+ } | null>(null);
- const [editOpen, setEditOpen] = useState(false);
+ const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
- const [deleting, setDeleting] = useState(false);
+ const [deleting, setDeleting] = useState(false);
- const showToast = useCallback((type: "success" | "error", message: string) => {
- setToast({ type, message });
- setTimeout(() => setToast(null), 4000);
- }, []);
+ const showToast = useCallback(
+ (type: "success" | "error", message: string) => {
+ setToast({ type, message });
+ setTimeout(() => setToast(null), 4000);
+ },
+ [],
+ );
const loadDriver = useCallback(async () => {
if (!driverId) return;
@@ -99,23 +160,34 @@ export default function DriverDetailPage() {
const data = await driverService.getById(driverId, token);
setDriver(data);
} catch (err: unknown) {
- setError(err instanceof Error ? err.message : "تعذّر تحميل بيانات السائق.");
+ setError(
+ err instanceof Error ? err.message : "تعذّر تحميل بيانات السائق.",
+ );
} finally {
setLoading(false);
}
}, [driverId]);
- useEffect(() => { queueMicrotask(loadDriver); }, [loadDriver]);
+ useEffect(() => {
+ queueMicrotask(loadDriver);
+ }, [loadDriver]);
const handleEditSubmit = useCallback(
- async (payload: CreateDriverPayload | UpdateDriverPayload): Promise => {
+ async (
+ payload: CreateDriverPayload | UpdateDriverPayload,
+ ): Promise => {
if (!driver) return false;
try {
const token = getStoredToken();
const typedPayload = payload as UpdateDriverPayload & {
- photo?: File; nationalPhoto?: File; driverCardPhoto?: File;
+ photo?: File;
+ nationalPhoto?: File;
+ driverCardPhoto?: File;
};
- const hasFiles = typedPayload.photo || typedPayload.nationalPhoto || typedPayload.driverCardPhoto;
+ const hasFiles =
+ typedPayload.photo ||
+ typedPayload.nationalPhoto ||
+ typedPayload.driverCardPhoto;
if (hasFiles) {
await driverService.updateWithImages(driver.id, typedPayload, token);
@@ -127,7 +199,8 @@ export default function DriverDetailPage() {
showToast("success", "تم تحديث بيانات السائق بنجاح.");
return true;
} catch (err) {
- const message = err instanceof Error ? err.message : "تعذّر تحديث بيانات السائق.";
+ const message =
+ err instanceof Error ? err.message : "تعذّر تحديث بيانات السائق.";
showToast("error", message);
return false;
}
@@ -153,7 +226,16 @@ export default function DriverDetailPage() {
if (loading) {
return (
-
+
جارٍ التحميل…
@@ -162,9 +244,26 @@ export default function DriverDetailPage() {
if (error || !driver) {
return (
-
-
⚠ {error ?? "السائق غير موجود"}
-
router.back()} className="mt-4">
+
+
+ ⚠ {error ?? "السائق غير موجود"}
+
+
router.back()}
+ className="mt-4"
+ >
← رجوع
@@ -173,50 +272,159 @@ export default function DriverDetailPage() {
return (
<>
-
-
+
setToast(null)} />
-
- router.back()} className="mb-4 !px-0">
-
+
+ router.back()}
+ className="mb-4 !px-0"
+ >
+
العودة إلى قائمة السائقين
-
+
-
+
{driver.photoUrl && !avatarError ? (
// eslint-disable-next-line @next/next/no-img-element
-
setAvatarError(true)} />
+
setAvatarError(true)}
+ />
) : (
-
{driver.name.charAt(0)}
+
+ {driver.name.charAt(0)}
+
)}
-
ملف السائق
-
{driver.name}
-
+
+ ملف السائق
+
+
+ {driver.name}
+
+
{driver.userName && (
- @{driver.userName}
+
+ @{driver.userName}
+
)}
- {driver.phone}
+
+ {driver.phone}
+
{statusConfig && (
-
-
+
+
{statusConfig.label}
)}
@@ -229,65 +437,153 @@ export default function DriverDetailPage() {
حذف السائق
setEditOpen(true)}>
-
+
تعديل السائق
-
+
-
-
+
+
-
-
-
-
+
+
+
+
-
-
-
+
+
+
-
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
{driver.statusHistory && driver.statusHistory.length > 0 && (
{driver.statusHistory.map((h) => {
- const s = DRIVER_STATUS_MAP[h.status] ?? DRIVER_STATUS_MAP.Active;
+ const s =
+ DRIVER_STATUS_MAP[h.status] ?? DRIVER_STATUS_MAP.Active;
return (
-
+
- {s.label}
- {h.reason && — {h.reason} }
+
+ {s.label}
+
+ {h.reason && (
+
+ — {h.reason}
+
+ )}
-
{fmtDate(h.createdAt)}
+
+ {fmtDate(h.createdAt)}
+
);
})}
@@ -297,23 +593,42 @@ export default function DriverDetailPage() {
-
-
-
-
+
-
+
{editOpen && (
setEditOpen(false)}
@@ -331,4 +646,4 @@ export default function DriverDetailPage() {
/>
>
);
-}
\ No newline at end of file
+}
diff --git a/app/dashboard/drivers/page.tsx b/app/dashboard/drivers/page.tsx
index 401150e..33defb7 100644
--- a/app/dashboard/drivers/page.tsx
+++ b/app/dashboard/drivers/page.tsx
@@ -5,28 +5,42 @@ import { Alert, ArchiveButton, ConfirmDialog } from "@/src/Components/UI";
import { ArchivedDrivers } from "@/src/Components/Driver/archive/ArchivedDrivers";
import { DriverTable } from "@/src/Components/Driver/DriverTable";
import { useDrivers } from "@/src/hooks/useDriver";
-import { CreateDriverPayload, Driver, UpdateDriverPayload } from "@/src/types/driver";
-import { DriverDetailPanel, DriverFormModal , } from "@/src/Components/Driver";
+import {
+ CreateDriverPayload,
+ Driver,
+ UpdateDriverPayload,
+} from "@/src/types/driver";
+import { DriverDetailPanel, DriverFormModal } from "@/src/Components/Driver";
// ── Page Component ────────────────────────────────────────────────────────────
export default function DriversPage() {
const {
- drivers, loading, error, total, pages, page,
- search, setPage, handleSearch, clearError,
- createDriver, updateDriver, deleteDriver,
+ drivers,
+ loading,
+ error,
+ total,
+ pages,
+ page,
+ search,
+ setPage,
+ handleSearch,
+ clearError,
+ createDriver,
+ updateDriver,
+ deleteDriver,
notification,
} = useDrivers();
// ── Panel / modal state ───────────────────────────────────────────────────
const [selectedDriverId, setSelectedDriverId] = useState(null);
- const [formDriver, setFormDriver] = useState(null);
- const [deleteTarget, setDeleteTarget] = useState(null);
- const [deleting, setDeleting] = useState(false);
+ const [formDriver, setFormDriver] = useState(null);
+ const [deleteTarget, setDeleteTarget] = useState(null);
+ const [deleting, setDeleting] = useState(false);
// Bumped after a successful edit to force the detail panel to re-fetch
- const [panelRefreshKey, setPanelRefreshKey] = useState(0);
+ const [panelRefreshKey, setPanelRefreshKey] = useState(0);
// Archive browser modal open/closed
- const [archiveOpen, setArchiveOpen] = useState(false);
+ const [archiveOpen, setArchiveOpen] = useState(false);
// ── Handlers ──────────────────────────────────────────────────────────────
const handleEdit = useCallback((driver: Driver) => {
@@ -84,39 +98,95 @@ export default function DriversPage() {
// ── Render ────────────────────────────────────────────────────────────────
return (
<>
-
-
+
{/* ── Header ── */}
-
-
+
+
إدارة الكوادر
-
-
+
+
-
+
السائقون
-
+
إجمالي{" "}
- {total} {" "}
+
+ {total}
+ {" "}
سائق مسجل
-
+
{/* Search */}
handleSearch(e.target.value)}
dir="rtl"
style={{
- width: "100%", height: 40, paddingRight: 36, paddingLeft: 12,
+ width: "100%",
+ height: 40,
+ paddingRight: 36,
+ paddingLeft: 12,
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
- fontSize: 13, color: "var(--color-text-primary)",
- outline: "none", fontFamily: "var(--font-sans)",
+ fontSize: 13,
+ color: "var(--color-text-primary)",
+ outline: "none",
+ fontFamily: "var(--font-sans)",
}}
/>
@@ -140,18 +215,27 @@ export default function DriversPage() {
type="button"
onClick={() => setFormDriver("new")}
style={{
- height: 40, padding: "0 1.25rem",
+ height: 40,
+ padding: "0 1.25rem",
borderRadius: "var(--radius-lg)",
border: "none",
background: "var(--color-brand-600)",
- fontSize: 13, fontWeight: 700, color: "#FFF",
+ fontSize: 13,
+ fontWeight: 700,
+ color: "#FFF",
cursor: "pointer",
- display: "flex", alignItems: "center", gap: 8,
+ display: "flex",
+ alignItems: "center",
+ gap: 8,
fontFamily: "var(--font-sans)",
flexShrink: 0,
}}
>
-
+
إضافة سائق
@@ -163,9 +247,7 @@ export default function DriversPage() {
{notification && (
)}
- {error && (
-
- )}
+ {error &&
}
{/* ── Table ── */}
setFormDriver(null)}
@@ -213,12 +300,10 @@ export default function DriversPage() {
/>
{/* ── Archive browser modal ── */}
- {archiveOpen && (
- setArchiveOpen(false)} />
- )}
+ {archiveOpen && setArchiveOpen(false)} />}
{/* ── Floating button to open the archive browser ── */}
setArchiveOpen(true)} />
>
);
-}
\ No newline at end of file
+}
diff --git a/app/dashboard/trips/[tripId]/page.tsx b/app/dashboard/trips/[tripId]/page.tsx
index cdd4731..ac6afa2 100644
--- a/app/dashboard/trips/[tripId]/page.tsx
+++ b/app/dashboard/trips/[tripId]/page.tsx
@@ -1,7 +1,5 @@
"use client";
-
-
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Button, ConfirmDialog, Spinner } from "@/src/Components/UI";
@@ -9,7 +7,11 @@ 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";
-import type { Trip, CreateTripPayload, UpdateTripPayload } from "@/src/types/trip";
+import type {
+ Trip,
+ CreateTripPayload,
+ UpdateTripPayload,
+} from "@/src/types/trip";
import { TRIP_STATUS_MAP } from "@/src/types/trip";
import { Toast, type ToastNotification } from "@/src/Components/UI/Toast";
@@ -22,14 +24,20 @@ function extractApiMessage(err: unknown, fallback: string): string {
if (err && typeof err === "object") {
const e = err as Record;
- const responseData = (e["response"] as Record | undefined)?.["data"];
+ const responseData = (
+ e["response"] as Record | undefined
+ )?.["data"];
if (responseData && typeof responseData === "object") {
const rd = responseData as Record;
- if (typeof rd["message"] === "string" && rd["message"].trim()) return rd["message"];
- if (Array.isArray(rd["message"])) return (rd["message"] as string[]).join(" — ");
- if (typeof rd["error"] === "string" && rd["error"].trim()) return rd["error"];
+ if (typeof rd["message"] === "string" && rd["message"].trim())
+ return rd["message"];
+ if (Array.isArray(rd["message"]))
+ return (rd["message"] as string[]).join(" — ");
+ if (typeof rd["error"] === "string" && rd["error"].trim())
+ return rd["error"];
}
- if (typeof e["message"] === "string" && e["message"].trim()) return e["message"];
+ if (typeof e["message"] === "string" && e["message"].trim())
+ return e["message"];
}
return fallback;
@@ -119,7 +127,13 @@ function DetailRow({
borderBottom: "1px solid var(--color-border)",
}}
>
-
+
{label}
-
+
{label}
-
+
{value}
@@ -179,17 +207,19 @@ export default function TripDetailPage() {
const router = useRouter();
const tripId = params?.tripId as string;
- const [trip, setTrip] = useState
(null);
+ const [trip, setTrip] = useState(null);
const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
// ── Modal state ───────────────────────────────────────────────────────────
- const [editOpen, setEditOpen] = useState(false);
+ const [editOpen, setEditOpen] = useState(false);
const [deleteOpen, setDeleteOpen] = useState(false);
- const [deleting, setDeleting] = useState(false);
+ const [deleting, setDeleting] = useState(false);
// ── Notifications (standalone — no longer borrowed from the list hook) ───
- const [notification, setNotification] = useState(null);
+ const [notification, setNotification] = useState(
+ null,
+ );
const timerRef = useRef | null>(null);
const notify = useCallback((n: ToastNotification) => {
@@ -203,7 +233,12 @@ export default function TripDetailPage() {
setNotification(null);
}, []);
- useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
+ useEffect(
+ () => () => {
+ if (timerRef.current) clearTimeout(timerRef.current);
+ },
+ [],
+ );
// ── Load trip ─────────────────────────────────────────────────────────────
const loadTrip = useCallback(async () => {
@@ -232,12 +267,19 @@ export default function TripDetailPage() {
if (!trip) return false;
try {
const token = getStoredToken();
- const updated = await tripService.update(trip.id, payload as UpdateTripPayload, token);
+ 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, "تعذّر تحديث الرحلة.") });
+ notify({
+ type: "error",
+ message: extractApiMessage(err, "تعذّر تحديث الرحلة."),
+ });
return false;
}
},
@@ -253,7 +295,10 @@ export default function TripDetailPage() {
await tripService.delete(trip.id, token);
router.push("/dashboard/trips");
} catch (err) {
- notify({ type: "error", message: extractApiMessage(err, "تعذّر حذف الرحلة.") });
+ notify({
+ type: "error",
+ message: extractApiMessage(err, "تعذّر حذف الرحلة."),
+ });
setDeleting(false);
}
}, [trip, router, notify]);
@@ -297,7 +342,12 @@ export default function TripDetailPage() {
⚠ {error ?? "الرحلة غير موجودة"}
- router.back()} style={{ marginTop: "1rem" }}>
+ router.back()}
+ style={{ marginTop: "1rem" }}
+ >
← رجوع
@@ -310,8 +360,9 @@ export default function TripDetailPage() {
{/* ── Toast notification ── */}
-
-
+
{/* ── Page header ── */}
router.back()}
- style={{ height: "auto", padding: 0, background: "none", marginBottom: "1rem" }}
+ style={{
+ height: "auto",
+ padding: 0,
+ background: "none",
+ marginBottom: "1rem",
+ }}
>
-
+
العودة إلى قائمة الرحلات
-
+
{/* Icon + title */}
-
+
{trip.title.charAt(0)}
-
+
ملف الرحلة
-
+
{trip.title}
-
-
+
+
#{trip.tripNumber}
{statusConfig && (
@@ -381,7 +485,15 @@ export default function TripDetailPage() {
gap: 5,
}}
>
-
+
{statusConfig.label}
)}
@@ -428,7 +540,11 @@ export default function TripDetailPage() {
fontFamily: "var(--font-sans)",
}}
>
-
+
تعديل الرحلة
@@ -436,13 +552,35 @@ export default function TripDetailPage() {
{/* ── Stats row ── */}
-
-
-
-
+
+
+
+
@@ -457,45 +595,77 @@ export default function TripDetailPage() {
>
{/* Trip Info */}
-
+
-
- {trip.notes && }
- {trip.endReason && }
+
+ {trip.notes && }
+ {trip.endReason && (
+
+ )}
{/* Driver */}
{trip.driver && (
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
)}
{/* Car */}
{trip.car && (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
)}
{/* System Info */}
-
+
-
+
@@ -511,12 +681,14 @@ export default function TripDetailPage() {
>
-
{/* ── Edit modal ── */}
{editOpen && (
setEditOpen(false)}
onSubmit={handleEditSubmit}
@@ -534,4 +706,4 @@ export default function TripDetailPage() {
/>
>
);
-}
\ No newline at end of file
+}
diff --git a/app/dashboard/trips/page.tsx b/app/dashboard/trips/page.tsx
index 2d6b1f5..8d8f5b9 100644
--- a/app/dashboard/trips/page.tsx
+++ b/app/dashboard/trips/page.tsx
@@ -15,8 +15,17 @@ import { useRouter } from "next/navigation";
import { useTrips } from "@/src/hooks/useTrip";
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 {
+ Alert,
+ ArchiveButton,
+ ConfirmDialog,
+ Toast,
+} from "@/src/Components/UI";
+import type {
+ Trip,
+ CreateTripPayload,
+ UpdateTripPayload,
+} from "@/src/types/trip";
import Header from "@/src/Components/UI/Header";
// ── Page ──────────────────────────────────────────────────────────────────────
@@ -147,6 +156,9 @@ export default function TripsPage() {
{/* ── Form modal ── */}
{showForm && (
setShowForm(false)}
onSubmit={async (payload, isNew) => {
@@ -170,7 +182,9 @@ export default function TripsPage() {
{/* ── 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)} />}
+ {archiveOpen && (
+ setArchiveOpen(false)} />
+ )}
{/* ── Floating archive button — same component Driver page uses ── */}
setArchiveOpen(true)} label="الأرشيف" />
@@ -178,4 +192,4 @@ export default function TripsPage() {
);
-}
\ No newline at end of file
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index ac9ecdc..491b268 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -17,13 +17,12 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
- // ⚠️ اتصلح: الـ Navbar والـ footer كانوا برة / وده HTML غير صالح
+
- {/* CHANGE: mounted globally so every route (not just the dashboard
- form pages) gets protected from extension-injected attributes */}
+ // CHANGE: added — fixes hydration mismatch caused by browser extensions
- {/* الناف بار بيظهر بس لو مفيش يوزر مسجل دخول */}
+ // CHANGE: navbar is conditional based on route, so we can hide it on login/register pages
{children}
diff --git a/src/Components/Driver/DriverFormModal.tsx b/src/Components/Driver/DriverFormModal.tsx
index fde09e8..bb4a547 100644
--- a/src/Components/Driver/DriverFormModal.tsx
+++ b/src/Components/Driver/DriverFormModal.tsx
@@ -6,7 +6,11 @@ import { yupResolver } from "@hookform/resolvers/yup";
import { Alert, Button, FileInput, Input, Modal, Select } from "../UI";
import { get } from "@/src/services/api";
import { getStoredToken } from "@/src/lib/auth";
-import { createDriverSchema, updateDriverSchema } from "@/src/validations/driver.validator";
+import {
+ createDriverSchema,
+ updateDriverSchema,
+} from "@/src/validations/driver.validator";
+import { useEditFormSync } from "@/src/hooks/useEditFormSync";
import type {
Driver,
CreateDriverPayload,
@@ -105,7 +109,9 @@ export function DriverFormModal({
// 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,
+ resolver: yupResolver(
+ (isNew ? createDriverSchema : updateDriverSchema) as any,
+ ) as any,
defaultValues: {
name: editDriver?.name ?? "",
phone: editDriver?.phone ?? "",
@@ -113,9 +119,12 @@ export function DriverFormModal({
address: editDriver?.address ?? "",
nationality: editDriver?.nationality ?? "",
nationalIdType: editDriver?.nationalIdType ?? "",
- nationalId: (editDriver as Driver & { nationalId?: string })?.nationalId ?? "",
+ nationalId:
+ (editDriver as Driver & { nationalId?: string })?.nationalId ?? "",
nationalIdExpiry:
- (editDriver as Driver & { nationalIdExpiry?: string })?.nationalIdExpiry?.slice(0, 10) ?? "",
+ (
+ editDriver as Driver & { nationalIdExpiry?: string }
+ )?.nationalIdExpiry?.slice(0, 10) ?? "",
gosiNumber: editDriver?.gosiNumber ?? "",
licenseNumber: editDriver?.licenseNumber ?? "",
licenseType: editDriver?.licenseType ?? "",
@@ -135,29 +144,30 @@ 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
- // had no matching yet and silently fell back to "" —
- // reapply the saved id now that the option actually exists in the DOM.
- const savedBranchId = (editDriver as Driver & { branchId?: string })?.branchId;
-
if (branchesProp.length > 0) {
- queueMicrotask(() => {
- setBranches(branchesProp);
- if (savedBranchId) setValue("branchId", savedBranchId);
- });
+ 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 ?? [];
+ const list =
+ (res as unknown as { data: { data: Branch[] } }).data?.data ?? [];
setBranches(list);
- if (savedBranchId) setValue("branchId", savedBranchId);
})
.catch(() => {
/* silently ignore */
});
- }, [branchesProp, editDriver, setValue]);
+ }, [branchesProp]);
+
+ // CHANGE: replaced the manual `if (savedBranchId) setValue("branchId", ...)`
+ // calls above with useEditFormSync — same fix, shared implementation.
+ useEditFormSync(
+ setValue,
+ "branchId",
+ (editDriver as Driver & { branchId?: string })?.branchId,
+ branches,
+ );
const [apiError, setApiError] = useState("");
@@ -171,20 +181,27 @@ export function DriverFormModal({
const submitHandler = useCallback(
async (data: DriverFormValues) => {
- const payload: Record = { name: data.name, phone: data.phone };
+ const payload: Record = {
+ name: data.name,
+ phone: data.phone,
+ };
if (data.email) payload.email = data.email;
if (data.address) payload.address = data.address;
if (data.nationality) payload.nationality = data.nationality;
if (data.nationalIdType) payload.nationalIdType = data.nationalIdType;
if (data.nationalId) payload.nationalId = data.nationalId;
- if (data.nationalIdExpiry) payload.nationalIdExpiry = toIsoDateTime(data.nationalIdExpiry);
+ if (data.nationalIdExpiry)
+ payload.nationalIdExpiry = toIsoDateTime(data.nationalIdExpiry);
if (data.gosiNumber) payload.gosiNumber = data.gosiNumber;
if (data.licenseNumber) payload.licenseNumber = data.licenseNumber;
if (data.licenseType) payload.licenseType = data.licenseType;
- if (data.licenseExpiry) payload.licenseExpiry = toIsoDateTime(data.licenseExpiry);
- if (data.driverCardNumber) payload.driverCardNumber = data.driverCardNumber;
+ if (data.licenseExpiry)
+ payload.licenseExpiry = toIsoDateTime(data.licenseExpiry);
+ if (data.driverCardNumber)
+ payload.driverCardNumber = data.driverCardNumber;
if (data.driverCardType) payload.driverCardType = data.driverCardType;
- if (data.driverCardExpiry) payload.driverCardExpiry = toIsoDateTime(data.driverCardExpiry);
+ if (data.driverCardExpiry)
+ payload.driverCardExpiry = toIsoDateTime(data.driverCardExpiry);
if (data.driverType) payload.driverType = data.driverType;
if (data.branchId) payload.branchId = data.branchId;
if (!isNew) payload.status = data.status;
@@ -196,15 +213,23 @@ export function DriverFormModal({
setApiError("");
try {
- const ok = await onSubmit(payload as unknown as CreateDriverPayload, isNew);
+ const ok = await onSubmit(
+ payload as unknown as CreateDriverPayload,
+ isNew,
+ );
if (ok) {
onClose();
} else {
- setError("name", { message: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً." });
+ setError("name", {
+ message: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.",
+ });
setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.");
}
} catch (err) {
- const message = err instanceof Error ? err.message : "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.";
+ const message =
+ err instanceof Error
+ ? err.message
+ : "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.";
setError("name", { message });
setApiError(message);
}
@@ -220,10 +245,15 @@ export function DriverFormModal({
onClose={onClose}
size="lg"
subtitle={isNew ? "إضافة سائق" : "تعديل سائق"}
- title={isNew ? "سائق جديد" : editDriver?.name ?? ""}
+ title={isNew ? "سائق جديد" : (editDriver?.name ?? "")}
footer={
<>
-
+
إلغاء
@@ -240,7 +270,11 @@ export function DriverFormModal({
className="flex flex-col gap-4"
>
{errors.name?.type === "manual" && (
- setApiError("")} />
+ setApiError("")}
+ />
)}
{/* ── Section: Personal Info ── */}
@@ -252,7 +286,9 @@ export function DriverFormModal({
placeholder="محمد عبدالله"
dir="rtl"
autoComplete="off"
- error={errors.name?.type !== "manual" ? errors.name?.message : undefined}
+ error={
+ errors.name?.type !== "manual" ? errors.name?.message : undefined
+ }
{...register("name")}
/>
@@ -404,7 +440,9 @@ export function DriverFormModal({
>
اختر الفرع
{branches.map((b) => (
- {b.name}
+
+ {b.name}
+
))}
@@ -429,7 +467,14 @@ export function DriverFormModal({
{/* ── Section: Photos ── */}
الصور والمستندات
-
+
جميع حقول الصور اختيارية
@@ -440,25 +485,37 @@ export function DriverFormModal({
name="photo"
control={control}
render={({ field }) => (
-
+
)}
/>
(
-
+
)}
/>
(
-
+
)}
/>
);
-}
\ No newline at end of file
+}
diff --git a/src/Components/Trip/Tripformmodal.tsx b/src/Components/Trip/Tripformmodal.tsx
index 7de7081..f572d6e 100644
--- a/src/Components/Trip/Tripformmodal.tsx
+++ b/src/Components/Trip/Tripformmodal.tsx
@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { Alert, Button, Input, Modal, Select, Textarea } from "../UI";
import { getStoredToken } from "@/src/lib/auth";
+import { useEditFormSync } from "@/src/hooks/useEditFormSync";
import {
createTripSchema,
updateTripSchema,
@@ -126,30 +127,25 @@ export function TripFormModal({
},
});
- useEffect(() => {
- const token = getStoredToken();
+ // ── Fetch dropdown options once on mount ────────────────────────────────────
+// CHANGE: split fetching from syncing — fetching stays a plain useEffect,
+// syncing the saved id back into the form is now handled by useEditFormSync
+// below, once per field, instead of being inlined into each .then().
+useEffect(() => {
+ const token = getStoredToken();
- driverService.getActiveOptions(token).then((list) => {
- setDrivers(list);
- // defaultValues were applied before this list existed, so the
- // had no matching 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(() => {});
+ driverService.getActiveOptions(token).then(setDrivers).catch(() => {});
+ carService.getActiveOptions(token).then(setCars).catch(() => {});
+ branchService.getOptions(token).then(setBranches).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]);
+// CHANGE: replaced the three manual `if (savedXId) setValue(...)` calls
+// (previously inlined inside each .then() above) with useEditFormSync —
+// each hook call re-applies the saved id once its matching option list
+// has loaded, regardless of which fetch resolves first.
+useEditFormSync(setValue, "driverId", editTrip?.driverId ?? editTrip?.driver?.id, drivers);
+useEditFormSync(setValue, "carId", editTrip?.carId ?? editTrip?.car?.id, cars);
+useEditFormSync(setValue, "branchId", editTrip?.branchId ?? editTrip?.branch?.id, branches);
const [apiError, setApiError] = useState("");
diff --git a/src/Components/car/CarFormModal.tsx b/src/Components/car/CarFormModal.tsx
index c69a9c0..3749e40 100644
--- a/src/Components/car/CarFormModal.tsx
+++ b/src/Components/car/CarFormModal.tsx
@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { Alert, Button, Input, Modal, Select } from "../UI";
import { getStoredToken } from "@/src/lib/auth";
+import { useEditFormSync } from "@/src/hooks/useEditFormSync";
import {
createCarSchema,
updateCarSchema,
@@ -134,38 +135,31 @@ export function CarFormModal({
},
});
- // ── Local branches (auto-fetched if prop is empty) ─────────────────────────
- const [branches, setBranches] = useState(branchesProp);
- const loadBranches = useCallback(() => {
- // defaultValues.branchId was applied before this list existed, so the
- // had no matching yet and silently fell back to "" —
- // reapply the saved id now that the option actually exists in the DOM.
- const savedBranchId = editCar?.branch?.id;
+ // ── Local branches (auto-fetched if prop is empty) ─────────────────────────
+const [branches, setBranches] = useState(branchesProp);
+const loadBranches = useCallback(() => {
+ if (branchesProp.length > 0) {
+ queueMicrotask(() => setBranches(branchesProp));
+ return;
+ }
+ const token = getStoredToken();
+ branchService
+ .getOptions(token)
+ .then((list) => {
+ queueMicrotask(() => setBranches(list as unknown as Branch[]));
+ })
+ .catch(() => {
+ /* silently ignore */
+ });
+}, [branchesProp]);
- if (branchesProp.length > 0) {
- queueMicrotask(() => {
- setBranches(branchesProp);
- if (savedBranchId) setValue("branchId", savedBranchId);
- });
- return;
- }
- const token = getStoredToken();
- branchService
- .getOptions(token)
- .then((list) => {
- queueMicrotask(() => {
- setBranches(list as unknown as Branch[]);
- if (savedBranchId) setValue("branchId", savedBranchId);
- });
- })
- .catch(() => {
- /* silently ignore */
- });
- }, [branchesProp, editCar, setValue]);
+useEffect(() => {
+ loadBranches();
+}, [loadBranches]);
- useEffect(() => {
- loadBranches();
- }, [loadBranches]);
+// CHANGE: replaced the manual `if (savedBranchId) setValue("branchId", ...)`
+// calls above with useEditFormSync.
+useEditFormSync(setValue, "branchId", editCar?.branch?.id, branches);
const [apiError, setApiError] = useState("");
diff --git a/src/hooks/useCurrentUser.ts b/src/hooks/useCurrentUser.ts
index e2dc148..aeee9b9 100644
--- a/src/hooks/useCurrentUser.ts
+++ b/src/hooks/useCurrentUser.ts
@@ -14,12 +14,14 @@ export function useCurrentUser() {
setLoading(true);
setError(null);
try {
+ //1. get token from local storage
const token = getStoredToken();
+ //2. call userService.getMe(token) to get the current user
const res = await userService.getMe(token);
- // ملاحظة: مبقاش بنعمل res.data ?? res هنا قبل التمرير.
- // extractMeUser هي المسؤولة الوحيدة عن تفكيك شكل الـ response،
- // فبنبعتلها الـ res زي ما هو عشان نتجنب تفكيك مزدوج.
+ // console.log("useCurrentUser: got me", res);
+ //3. extract the user from the response using extractMeUser(res)
const u = extractMeUser(res);
+ //4. if user is null, throw an error
if (!u) throw new Error("لا توجد بيانات");
setUser(u as UserMe);
} catch {
diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts
index 83bb00c..72096bd 100644
--- a/src/hooks/useUser.ts
+++ b/src/hooks/useUser.ts
@@ -82,7 +82,7 @@ useEffect(() => {
}, [page, search, loadUsers]);
// create new user
- const createUser = useCallback(async (data: UserFormData): Promise => {
+const createUser = useCallback(async (data: UserFormData): Promise => {
try {
const token = getStoredToken();
const user = await userService.create(data as UserFormData & { password: string }, token);
@@ -100,7 +100,7 @@ useEffect(() => {
try {
const token = getStoredToken();
const res = await userService.update(id, data, token);
- dispatch({ type: "UPDATE", user: res?.data });
+ dispatch({ type: "UPDATE", user: res });
notify({ type: "success", message: "تم تحديث بيانات المستخدم بنجاح." });
return true;
} catch (err) {