update client address add primary address and update client address , fixed profile page and update user layer

This commit is contained in:
m7amedez5511
2026-07-13 17:21:42 +03:00
parent 5707521b92
commit 0eebef3ba7
8 changed files with 233 additions and 50 deletions

View File

@@ -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<Notification | null>(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<string | null>(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<boolean> => {
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,
};
}

View File

@@ -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 {

View File

@@ -65,7 +65,14 @@ export const clientAddressService = {
delete: (clientId: string, addressId: string, token: string | null) =>
del<void>(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<ApiResponse<ClientAddress>>(`${addressBase(addressId)}/primary`, {}, token),
};

View File

@@ -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: <UserMe> }
// أي "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<MeApiResponse>("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: <user> }
* - أو بيرجع wrapper شكله axios-like: { data: { success, data: <user> } }
* فبناخد أول 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;
}
}

View File

@@ -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.

View File

@@ -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;
}