first action update user pages .

2-update driver pages [fixed image display and notyfi]
3-update car pages [fixed image]
4-build tripe pages crud
5-build some role pages
6-build audit page
7-update ui compounants
8-update saidebar and topbar
9-cheange view to be ar view
10-cheange stractcher to be app,src
11-add validation layer by yup
12-add api image-proxy to broke image corse bloken
This commit is contained in:
m7amedez5511
2026-06-23 15:54:07 +03:00
parent c1b77f89cd
commit db64b79fe3
140 changed files with 9266 additions and 2449 deletions

216
src/hooks/useCars.ts Normal file
View File

@@ -0,0 +1,216 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { getStoredToken } from "@/src/lib/auth";
import { carService } from "@/src/services/car.service";
import { get } from "@/src/services/api";
import type {
Car,
CarImage,
CarListResponse,
CreateCarPayload,
ImageStage,
ToastMsg,
UpdateCarPayload,
} from "../types/car";
// ── useCars ───────────────────────────────────────────────────────────────────
// Manages the paginated car list for the main page.
export function useCars(page: number, search: string) {
const [cars, setCars] = useState<Car[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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<CarListResponse>(`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);
})
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
}, [page, search]);
useEffect(() => { 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));
}, []);
return { cars, loading, error, total, pages, loadCars, removeCar, setError };
}
// ── useCarDetail ──────────────────────────────────────────────────────────────
// Fetches a single car by ID — used in CarDetailPanel.
export function useCarDetail(carId: string) {
const [car, setCar] = useState<Car | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const token = getStoredToken();
setLoading(true);
setError(null);
carService
.getById(carId, token)
.then(res => setCar((res as unknown as { data: Car }).data))
.catch((err: Error) => setError(err.message))
.finally(() => setLoading(false));
}, [carId]);
return { car, loading, error };
}
// ── useCarMutations ───────────────────────────────────────────────────────────
// Create, update, and delete operations — used in the main page.
interface UseCarMutationsOptions {
onSuccess: (msg: string) => void;
onError: (msg: string) => void;
onDeleted: (id: string) => void;
getEditTarget: () => Car | null;
}
export function useCarMutations({
onSuccess,
onError,
onDeleted,
getEditTarget,
}: UseCarMutationsOptions) {
const [deleting, setDeleting] = useState(false);
const handleFormSubmit = useCallback(async (
payload: CreateCarPayload | UpdateCarPayload,
isNew: boolean,
): Promise<boolean> => {
const token = getStoredToken();
try {
if (isNew) {
await carService.create(payload as CreateCarPayload, token);
onSuccess("تم إضافة المركبة بنجاح.");
} else {
const editTarget = getEditTarget();
if (!editTarget) return false;
await carService.update(editTarget.id, payload as UpdateCarPayload, token);
onSuccess("تم تحديث بيانات المركبة.");
}
return true;
} catch (err: unknown) {
onError(err instanceof Error ? err.message : "فشلت العملية.");
return false;
}
}, [onSuccess, onError, getEditTarget]);
const handleDeleteConfirm = useCallback(async (target: Car) => {
setDeleting(true);
const token = getStoredToken();
try {
await carService.delete(target.id, token);
onDeleted(target.id);
onSuccess(`تم حذف ${target.manufacturer} ${target.model} بنجاح.`);
} catch (err: unknown) {
onError(err instanceof Error ? err.message : "فشل الحذف.");
} finally {
setDeleting(false);
}
}, [onSuccess, onError, onDeleted]);
return { deleting, handleFormSubmit, handleDeleteConfirm };
}
// ── useToast ──────────────────────────────────────────────────────────────────
// Simple toast notification state.
export function useToast(duration = 3500) {
const [toast, setToast] = useState<ToastMsg | null>(null);
const notify = useCallback((t: ToastMsg) => {
setToast(t);
setTimeout(() => setToast(null), duration);
}, [duration]);
return { toast, notify };
}
// ── useCarImages ──────────────────────────────────────────────────────────────
// Manages gallery images — used in CarImageGallery.
export function useCarImages(carId: string, sortBy: "asc" | "desc") {
const [images, setImages] = useState<CarImage[]>([]);
const [loading, setLoading] = useState(true);
const [uploading, setUploading] = useState(false);
const [deleting, setDeleting] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const fetchImages = useCallback(async () => {
setLoading(true);
setError(null);
try {
const token = getStoredToken();
const res = await carService.getImages(carId, token, { sortBy });
const raw = (res as unknown as { data: CarImage[] }).data ?? [];
setImages(raw);
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "تعذّر تحميل الصور");
} finally {
setLoading(false);
}
}, [carId, sortBy]);
useEffect(() => { fetchImages(); }, [fetchImages]);
const uploadImages = useCallback(async (files: File[], stage: ImageStage) => {
setUploading(true);
setError(null);
try {
const token = getStoredToken();
await carService.uploadImages(carId, files, stage, token);
await fetchImages();
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "فشل رفع الصور");
} finally {
setUploading(false);
}
}, [carId, fetchImages]);
const deleteImage = useCallback(async (imageId: string) => {
setDeleting(imageId);
setError(null);
try {
const token = getStoredToken();
await carService.deleteImage(imageId, token);
setImages(prev => prev.filter(img => img.id !== imageId));
return true;
} catch (err: unknown) {
setError(err instanceof Error ? err.message : "فشل حذف الصورة");
return false;
} finally {
setDeleting(null);
}
}, []);
return {
images,
loading,
uploading,
deleting,
error,
setError,
fetchImages,
uploadImages,
deleteImage,
};
}

View File

@@ -0,0 +1,184 @@
"use client";
import { useCallback, useEffect, useReducer, useState } from "react";
import { getStoredToken } from "@/src/lib/auth";
import { clientAddressService } from "@/src/services/clientAddress.service";
import type {
ClientAddress,
ClientAddressFormData,
AddressTableAction,
AddressTableState,
} from "@/src/types/client";
// ── Notification ───────────────────────────────────────────────────────────
export interface Notification {
type: "success" | "error";
message: string;
}
// ── Reducer ────────────────────────────────────────────────────────────────
function reducer(s: AddressTableState, a: AddressTableAction): AddressTableState {
switch (a.type) {
case "LOAD_START":
return { ...s, loading: true, error: null };
case "LOAD_OK":
return { ...s, loading: false, addresses: a.addresses };
case "LOAD_ERR":
return { ...s, loading: false, error: a.error };
case "ADD":
return { ...s, addresses: [...s.addresses, a.address] };
case "UPDATE":
return {
...s,
addresses: s.addresses.map((a2) =>
a2.id === a.address.id ? a.address : a2
),
};
case "DELETE":
return {
...s,
addresses: s.addresses.filter((a2) => a2.id !== a.id),
};
case "CLEAR_ERR":
return { ...s, error: null };
default:
return s;
}
}
const initialState: AddressTableState = {
addresses: [],
loading: true,
error: null,
};
// ── Main hook ──────────────────────────────────────────────────────────────
export function useClientAddresses(clientId: string) {
const [state, dispatch] = useReducer(reducer, initialState);
const [notification, setNotification] = useState<Notification | null>(null);
/** Show a toast for 4 seconds then auto-dismiss. */
const notify = useCallback((n: Notification) => {
setNotification(n);
setTimeout(() => setNotification(null), 4000);
}, []);
// ── Fetch addresses ──────────────────────────────────────────────────────
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 any).data ?? res;
dispatch({
type: "LOAD_OK",
addresses: Array.isArray(payload) ? payload : (payload.data ?? []),
});
} catch {
dispatch({
type: "LOAD_ERR",
error: "تعذّر تحميل العناوين. يرجى المحاولة مجدداً.",
});
}
}, [clientId]);
useEffect(() => {
loadAddresses();
}, [loadAddresses]);
// ── CREATE ───────────────────────────────────────────────────────────────
const createAddress = useCallback(
async (data: ClientAddressFormData): Promise<boolean> => {
try {
const token = getStoredToken();
const res = await clientAddressService.create(clientId, data, token);
dispatch({ type: "ADD", address: res.data });
notify({ type: "success", message: "تم إضافة العنوان بنجاح." });
return true;
} catch (err) {
notify({
type: "error",
message: err instanceof Error ? err.message : "تعذّر إضافة العنوان.",
});
return false;
}
},
[clientId, notify],
);
// ── UPDATE ───────────────────────────────────────────────────────────────
const updateAddress = useCallback(
async (id: string, data: ClientAddressFormData): Promise<boolean> => {
try {
const token = getStoredToken();
const res = await clientAddressService.update(clientId, id, data, token);
dispatch({ type: "UPDATE", address: res.data });
notify({ type: "success", message: "تم تحديث العنوان." });
return true;
} catch (err) {
notify({
type: "error",
message: err instanceof Error ? err.message : "تعذّر تحديث العنوان.",
});
return false;
}
},
[clientId, notify],
);
// ── DELETE ───────────────────────────────────────────────────────────────
const deleteAddress = useCallback(
async (id: string): Promise<boolean> => {
try {
const token = getStoredToken();
await clientAddressService.delete(clientId, id, token);
dispatch({ type: "DELETE", id });
notify({ type: "success", message: "تم حذف العنوان بنجاح." });
return true;
} catch (err) {
notify({
type: "error",
message: err instanceof Error ? err.message : "تعذّر حذف العنوان.",
});
return false;
}
},
[clientId, notify],
);
// ── SET PRIMARY ──────────────────────────────────────────────────────────
const makePrimary = useCallback(
async (id: string): Promise<boolean> => {
try {
const token = getStoredToken();
const res = await clientAddressService.setPrimary(clientId, id, token);
// Backend demotes all others — reload to sync
dispatch({ type: "UPDATE", address: res.data });
// Reload to reflect the backend's cascade demotions
await loadAddresses();
notify({ type: "success", message: "تم تعيين العنوان الرئيسي." });
return true;
} catch (err) {
notify({
type: "error",
message: err instanceof Error ? err.message : "تعذّر تعيين العنوان الرئيسي.",
});
return false;
}
},
[clientId, notify, loadAddresses],
);
return {
...state,
notification,
clearError: () => dispatch({ type: "CLEAR_ERR" }),
createAddress,
updateAddress,
deleteAddress,
makePrimary,
reload: loadAddresses,
};
}

View File

@@ -0,0 +1,45 @@
"use client";
import { useEffect, useState } from "react";
import { clientService } from "@/src/services/client.service";
import type { Order } from "@/src/types/order";
interface State {
orders: Order[];
error: string | null;
loading: boolean;
}
/**
* Fetches all orders for a client session.
*
* @example
* const { orders, loading, error } = useClientOrders(session.id);
*/
export function useClientOrders(clientId: string): State {
const [state, setState] = useState<State>({
orders: [], error: null, loading: true,
});
useEffect(() => {
if (!clientId) {
setState({ orders: [], error: null, loading: false });
return;
}
let cancelled = false;
clientService
.getOrders(clientId)
.then(orders => {
if (!cancelled) setState({ orders, error: null, loading: false });
})
.catch((err: Error) => {
if (!cancelled) setState({ orders: [], error: err.message, loading: false });
});
return () => { cancelled = true; };
}, [clientId]);
return state;
}

191
src/hooks/useClients.ts Normal file
View File

@@ -0,0 +1,191 @@
"use client";
import { useCallback, useEffect, useReducer, useState } from "react";
import { getStoredToken } from "@/src/lib/auth";
import { clientService } from "@/src/services/client.service";
import type {
Client,
ClientFormData,
ClientTableAction,
ClientTableState,
} from "@/src/types/client";
// ── Notification (mirrors useUsers Notification) ───────────────────────────
export interface Notification {
type: "success" | "error";
message: string;
}
// ── Reducer ────────────────────────────────────────────────────────────────
function reducer(s: ClientTableState, a: ClientTableAction): ClientTableState {
switch (a.type) {
case "LOAD_START":
return { ...s, loading: true, error: null };
case "LOAD_OK":
return {
...s,
loading: false,
clients: a.clients,
total: a.total,
pages: a.pages,
};
case "LOAD_ERR":
return { ...s, loading: false, error: a.error };
case "ADD":
return { ...s, clients: [a.client, ...s.clients], total: s.total + 1 };
case "UPDATE":
return {
...s,
clients: s.clients.map((c) => (c.id === a.client.id ? a.client : c)),
};
case "DELETE":
return {
...s,
clients: s.clients.filter((c) => c.id !== a.id),
total: Math.max(0, s.total - 1),
};
case "CLEAR_ERR":
return { ...s, error: null };
default:
return s;
}
}
const initialState: ClientTableState = {
clients: [],
loading: true,
total: 0,
pages: 1,
error: null,
};
// ── Main hook ──────────────────────────────────────────────────────────────
export function useClients() {
const [state, dispatch] = useReducer(reducer, initialState);
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [notification, setNotification] = useState<Notification | null>(null);
const [roles, setRoles] = useState<[]>([]); // placeholder — extend if needed
const [branches, setBranches] = useState<[]>([]); // placeholder — extend if needed
/** Show a toast for 4 seconds then auto-dismiss — identical to useUsers. */
const notify = useCallback((n: Notification) => {
setNotification(n);
setTimeout(() => setNotification(null), 4000);
}, []);
// ── 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 payload = (res as any).data ?? res;
dispatch({
type: "LOAD_OK",
clients: payload.data ?? [],
total: payload.meta?.total ?? payload.pagination?.total ?? 0,
pages: payload.meta?.pages ?? payload.pagination?.pages ?? 1,
});
} catch {
dispatch({
type: "LOAD_ERR",
error: "تعذّر تحميل بيانات العملاء. يرجى المحاولة مجدداً.",
});
}
}, []);
// Reload whenever page or search changes
useEffect(() => {
loadClients(page, search);
}, [page, search, loadClients]);
// ── Search helper (resets to page 1) ────────────────────────────────────
const handleSearch = useCallback((q: string) => {
setSearch(q);
setPage(1);
}, []);
// ── CREATE ───────────────────────────────────────────────────────────────
const createClient = useCallback(
async (data: ClientFormData): Promise<boolean> => {
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],
);
// ── UPDATE ───────────────────────────────────────────────────────────────
const updateClient = useCallback(
async (id: string, data: ClientFormData): Promise<boolean> => {
try {
const token = getStoredToken();
const res = await clientService.update(id, data, token);
dispatch({ type: "UPDATE", client: res.data });
notify({ type: "success", message: "تم تحديث بيانات العميل." });
return true;
} catch (err) {
notify({
type: "error",
message: err instanceof Error ? err.message : "تعذّر تحديث العميل.",
});
return false;
}
},
[notify],
);
// ── DELETE ───────────────────────────────────────────────────────────────
const deleteClient = useCallback(
async (id: string): Promise<boolean> => {
try {
const token = getStoredToken();
await clientService.delete(id, token);
dispatch({ type: "DELETE", id });
notify({ type: "success", message: "تم حذف العميل بنجاح." });
return true;
} catch (err) {
notify({
type: "error",
message: err instanceof Error ? err.message : "تعذّر حذف العميل.",
});
return false;
}
},
[notify],
);
return {
// table state
...state,
// pagination & search
page,
search,
setPage,
handleSearch,
clearError: () => dispatch({ type: "CLEAR_ERR" }),
// CRUD
createClient,
updateClient,
deleteClient,
// toast
notification,
// extras (currently unused, kept for future parity with useUsers)
roles,
branches,
reload: () => loadClients(page, search),
};
}

View File

@@ -0,0 +1,49 @@
// hooks/useDashboardSummary.ts
"use client";
import { useEffect, useState } from "react";
import { dashboardService } from "@/src/services/dashboard.service";
import { getStoredToken } from "@/src/lib/auth";
import type { DashboardSummary } from "@/src/types/dashboard";
interface State {
data: DashboardSummary | null;
error: string | null;
loading: boolean;
}
/**
* Fetches the admin dashboard summary.
* Redirects to /login if no token is present (handled by the caller).
*
* @example
* const { data, loading, error } = useDashboardSummary();
*/
export function useDashboardSummary(): State {
const [state, setState] = useState<State>({
data: null, error: null, loading: true,
});
useEffect(() => {
let cancelled = false;
const token = getStoredToken();
if (!token) {
setState({ data: null, error: "Unauthenticated", loading: false });
return;
}
dashboardService
.getSummary(token)
.then(res => {
if (!cancelled) setState({ data: res.data, error: null, loading: false });
})
.catch((err: Error) => {
if (!cancelled) setState({ data: null, error: err.message, loading: false });
});
return () => { cancelled = true; };
}, []);
return state;
}

211
src/hooks/useDriver.ts Normal file
View File

@@ -0,0 +1,211 @@
"use client";
import { useCallback, useEffect, useReducer, useState } from "react";
import { getStoredToken } from "@/src/lib/auth";
import { driverService } from "@/src/services/driver.service";
import type { Driver, CreateDriverPayload, UpdateDriverPayload } from "@/src/types/driver";
import type { ToastNotification } from "@/src/Components/UI";
export type DriverNotification = ToastNotification;
// ── Table state / reducer ──────────────────────────────────────────────────
interface TableState {
drivers: Driver[];
loading: boolean;
total: number;
pages: number;
error: string | null;
}
type TableAction =
| { type: "LOAD_START" }
| { type: "LOAD_OK"; drivers: Driver[]; total: number; pages: number }
| { type: "LOAD_ERR"; error: string }
| { type: "DELETE"; id: string }
| { type: "UPDATE"; driver: Driver }
| { type: "CLEAR_ERR" };
function reducer(s: TableState, a: TableAction): TableState {
switch (a.type) {
case "LOAD_START":
return { ...s, loading: true, error: null };
case "LOAD_OK":
return { ...s, loading: false, drivers: a.drivers, total: a.total, pages: a.pages };
case "LOAD_ERR":
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":
return { ...s, error: null };
default:
return s;
}
}
const initialState: TableState = {
drivers: [],
loading: true,
total: 0,
pages: 1,
error: null,
};
// ── Main hook ──────────────────────────────────────────────────────────────
export function useDrivers() {
const [state, dispatch] = useReducer(reducer, initialState);
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [notification, setNotification] = useState<ToastNotification | null>(null);
// Show a toast for 4 seconds then auto-dismiss
const notify = useCallback((n: ToastNotification) => {
setNotification(n);
setTimeout(() => setNotification(null), 4000);
}, []);
// ── Fetch drivers ─────────────────────────────────────────────────────────
const loadDrivers = useCallback(async (p: number, q: string) => {
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,
});
} catch {
dispatch({ type: "LOAD_ERR", error: "تعذّر تحميل بيانات السائقين. يرجى المحاولة مجدداً." });
}
}, []);
useEffect(() => {
loadDrivers(page, search);
}, [page, search, loadDrivers]);
// ── Create ────────────────────────────────────────────────────────────────
const createDriver = useCallback(
async (
payload: CreateDriverPayload & {
photo?: File;
nationalPhoto?: File;
driverCardPhoto?: File;
},
): Promise<boolean> => {
try {
const token = getStoredToken();
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;
if (created?.id) {
await driverService.getById(created.id, token).catch(() => null);
}
} else {
await driverService.create(payload, token);
}
notify({ type: "success", message: "تم إضافة السائق بنجاح." });
await loadDrivers(page, search);
return true;
} catch (err) {
// ✅ بنعرض رسالة الخطأ كـ toast برضو، مش بس نرميها لفوق
const message = err instanceof Error ? err.message : "تعذّر إضافة السائق. يرجى المحاولة لاحقاً.";
notify({ type: "error", message });
throw err; // يفضل يترمي عشان DriverFormModal يعرضه جوه المودال كمان لو محتاج
}
},
[notify, loadDrivers, page, search],
);
// ── Update ────────────────────────────────────────────────────────────────
const updateDriver = useCallback(
async (
id: string,
payload: UpdateDriverPayload & {
photo?: File;
nationalPhoto?: File;
driverCardPhoto?: File;
},
): Promise<boolean> => {
try {
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;
}
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;
}
},
[notify],
);
// ── Delete ────────────────────────────────────────────────────────────────
const deleteDriver = useCallback(
async (id: string): Promise<boolean> => {
try {
const token = getStoredToken();
await driverService.delete(id, token);
dispatch({ type: "DELETE", id });
notify({ type: "success", message: "تم حذف السائق بنجاح." });
return true;
} catch (err) {
const message = err instanceof Error ? err.message : "تعذّر حذف السائق.";
notify({ type: "error", message });
return false;
}
},
[notify],
);
// ── Search helper ─────────────────────────────────────────────────────────
const handleSearch = useCallback((q: string) => {
setSearch(q);
setPage(1);
}, []);
return {
...state,
page,
search,
setPage,
handleSearch,
clearError: () => dispatch({ type: "CLEAR_ERR" }),
createDriver,
updateDriver,
deleteDriver,
notification,
reload: () => loadDrivers(page, search),
};
}

181
src/hooks/useOrder.ts Normal file
View File

@@ -0,0 +1,181 @@
"use client";
import { useCallback, useEffect, useReducer } from "react";
import { orderService } from "@/src/services/order.service";
import type {
Order,
CreateOrderPayload,
UpdateOrderPayload,
UpdateOrderStatusPayload,
} from "@/src/types/order";
// ── State ─────────────────────────────────────────────────
interface State {
orders: Order[];
total: number;
page: number;
totalPages: number;
loading: boolean;
submitting: boolean;
error: string | null;
}
type Action =
| { type: "FETCH_START" }
| { type: "FETCH_SUCCESS"; payload: { orders: Order[]; total: number; totalPages: number; page: number } }
| { type: "FETCH_ERROR"; error: string }
| { type: "SUBMIT_START" }
| { type: "SUBMIT_SUCCESS"; order: Order; isNew: boolean }
| { type: "SUBMIT_ERROR"; error: string }
| { type: "DELETE_SUCCESS"; id: string }
| { type: "CLEAR_ERROR" };
const initialState: State = {
orders: [],
total: 0,
page: 1,
totalPages: 1,
loading: false,
submitting: false,
error: null,
};
function reducer(state: State, action: Action): State {
switch (action.type) {
case "FETCH_START":
return { ...state, loading: true, error: null };
case "FETCH_SUCCESS":
return {
...state,
loading: false,
orders: action.payload.orders,
total: action.payload.total,
totalPages: action.payload.totalPages,
page: action.payload.page,
};
case "FETCH_ERROR":
return { ...state, loading: false, error: action.error };
case "SUBMIT_START":
return { ...state, submitting: true, error: null };
case "SUBMIT_SUCCESS":
return {
...state,
submitting: false,
orders: action.isNew
? [action.order, ...state.orders]
: state.orders.map((o) => (o.id === action.order.id ? action.order : o)),
};
case "SUBMIT_ERROR":
return { ...state, submitting: false, error: action.error };
case "DELETE_SUCCESS":
return { ...state, orders: state.orders.filter((o) => o.id !== action.id) };
case "CLEAR_ERROR":
return { ...state, error: null };
default:
return state;
}
}
// ── Hook ──────────────────────────────────────────────────
export function useOrders(autoFetch = true) {
const [state, dispatch] = useReducer(reducer, initialState);
const fetchOrders = useCallback(
async (page = 1, limit = 20) => {
dispatch({ type: "FETCH_START" });
try {
const res = await orderService.getAll({ page, limit });
const body = (res as unknown as { data: { data: Order[]; meta: { total: number; totalPages: number; page: number } } }).data;
dispatch({
type: "FETCH_SUCCESS",
payload: {
orders: body.data,
total: body.meta.total,
totalPages: body.meta.totalPages,
page: body.meta.page,
},
});
} catch (err: unknown) {
dispatch({
type: "FETCH_ERROR",
error: err instanceof Error ? err.message : "Failed to load orders",
});
}
},
[],
);
const createOrder = useCallback(async (payload: CreateOrderPayload) => {
dispatch({ type: "SUBMIT_START" });
try {
const res = await orderService.create(payload);
const order = (res as unknown as { data: Order }).data;
dispatch({ type: "SUBMIT_SUCCESS", order, isNew: true });
return order;
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "Failed to create order";
dispatch({ type: "SUBMIT_ERROR", error: msg });
throw new Error(msg);
}
}, []);
const updateOrder = useCallback(
async (id: string, payload: UpdateOrderPayload) => {
dispatch({ type: "SUBMIT_START" });
try {
const res = await orderService.update(id, payload);
const order = (res as unknown as { data: Order }).data;
dispatch({ type: "SUBMIT_SUCCESS", order, isNew: false });
return order;
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "Failed to update order";
dispatch({ type: "SUBMIT_ERROR", error: msg });
throw new Error(msg);
}
},
[],
);
const updateStatus = useCallback(
async (id: string, payload: UpdateOrderStatusPayload) => {
dispatch({ type: "SUBMIT_START" });
try {
const res = await orderService.updateStatus(id, payload);
const order = (res as unknown as { data: Order }).data;
dispatch({ type: "SUBMIT_SUCCESS", order, isNew: false });
return order;
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "Failed to update status";
dispatch({ type: "SUBMIT_ERROR", error: msg });
throw new Error(msg);
}
},
[],
);
const deleteOrder = useCallback(async (id: string) => {
try {
await orderService.delete(id);
dispatch({ type: "DELETE_SUCCESS", id });
} catch (err: unknown) {
dispatch({
type: "FETCH_ERROR",
error: err instanceof Error ? err.message : "Failed to delete order",
});
}
}, []);
useEffect(() => {
if (autoFetch) fetchOrders();
}, [autoFetch, fetchOrders]);
return {
...state,
fetchOrders,
createOrder,
updateOrder,
updateStatus,
deleteOrder,
clearError: () => dispatch({ type: "CLEAR_ERROR" }),
};
}

284
src/hooks/useRole.ts Normal file
View File

@@ -0,0 +1,284 @@
"use client";
import { useCallback, useEffect, useReducer, useState } from "react";
import { getStoredToken } from "@/src/lib/auth";
import { roleService } from "@/src/services/role.service";
import type { Role, RoleFormData, Permission } from "@/src/services/role.service";
// ── Notification ──────────────────────────────────────────────────────────────
export interface RoleNotification {
type: "success" | "error";
message: string;
}
// ── Table state / reducer ─────────────────────────────────────────────────────
interface TableState {
roles: Role[];
loading: boolean;
total: number;
pages: number;
error: string | null;
}
type TableAction =
| { type: "LOAD_START" }
| { type: "LOAD_OK"; roles: Role[]; total: number; pages: number }
| { type: "LOAD_ERR"; error: string }
| { type: "ADD"; role: Role }
| { type: "UPDATE"; role: Role }
| { type: "DELETE"; id: string }
| { type: "CLEAR_ERR" };
function tableReducer(s: TableState, a: TableAction): TableState {
switch (a.type) {
case "LOAD_START":
return { ...s, loading: true, error: null };
case "LOAD_OK":
return {
...s,
loading: false,
roles: a.roles,
total: a.total,
pages: a.pages,
};
case "LOAD_ERR":
return { ...s, loading: false, error: a.error };
case "ADD":
return { ...s, roles: [a.role, ...s.roles] };
case "UPDATE":
return {
...s,
roles: s.roles.map((r) => (r.id === a.role.id ? a.role : r)),
};
case "DELETE":
return { ...s, roles: s.roles.filter((r) => r.id !== a.id) };
case "CLEAR_ERR":
return { ...s, error: null };
default:
return s;
}
}
const initialState: TableState = {
roles: [],
loading: true,
total: 0,
pages: 1,
error: null,
};
// ── Main hook ─────────────────────────────────────────────────────────────────
export function useRoles() {
const [state, dispatch] = useReducer(tableReducer, initialState);
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [permissions, setPermissions] = useState<Permission[]>([]);
const [notification, setNotification] = useState<RoleNotification | null>(
null,
);
const notify = useCallback((n: RoleNotification) => {
setNotification(n);
setTimeout(() => setNotification(null), 4000);
}, []);
// ── 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: "تعذّر تحميل بيانات الأدوار. يرجى المحاولة مجدداً.",
});
}
}, []);
// ── Load permissions on mount ────────────────────────────────────────────────
useEffect(() => {
const token = getStoredToken();
roleService
.getPermissions(token)
.then((res) => {
//console.log("permissions response:", JSON.stringify(res, null, 2));
const raw = (
res as unknown as { data: { premissions: { data: Permission[] } } }
).data;
setPermissions(raw?.premissions?.data ?? []);
})
.catch(() => {});
}, []);
// ── Reload when page / search changes ────────────────────────────────────────
useEffect(() => {
loadRoles(page, search);
}, [page, search, loadRoles]);
// ── CRUD actions ─────────────────────────────────────────────────────────────
const createRole = useCallback(
async (data: RoleFormData): Promise<boolean> => {
try {
const token = getStoredToken();
const res = await roleService.create(data, token);
const role = (res as unknown as { data: Role }).data;
// Assign permissions sequentially (backend handles individually)
for (const pid of data.permissionIds) {
try {
await roleService.assignPermission(role.id, pid, token);
} catch {
/* non-fatal: skip individual failures */
}
}
// Re-fetch to get fresh data with permissions attached
await loadRoles(page, search);
notify({
type: "success",
message: "تم إنشاء الدور وتعيين الصلاحيات بنجاح.",
});
return true;
} catch (err) {
notify({
type: "error",
message: err instanceof Error ? err.message : "تعذّر إنشاء الدور.",
});
return false;
}
},
[page, search, loadRoles, notify],
);
const updateRole = useCallback(
async (
id: string,
data: RoleFormData,
currentPermIds: string[],
): Promise<boolean> => {
try {
const token = getStoredToken();
// Update basic fields
await roleService.update(id, data, token);
// Diff permissions: add new, remove removed
const toAdd = data.permissionIds.filter(
(p) => !currentPermIds.includes(p),
);
const toRemove = currentPermIds.filter(
(p) => !data.permissionIds.includes(p),
);
for (const pid of toAdd) {
try {
await roleService.assignPermission(id, pid, token);
} catch {
/* skip */
}
}
for (const pid of toRemove) {
try {
await roleService.removePermission(id, pid, token);
} catch {
/* skip */
}
}
await loadRoles(page, search);
notify({
type: "success",
message: "تم تحديث الدور والصلاحيات بنجاح.",
});
return true;
} catch (err) {
notify({
type: "error",
message: err instanceof Error ? err.message : "تعذّر تحديث الدور.",
});
return false;
}
},
[page, search, loadRoles, notify],
);
const deleteRole = useCallback(
async (id: string): Promise<boolean> => {
try {
const token = getStoredToken();
await roleService.delete(id, token);
dispatch({ type: "DELETE", id });
notify({ type: "success", message: "تم حذف الدور بنجاح." });
return true;
} catch (err) {
notify({
type: "error",
message: err instanceof Error ? err.message : "تعذّر حذف الدور.",
});
return false;
}
},
[notify],
);
const updateRolePermissionsBulk = useCallback(
async (roleId: string, permissionIds: string[]): Promise<boolean> => {
try {
const token = getStoredToken();
const res = await roleService.setPermissions(roleId, permissionIds, token);
const updated = (res as unknown as { data: Role }).data;
dispatch({ type: "UPDATE", role: updated });
notify({ type: "success", message: "تم تحديث صلاحيات الدور بنجاح." });
return true;
} catch (err) {
notify({
type: "error",
message: err instanceof Error ? err.message : "تعذّر تحديث صلاحيات الدور.",
});
return false;
}
},
[notify],
);
const handleSearch = useCallback((q: string) => {
setSearch(q);
setPage(1);
}, []);
return {
...state,
permissions,
page,
search,
setPage,
handleSearch,
clearError: () => dispatch({ type: "CLEAR_ERR" }),
createRole,
updateRole,
deleteRole,
updateRolePermissionsBulk,
notification,
};
}

227
src/hooks/useTrip.ts Normal file
View File

@@ -0,0 +1,227 @@
"use client";
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
import { getStoredToken } from "@/src/lib/auth";
import { tripService } from "@/src/services/trip.service";
import type { Trip, TripStatus, CreateTripPayload, UpdateTripPayload } from "@/src/types/trip";
// ── Notification type ──────────────────────────────────────────────────────
export interface TripNotification {
type: "success" | "error";
message: string;
}
// ── API error extractor ────────────────────────────────────────────────────
// Walks common API error shapes to pull the real backend message.
// Falls back to the provided default only when nothing useful is found.
function extractApiMessage(err: unknown, fallback: string): string {
if (typeof err === "string" && err.trim()) return err.trim();
if (err && typeof err === "object") {
const e = err as Record<string, unknown>;
// Shape: { response: { data: { message: string } } } (axios-style)
const responseData = (e["response"] as Record<string, unknown> | undefined)?.["data"];
if (responseData && typeof responseData === "object") {
const rd = responseData as Record<string, unknown>;
if (typeof rd["message"] === "string" && rd["message"].trim()) return rd["message"];
if (Array.isArray(rd["message"])) return (rd["message"] as string[]).join(" — ");
if (typeof rd["error"] === "string" && rd["error"].trim()) return rd["error"];
}
// Shape: { message: string } (plain Error / fetch wrapper)
if (typeof e["message"] === "string" && e["message"].trim()) return e["message"];
}
return fallback;
}
// ── Table state / reducer ──────────────────────────────────────────────────
interface TableState {
trips: Trip[];
loading: boolean;
total: number;
pages: number;
error: string | null;
}
type TableAction =
| { type: "LOAD_START" }
| { type: "LOAD_OK"; trips: Trip[]; total: number; pages: number }
| { type: "LOAD_ERR"; error: string }
| { type: "ADD"; trip: Trip }
| { type: "UPDATE"; trip: Trip }
| { type: "DELETE"; id: string }
| { type: "CLEAR_ERR" };
function reducer(s: TableState, a: TableAction): TableState {
switch (a.type) {
case "LOAD_START": return { ...s, loading: true, error: null };
case "LOAD_OK": return { ...s, loading: false, trips: a.trips, total: a.total, pages: a.pages };
case "LOAD_ERR": return { ...s, loading: false, error: a.error };
case "ADD": return { ...s, trips: [a.trip, ...s.trips], total: s.total + 1 };
case "UPDATE": return { ...s, trips: s.trips.map(t => (t.id === a.trip.id ? a.trip : t)) };
case "DELETE": return { ...s, trips: s.trips.filter(t => t.id !== a.id) };
case "CLEAR_ERR": return { ...s, error: null };
default: return s;
}
}
const initialState: TableState = {
trips: [],
loading: true,
total: 0,
pages: 1,
error: null,
};
// ── Main hook ──────────────────────────────────────────────────────────────
export function useTrips() {
const [state, dispatch] = useReducer(reducer, initialState);
const [search, setSearch] = useState("");
const [status, setStatus] = useState<TripStatus | "">("");
const [page, setPage] = useState(1);
const [notification, setNotification] = useState<TripNotification | null>(null);
// Ref so the dismiss timer can be cleared if a new notification arrives
// before the previous one expires — prevents stale-closure memory leaks.
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const notify = useCallback((n: TripNotification) => {
if (timerRef.current) clearTimeout(timerRef.current);
setNotification(n);
timerRef.current = setTimeout(() => setNotification(null), 4000);
}, []);
// Clear pending timer on unmount
useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
// ── 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, "تعذّر تحميل بيانات الرحلات. يرجى المحاولة مجدداً."),
});
}
},
[],
);
useEffect(() => {
loadTrips(page, search, status);
}, [page, search, status, loadTrips]);
// ── Create ────────────────────────────────────────────────────────────────
const createTrip = useCallback(
async (payload: CreateTripPayload): Promise<boolean> => {
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],
);
// ── Update ────────────────────────────────────────────────────────────────
const updateTrip = useCallback(
async (id: string, payload: UpdateTripPayload): Promise<boolean> => {
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],
);
// ── Delete ────────────────────────────────────────────────────────────────
const deleteTrip = useCallback(
async (id: string): Promise<boolean> => {
try {
const token = getStoredToken();
await tripService.delete(id, token);
dispatch({ type: "DELETE", id });
notify({ type: "success", message: "تم حذف الرحلة بنجاح." });
return true;
} catch (err) {
notify({ type: "error", message: extractApiMessage(err, "تعذّر حذف الرحلة.") });
return false;
}
},
[notify],
);
// ── Helpers ───────────────────────────────────────────────────────────────
const handleSearch = useCallback((q: string) => {
setSearch(q);
setPage(1);
}, []);
const handleStatusFilter = useCallback((s: TripStatus | "") => {
setStatus(s);
setPage(1);
}, []);
return {
...state,
page,
search,
status,
setPage,
handleSearch,
handleStatusFilter,
clearError: () => dispatch({ type: "CLEAR_ERR" }),
createTrip,
updateTrip,
deleteTrip,
notification,
dismissNotification: () => {
if (timerRef.current) clearTimeout(timerRef.current);
setNotification(null);
},
reload: () => loadTrips(page, search, status),
};
}

163
src/hooks/useUser.ts Normal file
View File

@@ -0,0 +1,163 @@
"use client";
import { useCallback, useEffect, useReducer, useState } from "react";
import { getStoredToken } from "@/src/lib/auth";
import { userService } from "@/src/services/user.service";
import type { Branch } from "@/src/types/branch";
import type { Role } from "@/src/types/role";
import type {
TableAction,
TableState,
User,
UserFormData,
} from "@/src/types/user";
import type { ToastNotification } from "@/src/Components/UI"
export type Notification = ToastNotification;
// ── Reducer ──────────────────────────────────────
function tableReducer(s: TableState, a: TableAction): TableState {
switch (a.type) {
case "LOAD_START": return { ...s, loading: true, error: null };
case "LOAD_OK": return { ...s, loading: false, users: a.users, total: a.total, pages: a.pages };
case "LOAD_ERR": return { ...s, loading: false, error: a.error };
case "ADD": return { ...s, users: [a.user, ...s.users] };
case "UPDATE": return { ...s, users: s.users.map(u => u.id === a.user.id ? a.user : u) };
case "DELETE": return { ...s, users: s.users.filter(u => u.id !== a.id) };
case "CLEAR_ERR": return { ...s, error: null };
default: return s;
}
}
// ── first status ─────────────────────────────────────────────────────────
const initialState: TableState = {
users: [], loading: true, total: 0, pages: 1, error: null,
};
// ── main hook ──────────────────────────────────────────────────────────────
export function useUsers() {
const [state, dispatch] = useReducer(tableReducer, initialState);
const [search, setSearch] = useState("");
const [page, setPage] = useState(1);
const [roles, setRoles] = useState<Role[]>([]);
const [branches, setBranches] = useState<Branch[]>([]);
const [notification, setNotification] = useState<ToastNotification | null>(null);
// notify with auto-dismiss after 4s
const notify = useCallback((n: ToastNotification) => {
setNotification(n);
setTimeout(() => setNotification(null), 4000);
}, []);
// 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 token = getStoredToken();
userService.getRoles(token)
.then(res => setRoles((res.data ?? res).data ?? []))
.catch(() => {});
userService.getBranches(token)
.then(res => setBranches((res.data ?? res).data ?? []))
.catch(() => {});
}, []);
// reload users when page or search changes
useEffect(() => {
loadUsers(page, search);
}, [page, search, loadUsers]);
// create new user
const createUser = useCallback(async (data: UserFormData): Promise<boolean> => {
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]);
// update existing user
const updateUser = useCallback(async (id: string, data: UserFormData): Promise<boolean> => {
try {
const token = getStoredToken();
const res = await userService.update(id, data, token);
dispatch({ type: "UPDATE", 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]);
// delete user
const deleteUser = useCallback(async (id: string): Promise<boolean> => {
try {
const token = getStoredToken();
await userService.delete(id, token);
dispatch({ type: "DELETE", id });
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 handleSearch = useCallback((q: string) => {
setSearch(q);
setPage(1);
}, []);
const clearError = useCallback(() => dispatch({ type: "CLEAR_ERR" }), []);
return {
//table data and status
...state,
// dropdown data for forms
roles,
branches,
// pagination and search state
page,
search,
setPage,
handleSearch,
clearError,
//main actions
createUser,
updateUser,
deleteUser,
// notifications for success/error messages
notification,
};
}