feat:update web
This commit is contained in:
+5
-8
@@ -57,13 +57,9 @@ export default function FormPassword() {
|
||||
http: authServices.login({
|
||||
username: dataLoginStorage?.usernameStorage || "",
|
||||
|
||||
password: md5(
|
||||
`${form.password}_${process.env.NEXT_PUBLIC_KEY_PASSWORD}`,
|
||||
),
|
||||
password: md5(`${form.password}_${process.env.NEXT_PUBLIC_KEY_PASS}`),
|
||||
|
||||
ip: "",
|
||||
address: "",
|
||||
type: 0,
|
||||
deviceId: dataLoginStorage?.deviceIdStorage || "web-browser",
|
||||
}),
|
||||
});
|
||||
},
|
||||
@@ -73,11 +69,11 @@ export default function FormPassword() {
|
||||
|
||||
store.dispatch(setStateLogin(true));
|
||||
|
||||
store.dispatch(setToken(data.accessToken));
|
||||
store.dispatch(setToken(data.token));
|
||||
|
||||
store.dispatch(
|
||||
setInfoUser({
|
||||
accessToken: data?.accessToken || "",
|
||||
accessToken: data?.token || "",
|
||||
refreshToken: data?.refreshToken || "",
|
||||
accessExpiresAt: data?.accessExpiresAt || "",
|
||||
refreshExpiresAt: data?.refreshExpiresAt || "",
|
||||
@@ -92,6 +88,7 @@ export default function FormPassword() {
|
||||
usernameStorage: dataLoginStorage?.usernameStorage || "",
|
||||
|
||||
passwordStorage: form.password,
|
||||
deviceIdStorage: dataLoginStorage?.deviceIdStorage || "web-browser",
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -26,7 +26,7 @@ import { PATH } from "@/constant/config";
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import CustomLoading from "@/components/customs/custom-loading";
|
||||
// import FormCustom, { InputForm } from "@/components/common/FormCustom";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
|
||||
export default function MainLogin() {
|
||||
const router = useRouter();
|
||||
@@ -39,6 +39,8 @@ export default function MainLogin() {
|
||||
username: isRememberPassword ? dataLoginStorage?.usernameStorage || "" : "",
|
||||
|
||||
password: isRememberPassword ? dataLoginStorage?.passwordStorage || "" : "",
|
||||
|
||||
deviceId: isRememberPassword ? dataLoginStorage?.deviceIdStorage || "" : "",
|
||||
});
|
||||
|
||||
const loginMutation = useMutation({
|
||||
@@ -50,9 +52,7 @@ export default function MainLogin() {
|
||||
http: authServices.login({
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
ip: "",
|
||||
address: "",
|
||||
type: 0,
|
||||
deviceId: form.deviceId,
|
||||
}),
|
||||
});
|
||||
},
|
||||
@@ -62,11 +62,11 @@ export default function MainLogin() {
|
||||
|
||||
store.dispatch(setStateLogin(true));
|
||||
|
||||
store.dispatch(setToken(data.accessToken));
|
||||
store.dispatch(setToken(data.token));
|
||||
|
||||
store.dispatch(
|
||||
setInfoUser({
|
||||
accessToken: data?.accessToken || "",
|
||||
accessToken: data?.token || "",
|
||||
refreshToken: data?.refreshToken || "",
|
||||
accessExpiresAt: data?.accessExpiresAt || "",
|
||||
refreshExpiresAt: data?.refreshExpiresAt || "",
|
||||
@@ -80,6 +80,7 @@ export default function MainLogin() {
|
||||
setDataLoginStorage({
|
||||
usernameStorage: form.username,
|
||||
passwordStorage: form.password,
|
||||
deviceIdStorage: form.deviceId,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
@@ -141,6 +142,25 @@ export default function MainLogin() {
|
||||
icon={<User size={22} />}
|
||||
/>
|
||||
|
||||
<div className="mt-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
deviceId
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="deviceId"
|
||||
type="text"
|
||||
name="deviceId"
|
||||
onClean
|
||||
isRequired
|
||||
isBlur
|
||||
showDone
|
||||
icon={<ShieldPlus size={22} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* PASSWORD */}
|
||||
<div className="mt-5">
|
||||
<InputForm
|
||||
@@ -202,42 +222,42 @@ export default function MainLogin() {
|
||||
|
||||
{/* BUTTONS */}
|
||||
<div className="mt-6">
|
||||
{/* LOGIN */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loginMutation.isPending}
|
||||
className="
|
||||
flex
|
||||
h-[52px]
|
||||
w-full
|
||||
items-center
|
||||
justify-center
|
||||
rounded-full
|
||||
bg-[#0011AB]
|
||||
px-6
|
||||
text-white
|
||||
font-bold
|
||||
transition-all
|
||||
hover:opacity-90
|
||||
disabled:cursor-not-allowed
|
||||
disabled:opacity-50
|
||||
"
|
||||
>
|
||||
Đăng nhập
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
{/* LOGIN */}
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
disabled={loginMutation.isPending}
|
||||
rounded="full"
|
||||
type="submit"
|
||||
className="font-bold text-2xl disabled:cursor-not-allowed
|
||||
disabled:opacity-50 h-[52px]"
|
||||
>
|
||||
Đăng nhập
|
||||
</CustomButton>
|
||||
{/* REGISTER */}
|
||||
<CustomButton
|
||||
variant="default"
|
||||
rounded="full"
|
||||
className="font-bold text-2xl disabled:cursor-not-allowed
|
||||
disabled:opacity-50 h-[52px]"
|
||||
onClick={() => router.push(PATH.REGISTER)}
|
||||
>
|
||||
Đăng ký
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{/* LINE */}
|
||||
<div
|
||||
{/* <div
|
||||
className="
|
||||
my-5
|
||||
h-[1px]
|
||||
w-full
|
||||
bg-[#E5E5E5]
|
||||
"
|
||||
/>
|
||||
/> */}
|
||||
|
||||
{/* FORGOT PASSWORD */}
|
||||
<button
|
||||
{/* <button
|
||||
type="button"
|
||||
onClick={() => router.push(PATH.FORGOT_PASSWORD)}
|
||||
className="
|
||||
@@ -260,7 +280,7 @@ export default function MainLogin() {
|
||||
>
|
||||
<LockKeyhole size={22} />
|
||||
Quên mật khẩu
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { ShieldPlus, User, Mail, BadgeInfo } from "lucide-react";
|
||||
|
||||
import authServices from "@/services/authServices";
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import { PATH } from "@/constant/config";
|
||||
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import CustomLoading from "@/components/customs/custom-loading";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
|
||||
export default function MainRegister() {
|
||||
const router = useRouter();
|
||||
|
||||
const [form, setForm] = useState({
|
||||
username: "",
|
||||
password: "",
|
||||
fullName: "",
|
||||
email: "",
|
||||
});
|
||||
|
||||
const registerMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Đăng ký thành công!",
|
||||
http: authServices.register({
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
fullName: form.fullName,
|
||||
email: form.email,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess(data: any) {
|
||||
if (!data) return;
|
||||
|
||||
router.replace(PATH.LOGIN);
|
||||
},
|
||||
});
|
||||
|
||||
const handleRegister = () => {
|
||||
registerMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<CustomLoading loading={registerMutation.isPending} />
|
||||
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleRegister}>
|
||||
{/* TITLE */}
|
||||
<h3
|
||||
className="
|
||||
text-[34px]
|
||||
font-semibold
|
||||
text-[#1A1B2D]
|
||||
"
|
||||
>
|
||||
Đăng ký
|
||||
</h3>
|
||||
|
||||
<p
|
||||
className="
|
||||
mt-1
|
||||
text-[14px]
|
||||
font-medium
|
||||
text-[#6F767E]
|
||||
"
|
||||
>
|
||||
Tạo tài khoản mới cho hệ thống
|
||||
</p>
|
||||
|
||||
{/* FORM */}
|
||||
<div className="mt-6">
|
||||
{/* FULL NAME */}
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Họ và tên
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Nhập họ và tên"
|
||||
type="text"
|
||||
name="fullName"
|
||||
onClean
|
||||
isRequired
|
||||
isBlur
|
||||
showDone
|
||||
icon={<BadgeInfo size={22} />}
|
||||
/>
|
||||
|
||||
{/* EMAIL */}
|
||||
<div className="mt-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Email
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Nhập email"
|
||||
type="email"
|
||||
name="email"
|
||||
onClean
|
||||
isRequired
|
||||
isBlur
|
||||
showDone
|
||||
icon={<Mail size={22} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* USERNAME */}
|
||||
<div className="mt-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Tài khoản
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Nhập tài khoản"
|
||||
type="text"
|
||||
name="username"
|
||||
onClean
|
||||
isRequired
|
||||
isBlur
|
||||
showDone
|
||||
icon={<User size={22} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* PASSWORD */}
|
||||
<div className="mt-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Mật khẩu
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Nhập mật khẩu"
|
||||
type="password"
|
||||
name="password"
|
||||
onClean
|
||||
isRequired
|
||||
isBlur
|
||||
showDone
|
||||
icon={<ShieldPlus size={22} />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* BUTTON */}
|
||||
<div className="mt-6 flex gap-2">
|
||||
{/* REGISTER */}
|
||||
<CustomButton
|
||||
type="submit"
|
||||
variant="midnightBlue"
|
||||
disabled={registerMutation.isPending}
|
||||
rounded="full"
|
||||
className="
|
||||
h-[52px]
|
||||
font-bold
|
||||
text-2xl
|
||||
disabled:cursor-not-allowed
|
||||
disabled:opacity-50
|
||||
"
|
||||
>
|
||||
Đăng ký
|
||||
</CustomButton>
|
||||
|
||||
{/* LOGIN */}
|
||||
<CustomButton
|
||||
type="button"
|
||||
variant="default"
|
||||
rounded="full"
|
||||
className="
|
||||
h-[52px]
|
||||
font-bold
|
||||
text-2xl
|
||||
"
|
||||
onClick={() => router.push(PATH.LOGIN)}
|
||||
>
|
||||
Đăng nhập
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./MainRegister";
|
||||
@@ -0,0 +1,237 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
|
||||
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationDetail,
|
||||
} from "@/services/consultationServices";
|
||||
|
||||
import StateActive from "@/components/customs/StateActive";
|
||||
|
||||
const DetailConsultation = () => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const consultationId = params?.id as string;
|
||||
|
||||
const consulDetailQuery = useQuery<ConsultationDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId],
|
||||
|
||||
enabled: !!consultationId,
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationDetail>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: consultationServices.detailConsultations(consultationId),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
patientId: "",
|
||||
sessionCode: "",
|
||||
visitDate: "",
|
||||
status: 1,
|
||||
chiefComplaint: null,
|
||||
symptomsText: null,
|
||||
vitalSigns: null,
|
||||
diagnosis: null,
|
||||
treatmentPlan: null,
|
||||
doctorNotes: null,
|
||||
createdBy: "",
|
||||
isDeleted: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const consultation = consulDetailQuery.data;
|
||||
|
||||
if (consulDetailQuery.isLoading) {
|
||||
return <div>Đang tải dữ liệu...</div>;
|
||||
}
|
||||
|
||||
if (!consultation) {
|
||||
return <div>Không tìm thấy dữ liệu phiên khám</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center gap-3">
|
||||
<ArrowLeft
|
||||
onClick={() => router.back()}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
|
||||
<h2 className="text-[28px] font-semibold text-[#111827]">
|
||||
Chi tiết phiên khám
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* CONTENT */}
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
{/* ID PHIÊN KHÁM */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
ID PHIÊN KHÁM
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.id}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* MÃ PHIÊN KHÁM */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
MÃ PHIÊN KHÁM
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.sessionCode}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* NGÀY GIỜ KHÁM */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
NGÀY, GIỜ KHÁM
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.visitDate}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* TRIỆU CHỨNG */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">DẤU HIỆU</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.symptomsText || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* KHIẾU NẠI CHÍNH */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
KHIẾU NẠI CHÍNH
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.chiefComplaint || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* DẤU HIỆU SINH TỒN */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
DẤU HIỆU SINH TỒN
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.vitalSigns || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CHẨN ĐOÁN */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
CHUẨN ĐOÁN
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.diagnosis || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* KẾ HOẠCH ĐIỀU TRỊ */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
KẾ HOẠCH ĐIỀU TRỊ
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.treatmentPlan || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* GHI CHÚ BÁC SĨ */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
GHI CHÚ BÁC SĨ
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.doctorNotes || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* NGƯỜI TẠO */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
NGƯỜI TẠO
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.createdBy || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* TRẠNG THÁI */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
TRẠNG THÁI
|
||||
</p>
|
||||
|
||||
<StateActive
|
||||
stateActive={consultation.status}
|
||||
listState={[
|
||||
{
|
||||
state: TYPE_STATUS.Draft,
|
||||
text: "Đang chờ",
|
||||
textColor: "#000",
|
||||
backgroundColor: "#FFE4C4",
|
||||
},
|
||||
|
||||
{
|
||||
state: TYPE_STATUS.InProgress,
|
||||
text: "Đang thực hiện",
|
||||
textColor: "#000",
|
||||
backgroundColor: "#ff3300",
|
||||
},
|
||||
|
||||
{
|
||||
state: TYPE_STATUS.Completed,
|
||||
text: "Hoàn thành",
|
||||
textColor: "#000",
|
||||
backgroundColor: "#3d69eb",
|
||||
},
|
||||
{
|
||||
state: TYPE_STATUS.Cancelled,
|
||||
text: "Đã hủy",
|
||||
textColor: "#000",
|
||||
backgroundColor: "#cccc",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailConsultation;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./DetailConsultation";
|
||||
@@ -0,0 +1,481 @@
|
||||
"use client";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import CustomDialog from "@/components/customs/custom-dialog";
|
||||
import CustomPopup from "@/components/customs/custom-popup";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import Search from "@/components/customs/custom-search";
|
||||
import StateActive from "@/components/customs/StateActive";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
|
||||
import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationItem,
|
||||
ConsultationResponse,
|
||||
} from "@/services/consultationServices";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { Pencil, ClipboardPlus } from "lucide-react";
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
import PopupUpdateConsultation from "../PopupUpdateConsultation";
|
||||
import PopupCreateConsultation from "../PopupCreateConsultation";
|
||||
|
||||
const MainPageConsultation = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const pathname = usePathname();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
/* =========================
|
||||
QUERY PARAMS
|
||||
========================= */
|
||||
|
||||
const _page = searchParams.get("_page");
|
||||
|
||||
const _pageSize = searchParams.get("_pageSize");
|
||||
|
||||
const _keyword = searchParams.get("_keyword");
|
||||
|
||||
const _create = searchParams.get("_create");
|
||||
|
||||
const _update = searchParams.get("_update");
|
||||
|
||||
/* =========================
|
||||
STATE
|
||||
========================= */
|
||||
|
||||
const [selectedConsultationId, setSelectedConsultationId] =
|
||||
useState<string>("");
|
||||
|
||||
const [openCancelDialog, setOpenCancelDialog] = useState(false);
|
||||
|
||||
const [openCompleteDialog, setOpenCompleteDialog] = useState(false);
|
||||
|
||||
/* =========================
|
||||
GET LIST
|
||||
========================= */
|
||||
|
||||
const consultationQuery = useQuery<ConsultationResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_consultation, _page, _pageSize, _keyword],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: consultationServices.getConsultations({
|
||||
Page: Number(_page || 1),
|
||||
|
||||
PageSize: Number(_pageSize || 10),
|
||||
|
||||
Search: _keyword || "",
|
||||
|
||||
SortBy: "id",
|
||||
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
DATA
|
||||
========================= */
|
||||
|
||||
const consultationData = useMemo(() => {
|
||||
return consultationQuery.data?.items || [];
|
||||
}, [consultationQuery.data]);
|
||||
|
||||
/* =========================
|
||||
UPDATE QUERY
|
||||
========================= */
|
||||
|
||||
const updateQuery = (key: string, value: string | number) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (!value || value === "" || (key === "_page" && value === 1)) {
|
||||
params.delete(key);
|
||||
} else {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
|
||||
if (key === "_pageSize") {
|
||||
params.delete("_page");
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
/* =========================
|
||||
CREATE POPUP
|
||||
========================= */
|
||||
|
||||
const handleOpenCreate = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.set("_create", "true");
|
||||
|
||||
router.push(`?${params.toString()}`);
|
||||
};
|
||||
|
||||
/* =========================
|
||||
UPDATE POPUP
|
||||
========================= */
|
||||
|
||||
const handleOpenUpdate = (item: ConsultationItem) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.set("_update", item.id);
|
||||
|
||||
router.push(`?${params.toString()}`);
|
||||
};
|
||||
|
||||
/* =========================
|
||||
CLOSE POPUP
|
||||
========================= */
|
||||
|
||||
const handleClosePopup = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.delete("_create");
|
||||
|
||||
params.delete("_update");
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
/* =========================
|
||||
DIALOG
|
||||
========================= */
|
||||
|
||||
const handleOpenCancel = (item: ConsultationItem) => {
|
||||
setSelectedConsultationId(item.id);
|
||||
|
||||
setOpenCancelDialog(true);
|
||||
};
|
||||
|
||||
const handleOpenComplete = (item: ConsultationItem) => {
|
||||
setSelectedConsultationId(item.id);
|
||||
|
||||
setOpenCompleteDialog(true);
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setOpenCancelDialog(false);
|
||||
|
||||
setOpenCompleteDialog(false);
|
||||
};
|
||||
|
||||
/* =========================
|
||||
CANCEL
|
||||
========================= */
|
||||
|
||||
const cancelConsultationMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
|
||||
showMessageSuccess: true,
|
||||
|
||||
msgSuccess: "Hủy phiên khám thành công!",
|
||||
|
||||
http: consultationServices.cancelConsultations(selectedConsultationId),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
COMPLETE
|
||||
========================= */
|
||||
|
||||
const completeConsultationMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
|
||||
showMessageSuccess: true,
|
||||
|
||||
msgSuccess: "Hoàn thành phiên khám thành công!",
|
||||
|
||||
http: consultationServices.completeConsultations(
|
||||
selectedConsultationId,
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
CONFIRM
|
||||
========================= */
|
||||
|
||||
const handleConfirmCancel = () => {
|
||||
cancelConsultationMutation.mutate();
|
||||
};
|
||||
|
||||
const handleConfirmComplete = () => {
|
||||
completeConsultationMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-black">Phiên khám</h2>
|
||||
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<ClipboardPlus />}
|
||||
onClick={handleOpenCreate}
|
||||
>
|
||||
Thêm mới
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{/* SEARCH */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="w-full md:min-w-[400px]">
|
||||
<Search
|
||||
keyword={_keyword ?? ""}
|
||||
setKeyword={(value) => updateQuery("_keyword", value)}
|
||||
placeholder="Tìm kiếm phiên khám..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TABLE */}
|
||||
<DataWrapper
|
||||
data={consultationData}
|
||||
loading={consultationQuery.isLoading}
|
||||
title="Không có dữ liệu"
|
||||
note="Vui lòng thử lại sau"
|
||||
>
|
||||
<Table
|
||||
rowKey={(item) => item.id}
|
||||
data={consultationData}
|
||||
column={[
|
||||
{
|
||||
fixedLeft: true,
|
||||
|
||||
title: "ID PHIÊN KHÁM",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<Link
|
||||
href={`/consultation/${item.id}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.id}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "MÃ PHIÊN KHÁM",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.sessionCode}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "NGÀY KHÁM",
|
||||
|
||||
render: (item: ConsultationItem) => <span>{item.visitDate}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
title: "DẤU HIỆU",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.symptomsText || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "CHUẨN ĐOÁN",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.diagnosis || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "KẾ HOẠCH ĐIỀU TRỊ",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.treatmentPlan || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "GHI CHÚ BÁC SĨ",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.doctorNotes || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "TRẠNG THÁI",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<StateActive
|
||||
stateActive={item.status}
|
||||
listState={[
|
||||
{
|
||||
state: 1,
|
||||
text: "Đang khám",
|
||||
textColor: "#000",
|
||||
backgroundColor: "#FFE4C4",
|
||||
},
|
||||
|
||||
{
|
||||
state: 2,
|
||||
text: "Hoàn thành",
|
||||
textColor: "#fff",
|
||||
backgroundColor: "#22c55e",
|
||||
},
|
||||
|
||||
{
|
||||
state: 3,
|
||||
text: "Đã hủy",
|
||||
textColor: "#fff",
|
||||
backgroundColor: "#ef4444",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
fixedRight: true,
|
||||
|
||||
title: "ACTION",
|
||||
|
||||
render: (item: ConsultationItem) => {
|
||||
const isCancelled = item.status === TYPE_STATUS.Cancelled;
|
||||
|
||||
const isCompleted = item.status === TYPE_STATUS.Completed;
|
||||
|
||||
const isDisabled = isCancelled || isCompleted;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* UPDATE */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
disabled={isDisabled}
|
||||
icon={<Pencil className="text-blue-400" />}
|
||||
onClick={() => handleOpenUpdate(item)}
|
||||
/>
|
||||
|
||||
{/* CANCEL */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
variant="red"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleOpenCancel(item)}
|
||||
>
|
||||
Hủy
|
||||
</CustomButton>
|
||||
|
||||
{/* COMPLETE */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
variant="green"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleOpenComplete(item)}
|
||||
>
|
||||
Hoàn thành
|
||||
</CustomButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
|
||||
{/* CANCEL DIALOG */}
|
||||
<CustomDialog
|
||||
open={openCancelDialog}
|
||||
title="Hủy phiên khám"
|
||||
note="Bạn có chắc chắn muốn hủy phiên khám này không?"
|
||||
type="error"
|
||||
onClose={handleCloseDialog}
|
||||
onSubmit={handleConfirmCancel}
|
||||
titleCancel="Đóng"
|
||||
titleSubmit="Xác nhận"
|
||||
/>
|
||||
|
||||
{/* COMPLETE DIALOG */}
|
||||
<CustomDialog
|
||||
open={openCompleteDialog}
|
||||
title="Hoàn thành phiên khám"
|
||||
note="Bạn có chắc chắn muốn hoàn thành phiên khám này không?"
|
||||
type="success"
|
||||
onClose={handleCloseDialog}
|
||||
onSubmit={handleConfirmComplete}
|
||||
titleCancel="Đóng"
|
||||
titleSubmit="Xác nhận"
|
||||
/>
|
||||
|
||||
{/* CREATE */}
|
||||
<CustomPopup open={!!_create} onClose={handleClosePopup}>
|
||||
<PopupCreateConsultation onClose={handleClosePopup} />
|
||||
</CustomPopup>
|
||||
|
||||
{/* UPDATE */}
|
||||
<CustomPopup open={!!_update} onClose={handleClosePopup}>
|
||||
<PopupUpdateConsultation
|
||||
id={_update || ""}
|
||||
onClose={handleClosePopup}
|
||||
/>
|
||||
</CustomPopup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPageConsultation;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./MainPageConsultation";
|
||||
@@ -0,0 +1,222 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import FormCustom from "@/components/utils/FormCustom/FormCustom";
|
||||
|
||||
import SelectForm from "@/components/utils/FormCustom/components/SelectForm";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import consultationServices from "@/services/consultationServices";
|
||||
|
||||
import patientServices, {
|
||||
PatientItem,
|
||||
PatientResponse,
|
||||
} from "@/services/patientServices";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
|
||||
export interface PopupCreateConsultationProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PopupCreateConsultation = ({ onClose }: PopupCreateConsultationProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [form, setForm] = useState<{
|
||||
patientId: string;
|
||||
}>({
|
||||
patientId: "",
|
||||
});
|
||||
|
||||
/* =========================
|
||||
GET LIST PATIENT
|
||||
========================= */
|
||||
const patientQuery = useQuery<PatientResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_patient],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: patientServices.getPatient({
|
||||
Page: 1,
|
||||
PageSize: 1000,
|
||||
Search: "",
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
OPTIONS SELECT
|
||||
========================= */
|
||||
const patientOptions = useMemo(() => {
|
||||
return patientQuery.data?.items || [];
|
||||
}, [patientQuery.data]);
|
||||
|
||||
/* =========================
|
||||
CREATE CONSULTATION
|
||||
========================= */
|
||||
const createConsultationMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Tạo phiên khám thành công!",
|
||||
|
||||
http: consultationServices.createConsultations({
|
||||
patientId: form.patientId,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
createConsultationMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
h-[400px]
|
||||
w-[640px]
|
||||
max-w-[95vw]
|
||||
rounded-3xl
|
||||
bg-white
|
||||
shadow-xl
|
||||
flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
{/* CLOSE */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="
|
||||
absolute
|
||||
right-5
|
||||
top-5
|
||||
z-10
|
||||
text-gray-400
|
||||
transition
|
||||
hover:text-gray-600
|
||||
"
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
|
||||
{/* HEADER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-b
|
||||
px-7
|
||||
py-5
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<h2 className="text-[30px] font-semibold text-[#111827]">
|
||||
Thêm phiên khám
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div className="flex-1 overflow-y-auto px-7 py-5">
|
||||
<div className="space-y-5">
|
||||
<SelectForm
|
||||
label={
|
||||
<span className="text-[14px] font-medium text-[#374151]">
|
||||
Bệnh nhân
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Chọn bệnh nhân"
|
||||
value={form.patientId}
|
||||
options={patientOptions}
|
||||
// loading={patientQuery.isLoading}
|
||||
onSelect={(item: PatientItem) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
patientId: item.id,
|
||||
}))
|
||||
}
|
||||
onClean={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
patientId: "",
|
||||
}))
|
||||
}
|
||||
getOptionLabel={(item: PatientItem) =>
|
||||
`${item.fullName} - ${item.identificationNumber}`
|
||||
}
|
||||
getOptionValue={(item: PatientItem) => item.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-t
|
||||
bg-white
|
||||
px-7
|
||||
py-5
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-3
|
||||
"
|
||||
>
|
||||
<CustomButton variant="grey" rounded="md" onClick={onClose}>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
|
||||
<CustomButton
|
||||
disabled={!form.patientId || createConsultationMutation.isPending}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{createConsultationMutation.isPending
|
||||
? "Đang tạo..."
|
||||
: "Tạo phiên khám"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupCreateConsultation;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./PopupCreateConsultation";
|
||||
@@ -0,0 +1,281 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationDetail,
|
||||
} from "@/services/consultationServices";
|
||||
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||
|
||||
interface PopupUpdateConsultationProps {
|
||||
id: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface IUpdateConsul {
|
||||
chiefComplaint: string;
|
||||
symptomsText: string;
|
||||
vitalSigns: string;
|
||||
diagnosis: string;
|
||||
treatmentPlan: string;
|
||||
doctorNotes: string;
|
||||
}
|
||||
|
||||
const defaultForm: IUpdateConsul = {
|
||||
chiefComplaint: "",
|
||||
symptomsText: "",
|
||||
vitalSigns: "",
|
||||
diagnosis: "",
|
||||
treatmentPlan: "",
|
||||
doctorNotes: "",
|
||||
};
|
||||
|
||||
const PopupUpdateConsultation = ({
|
||||
id,
|
||||
onClose,
|
||||
}: PopupUpdateConsultationProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [form, setForm] = useState<IUpdateConsul>(defaultForm);
|
||||
|
||||
const consulDetailQuery = useQuery<ConsultationDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_phien_kham, id],
|
||||
enabled: !!id,
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationDetail>({
|
||||
showMessageFailed: true,
|
||||
http: consultationServices.detailConsultations(id),
|
||||
});
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
patientId: "",
|
||||
sessionCode: "",
|
||||
visitDate: "",
|
||||
status: 1,
|
||||
chiefComplaint: null,
|
||||
symptomsText: null,
|
||||
vitalSigns: null,
|
||||
diagnosis: null,
|
||||
treatmentPlan: null,
|
||||
doctorNotes: null,
|
||||
createdBy: "",
|
||||
isDeleted: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const consul = consulDetailQuery.data;
|
||||
|
||||
if (!consul) return;
|
||||
|
||||
setForm({
|
||||
chiefComplaint: consul.chiefComplaint || "",
|
||||
symptomsText: consul.symptomsText || "",
|
||||
vitalSigns: consul.vitalSigns || "",
|
||||
diagnosis: consul.diagnosis || "",
|
||||
treatmentPlan: consul.treatmentPlan || "",
|
||||
doctorNotes: consul.doctorNotes || "",
|
||||
});
|
||||
}, [consulDetailQuery.data]);
|
||||
|
||||
/* =========================
|
||||
SUBMIT
|
||||
========================= */
|
||||
|
||||
const updateConsultationMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Cập nhật phiên khám thành công!",
|
||||
http: consultationServices.putConsultations(id, {
|
||||
chiefComplaint: form.chiefComplaint,
|
||||
symptomsText: form.symptomsText,
|
||||
vitalSigns: form.vitalSigns,
|
||||
diagnosis: form.diagnosis,
|
||||
treatmentPlan: form.treatmentPlan,
|
||||
doctorNotes: form.doctorNotes,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess(data) {
|
||||
if (!data) return;
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.chi_tiet_phien_kham, id],
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
updateConsultationMutation.mutate();
|
||||
};
|
||||
|
||||
if (consulDetailQuery.isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-10">
|
||||
Đang tải dữ liệu...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
h-[680px]
|
||||
w-[640px]
|
||||
max-w-[95vw]
|
||||
rounded-3xl
|
||||
bg-white
|
||||
shadow-xl
|
||||
flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
{/* CLOSE */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="
|
||||
absolute
|
||||
right-5
|
||||
top-5
|
||||
z-10
|
||||
text-gray-400
|
||||
transition
|
||||
hover:text-gray-600
|
||||
"
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
|
||||
{/* HEADER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-b
|
||||
px-7
|
||||
py-5
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<h2 className="text-[30px] font-semibold text-[#111827]">
|
||||
Chỉnh sửa phiên khám
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div className="flex-1 overflow-y-auto px-7 py-5">
|
||||
<div className="space-y-5">
|
||||
<InputForm
|
||||
label="Lý do khám"
|
||||
name="chiefComplaint"
|
||||
type="text"
|
||||
value={form.chiefComplaint}
|
||||
placeholder="Nhập lý do khám"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Triệu chứng"
|
||||
name="symptomsText"
|
||||
placeholder="Nhập triệu chứng"
|
||||
value={form.symptomsText}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Dấu hiệu sinh tồn"
|
||||
name="vitalSigns"
|
||||
placeholder="Nhập dấu hiệu sinh tồn"
|
||||
value={form.vitalSigns}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Chẩn đoán"
|
||||
name="diagnosis"
|
||||
placeholder="Nhập chẩn đoán"
|
||||
value={form.diagnosis}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Kế hoạch điều trị"
|
||||
name="treatmentPlan"
|
||||
placeholder="Nhập kế hoạch điều trị"
|
||||
value={form.treatmentPlan}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Ghi chú bác sĩ"
|
||||
name="doctorNotes"
|
||||
placeholder="Nhập ghi chú"
|
||||
value={form.doctorNotes}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-t
|
||||
bg-white
|
||||
px-7
|
||||
py-5
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-3
|
||||
"
|
||||
>
|
||||
<CustomButton variant="grey" rounded="md" onClick={onClose}>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
|
||||
<ContextFormCustom.Consumer>
|
||||
{({ isDone }) => (
|
||||
<CustomButton
|
||||
disabled={!isDone || updateConsultationMutation.isPending}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
type="submit"
|
||||
>
|
||||
{updateConsultationMutation.isPending
|
||||
? "Đang cập nhật..."
|
||||
: "Cập nhật"}
|
||||
</CustomButton>
|
||||
)}
|
||||
</ContextFormCustom.Consumer>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupUpdateConsultation;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./PopupUpdateConsultation";
|
||||
@@ -0,0 +1,189 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import patientServices, { PatientDetail } from "@/services/patientServices";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
const DetailPatient = () => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
||||
const patientId = params?.id as string;
|
||||
|
||||
const patientDetailQuery = useQuery<PatientDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, patientId],
|
||||
|
||||
enabled: !!patientId,
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientDetail>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: patientServices.detailPatient(patientId),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
patientCode: "",
|
||||
identificationNumber: "",
|
||||
fullName: "",
|
||||
dateOfBirth: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
emergencyContactName: null,
|
||||
emergencyContactPhone: null,
|
||||
allergyNotes: null,
|
||||
chronicDiseaseNotes: null,
|
||||
insuranceNumber: null,
|
||||
notes: null,
|
||||
isActive: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const patient = patientDetailQuery.data;
|
||||
|
||||
if (patientDetailQuery.isLoading) {
|
||||
return <div>Đang tải dữ liệu...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="flex flex-col gap-5">
|
||||
<h2 className="text-[28px] flex items-center gap-2 font-semibold text-[#111827]">
|
||||
<ArrowLeft
|
||||
onClick={() => router.back()}
|
||||
className="cursor-pointer"
|
||||
/>{" "}
|
||||
{patient?.fullName}
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Mã bệnh nhân
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.patientCode}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Họ tên</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.fullName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Mã định danh
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.identificationNumber}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Ngày sinh
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.dateOfBirth}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Giới tính
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.gender}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Số điện thoại
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.phone}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Email</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.email}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Địa chỉ</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.address || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Người liên hệ khẩn cấp
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.emergencyContactName || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
SĐT khẩn cấp
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.emergencyContactPhone || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Dị ứng</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.allergyNotes || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Bệnh nền</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.chronicDiseaseNotes || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailPatient;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./DetailPatient";
|
||||
@@ -0,0 +1,279 @@
|
||||
"use client";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import CustomDialog from "@/components/customs/custom-dialog";
|
||||
import CustomPopup from "@/components/customs/custom-popup";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import Search from "@/components/customs/custom-search";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import patientServices, {
|
||||
PatientItem,
|
||||
PatientResponse,
|
||||
} from "@/services/patientServices";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Pencil, Trash2, Users } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import PopupCreatePatient from "../PopupCreatePatient";
|
||||
import PopupUpdatePatient from "../PopupUpdatePatient";
|
||||
|
||||
const MainPagePatient = () => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
const [deleteItemId, setDeleteItemId] = useState<string>("");
|
||||
|
||||
const _page = searchParams.get("_page");
|
||||
const _pageSize = searchParams.get("_pageSize");
|
||||
const _keyword = searchParams.get("_keyword");
|
||||
|
||||
/**
|
||||
* PARAMS
|
||||
*/
|
||||
const createParam = searchParams.get("_create");
|
||||
const updateParam = searchParams.get("_update");
|
||||
|
||||
/**
|
||||
* OPEN POPUP
|
||||
*/
|
||||
const openCreatePopup = createParam !== null;
|
||||
const openUpdatePopup = !!updateParam;
|
||||
|
||||
/**
|
||||
* QUERY LIST PATIENT
|
||||
*/
|
||||
const patientQuery = useQuery<PatientResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_patient, _page, _pageSize, _keyword],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: patientServices.getPatient({
|
||||
Page: Number(_page || 1),
|
||||
PageSize: Number(_pageSize || 10),
|
||||
Search: _keyword || "",
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* UPDATE QUERY PARAMS
|
||||
*/
|
||||
const updateQuery = (key: string, value?: string | number) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (!value || value === "") {
|
||||
params.delete(key);
|
||||
} else {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `${pathname}?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
/**
|
||||
* OPEN CREATE
|
||||
*/
|
||||
const handleOpenCreate = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.set("_create", "");
|
||||
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* OPEN UPDATE
|
||||
*/
|
||||
const handleOpenUpdate = (id: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.delete("_create");
|
||||
params.set("_update", id);
|
||||
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* CLOSE ALL POPUP
|
||||
*/
|
||||
const handleClosePopup = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.delete("_create");
|
||||
params.delete("_update");
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `${pathname}?${queryString}` : pathname);
|
||||
|
||||
setOpenDeleteDialog(false);
|
||||
setDeleteItemId("");
|
||||
};
|
||||
|
||||
/**
|
||||
* DELETE
|
||||
*/
|
||||
const handleOpenDelete = (id: string) => {
|
||||
setDeleteItemId(id);
|
||||
setOpenDeleteDialog(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* DATA
|
||||
*/
|
||||
const patientData = useMemo(() => {
|
||||
return patientQuery.data?.items || [];
|
||||
}, [patientQuery.data]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-black">Bệnh nhân</h2>
|
||||
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<Users />}
|
||||
onClick={handleOpenCreate}
|
||||
>
|
||||
Tạo mới
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{/* SEARCH */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="w-full md:min-w-[400px]">
|
||||
<Search
|
||||
keyword={_keyword ?? ""}
|
||||
setKeyword={(value) => updateQuery("_keyword", value)}
|
||||
placeholder="Tìm kiếm theo tên người dùng, ID"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TABLE */}
|
||||
<DataWrapper
|
||||
data={patientData}
|
||||
loading={patientQuery.isLoading}
|
||||
title="No New Requests"
|
||||
note="Please check back later or view your schedule"
|
||||
>
|
||||
<Table
|
||||
rowKey={(item) => item.id}
|
||||
data={patientData}
|
||||
column={[
|
||||
{
|
||||
fixedLeft: true,
|
||||
title: "TÊN BỆNH NHÂN",
|
||||
|
||||
render: (item: PatientItem) => (
|
||||
<Link
|
||||
href={`/patient/${item.id}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.fullName}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "MÃ ĐỊNH DANH",
|
||||
|
||||
render: (item: PatientItem) => (
|
||||
<span>{item.identificationNumber}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "SỐ ĐIỆN THOẠI",
|
||||
|
||||
render: (item: PatientItem) => <span>{item.phone}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
title: "EMAIL",
|
||||
|
||||
render: (item: PatientItem) => <span>{item.email}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
title: "GIỚI TÍNH",
|
||||
|
||||
render: (item: PatientItem) => <span>{item.gender}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
fixedRight: true,
|
||||
title: "ACTION",
|
||||
|
||||
render: (item: PatientItem) => (
|
||||
<div className="flex items-center gap-2">
|
||||
{/* UPDATE */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
icon={<Pencil className="text-blue-400" />}
|
||||
onClick={() => handleOpenUpdate(item.id)}
|
||||
/>
|
||||
|
||||
{/* DELETE */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
icon={<Trash2 className="text-red-400" />}
|
||||
onClick={() => handleOpenDelete(item.id)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
|
||||
{/* DELETE DIALOG */}
|
||||
<CustomDialog
|
||||
open={openDeleteDialog}
|
||||
title="Xóa bệnh nhân"
|
||||
note="Bạn có chắc chắn muốn xóa bệnh nhân này?"
|
||||
type="error"
|
||||
onClose={handleClosePopup}
|
||||
onSubmit={() => {}}
|
||||
titleCancel="Hủy"
|
||||
titleSubmit="Xác nhận"
|
||||
/>
|
||||
|
||||
{/* CREATE POPUP */}
|
||||
<CustomPopup open={openCreatePopup} onClose={handleClosePopup}>
|
||||
<PopupCreatePatient onClose={handleClosePopup} />
|
||||
</CustomPopup>
|
||||
|
||||
{/* UPDATE POPUP */}
|
||||
<CustomPopup open={openUpdatePopup} onClose={handleClosePopup}>
|
||||
<PopupUpdatePatient onClose={handleClosePopup} id={updateParam || ""} />
|
||||
</CustomPopup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPagePatient;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./MainPagePatient";
|
||||
@@ -0,0 +1,259 @@
|
||||
"use client";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||
import { QUERY_KEY, TYPE_GENDER } from "@/constant/config/enum";
|
||||
import { X } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import patientServices from "@/services/patientServices";
|
||||
|
||||
import CustomLoading from "@/components/customs/custom-loading";
|
||||
|
||||
export interface ICreatePatient {
|
||||
identificationNumber: string;
|
||||
fullName: string;
|
||||
dateOfBirth: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface PropsPopupCreatePatient {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
/* QUERY KEY */
|
||||
// export const QUERY_KEY = {
|
||||
// table_list_patient: "table_list_patient",
|
||||
// };
|
||||
|
||||
const PopupCreatePatient = ({ onClose }: PropsPopupCreatePatient) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [form, setForm] = useState<ICreatePatient>({
|
||||
identificationNumber: "",
|
||||
fullName: "",
|
||||
dateOfBirth: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
});
|
||||
|
||||
const createPatientMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Tạo bệnh nhân thành công!",
|
||||
|
||||
http: patientServices.createPatient({
|
||||
identificationNumber: form.identificationNumber,
|
||||
fullName: form.fullName,
|
||||
dateOfBirth: form.dateOfBirth,
|
||||
gender: form.gender,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
address: form.address,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
/* REFRESH LIST */
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_patient],
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
createPatientMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
h-[680px]
|
||||
w-[640px]
|
||||
max-w-[95vw]
|
||||
rounded-3xl
|
||||
bg-white
|
||||
shadow-xl
|
||||
flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
{/* CLOSE */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="
|
||||
absolute
|
||||
right-5
|
||||
top-5
|
||||
z-10
|
||||
text-gray-400
|
||||
transition
|
||||
hover:text-gray-600
|
||||
"
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
|
||||
{/* HEADER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-b
|
||||
px-7
|
||||
py-5
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<h2 className="text-[30px] font-semibold text-[#111827]">
|
||||
Thêm bệnh nhân
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div
|
||||
className="
|
||||
flex-1
|
||||
overflow-y-auto
|
||||
px-7
|
||||
py-5
|
||||
"
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Mã định danh
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Mã định danh"
|
||||
name="identificationNumber"
|
||||
type="text"
|
||||
value={form.identificationNumber}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Tên bệnh nhân
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Tên bệnh nhân"
|
||||
name="fullName"
|
||||
type="text"
|
||||
value={form.fullName}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Ngày sinh
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Ngày sinh"
|
||||
name="dateOfBirth"
|
||||
type="Date"
|
||||
value={form.dateOfBirth}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Số điện thoại
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Số điện thoại"
|
||||
name="phone"
|
||||
type="text"
|
||||
value={form.phone}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Email
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
type="text"
|
||||
value={form.email}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="address"
|
||||
placeholder="Địa chỉ"
|
||||
label="Địa chỉ"
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-t
|
||||
bg-white
|
||||
px-7
|
||||
py-5
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-3
|
||||
"
|
||||
>
|
||||
<CustomButton variant="grey" rounded="md" onClick={onClose}>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
|
||||
<ContextFormCustom.Consumer>
|
||||
{({ isDone }) => (
|
||||
<CustomButton
|
||||
disabled={!isDone || createPatientMutation.isPending}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
type="submit"
|
||||
>
|
||||
{createPatientMutation.isPending ? "Đang tạo..." : "Tạo"}
|
||||
</CustomButton>
|
||||
)}
|
||||
</ContextFormCustom.Consumer>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupCreatePatient;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./PopupCreatePatient";
|
||||
@@ -0,0 +1,419 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { X } from "lucide-react";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import CustomLoading from "@/components/customs/custom-loading";
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||
import { QUERY_KEY, TYPE_GENDER } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import patientServices, { PatientDetail } from "@/services/patientServices";
|
||||
interface PopupUpdatePatientProps {
|
||||
onClose: () => void;
|
||||
id: string;
|
||||
}
|
||||
interface IUpdatePatient {
|
||||
fullName: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
emergencyContactName: string;
|
||||
emergencyContactPhone: string;
|
||||
allergyNotes: string;
|
||||
chronicDiseaseNotes: string;
|
||||
insuranceNumber: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
const defaultForm: IUpdatePatient = {
|
||||
fullName: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
emergencyContactName: "",
|
||||
emergencyContactPhone: "",
|
||||
allergyNotes: "",
|
||||
chronicDiseaseNotes: "",
|
||||
insuranceNumber: "",
|
||||
notes: "",
|
||||
};
|
||||
|
||||
const PopupUpdatePatient = ({ onClose, id }: PopupUpdatePatientProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [form, setForm] = useState<IUpdatePatient>(defaultForm);
|
||||
|
||||
const patientDetailQuery = useQuery<PatientDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, id],
|
||||
enabled: !!id,
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientDetail>({
|
||||
showMessageFailed: true,
|
||||
http: patientServices.detailPatient(id),
|
||||
});
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
patientCode: "",
|
||||
identificationNumber: "",
|
||||
fullName: "",
|
||||
dateOfBirth: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
emergencyContactName: null,
|
||||
emergencyContactPhone: null,
|
||||
allergyNotes: null,
|
||||
chronicDiseaseNotes: null,
|
||||
insuranceNumber: null,
|
||||
notes: null,
|
||||
isActive: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const patient = patientDetailQuery.data;
|
||||
if (!patient) return;
|
||||
setForm((prev) => {
|
||||
/**
|
||||
* tránh render loop
|
||||
*/
|
||||
if (
|
||||
prev.fullName === patient.fullName &&
|
||||
prev.phone === patient.phone &&
|
||||
prev.email === patient.email
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
fullName: patient.fullName || "",
|
||||
gender: patient.gender || TYPE_GENDER.MALE,
|
||||
phone: patient.phone || "",
|
||||
email: patient.email || "",
|
||||
address: patient.address || "",
|
||||
emergencyContactName: patient.emergencyContactName || "",
|
||||
emergencyContactPhone: patient.emergencyContactPhone || "",
|
||||
allergyNotes: patient.allergyNotes || "",
|
||||
chronicDiseaseNotes: patient.chronicDiseaseNotes || "",
|
||||
insuranceNumber: patient.insuranceNumber || "",
|
||||
notes: patient.notes || "",
|
||||
};
|
||||
});
|
||||
}, [patientDetailQuery.data]);
|
||||
|
||||
const updatePatientMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Cập nhật bệnh nhân thành công!",
|
||||
http: patientServices.putPatient(id, {
|
||||
fullName: form.fullName,
|
||||
gender: form.gender,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
address: form.address,
|
||||
emergencyContactName: form.emergencyContactName,
|
||||
emergencyContactPhone: form.emergencyContactPhone,
|
||||
allergyNotes: form.allergyNotes,
|
||||
chronicDiseaseNotes: form.chronicDiseaseNotes,
|
||||
insuranceNumber: form.insuranceNumber,
|
||||
notes: form.notes,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess(data) {
|
||||
if (!data) return;
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_patient],
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, id],
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
updatePatientMutation.mutate();
|
||||
};
|
||||
|
||||
if (patientDetailQuery.isLoading) {
|
||||
return (
|
||||
<div className="flex h-[400px] items-center justify-center">
|
||||
<CustomLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
h-[680px]
|
||||
w-[640px]
|
||||
max-w-[95vw]
|
||||
rounded-3xl
|
||||
bg-white
|
||||
shadow-xl
|
||||
flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
{/* CLOSE */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="
|
||||
absolute
|
||||
right-5
|
||||
top-5
|
||||
z-10
|
||||
text-gray-400
|
||||
transition
|
||||
hover:text-gray-600
|
||||
"
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
|
||||
{/* HEADER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-b
|
||||
px-7
|
||||
py-5
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<h2 className="text-[30px] font-semibold text-[#111827]">
|
||||
Cập nhật thông tin bệnh nhân
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div
|
||||
className="
|
||||
flex-1
|
||||
overflow-y-auto
|
||||
px-7
|
||||
py-5
|
||||
"
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Tên bệnh nhân
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Tên bệnh nhân"
|
||||
name="fullName"
|
||||
type="text"
|
||||
value={form.fullName}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
{/* GENDER */}
|
||||
<div>
|
||||
<label className="text-base font-medium">Giới tính</label>
|
||||
|
||||
<div className="mt-2 flex items-center gap-6">
|
||||
{/* NAM */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="male"
|
||||
checked={form.gender === TYPE_GENDER.MALE}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
gender: TYPE_GENDER.MALE,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<label htmlFor="male">Nam</label>
|
||||
</div>
|
||||
|
||||
{/* NỮ */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="female"
|
||||
checked={form.gender === TYPE_GENDER.FEMALE}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
gender: TYPE_GENDER.FEMALE,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<label htmlFor="female">Nữ</label>
|
||||
</div>
|
||||
|
||||
{/* KHÁC */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="other"
|
||||
checked={form.gender === TYPE_GENDER.OTHER}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
gender: TYPE_GENDER.OTHER,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<label htmlFor="other">Khác</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Số điện thoại
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Số điện thoại"
|
||||
name="phone"
|
||||
type="text"
|
||||
value={form.phone}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Email
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
type="text"
|
||||
value={form.email}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="address"
|
||||
placeholder="Địa chỉ"
|
||||
label="Địa chỉ"
|
||||
value={form.address}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Người liên hệ khẩn cấp"
|
||||
placeholder="Người liên hệ khẩn cấp"
|
||||
name="emergencyContactName"
|
||||
type="text"
|
||||
value={form.emergencyContactName}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="SĐT khẩn cấp"
|
||||
placeholder="SĐT khẩn cấp"
|
||||
name="emergencyContactPhone"
|
||||
type="text"
|
||||
value={form.emergencyContactPhone}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="allergyNotes"
|
||||
placeholder="Dị ứng"
|
||||
label="Dị ứng"
|
||||
value={form.allergyNotes}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="chronicDiseaseNotes"
|
||||
placeholder="Bệnh nền"
|
||||
label="Bệnh nền"
|
||||
value={form.chronicDiseaseNotes}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Số bảo hiểm"
|
||||
placeholder="Số bảo hiểm"
|
||||
name="insuranceNumber"
|
||||
type="text"
|
||||
value={form.insuranceNumber}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="notes"
|
||||
placeholder="Ghi chú"
|
||||
label="Ghi chú"
|
||||
value={form.notes}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-t
|
||||
bg-white
|
||||
px-7
|
||||
py-5
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-3
|
||||
"
|
||||
>
|
||||
<CustomButton variant="grey" rounded="md" onClick={onClose}>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
|
||||
<ContextFormCustom.Consumer>
|
||||
{({ isDone }) => (
|
||||
<CustomButton
|
||||
disabled={!isDone || updatePatientMutation.isPending}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
type="submit"
|
||||
>
|
||||
{updatePatientMutation.isPending
|
||||
? "Đang cập nhật..."
|
||||
: "Cập nhật"}
|
||||
</CustomButton>
|
||||
)}
|
||||
</ContextFormCustom.Consumer>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupUpdatePatient;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./PopupUpdatePatient";
|
||||
@@ -0,0 +1,409 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
|
||||
import SelectForm from "@/components/utils/FormCustom/components/SelectForm";
|
||||
|
||||
import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import prescriptionServices from "@/services/prescriptionServices";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationItem,
|
||||
ConsultationResponse,
|
||||
} from "@/services/consultationServices";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
import { PATH } from "@/constant/config";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface PrescriptionItem {
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
interface PrescriptionForm {
|
||||
examinationSessionId: string;
|
||||
notes: string;
|
||||
prescriptionImg: string;
|
||||
items: PrescriptionItem[];
|
||||
}
|
||||
|
||||
interface UploadImageItem {
|
||||
url?: string;
|
||||
path?: string;
|
||||
file?: File;
|
||||
img?: string;
|
||||
}
|
||||
|
||||
const defaultMedicine: PrescriptionItem = {
|
||||
medicineName: "",
|
||||
dosage: "",
|
||||
frequency: "",
|
||||
duration: "",
|
||||
instructions: "",
|
||||
};
|
||||
|
||||
const CreatePrescription = () => {
|
||||
const router = useRouter();
|
||||
const [images, setImages] = useState<UploadImageItem[]>([]);
|
||||
|
||||
const [form, setForm] = useState<PrescriptionForm>({
|
||||
examinationSessionId: "",
|
||||
notes: "",
|
||||
prescriptionImg: "",
|
||||
items: [defaultMedicine],
|
||||
});
|
||||
|
||||
/**
|
||||
* GET EXAMINATION SESSION
|
||||
*/
|
||||
const examinationSessionQuery = useQuery<ConsultationResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: consultationServices.getConsultations({
|
||||
Page: 1,
|
||||
PageSize: 100,
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const examinationSessionOptions = examinationSessionQuery.data?.items || [];
|
||||
|
||||
const createPrescriptionMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Tạo đơn thuốc thành công!",
|
||||
|
||||
http: prescriptionServices.createPrescription({
|
||||
examinationSessionId: form.examinationSessionId,
|
||||
notes: form.notes,
|
||||
prescriptionImg: form.prescriptionImg,
|
||||
items: form.items,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
router.push(PATH.PRESCRIPTION);
|
||||
},
|
||||
|
||||
onError: (error: any) => {
|
||||
// 👇 BACKEND ERROR HERE
|
||||
const message =
|
||||
error?.message || error?.error?.message || "Có lỗi xảy ra";
|
||||
|
||||
const code = error?.code || error?.error?.code;
|
||||
|
||||
toastWarn({
|
||||
msg: message,
|
||||
});
|
||||
|
||||
console.log("ERROR CODE:", code);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* CHANGE MEDICINE FIELD
|
||||
*/
|
||||
const handleChangeMedicine = (
|
||||
index: number,
|
||||
key: keyof PrescriptionItem,
|
||||
value: string,
|
||||
) => {
|
||||
const cloneItems = [...form.items];
|
||||
|
||||
cloneItems[index] = {
|
||||
...cloneItems[index],
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
items: cloneItems,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* ADD MEDICINE
|
||||
*/
|
||||
const handleAddMedicine = () => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
|
||||
items: [
|
||||
...prev.items,
|
||||
{
|
||||
medicineName: "",
|
||||
dosage: "",
|
||||
frequency: "",
|
||||
duration: "",
|
||||
instructions: "",
|
||||
},
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* REMOVE MEDICINE
|
||||
*/
|
||||
const handleRemoveMedicine = (index: number) => {
|
||||
const newItems = form.items.filter((_, i) => i !== index);
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
items: newItems,
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* SUBMIT
|
||||
*/
|
||||
const handleSubmit = () => {
|
||||
createPrescriptionMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-6 rounded-2xl bg-white p-6 shadow">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-[28px] font-semibold text-[#111827]">
|
||||
Thêm đơn thuốc
|
||||
</h2>
|
||||
|
||||
<p className="mt-1 text-sm text-[#697586]">
|
||||
Tạo mới đơn thuốc cho bệnh nhân
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
variant="midnightBlue"
|
||||
disabled={createPrescriptionMutation.isPending}
|
||||
>
|
||||
{createPrescriptionMutation.isPending
|
||||
? "Đang lưu..."
|
||||
: "Lưu đơn thuốc"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GENERAL INFO */}
|
||||
<div className="rounded-2xl border border-gray-200 p-5">
|
||||
<h3 className="mb-5 text-lg font-semibold text-[#111827]">
|
||||
Thông tin chung
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5">
|
||||
{/* EXAMINATION SESSION */}
|
||||
<SelectForm
|
||||
label={
|
||||
<span className="text-[14px] font-medium text-[#374151]">
|
||||
Phiên khám
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Chọn phiên khám"
|
||||
value={form.examinationSessionId}
|
||||
options={examinationSessionOptions}
|
||||
onSelect={(item: ConsultationItem) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
examinationSessionId: item.id,
|
||||
}))
|
||||
}
|
||||
onClean={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
examinationSessionId: "",
|
||||
}))
|
||||
}
|
||||
getOptionLabel={(item: ConsultationItem) =>
|
||||
`${item.sessionCode} `
|
||||
}
|
||||
getOptionValue={(item: ConsultationItem) => item.id}
|
||||
/>
|
||||
|
||||
{/* IMAGE */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<UploadMultipleFile
|
||||
images={images}
|
||||
setImages={(files: UploadImageItem[]) => {
|
||||
setImages(files);
|
||||
|
||||
const imagePath =
|
||||
files?.[0]?.path ||
|
||||
files?.[0]?.url ||
|
||||
files?.[0]?.img ||
|
||||
"";
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
prescriptionImg: imagePath,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* NOTES */}
|
||||
<TextArea
|
||||
label="Ghi chú"
|
||||
name="notes"
|
||||
placeholder="Nhập ghi chú"
|
||||
value={form.notes}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MEDICINE LIST */}
|
||||
<div className="rounded-2xl border border-gray-200 p-5">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-[#111827]">
|
||||
Danh sách thuốc
|
||||
</h3>
|
||||
<div>
|
||||
<CustomButton
|
||||
type="button"
|
||||
variant="midnightBlue"
|
||||
icon={<Plus size={18} />}
|
||||
onClick={handleAddMedicine}
|
||||
>
|
||||
Thêm thuốc
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{form.items.map((medicine, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-2xl border border-gray-200 p-5"
|
||||
>
|
||||
{/* TITLE */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h4 className="font-semibold text-[#111827]">
|
||||
Thuốc #{index + 1}
|
||||
</h4>
|
||||
|
||||
{form.items.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveMedicine(index)}
|
||||
className="text-red-500 transition hover:text-red-600"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* FORM */}
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<InputForm
|
||||
label="Tên thuốc"
|
||||
name={`items.${index}.medicineName`}
|
||||
placeholder="Nhập tên thuốc"
|
||||
value={medicine.medicineName}
|
||||
type="text"
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "medicineName", String(value))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Liều lượng"
|
||||
name={`items.${index}.dosage`}
|
||||
placeholder="Ví dụ: 500mg"
|
||||
value={medicine.dosage}
|
||||
type="text"
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "dosage", String(value))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Tần suất"
|
||||
name={`items.${index}.frequency`}
|
||||
placeholder="Ví dụ: 2 lần/ngày"
|
||||
value={medicine.frequency}
|
||||
type="text"
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "frequency", String(value))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Thời gian dùng"
|
||||
name={`items.${index}.duration`}
|
||||
placeholder="Ví dụ: 7 ngày"
|
||||
value={medicine.duration}
|
||||
type="text"
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "duration", String(value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5">
|
||||
<TextArea
|
||||
label="Hướng dẫn sử dụng"
|
||||
name={`items.${index}.instructions`}
|
||||
placeholder="Nhập hướng dẫn sử dụng thuốc"
|
||||
value={medicine.instructions}
|
||||
isBlur
|
||||
max={5000}
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "instructions", String(value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePrescription;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./CreatePrescription";
|
||||
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import prescriptionServices, {
|
||||
PrescriptionDetail,
|
||||
} from "@/services/prescriptionServices";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useParams } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
const DetailPrescription = () => {
|
||||
const params = useParams();
|
||||
const PrescriptionId = params?.id as string;
|
||||
|
||||
const prescriptionDetailQuery = useQuery<PrescriptionDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_don_thuoc, PrescriptionId],
|
||||
|
||||
enabled: !!PrescriptionId,
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PrescriptionDetail>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: prescriptionServices.detailPrescription(PrescriptionId),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
examinationSessionId: "",
|
||||
prescriptionCode: "",
|
||||
prescriptionImg: null,
|
||||
notes: "",
|
||||
createdBy: "",
|
||||
items: [],
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const data = prescriptionDetailQuery.data;
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<h1 className="text-xl font-semibold mb-4">Chi tiết đơn thuốc</h1>
|
||||
|
||||
{/* LOADING */}
|
||||
{prescriptionDetailQuery.isLoading && <p>Đang tải...</p>}
|
||||
|
||||
{/* ERROR */}
|
||||
{prescriptionDetailQuery.isError && (
|
||||
<p className="text-red-500">Lỗi tải dữ liệu</p>
|
||||
)}
|
||||
|
||||
{/* CONTENT */}
|
||||
{data && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<strong>Mã đơn thuốc:</strong> {data.prescriptionCode}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Phiên khám:</strong> {data.examinationSessionId}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Ghi chú:</strong> {data.notes || "-"}
|
||||
</div>
|
||||
|
||||
{/* IMAGE */}
|
||||
<div>
|
||||
<strong>Ảnh đơn thuốc:</strong>
|
||||
<div className="mt-2">
|
||||
{data.prescriptionImg ? (
|
||||
<img
|
||||
src={data.prescriptionImg}
|
||||
alt="prescription"
|
||||
className="h-32 w-32 object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-400">Không có ảnh</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ITEMS */}
|
||||
<div>
|
||||
<strong>Danh sách thuốc:</strong>
|
||||
|
||||
<div className="mt-2 space-y-2">
|
||||
{data.items?.length ? (
|
||||
data.items.map((item, index) => (
|
||||
<div key={item.id || index} className="p-3 border rounded">
|
||||
<p>
|
||||
<b>Tên thuốc:</b> {item.medicineName}
|
||||
</p>
|
||||
<p>
|
||||
<b>Liều lượng:</b> {item.dosage}
|
||||
</p>
|
||||
<p>
|
||||
<b>Tần suất:</b> {item.frequency}
|
||||
</p>
|
||||
<p>
|
||||
<b>Thời gian:</b> {item.duration}
|
||||
</p>
|
||||
<p>
|
||||
<b>Hướng dẫn:</b> {item.instructions}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-400">Không có thuốc</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailPrescription;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./DetailPrescription";
|
||||
@@ -0,0 +1,180 @@
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import Search from "@/components/customs/custom-search";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import { PATH } from "@/constant/config";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import prescriptionServices, {
|
||||
PrescriptionItem,
|
||||
PrescriptionResponse,
|
||||
} from "@/services/prescriptionServices";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Pencil, Pill, Trash2 } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
const MainPrescription = () => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const _page = searchParams.get("_page");
|
||||
const _pageSize = searchParams.get("_pageSize");
|
||||
const _keyword = searchParams.get("_keyword");
|
||||
|
||||
const prescriptionQuery = useQuery<PrescriptionResponse>({
|
||||
queryKey: [
|
||||
QUERY_KEY.table_list_patientrescription,
|
||||
_page,
|
||||
_pageSize,
|
||||
_keyword,
|
||||
],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PrescriptionResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: prescriptionServices.getPrescription({
|
||||
Page: Number(_page || 1),
|
||||
PageSize: Number(_pageSize || 10),
|
||||
Search: _keyword || "",
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const updateQuery = (key: string, value?: string | number) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (!value) {
|
||||
params.delete(key);
|
||||
} else {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `${pathname}?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-black">Đơn thuốc</h2>
|
||||
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<Pill />}
|
||||
href={PATH.CREATE_PRESCRIPTION}
|
||||
>
|
||||
Tạo mới
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{/* SEARCH */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="w-full md:min-w-[400px]">
|
||||
<Search
|
||||
keyword={_keyword ?? ""}
|
||||
setKeyword={(value) => updateQuery("_keyword", value)}
|
||||
placeholder="Tìm theo mã đơn thuốc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataWrapper
|
||||
data={prescriptionQuery.data?.items || []}
|
||||
loading={prescriptionQuery.isLoading}
|
||||
title="No Data"
|
||||
note="Không có đơn thuốc nào"
|
||||
>
|
||||
<Table
|
||||
rowKey={(item: any) => item.id}
|
||||
data={prescriptionQuery.data?.items || []}
|
||||
column={[
|
||||
// ================= ID / CODE =================
|
||||
{
|
||||
title: "MÃ ĐƠN THUỐC",
|
||||
render: (item: any) => (
|
||||
<Link
|
||||
href={`/prescription/${item.id}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.prescriptionCode}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
|
||||
// ================= SESSION =================
|
||||
{
|
||||
title: "PHIÊN KHÁM",
|
||||
render: (item: any) => <span>{item.examinationSessionId}</span>,
|
||||
},
|
||||
|
||||
// ================= NOTES =================
|
||||
{
|
||||
title: "GHI CHÚ",
|
||||
render: (item: any) => <span>{item.notes || "-"}</span>,
|
||||
},
|
||||
|
||||
// ================= IMAGE =================
|
||||
{
|
||||
title: "ẢNH ĐƠN THUỐC",
|
||||
render: (item: any) =>
|
||||
item.prescriptionImg ? (
|
||||
<Image
|
||||
src={item.prescriptionImg}
|
||||
alt="prescription"
|
||||
className="h-10 w-10 rounded object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-400">Không có ảnh</span>
|
||||
),
|
||||
},
|
||||
|
||||
// ================= ITEMS COUNT =================
|
||||
{
|
||||
title: "SỐ THUỐC",
|
||||
render: (item: any) => <span>{item.items?.length || 0}</span>,
|
||||
},
|
||||
|
||||
// ================= ACTION =================
|
||||
{
|
||||
fixedRight: true,
|
||||
title: "ACTION",
|
||||
|
||||
render: (item: any) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<CustomButton
|
||||
size="sm"
|
||||
icon={<Pencil className="text-blue-400" />}
|
||||
href={`${PATH.PRESCRIPTION}/update?_id=${item?.id}`}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPrescription;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./MainPrescription";
|
||||
@@ -0,0 +1,514 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
import SelectForm from "@/components/utils/FormCustom/components/SelectForm";
|
||||
import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
import prescriptionServices from "@/services/prescriptionServices";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationItem,
|
||||
ConsultationResponse,
|
||||
} from "@/services/consultationServices";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { PATH } from "@/constant/config";
|
||||
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
|
||||
import type { PrescriptionDetail } from "@/services/prescriptionServices";
|
||||
|
||||
/* =========================
|
||||
TYPES
|
||||
========================= */
|
||||
|
||||
interface PrescriptionItem {
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
interface PrescriptionForm {
|
||||
examinationSessionId: string;
|
||||
notes: string;
|
||||
prescriptionImg: string;
|
||||
items: PrescriptionItem[];
|
||||
}
|
||||
|
||||
interface UploadImageItem {
|
||||
url?: string;
|
||||
path?: string;
|
||||
img?: string;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
DEFAULT
|
||||
========================= */
|
||||
|
||||
const defaultMedicine: PrescriptionItem = {
|
||||
medicineName: "",
|
||||
dosage: "",
|
||||
frequency: "",
|
||||
duration: "",
|
||||
instructions: "",
|
||||
};
|
||||
|
||||
/* =========================
|
||||
COMPONENT
|
||||
========================= */
|
||||
|
||||
const UpdatePrescription = () => {
|
||||
const router = useRouter();
|
||||
|
||||
const params = useParams();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const idFromParams = params?.id as string | undefined;
|
||||
|
||||
const idFromSearch = searchParams?.get("_id") ?? undefined;
|
||||
|
||||
const id = idFromParams || idFromSearch || "";
|
||||
|
||||
/* =========================
|
||||
GET EXAMINATION SESSION
|
||||
========================= */
|
||||
|
||||
const examinationSessionQuery = useQuery<ConsultationResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: consultationServices.getConsultations({
|
||||
Page: 1,
|
||||
PageSize: 100,
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const examinationSessionOptions = examinationSessionQuery.data?.items || [];
|
||||
|
||||
/* =========================
|
||||
GET DETAIL PRESCRIPTION
|
||||
========================= */
|
||||
|
||||
const detailQuery = useQuery<PrescriptionDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_don_thuoc, id],
|
||||
|
||||
enabled: !!id,
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PrescriptionDetail>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: prescriptionServices.detailPrescription(id),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
examinationSessionId: "",
|
||||
prescriptionCode: "",
|
||||
prescriptionImg: null,
|
||||
notes: "",
|
||||
createdBy: "",
|
||||
items: [],
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
MAP DATA
|
||||
========================= */
|
||||
|
||||
const mappedForm: PrescriptionForm = useMemo(() => {
|
||||
const data = detailQuery.data;
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
examinationSessionId: "",
|
||||
notes: "",
|
||||
prescriptionImg: "",
|
||||
items: [defaultMedicine],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
examinationSessionId: data.examinationSessionId ?? "",
|
||||
|
||||
notes: data.notes ?? "",
|
||||
|
||||
prescriptionImg: data.prescriptionImg ?? "",
|
||||
|
||||
items:
|
||||
data.items && data.items.length > 0
|
||||
? data.items.map((i) => ({
|
||||
medicineName: i.medicineName ?? "",
|
||||
dosage: i.dosage ?? "",
|
||||
frequency: i.frequency ?? "",
|
||||
duration: i.duration ?? "",
|
||||
instructions: i.instructions ?? "",
|
||||
}))
|
||||
: [defaultMedicine],
|
||||
};
|
||||
}, [detailQuery.data]);
|
||||
|
||||
/* =========================
|
||||
FORM STATE
|
||||
========================= */
|
||||
|
||||
const [form, setForm] = useState<PrescriptionForm>({
|
||||
examinationSessionId: "",
|
||||
notes: "",
|
||||
prescriptionImg: "",
|
||||
items: [defaultMedicine],
|
||||
});
|
||||
|
||||
const [images, setImages] = useState<UploadImageItem[]>([]);
|
||||
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
/* =========================
|
||||
INIT FORM
|
||||
========================= */
|
||||
|
||||
useEffect(() => {
|
||||
if (!detailQuery.data || initialized) return;
|
||||
|
||||
queueMicrotask(() => {
|
||||
setForm(mappedForm);
|
||||
|
||||
setImages(
|
||||
mappedForm.prescriptionImg ? [{ url: mappedForm.prescriptionImg }] : [],
|
||||
);
|
||||
|
||||
setInitialized(true);
|
||||
});
|
||||
}, [detailQuery.data, initialized, mappedForm]);
|
||||
|
||||
/* =========================
|
||||
UPDATE MUTATION
|
||||
========================= */
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!id || id === "undefined") {
|
||||
throw new Error("Không tìm thấy ID đơn thuốc!");
|
||||
}
|
||||
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
|
||||
showMessageSuccess: true,
|
||||
|
||||
msgSuccess: "Cập nhật đơn thuốc thành công!",
|
||||
|
||||
http: prescriptionServices.putPrescription(id, {
|
||||
// examinationSessionId: form.examinationSessionId,
|
||||
notes: form.notes,
|
||||
prescriptionImg: form.prescriptionImg,
|
||||
items: form.items,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
router.push(PATH.PRESCRIPTION);
|
||||
},
|
||||
|
||||
onError: (error: any) => {
|
||||
const message =
|
||||
error?.message || error?.error?.message || "Có lỗi xảy ra";
|
||||
|
||||
toastWarn({
|
||||
msg: message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
HANDLERS
|
||||
========================= */
|
||||
|
||||
const handleChangeMedicine = (
|
||||
index: number,
|
||||
key: keyof PrescriptionItem,
|
||||
value: string,
|
||||
) => {
|
||||
const clone = [...form.items];
|
||||
|
||||
clone[index] = {
|
||||
...clone[index],
|
||||
[key]: value,
|
||||
};
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
items: clone,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleAddMedicine = () => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
|
||||
items: [
|
||||
...prev.items,
|
||||
{
|
||||
medicineName: "",
|
||||
dosage: "",
|
||||
frequency: "",
|
||||
duration: "",
|
||||
instructions: "",
|
||||
},
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
const handleRemoveMedicine = (index: number) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
|
||||
items: prev.items.filter((_, i) => i !== index),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
updateMutation.mutate();
|
||||
};
|
||||
|
||||
const isLoading = updateMutation.isPending || detailQuery.isLoading;
|
||||
|
||||
/* =========================
|
||||
UI
|
||||
========================= */
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-6 rounded-2xl bg-white p-6 shadow">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-[28px] font-semibold text-[#111827]">
|
||||
Cập nhật đơn thuốc
|
||||
</h2>
|
||||
|
||||
<p className="mt-1 text-sm text-[#697586]">
|
||||
Chỉnh sửa đơn thuốc cho bệnh nhân
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
variant="midnightBlue"
|
||||
// Vừa loading vừa check id, nếu id không tồn tại thì không cho click submit
|
||||
disabled={isLoading || !id || id === "undefined"}
|
||||
>
|
||||
{isLoading ? "Đang cập nhật..." : "Lưu đơn thuốc"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GENERAL INFO */}
|
||||
<div className="rounded-2xl border border-gray-200 p-5">
|
||||
<h3 className="mb-5 text-lg font-semibold text-[#111827]">
|
||||
Thông tin chung
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5">
|
||||
{/* EXAM SESSION */}
|
||||
<SelectForm
|
||||
label={
|
||||
<span className="text-[14px] font-medium text-[#374151]">
|
||||
Phiên khám <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Chọn phiên khám"
|
||||
value={form.examinationSessionId}
|
||||
readOnly
|
||||
options={examinationSessionOptions}
|
||||
onSelect={(item: ConsultationItem) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
examinationSessionId: item.id,
|
||||
}))
|
||||
}
|
||||
onClean={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
examinationSessionId: "",
|
||||
}))
|
||||
}
|
||||
getOptionLabel={(item: ConsultationItem) => item.sessionCode}
|
||||
getOptionValue={(item: ConsultationItem) => item.id}
|
||||
/>
|
||||
|
||||
{/* IMAGE */}
|
||||
<UploadMultipleFile
|
||||
images={images}
|
||||
setImages={(files: UploadImageItem[]) => {
|
||||
setImages(files);
|
||||
|
||||
const imagePath =
|
||||
files?.[0]?.path || files?.[0]?.url || files?.[0]?.img || "";
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
prescriptionImg: imagePath,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* NOTES */}
|
||||
<TextArea
|
||||
label="Ghi chú"
|
||||
name="notes"
|
||||
placeholder="Nhập ghi chú"
|
||||
value={form.notes}
|
||||
isBlur
|
||||
max={5000}
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, notes: String(v) }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MEDICINE LIST */}
|
||||
<div className="rounded-2xl border border-gray-200 p-5">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-[#111827]">
|
||||
Danh sách thuốc
|
||||
</h3>
|
||||
<div>
|
||||
<CustomButton
|
||||
type="button"
|
||||
variant="midnightBlue"
|
||||
icon={<Plus size={18} />}
|
||||
onClick={handleAddMedicine}
|
||||
>
|
||||
Thêm thuốc
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{form.items.map((medicine, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="rounded-2xl border border-gray-200 p-5"
|
||||
>
|
||||
{/* TITLE */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h4 className="font-semibold text-[#111827]">
|
||||
Thuốc #{index + 1}
|
||||
</h4>
|
||||
|
||||
{form.items.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveMedicine(index)}
|
||||
className="text-red-500 transition hover:text-red-600"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* INPUTS */}
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<InputForm
|
||||
label="Tên thuốc"
|
||||
name={`medicineName-${index}`}
|
||||
type="text"
|
||||
placeholder="Nhập tên thuốc"
|
||||
value={medicine.medicineName}
|
||||
onChangeValue={(v) =>
|
||||
handleChangeMedicine(index, "medicineName", String(v))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Liều lượng"
|
||||
name={`dosage-${index}`}
|
||||
type="text"
|
||||
placeholder="Nhập liều lượng"
|
||||
value={medicine.dosage}
|
||||
onChangeValue={(v) =>
|
||||
handleChangeMedicine(index, "dosage", String(v))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Tần suất"
|
||||
name={`frequency-${index}`}
|
||||
type="text"
|
||||
placeholder="Nhập tần suất"
|
||||
value={medicine.frequency}
|
||||
onChangeValue={(v) =>
|
||||
handleChangeMedicine(index, "frequency", String(v))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Thời gian"
|
||||
name={`duration-${index}`}
|
||||
type="text"
|
||||
placeholder="Nhập thời gian"
|
||||
value={medicine.duration}
|
||||
onChangeValue={(v) =>
|
||||
handleChangeMedicine(index, "duration", String(v))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* INSTRUCTIONS */}
|
||||
<div className="mt-5">
|
||||
<TextArea
|
||||
label="Hướng dẫn sử dụng"
|
||||
name={`instructions-${index}`}
|
||||
placeholder="Nhập hướng dẫn sử dụng"
|
||||
value={medicine.instructions}
|
||||
onChangeValue={(v) =>
|
||||
handleChangeMedicine(index, "instructions", String(v))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdatePrescription;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./UpdatePrescription";
|
||||
Reference in New Issue
Block a user