metms_fe/src/components/page/Home/MainPageHome/MainPageHome.tsx
2026-06-04 15:59:39 +07:00

341 lines
11 KiB
TypeScript

"use client";
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 = () => {
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({
FromDate: 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;