feat:update-project

This commit is contained in:
TuanVT
2026-08-17 15:48:39 +07:00
parent 684d7d2490
commit 94e7975cd0
11 changed files with 453 additions and 122 deletions
@@ -28,6 +28,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import { SPECIALTY_DISPLAY_NAME, SPECIALTY_ORDER } from "@/constant/config";
const DetailConsultation = () => {
const params = useParams();
@@ -37,9 +38,12 @@ const DetailConsultation = () => {
const consultationId = params?.id as string;
const [openTooltip, setOpenTooltip] = useState(false);
const [prescriptionPage, setPrescriptionPage] = useState<number>(1);
const [prescriptionPageSize, setPrescriptionPageSize] = useState<number>(10);
const normalize = (text?: string) => (text ?? "").trim().toLowerCase();
const consulDetailQuery = useQuery<ConsultationDetail>({
queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId],
@@ -79,15 +83,72 @@ const DetailConsultation = () => {
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]);
const specialtyTabs = useMemo(() => {
return (
consultation?.specialtyClinics?.map((item) => ({
pathname: `/consultation/${consultationId}`,
query: item.id,
title: item.specialtyName,
})) || []
);
if (!consultation?.specialtyClinics?.length) return [];
//------------------------------------
// Chuẩn hóa tên
//------------------------------------
const specialties = consultation.specialtyClinics.map((item) => ({
...item,
displayName:
SPECIALTY_DISPLAY_NAME[item.specialtyName] ?? item.specialtyName,
}));
//------------------------------------
// Ưu tiên chuyên khoa đã khám
//------------------------------------
specialties.sort((a, b) => {
const aVisited = a.specialtyStatus === 3;
const bVisited = b.specialtyStatus === 3;
// Đã khám lên trước
if (aVisited !== bVisited) {
return aVisited ? -1 : 1;
}
// Sau đó theo SPECIALTY_ORDER
const indexA = SPECIALTY_ORDER.findIndex(
(x) => normalize(x) === normalize(a.displayName),
);
const indexB = SPECIALTY_ORDER.findIndex(
(x) => normalize(x) === normalize(b.displayName),
);
if (indexA === -1 && indexB === -1) {
return a.displayName.localeCompare(b.displayName);
}
if (indexA === -1) return 1;
if (indexB === -1) return -1;
return indexA - indexB;
});
return specialties.map((item) => ({
pathname: `/consultation/${consultationId}`,
query: item.id,
title: item.displayName,
}));
}, [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 || [];
@@ -153,18 +214,18 @@ const DetailConsultation = () => {
return (
<div className="flex flex-col gap-4">
<div className="flex flex-row gap-4">
<GridColumn col={2}>
<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 flex-col items-start gap-4 lg:flex-row lg:items-center lg:justify-between lg:gap-0">
<div
className="flex cursor-pointer items-center gap-3"
onClick={() => router.back()}
>
<ArrowLeft className="h-6 w-6" />
<ArrowLeft className="h-6 w-6 shrink-0" />
<h2 className="text-[28px] font-semibold text-[#111827]">
<h2 className="text-[22px] font-semibold text-[#111827] sm:text-[28px]">
Chi tiết phiên khám
</h2>
</div>
@@ -265,7 +326,7 @@ const DetailConsultation = () => {
{/* CHẨN ĐOÁN */}
<div className="space-y-1">
<p className="text-[14px] font-medium text-[#697586]">
CHUẨN ĐOÁN
CHẨN ĐOÁN
</p>
<p className="text-[15px] font-medium text-[#202939]">
@@ -338,18 +399,24 @@ const DetailConsultation = () => {
</p>
{consultation.generalAttachmentUrls?.length > 0 ? (
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltip} onOpenChange={setOpenTooltip}>
<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">
<div
onClick={() => setOpenTooltip((prev) => !prev)}
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 active:bg-blue-200"
>
<span>📎</span>
<span>Chi tiết các file</span>
<span>
Chi tiết các file (
{consultation.generalAttachmentUrls.length})
</span>
</div>
</TooltipTrigger>
<TooltipContent
side="bottom"
align="start"
className="w-[350px] rounded-xl border bg-white p-4 shadow-xl"
className="w-[calc(100vw-32px)] sm: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">
@@ -383,10 +450,10 @@ const DetailConsultation = () => {
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"
className="flex items-center justify-between rounded-lg border border-gray-200 p-3 transition hover:bg-gray-50 active:bg-gray-100"
>
<div className="flex min-w-0 items-center gap-3">
<span className="text-xl">
<span className="text-xl shrink-0">
{isImage ? "🖼️" : "📄"}
</span>
@@ -401,7 +468,7 @@ const DetailConsultation = () => {
</div>
</div>
<span className="text-xs text-blue-500">
<span className="text-xs font-medium text-blue-500 shrink-0 ml-2">
Mở
</span>
</a>
@@ -414,7 +481,9 @@ const DetailConsultation = () => {
</Tooltip>
</TooltipProvider>
) : (
<div>Không file nào đưc tải lên</div>
<div className="text-sm text-gray-500">
Không file nào đưc tải lên
</div>
)}
</div>
@@ -546,7 +615,7 @@ const DetailConsultation = () => {
)}
</div>
</div>
</div>
</GridColumn>
{/* TAB CHUYÊN KHOA */}
{specialtyTabs.length > 0 && (
<div className="mt-4">
@@ -556,8 +625,8 @@ const DetailConsultation = () => {
{/* 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">
<div className="flex flex-col items-start mb-6 gap-4 lg:flex-row lg:items-center lg:justify-between lg:gap-0">
<h3 className=" text-xl font-semibold">
Thông tin chuyên khoa: {selectedSpecialty.specialtyName}
</h3>
{canUpdateConsultation && (
@@ -611,9 +680,12 @@ const DetailConsultation = () => {
</p>
{selectedSpecialty.attachmentUrls?.length > 0 ? (
<TooltipProvider>
<Tooltip>
<Tooltip open={openTooltip} onOpenChange={setOpenTooltip}>
<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">
<div
onClick={() => setOpenTooltip((prev) => !prev)}
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 active:bg-blue-200"
>
<span>📎</span>
<span>Chi tiết các file</span>
</div>
@@ -622,7 +694,7 @@ const DetailConsultation = () => {
<TooltipContent
side="bottom"
align="start"
className="w-[350px] rounded-xl border bg-white p-4 shadow-xl"
className="w-[calc(100vw-32px)] sm: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">
@@ -654,10 +726,10 @@ const DetailConsultation = () => {
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"
className="flex items-center justify-between rounded-lg border border-gray-200 p-3 transition hover:bg-gray-50 active:bg-gray-100"
>
<div className="flex min-w-0 items-center gap-3">
<span className="text-xl">
<span className="text-xl shrink-0">
{isImage ? "🖼️" : "📄"}
</span>
@@ -672,7 +744,7 @@ const DetailConsultation = () => {
</div>
</div>
<span className="text-xs text-blue-500">
<span className="ml-2 text-xs text-blue-500 shrink-0">
Mở
</span>
</a>
@@ -685,7 +757,9 @@ const DetailConsultation = () => {
</Tooltip>
</TooltipProvider>
) : (
<div>Không file nào đưc tải lên</div>
<div className="text-sm text-gray-500">
Không file nào đưc tải lên
</div>
)}
</div>
@@ -41,6 +41,7 @@ import * as Popover from "@radix-ui/react-popover";
import clsx from "clsx";
import TabNavLink from "@/components/customs/custom-tabs";
import { removeVietnameseTones } from "@/common/funcs/optionConvert";
import useSpecialtyTabs from "@/hooks/useSpecialtyTabs";
const MainPageConsultation = () => {
const router = useRouter();
@@ -98,6 +99,10 @@ const MainPageConsultation = () => {
},
});
const { tabs: specialtyTabs } = useSpecialtyTabs(
consultationQuery.data?.items ?? [],
);
const specialtyQuery = useQuery({
queryKey: [QUERY_KEY.list_specialty_lookup],
queryFn: async () => {
@@ -183,30 +188,30 @@ const MainPageConsultation = () => {
const consultation = consultationQuery.data;
const specialtyTabs = useMemo(() => {
const specialtyMap = new Map();
// const specialtyTabs = useMemo(() => {
// const specialtyMap = new Map();
consultation?.items?.forEach((consultationItem) => {
consultationItem.specialtyClinics?.forEach((specialty) => {
if (!specialtyMap.has(specialty.specialtyId)) {
specialtyMap.set(specialty.specialtyId, {
pathname: "/consultation",
query: specialty.specialtyId,
title: specialty.specialtyName,
});
}
});
});
// consultation?.items?.forEach((consultationItem) => {
// consultationItem.specialtyClinics?.forEach((specialty) => {
// if (!specialtyMap.has(specialty.specialtyId)) {
// specialtyMap.set(specialty.specialtyId, {
// pathname: "/consultation",
// query: specialty.specialtyId,
// title: specialty.specialtyName,
// });
// }
// });
// });
return [
{
pathname: "/consultation",
query: "all",
title: "Tất cả",
},
...Array.from(specialtyMap.values()),
];
}, [consultation?.items]);
// return [
// {
// pathname: "/consultation",
// query: "all",
// title: "Tất cả",
// },
// ...Array.from(specialtyMap.values()),
// ];
// }, [consultation?.items]);
const handleOpenCancel = (item: ConsultationItem) => {
setSelectedConsultationId(item.id);
@@ -381,7 +386,6 @@ const MainPageConsultation = () => {
column={[
{
title: "TÊN BỆNH NHÂN",
render: (item: ConsultationItem) => (
<TooltipProvider>
<Tooltip>
@@ -393,9 +397,8 @@ const MainPageConsultation = () => {
{item.patientName}
</Link>
</TooltipTrigger>
<TooltipContent>
<p className="p-2 bg-gray-400 border rounded-full">
<p className="rounded-full border bg-gray-400 p-2">
Chi tiết bệnh nhân
</p>
</TooltipContent>
@@ -427,7 +430,6 @@ const MainPageConsultation = () => {
{item.sessionCode}
</Link>
</TooltipTrigger>
<TooltipContent>
<p className="rounded-full border bg-gray-400 p-2">
Chi tiết phiên khám
@@ -450,41 +452,72 @@ const MainPageConsultation = () => {
),
},
{
// 🔴 ẨN "DẤU HIỆU" khi KHÔNG PHẢI Tab All (!isAllTab)
isAllTab && {
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: "CHẨN ĐOÁN",
render: (item: ConsultationItem) => {
if (!isAllTab) {
const specialty = item.specialtyClinics.find(
(x) => x.specialtyId === _type,
);
return <span>{specialty?.diagnosis || "---"}</span>;
}
return <span>{item.diagnosis || "---"}</span>;
},
},
{
title: "KẾ HOẠCH ĐIỀU TRỊ",
// 🔴 ẨN "DẤU HIỆU" khi KHÔNG PHẢI Tab All (!isAllTab)
// !isAllTab && {
// title: "BÁC SĨ KHÁM",
// render: (item: ConsultationItem) => {
// const doctorNames = item.doctors?.join(", ");
// return <span>{doctorNames || "---"}</span>;
// },
// },
// 🔴 ẨN "KẾ HOẠCH ĐIỀU TRỊ" khi KHÔNG PHẢI Tab All (!isAllTab)
isAllTab && {
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: isAllTab ? "GHI CHÚ BÁC SĨ" : "GHI CHÚ KHÁM",
render: (item: ConsultationItem) => {
if (!isAllTab) {
const specialty = item.specialtyClinics.find(
(x) => x.specialtyId === _type,
);
return <span>{specialty?.specialtyNotes || "---"}</span>;
}
return <span>{item.doctorNotes || "---"}</span>;
},
},
// 🔴 ẨN "THỦ THUẬT" khi LÀ Tab All (isAllTab)
!isAllTab && {
title: "THỦ THUẬT",
render: (item: ConsultationItem) => {
const specialty = item.specialtyClinics.find(
(x) => x.specialtyId === _type,
);
return <span>{specialty?.procedureNotes || "---"}</span>;
},
},
{
title: "TRẠNG THÁI",
render: (item: ConsultationItem) => {
// ===== TAB CHUYÊN KHOA =====
if (!isAllTab) {
const specialty = item.specialtyClinics.find(
(x) => x.specialtyId === _type,
@@ -517,7 +550,6 @@ const MainPageConsultation = () => {
);
}
// ===== TAB TẤT CẢ =====
return (
<StateActive
stateActive={item.status}
@@ -563,19 +595,15 @@ const MainPageConsultation = () => {
);
},
},
{
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;
// ===== TAB CHUYÊN KHOA =====
if (!isAllTab) {
const specialty = item.specialtyClinics.find(
(x) => x.specialtyId === _type,
@@ -599,14 +627,7 @@ const MainPageConsultation = () => {
);
}
// ===== TAB TẤT CẢ =====
return (
/*
Chỉnh sửa tại đây:
- flex-col: xếp dọc trên màn mobile
- sm:flex-row: quay lại xếp ngang trên tablet / desktop (>= 640px)
- w-full sm:w-auto: mở rộng full chiều rộng ô khi ở màn mobile cho cân đối
*/
<div className="flex flex-col sm:flex-row items-center justify-center gap-2 w-full sm:w-auto">
<Popover.Root>
<Popover.Trigger asChild>
@@ -628,27 +649,14 @@ const MainPageConsultation = () => {
side="bottom"
align="end"
sideOffset={8}
className="
z-50
min-w-[300px]
rounded-xl
border
bg-white
p-2
shadow-xl
"
className="z-50 min-w-[300px] rounded-xl border bg-white p-2 shadow-xl"
>
<div className="space-y-1">
{item.specialtyClinics.map((specialty) => (
<Link
key={specialty.id}
href={`/consultation/update-specialty?_id=${item.id}&specialtyClinicId=${specialty.id}`}
className="
flex items-center gap-2
rounded-lg px-3 py-2
text-sm
hover:bg-blue-50
"
className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm hover:bg-blue-50"
>
<Pencil size={15} />
<span>{specialty.specialtyName}</span>
@@ -682,7 +690,7 @@ const MainPageConsultation = () => {
);
},
},
]}
].filter(Boolean)} // 💡 Lọc bỏ các giá trị false để ẩn cột
/>
</DataWrapper>
<Pagination
@@ -150,6 +150,47 @@ const TableHistoryPatient = () => {
<span>{item.diagnosis || "---"}</span>
),
},
{
title: "KẾ HOẠCH ĐIỀU TRỊ",
render: (item: ConsultationItem) => (
<span>{item.treatmentPlan || "---"}</span>
),
},
{
title: "CÁC CHUYÊN KHOA ĐANG CHỜ KHÁM",
render: (item: ConsultationItem) => {
const waitingSpecialties = item.specialtyClinics
?.filter((specialty) => specialty.specialtyStatus === 1)
.map((specialty) => specialty.specialtyName)
.join(", ");
return <span>{waitingSpecialties || "---"}</span>;
},
},
{
title: "CÁC CHUYÊN KHOA ĐÃ KHÁM",
render: (item: ConsultationItem) => {
const completedSpecialties = item.specialtyClinics
?.filter((specialty) => specialty.specialtyStatus === 3)
.map((specialty) => specialty.specialtyName)
.join(", ");
return <span>{completedSpecialties || "---"}</span>;
},
},
{
title: "BÁC SĨ KHÁM",
render: (item: ConsultationItem) => {
const doctorNames = item.doctors?.join(", ");
return <span>{doctorNames || "---"}</span>;
},
},
{
title: "TRẠNG THÁI",
render: (item: ConsultationItem) => (
@@ -324,7 +324,7 @@ const DetailPatient = () => {
},
{
title: "CHUẨN ĐOÁN",
title: "CHẨN ĐOÁN",
render: (item: ConsultationItem) => (
<span>{item.diagnosis || "---"}</span>
@@ -339,13 +339,6 @@ const DetailPatient = () => {
),
},
{
title: "GHI CHÚ BÁC SĨ",
render: (item: ConsultationItem) => (
<span>{item.doctorNotes || "---"}</span>
),
},
{
title: "CÁC CHUYÊN KHOA ĐANG CHỜ KHÁM",
@@ -370,6 +363,15 @@ const DetailPatient = () => {
return <span>{completedSpecialties || "---"}</span>;
},
},
{
title: "BÁC SĨ KHÁM",
render: (item: ConsultationItem) => {
const doctorNames = item.doctors?.join(", ");
return <span>{doctorNames || "---"}</span>;
},
},
{
title: "TRẠNG THÁI",
fixedRight: true,
@@ -509,7 +509,7 @@ const CreatePrescription = () => {
<InputForm
label={
<span>
Thời gian dùng<span style={{ color: "red" }}>*</span>
Số lượng thuốc<span style={{ color: "red" }}>*</span>
</span>
}
isRequired
@@ -14,6 +14,7 @@ import { useParams, useRouter } from "next/navigation";
import React, { useState } from "react";
import CustomPopup from "@/components/customs/custom-popup";
import { ArrowLeft } from "lucide-react";
import Link from "next/link";
const DetailPrescription = () => {
const router = useRouter();
@@ -104,16 +105,25 @@ const DetailPrescription = () => {
<div>
<strong> đơn thuốc:</strong> {data.prescriptionCode}
</div>
<div>
<strong> bệnh nhân:</strong>
<Link
href={`/patient/${data.patientId}`}
className="text-blue-500 hover:underline"
>
{data.patientCode || "---"}
</Link>
</div>{" "}
<div>
<strong>Tên bệnh nhân:</strong> {data.patientName || "---"}
</div>
<div>
<strong>Chuyên khoa:</strong>{" "}
{specialtyName || data.examinationSessionSpecialtyId || "---"}
</div>
<div>
<strong>Ghi chú:</strong> {data.notes || "---"}
</div>
{/* IMAGE */}
<div>
<strong>nh đơn thuốc:</strong>
@@ -152,7 +162,6 @@ const DetailPrescription = () => {
)}
</div>
</div>
{/* ITEMS */}
<div>
<strong>Danh sách thuốc:</strong>
@@ -171,7 +180,7 @@ const DetailPrescription = () => {
<b>Tần suất:</b> {item.frequency}
</p>
<p>
<b>Thời gian:</b> {item.duration}
<b>Số lượng thuốc:</b> {item.quantity}
</p>
<p>
<b>Hướng dẫn:</b> {item.instructions}
@@ -148,12 +148,37 @@ const MainPrescription = () => {
),
},
// ================= NOTES =================
{
title: "GHI CHÚ",
render: (item: any) => <span>{item.notes || "-"}</span>,
},
title: "MÃ BỆNH NHÂN",
render: (item: any) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={`/patient/${item.patientId}`}
className="text-blue-500 hover:underline"
>
{item.patientCode}
</Link>
</TooltipTrigger>
<TooltipContent>
<p className="p-2 bg-gray-400 border rounded-full">
Chi tiết bệnh nhân
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
),
},
{
title: "TÊN BỆNH NHÂN",
render: (item: any) => <span>{item.patientName || "-"}</span>,
},
{
title: "TÊN CHUYÊN KHOA",
render: (item: any) => <span>{item.specialtyName || "-"}</span>,
},
// ================= IMAGE =================
{
title: "ẢNH ĐƠN THUỐC",
@@ -174,11 +199,15 @@ const MainPrescription = () => {
},
// ================= ITEMS COUNT =================
// {
// title: "SỐ THUỐC",
// render: (item: any) => <span>{item.items?.length || 0}</span>,
// },
// ================= NOTES =================
{
title: "SỐ THUỐC",
render: (item: any) => <span>{item.items?.length || 0}</span>,
title: "GHI CHÚ",
render: (item: any) => <span>{item.notes || "-"}</span>,
},
// ================= ACTION =================
{
fixedRight: true,