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";
|
"use client";
|
||||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||||
|
import MainPageConsultation from "@/components/page/consultation/MainPageConsultation";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const page = () => {
|
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;
|
export default page;
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import BaseLayout from "@/components/layouts/BaseLayout/BaseLayout";
|
import BaseLayout from "@/components/layouts/BaseLayout/BaseLayout";
|
||||||
import MainPageHome from "@/components/page/Home/MainPageHome/MainPageHome";
|
import MainPagePatient from "@/components/page/patient/MainPagePatient";
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<BaseLayout title="Dashboard">
|
<BaseLayout title="Bệnh nhân">
|
||||||
<MainPageHome />
|
<MainPagePatient />
|
||||||
</BaseLayout>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||||
|
import MainPagePatient from "@/components/page/patient/MainPagePatient/MainPagePatient";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const page = () => {
|
const page = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<BaseLayout title="Bệnh nhân">Bệnh nhân</BaseLayout>
|
<BaseLayout title="Bệnh nhân">
|
||||||
|
<MainPagePatient />
|
||||||
|
</BaseLayout>
|
||||||
</div>
|
</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";
|
"use client";
|
||||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||||
|
import MainPrescription from "@/components/page/prescription/MainPrescription";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
const page = () => {
|
const page = () => {
|
||||||
return <BaseLayout title="Đơn thuốc">Đơn thuốc</BaseLayout>;
|
return (
|
||||||
|
<BaseLayout title="Đơn thuốc">
|
||||||
|
<MainPrescription />
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default page;
|
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;
|
target?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
|
||||||
|
type?: "button" | "submit" | "reset";
|
||||||
|
|
||||||
variant?:
|
variant?:
|
||||||
| "default"
|
| "default"
|
||||||
| "midnightBlue"
|
| "midnightBlue"
|
||||||
@ -36,6 +38,9 @@ export default function CustomButton({
|
|||||||
className,
|
className,
|
||||||
target,
|
target,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
|
||||||
|
type = "button",
|
||||||
|
|
||||||
variant = "default",
|
variant = "default",
|
||||||
size = "md",
|
size = "md",
|
||||||
rounded = "md",
|
rounded = "md",
|
||||||
@ -112,7 +117,7 @@ export default function CustomButton({
|
|||||||
// 🔘 Button mode
|
// 🔘 Button mode
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type={type}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className={baseClass}
|
className={baseClass}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
|||||||
const [showFull, setShowFull] = React.useState(false);
|
const [showFull, setShowFull] = React.useState(false);
|
||||||
const [openMenuMobile, setOpenMenuMobile] = React.useState(false);
|
const [openMenuMobile, setOpenMenuMobile] = React.useState(false);
|
||||||
return (
|
return (
|
||||||
// <RequireAuth>
|
<RequireAuth>
|
||||||
<ContextBaseLayout
|
<ContextBaseLayout
|
||||||
value={{ showFull, setShowFull, openMenuMobile, setOpenMenuMobile }}
|
value={{ showFull, setShowFull, openMenuMobile, setOpenMenuMobile }}
|
||||||
>
|
>
|
||||||
@ -64,7 +64,7 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</ContextBaseLayout>
|
</ContextBaseLayout>
|
||||||
// </RequireAuth>
|
</RequireAuth>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { use, useCallback, useState } from "react";
|
import React, { useCallback, useState } from "react";
|
||||||
import { PropsMenuProfile } from "./interface";
|
import { PropsMenuProfile } from "./interface";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
@ -11,15 +11,12 @@ import { PATH } from "@/constant/config";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { LogOut, ShieldCog, ShieldUser } from "lucide-react";
|
import { LogOut, ShieldCog, ShieldUser } from "lucide-react";
|
||||||
import CustomDialog from "@/components/customs/custom-dialog";
|
|
||||||
|
|
||||||
const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
const [openLogout, setOpenLogout] = useState(false);
|
|
||||||
|
|
||||||
/* ================== CHECK ACTIVE ================== */
|
/* ================== CHECK ACTIVE ================== */
|
||||||
const checkActive = useCallback(
|
const checkActive = useCallback(
|
||||||
(path: string) => {
|
(path: string) => {
|
||||||
@ -32,16 +29,18 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
|||||||
const logoutMutation = useMutation({
|
const logoutMutation = useMutation({
|
||||||
mutationFn: () =>
|
mutationFn: () =>
|
||||||
httpRequest({
|
httpRequest({
|
||||||
showMessageFailed: true,
|
showMessageFailed: false,
|
||||||
showMessageSuccess: false,
|
showMessageSuccess: false,
|
||||||
http: authServices.logout(),
|
http: authServices.logout(),
|
||||||
}),
|
}),
|
||||||
onSuccess(data) {
|
|
||||||
if (data) {
|
onSettled() {
|
||||||
store.dispatch(logout());
|
store.dispatch(logout());
|
||||||
store.dispatch(setInfoUser(null));
|
store.dispatch(setInfoUser(null));
|
||||||
router.push(PATH.LOGIN);
|
|
||||||
}
|
onClose();
|
||||||
|
|
||||||
|
router.replace(PATH.LOGIN);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -90,8 +89,7 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
|||||||
{/* LOGOUT */}
|
{/* LOGOUT */}
|
||||||
<div
|
<div
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpenLogout(true);
|
handleLogout();
|
||||||
onClose();
|
|
||||||
}}
|
}}
|
||||||
className="flex items-center gap-3 p-3 rounded-lg hover:bg-red-50 cursor-pointer transition"
|
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>
|
<p className="text-xs text-gray-500">Đăng xuất khỏi hệ thống</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { Menus, PATH } from "@/constant/config";
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import icons from "@/constant/images/icons";
|
import icons from "@/constant/images/icons";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import { Hospital } from "lucide-react";
|
||||||
|
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@ -22,10 +23,11 @@ const Navbar = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="h-full w-full flex flex-col items-center p-3 bg-white">
|
<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">
|
<Link href={PATH.HOME} className="flex items-center justify-center py-2">
|
||||||
<Image alt="Logo" src={icons.avatar} width={40} height={40} />
|
{/* <Image alt="Logo" src={icons.avatar} width={40} height={40} /> */}
|
||||||
<h4 className="ml-2 text-sm font-semibold text-blue-900 select-none">
|
<Hospital className="text-blue-900" />
|
||||||
Quản lý
|
<h2 className="ml-2 text-sm font-semibold text-blue-900 select-none">
|
||||||
</h4>
|
Quản lý khám bệnh
|
||||||
|
</h2>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex-1 w-full overflow-auto pt-3">
|
<div className="flex-1 w-full overflow-auto pt-3">
|
||||||
{Menus.map((menu, i) => (
|
{Menus.map((menu, i) => (
|
||||||
|
|||||||
@ -57,13 +57,9 @@ export default function FormPassword() {
|
|||||||
http: authServices.login({
|
http: authServices.login({
|
||||||
username: dataLoginStorage?.usernameStorage || "",
|
username: dataLoginStorage?.usernameStorage || "",
|
||||||
|
|
||||||
password: md5(
|
password: md5(`${form.password}_${process.env.NEXT_PUBLIC_KEY_PASS}`),
|
||||||
`${form.password}_${process.env.NEXT_PUBLIC_KEY_PASSWORD}`,
|
|
||||||
),
|
|
||||||
|
|
||||||
ip: "",
|
deviceId: dataLoginStorage?.deviceIdStorage || "web-browser",
|
||||||
address: "",
|
|
||||||
type: 0,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -73,11 +69,11 @@ export default function FormPassword() {
|
|||||||
|
|
||||||
store.dispatch(setStateLogin(true));
|
store.dispatch(setStateLogin(true));
|
||||||
|
|
||||||
store.dispatch(setToken(data.accessToken));
|
store.dispatch(setToken(data.token));
|
||||||
|
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
setInfoUser({
|
setInfoUser({
|
||||||
accessToken: data?.accessToken || "",
|
accessToken: data?.token || "",
|
||||||
refreshToken: data?.refreshToken || "",
|
refreshToken: data?.refreshToken || "",
|
||||||
accessExpiresAt: data?.accessExpiresAt || "",
|
accessExpiresAt: data?.accessExpiresAt || "",
|
||||||
refreshExpiresAt: data?.refreshExpiresAt || "",
|
refreshExpiresAt: data?.refreshExpiresAt || "",
|
||||||
@ -92,6 +88,7 @@ export default function FormPassword() {
|
|||||||
usernameStorage: dataLoginStorage?.usernameStorage || "",
|
usernameStorage: dataLoginStorage?.usernameStorage || "",
|
||||||
|
|
||||||
passwordStorage: form.password,
|
passwordStorage: form.password,
|
||||||
|
deviceIdStorage: dataLoginStorage?.deviceIdStorage || "web-browser",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -26,7 +26,7 @@ import { PATH } from "@/constant/config";
|
|||||||
import FormCustom from "@/components/utils/FormCustom";
|
import FormCustom from "@/components/utils/FormCustom";
|
||||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||||
import CustomLoading from "@/components/customs/custom-loading";
|
import CustomLoading from "@/components/customs/custom-loading";
|
||||||
// import FormCustom, { InputForm } from "@/components/common/FormCustom";
|
import CustomButton from "@/components/customs/custom-button";
|
||||||
|
|
||||||
export default function MainLogin() {
|
export default function MainLogin() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -39,6 +39,8 @@ export default function MainLogin() {
|
|||||||
username: isRememberPassword ? dataLoginStorage?.usernameStorage || "" : "",
|
username: isRememberPassword ? dataLoginStorage?.usernameStorage || "" : "",
|
||||||
|
|
||||||
password: isRememberPassword ? dataLoginStorage?.passwordStorage || "" : "",
|
password: isRememberPassword ? dataLoginStorage?.passwordStorage || "" : "",
|
||||||
|
|
||||||
|
deviceId: isRememberPassword ? dataLoginStorage?.deviceIdStorage || "" : "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginMutation = useMutation({
|
const loginMutation = useMutation({
|
||||||
@ -50,9 +52,7 @@ export default function MainLogin() {
|
|||||||
http: authServices.login({
|
http: authServices.login({
|
||||||
username: form.username,
|
username: form.username,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
ip: "",
|
deviceId: form.deviceId,
|
||||||
address: "",
|
|
||||||
type: 0,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -62,11 +62,11 @@ export default function MainLogin() {
|
|||||||
|
|
||||||
store.dispatch(setStateLogin(true));
|
store.dispatch(setStateLogin(true));
|
||||||
|
|
||||||
store.dispatch(setToken(data.accessToken));
|
store.dispatch(setToken(data.token));
|
||||||
|
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
setInfoUser({
|
setInfoUser({
|
||||||
accessToken: data?.accessToken || "",
|
accessToken: data?.token || "",
|
||||||
refreshToken: data?.refreshToken || "",
|
refreshToken: data?.refreshToken || "",
|
||||||
accessExpiresAt: data?.accessExpiresAt || "",
|
accessExpiresAt: data?.accessExpiresAt || "",
|
||||||
refreshExpiresAt: data?.refreshExpiresAt || "",
|
refreshExpiresAt: data?.refreshExpiresAt || "",
|
||||||
@ -80,6 +80,7 @@ export default function MainLogin() {
|
|||||||
setDataLoginStorage({
|
setDataLoginStorage({
|
||||||
usernameStorage: form.username,
|
usernameStorage: form.username,
|
||||||
passwordStorage: form.password,
|
passwordStorage: form.password,
|
||||||
|
deviceIdStorage: form.deviceId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -141,6 +142,25 @@ export default function MainLogin() {
|
|||||||
icon={<User size={22} />}
|
icon={<User size={22} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="mt-5">
|
||||||
|
<InputForm
|
||||||
|
label={
|
||||||
|
<span>
|
||||||
|
deviceId
|
||||||
|
<span className="text-red-500"> *</span>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
placeholder="deviceId"
|
||||||
|
type="text"
|
||||||
|
name="deviceId"
|
||||||
|
onClean
|
||||||
|
isRequired
|
||||||
|
isBlur
|
||||||
|
showDone
|
||||||
|
icon={<ShieldPlus size={22} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* PASSWORD */}
|
{/* PASSWORD */}
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<InputForm
|
<InputForm
|
||||||
@ -202,42 +222,42 @@ export default function MainLogin() {
|
|||||||
|
|
||||||
{/* BUTTONS */}
|
{/* BUTTONS */}
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
|
<div className="flex gap-2">
|
||||||
{/* LOGIN */}
|
{/* LOGIN */}
|
||||||
<button
|
<CustomButton
|
||||||
type="submit"
|
variant="midnightBlue"
|
||||||
disabled={loginMutation.isPending}
|
disabled={loginMutation.isPending}
|
||||||
className="
|
rounded="full"
|
||||||
flex
|
type="submit"
|
||||||
h-[52px]
|
className="font-bold text-2xl disabled:cursor-not-allowed
|
||||||
w-full
|
disabled:opacity-50 h-[52px]"
|
||||||
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
|
Đăng nhập
|
||||||
</button>
|
</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 */}
|
{/* LINE */}
|
||||||
<div
|
{/* <div
|
||||||
className="
|
className="
|
||||||
my-5
|
my-5
|
||||||
h-[1px]
|
h-[1px]
|
||||||
w-full
|
w-full
|
||||||
bg-[#E5E5E5]
|
bg-[#E5E5E5]
|
||||||
"
|
"
|
||||||
/>
|
/> */}
|
||||||
|
|
||||||
{/* FORGOT PASSWORD */}
|
{/* FORGOT PASSWORD */}
|
||||||
<button
|
{/* <button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push(PATH.FORGOT_PASSWORD)}
|
onClick={() => router.push(PATH.FORGOT_PASSWORD)}
|
||||||
className="
|
className="
|
||||||
@ -260,7 +280,7 @@ export default function MainLogin() {
|
|||||||
>
|
>
|
||||||
<LockKeyhole size={22} />
|
<LockKeyhole size={22} />
|
||||||
Quên mật khẩu
|
Quên mật khẩu
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormCustom>
|
</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;
|
isUppercase?: boolean;
|
||||||
|
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
|
|
||||||
onChangeValue?: (val: string | number) => void;
|
onChangeValue?: (val: string | number) => void;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,7 +81,7 @@ function SelectForm<OptionType>({
|
|||||||
onClick={() => !readOnly && setOpen((prev) => !prev)}
|
onClick={() => !readOnly && setOpen((prev) => !prev)}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center justify-between px-4 h-12 border rounded-full cursor-pointer transition",
|
"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",
|
open && "border-blue-600",
|
||||||
readOnly &&
|
readOnly &&
|
||||||
"bg-gray-100 border-gray-200 cursor-not-allowed hover:border-gray-200",
|
"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 React, { useContext, useEffect, useState } from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
import { PropsTextArea } from "./interface";
|
import { PropsTextArea } from "./interface";
|
||||||
import { ContextFormCustom, IContextFormCustom } from "../../contexts";
|
import { ContextFormCustom, IContextFormCustom } from "../../contexts";
|
||||||
|
|
||||||
@ -18,6 +17,7 @@ function TextArea({
|
|||||||
min,
|
min,
|
||||||
isRequired,
|
isRequired,
|
||||||
textRequired,
|
textRequired,
|
||||||
|
onChangeValue, // Nhận callback custom từ cha
|
||||||
}: PropsTextArea) {
|
}: PropsTextArea) {
|
||||||
const [isFocus, setIsFocus] = useState(false);
|
const [isFocus, setIsFocus] = useState(false);
|
||||||
|
|
||||||
@ -31,159 +31,175 @@ function TextArea({
|
|||||||
setErrorText,
|
setErrorText,
|
||||||
} = useContext<IContextFormCustom<any>>(ContextFormCustom);
|
} = 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 handleValidate = (inputValue?: string) => {
|
||||||
const currentValue = `${form[name] || ""}`.trim();
|
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,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// =========================
|
// Cập nhật lỗi và trạng thái validate vào Context
|
||||||
// ERROR MESSAGE
|
const handleSetMessage = (inputValue?: string) => {
|
||||||
// =========================
|
const result = handleValidate(inputValue);
|
||||||
|
|
||||||
const handleSetMessage = () => {
|
|
||||||
const currentValue = `${form[name] || ""}`.trim();
|
|
||||||
|
|
||||||
setErrorText((prev) => ({
|
setErrorText((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: null,
|
[name]: result.message,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (isRequired && currentValue === "") {
|
setValidate((prev) => ({
|
||||||
return setErrorText((prev) => ({
|
|
||||||
...prev,
|
...prev,
|
||||||
[name]: textRequired || "Vui lòng nhập trường này",
|
[name]: result.valid,
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
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ự`,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// EFFECTS
|
// EFFECTS
|
||||||
// =========================
|
// =========================
|
||||||
|
|
||||||
|
// Chạy validate toàn cục khi Form yêu cầu (countValidate tăng)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (countValidate > 0) {
|
if (countValidate > 0) {
|
||||||
handleSetMessage();
|
handleSetMessage();
|
||||||
}
|
}
|
||||||
}, [countValidate]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
|
const result = handleValidate();
|
||||||
setValidate((prev) => ({
|
setValidate((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: handleValidate(),
|
[name]: result.valid,
|
||||||
}));
|
}));
|
||||||
}, [form[name]]);
|
}, [currentValue]);
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// EVENTS
|
// EVENT HANDLERS
|
||||||
// =========================
|
// =========================
|
||||||
|
|
||||||
const handlerFocused = () => {
|
const handleFocus = () => {
|
||||||
setIsFocus(true);
|
setIsFocus(true);
|
||||||
|
// Xóa thông báo lỗi khi người dùng bắt đầu sửa lại
|
||||||
setErrorText((prev) => ({
|
setErrorText((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
[name]: null,
|
[name]: null,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlerBlur = () => {
|
const handleBlur = () => {
|
||||||
setIsFocus(false);
|
setIsFocus(false);
|
||||||
|
|
||||||
if (isBlur) {
|
if (isBlur) {
|
||||||
handleSetMessage();
|
handleSetMessage();
|
||||||
|
|
||||||
setValidate((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[name]: handleValidate(),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
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) => ({
|
setForm((prev: any) => ({
|
||||||
...prev,
|
...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 (
|
return (
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
{/* LABEL */}
|
{/* LABEL */}
|
||||||
{label && (
|
{label && (
|
||||||
<label
|
<label
|
||||||
htmlFor={`textarea_${name}`}
|
htmlFor={`textarea_${name}`}
|
||||||
className="block text-[16px] font-medium"
|
className="block text-[16px] font-medium text-[#171717]"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* TEXTAREA */}
|
{/* TEXTAREA WRAPPER */}
|
||||||
<textarea
|
<textarea
|
||||||
id={`textarea_${name}`}
|
id={`textarea_${name}`}
|
||||||
name={name}
|
name={name}
|
||||||
value={value ?? form[name] ?? ""}
|
value={String(currentValue)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onFocus={handlerFocused}
|
onFocus={handleFocus}
|
||||||
onBlur={handlerBlur}
|
onBlur={handleBlur}
|
||||||
className={clsx(
|
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
|
// Trạng thái mặc định
|
||||||
"border-[#CDD5DF] bg-white",
|
"border-[#CDD5DF] bg-white text-[#171717]",
|
||||||
|
|
||||||
// hover
|
// Hover (chỉ khi không readOnly)
|
||||||
!readOnly && "hover:border-[#0019F6]",
|
!readOnly && "hover:border-[#0019F6]",
|
||||||
|
|
||||||
// focus
|
// Focus
|
||||||
isFocus && "border-[#0019F6]",
|
isFocus && "border-[#0019F6]",
|
||||||
|
|
||||||
// error
|
// Có lỗi
|
||||||
errorText[name] && "border-[#E5493F]",
|
errorText?.[name] && "border-red-500",
|
||||||
|
|
||||||
// done
|
// Đã hoàn tất và không có lỗi
|
||||||
showDone && isDone && "border-[#1CAD4C]",
|
showDone && isDone && !errorText?.[name] && "border-green-600",
|
||||||
|
|
||||||
// readonly
|
// Trạng thái ReadOnly/Disabled
|
||||||
readOnly &&
|
readOnly && "cursor-not-allowed border-gray-200 bg-gray-100",
|
||||||
"cursor-not-allowed border-gray-200 bg-gray-100 hover:border-gray-200",
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ERROR */}
|
{/* ERROR MESSAGE */}
|
||||||
{errorText[name] && (
|
{errorText?.[name] && (
|
||||||
<p className="text-[12px] text-red-500">{errorText[name]}</p>
|
<p className="mt-1 text-xs font-medium text-red-500">
|
||||||
|
{errorText[name]}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -14,4 +14,5 @@ export interface PropsTextArea {
|
|||||||
min?: number;
|
min?: number;
|
||||||
|
|
||||||
isRequired?: boolean;
|
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 {
|
export enum TYPE_DATE {
|
||||||
ALL = -1,
|
ALL = -1,
|
||||||
TODAY = 1,
|
TODAY = 1,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ export enum PATH {
|
|||||||
PATIENT = "/patient",
|
PATIENT = "/patient",
|
||||||
CONSULTATION = "/consultation",
|
CONSULTATION = "/consultation",
|
||||||
PRESCRIPTION = "/prescription",
|
PRESCRIPTION = "/prescription",
|
||||||
|
CREATE_PRESCRIPTION = "/prescription/create",
|
||||||
|
|
||||||
LOGIN = "/auth/login",
|
LOGIN = "/auth/login",
|
||||||
REGISTER = "/auth/register",
|
REGISTER = "/auth/register",
|
||||||
@ -26,17 +27,17 @@ export const Menus: {
|
|||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
group: [
|
group: [
|
||||||
{
|
// {
|
||||||
title: "Dashboard",
|
// title: "Dashboard",
|
||||||
icon: CircleGauge,
|
// icon: CircleGauge,
|
||||||
path: PATH.DASHBOARD || PATH.HOME,
|
// path: PATH.DASHBOARD || PATH.HOME,
|
||||||
pathActive: PATH.DASHBOARD,
|
// pathActive: PATH.DASHBOARD,
|
||||||
},
|
// },
|
||||||
{
|
{
|
||||||
title: "Bệnh nhân",
|
title: "Bệnh nhân",
|
||||||
icon: Users,
|
icon: Users,
|
||||||
path: PATH.PATIENT,
|
path: PATH.PATIENT || PATH.HOME,
|
||||||
pathActive: PATH.PATIENT,
|
pathActive: PATH.PATIENT || PATH.HOME,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Phiên khám",
|
title: "Phiên khám",
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
|||||||
export interface IDataLoginStorage {
|
export interface IDataLoginStorage {
|
||||||
usernameStorage: string;
|
usernameStorage: string;
|
||||||
passwordStorage: string;
|
passwordStorage: string;
|
||||||
|
deviceIdStorage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthState {
|
export interface AuthState {
|
||||||
|
|||||||
@ -5,9 +5,7 @@ const authServices = {
|
|||||||
data: {
|
data: {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
ip: string;
|
deviceId: string;
|
||||||
address: string;
|
|
||||||
type: number;
|
|
||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
@ -15,11 +13,38 @@ const authServices = {
|
|||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
logout: (data: void, tokenAxios?: any) => {
|
register: (
|
||||||
return axiosClient.post(`/Auth/logout`, data, {
|
data: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
},
|
||||||
|
tokenAxios?: any,
|
||||||
|
) => {
|
||||||
|
return axiosClient.post(`/Auth/register`, data, {
|
||||||
cancelToken: tokenAxios,
|
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;
|
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, {
|
import axios, {
|
||||||
AxiosError,
|
AxiosError,
|
||||||
AxiosInstance,
|
AxiosInstance,
|
||||||
AxiosResponse,
|
|
||||||
InternalAxiosRequestConfig,
|
InternalAxiosRequestConfig,
|
||||||
} from "axios";
|
} from "axios";
|
||||||
|
|
||||||
@ -9,17 +8,22 @@ import { delay } from "@/common/funcs/delay";
|
|||||||
import { getKeyCert } from "@/common/funcs/optionConvert";
|
import { getKeyCert } from "@/common/funcs/optionConvert";
|
||||||
import { toastInfo, toastSuccess, toastWarn } from "@/common/funcs/toast";
|
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 { setInfoUser } from "@/redux/reducer/user";
|
||||||
import { store } from "@/redux/store";
|
import { store } from "@/redux/store";
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
* TYPES
|
||||||
|
* =======================================================*/
|
||||||
|
|
||||||
interface ApiError {
|
interface ApiError {
|
||||||
code: number;
|
code: string | number;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApiResponse<T = unknown> {
|
export interface ApiResponse<T = unknown> {
|
||||||
error: ApiError;
|
success?: boolean;
|
||||||
|
error?: ApiError | null;
|
||||||
data?: T;
|
data?: T;
|
||||||
items?: T[];
|
items?: T[];
|
||||||
pagination?: unknown;
|
pagination?: unknown;
|
||||||
@ -42,16 +46,20 @@ interface HttpRequestOptions<T> {
|
|||||||
isData?: boolean;
|
isData?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const axiosClient = axios.create({
|
/* =========================================================
|
||||||
headers: {
|
* AXIOS CLIENT
|
||||||
"content-type": "application/json",
|
* =======================================================*/
|
||||||
},
|
|
||||||
|
|
||||||
baseURL: "/api-proxy",
|
const axiosClient = axios.create({
|
||||||
|
baseURL: process.env.NEXT_PUBLIC_API_URL_DEV,
|
||||||
|
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
|
|
||||||
timeoutErrorMessage: "Timeout error request",
|
timeoutErrorMessage: "Timeout error request",
|
||||||
|
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
}) as AxiosInstance & {
|
}) as AxiosInstance & {
|
||||||
get<T = unknown>(url: string, config?: any): Promise<ApiResponse<T>>;
|
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>>;
|
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(
|
axiosClient.interceptors.request.use(
|
||||||
async (
|
async (
|
||||||
config: InternalAxiosRequestConfig,
|
config: InternalAxiosRequestConfig,
|
||||||
): Promise<InternalAxiosRequestConfig> => {
|
): Promise<InternalAxiosRequestConfig> => {
|
||||||
const token = store.getState().auth.token;
|
const token = store.getState().auth.token;
|
||||||
|
|
||||||
|
config.headers = config.headers || {};
|
||||||
|
|
||||||
|
/* AUTHORIZATION */
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`;
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* AUTO ADD CERT */
|
||||||
if (!(config.data instanceof FormData)) {
|
if (!(config.data instanceof FormData)) {
|
||||||
config.data = {
|
config.data = {
|
||||||
...getKeyCert(),
|
...getKeyCert(),
|
||||||
@ -91,106 +203,131 @@ axiosClient.interceptors.request.use(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
* RESPONSE INTERCEPTOR
|
||||||
|
* =======================================================*/
|
||||||
|
|
||||||
axiosClient.interceptors.response.use(
|
axiosClient.interceptors.response.use(
|
||||||
(response) => {
|
(response) => {
|
||||||
return response.data;
|
return (
|
||||||
|
response.data || {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: "UNKNOWN_ERROR",
|
||||||
|
message: "Unknown error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
(error: AxiosError<ApiError>) => {
|
async (error: AxiosError<ApiResponse>) => {
|
||||||
return Promise.reject(error.response?.data || error);
|
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;
|
export default axiosClient;
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
* HTTP REQUEST
|
||||||
|
* =======================================================*/
|
||||||
|
|
||||||
export const httpRequest = async <T>({
|
export const httpRequest = async <T>({
|
||||||
http,
|
http,
|
||||||
|
|
||||||
setLoading,
|
setLoading,
|
||||||
|
|
||||||
msgSuccess = "Thành công",
|
msgSuccess = "Thành công",
|
||||||
|
|
||||||
showMessageSuccess = false,
|
showMessageSuccess = false,
|
||||||
showMessageFailed = false,
|
showMessageFailed = false,
|
||||||
|
|
||||||
onError,
|
onError,
|
||||||
|
}: HttpRequestOptions<T>): Promise<T> => {
|
||||||
isDropdown = false,
|
|
||||||
isPagination = false,
|
|
||||||
isData = false,
|
|
||||||
}: HttpRequestOptions<T>) => {
|
|
||||||
try {
|
try {
|
||||||
setLoading?.(true);
|
setLoading?.(true);
|
||||||
|
|
||||||
await delay(500);
|
await delay(300);
|
||||||
|
|
||||||
const res = await http;
|
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) {
|
if (showMessageSuccess) {
|
||||||
toastSuccess({
|
toastSuccess({ msg: msgSuccess });
|
||||||
msg: msgSuccess || res.error.message,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDropdown) {
|
return safeRes.data as T;
|
||||||
return res.items || [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPagination) {
|
// ================= FAIL =================
|
||||||
return {
|
const error = {
|
||||||
...res.data,
|
message: safeRes.error?.message || "Có lỗi xảy ra",
|
||||||
items: res.items || [],
|
code: safeRes.error?.code,
|
||||||
pagination: res.pagination,
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
if (isData) {
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.data || true;
|
|
||||||
}
|
|
||||||
|
|
||||||
onError?.();
|
onError?.();
|
||||||
|
|
||||||
throw new Error(res.error.message);
|
throw error; // 👈 QUAN TRỌNG
|
||||||
} catch (error: unknown) {
|
} catch (error) {
|
||||||
const err = error as AxiosError<ApiResponse> | Error;
|
throw error; // 👈 QUAN TRỌNG: KHÔNG NUỐT
|
||||||
|
|
||||||
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!",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading?.(false);
|
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