feat(userAccount): enhance user profile handling with avatar upload and additional user data
This commit is contained in:
+25
-8
@@ -27,20 +27,37 @@ export default async function Dashboard({ searchParams }) {
|
|||||||
// may still carry the OTP uuid, which 404s against user-profile.
|
// may still carry the OTP uuid, which 404s against user-profile.
|
||||||
const userInfo = safeJsonParse(cookieStore.get("userInfo")?.value);
|
const userInfo = safeJsonParse(cookieStore.get("userInfo")?.value);
|
||||||
const userUuid = userInfo?.uuid || cookieStore.get("uuid")?.value;
|
const userUuid = userInfo?.uuid || cookieStore.get("uuid")?.value;
|
||||||
let profile = null;
|
const authHeader = { headers: { Authorization: `Bearer ${token.value}` } };
|
||||||
if (userUuid) {
|
const API = process.env.NEXT_PUBLIC_API_URL;
|
||||||
profile = await fetchReq(
|
|
||||||
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/user-profile/${userUuid}`,
|
const [profile, appointmentsRes, paymentsRes] = await Promise.all([
|
||||||
{ headers: { Authorization: `Bearer ${token.value}` } }
|
userUuid
|
||||||
);
|
? fetchReq(`${API}/api/v1/user-profile/${userUuid}`, authHeader)
|
||||||
}
|
: Promise.resolve(null),
|
||||||
|
fetchReq(`${API}/api/v1/my/appointments?page=1&limit=1`, authHeader),
|
||||||
|
fetchReq(`${API}/api/v1/my/payments?page=1&limit=200`, authHeader),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const numberOfTurns = appointmentsRes?.meta?.totalRecords ?? 0;
|
||||||
|
const payments = paymentsRes?.data ?? [];
|
||||||
|
const totalRials = payments
|
||||||
|
.filter((p) => p.status === "success")
|
||||||
|
.reduce((sum, p) => sum + (Number(p.amount_rials) || 0), 0);
|
||||||
|
const totalTransactions = Math.round(totalRials / 10); // ریال → تومان
|
||||||
|
|
||||||
|
const extras = {
|
||||||
|
mobile: userInfo?.mobile_number ?? "",
|
||||||
|
realName: userInfo?.realName ?? "",
|
||||||
|
number_of_turns: numberOfTurns,
|
||||||
|
total_transactions: totalTransactions,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Content
|
<Content
|
||||||
logged={token.value}
|
logged={token.value}
|
||||||
params={awaitedSearchParams}
|
params={awaitedSearchParams}
|
||||||
matchedCity={matchedCity}
|
matchedCity={matchedCity}
|
||||||
user={buildPatientUser(profile)}
|
user={buildPatientUser(profile, extras)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ function Head({ user }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-[8px] items-start justify-start">
|
<div className="flex flex-col gap-[8px] items-start justify-start">
|
||||||
<p className="text-[#3B3B3B] text-nowrap text-[16px] font-medium">
|
<p className="text-[#3B3B3B] text-nowrap text-[16px] font-medium">
|
||||||
{user.total_transactions}{" "}
|
{Number(user?.total_transactions || 0).toLocaleString("fa-IR")}{" "}
|
||||||
<span className="text-[12px]">تومان</span>
|
<span className="text-[12px]">تومان</span>
|
||||||
</p>
|
</p>
|
||||||
<h1 className="text-[#616161] text-nowrap text-[14px] font-normal">
|
<h1 className="text-[#616161] text-nowrap text-[14px] font-normal">
|
||||||
@@ -51,7 +51,7 @@ function Head({ user }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-[8px] items-start justify-start">
|
<div className="flex flex-col gap-[8px] items-start justify-start">
|
||||||
<p className="text-[#3B3B3B] text-nowrap text-[16px] font-medium">
|
<p className="text-[#3B3B3B] text-nowrap text-[16px] font-medium">
|
||||||
{user.number_of_turns}
|
{Number(user?.number_of_turns || 0).toLocaleString("fa-IR")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[#616161] text-nowrap text-[14px] font-normal">
|
<p className="text-[#616161] text-nowrap text-[14px] font-normal">
|
||||||
تعداد نوبت های شما
|
تعداد نوبت های شما
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function Form({ insurance, errors, changeData, information }) {
|
|||||||
name="work_phone"
|
name="work_phone"
|
||||||
isTransparent={true}
|
isTransparent={true}
|
||||||
changeData={changeData}
|
changeData={changeData}
|
||||||
data={parsedUserInfo?.username}
|
data={parsedUserInfo?.mobile_number ?? parsedUserInfo?.username}
|
||||||
/>
|
/>
|
||||||
</Content>
|
</Content>
|
||||||
<Content
|
<Content
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
import Form from "./form";
|
import Form from "./form";
|
||||||
import { request } from "@/services/response";
|
import { request } from "@/services/response";
|
||||||
import ButtonSendData from "./ButtonSendData";
|
import ButtonSendData from "./ButtonSendData";
|
||||||
import { changeDateType, getParsedUserInfo } from "@/helper";
|
import { changeDateType, getParsedUserInfo, imageUrl } from "@/helper";
|
||||||
import { disease } from "./form/disease";
|
import { disease } from "./form/disease";
|
||||||
import LoadingComponent from "@/app/component/LoadingComponent";
|
import LoadingComponent from "@/app/component/LoadingComponent";
|
||||||
|
|
||||||
@@ -11,6 +13,26 @@ function Information({ information, setInformation }) {
|
|||||||
const [insurance, setInsurance] = useState();
|
const [insurance, setInsurance] = useState();
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [loadingInfo, setLoadingInfo] = useState(true);
|
const [loadingInfo, setLoadingInfo] = useState(true);
|
||||||
|
const [avatarUploading, setAvatarUploading] = useState(false);
|
||||||
|
|
||||||
|
const handleAvatarChange = async (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
setAvatarUploading(true);
|
||||||
|
try {
|
||||||
|
const res = await request.uploadUserAvatar(file);
|
||||||
|
const url = res?.data?.avatar || res?.data?.url;
|
||||||
|
if (url) {
|
||||||
|
setInformation((prev) => ({ ...(prev || {}), avatar: url }));
|
||||||
|
toast.success("عکس پروفایل بهروزرسانی شد");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
toast.error("خطا در آپلود عکس");
|
||||||
|
} finally {
|
||||||
|
setAvatarUploading(false);
|
||||||
|
e.target.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parsedUserInfo = getParsedUserInfo();
|
const parsedUserInfo = getParsedUserInfo();
|
||||||
@@ -101,6 +123,25 @@ function Information({ information, setInformation }) {
|
|||||||
return (
|
return (
|
||||||
<LoadingComponent loading={loadingInfo}>
|
<LoadingComponent loading={loadingInfo}>
|
||||||
<div className="opacity-page">
|
<div className="opacity-page">
|
||||||
|
<div className="flex items-center gap-[16px] mb-[24px]">
|
||||||
|
<Image
|
||||||
|
src={imageUrl(information?.avatar, "/assets/images/profile-user.png")}
|
||||||
|
alt="avatar"
|
||||||
|
width={72}
|
||||||
|
height={72}
|
||||||
|
className="w-[72px] h-[72px] rounded-full object-cover border border-[#EFEFEF]"
|
||||||
|
/>
|
||||||
|
<label className="cursor-pointer text-[#5559CE] text-[14px] font-medium">
|
||||||
|
{avatarUploading ? "در حال آپلود..." : "تغییر عکس"}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
hidden
|
||||||
|
disabled={avatarUploading}
|
||||||
|
onChange={handleAvatarChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<Form
|
<Form
|
||||||
errors={errors}
|
errors={errors}
|
||||||
insurance={insurance}
|
insurance={insurance}
|
||||||
|
|||||||
@@ -38,11 +38,16 @@ function Turns({ user, setIsTurnsDetails, loading }) {
|
|||||||
const items = response?.data?.data;
|
const items = response?.data?.data;
|
||||||
|
|
||||||
if (Array.isArray(items)) {
|
if (Array.isArray(items)) {
|
||||||
setAppointments(items);
|
// API برمیگرداند doctor_name/doctor_uuid (تخت)؛ کامپوننتها doctor.name (تودرتو) میخواهند.
|
||||||
|
const normalized = items.map((a) => ({
|
||||||
|
...a,
|
||||||
|
doctor: { uuid: a.doctor_uuid, name: a.doctor_name, specialties: [] },
|
||||||
|
}));
|
||||||
|
setAppointments(normalized);
|
||||||
} else {
|
} else {
|
||||||
setAppointments([]);
|
setAppointments([]);
|
||||||
}
|
}
|
||||||
setTotalPages(1);
|
setTotalPages(response?.meta?.totalPages || 1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching appointments:", error);
|
console.error("Error fetching appointments:", error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -51,16 +51,19 @@ export function adaptRepresentationDashboard(dashboard) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildPatientUser(profile) {
|
export function buildPatientUser(profile, extras = {}) {
|
||||||
// GET /user-profile is double-nested: { data: { data: {...} } } (after fetchReq → response.data)
|
// GET /user-profile is double-nested: { data: { data: {...} } } (after fetchReq → response.data)
|
||||||
const data = profile?.data?.data ?? profile?.data ?? profile ?? {};
|
const data = profile?.data?.data ?? profile?.data ?? profile ?? {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
profile: data.avatar ?? "/assets/images/profile-user.png",
|
profile: data.avatar ?? extras.avatar ?? "/assets/images/profile-user.png",
|
||||||
name: [data.label ?? data.name, data.family].filter(Boolean).join(" ").trim(),
|
name:
|
||||||
phone: data.phone ?? data.mobile ?? "",
|
[data.label ?? data.name, data.family].filter(Boolean).join(" ").trim() ||
|
||||||
total_transactions: 0,
|
extras.realName ||
|
||||||
number_of_turns: 0,
|
"",
|
||||||
|
phone: data.phone ?? data.mobile ?? extras.mobile ?? "",
|
||||||
|
total_transactions: extras.total_transactions ?? 0,
|
||||||
|
number_of_turns: extras.number_of_turns ?? 0,
|
||||||
national_code: data.national_code ?? "",
|
national_code: data.national_code ?? "",
|
||||||
gender: data.gender ?? "",
|
gender: data.gender ?? "",
|
||||||
date_of_birth: data.birthday ?? "",
|
date_of_birth: data.birthday ?? "",
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ export const request = {
|
|||||||
postUserProfile: (data) => api.post("api/v1/user-profile", data, { requireAuth: true }),
|
postUserProfile: (data) => api.post("api/v1/user-profile", data, { requireAuth: true }),
|
||||||
patchUserProfile: (data, uuid) =>
|
patchUserProfile: (data, uuid) =>
|
||||||
api.patch(`api/v1/user-profile/${uuid}`, data, { requireAuth: true }),
|
api.patch(`api/v1/user-profile/${uuid}`, data, { requireAuth: true }),
|
||||||
|
uploadUserAvatar: (file) =>
|
||||||
|
api.post("api/v1/user-profile/avatar", file, {
|
||||||
|
requireAuth: true,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
"Content-Disposition": `filename="${file?.name || "avatar.jpg"}"`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
getUserInfo: (headers) => api.get("oauth/userinfo", { ...headers, requireAuth: true }),
|
getUserInfo: (headers) => api.get("oauth/userinfo", { ...headers, requireAuth: true }),
|
||||||
getInsuranceType: () =>
|
getInsuranceType: () =>
|
||||||
api.get(`api/v1/insurances?type=basic`, { requireAuth: true }),
|
api.get(`api/v1/insurances?type=basic`, { requireAuth: true }),
|
||||||
|
|||||||
Reference in New Issue
Block a user