feat:update web
This commit is contained in:
parent
b6ccde2c74
commit
b0baa509d9
15
src/app/auth/register/page.tsx
Normal file
15
src/app/auth/register/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import LayoutAuth from "@/components/layouts/LayoutAuth";
|
||||
import MainRegister from "@/components/page/auth/MainRegister";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<LayoutAuth>
|
||||
<MainRegister />
|
||||
</LayoutAuth>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
16
src/app/consultation/[id]/page.tsx
Normal file
16
src/app/consultation/[id]/page.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import DetailConsultation from "@/components/page/consultation/DetailConsultation";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<BaseLayout title="Chi tiết phiên khám">
|
||||
<DetailConsultation />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@ -1,9 +1,14 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import MainPageConsultation from "@/components/page/consultation/MainPageConsultation";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <BaseLayout title="Phiên khám">Phiên khám</BaseLayout>;
|
||||
return (
|
||||
<BaseLayout title="Phiên khám">
|
||||
<MainPageConsultation />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import BaseLayout from "@/components/layouts/BaseLayout/BaseLayout";
|
||||
import MainPageHome from "@/components/page/Home/MainPageHome/MainPageHome";
|
||||
import Image from "next/image";
|
||||
import MainPagePatient from "@/components/page/patient/MainPagePatient";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<BaseLayout title="Dashboard">
|
||||
<MainPageHome />
|
||||
<BaseLayout title="Bệnh nhân">
|
||||
<MainPagePatient />
|
||||
</BaseLayout>
|
||||
);
|
||||
}
|
||||
|
||||
16
src/app/patient/[id]/page.tsx
Normal file
16
src/app/patient/[id]/page.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import DetailPatient from "@/components/page/patient/DetailPatient";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<BaseLayout title="Chi tiết bệnh nhân">
|
||||
<DetailPatient />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@ -1,12 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import MainPagePatient from "@/components/page/patient/MainPagePatient/MainPagePatient";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div>
|
||||
<BaseLayout title="Bệnh nhân">Bệnh nhân</BaseLayout>
|
||||
<BaseLayout title="Bệnh nhân">
|
||||
<MainPagePatient />
|
||||
</BaseLayout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
14
src/app/prescription/[id]/page.tsx
Normal file
14
src/app/prescription/[id]/page.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import DetailPrescription from "@/components/page/prescription/DetailPrescription";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Chi tiết đơn thuốc">
|
||||
<DetailPrescription />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
14
src/app/prescription/create/page.tsx
Normal file
14
src/app/prescription/create/page.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import CreatePrescription from "@/components/page/prescription/CreatePrescription";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Tạo đơn thuốc">
|
||||
<CreatePrescription />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@ -1,9 +1,14 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import MainPrescription from "@/components/page/prescription/MainPrescription";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return <BaseLayout title="Đơn thuốc">Đơn thuốc</BaseLayout>;
|
||||
return (
|
||||
<BaseLayout title="Đơn thuốc">
|
||||
<MainPrescription />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
|
||||
14
src/app/prescription/update/page.tsx
Normal file
14
src/app/prescription/update/page.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import UpdatePrescription from "@/components/page/prescription/UpdatePrescription";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Cập nhật đơn thuốc">
|
||||
<UpdatePrescription />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@ -13,6 +13,8 @@ export interface PropsButton {
|
||||
target?: string;
|
||||
disabled?: boolean;
|
||||
|
||||
type?: "button" | "submit" | "reset";
|
||||
|
||||
variant?:
|
||||
| "default"
|
||||
| "midnightBlue"
|
||||
@ -36,6 +38,9 @@ export default function CustomButton({
|
||||
className,
|
||||
target,
|
||||
disabled = false,
|
||||
|
||||
type = "button",
|
||||
|
||||
variant = "default",
|
||||
size = "md",
|
||||
rounded = "md",
|
||||
@ -112,7 +117,7 @@ export default function CustomButton({
|
||||
// 🔘 Button mode
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
type={type}
|
||||
onClick={handleClick}
|
||||
className={baseClass}
|
||||
disabled={disabled}
|
||||
|
||||
@ -12,59 +12,59 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
||||
const [showFull, setShowFull] = React.useState(false);
|
||||
const [openMenuMobile, setOpenMenuMobile] = React.useState(false);
|
||||
return (
|
||||
// <RequireAuth>
|
||||
<ContextBaseLayout
|
||||
value={{ showFull, setShowFull, openMenuMobile, setOpenMenuMobile }}
|
||||
>
|
||||
<div className="min-h-screen bg-[#f8f8f8]">
|
||||
{/* OVERLAY cho Mobile */}
|
||||
<div
|
||||
className={clsx(
|
||||
"fixed inset-0 bg-black/50 backdrop-blur-sm z-[20] transition-opacity duration-300 xl:hidden",
|
||||
openMenuMobile ? "opacity-100 visible" : "opacity-0 invisible",
|
||||
)}
|
||||
onClick={() => setOpenMenuMobile(false)}
|
||||
/>
|
||||
<RequireAuth>
|
||||
<ContextBaseLayout
|
||||
value={{ showFull, setShowFull, openMenuMobile, setOpenMenuMobile }}
|
||||
>
|
||||
<div className="min-h-screen bg-[#f8f8f8]">
|
||||
{/* OVERLAY cho Mobile */}
|
||||
<div
|
||||
className={clsx(
|
||||
"fixed inset-0 bg-black/50 backdrop-blur-sm z-[20] transition-opacity duration-300 xl:hidden",
|
||||
openMenuMobile ? "opacity-100 visible" : "opacity-0 invisible",
|
||||
)}
|
||||
onClick={() => setOpenMenuMobile(false)}
|
||||
/>
|
||||
|
||||
{/* SIDEBAR (Dùng chung cho cả Desktop & Mobile) */}
|
||||
<nav
|
||||
className={clsx(
|
||||
"fixed top-0 left-0 h-full w-[240px] z-[21] bg-white transition-all duration-300 border-r border-[#f4f7fa]",
|
||||
// Desktop logic
|
||||
showFull ? "xl:translate-x-0" : "xl:-translate-x-full",
|
||||
// Mobile logic
|
||||
openMenuMobile ? "translate-x-0" : "-translate-x-full",
|
||||
)}
|
||||
>
|
||||
<Navbar />
|
||||
</nav>
|
||||
{/* SIDEBAR (Dùng chung cho cả Desktop & Mobile) */}
|
||||
<nav
|
||||
className={clsx(
|
||||
"fixed top-0 left-0 h-full w-[240px] z-[21] bg-white transition-all duration-300 border-r border-[#f4f7fa]",
|
||||
// Desktop logic
|
||||
showFull ? "xl:translate-x-0" : "xl:-translate-x-full",
|
||||
// Mobile logic
|
||||
openMenuMobile ? "translate-x-0" : "-translate-x-full",
|
||||
)}
|
||||
>
|
||||
<Navbar />
|
||||
</nav>
|
||||
|
||||
{/* HEADER */}
|
||||
<header
|
||||
className={clsx(
|
||||
"fixed top-0 right-0 h-[68px] z-[11] bg-white transition-all duration-300 border-b border-[#f4f7fa]",
|
||||
showFull
|
||||
? "xl:left-[240px] xl:w-[calc(100%-240px)]"
|
||||
: "left-0 w-full",
|
||||
"left-0 w-full", // Mặc định full width trên mobile
|
||||
)}
|
||||
>
|
||||
<Header title={title} breadcrumb={breadcrumb} />
|
||||
</header>
|
||||
{/* HEADER */}
|
||||
<header
|
||||
className={clsx(
|
||||
"fixed top-0 right-0 h-[68px] z-[11] bg-white transition-all duration-300 border-b border-[#f4f7fa]",
|
||||
showFull
|
||||
? "xl:left-[240px] xl:w-[calc(100%-240px)]"
|
||||
: "left-0 w-full",
|
||||
"left-0 w-full", // Mặc định full width trên mobile
|
||||
)}
|
||||
>
|
||||
<Header title={title} breadcrumb={breadcrumb} />
|
||||
</header>
|
||||
|
||||
{/* MAIN CONTENT */}
|
||||
<main
|
||||
className={clsx(
|
||||
"pt-[92px] pb-6 px-6 transition-all duration-300",
|
||||
showFull ? "xl:pl-[264px]" : "xl:pl-6",
|
||||
"pl-6", // Mặc định padding trên mobile
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</ContextBaseLayout>
|
||||
// </RequireAuth>
|
||||
{/* MAIN CONTENT */}
|
||||
<main
|
||||
className={clsx(
|
||||
"pt-[92px] pb-6 px-6 transition-all duration-300",
|
||||
showFull ? "xl:pl-[264px]" : "xl:pl-6",
|
||||
"pl-6", // Mặc định padding trên mobile
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</ContextBaseLayout>
|
||||
</RequireAuth>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { use, useCallback, useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { PropsMenuProfile } from "./interface";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
@ -11,15 +11,12 @@ import { PATH } from "@/constant/config";
|
||||
import Link from "next/link";
|
||||
import clsx from "clsx";
|
||||
import { LogOut, ShieldCog, ShieldUser } from "lucide-react";
|
||||
import CustomDialog from "@/components/customs/custom-dialog";
|
||||
|
||||
const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [openLogout, setOpenLogout] = useState(false);
|
||||
|
||||
/* ================== CHECK ACTIVE ================== */
|
||||
const checkActive = useCallback(
|
||||
(path: string) => {
|
||||
@ -32,16 +29,18 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
||||
const logoutMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageFailed: false,
|
||||
showMessageSuccess: false,
|
||||
http: authServices.logout(),
|
||||
}),
|
||||
onSuccess(data) {
|
||||
if (data) {
|
||||
store.dispatch(logout());
|
||||
store.dispatch(setInfoUser(null));
|
||||
router.push(PATH.LOGIN);
|
||||
}
|
||||
|
||||
onSettled() {
|
||||
store.dispatch(logout());
|
||||
store.dispatch(setInfoUser(null));
|
||||
|
||||
onClose();
|
||||
|
||||
router.replace(PATH.LOGIN);
|
||||
},
|
||||
});
|
||||
|
||||
@ -90,8 +89,7 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
||||
{/* LOGOUT */}
|
||||
<div
|
||||
onClick={() => {
|
||||
setOpenLogout(true);
|
||||
onClose();
|
||||
handleLogout();
|
||||
}}
|
||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-red-50 cursor-pointer transition"
|
||||
>
|
||||
@ -101,18 +99,6 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
||||
<p className="text-xs text-gray-500">Đăng xuất khỏi hệ thống</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DIALOG */}
|
||||
<CustomDialog
|
||||
open={openLogout}
|
||||
onClose={() => setOpenLogout(false)}
|
||||
onSubmit={handleLogout}
|
||||
title="Đăng xuất"
|
||||
note="Bạn có muốn đăng xuất khỏi hệ thống không?"
|
||||
titleCancel="Không"
|
||||
titleSubmit="Đăng xuất"
|
||||
type="error"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -7,6 +7,7 @@ import { Menus, PATH } from "@/constant/config";
|
||||
import Image from "next/image";
|
||||
import icons from "@/constant/images/icons";
|
||||
import clsx from "clsx";
|
||||
import { Hospital } from "lucide-react";
|
||||
|
||||
const Navbar = () => {
|
||||
const pathname = usePathname();
|
||||
@ -22,10 +23,11 @@ const Navbar = () => {
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col items-center p-3 bg-white">
|
||||
<Link href={PATH.HOME} className="flex items-center justify-center py-2">
|
||||
<Image alt="Logo" src={icons.avatar} width={40} height={40} />
|
||||
<h4 className="ml-2 text-sm font-semibold text-blue-900 select-none">
|
||||
Quản lý
|
||||
</h4>
|
||||
{/* <Image alt="Logo" src={icons.avatar} width={40} height={40} /> */}
|
||||
<Hospital className="text-blue-900" />
|
||||
<h2 className="ml-2 text-sm font-semibold text-blue-900 select-none">
|
||||
Quản lý khám bệnh
|
||||
</h2>
|
||||
</Link>
|
||||
<div className="flex-1 w-full overflow-auto pt-3">
|
||||
{Menus.map((menu, i) => (
|
||||
|
||||
@ -57,13 +57,9 @@ export default function FormPassword() {
|
||||
http: authServices.login({
|
||||
username: dataLoginStorage?.usernameStorage || "",
|
||||
|
||||
password: md5(
|
||||
`${form.password}_${process.env.NEXT_PUBLIC_KEY_PASSWORD}`,
|
||||
),
|
||||
password: md5(`${form.password}_${process.env.NEXT_PUBLIC_KEY_PASS}`),
|
||||
|
||||
ip: "",
|
||||
address: "",
|
||||
type: 0,
|
||||
deviceId: dataLoginStorage?.deviceIdStorage || "web-browser",
|
||||
}),
|
||||
});
|
||||
},
|
||||
@ -73,11 +69,11 @@ export default function FormPassword() {
|
||||
|
||||
store.dispatch(setStateLogin(true));
|
||||
|
||||
store.dispatch(setToken(data.accessToken));
|
||||
store.dispatch(setToken(data.token));
|
||||
|
||||
store.dispatch(
|
||||
setInfoUser({
|
||||
accessToken: data?.accessToken || "",
|
||||
accessToken: data?.token || "",
|
||||
refreshToken: data?.refreshToken || "",
|
||||
accessExpiresAt: data?.accessExpiresAt || "",
|
||||
refreshExpiresAt: data?.refreshExpiresAt || "",
|
||||
@ -92,6 +88,7 @@ export default function FormPassword() {
|
||||
usernameStorage: dataLoginStorage?.usernameStorage || "",
|
||||
|
||||
passwordStorage: form.password,
|
||||
deviceIdStorage: dataLoginStorage?.deviceIdStorage || "web-browser",
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
|
||||
@ -26,7 +26,7 @@ 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 FormCustom, { InputForm } from "@/components/common/FormCustom";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
|
||||
export default function MainLogin() {
|
||||
const router = useRouter();
|
||||
@ -39,6 +39,8 @@ export default function MainLogin() {
|
||||
username: isRememberPassword ? dataLoginStorage?.usernameStorage || "" : "",
|
||||
|
||||
password: isRememberPassword ? dataLoginStorage?.passwordStorage || "" : "",
|
||||
|
||||
deviceId: isRememberPassword ? dataLoginStorage?.deviceIdStorage || "" : "",
|
||||
});
|
||||
|
||||
const loginMutation = useMutation({
|
||||
@ -50,9 +52,7 @@ export default function MainLogin() {
|
||||
http: authServices.login({
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
ip: "",
|
||||
address: "",
|
||||
type: 0,
|
||||
deviceId: form.deviceId,
|
||||
}),
|
||||
});
|
||||
},
|
||||
@ -62,11 +62,11 @@ export default function MainLogin() {
|
||||
|
||||
store.dispatch(setStateLogin(true));
|
||||
|
||||
store.dispatch(setToken(data.accessToken));
|
||||
store.dispatch(setToken(data.token));
|
||||
|
||||
store.dispatch(
|
||||
setInfoUser({
|
||||
accessToken: data?.accessToken || "",
|
||||
accessToken: data?.token || "",
|
||||
refreshToken: data?.refreshToken || "",
|
||||
accessExpiresAt: data?.accessExpiresAt || "",
|
||||
refreshExpiresAt: data?.refreshExpiresAt || "",
|
||||
@ -80,6 +80,7 @@ export default function MainLogin() {
|
||||
setDataLoginStorage({
|
||||
usernameStorage: form.username,
|
||||
passwordStorage: form.password,
|
||||
deviceIdStorage: form.deviceId,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
@ -141,6 +142,25 @@ export default function MainLogin() {
|
||||
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 */}
|
||||
<div className="mt-5">
|
||||
<InputForm
|
||||
@ -202,42 +222,42 @@ export default function MainLogin() {
|
||||
|
||||
{/* BUTTONS */}
|
||||
<div className="mt-6">
|
||||
{/* LOGIN */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loginMutation.isPending}
|
||||
className="
|
||||
flex
|
||||
h-[52px]
|
||||
w-full
|
||||
items-center
|
||||
justify-center
|
||||
rounded-full
|
||||
bg-[#0011AB]
|
||||
px-6
|
||||
text-white
|
||||
font-bold
|
||||
transition-all
|
||||
hover:opacity-90
|
||||
disabled:cursor-not-allowed
|
||||
disabled:opacity-50
|
||||
"
|
||||
>
|
||||
Đăng nhập
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
{/* LOGIN */}
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
disabled={loginMutation.isPending}
|
||||
rounded="full"
|
||||
type="submit"
|
||||
className="font-bold text-2xl disabled:cursor-not-allowed
|
||||
disabled:opacity-50 h-[52px]"
|
||||
>
|
||||
Đăng nhập
|
||||
</CustomButton>
|
||||
{/* REGISTER */}
|
||||
<CustomButton
|
||||
variant="default"
|
||||
rounded="full"
|
||||
className="font-bold text-2xl disabled:cursor-not-allowed
|
||||
disabled:opacity-50 h-[52px]"
|
||||
onClick={() => router.push(PATH.REGISTER)}
|
||||
>
|
||||
Đăng ký
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{/* LINE */}
|
||||
<div
|
||||
{/* <div
|
||||
className="
|
||||
my-5
|
||||
h-[1px]
|
||||
w-full
|
||||
bg-[#E5E5E5]
|
||||
"
|
||||
/>
|
||||
/> */}
|
||||
|
||||
{/* FORGOT PASSWORD */}
|
||||
<button
|
||||
{/* <button
|
||||
type="button"
|
||||
onClick={() => router.push(PATH.FORGOT_PASSWORD)}
|
||||
className="
|
||||
@ -260,7 +280,7 @@ export default function MainLogin() {
|
||||
>
|
||||
<LockKeyhole size={22} />
|
||||
Quên mật khẩu
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
|
||||
200
src/components/page/auth/MainRegister/MainRegister.tsx
Normal file
200
src/components/page/auth/MainRegister/MainRegister.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ShieldPlus, User, Mail, BadgeInfo } from "lucide-react";
|
||||
|
||||
import authServices 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";
|
||||
|
||||
export default function MainRegister() {
|
||||
const router = useRouter();
|
||||
|
||||
const [form, setForm] = useState({
|
||||
username: "",
|
||||
password: "",
|
||||
fullName: "",
|
||||
email: "",
|
||||
});
|
||||
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Đăng ký thành công!",
|
||||
http: authServices.register({
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
fullName: form.fullName,
|
||||
email: form.email,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess(data: any) {
|
||||
if (!data) return;
|
||||
|
||||
router.replace(PATH.LOGIN);
|
||||
},
|
||||
});
|
||||
|
||||
const handleRegister = () => {
|
||||
registerMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<CustomLoading loading={registerMutation.isPending} />
|
||||
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleRegister}>
|
||||
{/* TITLE */}
|
||||
<h3
|
||||
className="
|
||||
text-[34px]
|
||||
font-semibold
|
||||
text-[#1A1B2D]
|
||||
"
|
||||
>
|
||||
Đăng ký
|
||||
</h3>
|
||||
|
||||
<p
|
||||
className="
|
||||
mt-1
|
||||
text-[14px]
|
||||
font-medium
|
||||
text-[#6F767E]
|
||||
"
|
||||
>
|
||||
Tạo tài khoản mới cho hệ thống
|
||||
</p>
|
||||
|
||||
{/* FORM */}
|
||||
<div className="mt-6">
|
||||
{/* FULL NAME */}
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Họ và tên
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Nhập họ và tên"
|
||||
type="text"
|
||||
name="fullName"
|
||||
onClean
|
||||
isRequired
|
||||
isBlur
|
||||
showDone
|
||||
icon={<BadgeInfo size={22} />}
|
||||
/>
|
||||
|
||||
{/* EMAIL */}
|
||||
<div className="mt-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Email
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Nhập email"
|
||||
type="email"
|
||||
name="email"
|
||||
onClean
|
||||
isRequired
|
||||
isBlur
|
||||
showDone
|
||||
icon={<Mail size={22} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* USERNAME */}
|
||||
<div className="mt-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Tài khoản
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Nhập tài khoản"
|
||||
type="text"
|
||||
name="username"
|
||||
onClean
|
||||
isRequired
|
||||
isBlur
|
||||
showDone
|
||||
icon={<User size={22} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* PASSWORD */}
|
||||
<div className="mt-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Mật khẩu
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Nhập mật khẩu"
|
||||
type="password"
|
||||
name="password"
|
||||
onClean
|
||||
isRequired
|
||||
isBlur
|
||||
showDone
|
||||
icon={<ShieldPlus size={22} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* BUTTON */}
|
||||
<div className="mt-6 flex gap-2">
|
||||
{/* REGISTER */}
|
||||
<CustomButton
|
||||
type="submit"
|
||||
variant="midnightBlue"
|
||||
disabled={registerMutation.isPending}
|
||||
rounded="full"
|
||||
className="
|
||||
h-[52px]
|
||||
font-bold
|
||||
text-2xl
|
||||
disabled:cursor-not-allowed
|
||||
disabled:opacity-50
|
||||
"
|
||||
>
|
||||
Đăng ký
|
||||
</CustomButton>
|
||||
|
||||
{/* LOGIN */}
|
||||
<CustomButton
|
||||
type="button"
|
||||
variant="default"
|
||||
rounded="full"
|
||||
className="
|
||||
h-[52px]
|
||||
font-bold
|
||||
text-2xl
|
||||
"
|
||||
onClick={() => router.push(PATH.LOGIN)}
|
||||
>
|
||||
Đăng nhập
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
src/components/page/auth/MainRegister/index.ts
Normal file
1
src/components/page/auth/MainRegister/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./MainRegister";
|
||||
@ -0,0 +1,237 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
|
||||
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationDetail,
|
||||
} from "@/services/consultationServices";
|
||||
|
||||
import StateActive from "@/components/customs/StateActive";
|
||||
|
||||
const DetailConsultation = () => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const consultationId = params?.id as string;
|
||||
|
||||
const consulDetailQuery = useQuery<ConsultationDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId],
|
||||
|
||||
enabled: !!consultationId,
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationDetail>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: consultationServices.detailConsultations(consultationId),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
patientId: "",
|
||||
sessionCode: "",
|
||||
visitDate: "",
|
||||
status: 1,
|
||||
chiefComplaint: null,
|
||||
symptomsText: null,
|
||||
vitalSigns: null,
|
||||
diagnosis: null,
|
||||
treatmentPlan: null,
|
||||
doctorNotes: null,
|
||||
createdBy: "",
|
||||
isDeleted: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const consultation = consulDetailQuery.data;
|
||||
|
||||
if (consulDetailQuery.isLoading) {
|
||||
return <div>Đang tải dữ liệu...</div>;
|
||||
}
|
||||
|
||||
if (!consultation) {
|
||||
return <div>Không tìm thấy dữ liệu phiên khám</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center gap-3">
|
||||
<ArrowLeft
|
||||
onClick={() => router.back()}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
|
||||
<h2 className="text-[28px] font-semibold text-[#111827]">
|
||||
Chi tiết phiên khám
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* CONTENT */}
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
{/* ID PHIÊN KHÁM */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
ID PHIÊN KHÁM
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.id}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* MÃ PHIÊN KHÁM */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
MÃ PHIÊN KHÁM
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.sessionCode}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* NGÀY GIỜ KHÁM */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
NGÀY, GIỜ KHÁM
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.visitDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* TRIỆU CHỨNG */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">DẤU HIỆU</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.symptomsText || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* KHIẾU NẠI CHÍNH */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
KHIẾU NẠI CHÍNH
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.chiefComplaint || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* DẤU HIỆU SINH TỒN */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
DẤU HIỆU SINH TỒN
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.vitalSigns || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CHẨN ĐOÁN */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
CHUẨN ĐOÁN
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.diagnosis || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* KẾ HOẠCH ĐIỀU TRỊ */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
KẾ HOẠCH ĐIỀU TRỊ
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.treatmentPlan || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* GHI CHÚ BÁC SĨ */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
GHI CHÚ BÁC SĨ
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.doctorNotes || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* NGƯỜI TẠO */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
NGƯỜI TẠO
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.createdBy || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* TRẠNG THÁI */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
TRẠNG THÁI
|
||||
</p>
|
||||
|
||||
<StateActive
|
||||
stateActive={consultation.status}
|
||||
listState={[
|
||||
{
|
||||
state: TYPE_STATUS.Draft,
|
||||
text: "Đang chờ",
|
||||
textColor: "#000",
|
||||
backgroundColor: "#FFE4C4",
|
||||
},
|
||||
|
||||
{
|
||||
state: TYPE_STATUS.InProgress,
|
||||
text: "Đang thực hiện",
|
||||
textColor: "#000",
|
||||
backgroundColor: "#ff3300",
|
||||
},
|
||||
|
||||
{
|
||||
state: TYPE_STATUS.Completed,
|
||||
text: "Hoàn thành",
|
||||
textColor: "#000",
|
||||
backgroundColor: "#3d69eb",
|
||||
},
|
||||
{
|
||||
state: TYPE_STATUS.Cancelled,
|
||||
text: "Đã hủy",
|
||||
textColor: "#000",
|
||||
backgroundColor: "#cccc",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailConsultation;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from "./DetailConsultation";
|
||||
@ -0,0 +1,481 @@
|
||||
"use client";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import CustomDialog from "@/components/customs/custom-dialog";
|
||||
import CustomPopup from "@/components/customs/custom-popup";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import Search from "@/components/customs/custom-search";
|
||||
import StateActive from "@/components/customs/StateActive";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
|
||||
import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationItem,
|
||||
ConsultationResponse,
|
||||
} from "@/services/consultationServices";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { Pencil, ClipboardPlus } from "lucide-react";
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
import PopupUpdateConsultation from "../PopupUpdateConsultation";
|
||||
import PopupCreateConsultation from "../PopupCreateConsultation";
|
||||
|
||||
const MainPageConsultation = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
/* =========================
|
||||
QUERY PARAMS
|
||||
========================= */
|
||||
|
||||
const _page = searchParams.get("_page");
|
||||
|
||||
const _pageSize = searchParams.get("_pageSize");
|
||||
|
||||
const _keyword = searchParams.get("_keyword");
|
||||
|
||||
const _create = searchParams.get("_create");
|
||||
|
||||
const _update = searchParams.get("_update");
|
||||
|
||||
/* =========================
|
||||
STATE
|
||||
========================= */
|
||||
|
||||
const [selectedConsultationId, setSelectedConsultationId] =
|
||||
useState<string>("");
|
||||
|
||||
const [openCancelDialog, setOpenCancelDialog] = useState(false);
|
||||
|
||||
const [openCompleteDialog, setOpenCompleteDialog] = useState(false);
|
||||
|
||||
/* =========================
|
||||
GET LIST
|
||||
========================= */
|
||||
|
||||
const consultationQuery = useQuery<ConsultationResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_consultation, _page, _pageSize, _keyword],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: consultationServices.getConsultations({
|
||||
Page: Number(_page || 1),
|
||||
|
||||
PageSize: Number(_pageSize || 10),
|
||||
|
||||
Search: _keyword || "",
|
||||
|
||||
SortBy: "id",
|
||||
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
DATA
|
||||
========================= */
|
||||
|
||||
const consultationData = useMemo(() => {
|
||||
return consultationQuery.data?.items || [];
|
||||
}, [consultationQuery.data]);
|
||||
|
||||
/* =========================
|
||||
UPDATE QUERY
|
||||
========================= */
|
||||
|
||||
const updateQuery = (key: string, value: string | number) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (!value || value === "" || (key === "_page" && value === 1)) {
|
||||
params.delete(key);
|
||||
} else {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
|
||||
if (key === "_pageSize") {
|
||||
params.delete("_page");
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
/* =========================
|
||||
CREATE POPUP
|
||||
========================= */
|
||||
|
||||
const handleOpenCreate = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.set("_create", "true");
|
||||
|
||||
router.push(`?${params.toString()}`);
|
||||
};
|
||||
|
||||
/* =========================
|
||||
UPDATE POPUP
|
||||
========================= */
|
||||
|
||||
const handleOpenUpdate = (item: ConsultationItem) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.set("_update", item.id);
|
||||
|
||||
router.push(`?${params.toString()}`);
|
||||
};
|
||||
|
||||
/* =========================
|
||||
CLOSE POPUP
|
||||
========================= */
|
||||
|
||||
const handleClosePopup = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.delete("_create");
|
||||
|
||||
params.delete("_update");
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
/* =========================
|
||||
DIALOG
|
||||
========================= */
|
||||
|
||||
const handleOpenCancel = (item: ConsultationItem) => {
|
||||
setSelectedConsultationId(item.id);
|
||||
|
||||
setOpenCancelDialog(true);
|
||||
};
|
||||
|
||||
const handleOpenComplete = (item: ConsultationItem) => {
|
||||
setSelectedConsultationId(item.id);
|
||||
|
||||
setOpenCompleteDialog(true);
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setOpenCancelDialog(false);
|
||||
|
||||
setOpenCompleteDialog(false);
|
||||
};
|
||||
|
||||
/* =========================
|
||||
CANCEL
|
||||
========================= */
|
||||
|
||||
const cancelConsultationMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
|
||||
showMessageSuccess: true,
|
||||
|
||||
msgSuccess: "Hủy phiên khám thành công!",
|
||||
|
||||
http: consultationServices.cancelConsultations(selectedConsultationId),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
COMPLETE
|
||||
========================= */
|
||||
|
||||
const completeConsultationMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
|
||||
showMessageSuccess: true,
|
||||
|
||||
msgSuccess: "Hoàn thành phiên khám thành công!",
|
||||
|
||||
http: consultationServices.completeConsultations(
|
||||
selectedConsultationId,
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
CONFIRM
|
||||
========================= */
|
||||
|
||||
const handleConfirmCancel = () => {
|
||||
cancelConsultationMutation.mutate();
|
||||
};
|
||||
|
||||
const handleConfirmComplete = () => {
|
||||
completeConsultationMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-black">Phiên khám</h2>
|
||||
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<ClipboardPlus />}
|
||||
onClick={handleOpenCreate}
|
||||
>
|
||||
Thêm mới
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{/* SEARCH */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="w-full md:min-w-[400px]">
|
||||
<Search
|
||||
keyword={_keyword ?? ""}
|
||||
setKeyword={(value) => updateQuery("_keyword", value)}
|
||||
placeholder="Tìm kiếm phiên khám..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TABLE */}
|
||||
<DataWrapper
|
||||
data={consultationData}
|
||||
loading={consultationQuery.isLoading}
|
||||
title="Không có dữ liệu"
|
||||
note="Vui lòng thử lại sau"
|
||||
>
|
||||
<Table
|
||||
rowKey={(item) => item.id}
|
||||
data={consultationData}
|
||||
column={[
|
||||
{
|
||||
fixedLeft: true,
|
||||
|
||||
title: "ID PHIÊN KHÁM",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<Link
|
||||
href={`/consultation/${item.id}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.id}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "MÃ PHIÊN KHÁM",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.sessionCode}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "NGÀY KHÁM",
|
||||
|
||||
render: (item: ConsultationItem) => <span>{item.visitDate}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
title: "DẤU HIỆU",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.symptomsText || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "CHUẨN ĐOÁN",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.diagnosis || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "KẾ HOẠCH ĐIỀU TRỊ",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.treatmentPlan || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "GHI CHÚ BÁC SĨ",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.doctorNotes || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "TRẠNG THÁI",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<StateActive
|
||||
stateActive={item.status}
|
||||
listState={[
|
||||
{
|
||||
state: 1,
|
||||
text: "Đang khám",
|
||||
textColor: "#000",
|
||||
backgroundColor: "#FFE4C4",
|
||||
},
|
||||
|
||||
{
|
||||
state: 2,
|
||||
text: "Hoàn thành",
|
||||
textColor: "#fff",
|
||||
backgroundColor: "#22c55e",
|
||||
},
|
||||
|
||||
{
|
||||
state: 3,
|
||||
text: "Đã hủy",
|
||||
textColor: "#fff",
|
||||
backgroundColor: "#ef4444",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
fixedRight: true,
|
||||
|
||||
title: "ACTION",
|
||||
|
||||
render: (item: ConsultationItem) => {
|
||||
const isCancelled = item.status === TYPE_STATUS.Cancelled;
|
||||
|
||||
const isCompleted = item.status === TYPE_STATUS.Completed;
|
||||
|
||||
const isDisabled = isCancelled || isCompleted;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* UPDATE */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
disabled={isDisabled}
|
||||
icon={<Pencil className="text-blue-400" />}
|
||||
onClick={() => handleOpenUpdate(item)}
|
||||
/>
|
||||
|
||||
{/* CANCEL */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
variant="red"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleOpenCancel(item)}
|
||||
>
|
||||
Hủy
|
||||
</CustomButton>
|
||||
|
||||
{/* COMPLETE */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
variant="green"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleOpenComplete(item)}
|
||||
>
|
||||
Hoàn thành
|
||||
</CustomButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
|
||||
{/* CANCEL DIALOG */}
|
||||
<CustomDialog
|
||||
open={openCancelDialog}
|
||||
title="Hủy phiên khám"
|
||||
note="Bạn có chắc chắn muốn hủy phiên khám này không?"
|
||||
type="error"
|
||||
onClose={handleCloseDialog}
|
||||
onSubmit={handleConfirmCancel}
|
||||
titleCancel="Đóng"
|
||||
titleSubmit="Xác nhận"
|
||||
/>
|
||||
|
||||
{/* COMPLETE DIALOG */}
|
||||
<CustomDialog
|
||||
open={openCompleteDialog}
|
||||
title="Hoàn thành phiên khám"
|
||||
note="Bạn có chắc chắn muốn hoàn thành phiên khám này không?"
|
||||
type="success"
|
||||
onClose={handleCloseDialog}
|
||||
onSubmit={handleConfirmComplete}
|
||||
titleCancel="Đóng"
|
||||
titleSubmit="Xác nhận"
|
||||
/>
|
||||
|
||||
{/* CREATE */}
|
||||
<CustomPopup open={!!_create} onClose={handleClosePopup}>
|
||||
<PopupCreateConsultation onClose={handleClosePopup} />
|
||||
</CustomPopup>
|
||||
|
||||
{/* UPDATE */}
|
||||
<CustomPopup open={!!_update} onClose={handleClosePopup}>
|
||||
<PopupUpdateConsultation
|
||||
id={_update || ""}
|
||||
onClose={handleClosePopup}
|
||||
/>
|
||||
</CustomPopup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPageConsultation;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from "./MainPageConsultation";
|
||||
@ -0,0 +1,222 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import FormCustom from "@/components/utils/FormCustom/FormCustom";
|
||||
|
||||
import SelectForm from "@/components/utils/FormCustom/components/SelectForm";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import consultationServices from "@/services/consultationServices";
|
||||
|
||||
import patientServices, {
|
||||
PatientItem,
|
||||
PatientResponse,
|
||||
} from "@/services/patientServices";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
|
||||
export interface PopupCreateConsultationProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PopupCreateConsultation = ({ onClose }: PopupCreateConsultationProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [form, setForm] = useState<{
|
||||
patientId: string;
|
||||
}>({
|
||||
patientId: "",
|
||||
});
|
||||
|
||||
/* =========================
|
||||
GET LIST PATIENT
|
||||
========================= */
|
||||
const patientQuery = useQuery<PatientResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_patient],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: patientServices.getPatient({
|
||||
Page: 1,
|
||||
PageSize: 1000,
|
||||
Search: "",
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
OPTIONS SELECT
|
||||
========================= */
|
||||
const patientOptions = useMemo(() => {
|
||||
return patientQuery.data?.items || [];
|
||||
}, [patientQuery.data]);
|
||||
|
||||
/* =========================
|
||||
CREATE CONSULTATION
|
||||
========================= */
|
||||
const createConsultationMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Tạo phiên khám thành công!",
|
||||
|
||||
http: consultationServices.createConsultations({
|
||||
patientId: form.patientId,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
createConsultationMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
h-[400px]
|
||||
w-[640px]
|
||||
max-w-[95vw]
|
||||
rounded-3xl
|
||||
bg-white
|
||||
shadow-xl
|
||||
flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
{/* CLOSE */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="
|
||||
absolute
|
||||
right-5
|
||||
top-5
|
||||
z-10
|
||||
text-gray-400
|
||||
transition
|
||||
hover:text-gray-600
|
||||
"
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
|
||||
{/* HEADER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-b
|
||||
px-7
|
||||
py-5
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<h2 className="text-[30px] font-semibold text-[#111827]">
|
||||
Thêm phiên khám
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div className="flex-1 overflow-y-auto px-7 py-5">
|
||||
<div className="space-y-5">
|
||||
<SelectForm
|
||||
label={
|
||||
<span className="text-[14px] font-medium text-[#374151]">
|
||||
Bệnh nhân
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Chọn bệnh nhân"
|
||||
value={form.patientId}
|
||||
options={patientOptions}
|
||||
// loading={patientQuery.isLoading}
|
||||
onSelect={(item: PatientItem) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
patientId: item.id,
|
||||
}))
|
||||
}
|
||||
onClean={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
patientId: "",
|
||||
}))
|
||||
}
|
||||
getOptionLabel={(item: PatientItem) =>
|
||||
`${item.fullName} - ${item.identificationNumber}`
|
||||
}
|
||||
getOptionValue={(item: PatientItem) => item.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-t
|
||||
bg-white
|
||||
px-7
|
||||
py-5
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-3
|
||||
"
|
||||
>
|
||||
<CustomButton variant="grey" rounded="md" onClick={onClose}>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
|
||||
<CustomButton
|
||||
disabled={!form.patientId || createConsultationMutation.isPending}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{createConsultationMutation.isPending
|
||||
? "Đang tạo..."
|
||||
: "Tạo phiên khám"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupCreateConsultation;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from "./PopupCreateConsultation";
|
||||
@ -0,0 +1,281 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationDetail,
|
||||
} from "@/services/consultationServices";
|
||||
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||
|
||||
interface PopupUpdateConsultationProps {
|
||||
id: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface IUpdateConsul {
|
||||
chiefComplaint: string;
|
||||
symptomsText: string;
|
||||
vitalSigns: string;
|
||||
diagnosis: string;
|
||||
treatmentPlan: string;
|
||||
doctorNotes: string;
|
||||
}
|
||||
|
||||
const defaultForm: IUpdateConsul = {
|
||||
chiefComplaint: "",
|
||||
symptomsText: "",
|
||||
vitalSigns: "",
|
||||
diagnosis: "",
|
||||
treatmentPlan: "",
|
||||
doctorNotes: "",
|
||||
};
|
||||
|
||||
const PopupUpdateConsultation = ({
|
||||
id,
|
||||
onClose,
|
||||
}: PopupUpdateConsultationProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [form, setForm] = useState<IUpdateConsul>(defaultForm);
|
||||
|
||||
const consulDetailQuery = useQuery<ConsultationDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_phien_kham, id],
|
||||
enabled: !!id,
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationDetail>({
|
||||
showMessageFailed: true,
|
||||
http: consultationServices.detailConsultations(id),
|
||||
});
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
patientId: "",
|
||||
sessionCode: "",
|
||||
visitDate: "",
|
||||
status: 1,
|
||||
chiefComplaint: null,
|
||||
symptomsText: null,
|
||||
vitalSigns: null,
|
||||
diagnosis: null,
|
||||
treatmentPlan: null,
|
||||
doctorNotes: null,
|
||||
createdBy: "",
|
||||
isDeleted: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const consul = consulDetailQuery.data;
|
||||
|
||||
if (!consul) return;
|
||||
|
||||
setForm({
|
||||
chiefComplaint: consul.chiefComplaint || "",
|
||||
symptomsText: consul.symptomsText || "",
|
||||
vitalSigns: consul.vitalSigns || "",
|
||||
diagnosis: consul.diagnosis || "",
|
||||
treatmentPlan: consul.treatmentPlan || "",
|
||||
doctorNotes: consul.doctorNotes || "",
|
||||
});
|
||||
}, [consulDetailQuery.data]);
|
||||
|
||||
/* =========================
|
||||
SUBMIT
|
||||
========================= */
|
||||
|
||||
const updateConsultationMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Cập nhật phiên khám thành công!",
|
||||
http: consultationServices.putConsultations(id, {
|
||||
chiefComplaint: form.chiefComplaint,
|
||||
symptomsText: form.symptomsText,
|
||||
vitalSigns: form.vitalSigns,
|
||||
diagnosis: form.diagnosis,
|
||||
treatmentPlan: form.treatmentPlan,
|
||||
doctorNotes: form.doctorNotes,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess(data) {
|
||||
if (!data) return;
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.chi_tiet_phien_kham, id],
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
updateConsultationMutation.mutate();
|
||||
};
|
||||
|
||||
if (consulDetailQuery.isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-10">
|
||||
Đang tải dữ liệu...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
h-[680px]
|
||||
w-[640px]
|
||||
max-w-[95vw]
|
||||
rounded-3xl
|
||||
bg-white
|
||||
shadow-xl
|
||||
flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
{/* CLOSE */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="
|
||||
absolute
|
||||
right-5
|
||||
top-5
|
||||
z-10
|
||||
text-gray-400
|
||||
transition
|
||||
hover:text-gray-600
|
||||
"
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
|
||||
{/* HEADER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-b
|
||||
px-7
|
||||
py-5
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<h2 className="text-[30px] font-semibold text-[#111827]">
|
||||
Chỉnh sửa phiên khám
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div className="flex-1 overflow-y-auto px-7 py-5">
|
||||
<div className="space-y-5">
|
||||
<InputForm
|
||||
label="Lý do khám"
|
||||
name="chiefComplaint"
|
||||
type="text"
|
||||
value={form.chiefComplaint}
|
||||
placeholder="Nhập lý do khám"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Triệu chứng"
|
||||
name="symptomsText"
|
||||
placeholder="Nhập triệu chứng"
|
||||
value={form.symptomsText}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Dấu hiệu sinh tồn"
|
||||
name="vitalSigns"
|
||||
placeholder="Nhập dấu hiệu sinh tồn"
|
||||
value={form.vitalSigns}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Chẩn đoán"
|
||||
name="diagnosis"
|
||||
placeholder="Nhập chẩn đoán"
|
||||
value={form.diagnosis}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Kế hoạch điều trị"
|
||||
name="treatmentPlan"
|
||||
placeholder="Nhập kế hoạch điều trị"
|
||||
value={form.treatmentPlan}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Ghi chú bác sĩ"
|
||||
name="doctorNotes"
|
||||
placeholder="Nhập ghi chú"
|
||||
value={form.doctorNotes}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-t
|
||||
bg-white
|
||||
px-7
|
||||
py-5
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-3
|
||||
"
|
||||
>
|
||||
<CustomButton variant="grey" rounded="md" onClick={onClose}>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
|
||||
<ContextFormCustom.Consumer>
|
||||
{({ isDone }) => (
|
||||
<CustomButton
|
||||
disabled={!isDone || updateConsultationMutation.isPending}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
type="submit"
|
||||
>
|
||||
{updateConsultationMutation.isPending
|
||||
? "Đang cập nhật..."
|
||||
: "Cập nhật"}
|
||||
</CustomButton>
|
||||
)}
|
||||
</ContextFormCustom.Consumer>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupUpdateConsultation;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from "./PopupUpdateConsultation";
|
||||
189
src/components/page/patient/DetailPatient/DetailPatient.tsx
Normal file
189
src/components/page/patient/DetailPatient/DetailPatient.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import patientServices, { PatientDetail } from "@/services/patientServices";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
const DetailPatient = () => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const patientId = params?.id as string;
|
||||
|
||||
const patientDetailQuery = useQuery<PatientDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, patientId],
|
||||
|
||||
enabled: !!patientId,
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientDetail>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: patientServices.detailPatient(patientId),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
patientCode: "",
|
||||
identificationNumber: "",
|
||||
fullName: "",
|
||||
dateOfBirth: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
emergencyContactName: null,
|
||||
emergencyContactPhone: null,
|
||||
allergyNotes: null,
|
||||
chronicDiseaseNotes: null,
|
||||
insuranceNumber: null,
|
||||
notes: null,
|
||||
isActive: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const patient = patientDetailQuery.data;
|
||||
|
||||
if (patientDetailQuery.isLoading) {
|
||||
return <div>Đang tải dữ liệu...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="flex flex-col gap-5">
|
||||
<h2 className="text-[28px] flex items-center gap-2 font-semibold text-[#111827]">
|
||||
<ArrowLeft
|
||||
onClick={() => router.back()}
|
||||
className="cursor-pointer"
|
||||
/>{" "}
|
||||
{patient?.fullName}
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Mã bệnh nhân
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.patientCode}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Họ tên</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.fullName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Mã định danh
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.identificationNumber}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Ngày sinh
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.dateOfBirth}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Giới tính
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.gender}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Số điện thoại
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.phone}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Email</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.email}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Địa chỉ</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.address || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Người liên hệ khẩn cấp
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.emergencyContactName || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
SĐT khẩn cấp
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.emergencyContactPhone || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Dị ứng</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.allergyNotes || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Bệnh nền</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.chronicDiseaseNotes || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailPatient;
|
||||
1
src/components/page/patient/DetailPatient/index.ts
Normal file
1
src/components/page/patient/DetailPatient/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./DetailPatient";
|
||||
279
src/components/page/patient/MainPagePatient/MainPagePatient.tsx
Normal file
279
src/components/page/patient/MainPagePatient/MainPagePatient.tsx
Normal file
@ -0,0 +1,279 @@
|
||||
"use client";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import CustomDialog from "@/components/customs/custom-dialog";
|
||||
import CustomPopup from "@/components/customs/custom-popup";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import Search from "@/components/customs/custom-search";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import patientServices, {
|
||||
PatientItem,
|
||||
PatientResponse,
|
||||
} from "@/services/patientServices";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Pencil, Trash2, Users } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import PopupCreatePatient from "../PopupCreatePatient";
|
||||
import PopupUpdatePatient from "../PopupUpdatePatient";
|
||||
|
||||
const MainPagePatient = () => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
const [deleteItemId, setDeleteItemId] = useState<string>("");
|
||||
|
||||
const _page = searchParams.get("_page");
|
||||
const _pageSize = searchParams.get("_pageSize");
|
||||
const _keyword = searchParams.get("_keyword");
|
||||
|
||||
/**
|
||||
* PARAMS
|
||||
*/
|
||||
const createParam = searchParams.get("_create");
|
||||
const updateParam = searchParams.get("_update");
|
||||
|
||||
/**
|
||||
* OPEN POPUP
|
||||
*/
|
||||
const openCreatePopup = createParam !== null;
|
||||
const openUpdatePopup = !!updateParam;
|
||||
|
||||
/**
|
||||
* QUERY LIST PATIENT
|
||||
*/
|
||||
const patientQuery = useQuery<PatientResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_patient, _page, _pageSize, _keyword],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: patientServices.getPatient({
|
||||
Page: Number(_page || 1),
|
||||
PageSize: Number(_pageSize || 10),
|
||||
Search: _keyword || "",
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* UPDATE QUERY PARAMS
|
||||
*/
|
||||
const updateQuery = (key: string, value?: string | number) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (!value || value === "") {
|
||||
params.delete(key);
|
||||
} else {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `${pathname}?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
/**
|
||||
* OPEN CREATE
|
||||
*/
|
||||
const handleOpenCreate = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.set("_create", "");
|
||||
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* OPEN UPDATE
|
||||
*/
|
||||
const handleOpenUpdate = (id: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.delete("_create");
|
||||
params.set("_update", id);
|
||||
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* CLOSE ALL POPUP
|
||||
*/
|
||||
const handleClosePopup = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.delete("_create");
|
||||
params.delete("_update");
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `${pathname}?${queryString}` : pathname);
|
||||
|
||||
setOpenDeleteDialog(false);
|
||||
setDeleteItemId("");
|
||||
};
|
||||
|
||||
/**
|
||||
* DELETE
|
||||
*/
|
||||
const handleOpenDelete = (id: string) => {
|
||||
setDeleteItemId(id);
|
||||
setOpenDeleteDialog(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* DATA
|
||||
*/
|
||||
const patientData = useMemo(() => {
|
||||
return patientQuery.data?.items || [];
|
||||
}, [patientQuery.data]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-black">Bệnh nhân</h2>
|
||||
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<Users />}
|
||||
onClick={handleOpenCreate}
|
||||
>
|
||||
Tạo mới
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{/* SEARCH */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="w-full md:min-w-[400px]">
|
||||
<Search
|
||||
keyword={_keyword ?? ""}
|
||||
setKeyword={(value) => updateQuery("_keyword", value)}
|
||||
placeholder="Tìm kiếm theo tên người dùng, ID"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TABLE */}
|
||||
<DataWrapper
|
||||
data={patientData}
|
||||
loading={patientQuery.isLoading}
|
||||
title="No New Requests"
|
||||
note="Please check back later or view your schedule"
|
||||
>
|
||||
<Table
|
||||
rowKey={(item) => item.id}
|
||||
data={patientData}
|
||||
column={[
|
||||
{
|
||||
fixedLeft: true,
|
||||
title: "TÊN BỆNH NHÂN",
|
||||
|
||||
render: (item: PatientItem) => (
|
||||
<Link
|
||||
href={`/patient/${item.id}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.fullName}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "MÃ ĐỊNH DANH",
|
||||
|
||||
render: (item: PatientItem) => (
|
||||
<span>{item.identificationNumber}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "SỐ ĐIỆN THOẠI",
|
||||
|
||||
render: (item: PatientItem) => <span>{item.phone}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
title: "EMAIL",
|
||||
|
||||
render: (item: PatientItem) => <span>{item.email}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
title: "GIỚI TÍNH",
|
||||
|
||||
render: (item: PatientItem) => <span>{item.gender}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
fixedRight: true,
|
||||
title: "ACTION",
|
||||
|
||||
render: (item: PatientItem) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* UPDATE */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
icon={<Pencil className="text-blue-400" />}
|
||||
onClick={() => handleOpenUpdate(item.id)}
|
||||
/>
|
||||
|
||||
{/* DELETE */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
icon={<Trash2 className="text-red-400" />}
|
||||
onClick={() => handleOpenDelete(item.id)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
|
||||
{/* DELETE DIALOG */}
|
||||
<CustomDialog
|
||||
open={openDeleteDialog}
|
||||
title="Xóa bệnh nhân"
|
||||
note="Bạn có chắc chắn muốn xóa bệnh nhân này?"
|
||||
type="error"
|
||||
onClose={handleClosePopup}
|
||||
onSubmit={() => {}}
|
||||
titleCancel="Hủy"
|
||||
titleSubmit="Xác nhận"
|
||||
/>
|
||||
|
||||
{/* CREATE POPUP */}
|
||||
<CustomPopup open={openCreatePopup} onClose={handleClosePopup}>
|
||||
<PopupCreatePatient onClose={handleClosePopup} />
|
||||
</CustomPopup>
|
||||
|
||||
{/* UPDATE POPUP */}
|
||||
<CustomPopup open={openUpdatePopup} onClose={handleClosePopup}>
|
||||
<PopupUpdatePatient onClose={handleClosePopup} id={updateParam || ""} />
|
||||
</CustomPopup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPagePatient;
|
||||
1
src/components/page/patient/MainPagePatient/index.ts
Normal file
1
src/components/page/patient/MainPagePatient/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./MainPagePatient";
|
||||
@ -0,0 +1,259 @@
|
||||
"use client";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||
import { QUERY_KEY, TYPE_GENDER } from "@/constant/config/enum";
|
||||
import { X } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import patientServices from "@/services/patientServices";
|
||||
|
||||
import CustomLoading from "@/components/customs/custom-loading";
|
||||
|
||||
export interface ICreatePatient {
|
||||
identificationNumber: string;
|
||||
fullName: string;
|
||||
dateOfBirth: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface PropsPopupCreatePatient {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/* QUERY KEY */
|
||||
// export const QUERY_KEY = {
|
||||
// table_list_patient: "table_list_patient",
|
||||
// };
|
||||
|
||||
const PopupCreatePatient = ({ onClose }: PropsPopupCreatePatient) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [form, setForm] = useState<ICreatePatient>({
|
||||
identificationNumber: "",
|
||||
fullName: "",
|
||||
dateOfBirth: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
});
|
||||
|
||||
const createPatientMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Tạo bệnh nhân thành công!",
|
||||
|
||||
http: patientServices.createPatient({
|
||||
identificationNumber: form.identificationNumber,
|
||||
fullName: form.fullName,
|
||||
dateOfBirth: form.dateOfBirth,
|
||||
gender: form.gender,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
address: form.address,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
/* REFRESH LIST */
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_patient],
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
createPatientMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
h-[680px]
|
||||
w-[640px]
|
||||
max-w-[95vw]
|
||||
rounded-3xl
|
||||
bg-white
|
||||
shadow-xl
|
||||
flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
{/* CLOSE */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="
|
||||
absolute
|
||||
right-5
|
||||
top-5
|
||||
z-10
|
||||
text-gray-400
|
||||
transition
|
||||
hover:text-gray-600
|
||||
"
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
|
||||
{/* HEADER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-b
|
||||
px-7
|
||||
py-5
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<h2 className="text-[30px] font-semibold text-[#111827]">
|
||||
Thêm bệnh nhân
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div
|
||||
className="
|
||||
flex-1
|
||||
overflow-y-auto
|
||||
px-7
|
||||
py-5
|
||||
"
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Mã định danh
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Mã định danh"
|
||||
name="identificationNumber"
|
||||
type="text"
|
||||
value={form.identificationNumber}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Tên bệnh nhân
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Tên bệnh nhân"
|
||||
name="fullName"
|
||||
type="text"
|
||||
value={form.fullName}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Ngày sinh
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Ngày sinh"
|
||||
name="dateOfBirth"
|
||||
type="Date"
|
||||
value={form.dateOfBirth}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Số điện thoại
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Số điện thoại"
|
||||
name="phone"
|
||||
type="text"
|
||||
value={form.phone}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Email
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
type="text"
|
||||
value={form.email}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="address"
|
||||
placeholder="Địa chỉ"
|
||||
label="Địa chỉ"
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-t
|
||||
bg-white
|
||||
px-7
|
||||
py-5
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-3
|
||||
"
|
||||
>
|
||||
<CustomButton variant="grey" rounded="md" onClick={onClose}>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
|
||||
<ContextFormCustom.Consumer>
|
||||
{({ isDone }) => (
|
||||
<CustomButton
|
||||
disabled={!isDone || createPatientMutation.isPending}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
type="submit"
|
||||
>
|
||||
{createPatientMutation.isPending ? "Đang tạo..." : "Tạo"}
|
||||
</CustomButton>
|
||||
)}
|
||||
</ContextFormCustom.Consumer>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupCreatePatient;
|
||||
1
src/components/page/patient/PopupCreatePatient/index.ts
Normal file
1
src/components/page/patient/PopupCreatePatient/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./PopupCreatePatient";
|
||||
@ -0,0 +1,419 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { X } from "lucide-react";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import CustomLoading from "@/components/customs/custom-loading";
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||
import { QUERY_KEY, TYPE_GENDER } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import patientServices, { PatientDetail } from "@/services/patientServices";
|
||||
interface PopupUpdatePatientProps {
|
||||
onClose: () => void;
|
||||
id: string;
|
||||
}
|
||||
interface IUpdatePatient {
|
||||
fullName: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
emergencyContactName: string;
|
||||
emergencyContactPhone: string;
|
||||
allergyNotes: string;
|
||||
chronicDiseaseNotes: string;
|
||||
insuranceNumber: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
const defaultForm: IUpdatePatient = {
|
||||
fullName: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
emergencyContactName: "",
|
||||
emergencyContactPhone: "",
|
||||
allergyNotes: "",
|
||||
chronicDiseaseNotes: "",
|
||||
insuranceNumber: "",
|
||||
notes: "",
|
||||
};
|
||||
|
||||
const PopupUpdatePatient = ({ onClose, id }: PopupUpdatePatientProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [form, setForm] = useState<IUpdatePatient>(defaultForm);
|
||||
|
||||
const patientDetailQuery = useQuery<PatientDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, id],
|
||||
enabled: !!id,
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientDetail>({
|
||||
showMessageFailed: true,
|
||||
http: patientServices.detailPatient(id),
|
||||
});
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
patientCode: "",
|
||||
identificationNumber: "",
|
||||
fullName: "",
|
||||
dateOfBirth: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
emergencyContactName: null,
|
||||
emergencyContactPhone: null,
|
||||
allergyNotes: null,
|
||||
chronicDiseaseNotes: null,
|
||||
insuranceNumber: null,
|
||||
notes: null,
|
||||
isActive: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const patient = patientDetailQuery.data;
|
||||
if (!patient) return;
|
||||
setForm((prev) => {
|
||||
/**
|
||||
* tránh render loop
|
||||
*/
|
||||
if (
|
||||
prev.fullName === patient.fullName &&
|
||||
prev.phone === patient.phone &&
|
||||
prev.email === patient.email
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
fullName: patient.fullName || "",
|
||||
gender: patient.gender || TYPE_GENDER.MALE,
|
||||
phone: patient.phone || "",
|
||||
email: patient.email || "",
|
||||
address: patient.address || "",
|
||||
emergencyContactName: patient.emergencyContactName || "",
|
||||
emergencyContactPhone: patient.emergencyContactPhone || "",
|
||||
allergyNotes: patient.allergyNotes || "",
|
||||
chronicDiseaseNotes: patient.chronicDiseaseNotes || "",
|
||||
insuranceNumber: patient.insuranceNumber || "",
|
||||
notes: patient.notes || "",
|
||||
};
|
||||
});
|
||||
}, [patientDetailQuery.data]);
|
||||
|
||||
const updatePatientMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Cập nhật bệnh nhân thành công!",
|
||||
http: patientServices.putPatient(id, {
|
||||
fullName: form.fullName,
|
||||
gender: form.gender,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
address: form.address,
|
||||
emergencyContactName: form.emergencyContactName,
|
||||
emergencyContactPhone: form.emergencyContactPhone,
|
||||
allergyNotes: form.allergyNotes,
|
||||
chronicDiseaseNotes: form.chronicDiseaseNotes,
|
||||
insuranceNumber: form.insuranceNumber,
|
||||
notes: form.notes,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess(data) {
|
||||
if (!data) return;
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_patient],
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, id],
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
updatePatientMutation.mutate();
|
||||
};
|
||||
|
||||
if (patientDetailQuery.isLoading) {
|
||||
return (
|
||||
<div className="flex h-[400px] items-center justify-center">
|
||||
<CustomLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
h-[680px]
|
||||
w-[640px]
|
||||
max-w-[95vw]
|
||||
rounded-3xl
|
||||
bg-white
|
||||
shadow-xl
|
||||
flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
{/* CLOSE */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="
|
||||
absolute
|
||||
right-5
|
||||
top-5
|
||||
z-10
|
||||
text-gray-400
|
||||
transition
|
||||
hover:text-gray-600
|
||||
"
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
|
||||
{/* HEADER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-b
|
||||
px-7
|
||||
py-5
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<h2 className="text-[30px] font-semibold text-[#111827]">
|
||||
Cập nhật thông tin bệnh nhân
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div
|
||||
className="
|
||||
flex-1
|
||||
overflow-y-auto
|
||||
px-7
|
||||
py-5
|
||||
"
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Tên bệnh nhân
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Tên bệnh nhân"
|
||||
name="fullName"
|
||||
type="text"
|
||||
value={form.fullName}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
{/* GENDER */}
|
||||
<div>
|
||||
<label className="text-base font-medium">Giới tính</label>
|
||||
|
||||
<div className="mt-2 flex items-center gap-6">
|
||||
{/* NAM */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="male"
|
||||
checked={form.gender === TYPE_GENDER.MALE}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
gender: TYPE_GENDER.MALE,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<label htmlFor="male">Nam</label>
|
||||
</div>
|
||||
|
||||
{/* NỮ */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="female"
|
||||
checked={form.gender === TYPE_GENDER.FEMALE}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
gender: TYPE_GENDER.FEMALE,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<label htmlFor="female">Nữ</label>
|
||||
</div>
|
||||
|
||||
{/* KHÁC */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="other"
|
||||
checked={form.gender === TYPE_GENDER.OTHER}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
gender: TYPE_GENDER.OTHER,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<label htmlFor="other">Khác</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Số điện thoại
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Số điện thoại"
|
||||
name="phone"
|
||||
type="text"
|
||||
value={form.phone}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Email
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
type="text"
|
||||
value={form.email}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="address"
|
||||
placeholder="Địa chỉ"
|
||||
label="Địa chỉ"
|
||||
value={form.address}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Người liên hệ khẩn cấp"
|
||||
placeholder="Người liên hệ khẩn cấp"
|
||||
name="emergencyContactName"
|
||||
type="text"
|
||||
value={form.emergencyContactName}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="SĐT khẩn cấp"
|
||||
placeholder="SĐT khẩn cấp"
|
||||
name="emergencyContactPhone"
|
||||
type="text"
|
||||
value={form.emergencyContactPhone}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="allergyNotes"
|
||||
placeholder="Dị ứng"
|
||||
label="Dị ứng"
|
||||
value={form.allergyNotes}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="chronicDiseaseNotes"
|
||||
placeholder="Bệnh nền"
|
||||
label="Bệnh nền"
|
||||
value={form.chronicDiseaseNotes}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Số bảo hiểm"
|
||||
placeholder="Số bảo hiểm"
|
||||
name="insuranceNumber"
|
||||
type="text"
|
||||
value={form.insuranceNumber}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="notes"
|
||||
placeholder="Ghi chú"
|
||||
label="Ghi chú"
|
||||
value={form.notes}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-t
|
||||
bg-white
|
||||
px-7
|
||||
py-5
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-3
|
||||
"
|
||||
>
|
||||
<CustomButton variant="grey" rounded="md" onClick={onClose}>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
|
||||
<ContextFormCustom.Consumer>
|
||||
{({ isDone }) => (
|
||||
<CustomButton
|
||||
disabled={!isDone || updatePatientMutation.isPending}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
type="submit"
|
||||
>
|
||||
{updatePatientMutation.isPending
|
||||
? "Đang cập nhật..."
|
||||
: "Cập nhật"}
|
||||
</CustomButton>
|
||||
)}
|
||||
</ContextFormCustom.Consumer>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupUpdatePatient;
|
||||
1
src/components/page/patient/PopupUpdatePatient/index.ts
Normal file
1
src/components/page/patient/PopupUpdatePatient/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./PopupUpdatePatient";
|
||||
@ -0,0 +1,409 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
|
||||
import SelectForm from "@/components/utils/FormCustom/components/SelectForm";
|
||||
|
||||
import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import prescriptionServices from "@/services/prescriptionServices";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationItem,
|
||||
ConsultationResponse,
|
||||
} from "@/services/consultationServices";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
import { PATH } from "@/constant/config";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface PrescriptionItem {
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
interface PrescriptionForm {
|
||||
examinationSessionId: string;
|
||||
notes: string;
|
||||
prescriptionImg: string;
|
||||
items: PrescriptionItem[];
|
||||
}
|
||||
|
||||
interface UploadImageItem {
|
||||
url?: string;
|
||||
path?: string;
|
||||
file?: File;
|
||||
img?: string;
|
||||
}
|
||||
|
||||
const defaultMedicine: PrescriptionItem = {
|
||||
medicineName: "",
|
||||
dosage: "",
|
||||
frequency: "",
|
||||
duration: "",
|
||||
instructions: "",
|
||||
};
|
||||
|
||||
const CreatePrescription = () => {
|
||||
const router = useRouter();
|
||||
const [images, setImages] = useState<UploadImageItem[]>([]);
|
||||
|
||||
const [form, setForm] = useState<PrescriptionForm>({
|
||||
examinationSessionId: "",
|
||||
notes: "",
|
||||
prescriptionImg: "",
|
||||
items: [defaultMedicine],
|
||||
});
|
||||
|
||||
/**
|
||||
* GET EXAMINATION SESSION
|
||||
*/
|
||||
const examinationSessionQuery = useQuery<ConsultationResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: consultationServices.getConsultations({
|
||||
Page: 1,
|
||||
PageSize: 100,
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const examinationSessionOptions = examinationSessionQuery.data?.items || [];
|
||||
|
||||
const createPrescriptionMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Tạo đơn thuốc thành công!",
|
||||
|
||||
http: prescriptionServices.createPrescription({
|
||||
examinationSessionId: form.examinationSessionId,
|
||||
notes: form.notes,
|
||||
prescriptionImg: form.prescriptionImg,
|
||||
items: form.items,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
router.push(PATH.PRESCRIPTION);
|
||||
},
|
||||
|
||||
onError: (error: any) => {
|
||||
// 👇 BACKEND ERROR HERE
|
||||
const message =
|
||||
error?.message || error?.error?.message || "Có lỗi xảy ra";
|
||||
|
||||
const code = error?.code || error?.error?.code;
|
||||
|
||||
toastWarn({
|
||||
msg: message,
|
||||
});
|
||||
|
||||
console.log("ERROR CODE:", code);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* CHANGE MEDICINE FIELD
|
||||
*/
|
||||
const handleChangeMedicine = (
|
||||
index: number,
|
||||
key: keyof PrescriptionItem,
|
||||
value: string,
|
||||
) => {
|
||||
const cloneItems = [...form.items];
|
||||
|
||||
cloneItems[index] = {
|
||||
...cloneItems[index],
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
items: cloneItems,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* ADD MEDICINE
|
||||
*/
|
||||
const handleAddMedicine = () => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
|
||||
items: [
|
||||
...prev.items,
|
||||
{
|
||||
medicineName: "",
|
||||
dosage: "",
|
||||
frequency: "",
|
||||
duration: "",
|
||||
instructions: "",
|
||||
},
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* REMOVE MEDICINE
|
||||
*/
|
||||
const handleRemoveMedicine = (index: number) => {
|
||||
const newItems = form.items.filter((_, i) => i !== index);
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
items: newItems,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* SUBMIT
|
||||
*/
|
||||
const handleSubmit = () => {
|
||||
createPrescriptionMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-6 rounded-2xl bg-white p-6 shadow">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-[28px] font-semibold text-[#111827]">
|
||||
Thêm đơn thuốc
|
||||
</h2>
|
||||
|
||||
<p className="mt-1 text-sm text-[#697586]">
|
||||
Tạo mới đơn thuốc cho bệnh nhân
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
variant="midnightBlue"
|
||||
disabled={createPrescriptionMutation.isPending}
|
||||
>
|
||||
{createPrescriptionMutation.isPending
|
||||
? "Đang lưu..."
|
||||
: "Lưu đơn thuốc"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GENERAL INFO */}
|
||||
<div className="rounded-2xl border border-gray-200 p-5">
|
||||
<h3 className="mb-5 text-lg font-semibold text-[#111827]">
|
||||
Thông tin chung
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5">
|
||||
{/* EXAMINATION SESSION */}
|
||||
<SelectForm
|
||||
label={
|
||||
<span className="text-[14px] font-medium text-[#374151]">
|
||||
Phiên khám
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Chọn phiên khám"
|
||||
value={form.examinationSessionId}
|
||||
options={examinationSessionOptions}
|
||||
onSelect={(item: ConsultationItem) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
examinationSessionId: item.id,
|
||||
}))
|
||||
}
|
||||
onClean={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
examinationSessionId: "",
|
||||
}))
|
||||
}
|
||||
getOptionLabel={(item: ConsultationItem) =>
|
||||
`${item.sessionCode} `
|
||||
}
|
||||
getOptionValue={(item: ConsultationItem) => item.id}
|
||||
/>
|
||||
|
||||
{/* IMAGE */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<UploadMultipleFile
|
||||
images={images}
|
||||
setImages={(files: UploadImageItem[]) => {
|
||||
setImages(files);
|
||||
|
||||
const imagePath =
|
||||
files?.[0]?.path ||
|
||||
files?.[0]?.url ||
|
||||
files?.[0]?.img ||
|
||||
"";
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
prescriptionImg: imagePath,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* NOTES */}
|
||||
<TextArea
|
||||
label="Ghi chú"
|
||||
name="notes"
|
||||
placeholder="Nhập ghi chú"
|
||||
value={form.notes}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MEDICINE LIST */}
|
||||
<div className="rounded-2xl border border-gray-200 p-5">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-[#111827]">
|
||||
Danh sách thuốc
|
||||
</h3>
|
||||
<div>
|
||||
<CustomButton
|
||||
type="button"
|
||||
variant="midnightBlue"
|
||||
icon={<Plus size={18} />}
|
||||
onClick={handleAddMedicine}
|
||||
>
|
||||
Thêm thuốc
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{form.items.map((medicine, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-2xl border border-gray-200 p-5"
|
||||
>
|
||||
{/* TITLE */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h4 className="font-semibold text-[#111827]">
|
||||
Thuốc #{index + 1}
|
||||
</h4>
|
||||
|
||||
{form.items.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveMedicine(index)}
|
||||
className="text-red-500 transition hover:text-red-600"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* FORM */}
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<InputForm
|
||||
label="Tên thuốc"
|
||||
name={`items.${index}.medicineName`}
|
||||
placeholder="Nhập tên thuốc"
|
||||
value={medicine.medicineName}
|
||||
type="text"
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "medicineName", String(value))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Liều lượng"
|
||||
name={`items.${index}.dosage`}
|
||||
placeholder="Ví dụ: 500mg"
|
||||
value={medicine.dosage}
|
||||
type="text"
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "dosage", String(value))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Tần suất"
|
||||
name={`items.${index}.frequency`}
|
||||
placeholder="Ví dụ: 2 lần/ngày"
|
||||
value={medicine.frequency}
|
||||
type="text"
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "frequency", String(value))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Thời gian dùng"
|
||||
name={`items.${index}.duration`}
|
||||
placeholder="Ví dụ: 7 ngày"
|
||||
value={medicine.duration}
|
||||
type="text"
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "duration", String(value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<TextArea
|
||||
label="Hướng dẫn sử dụng"
|
||||
name={`items.${index}.instructions`}
|
||||
placeholder="Nhập hướng dẫn sử dụng thuốc"
|
||||
value={medicine.instructions}
|
||||
isBlur
|
||||
max={5000}
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "instructions", String(value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePrescription;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from "./CreatePrescription";
|
||||
@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import prescriptionServices, {
|
||||
PrescriptionDetail,
|
||||
} from "@/services/prescriptionServices";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useParams } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
const DetailPrescription = () => {
|
||||
const params = useParams();
|
||||
const PrescriptionId = params?.id as string;
|
||||
|
||||
const prescriptionDetailQuery = useQuery<PrescriptionDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_don_thuoc, PrescriptionId],
|
||||
|
||||
enabled: !!PrescriptionId,
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PrescriptionDetail>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: prescriptionServices.detailPrescription(PrescriptionId),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
examinationSessionId: "",
|
||||
prescriptionCode: "",
|
||||
prescriptionImg: null,
|
||||
notes: "",
|
||||
createdBy: "",
|
||||
items: [],
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const data = prescriptionDetailQuery.data;
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h1 className="text-xl font-semibold mb-4">Chi tiết đơn thuốc</h1>
|
||||
|
||||
{/* LOADING */}
|
||||
{prescriptionDetailQuery.isLoading && <p>Đang tải...</p>}
|
||||
|
||||
{/* ERROR */}
|
||||
{prescriptionDetailQuery.isError && (
|
||||
<p className="text-red-500">Lỗi tải dữ liệu</p>
|
||||
)}
|
||||
|
||||
{/* CONTENT */}
|
||||
{data && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<strong>Mã đơn thuốc:</strong> {data.prescriptionCode}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Phiên khám:</strong> {data.examinationSessionId}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Ghi chú:</strong> {data.notes || "-"}
|
||||
</div>
|
||||
|
||||
{/* IMAGE */}
|
||||
<div>
|
||||
<strong>Ảnh đơn thuốc:</strong>
|
||||
<div className="mt-2">
|
||||
{data.prescriptionImg ? (
|
||||
<img
|
||||
src={data.prescriptionImg}
|
||||
alt="prescription"
|
||||
className="h-32 w-32 object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-400">Không có ảnh</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ITEMS */}
|
||||
<div>
|
||||
<strong>Danh sách thuốc:</strong>
|
||||
|
||||
<div className="mt-2 space-y-2">
|
||||
{data.items?.length ? (
|
||||
data.items.map((item, index) => (
|
||||
<div key={item.id || index} className="p-3 border rounded">
|
||||
<p>
|
||||
<b>Tên thuốc:</b> {item.medicineName}
|
||||
</p>
|
||||
<p>
|
||||
<b>Liều lượng:</b> {item.dosage}
|
||||
</p>
|
||||
<p>
|
||||
<b>Tần suất:</b> {item.frequency}
|
||||
</p>
|
||||
<p>
|
||||
<b>Thời gian:</b> {item.duration}
|
||||
</p>
|
||||
<p>
|
||||
<b>Hướng dẫn:</b> {item.instructions}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-400">Không có thuốc</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailPrescription;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from "./DetailPrescription";
|
||||
@ -0,0 +1,180 @@
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import Search from "@/components/customs/custom-search";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import { PATH } from "@/constant/config";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import prescriptionServices, {
|
||||
PrescriptionItem,
|
||||
PrescriptionResponse,
|
||||
} from "@/services/prescriptionServices";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Pencil, Pill, Trash2 } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
const MainPrescription = () => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const _page = searchParams.get("_page");
|
||||
const _pageSize = searchParams.get("_pageSize");
|
||||
const _keyword = searchParams.get("_keyword");
|
||||
|
||||
const prescriptionQuery = useQuery<PrescriptionResponse>({
|
||||
queryKey: [
|
||||
QUERY_KEY.table_list_patientrescription,
|
||||
_page,
|
||||
_pageSize,
|
||||
_keyword,
|
||||
],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PrescriptionResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: prescriptionServices.getPrescription({
|
||||
Page: Number(_page || 1),
|
||||
PageSize: Number(_pageSize || 10),
|
||||
Search: _keyword || "",
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const updateQuery = (key: string, value?: string | number) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (!value) {
|
||||
params.delete(key);
|
||||
} else {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `${pathname}?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-black">Đơn thuốc</h2>
|
||||
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<Pill />}
|
||||
href={PATH.CREATE_PRESCRIPTION}
|
||||
>
|
||||
Tạo mới
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{/* SEARCH */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="w-full md:min-w-[400px]">
|
||||
<Search
|
||||
keyword={_keyword ?? ""}
|
||||
setKeyword={(value) => updateQuery("_keyword", value)}
|
||||
placeholder="Tìm theo mã đơn thuốc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataWrapper
|
||||
data={prescriptionQuery.data?.items || []}
|
||||
loading={prescriptionQuery.isLoading}
|
||||
title="No Data"
|
||||
note="Không có đơn thuốc nào"
|
||||
>
|
||||
<Table
|
||||
rowKey={(item: any) => item.id}
|
||||
data={prescriptionQuery.data?.items || []}
|
||||
column={[
|
||||
// ================= ID / CODE =================
|
||||
{
|
||||
title: "MÃ ĐƠN THUỐC",
|
||||
render: (item: any) => (
|
||||
<Link
|
||||
href={`/prescription/${item.id}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.prescriptionCode}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
|
||||
// ================= SESSION =================
|
||||
{
|
||||
title: "PHIÊN KHÁM",
|
||||
render: (item: any) => <span>{item.examinationSessionId}</span>,
|
||||
},
|
||||
|
||||
// ================= NOTES =================
|
||||
{
|
||||
title: "GHI CHÚ",
|
||||
render: (item: any) => <span>{item.notes || "-"}</span>,
|
||||
},
|
||||
|
||||
// ================= IMAGE =================
|
||||
{
|
||||
title: "ẢNH ĐƠN THUỐC",
|
||||
render: (item: any) =>
|
||||
item.prescriptionImg ? (
|
||||
<Image
|
||||
src={item.prescriptionImg}
|
||||
alt="prescription"
|
||||
className="h-10 w-10 rounded object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-400">Không có ảnh</span>
|
||||
),
|
||||
},
|
||||
|
||||
// ================= ITEMS COUNT =================
|
||||
{
|
||||
title: "SỐ THUỐC",
|
||||
render: (item: any) => <span>{item.items?.length || 0}</span>,
|
||||
},
|
||||
|
||||
// ================= ACTION =================
|
||||
{
|
||||
fixedRight: true,
|
||||
title: "ACTION",
|
||||
|
||||
render: (item: any) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<CustomButton
|
||||
size="sm"
|
||||
icon={<Pencil className="text-blue-400" />}
|
||||
href={`${PATH.PRESCRIPTION}/update?_id=${item?.id}`}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPrescription;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from "./MainPrescription";
|
||||
@ -0,0 +1,514 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
import SelectForm from "@/components/utils/FormCustom/components/SelectForm";
|
||||
import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
import prescriptionServices from "@/services/prescriptionServices";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationItem,
|
||||
ConsultationResponse,
|
||||
} from "@/services/consultationServices";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { PATH } from "@/constant/config";
|
||||
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
|
||||
import type { PrescriptionDetail } from "@/services/prescriptionServices";
|
||||
|
||||
/* =========================
|
||||
TYPES
|
||||
========================= */
|
||||
|
||||
interface PrescriptionItem {
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
interface PrescriptionForm {
|
||||
examinationSessionId: string;
|
||||
notes: string;
|
||||
prescriptionImg: string;
|
||||
items: PrescriptionItem[];
|
||||
}
|
||||
|
||||
interface UploadImageItem {
|
||||
url?: string;
|
||||
path?: string;
|
||||
img?: string;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
DEFAULT
|
||||
========================= */
|
||||
|
||||
const defaultMedicine: PrescriptionItem = {
|
||||
medicineName: "",
|
||||
dosage: "",
|
||||
frequency: "",
|
||||
duration: "",
|
||||
instructions: "",
|
||||
};
|
||||
|
||||
/* =========================
|
||||
COMPONENT
|
||||
========================= */
|
||||
|
||||
const UpdatePrescription = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const params = useParams();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const idFromParams = params?.id as string | undefined;
|
||||
|
||||
const idFromSearch = searchParams?.get("_id") ?? undefined;
|
||||
|
||||
const id = idFromParams || idFromSearch || "";
|
||||
|
||||
/* =========================
|
||||
GET EXAMINATION SESSION
|
||||
========================= */
|
||||
|
||||
const examinationSessionQuery = useQuery<ConsultationResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: consultationServices.getConsultations({
|
||||
Page: 1,
|
||||
PageSize: 100,
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const examinationSessionOptions = examinationSessionQuery.data?.items || [];
|
||||
|
||||
/* =========================
|
||||
GET DETAIL PRESCRIPTION
|
||||
========================= */
|
||||
|
||||
const detailQuery = useQuery<PrescriptionDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_don_thuoc, id],
|
||||
|
||||
enabled: !!id,
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PrescriptionDetail>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: prescriptionServices.detailPrescription(id),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
examinationSessionId: "",
|
||||
prescriptionCode: "",
|
||||
prescriptionImg: null,
|
||||
notes: "",
|
||||
createdBy: "",
|
||||
items: [],
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
MAP DATA
|
||||
========================= */
|
||||
|
||||
const mappedForm: PrescriptionForm = useMemo(() => {
|
||||
const data = detailQuery.data;
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
examinationSessionId: "",
|
||||
notes: "",
|
||||
prescriptionImg: "",
|
||||
items: [defaultMedicine],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
examinationSessionId: data.examinationSessionId ?? "",
|
||||
|
||||
notes: data.notes ?? "",
|
||||
|
||||
prescriptionImg: data.prescriptionImg ?? "",
|
||||
|
||||
items:
|
||||
data.items && data.items.length > 0
|
||||
? data.items.map((i) => ({
|
||||
medicineName: i.medicineName ?? "",
|
||||
dosage: i.dosage ?? "",
|
||||
frequency: i.frequency ?? "",
|
||||
duration: i.duration ?? "",
|
||||
instructions: i.instructions ?? "",
|
||||
}))
|
||||
: [defaultMedicine],
|
||||
};
|
||||
}, [detailQuery.data]);
|
||||
|
||||
/* =========================
|
||||
FORM STATE
|
||||
========================= */
|
||||
|
||||
const [form, setForm] = useState<PrescriptionForm>({
|
||||
examinationSessionId: "",
|
||||
notes: "",
|
||||
prescriptionImg: "",
|
||||
items: [defaultMedicine],
|
||||
});
|
||||
|
||||
const [images, setImages] = useState<UploadImageItem[]>([]);
|
||||
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
/* =========================
|
||||
INIT FORM
|
||||
========================= */
|
||||
|
||||
useEffect(() => {
|
||||
if (!detailQuery.data || initialized) return;
|
||||
|
||||
queueMicrotask(() => {
|
||||
setForm(mappedForm);
|
||||
|
||||
setImages(
|
||||
mappedForm.prescriptionImg ? [{ url: mappedForm.prescriptionImg }] : [],
|
||||
);
|
||||
|
||||
setInitialized(true);
|
||||
});
|
||||
}, [detailQuery.data, initialized, mappedForm]);
|
||||
|
||||
/* =========================
|
||||
UPDATE MUTATION
|
||||
========================= */
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!id || id === "undefined") {
|
||||
throw new Error("Không tìm thấy ID đơn thuốc!");
|
||||
}
|
||||
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
|
||||
showMessageSuccess: true,
|
||||
|
||||
msgSuccess: "Cập nhật đơn thuốc thành công!",
|
||||
|
||||
http: prescriptionServices.putPrescription(id, {
|
||||
// examinationSessionId: form.examinationSessionId,
|
||||
notes: form.notes,
|
||||
prescriptionImg: form.prescriptionImg,
|
||||
items: form.items,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
router.push(PATH.PRESCRIPTION);
|
||||
},
|
||||
|
||||
onError: (error: any) => {
|
||||
const message =
|
||||
error?.message || error?.error?.message || "Có lỗi xảy ra";
|
||||
|
||||
toastWarn({
|
||||
msg: message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
HANDLERS
|
||||
========================= */
|
||||
|
||||
const handleChangeMedicine = (
|
||||
index: number,
|
||||
key: keyof PrescriptionItem,
|
||||
value: string,
|
||||
) => {
|
||||
const clone = [...form.items];
|
||||
|
||||
clone[index] = {
|
||||
...clone[index],
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
items: clone,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddMedicine = () => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
|
||||
items: [
|
||||
...prev.items,
|
||||
{
|
||||
medicineName: "",
|
||||
dosage: "",
|
||||
frequency: "",
|
||||
duration: "",
|
||||
instructions: "",
|
||||
},
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRemoveMedicine = (index: number) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
|
||||
items: prev.items.filter((_, i) => i !== index),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
updateMutation.mutate();
|
||||
};
|
||||
|
||||
const isLoading = updateMutation.isPending || detailQuery.isLoading;
|
||||
|
||||
/* =========================
|
||||
UI
|
||||
========================= */
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-6 rounded-2xl bg-white p-6 shadow">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-[28px] font-semibold text-[#111827]">
|
||||
Cập nhật đơn thuốc
|
||||
</h2>
|
||||
|
||||
<p className="mt-1 text-sm text-[#697586]">
|
||||
Chỉnh sửa đơn thuốc cho bệnh nhân
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
variant="midnightBlue"
|
||||
// Vừa loading vừa check id, nếu id không tồn tại thì không cho click submit
|
||||
disabled={isLoading || !id || id === "undefined"}
|
||||
>
|
||||
{isLoading ? "Đang cập nhật..." : "Lưu đơn thuốc"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GENERAL INFO */}
|
||||
<div className="rounded-2xl border border-gray-200 p-5">
|
||||
<h3 className="mb-5 text-lg font-semibold text-[#111827]">
|
||||
Thông tin chung
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5">
|
||||
{/* EXAM SESSION */}
|
||||
<SelectForm
|
||||
label={
|
||||
<span className="text-[14px] font-medium text-[#374151]">
|
||||
Phiên khám <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Chọn phiên khám"
|
||||
value={form.examinationSessionId}
|
||||
readOnly
|
||||
options={examinationSessionOptions}
|
||||
onSelect={(item: ConsultationItem) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
examinationSessionId: item.id,
|
||||
}))
|
||||
}
|
||||
onClean={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
examinationSessionId: "",
|
||||
}))
|
||||
}
|
||||
getOptionLabel={(item: ConsultationItem) => item.sessionCode}
|
||||
getOptionValue={(item: ConsultationItem) => item.id}
|
||||
/>
|
||||
|
||||
{/* IMAGE */}
|
||||
<UploadMultipleFile
|
||||
images={images}
|
||||
setImages={(files: UploadImageItem[]) => {
|
||||
setImages(files);
|
||||
|
||||
const imagePath =
|
||||
files?.[0]?.path || files?.[0]?.url || files?.[0]?.img || "";
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
prescriptionImg: imagePath,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* NOTES */}
|
||||
<TextArea
|
||||
label="Ghi chú"
|
||||
name="notes"
|
||||
placeholder="Nhập ghi chú"
|
||||
value={form.notes}
|
||||
isBlur
|
||||
max={5000}
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, notes: String(v) }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MEDICINE LIST */}
|
||||
<div className="rounded-2xl border border-gray-200 p-5">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-[#111827]">
|
||||
Danh sách thuốc
|
||||
</h3>
|
||||
<div>
|
||||
<CustomButton
|
||||
type="button"
|
||||
variant="midnightBlue"
|
||||
icon={<Plus size={18} />}
|
||||
onClick={handleAddMedicine}
|
||||
>
|
||||
Thêm thuốc
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{form.items.map((medicine, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-2xl border border-gray-200 p-5"
|
||||
>
|
||||
{/* TITLE */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h4 className="font-semibold text-[#111827]">
|
||||
Thuốc #{index + 1}
|
||||
</h4>
|
||||
|
||||
{form.items.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveMedicine(index)}
|
||||
className="text-red-500 transition hover:text-red-600"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* INPUTS */}
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<InputForm
|
||||
label="Tên thuốc"
|
||||
name={`medicineName-${index}`}
|
||||
type="text"
|
||||
placeholder="Nhập tên thuốc"
|
||||
value={medicine.medicineName}
|
||||
onChangeValue={(v) =>
|
||||
handleChangeMedicine(index, "medicineName", String(v))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Liều lượng"
|
||||
name={`dosage-${index}`}
|
||||
type="text"
|
||||
placeholder="Nhập liều lượng"
|
||||
value={medicine.dosage}
|
||||
onChangeValue={(v) =>
|
||||
handleChangeMedicine(index, "dosage", String(v))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Tần suất"
|
||||
name={`frequency-${index}`}
|
||||
type="text"
|
||||
placeholder="Nhập tần suất"
|
||||
value={medicine.frequency}
|
||||
onChangeValue={(v) =>
|
||||
handleChangeMedicine(index, "frequency", String(v))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Thời gian"
|
||||
name={`duration-${index}`}
|
||||
type="text"
|
||||
placeholder="Nhập thời gian"
|
||||
value={medicine.duration}
|
||||
onChangeValue={(v) =>
|
||||
handleChangeMedicine(index, "duration", String(v))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* INSTRUCTIONS */}
|
||||
<div className="mt-5">
|
||||
<TextArea
|
||||
label="Hướng dẫn sử dụng"
|
||||
name={`instructions-${index}`}
|
||||
placeholder="Nhập hướng dẫn sử dụng"
|
||||
value={medicine.instructions}
|
||||
onChangeValue={(v) =>
|
||||
handleChangeMedicine(index, "instructions", String(v))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdatePrescription;
|
||||
@ -0,0 +1 @@
|
||||
export { default } from "./UpdatePrescription";
|
||||
@ -31,5 +31,6 @@ export interface PropsInputForm {
|
||||
isUppercase?: boolean;
|
||||
|
||||
onBlur?: () => void;
|
||||
|
||||
onChangeValue?: (val: string | number) => void;
|
||||
}
|
||||
|
||||
@ -81,7 +81,7 @@ function SelectForm<OptionType>({
|
||||
onClick={() => !readOnly && setOpen((prev) => !prev)}
|
||||
className={clsx(
|
||||
"flex items-center justify-between px-4 h-12 border rounded-full cursor-pointer transition",
|
||||
"border-gray-300 bg-white hover:border-blue-600",
|
||||
"border-gray-300 bg-gray-300 hover:border-blue-600",
|
||||
open && "border-blue-600",
|
||||
readOnly &&
|
||||
"bg-gray-100 border-gray-200 cursor-not-allowed hover:border-gray-200",
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { PropsTextArea } from "./interface";
|
||||
import { ContextFormCustom, IContextFormCustom } from "../../contexts";
|
||||
|
||||
@ -18,6 +17,7 @@ function TextArea({
|
||||
min,
|
||||
isRequired,
|
||||
textRequired,
|
||||
onChangeValue, // Nhận callback custom từ cha
|
||||
}: PropsTextArea) {
|
||||
const [isFocus, setIsFocus] = useState(false);
|
||||
|
||||
@ -31,159 +31,175 @@ function TextArea({
|
||||
setErrorText,
|
||||
} = useContext<IContextFormCustom<any>>(ContextFormCustom);
|
||||
|
||||
// Lấy giá trị hiện tại (ưu tiên value từ props, sau đó là value trong context)
|
||||
const currentValue = value ?? form?.[name] ?? "";
|
||||
|
||||
// =========================
|
||||
// VALIDATE
|
||||
// VALIDATE LOGIC
|
||||
// =========================
|
||||
|
||||
const handleValidate = (): boolean => {
|
||||
const currentValue = `${form[name] || ""}`.trim();
|
||||
const handleValidate = (inputValue?: string) => {
|
||||
const finalValue = (inputValue ?? String(currentValue)).trim();
|
||||
|
||||
if (isRequired && currentValue === "") return false;
|
||||
if (isRequired && finalValue === "") {
|
||||
return {
|
||||
valid: false,
|
||||
message: textRequired || "Vui lòng nhập trường này",
|
||||
};
|
||||
}
|
||||
|
||||
if (min && currentValue.length < min) return false;
|
||||
if (max && finalValue.length > max) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Nhập tối đa ${max} kí tự`,
|
||||
};
|
||||
}
|
||||
|
||||
if (max && currentValue.length > max) return false;
|
||||
if (min && finalValue.length < min) {
|
||||
return {
|
||||
valid: false,
|
||||
message: `Nhập tối thiểu ${min} kí tự`,
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
return {
|
||||
valid: true,
|
||||
message: null,
|
||||
};
|
||||
};
|
||||
|
||||
// =========================
|
||||
// ERROR MESSAGE
|
||||
// =========================
|
||||
|
||||
const handleSetMessage = () => {
|
||||
const currentValue = `${form[name] || ""}`.trim();
|
||||
// Cập nhật lỗi và trạng thái validate vào Context
|
||||
const handleSetMessage = (inputValue?: string) => {
|
||||
const result = handleValidate(inputValue);
|
||||
|
||||
setErrorText((prev) => ({
|
||||
...prev,
|
||||
[name]: null,
|
||||
[name]: result.message,
|
||||
}));
|
||||
|
||||
if (isRequired && currentValue === "") {
|
||||
return setErrorText((prev) => ({
|
||||
...prev,
|
||||
[name]: textRequired || "Vui lòng nhập trường này",
|
||||
}));
|
||||
}
|
||||
|
||||
if (max && currentValue.length > max) {
|
||||
return setErrorText((prev) => ({
|
||||
...prev,
|
||||
[name]: `Nhập tối đa ${max} kí tự`,
|
||||
}));
|
||||
}
|
||||
|
||||
if (min && currentValue.length < min) {
|
||||
return setErrorText((prev) => ({
|
||||
...prev,
|
||||
[name]: `Nhập tối thiểu ${min} kí tự`,
|
||||
}));
|
||||
}
|
||||
setValidate((prev) => ({
|
||||
...prev,
|
||||
[name]: result.valid,
|
||||
}));
|
||||
};
|
||||
|
||||
// =========================
|
||||
// EFFECTS
|
||||
// =========================
|
||||
|
||||
// Chạy validate toàn cục khi Form yêu cầu (countValidate tăng)
|
||||
useEffect(() => {
|
||||
if (countValidate > 0) {
|
||||
handleSetMessage();
|
||||
}
|
||||
}, [countValidate]);
|
||||
|
||||
// Cập nhật trạng thái valid thầm lặng vào context mỗi khi dữ liệu thay đổi
|
||||
useEffect(() => {
|
||||
const result = handleValidate();
|
||||
setValidate((prev) => ({
|
||||
...prev,
|
||||
[name]: handleValidate(),
|
||||
[name]: result.valid,
|
||||
}));
|
||||
}, [form[name]]);
|
||||
}, [currentValue]);
|
||||
|
||||
// =========================
|
||||
// EVENTS
|
||||
// EVENT HANDLERS
|
||||
// =========================
|
||||
|
||||
const handlerFocused = () => {
|
||||
const handleFocus = () => {
|
||||
setIsFocus(true);
|
||||
|
||||
// Xóa thông báo lỗi khi người dùng bắt đầu sửa lại
|
||||
setErrorText((prev) => ({
|
||||
...prev,
|
||||
[name]: null,
|
||||
}));
|
||||
};
|
||||
|
||||
const handlerBlur = () => {
|
||||
const handleBlur = () => {
|
||||
setIsFocus(false);
|
||||
|
||||
if (isBlur) {
|
||||
handleSetMessage();
|
||||
|
||||
setValidate((prev) => ({
|
||||
...prev,
|
||||
[name]: handleValidate(),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const { value } = e.target;
|
||||
const inputValue = e.target.value;
|
||||
|
||||
// 1. Nếu có hàm xử lý riêng (như cập nhật vào mảng ở component cha)
|
||||
if (onChangeValue) {
|
||||
onChangeValue(inputValue);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Mặc định: cập nhật vào state chung của FormCustom qua Context
|
||||
setForm((prev: any) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
[name]: inputValue,
|
||||
}));
|
||||
|
||||
// Nếu form đã từng submit (countValidate > 0), thực hiện validate ngay lập tức (live validation)
|
||||
if (countValidate > 0) {
|
||||
handleSetMessage(inputValue);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================
|
||||
// RENDER
|
||||
// =========================
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-1">
|
||||
{/* LABEL */}
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={`textarea_${name}`}
|
||||
className="block text-[16px] font-medium"
|
||||
className="block text-[16px] font-medium text-[#171717]"
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
|
||||
{/* TEXTAREA */}
|
||||
{/* TEXTAREA WRAPPER */}
|
||||
<textarea
|
||||
id={`textarea_${name}`}
|
||||
name={name}
|
||||
value={value ?? form[name] ?? ""}
|
||||
value={String(currentValue)}
|
||||
placeholder={placeholder}
|
||||
autoComplete="off"
|
||||
readOnly={readOnly}
|
||||
disabled={readOnly}
|
||||
onChange={handleChange}
|
||||
onFocus={handlerFocused}
|
||||
onBlur={handlerBlur}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
className={clsx(
|
||||
"w-full min-h-[140px] resize-none rounded-[20px] border px-4 py-3 text-[16px] font-medium outline-none transition",
|
||||
"w-full min-h-[140px] resize-none rounded-[20px] border px-4 py-3 text-[16px] font-medium outline-none transition-all",
|
||||
|
||||
// default
|
||||
"border-[#CDD5DF] bg-white",
|
||||
// Trạng thái mặc định
|
||||
"border-[#CDD5DF] bg-white text-[#171717]",
|
||||
|
||||
// hover
|
||||
// Hover (chỉ khi không readOnly)
|
||||
!readOnly && "hover:border-[#0019F6]",
|
||||
|
||||
// focus
|
||||
// Focus
|
||||
isFocus && "border-[#0019F6]",
|
||||
|
||||
// error
|
||||
errorText[name] && "border-[#E5493F]",
|
||||
// Có lỗi
|
||||
errorText?.[name] && "border-red-500",
|
||||
|
||||
// done
|
||||
showDone && isDone && "border-[#1CAD4C]",
|
||||
// Đã hoàn tất và không có lỗi
|
||||
showDone && isDone && !errorText?.[name] && "border-green-600",
|
||||
|
||||
// readonly
|
||||
readOnly &&
|
||||
"cursor-not-allowed border-gray-200 bg-gray-100 hover:border-gray-200",
|
||||
// Trạng thái ReadOnly/Disabled
|
||||
readOnly && "cursor-not-allowed border-gray-200 bg-gray-100",
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* ERROR */}
|
||||
{errorText[name] && (
|
||||
<p className="text-[12px] text-red-500">{errorText[name]}</p>
|
||||
{/* ERROR MESSAGE */}
|
||||
{errorText?.[name] && (
|
||||
<p className="mt-1 text-xs font-medium text-red-500">
|
||||
{errorText[name]}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -14,4 +14,5 @@ export interface PropsTextArea {
|
||||
min?: number;
|
||||
|
||||
isRequired?: boolean;
|
||||
onChangeValue?: (val: string) => void;
|
||||
}
|
||||
|
||||
104
src/components/utils/UploadMultipleFile/UploadMultipleFile.tsx
Normal file
104
src/components/utils/UploadMultipleFile/UploadMultipleFile.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { X, CirclePlus } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { PropsUploadMultipleFile } from "./interface";
|
||||
|
||||
export default function UploadMultipleFile({
|
||||
images = [],
|
||||
setImages,
|
||||
isDisableDelete = false,
|
||||
}: PropsUploadMultipleFile) {
|
||||
// 👉 upload files
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (!files) return;
|
||||
|
||||
const newImages = Array.from(files).map((file) => ({
|
||||
url: URL.createObjectURL(file),
|
||||
file,
|
||||
}));
|
||||
|
||||
setImages((prev: any) => [...prev, ...newImages]);
|
||||
};
|
||||
|
||||
// 👉 delete file
|
||||
const handleDelete = (index: number) => {
|
||||
setImages((prev: any) => {
|
||||
const target = prev[index];
|
||||
if (target?.url) URL.revokeObjectURL(target.url);
|
||||
|
||||
return prev.filter((_: any, i: number) => i !== index);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center flex-wrap gap-2">
|
||||
{/* LIST IMAGE */}
|
||||
{images?.length > 0 && (
|
||||
<div className="flex items-center flex-wrap gap-2">
|
||||
{images.map((image, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative w-[72px] h-[72px] rounded-md border border-[#99a2b34d] overflow-hidden select-none"
|
||||
>
|
||||
<Image
|
||||
src={
|
||||
image?.url ||
|
||||
`${process.env.NEXT_PUBLIC_IMAGE}/${image?.path}`
|
||||
}
|
||||
alt="image"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
|
||||
{/* DELETE BUTTON */}
|
||||
{isDisableDelete && !image?.file && image?.img ? null : (
|
||||
<div
|
||||
onClick={() => handleDelete(index)}
|
||||
className="absolute top-[2px] right-[2px] w-[18px] h-[18px] bg-white rounded-sm flex items-center justify-center cursor-pointer transition active:scale-90"
|
||||
>
|
||||
<X size={14} color="#8496AC" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* UPLOAD BUTTON */}
|
||||
<div className="flex items-center gap-2">
|
||||
<label
|
||||
className={clsx(
|
||||
"flex items-center justify-center",
|
||||
"w-[72px] h-[72px] rounded-md border cursor-pointer bg-white",
|
||||
"border-[#99a2b34d] hover:border-gray-400 transition",
|
||||
)}
|
||||
>
|
||||
<CirclePlus color="rgba(198, 201, 206, 1)" />
|
||||
|
||||
<input
|
||||
hidden
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
onClick={(e) => {
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
}}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{/* NOTE */}
|
||||
<div className="flex flex-col">
|
||||
<p className="text-[14px] font-medium text-[#2f3643]">Upload file</p>
|
||||
<p className="text-[12px] font-medium text-[#99a2b3]">
|
||||
File không vượt quá 50MB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
src/components/utils/UploadMultipleFile/index.ts
Normal file
1
src/components/utils/UploadMultipleFile/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./UploadMultipleFile";
|
||||
@ -0,0 +1,5 @@
|
||||
export interface PropsUploadMultipleFile {
|
||||
images: any[];
|
||||
setImages: (any: any) => void;
|
||||
isDisableDelete?: boolean;
|
||||
}
|
||||
@ -1,3 +1,26 @@
|
||||
export enum QUERY_KEY {
|
||||
table_list_patient,
|
||||
table_list_consultation,
|
||||
table_list_patientrescription,
|
||||
|
||||
chi_tiet_benh_nhan,
|
||||
chi_tiet_don_thuoc,
|
||||
chi_tiet_phien_kham,
|
||||
}
|
||||
|
||||
export enum TYPE_STATUS {
|
||||
Draft = 1,
|
||||
InProgress = 2,
|
||||
Completed = 3,
|
||||
Cancelled = 4,
|
||||
}
|
||||
|
||||
export enum TYPE_GENDER {
|
||||
MALE = "male",
|
||||
FEMALE = "female",
|
||||
OTHER = "other",
|
||||
}
|
||||
|
||||
export enum TYPE_DATE {
|
||||
ALL = -1,
|
||||
TODAY = 1,
|
||||
|
||||
@ -6,6 +6,7 @@ export enum PATH {
|
||||
PATIENT = "/patient",
|
||||
CONSULTATION = "/consultation",
|
||||
PRESCRIPTION = "/prescription",
|
||||
CREATE_PRESCRIPTION = "/prescription/create",
|
||||
|
||||
LOGIN = "/auth/login",
|
||||
REGISTER = "/auth/register",
|
||||
@ -26,17 +27,17 @@ export const Menus: {
|
||||
}[] = [
|
||||
{
|
||||
group: [
|
||||
{
|
||||
title: "Dashboard",
|
||||
icon: CircleGauge,
|
||||
path: PATH.DASHBOARD || PATH.HOME,
|
||||
pathActive: PATH.DASHBOARD,
|
||||
},
|
||||
// {
|
||||
// title: "Dashboard",
|
||||
// icon: CircleGauge,
|
||||
// path: PATH.DASHBOARD || PATH.HOME,
|
||||
// pathActive: PATH.DASHBOARD,
|
||||
// },
|
||||
{
|
||||
title: "Bệnh nhân",
|
||||
icon: Users,
|
||||
path: PATH.PATIENT,
|
||||
pathActive: PATH.PATIENT,
|
||||
path: PATH.PATIENT || PATH.HOME,
|
||||
pathActive: PATH.PATIENT || PATH.HOME,
|
||||
},
|
||||
{
|
||||
title: "Phiên khám",
|
||||
|
||||
@ -3,6 +3,7 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||
export interface IDataLoginStorage {
|
||||
usernameStorage: string;
|
||||
passwordStorage: string;
|
||||
deviceIdStorage: string;
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
|
||||
@ -5,9 +5,7 @@ const authServices = {
|
||||
data: {
|
||||
username: string;
|
||||
password: string;
|
||||
ip: string;
|
||||
address: string;
|
||||
type: number;
|
||||
deviceId: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
@ -15,11 +13,38 @@ const authServices = {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
logout: (data: void, tokenAxios?: any) => {
|
||||
return axiosClient.post(`/Auth/logout`, data, {
|
||||
register: (
|
||||
data: {
|
||||
username: string;
|
||||
password: string;
|
||||
fullName: string;
|
||||
email: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.post(`/Auth/register`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
refresh: (
|
||||
data: {
|
||||
identifier: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.post(`/Auth/refresh`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
logout: (tokenAxios?: any) => {
|
||||
return axiosClient.post(
|
||||
`/Auth/logout`,
|
||||
{},
|
||||
{
|
||||
cancelToken: tokenAxios,
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default authServices;
|
||||
|
||||
101
src/services/consultationServices.ts
Normal file
101
src/services/consultationServices.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
export interface GetConsultationParams {
|
||||
Page?: number;
|
||||
PageSize?: number;
|
||||
Search?: string;
|
||||
PatientId?: string;
|
||||
SortBy?: string;
|
||||
Desc?: boolean;
|
||||
}
|
||||
|
||||
export interface ConsultationItem {
|
||||
id: string;
|
||||
patientId: string;
|
||||
sessionCode: string;
|
||||
visitDate: string;
|
||||
status: number;
|
||||
chiefComplaint: string | null;
|
||||
symptomsText: string | null;
|
||||
vitalSigns: string | null;
|
||||
diagnosis: string | null;
|
||||
treatmentPlan: string | null;
|
||||
doctorNotes: string | null;
|
||||
createdBy: string;
|
||||
isDeleted: boolean;
|
||||
}
|
||||
|
||||
export interface ConsultationDetail {
|
||||
id: string;
|
||||
patientId: string;
|
||||
sessionCode: string;
|
||||
visitDate: string;
|
||||
status: number;
|
||||
chiefComplaint: string | null;
|
||||
symptomsText: string | null;
|
||||
vitalSigns: string | null;
|
||||
diagnosis: string | null;
|
||||
treatmentPlan: string | null;
|
||||
doctorNotes: string | null;
|
||||
createdBy: string;
|
||||
isDeleted: boolean;
|
||||
}
|
||||
|
||||
export interface ConsultationResponse {
|
||||
items: ConsultationItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const consultationServices = {
|
||||
getConsultations: (params?: GetConsultationParams, tokenAxios?: any) => {
|
||||
return axiosClient.get<ConsultationResponse>(`/ExaminationSession/list`, {
|
||||
params,
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
createConsultations: (data?: { patientId: string }, tokenAxios?: any) => {
|
||||
return axiosClient.post(`/ExaminationSession`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
detailConsultations: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.get<ConsultationDetail>(`/ExaminationSession/${id}`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
putConsultations: (
|
||||
id: string,
|
||||
data: {
|
||||
chiefComplaint: string | null;
|
||||
symptomsText: string | null;
|
||||
vitalSigns: string | null;
|
||||
diagnosis: string | null;
|
||||
treatmentPlan: string | null;
|
||||
doctorNotes: string | null;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.put(`/ExaminationSession/${id}`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
cancelConsultations: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.post(`/ExaminationSession/${id}/cancel`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
completeConsultations: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.post(`/ExaminationSession/${id}/complete`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default consultationServices;
|
||||
@ -1,7 +1,6 @@
|
||||
import axios, {
|
||||
AxiosError,
|
||||
AxiosInstance,
|
||||
AxiosResponse,
|
||||
InternalAxiosRequestConfig,
|
||||
} from "axios";
|
||||
|
||||
@ -9,17 +8,22 @@ import { delay } from "@/common/funcs/delay";
|
||||
import { getKeyCert } from "@/common/funcs/optionConvert";
|
||||
import { toastInfo, toastSuccess, toastWarn } from "@/common/funcs/toast";
|
||||
|
||||
import { logout } from "@/redux/reducer/auth";
|
||||
import { logout, setToken } from "@/redux/reducer/auth";
|
||||
import { setInfoUser } from "@/redux/reducer/user";
|
||||
import { store } from "@/redux/store";
|
||||
|
||||
/* =========================================================
|
||||
* TYPES
|
||||
* =======================================================*/
|
||||
|
||||
interface ApiError {
|
||||
code: number;
|
||||
code: string | number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface ApiResponse<T = unknown> {
|
||||
error: ApiError;
|
||||
export interface ApiResponse<T = unknown> {
|
||||
success?: boolean;
|
||||
error?: ApiError | null;
|
||||
data?: T;
|
||||
items?: T[];
|
||||
pagination?: unknown;
|
||||
@ -42,16 +46,20 @@ interface HttpRequestOptions<T> {
|
||||
isData?: boolean;
|
||||
}
|
||||
|
||||
const axiosClient = axios.create({
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
/* =========================================================
|
||||
* AXIOS CLIENT
|
||||
* =======================================================*/
|
||||
|
||||
baseURL: "/api-proxy",
|
||||
const axiosClient = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL_DEV,
|
||||
|
||||
timeout: 15000,
|
||||
|
||||
timeoutErrorMessage: "Timeout error request",
|
||||
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
}) as AxiosInstance & {
|
||||
get<T = unknown>(url: string, config?: any): Promise<ApiResponse<T>>;
|
||||
|
||||
@ -70,16 +78,120 @@ const axiosClient = axios.create({
|
||||
delete<T = unknown>(url: string, config?: any): Promise<ApiResponse<T>>;
|
||||
};
|
||||
|
||||
/* =========================================================
|
||||
* REFRESH CLIENT
|
||||
* =======================================================*/
|
||||
|
||||
const refreshClient = axios.create({
|
||||
baseURL: process.env.NEXT_PUBLIC_API_URL_DEV,
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
/* =========================================================
|
||||
* REFRESH REQUEST INTERCEPTOR
|
||||
* =======================================================*/
|
||||
|
||||
refreshClient.interceptors.request.use((config) => {
|
||||
if (!(config.data instanceof FormData)) {
|
||||
config.data = {
|
||||
...getKeyCert(),
|
||||
...(config.data || {}),
|
||||
};
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
/* =========================================================
|
||||
* REFRESH TOKEN
|
||||
* =======================================================*/
|
||||
|
||||
let refreshPromise: Promise<string | null> | null = null;
|
||||
|
||||
const refreshAccessToken = async (): Promise<string | null> => {
|
||||
if (refreshPromise) {
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
refreshPromise = (async () => {
|
||||
try {
|
||||
const state = store.getState();
|
||||
|
||||
const refreshToken = state.user?.infoUser?.refreshToken || null;
|
||||
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await refreshClient.post<{
|
||||
token?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
}>("/Auth/refresh", {
|
||||
identifier: refreshToken,
|
||||
});
|
||||
|
||||
const responseData = response.data;
|
||||
|
||||
const newToken = responseData?.token || responseData?.accessToken;
|
||||
|
||||
if (!newToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/* UPDATE ACCESS TOKEN */
|
||||
store.dispatch(setToken(newToken));
|
||||
|
||||
/* UPDATE USER INFO */
|
||||
const prev = store.getState().user.infoUser;
|
||||
|
||||
store.dispatch(
|
||||
setInfoUser({
|
||||
accessToken: newToken,
|
||||
|
||||
refreshToken: responseData?.refreshToken || prev?.refreshToken || "",
|
||||
|
||||
accessExpiresAt: prev?.accessExpiresAt || "",
|
||||
|
||||
refreshExpiresAt: prev?.refreshExpiresAt || "",
|
||||
|
||||
avatar: prev?.avatar || "",
|
||||
|
||||
fullname: prev?.fullname || "",
|
||||
}),
|
||||
);
|
||||
|
||||
return newToken;
|
||||
} catch (error) {
|
||||
console.error("Refresh token failed:", error);
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
refreshPromise = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return refreshPromise;
|
||||
};
|
||||
|
||||
/* =========================================================
|
||||
* REQUEST INTERCEPTOR
|
||||
* =======================================================*/
|
||||
|
||||
axiosClient.interceptors.request.use(
|
||||
async (
|
||||
config: InternalAxiosRequestConfig,
|
||||
): Promise<InternalAxiosRequestConfig> => {
|
||||
const token = store.getState().auth.token;
|
||||
|
||||
config.headers = config.headers || {};
|
||||
|
||||
/* AUTHORIZATION */
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
/* AUTO ADD CERT */
|
||||
if (!(config.data instanceof FormData)) {
|
||||
config.data = {
|
||||
...getKeyCert(),
|
||||
@ -91,106 +203,131 @@ axiosClient.interceptors.request.use(
|
||||
},
|
||||
);
|
||||
|
||||
/* =========================================================
|
||||
* RESPONSE INTERCEPTOR
|
||||
* =======================================================*/
|
||||
|
||||
axiosClient.interceptors.response.use(
|
||||
(response) => {
|
||||
return response.data;
|
||||
return (
|
||||
response.data || {
|
||||
success: false,
|
||||
error: {
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: "Unknown error",
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
(error: AxiosError<ApiError>) => {
|
||||
return Promise.reject(error.response?.data || error);
|
||||
async (error: AxiosError<ApiResponse>) => {
|
||||
const status = error.response?.status;
|
||||
|
||||
const data = error.response?.data;
|
||||
|
||||
const config = error.config as InternalAxiosRequestConfig & {
|
||||
_retry?: boolean;
|
||||
};
|
||||
|
||||
/* =====================================================
|
||||
* AUTO REFRESH TOKEN
|
||||
* ===================================================*/
|
||||
|
||||
if (status === 401 && !config._retry) {
|
||||
config._retry = true;
|
||||
|
||||
const newToken = await refreshAccessToken();
|
||||
|
||||
if (newToken) {
|
||||
config.headers.Authorization = `Bearer ${newToken}`;
|
||||
|
||||
return axiosClient(config);
|
||||
}
|
||||
|
||||
/* REFRESH FAILED */
|
||||
store.dispatch(logout());
|
||||
|
||||
store.dispatch(setInfoUser(null));
|
||||
}
|
||||
|
||||
/* =====================================================
|
||||
* HANDLE EMPTY RESPONSE
|
||||
* ===================================================*/
|
||||
|
||||
if ((status === 401 || status === 403) && !data) {
|
||||
return Promise.reject({
|
||||
success: false,
|
||||
|
||||
error: {
|
||||
code: status,
|
||||
message: error.response?.statusText || "Unauthorized",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(
|
||||
data || {
|
||||
success: false,
|
||||
error: {
|
||||
code: "SERVER_ERROR",
|
||||
message: "Có lỗi xảy ra",
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default axiosClient;
|
||||
|
||||
/* =========================================================
|
||||
* HTTP REQUEST
|
||||
* =======================================================*/
|
||||
|
||||
export const httpRequest = async <T>({
|
||||
http,
|
||||
|
||||
setLoading,
|
||||
|
||||
msgSuccess = "Thành công",
|
||||
|
||||
showMessageSuccess = false,
|
||||
showMessageFailed = false,
|
||||
|
||||
onError,
|
||||
|
||||
isDropdown = false,
|
||||
isPagination = false,
|
||||
isData = false,
|
||||
}: HttpRequestOptions<T>) => {
|
||||
}: HttpRequestOptions<T>): Promise<T> => {
|
||||
try {
|
||||
setLoading?.(true);
|
||||
|
||||
await delay(500);
|
||||
await delay(300);
|
||||
|
||||
const res = await http;
|
||||
|
||||
if (res.error.code === 0) {
|
||||
const safeRes =
|
||||
res ??
|
||||
({
|
||||
success: false,
|
||||
error: {
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: "Unknown error",
|
||||
},
|
||||
} as ApiResponse<T>);
|
||||
|
||||
// ================= SUCCESS =================
|
||||
if (safeRes.success) {
|
||||
if (showMessageSuccess) {
|
||||
toastSuccess({
|
||||
msg: msgSuccess || res.error.message,
|
||||
});
|
||||
toastSuccess({ msg: msgSuccess });
|
||||
}
|
||||
|
||||
if (isDropdown) {
|
||||
return res.items || [];
|
||||
}
|
||||
|
||||
if (isPagination) {
|
||||
return {
|
||||
...res.data,
|
||||
items: res.items || [],
|
||||
pagination: res.pagination,
|
||||
};
|
||||
}
|
||||
|
||||
if (isData) {
|
||||
return res;
|
||||
}
|
||||
|
||||
return res.data || true;
|
||||
return safeRes.data as T;
|
||||
}
|
||||
|
||||
// ================= FAIL =================
|
||||
const error = {
|
||||
message: safeRes.error?.message || "Có lỗi xảy ra",
|
||||
code: safeRes.error?.code,
|
||||
};
|
||||
|
||||
onError?.();
|
||||
|
||||
throw new Error(res.error.message);
|
||||
} catch (error: unknown) {
|
||||
const err = error as AxiosError<ApiResponse> | Error;
|
||||
|
||||
const errorData = (err as AxiosError<ApiResponse>)?.response?.data;
|
||||
|
||||
if (errorData?.error?.code === 401 || errorData?.error?.code === 403) {
|
||||
store.dispatch(logout());
|
||||
|
||||
store.dispatch(setInfoUser(null));
|
||||
|
||||
if (showMessageFailed) {
|
||||
toastWarn({
|
||||
msg: errorData?.error?.message || "Có lỗi đã xảy ra!",
|
||||
});
|
||||
}
|
||||
} else if (err instanceof Error) {
|
||||
if (err.message === "ERR_NETWORK" || err.message === "ECONNABORTED") {
|
||||
if (showMessageFailed) {
|
||||
toastInfo({
|
||||
msg: "Kiểm tra kết nối internet",
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (showMessageFailed) {
|
||||
toastWarn({
|
||||
msg: err.message || "Có lỗi đã xảy ra!",
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (showMessageFailed) {
|
||||
toastWarn({
|
||||
msg: "Có lỗi đã xảy ra!",
|
||||
});
|
||||
}
|
||||
}
|
||||
throw error; // 👈 QUAN TRỌNG
|
||||
} catch (error) {
|
||||
throw error; // 👈 QUAN TRỌNG: KHÔNG NUỐT
|
||||
} finally {
|
||||
setLoading?.(false);
|
||||
}
|
||||
|
||||
118
src/services/patientServices.ts
Normal file
118
src/services/patientServices.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
export interface PatientItem {
|
||||
id: string;
|
||||
patientCode: string;
|
||||
identificationNumber: string;
|
||||
fullName: string;
|
||||
dateOfBirth: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
emergencyContactName?: string;
|
||||
emergencyContactPhone?: string;
|
||||
allergyNotes?: string;
|
||||
chronicDiseaseNotes?: string;
|
||||
insuranceNumber?: string;
|
||||
notes?: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export interface PatientDetail {
|
||||
id: string;
|
||||
patientCode: string;
|
||||
identificationNumber: string;
|
||||
fullName: string;
|
||||
dateOfBirth: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
emergencyContactName: string | null;
|
||||
emergencyContactPhone: string | null;
|
||||
allergyNotes: string | null;
|
||||
chronicDiseaseNotes: string | null;
|
||||
insuranceNumber: string | null;
|
||||
notes: string | null;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export interface GetPatientParams {
|
||||
Page?: number;
|
||||
PageSize?: number;
|
||||
Search?: string;
|
||||
IdentificationNumber?: string;
|
||||
SortBy?: string;
|
||||
Desc?: boolean;
|
||||
}
|
||||
|
||||
export interface PatientResponse {
|
||||
items: PatientItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const patientServices = {
|
||||
getPatient: (params?: GetPatientParams, tokenAxios?: any) => {
|
||||
return axiosClient.get<PatientResponse>(`/Patient/list`, {
|
||||
params,
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
createPatient: (
|
||||
data: {
|
||||
identificationNumber: string;
|
||||
fullName: string;
|
||||
dateOfBirth: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.post(`/Patient`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
putPatient: (
|
||||
id: string,
|
||||
data: {
|
||||
fullName: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
emergencyContactName: string;
|
||||
emergencyContactPhone: string;
|
||||
allergyNotes: string;
|
||||
chronicDiseaseNotes: string;
|
||||
insuranceNumber: string;
|
||||
notes: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.put(`/Patient/${id}`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
detailPatient: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.get<PatientDetail>(`/Patient/${id}`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
deletePatient: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.delete(`/Patient/${id}`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default patientServices;
|
||||
109
src/services/prescriptionServices.ts
Normal file
109
src/services/prescriptionServices.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
export interface GetPrescriptionParams {
|
||||
Page?: number;
|
||||
PageSize?: number;
|
||||
Search?: string;
|
||||
SortBy?: string;
|
||||
Desc?: boolean;
|
||||
}
|
||||
|
||||
export interface PrescriptionItem {
|
||||
id: string;
|
||||
examinationSessionId: string;
|
||||
prescriptionCode: string;
|
||||
prescriptionImg: string | null;
|
||||
notes: string;
|
||||
createdBy: string;
|
||||
items: {
|
||||
id: string;
|
||||
prescriptionId: string;
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface PrescriptionDetail {
|
||||
id: string;
|
||||
examinationSessionId: string;
|
||||
prescriptionCode: string;
|
||||
prescriptionImg: string | null;
|
||||
notes: string;
|
||||
createdBy: string;
|
||||
items: {
|
||||
id: string;
|
||||
prescriptionId: string;
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface PrescriptionResponse {
|
||||
items: PrescriptionItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const prescriptionServices = {
|
||||
createPrescription: (
|
||||
data: {
|
||||
examinationSessionId: string;
|
||||
notes: string;
|
||||
prescriptionImg: string;
|
||||
items: {
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}[];
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.post(`/Prescription`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
getPrescription: (params?: GetPrescriptionParams, tokenAxios?: any) => {
|
||||
return axiosClient.get<PrescriptionResponse>(`/Prescription`, {
|
||||
params,
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
putPrescription: (
|
||||
id: string,
|
||||
data: {
|
||||
notes: string;
|
||||
prescriptionImg: string;
|
||||
items: {
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}[];
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.put(`/Prescription/${id}`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
detailPrescription: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.get<PrescriptionDetail>(`/Prescription/${id}`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default prescriptionServices;
|
||||
68
src/services/userServices.ts
Normal file
68
src/services/userServices.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
interface GetUserParams {
|
||||
Page?: number;
|
||||
PageSize?: number;
|
||||
Search?: string;
|
||||
Role?: number;
|
||||
IsActive?: boolean;
|
||||
SortBy?: string;
|
||||
Desc?: boolean;
|
||||
}
|
||||
|
||||
const userServices = {
|
||||
getUser: (params?: GetUserParams, tokenAxios?: any) => {
|
||||
return axiosClient.get(`/User/list`, {
|
||||
params,
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
postUser: (
|
||||
data: {
|
||||
username: string;
|
||||
password: string;
|
||||
role: number;
|
||||
roleType: number;
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.post(`/Users/`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
putUser: (
|
||||
id: string,
|
||||
data: {
|
||||
role: number;
|
||||
roleType: number;
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.put(`/Users/${id}`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
deleteUser: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.delete(`/Users/${id}`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
detailUser: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.get(`/Users/${id}`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
meUser: (tokenAxios?: any) => {
|
||||
return axiosClient.get(`/Users/me`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default userServices;
|
||||
Loading…
Reference in New Issue
Block a user