)}
- {/* ── Page header ── */}
- {/* ── Content grid ── */}
@@ -380,14 +357,14 @@ export default function DriverDetailPage() {
/>
)}
- {deleteOpen && (
- setDeleteOpen(false)}
- onConfirm={handleConfirmDelete}
- />
- )}
+ setDeleteOpen(false)}
+ onConfirm={handleConfirmDelete}
+ title="حذف السائق"
+ description={`هل أنت متأكد من حذف ${driver.name} (${driver.phone})؟ لا يمكن التراجع عن هذا الإجراء.`}
+ />
>
);
}
\ No newline at end of file
diff --git a/app/dashboard/drivers/page.tsx b/app/dashboard/drivers/page.tsx
index 58ed8ff..d42c735 100644
--- a/app/dashboard/drivers/page.tsx
+++ b/app/dashboard/drivers/page.tsx
@@ -1,13 +1,11 @@
"use client";
import { useState, useCallback } from "react";
-import { Alert, Spinner, ArchiveButton } from "@/src/Components/UI";
-import { DriverFormModal } from "@/src/Components/Driver/DriverFormModal";
-import { DriverDeleteModal } from "@/src/Components/Driver/DriverDeleteModal";
+import { Alert, Spinner, ArchiveButton, ConfirmDialog } from "@/src/Components/UI";
import { ArchivedDrivers } from "@/src/Components/Driver/archive/ArchivedDrivers";
import { useDrivers } from "@/src/hooks/useDriver";
import { CreateDriverPayload, Driver, DRIVER_STATUS_MAP, UpdateDriverPayload } from "@/src/types/driver";
-import { DriverDetailPanel } from "@/src/Components/Driver/DriverDetailPanel";
+import { DriverDetailPanel, DriverFormModal , } from "@/src/Components/Driver";
// ── Helpers ───────────────────────────────────────────────────────────────────
@@ -413,14 +411,14 @@ export default function DriversPage() {
)}
{/* ── Delete modal ── */}
- {deleteTarget && (
- setDeleteTarget(null)}
- onConfirm={handleConfirmDelete}
- />
- )}
+ setDeleteTarget(null)}
+ onConfirm={handleConfirmDelete}
+ title="حذف السائق"
+ description={`هل أنت متأكد من حذف ${deleteTarget?.name ?? ""} (${deleteTarget?.phone ?? ""})؟ لا يمكن التراجع عن هذا الإجراء.`}
+ />
{/* ── Archive browser modal ── */}
{archiveOpen && (
diff --git a/app/dashboard/orders/page.tsx b/app/dashboard/orders/page.tsx
index 50792a3..962f3fa 100644
--- a/app/dashboard/orders/page.tsx
+++ b/app/dashboard/orders/page.tsx
@@ -3,13 +3,12 @@
import { useState, useCallback } from "react";
-import { Alert, Spinner, Toast, ArchiveButton } from "@/src/Components/UI";
-import { OrderFormModal } from "@/src/Components/Order/OrderFormModal";
-import { OrderDeleteModal } from "@/src/Components/Order/OrderDeleteModal";
-import { OrderDetailPanel, ORDER_STATUS_MAP } from "@/src/Components/Order/OrderDetailBanel";
+import { Alert, Spinner, Toast, ArchiveButton, ConfirmDialog } from "@/src/Components/UI";
+import { ORDER_STATUS_MAP } from "@/src/Components/Order/OrderDetailBanel";
import { ArchivedOrdersModal } from "@/src/Components/Order/archive/ArchivedOrdersModal";
import { useOrders } from "@/src/hooks/useOrder";
import type { CreateOrderPayload, Order, UpdateOrderPayload } from "@/src/types/order";
+import { OrderFormModal , OrderDetailPanel} from "@/src/Components/Order";
// ── Helpers — copied verbatim from app/dashboard/drivers/page.tsx ───────
@@ -441,14 +440,14 @@ export default function OrderComponent() {
)}
{/* ── Delete modal ── */}
- {deleteTarget && (
- setDeleteTarget(null)}
- onConfirm={handleConfirmDelete}
- />
- )}
+ setDeleteTarget(null)}
+ onConfirm={handleConfirmDelete}
+ title="حذف الطلب"
+ description={`هل أنت متأكد من حذف الطلب ${deleteTarget?.shipmentNumber ?? ""} الخاص بـ ${deleteTarget?.recipientName ?? ""}؟ لا يمكن التراجع عن هذا الإجراء.`}
+ />
{/* ── Archive browser modal ── */}
{archiveOpen && (
diff --git a/app/dashboard/roles/page.tsx b/app/dashboard/roles/page.tsx
index 7b84745..04adb45 100644
--- a/app/dashboard/roles/page.tsx
+++ b/app/dashboard/roles/page.tsx
@@ -1,12 +1,9 @@
"use client";
import { useState } from "react";
-import { Alert, ArchiveButton } from "@/src/Components/UI";
+import { Alert, ArchiveButton, ConfirmDialog,Toast } from "@/src/Components/UI";
import { RoleTable } from "@/src/Components/role/RoleTable";
-import { RoleFormModal } from "@/src/Components/role/RoleFormModal";
-import { RoleDetailModal } from "@/src/Components/role/RoleDetailModal";
-import { DeleteRoleModal } from "@/src/Components/role/DeleteRoleModal";
-import { Toast } from "@/src/Components/UI";
+import { RoleFormModal,RoleDetailModal } from "@/src/Components/role";
import { useRoles } from "@/src/hooks/useRole";
import { Role, RoleFormData } from "@/src/types/role";
import { ArchivedRolesModal } from "@/src/Components/role/archive/ArchivedRolesModal";
@@ -77,14 +74,14 @@ export default function RolesPage() {
)}
{/* Delete confirmation */}
- {deleteTarget && (
- { if (!deleting) setDeleteTarget(null); }}
- onConfirm={handleDeleteConfirm}
- />
- )}
+ { if (!deleting) setDeleteTarget(null); }}
+ onConfirm={handleDeleteConfirm}
+ title="حذف الدور"
+ description={`هل أنت متأكد من حذف دور ${deleteTarget?.name ?? ""}؟ لا يمكن التراجع عن هذا الإجراء.`}
+ />
{/* Archive browser modal */}
{archiveOpen && (
diff --git a/app/dashboard/trips/[tripId]/page.tsx b/app/dashboard/trips/[tripId]/page.tsx
index 944feab..0ea45ac 100644
--- a/app/dashboard/trips/[tripId]/page.tsx
+++ b/app/dashboard/trips/[tripId]/page.tsx
@@ -2,9 +2,8 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useParams, useRouter } from "next/navigation";
-import { Spinner } from "@/src/Components/UI";
+import { ConfirmDialog, Spinner } from "@/src/Components/UI";
import { TripFormModal } from "@/src/Components/Trip/Tripformmodal";
-import { TripDeleteModal } from "@/src/Components/Trip/Tripdeletemodal";
import { TripReportPanel } from "@/src/Components/Trip_Report/Tripreportpanel";
import { tripService } from "@/src/services/trip.service";
import { getStoredToken } from "@/src/lib/auth";
@@ -206,44 +205,42 @@ export default function TripDetailPage() {
// ── Load trip ─────────────────────────────────────────────────────────────
const loadTrip = useCallback(async () => {
- if (!tripId) return;
- setLoading(true);
- setError(null);
- try {
- const token = getStoredToken();
- const res = await tripService.getById(tripId, token);
- setTrip((res as unknown as { data: Trip }).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(err instanceof Error ? err.message : "تعذّر تحميل بيانات الرحلة.");
+ } 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 res = await tripService.update(trip.id, payload as UpdateTripPayload, token);
- const updated = (res as unknown as { data: Trip }).data;
- 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 () => {
@@ -558,14 +555,14 @@ export default function TripDetailPage() {
)}
{/* ── Delete modal ── */}
- {deleteOpen && (
- setDeleteOpen(false)}
- onConfirm={handleConfirmDelete}
- />
- )}
+ setDeleteOpen(false)}
+ onConfirm={handleConfirmDelete}
+ title="حذف الرحلة"
+ description={`هل أنت متأكد من حذف رحلة ${trip.title} (${trip.tripNumber})؟ لا يمكن التراجع عن هذا الإجراء.`}
+ />
>
);
}
\ No newline at end of file
diff --git a/app/dashboard/trips/archived/[tripId]/page.tsx b/app/dashboard/trips/archived/[tripId]/page.tsx
index 3ffbe63..5837b83 100644
--- a/app/dashboard/trips/archived/[tripId]/page.tsx
+++ b/app/dashboard/trips/archived/[tripId]/page.tsx
@@ -31,10 +31,8 @@ export default function ArchivedTripDetailPage() {
setError(null);
try {
const token = getStoredToken();
- // Archived trips must go through the archived endpoint —
- // the normal /trip/:id endpoint won't return soft-deleted trips.
- const res = await tripService.getArchivedById(tripId, token);
- if (!cancelled) setTrip((res as unknown as { data: Trip }).data);
+ const data = await tripService.getArchivedById(tripId, token);
+ if (!cancelled) setTrip(data);
} catch {
if (!cancelled) setError("لم يتم العثور على هذه الرحلة في الأرشيف.");
} finally {
@@ -54,7 +52,6 @@ export default function ArchivedTripDetailPage() {
);
}
- // Edge case: invalid/missing/unreachable trip ID
if (error || !trip) {
return (
setDeleteTarget(null)}
- onConfirm={handleConfirmDelete}
- />
- )}
+ setDeleteTarget(null)}
+ onConfirm={handleConfirmDelete}
+ title="حذف الرحلة"
+ description={`هل أنت متأكد من حذف رحلة ${deleteTarget?.title ?? ""} (${deleteTarget?.tripNumber ?? ""})؟ لا يمكن التراجع عن هذا الإجراء.`}
+ />
{/* ── Archive browser modal ── */}
{archiveOpen && (
@@ -768,4 +766,4 @@ export default function TripsPage() {
);
-}
+}
\ No newline at end of file
diff --git a/app/dashboard/users/page.tsx b/app/dashboard/users/page.tsx
index 1482c4e..8af9c8a 100644
--- a/app/dashboard/users/page.tsx
+++ b/app/dashboard/users/page.tsx
@@ -1,11 +1,8 @@
"use client";
import { useState } from "react";
-import { Alert, Toast, ArchiveButton } from "@/src/Components/UI";
-import { UserTable } from "@/src/Components/User/UserTable";
-import { UserFormModal } from "@/src/Components/User/UserFormModal";
-import { UserDetailModal } from "@/src/Components/User/UserDetailModal";
-import { DeleteConfirmModal } from "@/src/Components/User/DeleteConfirmModal";
+import { Alert, Toast, ArchiveButton, ConfirmDialog } from "@/src/Components/UI";
+import { UserFormModal,UserDetailModal,UserTable } from "@/src/Components/User";
import { useUsers } from "@/src/hooks/useUser";
import type { User, UserFormData } from "@/src/types/user";
import { ArchivedUsersModal } from "@/src/Components/User/archive/Archivedusersmodal";
@@ -76,16 +73,14 @@ export default function UsersPage() {
)}
{/* Delete confirmation dialog */}
- {deleteTarget && (
-
{
- if (!deleting) setDeleteTarget(null);
- }}
- onConfirm={handleDeleteConfirm}
- />
- )}
+ { if (!deleting) setDeleteTarget(null); }}
+ onConfirm={handleDeleteConfirm}
+ title="حذف المستخدم"
+ description={`هل أنت متأكد من حذف ${deleteTarget?.name ?? ""}؟ لا يمكن التراجع عن هذا الإجراء.`}
+ />
{/* Archive browser modal */}
{archiveOpen && (
diff --git a/app/layout.tsx b/app/layout.tsx
index c5b1f31..da6e053 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -18,7 +18,7 @@ export default function RootLayout({
return (
// ⚠️ اتصلح: الـ Navbar والـ footer كانوا برة / وده HTML غير صالح
-
+
{/* الناف بار بيظهر بس لو مفيش يوزر مسجل دخول */}
{children}
diff --git a/src/Components/Branch/BranchDetailModal.tsx b/src/Components/Branch/BranchDetailModal.tsx
index bf81ddd..b7fd5c3 100644
--- a/src/Components/Branch/BranchDetailModal.tsx
+++ b/src/Components/Branch/BranchDetailModal.tsx
@@ -78,20 +78,20 @@ export function BranchDetailModal({ branchId, onClose }: BranchDetailModalProps)
// fetch branch details on mount
useEffect(() => {
- let cancelled = false;
- (async () => {
- try {
- const token = getStoredToken();
- const res = await branchService.getById(branchId, token);
- if (!cancelled) setBranch(res.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) =>
diff --git a/src/Components/Branch/DeleteConfirmModal.tsx b/src/Components/Branch/DeleteConfirmModal.tsx
deleted file mode 100644
index 22dc4f9..0000000
--- a/src/Components/Branch/DeleteConfirmModal.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { Spinner } from "../UI";
-import type { Branch } from "@/src/types/branch";
-
-interface DeleteConfirmModalProps {
- branch: Branch;
- deleting: boolean;
- onCancel: () => void;
- onConfirm: () => void;
-}
-
-export function DeleteConfirmModal({ branch, deleting, onCancel, onConfirm }: DeleteConfirmModalProps) {
- useEffect(() => {
- const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); };
- window.addEventListener("keydown", handler);
- return () => window.removeEventListener("keydown", handler);
- }, [onCancel]);
-
- return (
- { if (e.target === e.currentTarget) onCancel(); }}
- style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem" }}
- >
-
e.stopPropagation()}
- style={{ width: "100%", maxWidth: 400, background: "var(--color-surface)", borderRadius: "var(--radius-xl)", border: "1px solid #FECACA", boxShadow: "0 20px 48px rgba(0,0,0,.18)", padding: "2rem", display: "flex", flexDirection: "column", gap: "1rem", textAlign: "center" }}
- >
- {/* icon delete */}
-
-
-
حذف الفرع
-
- هل أنت متأكد من حذف فرع {branch.name} ؟ لا يمكن التراجع عن هذا الإجراء.
-
-
-
-
- إلغاء
-
-
- {deleting && }
- {deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/Components/Car_Maintanance/CarMaintananceDeleteModal.tsx b/src/Components/Car_Maintanance/CarMaintananceDeleteModal.tsx
deleted file mode 100644
index 856150a..0000000
--- a/src/Components/Car_Maintanance/CarMaintananceDeleteModal.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
- "use client";
-
-import { useEffect } from "react";
-import { Spinner } from "../UI";
-import { fmtCost } from "@/src/types/carMaintanance";
-import type { CarMaintenance } from "@/src/types/carMaintanance";
-
-interface CarMaintenanceDeleteModalProps {
- record: CarMaintenance;
- deleting: boolean;
- onCancel: () => void;
- onConfirm: () => void;
-}
-
-export function CarMaintenanceDeleteModal({
- record,
- deleting,
- onCancel,
- onConfirm,
-}: CarMaintenanceDeleteModalProps) {
- useEffect(() => {
- const h = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); };
- window.addEventListener("keydown", h);
- return () => window.removeEventListener("keydown", h);
- }, [onCancel]);
-
- return (
- { if (e.target === e.currentTarget) onCancel(); }}
- style={{
- position: "fixed", inset: 0, zIndex: 60,
- background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
- display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem",
- }}
- >
-
e.stopPropagation()}
- style={{
- width: "100%", maxWidth: 420,
- background: "var(--color-surface)",
- borderRadius: "var(--radius-xl)",
- border: "1px solid #FECACA",
- boxShadow: "0 20px 48px rgba(0,0,0,.18)",
- padding: "2rem",
- display: "flex", flexDirection: "column", gap: "1rem",
- textAlign: "center",
- }}
- >
- {/* Icon */}
-
-
-
-
- حذف سجل الصيانة
-
-
- هل أنت متأكد من حذف سجل{" "}
-
- {record.reason}
-
- {" "}(
-
- {fmtCost(record.cost)}
-
- )؟ لا يمكن التراجع عن هذا الإجراء.
-
-
-
-
-
- إلغاء
-
-
- {deleting && }
- {deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/Components/Client/Deleteconfirmmodal.tsx b/src/Components/Client/Deleteconfirmmodal.tsx
deleted file mode 100644
index 20e805d..0000000
--- a/src/Components/Client/Deleteconfirmmodal.tsx
+++ /dev/null
@@ -1,171 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { Spinner } from "../UI";
-import type { Client } from "@/src/types/client";
-
-interface DeleteConfirmModalProps {
- client: Client;
- deleting: boolean;
- onCancel: () => void;
- onConfirm: () => void;
-}
-
-export function DeleteConfirmModal({
- client,
- deleting,
- onCancel,
- onConfirm,
-}: DeleteConfirmModalProps) {
- // Close on Escape — identical to User/DeleteConfirmModal
- useEffect(() => {
- const handler = (e: KeyboardEvent) => {
- if (e.key === "Escape") onCancel();
- };
- window.addEventListener("keydown", handler);
- return () => window.removeEventListener("keydown", handler);
- }, [onCancel]);
-
- return (
- {
- if (e.target === e.currentTarget) onCancel();
- }}
- style={{
- position: "fixed",
- inset: 0,
- zIndex: 60,
- background: "rgba(15,23,42,0.55)",
- backdropFilter: "blur(4px)",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- padding: "1rem",
- }}
- >
-
e.stopPropagation()}
- style={{
- width: "100%",
- maxWidth: 400,
- background: "var(--color-surface)",
- borderRadius: "var(--radius-xl)",
- border: "1px solid #FECACA",
- boxShadow: "0 20px 48px rgba(0,0,0,.18)",
- padding: "2rem",
- display: "flex",
- flexDirection: "column",
- gap: "1rem",
- textAlign: "center",
- }}
- >
- {/* أيقونة الحذف */}
-
-
-
-
- حذف العميل
-
-
- هل أنت متأكد من حذف{" "}
-
- {client.name}
-
- ؟ سيتم حذف جميع عناوينه أيضاً. لا يمكن التراجع عن هذا الإجراء.
-
-
-
-
-
- إلغاء
-
-
- {deleting && }
- {deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/Components/Driver/DriverDeleteModal.tsx b/src/Components/Driver/DriverDeleteModal.tsx
deleted file mode 100644
index 98b7d3c..0000000
--- a/src/Components/Driver/DriverDeleteModal.tsx
+++ /dev/null
@@ -1,100 +0,0 @@
-"use client";
-
-
-import { useEffect } from "react";
-import { Spinner } from "../UI";
-import type { Driver } from "@/src/types/driver";
-
-interface DriverDeleteModalProps {
- driver: Driver;
- deleting: boolean;
- onCancel: () => void;
- onConfirm: () => void;
-}
-
-export function DriverDeleteModal({ driver, deleting, onCancel, onConfirm }: DriverDeleteModalProps) {
- useEffect(() => {
- const h = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); };
- window.addEventListener("keydown", h);
- return () => window.removeEventListener("keydown", h);
- }, [onCancel]);
-
- return (
- { if (e.target === e.currentTarget) onCancel(); }}
- style={{
- position: "fixed", inset: 0, zIndex: 60,
- background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
- display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem",
- }}
- >
-
e.stopPropagation()}
- style={{
- width: "100%", maxWidth: 420,
- background: "var(--color-surface)",
- borderRadius: "var(--radius-xl)",
- border: "1px solid #FECACA",
- boxShadow: "0 20px 48px rgba(0,0,0,.18)",
- padding: "2rem",
- display: "flex", flexDirection: "column", gap: "1rem",
- textAlign: "center",
- }}
- >
- {/* Icon */}
-
-
-
-
- حذف السائق
-
-
- هل أنت متأكد من حذف{" "}
- {driver.name}
- {" "}(
-
- {driver.phone}
-
- )؟ لا يمكن التراجع عن هذا الإجراء.
-
-
-
-
-
- إلغاء
-
-
- {deleting && }
- {deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/Components/Driver/DriverDetailPanel.tsx b/src/Components/Driver/DriverDetailPanel.tsx
index 35d986a..a6df45f 100644
--- a/src/Components/Driver/DriverDetailPanel.tsx
+++ b/src/Components/Driver/DriverDetailPanel.tsx
@@ -12,8 +12,6 @@ import {
NATIONAL_ID_TYPE_MAP,
} from "@/src/types/driver";
-// ── Helpers ───────────────────────────────────────────────────────────────────
-
function fmtDate(iso?: string | null): string {
if (!iso) return "—";
return new Date(iso).toLocaleDateString("ar-SA", {
@@ -28,8 +26,6 @@ function isExpiringSoon(iso?: string | null): boolean {
return new Date(iso).getTime() - Date.now() <= 90 * 86_400_000;
}
-// ── Sub-components ────────────────────────────────────────────────────────────
-
function DetailRow({
label,
value,
@@ -89,10 +85,6 @@ function SectionHeading({ title }: { title: string }) {
);
}
-// ── PhotoCard ─────────────────────────────────────────────────────────────────
-// key={url} on the forces a full remount whenever the URL changes,
-// which clears any stale onError state from a previously failed load.
-
function PhotoCard({ url, label }: { url?: string | null; label: string }) {
const [imgError, setImgError] = useState(false);
@@ -161,8 +153,6 @@ function PhotoCard({ url, label }: { url?: string | null; label: string }) {
);
}
-// ── Props ─────────────────────────────────────────────────────────────────────
-
interface DriverDetailPanelProps {
driverId: string;
onClose: () => void;
@@ -170,8 +160,6 @@ interface DriverDetailPanelProps {
onDelete: (driver: Driver) => void;
}
-// ── Component ─────────────────────────────────────────────────────────────────
-
export function DriverDetailPanel({
driverId,
onClose,
@@ -187,8 +175,8 @@ export function DriverDetailPanel({
setError(null);
try {
const token = getStoredToken();
- const res = await driverService.getById(driverId, token);
- setDriver((res as unknown as { data: Driver }).data);
+ const data = await driverService.getById(driverId, token);
+ setDriver(data);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "تعذّر تحميل بيانات السائق.");
} finally {
@@ -208,7 +196,6 @@ export function DriverDetailPanel({
return (
<>
- {/* Backdrop */}
- {/* Slide-in panel */}
- {/* ── Header ── */}
- {/* Avatar — key={photoUrl} forces remount on photo change */}
- {/* ── Scrollable content ── */}
{loading && (
@@ -338,7 +321,6 @@ export function DriverDetailPanel({
{driver && !loading && (
<>
- {/* Status badges */}
{statusConfig && (
- {/* ── Footer actions ── */}
{driver && (
{
- let cancelled = false;
- (async () => {
- try {
- const token = getStoredToken();
- const [driverRes, historyRes] = await Promise.all([
- archivedDriverService.getById(driverId, token),
- archivedDriverService.getStatusHistory(driverId, token),
- ]);
- if (!cancelled) {
- setDriver(driverRes.data);
- setHistory(historyRes.data ?? []);
- }
- } catch {
- if (!cancelled) setError("تعذّر تحميل بيانات السائق المؤرشف. يرجى المحاولة لاحقاً.");
- } finally {
- if (!cancelled) setLoading(false);
+ useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const token = getStoredToken();
+ const [driverData, historyList] = await Promise.all([
+ archivedDriverService.getByIdUnwrapped(driverId, token),
+ archivedDriverService.getStatusHistoryUnwrapped(driverId, token),
+ ]);
+ if (!cancelled) {
+ setDriver(driverData);
+ setHistory(historyList ?? []);
}
- })();
- return () => { cancelled = true; };
- }, [driverId]);
+ } catch {
+ if (!cancelled) setError("تعذّر تحميل بيانات السائق المؤرشف. يرجى المحاولة لاحقاً.");
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => { cancelled = true; };
+}, [driverId]);
// ── helpers ───────────────────────────────────────────────────────────────
const fmt = (iso?: string | null) =>
diff --git a/src/Components/Driver_Report/driverReport.tsx b/src/Components/Driver_Report/driverReport.tsx
index c131dd8..93de4ca 100644
--- a/src/Components/Driver_Report/driverReport.tsx
+++ b/src/Components/Driver_Report/driverReport.tsx
@@ -5,14 +5,10 @@ import { Spinner } from "../UI";
import { driverService } from "@/src/services/driver.service";
import { getStoredToken } from "@/src/lib/auth";
-// ── Props ─────────────────────────────────────────────────────────────────────
-
interface DriverReportPanelProps {
driverId: string;
}
-// ── Component ─────────────────────────────────────────────────────────────────
-
export function DriverReportPanel({ driverId }: DriverReportPanelProps) {
const today = new Date().toISOString().slice(0, 10);
const [date, setDate] = useState(today);
@@ -25,15 +21,10 @@ export function DriverReportPanel({ driverId }: DriverReportPanelProps) {
setError(null);
try {
const token = getStoredToken();
- const res = await driverService.getDailyReport(driverId, date, token);
- // The response shape: { data: { reportUrl, filename } }
- const reportUrl = (
- res as unknown as { data: { reportUrl: string; filename: string } }
- ).data?.reportUrl;
+ const { reportUrl } = await driverService.getDailyReport(driverId, date, token);
if (!reportUrl) throw new Error("لم يتم إرجاع رابط التقرير.");
- // Navigate directly to the report URL
window.location.href = reportUrl;
} catch (err) {
setError(err instanceof Error ? err.message : "تعذّر إنشاء التقرير.");
@@ -52,7 +43,6 @@ export function DriverReportPanel({ driverId }: DriverReportPanelProps) {
padding: "1rem",
}}
>
- {/* Heading */}
- {/* Date input */}
- {/* Generate button */}
- {/* Error */}
{error && (
void;
- onConfirm: () => void;
-}
-
-/**
- * Mirrors DriverDeleteModal.tsx 1:1 — same alertdialog role, same
- * backdrop/click-outside-to-close behavior, same Escape-to-close effect,
- * same button layout. Only the copy and the identifying fields differ.
- */
-export function OrderDeleteModal({ order, deleting, onCancel, onConfirm }: OrderDeleteModalProps) {
- useEffect(() => {
- const h = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); };
- window.addEventListener("keydown", h);
- return () => window.removeEventListener("keydown", h);
- }, [onCancel]);
-
- return (
-
{ if (e.target === e.currentTarget) onCancel(); }}
- style={{
- position: "fixed", inset: 0, zIndex: 60,
- background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
- display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem",
- }}
- >
-
e.stopPropagation()}
- style={{
- width: "100%", maxWidth: 420,
- background: "var(--color-surface)",
- borderRadius: "var(--radius-xl)",
- border: "1px solid #FECACA",
- boxShadow: "0 20px 48px rgba(0,0,0,.18)",
- padding: "2rem",
- display: "flex", flexDirection: "column", gap: "1rem",
- textAlign: "center",
- }}
- >
- {/* Icon */}
-
-
-
-
- حذف الطلب
-
-
- هل أنت متأكد من حذف الطلب{" "}
- {order.shipmentNumber}
- {" "}الخاص بـ{" "}
-
- {order.recipientName}
-
- ؟ لا يمكن التراجع عن هذا الإجراء.
-
-
-
-
-
- إلغاء
-
-
- {deleting && }
- {deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/Components/Order/OrderDetailBanel.tsx b/src/Components/Order/OrderDetailBanel.tsx
index 91c1dc1..fe4cd14 100644
--- a/src/Components/Order/OrderDetailBanel.tsx
+++ b/src/Components/Order/OrderDetailBanel.tsx
@@ -5,13 +5,7 @@ import { Spinner, Alert } from "../UI";
import { orderService } from "@/src/services/order.service";
import type { Order, OrderStatus, UpdateOrderStatusPayload } from "@/src/types/order";
-// ── Status config ────────────────────────────────────────────────────────
-// Same shape as DRIVER_STATUS_MAP in src/types/driver.ts, kept local here
-// since order.ts doesn't currently export a colour map of its own.
-const ORDER_STATUS_MAP: Record<
- OrderStatus,
- { label: string; color: string; bg: string; border: string; dot: string }
-> = {
+const ORDER_STATUS_MAP: Record
= {
Created: { label: "تم الإنشاء", color: "#1E40AF", bg: "#EFF6FF", border: "#BFDBFE", dot: "#3B82F6" },
Assigned: { label: "مُعيَّن", color: "#5B21B6", bg: "#F5F3FF", border: "#DDD6FE", dot: "#8B5CF6" },
InTransit: { label: "قيد التوصيل", color: "#854D0E", bg: "#FFFBEB", border: "#FDE68A", dot: "#D97706" },
@@ -33,7 +27,6 @@ const PAY_METHOD_LABEL: Record = {
Prepaid: "مدفوع مسبقاً",
};
-// ── Helpers — identical to DriverDetailPanel.tsx's fmtDate ──────────────
function fmtDate(iso?: string | null): string {
if (!iso) return "—";
return new Date(iso).toLocaleDateString("ar-SA", {
@@ -50,11 +43,6 @@ function fmtAmount(n?: number | string | null): string {
return `${num.toFixed(2)} ر.س`;
}
-// ── Sub-components — copied verbatim from DriverDetailPanel.tsx ─────────
-// (DetailRow / SectionHeading carry no Driver-specific logic, so they are
-// reproduced here rather than imported, matching how the Driver files keep
-// their detail-panel building blocks local to the panel that uses them.)
-
function DetailRow({
label,
value,
@@ -114,22 +102,14 @@ function SectionHeading({ title }: { title: string }) {
);
}
-// ── Props ─────────────────────────────────────────────────────────────────
-
interface OrderDetailPanelProps {
orderId: string;
onClose: () => void;
onEdit: (order: Order) => void;
onDelete: (order: Order) => void;
- /** Bubble a status change up so the list row updates without a full reload */
onStatusChanged?: (order: Order) => void;
}
-// ── Component ─────────────────────────────────────────────────────────────
-// Structurally identical to DriverDetailPanel.tsx: fixed backdrop + slide-in
-// , header with avatar-equivalent badge, scrollable body of
-// SectionHeading/DetailRow blocks, sticky footer actions.
-
export function OrderDetailPanel({
orderId,
onClose,
@@ -141,7 +121,6 @@ export function OrderDetailPanel({
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
- // Inline status-update control state
const [statusDraft, setStatusDraft] = useState("");
const [statusReason, setStatusReason] = useState("");
const [updatingStatus, setUpdatingStatus] = useState(false);
@@ -151,8 +130,7 @@ export function OrderDetailPanel({
setLoading(true);
setError(null);
try {
- const res = await orderService.getById(orderId);
- const data = (res as unknown as { data: Order }).data ?? (res as unknown as Order);
+ const data = await orderService.getById(orderId);
setOrder(data);
setStatusDraft(data.currentStatus);
} catch (err: unknown) {
@@ -170,7 +148,6 @@ export function OrderDetailPanel({
return () => window.removeEventListener("keydown", h);
}, [onClose]);
- // ── Status update — alert shown inline in the panel, success bubbled up ──
const handleStatusUpdate = useCallback(async () => {
if (!order || !statusDraft || statusDraft === order.currentStatus) return;
setUpdatingStatus(true);
@@ -178,8 +155,7 @@ export function OrderDetailPanel({
try {
const payload: UpdateOrderStatusPayload = { status: statusDraft };
if (statusReason) payload.reason = statusReason;
- const res = await orderService.updateStatus(order.id, payload);
- const updated = (res as unknown as { data: Order }).data ?? (res as unknown as Order);
+ const updated = await orderService.updateStatus(order.id, payload);
setOrder(updated);
setStatusReason("");
onStatusChanged?.(updated);
@@ -195,7 +171,6 @@ export function OrderDetailPanel({
return (
<>
- {/* Backdrop */}
- {/* Slide-in panel */}
- {/* ── Header ── */}
- {/* Shipment "avatar" badge — order has no photo, so a glyph stands in for it,
- matching the circular-badge slot Driver uses for its avatar image. */}
- {/* ── Scrollable content ── */}
{loading && (
@@ -322,7 +292,6 @@ export function OrderDetailPanel({
{order && !loading && (
<>
- {/* Status badges */}
{statusConfig && (
- {/* ── Delivery Address ── */}
{order.deliveryAddress?.details && Object.values(order.deliveryAddress.details).some(Boolean) && (
<>
@@ -394,7 +362,6 @@ export function OrderDetailPanel({
>
)}
- {/* ── Pickup Address ── */}
{order.pickupAddress?.details && Object.values(order.pickupAddress.details).some(Boolean) && (
<>
@@ -416,11 +383,6 @@ export function OrderDetailPanel({
- {/* ── Inline status update ──
- Same alert/feedback pattern as the rest of the panel: a
- local Alert renders any failure, success simply updates
- the badge above (no extra toast — the parent page's
- global toast already fires via onStatusChanged). */}
{statusError && (
setStatusError(null)} />
@@ -519,7 +481,6 @@ export function OrderDetailPanel({
)}
- {/* ── Footer actions ── */}
{order && (
void;
@@ -91,8 +86,6 @@ interface OrderFormModalProps {
) => Promise
;
}
-// ── Component ────────────────────────────────────────────────────────────────
-
export function OrderFormModal({
editOrder,
onClose,
@@ -100,12 +93,10 @@ export function OrderFormModal({
}: OrderFormModalProps) {
const isNew = editOrder === null;
- // ── Relation data — clients & trips loaded once on mount ─────────────────
const [clients, setClients] = useState([]);
const [trips, setTrips] = useState([]);
const [relLoading, setRelLoading] = useState(true);
- // ── Form state ────────────────────────────────────────────────────────────
const [shipmentNumber, setShipmentNumber] = useState(
editOrder?.shipmentNumber ?? "",
);
@@ -135,8 +126,6 @@ export function OrderFormModal({
editOrder?.paymentStatus ?? "",
);
- // ── Address state ─────────────────────────────────────────────────────────
- // Delivery address (عنوان التسليم) — always creates new MongoDB doc
const [delCity, setDelCity] = useState(
editOrder?.deliveryAddress?.details?.city ?? "",
);
@@ -155,7 +144,6 @@ export function OrderFormModal({
const [delZipCode, setDelZipCode] = useState(
editOrder?.deliveryAddress?.details?.zipCode ?? "",
);
- // GeoJSON coordinates [longitude, latitude] — required by backend
const [delLng, setDelLng] = useState(
editOrder?.deliveryAddress?.location?.coordinates?.[0] != null
? String(editOrder.deliveryAddress!.location!.coordinates![0])
@@ -167,7 +155,6 @@ export function OrderFormModal({
: "",
);
- // Pickup address (عنوان الاستلام) — إما ID موجود أو object جديد
const [pickupMode, setPickupMode] = useState<"id" | "new">(
editOrder?.pickupAddressId ? "id" : "new",
);
@@ -192,7 +179,6 @@ export function OrderFormModal({
const [pkpZipCode, setPkpZipCode] = useState(
editOrder?.pickupAddress?.details?.zipCode ?? "",
);
- // GeoJSON coordinates [longitude, latitude] — optional for pickup
const [pkpLng, setPkpLng] = useState(
editOrder?.pickupAddress?.location?.coordinates?.[0] != null
? String(editOrder.pickupAddress!.location!.coordinates![0])
@@ -209,36 +195,20 @@ export function OrderFormModal({
const [apiError, setApiError] = useState("");
const firstRef = useRef(null);
- // ── Load clients + trips in parallel on mount ─────────────────────────────
useEffect(() => {
let cancelled = false;
(async () => {
try {
const token = getStoredToken();
- // Fetch first 100 clients (enough for a practical dropdown)
- const clientRes = await clientService.getAll(1, "", token);
- const clientPayload = (clientRes as unknown as { data?: { data?: Client[] } }).data ?? clientRes;
- const clientList: Client[] = (clientPayload as { data?: Client[] }).data ?? [];
-
- // Fetch first 100 trips (active ones the order can be assigned to)
- const tripRes = await tripService.getAll(
- { page: 1, limit: 100 },
- token,
- );
- const tripPayload = (tripRes as unknown as { data?: { data?: Trip[] } }).data ?? tripRes;
- const tripList: Trip[] = (tripPayload as { data?: Trip[] }).data ?? [];
+ const { items: clientList } = await clientService.getAll(1, "", token);
+ const { items: tripList } = await tripService.getAll({ page: 1, limit: 100 }, token);
if (!cancelled) {
setClients(clientList);
setTrips(tripList);
}
} catch (err) {
- // Logged so failures are visible during development instead of
- // failing completely silently when clientService/tripService error out
- // (e.g. expired token, CORS, 500). Dropdowns stay empty either way,
- // and the user can still navigate to the create pages via the helper
- // links — but now they also see a message explaining why.
console.error("OrderFormModal: failed to load clients/trips", err);
if (!cancelled) {
setApiError(
@@ -254,7 +224,6 @@ export function OrderFormModal({
};
}, []);
- // ── Keyboard / focus setup ────────────────────────────────────────────────
useEffect(() => {
firstRef.current?.focus();
}, []);
@@ -266,7 +235,6 @@ export function OrderFormModal({
return () => window.removeEventListener("keydown", h);
}, [onClose]);
- // ── Dynamic input error style ─────────────────────────────────────────────
const inputStyle = (field: keyof OrderSchemaErrors): React.CSSProperties => ({
...inputBase,
border: errors[field] ? "1px solid var(--color-danger)" : inputBase.border,
@@ -279,7 +247,6 @@ export function OrderFormModal({
[],
);
- // ── Yup validation ────────────────────────────────────────────────────────
const runValidation = useCallback(async (): Promise => {
const schema = isNew ? createOrderSchema : updateOrderSchema;
try {
@@ -326,7 +293,6 @@ export function OrderFormModal({
type,
]);
- // ── Submit ────────────────────────────────────────────────────────────────
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
@@ -334,10 +300,6 @@ export function OrderFormModal({
const valid = await runValidation();
if (!valid) return;
- // Build clean payload.
- // NOTE: deliveryAddressId / pickupAddressId are intentionally omitted
- // from create payloads — the backend creates address records from
- // deliveryAddressData / pickupAddressData instead.
const payload: Record = {
shipmentNumber,
recipientName,
@@ -346,10 +308,8 @@ export function OrderFormModal({
quantity: Number(quantity) || 1,
};
- // tripId: only include when a value is selected
if (tripId) payload.tripId = tripId;
- // Optional fields — include only when filled
if (type) payload.type = type;
if (weight) payload.weight = Number(weight);
if (subTotal) payload.subTotal = Number(subTotal);
@@ -357,7 +317,6 @@ export function OrderFormModal({
if (paymentMethod) payload.paymentMethod = paymentMethod;
if (paymentStatus) payload.paymentStatus = paymentStatus;
- // ── Build delivery address object (only if at least one field filled) ──
const delDetails = {
...(delCity && { city: delCity }),
...(delDistrict && { district: delDistrict }),
@@ -366,7 +325,6 @@ export function OrderFormModal({
...(delUnitNo && { unitNo: delUnitNo }),
...(delZipCode && { zipCode: delZipCode }),
};
- // coordinates are always included when provided (required by backend GeoJSON schema)
const delCoordinates =
delLng.trim() && delLat.trim()
? [Number(delLng), Number(delLat)]
@@ -379,7 +337,6 @@ export function OrderFormModal({
};
}
- // ── Build pickup address: ID or new object ──
if (pickupMode === "id" && pickupAddressId.trim()) {
payload.pickupAddressId = pickupAddressId.trim();
} else {
@@ -428,7 +385,6 @@ export function OrderFormModal({
}
};
- // ── Render ────────────────────────────────────────────────────────────────
return (
- {/* ── Header ── */}
- {/* ── Form ── */}
)}
- {/* ── Section: Shipment & Recipient ── */}
بيانات الشحنة والمستلم
- {/* Shipment number — required */}
رقم الشحنة *
- {/* ── Client dropdown — required ── */}
العميل *
{errors.clientId}
)}
- {/* Helper link */}
- {/* Recipient name — required */}
اسم المستلم *
- {/* Recipient phone — required */}
رقم جوال المستلم *
- {/* ── Trip dropdown — required on create, optional on edit ── */}
{isNew ? "الرحلة *" : "الرحلة"}
{!isNew && (اختياري) }
@@ -693,7 +639,6 @@ export function OrderFormModal({
{errors.tripId && (
{errors.tripId}
)}
- {/* Helper link */}
- {/* ── Section: Shipment details ── */}
تفاصيل الشحنة
- {/* Type — optional */}
نوع الشحنة
(اختياري)
@@ -738,7 +681,6 @@ export function OrderFormModal({
/>
- {/* Quantity — required, defaults to 1 */}
الكمية *
- {/* Weight — optional */}
الوزن (كجم)
(اختياري)
@@ -773,7 +714,6 @@ export function OrderFormModal({
- {/* ── Section: Payment ── */}
بيانات الدفع
- {/* Subtotal — optional */}
الإجمالي الفرعي
(اختياري)
@@ -804,7 +743,6 @@ export function OrderFormModal({
)}
- {/* VAT rate — optional */}
نسبة الضريبة (%)
(اختياري)
@@ -825,7 +763,6 @@ export function OrderFormModal({
)}
- {/* Payment method — optional */}
طريقة الدفع
(اختياري)
@@ -844,7 +781,6 @@ export function OrderFormModal({
- {/* Payment status — edit-only, mirrors Driver's status-on-edit pattern */}
{!isNew && (
حالة الدفع
@@ -866,7 +802,6 @@ export function OrderFormModal({
)}
- {/* ── Section: Delivery Address ── */}
عنوان التسليم
- {/* GeoJSON coordinates — required by backend */}
خط الطول (Longitude) *
- {/* ── Section: Pickup Address ── */}
عنوان الاستلام
- {/* Toggle: existing ID or new address */}
@@ -1090,7 +1022,6 @@ export function OrderFormModal({
dir="ltr"
/>
- {/* GeoJSON coordinates — optional for pickup */}
خط الطول (Longitude)
(اختياري)
@@ -1120,7 +1051,6 @@ export function OrderFormModal({
)}
- {/* ── Actions ── */}
);
-}
+}
\ No newline at end of file
diff --git a/src/Components/Order/index.ts b/src/Components/Order/index.ts
new file mode 100644
index 0000000..8c09db9c
--- /dev/null
+++ b/src/Components/Order/index.ts
@@ -0,0 +1,2 @@
+export {OrderFormModal} from './OrderFormModal';
+export {OrderDetailPanel} from './OrderDetailBanel';
\ No newline at end of file
diff --git a/src/Components/Trip/Tripdeletemodal.tsx b/src/Components/Trip/Tripdeletemodal.tsx
deleted file mode 100644
index 193a5e9..0000000
--- a/src/Components/Trip/Tripdeletemodal.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { Spinner } from "../UI";
-import type { Trip } from "@/src/types/trip";
-
-interface TripDeleteModalProps {
- trip: Trip;
- deleting: boolean;
- onCancel: () => void;
- onConfirm: () => void;
-}
-
-export function TripDeleteModal({ trip, deleting, onCancel, onConfirm }: TripDeleteModalProps) {
- useEffect(() => {
- const h = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); };
- window.addEventListener("keydown", h);
- return () => window.removeEventListener("keydown", h);
- }, [onCancel]);
-
- return (
- { if (e.target === e.currentTarget) onCancel(); }}
- style={{
- position: "fixed", inset: 0, zIndex: 60,
- background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
- display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem",
- }}
- >
-
e.stopPropagation()}
- style={{
- width: "100%", maxWidth: 420,
- background: "var(--color-surface)",
- borderRadius: "var(--radius-xl)",
- border: "1px solid #FECACA",
- boxShadow: "0 20px 48px rgba(0,0,0,.18)",
- padding: "2rem",
- display: "flex", flexDirection: "column", gap: "1rem",
- textAlign: "center",
- }}
- >
- {/* Icon */}
-
-
-
-
- حذف الرحلة
-
-
- هل أنت متأكد من حذف رحلة{" "}
- {trip.title}
- {" "}(
-
- {trip.tripNumber}
-
- )؟ لا يمكن التراجع عن هذا الإجراء.
-
-
-
-
-
- إلغاء
-
-
- {deleting && }
- {deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/Components/Trip/Tripformmodal.tsx b/src/Components/Trip/Tripformmodal.tsx
index da8c813..2c610a2 100644
--- a/src/Components/Trip/Tripformmodal.tsx
+++ b/src/Components/Trip/Tripformmodal.tsx
@@ -3,7 +3,6 @@
import { useEffect, useRef, useState } from "react";
import * as yup from "yup";
import { Spinner } from "../UI";
-import { get } from "@/src/services/api";
import { getStoredToken } from "@/src/lib/auth";
import {
createTripSchema,
@@ -16,6 +15,12 @@ import type {
CreateTripPayload,
UpdateTripPayload,
} from "@/src/types/trip";
+import { carService, driverService } from "@/src/services";
+import { branchService } from "@/src/services/branch.service";
+import type { DriverOption } from "@/src/types/driver";
+import type { CarOption } from "@/src/types/car";
+import type { BranchOption } from "@/src/types/branch";
+
// ── Shared styles ────────────────────────────────────────────────────────────
@@ -63,25 +68,9 @@ const optionalLabelStyle: React.CSSProperties = {
marginRight: 4,
};
-// ── Driver / Car / Branch minimal types ─────────────────────────────────────
-interface DriverOption {
- id: string;
- name: string;
- phone: string;
- status: string;
-}
-interface CarOption {
- id: string;
- manufacturer: string;
- model: string;
- plateNumber: string;
- status?: string;
-}
-interface BranchOption {
- id: string;
- name: string;
-}
+
+
// ── Props ────────────────────────────────────────────────────────────────────
@@ -108,35 +97,12 @@ export function TripFormModal({
const [cars, setCars] = useState([]);
const [branches, setBranches] = useState([]);
- useEffect(() => {
- const token = getStoredToken();
- get<{ data: { data: DriverOption[] } }>(
- "driver?limit=100&status=Active",
- token,
- )
- .then((res) =>
- setDrivers(
- (res as unknown as { data: { data: DriverOption[] } }).data?.data ??
- [],
- ),
- )
- .catch(() => {});
- get<{ data: { data: CarOption[] } }>("cars?limit=100¤tStatus=Active", token)
- .then((res) =>
- setCars(
- (res as unknown as { data: { data: CarOption[] } }).data?.data ?? [],
- ),
- )
- .catch(() => {});
- get<{ data: { data: BranchOption[] } }>("branches?limit=100", token)
- .then((res) =>
- setBranches(
- (res as unknown as { data: { data: BranchOption[] } }).data?.data ??
- [],
- ),
- )
- .catch(() => {});
- }, []);
+ useEffect(() => {
+ const token = getStoredToken();
+ driverService.getActiveOptions(token).then(setDrivers).catch(() => {});
+ carService.getActiveOptions(token).then(setCars).catch(() => {});
+ branchService.getOptions(token).then(setBranches).catch(() => {});
+}, []);
//console.log("cars", cars);
// ── Form state ────────────────────────────────────────────────────────────
const [title, setTitle] = useState(editTrip?.title ?? "");
diff --git a/src/Components/Trip_Report/Tripreportpanel.tsx b/src/Components/Trip_Report/Tripreportpanel.tsx
index 8f4bc0e..05f6240 100644
--- a/src/Components/Trip_Report/Tripreportpanel.tsx
+++ b/src/Components/Trip_Report/Tripreportpanel.tsx
@@ -1,8 +1,7 @@
"use client";
import { useRef, useState } from "react";
-import { Spinner } from "../UI";
-import { Toast } from "../UI/Toast";
+import { Spinner ,Toast} from "../UI";
import { tripService } from "@/src/services/trip.service";
import { getStoredToken } from "@/src/lib/auth";
import type { TripNotification } from "@/src/hooks/useTrip";
@@ -11,7 +10,6 @@ interface TripReportPanelProps {
tripId: string;
}
-// Extracts a readable message from any error shape the API might return.
function extractApiMessage(err: unknown, fallback: string): string {
if (err && typeof err === "object") {
const e = err as Record;
@@ -40,15 +38,14 @@ export function TripReportPanel({ tripId }: TripReportPanelProps) {
setLoading(true);
try {
const token = getStoredToken();
- const res = await tripService.getReport(tripId, token);
- const url = (res as unknown as { data: { reportUrl: string } }).data?.reportUrl;
+ const { reportUrl } = await tripService.getReport(tripId, token);
- if (!url) {
+ if (!reportUrl) {
notify({ type: "error", message: "لم يتم إرجاع رابط التقرير من الخادم." });
return;
}
- window.open(url, "_blank", "noopener,noreferrer");
+ window.open(reportUrl, "_blank", "noopener,noreferrer");
notify({ type: "success", message: "تم إنشاء التقرير بنجاح." });
} catch (err) {
notify({ type: "error", message: extractApiMessage(err, "تعذّر إنشاء التقرير.") });
@@ -107,7 +104,6 @@ export function TripReportPanel({ tripId }: TripReportPanelProps) {
- {/* Toast scoped to this panel — sits at the bottom of the screen */}
{
diff --git a/src/Components/User/DeleteConfirmModal.tsx b/src/Components/User/DeleteConfirmModal.tsx
deleted file mode 100644
index c4b014d..0000000
--- a/src/Components/User/DeleteConfirmModal.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { Spinner } from "../UI";
-import type { User } from "@/src/types/user";
-
-interface DeleteConfirmModalProps {
- user: User;
- deleting: boolean;
- onCancel: () => void;
- onConfirm: () => void;
-}
-
-export function DeleteConfirmModal({ user, deleting, onCancel, onConfirm }: DeleteConfirmModalProps) {
- useEffect(() => {
- const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); };
- window.addEventListener("keydown", handler);
- return () => window.removeEventListener("keydown", handler);
- }, [onCancel]);
-
- return (
- { if (e.target === e.currentTarget) onCancel(); }}
- style={{ position: "fixed", inset: 0, zIndex: 60, background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)", display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem" }}
- >
-
e.stopPropagation()}
- style={{ width: "100%", maxWidth: 400, background: "var(--color-surface)", borderRadius: "var(--radius-xl)", border: "1px solid #FECACA", boxShadow: "0 20px 48px rgba(0,0,0,.18)", padding: "2rem", display: "flex", flexDirection: "column", gap: "1rem", textAlign: "center" }}
- >
- {/* icon delete */}
-
-
-
حذف المستخدم
-
- هل أنت متأكد من حذف {user.name} ؟ لا يمكن التراجع عن هذا الإجراء.
-
-
-
-
- إلغاء
-
-
- {deleting && }
- {deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/Components/User/UserDetailModal.tsx b/src/Components/User/UserDetailModal.tsx
index 3bee682..0f262d1 100644
--- a/src/Components/User/UserDetailModal.tsx
+++ b/src/Components/User/UserDetailModal.tsx
@@ -77,20 +77,20 @@ export function UserDetailModal({ userId, onClose }: UserDetailModalProps) {
// fetch user details on mount
useEffect(() => {
- let cancelled = false;
- (async () => {
- try {
- const token = getStoredToken();
- const res = await userService.getById(userId, token);
- if (!cancelled) setUser(res.data);
- } catch {
- if (!cancelled) setError("تعذّر تحميل بيانات المستخدم. يرجى المحاولة لاحقاً.");
- } finally {
- if (!cancelled) setLoading(false);
- }
- })();
- return () => { cancelled = true; };
- }, [userId]);
+ let cancelled = false;
+ (async () => {
+ try {
+ const token = getStoredToken();
+ const data = await userService.getById(userId, token);
+ if (!cancelled) setUser(data);
+ } catch {
+ if (!cancelled) setError("تعذّر تحميل بيانات المستخدم. يرجى المحاولة لاحقاً.");
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => { cancelled = true; };
+}, [userId]);
// ── helpers ───────────────────────────────────────────────────────────────
const fmt = (iso?: string | null) =>
diff --git a/src/Components/User/archive/Archiveduserdetailmodal.tsx b/src/Components/User/archive/Archiveduserdetailmodal.tsx
index 0bd9de1..c73535d 100644
--- a/src/Components/User/archive/Archiveduserdetailmodal.tsx
+++ b/src/Components/User/archive/Archiveduserdetailmodal.tsx
@@ -76,21 +76,21 @@ export function ArchivedUserDetailModal({ userId, onClose }: ArchivedUserDetailM
}, [onClose]);
// fetch archived user details on mount
- useEffect(() => {
- let cancelled = false;
- (async () => {
- try {
- const token = getStoredToken();
- const res = await archivedUserService.getById(userId, token);
- if (!cancelled) setUser(res.data);
- } catch {
- if (!cancelled) setError("تعذّر تحميل بيانات المستخدم المؤرشف. يرجى المحاولة لاحقاً.");
- } finally {
- if (!cancelled) setLoading(false);
- }
- })();
- return () => { cancelled = true; };
- }, [userId]);
+useEffect(() => {
+ let cancelled = false;
+ (async () => {
+ try {
+ const token = getStoredToken();
+ const data = await archivedUserService.getById(userId, token);
+ if (!cancelled) setUser(data);
+ } catch {
+ if (!cancelled) setError("تعذّر تحميل بيانات المستخدم المؤرشف. يرجى المحاولة لاحقاً.");
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => { cancelled = true; };
+}, [userId]);
// ── helpers ───────────────────────────────────────────────────────────────
const fmt = (iso?: string | null) =>
diff --git a/src/Components/car/CarDeleteModal.tsx b/src/Components/car/CarDeleteModal.tsx
deleted file mode 100644
index 8f7e475..0000000
--- a/src/Components/car/CarDeleteModal.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-"use client";
-
-
-import { useEffect } from "react";
-import { Spinner } from "../UI";
-import type { Car } from "@/src/types/car";
-
-interface CarDeleteModalProps {
- car: Car;
- deleting: boolean;
- onCancel: () => void;
- onConfirm: () => void;
-}
-
-export function CarDeleteModal({ car, deleting, onCancel, onConfirm }: CarDeleteModalProps) {
- useEffect(() => {
- const h = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); };
- window.addEventListener("keydown", h);
- return () => window.removeEventListener("keydown", h);
- }, [onCancel]);
-
- return (
- { if (e.target === e.currentTarget) onCancel(); }}
- style={{
- position: "fixed", inset: 0, zIndex: 60,
- background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
- display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem",
- }}
- >
-
e.stopPropagation()}
- style={{
- width: "100%", maxWidth: 420,
- background: "var(--color-surface)",
- borderRadius: "var(--radius-xl)",
- border: "1px solid #FECACA",
- boxShadow: "0 20px 48px rgba(0,0,0,.18)",
- padding: "2rem",
- display: "flex", flexDirection: "column", gap: "1rem",
- textAlign: "center",
- }}
- >
- {/* Icon */}
-
-
-
-
- حذف المركبة
-
-
- هل أنت متأكد من حذف{" "}
-
- {car.manufacturer} {car.model}
-
- {" "}(
-
- {car.plateLetters} {car.plateNumber}
-
- )؟ لا يمكن التراجع عن هذا الإجراء.
-
-
-
-
-
- إلغاء
-
-
- {deleting && }
- {deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/Components/car/CarFormModal.tsx b/src/Components/car/CarFormModal.tsx
index 19c123a..ef4bfed 100644
--- a/src/Components/car/CarFormModal.tsx
+++ b/src/Components/car/CarFormModal.tsx
@@ -3,9 +3,11 @@
import { useCallback, useEffect, useRef, useState } from "react";
import * as yup from "yup";
import { Alert, Spinner } from "../UI";
-import { get } from "@/src/services/api";
import { getStoredToken } from "@/src/lib/auth";
-import { createCarSchema, updateCarSchema } from "@/src/validations/car.validator";
+import {
+ createCarSchema,
+ updateCarSchema,
+} from "@/src/validations/car.validator";
import type {
Car,
CarFormErrors,
@@ -14,6 +16,8 @@ import type {
UpdateCarPayload,
} from "@/src/types/car";
import type { Branch } from "@/src/types/branch";
+import { branchService } from "@/src/services/branch.service";
+
// ── Shared input style ────────────────────────────────────────────────────────
@@ -100,23 +104,22 @@ export function CarFormModal({
const isNew = editCar === null;
// ── 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();
- get<{ data: { data: Branch[] } }>("branches?limit=100", token)
- .then((res) => {
- const list =
- (res as unknown as { data: { data: Branch[] } }).data?.data ?? [];
- queueMicrotask(() => setBranches(list));
- })
- .catch(() => {
- /* silently ignore */
- });
- }, [branchesProp]);
+ 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]);
useEffect(() => {
loadBranches();
@@ -787,4 +790,4 @@ export function CarFormModal({
);
-}
\ No newline at end of file
+}
diff --git a/src/Components/role/DeleteRoleModal.tsx b/src/Components/role/DeleteRoleModal.tsx
deleted file mode 100644
index 322b8cd..0000000
--- a/src/Components/role/DeleteRoleModal.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-"use client";
-
-import { useEffect } from "react";
-import { Spinner } from "../UI";
-import { Role } from "@/src/types/role";
-
-interface DeleteRoleModalProps {
- role: Role;
- deleting: boolean;
- onCancel: () => void;
- onConfirm: () => void;
-}
-
-export function DeleteRoleModal({ role, deleting, onCancel, onConfirm }: DeleteRoleModalProps) {
- useEffect(() => {
- const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onCancel(); };
- window.addEventListener("keydown", handler);
- return () => window.removeEventListener("keydown", handler);
- }, [onCancel]);
-
- return (
-
{ if (e.target === e.currentTarget) onCancel(); }}
- style={{
- position: "fixed", inset: 0, zIndex: 60,
- background: "rgba(15,23,42,0.55)", backdropFilter: "blur(4px)",
- display: "flex", alignItems: "center", justifyContent: "center", padding: "1rem",
- }}
- >
-
e.stopPropagation()} style={{
- width: "100%", maxWidth: 400,
- background: "var(--color-surface)",
- borderRadius: "var(--radius-xl)",
- border: "1px solid #FECACA",
- boxShadow: "0 20px 48px rgba(0,0,0,.18)",
- padding: "2rem",
- display: "flex", flexDirection: "column", gap: "1rem", textAlign: "center",
- }}>
- {/* Icon */}
-
-
-
-
- حذف الدور
-
-
- هل أنت متأكد من حذف دور {role.name} ؟
- لا يمكن التراجع عن هذا الإجراء.
-
-
-
-
-
- إلغاء
-
-
- {deleting && }
- {deleting ? "جارٍ الحذف…" : "تأكيد الحذف"}
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/src/Components/role/RoleDetailModal.tsx b/src/Components/role/RoleDetailModal.tsx
index e5e9cca..a3092a1 100644
--- a/src/Components/role/RoleDetailModal.tsx
+++ b/src/Components/role/RoleDetailModal.tsx
@@ -8,7 +8,7 @@ import { Permission, Role } from "@/src/types/role";
interface RoleDetailModalProps {
roleId: string;
- permissions: Permission[]; // full catalog, for the "add permission" select
+ permissions: Permission[];
onClose: () => void;
onAssign: (roleId: string, permissionId: string) => Promise
;
onRemove: (roleId: string, permissionId: string) => Promise;
@@ -51,7 +51,6 @@ export function RoleDetailModal({ roleId, permissions, onClose, onAssign, onRemo
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
- // ── Inline permission mutation state ─────────────────────────────────────────
const [pendingPermId, setPendingPermId] = useState("");
const [mutating, setMutating] = useState(false);
@@ -64,8 +63,8 @@ export function RoleDetailModal({ roleId, permissions, onClose, onAssign, onRemo
const loadRole = async () => {
try {
const token = getStoredToken();
- const res = await roleService.getById(roleId, token);
- setRole((res as unknown as { data: Role }).data);
+ const data = await roleService.getById(roleId, token);
+ setRole(data);
} catch {
setError("تعذّر تحميل بيانات الدور. يرجى المحاولة لاحقاً.");
} finally {
@@ -78,8 +77,8 @@ export function RoleDetailModal({ roleId, permissions, onClose, onAssign, onRemo
(async () => {
try {
const token = getStoredToken();
- const res = await roleService.getById(roleId, token);
- if (!cancelled) setRole((res as unknown as { data: Role }).data);
+ const data = await roleService.getById(roleId, token);
+ if (!cancelled) setRole(data);
} catch {
if (!cancelled) setError("تعذّر تحميل بيانات الدور. يرجى المحاولة لاحقاً.");
} finally {
@@ -89,7 +88,6 @@ export function RoleDetailModal({ roleId, permissions, onClose, onAssign, onRemo
return () => { cancelled = true; };
}, [roleId]);
- // Endpoint 1: POST /role/{id}/permissions
const handleAssign = async () => {
if (!pendingPermId || mutating) return;
setMutating(true);
@@ -101,7 +99,6 @@ export function RoleDetailModal({ roleId, permissions, onClose, onAssign, onRemo
setMutating(false);
};
- // Endpoint 3: DELETE /role/{id}/permissions/{permissionId}
const handleRemove = async (permissionId: string) => {
if (mutating) return;
setMutating(true);
@@ -135,7 +132,6 @@ export function RoleDetailModal({ roleId, permissions, onClose, onAssign, onRemo
boxShadow: "0 24px 64px rgba(0,0,0,.18)",
overflow: "hidden", display: "flex", flexDirection: "column",
}}>
- {/* Header */}
- {/* Body */}
{loading && (
@@ -173,7 +168,6 @@ export function RoleDetailModal({ roleId, permissions, onClose, onAssign, onRemo
{!loading && role && (
- {/* Role icon + name + status */}
{role.updatedAt &&
}
- {/* Permissions list */}
الصلاحيات ({role.permissions?.length ?? 0})
- {/* Assign new permission — Endpoint 1 */}
{assignablePermissions.length > 0 && (
{permission.name}
- {/* Endpoint 3: remove this permission */}
handleRemove(permission.id)} disabled={mutating}
aria-label={`إزالة ${permission.name}`}
@@ -273,7 +264,6 @@ export function RoleDetailModal({ roleId, permissions, onClose, onAssign, onRemo
)}
- {/* Footer */}
{
- let cancelled = false;
- (async () => {
- try {
- const token = getStoredToken();
- const res = await archivedRoleService.getById(roleId, token);
- if (!cancelled) setRole(res.data);
- } catch {
- if (!cancelled) setError("تعذّر تحميل بيانات الدور المؤرشف. يرجى المحاولة لاحقاً.");
- } finally {
- if (!cancelled) setLoading(false);
- }
- })();
- return () => { cancelled = true; };
- }, [roleId]);
+ let cancelled = false;
+ (async () => {
+ try {
+ const token = getStoredToken();
+ const data = await archivedRoleService.getByIdUnwrapped(roleId, token);
+ if (!cancelled) setRole(data);
+ } catch {
+ if (!cancelled) setError("تعذّر تحميل بيانات الدور المؤرشف. يرجى المحاولة لاحقاً.");
+ } finally {
+ if (!cancelled) setLoading(false);
+ }
+ })();
+ return () => { cancelled = true; };
+}, [roleId]);
// ── helpers ───────────────────────────────────────────────────────────────
const fmt = (iso?: string | null) =>
diff --git a/src/hooks/UseCarsMaintanance.ts b/src/hooks/UseCarsMaintanance.ts
index 464523f..c524ed4 100644
--- a/src/hooks/UseCarsMaintanance.ts
+++ b/src/hooks/UseCarsMaintanance.ts
@@ -33,20 +33,19 @@ export function useCarMaintenanceList(
const [error, setError] = useState
(null);
const loadRecords = useCallback(() => {
- if (!carId) return;
- const token = getStoredToken();
- setLoading(true);
- setError(null);
- carMaintenanceService
- .getAll(carId, token)
- .then((res) => {
- const list = (res as unknown as { data: CarMaintenance[] }).data ?? [];
- list.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
- setAllRecords(list);
- })
- .catch((err: Error) => setError(err.message))
- .finally(() => setLoading(false));
- }, [carId]);
+ if (!carId) return;
+ const token = getStoredToken();
+ setLoading(true);
+ setError(null);
+ carMaintenanceService
+ .getAllUnwrapped(carId, token)
+ .then((list) => {
+ list.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
+ setAllRecords(list);
+ })
+ .catch((err: Error) => setError(err.message))
+ .finally(() => setLoading(false));
+}, [carId]);
useEffect(() => { queueMicrotask(loadRecords); }, [loadRecords]);
diff --git a/src/hooks/archive/Usearchivedcars.ts b/src/hooks/archive/Usearchivedcars.ts
index 547906a..d2dc580 100644
--- a/src/hooks/archive/Usearchivedcars.ts
+++ b/src/hooks/archive/Usearchivedcars.ts
@@ -21,8 +21,7 @@ export function useArchivedCars() {
carService
.getArchived(token)
.then((res) => {
- const payload = (res as unknown as { data: { data: Car[] } }).data ?? res;
- setAllCars((payload as { data: Car[] }).data ?? []);
+ setAllCars(res ?? []);
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
diff --git a/src/hooks/archive/useArchiveClientAdresses.ts b/src/hooks/archive/useArchiveClientAdresses.ts
index 3437ee8..9b24cdc 100644
--- a/src/hooks/archive/useArchiveClientAdresses.ts
+++ b/src/hooks/archive/useArchiveClientAdresses.ts
@@ -18,19 +18,18 @@ export function useArchivedClientAddresses(clientId?: string) {
const [error, setError] = useState(null);
const [search, setSearch] = useState("");
- const load = useCallback(async () => {
- setLoading(true);
- try {
- const token = getStoredToken();
- const res = await archivedClientAddressService.getAll(token);
- setAddresses(res.data);
- setError(null);
- } catch {
- setError("تعذّر تحميل أرشيف العناوين. يرجى المحاولة لاحقاً.");
- } finally {
- setLoading(false);
- }
- }, []);
+const load = useCallback(async () => {
+ setLoading(true);
+ try {
+ const token = getStoredToken();
+ setAddresses(await archivedClientAddressService.getAllUnwrapped(token));
+ setError(null);
+ } catch {
+ setError("تعذّر تحميل أرشيف العناوين. يرجى المحاولة لاحقاً.");
+ } finally {
+ setLoading(false);
+ }
+}, []);
useEffect(() => { queueMicrotask(load); }, [load]);
diff --git a/src/hooks/archive/useArchiveRole.ts b/src/hooks/archive/useArchiveRole.ts
index 4787e46..1daafb0 100644
--- a/src/hooks/archive/useArchiveRole.ts
+++ b/src/hooks/archive/useArchiveRole.ts
@@ -5,24 +5,7 @@ import type { ArchivedRole } from "@/src/types/role";
const PAGE_SIZE = 10;
-// ── normalize whatever shape the API actually returns into a flat array ──────
-// Handles both a flat `data: ArchivedRole[]` (per the documented endpoint)
-// and, defensively, a nested `data: { data: ArchivedRole[] }` shape in case
-// the backend later adds pagination like the users/branches archive endpoints.
-function extractList(res: unknown): ArchivedRole[] {
- const body = res as { data?: unknown } | null | undefined;
- const inner = body?.data;
- if (Array.isArray(inner)) return inner;
- if (inner && typeof inner === "object" && Array.isArray((inner as { data?: unknown }).data)) {
- return (inner as { data: ArchivedRole[] }).data;
- }
- if (Array.isArray(res)) return res as ArchivedRole[];
-
- // Unexpected shape — log for debugging instead of crashing the UI.
- console.error("useArchivedRoles: unexpected /role/archived response shape", res);
- return [];
-}
export function useArchivedRoles() {
// full unfiltered list as returned by the API
@@ -33,19 +16,18 @@ export function useArchivedRoles() {
const [error, setError] = useState(null);
const load = useCallback(async () => {
- setLoading(true);
- try {
- const token = getStoredToken();
- const res = await archivedRoleService.getAll(token);
- setAllRoles(extractList(res));
- setError(null);
- } catch {
- setError("تعذّر تحميل قائمة الأدوار المؤرشفة. يرجى المحاولة لاحقاً.");
- setAllRoles([]);
- } finally {
- setLoading(false);
- }
- }, []);
+ setLoading(true);
+ try {
+ const token = getStoredToken();
+ setAllRoles(await archivedRoleService.getAllUnwrapped(token));
+ setError(null);
+ } catch {
+ setError("تعذّر تحميل قائمة الأدوار المؤرشفة. يرجى المحاولة لاحقاً.");
+ setAllRoles([]);
+ } finally {
+ setLoading(false);
+ }
+}, []);
useEffect(() => { queueMicrotask(load); }, [load]);
diff --git a/src/hooks/archive/useArchivedOrders.ts b/src/hooks/archive/useArchivedOrders.ts
index 276da09..09473db 100644
--- a/src/hooks/archive/useArchivedOrders.ts
+++ b/src/hooks/archive/useArchivedOrders.ts
@@ -26,7 +26,7 @@ function reducer(s: TableState, a: TableAction): TableState {
case "LOAD_START":
return { ...s, loading: true, error: null };
case "LOAD_OK":
- return { ...s, loading: false, orders: a.orders };
+ return { ...s, loading: false, orders: Array.isArray(a.orders) ? a.orders : [] };
case "LOAD_ERR":
return { ...s, loading: false, error: a.error };
case "CLEAR_ERR":
@@ -50,27 +50,15 @@ export function useArchivedOrders() {
// ── Fetch archived orders ─────────────────────────────────────────────
const load = useCallback(async () => {
- dispatch({ type: "LOAD_START" });
- try {
- const token = getStoredToken();
- const res = await archivedOrderService.getAll(token);
-
- // نفس الـ pattern بتاع useOrder.ts:
- // get() بترجع axios response كامل، فـ res.data هو الـ JSON body
- // {success, message, responseAt, data}. الـ "?? res" احتياطي لو
- // الـ interceptor بتاع axios بيرجّع الـ body مباشرة من غير .data
- const payload =
- (res as unknown as { data: { data: ArchivedOrder[] } }).data ?? res;
-
- dispatch({ type: "LOAD_OK", orders: payload.data ?? [] });
- } catch {
- dispatch({
- type: "LOAD_ERR",
- error: "تعذّر تحميل الطلبات المؤرشفة. يرجى المحاولة لاحقاً.",
- });
- }
- }, []);
-
+ dispatch({ type: "LOAD_START" });
+ try {
+ const token = getStoredToken();
+ const orders = await archivedOrderService.getAllUnwrapped(token);
+ dispatch({ type: "LOAD_OK", orders });
+ } catch {
+ dispatch({ type: "LOAD_ERR", error: "تعذّر تحميل الطلبات المؤرشفة. يرجى المحاولة لاحقاً." });
+ }
+}, []);
useEffect(() => {
load();
}, [load]);
diff --git a/src/hooks/archive/useArchivedUsers.ts b/src/hooks/archive/useArchivedUsers.ts
index 39c48f8..3c9ccbb 100644
--- a/src/hooks/archive/useArchivedUsers.ts
+++ b/src/hooks/archive/useArchivedUsers.ts
@@ -12,21 +12,19 @@ export function useArchivedUsers() {
const [search, setSearch] = useState("");
const [error, setError] = useState(null);
- const load = useCallback(async () => {
- setLoading(true);
- try {
- const token = getStoredToken();
- const res = await archivedUserService.getAll(page, search, token);
- setUsers(res.data.data);
- setTotal(res.data.meta.total);
- setPages(res.data.meta.totalPages);
- setError(null);
- } catch {
- setError("تعذّر تحميل قائمة الأرشيف. يرجى المحاولة لاحقاً.");
- } finally {
- setLoading(false);
- }
- }, [page, search]);
+ const load = useCallback(async () => {
+ setLoading(true);
+ try {
+ const token = getStoredToken();
+ const { items, total, pages } = await archivedUserService.getAllUnwrapped(page, search, token);
+ setUsers(items); setTotal(total); setPages(pages);
+ setError(null);
+ } catch {
+ setError("تعذّر تحميل قائمة الأرشيف. يرجى المحاولة لاحقاً.");
+ } finally {
+ setLoading(false);
+ }
+}, [page, search]);
useEffect(() => { queueMicrotask(load); }, [load]);
diff --git a/src/hooks/useBranch.ts b/src/hooks/useBranch.ts
index 4b32a97..731e896 100644
--- a/src/hooks/useBranch.ts
+++ b/src/hooks/useBranch.ts
@@ -46,21 +46,15 @@ export function useBranches() {
// fetch branches with pagination and search
const loadBranches = useCallback(async (p: number, q: string) => {
- dispatch({ type: "LOAD_START" });
- try {
- const token = getStoredToken();
- const res = await branchService.getAll(p, q, token);
- const payload = res.data ?? res;
- dispatch({
- type: "LOAD_OK",
- branches: payload.data ?? [],
- total: payload.meta?.total ?? payload.pagination?.total ?? 0,
- pages: payload.meta?.pages ?? payload.pagination?.pages ?? 1,
- });
- } catch {
- dispatch({ type: "LOAD_ERR", error: "عذراً، حدث خطأ أثناء تحميل بيانات الفروع. يرجى المحاولة لاحقاً." });
- }
- }, []);
+ dispatch({ type: "LOAD_START" });
+ try {
+ const token = getStoredToken();
+ const { items, total, pages } = await branchService.getAll(p, q, token);
+ dispatch({ type: "LOAD_OK", branches: items, total, pages });
+ } catch {
+ dispatch({ type: "LOAD_ERR", error: "عذراً، حدث خطأ أثناء تحميل بيانات الفروع. يرجى المحاولة لاحقاً." });
+ }
+}, []);
// reload branches when page or search changes
useEffect(() => {
@@ -69,27 +63,24 @@ export function useBranches() {
// create new branch
const createBranch = useCallback(async (data: BranchFormData): Promise => {
- try {
- const token = getStoredToken();
- const res = await branchService.create(data, token);
- dispatch({ type: "ADD", branch: res.data });
- notify({ type: "success", message: "تم إضافة الفرع بنجاح." });
- return true;
- } catch (err) {
- const msg = err instanceof Error && err.message
- ? err.message
- : "تعذر الاتصال بالخادم. تحقق من اتصالك بالإنترنت.";
- notify({ type: "error", message: msg });
- return false;
- }
- }, [notify]);
+ try {
+ const token = getStoredToken();
+ const branch = await branchService.create(data, token);
+ dispatch({ type: "ADD", branch });
+ notify({ type: "success", message: "تم إضافة الفرع بنجاح." });
+ return true;
+ } catch (err) {
+ notify({ type: "error", message: err instanceof Error ? err.message : "تعذر الاتصال بالخادم." });
+ return false;
+ }
+}, [notify]);
// update existing branch
const updateBranch = useCallback(async (id: string, data: BranchFormData): Promise => {
try {
const token = getStoredToken();
const res = await branchService.update(id, data, token);
- dispatch({ type: "UPDATE", branch: res.data });
+ dispatch({ type: "UPDATE", branch: res?.data });
notify({ type: "success", message: "تم تحديث الفرع بنجاح." });
return true;
} catch (err) {
diff --git a/src/hooks/useCars.ts b/src/hooks/useCars.ts
index 010ed9b..1bc7c2a 100644
--- a/src/hooks/useCars.ts
+++ b/src/hooks/useCars.ts
@@ -19,23 +19,22 @@ import type {
// Manages the paginated car list for the main page.
export function useCars(page: number, search: string) {
- const [cars, setCars] = useState([]);
+ const [cars, setCars] = useState([]);
const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
- const [total, setTotal] = useState(0);
- const [pages, setPages] = useState(1);
+ const [error, setError] = useState(null);
+ const [total, setTotal] = useState(0);
+ const [pages, setPages] = useState(1);
const loadCars = useCallback(() => {
const token = getStoredToken();
setLoading(true);
setError(null);
- const query = `?page=${page}&limit=12${search ? `&search=${encodeURIComponent(search)}` : ""}`;
- get(`cars${query}`, token)
- .then((res) => {
- const payload = (res as unknown as { data: CarListResponse["data"] }).data ?? res;
- setCars((payload as CarListResponse["data"]).data ?? []);
- setTotal((payload as CarListResponse["data"]).meta?.total ?? 0);
- setPages((payload as CarListResponse["data"]).meta?.pages ?? 1);
+ carService
+ .getAll(page, search, token)
+ .then(({ items, total, pages }) => {
+ setCars(items);
+ setTotal(total);
+ setPages(pages);
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
@@ -43,10 +42,9 @@ export function useCars(page: number, search: string) {
useEffect(() => { queueMicrotask(loadCars); }, [loadCars]);
- // Optimistic removal after delete
const removeCar = useCallback((id: string) => {
- setCars(prev => prev.filter(c => c.id !== id));
- setTotal(prev => Math.max(0, prev - 1));
+ setCars((prev) => prev.filter((c) => c.id !== id));
+ setTotal((prev) => Math.max(0, prev - 1));
}, []);
return { cars, loading, error, total, pages, loadCars, removeCar, setError };
@@ -58,9 +56,9 @@ export function useCars(page: number, search: string) {
// record is created/updated/deleted elsewhere in the tree.
export function useCarDetail(carId: string) {
- const [car, setCar] = useState(null);
+ const [car, setCar] = useState(null);
const [loading, setLoading] = useState(true);
- const [error, setError] = useState(null);
+ const [error, setError] = useState(null);
const loadCar = useCallback(() => {
const token = getStoredToken();
@@ -68,16 +66,16 @@ export function useCarDetail(carId: string) {
setError(null);
carService
.getById(carId, token)
- .then(res => setCar((res as unknown as { data: Car }).data))
+ .then(setCar)
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
}, [carId]);
useEffect(() => { queueMicrotask(loadCar); }, [loadCar]);
-
return { car, loading, error, refetch: loadCar };
}
+
// ── useCarMutations ───────────────────────────────────────────────────────────
// Create, update, and delete operations — used in the main page.
@@ -165,8 +163,8 @@ export function useCarImages(carId: string, sortBy: "asc" | "desc") {
try {
const token = getStoredToken();
const res = await carService.getImages(carId, token, { sortBy });
- const raw = (res as unknown as { data: CarImage[] }).data ?? [];
- setImages(raw);
+ // The response shape: { data: CarImage[] }
+ setImages(res);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "تعذّر تحميل الصور");
} finally {
diff --git a/src/hooks/useClientAddresses.ts b/src/hooks/useClientAddresses.ts
index 8d9a48d..3eeeda8 100644
--- a/src/hooks/useClientAddresses.ts
+++ b/src/hooks/useClientAddresses.ts
@@ -15,8 +15,6 @@ import type {
} from "../types/client_adresses";
import { Notification } from "../types/notif";
-
-
function normalizeAddress(raw: unknown): ClientAddress {
const data = raw as { id?: string; _id?: string; [key: string]: unknown };
@@ -34,7 +32,10 @@ function normalizeAddresses(rawList: unknown[]): ClientAddress[] {
}
// ── Reducer ────────────────────────────────────────────────────────────────
-function reducer(s: AddressTableState, a: AddressTableAction): AddressTableState {
+function reducer(
+ s: AddressTableState,
+ a: AddressTableAction,
+): AddressTableState {
switch (a.type) {
case "LOAD_START":
return { ...s, loading: true, error: null };
@@ -48,7 +49,7 @@ function reducer(s: AddressTableState, a: AddressTableAction): AddressTableState
return {
...s,
addresses: s.addresses.map((a2) =>
- a2.id === a.address.id ? a.address : a2
+ a2.id === a.address.id ? a.address : a2,
),
};
case "DELETE":
@@ -91,33 +92,23 @@ export function useClientAddresses(clientId: string) {
}, []);
// ── Fetch ────────────────────────────────────────────────────────────────
- const loadAddresses = useCallback(async () => {
- if (!clientId) return;
- dispatch({ type: "LOAD_START" });
- try {
- const token = getStoredToken();
- const res = await clientAddressService.getAll(clientId, token);
- const payload = (res as unknown as { data?: unknown }).data ?? res;
-
- // FIXED: explicit Array.isArray checks instead of relying on
- // TS to narrow the ternary's return type on its own.
- const rawList: unknown[] = Array.isArray(payload)
- ? payload
- : Array.isArray((payload as { data?: unknown })?.data)
- ? ((payload as { data?: unknown }).data as unknown[])
- : [];
-
- dispatch({
- type: "LOAD_OK",
- addresses: normalizeAddresses(rawList),
- });
- } catch {
- dispatch({
- type: "LOAD_ERR",
- error: "تعذّر تحميل العناوين. يرجى المحاولة مجدداً.",
- });
- }
-}, [clientId]);
+ const loadAddresses = useCallback(async () => {
+ if (!clientId) return;
+ dispatch({ type: "LOAD_START" });
+ try {
+ const token = getStoredToken();
+ const addresses = await clientAddressService.getAllNormalized(
+ clientId,
+ token,
+ );
+ dispatch({ type: "LOAD_OK", addresses });
+ } catch {
+ dispatch({
+ type: "LOAD_ERR",
+ error: "تعذّر تحميل العناوين. يرجى المحاولة مجدداً.",
+ });
+ }
+ }, [clientId]);
useEffect(() => {
loadAddresses();
@@ -129,7 +120,7 @@ export function useClientAddresses(clientId: string) {
try {
const token = getStoredToken();
const res = await clientAddressService.create(clientId, data, token);
- dispatch({ type: "ADD", address: normalizeAddress(res.data) });
+ dispatch({ type: "ADD", address: normalizeAddress(res.data as any) });
notify({ type: "success", message: "تم إضافة العنوان بنجاح." });
return true;
} catch (err) {
@@ -148,7 +139,12 @@ export function useClientAddresses(clientId: string) {
async (id: string, data: UpdateAddressFormValues): Promise => {
try {
const token = getStoredToken();
- const res = await clientAddressService.update(clientId, id, data, token);
+ const res = await clientAddressService.update(
+ clientId,
+ id,
+ data,
+ token,
+ );
dispatch({ type: "UPDATE", address: normalizeAddress(res.data) });
notify({ type: "success", message: "تم تحديث العنوان." });
return true;
@@ -200,7 +196,8 @@ export function useClientAddresses(clientId: string) {
} catch (err) {
notify({
type: "error",
- message: err instanceof Error ? err.message : "تعذّر تعيين العنوان كأساسي.",
+ message:
+ err instanceof Error ? err.message : "تعذّر تعيين العنوان كأساسي.",
});
return false;
} finally {
@@ -221,4 +218,4 @@ export function useClientAddresses(clientId: string) {
settingPrimaryId,
reload: loadAddresses,
};
-}
\ No newline at end of file
+}
diff --git a/src/hooks/useClients.ts b/src/hooks/useClients.ts
index b19cc21..d3783b0 100644
--- a/src/hooks/useClients.ts
+++ b/src/hooks/useClients.ts
@@ -71,33 +71,16 @@ export function useClients() {
}, []);
// ── Fetch clients ────────────────────────────────────────────────────────
- const loadClients = useCallback(async (p: number, q: string) => {
- dispatch({ type: "LOAD_START" });
- try {
- const token = getStoredToken();
- const res = await clientService.getAll(p, q, token);
-
- // Normalise both pagination shapes the backend might return
- const apiResponse = res as { data?: unknown };
- const payload = apiResponse.data ?? res;
- const response = payload as {
- data?: Client[];
- meta?: { total?: number; pages?: number };
- pagination?: { total?: number; pages?: number };
- };
- dispatch({
- type: "LOAD_OK",
- clients: response.data ?? [],
- total: response.meta?.total ?? response.pagination?.total ?? 0,
- pages: response.meta?.pages ?? response.pagination?.pages ?? 1,
- });
- } catch {
- dispatch({
- type: "LOAD_ERR",
- error: "تعذّر تحميل بيانات العملاء. يرجى المحاولة مجدداً.",
- });
- }
- }, []);
+const loadClients = useCallback(async (p: number, q: string) => {
+ dispatch({ type: "LOAD_START" });
+ try {
+ const token = getStoredToken();
+ const { items, total, pages } = await clientService.getAll(p, q, token);
+ dispatch({ type: "LOAD_OK", clients: items, total, pages });
+ } catch {
+ dispatch({ type: "LOAD_ERR", error: "تعذّر تحميل بيانات العملاء. يرجى المحاولة مجدداً." });
+ }
+}, []);
// Reload whenever page or search changes
useEffect(() => {
@@ -111,24 +94,18 @@ export function useClients() {
}, []);
// ── CREATE ───────────────────────────────────────────────────────────────
- const createClient = useCallback(
- async (data: ClientFormData): Promise => {
- try {
- const token = getStoredToken();
- const res = await clientService.create(data, token);
- dispatch({ type: "ADD", client: res.data });
- notify({ type: "success", message: "تم إضافة العميل بنجاح." });
- return true;
- } catch (err) {
- notify({
- type: "error",
- message: err instanceof Error ? err.message : "تعذّر إضافة العميل.",
- });
- return false;
- }
- },
- [notify],
- );
+ const createClient = useCallback(async (data: ClientFormData): Promise => {
+ try {
+ const token = getStoredToken();
+ const client = await clientService.create(data, token);
+ dispatch({ type: "ADD", client });
+ notify({ type: "success", message: "تم إضافة العميل بنجاح." });
+ return true;
+ } catch (err) {
+ notify({ type: "error", message: err instanceof Error ? err.message : "تعذّر إضافة العميل." });
+ return false;
+ }
+}, [notify]);
// ── UPDATE ───────────────────────────────────────────────────────────────
const updateClient = useCallback(
@@ -136,7 +113,7 @@ export function useClients() {
try {
const token = getStoredToken();
const res = await clientService.update(id, data, token);
- dispatch({ type: "UPDATE", client: res.data });
+ dispatch({ type: "UPDATE", client: res?.data });
notify({ type: "success", message: "تم تحديث بيانات العميل." });
return true;
} catch (err) {
diff --git a/src/hooks/useDriver.ts b/src/hooks/useDriver.ts
index 1fffc92..ebcd9b1 100644
--- a/src/hooks/useDriver.ts
+++ b/src/hooks/useDriver.ts
@@ -35,7 +35,6 @@ function reducer(s: TableState, a: TableAction): TableState {
return { ...s, loading: false, error: a.error };
case "DELETE":
return { ...s, drivers: s.drivers.filter((d) => d.id !== a.id) };
- // Instantly reflect updated driver in the list without a full reload
case "UPDATE":
return { ...s, drivers: s.drivers.map((d) => (d.id === a.driver.id ? a.driver : d)) };
case "CLEAR_ERR":
@@ -60,7 +59,6 @@ export function useDrivers() {
const [page, setPage] = useState(1);
const [notification, setNotification] = useState(null);
- // Show a toast for 4 seconds then auto-dismiss
const notify = useCallback((n: ToastNotification) => {
setNotification(n);
setTimeout(() => setNotification(null), 4000);
@@ -71,23 +69,8 @@ export function useDrivers() {
dispatch({ type: "LOAD_START" });
try {
const token = getStoredToken();
- const res = await driverService.getAll(p, q, token);
- const payload = (
- res as unknown as {
- data: {
- data: Driver[];
- pagination?: { total: number; pages: number };
- meta?: { total: number; pages: number };
- };
- }
- ).data ?? res;
-
- dispatch({
- type: "LOAD_OK",
- drivers: payload.data ?? [],
- total: payload.meta?.total ?? payload.pagination?.total ?? 0,
- pages: payload.meta?.pages ?? payload.pagination?.pages ?? 1,
- });
+ const { items, total, pages } = await driverService.getAll(p, q, token);
+ dispatch({ type: "LOAD_OK", drivers: items, total, pages });
} catch {
dispatch({ type: "LOAD_ERR", error: "تعذّر تحميل بيانات السائقين. يرجى المحاولة مجدداً." });
}
@@ -111,8 +94,7 @@ export function useDrivers() {
const hasFiles = payload.photo || payload.nationalPhoto || payload.driverCardPhoto;
if (hasFiles) {
- const res = await driverService.createWithImages(payload, token);
- const created = (res as unknown as { data: Driver }).data;
+ const created = await driverService.createWithImages(payload, token);
if (created?.id) {
await driverService.getById(created.id, token).catch(() => null);
}
@@ -124,10 +106,9 @@ export function useDrivers() {
await loadDrivers(page, search);
return true;
} catch (err) {
- // ✅ بنعرض رسالة الخطأ كـ toast برضو، مش بس نرميها لفوق
const message = err instanceof Error ? err.message : "تعذّر إضافة السائق. يرجى المحاولة لاحقاً.";
notify({ type: "error", message });
- throw err; // يفضل يترمي عشان DriverFormModal يعرضه جوه المودال كمان لو محتاج
+ throw err;
}
},
[notify, loadDrivers, page, search],
@@ -147,22 +128,14 @@ export function useDrivers() {
const token = getStoredToken();
const hasFiles = payload.photo || payload.nationalPhoto || payload.driverCardPhoto;
- let updatedDriver: Driver;
-
- if (hasFiles) {
- await driverService.updateWithImages(id, payload, token);
- const fresh = await driverService.getById(id, token);
- updatedDriver = (fresh as unknown as { data: Driver }).data;
- } else {
- const res = await driverService.update(id, payload, token);
- updatedDriver = (res as unknown as { data: Driver }).data;
- }
+ const updatedDriver = hasFiles
+ ? await driverService.updateWithImages(id, payload, token)
+ : await driverService.update(id, payload, token);
dispatch({ type: "UPDATE", driver: updatedDriver });
notify({ type: "success", message: "تم تحديث بيانات السائق بنجاح." });
return true;
} catch (err) {
- // ✅ نفس الفكرة هنا — أي فشل في التحديث يبان كـ toast أحمر
const message = err instanceof Error ? err.message : "تعذّر تحديث بيانات السائق. يرجى المحاولة لاحقاً.";
notify({ type: "error", message });
throw err;
diff --git a/src/hooks/useOrder.ts b/src/hooks/useOrder.ts
index ba7bea5..60b2291 100644
--- a/src/hooks/useOrder.ts
+++ b/src/hooks/useOrder.ts
@@ -13,8 +13,6 @@ import type { ToastNotification } from "@/src/Components/UI";
export type OrderNotification = ToastNotification;
// ── Table state / reducer ──────────────────────────────────────────────────
-// Mirrors useDriver.ts's TableState/TableAction shape so the two resource
-// hooks stay readable side by side.
interface TableState {
orders: Order[];
loading: boolean;
@@ -41,7 +39,6 @@ function reducer(s: TableState, a: TableAction): TableState {
return { ...s, loading: false, error: a.error };
case "DELETE":
return { ...s, orders: s.orders.filter((o) => o.id !== a.id) };
- // Instantly reflect the updated order in the list without a full reload
case "UPDATE":
return { ...s, orders: s.orders.map((o) => (o.id === a.order.id ? a.order : o)) };
case "CLEAR_ERR":
@@ -67,7 +64,6 @@ export function useOrders() {
const [statusFilter, setStatusFilter] = useState("");
const [notification, setNotification] = useState(null);
- // Show a toast for 4 seconds then auto-dismiss — same timing as useDriver.ts
const notify = useCallback((n: ToastNotification) => {
setNotification(n);
setTimeout(() => setNotification(null), 4000);
@@ -77,20 +73,14 @@ export function useOrders() {
const loadOrders = useCallback(async (p: number, q: string, status: string) => {
dispatch({ type: "LOAD_START" });
try {
- const res = await orderService.getAll({
+ const { items, total, pages } = await orderService.getAll({
page: p,
limit: 12,
...(q ? { search: q } : {}),
...(status ? { currentStatus: status } : {}),
});
- const payload = (res as unknown as { data: { data: Order[]; meta: { total: number; totalPages: number } } }).data ?? res;
- dispatch({
- type: "LOAD_OK",
- orders: payload.data ?? [],
- total: payload.meta?.total ?? 0,
- pages: payload.meta?.totalPages ?? 1,
- });
+ dispatch({ type: "LOAD_OK", orders: items, total, pages });
} catch {
dispatch({ type: "LOAD_ERR", error: "تعذّر تحميل بيانات الطلبات. يرجى المحاولة مجدداً." });
}
@@ -109,10 +99,9 @@ export function useOrders() {
await loadOrders(page, search, statusFilter);
return true;
} catch (err) {
- // نعرض رسالة الخطأ كـ toast برضو، مش بس نرميها لفوق
const message = err instanceof Error ? err.message : "تعذّر إنشاء الطلب. يرجى المحاولة لاحقاً.";
notify({ type: "error", message });
- throw err; // يفضل يترمي عشان OrderFormModal يعرضه جوه المودال كمان لو محتاج
+ throw err;
}
},
[notify, loadOrders, page, search, statusFilter],
@@ -122,8 +111,7 @@ export function useOrders() {
const updateOrder = useCallback(
async (id: string, payload: UpdateOrderPayload): Promise => {
try {
- const res = await orderService.update(id, payload);
- const updated = (res as unknown as { data: Order }).data ?? (res as unknown as Order);
+ const updated = await orderService.update(id, payload);
dispatch({ type: "UPDATE", order: updated });
notify({ type: "success", message: "تم تحديث الطلب بنجاح." });
return true;
@@ -140,8 +128,7 @@ export function useOrders() {
const updateStatus = useCallback(
async (id: string, payload: UpdateOrderStatusPayload): Promise => {
try {
- const res = await orderService.updateStatus(id, payload);
- const updated = (res as unknown as { data: Order }).data ?? (res as unknown as Order);
+ const updated = await orderService.updateStatus(id, payload);
dispatch({ type: "UPDATE", order: updated });
notify({ type: "success", message: "تم تحديث حالة الطلب بنجاح." });
return true;
diff --git a/src/hooks/useRole.ts b/src/hooks/useRole.ts
index dd816a4..f945e3c 100644
--- a/src/hooks/useRole.ts
+++ b/src/hooks/useRole.ts
@@ -82,33 +82,20 @@ export function useRoles() {
// ── Fetch roles ─────────────────────────────────────────────────────────────
const loadRoles = useCallback(async (p: number, q: string) => {
- dispatch({ type: "LOAD_START" });
- try {
- const token = getStoredToken();
- const res = await roleService.getAll(p, q, token);
- const payload =
- (
- res as unknown as {
- data: {
- data: Role[];
- meta?: { total: number; pages: number };
- pagination?: { total: number; pages: number };
- };
- }
- ).data ?? res;
- dispatch({
- type: "LOAD_OK",
- roles: payload.data ?? [],
- total: payload.meta?.total ?? payload.pagination?.total ?? 0,
- pages: payload.meta?.pages ?? payload.pagination?.pages ?? 1,
- });
- } catch {
- dispatch({
- type: "LOAD_ERR",
- error: "تعذّر تحميل بيانات الأدوار. يرجى المحاولة مجدداً.",
- });
- }
- }, []);
+ dispatch({ type: "LOAD_START" });
+ try {
+ const token = getStoredToken();
+ const { items, total, pages } = await roleService.getAll(p, q, token);
+ dispatch({ type: "LOAD_OK", roles: items, total, pages });
+ } catch {
+ dispatch({ type: "LOAD_ERR", error: "تعذّر تحميل بيانات الأدوار. يرجى المحاولة مجدداً." });
+ }
+}, []);
+
+useEffect(() => {
+ const token = getStoredToken();
+ roleService.getPermissions(token).then(setPermissions).catch(() => {});
+}, []);
// ── Load permissions on mount ────────────────────────────────────────────────
useEffect(() => {
@@ -131,36 +118,24 @@ export function useRoles() {
// ── CRUD actions ─────────────────────────────────────────────────────────────
- const createRole = useCallback(
- async (data: RoleFormData): Promise => {
- try {
- const token = getStoredToken();
- const res = await roleService.create(data, token);
- const role = (res as unknown as { data: Role }).data;
-
- // Endpoint 2 (PATCH /role/{id}/permissions/bulk): attach selected
- // permissions right after creation, since POST /role only accepts
- // name/description.
- if (data.permissionIds.length) {
- await roleService.bulkAssignPermissions(role.id, data.permissionIds, token);
- }
-
- await loadRoles(page, search);
- notify({
- type: "success",
- message: "تم إنشاء الدور وتعيين الصلاحيات بنجاح.",
- });
- return true;
- } catch (err) {
- notify({
- type: "error",
- message: parseApiError(err, "تعذّر إنشاء الدور."),
- });
- return false;
+ const createRole = useCallback(
+ async (data: RoleFormData): Promise => {
+ try {
+ const token = getStoredToken();
+ const role = await roleService.create(data, token);
+ if (data.permissionIds.length) {
+ await roleService.bulkAssignPermissions(role.id, data.permissionIds, token);
}
- },
- [page, search, loadRoles, notify],
- );
+ await loadRoles(page, search);
+ notify({ type: "success", message: "تم إنشاء الدور وتعيين الصلاحيات بنجاح." });
+ return true;
+ } catch (err) {
+ notify({ type: "error", message: parseApiError(err, "تعذّر إنشاء الدور.") });
+ return false;
+ }
+ },
+ [page, search, loadRoles, notify],
+);
const updateRole = useCallback(
async (
diff --git a/src/hooks/useTrip.ts b/src/hooks/useTrip.ts
index ae5a879..dbc9dbc 100644
--- a/src/hooks/useTrip.ts
+++ b/src/hooks/useTrip.ts
@@ -102,36 +102,22 @@ export function useTrips() {
// ── Fetch trips ───────────────────────────────────────────────────────────
- const loadTrips = useCallback(
- async (p: number, q: string, st: TripStatus | "") => {
- dispatch({ type: "LOAD_START" });
- try {
- const token = getStoredToken();
- const res = await tripService.getAll(
- { page: p, limit: 12, search: q || undefined, status: st || undefined },
- token,
- );
- const payload = (
- res as unknown as {
- data: { data: Trip[]; pagination?: { total: number; totalPages: number } };
- }
- ).data ?? res;
-
- dispatch({
- type: "LOAD_OK",
- trips: payload.data ?? [],
- total: payload.pagination?.total ?? 0,
- pages: payload.pagination?.totalPages ?? 1,
- });
- } catch (err) {
- dispatch({
- type: "LOAD_ERR",
- error: extractApiMessage(err, "تعذّر تحميل بيانات الرحلات. يرجى المحاولة مجدداً."),
- });
- }
- },
- [],
- );
+ const loadTrips = useCallback(
+ async (p: number, q: string, st: TripStatus | "") => {
+ dispatch({ type: "LOAD_START" });
+ try {
+ const token = getStoredToken();
+ const { items, total, pages } = await tripService.getAll(
+ { page: p, limit: 12, search: q || undefined, status: st || undefined },
+ token,
+ );
+ dispatch({ type: "LOAD_OK", trips: items, total, pages });
+ } catch (err) {
+ dispatch({ type: "LOAD_ERR", error: extractApiMessage(err, "تعذّر تحميل بيانات الرحلات. يرجى المحاولة مجدداً.") });
+ }
+ },
+ [],
+);
useEffect(() => {
loadTrips(page, search, status);
@@ -139,42 +125,38 @@ export function useTrips() {
// ── Create ────────────────────────────────────────────────────────────────
- const createTrip = useCallback(
- async (payload: CreateTripPayload): Promise => {
- try {
- const token = getStoredToken();
- const res = await tripService.create(payload, token);
- const newTrip = (res as unknown as { data: Trip }).data;
- dispatch({ type: "ADD", trip: newTrip });
- notify({ type: "success", message: "تم إضافة الرحلة بنجاح." });
- return true;
- } catch (err) {
- notify({ type: "error", message: extractApiMessage(err, "تعذّر إضافة الرحلة.") });
- return false;
- }
- },
- [notify],
- );
-
+ const createTrip = useCallback(
+ async (payload: CreateTripPayload): Promise => {
+ try {
+ const token = getStoredToken();
+ const newTrip = await tripService.create(payload, token);
+ dispatch({ type: "ADD", trip: newTrip });
+ notify({ type: "success", message: "تم إضافة الرحلة بنجاح." });
+ return true;
+ } catch (err) {
+ notify({ type: "error", message: extractApiMessage(err, "تعذّر إضافة الرحلة.") });
+ return false;
+ }
+ },
+ [notify],
+);
// ── Update ────────────────────────────────────────────────────────────────
- const updateTrip = useCallback(
- async (id: string, payload: UpdateTripPayload): Promise => {
- try {
- const token = getStoredToken();
- const res = await tripService.update(id, payload, token);
- const updated = (res as unknown as { data: Trip }).data;
- dispatch({ type: "UPDATE", trip: updated });
- notify({ type: "success", message: "تم تحديث بيانات الرحلة بنجاح." });
- return true;
- } catch (err) {
- notify({ type: "error", message: extractApiMessage(err, "تعذّر تحديث الرحلة.") });
- return false;
- }
- },
- [notify],
- );
-
+ const updateTrip = useCallback(
+ async (id: string, payload: UpdateTripPayload): Promise => {
+ try {
+ const token = getStoredToken();
+ const updated = await tripService.update(id, payload, token);
+ dispatch({ type: "UPDATE", trip: updated });
+ notify({ type: "success", message: "تم تحديث بيانات الرحلة بنجاح." });
+ return true;
+ } catch (err) {
+ notify({ type: "error", message: extractApiMessage(err, "تعذّر تحديث الرحلة.") });
+ return false;
+ }
+ },
+ [notify],
+);
// ── Delete ────────────────────────────────────────────────────────────────
const deleteTrip = useCallback(
diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts
index a9abc38..eb04ed6 100644
--- a/src/hooks/useUser.ts
+++ b/src/hooks/useUser.ts
@@ -49,33 +49,22 @@ export function useUsers() {
}, []);
// fetch users with pagination and search
- const loadUsers = useCallback(async (p: number, q: string) => {
- dispatch({ type: "LOAD_START" });
- try {
- const token = getStoredToken();
- const res = await userService.getAll(p, q, token);
- const payload = res.data ?? res;
- dispatch({
- type: "LOAD_OK",
- users: payload.data ?? [],
- total: payload.meta?.total ?? 0,
- pages: payload.meta?.pages ?? 1,
- });
- } catch {
- dispatch({ type: "LOAD_ERR", error: "عذراً، حدث خطأ أثناء تحميل بيانات المستخدمين. يرجى المحاولة لاحقاً." });
- }
- }, []);
-
- // fetch roles and branches for form dropdowns on mount
- useEffect(() => {
+ const loadUsers = useCallback(async (p: number, q: string) => {
+ dispatch({ type: "LOAD_START" });
+ try {
const token = getStoredToken();
- userService.getRoles(token)
- .then(res => setRoles((res.data ?? res).data ?? []))
- .catch(() => {});
- userService.getBranches(token)
- .then(res => setBranches((res.data ?? res).data ?? []))
- .catch(() => {});
- }, []);
+ const { items, total, pages } = await userService.getAll(p, q, token);
+ dispatch({ type: "LOAD_OK", users: items, total, pages });
+ } catch {
+ dispatch({ type: "LOAD_ERR", error: "عذراً، حدث خطأ أثناء تحميل بيانات المستخدمين. يرجى المحاولة لاحقاً." });
+ }
+}, []);
+
+useEffect(() => {
+ const token = getStoredToken();
+ userService.getRoles(token).then(setRoles).catch(() => {});
+ userService.getBranches(token).then(setBranches).catch(() => {});
+}, []);
// reload users when page or search changes
useEffect(() => {
@@ -83,28 +72,25 @@ export function useUsers() {
}, [page, search, loadUsers]);
// create new user
- const createUser = useCallback(async (data: UserFormData): Promise => {
- try {
- const token = getStoredToken();
- const res = await userService.create(data as UserFormData & { password: string }, token);
- dispatch({ type: "ADD", user: res.data });
- notify({ type: "success", message: "تم إنشاء مستخدم جديد بنجاح." });
- return true;
- } catch (err) {
- const msg = err instanceof Error && err.message
- ? err.message
- : "تعذر الاتصال بالخادم. تحقق من اتصالك بالإنترنت.";
- notify({ type: "error", message: msg });
- return false;
- }
- }, [notify]);
+ const createUser = useCallback(async (data: UserFormData): Promise => {
+ try {
+ const token = getStoredToken();
+ const user = await userService.create(data as UserFormData & { password: string }, token);
+ dispatch({ type: "ADD", user });
+ notify({ type: "success", message: "تم إنشاء مستخدم جديد بنجاح." });
+ return true;
+ } catch (err) {
+ notify({ type: "error", message: err instanceof Error && err.message ? err.message : "تعذر الاتصال بالخادم." });
+ return false;
+ }
+}, [notify]);
// update existing user
const updateUser = useCallback(async (id: string, data: UserFormData): Promise => {
try {
const token = getStoredToken();
const res = await userService.update(id, data, token);
- dispatch({ type: "UPDATE", user: res.data });
+ dispatch({ type: "UPDATE", user: res?.data });
notify({ type: "success", message: "تم تحديث بيانات المستخدم بنجاح." });
return true;
} catch (err) {
diff --git a/src/services/archive/archivedBranch.service.ts b/src/services/archive/archivedBranch.service.ts
index e32d788..6e8a1fa 100644
--- a/src/services/archive/archivedBranch.service.ts
+++ b/src/services/archive/archivedBranch.service.ts
@@ -16,4 +16,8 @@ export const archivedBranchService = {
`branches/archived${buildArchivedQuery(page, search)}`,
token,
),
+ getAllUnwrapped: async (page, search, token) => {
+ const res = await archivedBranchService.getAll(page, search, token);
+ return { items: res.data.data, total: res.data.meta.total, pages: res.data.meta.totalPages };
+},
};
\ No newline at end of file
diff --git a/src/services/archive/archivedClient.service.ts b/src/services/archive/archivedClient.service.ts
index b75e03e..24db4a9 100644
--- a/src/services/archive/archivedClient.service.ts
+++ b/src/services/archive/archivedClient.service.ts
@@ -28,7 +28,15 @@ export const archivedClientService = {
`client/${clientId}/orders/archived?page=${page}&limit=10`,
token,
),
-
+getAllUnwrapped: async (page, search, token) => {
+ const res = await archivedClientService.getAll(page, search, token);
+ return { items: res.data.data, total: res.data.meta.total, pages: res.data.meta.totalPages };
+},
+getByIdUnwrapped: async (id, token) => (await archivedClientService.getById(id, token)).data,
+getArchivedOrdersUnwrapped: async (clientId, page, token) => {
+ const res = await archivedClientService.getArchivedOrders(clientId, page, token);
+ return { items: res.data.data, total: res.data.meta.total, pages: res.data.meta.totalPages };
+},
// NOTE: delete/restore intentionally left out for now — mirrors
// archivedUser.service.ts, see ticket follow-up.
};
\ No newline at end of file
diff --git a/src/services/archive/archivedClientAdresses.service.ts b/src/services/archive/archivedClientAdresses.service.ts
index f8598f4..e8e91ed 100644
--- a/src/services/archive/archivedClientAdresses.service.ts
+++ b/src/services/archive/archivedClientAdresses.service.ts
@@ -1,16 +1,8 @@
import { get } from "../api";
-import type { ArchivedClientAddressListResponse } from "@/src/types/client_adresses";
+import type { ArchivedClientAddress, ArchivedClientAddressListResponse } from "@/src/types/client_adresses";
export const archivedClientAddressService = {
- /**
- * Fetching every archived (soft-deleted) address across all clients.
- * NOTE: this endpoint returns a flat array with no pagination meta,
- * unlike users/archived and client/archived — so no page/limit params here.
- */
- getAll: (token: string | null) =>
- get("addresses/archived", token),
-
- // NOTE: no GET /addresses/archived/{id} was specified — the list endpoint
- // already returns the full address shape, so the detail modal renders
- // directly from the list item instead of firing a second request.
+ getAll: (token: string | null) => get("addresses/archived", token),
+ getAllUnwrapped: async (token: string | null): Promise =>
+ (await archivedClientAddressService.getAll(token)).data,
};
\ No newline at end of file
diff --git a/src/services/archive/archivedDriver.service.ts b/src/services/archive/archivedDriver.service.ts
index 9e6840c..e4d3780 100644
--- a/src/services/archive/archivedDriver.service.ts
+++ b/src/services/archive/archivedDriver.service.ts
@@ -43,4 +43,10 @@ export const archivedDriverService = {
*/
getStatusHistory: (id: string, token: string | null) =>
get(`driver/archived/driverStatus/${id}`, token),
+ getAllUnwrapped: async (page, search, token) => {
+ const res = await archivedDriverService.getAll(page, search, token);
+ return { items: res.data.data, total: res.data.meta.total, pages: res.data.meta.totalPages };
+},
+getByIdUnwrapped: async (id, token) => (await archivedDriverService.getById(id, token)).data,
+getStatusHistoryUnwrapped: async (id, token) => (await archivedDriverService.getStatusHistory(id, token)).data ?? [],
};
\ No newline at end of file
diff --git a/src/services/archive/archivedOrder.service.ts b/src/services/archive/archivedOrder.service.ts
index 8043925..5a4c0a7 100644
--- a/src/services/archive/archivedOrder.service.ts
+++ b/src/services/archive/archivedOrder.service.ts
@@ -1,8 +1,10 @@
import { get } from "../api";
-import type { ArchivedOrderListResponse } from "@/src/types/order";
+import type { ArchivedOrder, ArchivedOrderListResponse } from "@/src/types/order";
export const archivedOrderService = {
- /** Fetching the full archived order list (endpoint returns no pagination meta) */
- getAll: (token: string | null) =>
- get("orders/archived", token),
+ getAll: (token: string | null) => get("orders/archived", token),
+ getAllUnwrapped: async (token: string | null): Promise => {
+ const res = await archivedOrderService.getAll(token);
+ return Array.isArray(res.data?.data) ? res.data.data : [];
+ },
};
\ No newline at end of file
diff --git a/src/services/archive/archivedRole.service.ts b/src/services/archive/archivedRole.service.ts
index b070601..4bfc511 100644
--- a/src/services/archive/archivedRole.service.ts
+++ b/src/services/archive/archivedRole.service.ts
@@ -1,19 +1,21 @@
import { get } from "../api";
import type {
+ ArchivedRole,
ArchivedRoleListResponse,
ArchivedRoleResponse,
} from "@/src/types/role";
export const archivedRoleService = {
- /** Fetching the full archived role list — API returns a plain array,
- * no server-side pagination/search, unlike users/branches archives. */
- getAll: (token: string | null) =>
- get("role/archived", token),
+ getAll: (token: string | null) => get("role/archived", token),
+
+ getAllUnwrapped: async (token: string | null): Promise => {
+ const res = await archivedRoleService.getAll(token);
+ return Array.isArray(res.data?.data) ? res.data.data : [];
+ },
- /** Get a single archived role by id */
getById: (id: string, token: string | null) =>
get(`role/archived/${id}`, token),
- // NOTE: delete/restore intentionally left out for now — mirrors the
- // archivedUser.service.ts / archivedBranch.service.ts follow-up ticket.
+ getByIdUnwrapped: async (id: string, token: string | null): Promise =>
+ (await archivedRoleService.getById(id, token)).data,
};
\ No newline at end of file
diff --git a/src/services/archive/archivedTrip.service.ts b/src/services/archive/archivedTrip.service.ts
index c8a36d2..1ea5728 100644
--- a/src/services/archive/archivedTrip.service.ts
+++ b/src/services/archive/archivedTrip.service.ts
@@ -17,4 +17,9 @@ export const archivedTripService = {
/** Get a single archived trip by id */
getById: (id: string, token: string | null) =>
get(`trip/archived/${id}`, token),
+ getAllUnwrapped: async (page, search, token) => {
+ const res = await archivedTripService.getAll(page, search, token);
+ return { items: res.data.data, total: res.data.meta.total, pages: res.data.meta.totalPages };
+},
+getByIdUnwrapped: async (id, token) => (await archivedTripService.getById(id, token)).data,
};
\ No newline at end of file
diff --git a/src/services/archive/archivedUser.service.ts b/src/services/archive/archivedUser.service.ts
index 387ecb4..683c21d 100644
--- a/src/services/archive/archivedUser.service.ts
+++ b/src/services/archive/archivedUser.service.ts
@@ -10,16 +10,16 @@ function buildArchivedQuery(page: number, search: string): string {
}
export const archivedUserService = {
- /** Fetching the archived user list, paginated */
getAll: (page: number, search: string, token: string | null) =>
- get(
- `users/archived${buildArchivedQuery(page, search)}`,
- token,
- ),
+ get(`users/archived${buildArchivedQuery(page, search)}`, token),
- /** Get a single archived user by id */
- getById: (id: string, token: string | null) =>
- get(`users/archived/${id}`, token),
+ getAllUnwrapped: async (page: number, search: string, token: string | null) => {
+ const res = await archivedUserService.getAll(page, search, token);
+ return { items: res.data.data, total: res.data.meta.total, pages: res.data.meta.totalPages };
+ },
- // NOTE: delete/restore intentionally left out for now — see ticket follow-up.
+ getById: async (id: string, token: string | null) => {
+ const res = await get(`users/archived/${id}`, token);
+ return res.data;
+ },
};
\ No newline at end of file
diff --git a/src/services/branch.service.ts b/src/services/branch.service.ts
index c05c9c8..d1b1002 100644
--- a/src/services/branch.service.ts
+++ b/src/services/branch.service.ts
@@ -1,56 +1,64 @@
-
import { get, post, patch, del } from "./api";
-import type { ApiListResponse, Branch, BranchDetail, BranchFormData, BranchResponse } from "@/src/types/branch";
+import type {
+ ApiListResponse, Branch, BranchDetail, BranchFormData, BranchResponse,
+ BranchListResult, BranchOption,
+} from "@/src/types/branch";
-
-
-
-/** Building a query string to fetch branches with search and pagination*/
function buildBranchesQuery(page: number, search: string): string {
return `?page=${page}&limit=10${search ? `&search=${encodeURIComponent(search)}` : ""}`;
}
export const branchService = {
- /** Bring up the list of branches with the ability to search and browse between pages*/
- getAll: (page: number, search: string, token: string | null) =>
- get>(`branches${buildBranchesQuery(page, search)}`, token),
+ getAll: async (page: number, search: string, token: string | null): Promise => {
+ const res: ApiListResponse = await get>(
+ `branches${buildBranchesQuery(page, search)}`,
+ token,
+ );
+ return {
+ items: res.data.data,
+ total: res.data.meta?.total ?? res.data.pagination?.total ?? 0,
+ pages: res.data.meta?.pages ?? res.data.pagination?.pages ?? 1,
+ };
+ },
- /** create new branch*/
- create: (data: BranchFormData, token: string | null) =>
- post("branches", buildPayload(data), token),
+ create: async (data: BranchFormData, token: string | null): Promise => {
+ const res: BranchResponse = await post("branches", buildPayload(data), token);
+ return res.data;
+ },
- /** update branch*/
- update: (id: string, data: Partial, token: string | null) =>
- patch(`branches/${id}`, buildPayload(data), token),
+ update: async (id: string, data: Partial, token: string | null): Promise => {
+ const res: BranchResponse = await patch(`branches/${id}`, buildPayload(data), token);
+ return res.data;
+ },
- /**delete branch*/
delete: (id: string, token: string | null) => del(`branches/${id}`, token),
- /** get branch by id*/
- getById: (id: string, token: string | null) =>
- get<{ data: BranchDetail }>(`branches/${id}`, token),
+ getById: async (id: string, token: string | null): Promise => {
+ const res = await get<{ data: BranchDetail }>(`branches/${id}`, token);
+ return res.data;
+ },
+
+ /** Dropdown helper — replaces raw `get<{data:{data:Branch[]}}>("branches?limit=100")` in form modals. */
+ getOptions: async (token: string | null): Promise => {
+ const res = await get<{ data: { data: BranchOption[] } }>("branches?limit=100", token);
+ return res.data.data;
+ },
};
-/** Converting the form data into a payload suitable for the API*/
-function buildPayload(
- data: Partial,
-): Record {
+function buildPayload(data: Partial): Record {
const payload: Record = {};
-
if (data.name !== undefined) payload.name = data.name.trim();
- if (data.email) payload.email = data.email.trim();
- if (data.phone) payload.phone = data.phone.trim();
- if (data.country) payload.country = data.country.trim();
+ if (data.email) payload.email = data.email.trim();
+ if (data.phone) payload.phone = data.phone.trim();
+ if (data.country) payload.country = data.country.trim();
if (data.city !== undefined) payload.city = data.city.trim();
- if (data.state) payload.state = data.state.trim();
- if (data.district) payload.district = data.district.trim();
+ if (data.state) payload.state = data.state.trim();
+ if (data.district) payload.district = data.district.trim();
if (data.street !== undefined) payload.street = data.street.trim();
- if (data.buildingNo) payload.buildingNo = data.buildingNo.trim();
- if (data.unitNo) payload.unitNo = data.unitNo.trim();
- if (data.zipCode) payload.zipCode = data.zipCode.trim();
-
- if (data.latitude && !isNaN(Number(data.latitude))) payload.latitude = Number(data.latitude);
+ if (data.buildingNo) payload.buildingNo = data.buildingNo.trim();
+ if (data.unitNo) payload.unitNo = data.unitNo.trim();
+ if (data.zipCode) payload.zipCode = data.zipCode.trim();
+ if (data.latitude && !isNaN(Number(data.latitude))) payload.latitude = Number(data.latitude);
if (data.longitude && !isNaN(Number(data.longitude))) payload.longitude = Number(data.longitude);
-
return payload;
}
\ No newline at end of file
diff --git a/src/services/car.service.ts b/src/services/car.service.ts
index 463eb11..3a05ce4 100644
--- a/src/services/car.service.ts
+++ b/src/services/car.service.ts
@@ -1,14 +1,9 @@
import { get, post, patch, del } from "./api";
import type {
- Car,
- CarListResponse,
- CarDetailResponse,
- CarImageListResponse,
- CreateCarPayload,
- UpdateCarPayload,
+ Car, CarImage, CarListResponse, CarDetailResponse, CarImageListResponse,
+ CarListResult, CarOption, CreateCarPayload, UpdateCarPayload,
} from "@/src/types/car";
-/** Build a query string for list endpoints */
function buildQuery(params: Record): string {
const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== "");
if (!entries.length) return "";
@@ -16,91 +11,64 @@ function buildQuery(params: Record): string
}
export const carService = {
- /**
- * GET /cars
- * Fetch paginated + searchable car list.
- */
- getAll: (
- page = 1,
- search = "",
- token: string | null,
- ) =>
- get(
+ getAll: async (page = 1, search = "", token: string | null): Promise => {
+ const res: CarListResponse = await get(
`cars${buildQuery({ page, limit: 10, search: search || undefined })}`,
token,
- ),
+ );
+ return {
+ items: res.data.data,
+ total: res.data.meta?.total ?? res.data.pagination?.total ?? 0,
+ pages: res.data.meta?.pages ?? res.data.pagination?.pages ?? 1,
+ };
+ },
- /**
- * GET /cars/archived
- * Fetch soft-deleted cars.
- */
- getArchived: (token: string | null) =>
- get("cars/archived", token),
+ getArchived: async (token: string | null): Promise => {
+ const res: CarListResponse = await get("cars/archived", token);
+ return res.data.data;
+ },
- /**
- * GET /cars/:id
- * Fetch a single car with status history.
- */
- getById: (id: string, token: string | null) =>
- get(`cars/${id}`, token),
+ getById: async (id: string, token: string | null): Promise => {
+ const res: CarDetailResponse = await get(`cars/${id}`, token);
+ return res.data;
+ },
- /**
- * POST /cars
- * Create a new car record.
- */
- create: (payload: CreateCarPayload, token: string | null) =>
- post<{ data: Car }>("cars", payload, token),
+ create: async (payload: CreateCarPayload, token: string | null): Promise => {
+ const res = await post<{ data: Car }>("cars", payload, token);
+ return res.data;
+ },
- /**
- * PATCH /cars/:id
- * Update an existing car's details.
- */
- update: (id: string, payload: UpdateCarPayload, token: string | null) =>
- patch<{ data: Car }>(`cars/${id}`, payload, token),
+ update: async (id: string, payload: UpdateCarPayload, token: string | null): Promise => {
+ const res = await patch<{ data: Car }>(`cars/${id}`, payload, token);
+ return res.data;
+ },
- /**
- * DELETE /cars/:id
- * Soft-delete a car (returns 204 No Content).
- */
- delete: (id: string, token: string | null) =>
- del(`cars/${id}`, token),
+ delete: (id: string, token: string | null) => del(`cars/${id}`, token),
- // ── Images ──────────────────────────────────────────────────────────────
-
- /**
- * GET /car-images/car/:id
- * Fetch all active images for a car.
- * Supports optional query params: day, month, year, date, sortBy.
- */
- getImages: (
+ getImages: async (
carId: string,
token: string | null,
filters: { day?: string; month?: string; year?: string; date?: string; sortBy?: "asc" | "desc" } = {},
- ) =>
- get(
+ ): Promise => {
+ const res: CarImageListResponse = await get(
`car-images/car/${carId}${buildQuery(filters as Record)}`,
token,
- ),
+ );
+ return res.data;
+ },
- /**
- * GET /car-images/car/:id/archive
- * Fetch soft-deleted images for a car.
- */
- getArchivedImages: (carId: string, token: string | null) =>
- get(`car-images/car/${carId}/archive`, token),
+ getArchivedImages: async (carId: string, token: string | null): Promise => {
+ const res: CarImageListResponse = await get(`car-images/car/${carId}/archive`, token);
+ return res.data;
+ },
- /**
- * POST /car-images/car/:id (multipart/form-data)
- * Upload one or more images.
- * Uses raw fetch because multer expects FormData, not JSON.
- */
uploadImages: async (
carId: string,
files: File[],
stage: "BEFORE" | "AFTER" | "GENERAL" = "GENERAL",
token: string | null,
maintenanceId?: string,
- ): Promise => {
+ ): Promise => {
const form = new FormData();
files.forEach((f) => form.append("images", f));
form.append("stage", stage);
@@ -112,18 +80,19 @@ export const carService = {
body: form,
cache: "no-store",
});
-
if (!res.ok) {
const json = await res.json().catch(() => null);
throw new Error(json?.message ?? `HTTP ${res.status}`);
}
- return res.json() as Promise;
+ const data = (await res.json()) as CarImageListResponse;
+ return data.data;
},
- /**
- * DELETE /car-images/:imageId
- * Soft-delete a single image.
- */
- deleteImage: (imageId: string, token: string | null) =>
- del(`car-images/${imageId}`, token),
+ deleteImage: (imageId: string, token: string | null) => del(`car-images/${imageId}`, token),
+
+ /** Dropdown helper — replaces raw get() calls in TripFormModal / CarFormModal. */
+ getActiveOptions: async (token: string | null): Promise => {
+ const res = await get<{ data: { data: CarOption[] } }>("cars?limit=100¤tStatus=Active", token);
+ return res.data.data;
+ },
};
\ No newline at end of file
diff --git a/src/services/carMaintanance.service.ts b/src/services/carMaintanance.service.ts
index 6e93b5f..f84b7cb 100644
--- a/src/services/carMaintanance.service.ts
+++ b/src/services/carMaintanance.service.ts
@@ -95,8 +95,19 @@ export const carMaintenanceService = {
*/
getAllArchivedGlobal: (token: string | null) =>
get("maintenance/archived", token),
+
+ getAllUnwrapped: async (carId: string, token: string | null): Promise => {
+ const res = await carMaintenanceService.getAll(carId, token);
+ return res.data;
+ },
+
+ getArchivedUnwrapped: async (carId: string, token: string | null): Promise => {
+ const res = await carMaintenanceService.getArchived(carId, token);
+ return res.data;
+ },
};
+
// Re-exported so callers can build extra filters (day/month/year, search…)
// the same way carService does, without duplicating the helper.
export { buildQuery as buildMaintenanceQuery };
\ No newline at end of file
diff --git a/src/services/client.service.ts b/src/services/client.service.ts
index 470cf97..83c3405 100644
--- a/src/services/client.service.ts
+++ b/src/services/client.service.ts
@@ -1,42 +1,42 @@
-
-// Mirrors user.service.ts exactly: token parameter, buildPayload, buildQuery.
import { get, post, put, del } from "./api";
-import type { ApiResponse, ApiListResponse, Client, ClientFormData, ClientResponse } from "@/src/types/client";
+import type { ApiResponse, ApiListResponse, Client, ClientFormData, ClientResponse, ClientListResult } from "@/src/types/client";
-
-
-
-/** بناء query string لجلب العملاء مع البحث والصفحات */
function buildClientsQuery(page: number, search: string): string {
return `?page=${page}&limit=10${search ? `&search=${encodeURIComponent(search)}` : ""}`;
}
export const clientService = {
- /** جلب قائمة العملاء مع إمكانية البحث والتصفح بين الصفحات */
- getAll: (page: number, search: string, token: string | null) =>
- get>(`client${buildClientsQuery(page, search)}`, token),
+ getAll: async (page: number, search: string, token: string | null): Promise => {
+ const res: ApiListResponse = await get>(
+ `client${buildClientsQuery(page, search)}`, token,
+ );
+ return {
+ items: res.data.data,
+ total: res.data.meta?.total ?? res.data.pagination?.total ?? 0,
+ pages: res.data.meta?.pages ?? res.data.pagination?.pages ?? 1,
+ };
+ },
- /** جلب عميل واحد بالـ ID (مع العناوين المرفقة) */
- getById: (id: string, token: string | null) =>
- get>(`client/${id}`, token),
+ getById: async (id: string, token: string | null): Promise => {
+ const res: ApiResponse = await get>(`client/${id}`, token);
+ return res.data;
+ },
- /** إنشاء عميل جديد */
- create: (data: ClientFormData, token: string | null) =>
- post("client", buildPayload(data), token),
+ create: async (data: ClientFormData, token: string | null): Promise => {
+ const res: ClientResponse = await post("client", buildPayload(data), token);
+ return res.data;
+ },
- /** تعديل بيانات عميل موجود */
- update: (id: string, data: ClientFormData, token: string | null) =>
- put(`client/${id}`, buildPayload(data), token),
+ update: async (id: string, data: ClientFormData, token: string | null): Promise => {
+ const res: ClientResponse = await put(`client/${id}`, buildPayload(data), token);
+ return res.data;
+ },
- /** حذف عميل */
- delete: (id: string, token: string | null) =>
- del(`client/${id}`, token),
+ delete: (id: string, token: string | null) => del(`client/${id}`, token),
};
-/** تحويل بيانات النموذج إلى payload مناسب للـ API */
function buildPayload(data: ClientFormData): Record {
const payload: Record = {};
-
if (data.name?.trim()) payload.name = data.name.trim();
if (data.email?.trim()) payload.email = data.email.trim();
if (data.phone?.trim()) payload.phone = data.phone.trim();
@@ -44,6 +44,5 @@ function buildPayload(data: ClientFormData): Record {
if (data.notes?.trim()) payload.notes = data.notes.trim();
if (data.clientType) payload.clientType = data.clientType;
if (typeof data.isActive === "boolean") payload.isActive = data.isActive;
-
return payload;
}
\ No newline at end of file
diff --git a/src/services/clientAddress.service.ts b/src/services/clientAddress.service.ts
index 5eb32c8..0029d74 100644
--- a/src/services/clientAddress.service.ts
+++ b/src/services/clientAddress.service.ts
@@ -6,6 +6,9 @@ import type {
UpdateAddressFormValues,
} from "@/src/validations/client_address.validator";
+ function normalizeAddress(raw: { id?: string; _id?: string; [k: string]: unknown }): ClientAddress {
+ return { ...raw, id: raw.id ?? raw._id ?? "" } as ClientAddress;
+}
const collectionBase = (clientId: string) => {
@@ -73,6 +76,19 @@ export const clientAddressService = {
setPrimary: (addressId: string, token: string | null) =>
patch>(`${addressBase(addressId)}/primary`, {}, token),
+ getAllNormalized: async (clientId: string, token: string | null): Promise => {
+ const res = await get | { data: { data: ClientAddress[] } }>(
+ collectionBase(clientId), token,
+ );
+ const payload = res.data;
+ const rawList: unknown[] = Array.isArray(payload)
+ ? payload
+ : Array.isArray((payload as { data?: unknown })?.data)
+ ? (payload as { data: unknown[] }).data
+ : [];
+ return rawList.map((r) => normalizeAddress(r as { id?: string; _id?: string }));
+ },
+
};
@@ -87,4 +103,6 @@ function buildPayload(
details: data.details,
location: data.location,
};
-}
\ No newline at end of file
+}
+
+export default { normalizeAddress };
\ No newline at end of file
diff --git a/src/services/driver.service.ts b/src/services/driver.service.ts
index 2a9c275..4a64936 100644
--- a/src/services/driver.service.ts
+++ b/src/services/driver.service.ts
@@ -4,6 +4,9 @@ import type {
DriverListResponse,
DriverDetailResponse,
DriverReportResponse,
+ DriverListResult,
+ DriverReportResult,
+ DriverOption,
CreateDriverPayload,
UpdateDriverPayload,
} from "@/src/types/driver";
@@ -15,10 +18,6 @@ function buildQuery(params: Record): string
}
// ── Image URL normaliser ──────────────────────────────────────────────────────
-// Returns the URL as-is — tags are not subject to CORS, so we can point
-// directly at the backend without a Next.js proxy hop.
-// Only strips query-string noise or fixes obviously malformed paths if needed.
-
function normaliseImageUrl(url: string | null | undefined): string | null {
if (!url) return null;
return url;
@@ -29,7 +28,7 @@ function mapDriverImages<
photoUrl?: string | null;
nationalPhotoUrl?: string | null;
driverCardPhotoUrl?: string | null;
- },
+ }
>(driver: T): T {
return {
...driver,
@@ -39,11 +38,7 @@ function mapDriverImages<
};
}
-
-
// ── Image compression ─────────────────────────────────────────────────────────
-// Resizes to max 1024px while preserving the original file type and name.
-
async function compressImage(file: File, maxPx = 1024, quality = 0.82): Promise {
return new Promise((resolve) => {
const img = new Image();
@@ -54,13 +49,11 @@ async function compressImage(file: File, maxPx = 1024, quality = 0.82): Promise<
let { width, height } = img;
- // Guard: malformed/zero-dimension image — bail out to original file
if (!width || !height) {
resolve(file);
return;
}
- // Only resize if actually oversized
if (width <= maxPx && height <= maxPx) {
resolve(file);
return;
@@ -81,9 +74,6 @@ async function compressImage(file: File, maxPx = 1024, quality = 0.82): Promise<
const ctx = canvas.getContext("2d");
if (!ctx) { resolve(file); return; }
- // PNG/GIF have transparency — paint white behind them first so toBlob
- // doesn't hand back a canvas with undefined/black fill in some browsers.
- // (Skip this for formats that don't carry alpha, like plain JPEG.)
const mimeType = file.type || "image/jpeg";
if (mimeType !== "image/jpeg") {
ctx.fillStyle = "#FFFFFF";
@@ -92,14 +82,10 @@ async function compressImage(file: File, maxPx = 1024, quality = 0.82): Promise<
ctx.drawImage(img, 0, 0, width, height);
- // quality param is meaningless (and occasionally mishandled) for
- // lossless formats — only pass it for jpeg/webp.
const isLossy = mimeType === "image/jpeg" || mimeType === "image/webp";
canvas.toBlob(
(blob) => {
- // Guard: empty/undersized blob means the encode failed — fall
- // back to the original file rather than uploading garbage.
if (!blob || blob.size < 100) {
resolve(file);
return;
@@ -117,7 +103,7 @@ async function compressImage(file: File, maxPx = 1024, quality = 0.82): Promise<
}
async function compressPayloadImages<
- T extends { photo?: File; nationalPhoto?: File; driverCardPhoto?: File },
+ T extends { photo?: File; nationalPhoto?: File; driverCardPhoto?: File }
>(payload: T): Promise {
const result = { ...payload };
if (result.photo) result.photo = await compressImage(result.photo);
@@ -129,51 +115,48 @@ async function compressPayloadImages<
// ── Service ───────────────────────────────────────────────────────────────────
export const driverService = {
- /** GET /driver — paginated + searchable list */
- getAll: async (page = 1, search = "", token: string | null): Promise => {
- const res = await get(
+ /** GET /driver — returns a clean, unwrapped, fully-typed result. */
+ getAll: async (page = 1, search = "", token: string | null): Promise => {
+ const res: DriverListResponse = await get(
`driver${buildQuery({ page, limit: 12, search: search || undefined })}`,
token,
);
- const body = (res as unknown as { data: { data: Driver[]; pagination: unknown; meta?: unknown } }).data;
- body.data = body.data.map(mapDriverImages);
- return res;
+ const body = res.data;
+ return {
+ items: body.data.map(mapDriverImages),
+ total: body.meta?.total ?? body.pagination?.total ?? 0,
+ pages: body.meta?.pages ?? body.pagination?.pages ?? 1,
+ };
},
/** GET /driver/archived */
- getArchived: async (token: string | null): Promise => {
- const res = await get("driver/archived", token);
- const body = (res as unknown as { data: { data: Driver[] } }).data;
- body.data = body.data.map(mapDriverImages);
- return res;
+ getArchived: async (token: string | null): Promise => {
+ const res: DriverListResponse = await get("driver/archived", token);
+ return res.data.data.map(mapDriverImages);
},
/** GET /driver/me */
- getMe: async (token: string | null): Promise => {
- const res = await get("driver/me", token);
- const body = res as unknown as { data: Driver };
- body.data = mapDriverImages(body.data);
- return res;
+ getMe: async (token: string | null): Promise => {
+ const res: DriverDetailResponse = await get("driver/me", token);
+ return mapDriverImages(res.data);
},
/** GET /driver/:id */
- getById: async (id: string, token: string | null): Promise => {
- const res = await get(`driver/${id}`, token);
- const body = res as unknown as { data: Driver };
- body.data = mapDriverImages(body.data);
- return res;
+ getById: async (id: string, token: string | null): Promise => {
+ const res: DriverDetailResponse = await get(`driver/${id}`, token);
+ return mapDriverImages(res.data);
},
/** POST /driver (JSON — no files) */
- create: (payload: CreateDriverPayload, token: string | null) =>
- post<{ data: Driver }>("driver", payload, token),
+ create: async (payload: CreateDriverPayload, token: string | null): Promise => {
+ const res = await post<{ data: Driver }>("driver", payload, token);
+ return res.data;
+ },
/** PATCH /driver/:id (JSON — no files) */
- update: async (id: string, payload: UpdateDriverPayload, token: string | null): Promise<{ data: Driver }> => {
+ update: async (id: string, payload: UpdateDriverPayload, token: string | null): Promise => {
const res = await patch<{ data: Driver }>(`driver/${id}`, payload, token);
- const body = res as unknown as { data: Driver };
- body.data = mapDriverImages(body.data);
- return res;
+ return mapDriverImages(res.data);
},
/** DELETE /driver/:id */
@@ -181,18 +164,16 @@ export const driverService = {
del(`driver/${id}`, token),
/** GET /drivers/:id/reports/daily?date=YYYY-MM-DD */
- getDailyReport: (id: string, date: string, token: string | null) =>
- get(
+ getDailyReport: async (id: string, date: string, token: string | null): Promise => {
+ const res: DriverReportResponse = await get(
`drivers/${id}/reports/daily?date=${encodeURIComponent(date)}`,
token,
- ),
+ );
+ return res.data;
+ },
// ── Multipart helpers (with auto compression) ─────────────────────────────
- /**
- * POST /driver (multipart/form-data)
- * Compresses images before upload to stay under backend LIMIT_FILE_SIZE.
- */
createWithImages: async (
payload: CreateDriverPayload & {
photo?: File;
@@ -200,7 +181,7 @@ export const driverService = {
driverCardPhoto?: File;
},
token: string | null,
- ): Promise<{ data: Driver }> => {
+ ): Promise => {
const compressed = await compressPayloadImages(payload);
const form = new FormData();
@@ -225,14 +206,9 @@ export const driverService = {
throw new Error(json?.message ?? `HTTP ${res.status}`);
}
const data = await res.json() as { data: Driver };
- data.data = mapDriverImages(data.data);
- return data;
+ return mapDriverImages(data.data);
},
- /**
- * PATCH /driver/:id (multipart/form-data)
- * Compresses images before upload to stay under backend LIMIT_FILE_SIZE.
- */
updateWithImages: async (
id: string,
payload: UpdateDriverPayload & {
@@ -241,7 +217,7 @@ export const driverService = {
driverCardPhoto?: File;
},
token: string | null,
- ): Promise<{ data: Driver }> => {
+ ): Promise => {
const compressed = await compressPayloadImages(payload);
const form = new FormData();
@@ -256,7 +232,6 @@ export const driverService = {
const res = await fetch(`/api/proxy/driver/${id}`, {
method: "PATCH",
- // Do NOT set Content-Type — browser sets it with the correct boundary
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: form,
cache: "no-store",
@@ -267,7 +242,15 @@ export const driverService = {
throw new Error(json?.message ?? `HTTP ${res.status}`);
}
const data = await res.json() as { data: Driver };
- data.data = mapDriverImages(data.data);
- return data;
+ return mapDriverImages(data.data);
+ },
+
+ /** Dropdown helper — replaces raw get() calls in TripFormModal. */
+ getActiveOptions: async (token: string | null): Promise => {
+ const res = await get<{ data: { data: DriverOption[] } }>(
+ "driver?limit=100&status=Active",
+ token,
+ );
+ return res.data.data;
},
};
\ No newline at end of file
diff --git a/src/services/order.service.ts b/src/services/order.service.ts
index de6cdcb..d9ce3b7 100644
--- a/src/services/order.service.ts
+++ b/src/services/order.service.ts
@@ -6,32 +6,56 @@ import type {
UpdateOrderPayload,
UpdateOrderStatusPayload,
TransferOrderPayload,
- OrdersResponse,
+ OrderListResult,
} from "@/src/types/order";
+/**
+ * One-time defensive unwrap for order endpoints. Documented here because
+ * the backend for this resource isn't 100% consistent about wrapping
+ * responses in `{ data }` — some handlers return the entity directly.
+ */
+function unwrapOrder(res: Order | { data: Order }): Order {
+ const maybeWrapped = res as { data?: Order };
+ return maybeWrapped?.data ? maybeWrapped.data : (res as Order);
+}
+
+interface OrdersListEnvelope {
+ data: {
+ data: Order[];
+ meta?: { total: number; page: number; limit: number; totalPages: number };
+ };
+}
+
export const orderService = {
- /** Get all orders (paginated) */
- getAll: (params?: Record) => {
+ /** Get all orders (paginated) — returns a clean, unwrapped result. */
+ getAll: async (params?: Record): Promise => {
const qs = params
? "?" + new URLSearchParams(params as Record).toString()
: "";
- return get(`orders${qs}`);
+ const res = await get(`orders${qs}`);
+ const payload = res.data;
+ return {
+ items: payload.data ?? [],
+ total: payload.meta?.total ?? 0,
+ pages: payload.meta?.totalPages ?? 1,
+ };
},
/** Get order by ID */
- getById: (id: string) => get(`orders/${id}`),
+ getById: async (id: string): Promise =>
+ unwrapOrder(await get(`orders/${id}`)),
/** Create new order */
- create: (payload: CreateOrderPayload) =>
- post("orders", payload),
+ create: async (payload: CreateOrderPayload): Promise =>
+ unwrapOrder(await post("orders", payload)),
/** Update order metadata */
- update: (id: string, payload: UpdateOrderPayload) =>
- patch(`orders/${id}`, payload),
+ update: async (id: string, payload: UpdateOrderPayload): Promise =>
+ unwrapOrder(await patch(`orders/${id}`, payload)),
/** Update order status with optional reason */
- updateStatus: (id: string, payload: UpdateOrderStatusPayload) =>
- patch(`orders/${id}/status`, payload),
+ updateStatus: async (id: string, payload: UpdateOrderStatusPayload): Promise =>
+ unwrapOrder(await patch(`orders/${id}/status`, payload)),
/** Transfer order to a different trip */
transfer: (id: string, payload: TransferOrderPayload) =>
@@ -41,5 +65,8 @@ export const orderService = {
delete: (id: string) => del(`orders/${id}`),
/** Get archived orders */
- getArchived: () => get("orders/archived"),
+ getArchived: async (): Promise => {
+ const res = await get<{ data: { data: Order[] } }>("orders/archived");
+ return res.data.data ?? [];
+ },
};
\ No newline at end of file
diff --git a/src/services/role.service.ts b/src/services/role.service.ts
index 7446eb6..d2bd251 100644
--- a/src/services/role.service.ts
+++ b/src/services/role.service.ts
@@ -1,53 +1,58 @@
import {
- ApiPaginatedResponse,
- AssignPermissionResponse,
- BulkAssignPermissionsResponse,
- PermissionsResponse,
- Role,
- RoleFormData,
- RoleResponse,
+ ApiPaginatedResponse, AssignPermissionResponse, BulkAssignPermissionsResponse,
+ PermissionsResponse, Permission, Role, RoleFormData, RoleResponse, RoleListResult,
} from "../types/role";
import { get, post, put, del, patch } from "./api";
-/** Build query string for paginated role listing */
function buildRolesQuery(page: number, search: string): string {
return `?page=${page}&limit=10${search ? `&search=${encodeURIComponent(search)}` : ""}`;
}
export const roleService = {
- /** Fetch paginated roles list */
- getAll: (page: number, search: string, token: string | null) =>
- get>(`role${buildRolesQuery(page, search)}`, token),
+ getAll: async (page: number, search: string, token: string | null): Promise => {
+ const res: ApiPaginatedResponse = await get>(
+ `role${buildRolesQuery(page, search)}`,
+ token,
+ );
+ return {
+ items: res.data.data,
+ total: res.data.meta?.total ?? res.data.pagination?.total ?? 0,
+ pages: res.data.meta?.pages ?? res.data.pagination?.pages ?? 1,
+ };
+ },
- /** Fetch a single role by ID (includes permissions) */
- getById: (id: string, token: string | null) =>
- get<{ data: Role }>(`role/${id}`, token),
+ getById: async (id: string, token: string | null): Promise => {
+ const res = await get<{ data: Role }>(`role/${id}`, token);
+ return res.data;
+ },
- /** Create a new role */
- create: (data: RoleFormData, token: string | null) =>
- post("role", { name: data.name, description: data.description }, token),
+ create: async (data: RoleFormData, token: string | null): Promise => {
+ const res: RoleResponse = await post(
+ "role", { name: data.name, description: data.description }, token,
+ );
+ return res.data;
+ },
- /** Update role name/description */
- update: (id: string, data: Partial, token: string | null) =>
- put(`role/${id}`, { name: data.name, description: data.description }, token),
+ update: async (id: string, data: Partial, token: string | null): Promise => {
+ const res: RoleResponse = await put(
+ `role/${id}`, { name: data.name, description: data.description }, token,
+ );
+ return res.data;
+ },
- /** Soft-delete a role */
- delete: (id: string, token: string | null) =>
- del(`role/${id}`, token),
+ delete: (id: string, token: string | null) => del(`role/${id}`, token),
- /** Fetch all available permissions for checkbox list */
- getPermissions: (token: string | null) =>
- get("premission?limit=200", token),
+ getPermissions: async (token: string | null): Promise => {
+ const res: PermissionsResponse = await get("premission?limit=200", token);
+ return res.data?.premissions?.data ?? [];
+ },
- /** POST /role/{id}/permissions — assign a single permission to a role */
assignPermission: (roleId: string, permissionId: string, token: string | null) =>
post(`role-permissions/${roleId}/permissions`, { permissionId }, token),
- /** PATCH /role/{id}/permissions/bulk — replace all permissions for a role */
bulkAssignPermissions: (roleId: string, permissionIds: string[], token: string | null) =>
patch(`role-permissions/${roleId}/permissions`, { permissionIds }, token),
- /** DELETE /role/{id}/permissions/{permissionId} — remove a single permission */
removePermission: (roleId: string, permissionId: string, token: string | null) =>
del(`role-permissions/${roleId}/permissions/${permissionId}`, token),
};
\ No newline at end of file
diff --git a/src/services/trip.service.ts b/src/services/trip.service.ts
index fd91b28..ce6f7f2 100644
--- a/src/services/trip.service.ts
+++ b/src/services/trip.service.ts
@@ -1,109 +1,68 @@
import { get, post, patch, del } from "./api";
import type {
- Trip,
- TripListResponse,
- TripDetailResponse,
- TripReportResponse,
- CreateTripPayload,
- UpdateTripPayload,
- TripListParams,
+ Trip, TripListResponse, TripDetailResponse, TripReportResponse,
+ TripListResult, TripReportResult,
+ CreateTripPayload, UpdateTripPayload, TripListParams,
+ ArchivedTripListResponse, ArchivedTripResponse,
} from "@/src/types/trip";
-/** Build a query string for list endpoints */
-function buildQuery(
- params: Record,
-): string {
- const entries = Object.entries(params).filter(
- ([, v]) => v !== undefined && v !== "",
- );
+function buildQuery(params: Record): string {
+ const entries = Object.entries(params).filter(([, v]) => v !== undefined && v !== "");
if (!entries.length) return "";
- return (
- "?" +
- entries
- .map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`)
- .join("&")
- );
+ return "?" + entries.map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`).join("&");
}
export const tripService = {
- /**
- * GET /trip
- * Fetch paginated + filterable + searchable trip list.
- * Requires: "read-trip" permission
- */
- getAll: (params: TripListParams = {}, token: string | null) =>
- get(`trip${buildQuery({ ...params })}`, token),
+ getAll: async (params: TripListParams = {}, token: string | null): Promise => {
+ const res: TripListResponse = await get(`trip${buildQuery({ ...params })}`, token);
+ return {
+ items: res.data.data,
+ total: res.data.pagination?.total ?? 0,
+ pages: res.data.pagination?.totalPages ?? 1,
+ };
+ },
- /**
- * GET /trip/archived
- * Fetch soft-deleted (archived) trips. Same filters as getAll.
- * Requires: "read-deleted-trip" permission
- */
- getArchived: (params: TripListParams = {}, token: string | null) =>
- get(`trip/archived${buildQuery({ ...params })}`, token),
+ getArchived: async (params: TripListParams = {}, token: string | null): Promise => {
+ const res: ArchivedTripListResponse = await get(
+ `trip/archived${buildQuery({ ...params })}`,
+ token,
+ );
+ return {
+ items: res.data.data,
+ total: res.data.meta.total,
+ pages: res.data.meta.totalPages,
+ };
+ },
- /**
- * GET /trip/archived/:id
- * Fetch a single archived trip with full details.
- * Requires: "read-deleted-trip" permission
- */
- getArchivedById: (id: string, token: string | null) =>
- get(`trip/archived/${id}`, token),
+ getArchivedById: async (id: string, token: string | null): Promise