From d4194d6f122c0b9af6a4f3b16e2f785d09a37a78 Mon Sep 17 00:00:00 2001 From: TuanVT Date: Sat, 6 Jun 2026 01:02:07 +0700 Subject: [PATCH] update-full --- next.config.ts | 9 +- src/app/profile/page.tsx | 14 + src/app/profile/update/page.tsx | 14 + src/components/customs/custom-button.tsx | 90 ++++-- .../BaseLayout/componets/Header/Header.tsx | 57 +++- .../componets/MenuProfile/MenuProfile.tsx | 19 -- .../page/auth/MainLogin/MainLogin.tsx | 242 +++++--------- .../DetailConsultation/DetailConsultation.tsx | 20 +- .../UpdateConsultation/UpdateConsultation.tsx | 68 +++- .../UpdateSpecialtyClinicId.tsx | 147 +++++---- .../PopupCreatePatient/PopupCreatePatient.tsx | 3 +- .../DetailPrescription/DetailPrescription.tsx | 2 +- .../MainPrescription/MainPrescription.tsx | 4 +- .../MainPageProfile/MainPageProfile.tsx | 175 ++++++++++ .../page/profile/MainPageProfile/index.ts | 1 + .../MainUpdateProfile/MainUpdateProfile.tsx | 300 ++++++++++++++++++ .../page/profile/MainUpdateProfile/index.ts | 1 + .../MainPageStatistical.tsx | 2 +- .../utils/UploadAvatar/UploadAvatar.tsx | 150 ++++----- .../UploadMultipleFile/UploadMultipleFile.tsx | 4 +- src/constant/config/enum.ts | 2 + src/constant/config/index.ts | 1 + src/redux/reducer/auth.ts | 2 +- src/redux/reducer/user.ts | 5 +- src/services/authServices.ts | 6 + src/services/consultationServices.ts | 3 + src/services/index.ts | 8 +- src/services/profileServices.ts | 50 +++ src/services/specialtyServices.ts | 1 + src/services/userServices.ts | 11 + 30 files changed, 1019 insertions(+), 392 deletions(-) create mode 100644 src/app/profile/page.tsx create mode 100644 src/app/profile/update/page.tsx create mode 100644 src/components/page/profile/MainPageProfile/MainPageProfile.tsx create mode 100644 src/components/page/profile/MainPageProfile/index.ts create mode 100644 src/components/page/profile/MainUpdateProfile/MainUpdateProfile.tsx create mode 100644 src/components/page/profile/MainUpdateProfile/index.ts create mode 100644 src/services/profileServices.ts diff --git a/next.config.ts b/next.config.ts index 8f2b429..e9dcf60 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,12 +5,19 @@ const nextConfig: NextConfig = { root: process.cwd(), }, images: { + unoptimized: true, remotePatterns: [ { protocol: "http", hostname: "127.0.0.1", port: "5264", - pathname: "/uploads/**", + pathname: "/**", + }, + { + protocol: "http", + hostname: "localhost", + port: "5264", + pathname: "/**", }, ], }, diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx new file mode 100644 index 0000000..2fc28d6 --- /dev/null +++ b/src/app/profile/page.tsx @@ -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 ( + + + + ); +}; + +export default page; diff --git a/src/app/profile/update/page.tsx b/src/app/profile/update/page.tsx new file mode 100644 index 0000000..4267d9b --- /dev/null +++ b/src/app/profile/update/page.tsx @@ -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 ( + + + + ); +}; + +export default page; diff --git a/src/components/customs/custom-button.tsx b/src/components/customs/custom-button.tsx index 59caf87..f91f9d3 100644 --- a/src/components/customs/custom-button.tsx +++ b/src/components/customs/custom-button.tsx @@ -1,16 +1,21 @@ "use client"; -import React from "react"; import Link from "next/link"; import clsx from "clsx"; export interface PropsButton { - onClick?: (e: React.MouseEvent) => void; children?: React.ReactNode; - href?: string; icon?: React.ReactNode; - className?: string; + + href?: string; target?: string; + + onClick?: ( + e: React.MouseEvent, + ) => void; + + className?: string; + disabled?: boolean; type?: "button" | "submit" | "reset"; @@ -26,17 +31,22 @@ export interface PropsButton { | "warning"; size?: "sm" | "md" | "lg"; - rounded?: "sm" | "md" | "lg" | "full"; + + rounded?: "sm" | "md" | "lg" | "xl" | "full"; + fullWidth?: boolean; + maxContent?: boolean; + maxHeight?: boolean; } export default function CustomButton({ children, - onClick, - href, icon, - className, + href, target, + onClick, + className, + disabled = false, type = "button", @@ -44,10 +54,12 @@ export default function CustomButton({ variant = "default", size = "md", rounded = "md", + fullWidth = true, + maxContent = false, + maxHeight = false, }: PropsButton) { - // 🎨 Variant - const variantClasses: Record = { + const variantClasses = { default: "bg-gray-100 text-gray-700 border border-gray-200 cursor-pointer", midnightBlue: "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", }; - // 📏 Size const sizeClasses = { - sm: "px-3 py-1 text-sm", - md: "px-5 py-2 text-sm", - lg: "px-6 py-3 text-base", + sm: "px-6 py-2 text-[13px]", + md: "px-6 py-3 text-sm", + lg: "px-8 py-3 text-base", }; - // 🔵 Rounded const roundedClasses = { - sm: "rounded-md", - md: "rounded-xl", - lg: "rounded-2xl", + sm: "rounded-sm", + md: "rounded-md", + lg: "rounded-lg", + xl: "rounded-xl", full: "rounded-full", }; const baseClass = clsx( "inline-flex items-center justify-center gap-2", - "transition-all duration-200 select-none", - "active:scale-[0.98] hover:opacity-90", + "font-medium whitespace-nowrap", + "select-none", + "transition-all duration-200", + "hover:opacity-80", + "active:scale-95", + variantClasses[variant], sizeClasses[size], 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, ); const content = ( <> - {icon && {icon}} - {children} + {icon && ( + + {icon} + + )} + + {children} ); - const handleClick = (e: React.MouseEvent) => { - if (disabled) return; + const handleClick = ( + e: React.MouseEvent, + ) => { + if (disabled) { + e.preventDefault(); + return; + } + onClick?.(e); }; - // 🔗 Link mode if (href) { return ( {content} ); } - // 🔘 Button mode return ( diff --git a/src/components/layouts/BaseLayout/componets/Header/Header.tsx b/src/components/layouts/BaseLayout/componets/Header/Header.tsx index b05e7b7..af7ba4d 100644 --- a/src/components/layouts/BaseLayout/componets/Header/Header.tsx +++ b/src/components/layouts/BaseLayout/componets/Header/Header.tsx @@ -1,5 +1,6 @@ "use client"; -import React, { useContext, useEffect, useState } from "react"; + +import React, { useContext, useEffect, useMemo, useState } from "react"; import { PropsHeader } from "./interface"; import { usePathname } from "next/dist/client/components/navigation"; import { useSelector } from "react-redux"; @@ -10,6 +11,10 @@ import icons from "@/constant/images/icons"; import { ChevronDown } from "lucide-react"; import clsx from "clsx"; 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 pathname = usePathname(); @@ -18,7 +23,7 @@ const Header = ({ title, breadcrumb }: PropsHeader) => { const [openProfile, setOpenProfile] = useState(false); - //Đóng menu mobile khi đổi route + // Đóng menu mobile khi đổi route useEffect(() => { if (window.innerWidth < 1280) { 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({ + queryKey: [QUERY_KEY.table_list_profile], + queryFn: async () => { + const res = await httpRequest({ + 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 ( -
+
{/* Nút bấm Mobile: Mở menu mobile */}
{
+ {/* PROFILE CONTROL AREA */}
setOpenProfile(!openProfile)} > -
+
avatar

- {infoUser?.fullname || "User admin"} + {currentUserProfile?.fullName || "User admin"}

{
- {/* CHANGE PASSWORD */} - {/* - -
-

Đổi mật khẩu

-

Thay đổi mật khẩu

-
- -
*/} - {/* LOGOUT */}
{ diff --git a/src/components/page/auth/MainLogin/MainLogin.tsx b/src/components/page/auth/MainLogin/MainLogin.tsx index 10d5c2e..215b899 100644 --- a/src/components/page/auth/MainLogin/MainLogin.tsx +++ b/src/components/page/auth/MainLogin/MainLogin.tsx @@ -1,97 +1,94 @@ "use client"; -import React, { useEffect, useState } from "react"; - +import React, { useMemo, useState } from "react"; import { useRouter } from "next/navigation"; 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 { RootState, store } from "@/redux/store"; - import { setDataLoginStorage, setStateLogin, setToken, } from "@/redux/reducer/auth"; - import { setRememberPassword } from "@/redux/reducer/site"; import { setInfoUser } from "@/redux/reducer/user"; - -import authServices from "@/services/authServices"; +import authServices, { LoginResponse } from "@/services/authServices"; import { httpRequest } from "@/services"; - import { PATH } from "@/constant/config"; import FormCustom from "@/components/utils/FormCustom"; import InputForm from "@/components/utils/FormCustom/components/InputForm"; import CustomLoading from "@/components/customs/custom-loading"; import CustomButton from "@/components/customs/custom-button"; +import { ContextFormCustom } from "@/components/utils/FormCustom/contexts"; export default function MainLogin() { const router = useRouter(); const { dataLoginStorage } = useSelector((state: RootState) => state.auth); - 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 || "" : "", - password: isRememberPassword ? dataLoginStorage?.passwordStorage || "" : "", + })); - deviceId: isRememberPassword ? dataLoginStorage?.deviceIdStorage || "" : "", - }); - - const loginMutation = useMutation({ + const loginMutation = useMutation({ mutationFn: async () => { - return httpRequest({ - showMessageFailed: true, - showMessageSuccess: true, - msgSuccess: "Đăng nhập thành công!", + return (await httpRequest({ http: authServices.login({ username: form.username, password: form.password, - deviceId: form.username, + deviceId: "Browser-Chrome-Windowns", }), - }); + })) as LoginResponse; }, - - onSuccess(data: any) { - if (!data) return; - - 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) { + onSuccess(data) { + if (data) { + store.dispatch(setToken(data?.token)); store.dispatch( - setDataLoginStorage({ - usernameStorage: form.username, - passwordStorage: form.password, - deviceIdStorage: form.deviceId, + setInfoUser({ + accessToken: data?.token || "", + refreshToken: data?.refreshToken || "", + userId: data?.userId || "", }), ); - } else { - store.dispatch(setDataLoginStorage(null)); - } + store.dispatch(setStateLogin(true)); - 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 = () => { + // 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(); }; @@ -101,24 +98,8 @@ export default function MainLogin() { {/* TITLE */} -

- Đăng nhập -

- -

+

Đăng nhập

+

Chào mừng bạn đến với hệ thống quản lý

@@ -128,8 +109,7 @@ export default function MainLogin() { - Tài khoản - * + Tài khoản * } placeholder="Tài khoản" @@ -142,32 +122,12 @@ export default function MainLogin() { icon={} /> - {/*
- - deviceId - * - - } - placeholder="deviceId" - type="text" - name="deviceId" - onClean - isRequired - isBlur - showDone - icon={} - /> -
*/} - {/* PASSWORD */}
- Mật khẩu - * + Mật khẩu * } placeholder="Mật khẩu" @@ -182,15 +142,7 @@ export default function MainLogin() {
{/* REMEMBER PASSWORD */} -
+
store.dispatch(setRememberPassword(!isRememberPassword)) } - className=" - h-5 - w-5 - cursor-pointer - accent-[#0011AB] - " + className="h-5 w-5 cursor-pointer accent-[#0011AB]" /> - @@ -223,64 +163,40 @@ export default function MainLogin() { {/* BUTTONS */}
- {/* LOGIN */} - - Đăng nhập - - {/* REGISTER */} + {/* LOGIN BUTTON */} + + {({ isDone }) => { + // Tự kiểm tra xem form hiện tại đã có sẵn dữ liệu hợp lệ chưa + const hasRememberedData = !!(form.username && form.password); + + // Sáng nút nếu FormCustom báo Done HOẶC form đã có sẵn dữ liệu từ Redux + const canSubmit = isDone || hasRememberedData; + + return ( + + Đăng nhập + + ); + }} + + + {/* REGISTER BUTTON */} router.push(PATH.REGISTER)} > Đăng ký
- - {/* LINE */} - {/*
*/} - - {/* FORGOT PASSWORD */} - {/* */}
diff --git a/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx b/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx index 11692a4..964c1b5 100644 --- a/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx +++ b/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx @@ -120,15 +120,29 @@ const DetailConsultation = () => { type PrescriptionItem = 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) => { if (typeof file === "string") { return { 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 ( @@ -282,7 +296,7 @@ const DetailConsultation = () => {

- {consultation.createdBy || "---"} + {consultation.createdByName || "---"}

diff --git a/src/components/page/consultation/UpdateConsultation/UpdateConsultation.tsx b/src/components/page/consultation/UpdateConsultation/UpdateConsultation.tsx index 8652e4b..0b63318 100644 --- a/src/components/page/consultation/UpdateConsultation/UpdateConsultation.tsx +++ b/src/components/page/consultation/UpdateConsultation/UpdateConsultation.tsx @@ -17,6 +17,7 @@ import consultationServices, { } from "@/services/consultationServices"; import uploadServices from "@/services/uploadServices"; import UploadMultipleFile from "@/components/utils/UploadMultipleFile"; +import TextArea from "@/components/utils/FormCustom/components/TextArea"; /* ========================= TYPES @@ -29,6 +30,7 @@ interface IUpdateConsul { treatmentPlan: string; doctorNotes: string; isGlassesDelivered: boolean; + glassType: string; generalAttachmentUrls: string[]; } @@ -43,6 +45,7 @@ const defaultForm: IUpdateConsul = { treatmentPlan: "", doctorNotes: "", isGlassesDelivered: false, + glassType: "", generalAttachmentUrls: [], }; @@ -101,9 +104,14 @@ const UpdateConsultation = () => { /* ========================= MAP & SET FORM ========================= */ + const consul = consulDetailQuery.data; useEffect(() => { - const consul = consulDetailQuery.data; if (!consul) return; + const eyeClinic = consul?.specialtyClinics?.find( + (item) => + item.specialtyName?.toLowerCase().includes("mắt") && + item.glassRequired === true, + ); setForm({ chiefComplaint: consul.chiefComplaint || "", @@ -112,7 +120,8 @@ const UpdateConsultation = () => { diagnosis: consul.diagnosis || "", treatmentPlan: consul.treatmentPlan || "", doctorNotes: consul.doctorNotes || "", - isGlassesDelivered: consul.isGlassesDelivered, + isGlassesDelivered: !!eyeClinic, + glassType: eyeClinic?.glassType || "", generalAttachmentUrls: consul.generalAttachmentUrls || [], }); @@ -212,6 +221,11 @@ const UpdateConsultation = () => {
); } + const eyeClinic = consul?.specialtyClinics?.find( + (item) => + item.specialtyName?.toLowerCase().includes("mắt") && + item.glassRequired === true, + ); /* ========================= MAIN UI @@ -324,6 +338,56 @@ const UpdateConsultation = () => { } /> + + {eyeClinic && ( +
+

+ Chỉ định thị lực (Khoa Mắt) +

+ +
+
+ +
+ +
+
+ +