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:
216
src/hooks/useCars.ts
Normal file
216
src/hooks/useCars.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
184
src/hooks/useClientAddresses.ts
Normal file
184
src/hooks/useClientAddresses.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
45
src/hooks/useClientOrders.ts
Normal file
45
src/hooks/useClientOrders.ts
Normal 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
191
src/hooks/useClients.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
49
src/hooks/useDashboardSummary.ts
Normal file
49
src/hooks/useDashboardSummary.ts
Normal 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
211
src/hooks/useDriver.ts
Normal 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
181
src/hooks/useOrder.ts
Normal 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
284
src/hooks/useRole.ts
Normal 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
227
src/hooks/useTrip.ts
Normal 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
163
src/hooks/useUser.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user