update-full
This commit is contained in:
parent
7791e1d6a8
commit
d4194d6f12
@ -5,12 +5,19 @@ const nextConfig: NextConfig = {
|
|||||||
root: process.cwd(),
|
root: process.cwd(),
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
|
unoptimized: true,
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
hostname: "127.0.0.1",
|
hostname: "127.0.0.1",
|
||||||
port: "5264",
|
port: "5264",
|
||||||
pathname: "/uploads/**",
|
pathname: "/**",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "http",
|
||||||
|
hostname: "localhost",
|
||||||
|
port: "5264",
|
||||||
|
pathname: "/**",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
14
src/app/profile/page.tsx
Normal file
14
src/app/profile/page.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"use client";
|
||||||
|
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||||
|
import MainPageProfile from "@/components/page/profile/MainPageProfile";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const page = () => {
|
||||||
|
return (
|
||||||
|
<BaseLayout title="Thông tin cá nhân">
|
||||||
|
<MainPageProfile />
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
14
src/app/profile/update/page.tsx
Normal file
14
src/app/profile/update/page.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
"use client";
|
||||||
|
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||||
|
import MainUpdateProfile from "@/components/page/profile/MainUpdateProfile";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const page = () => {
|
||||||
|
return (
|
||||||
|
<BaseLayout title="Cập nhật thông tin cá nhân">
|
||||||
|
<MainUpdateProfile />
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
@ -1,16 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export interface PropsButton {
|
export interface PropsButton {
|
||||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
href?: string;
|
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
className?: string;
|
|
||||||
|
href?: string;
|
||||||
target?: string;
|
target?: string;
|
||||||
|
|
||||||
|
onClick?: (
|
||||||
|
e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
|
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
type?: "button" | "submit" | "reset";
|
type?: "button" | "submit" | "reset";
|
||||||
@ -26,17 +31,22 @@ export interface PropsButton {
|
|||||||
| "warning";
|
| "warning";
|
||||||
|
|
||||||
size?: "sm" | "md" | "lg";
|
size?: "sm" | "md" | "lg";
|
||||||
rounded?: "sm" | "md" | "lg" | "full";
|
|
||||||
|
rounded?: "sm" | "md" | "lg" | "xl" | "full";
|
||||||
|
|
||||||
fullWidth?: boolean;
|
fullWidth?: boolean;
|
||||||
|
maxContent?: boolean;
|
||||||
|
maxHeight?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CustomButton({
|
export default function CustomButton({
|
||||||
children,
|
children,
|
||||||
onClick,
|
|
||||||
href,
|
|
||||||
icon,
|
icon,
|
||||||
className,
|
href,
|
||||||
target,
|
target,
|
||||||
|
onClick,
|
||||||
|
className,
|
||||||
|
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
|
||||||
type = "button",
|
type = "button",
|
||||||
@ -44,10 +54,12 @@ export default function CustomButton({
|
|||||||
variant = "default",
|
variant = "default",
|
||||||
size = "md",
|
size = "md",
|
||||||
rounded = "md",
|
rounded = "md",
|
||||||
|
|
||||||
fullWidth = true,
|
fullWidth = true,
|
||||||
|
maxContent = false,
|
||||||
|
maxHeight = false,
|
||||||
}: PropsButton) {
|
}: PropsButton) {
|
||||||
// 🎨 Variant
|
const variantClasses = {
|
||||||
const variantClasses: Record<string, string> = {
|
|
||||||
default: "bg-gray-100 text-gray-700 border border-gray-200 cursor-pointer",
|
default: "bg-gray-100 text-gray-700 border border-gray-200 cursor-pointer",
|
||||||
midnightBlue:
|
midnightBlue:
|
||||||
"bg-blue-900 text-white border border-blue-900 cursor-pointer",
|
"bg-blue-900 text-white border border-blue-900 cursor-pointer",
|
||||||
@ -60,67 +72,85 @@ export default function CustomButton({
|
|||||||
warning: "bg-orange-400 text-white border border-orange-400 cursor-pointer",
|
warning: "bg-orange-400 text-white border border-orange-400 cursor-pointer",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 📏 Size
|
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
sm: "px-3 py-1 text-sm",
|
sm: "px-6 py-2 text-[13px]",
|
||||||
md: "px-5 py-2 text-sm",
|
md: "px-6 py-3 text-sm",
|
||||||
lg: "px-6 py-3 text-base",
|
lg: "px-8 py-3 text-base",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🔵 Rounded
|
|
||||||
const roundedClasses = {
|
const roundedClasses = {
|
||||||
sm: "rounded-md",
|
sm: "rounded-sm",
|
||||||
md: "rounded-xl",
|
md: "rounded-md",
|
||||||
lg: "rounded-2xl",
|
lg: "rounded-lg",
|
||||||
|
xl: "rounded-xl",
|
||||||
full: "rounded-full",
|
full: "rounded-full",
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseClass = clsx(
|
const baseClass = clsx(
|
||||||
"inline-flex items-center justify-center gap-2",
|
"inline-flex items-center justify-center gap-2",
|
||||||
"transition-all duration-200 select-none",
|
"font-medium whitespace-nowrap",
|
||||||
"active:scale-[0.98] hover:opacity-90",
|
"select-none",
|
||||||
|
"transition-all duration-200",
|
||||||
|
"hover:opacity-80",
|
||||||
|
"active:scale-95",
|
||||||
|
|
||||||
variantClasses[variant],
|
variantClasses[variant],
|
||||||
sizeClasses[size],
|
sizeClasses[size],
|
||||||
roundedClasses[rounded],
|
roundedClasses[rounded],
|
||||||
fullWidth ? "w-full" : "w-fit",
|
|
||||||
disabled && "opacity-40 pointer-events-none cursor-not-allowed",
|
fullWidth && "w-full",
|
||||||
|
maxContent && "w-max",
|
||||||
|
maxHeight && "h-full",
|
||||||
|
|
||||||
|
disabled &&
|
||||||
|
"opacity-30 cursor-not-allowed pointer-events-none active:scale-100",
|
||||||
|
|
||||||
className,
|
className,
|
||||||
);
|
);
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
{icon && <span className="flex items-center">{icon}</span>}
|
{icon && (
|
||||||
<span>{children}</span>
|
<span className="flex items-center justify-center shrink-0">
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="flex items-center justify-center">{children}</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = (
|
||||||
if (disabled) return;
|
e: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>,
|
||||||
|
) => {
|
||||||
|
if (disabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
onClick?.(e);
|
onClick?.(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🔗 Link mode
|
|
||||||
if (href) {
|
if (href) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={href}
|
href={href}
|
||||||
target={target}
|
target={target}
|
||||||
className={baseClass}
|
className={baseClass}
|
||||||
|
onClick={handleClick}
|
||||||
aria-disabled={disabled}
|
aria-disabled={disabled}
|
||||||
tabIndex={disabled ? -1 : undefined}
|
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔘 Button mode
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type={type}
|
type={type}
|
||||||
|
disabled={disabled}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={baseClass}
|
className={baseClass}
|
||||||
disabled={disabled}
|
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import React, { useContext, useEffect, useState } from "react";
|
|
||||||
|
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { PropsHeader } from "./interface";
|
import { PropsHeader } from "./interface";
|
||||||
import { usePathname } from "next/dist/client/components/navigation";
|
import { usePathname } from "next/dist/client/components/navigation";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
@ -10,6 +11,10 @@ import icons from "@/constant/images/icons";
|
|||||||
import { ChevronDown } from "lucide-react";
|
import { ChevronDown } from "lucide-react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import MenuProfile from "../MenuProfile";
|
import MenuProfile from "../MenuProfile";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import profileServices from "@/services/profileServices";
|
||||||
|
import { QUERY_KEY } from "@/constant/config/enum";
|
||||||
|
import { httpRequest } from "@/services";
|
||||||
|
|
||||||
const Header = ({ title, breadcrumb }: PropsHeader) => {
|
const Header = ({ title, breadcrumb }: PropsHeader) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@ -18,7 +23,7 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
|||||||
|
|
||||||
const [openProfile, setOpenProfile] = useState(false);
|
const [openProfile, setOpenProfile] = useState(false);
|
||||||
|
|
||||||
//Đóng menu mobile khi đổi route
|
// Đóng menu mobile khi đổi route
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (window.innerWidth < 1280) {
|
if (window.innerWidth < 1280) {
|
||||||
context.setOpenMenuMobile?.(false);
|
context.setOpenMenuMobile?.(false);
|
||||||
@ -33,8 +38,44 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
CALL API GET LIST PROFILE & KHỚP NỐI THEO CHUẨN DATA MỚI
|
||||||
|
========================================================= */
|
||||||
|
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 (
|
return (
|
||||||
<div className="h-full w-full px-6 flex items-center justify-between bg-white relatives">
|
<div className="h-full w-full px-6 flex items-center justify-between bg-white relative">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{/* Nút bấm Mobile: Mở menu mobile */}
|
{/* Nút bấm Mobile: Mở menu mobile */}
|
||||||
<div
|
<div
|
||||||
@ -62,24 +103,26 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
|||||||
<Image src={icons.full_screen} alt="" width={24} height={24} />
|
<Image src={icons.full_screen} alt="" width={24} height={24} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* PROFILE CONTROL AREA */}
|
||||||
<div
|
<div
|
||||||
className="relative flex items-center gap-2 cursor-pointer"
|
className="relative flex items-center gap-2 cursor-pointer"
|
||||||
onClick={() => setOpenProfile(!openProfile)}
|
onClick={() => setOpenProfile(!openProfile)}
|
||||||
>
|
>
|
||||||
<div className="w-10 h-10 rounded-full border-2 border-blue-500 overflow-hidden">
|
<div className="w-10 h-10 rounded-full border-2 border-blue-500 overflow-hidden relative">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={
|
||||||
infoUser?.avatar
|
currentUserProfile?.avatarUrl
|
||||||
? `${process.env.NEXT_PUBLIC_IMAGE}/${infoUser.avatar}`
|
? `${process.env.NEXT_PUBLIC_IMAGE}${currentUserProfile.avatarUrl}`
|
||||||
: icons.avatar
|
: icons.avatar
|
||||||
}
|
}
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
|
className="object-cover h-full w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="hidden md:block text-sm font-semibold text-[#171832]">
|
<p className="hidden md:block text-sm font-semibold text-[#171832]">
|
||||||
{infoUser?.fullname || "User admin"}
|
{currentUserProfile?.fullName || "User admin"}
|
||||||
</p>
|
</p>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
size={16}
|
size={16}
|
||||||
|
|||||||
@ -67,25 +67,6 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
|||||||
</Link>
|
</Link>
|
||||||
<div className="h-px bg-gray-200 my-1" />
|
<div className="h-px bg-gray-200 my-1" />
|
||||||
|
|
||||||
{/* CHANGE PASSWORD */}
|
|
||||||
{/* <Link
|
|
||||||
href={`${PATH.PROFILE}?_action=change-password`}
|
|
||||||
onClick={onClose}
|
|
||||||
className={clsx(
|
|
||||||
"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-100 transition",
|
|
||||||
pathname === PATH.PROFILE &&
|
|
||||||
searchParams.get("_action") === "change-password" &&
|
|
||||||
"bg-gray-100",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ShieldCog size={20} />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">Đổi mật khẩu</p>
|
|
||||||
<p className="text-xs text-gray-500">Thay đổi mật khẩu</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
<div className="h-px bg-gray-200 my-1" /> */}
|
|
||||||
|
|
||||||
{/* LOGOUT */}
|
{/* LOGOUT */}
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@ -1,97 +1,94 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { LockKeyhole, ShieldPlus, User } from "lucide-react";
|
import { ShieldPlus, User } from "lucide-react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
import { RootState, store } from "@/redux/store";
|
import { RootState, store } from "@/redux/store";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
setDataLoginStorage,
|
setDataLoginStorage,
|
||||||
setStateLogin,
|
setStateLogin,
|
||||||
setToken,
|
setToken,
|
||||||
} from "@/redux/reducer/auth";
|
} from "@/redux/reducer/auth";
|
||||||
|
|
||||||
import { setRememberPassword } from "@/redux/reducer/site";
|
import { setRememberPassword } from "@/redux/reducer/site";
|
||||||
import { setInfoUser } from "@/redux/reducer/user";
|
import { setInfoUser } from "@/redux/reducer/user";
|
||||||
|
import authServices, { LoginResponse } from "@/services/authServices";
|
||||||
import authServices from "@/services/authServices";
|
|
||||||
import { httpRequest } from "@/services";
|
import { httpRequest } from "@/services";
|
||||||
|
|
||||||
import { PATH } from "@/constant/config";
|
import { PATH } from "@/constant/config";
|
||||||
|
|
||||||
import FormCustom from "@/components/utils/FormCustom";
|
import FormCustom from "@/components/utils/FormCustom";
|
||||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||||
import CustomLoading from "@/components/customs/custom-loading";
|
import CustomLoading from "@/components/customs/custom-loading";
|
||||||
import CustomButton from "@/components/customs/custom-button";
|
import CustomButton from "@/components/customs/custom-button";
|
||||||
|
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||||
|
|
||||||
export default function MainLogin() {
|
export default function MainLogin() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { dataLoginStorage } = useSelector((state: RootState) => state.auth);
|
const { dataLoginStorage } = useSelector((state: RootState) => state.auth);
|
||||||
|
|
||||||
const { isRememberPassword } = useSelector((state: RootState) => state.site);
|
const { isRememberPassword } = useSelector((state: RootState) => state.site);
|
||||||
|
|
||||||
const [form, setForm] = useState({
|
// Khởi tạo state form từ data lưu trữ
|
||||||
|
const [form, setForm] = useState(() => ({
|
||||||
username: isRememberPassword ? dataLoginStorage?.usernameStorage || "" : "",
|
username: isRememberPassword ? dataLoginStorage?.usernameStorage || "" : "",
|
||||||
|
|
||||||
password: isRememberPassword ? dataLoginStorage?.passwordStorage || "" : "",
|
password: isRememberPassword ? dataLoginStorage?.passwordStorage || "" : "",
|
||||||
|
}));
|
||||||
|
|
||||||
deviceId: isRememberPassword ? dataLoginStorage?.deviceIdStorage || "" : "",
|
const loginMutation = useMutation<LoginResponse>({
|
||||||
});
|
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
return httpRequest({
|
return (await httpRequest({
|
||||||
showMessageFailed: true,
|
|
||||||
showMessageSuccess: true,
|
|
||||||
msgSuccess: "Đăng nhập thành công!",
|
|
||||||
http: authServices.login({
|
http: authServices.login({
|
||||||
username: form.username,
|
username: form.username,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
deviceId: form.username,
|
deviceId: "Browser-Chrome-Windowns",
|
||||||
}),
|
}),
|
||||||
});
|
})) as LoginResponse;
|
||||||
},
|
},
|
||||||
|
onSuccess(data) {
|
||||||
onSuccess(data: any) {
|
if (data) {
|
||||||
if (!data) return;
|
store.dispatch(setToken(data?.token));
|
||||||
|
|
||||||
store.dispatch(setStateLogin(true));
|
|
||||||
|
|
||||||
store.dispatch(setToken(data.token));
|
|
||||||
|
|
||||||
store.dispatch(
|
|
||||||
setInfoUser({
|
|
||||||
accessToken: data?.token || "",
|
|
||||||
refreshToken: data?.refreshToken || "",
|
|
||||||
accessExpiresAt: data?.accessExpiresAt || "",
|
|
||||||
refreshExpiresAt: data?.refreshExpiresAt || "",
|
|
||||||
avatar: data?.avatar || "",
|
|
||||||
fullname: data?.fullname || "",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isRememberPassword) {
|
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
setDataLoginStorage({
|
setInfoUser({
|
||||||
usernameStorage: form.username,
|
accessToken: data?.token || "",
|
||||||
passwordStorage: form.password,
|
refreshToken: data?.refreshToken || "",
|
||||||
deviceIdStorage: form.deviceId,
|
userId: data?.userId || "",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
store.dispatch(setStateLogin(true));
|
||||||
store.dispatch(setDataLoginStorage(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
router.replace(PATH.HOME);
|
if (isRememberPassword) {
|
||||||
|
store.dispatch(
|
||||||
|
setDataLoginStorage({
|
||||||
|
usernameStorage: form.username,
|
||||||
|
passwordStorage: form.password,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
store.dispatch(setDataLoginStorage(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
router.replace(PATH.HOME, undefined);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleLogin = () => {
|
const handleLogin = () => {
|
||||||
|
// Kiểm tra nhanh điều kiện dữ liệu trước khi trigger API
|
||||||
|
if (!form.username || !form.password) return;
|
||||||
|
|
||||||
|
if (isRememberPassword) {
|
||||||
|
store.dispatch(
|
||||||
|
setDataLoginStorage({
|
||||||
|
usernameStorage: form.username,
|
||||||
|
passwordStorage: form.password,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
store.dispatch(setDataLoginStorage(null));
|
||||||
|
}
|
||||||
|
|
||||||
loginMutation.mutate();
|
loginMutation.mutate();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,24 +98,8 @@ export default function MainLogin() {
|
|||||||
|
|
||||||
<FormCustom form={form} setForm={setForm} onSubmit={handleLogin}>
|
<FormCustom form={form} setForm={setForm} onSubmit={handleLogin}>
|
||||||
{/* TITLE */}
|
{/* TITLE */}
|
||||||
<h3
|
<h3 className="text-[34px] font-semibold text-[#1A1B2D]">Đăng nhập</h3>
|
||||||
className="
|
<p className="mt-1 text-[14px] font-medium text-[#6F767E]">
|
||||||
text-[34px]
|
|
||||||
font-semibold
|
|
||||||
text-[#1A1B2D]
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Đăng nhập
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p
|
|
||||||
className="
|
|
||||||
mt-1
|
|
||||||
text-[14px]
|
|
||||||
font-medium
|
|
||||||
text-[#6F767E]
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Chào mừng bạn đến với hệ thống quản lý
|
Chào mừng bạn đến với hệ thống quản lý
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -128,8 +109,7 @@ export default function MainLogin() {
|
|||||||
<InputForm
|
<InputForm
|
||||||
label={
|
label={
|
||||||
<span>
|
<span>
|
||||||
Tài khoản
|
Tài khoản<span className="text-red-500"> *</span>
|
||||||
<span className="text-red-500"> *</span>
|
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
placeholder="Tài khoản"
|
placeholder="Tài khoản"
|
||||||
@ -142,32 +122,12 @@ export default function MainLogin() {
|
|||||||
icon={<User size={22} />}
|
icon={<User size={22} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <div className="mt-5">
|
|
||||||
<InputForm
|
|
||||||
label={
|
|
||||||
<span>
|
|
||||||
deviceId
|
|
||||||
<span className="text-red-500"> *</span>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
placeholder="deviceId"
|
|
||||||
type="text"
|
|
||||||
name="deviceId"
|
|
||||||
onClean
|
|
||||||
isRequired
|
|
||||||
isBlur
|
|
||||||
showDone
|
|
||||||
icon={<ShieldPlus size={22} />}
|
|
||||||
/>
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* PASSWORD */}
|
{/* PASSWORD */}
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<InputForm
|
<InputForm
|
||||||
label={
|
label={
|
||||||
<span>
|
<span>
|
||||||
Mật khẩu
|
Mật khẩu<span className="text-red-500"> *</span>
|
||||||
<span className="text-red-500"> *</span>
|
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
placeholder="Mật khẩu"
|
placeholder="Mật khẩu"
|
||||||
@ -182,15 +142,7 @@ export default function MainLogin() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* REMEMBER PASSWORD */}
|
{/* REMEMBER PASSWORD */}
|
||||||
<div
|
<div className="mt-[14px] flex items-center gap-2 cursor-pointer">
|
||||||
className="
|
|
||||||
mt-[14px]
|
|
||||||
flex
|
|
||||||
items-center
|
|
||||||
gap-2
|
|
||||||
cursor-pointer
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
id="rememberPassword"
|
id="rememberPassword"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -198,23 +150,11 @@ export default function MainLogin() {
|
|||||||
onChange={() =>
|
onChange={() =>
|
||||||
store.dispatch(setRememberPassword(!isRememberPassword))
|
store.dispatch(setRememberPassword(!isRememberPassword))
|
||||||
}
|
}
|
||||||
className="
|
className="h-5 w-5 cursor-pointer accent-[#0011AB]"
|
||||||
h-5
|
|
||||||
w-5
|
|
||||||
cursor-pointer
|
|
||||||
accent-[#0011AB]
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
htmlFor="rememberPassword"
|
htmlFor="rememberPassword"
|
||||||
className="
|
className="cursor-pointer select-none text-[14px] font-medium text-[#171717]"
|
||||||
cursor-pointer
|
|
||||||
select-none
|
|
||||||
text-[14px]
|
|
||||||
font-medium
|
|
||||||
text-[#171717]
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
Nhớ mật khẩu
|
Nhớ mật khẩu
|
||||||
</label>
|
</label>
|
||||||
@ -223,64 +163,40 @@ export default function MainLogin() {
|
|||||||
{/* BUTTONS */}
|
{/* BUTTONS */}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{/* LOGIN */}
|
{/* LOGIN BUTTON */}
|
||||||
<CustomButton
|
<ContextFormCustom.Consumer>
|
||||||
variant="midnightBlue"
|
{({ isDone }) => {
|
||||||
disabled={loginMutation.isPending}
|
// Tự kiểm tra xem form hiện tại đã có sẵn dữ liệu hợp lệ chưa
|
||||||
rounded="full"
|
const hasRememberedData = !!(form.username && form.password);
|
||||||
type="submit"
|
|
||||||
className="font-bold text-2xl disabled:cursor-not-allowed
|
// Sáng nút nếu FormCustom báo Done HOẶC form đã có sẵn dữ liệu từ Redux
|
||||||
disabled:opacity-50 h-[52px]"
|
const canSubmit = isDone || hasRememberedData;
|
||||||
>
|
|
||||||
Đăng nhập
|
return (
|
||||||
</CustomButton>
|
<CustomButton
|
||||||
{/* REGISTER */}
|
variant="midnightBlue"
|
||||||
|
disabled={!canSubmit}
|
||||||
|
rounded="full"
|
||||||
|
type="button" // Chuyển từ "submit" sang "button" để tránh bị FormCustom chặn
|
||||||
|
onClick={handleLogin} // Gọi trực tiếp hàm submit khi click
|
||||||
|
className="font-bold text-2xl disabled:cursor-not-allowed disabled:opacity-50 h-[52px]"
|
||||||
|
>
|
||||||
|
Đăng nhập
|
||||||
|
</CustomButton>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</ContextFormCustom.Consumer>
|
||||||
|
|
||||||
|
{/* REGISTER BUTTON */}
|
||||||
<CustomButton
|
<CustomButton
|
||||||
variant="default"
|
variant="default"
|
||||||
rounded="full"
|
rounded="full"
|
||||||
className="font-bold text-2xl disabled:cursor-not-allowed
|
className="font-bold text-2xl disabled:cursor-not-allowed disabled:opacity-50 h-[52px]"
|
||||||
disabled:opacity-50 h-[52px]"
|
|
||||||
onClick={() => router.push(PATH.REGISTER)}
|
onClick={() => router.push(PATH.REGISTER)}
|
||||||
>
|
>
|
||||||
Đăng ký
|
Đăng ký
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* LINE */}
|
|
||||||
{/* <div
|
|
||||||
className="
|
|
||||||
my-5
|
|
||||||
h-[1px]
|
|
||||||
w-full
|
|
||||||
bg-[#E5E5E5]
|
|
||||||
"
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
{/* FORGOT PASSWORD */}
|
|
||||||
{/* <button
|
|
||||||
type="button"
|
|
||||||
onClick={() => router.push(PATH.FORGOT_PASSWORD)}
|
|
||||||
className="
|
|
||||||
flex
|
|
||||||
h-[52px]
|
|
||||||
w-full
|
|
||||||
items-center
|
|
||||||
justify-center
|
|
||||||
gap-2
|
|
||||||
rounded-full
|
|
||||||
border
|
|
||||||
border-gray-300
|
|
||||||
bg-white
|
|
||||||
px-6
|
|
||||||
font-bold
|
|
||||||
text-[#171717]
|
|
||||||
transition-all
|
|
||||||
hover:bg-gray-50
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<LockKeyhole size={22} />
|
|
||||||
Quên mật khẩu
|
|
||||||
</button> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormCustom>
|
</FormCustom>
|
||||||
|
|||||||
@ -120,15 +120,29 @@ const DetailConsultation = () => {
|
|||||||
type PrescriptionItem =
|
type PrescriptionItem =
|
||||||
ConsultationDetail["aggregatedPrescriptionItems"][number];
|
ConsultationDetail["aggregatedPrescriptionItems"][number];
|
||||||
|
|
||||||
|
// const getFileInfo = (file: AttachmentFile | string, index: number) => {
|
||||||
|
// if (typeof file === "string") {
|
||||||
|
// return {
|
||||||
|
// fileName: file.split("/").pop() || `File ${index + 1}`,
|
||||||
|
// path: file,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return file;
|
||||||
|
// };
|
||||||
|
|
||||||
const getFileInfo = (file: AttachmentFile | string, index: number) => {
|
const getFileInfo = (file: AttachmentFile | string, index: number) => {
|
||||||
if (typeof file === "string") {
|
if (typeof file === "string") {
|
||||||
return {
|
return {
|
||||||
fileName: file.split("/").pop() || `File ${index + 1}`,
|
fileName: file.split("/").pop() || `File ${index + 1}`,
|
||||||
path: file,
|
path: `${process.env.NEXT_PUBLIC_IMAGE}${file}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return file;
|
return {
|
||||||
|
...file,
|
||||||
|
path: `${process.env.NEXT_PUBLIC_IMAGE}${file.path}`,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -282,7 +296,7 @@ const DetailConsultation = () => {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-[15px] font-medium text-[#202939]">
|
<p className="text-[15px] font-medium text-[#202939]">
|
||||||
{consultation.createdBy || "---"}
|
{consultation.createdByName || "---"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import consultationServices, {
|
|||||||
} from "@/services/consultationServices";
|
} from "@/services/consultationServices";
|
||||||
import uploadServices from "@/services/uploadServices";
|
import uploadServices from "@/services/uploadServices";
|
||||||
import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
|
import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
|
||||||
|
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
TYPES
|
TYPES
|
||||||
@ -29,6 +30,7 @@ interface IUpdateConsul {
|
|||||||
treatmentPlan: string;
|
treatmentPlan: string;
|
||||||
doctorNotes: string;
|
doctorNotes: string;
|
||||||
isGlassesDelivered: boolean;
|
isGlassesDelivered: boolean;
|
||||||
|
glassType: string;
|
||||||
generalAttachmentUrls: string[];
|
generalAttachmentUrls: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,6 +45,7 @@ const defaultForm: IUpdateConsul = {
|
|||||||
treatmentPlan: "",
|
treatmentPlan: "",
|
||||||
doctorNotes: "",
|
doctorNotes: "",
|
||||||
isGlassesDelivered: false,
|
isGlassesDelivered: false,
|
||||||
|
glassType: "",
|
||||||
generalAttachmentUrls: [],
|
generalAttachmentUrls: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,9 +104,14 @@ const UpdateConsultation = () => {
|
|||||||
/* =========================
|
/* =========================
|
||||||
MAP & SET FORM
|
MAP & SET FORM
|
||||||
========================= */
|
========================= */
|
||||||
|
const consul = consulDetailQuery.data;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const consul = consulDetailQuery.data;
|
|
||||||
if (!consul) return;
|
if (!consul) return;
|
||||||
|
const eyeClinic = consul?.specialtyClinics?.find(
|
||||||
|
(item) =>
|
||||||
|
item.specialtyName?.toLowerCase().includes("mắt") &&
|
||||||
|
item.glassRequired === true,
|
||||||
|
);
|
||||||
|
|
||||||
setForm({
|
setForm({
|
||||||
chiefComplaint: consul.chiefComplaint || "",
|
chiefComplaint: consul.chiefComplaint || "",
|
||||||
@ -112,7 +120,8 @@ const UpdateConsultation = () => {
|
|||||||
diagnosis: consul.diagnosis || "",
|
diagnosis: consul.diagnosis || "",
|
||||||
treatmentPlan: consul.treatmentPlan || "",
|
treatmentPlan: consul.treatmentPlan || "",
|
||||||
doctorNotes: consul.doctorNotes || "",
|
doctorNotes: consul.doctorNotes || "",
|
||||||
isGlassesDelivered: consul.isGlassesDelivered,
|
isGlassesDelivered: !!eyeClinic,
|
||||||
|
glassType: eyeClinic?.glassType || "",
|
||||||
generalAttachmentUrls: consul.generalAttachmentUrls || [],
|
generalAttachmentUrls: consul.generalAttachmentUrls || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -212,6 +221,11 @@ const UpdateConsultation = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const eyeClinic = consul?.specialtyClinics?.find(
|
||||||
|
(item) =>
|
||||||
|
item.specialtyName?.toLowerCase().includes("mắt") &&
|
||||||
|
item.glassRequired === true,
|
||||||
|
);
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
MAIN UI
|
MAIN UI
|
||||||
@ -324,6 +338,56 @@ const UpdateConsultation = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
|
|
||||||
|
{eyeClinic && (
|
||||||
|
<div className="mt-2 rounded-xl border border-blue-100 bg-[#f8fafc] p-5 transition-all">
|
||||||
|
<h3 className="mb-4 text-sm font-bold uppercase tracking-wider text-blue-600">
|
||||||
|
Chỉ định thị lực (Khoa Mắt)
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
Bệnh nhân có chỉ định đeo kính không?
|
||||||
|
</label>
|
||||||
|
<div className="mt-2.5 flex items-center gap-8">
|
||||||
|
<label className="flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-900">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="glassRequired"
|
||||||
|
className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
checked={form.isGlassesDelivered === true}
|
||||||
|
onChange={() =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
isGlassesDelivered: true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Có chỉ định
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextArea
|
||||||
|
name="glassType"
|
||||||
|
placeholder="Nhập thông số chi tiết loại kính được chỉ định"
|
||||||
|
label="Thông số / Loại kính chỉ định"
|
||||||
|
value={form.glassType}
|
||||||
|
isRequired
|
||||||
|
readOnly
|
||||||
|
isBlur
|
||||||
|
max={5000}
|
||||||
|
onChangeValue={(v) =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
glassType: String(v),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<label className="text-sm font-medium">Tài liệu đính kèm</label>
|
<label className="text-sm font-medium">Tài liệu đính kèm</label>
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import consultationServices, {
|
|||||||
} from "@/services/consultationServices";
|
} from "@/services/consultationServices";
|
||||||
import uploadServices from "@/services/uploadServices";
|
import uploadServices from "@/services/uploadServices";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import moment from "moment";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
@ -62,6 +63,7 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
res || {
|
res || {
|
||||||
id: "",
|
id: "",
|
||||||
patientId: "",
|
patientId: "",
|
||||||
|
patientName: "",
|
||||||
sessionCode: "",
|
sessionCode: "",
|
||||||
visitDate: "",
|
visitDate: "",
|
||||||
status: 1,
|
status: 1,
|
||||||
@ -71,50 +73,34 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
diagnosis: null,
|
diagnosis: null,
|
||||||
treatmentPlan: null,
|
treatmentPlan: null,
|
||||||
doctorNotes: null,
|
doctorNotes: null,
|
||||||
|
isGlassesDelivered: false,
|
||||||
createdBy: "",
|
createdBy: "",
|
||||||
|
createdByName: "",
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
specialtyClinics: [
|
generalAttachmentUrls: [],
|
||||||
{
|
specialtyClinics: [],
|
||||||
id: "",
|
|
||||||
specialtyId: "",
|
|
||||||
specialtyName: "",
|
|
||||||
specialtyNotes: null,
|
|
||||||
diagnosis: null,
|
|
||||||
procedureNotes: null,
|
|
||||||
glassRequired: null,
|
|
||||||
glassType: null,
|
|
||||||
examinerName: "",
|
|
||||||
specialtyStatus: 1,
|
|
||||||
attachmentUrls: [],
|
|
||||||
sharedItems: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
aggregatedPrescriptionItems: [],
|
aggregatedPrescriptionItems: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tìm chuyên khoa hiện tại từ data trả về
|
// Khai báo dữ liệu lấy từ Query phục vụ hiển thị
|
||||||
const selectedSpecialtyClinic =
|
const generalData = consulDetailQuery.data;
|
||||||
consulDetailQuery.data?.specialtyClinics?.find(
|
|
||||||
(item) => item.id === specialtyClinicId,
|
// Tìm chuyên khoa hiện tại từ data trả về
|
||||||
);
|
const selectedSpecialtyClinic = generalData?.specialtyClinics?.find(
|
||||||
|
(item) => item.id === specialtyClinicId,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* ĐIỀU KIỆN KIỂM TRA KHOA MẮT
|
|
||||||
* Bạn hãy thay đổi điều kiện này tùy thuộc vào database của bạn.
|
|
||||||
* Ví dụ: selectedSpecialtyClinic?.specialtyId === "MẮT" hoặc kiểm tra theo tên.
|
|
||||||
*/
|
|
||||||
const isEyeClinic =
|
const isEyeClinic =
|
||||||
selectedSpecialtyClinic?.specialtyName?.toLowerCase().includes("mắt") ||
|
selectedSpecialtyClinic?.specialtyName?.toLowerCase().includes("mắt") ||
|
||||||
selectedSpecialtyClinic?.specialtyId?.toLowerCase().includes("mat");
|
selectedSpecialtyClinic?.specialtyId?.toLowerCase().includes("mat");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const consul = consulDetailQuery.data;
|
if (!generalData) return;
|
||||||
if (!consul) return;
|
|
||||||
|
|
||||||
const specialtyClinic = consul.specialtyClinics?.find(
|
const specialtyClinic = generalData.specialtyClinics?.find(
|
||||||
(item) => item.id === specialtyClinicId,
|
(item) => item.id === specialtyClinicId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -130,18 +116,17 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
fileUrls: specialtyClinic.attachmentUrls || [],
|
fileUrls: specialtyClinic.attachmentUrls || [],
|
||||||
});
|
});
|
||||||
setImages(
|
setImages(
|
||||||
(consul.generalAttachmentUrls || []).map((url) => ({
|
(specialtyClinic.attachmentUrls || []).map((url) => ({
|
||||||
file: null,
|
file: null,
|
||||||
img: url,
|
img: url,
|
||||||
path: url,
|
path: url,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}, [consulDetailQuery.data, specialtyClinicId]);
|
}, [generalData, specialtyClinicId]);
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
UPDATE MUTATION
|
UPDATE MUTATION
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
const specialtyResultsMutation = useMutation({
|
const specialtyResultsMutation = useMutation({
|
||||||
mutationFn: async (body: { paths: string[] }) => {
|
mutationFn: async (body: { paths: string[] }) => {
|
||||||
if (!consultationId) {
|
if (!consultationId) {
|
||||||
@ -151,14 +136,13 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
return httpRequest({
|
return httpRequest({
|
||||||
showMessageFailed: true,
|
showMessageFailed: true,
|
||||||
showMessageSuccess: true,
|
showMessageSuccess: true,
|
||||||
msgSuccess: "Cập nhật phiên khám thành công!",
|
msgSuccess: "Cập nhật kết quả khám chuyên khoa thành công!",
|
||||||
http: consultationServices.specialtyResults(
|
http: consultationServices.specialtyResults(
|
||||||
form.specialtyAssignmentId,
|
form.specialtyAssignmentId,
|
||||||
{
|
{
|
||||||
specialtyNotes: form.specialtyNotes,
|
specialtyNotes: form.specialtyNotes,
|
||||||
diagnosis: form.diagnosis,
|
diagnosis: form.diagnosis,
|
||||||
procedureNotes: form.procedureNotes,
|
procedureNotes: form.procedureNotes,
|
||||||
// Nếu không phải khoa mắt, gán mặc định false và chuỗi rỗng khi gửi API
|
|
||||||
glassRequired: isEyeClinic ? form.glassRequired : false,
|
glassRequired: isEyeClinic ? form.glassRequired : false,
|
||||||
glassType: isEyeClinic ? form.glassType : "",
|
glassType: isEyeClinic ? form.glassType : "",
|
||||||
fileUrls: body.paths,
|
fileUrls: body.paths,
|
||||||
@ -177,7 +161,6 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.back();
|
router.back();
|
||||||
|
|
||||||
router.push(PATH.CONSULTATION || "/consultation");
|
router.push(PATH.CONSULTATION || "/consultation");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -195,12 +178,9 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
const uploadResult: any = await uploadServices.uploadImage({
|
const uploadResult: any = await uploadServices.uploadImage({
|
||||||
Files: files,
|
Files: files,
|
||||||
SessionCode: consulDetailQuery.data?.sessionCode,
|
SessionCode: generalData?.sessionCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Cấu trúc log kiểm tra uploadResult:", uploadResult);
|
|
||||||
|
|
||||||
// CẬP NHẬT Ở ĐÂY: Lấy trực tiếp từ uploadResult hoặc bóc tách đúng tầng data
|
|
||||||
const uploadedUrls =
|
const uploadedUrls =
|
||||||
uploadResult?.fileUrls || uploadResult?.data?.fileUrls || [];
|
uploadResult?.fileUrls || uploadResult?.data?.fileUrls || [];
|
||||||
|
|
||||||
@ -243,26 +223,64 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CHUYÊN KHOA INFO */}
|
{/* THÔNG TIN HÀNH CHÍNH & PHIÊN KHÁM TỔNG QUÁT */}
|
||||||
<InputForm
|
<div className="rounded-xl border border-gray-200 bg-[#f8fafc] p-5">
|
||||||
label="Chuyên khoa đảm nhận"
|
<h3 className="mb-4 text-xs font-bold uppercase tracking-wider text-gray-500">
|
||||||
name="specialtyAssignmentId"
|
Thông tin tổng quan phiên khám
|
||||||
type="text"
|
|
||||||
value={
|
|
||||||
selectedSpecialtyClinic
|
|
||||||
? `${selectedSpecialtyClinic.specialtyName}`
|
|
||||||
: "Chưa có thông tin chuyên khoa"
|
|
||||||
}
|
|
||||||
placeholder="Thông tin chuyên khoa"
|
|
||||||
isRequired
|
|
||||||
readOnly
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* KHỐI THÔNG TIN KHÁM CHUNG (Luôn cố định 3 cột đẹp mắt) */}
|
|
||||||
<div className="mt-2">
|
|
||||||
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400">
|
|
||||||
Thông tin khám chung
|
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-medium text-gray-400 block uppercase">
|
||||||
|
Tên bệnh nhân
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-bold text-gray-700 mt-0.5 block">
|
||||||
|
{generalData?.patientName || "Chưa có thông tin"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-medium text-gray-400 block uppercase">
|
||||||
|
Ngày sinh
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-medium text-gray-700 mt-0.5 block">
|
||||||
|
{generalData?.patientDateOfBirth
|
||||||
|
? moment(generalData?.patientDateOfBirth).format("DD/MM/YYYY")
|
||||||
|
: "---"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs font-medium text-gray-400 block uppercase">
|
||||||
|
Mã phiên khám tổng quát
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-semibold text-blue-600 mt-0.5 block">
|
||||||
|
#{generalData?.sessionCode || "Chưa có mã"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KHỐI THÔNG TIN KHÁM CHUYÊN KHOA */}
|
||||||
|
<div className="mt-2">
|
||||||
|
<h3 className="mb-4 text-xs font-bold uppercase tracking-wider text-gray-500">
|
||||||
|
Kết quả khám chuyên khoa đảm nhận
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="mb-5">
|
||||||
|
<InputForm
|
||||||
|
label="Chuyên khoa tiếp nhận"
|
||||||
|
name="specialtyAssignmentId"
|
||||||
|
type="text"
|
||||||
|
value={
|
||||||
|
selectedSpecialtyClinic
|
||||||
|
? `${selectedSpecialtyClinic.specialtyName}`
|
||||||
|
: "Chưa có thông tin chuyên khoa"
|
||||||
|
}
|
||||||
|
placeholder="Thông tin chuyên khoa"
|
||||||
|
isRequired
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<GridColumn col={3}>
|
<GridColumn col={3}>
|
||||||
<InputForm
|
<InputForm
|
||||||
label="Ghi chú chuyên khoa"
|
label="Ghi chú chuyên khoa"
|
||||||
@ -277,9 +295,9 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
/>
|
/>
|
||||||
<InputForm
|
<InputForm
|
||||||
type="text"
|
type="text"
|
||||||
label="Chuẩn đoán chuyên khoa"
|
label="Chẩn đoán chuyên khoa"
|
||||||
name="diagnosis"
|
name="diagnosis"
|
||||||
placeholder="Nhập Chuẩn đoán"
|
placeholder="Nhập chẩn đoán chuyên khoa"
|
||||||
isRequired
|
isRequired
|
||||||
value={form.diagnosis}
|
value={form.diagnosis}
|
||||||
onChangeValue={(v) =>
|
onChangeValue={(v) =>
|
||||||
@ -308,7 +326,6 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
{/* Radio Chọn Có/Không */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-sm font-medium text-gray-700">
|
<label className="text-sm font-medium text-gray-700">
|
||||||
Bệnh nhân có chỉ định đeo kính không?
|
Bệnh nhân có chỉ định đeo kính không?
|
||||||
@ -342,7 +359,6 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TextArea Loại Kính (Trải rộng ra 100% dòng tạo cảm giác thoáng đãng) */}
|
|
||||||
{form.glassRequired && (
|
{form.glassRequired && (
|
||||||
<div className="mt-2 animate-fadeIn ">
|
<div className="mt-2 animate-fadeIn ">
|
||||||
<TextArea
|
<TextArea
|
||||||
@ -359,11 +375,12 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<label className="text-sm font-medium">Tài liệu đính kèm</label>
|
<label className="text-sm font-medium">
|
||||||
|
Tài liệu đính kèm chuyên khoa
|
||||||
|
</label>
|
||||||
<UploadMultipleFile images={images} setImages={setImages} />
|
<UploadMultipleFile images={images} setImages={setImages} />
|
||||||
|
|
||||||
{uploading && (
|
{uploading && (
|
||||||
<p className="text-sm text-blue-500">Đang upload file...</p>
|
<p className="text-sm text-blue-500">Đang upload file...</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -159,7 +159,8 @@ const PopupCreatePatient = ({ onClose }: PropsPopupCreatePatient) => {
|
|||||||
}
|
}
|
||||||
placeholder="Mã định danh"
|
placeholder="Mã định danh"
|
||||||
name="identificationNumber"
|
name="identificationNumber"
|
||||||
type="text"
|
type="number"
|
||||||
|
isNumber
|
||||||
value={form.identificationNumber}
|
value={form.identificationNumber}
|
||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -121,7 +121,7 @@ const DetailPrescription = () => {
|
|||||||
{data.prescriptionImg ? (
|
{data.prescriptionImg ? (
|
||||||
<>
|
<>
|
||||||
<Image
|
<Image
|
||||||
src={`${data.prescriptionImg}`}
|
src={`${process.env.NEXT_PUBLIC_IMAGE}/${data.prescriptionImg}`}
|
||||||
alt="prescription"
|
alt="prescription"
|
||||||
width={140}
|
width={140}
|
||||||
height={140}
|
height={140}
|
||||||
|
|||||||
@ -160,8 +160,8 @@ const MainPrescription = () => {
|
|||||||
render: (item: any) =>
|
render: (item: any) =>
|
||||||
item.prescriptionImg ? (
|
item.prescriptionImg ? (
|
||||||
<Image
|
<Image
|
||||||
// src={`${process.env.NEXT_PUBLIC_IMAGE}${item.prescriptionImg}`}
|
src={`${process.env.NEXT_PUBLIC_IMAGE}${item.prescriptionImg}`}
|
||||||
src={item.prescriptionImg}
|
// src={item.prescriptionImg}
|
||||||
alt="prescription"
|
alt="prescription"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
|
|||||||
175
src/components/page/profile/MainPageProfile/MainPageProfile.tsx
Normal file
175
src/components/page/profile/MainPageProfile/MainPageProfile.tsx
Normal file
@ -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 cơ 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ọ và 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);
|
||||||
1
src/components/page/profile/MainPageProfile/index.ts
Normal file
1
src/components/page/profile/MainPageProfile/index.ts
Normal file
@ -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);
|
||||||
1
src/components/page/profile/MainUpdateProfile/index.ts
Normal file
1
src/components/page/profile/MainUpdateProfile/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./MainUpdateProfile";
|
||||||
@ -136,7 +136,7 @@ const MainPageStatistical = () => {
|
|||||||
column={[
|
column={[
|
||||||
{
|
{
|
||||||
title: "MÃ CHUYÊN KHOA",
|
title: "MÃ CHUYÊN KHOA",
|
||||||
render: (item) => <span>{item.specialtyId}</span>,
|
render: (item) => <span>{item.specialtyCode}</span>,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "TÊN CHUYÊN KHOA",
|
title: "TÊN CHUYÊN KHOA",
|
||||||
|
|||||||
@ -1,132 +1,116 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import Image from "next/image";
|
|
||||||
import { Upload, Trash } from "lucide-react";
|
|
||||||
|
|
||||||
import { toastError, toastWarn } from "@/common/funcs/toast";
|
|
||||||
import { PropsUploadAvatar } from "./interface";
|
import { PropsUploadAvatar } from "./interface";
|
||||||
|
// import { toastError, toastWarn } from "~/common/funcs/toast";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { toastError, toastWarn } from "@/common/funcs/toast";
|
||||||
|
import { Trash } from "lucide-react";
|
||||||
|
// import icons from "~/constants/images/icons";
|
||||||
|
|
||||||
const MAXIMUM_FILE = 10;
|
const MAXIMUM_FILE = 10;
|
||||||
|
|
||||||
export default function UploadAvatar({
|
function UploadAvatar({ path, name, onSetFile, resetPath }: PropsUploadAvatar) {
|
||||||
path,
|
const [imageBase64, setImageBase64] = useState<string>("");
|
||||||
name,
|
|
||||||
onSetFile,
|
|
||||||
resetPath,
|
|
||||||
}: PropsUploadAvatar) {
|
|
||||||
const [imageUrl, setImageUrl] = useState<string>("");
|
|
||||||
const [fileName, setFileName] = useState<string>("");
|
const [fileName, setFileName] = useState<string>("");
|
||||||
|
|
||||||
const handleSelectImg = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSelectImg = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const { size, type, name } = file;
|
if (file) {
|
||||||
|
const { size, type, name } = file;
|
||||||
|
const maxSize = MAXIMUM_FILE; // MB
|
||||||
|
|
||||||
if (size / 1024 / 1024 > MAXIMUM_FILE) {
|
if (size / 1000000 > maxSize) {
|
||||||
toastError({ msg: `Kích thước tối đa ${MAXIMUM_FILE}MB` });
|
return toastError({
|
||||||
return;
|
msg: `Kích thước tối đa của ảnh là ${maxSize} MB`,
|
||||||
}
|
});
|
||||||
|
} else if (!["image/jpeg", "image/jpg", "image/png"].includes(type)) {
|
||||||
|
return toastWarn({
|
||||||
|
msg: `Định dạng tệp không chính xác, chỉ chấp nhận .jpg, .jpeg, .png`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!["image/jpeg", "image/jpg", "image/png"].includes(type)) {
|
const imageUrl = URL.createObjectURL(file);
|
||||||
toastWarn({
|
setImageBase64((prev) => {
|
||||||
msg: "Chỉ hỗ trợ định dạng .jpg, .jpeg, .png",
|
URL.revokeObjectURL(prev);
|
||||||
|
return imageUrl;
|
||||||
});
|
});
|
||||||
return;
|
setFileName(name);
|
||||||
|
onSetFile && onSetFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = URL.createObjectURL(file);
|
|
||||||
|
|
||||||
setImageUrl((prev) => {
|
|
||||||
if (prev) URL.revokeObjectURL(prev);
|
|
||||||
return url;
|
|
||||||
});
|
|
||||||
|
|
||||||
setFileName(name);
|
|
||||||
onSetFile?.(file);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (imageUrl) URL.revokeObjectURL(imageUrl);
|
if (imageBase64) {
|
||||||
|
URL.revokeObjectURL(imageBase64);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [imageUrl]);
|
}, [imageBase64]);
|
||||||
|
|
||||||
const handleRemoveImg = () => {
|
const handleRemoveImg = () => {
|
||||||
if (imageUrl) URL.revokeObjectURL(imageUrl);
|
setImageBase64("");
|
||||||
|
|
||||||
setImageUrl("");
|
|
||||||
setFileName("");
|
setFileName("");
|
||||||
|
onSetFile && onSetFile(null);
|
||||||
onSetFile?.(null);
|
resetPath && resetPath();
|
||||||
resetPath?.();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayImage = imageUrl || path;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col md:flex-row gap-3">
|
<div className="flex gap-3">
|
||||||
{/* AVATAR */}
|
|
||||||
<Image
|
<Image
|
||||||
src={displayImage}
|
alt="update avatar"
|
||||||
alt="avatar"
|
src={!!imageBase64 ? imageBase64 : path}
|
||||||
width={104}
|
width={80}
|
||||||
height={104}
|
height={80}
|
||||||
className="rounded-md border border-blue-500 object-cover"
|
className="rounded-md border border-[#6e99fd]"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* RIGHT CONTENT */}
|
<div>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="mb-3 flex items-center gap-3">
|
||||||
{/* CONTROL */}
|
<label className="flex h-12 cursor-pointer select-none">
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-3">
|
|
||||||
{/* UPLOAD INPUT */}
|
|
||||||
<label className="flex cursor-pointer select-none h-12 w-full md:w-auto">
|
|
||||||
{/* BUTTON */}
|
|
||||||
<div className="flex items-center justify-center gap-2 px-5 bg-gray-100 rounded-l-md shadow-inner">
|
|
||||||
<Upload size={18} />
|
|
||||||
<span className="text-sm font-semibold">Chọn file</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* FILE NAME */}
|
|
||||||
<div className="flex items-center px-5 w-full md:w-[328px] bg-blue-50 rounded-r-md shadow-inner overflow-hidden">
|
|
||||||
<p className="text-sm font-semibold text-gray-600 truncate w-full">
|
|
||||||
{fileName || "Tên file"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
hidden
|
hidden
|
||||||
type="file"
|
type="file"
|
||||||
name={name}
|
|
||||||
accept="image/png, image/jpeg, image/jpg"
|
accept="image/png, image/jpeg, image/jpg"
|
||||||
|
name={name}
|
||||||
onChange={handleSelectImg}
|
onChange={handleSelectImg}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
(e.target as HTMLInputElement).value = "";
|
(e.target as HTMLInputElement).value = "";
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Button chọn file */}
|
||||||
|
<div className="flex h-full items-center justify-center rounded-l-lg bg-[#fafafb] px-5 shadow-[inset_0px_0px_3px_0px_rgba(175,190,211,0.2)]">
|
||||||
|
<p className="text-sm font-bold text-[#445463]">Chọn file</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tên file */}
|
||||||
|
<div className="flex h-full w-[328px] items-center justify-start rounded-r-lg bg-[#f1f8ff] px-5 shadow-[inset_0px_3px_3px_0px_rgba(175,190,211,0.2)]">
|
||||||
|
<p className="truncate text-sm font-bold text-[#445463]">
|
||||||
|
{fileName || "Tên file"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{/* DELETE BUTTON */}
|
{/* Xóa avatar */}
|
||||||
<button
|
<div
|
||||||
onClick={handleRemoveImg}
|
onClick={handleRemoveImg}
|
||||||
className="flex items-center gap-2 px-6 h-12 bg-gray-100 rounded-md transition active:scale-95 hover:opacity-70"
|
className="flex h-12 cursor-pointer select-none items-center gap-2.5 rounded-lg bg-[rgba(154,169,182,0.1)] px-6 transition duration-300 hover:opacity-70 active:scale-95"
|
||||||
>
|
>
|
||||||
<Trash size={18} color="#AF0000" />
|
<Trash width={20} height={20} />
|
||||||
<span className="text-sm font-semibold text-gray-700">
|
<p className="text-base font-bold text-[#445463]">
|
||||||
Gỡ ảnh đại diện
|
Xóa ảnh đại diện
|
||||||
</span>
|
</p>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* DESCRIPTION */}
|
<p className="text-sm font-normal text-[#9aa9b6]">
|
||||||
<p className="text-sm text-gray-400">
|
Hình ảnh được dùng làm ảnh đại diện, kích thước tối thiểu 300px X
|
||||||
Hình ảnh dùng làm avatar, tối thiểu 300x300px
|
300px để đảm bảo độ sắc nét.
|
||||||
</p>
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
Định dạng hỗ trợ: JPG, JPEG, PNG
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default UploadAvatar;
|
||||||
|
|||||||
@ -112,7 +112,7 @@ export default function UploadMultipleFile({
|
|||||||
const fileUrl =
|
const fileUrl =
|
||||||
item?.url ||
|
item?.url ||
|
||||||
(item?.path
|
(item?.path
|
||||||
? `${process.env.NEXT_PUBLIC_IMAGE}/${item.path}`
|
? `${process.env.NEXT_PUBLIC_IMAGE}${item.path}`
|
||||||
: "");
|
: "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -137,7 +137,7 @@ export default function UploadMultipleFile({
|
|||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
href={fileUrl}
|
href={`${process.env.NEXT_PUBLIC_IMAGE}/${fileUrl}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="flex h-full w-full flex-col items-center justify-center gap-1 bg-gray-50 p-1"
|
className="flex h-full w-full flex-col items-center justify-center gap-1 bg-gray-50 p-1"
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export enum QUERY_KEY {
|
|||||||
table_list_patientrescription,
|
table_list_patientrescription,
|
||||||
table_list_specialty,
|
table_list_specialty,
|
||||||
table_list_report,
|
table_list_report,
|
||||||
|
table_list_profile,
|
||||||
|
|
||||||
list_specialty_lookup,
|
list_specialty_lookup,
|
||||||
examination_specialty_list,
|
examination_specialty_list,
|
||||||
@ -11,6 +12,7 @@ export enum QUERY_KEY {
|
|||||||
chi_tiet_benh_nhan,
|
chi_tiet_benh_nhan,
|
||||||
chi_tiet_don_thuoc,
|
chi_tiet_don_thuoc,
|
||||||
chi_tiet_phien_kham,
|
chi_tiet_phien_kham,
|
||||||
|
chi_tiet_thong_tin,
|
||||||
|
|
||||||
user_me,
|
user_me,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export enum PATH {
|
|||||||
FORGOT_PASSWORD = "/auth/forgot-password",
|
FORGOT_PASSWORD = "/auth/forgot-password",
|
||||||
|
|
||||||
PROFILE = "/profile",
|
PROFILE = "/profile",
|
||||||
|
UPDATEPROFILE = "/profile/update",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Menus: {
|
export const Menus: {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
|||||||
export interface IDataLoginStorage {
|
export interface IDataLoginStorage {
|
||||||
usernameStorage: string;
|
usernameStorage: string;
|
||||||
passwordStorage: string;
|
passwordStorage: string;
|
||||||
deviceIdStorage: string;
|
// deviceIdStorage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthState {
|
export interface AuthState {
|
||||||
|
|||||||
@ -3,10 +3,7 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
|||||||
interface IUser {
|
interface IUser {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
accessExpiresAt: string;
|
userId: string;
|
||||||
refreshExpiresAt: string;
|
|
||||||
avatar: string;
|
|
||||||
fullname: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserState {
|
export interface UserState {
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
import axiosClient from ".";
|
import axiosClient from ".";
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
token: string;
|
||||||
|
refreshToken: string;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
const authServices = {
|
const authServices = {
|
||||||
login: (
|
login: (
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export interface ConsultationDetail {
|
|||||||
id: string;
|
id: string;
|
||||||
patientId: string;
|
patientId: string;
|
||||||
patientName: string;
|
patientName: string;
|
||||||
|
patientDateOfBirth: string | null;
|
||||||
sessionCode: string;
|
sessionCode: string;
|
||||||
visitDate: string;
|
visitDate: string;
|
||||||
status: string | number;
|
status: string | number;
|
||||||
@ -76,6 +77,7 @@ export interface ConsultationDetail {
|
|||||||
doctorNotes: string | null;
|
doctorNotes: string | null;
|
||||||
isGlassesDelivered: boolean;
|
isGlassesDelivered: boolean;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
|
createdByName: string;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
generalAttachmentUrls: string[];
|
generalAttachmentUrls: string[];
|
||||||
specialtyClinics: {
|
specialtyClinics: {
|
||||||
@ -126,6 +128,7 @@ export interface ReportResponse {
|
|||||||
totalSpecialtySubSessions: number;
|
totalSpecialtySubSessions: number;
|
||||||
specialtyDetails: {
|
specialtyDetails: {
|
||||||
specialtyId: string;
|
specialtyId: string;
|
||||||
|
specialtyCode: string;
|
||||||
specialtyName: string;
|
specialtyName: string;
|
||||||
totalCases: number;
|
totalCases: number;
|
||||||
}[];
|
}[];
|
||||||
|
|||||||
@ -151,13 +151,7 @@ const refreshAccessToken = async (): Promise<string | null> => {
|
|||||||
|
|
||||||
refreshToken: responseData?.refreshToken || prev?.refreshToken || "",
|
refreshToken: responseData?.refreshToken || prev?.refreshToken || "",
|
||||||
|
|
||||||
accessExpiresAt: prev?.accessExpiresAt || "",
|
userId: prev?.userId || "",
|
||||||
|
|
||||||
refreshExpiresAt: prev?.refreshExpiresAt || "",
|
|
||||||
|
|
||||||
avatar: prev?.avatar || "",
|
|
||||||
|
|
||||||
fullname: prev?.fullname || "",
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
50
src/services/profileServices.ts
Normal file
50
src/services/profileServices.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import axiosClient from ".";
|
||||||
|
|
||||||
|
export interface GetProfileParams {
|
||||||
|
Page?: number;
|
||||||
|
PageSize?: number;
|
||||||
|
Search?: string;
|
||||||
|
SortBy?: string;
|
||||||
|
Desc?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfileResponse {
|
||||||
|
items: ProfileItem[];
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProfileItem {
|
||||||
|
id: string;
|
||||||
|
accountId: string;
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
phone: string | null;
|
||||||
|
dateOfBirth: string | null;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
}
|
||||||
|
const profileServices = {
|
||||||
|
getProfile: (params?: GetProfileParams, tokenAxios?: any) => {
|
||||||
|
return axiosClient.get<ProfileResponse>(`/api/v1/UserProfile/list`, {
|
||||||
|
params,
|
||||||
|
|
||||||
|
cancelToken: tokenAxios,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
putProfile: (
|
||||||
|
id: string,
|
||||||
|
data: {
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
birthDate: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
return axiosClient.put<ProfileItem>(`/api/v1/UserProfile/${id}`, data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default profileServices;
|
||||||
@ -2,6 +2,7 @@ import axiosClient from ".";
|
|||||||
|
|
||||||
export interface SpecialtyItem {
|
export interface SpecialtyItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
code: string;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,17 @@ export interface UserMe {
|
|||||||
phone: string | number | null;
|
phone: string | number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserDetail {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
role?: number;
|
||||||
|
roleType?: number | string | null;
|
||||||
|
isActive?: boolean;
|
||||||
|
fullName: string | null;
|
||||||
|
email: string;
|
||||||
|
phone: string;
|
||||||
|
}
|
||||||
|
|
||||||
const userServices = {
|
const userServices = {
|
||||||
getUser: (params?: GetUserParams, tokenAxios?: any) => {
|
getUser: (params?: GetUserParams, tokenAxios?: any) => {
|
||||||
return axiosClient.get(`/api/v1/User/list`, {
|
return axiosClient.get(`/api/v1/User/list`, {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user