341 lines
11 KiB
TypeScript
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 và 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 có
|
|
</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 và đơ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;
|