diff --git a/app/dashboard/clients/[clientId]/addresses/page.tsx b/app/dashboard/clients/[clientId]/addresses/page.tsx index 8410c72..2111610 100644 --- a/app/dashboard/clients/[clientId]/addresses/page.tsx +++ b/app/dashboard/clients/[clientId]/addresses/page.tsx @@ -40,12 +40,14 @@ function labelIcon(label: string): string { // ─── AddressCard ─────────────────────────────────────────────────────────── interface AddressCardProps { - address: ClientAddress; - onEdit: () => void; - onDelete: () => void; + address: ClientAddress; + onEdit: () => void; + onDelete: () => void; + onSetPrimary: () => void; + settingPrimary: boolean; // true only while THIS card's request is in flight } -function AddressCard({ address, onEdit, onDelete }: AddressCardProps) { +function AddressCard({ address, onEdit, onDelete, onSetPrimary, settingPrimary }: AddressCardProps) { const { details, contactPerson } = address; const { street, @@ -195,6 +197,19 @@ function AddressCard({ address, onEdit, onDelete }: AddressCardProps) { تعديل + + {!address.isPrimary && ( + + {settingPrimary ? "جارٍ التعيين…" : "تعيين كأساسي"} + + )} + { + await setPrimaryAddress(address.id); + }; + // ── Client edit handler ────────────────────────────────────────────────── const handleClientEditSubmit = async ( data: ClientFormData, @@ -661,6 +683,8 @@ export default function ClientAddressesPage() { address={addr} onEdit={() => setAddrFormTarget(addr)} onDelete={() => setDeleteTarget(addr)} + onSetPrimary={() => handleSetPrimary(addr)} + settingPrimary={settingPrimaryId === addr.id} /> ))} diff --git a/app/dashboard/profile/page.tsx b/app/dashboard/profile/page.tsx index 15308d2..abf6410 100644 --- a/app/dashboard/profile/page.tsx +++ b/app/dashboard/profile/page.tsx @@ -32,19 +32,32 @@ export default function ProfilePage() { { label: "آخر تحديث", value: new Date(user.updatedAt).toLocaleDateString("ar-SA") }, ]; + // تجميع الصلاحيات حسب الموديول عشان تبقى منظمة وسهلة القراءة + const permissionsByModule = (user.role?.permissions ?? []).reduce>( + (acc, entry) => { + const mod = entry.permission.module || "أخرى"; + if (!acc[mod]) acc[mod] = []; + acc[mod].push(entry.permission.name); + return acc; + }, + {}, + ); + const modules = Object.keys(permissionsByModule); + return ( -
+
+ {/* بطاقة البيانات الأساسية */}
- {user.name} { - (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