update-full

This commit is contained in:
TuanVT
2026-06-06 01:02:07 +07:00
parent 7791e1d6a8
commit d4194d6f12
30 changed files with 1018 additions and 391 deletions
@@ -0,0 +1,175 @@
"use client";
import { memo, useMemo } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import icons from "@/constant/images/icons";
import { PATH } from "@/constant/config";
import { QUERY_KEY } from "@/constant/config/enum";
import { useQuery } from "@tanstack/react-query";
import { httpRequest } from "@/services";
import Moment from "react-moment";
import {
Cake,
Phone,
MapPin,
Mail,
SquareUser,
ShieldUser,
} from "lucide-react";
import userServices from "@/services/userServices";
import CustomButton from "@/components/customs/custom-button";
import profileServices, { ProfileResponse } from "@/services/profileServices";
import { useSelector } from "react-redux";
import { RootState } from "@/redux/store";
function MainPageProfile() {
const router = useRouter();
const { infoUser } = useSelector((state: RootState) => state.user);
const profileQuery = useQuery<any>({
queryKey: [QUERY_KEY.table_list_profile],
queryFn: async () => {
const res = await httpRequest<any>({
showMessageFailed: true,
http: profileServices.getProfile(), // Đảm bảo endpoint trỏ về /api/v1/UserProfile/list
});
// Trả về đúng object chứa items theo cấu trúc của API
return (
res || {
items: [],
page: 1,
pageSize: 10,
total: 0,
totalPages: 0,
}
);
},
});
// Tìm kiếm thông tin profile của user hiện tại đang đăng nhập bằng useMemo
// eslint-disable-next-line react-hooks/preserve-manual-memoization
const currentUserProfile = useMemo(() => {
const listItems = profileQuery.data?.items || [];
if (!infoUser?.userId) return null;
// So khớp accountId của API trả về với userId trong Redux (infoUser)
return (
listItems.find((item: any) => item.accountId === infoUser.userId) || null
);
}, [profileQuery.data, infoUser?.userId]);
return (
<div className="flex flex-col p-6 bg-white/80 rounded-2xl">
{/* HEADER */}
<div className="flex items-center justify-between w-full">
<h2 className="text-xl font-semibold">Thông tin tài khoản</h2>
<div className="flex gap-2.5">
<CustomButton
variant="midnightBlue"
size="md"
rounded="full"
fullWidth={false}
href={PATH.UPDATEPROFILE}
>
Chỉnh sửa
</CustomButton>
</div>
</div>
{/* AVATAR */}
<div className="flex gap-4 mt-6 mb-4">
<Image
src={
currentUserProfile?.avatarUrl
? `${process.env.NEXT_PUBLIC_IMAGE}${currentUserProfile.avatarUrl}`
: icons.avatar
}
alt="avatar"
width={120}
height={120}
className="object-cover rounded-lg "
/>
</div>
{/* MAIN */}
<div className="flex gap-4 mt-6 flex-col md:flex-row">
{/* BASIC INFO */}
<div className="flex flex-col border border-gray-200 bg-gray-50 rounded-xl w-full md:w-1/2 p-5 text-gray-600">
<div className="text-xl font-semibold mb-2 text-gray-800">
Thông tin bản
</div>
<div className="flex flex-col gap-3">
{/* NAME */}
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
<p className="flex gap-2">
<SquareUser size={24} />
Họ tên
</p>
<span className="text-gray-900">
{currentUserProfile?.fullName || "---"}
</span>
</div>
{/* DOB */}
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
<p className="flex gap-2">
<Cake size={24} />
Ngày sinh
</p>
<span className="text-gray-900">
{currentUserProfile?.birthDate ? (
<Moment format="DD/MM/YYYY">
{currentUserProfile.birthDate}
</Moment>
) : (
"---"
)}
</span>
</div>
</div>
</div>
{/* CONTACT */}
<div className="flex flex-col border border-gray-200 bg-gray-50 rounded-xl w-full md:w-1/2 p-5 text-gray-600">
<div className="text-xl font-semibold mb-2 text-gray-800">
Liên hệ
</div>
<div className="flex flex-col gap-3">
{/* EMAIL */}
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
<p className="flex gap-2">
<Mail size={24} />
Email
</p>
<span className="text-gray-900">
{currentUserProfile?.email || "---"}
</span>
</div>
{/* PHONE */}
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
<p className="flex gap-2">
<Phone size={24} />
Số điện thoại
</p>
<span className="text-gray-900">
{currentUserProfile?.phone || "---"}
</span>
</div>
</div>
</div>
</div>
</div>
);
}
export default memo(MainPageProfile);
@@ -0,0 +1 @@
export { default } from "./MainPageProfile";
@@ -0,0 +1,300 @@
"use client";
import { Fragment, memo, useEffect, useMemo, useState } from "react";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import Button from "@/components/customs/custom-button";
import UploadAvatar from "@/components/utils/UploadAvatar";
import GridColumn from "@/components/layouts/GridColumn";
import Loading from "@/components/customs/custom-loading";
import { PATH } from "@/constant/config";
import { QUERY_KEY } from "@/constant/config/enum";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { httpRequest } from "@/services";
import uploadServices from "@/services/uploadServices";
import profileServices from "@/services/profileServices";
import { toastWarn } from "@/common/funcs/toast";
import { useSelector } from "react-redux";
import { RootState } from "@/redux/store";
import icons from "@/constant/images/icons";
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
import InputForm from "@/components/utils/FormCustom/components/InputForm";
import FormCustom from "@/components/utils/FormCustom/FormCustom";
import moment from "moment";
import { timeSubmit } from "@/common/funcs/optionConvert";
import CustomButton from "@/components/customs/custom-button";
function MainUpdateProfile() {
const router = useRouter();
const queryClient = useQueryClient();
const { infoUser } = useSelector((state: RootState) => state.user);
const [fileAvatar, setFileAvatar] = useState<File | null>(null);
// Khởi tạo form dựa hoàn toàn trên các trường Backend trả về
const [form, setForm] = useState<{
fullName: string;
email: string;
phone: string;
birthDate: string;
avatarUrl: string;
}>({
fullName: "",
email: "",
phone: "",
birthDate: "",
avatarUrl: "",
});
/* =========================================================
1. GỌI API LIST PROFILE & LỌC USER ĐANG ĐĂNG NHẬP
========================================================= */
const profileQuery = useQuery<any>({
queryKey: [QUERY_KEY.table_list_profile],
queryFn: async () => {
const res = await httpRequest<any>({
showMessageFailed: true,
http: profileServices.getProfile(),
});
return (
res || {
items: [],
page: 1,
pageSize: 10,
total: 0,
totalPages: 0,
}
);
},
});
// eslint-disable-next-line react-hooks/preserve-manual-memoization
const currentUserProfile = useMemo(() => {
const listItems = profileQuery.data?.items || [];
if (!infoUser?.userId) return null;
return (
listItems.find((item: any) => item.accountId === infoUser.userId) || null
);
}, [profileQuery.data, infoUser?.userId]);
/* =========================================================
2. ĐỔ DỮ LIỆU TỪ PROFILE VÀO FORM KHI TẢI TRANG
========================================================= */
useEffect(() => {
if (currentUserProfile) {
setForm({
fullName: currentUserProfile.fullName || "",
email: currentUserProfile.email || "",
phone: currentUserProfile.phone || "",
// Định dạng input date HTML yêu cầu format YYYY-MM-DD
birthDate: currentUserProfile.birthDate
? moment(currentUserProfile.birthDate).format("YYYY-MM-DD")
: "",
avatarUrl: currentUserProfile.avatarUrl || "",
});
}
}, [currentUserProfile]);
/* =========================================================
3. API MUTATION UPDATE PROFILE (Thay thế API userServices cũ)
========================================================= */
const funcUpdateProfile = useMutation({
mutationFn: (body: { path: string }) =>
httpRequest({
showMessageFailed: true,
showMessageSuccess: true,
msgSuccess: "Chỉnh sửa thông tin thành công!",
// Bạn có thể đổi sang endpoint PATCH/PUT tùy thuộc vào route Update của bạn
http: profileServices.putProfile(currentUserProfile?.id, {
fullName: form.fullName,
email: form.email,
phone: form.phone,
birthDate: form.birthDate,
avatarUrl: body.path || form.avatarUrl,
}), // Lưu ý: Hãy cập nhật đúng service update của user profile tại đây
}),
onSuccess() {
// Làm mới dữ liệu list profile trên toàn hệ thống (bao gồm cả Header và Profile Page)
queryClient.invalidateQueries({
queryKey: [QUERY_KEY.table_list_profile],
});
router.push(PATH.PROFILE || "/profile");
},
onError() {
toastWarn({ msg: "Cập nhật thông tin thất bại!" });
},
});
/* =========================================================
4. XỬ LÝ SUBMIT (UPLOAD AVATAR TRƯỚC - UPDATE PROFILE SAU)
========================================================= */
const handleSubmit = async () => {
const today = new Date(timeSubmit(new Date())!);
const birthDay = new Date(form.birthDate);
if (today < birthDay) {
return toastWarn({ msg: "Ngày sinh không hợp lệ!" });
}
try {
// Nếu người dùng chọn file avatar mới
if (fileAvatar) {
const uploadResult: any = await uploadServices.uploadImage({
Files: [fileAvatar],
});
const uploadedUrl =
uploadResult?.fileUrls?.[0] ||
uploadResult?.data?.fileUrls?.[0] ||
"";
return funcUpdateProfile.mutate({ path: uploadedUrl });
}
// Nếu không thay đổi ảnh, dùng lại link avatar cũ
return funcUpdateProfile.mutate({ path: form.avatarUrl });
} catch (error) {
console.error("Lỗi cập nhật ảnh đại diện:", error);
}
};
return (
<Fragment>
<Loading
loading={funcUpdateProfile.isPending || profileQuery.isLoading}
/>
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
<div className="flex flex-col p-6 bg-white/80 rounded-2xl gap-6 shadow-sm">
{/* HEADER */}
<div className="flex items-center justify-between w-full border-b pb-4">
<h2 className="text-xl font-semibold text-gray-800">
Chỉnh sửa thông tin tài khoản
</h2>
<div className="flex items-center gap-2.5">
<CustomButton
variant="grey"
rounded="full"
size="md"
fullWidth={false}
type="button"
onClick={() => router.back()}
>
Hủy bỏ
</CustomButton>
<ContextFormCustom.Consumer>
{({ isDone }) => {
const hasRememberedData = !!(
form.fullName &&
form.email &&
form.birthDate &&
form.phone
);
const canSubmit = isDone || hasRememberedData;
return (
<CustomButton
variant="midnightBlue"
rounded="full"
size="md"
fullWidth={false}
type="button"
disabled={!canSubmit}
onClick={handleSubmit}
>
{funcUpdateProfile.isPending ? "Đang lưu..." : "Cập nhật"}
</CustomButton>
);
}}
</ContextFormCustom.Consumer>
</div>
</div>
{/* MAIN FORM */}
<div className="space-y-6">
{/* KHỐI AVATAR */}
<div className="flex justify-start">
<UploadAvatar
path={
form?.avatarUrl
? `${process.env.NEXT_PUBLIC_IMAGE}${form?.avatarUrl}`
: icons.avatar
}
name="avatar"
onSetFile={(file) => setFileAvatar(file)}
resetPath={() => {
setFileAvatar(null);
setForm((prev) => ({
...prev,
avatarUrl: "",
}));
}}
/>
</div>
{/* KHỐI THÔNG TIN CÁ NHÂN CHUẨN BACKEND */}
<div className="mt-4">
<div className="grid grid-cols-2 gap-6">
{/* Cột trái */}
<div className="space-y-4">
<InputForm
type="text"
placeholder="Nhập họ và tên"
name="fullName"
value={form.fullName}
label="Họ và tên"
isRequired
onChangeValue={(v) =>
setForm((prev) => ({ ...prev, fullName: String(v) }))
}
/>
<InputForm
type="date"
placeholder="Nhập ngày sinh"
name="birthDate"
label="Ngày sinh"
value={form.birthDate}
onChangeValue={(v) =>
setForm((prev) => ({ ...prev, birthDate: String(v) }))
}
/>
</div>
{/* Cột phải */}
<div className="space-y-4">
<InputForm
placeholder="Nhập email"
name="email"
type="email"
value={form.email}
label="Email"
isRequired
onChangeValue={(v) =>
setForm((prev) => ({ ...prev, email: String(v) }))
}
/>
<InputForm
placeholder="Nhập số điện thoại"
name="phone"
type="number"
isNumber
value={form.phone}
label="Số điện thoại"
onChangeValue={(v) =>
setForm((prev) => ({ ...prev, phone: String(v) }))
}
/>
</div>
</div>
</div>
</div>
</div>
</FormCustom>
</Fragment>
);
}
export default memo(MainUpdateProfile);
@@ -0,0 +1 @@
export { default } from "./MainUpdateProfile";