metms_fe/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx
2026-06-11 11:25:47 +07:00

729 lines
27 KiB
TypeScript

"use client";
import React, { useEffect, useMemo, useState } from "react";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
import { httpRequest } from "@/services";
import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
import { ArrowLeft, Pencil } from "lucide-react";
import consultationServices, {
AttachmentFile,
ConsultationDetail,
} from "@/services/consultationServices";
import StateActive from "@/components/customs/StateActive";
import TabNavLink from "@/components/customs/custom-tabs";
import moment from "moment";
import GridColumn from "@/components/layouts/GridColumn";
import DataWrapper from "@/components/customs/DataWrapper";
import Table from "@/components/customs/custom-table";
import CustomButton from "@/components/customs/custom-button";
import Pagination from "@/components/customs/custom-pagination";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
const DetailConsultation = () => {
const params = useParams();
const router = useRouter();
const searchParams = useSearchParams();
const specialtyId = searchParams.get("_type");
const consultationId = params?.id as string;
const [prescriptionPage, setPrescriptionPage] = useState<number>(1);
const [prescriptionPageSize, setPrescriptionPageSize] = useState<number>(10);
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,
specialtyClinics: [],
}
);
},
});
const consultation = consulDetailQuery.data;
const canUpdateConsultation =
consultation?.status !== TYPE_STATUS.Completed &&
consultation?.status !== TYPE_STATUS.Cancelled;
const specialtyTabs = useMemo(() => {
return (
consultation?.specialtyClinics?.map((item) => ({
pathname: `/consultation/${consultationId}`,
query: item.id,
title: item.specialtyName,
})) || []
);
}, [consultation?.specialtyClinics, consultationId]);
// Tính toán cắt mảng dữ liệu thuốc để hiển thị phân trang trên Client
const paginatedPrescriptionItems = useMemo(() => {
const items = consultation?.aggregatedPrescriptionItems || [];
const startIndex = (prescriptionPage - 1) * prescriptionPageSize;
const endIndex = startIndex + prescriptionPageSize;
return items.slice(startIndex, endIndex);
}, [
consultation?.aggregatedPrescriptionItems,
prescriptionPage,
prescriptionPageSize,
]);
const specialtyClinics = consultation?.specialtyClinics;
const selectedSpecialty = useMemo(() => {
if (!specialtyClinics?.length) return null;
return (
specialtyClinics.find((item) => item.id === specialtyId) ||
specialtyClinics[0]
);
}, [specialtyClinics, specialtyId]);
useEffect(() => {
if (consultation?.specialtyClinics?.length && !specialtyId) {
router.replace(`?&_type=${consultation.specialtyClinics[0].id}`);
}
}, [consultation?.specialtyClinics, specialtyId, router]);
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>;
}
type PrescriptionItem =
ConsultationDetail["aggregatedPrescriptionItems"][number];
// const getFileInfo = (file: AttachmentFile | string, index: number) => {
// if (typeof file === "string") {
// return {
// fileName: file.split("/").pop() || `File ${index + 1}`,
// path: file,
// };
// }
// return file;
// };
const getFileInfo = (file: AttachmentFile | string, index: number) => {
if (typeof file === "string") {
return {
fileName: file.split("/").pop() || `File ${index + 1}`,
path: `${process.env.NEXT_PUBLIC_IMAGE}${file}`,
};
}
return {
...file,
path: `${process.env.NEXT_PUBLIC_IMAGE}${file.path}`,
};
};
return (
<div className="flex flex-col gap-4">
<div className="flex flex-row 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 justify-between">
<div
className="flex cursor-pointer items-center gap-3"
onClick={() => router.back()}
>
<ArrowLeft className="h-6 w-6" />
<h2 className="text-[28px] font-semibold text-[#111827]">
Chi tiết phiên khám
</h2>
</div>
{consultation.status === TYPE_STATUS.PendingPrescription &&
canUpdateConsultation && (
<CustomButton
variant="midnightBlue"
fullWidth={false}
icon={<Pencil />}
href={`/consultation/update?_id=${consultation.id}`}
>
Cập nhật tổng quát
</CustomButton>
)}
</div>
<GridColumn col={3}>
{/* 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]">
PHIÊN KHÁM
</p>
<p className="text-[15px] font-medium text-[#202939]">
{consultation.sessionCode}
</p>
</div>
{/* TÊN BỆNH NHÂN */}
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
TÊN BỆNH NHÂN
</p>
<p className="text-[15px] font-medium text-[#202939]">
{consultation.patientName}
</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
? moment(consultation.visitDate).format("DD/MM/YYYY")
: "---"}
</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>
{/* CHỈ SỐ HUYẾT ÁP, CHỈ SỐ MẠCH */}
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
CHỈ SỐ HUYẾT ÁP, CHỈ SỐ MẠCH
</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
</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.createdByName || "---"}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
CÁC CHUYÊN KHOA ĐANG CHỜ KHÁM
</p>
<p className="text-[15px] font-medium text-[#202939]">
{consultation.specialtyClinics
?.filter((specialty) => specialty.specialtyStatus === 1)
.map((specialty) => specialty.specialtyName)
.join(", ") || "---"}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
CÁC CHUYÊN KHOA ĐÃ KHÁM
</p>
<p className="text-[15px] font-medium text-[#202939]">
{consultation.specialtyClinics
?.filter((specialty) => specialty.specialtyStatus === 3)
.map((specialty) => specialty.specialtyName)
.join(", ") || "---"}
</p>
</div>
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
FILE UPLOAD
</p>
{consultation.generalAttachmentUrls?.length > 0 ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-blue-100 bg-blue-50 px-3 py-2 text-sm font-medium text-blue-600 transition hover:bg-blue-100">
<span>📎</span>
<span>Chi tiết các file</span>
</div>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="start"
className="w-[350px] rounded-xl border bg-white p-4 shadow-xl"
>
<div className="space-y-3">
<h4 className="text-sm font-semibold text-gray-800">
Danh sách file đính kèm
</h4>
<div className="max-h-[300px] space-y-2 overflow-y-auto">
{consultation.generalAttachmentUrls.map(
(item, index) => {
const file = getFileInfo(item, index);
const extension =
file.fileName
.split(".")
.pop()
?.toLowerCase() || "";
const isImage = [
"jpg",
"jpeg",
"png",
"gif",
"webp",
"bmp",
"svg",
].includes(extension);
return (
<a
key={index}
href={file.path}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between rounded-lg border border-gray-200 p-3 transition hover:bg-gray-50"
>
<div className="flex min-w-0 items-center gap-3">
<span className="text-xl">
{isImage ? "🖼️" : "📄"}
</span>
<div className="min-w-0">
<p className="truncate text-sm font-medium text-gray-800">
{file.fileName}
</p>
<p className="text-xs text-gray-500 uppercase">
{extension || "FILE"}
</p>
</div>
</div>
<span className="text-xs text-blue-500">
Mở
</span>
</a>
);
},
)}
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<div>Không file nào đưc tải lên</div>
)}
</div>
<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ờ khám",
textColor: "#92400E",
backgroundColor: "#FEF3C7",
},
{
state: TYPE_STATUS.InProgress,
text: "Đang khám tổng quát",
textColor: "#1E3A8A",
backgroundColor: "#DBEAFE",
},
{
state: TYPE_STATUS.ToSpecialty,
text: "Chờ khám chuyên khoa",
textColor: "#6B21A8",
backgroundColor: "#E9D5FF",
},
{
state: TYPE_STATUS.PendingPrescription,
text: "Chờ nhận thuốc",
textColor: "#9A3412",
backgroundColor: "#FED7AA",
},
{
state: TYPE_STATUS.Completed,
text: "Hoàn thành",
textColor: "#166534",
backgroundColor: "#DCFCE7",
},
{
state: TYPE_STATUS.Cancelled,
text: "Đã hủy",
textColor: "#991B1B",
backgroundColor: "#FEE2E2",
},
]}
/>
</div>
</GridColumn>
</div>
</div>
<div>
<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">
Danh sách đơn thuốc
</h2>
</div>
<DataWrapper
data={consultation.aggregatedPrescriptionItems}
loading={consulDetailQuery.isLoading}
title="Không có dữ liệu"
note="Vui lòng thử lại sau"
>
<Table
// Nếu item không thuộc tính id, bạn thể đổi thành (item, index) => String(index)
rowKey={(item: PrescriptionItem) => {
// Tìm vị trí của item hiện tại trong mảng paginatedPrescriptionItems
const idx = paginatedPrescriptionItems.indexOf(item);
return item.id || String(idx);
}}
data={paginatedPrescriptionItems} // Hiển thị dữ liệu trang hiện tại
column={[
{
title: "Tên thuốc",
render: (item: PrescriptionItem) => (
<span className="font-medium text-gray-900">
{item.medicineName}
</span>
),
},
{
title: "Liều dùng",
render: (item: PrescriptionItem) => (
<span>{item.dosage}</span>
),
},
{
title: "Tần suất",
render: (item: PrescriptionItem) => (
<span>{item.frequency}</span>
),
},
{
title: "Thời gian",
render: (item: PrescriptionItem) => (
<span>{item.duration}</span>
),
},
{
title: "Hướng dẫn",
render: (item: PrescriptionItem) => (
<span className="text-sm text-gray-500">
{item.instructions || "---"}
</span>
),
},
]}
/>
</DataWrapper>
{/* Chỉ hiển thị phân trang khi tổng số lượng thuốc lớn hơn 10 phần tử */}
{(consultation.aggregatedPrescriptionItems?.length || 0) > 10 && (
<Pagination
total={consultation.aggregatedPrescriptionItems?.length || 0}
page={prescriptionPage}
pageSize={prescriptionPageSize}
onSetPage={(value) => setPrescriptionPage(value)}
onSetPageSize={(value) => {
setPrescriptionPageSize(value);
setPrescriptionPage(1); // Reset về trang 1 khi thay đổi kích thước hiển thị hàng
}}
dependencies={[prescriptionPage, prescriptionPageSize]}
/>
)}
</div>
</div>
</div>
{/* TAB CHUYÊN KHOA */}
{specialtyTabs.length > 0 && (
<div className="mt-4">
<TabNavLink query="_type" listHref={specialtyTabs} />
</div>
)}
{/* CHI TIẾT CHUYÊN KHOA */}
{selectedSpecialty && (
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<div className="flex items-center justify-between">
<h3 className="mb-6 text-xl font-semibold">
Thông tin chuyên khoa: {selectedSpecialty.specialtyName}
</h3>
{canUpdateConsultation && (
<CustomButton
variant="midnightBlue"
fullWidth={false}
icon={<Pencil />}
href={`/consultation/update-specialty?_id=${consultation.id}&specialtyClinicId=${selectedSpecialty.id}`}
>
Cập nhật chuyên khoa
</CustomButton>
)}
</div>
<GridColumn col={3}>
<div>
<p className="text-sm text-[#697586]">CHUYÊN KHOA</p>
{/* Thêm class để bọc text */}
<p className="break-words overflow-hidden">
{selectedSpecialty.specialtyName || "---"}
</p>
</div>
<div>
<p className="text-sm text-[#697586]">CHẨN ĐOÁN</p>
{/* Thêm class để bọc text */}
<p className="break-words overflow-hidden">
{selectedSpecialty.diagnosis || "---"}
</p>
</div>
<div>
<p className="text-sm text-[#697586]">GHI CHÚ KHÁM</p>
{/* Thêm class để bọc text */}
<p className="break-words overflow-hidden">
{selectedSpecialty.specialtyNotes || "---"}
</p>
</div>
<div>
<p className="text-sm text-[#697586]">THỦ THUẬT</p>
{/* Thêm class để bọc text */}
<p className="break-words overflow-hidden">
{selectedSpecialty.procedureNotes || "---"}
</p>
</div>
<div className="space-y-1">
<p className="text-sm font-medium uppercase tracking-wide text-[#697586]">
FILE UPLOAD
</p>
{selectedSpecialty.attachmentUrls?.length > 0 ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-blue-100 bg-blue-50 px-3 py-2 text-sm font-medium text-blue-600 transition hover:bg-blue-100">
<span>📎</span>
<span>Chi tiết các file</span>
</div>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="start"
className="w-[350px] rounded-xl border bg-white p-4 shadow-xl"
>
<div className="space-y-3">
<h4 className="text-sm font-semibold text-gray-800">
Danh sách file đính kèm
</h4>
<div className="max-h-[300px] space-y-2 overflow-y-auto">
{selectedSpecialty.attachmentUrls.map(
(item, index) => {
const file = getFileInfo(item, index);
const extension =
file.fileName.split(".").pop()?.toLowerCase() ||
"";
const isImage = [
"jpg",
"jpeg",
"png",
"gif",
"webp",
"bmp",
"svg",
].includes(extension);
return (
<a
key={index}
href={file.path}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-between rounded-lg border border-gray-200 p-3 transition hover:bg-gray-50"
>
<div className="flex min-w-0 items-center gap-3">
<span className="text-xl">
{isImage ? "🖼️" : "📄"}
</span>
<div className="min-w-0">
<p className="truncate text-sm font-medium text-gray-800">
{file.fileName}
</p>
<p className="text-xs text-gray-500 uppercase">
{extension || "FILE"}
</p>
</div>
</div>
<span className="text-xs text-blue-500">
Mở
</span>
</a>
);
},
)}
</div>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<div>Không file nào đưc tải lên</div>
)}
</div>
{selectedSpecialty.glassRequired && (
<>
<div>
<p className="text-sm text-[#697586]">CẦN ĐEO KÍNH</p>
<p></p>
</div>
<div>
<p className="text-sm text-[#697586]">LOẠI KÍNH</p>
<p>{selectedSpecialty.glassType || "---"}</p>
</div>
<div>
<p className="text-sm text-[#697586]">ĐÃ PHÁT KÍNH CHƯA</p>
{selectedSpecialty.isGlassesDelivered ? (
<p className="bg-[#DCFCE7] text-[#166534] p-2 border rounded-full w-fit">
Đã phát kính
</p>
) : (
<p className="bg-[#fee2e2] text-[#991B1B] p-2 border rounded-full w-fit">
Chưa phát kính
</p>
)}
</div>
</>
)}
</GridColumn>
</div>
)}
</div>
);
};
export default DetailConsultation;