+
+ {/* بطاقة البيانات الأساسية */}
-

{
- (e.target as HTMLImageElement).src = "/images/avatar-placeholder.png";
- }}
- />
+
+ {user.name?.trim().charAt(0) ?? "?"}
+
{user.name}
+ {user.role?.name && (
+
+ {user.role.name}
+
+ )}
{/* عرض البيانات بشكل read-only بدون أي فورم أو زرار تعديل */}
@@ -57,6 +70,54 @@ export default function ProfilePage() {
))}
+
+ {/* بطاقة الدور والصلاحيات */}
+ {user.role && (
+
+
+
الدور والصلاحيات
+ {user.role.description && (
+
{user.role.description}
+ )}
+
+
+ {modules.length === 0 ? (
+
لا توجد صلاحيات مسجّلة لهذا الدور.
+ ) : (
+
+ {modules.map((mod) => (
+
+
+
+ {mod}
+
+ {permissionsByModule[mod].length}
+
+
+
+ ▾
+
+
+
+ {permissionsByModule[mod].map((name) => (
+
+ {name}
+
+ ))}
+
+
+ ))}
+
+ )}
+
+ )}
);
}
\ No newline at end of file
diff --git a/src/hooks/useClientAddresses.ts b/src/hooks/useClientAddresses.ts
index 0a6f337..8d9a48d 100644
--- a/src/hooks/useClientAddresses.ts
+++ b/src/hooks/useClientAddresses.ts
@@ -19,10 +19,14 @@ import { Notification } from "../types/notif";
function normalizeAddress(raw: unknown): ClientAddress {
const data = raw as { id?: string; _id?: string; [key: string]: unknown };
+
+ // FIXED: fallback to "" so the type is always `string`, never `string | undefined`
+ const id = data.id ?? data._id ?? "";
+
return {
...data,
- id: data.id ?? data._id,
- };
+ id,
+ } as ClientAddress;
}
function normalizeAddresses(rawList: unknown[]): ClientAddress[] {
@@ -54,6 +58,14 @@ function reducer(s: AddressTableState, a: AddressTableAction): AddressTableState
};
case "CLEAR_ERR":
return { ...s, error: null };
+ case "SET_PRIMARY":
+ return {
+ ...s,
+ addresses: s.addresses.map((addr) => ({
+ ...addr,
+ isPrimary: addr.id === a.id,
+ })),
+ };
default:
return s;
}
@@ -69,6 +81,9 @@ const initialState: AddressTableState = {
export function useClientAddresses(clientId: string) {
const [state, dispatch] = useReducer(reducer, initialState);
const [notification, setNotification] = useState
(null);
+ // Tracks which address is mid-request so its button can disable itself
+ // and duplicate clicks are ignored while a request is in flight.
+ const [settingPrimaryId, setSettingPrimaryId] = useState(null);
const notify = useCallback((n: Notification) => {
setNotification(n);
@@ -76,25 +91,33 @@ export function useClientAddresses(clientId: string) {
}, []);
// ── Fetch ────────────────────────────────────────────────────────────────
- const loadAddresses = useCallback(async () => {
- if (!clientId) return;
- dispatch({ type: "LOAD_START" });
- try {
- const token = getStoredToken();
- const res = await clientAddressService.getAll(clientId, token);
- const payload = (res as unknown as { data?: unknown }).data ?? res;
- const rawList = Array.isArray(payload) ? payload : ((payload as { data?: unknown }).data ?? []);
- dispatch({
- type: "LOAD_OK",
- addresses: normalizeAddresses(rawList),
- });
- } catch {
- dispatch({
- type: "LOAD_ERR",
- error: "تعذّر تحميل العناوين. يرجى المحاولة مجدداً.",
- });
- }
- }, [clientId]);
+ const loadAddresses = useCallback(async () => {
+ if (!clientId) return;
+ dispatch({ type: "LOAD_START" });
+ try {
+ const token = getStoredToken();
+ const res = await clientAddressService.getAll(clientId, token);
+ const payload = (res as unknown as { data?: unknown }).data ?? res;
+
+ // FIXED: explicit Array.isArray checks instead of relying on
+ // TS to narrow the ternary's return type on its own.
+ const rawList: unknown[] = Array.isArray(payload)
+ ? payload
+ : Array.isArray((payload as { data?: unknown })?.data)
+ ? ((payload as { data?: unknown }).data as unknown[])
+ : [];
+
+ dispatch({
+ type: "LOAD_OK",
+ addresses: normalizeAddresses(rawList),
+ });
+ } catch {
+ dispatch({
+ type: "LOAD_ERR",
+ error: "تعذّر تحميل العناوين. يرجى المحاولة مجدداً.",
+ });
+ }
+}, [clientId]);
useEffect(() => {
loadAddresses();
@@ -160,6 +183,33 @@ export function useClientAddresses(clientId: string) {
[clientId, notify],
);
+ // ── SET PRIMARY ──────────────────────────────────────────────────────────
+ const setPrimaryAddress = useCallback(
+ async (id: string): Promise => {
+ if (settingPrimaryId) return false; // guard against overlapping requests
+ setSettingPrimaryId(id);
+ try {
+ const token = getStoredToken();
+ await clientAddressService.setPrimary(id, token);
+ // We already know the shape of the change (target -> true, everyone
+ // else -> false), so we update local state directly instead of
+ // refetching the whole list.
+ dispatch({ type: "SET_PRIMARY", id });
+ notify({ type: "success", message: "تم تعيين العنوان كعنوان أساسي." });
+ return true;
+ } catch (err) {
+ notify({
+ type: "error",
+ message: err instanceof Error ? err.message : "تعذّر تعيين العنوان كأساسي.",
+ });
+ return false;
+ } finally {
+ setSettingPrimaryId(null);
+ }
+ },
+ [notify, settingPrimaryId],
+ );
+
return {
...state,
notification,
@@ -167,6 +217,8 @@ export function useClientAddresses(clientId: string) {
createAddress,
updateAddress,
deleteAddress,
+ setPrimaryAddress,
+ settingPrimaryId,
reload: loadAddresses,
};
}
\ No newline at end of file
diff --git a/src/hooks/useCurrentUser.ts b/src/hooks/useCurrentUser.ts
index b24b936..e2dc148 100644
--- a/src/hooks/useCurrentUser.ts
+++ b/src/hooks/useCurrentUser.ts
@@ -16,7 +16,10 @@ export function useCurrentUser() {
try {
const token = getStoredToken();
const res = await userService.getMe(token);
- const u = extractMeUser(res.data ?? res);
+ // ملاحظة: مبقاش بنعمل res.data ?? res هنا قبل التمرير.
+ // extractMeUser هي المسؤولة الوحيدة عن تفكيك شكل الـ response،
+ // فبنبعتلها الـ res زي ما هو عشان نتجنب تفكيك مزدوج.
+ const u = extractMeUser(res);
if (!u) throw new Error("لا توجد بيانات");
setUser(u as UserMe);
} catch {
diff --git a/src/services/clientAddress.service.ts b/src/services/clientAddress.service.ts
index 3c3bce6..5eb32c8 100644
--- a/src/services/clientAddress.service.ts
+++ b/src/services/clientAddress.service.ts
@@ -65,7 +65,14 @@ export const clientAddressService = {
delete: (clientId: string, addressId: string, token: string | null) =>
del(addressBase(addressId), token),
-
+ /**
+ * Set an address as primary for its client.
+ * PATCH /v1/addresses/:id/primary — no request body.
+ * Backend unsets isPrimary on all sibling addresses automatically.
+ */
+ setPrimary: (addressId: string, token: string | null) =>
+ patch>(`${addressBase(addressId)}/primary`, {}, token),
+
};
diff --git a/src/services/user.service.ts b/src/services/user.service.ts
index 5579609..4720205 100644
--- a/src/services/user.service.ts
+++ b/src/services/user.service.ts
@@ -1,17 +1,16 @@
-
import { get, post, put, del } from "./api";
-import type { ApiListResponse, User, UserFormData, UserResponse } from "@/src/types/user";
+import type { ApiListResponse, User, UserFormData, UserResponse, UserMe } from "@/src/types/user";
import type { Role } from "@/src/types/role";
import type { Branch } from "@/src/types/branch";
+// الشكل الحقيقي لاستجابة /users/me:
+// { success, message, responseAt, data: }
+// أي "data" هنا هي بيانات اليوزر مباشرة، مفيش data.data متداخلة.
export interface MeApiResponse {
success: boolean;
message: string;
responseAt: string;
- data: {
- data: User[] | User;
- meta?: { total: number; page: number; limit: number; totalPages: number };
- };
+ data: UserMe;
}
@@ -60,10 +59,25 @@ export const userService = {
getMe: (token: string | null) =>
get("users/me", token),
};
-export function extractMeUser(res: MeApiResponse): User | null {
- const d = res.data?.data;
- if (Array.isArray(d)) return d[0] ?? null;
- return d ?? null;
+
+/**
+ * بتفكك استجابة /users/me لأي شكل ييجي بيه من السيرفر:
+ * - سواء الـ get() بيرجع الـ body مباشرة: { success, data: }
+ * - أو بيرجع wrapper شكله axios-like: { data: { success, data: } }
+ * فبناخد أول data موجودة (لو فيها success/message نبقى في المكان الصح)،
+ * وبعدين ناخد الـ data اللي جواها (بيانات اليوزر الفعلية).
+ * ملحوظة: مفيش تفكيك مزدوج بره الدالة دي، وأي حد بينادي عليها
+ * لازم يبعتلها الـ response زي ما هو من غير ما يعمل res.data قبلها.
+ */
+export function extractMeUser(res: MeApiResponse | { data: MeApiResponse }): UserMe | null {
+ const body: MeApiResponse = (res as { data: MeApiResponse })?.data?.data
+ ? (res as { data: MeApiResponse }).data
+ : (res as MeApiResponse);
+
+ const d = body?.data;
+ if (!d) return null;
+ if (Array.isArray(d)) return (d[0] as UserMe) ?? null;
+ return d as UserMe;
}
/** Converting the form data into a payload suitable for the API */
@@ -81,4 +95,4 @@ function buildPayload(
if (isNew && data.password) payload.password = data.password;
else if (!isNew && data.password) payload.password = data.password;
return payload;
-}
+}
\ No newline at end of file
diff --git a/src/types/client_adresses.ts b/src/types/client_adresses.ts
index 75e6610..be06e28 100644
--- a/src/types/client_adresses.ts
+++ b/src/types/client_adresses.ts
@@ -80,7 +80,9 @@ export type AddressTableAction =
| { type: "ADD"; address: ClientAddress }
| { type: "UPDATE"; address: ClientAddress }
| { type: "DELETE"; id: string }
- | { type: "CLEAR_ERR" };
+ | { type: "CLEAR_ERR" }
+ // ADDED: Set primary address — flips isPrimary across the whole list in one dispatch
+ | { type: "SET_PRIMARY"; id: string };
// ─── Archive: Client Addresses ─────────────────────────────────────────────
// Archived address resource returned by GET /addresses/archived.
diff --git a/src/types/user.ts b/src/types/user.ts
index c8c2a4e..3d31edc 100644
--- a/src/types/user.ts
+++ b/src/types/user.ts
@@ -6,8 +6,8 @@ export interface User {
phone: string;
isActive: boolean;
createdAt: string;
- role?: { id?: string; name: string };
- branch?: { id?: string; name: string };
+ role?: { id?: string; name: string } | null;
+ branch?: { id?: string; name: string } | null;
}
export interface UserResponse {
data: User;
@@ -113,8 +113,28 @@ export interface ApiErrorResponse {
};
}
+export interface Permission {
+ name: string;
+ slug: string;
+ module: string;
+}
+
+export interface RolePermissionEntry {
+ permission: Permission;
+}
+
+export interface UserRoleDetail {
+ name: string;
+ description?: string;
+ permissions?: RolePermissionEntry[];
+ isActive?: boolean;
+ isDeleted?: boolean;
+}
+
export interface UserMe extends User {
photo: string | null;
isDeleted: boolean;
updatedAt: string;
+ role?: UserRoleDetail | null;
+ branch?: { id?: string; name: string } | null;
}
\ No newline at end of file