feat:update all

This commit is contained in:
TuanVT
2026-06-03 16:42:07 +07:00
parent 62ca6dd0f9
commit 77dd05932e
36 changed files with 3098 additions and 693 deletions
@@ -1,8 +1,340 @@
"use client";
import React from "react";
import React, { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import moment from "moment";
import {
Activity,
CalendarDays,
ClipboardList,
Hospital,
Stethoscope,
Users,
} from "lucide-react";
import { QUERY_KEY } from "@/constant/config/enum";
import { httpRequest } from "@/services";
import consultationServices, {
ConsultationItem,
ConsultationResponse,
ReportResponse,
} from "@/services/consultationServices";
import specialtyServices, { SpecialtyItem } from "@/services/specialtyServices";
import DataWrapper from "@/components/customs/DataWrapper";
const MainPageHome = () => {
return <div>MainPageHome</div>;
const today = moment().format("YYYY-MM-DD");
const consultationQuery = useQuery<ConsultationResponse>({
queryKey: [QUERY_KEY.table_list_consultation, "dashboard"],
queryFn: async () => {
const res = await httpRequest<ConsultationResponse>({
showMessageFailed: true,
http: consultationServices.getConsultations({
Page: 1,
PageSize: 100,
SortBy: "visitDate",
Desc: true,
}),
});
return (
res || {
items: [],
page: 1,
pageSize: 100,
total: 0,
totalPages: 0,
}
);
},
});
const reportQuery = useQuery<ReportResponse>({
queryKey: [QUERY_KEY.table_list_report, today],
queryFn: async () => {
const res = await httpRequest<ReportResponse>({
showMessageFailed: true,
http: consultationServices.getConsultationsReport({
FormDate: today,
ToDate: today,
}),
});
return res as ReportResponse;
},
});
const specialtyQuery = useQuery<SpecialtyItem[]>({
queryKey: [QUERY_KEY.list_specialty_lookup],
queryFn: async () => {
const res = await httpRequest<SpecialtyItem[]>({
showMessageFailed: true,
http: specialtyServices.getSpecialty(),
});
return res || [];
},
});
const consultationItems = consultationQuery.data?.items || [];
const consultationReport = reportQuery.data;
const specialtyItems = specialtyQuery.data || [];
const todayConsultations = useMemo(() => {
return consultationItems.filter((item) => {
if (!item.visitDate) return false;
return moment(item.visitDate).format("YYYY-MM-DD") === today;
});
}, [consultationItems, today]);
const statusCounts = useMemo(() => {
return {
total: todayConsultations.length,
waiting: todayConsultations.filter((item) => item.status === 1).length,
inProgress: todayConsultations.filter((item) => item.status === 2).length,
completed: todayConsultations.filter((item) => item.status === 5).length,
cancelled: todayConsultations.filter((item) => item.status === 6).length,
};
}, [todayConsultations]);
const specialtyActiveCounts = useMemo(() => {
const map = new Map<string, number>();
todayConsultations.forEach((item) => {
item.specialtyClinics?.forEach((specialty) => {
const count = map.get(specialty.specialtyId) || 0;
map.set(specialty.specialtyId, count + 1);
});
});
return specialtyItems.map((specialty) => ({
...specialty,
active: map.get(specialty.id) || 0,
}));
}, [todayConsultations, specialtyItems]);
const summaryCards = useMemo(
() => [
{
title: "Tất cả đợt khám",
value: statusCounts.total.toString(),
subtitle: "Số đợt khám trong ngày",
icon: CalendarDays,
color: "bg-blue-50 text-blue-600",
},
{
title: "Đang khám",
value: statusCounts.inProgress.toString(),
subtitle: "Đang khám tổng quát",
icon: Stethoscope,
color: "bg-emerald-50 text-emerald-600",
},
{
title: "Hoàn tất",
value: statusCounts.completed.toString(),
subtitle: "Đã hoàn thành",
icon: ClipboardList,
color: "bg-sky-50 text-sky-600",
},
{
title: "Chờ khám",
value: statusCounts.waiting.toString(),
subtitle: "Đang chờ bác sĩ",
icon: Activity,
color: "bg-orange-50 text-orange-600",
},
],
[statusCounts],
);
const generalInfo = useMemo(
() => [
{
label: "Tổng đợt khám",
value: statusCounts.total.toString(),
icon: Users,
color: "bg-violet-50 text-violet-600",
},
{
label: "Đợt khám chuyên khoa",
value: specialtyActiveCounts
.reduce((sum, item) => sum + item.active, 0)
.toString(),
icon: Hospital,
color: "bg-cyan-50 text-cyan-600",
},
{
label: "Đơn thuốc",
value: consultationReport?.totalPrescriptionsIssued?.toString() || "0",
icon: ClipboardList,
color: "bg-amber-50 text-amber-600",
},
{
label: "Chuyên khoa",
value: specialtyItems.length.toString(),
icon: Activity,
color: "bg-teal-50 text-teal-600",
},
],
[
consultationReport,
statusCounts.total,
specialtyActiveCounts,
specialtyItems.length,
],
);
return (
<section className="space-y-6">
<div className="rounded-[20px] bg-white p-6 shadow-sm border border-gray-100">
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
<div>
<h1 className="text-2xl font-semibold text-slate-900">
Dashboard khám bệnh
</h1>
<p className="mt-2 text-sm text-slate-500">
Theo dõi nhanh các đt khám, chuyên khoa thông tin tổng quát
trong ngày.
</p>
</div>
<div className="rounded-full bg-slate-100 px-4 py-2 text-sm text-slate-700">
Cập nhật: {moment().format("DD/MM/YYYY")}
</div>
</div>
<DataWrapper
loading={
consultationQuery.isLoading ||
reportQuery.isLoading ||
specialtyQuery.isLoading
}
data={summaryCards}
title="Không có dữ liệu"
note="Vui lòng thử lại sau"
>
<div className="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
{summaryCards.map((item) => {
const Icon = item.icon;
return (
<div
key={item.title}
className="rounded-[20px] border border-slate-200 bg-slate-50 p-4"
>
<div className="flex items-center justify-between gap-3">
<div className="text-sm font-medium text-slate-500">
{item.title}
</div>
<div
className={`inline-flex h-10 w-10 items-center justify-center rounded-2xl ${item.color}`}
>
<Icon size={20} />
</div>
</div>
<div className="mt-4 flex items-end justify-between gap-3">
<div>
<p className="text-3xl font-semibold text-slate-900">
{item.value}
</p>
<p className="mt-1 text-sm text-slate-500">
{item.subtitle}
</p>
</div>
</div>
</div>
);
})}
</div>
</DataWrapper>
</div>
<div className="grid gap-6 xl:grid-cols-[1.4fr_1fr]">
<div className="rounded-[20px] bg-white p-6 shadow-sm border border-gray-100">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold text-slate-900">
Chuyên khoa hiện
</h2>
<p className="mt-2 text-sm text-slate-500">
Danh sách chuyên khoa.
</p>
</div>
<div className="rounded-full bg-slate-100 px-3 py-1 text-sm text-slate-700">
{specialtyItems.length} chuyên khoa
</div>
</div>
<div className="mt-6 divide-y divide-slate-200">
{specialtyActiveCounts.map((specialty, idx) => (
<div
key={specialty.id}
className={`flex items-center justify-between py-4 ${idx !== 0 ? "border-t border-slate-200" : ""}`}
>
<div>
<p className="text-base font-medium text-slate-900">
{specialty.name}
</p>
<p className="mt-1 text-sm text-slate-500">
Số chuyên khoa: {specialty.active} ca khám hôm nay
</p>
</div>
<span className="rounded-full bg-slate-100 px-3 py-1 text-sm font-semibold text-slate-700">
{specialty.active}
</span>
</div>
))}
</div>
</div>
<div>
<div className="rounded-[20px] bg-white p-6 shadow-sm border border-gray-100">
<div>
<h2 className="text-xl font-semibold text-slate-900">
Thông tin khám tổng quát
</h2>
<p className="mt-2 text-sm text-slate-500">
Tổng số đt khám, ca khám chuyên khoa đơn thuốc trong ngày.
</p>
</div>
<div className="mt-6 grid gap-4 sm:grid-cols-2">
{generalInfo.map((item) => {
const Icon = item.icon;
return (
<div
key={item.label}
className="rounded-[18px] border border-slate-200 bg-slate-50 p-4"
>
<div className="flex items-center gap-3">
<div
className={`inline-flex h-11 w-11 items-center justify-center rounded-2xl ${item.color}`}
>
<Icon size={20} />
</div>
<div>
<p className="text-sm font-medium text-slate-500">
{item.label}
</p>
<p className="mt-1 text-2xl font-semibold text-slate-900">
{item.value}
</p>
</div>
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
</section>
);
};
export default MainPageHome;
@@ -1,23 +1,31 @@
"use client";
import React from "react";
import { useParams, useRouter } from "next/navigation";
import React, { useEffect, useMemo } 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 } from "lucide-react";
import { ArrowLeft, Pencil } from "lucide-react";
import consultationServices, {
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";
const DetailConsultation = () => {
const params = useParams();
const router = useRouter();
const searchParams = useSearchParams();
const specialtyId = searchParams.get("_type");
const consultationId = params?.id as string;
@@ -48,6 +56,7 @@ const DetailConsultation = () => {
doctorNotes: null,
createdBy: "",
isDeleted: false,
specialtyClinics: [],
}
);
},
@@ -55,6 +64,36 @@ const DetailConsultation = () => {
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]);
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>;
}
@@ -62,215 +101,360 @@ const DetailConsultation = () => {
if (!consultation) {
return <div>Không tìm thấy dữ liệu phiên khám</div>;
}
type PrescriptionItem =
ConsultationDetail["aggregatedPrescriptionItems"][number];
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"
/>
<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>
<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.createdBy || "---"}
</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]">
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/kính",
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>
{/* 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>
<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>
{/* 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>
{/* 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>
{/* 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.createdBy || "---"}
</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>
{/* 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={[
<DataWrapper
data={consultation.aggregatedPrescriptionItems}
loading={consulDetailQuery.isLoading}
title="Không có dữ liệu"
note="Vui lòng thử lại sau"
>
<Table
rowKey={(item) => item.id}
data={consultation.aggregatedPrescriptionItems}
column={[
{
state: TYPE_STATUS.Draft,
text: "Đang chờ khám",
textColor: "#92400E",
backgroundColor: "#FEF3C7",
title: "Tên thuốc",
render: (item: PrescriptionItem) => (
<span>{item.medicineName}</span>
),
},
{
state: TYPE_STATUS.InProgress,
text: "Đang khám tổng quát",
textColor: "#1E3A8A",
backgroundColor: "#DBEAFE",
title: "Liều dùng",
render: (item: PrescriptionItem) => (
<span>{item.dosage}</span>
),
},
{
state: TYPE_STATUS.ToSpecialty,
text: "Chờ khám chuyên khoa",
textColor: "#6B21A8",
backgroundColor: "#E9D5FF",
title: "Tần suất",
render: (item: PrescriptionItem) => (
<span>{item.frequency}</span>
),
},
{
state: TYPE_STATUS.PendingPrescription,
text: "Chờ nhận thuốc/kính",
textColor: "#9A3412",
backgroundColor: "#FED7AA",
title: "Thời gian",
render: (item: PrescriptionItem) => (
<span>{item.duration}</span>
),
},
{
state: TYPE_STATUS.Completed,
text: "Hoàn thành",
textColor: "#166534",
backgroundColor: "#DCFCE7",
},
{
state: TYPE_STATUS.Cancelled,
text: "Đã hủy",
textColor: "#991B1B",
backgroundColor: "#FEE2E2",
title: "Hướng dẫn",
render: (item: PrescriptionItem) => (
<span>{item.instructions || "---"}</span>
),
},
]}
/>
</div>
</DataWrapper>
</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>
<p>{selectedSpecialty.specialtyName || "---"}</p>
</div>
<div>
<p className="text-sm text-[#697586]">CHẨN ĐOÁN</p>
<p>{selectedSpecialty.diagnosis || "---"}</p>
</div>
<div>
<p className="text-sm text-[#697586]">GHI CHÚ KHÁM</p>
<p>{selectedSpecialty.specialtyNotes || "---"}</p>
</div>
<div>
<p className="text-sm text-[#697586]">THỦ THUẬT</p>
<p>{selectedSpecialty.procedureNotes || "---"}</p>
</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>
</>
)}
</GridColumn>
</div>
)}
</div>
);
};
@@ -20,44 +20,44 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import Link from "next/link";
import { Pencil, ClipboardPlus } from "lucide-react";
import { Pencil, ClipboardPlus, Ellipsis } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import Pagination from "@/components/customs/custom-pagination";
import moment from "moment";
import specialtyServices, { SpecialtyItem } from "@/services/specialtyServices";
import FilterCustom from "@/components/customs/FilterCustom";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
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";
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 _status = searchParams.get("_status");
const _specialtyId = searchParams.get("_specialtyId");
/* =========================
STATE
========================= */
const _type = searchParams.get("_type");
const isAllTab = !_type || _type === "all";
const [selectedConsultationId, setSelectedConsultationId] =
useState<string>("");
@@ -66,10 +66,6 @@ const MainPageConsultation = () => {
const [openCompleteDialog, setOpenCompleteDialog] = useState(false);
/* =========================
GET LIST
========================= */
const consultationQuery = useQuery<ConsultationResponse>({
queryKey: [QUERY_KEY.table_list_consultation, _page, _pageSize, _keyword],
@@ -102,10 +98,6 @@ const MainPageConsultation = () => {
},
});
/* =========================
DATA
========================= */
const specialtyQuery = useQuery({
queryKey: [QUERY_KEY.list_specialty_lookup],
queryFn: async () => {
@@ -121,10 +113,20 @@ const MainPageConsultation = () => {
const consultationData = useMemo(() => {
let data = consultationQuery.data?.items || [];
if (_status) {
data = data.filter((item) => item.status === Number(_status));
// =========================
// TAB CHUYÊN KHOA
// =========================
if (_type && _type !== "all") {
data = data.filter((item) =>
item.specialtyClinics?.some(
(specialty) => specialty.specialtyId === _type,
),
);
}
// =========================
// FILTER CHUYÊN KHOA
// =========================
if (_specialtyId) {
data = data.filter((item) =>
item.specialtyClinics?.some(
@@ -133,16 +135,29 @@ const MainPageConsultation = () => {
);
}
return data;
}, [consultationQuery.data, _status, _specialtyId]);
// =========================
// FILTER TRẠNG THÁI
// =========================
if (_status) {
data = data.filter((item) => item.status === Number(_status));
}
/* =========================
UPDATE QUERY
========================= */
return data;
}, [consultationQuery.data, _type, _specialtyId, _status]); // Bỏ _keyword khỏi dependencies vì không dùng lọc client nữa
const page = _page ? Number(_page) : 1;
const pageSize = _pageSize ? Number(_pageSize) : 10;
useEffect(() => {
if (!_type) {
const params = new URLSearchParams(searchParams.toString());
params.set("_type", "all");
router.replace(`${pathname}?${params.toString()}`);
}
}, [_type, pathname, router, searchParams]);
const updateQuery = (key: string, value: string | number) => {
const params = new URLSearchParams(searchParams.toString());
@@ -166,25 +181,32 @@ const MainPageConsultation = () => {
router.push(queryString ? `?${queryString}` : pathname);
};
/* =========================
CLOSE POPUP
========================= */
const consultation = consultationQuery.data;
// const handleClosePopup = () => {
// const params = new URLSearchParams(searchParams.toString());
const specialtyTabs = useMemo(() => {
const specialtyMap = new Map();
// params.delete("_create");
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,
});
}
});
});
// params.delete("_update");
// const queryString = params.toString();
// router.push(queryString ? `?${queryString}` : pathname);
// };
/* =========================
DIALOG
========================= */
return [
{
pathname: "/consultation",
query: "all",
title: "Tất cả",
},
...Array.from(specialtyMap.values()),
];
}, [consultation?.items]);
const handleOpenCancel = (item: ConsultationItem) => {
setSelectedConsultationId(item.id);
@@ -204,10 +226,6 @@ const MainPageConsultation = () => {
setOpenCompleteDialog(false);
};
/* =========================
CANCEL
========================= */
const cancelConsultationMutation = useMutation({
mutationFn: async () => {
return await httpRequest({
@@ -230,10 +248,6 @@ const MainPageConsultation = () => {
},
});
/* =========================
COMPLETE
========================= */
const completeConsultationMutation = useMutation({
mutationFn: async () => {
return await httpRequest({
@@ -258,10 +272,6 @@ const MainPageConsultation = () => {
},
});
/* =========================
CONFIRM
========================= */
const handleConfirmCancel = () => {
cancelConsultationMutation.mutate();
};
@@ -271,303 +281,412 @@ const MainPageConsultation = () => {
};
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}
href="/consultation/create"
>
Tạo phiên khám
</CustomButton>
<div>
<div className="rounded-lg bg-white shadow mb-6 p-6">
<TabNavLink query="_type" listHref={specialtyTabs} />
</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 className="flex flex-col gap-4 rounded-lg bg-white shadow p-6">
{/* HEADER */}
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-black">Phiên khám</h2>
{isAllTab && (
<CustomButton
variant="midnightBlue"
fullWidth={false}
icon={<ClipboardPlus />}
// onClick={handleOpenCreate}
href="/consultation/create"
>
Tạo phiên khám
</CustomButton>
)}
</div>
<FilterCustom<string | null>
name="Chuyên khoa"
value={_specialtyId}
onChange={(value) => updateQuery("_specialtyId", value ?? "")}
listOption={useMemo(() => {
return (
specialtyQuery.data?.map((item) => ({
uuid: item.id,
name: item.name,
})) || []
);
}, [specialtyQuery.data])}
/>
<FilterCustom<number | null>
name="Trạng thái"
value={_status ? Number(_status) : null}
onChange={(value) => updateQuery("_status", value ?? "")}
listOption={[
{
uuid: TYPE_STATUS.Draft,
name: "Đang chờ khám",
},
{
uuid: TYPE_STATUS.InProgress,
name: "Đang khám tổng quát",
},
{
uuid: TYPE_STATUS.ToSpecialty,
name: "Chờ khám chuyên khoa",
},
{
uuid: TYPE_STATUS.PendingPrescription,
name: "Chờ nhận thuốc/kính",
},
{
uuid: TYPE_STATUS.Completed,
name: "Hoàn thành",
},
{
uuid: TYPE_STATUS.Cancelled,
name: "Đã hủy",
},
]}
{/* SEARCH */}
<div
className={clsx(
"flex gap-4",
isAllTab
? "flex-col lg:flex-row lg:items-center lg:justify-between"
: "flex-col",
)}
>
<div className="w-full lg:min-w-[400px]">
<Search
keyword={_keyword ?? ""}
setKeyword={(value) => updateQuery("_keyword", value)}
placeholder="Tìm kiếm phiên khám..."
/>
</div>
{isAllTab && (
<div className="flex flex-col gap-4 sm:flex-row">
<FilterCustom<string | null>
name="Chuyên khoa"
value={_specialtyId}
onChange={(value) => updateQuery("_specialtyId", value ?? "")}
listOption={
specialtyQuery.data?.map((item) => ({
uuid: item.id,
name: item.name,
})) || []
}
/>
<FilterCustom<number | null>
name="Trạng thái"
value={_status ? Number(_status) : null}
onChange={(value) => updateQuery("_status", value ?? "")}
listOption={[
{
uuid: TYPE_STATUS.Draft,
name: "Đang chờ khám",
},
{
uuid: TYPE_STATUS.InProgress,
name: "Đang khám tổng quát",
},
{
uuid: TYPE_STATUS.ToSpecialty,
name: "Chờ khám chuyên khoa",
},
{
uuid: TYPE_STATUS.PendingPrescription,
name: "Chờ nhận thuốc/kính",
},
{
uuid: TYPE_STATUS.Completed,
name: "Hoàn thành",
},
{
uuid: TYPE_STATUS.Cancelled,
name: "Đã hủy",
},
]}
/>
</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={[
{
title: "TÊN BỆNH NHÂN",
render: (item: ConsultationItem) => (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={`/patient/${item.patientId}`}
className="text-blue-500 hover:underline"
>
{item.patientName}
</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: "MÃ PHIÊN KHÁM",
fixedLeft: true,
render: (item: ConsultationItem) => {
const specialty = item.specialtyClinics.find(
(x) => x.specialtyId === _type,
);
const detailHref = isAllTab
? `/consultation/${item.id}`
: `/consultation/${item.id}?_type=${specialty?.id ?? ""}`;
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={detailHref}
className="text-blue-500 hover:underline"
>
{item.sessionCode}
</Link>
</TooltipTrigger>
<TooltipContent>
<p className="rounded-full border bg-gray-400 p-2">
Chi tiết phiên khám
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
},
},
{
title: "NGÀY KHÁM",
render: (item: ConsultationItem) => (
<span>
{item.visitDate
? moment(item.visitDate).format("DD/MM/YYYY")
: "---"}
</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) => {
// ===== TAB CHUYÊN KHOA =====
if (!isAllTab) {
const specialty = item.specialtyClinics.find(
(x) => x.specialtyId === _type,
);
return (
<StateActive
stateActive={specialty?.specialtyStatus}
listState={[
{
state: 1,
text: "Mới được chỉ định",
textColor: "#92400E",
backgroundColor: "#FEF3C7",
},
{
state: 2,
text: "Đang khám",
textColor: "#1E3A8A",
backgroundColor: "#DBEAFE",
},
{
state: 3,
text: "Đã Hoàn thành",
textColor: "#166534",
backgroundColor: "#DCFCE7",
},
]}
/>
);
}
// ===== TAB TẤT CẢ =====
return (
<StateActive
stateActive={item.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/kính",
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",
},
]}
/>
);
},
},
{
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,
);
if (!specialty) return null;
return (
<CustomButton
size="sm"
variant="midnightBlue"
disabled={isDisabled}
href={
!isDisabled
? `/consultation/update-specialty?_id=${item.id}&specialtyClinicId=${specialty.id}`
: undefined
}
>
Cập nhật
</CustomButton>
);
}
// ===== TAB TẤT CẢ =====
return (
<div className="flex items-center gap-2">
<Popover.Root>
<Popover.Trigger asChild>
<button
disabled={isDisabled}
className={clsx(
"flex h-8 w-8 items-center justify-center rounded-full border border-gray-200",
isDisabled
? "cursor-not-allowed opacity-50"
: "hover:bg-gray-100",
)}
>
<Ellipsis size={20} />
</button>
</Popover.Trigger>
{!isDisabled && (
<Popover.Content
side="bottom"
align="end"
sideOffset={8}
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
"
>
<Pencil size={15} />
<span>{specialty.specialtyName}</span>
</Link>
))}
</div>
</Popover.Content>
)}
</Popover.Root>
<CustomButton
size="sm"
variant="red"
disabled={isDisabled}
onClick={() => handleOpenCancel(item)}
>
Hủy
</CustomButton>
<CustomButton
size="sm"
variant="green"
disabled={isDisabled}
onClick={() => handleOpenComplete(item)}
>
Hoàn thành
</CustomButton>
</div>
);
},
},
]}
/>
</DataWrapper>
<Pagination
total={consultationData.length}
page={page}
pageSize={pageSize}
onSetPage={(value) => updateQuery("_page", value)}
onSetPageSize={(value) => updateQuery("_pageSize", value)}
dependencies={[_pageSize, _keyword, _specialtyId, _status]}
/>
</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={[
{
title: "TÊN BỆNH NHÂN",
render: (item: ConsultationItem) => (
<span>{item.patientName}</span>
),
},
{
title: "MÃ PHIÊN KHÁM",
fixedLeft: true,
render: (item: ConsultationItem) => (
<Link
href={`/consultation/${item.id}`}
className="text-blue-500 hover:underline"
>
{item.sessionCode}
</Link>
),
},
{
title: "NGÀY KHÁM",
render: (item: ConsultationItem) => (
<span>
{item.visitDate
? moment(item.visitDate).format("DD/MM/YYYY")
: "---"}
</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: "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: "TRẠNG THÁI",
render: (item: ConsultationItem) => (
<StateActive
stateActive={item.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/kính",
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",
},
]}
/>
),
},
{
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;
// Tìm chuyên khoa có specialtyStatus = 1
const specialtyClinicActive = item.specialtyClinics?.find(
(specialty) => specialty.specialtyStatus === 1,
);
return (
<div className="flex items-center gap-2">
{/* UPDATE */}
<CustomButton
size="sm"
disabled={isDisabled}
icon={
specialtyClinicActive ? undefined : (
<Pencil className="text-blue-400" />
)
}
href={
specialtyClinicActive
? `/consultation/update-specialty?_id=${item.id}&specialtyClinicId=${specialtyClinicActive.id}`
: `/consultation/update?_id=${item.id}`
}
>
{specialtyClinicActive
? `Cập nhật ${specialtyClinicActive.specialtyName}`
: undefined}
</CustomButton>
{/* 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>
<Pagination
total={consultationData.length}
page={page}
pageSize={pageSize}
onSetPage={(value) => updateQuery("_page", value)}
onSetPageSize={(value) => updateQuery("_pageSize", value)}
dependencies={[_pageSize, _keyword, _specialtyId, _status]}
/>
{/* CANCEL DIALOG */}
<CustomDialog
open={openCancelDialog}
@@ -145,6 +145,8 @@ const UpdateConsultation = () => {
queryKey: [QUERY_KEY.chi_tiet_phien_kham, id],
});
router.back();
// Quay lại trang quản lý danh sách phiên khám
router.push(PATH.CONSULTATION || "/consultation");
},
@@ -159,6 +159,8 @@ const UpdateSpecialtyClinicId = () => {
queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId],
});
router.back();
router.push(PATH.CONSULTATION || "/consultation");
},
});
@@ -21,6 +21,12 @@ import StateActive from "@/components/customs/StateActive";
import DataWrapper from "@/components/customs/DataWrapper";
import Table from "@/components/customs/custom-table";
import moment from "moment";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
// import CustomDialog from "@/components/customs/custom-dialog";
const DetailPatient = () => {
@@ -110,12 +116,11 @@ const DetailPatient = () => {
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<div className="flex flex-col gap-5">
<div className="flex items-center justify-between">
<h2 className="text-[28px] flex items-center gap-2 font-semibold text-[#111827]">
<ArrowLeft
onClick={() => router.back()}
className="cursor-pointer"
/>{" "}
{patient?.fullName}
<h2
className="text-[28px] flex items-center gap-2 font-semibold text-[#111827] cursor-pointer"
onClick={() => router.back()}
>
<ArrowLeft /> {patient?.fullName}
</h2>
<div>
<CustomButton
@@ -278,12 +283,24 @@ const DetailPatient = () => {
title: "MÃ PHIÊN KHÁM",
fixedLeft: true,
render: (item: ConsultationItem) => (
<Link
href={`/consultation/${item.id}`}
className="text-blue-500 hover:underline"
>
{item.sessionCode}
</Link>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={`/consultation/${item.id}`}
className="text-blue-500 hover:underline"
>
{item.sessionCode}
</Link>
</TooltipTrigger>
<TooltipContent>
<p className="p-2 bg-gray-400 border rounded-full">
Chi tiết phiên khám
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
),
},
@@ -20,6 +20,13 @@ import React, { useMemo, useState } from "react";
import PopupCreatePatient from "../PopupCreatePatient";
import PopupUpdatePatient from "../PopupUpdatePatient";
import Pagination from "@/components/customs/custom-pagination";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import moment from "moment";
const MainPagePatient = () => {
const router = useRouter();
@@ -61,6 +68,7 @@ const MainPagePatient = () => {
Page: Number(_page || 1),
PageSize: Number(_pageSize || 10),
Search: _keyword || "",
// IdentificationNumber: _keyword || "",
SortBy: "id",
Desc: true,
}),
@@ -171,8 +179,29 @@ const MainPagePatient = () => {
* DATA
*/
const patientData = useMemo(() => {
return patientQuery.data?.items || [];
}, [patientQuery.data]);
const items = patientQuery.data?.items || [];
if (!_keyword) return items;
const keyword = _keyword.toLowerCase().trim();
return items.filter((item) => {
return (
item.fullName?.toLowerCase().includes(keyword) ||
item.identificationNumber?.toLowerCase().includes(keyword) ||
item.phone?.toLowerCase().includes(keyword) ||
item.email?.toLowerCase().includes(keyword)
);
});
}, [patientQuery.data, _keyword]);
// const isReturnWithin14Days = (lastVisitDate?: string | null) => {
// if (!lastVisitDate) return false;
// const diffDays = moment().diff(moment(lastVisitDate), "days");
// return diffDays <= 14;
// };
return (
<div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow">
@@ -217,12 +246,34 @@ const MainPagePatient = () => {
title: "TÊN BỆNH NHÂN",
render: (item: PatientItem) => (
<Link
href={`/patient/${item.id}`}
className="text-blue-500 hover:underline"
>
{item.fullName || "---"}
</Link>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={`/patient/${item.id}`}
className="text-blue-500 hover:underline"
>
{item.fullName}
</Link>
{/* <Link
href={`/patient/${item.id}`}
className={
isReturnWithin14Days(item.lastVisitDate)
? "text-red-500 font-semibold hover:underline"
: "text-blue-500 hover:underline"
}
>
{item.fullName}
</Link> */}
</TooltipTrigger>
<TooltipContent>
<p className="p-2 bg-gray-400 border rounded-full">
Chi tiết bệnh nhân
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
),
},
@@ -56,6 +56,7 @@ const CreatePrescription = () => {
const [loading, setLoading] = useState<boolean>(false);
const queryKeys = [QUERY_KEY.table_list_consultation];
const [file, setFile] = useState<File | null>(null);
const [folderName, setFolderName] = useState("prescriptions");
const [form, setForm] = useState<PrescriptionForm>({
examinationSessionSpecialtyId: "",
@@ -181,7 +182,7 @@ const CreatePrescription = () => {
const handleAddMedicine = () => {
setForm((prev) => ({
...prev,
items: [...prev.items, { ...defaultMedicine }],
items: [{ ...defaultMedicine }, ...prev.items],
}));
};
@@ -228,7 +229,7 @@ const CreatePrescription = () => {
setLoading(true);
const uploadResponse: any = await prescriptionServices.uploadImage(
file,
"prescriptions",
folderName,
);
console.log("KẾT QUẢ PHẢN HỒI GỐC TỪ API UPLOAD:", uploadResponse);
@@ -412,8 +413,9 @@ const CreatePrescription = () => {
<div className="flex flex-col gap-6">
{form.items.map((medicine, index) => (
// Lưu ý: Nếu có trường id duy nhất thì nên dùng thay cho index ở đây
<div
key={index}
key={`${index}-${form.items.length}`}
className="rounded-2xl border border-gray-200 p-5"
>
{/* TITLE */}
@@ -5,13 +5,18 @@ import { httpRequest } from "@/services";
import prescriptionServices, {
PrescriptionDetail,
} from "@/services/prescriptionServices";
import consultationServices, {
ConsultationItem,
} from "@/services/consultationServices";
import { useQuery } from "@tanstack/react-query";
import Image from "next/image";
import { useParams } from "next/navigation";
import { useParams, useRouter } from "next/navigation";
import React, { useState } from "react";
import CustomPopup from "@/components/customs/custom-popup";
import { ArrowLeft } from "lucide-react";
const DetailPrescription = () => {
const router = useRouter();
const params = useParams();
const PrescriptionId = params?.id as string;
const [openImagePreview, setOpenImagePreview] = useState(false);
@@ -31,7 +36,7 @@ const DetailPrescription = () => {
return (
res || {
id: "",
examinationSessionId: "",
examinationSessionSpecialtyId: "",
prescriptionCode: "",
prescriptionImg: null,
notes: "",
@@ -44,9 +49,46 @@ const DetailPrescription = () => {
const data = prescriptionDetailQuery.data;
const specialtyListQuery = useQuery<ConsultationItem[]>({
queryKey: [QUERY_KEY.examination_specialty_list],
enabled: !!data?.examinationSessionSpecialtyId,
queryFn: async () => {
const res = await httpRequest<{
items: ConsultationItem[];
}>({
showMessageFailed: false,
http: consultationServices.getConsultations({ PageSize: 1000 }),
});
return res?.items || [];
},
});
const specialtyName = React.useMemo(() => {
const specialtyId = data?.examinationSessionSpecialtyId;
if (!specialtyId) return "";
const items = specialtyListQuery.data || [];
for (const session of items) {
const found = session.specialtyClinics?.find((s) => s.id === specialtyId);
if (found) return found.specialtyName || "";
}
return "";
}, [data?.examinationSessionSpecialtyId, specialtyListQuery.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>
<div
className="flex items-center gap-1 cursor-pointer"
onClick={() => router.back()}
>
<ArrowLeft />
<h2 className="text-xl font-semibold text-[#111827]">
Chi tiết đơn thuốc
</h2>
</div>
{/* LOADING */}
{prescriptionDetailQuery.isLoading && <p>Đang tải...</p>}
@@ -64,11 +106,12 @@ const DetailPrescription = () => {
</div>
<div>
<strong>Id phiên khám:</strong> {data.examinationSessionId}
<strong>Chuyên khoa:</strong>{" "}
{specialtyName || data.examinationSessionSpecialtyId || "---"}
</div>
<div>
<strong>Ghi chú:</strong> {data.notes || "-"}
<strong>Ghi chú:</strong> {data.notes || "---"}
</div>
{/* IMAGE */}
@@ -10,8 +10,14 @@ import prescriptionServices, {
PrescriptionItem,
PrescriptionResponse,
} from "@/services/prescriptionServices";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@radix-ui/react-tooltip";
import { useQuery } from "@tanstack/react-query";
import { Pencil, Pill, Trash2 } from "lucide-react";
import { Eye, Pencil, Pill, Trash2 } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
@@ -121,12 +127,24 @@ const MainPrescription = () => {
{
title: "MÃ ĐƠN THUỐC",
render: (item: any) => (
<Link
href={`/prescription/${item.id}`}
className="text-blue-500 hover:underline"
>
{item.prescriptionCode}
</Link>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Link
href={`/prescription/${item.id}`}
className="text-blue-500 hover:underline"
>
{item.prescriptionCode}
</Link>
</TooltipTrigger>
<TooltipContent>
<p className="p-2 bg-gray-400 border rounded-full">
Chi tiết đơn thuốc
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
),
},
@@ -162,20 +180,20 @@ const MainPrescription = () => {
},
// ================= ACTION =================
// {
// fixedRight: true,
// title: "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>
// ),
// },
render: (item: any) => (
<div className="flex items-center gap-2">
<CustomButton
size="sm"
icon={<Eye className="text-blue-400" />}
href={`${PATH.PRESCRIPTION}/${item.id}`}
/>
</div>
),
},
]}
/>
</DataWrapper>
@@ -1,30 +1,49 @@
"use client";
import React, { useMemo, useState } from "react";
import moment from "moment";
import { useQuery } from "@tanstack/react-query";
import { Users, Stethoscope, Pill } from "lucide-react";
import Table from "@/components/customs/custom-table";
import DataWrapper from "@/components/customs/DataWrapper";
import { QUERY_KEY } from "@/constant/config/enum";
import { QUERY_KEY, TYPE_DATE } from "@/constant/config/enum";
import { httpRequest } from "@/services";
import consultationServices, {
ReportResponse,
} from "@/services/consultationServices";
import { useQuery } from "@tanstack/react-query";
import moment from "moment";
// import { usePathname, useRouter, useSearchParams } from "next/navigation";
import React, { useMemo } from "react";
import FilterDateRange from "@/components/utils/FilterDateRage";
const MainPageStatistical = () => {
// const router = useRouter();
// const pathname = usePathname();
// const searchParams = useSearchParams();
const [date, setDate] = useState<{
from: Date | null;
to: Date | null;
} | null>(null);
const [typeDate, setTypeDate] = useState<TYPE_DATE>(TYPE_DATE.TODAY);
// const _page = searchParams.get("_page");
const isInvalidDate =
!!date?.from && moment(date.from).isAfter(moment().endOf("day"));
// const _pageSize = searchParams.get("_pageSize");
const reportQuery = useQuery<ReportResponse>({
queryKey: [QUERY_KEY.table_list_report],
queryKey: [
QUERY_KEY.table_list_report,
date?.from?.toISOString(),
date?.to?.toISOString(),
],
enabled: !!date?.from && !!date?.to && !isInvalidDate,
queryFn: async () => {
const toDate = moment().format("YYYY-MM-DD");
const fromDate = moment().subtract(14, "days").format("YYYY-MM-DD");
const fromDate = moment(date?.from).format("YYYY-MM-DD");
const toDate = moment(date?.to).format("YYYY-MM-DD");
console.log({
FormDate: fromDate,
ToDate: toDate,
});
const res = await httpRequest<ReportResponse>({
showMessageFailed: true,
@@ -38,68 +57,132 @@ const MainPageStatistical = () => {
},
});
const report = reportQuery.data;
const reportData = useMemo(() => {
return reportQuery.data?.specialtyDetails || [];
}, [reportQuery.data]);
// const page = _page ? Number(_page) : 1;
// const pageSize = _pageSize ? Number(_pageSize) : 10;
// 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 report?.specialtyDetails || [];
}, [report]);
return (
<div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow">
<div className="flex flex-col gap-6">
{/* HEADER */}
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold text-black">
Thống các phiên khám
</h2>
<div className="rounded-xl bg-white p-6 shadow">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-semibold">Thống phiên khám</h2>
<FilterDateRange
date={date}
setDate={setDate}
typeDate={typeDate}
setTypeDate={setTypeDate}
/>
</div>
</div>
<DataWrapper
data={reportData}
loading={reportQuery.isLoading}
title="Không có dữ liệu"
note="Vui lòng thử lại sau"
>
<Table
rowKey={(item) => item.specialtyId}
{/* CARDS */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<div className="rounded-xl bg-white p-5 shadow">
<div className="mb-3 flex items-center justify-between">
<Users size={22} />
</div>
<p className="text-sm text-gray-500">Tổng lượt khám</p>
<h3 className="mt-2 text-3xl font-bold">
{report?.totalSessions || 0}
</h3>
</div>
<div className="rounded-xl bg-white p-5 shadow">
<div className="mb-3 flex items-center justify-between">
<Stethoscope size={22} />
</div>
<p className="text-sm text-gray-500">Lượt khám chuyên khoa</p>
<h3 className="mt-2 text-3xl font-bold">
{report?.totalSpecialtySubSessions || 0}
</h3>
</div>
<div className="rounded-xl bg-white p-5 shadow">
<div className="mb-3 flex items-center justify-between">
<Pill size={22} />
</div>
<p className="text-sm text-gray-500">Đơn thuốc đã cấp</p>
<h3 className="mt-2 text-3xl font-bold">
{report?.totalPrescriptionsIssued || 0}
</h3>
</div>
</div>
{/* BẢNG THỐNG KÊ */}
<div className="rounded-xl bg-white p-6 shadow">
<h3 className="mb-4 text-lg font-semibold">
Thống theo chuyên khoa
</h3>
<DataWrapper
data={reportData}
column={[
{
title: "MÃ CHUYÊN KHOA",
render: (item) => <span>{item.specialtyId}</span>,
},
{
title: "TÊN CHUYÊN KHOA",
render: (item) => <span>{item.specialtyName}</span>,
},
{
title: "SỐ CA KHÁM",
render: (item) => <span>{item.totalCases}</span>,
},
]}
/>
</DataWrapper>
{/* <Pagination
total={reportData.length}
page={page}
pageSize={pageSize}
onSetPage={(value) => updateQuery("_page", value)}
onSetPageSize={(value) => updateQuery("_pageSize", value)}
dependencies={[_pageSize]}
/> */}
loading={reportQuery.isLoading}
title="Không có dữ liệu"
note="Vui lòng thử lại sau"
>
<Table
rowKey={(item) => item.specialtyId}
data={reportData}
column={[
{
title: "MÃ CHUYÊN KHOA",
render: (item) => <span>{item.specialtyId}</span>,
},
{
title: "TÊN CHUYÊN KHOA",
render: (item) => <span>{item.specialtyName}</span>,
},
{
title: "SỐ CA KHÁM",
render: (item) => (
<span className="font-semibold">{item.totalCases}</span>
),
},
]}
/>
</DataWrapper>
</div>
{/* BÁO CÁO TỔNG HỢP */}
<div className="rounded-xl bg-white p-6 shadow">
<h3 className="mb-4 text-lg font-semibold">Báo cáo tổng hợp</h3>
<div className="space-y-3 text-[15px]">
<p>
Số lượt khám bệnh nhân đo:
<strong> {report?.totalSpecialtySubSessions || 0}</strong> lượt
người dân đưc khám chuyên khoa, cụ thể như sau:
</p>
<ul className="list-disc space-y-2 pl-6">
{reportData.map((item) => (
<li key={item.specialtyId}>
<strong>{item.specialtyName}</strong>: {item.totalCases} ca
</li>
))}
</ul>
<p>
Tổng số đơn thuốc đã cấp:
<strong> {report?.totalPrescriptionsIssued || 0}</strong> đơn.
</p>
{/* <p>
Số suất quà được phát:
<strong> {report?.totalGiftPackagesIssued || 0}</strong> suất.
</p> */}
</div>
</div>
</div>
);
};