- {/* OVERLAY cho Mobile */}
-
setOpenMenuMobile(false)}
- />
+
+
+
+ {/* OVERLAY cho Mobile */}
+
setOpenMenuMobile(false)}
+ />
- {/* SIDEBAR (Dùng chung cho cả Desktop & Mobile) */}
-
+ {/* SIDEBAR (Dùng chung cho cả Desktop & Mobile) */}
+
- {/* HEADER */}
-
+ {/* HEADER */}
+
- {/* MAIN CONTENT */}
-
- {children}
-
-
-
- //
+ {/* MAIN CONTENT */}
+
+ {children}
+
+
+
+
);
};
diff --git a/src/components/layouts/BaseLayout/componets/MenuProfile/MenuProfile.tsx b/src/components/layouts/BaseLayout/componets/MenuProfile/MenuProfile.tsx
index 157fb72..7361e66 100644
--- a/src/components/layouts/BaseLayout/componets/MenuProfile/MenuProfile.tsx
+++ b/src/components/layouts/BaseLayout/componets/MenuProfile/MenuProfile.tsx
@@ -1,4 +1,4 @@
-import React, { use, useCallback, useState } from "react";
+import React, { useCallback, useState } from "react";
import { PropsMenuProfile } from "./interface";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
@@ -11,15 +11,12 @@ import { PATH } from "@/constant/config";
import Link from "next/link";
import clsx from "clsx";
import { LogOut, ShieldCog, ShieldUser } from "lucide-react";
-import CustomDialog from "@/components/customs/custom-dialog";
const MenuProfile = ({ onClose }: PropsMenuProfile) => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
- const [openLogout, setOpenLogout] = useState(false);
-
/* ================== CHECK ACTIVE ================== */
const checkActive = useCallback(
(path: string) => {
@@ -32,16 +29,18 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
const logoutMutation = useMutation({
mutationFn: () =>
httpRequest({
- showMessageFailed: true,
+ showMessageFailed: false,
showMessageSuccess: false,
http: authServices.logout(),
}),
- onSuccess(data) {
- if (data) {
- store.dispatch(logout());
- store.dispatch(setInfoUser(null));
- router.push(PATH.LOGIN);
- }
+
+ onSettled() {
+ store.dispatch(logout());
+ store.dispatch(setInfoUser(null));
+
+ onClose();
+
+ router.replace(PATH.LOGIN);
},
});
@@ -90,8 +89,7 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
{/* LOGOUT */}
{
- setOpenLogout(true);
- onClose();
+ handleLogout();
}}
className="flex items-center gap-3 p-3 rounded-lg hover:bg-red-50 cursor-pointer transition"
>
@@ -101,18 +99,6 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
Đăng xuất khỏi hệ thống
-
- {/* DIALOG */}
-
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"
- />
);
};
diff --git a/src/components/layouts/BaseLayout/componets/Navbar/Navbar.tsx b/src/components/layouts/BaseLayout/componets/Navbar/Navbar.tsx
index 6733296..2b3961e 100644
--- a/src/components/layouts/BaseLayout/componets/Navbar/Navbar.tsx
+++ b/src/components/layouts/BaseLayout/componets/Navbar/Navbar.tsx
@@ -7,6 +7,7 @@ import { Menus, PATH } from "@/constant/config";
import Image from "next/image";
import icons from "@/constant/images/icons";
import clsx from "clsx";
+import { Hospital } from "lucide-react";
const Navbar = () => {
const pathname = usePathname();
@@ -22,10 +23,11 @@ const Navbar = () => {
return (
-
-
- Quản lý
-
+ {/*
*/}
+
+
+ Quản lý khám bệnh
+
{Menus.map((menu, i) => (
diff --git a/src/components/page/auth/MainForgotPassword/components/FormPassword/FormPassword.tsx b/src/components/page/auth/MainForgotPassword/components/FormPassword/FormPassword.tsx
index fa111f2..3be9def 100644
--- a/src/components/page/auth/MainForgotPassword/components/FormPassword/FormPassword.tsx
+++ b/src/components/page/auth/MainForgotPassword/components/FormPassword/FormPassword.tsx
@@ -57,13 +57,9 @@ export default function FormPassword() {
http: authServices.login({
username: dataLoginStorage?.usernameStorage || "",
- password: md5(
- `${form.password}_${process.env.NEXT_PUBLIC_KEY_PASSWORD}`,
- ),
+ password: md5(`${form.password}_${process.env.NEXT_PUBLIC_KEY_PASS}`),
- ip: "",
- address: "",
- type: 0,
+ deviceId: dataLoginStorage?.deviceIdStorage || "web-browser",
}),
});
},
@@ -73,11 +69,11 @@ export default function FormPassword() {
store.dispatch(setStateLogin(true));
- store.dispatch(setToken(data.accessToken));
+ store.dispatch(setToken(data.token));
store.dispatch(
setInfoUser({
- accessToken: data?.accessToken || "",
+ accessToken: data?.token || "",
refreshToken: data?.refreshToken || "",
accessExpiresAt: data?.accessExpiresAt || "",
refreshExpiresAt: data?.refreshExpiresAt || "",
@@ -92,6 +88,7 @@ export default function FormPassword() {
usernameStorage: dataLoginStorage?.usernameStorage || "",
passwordStorage: form.password,
+ deviceIdStorage: dataLoginStorage?.deviceIdStorage || "web-browser",
}),
);
} else {
diff --git a/src/components/page/auth/MainLogin/MainLogin.tsx b/src/components/page/auth/MainLogin/MainLogin.tsx
index 8e77298..dd1b434 100644
--- a/src/components/page/auth/MainLogin/MainLogin.tsx
+++ b/src/components/page/auth/MainLogin/MainLogin.tsx
@@ -26,7 +26,7 @@ import { PATH } from "@/constant/config";
import FormCustom from "@/components/utils/FormCustom";
import InputForm from "@/components/utils/FormCustom/components/InputForm";
import CustomLoading from "@/components/customs/custom-loading";
-// import FormCustom, { InputForm } from "@/components/common/FormCustom";
+import CustomButton from "@/components/customs/custom-button";
export default function MainLogin() {
const router = useRouter();
@@ -39,6 +39,8 @@ export default function MainLogin() {
username: isRememberPassword ? dataLoginStorage?.usernameStorage || "" : "",
password: isRememberPassword ? dataLoginStorage?.passwordStorage || "" : "",
+
+ deviceId: isRememberPassword ? dataLoginStorage?.deviceIdStorage || "" : "",
});
const loginMutation = useMutation({
@@ -50,9 +52,7 @@ export default function MainLogin() {
http: authServices.login({
username: form.username,
password: form.password,
- ip: "",
- address: "",
- type: 0,
+ deviceId: form.deviceId,
}),
});
},
@@ -62,11 +62,11 @@ export default function MainLogin() {
store.dispatch(setStateLogin(true));
- store.dispatch(setToken(data.accessToken));
+ store.dispatch(setToken(data.token));
store.dispatch(
setInfoUser({
- accessToken: data?.accessToken || "",
+ accessToken: data?.token || "",
refreshToken: data?.refreshToken || "",
accessExpiresAt: data?.accessExpiresAt || "",
refreshExpiresAt: data?.refreshExpiresAt || "",
@@ -80,6 +80,7 @@ export default function MainLogin() {
setDataLoginStorage({
usernameStorage: form.username,
passwordStorage: form.password,
+ deviceIdStorage: form.deviceId,
}),
);
} else {
@@ -141,6 +142,25 @@ export default function MainLogin() {
icon={
}
/>
+
+
+ deviceId
+ *
+
+ }
+ placeholder="deviceId"
+ type="text"
+ name="deviceId"
+ onClean
+ isRequired
+ isBlur
+ showDone
+ icon={}
+ />
+
+
{/* PASSWORD */}
- {/* LOGIN */}
-
+
+ {/* LOGIN */}
+
+ Đăng nhập
+
+ {/* REGISTER */}
+ router.push(PATH.REGISTER)}
+ >
+ Đăng ký
+
+
{/* LINE */}
-
+ /> */}
{/* FORGOT PASSWORD */}
-
+ */}
diff --git a/src/components/page/auth/MainRegister/MainRegister.tsx b/src/components/page/auth/MainRegister/MainRegister.tsx
new file mode 100644
index 0000000..1936ff1
--- /dev/null
+++ b/src/components/page/auth/MainRegister/MainRegister.tsx
@@ -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 (
+
+
+
+
+ {/* TITLE */}
+
+ Đăng ký
+
+
+
+ Tạo tài khoản mới cho hệ thống
+
+
+ {/* FORM */}
+
+ {/* FULL NAME */}
+
+ Họ và tên
+ *
+
+ }
+ placeholder="Nhập họ và tên"
+ type="text"
+ name="fullName"
+ onClean
+ isRequired
+ isBlur
+ showDone
+ icon={}
+ />
+
+ {/* EMAIL */}
+
+
+ Email
+ *
+
+ }
+ placeholder="Nhập email"
+ type="email"
+ name="email"
+ onClean
+ isRequired
+ isBlur
+ showDone
+ icon={}
+ />
+
+
+ {/* USERNAME */}
+
+
+ Tài khoản
+ *
+
+ }
+ placeholder="Nhập tài khoản"
+ type="text"
+ name="username"
+ onClean
+ isRequired
+ isBlur
+ showDone
+ icon={}
+ />
+
+
+ {/* PASSWORD */}
+
+
+ Mật khẩu
+ *
+
+ }
+ placeholder="Nhập mật khẩu"
+ type="password"
+ name="password"
+ onClean
+ isRequired
+ isBlur
+ showDone
+ icon={}
+ />
+
+
+ {/* BUTTON */}
+
+ {/* REGISTER */}
+
+ Đăng ký
+
+
+ {/* LOGIN */}
+ router.push(PATH.LOGIN)}
+ >
+ Đăng nhập
+
+
+
+
+
+ );
+}
diff --git a/src/components/page/auth/MainRegister/index.ts b/src/components/page/auth/MainRegister/index.ts
new file mode 100644
index 0000000..2457a6d
--- /dev/null
+++ b/src/components/page/auth/MainRegister/index.ts
@@ -0,0 +1 @@
+export { default } from "./MainRegister";
diff --git a/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx b/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx
new file mode 100644
index 0000000..41ee9d7
--- /dev/null
+++ b/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx
@@ -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
({
+ queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId],
+
+ enabled: !!consultationId,
+
+ queryFn: async () => {
+ const res = await httpRequest({
+ 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 Đang tải dữ liệu...
;
+ }
+
+ if (!consultation) {
+ return Không tìm thấy dữ liệu phiên khám
;
+ }
+
+ return (
+
+
+
+ {/* HEADER */}
+
+
router.back()}
+ className="cursor-pointer"
+ />
+
+
+ Chi tiết phiên khám
+
+
+
+ {/* CONTENT */}
+
+ {/* ID PHIÊN KHÁM */}
+
+
+ ID PHIÊN KHÁM
+
+
+
+ {consultation.id}
+
+
+
+ {/* MÃ PHIÊN KHÁM */}
+
+
+ MÃ PHIÊN KHÁM
+
+
+
+ {consultation.sessionCode}
+
+
+
+ {/* NGÀY GIỜ KHÁM */}
+
+
+ NGÀY, GIỜ KHÁM
+
+
+
+ {consultation.visitDate}
+
+
+
+ {/* TRIỆU CHỨNG */}
+
+
DẤU HIỆU
+
+
+ {consultation.symptomsText || "---"}
+
+
+
+ {/* KHIẾU NẠI CHÍNH */}
+
+
+ KHIẾU NẠI CHÍNH
+
+
+
+ {consultation.chiefComplaint || "---"}
+
+
+
+ {/* DẤU HIỆU SINH TỒN */}
+
+
+ DẤU HIỆU SINH TỒN
+
+
+
+ {consultation.vitalSigns || "---"}
+
+
+
+ {/* CHẨN ĐOÁN */}
+
+
+ CHUẨN ĐOÁN
+
+
+
+ {consultation.diagnosis || "---"}
+
+
+
+ {/* KẾ HOẠCH ĐIỀU TRỊ */}
+
+
+ KẾ HOẠCH ĐIỀU TRỊ
+
+
+
+ {consultation.treatmentPlan || "---"}
+
+
+
+ {/* GHI CHÚ BÁC SĨ */}
+
+
+ GHI CHÚ BÁC SĨ
+
+
+
+ {consultation.doctorNotes || "---"}
+
+
+
+ {/* NGƯỜI TẠO */}
+
+
+ NGƯỜI TẠO
+
+
+
+ {consultation.createdBy || "---"}
+
+
+
+ {/* TRẠNG THÁI */}
+
+
+
+
+
+ );
+};
+
+export default DetailConsultation;
diff --git a/src/components/page/consultation/DetailConsultation/index.ts b/src/components/page/consultation/DetailConsultation/index.ts
new file mode 100644
index 0000000..2038c73
--- /dev/null
+++ b/src/components/page/consultation/DetailConsultation/index.ts
@@ -0,0 +1 @@
+export { default } from "./DetailConsultation";
diff --git a/src/components/page/consultation/MainPageConsultation/MainPageConsultation.tsx b/src/components/page/consultation/MainPageConsultation/MainPageConsultation.tsx
new file mode 100644
index 0000000..b7f61f3
--- /dev/null
+++ b/src/components/page/consultation/MainPageConsultation/MainPageConsultation.tsx
@@ -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("");
+
+ const [openCancelDialog, setOpenCancelDialog] = useState(false);
+
+ const [openCompleteDialog, setOpenCompleteDialog] = useState(false);
+
+ /* =========================
+ GET LIST
+ ========================= */
+
+ const consultationQuery = useQuery({
+ queryKey: [QUERY_KEY.table_list_consultation, _page, _pageSize, _keyword],
+
+ queryFn: async () => {
+ const res = await httpRequest({
+ 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 (
+
+ {/* HEADER */}
+
+
Phiên khám
+
+ }
+ onClick={handleOpenCreate}
+ >
+ Thêm mới
+
+
+
+ {/* SEARCH */}
+
+
+ updateQuery("_keyword", value)}
+ placeholder="Tìm kiếm phiên khám..."
+ />
+
+
+
+ {/* TABLE */}
+
+ item.id}
+ data={consultationData}
+ column={[
+ {
+ fixedLeft: true,
+
+ title: "ID PHIÊN KHÁM",
+
+ render: (item: ConsultationItem) => (
+
+ {item.id}
+
+ ),
+ },
+
+ {
+ title: "MÃ PHIÊN KHÁM",
+
+ render: (item: ConsultationItem) => (
+ {item.sessionCode}
+ ),
+ },
+
+ {
+ title: "NGÀY KHÁM",
+
+ render: (item: ConsultationItem) => {item.visitDate},
+ },
+
+ {
+ title: "DẤU HIỆU",
+
+ render: (item: ConsultationItem) => (
+ {item.symptomsText || "---"}
+ ),
+ },
+
+ {
+ title: "CHUẨN ĐOÁN",
+
+ render: (item: ConsultationItem) => (
+ {item.diagnosis || "---"}
+ ),
+ },
+
+ {
+ title: "KẾ HOẠCH ĐIỀU TRỊ",
+
+ render: (item: ConsultationItem) => (
+ {item.treatmentPlan || "---"}
+ ),
+ },
+
+ {
+ title: "GHI CHÚ BÁC SĨ",
+
+ render: (item: ConsultationItem) => (
+ {item.doctorNotes || "---"}
+ ),
+ },
+
+ {
+ title: "TRẠNG THÁI",
+
+ render: (item: ConsultationItem) => (
+
+ ),
+ },
+
+ {
+ 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 (
+
+ {/* UPDATE */}
+ }
+ onClick={() => handleOpenUpdate(item)}
+ />
+
+ {/* CANCEL */}
+ handleOpenCancel(item)}
+ >
+ Hủy
+
+
+ {/* COMPLETE */}
+ handleOpenComplete(item)}
+ >
+ Hoàn thành
+
+
+ );
+ },
+ },
+ ]}
+ />
+
+
+ {/* CANCEL DIALOG */}
+
+
+ {/* COMPLETE DIALOG */}
+
+
+ {/* CREATE */}
+
+
+
+
+ {/* UPDATE */}
+
+
+
+
+ );
+};
+
+export default MainPageConsultation;
diff --git a/src/components/page/consultation/MainPageConsultation/index.ts b/src/components/page/consultation/MainPageConsultation/index.ts
new file mode 100644
index 0000000..5f96bbb
--- /dev/null
+++ b/src/components/page/consultation/MainPageConsultation/index.ts
@@ -0,0 +1 @@
+export { default } from "./MainPageConsultation";
diff --git a/src/components/page/consultation/PopupCreateConsultation/PopupCreateConsultation.tsx b/src/components/page/consultation/PopupCreateConsultation/PopupCreateConsultation.tsx
new file mode 100644
index 0000000..b0fe972
--- /dev/null
+++ b/src/components/page/consultation/PopupCreateConsultation/PopupCreateConsultation.tsx
@@ -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({
+ queryKey: [QUERY_KEY.table_list_patient],
+
+ queryFn: async () => {
+ const res = await httpRequest({
+ 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 (
+
+
+ {/* CLOSE */}
+
+
+ {/* HEADER */}
+
+
+ Thêm phiên khám
+
+
+
+ {/* BODY */}
+
+
+
+ Bệnh nhân
+ *
+
+ }
+ 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}
+ />
+
+
+
+ {/* FOOTER */}
+
+
+ Hủy bỏ
+
+
+
+ {createConsultationMutation.isPending
+ ? "Đang tạo..."
+ : "Tạo phiên khám"}
+
+
+
+
+ );
+};
+
+export default PopupCreateConsultation;
diff --git a/src/components/page/consultation/PopupCreateConsultation/index.ts b/src/components/page/consultation/PopupCreateConsultation/index.ts
new file mode 100644
index 0000000..2953914
--- /dev/null
+++ b/src/components/page/consultation/PopupCreateConsultation/index.ts
@@ -0,0 +1 @@
+export { default } from "./PopupCreateConsultation";
diff --git a/src/components/page/consultation/PopupUpdateConsultation/PopupUpdateConsultation.tsx b/src/components/page/consultation/PopupUpdateConsultation/PopupUpdateConsultation.tsx
new file mode 100644
index 0000000..72c28e6
--- /dev/null
+++ b/src/components/page/consultation/PopupUpdateConsultation/PopupUpdateConsultation.tsx
@@ -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(defaultForm);
+
+ const consulDetailQuery = useQuery({
+ queryKey: [QUERY_KEY.chi_tiet_phien_kham, id],
+ enabled: !!id,
+ queryFn: async () => {
+ const res = await httpRequest({
+ 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 (
+
+ Đang tải dữ liệu...
+
+ );
+ }
+
+ return (
+
+
+ {/* CLOSE */}
+
+
+ {/* HEADER */}
+
+
+ Chỉnh sửa phiên khám
+
+
+
+ {/* BODY */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* FOOTER */}
+
+
+ Hủy bỏ
+
+
+
+ {({ isDone }) => (
+
+ {updateConsultationMutation.isPending
+ ? "Đang cập nhật..."
+ : "Cập nhật"}
+
+ )}
+
+
+
+
+ );
+};
+
+export default PopupUpdateConsultation;
diff --git a/src/components/page/consultation/PopupUpdateConsultation/index.ts b/src/components/page/consultation/PopupUpdateConsultation/index.ts
new file mode 100644
index 0000000..89764a2
--- /dev/null
+++ b/src/components/page/consultation/PopupUpdateConsultation/index.ts
@@ -0,0 +1 @@
+export { default } from "./PopupUpdateConsultation";
diff --git a/src/components/page/patient/DetailPatient/DetailPatient.tsx b/src/components/page/patient/DetailPatient/DetailPatient.tsx
new file mode 100644
index 0000000..ba7aa78
--- /dev/null
+++ b/src/components/page/patient/DetailPatient/DetailPatient.tsx
@@ -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({
+ queryKey: [QUERY_KEY.chi_tiet_benh_nhan, patientId],
+
+ enabled: !!patientId,
+
+ queryFn: async () => {
+ const res = await httpRequest({
+ 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 Đang tải dữ liệu...
;
+ }
+
+ return (
+
+
+
+
+ router.back()}
+ className="cursor-pointer"
+ />{" "}
+ {patient?.fullName}
+
+
+
+
+
+ Mã bệnh nhân
+
+
+
+ {patient?.patientCode}
+
+
+
+
+
Họ tên
+
+
+ {patient?.fullName}
+
+
+
+
+
+ Mã định danh
+
+
+
+ {patient?.identificationNumber}
+
+
+
+
+
+ Ngày sinh
+
+
+
+ {patient?.dateOfBirth}
+
+
+
+
+
+ Giới tính
+
+
+
+ {patient?.gender}
+
+
+
+
+
+ Số điện thoại
+
+
+
+ {patient?.phone}
+
+
+
+
+
Email
+
+
+ {patient?.email}
+
+
+
+
+
Địa chỉ
+
+
+ {patient?.address || "-"}
+
+
+
+
+
+ Người liên hệ khẩn cấp
+
+
+
+ {patient?.emergencyContactName || "-"}
+
+
+
+
+
+ SĐT khẩn cấp
+
+
+
+ {patient?.emergencyContactPhone || "-"}
+
+
+
+
+
Dị ứng
+
+
+ {patient?.allergyNotes || "-"}
+
+
+
+
+
Bệnh nền
+
+
+ {patient?.chronicDiseaseNotes || "-"}
+
+
+
+
+
+
+ );
+};
+
+export default DetailPatient;
diff --git a/src/components/page/patient/DetailPatient/index.ts b/src/components/page/patient/DetailPatient/index.ts
new file mode 100644
index 0000000..e2ba4d7
--- /dev/null
+++ b/src/components/page/patient/DetailPatient/index.ts
@@ -0,0 +1 @@
+export { default } from "./DetailPatient";
diff --git a/src/components/page/patient/MainPagePatient/MainPagePatient.tsx b/src/components/page/patient/MainPagePatient/MainPagePatient.tsx
new file mode 100644
index 0000000..6b5a5a8
--- /dev/null
+++ b/src/components/page/patient/MainPagePatient/MainPagePatient.tsx
@@ -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("");
+
+ 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({
+ queryKey: [QUERY_KEY.table_list_patient, _page, _pageSize, _keyword],
+
+ queryFn: async () => {
+ const res = await httpRequest({
+ 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 (
+
+ {/* HEADER */}
+
+
Bệnh nhân
+
+ }
+ onClick={handleOpenCreate}
+ >
+ Tạo mới
+
+
+
+ {/* SEARCH */}
+
+
+ updateQuery("_keyword", value)}
+ placeholder="Tìm kiếm theo tên người dùng, ID"
+ />
+
+
+
+ {/* TABLE */}
+
+ item.id}
+ data={patientData}
+ column={[
+ {
+ fixedLeft: true,
+ title: "TÊN BỆNH NHÂN",
+
+ render: (item: PatientItem) => (
+
+ {item.fullName}
+
+ ),
+ },
+
+ {
+ title: "MÃ ĐỊNH DANH",
+
+ render: (item: PatientItem) => (
+ {item.identificationNumber}
+ ),
+ },
+
+ {
+ title: "SỐ ĐIỆN THOẠI",
+
+ render: (item: PatientItem) => {item.phone},
+ },
+
+ {
+ title: "EMAIL",
+
+ render: (item: PatientItem) => {item.email},
+ },
+
+ {
+ title: "GIỚI TÍNH",
+
+ render: (item: PatientItem) => {item.gender},
+ },
+
+ {
+ fixedRight: true,
+ title: "ACTION",
+
+ render: (item: PatientItem) => (
+
+ {/* UPDATE */}
+ }
+ onClick={() => handleOpenUpdate(item.id)}
+ />
+
+ {/* DELETE */}
+ }
+ onClick={() => handleOpenDelete(item.id)}
+ />
+
+ ),
+ },
+ ]}
+ />
+
+
+ {/* DELETE DIALOG */}
+ {}}
+ titleCancel="Hủy"
+ titleSubmit="Xác nhận"
+ />
+
+ {/* CREATE POPUP */}
+
+
+
+
+ {/* UPDATE POPUP */}
+
+
+
+
+ );
+};
+
+export default MainPagePatient;
diff --git a/src/components/page/patient/MainPagePatient/index.ts b/src/components/page/patient/MainPagePatient/index.ts
new file mode 100644
index 0000000..07c26e4
--- /dev/null
+++ b/src/components/page/patient/MainPagePatient/index.ts
@@ -0,0 +1 @@
+export { default } from "./MainPagePatient";
diff --git a/src/components/page/patient/PopupCreatePatient/PopupCreatePatient.tsx b/src/components/page/patient/PopupCreatePatient/PopupCreatePatient.tsx
new file mode 100644
index 0000000..8aa2ebb
--- /dev/null
+++ b/src/components/page/patient/PopupCreatePatient/PopupCreatePatient.tsx
@@ -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({
+ 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 (
+
+
+ {/* CLOSE */}
+
+
+ {/* HEADER */}
+
+
+ Thêm bệnh nhân
+
+
+
+ {/* BODY */}
+
+
+
+ Mã định danh
+ *
+
+ }
+ placeholder="Mã định danh"
+ name="identificationNumber"
+ type="text"
+ value={form.identificationNumber}
+ isRequired
+ />
+
+
+ Tên bệnh nhân
+ *
+
+ }
+ placeholder="Tên bệnh nhân"
+ name="fullName"
+ type="text"
+ value={form.fullName}
+ isRequired
+ />
+
+
+ Ngày sinh
+ *
+
+ }
+ placeholder="Ngày sinh"
+ name="dateOfBirth"
+ type="Date"
+ value={form.dateOfBirth}
+ isRequired
+ />
+
+
+ Số điện thoại
+ *
+
+ }
+ placeholder="Số điện thoại"
+ name="phone"
+ type="text"
+ value={form.phone}
+ isRequired
+ />
+
+
+ Email
+ *
+
+ }
+ placeholder="Email"
+ name="email"
+ type="text"
+ value={form.email}
+ isRequired
+ />
+
+
+
+
+
+ {/* FOOTER */}
+
+
+ Hủy bỏ
+
+
+
+ {({ isDone }) => (
+
+ {createPatientMutation.isPending ? "Đang tạo..." : "Tạo"}
+
+ )}
+
+
+
+
+ );
+};
+
+export default PopupCreatePatient;
diff --git a/src/components/page/patient/PopupCreatePatient/index.ts b/src/components/page/patient/PopupCreatePatient/index.ts
new file mode 100644
index 0000000..f4ca8b8
--- /dev/null
+++ b/src/components/page/patient/PopupCreatePatient/index.ts
@@ -0,0 +1 @@
+export { default } from "./PopupCreatePatient";
diff --git a/src/components/page/patient/PopupUpdatePatient/PopupUpdatePatient.tsx b/src/components/page/patient/PopupUpdatePatient/PopupUpdatePatient.tsx
new file mode 100644
index 0000000..2fe88f7
--- /dev/null
+++ b/src/components/page/patient/PopupUpdatePatient/PopupUpdatePatient.tsx
@@ -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(defaultForm);
+
+ const patientDetailQuery = useQuery({
+ queryKey: [QUERY_KEY.chi_tiet_benh_nhan, id],
+ enabled: !!id,
+ queryFn: async () => {
+ const res = await httpRequest({
+ 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 (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* CLOSE */}
+
+
+ {/* HEADER */}
+
+
+ Cập nhật thông tin bệnh nhân
+
+
+
+ {/* BODY */}
+
+
+
+ Tên bệnh nhân
+ *
+
+ }
+ placeholder="Tên bệnh nhân"
+ name="fullName"
+ type="text"
+ value={form.fullName}
+ isRequired
+ />
+
+ {/* GENDER */}
+
+
+
+ Số điện thoại
+ *
+
+ }
+ placeholder="Số điện thoại"
+ name="phone"
+ type="text"
+ value={form.phone}
+ isRequired
+ />
+
+
+ Email
+ *
+
+ }
+ placeholder="Email"
+ name="email"
+ type="text"
+ value={form.email}
+ isRequired
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* FOOTER */}
+
+
+ Hủy bỏ
+
+
+
+ {({ isDone }) => (
+
+ {updatePatientMutation.isPending
+ ? "Đang cập nhật..."
+ : "Cập nhật"}
+
+ )}
+
+
+
+
+ );
+};
+
+export default PopupUpdatePatient;
diff --git a/src/components/page/patient/PopupUpdatePatient/index.ts b/src/components/page/patient/PopupUpdatePatient/index.ts
new file mode 100644
index 0000000..b14b717
--- /dev/null
+++ b/src/components/page/patient/PopupUpdatePatient/index.ts
@@ -0,0 +1 @@
+export { default } from "./PopupUpdatePatient";
diff --git a/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx b/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx
new file mode 100644
index 0000000..9b9b72b
--- /dev/null
+++ b/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx
@@ -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([]);
+
+ const [form, setForm] = useState({
+ examinationSessionId: "",
+ notes: "",
+ prescriptionImg: "",
+ items: [defaultMedicine],
+ });
+
+ /**
+ * GET EXAMINATION SESSION
+ */
+ const examinationSessionQuery = useQuery({
+ queryKey: [QUERY_KEY.table_list_consultation],
+
+ queryFn: async () => {
+ const res = await httpRequest({
+ 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 (
+
+
+ {/* HEADER */}
+
+
+
+ Thêm đơn thuốc
+
+
+
+ Tạo mới đơn thuốc cho bệnh nhân
+
+
+
+
+ {createPrescriptionMutation.isPending
+ ? "Đang lưu..."
+ : "Lưu đơn thuốc"}
+
+
+
+
+ {/* GENERAL INFO */}
+
+
+ Thông tin chung
+
+
+
+ {/* EXAMINATION SESSION */}
+
+ Phiên khám
+ *
+
+ }
+ 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 */}
+
+ {
+ setImages(files);
+
+ const imagePath =
+ files?.[0]?.path ||
+ files?.[0]?.url ||
+ files?.[0]?.img ||
+ "";
+
+ setForm((prev) => ({
+ ...prev,
+ prescriptionImg: imagePath,
+ }));
+ }}
+ />
+
+
+ {/* NOTES */}
+
+
+
+
+ {/* MEDICINE LIST */}
+
+
+
+ Danh sách thuốc
+
+
+ }
+ onClick={handleAddMedicine}
+ >
+ Thêm thuốc
+
+
+
+
+
+ {form.items.map((medicine, index) => (
+
+ {/* TITLE */}
+
+
+ Thuốc #{index + 1}
+
+
+ {form.items.length > 1 && (
+
+ )}
+
+
+ {/* FORM */}
+
+
+ handleChangeMedicine(index, "medicineName", String(value))
+ }
+ />
+
+
+ handleChangeMedicine(index, "dosage", String(value))
+ }
+ />
+
+
+ handleChangeMedicine(index, "frequency", String(value))
+ }
+ />
+
+
+ handleChangeMedicine(index, "duration", String(value))
+ }
+ />
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+};
+
+export default CreatePrescription;
diff --git a/src/components/page/prescription/CreatePrescription/index.ts b/src/components/page/prescription/CreatePrescription/index.ts
new file mode 100644
index 0000000..7acf7f4
--- /dev/null
+++ b/src/components/page/prescription/CreatePrescription/index.ts
@@ -0,0 +1 @@
+export { default } from "./CreatePrescription";
diff --git a/src/components/page/prescription/DetailPrescription/DetailPrescription.tsx b/src/components/page/prescription/DetailPrescription/DetailPrescription.tsx
new file mode 100644
index 0000000..5aa0999
--- /dev/null
+++ b/src/components/page/prescription/DetailPrescription/DetailPrescription.tsx
@@ -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({
+ queryKey: [QUERY_KEY.chi_tiet_don_thuoc, PrescriptionId],
+
+ enabled: !!PrescriptionId,
+
+ queryFn: async () => {
+ const res = await httpRequest({
+ showMessageFailed: true,
+
+ http: prescriptionServices.detailPrescription(PrescriptionId),
+ });
+
+ return (
+ res || {
+ id: "",
+ examinationSessionId: "",
+ prescriptionCode: "",
+ prescriptionImg: null,
+ notes: "",
+ createdBy: "",
+ items: [],
+ }
+ );
+ },
+ });
+
+ const data = prescriptionDetailQuery.data;
+
+ return (
+
+
Chi tiết đơn thuốc
+
+ {/* LOADING */}
+ {prescriptionDetailQuery.isLoading &&
Đang tải...
}
+
+ {/* ERROR */}
+ {prescriptionDetailQuery.isError && (
+
Lỗi tải dữ liệu
+ )}
+
+ {/* CONTENT */}
+ {data && (
+
+
+ Mã đơn thuốc: {data.prescriptionCode}
+
+
+
+ Phiên khám: {data.examinationSessionId}
+
+
+
+ Ghi chú: {data.notes || "-"}
+
+
+ {/* IMAGE */}
+
+
Ảnh đơn thuốc:
+
+ {data.prescriptionImg ? (
+

+ ) : (
+
Không có ảnh
+ )}
+
+
+
+ {/* ITEMS */}
+
+
Danh sách thuốc:
+
+
+ {data.items?.length ? (
+ data.items.map((item, index) => (
+
+
+ Tên thuốc: {item.medicineName}
+
+
+ Liều lượng: {item.dosage}
+
+
+ Tần suất: {item.frequency}
+
+
+ Thời gian: {item.duration}
+
+
+ Hướng dẫn: {item.instructions}
+
+
+ ))
+ ) : (
+
Không có thuốc
+ )}
+
+
+
+ )}
+
+ );
+};
+
+export default DetailPrescription;
diff --git a/src/components/page/prescription/DetailPrescription/index.ts b/src/components/page/prescription/DetailPrescription/index.ts
new file mode 100644
index 0000000..5d46b06
--- /dev/null
+++ b/src/components/page/prescription/DetailPrescription/index.ts
@@ -0,0 +1 @@
+export { default } from "./DetailPrescription";
diff --git a/src/components/page/prescription/MainPrescription/MainPrescription.tsx b/src/components/page/prescription/MainPrescription/MainPrescription.tsx
new file mode 100644
index 0000000..b903e39
--- /dev/null
+++ b/src/components/page/prescription/MainPrescription/MainPrescription.tsx
@@ -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({
+ queryKey: [
+ QUERY_KEY.table_list_patientrescription,
+ _page,
+ _pageSize,
+ _keyword,
+ ],
+
+ queryFn: async () => {
+ const res = await httpRequest({
+ 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 (
+
+ {/* HEADER */}
+
+
Đơn thuốc
+
+ }
+ href={PATH.CREATE_PRESCRIPTION}
+ >
+ Tạo mới
+
+
+
+ {/* SEARCH */}
+
+
+ updateQuery("_keyword", value)}
+ placeholder="Tìm theo mã đơn thuốc"
+ />
+
+
+
+
+ item.id}
+ data={prescriptionQuery.data?.items || []}
+ column={[
+ // ================= ID / CODE =================
+ {
+ title: "MÃ ĐƠN THUỐC",
+ render: (item: any) => (
+
+ {item.prescriptionCode}
+
+ ),
+ },
+
+ // ================= SESSION =================
+ {
+ title: "PHIÊN KHÁM",
+ render: (item: any) => {item.examinationSessionId},
+ },
+
+ // ================= NOTES =================
+ {
+ title: "GHI CHÚ",
+ render: (item: any) => {item.notes || "-"},
+ },
+
+ // ================= IMAGE =================
+ {
+ title: "ẢNH ĐƠN THUỐC",
+ render: (item: any) =>
+ item.prescriptionImg ? (
+
+ ) : (
+ Không có ảnh
+ ),
+ },
+
+ // ================= ITEMS COUNT =================
+ {
+ title: "SỐ THUỐC",
+ render: (item: any) => {item.items?.length || 0},
+ },
+
+ // ================= ACTION =================
+ {
+ fixedRight: true,
+ title: "ACTION",
+
+ render: (item: any) => (
+
+ }
+ href={`${PATH.PRESCRIPTION}/update?_id=${item?.id}`}
+ />
+
+ ),
+ },
+ ]}
+ />
+
+
+ );
+};
+
+export default MainPrescription;
diff --git a/src/components/page/prescription/MainPrescription/index.tsx b/src/components/page/prescription/MainPrescription/index.tsx
new file mode 100644
index 0000000..46b4fe5
--- /dev/null
+++ b/src/components/page/prescription/MainPrescription/index.tsx
@@ -0,0 +1 @@
+export { default } from "./MainPrescription";
diff --git a/src/components/page/prescription/UpdatePrescription/UpdatePrescription.tsx b/src/components/page/prescription/UpdatePrescription/UpdatePrescription.tsx
new file mode 100644
index 0000000..68be0f2
--- /dev/null
+++ b/src/components/page/prescription/UpdatePrescription/UpdatePrescription.tsx
@@ -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({
+ queryKey: [QUERY_KEY.table_list_consultation],
+
+ queryFn: async () => {
+ const res = await httpRequest({
+ 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({
+ queryKey: [QUERY_KEY.chi_tiet_don_thuoc, id],
+
+ enabled: !!id,
+
+ queryFn: async () => {
+ const res = await httpRequest({
+ 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({
+ examinationSessionId: "",
+ notes: "",
+ prescriptionImg: "",
+ items: [defaultMedicine],
+ });
+
+ const [images, setImages] = useState([]);
+
+ 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 (
+
+
+ {/* HEADER */}
+
+
+
+ Cập nhật đơn thuốc
+
+
+
+ Chỉnh sửa đơn thuốc cho bệnh nhân
+
+
+
+
+ {isLoading ? "Đang cập nhật..." : "Lưu đơn thuốc"}
+
+
+
+
+ {/* GENERAL INFO */}
+
+
+ Thông tin chung
+
+
+
+ {/* EXAM SESSION */}
+
+ Phiên khám *
+
+ }
+ 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 */}
+ {
+ setImages(files);
+
+ const imagePath =
+ files?.[0]?.path || files?.[0]?.url || files?.[0]?.img || "";
+
+ setForm((prev) => ({
+ ...prev,
+ prescriptionImg: imagePath,
+ }));
+ }}
+ />
+
+ {/* NOTES */}
+
+
+
+ {/* MEDICINE LIST */}
+
+
+
+ Danh sách thuốc
+
+
+ }
+ onClick={handleAddMedicine}
+ >
+ Thêm thuốc
+
+
+
+
+
+ {form.items.map((medicine, index) => (
+
+ {/* TITLE */}
+
+
+ Thuốc #{index + 1}
+
+
+ {form.items.length > 1 && (
+
+ )}
+
+
+ {/* INPUTS */}
+
+
+ handleChangeMedicine(index, "medicineName", String(v))
+ }
+ />
+
+
+ handleChangeMedicine(index, "dosage", String(v))
+ }
+ />
+
+
+ handleChangeMedicine(index, "frequency", String(v))
+ }
+ />
+
+
+ handleChangeMedicine(index, "duration", String(v))
+ }
+ />
+
+
+ {/* INSTRUCTIONS */}
+
+
+
+ ))}
+
+
+
+
+ );
+};
+
+export default UpdatePrescription;
diff --git a/src/components/page/prescription/UpdatePrescription/index.ts b/src/components/page/prescription/UpdatePrescription/index.ts
new file mode 100644
index 0000000..dbbd9e4
--- /dev/null
+++ b/src/components/page/prescription/UpdatePrescription/index.ts
@@ -0,0 +1 @@
+export { default } from "./UpdatePrescription";
diff --git a/src/components/utils/FormCustom/components/InputForm/interface/index.ts b/src/components/utils/FormCustom/components/InputForm/interface/index.ts
index 7942454..ef3882d 100644
--- a/src/components/utils/FormCustom/components/InputForm/interface/index.ts
+++ b/src/components/utils/FormCustom/components/InputForm/interface/index.ts
@@ -31,5 +31,6 @@ export interface PropsInputForm {
isUppercase?: boolean;
onBlur?: () => void;
+
onChangeValue?: (val: string | number) => void;
}
diff --git a/src/components/utils/FormCustom/components/SelectForm/SelectForm.tsx b/src/components/utils/FormCustom/components/SelectForm/SelectForm.tsx
index 791eb36..1415731 100644
--- a/src/components/utils/FormCustom/components/SelectForm/SelectForm.tsx
+++ b/src/components/utils/FormCustom/components/SelectForm/SelectForm.tsx
@@ -81,7 +81,7 @@ function SelectForm({
onClick={() => !readOnly && setOpen((prev) => !prev)}
className={clsx(
"flex items-center justify-between px-4 h-12 border rounded-full cursor-pointer transition",
- "border-gray-300 bg-white hover:border-blue-600",
+ "border-gray-300 bg-gray-300 hover:border-blue-600",
open && "border-blue-600",
readOnly &&
"bg-gray-100 border-gray-200 cursor-not-allowed hover:border-gray-200",
diff --git a/src/components/utils/FormCustom/components/TextArea/TextArea.tsx b/src/components/utils/FormCustom/components/TextArea/TextArea.tsx
index 85f19d2..98f98d5 100644
--- a/src/components/utils/FormCustom/components/TextArea/TextArea.tsx
+++ b/src/components/utils/FormCustom/components/TextArea/TextArea.tsx
@@ -2,7 +2,6 @@
import React, { useContext, useEffect, useState } from "react";
import clsx from "clsx";
-
import { PropsTextArea } from "./interface";
import { ContextFormCustom, IContextFormCustom } from "../../contexts";
@@ -18,6 +17,7 @@ function TextArea({
min,
isRequired,
textRequired,
+ onChangeValue, // Nhận callback custom từ cha
}: PropsTextArea) {
const [isFocus, setIsFocus] = useState(false);
@@ -31,159 +31,175 @@ function TextArea({
setErrorText,
} = useContext>(ContextFormCustom);
+ // Lấy giá trị hiện tại (ưu tiên value từ props, sau đó là value trong context)
+ const currentValue = value ?? form?.[name] ?? "";
+
// =========================
- // VALIDATE
+ // VALIDATE LOGIC
// =========================
- const handleValidate = (): boolean => {
- const currentValue = `${form[name] || ""}`.trim();
+ const handleValidate = (inputValue?: string) => {
+ const finalValue = (inputValue ?? String(currentValue)).trim();
- if (isRequired && currentValue === "") return false;
+ if (isRequired && finalValue === "") {
+ return {
+ valid: false,
+ message: textRequired || "Vui lòng nhập trường này",
+ };
+ }
- if (min && currentValue.length < min) return false;
+ if (max && finalValue.length > max) {
+ return {
+ valid: false,
+ message: `Nhập tối đa ${max} kí tự`,
+ };
+ }
- if (max && currentValue.length > max) return false;
+ if (min && finalValue.length < min) {
+ return {
+ valid: false,
+ message: `Nhập tối thiểu ${min} kí tự`,
+ };
+ }
- return true;
+ return {
+ valid: true,
+ message: null,
+ };
};
- // =========================
- // ERROR MESSAGE
- // =========================
-
- const handleSetMessage = () => {
- const currentValue = `${form[name] || ""}`.trim();
+ // Cập nhật lỗi và trạng thái validate vào Context
+ const handleSetMessage = (inputValue?: string) => {
+ const result = handleValidate(inputValue);
setErrorText((prev) => ({
...prev,
- [name]: null,
+ [name]: result.message,
}));
- if (isRequired && currentValue === "") {
- return setErrorText((prev) => ({
- ...prev,
- [name]: textRequired || "Vui lòng nhập trường này",
- }));
- }
-
- if (max && currentValue.length > max) {
- return setErrorText((prev) => ({
- ...prev,
- [name]: `Nhập tối đa ${max} kí tự`,
- }));
- }
-
- if (min && currentValue.length < min) {
- return setErrorText((prev) => ({
- ...prev,
- [name]: `Nhập tối thiểu ${min} kí tự`,
- }));
- }
+ setValidate((prev) => ({
+ ...prev,
+ [name]: result.valid,
+ }));
};
// =========================
// EFFECTS
// =========================
+ // Chạy validate toàn cục khi Form yêu cầu (countValidate tăng)
useEffect(() => {
if (countValidate > 0) {
handleSetMessage();
}
}, [countValidate]);
+ // Cập nhật trạng thái valid thầm lặng vào context mỗi khi dữ liệu thay đổi
useEffect(() => {
+ const result = handleValidate();
setValidate((prev) => ({
...prev,
- [name]: handleValidate(),
+ [name]: result.valid,
}));
- }, [form[name]]);
+ }, [currentValue]);
// =========================
- // EVENTS
+ // EVENT HANDLERS
// =========================
- const handlerFocused = () => {
+ const handleFocus = () => {
setIsFocus(true);
-
+ // Xóa thông báo lỗi khi người dùng bắt đầu sửa lại
setErrorText((prev) => ({
...prev,
[name]: null,
}));
};
- const handlerBlur = () => {
+ const handleBlur = () => {
setIsFocus(false);
-
if (isBlur) {
handleSetMessage();
-
- setValidate((prev) => ({
- ...prev,
- [name]: handleValidate(),
- }));
}
};
const handleChange = (e: React.ChangeEvent) => {
- const { value } = e.target;
+ const inputValue = e.target.value;
+ // 1. Nếu có hàm xử lý riêng (như cập nhật vào mảng ở component cha)
+ if (onChangeValue) {
+ onChangeValue(inputValue);
+ return;
+ }
+
+ // 2. Mặc định: cập nhật vào state chung của FormCustom qua Context
setForm((prev: any) => ({
...prev,
- [name]: value,
+ [name]: inputValue,
}));
+
+ // Nếu form đã từng submit (countValidate > 0), thực hiện validate ngay lập tức (live validation)
+ if (countValidate > 0) {
+ handleSetMessage(inputValue);
+ }
};
+ // =========================
+ // RENDER
+ // =========================
+
return (
{/* LABEL */}
{label && (
)}
- {/* TEXTAREA */}
+ {/* TEXTAREA WRAPPER */}
- {/* ERROR */}
- {errorText[name] && (
-
{errorText[name]}
+ {/* ERROR MESSAGE */}
+ {errorText?.[name] && (
+
+ {errorText[name]}
+
)}
);
diff --git a/src/components/utils/FormCustom/components/TextArea/interface/index.ts b/src/components/utils/FormCustom/components/TextArea/interface/index.ts
index 03a58e7..599ffdb 100644
--- a/src/components/utils/FormCustom/components/TextArea/interface/index.ts
+++ b/src/components/utils/FormCustom/components/TextArea/interface/index.ts
@@ -14,4 +14,5 @@ export interface PropsTextArea {
min?: number;
isRequired?: boolean;
+ onChangeValue?: (val: string) => void;
}
diff --git a/src/components/utils/UploadMultipleFile/UploadMultipleFile.tsx b/src/components/utils/UploadMultipleFile/UploadMultipleFile.tsx
new file mode 100644
index 0000000..1fd594c
--- /dev/null
+++ b/src/components/utils/UploadMultipleFile/UploadMultipleFile.tsx
@@ -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) => {
+ 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 (
+
+ {/* LIST IMAGE */}
+ {images?.length > 0 && (
+
+ {images.map((image, index) => (
+
+
+
+ {/* DELETE BUTTON */}
+ {isDisableDelete && !image?.file && image?.img ? null : (
+
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"
+ >
+
+
+ )}
+
+ ))}
+
+ )}
+
+ {/* UPLOAD BUTTON */}
+
+
+
+ {/* NOTE */}
+
+
Upload file
+
+ File không vượt quá 50MB
+
+
+
+
+ );
+}
diff --git a/src/components/utils/UploadMultipleFile/index.ts b/src/components/utils/UploadMultipleFile/index.ts
new file mode 100644
index 0000000..7f24387
--- /dev/null
+++ b/src/components/utils/UploadMultipleFile/index.ts
@@ -0,0 +1 @@
+export { default } from "./UploadMultipleFile";
diff --git a/src/components/utils/UploadMultipleFile/interface/index.ts b/src/components/utils/UploadMultipleFile/interface/index.ts
new file mode 100644
index 0000000..adcf026
--- /dev/null
+++ b/src/components/utils/UploadMultipleFile/interface/index.ts
@@ -0,0 +1,5 @@
+export interface PropsUploadMultipleFile {
+ images: any[];
+ setImages: (any: any) => void;
+ isDisableDelete?: boolean;
+}
diff --git a/src/constant/config/enum.ts b/src/constant/config/enum.ts
index e71b2b3..e09d018 100644
--- a/src/constant/config/enum.ts
+++ b/src/constant/config/enum.ts
@@ -1,3 +1,26 @@
+export enum QUERY_KEY {
+ table_list_patient,
+ table_list_consultation,
+ table_list_patientrescription,
+
+ chi_tiet_benh_nhan,
+ chi_tiet_don_thuoc,
+ chi_tiet_phien_kham,
+}
+
+export enum TYPE_STATUS {
+ Draft = 1,
+ InProgress = 2,
+ Completed = 3,
+ Cancelled = 4,
+}
+
+export enum TYPE_GENDER {
+ MALE = "male",
+ FEMALE = "female",
+ OTHER = "other",
+}
+
export enum TYPE_DATE {
ALL = -1,
TODAY = 1,
diff --git a/src/constant/config/index.ts b/src/constant/config/index.ts
index 0d7d9a8..afb4da4 100644
--- a/src/constant/config/index.ts
+++ b/src/constant/config/index.ts
@@ -6,6 +6,7 @@ export enum PATH {
PATIENT = "/patient",
CONSULTATION = "/consultation",
PRESCRIPTION = "/prescription",
+ CREATE_PRESCRIPTION = "/prescription/create",
LOGIN = "/auth/login",
REGISTER = "/auth/register",
@@ -26,17 +27,17 @@ export const Menus: {
}[] = [
{
group: [
- {
- title: "Dashboard",
- icon: CircleGauge,
- path: PATH.DASHBOARD || PATH.HOME,
- pathActive: PATH.DASHBOARD,
- },
+ // {
+ // title: "Dashboard",
+ // icon: CircleGauge,
+ // path: PATH.DASHBOARD || PATH.HOME,
+ // pathActive: PATH.DASHBOARD,
+ // },
{
title: "Bệnh nhân",
icon: Users,
- path: PATH.PATIENT,
- pathActive: PATH.PATIENT,
+ path: PATH.PATIENT || PATH.HOME,
+ pathActive: PATH.PATIENT || PATH.HOME,
},
{
title: "Phiên khám",
diff --git a/src/redux/reducer/auth.ts b/src/redux/reducer/auth.ts
index 06f0944..09961f3 100644
--- a/src/redux/reducer/auth.ts
+++ b/src/redux/reducer/auth.ts
@@ -3,6 +3,7 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit";
export interface IDataLoginStorage {
usernameStorage: string;
passwordStorage: string;
+ deviceIdStorage: string;
}
export interface AuthState {
diff --git a/src/services/authServices.ts b/src/services/authServices.ts
index 989dc00..4977eef 100644
--- a/src/services/authServices.ts
+++ b/src/services/authServices.ts
@@ -5,9 +5,7 @@ const authServices = {
data: {
username: string;
password: string;
- ip: string;
- address: string;
- type: number;
+ deviceId: string;
},
tokenAxios?: any,
) => {
@@ -15,11 +13,38 @@ const authServices = {
cancelToken: tokenAxios,
});
},
- logout: (data: void, tokenAxios?: any) => {
- return axiosClient.post(`/Auth/logout`, data, {
+ register: (
+ data: {
+ username: string;
+ password: string;
+ fullName: string;
+ email: string;
+ },
+ tokenAxios?: any,
+ ) => {
+ return axiosClient.post(`/Auth/register`, data, {
cancelToken: tokenAxios,
});
},
+ refresh: (
+ data: {
+ identifier: string;
+ },
+ tokenAxios?: any,
+ ) => {
+ return axiosClient.post(`/Auth/refresh`, data, {
+ cancelToken: tokenAxios,
+ });
+ },
+ logout: (tokenAxios?: any) => {
+ return axiosClient.post(
+ `/Auth/logout`,
+ {},
+ {
+ cancelToken: tokenAxios,
+ },
+ );
+ },
};
export default authServices;
diff --git a/src/services/consultationServices.ts b/src/services/consultationServices.ts
new file mode 100644
index 0000000..59d3448
--- /dev/null
+++ b/src/services/consultationServices.ts
@@ -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(`/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(`/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;
diff --git a/src/services/index.ts b/src/services/index.ts
index d81af29..6077f94 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -1,7 +1,6 @@
import axios, {
AxiosError,
AxiosInstance,
- AxiosResponse,
InternalAxiosRequestConfig,
} from "axios";
@@ -9,17 +8,22 @@ import { delay } from "@/common/funcs/delay";
import { getKeyCert } from "@/common/funcs/optionConvert";
import { toastInfo, toastSuccess, toastWarn } from "@/common/funcs/toast";
-import { logout } from "@/redux/reducer/auth";
+import { logout, setToken } from "@/redux/reducer/auth";
import { setInfoUser } from "@/redux/reducer/user";
import { store } from "@/redux/store";
+/* =========================================================
+ * TYPES
+ * =======================================================*/
+
interface ApiError {
- code: number;
+ code: string | number;
message: string;
}
-interface ApiResponse {
- error: ApiError;
+export interface ApiResponse {
+ success?: boolean;
+ error?: ApiError | null;
data?: T;
items?: T[];
pagination?: unknown;
@@ -42,16 +46,20 @@ interface HttpRequestOptions {
isData?: boolean;
}
-const axiosClient = axios.create({
- headers: {
- "content-type": "application/json",
- },
+/* =========================================================
+ * AXIOS CLIENT
+ * =======================================================*/
- baseURL: "/api-proxy",
+const axiosClient = axios.create({
+ baseURL: process.env.NEXT_PUBLIC_API_URL_DEV,
timeout: 15000,
timeoutErrorMessage: "Timeout error request",
+
+ headers: {
+ "content-type": "application/json",
+ },
}) as AxiosInstance & {
get(url: string, config?: any): Promise>;
@@ -70,16 +78,120 @@ const axiosClient = axios.create({
delete(url: string, config?: any): Promise>;
};
+/* =========================================================
+ * 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 | null = null;
+
+const refreshAccessToken = async (): Promise => {
+ if (refreshPromise) {
+ return refreshPromise;
+ }
+
+ refreshPromise = (async () => {
+ try {
+ const state = store.getState();
+
+ const refreshToken = state.user?.infoUser?.refreshToken || null;
+
+ if (!refreshToken) {
+ return null;
+ }
+
+ const response = await refreshClient.post<{
+ token?: string;
+ accessToken?: string;
+ refreshToken?: string;
+ }>("/Auth/refresh", {
+ identifier: refreshToken,
+ });
+
+ const responseData = response.data;
+
+ const newToken = responseData?.token || responseData?.accessToken;
+
+ if (!newToken) {
+ return null;
+ }
+
+ /* UPDATE ACCESS TOKEN */
+ store.dispatch(setToken(newToken));
+
+ /* UPDATE USER INFO */
+ const prev = store.getState().user.infoUser;
+
+ store.dispatch(
+ setInfoUser({
+ accessToken: newToken,
+
+ refreshToken: responseData?.refreshToken || prev?.refreshToken || "",
+
+ accessExpiresAt: prev?.accessExpiresAt || "",
+
+ refreshExpiresAt: prev?.refreshExpiresAt || "",
+
+ avatar: prev?.avatar || "",
+
+ fullname: prev?.fullname || "",
+ }),
+ );
+
+ return newToken;
+ } catch (error) {
+ console.error("Refresh token failed:", error);
+
+ return null;
+ } finally {
+ refreshPromise = null;
+ }
+ })();
+
+ return refreshPromise;
+};
+
+/* =========================================================
+ * REQUEST INTERCEPTOR
+ * =======================================================*/
+
axiosClient.interceptors.request.use(
async (
config: InternalAxiosRequestConfig,
): Promise => {
const token = store.getState().auth.token;
+ config.headers = config.headers || {};
+
+ /* AUTHORIZATION */
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
+ /* AUTO ADD CERT */
if (!(config.data instanceof FormData)) {
config.data = {
...getKeyCert(),
@@ -91,106 +203,131 @@ axiosClient.interceptors.request.use(
},
);
+/* =========================================================
+ * RESPONSE INTERCEPTOR
+ * =======================================================*/
+
axiosClient.interceptors.response.use(
(response) => {
- return response.data;
+ return (
+ response.data || {
+ success: false,
+ error: {
+ code: "UNKNOWN_ERROR",
+ message: "Unknown error",
+ },
+ }
+ );
},
- (error: AxiosError) => {
- return Promise.reject(error.response?.data || error);
+ async (error: AxiosError) => {
+ const status = error.response?.status;
+
+ const data = error.response?.data;
+
+ const config = error.config as InternalAxiosRequestConfig & {
+ _retry?: boolean;
+ };
+
+ /* =====================================================
+ * AUTO REFRESH TOKEN
+ * ===================================================*/
+
+ if (status === 401 && !config._retry) {
+ config._retry = true;
+
+ const newToken = await refreshAccessToken();
+
+ if (newToken) {
+ config.headers.Authorization = `Bearer ${newToken}`;
+
+ return axiosClient(config);
+ }
+
+ /* REFRESH FAILED */
+ store.dispatch(logout());
+
+ store.dispatch(setInfoUser(null));
+ }
+
+ /* =====================================================
+ * HANDLE EMPTY RESPONSE
+ * ===================================================*/
+
+ if ((status === 401 || status === 403) && !data) {
+ return Promise.reject({
+ success: false,
+
+ error: {
+ code: status,
+ message: error.response?.statusText || "Unauthorized",
+ },
+ });
+ }
+
+ return Promise.reject(
+ data || {
+ success: false,
+ error: {
+ code: "SERVER_ERROR",
+ message: "Có lỗi xảy ra",
+ },
+ },
+ );
},
);
export default axiosClient;
+/* =========================================================
+ * HTTP REQUEST
+ * =======================================================*/
+
export const httpRequest = async ({
http,
-
setLoading,
-
msgSuccess = "Thành công",
-
showMessageSuccess = false,
showMessageFailed = false,
-
onError,
-
- isDropdown = false,
- isPagination = false,
- isData = false,
-}: HttpRequestOptions) => {
+}: HttpRequestOptions): Promise => {
try {
setLoading?.(true);
- await delay(500);
+ await delay(300);
const res = await http;
- if (res.error.code === 0) {
+ const safeRes =
+ res ??
+ ({
+ success: false,
+ error: {
+ code: "UNKNOWN_ERROR",
+ message: "Unknown error",
+ },
+ } as ApiResponse);
+
+ // ================= SUCCESS =================
+ if (safeRes.success) {
if (showMessageSuccess) {
- toastSuccess({
- msg: msgSuccess || res.error.message,
- });
+ toastSuccess({ msg: msgSuccess });
}
- if (isDropdown) {
- return res.items || [];
- }
-
- if (isPagination) {
- return {
- ...res.data,
- items: res.items || [],
- pagination: res.pagination,
- };
- }
-
- if (isData) {
- return res;
- }
-
- return res.data || true;
+ return safeRes.data as T;
}
+ // ================= FAIL =================
+ const error = {
+ message: safeRes.error?.message || "Có lỗi xảy ra",
+ code: safeRes.error?.code,
+ };
+
onError?.();
- throw new Error(res.error.message);
- } catch (error: unknown) {
- const err = error as AxiosError | Error;
-
- const errorData = (err as AxiosError)?.response?.data;
-
- if (errorData?.error?.code === 401 || errorData?.error?.code === 403) {
- store.dispatch(logout());
-
- store.dispatch(setInfoUser(null));
-
- if (showMessageFailed) {
- toastWarn({
- msg: errorData?.error?.message || "Có lỗi đã xảy ra!",
- });
- }
- } else if (err instanceof Error) {
- if (err.message === "ERR_NETWORK" || err.message === "ECONNABORTED") {
- if (showMessageFailed) {
- toastInfo({
- msg: "Kiểm tra kết nối internet",
- });
- }
- } else {
- if (showMessageFailed) {
- toastWarn({
- msg: err.message || "Có lỗi đã xảy ra!",
- });
- }
- }
- } else {
- if (showMessageFailed) {
- toastWarn({
- msg: "Có lỗi đã xảy ra!",
- });
- }
- }
+ throw error; // 👈 QUAN TRỌNG
+ } catch (error) {
+ throw error; // 👈 QUAN TRỌNG: KHÔNG NUỐT
} finally {
setLoading?.(false);
}
diff --git a/src/services/patientServices.ts b/src/services/patientServices.ts
new file mode 100644
index 0000000..0bff7e9
--- /dev/null
+++ b/src/services/patientServices.ts
@@ -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(`/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(`/Patient/${id}`, {
+ cancelToken: tokenAxios,
+ });
+ },
+
+ deletePatient: (id: string, tokenAxios?: any) => {
+ return axiosClient.delete(`/Patient/${id}`, {
+ cancelToken: tokenAxios,
+ });
+ },
+};
+
+export default patientServices;
diff --git a/src/services/prescriptionServices.ts b/src/services/prescriptionServices.ts
new file mode 100644
index 0000000..aebd9a7
--- /dev/null
+++ b/src/services/prescriptionServices.ts
@@ -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(`/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(`/Prescription/${id}`, {
+ cancelToken: tokenAxios,
+ });
+ },
+};
+
+export default prescriptionServices;
diff --git a/src/services/userServices.ts b/src/services/userServices.ts
new file mode 100644
index 0000000..5c38277
--- /dev/null
+++ b/src/services/userServices.ts
@@ -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;