diff --git a/package.json b/package.json index 2ef519f..7678d58 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "eslint" }, "dependencies": { + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-tooltip": "^1.2.8", "@reduxjs/toolkit": "^2.12.0", "@tanstack/react-query": "^5.100.11", "axios": "^1.16.1", @@ -21,6 +23,7 @@ "nprogress": "^0.2.0", "react": "19.2.4", "react-dom": "19.2.4", + "react-moment": "^2.0.2", "react-redux": "^9.3.0", "react-toastify": "^11.1.0" }, diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d6a36ff..1cbd287 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -15,7 +15,7 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Create Next App", + title: "Khám Bệnh Mơ Phố", description: "Generated by create next app", }; diff --git a/src/app/page.tsx b/src/app/page.tsx index ceaf716..1a7fc19 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,10 +1,15 @@ -import BaseLayout from "@/components/layouts/BaseLayout/BaseLayout"; -import MainPagePatient from "@/components/page/patient/MainPagePatient"; +"use client"; -export default function Home() { +import BaseLayout from "@/components/layouts/BaseLayout/BaseLayout"; +import MainPageHome from "@/components/page/Home/MainPageHome"; +import React from "react"; + +const page = () => { return ( - - + + ); -} +}; + +export default page; diff --git a/src/components/customs/DateRangeField.tsx b/src/components/customs/DateRangeField.tsx new file mode 100644 index 0000000..e65888a --- /dev/null +++ b/src/components/customs/DateRangeField.tsx @@ -0,0 +1,164 @@ +"use client"; + +import React, { useState } from "react"; +import clsx from "clsx"; +import * as Popover from "@radix-ui/react-popover"; +import Moment from "react-moment"; +import { CalendarDays, CircleX } from "lucide-react"; + +import RangeDatePicker from "../utils/FilterDateRage/components/RangeDatePicker"; + +interface DateRangeValue { + from: Date | null; + to: Date | null; +} + +interface PropsDateRangeField { + name: string; + + label?: string | React.ReactNode; + placeholder?: string; + + value: DateRangeValue; + + required?: boolean; + readOnly?: boolean; + note?: string; + + onChange: (value: DateRangeValue) => void; + onClean?: () => void; +} + +function DateRangeField({ + name, + label, + placeholder = "Chọn khoảng thời gian", + value, + required, + readOnly, + note, + onChange, + onClean, +}: PropsDateRangeField) { + const [open, setOpen] = useState(false); + const [isFocus, setIsFocus] = useState(false); + + return ( +
+ {/* LABEL */} + {label && ( + + )} + + { + if (readOnly) return; + + setOpen(value); + setIsFocus(value); + }} + > + + + + + {/* POPUP */} + + { + onChange(data); + }} + onClose={() => { + setOpen(false); + setIsFocus(false); + }} + /> + + + + {/* NOTE */} + {note &&

{note}

} +
+ ); +} + +export default DateRangeField; diff --git a/src/components/customs/FilterCustom.tsx b/src/components/customs/FilterCustom.tsx index e8e0ce1..5ae98a1 100644 --- a/src/components/customs/FilterCustom.tsx +++ b/src/components/customs/FilterCustom.tsx @@ -81,7 +81,7 @@ function FilterCustom({ "border border-[#e1e5ed] bg-white", "transition-all", "hover:border-[#0019f6]", - styleRounded ? "rounded-full" : "rounded-md", + styleRounded ? "rounded-full" : "rounded-full", open && "border-[#0019f6]", )} > diff --git a/src/components/customs/StateActive.tsx b/src/components/customs/StateActive.tsx index dcf6dc5..7338221 100644 --- a/src/components/customs/StateActive.tsx +++ b/src/components/customs/StateActive.tsx @@ -11,7 +11,7 @@ interface StateItem { interface PropsStateActive { isBox?: boolean; - stateActive: number | string; + stateActive: number | string | undefined; listState: StateItem[]; } diff --git a/src/components/customs/custom-tabs.tsx b/src/components/customs/custom-tabs.tsx new file mode 100644 index 0000000..877151d --- /dev/null +++ b/src/components/customs/custom-tabs.tsx @@ -0,0 +1,63 @@ +"use client"; + +import React from "react"; +import clsx from "clsx"; +import { useRouter, useSearchParams } from "next/navigation"; + +interface PropsTabNavLink { + query: string; + listHref: { + title: string; + pathname: string; + query: string | null; + }[]; +} + +export default function TabNavLink({ query, listHref }: PropsTabNavLink) { + const router = useRouter(); + const searchParams = useSearchParams(); + + const currentValue = searchParams.get(query); + + const handleActive = (value: string | null) => { + const params = new URLSearchParams(searchParams.toString()); + + if (value === null) { + params.delete(query); + } else { + params.set(query, value); + } + + router.replace(`?${params.toString()}`, { scroll: false }); + }; + + return ( +
+ {listHref.map((item, i) => { + const isActive = + currentValue === item.query || + (!currentValue && item.query === null && i === 0); + + return ( + + ); + })} +
+ ); +} diff --git a/src/components/layouts/BaseLayout/componets/Navbar/Navbar.tsx b/src/components/layouts/BaseLayout/componets/Navbar/Navbar.tsx index 2b3961e..8f0d5f9 100644 --- a/src/components/layouts/BaseLayout/componets/Navbar/Navbar.tsx +++ b/src/components/layouts/BaseLayout/componets/Navbar/Navbar.tsx @@ -4,8 +4,6 @@ import React, { useCallback } from "react"; import { usePathname } from "next/navigation"; import Link from "next/link"; import { Menus, PATH } from "@/constant/config"; -import Image from "next/image"; -import icons from "@/constant/images/icons"; import clsx from "clsx"; import { Hospital } from "lucide-react"; @@ -14,6 +12,11 @@ const Navbar = () => { const checkActive = useCallback( (pathActive: string) => { + // Handle root path "/" as dashboard + if (pathname === "/" && pathActive === "/dashboard") { + return true; + } + const currentRoute = pathname.split("/")[1]; return pathActive === `/${currentRoute}`; }, diff --git a/src/components/page/Home/MainPageHome/MainPageHome.tsx b/src/components/page/Home/MainPageHome/MainPageHome.tsx index 59c3551..def620f 100644 --- a/src/components/page/Home/MainPageHome/MainPageHome.tsx +++ b/src/components/page/Home/MainPageHome/MainPageHome.tsx @@ -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
MainPageHome
; + const today = moment().format("YYYY-MM-DD"); + + const consultationQuery = useQuery({ + queryKey: [QUERY_KEY.table_list_consultation, "dashboard"], + queryFn: async () => { + const res = await httpRequest({ + 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({ + queryKey: [QUERY_KEY.table_list_report, today], + queryFn: async () => { + const res = await httpRequest({ + showMessageFailed: true, + http: consultationServices.getConsultationsReport({ + FormDate: today, + ToDate: today, + }), + }); + + return res as ReportResponse; + }, + }); + + const specialtyQuery = useQuery({ + queryKey: [QUERY_KEY.list_specialty_lookup], + queryFn: async () => { + const res = await httpRequest({ + 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(); + + 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 ( +
+
+
+
+

+ Dashboard khám bệnh +

+

+ Theo dõi nhanh các đợt khám, chuyên khoa và thông tin tổng quát + trong ngày. +

+
+
+ Cập nhật: {moment().format("DD/MM/YYYY")} +
+
+ + +
+ {summaryCards.map((item) => { + const Icon = item.icon; + + return ( +
+
+
+ {item.title} +
+
+ +
+
+
+
+

+ {item.value} +

+

+ {item.subtitle} +

+
+
+
+ ); + })} +
+
+
+ +
+
+
+
+

+ Chuyên khoa hiện có +

+

+ Danh sách chuyên khoa. +

+
+
+ {specialtyItems.length} chuyên khoa +
+
+ +
+ {specialtyActiveCounts.map((specialty, idx) => ( +
+
+

+ {specialty.name} +

+

+ Số chuyên khoa: {specialty.active} ca khám hôm nay +

+
+ + {specialty.active} + +
+ ))} +
+
+
+
+
+

+ Thông tin khám tổng quát +

+

+ Tổng số đợt khám, ca khám chuyên khoa và đơn thuốc trong ngày. +

+
+ +
+ {generalInfo.map((item) => { + const Icon = item.icon; + + return ( +
+
+
+ +
+
+

+ {item.label} +

+

+ {item.value} +

+
+
+
+ ); + })} +
+
+
+
+
+ ); }; export default MainPageHome; diff --git a/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx b/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx index b350a9c..385ccd0 100644 --- a/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx +++ b/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx @@ -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
Đang tải dữ liệu...
; } @@ -62,215 +101,360 @@ const DetailConsultation = () => { if (!consultation) { return
Không tìm thấy dữ liệu phiên khám
; } + type PrescriptionItem = + ConsultationDetail["aggregatedPrescriptionItems"][number]; return (
-
-
- {/* HEADER */} -
- router.back()} - className="cursor-pointer" - /> +
+
+
+ {/* HEADER */} +
+
router.back()} + > + -

- Chi tiết phiên khám -

+

+ Chi tiết phiên khám +

+
+ + {consultation.status === TYPE_STATUS.PendingPrescription && canUpdateConsultation && ( + } + href={`/consultation/update?_id=${consultation.id}`} + > + Cập nhật tổng quát + + )} +
+ + + {/* ID PHIÊN KHÁM */} +
+

+ ID PHIÊN KHÁM +

+ +

+ {consultation.id} +

+
+ + {/* MÃ PHIÊN KHÁM */} +
+

+ MÃ PHIÊN KHÁM +

+ +

+ {consultation.sessionCode} +

+
+ + {/* TÊN BỆNH NHÂN */} +
+

+ TÊN BỆNH NHÂN +

+ +

+ {consultation.patientName} +

+
+ + {/* NGÀY GIỜ KHÁM */} +
+

+ NGÀY, GIỜ KHÁM +

+ +

+ {consultation.visitDate + ? moment(consultation.visitDate).format("DD/MM/YYYY") + : "---"} +

+
+ + {/* TRIỆU CHỨNG */} +
+

+ DẤU HIỆU +

+ +

+ {consultation.symptomsText || "---"} +

+
+ + {/* KHIẾU NẠI CHÍNH */} +
+

+ KHIẾU NẠI CHÍNH +

+ +

+ {consultation.chiefComplaint || "---"} +

+
+ + {/* CHỈ SỐ HUYẾT ÁP, CHỈ SỐ MẠCH */} +
+

+ CHỈ SỐ HUYẾT ÁP, CHỈ SỐ MẠCH +

+ +

+ {consultation.vitalSigns || "---"} +

+
+ + {/* CHẨN ĐOÁN */} +
+

+ CHUẨN ĐOÁN +

+ +

+ {consultation.diagnosis || "---"} +

+
+ + {/* KẾ HOẠCH ĐIỀU TRỊ */} +
+

+ KẾ HOẠCH ĐIỀU TRỊ +

+ +

+ {consultation.treatmentPlan || "---"} +

+
+ + {/* GHI CHÚ BÁC SĨ */} +
+

+ GHI CHÚ BÁC SĨ +

+ +

+ {consultation.doctorNotes || "---"} +

+
+ + {/* NGƯỜI TẠO */} +
+

+ NGƯỜI TẠO +

+ +

+ {consultation.createdBy || "---"} +

+
+ +
+

+ CÁC CHUYÊN KHOA ĐANG CHỜ KHÁM +

+ +

+ {consultation.specialtyClinics + ?.filter((specialty) => specialty.specialtyStatus === 1) + .map((specialty) => specialty.specialtyName) + .join(", ") || "---"} +

+
+ +
+

+ CÁC CHUYÊN KHOA ĐÃ KHÁM +

+ +

+ {consultation.specialtyClinics + ?.filter((specialty) => specialty.specialtyStatus === 3) + .map((specialty) => specialty.specialtyName) + .join(", ") || "---"} +

+
+
+

+ TRẠNG THÁI +

+ + +
+
- - {/* CONTENT */} -
- {/* ID PHIÊN KHÁM */} -
-

- ID PHIÊN KHÁM -

- -

- {consultation.id} -

+
+
+
+ {/* HEADER */} +
+

+ Danh sách đơn thuốc +

- - {/* MÃ PHIÊN KHÁM */} -
-

- MÃ PHIÊN KHÁM -

- -

- {consultation.sessionCode} -

-
- - {/* NGÀY GIỜ KHÁM */} -
-

- NGÀY, GIỜ KHÁM -

- -

- {consultation.visitDate} -

-
- - {/* TRIỆU CHỨNG */} -
-

DẤU HIỆU

- -

- {consultation.symptomsText || "---"} -

-
- - {/* KHIẾU NẠI CHÍNH */} -
-

- KHIẾU NẠI CHÍNH -

- -

- {consultation.chiefComplaint || "---"} -

-
- - {/* CHỈ SỐ HUYẾT ÁP, CHỈ SỐ MẠCH */} -
-

- CHỈ SỐ HUYẾT ÁP, CHỈ SỐ MẠCH -

- -

- {consultation.vitalSigns || "---"} -

-
- - {/* CHẨN ĐOÁN */} -
-

- CHUẨN ĐOÁN -

- -

- {consultation.diagnosis || "---"} -

-
- - {/* KẾ HOẠCH ĐIỀU TRỊ */} -
-

- KẾ HOẠCH ĐIỀU TRỊ -

- -

- {consultation.treatmentPlan || "---"} -

-
- - {/* GHI CHÚ BÁC SĨ */} -
-

- GHI CHÚ BÁC SĨ -

- -

- {consultation.doctorNotes || "---"} -

-
- - {/* NGƯỜI TẠO */} -
-

- NGƯỜI TẠO -

- -

- {consultation.createdBy || "---"} -

-
- -
-

- CÁC CHUYÊN KHOA ĐANG CHỜ KHÁM -

- -

- {consultation.specialtyClinics - ?.filter((specialty) => specialty.specialtyStatus === 1) - .map((specialty) => specialty.specialtyName) - .join(", ") || "---"} -

-
- -
-

- CÁC CHUYÊN KHOA ĐÃ KHÁM -

- -

- {consultation.specialtyClinics - ?.filter((specialty) => specialty.specialtyStatus === 3) - .map((specialty) => specialty.specialtyName) - .join(", ") || "---"} -

-
- - {/* TRẠNG THÁI */} -
-

- TRẠNG THÁI -

- - + 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) => ( + {item.medicineName} + ), }, - { - state: TYPE_STATUS.InProgress, - text: "Đang khám tổng quát", - textColor: "#1E3A8A", - backgroundColor: "#DBEAFE", + title: "Liều dùng", + render: (item: PrescriptionItem) => ( + {item.dosage} + ), }, - { - state: TYPE_STATUS.ToSpecialty, - text: "Chờ khám chuyên khoa", - textColor: "#6B21A8", - backgroundColor: "#E9D5FF", + title: "Tần suất", + render: (item: PrescriptionItem) => ( + {item.frequency} + ), }, - { - state: TYPE_STATUS.PendingPrescription, - text: "Chờ nhận thuốc/kính", - textColor: "#9A3412", - backgroundColor: "#FED7AA", + title: "Thời gian", + render: (item: PrescriptionItem) => ( + {item.duration} + ), }, - { - 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) => ( + {item.instructions || "---"} + ), }, ]} /> - + + {/* TAB CHUYÊN KHOA */} + {specialtyTabs.length > 0 && ( +
+ +
+ )} + {/* CHI TIẾT CHUYÊN KHOA */} + {selectedSpecialty && ( +
+
+

+ Thông tin chuyên khoa: {selectedSpecialty.specialtyName} +

+ {canUpdateConsultation && ( + } + href={`/consultation/update-specialty?_id=${consultation.id}&specialtyClinicId=${selectedSpecialty.id}`} + > + Cập nhật chuyên khoa + + )} +
+ + +
+

CHUYÊN KHOA

+ +

{selectedSpecialty.specialtyName || "---"}

+
+ +
+

CHẨN ĐOÁN

+ +

{selectedSpecialty.diagnosis || "---"}

+
+ +
+

GHI CHÚ KHÁM

+ +

{selectedSpecialty.specialtyNotes || "---"}

+
+ +
+

THỦ THUẬT

+ +

{selectedSpecialty.procedureNotes || "---"}

+
+ + {selectedSpecialty.glassRequired && ( + <> +
+

CẦN ĐEO KÍNH

+ +

+
+ +
+

LOẠI KÍNH

+ +

{selectedSpecialty.glassType || "---"}

+
+ + )} +
+
+ )} ); }; diff --git a/src/components/page/consultation/MainPageConsultation/MainPageConsultation.tsx b/src/components/page/consultation/MainPageConsultation/MainPageConsultation.tsx index 6cfcd38..7f8b975 100644 --- a/src/components/page/consultation/MainPageConsultation/MainPageConsultation.tsx +++ b/src/components/page/consultation/MainPageConsultation/MainPageConsultation.tsx @@ -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(""); @@ -66,10 +66,6 @@ const MainPageConsultation = () => { const [openCompleteDialog, setOpenCompleteDialog] = useState(false); - /* ========================= - GET LIST - ========================= */ - const consultationQuery = useQuery({ 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 ( -
- {/* HEADER */} -
-

Phiên khám

- - } - // onClick={handleOpenCreate} - href="/consultation/create" - > - Tạo phiên khám - +
+
+
- - {/* SEARCH */} -
-
- updateQuery("_keyword", value)} - placeholder="Tìm kiếm phiên khám..." - /> +
+ {/* HEADER */} +
+

Phiên khám

+ {isAllTab && ( + } + // onClick={handleOpenCreate} + href="/consultation/create" + > + Tạo phiên khám + + )}
- - 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])} - /> - - 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 */} +
+
+ updateQuery("_keyword", value)} + placeholder="Tìm kiếm phiên khám..." + /> +
+ + {isAllTab && ( +
+ + name="Chuyên khoa" + value={_specialtyId} + onChange={(value) => updateQuery("_specialtyId", value ?? "")} + listOption={ + specialtyQuery.data?.map((item) => ({ + uuid: item.id, + name: item.name, + })) || [] + } + /> + + + 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", + }, + ]} + /> +
+ )} +
+ + {/* TABLE */} + +
item.id} + data={consultationData} + column={[ + { + title: "TÊN BỆNH NHÂN", + + render: (item: ConsultationItem) => ( + + + + + {item.patientName} + + + + +

+ Chi tiết bệnh nhân +

+
+
+
+ ), + }, + + { + 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 ( + + + + + {item.sessionCode} + + + + +

+ Chi tiết phiên khám +

+
+
+
+ ); + }, + }, + + { + title: "NGÀY KHÁM", + render: (item: ConsultationItem) => ( + + {item.visitDate + ? moment(item.visitDate).format("DD/MM/YYYY") + : "---"} + + ), + }, + + { + title: "DẤU HIỆU", + + render: (item: ConsultationItem) => ( + {item.symptomsText || "---"} + ), + }, + + { + title: "CHUẨN ĐOÁN", + + render: (item: ConsultationItem) => ( + {item.diagnosis || "---"} + ), + }, + + { + title: "KẾ HOẠCH ĐIỀU TRỊ", + + render: (item: ConsultationItem) => ( + {item.treatmentPlan || "---"} + ), + }, + + { + title: "GHI CHÚ BÁC SĨ", + + render: (item: ConsultationItem) => ( + {item.doctorNotes || "---"} + ), + }, + { + title: "TRẠNG THÁI", + render: (item: ConsultationItem) => { + // ===== TAB CHUYÊN KHOA ===== + if (!isAllTab) { + const specialty = item.specialtyClinics.find( + (x) => x.specialtyId === _type, + ); + + return ( + + ); + } + + // ===== TAB TẤT CẢ ===== + return ( + + ); + }, + }, + { + 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 ( + + Cập nhật + + ); + } + + // ===== TAB TẤT CẢ ===== + return ( +
+ + + + + + {!isDisabled && ( + +
+ {item.specialtyClinics.map((specialty) => ( + + + {specialty.specialtyName} + + ))} +
+
+ )} +
+ + handleOpenCancel(item)} + > + Hủy + + + handleOpenComplete(item)} + > + Hoàn thành + +
+ ); + }, + }, + ]} + /> + + updateQuery("_page", value)} + onSetPageSize={(value) => updateQuery("_pageSize", value)} + dependencies={[_pageSize, _keyword, _specialtyId, _status]} /> - {/* TABLE */} - -
item.id} - data={consultationData} - column={[ - { - title: "TÊN BỆNH NHÂN", - - render: (item: ConsultationItem) => ( - {item.patientName} - ), - }, - - { - title: "MÃ PHIÊN KHÁM", - fixedLeft: true, - render: (item: ConsultationItem) => ( - - {item.sessionCode} - - ), - }, - - { - title: "NGÀY KHÁM", - render: (item: ConsultationItem) => ( - - {item.visitDate - ? moment(item.visitDate).format("DD/MM/YYYY") - : "---"} - - ), - }, - - { - title: "DẤU HIỆU", - - render: (item: ConsultationItem) => ( - {item.symptomsText || "---"} - ), - }, - - { - title: "CHUẨN ĐOÁN", - - render: (item: ConsultationItem) => ( - {item.diagnosis || "---"} - ), - }, - - { - title: "KẾ HOẠCH ĐIỀU TRỊ", - - render: (item: ConsultationItem) => ( - {item.treatmentPlan || "---"} - ), - }, - - { - title: "GHI CHÚ BÁC SĨ", - - render: (item: ConsultationItem) => ( - {item.doctorNotes || "---"} - ), - }, - { - 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 {waitingSpecialties || "---"}; - }, - }, - { - 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 {completedSpecialties || "---"}; - }, - }, - { - title: "TRẠNG THÁI", - - render: (item: ConsultationItem) => ( - - ), - }, - - { - 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 ( -
- {/* UPDATE */} - - ) - } - href={ - specialtyClinicActive - ? `/consultation/update-specialty?_id=${item.id}&specialtyClinicId=${specialtyClinicActive.id}` - : `/consultation/update?_id=${item.id}` - } - > - {specialtyClinicActive - ? `Cập nhật ${specialtyClinicActive.specialtyName}` - : undefined} - - - {/* CANCEL */} - handleOpenCancel(item)} - > - Hủy - - - {/* COMPLETE */} - handleOpenComplete(item)} - > - Hoàn thành - -
- ); - }, - }, - ]} - /> - - updateQuery("_page", value)} - onSetPageSize={(value) => updateQuery("_pageSize", value)} - dependencies={[_pageSize, _keyword, _specialtyId, _status]} - /> - {/* CANCEL DIALOG */} { 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"); }, diff --git a/src/components/page/consultation/UpdateSpecialtyClinicId/UpdateSpecialtyClinicId.tsx b/src/components/page/consultation/UpdateSpecialtyClinicId/UpdateSpecialtyClinicId.tsx index 95e679a..891f27d 100644 --- a/src/components/page/consultation/UpdateSpecialtyClinicId/UpdateSpecialtyClinicId.tsx +++ b/src/components/page/consultation/UpdateSpecialtyClinicId/UpdateSpecialtyClinicId.tsx @@ -159,6 +159,8 @@ const UpdateSpecialtyClinicId = () => { queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId], }); + router.back(); + router.push(PATH.CONSULTATION || "/consultation"); }, }); diff --git a/src/components/page/patient/DetailPatient/DetailPatient.tsx b/src/components/page/patient/DetailPatient/DetailPatient.tsx index 4e6b08b..c1e92e3 100644 --- a/src/components/page/patient/DetailPatient/DetailPatient.tsx +++ b/src/components/page/patient/DetailPatient/DetailPatient.tsx @@ -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 = () => {
-

- router.back()} - className="cursor-pointer" - />{" "} - {patient?.fullName} +

router.back()} + > + {patient?.fullName}

{ title: "MÃ PHIÊN KHÁM", fixedLeft: true, render: (item: ConsultationItem) => ( - - {item.sessionCode} - + + + + + {item.sessionCode} + + + + +

+ Chi tiết phiên khám +

+
+
+
), }, diff --git a/src/components/page/patient/MainPagePatient/MainPagePatient.tsx b/src/components/page/patient/MainPagePatient/MainPagePatient.tsx index cf8096e..48b747b 100644 --- a/src/components/page/patient/MainPagePatient/MainPagePatient.tsx +++ b/src/components/page/patient/MainPagePatient/MainPagePatient.tsx @@ -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 (
@@ -217,12 +246,34 @@ const MainPagePatient = () => { title: "TÊN BỆNH NHÂN", render: (item: PatientItem) => ( - - {item.fullName || "---"} - + + + + + {item.fullName} + + {/* + {item.fullName} + */} + + + +

+ Chi tiết bệnh nhân +

+
+
+
), }, diff --git a/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx b/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx index 32b69af..919e77d 100644 --- a/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx +++ b/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx @@ -56,6 +56,7 @@ const CreatePrescription = () => { const [loading, setLoading] = useState(false); const queryKeys = [QUERY_KEY.table_list_consultation]; const [file, setFile] = useState(null); + const [folderName, setFolderName] = useState("prescriptions"); const [form, setForm] = useState({ 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 = () => {
{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
{/* TITLE */} diff --git a/src/components/page/prescription/DetailPrescription/DetailPrescription.tsx b/src/components/page/prescription/DetailPrescription/DetailPrescription.tsx index b4018aa..943a448 100644 --- a/src/components/page/prescription/DetailPrescription/DetailPrescription.tsx +++ b/src/components/page/prescription/DetailPrescription/DetailPrescription.tsx @@ -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({ + 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 (
-

Chi tiết đơn thuốc

+
router.back()} + > + + +

+ Chi tiết đơn thuốc +

+
{/* LOADING */} {prescriptionDetailQuery.isLoading &&

Đang tải...

} @@ -64,11 +106,12 @@ const DetailPrescription = () => {
- Id phiên khám: {data.examinationSessionId} + Chuyên khoa:{" "} + {specialtyName || data.examinationSessionSpecialtyId || "---"}
- Ghi chú: {data.notes || "-"} + Ghi chú: {data.notes || "---"}
{/* IMAGE */} diff --git a/src/components/page/prescription/MainPrescription/MainPrescription.tsx b/src/components/page/prescription/MainPrescription/MainPrescription.tsx index 835279f..7147595 100644 --- a/src/components/page/prescription/MainPrescription/MainPrescription.tsx +++ b/src/components/page/prescription/MainPrescription/MainPrescription.tsx @@ -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) => ( - - {item.prescriptionCode} - + + + + + {item.prescriptionCode} + + + + +

+ Chi tiết đơn thuốc +

+
+
+
), }, @@ -162,20 +180,20 @@ const MainPrescription = () => { }, // ================= ACTION ================= - // { - // fixedRight: true, - // title: "ACTION", + { + fixedRight: true, + title: "ACTION", - // render: (item: any) => ( - //
- // } - // href={`${PATH.PRESCRIPTION}/update?_id=${item?.id}`} - // /> - //
- // ), - // }, + render: (item: any) => ( +
+ } + href={`${PATH.PRESCRIPTION}/${item.id}`} + /> +
+ ), + }, ]} /> diff --git a/src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx b/src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx index 685cc36..b43cf6f 100644 --- a/src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx +++ b/src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx @@ -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.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({ - 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({ 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 ( -
+
{/* HEADER */} -
-

- Thống kê các phiên khám -

+
+
+

Thống kê phiên khám

+ + +
- -

item.specialtyId} + + {/* CARDS */} +
+
+
+ +
+ +

Tổng lượt khám

+ +

+ {report?.totalSessions || 0} +

+
+ +
+
+ +
+ +

Lượt khám chuyên khoa

+ +

+ {report?.totalSpecialtySubSessions || 0} +

+
+ +
+
+ +
+ +

Đơn thuốc đã cấp

+ +

+ {report?.totalPrescriptionsIssued || 0} +

+
+
+ + {/* BẢNG THỐNG KÊ */} +
+

+ Thống kê theo chuyên khoa +

+ + {item.specialtyId}, - }, - { - title: "TÊN CHUYÊN KHOA", - render: (item) => {item.specialtyName}, - }, - { - title: "SỐ CA KHÁM", - render: (item) => {item.totalCases}, - }, - ]} - /> - - {/* 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" + > +
item.specialtyId} + data={reportData} + column={[ + { + title: "MÃ CHUYÊN KHOA", + render: (item) => {item.specialtyId}, + }, + { + title: "TÊN CHUYÊN KHOA", + render: (item) => {item.specialtyName}, + }, + { + title: "SỐ CA KHÁM", + render: (item) => ( + {item.totalCases} + ), + }, + ]} + /> + + + + {/* BÁO CÁO TỔNG HỢP */} +
+

Báo cáo tổng hợp

+ +
+

+ Số lượt khám bệnh nhân đạo: + {report?.totalSpecialtySubSessions || 0} lượt + người dân được khám chuyên khoa, cụ thể như sau: +

+ +
    + {reportData.map((item) => ( +
  • + {item.specialtyName}: {item.totalCases} ca +
  • + ))} +
+ +

+ Tổng số đơn thuốc đã cấp: + {report?.totalPrescriptionsIssued || 0} đơn. +

+ + {/*

+ Số suất quà được phát: + {report?.totalGiftPackagesIssued || 0} suất. +

*/} +
+
); }; diff --git a/src/components/utils/FilterDateRage/FilterDateRange.tsx b/src/components/utils/FilterDateRage/FilterDateRange.tsx new file mode 100644 index 0000000..9e52a04 --- /dev/null +++ b/src/components/utils/FilterDateRage/FilterDateRange.tsx @@ -0,0 +1,85 @@ +"use client"; + +import React, { useEffect, useState } from "react"; +import clsx from "clsx"; +import * as Popover from "@radix-ui/react-popover"; + +import { PropsFilterDateRange } from "./interface"; +import Moment from "react-moment"; +import { TYPE_DATE } from "@/constant/config/enum"; +import { getDateRange } from "@/common/funcs/selectData"; +import { ListOptionFilterDate } from "@/constant/config"; +import { ChevronDown } from "lucide-react"; +import DateOption from "./components/DateOption"; + +function FilterDateRange({ + styleRounded = false, + showOptionAll = false, + date, + setDate, + typeDate, + setTypeDate, + title = "Thời gian", +}: PropsFilterDateRange) { + const [openDate, setOpenDate] = useState(false); + + useEffect(() => { + if (typeDate !== null && typeDate !== TYPE_DATE.LUA_CHON) { + setDate(getDateRange(typeDate)); + } + }, [typeDate, setDate]); + + return ( + + + + + + + + + + ); +} + +export default FilterDateRange; diff --git a/src/components/utils/FilterDateRage/components/CalendarMain/CalendarMain.tsx b/src/components/utils/FilterDateRage/components/CalendarMain/CalendarMain.tsx new file mode 100644 index 0000000..be006da --- /dev/null +++ b/src/components/utils/FilterDateRage/components/CalendarMain/CalendarMain.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { memo, useCallback, useContext, useMemo } from "react"; +import clsx from "clsx"; +import { ContextCalendar } from "../RangeDatePicker/RangeDatePicker"; +import { PropsCalendarMain } from "./interface"; + +const months = Array.from({ length: 12 }, (_, i) => i + 1); + +function CalendarMain({ + date, + setDate, + setType, + type, + year, +}: PropsCalendarMain) { + // ✅ destructure context (fix React Compiler) + const { datePicker, dateHover, setDatePicker, setDateHover } = + useContext(ContextCalendar); + + // ===================== HOVER RANGE ===================== + const hoverRange = useMemo(() => { + if (datePicker?.from && dateHover) { + return dateHover > datePicker.from + ? { start: datePicker.from, end: dateHover } + : { start: dateHover, end: datePicker.from }; + } + return null; + }, [datePicker?.from, dateHover]); + + // ===================== HANDLE PICK ===================== + const handleDatePick = useCallback( + (datePick: Date) => { + if (!datePicker?.from || datePicker?.to) { + return setDatePicker({ + from: datePick, + to: null, + }); + } + + setDateHover(null); + + if (datePick < datePicker.from) { + return setDatePicker({ + from: datePick, + to: datePicker.from, + }); + } + + return setDatePicker({ + ...datePicker, + to: datePick, + }); + }, + [datePicker, setDatePicker, setDateHover], + ); + + // ===================== HANDLE HOVER ===================== + const handleHover = useCallback( + (d: Date) => { + if (datePicker?.from && !datePicker?.to) { + setDateHover(d); + } + }, + [datePicker?.from, datePicker?.to, setDateHover], + ); + + // ===================== BUILD CALENDAR ===================== + const rows = useMemo(() => { + const currentDate = new Date(); + const monthStart = new Date(date.getFullYear(), date.getMonth(), 1); + const monthEnd = new Date(date.getFullYear(), date.getMonth() + 1, 0); + + const startDate = new Date(monthStart); + const result: React.ReactNode[] = []; + + // lùi về CN + while (startDate.getDay() !== 0) { + startDate.setDate(startDate.getDate() - 1); + } + + while (startDate <= monthEnd) { + const cells: React.ReactNode[] = []; + + for (let i = 0; i < 7; i++) { + const currDate = new Date(startDate); + + const isStart = currDate.getTime() === datePicker?.from?.getTime(); + const isEnd = currDate.getTime() === datePicker?.to?.getTime(); + + const isBetween = + datePicker?.from && + datePicker?.to && + currDate > datePicker.from && + currDate < datePicker.to; + + const isHover = + hoverRange && + currDate > hoverRange.start && + currDate < hoverRange.end; + + const isStartHover = + hoverRange && currDate.getTime() === hoverRange.start.getTime(); + + const isEndHover = + hoverRange && currDate.getTime() === hoverRange.end.getTime(); + + const isDisabled = startDate.getMonth() !== date.getMonth(); + + const isToday = + startDate.toDateString() === currentDate.toDateString() && + startDate.getMonth() === date.getMonth(); + + cells.push( +
handleDatePick(currDate)} + onMouseOver={() => handleHover(currDate)} + className={clsx( + "flex items-center justify-center px-2 py-1 text-sm font-medium rounded border border-transparent transition", + + "cursor-pointer select-none hover:opacity-60", + + isDisabled && "pointer-events-none opacity-30", + + isToday && "border-blue-700", + + isStart && "bg-blue-700 text-white rounded-l-md", + isEnd && "bg-blue-700 text-white rounded-r-md", + + isBetween && "bg-gray-100", + + isHover && "border-y border-blue-700", + isStartHover && "border border-blue-700 rounded-l-md", + isEndHover && "border border-blue-700 rounded-r-md", + )} + > + {startDate.getDate()} +
, + ); + + startDate.setDate(startDate.getDate() + 1); + } + + result.push( +
+ {cells} +
, + ); + } + + return result; + }, [ + date, + datePicker?.from, + datePicker?.to, + handleDatePick, + handleHover, + hoverRange, + ]); + + // ===================== YEAR LIST ===================== + const listYear = useMemo(() => { + if (!year) return []; + return Array.from( + { length: year.last - year.first + 1 }, + (_, i) => year.first + i, + ); + }, [year]); + + // ===================== UI ===================== + return ( +
setDateHover(null)}> + {/* DATE */} + {type === 0 && rows} + + {/* MONTH */} + {type === 1 && ( +
+ {months.map((m) => ( +
{ + const newDate = new Date(date); + newDate.setMonth(m - 1); + setDate(newDate); + setType(0); + }} + className={clsx( + "cursor-pointer select-none rounded px-2 py-1 text-center text-sm font-semibold transition", + "hover:bg-gray-100 hover:opacity-70", + m === date.getMonth() + 1 && + "bg-blue-500 text-white hover:bg-blue-500 hover:opacity-100", + )} + > + Tháng {m} +
+ ))} +
+ )} + + {/* YEAR */} + {type === 2 && ( +
+ {listYear.map((y) => ( +
{ + const newDate = new Date(date); + newDate.setFullYear(y); + setDate(newDate); + setType(1); + }} + className={clsx( + "cursor-pointer select-none rounded px-2 py-1 text-center text-sm font-semibold transition", + "hover:bg-gray-100 hover:opacity-70", + y === date.getFullYear() && + "bg-blue-500 text-white hover:bg-blue-500 hover:opacity-100", + )} + > + {y} +
+ ))} +
+ )} +
+ ); +} + +export default memo(CalendarMain); diff --git a/src/components/utils/FilterDateRage/components/CalendarMain/index.ts b/src/components/utils/FilterDateRage/components/CalendarMain/index.ts new file mode 100644 index 0000000..cadb07e --- /dev/null +++ b/src/components/utils/FilterDateRage/components/CalendarMain/index.ts @@ -0,0 +1 @@ +export { default } from "./CalendarMain"; diff --git a/src/components/utils/FilterDateRage/components/CalendarMain/interface/index.ts b/src/components/utils/FilterDateRage/components/CalendarMain/interface/index.ts new file mode 100644 index 0000000..0a74bfd --- /dev/null +++ b/src/components/utils/FilterDateRage/components/CalendarMain/interface/index.ts @@ -0,0 +1,7 @@ +export interface PropsCalendarMain { + date: Date; + setDate: (date: Date) => void; + setType: (num: number) => void; + type: number; + year: any; +} diff --git a/src/components/utils/FilterDateRage/components/DateOption/DateOption.tsx b/src/components/utils/FilterDateRage/components/DateOption/DateOption.tsx new file mode 100644 index 0000000..1ad3a9d --- /dev/null +++ b/src/components/utils/FilterDateRage/components/DateOption/DateOption.tsx @@ -0,0 +1,80 @@ +"use client"; + +import clsx from "clsx"; +import { Check } from "lucide-react"; + +import { PropsDateOption } from "./interface"; + +import { TYPE_DATE } from "@/constant/config/enum"; +import { getDateRange } from "@/common/funcs/selectData"; +import { ListOptionFilterDate } from "@/constant/config"; + +import RangeDatePicker from "../RangeDatePicker"; + +export default function DateOption({ + showOptionAll, + date, + setDate, + typeDate, + setTypeDate, + show, + setShow, +}: PropsDateOption) { + const isCustom = Number(typeDate) === TYPE_DATE.LUA_CHON; + + return ( +
+ {/* MENU */} +
+ {(showOptionAll + ? ListOptionFilterDate + : ListOptionFilterDate.filter((item) => item.value !== TYPE_DATE.ALL) + ).map((item) => { + const active = item.value === typeDate; + + return ( +
{ + setTypeDate(item.value); + + if (item.value !== TYPE_DATE.LUA_CHON) { + setDate(getDateRange(item.value)); + setShow(false); + } + }} + className={clsx( + "flex cursor-pointer items-center justify-between rounded-lg px-3 py-2 text-sm", + active ? "bg-blue-50 text-blue-600" : "hover:bg-gray-50", + )} + > + {item.name} + + {active && item.value !== TYPE_DATE.LUA_CHON && ( + + )} +
+ ); + })} +
+ + {/* DATE PICKER */} + {isCustom && ( + setShow(false)} + /> + )} +
+ ); +} diff --git a/src/components/utils/FilterDateRage/components/DateOption/index.ts b/src/components/utils/FilterDateRage/components/DateOption/index.ts new file mode 100644 index 0000000..09e27e5 --- /dev/null +++ b/src/components/utils/FilterDateRage/components/DateOption/index.ts @@ -0,0 +1 @@ +export { default } from "./DateOption"; diff --git a/src/components/utils/FilterDateRage/components/DateOption/interface/index.ts b/src/components/utils/FilterDateRage/components/DateOption/interface/index.ts new file mode 100644 index 0000000..5a52221 --- /dev/null +++ b/src/components/utils/FilterDateRage/components/DateOption/interface/index.ts @@ -0,0 +1,12 @@ +export interface PropsDateOption { + showOptionAll?: boolean; + date: { + from: Date | null; + to: Date | null; + } | null; + setDate: (any: any) => void; + typeDate: number | null; + setTypeDate: (any: any) => void; + show: boolean; + setShow: (any: any) => void; +} diff --git a/src/components/utils/FilterDateRage/components/RangeDatePicker/RangeDatePicker.tsx b/src/components/utils/FilterDateRage/components/RangeDatePicker/RangeDatePicker.tsx new file mode 100644 index 0000000..5a5bb84 --- /dev/null +++ b/src/components/utils/FilterDateRage/components/RangeDatePicker/RangeDatePicker.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { createContext, memo, useCallback, useEffect, useState } from "react"; +import { PropsRangeDatePicker } from "./interface"; +import clsx from "clsx"; +import { ChevronLeft, MoveRight, ChevronRight } from "lucide-react"; +import CalendarMain from "../CalendarMain"; +import Button from "@/components/customs/custom-button"; +import Moment from "react-moment"; + +const daysOfWeek = ["CN", "T2", "T3", "T4", "T5", "T6", "T7"]; + +export const ContextCalendar = createContext(null); + +function RangeDatePicker({ + onClose, + onSetValue, + value, + open, + onSubmit, +}: PropsRangeDatePicker) { + const [typeCalendarLeft, setTypeCalendarLeft] = useState(0); + const [typeCalendarRight, setTypeCalendarRight] = useState(0); + const [dateLeft, setDateLeft] = useState(new Date()); + const [dateRight, setDateRight] = useState( + new Date(new Date().getFullYear(), new Date().getMonth() + 1), + ); + + const [dateHover, setDateHover] = useState(null); + const [yearTableLeft, setYearTableLeft] = useState(); + const [yearTableRight, setYearTableRight] = useState(); + const [datePicker, setDatePicker] = useState({ + from: null as Date | null, + to: null as Date | null, + }); + + // ===== HANDLE ===== + const handleNext = useCallback( + (isRight?: boolean) => { + if (isRight) { + if (typeCalendarRight === 0) { + setDateRight((prev) => { + const newDate = new Date(prev.getFullYear(), prev.getMonth() + 1); + return newDate; + }); + } + } else { + if (typeCalendarLeft === 0) { + setDateLeft((prev) => { + const newDate = new Date(prev.getFullYear(), prev.getMonth() + 1); + return newDate < dateRight ? newDate : prev; + }); + } + } + }, + [dateRight, typeCalendarLeft, typeCalendarRight], + ); + + const handlePrev = useCallback( + (isRight?: boolean) => { + if (isRight) { + if (typeCalendarRight === 0) { + setDateRight((prev) => { + const newDate = new Date(prev.getFullYear(), prev.getMonth() - 1); + return dateLeft < newDate ? newDate : prev; + }); + } + } else { + if (typeCalendarLeft === 0) { + setDateLeft((prev) => { + return new Date(prev.getFullYear(), prev.getMonth() - 1); + }); + } + } + }, + [dateLeft, typeCalendarLeft, typeCalendarRight], + ); + + const handleSubmit = useCallback(() => { + if (datePicker.from && datePicker.to) { + onSetValue(datePicker); + onClose(); + onSubmit && onSubmit(); + } + }, [datePicker]); + + // ===== EFFECT ===== + useEffect(() => { + if (!open) { + setDatePicker({ from: value?.from, to: value?.to }); + } + }, [open, value]); + + useEffect(() => { + setYearTableLeft({ + first: dateLeft.getFullYear() - 5, + last: dateLeft.getFullYear() + 6, + }); + setYearTableRight({ + first: dateRight.getFullYear() - 5, + last: dateRight.getFullYear() + 6, + }); + }, [dateLeft, dateRight]); + + return ( + +
+ {/* HEADER DATE */} +
+ + {datePicker.from ? ( + {datePicker.from} + ) : ( + "Ngày bắt đầu" + )} + + + + + + {datePicker.to ? ( + {datePicker.to} + ) : ( + "Ngày kết thúc" + )} + +
+ + {/* CALENDAR */} +
+ {[false, true].map((isRight, idx) => { + const date = isRight ? dateRight : dateLeft; + const type = isRight ? typeCalendarRight : typeCalendarLeft; + const setType = isRight + ? setTypeCalendarRight + : setTypeCalendarLeft; + const year = isRight ? yearTableRight : yearTableLeft; + + return ( +
+ {/* TITLE */} +
+ + +

setType(type === 0 ? 1 : type === 1 ? 2 : 0)} + > + Tháng {date.getMonth() + 1}, {date.getFullYear()} +

+ + +
+ + {/* DAYS */} + {type === 0 && ( +
+ {daysOfWeek.map((d) => ( +
+ {d} +
+ ))} +
+ )} + + {/* CALENDAR MAIN */} + +
+ ); + })} +
+ + {/* BUTTON */} +
+ + +
+
+
+ ); +} + +export default memo(RangeDatePicker); diff --git a/src/components/utils/FilterDateRage/components/RangeDatePicker/index.ts b/src/components/utils/FilterDateRage/components/RangeDatePicker/index.ts new file mode 100644 index 0000000..68339b9 --- /dev/null +++ b/src/components/utils/FilterDateRage/components/RangeDatePicker/index.ts @@ -0,0 +1 @@ +export { default } from "./RangeDatePicker"; diff --git a/src/components/utils/FilterDateRage/components/RangeDatePicker/interface/index.ts b/src/components/utils/FilterDateRage/components/RangeDatePicker/interface/index.ts new file mode 100644 index 0000000..1a4febd --- /dev/null +++ b/src/components/utils/FilterDateRage/components/RangeDatePicker/interface/index.ts @@ -0,0 +1,7 @@ +export interface PropsRangeDatePicker { + onClose: () => void; + onSetValue: (any: any) => void; + onSubmit?: () => void; + value: any; + open?: boolean; +} diff --git a/src/components/utils/FilterDateRage/index.ts b/src/components/utils/FilterDateRage/index.ts new file mode 100644 index 0000000..18d4824 --- /dev/null +++ b/src/components/utils/FilterDateRage/index.ts @@ -0,0 +1 @@ +export { default } from "./FilterDateRange"; diff --git a/src/components/utils/FilterDateRage/interface/index.ts b/src/components/utils/FilterDateRage/interface/index.ts new file mode 100644 index 0000000..5e6d459 --- /dev/null +++ b/src/components/utils/FilterDateRage/interface/index.ts @@ -0,0 +1,17 @@ +import { Dispatch, SetStateAction } from "react"; +import { TYPE_DATE } from "@/constant/config/enum"; + +export interface PropsFilterDateRange { + styleRounded?: boolean; + showOptionAll?: boolean; + date: { + from: Date | null; + to: Date | null; + } | null; + setDate: Dispatch< + SetStateAction<{ from: Date | null; to: Date | null } | null> + >; + typeDate: TYPE_DATE; + setTypeDate: Dispatch>; + title?: string; +} diff --git a/src/constant/config/enum.ts b/src/constant/config/enum.ts index c49cbcd..c45ae1f 100644 --- a/src/constant/config/enum.ts +++ b/src/constant/config/enum.ts @@ -6,6 +6,7 @@ export enum QUERY_KEY { table_list_report, list_specialty_lookup, + examination_specialty_list, chi_tiet_benh_nhan, chi_tiet_don_thuoc, @@ -34,7 +35,7 @@ export enum TYPE_GENDER { } export enum TYPE_DATE { - ALL = -1, + // ALL = -1, TODAY = 1, YESTERDAY = 2, THIS_WEEK = 3, diff --git a/src/constant/config/index.ts b/src/constant/config/index.ts index 4ec16e1..9194234 100644 --- a/src/constant/config/index.ts +++ b/src/constant/config/index.ts @@ -5,6 +5,7 @@ import { Pill, Users, } from "lucide-react"; +import { TYPE_DATE } from "./enum"; export enum PATH { HOME = "/", @@ -35,17 +36,17 @@ export const Menus: { }[] = [ { group: [ - // { - // title: "Dashboard", - // icon: CircleGauge, - // path: PATH.DASHBOARD || PATH.HOME, - // pathActive: PATH.DASHBOARD, - // }, + { + title: "Dashboard", + icon: CircleGauge, + path: PATH.DASHBOARD || PATH.HOME, + pathActive: PATH.DASHBOARD, + }, { title: "Bệnh nhân", icon: Users, - path: PATH.PATIENT || PATH.HOME, - pathActive: PATH.PATIENT || PATH.HOME, + path: PATH.PATIENT, + pathActive: PATH.PATIENT, }, { title: "Phiên khám", @@ -69,4 +70,50 @@ export const Menus: { }, ]; +export const ListOptionFilterDate: { + name: string; + value: number; +}[] = [ + { + name: "Tất cả", + value: TYPE_DATE.ALL, + }, + { + name: "Hôm nay", + value: TYPE_DATE.TODAY, + }, + { + name: "Hôm qua", + value: TYPE_DATE.YESTERDAY, + }, + { + name: "Tuần này", + value: TYPE_DATE.THIS_WEEK, + }, + { + name: "Tuần trước", + value: TYPE_DATE.LAST_WEEK, + }, + { + name: "7 ngày trước", + value: TYPE_DATE.LAST_7_DAYS, + }, + { + name: "Tháng này", + value: TYPE_DATE.THIS_MONTH, + }, + { + name: "Tháng trước", + value: TYPE_DATE.LAST_MONTH, + }, + { + name: "Năm này", + value: TYPE_DATE.THIS_YEAR, + }, + { + name: "Lựa chọn", + value: TYPE_DATE.LUA_CHON, + }, +]; + export const KEY_STORE = "management-kham-benh"; diff --git a/src/services/consultationServices.ts b/src/services/consultationServices.ts index d1d4b51..4f45b7d 100644 --- a/src/services/consultationServices.ts +++ b/src/services/consultationServices.ts @@ -34,10 +34,26 @@ export interface ConsultationItem { glassRequired: string | null; glassType: string | null; examinerName: string; - specialtyStatus: number; - sharedItems: any[]; + specialtyStatus: string | number; + sharedItems: { + id: string; + prescriptionId: string; + medicineName: string; + dosage: string; + frequency: string; + duration: string; + instructions: string; + }[]; + }[]; + aggregatedPrescriptionItems: { + id: string; + prescriptionId: string; + medicineName: string; + dosage: string; + frequency: string; + duration: string; + instructions: string; }[]; - aggregatedPrescriptionItems: any[]; } export interface ConsultationDetail { @@ -46,7 +62,7 @@ export interface ConsultationDetail { patientName: string; sessionCode: string; visitDate: string; - status: number; + status: string | number; chiefComplaint: string | null; symptomsText: string | null; vitalSigns: string | null; @@ -66,10 +82,26 @@ export interface ConsultationDetail { glassRequired: boolean | null; glassType: string | null; examinerName: string; - specialtyStatus: number; - sharedItems: any[]; + specialtyStatus: string | number | undefined; + sharedItems: { + id: string; + prescriptionId: string; + medicineName: string; + dosage: string; + frequency: string; + duration: string; + instructions: string; + }[]; + }[]; + aggregatedPrescriptionItems: { + id: string; + prescriptionId: string; + medicineName: string; + dosage: string; + frequency: string; + duration: string; + instructions: string; }[]; - aggregatedPrescriptionItems: any[]; } export interface ConsultationResponse { diff --git a/src/services/prescriptionServices.ts b/src/services/prescriptionServices.ts index 7cc86e6..454ead0 100644 --- a/src/services/prescriptionServices.ts +++ b/src/services/prescriptionServices.ts @@ -28,9 +28,9 @@ export interface PrescriptionItem { export interface PrescriptionDetail { id: string; - examinationSessionId: string; + examinationSessionSpecialtyId: string; prescriptionCode: string; - prescriptionImg: string | null; + prescriptionImg: string; notes: string; createdBy: string; items: { @@ -112,11 +112,7 @@ const prescriptionServices = { /** * UPLOAD IMAGE */ - /** - * UPLOAD IMAGE - * @param file Đối tượng File lấy từ thẻ input hoặc upload component - * @param folderName Tên thư mục lưu trữ (Không bắt buộc) - */ + uploadImage: (file: File, folderName?: string) => { const formData = new FormData(); diff --git a/yarn.lock b/yarn.lock index c91235e..1e4f209 100644 --- a/yarn.lock +++ b/yarn.lock @@ -303,6 +303,44 @@ __metadata: languageName: node linkType: hard +"@floating-ui/core@npm:^1.7.5": + version: 1.7.5 + resolution: "@floating-ui/core@npm:1.7.5" + dependencies: + "@floating-ui/utils": "npm:^0.2.11" + checksum: 10c0/f9c52205e198b231d63a387b09c659aab08c46a1899e0b0bbe147b8b4f048b546f15ba17cb5d2a471da9534f1883d979425e13e5c4ceee67be63e4b0abd4db5d + languageName: node + linkType: hard + +"@floating-ui/dom@npm:^1.7.6": + version: 1.7.6 + resolution: "@floating-ui/dom@npm:1.7.6" + dependencies: + "@floating-ui/core": "npm:^1.7.5" + "@floating-ui/utils": "npm:^0.2.11" + checksum: 10c0/5c098e0d7b58c9bc769f276cca1766994c2c9c70c92d091a61bba8b3e9be53c011e0a79a8457fc2fb2f3d91697a26eb52e0a4962ef936dc963b45f58613c212f + languageName: node + linkType: hard + +"@floating-ui/react-dom@npm:^2.0.0": + version: 2.1.8 + resolution: "@floating-ui/react-dom@npm:2.1.8" + dependencies: + "@floating-ui/dom": "npm:^1.7.6" + peerDependencies: + react: ">=16.8.0" + react-dom: ">=16.8.0" + checksum: 10c0/26260ca4bb23b57c73b824062505abf977a008ce6e0463bdacca74f7e49853c4cd1d2bbf1a77c6caa17fa37dfffda2c6c4cd07a8737ebd7474aaff7818401d75 + languageName: node + linkType: hard + +"@floating-ui/utils@npm:^0.2.11": + version: 0.2.11 + resolution: "@floating-ui/utils@npm:0.2.11" + checksum: 10c0/f4bcea1559bdbb721ecc8e8ead423ac58d6a5b6e70b602cf0810ba6ad4ed1c77211b207faa88b278a9042f0c743133de08a203ed6741c1b6443423332884d5b3 + languageName: node + linkType: hard + "@humanfs/core@npm:^0.19.2": version: 0.19.2 resolution: "@humanfs/core@npm:0.19.2" @@ -733,6 +771,423 @@ __metadata: languageName: node linkType: hard +"@radix-ui/primitive@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/primitive@npm:1.1.3" + checksum: 10c0/88860165ee7066fa2c179f32ffcd3ee6d527d9dcdc0e8be85e9cb0e2c84834be8e3c1a976c74ba44b193f709544e12f54455d892b28e32f0708d89deda6b9f1d + languageName: node + linkType: hard + +"@radix-ui/react-arrow@npm:1.1.7": + version: 1.1.7 + resolution: "@radix-ui/react-arrow@npm:1.1.7" + dependencies: + "@radix-ui/react-primitive": "npm:2.1.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/c3b46766238b3ee2a394d8806a5141432361bf1425110c9f0dcf480bda4ebd304453a53f294b5399c6ee3ccfcae6fd544921fd01ddc379cf5942acdd7168664b + languageName: node + linkType: hard + +"@radix-ui/react-compose-refs@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-compose-refs@npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/d36a9c589eb75d634b9b139c80f916aadaf8a68a7c1c4b8c6c6b88755af1a92f2e343457042089f04cc3f23073619d08bb65419ced1402e9d4e299576d970771 + languageName: node + linkType: hard + +"@radix-ui/react-context@npm:1.1.2": + version: 1.1.2 + resolution: "@radix-ui/react-context@npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/cece731f8cc25d494c6589cc681e5c01a93867d895c75889973afa1a255f163c286e390baa7bc028858eaabe9f6b57270d0ca6377356f652c5557c1c7a41ccce + languageName: node + linkType: hard + +"@radix-ui/react-dismissable-layer@npm:1.1.11": + version: 1.1.11 + resolution: "@radix-ui/react-dismissable-layer@npm:1.1.11" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-escape-keydown": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/c825572a64073c4d3853702029979f6658770ffd6a98eabc4984e1dee1b226b4078a2a4dc7003f96475b438985e9b21a58e75f51db74dd06848dcae1f2d395dc + languageName: node + linkType: hard + +"@radix-ui/react-focus-guards@npm:1.1.3": + version: 1.1.3 + resolution: "@radix-ui/react-focus-guards@npm:1.1.3" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/0bab65eb8d7e4f72f685d63de7fbba2450e3cb15ad6a20a16b42195e9d335c576356f5a47cb58d1ffc115393e46d7b14b12c5d4b10029b0ec090861255866985 + languageName: node + linkType: hard + +"@radix-ui/react-focus-scope@npm:1.1.7": + version: 1.1.7 + resolution: "@radix-ui/react-focus-scope@npm:1.1.7" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/8a6071331bdeeb79b223463de75caf759b8ad19339cab838e537b8dbb2db236891a1f4df252445c854d375d43d9d315dfcce0a6b01553a2984ec372bb8f1300e + languageName: node + linkType: hard + +"@radix-ui/react-id@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-id@npm:1.1.1" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/7d12e76818763d592c331277ef62b197e2e64945307e650bd058f0090e5ae48bbd07691b23b7e9e977901ef4eadcb3e2d5eaeb17a13859083384be83fc1292c7 + languageName: node + linkType: hard + +"@radix-ui/react-popover@npm:^1.1.15": + version: 1.1.15 + resolution: "@radix-ui/react-popover@npm:1.1.15" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-dismissable-layer": "npm:1.1.11" + "@radix-ui/react-focus-guards": "npm:1.1.3" + "@radix-ui/react-focus-scope": "npm:1.1.7" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-popper": "npm:1.2.8" + "@radix-ui/react-portal": "npm:1.1.9" + "@radix-ui/react-presence": "npm:1.1.5" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-slot": "npm:1.2.3" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + aria-hidden: "npm:^1.2.4" + react-remove-scroll: "npm:^2.6.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/c1c76b5e5985b128d03b621424fb453f769931d497759a1977734d303007da9f970570cf3ea1f6968ab609ab4a97f384168bff056197bd2d3d422abea0e3614b + languageName: node + linkType: hard + +"@radix-ui/react-popper@npm:1.2.8": + version: 1.2.8 + resolution: "@radix-ui/react-popper@npm:1.2.8" + dependencies: + "@floating-ui/react-dom": "npm:^2.0.0" + "@radix-ui/react-arrow": "npm:1.1.7" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + "@radix-ui/react-use-rect": "npm:1.1.1" + "@radix-ui/react-use-size": "npm:1.1.1" + "@radix-ui/rect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/48e3f13eac3b8c13aca8ded37d74db17e1bb294da8d69f142ab6b8719a06c3f90051668bed64520bf9f3abdd77b382ce7ce209d056bb56137cecc949b69b421c + languageName: node + linkType: hard + +"@radix-ui/react-portal@npm:1.1.9": + version: 1.1.9 + resolution: "@radix-ui/react-portal@npm:1.1.9" + dependencies: + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/45b432497c722720c72c493a29ef6085bc84b50eafe79d48b45c553121b63e94f9cdb77a3a74b9c49126f8feb3feee009fe400d48b7759d3552396356b192cd7 + languageName: node + linkType: hard + +"@radix-ui/react-presence@npm:1.1.5": + version: 1.1.5 + resolution: "@radix-ui/react-presence@npm:1.1.5" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/d0e61d314250eeaef5369983cb790701d667f51734bafd98cf759072755562018052c594e6cdc5389789f4543cb0a4d98f03ff4e8f37338d6b5bf51a1700c1d1 + languageName: node + linkType: hard + +"@radix-ui/react-primitive@npm:2.1.3": + version: 2.1.3 + resolution: "@radix-ui/react-primitive@npm:2.1.3" + dependencies: + "@radix-ui/react-slot": "npm:1.2.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/fdff9b84913bb4172ef6d3af7442fca5f9bba5f2709cba08950071f819d7057aec3a4a2d9ef44cf9cbfb8014d02573c6884a04cff175895823aaef809ebdb034 + languageName: node + linkType: hard + +"@radix-ui/react-slot@npm:1.2.3": + version: 1.2.3 + resolution: "@radix-ui/react-slot@npm:1.2.3" + dependencies: + "@radix-ui/react-compose-refs": "npm:1.1.2" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/5913aa0d760f505905779515e4b1f0f71a422350f077cc8d26d1aafe53c97f177fec0e6d7fbbb50d8b5e498aa9df9f707ca75ae3801540c283b26b0136138eef + languageName: node + linkType: hard + +"@radix-ui/react-tooltip@npm:^1.2.8": + version: 1.2.8 + resolution: "@radix-ui/react-tooltip@npm:1.2.8" + dependencies: + "@radix-ui/primitive": "npm:1.1.3" + "@radix-ui/react-compose-refs": "npm:1.1.2" + "@radix-ui/react-context": "npm:1.1.2" + "@radix-ui/react-dismissable-layer": "npm:1.1.11" + "@radix-ui/react-id": "npm:1.1.1" + "@radix-ui/react-popper": "npm:1.2.8" + "@radix-ui/react-portal": "npm:1.1.9" + "@radix-ui/react-presence": "npm:1.1.5" + "@radix-ui/react-primitive": "npm:2.1.3" + "@radix-ui/react-slot": "npm:1.2.3" + "@radix-ui/react-use-controllable-state": "npm:1.2.2" + "@radix-ui/react-visually-hidden": "npm:1.2.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/de0cbae9c571a00671f160928d819e59502f59be8749f536ab4b180181d9d70aee3925a5b2555f8f32d0bea622bc35f65b70ca7ff0449e4844f891302310cc48 + languageName: node + linkType: hard + +"@radix-ui/react-use-callback-ref@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-callback-ref@npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/5f6aff8592dea6a7e46589808912aba3fb3b626cf6edd2b14f01638b61dbbe49eeb9f67cd5601f4c15b2fb547b9a7e825f7c4961acd4dd70176c969ae405f8d8 + languageName: node + linkType: hard + +"@radix-ui/react-use-controllable-state@npm:1.2.2": + version: 1.2.2 + resolution: "@radix-ui/react-use-controllable-state@npm:1.2.2" + dependencies: + "@radix-ui/react-use-effect-event": "npm:0.0.2" + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/f55c4b06e895293aed4b44c9ef26fb24432539f5346fcd6519c7745800535b571058685314e83486a45bf61dc83887e24826490d3068acc317fb0a9010516e63 + languageName: node + linkType: hard + +"@radix-ui/react-use-effect-event@npm:0.0.2": + version: 0.0.2 + resolution: "@radix-ui/react-use-effect-event@npm:0.0.2" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/e84ff72a3e76c5ae9c94941028bb4b6472f17d4104481b9eab773deab3da640ecea035e54da9d6f4df8d84c18ef6913baf92b7511bee06930dc58bd0c0add417 + languageName: node + linkType: hard + +"@radix-ui/react-use-escape-keydown@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-escape-keydown@npm:1.1.1" + dependencies: + "@radix-ui/react-use-callback-ref": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/bff53be99e940fef1d3c4df7d560e1d9133182e5a98336255d3063327d1d3dd4ec54a95dc5afe15cca4fb6c184f0a956c70de2815578c318cf995a7f9beabaa1 + languageName: node + linkType: hard + +"@radix-ui/react-use-layout-effect@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-layout-effect@npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/9f98fdaba008dfc58050de60a77670b885792df473cf82c1cef8daee919a5dd5a77d270209f5f0b0abfaac78cb1627396e3ff56c81b735be550409426fe8b040 + languageName: node + linkType: hard + +"@radix-ui/react-use-rect@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-rect@npm:1.1.1" + dependencies: + "@radix-ui/rect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/271711404c05c589c8dbdaa748749e7daf44bcc6bffc9ecd910821c3ebca0ee245616cf5b39653ce690f53f875c3836fd3f36f51ab1c628273b6db599eee4864 + languageName: node + linkType: hard + +"@radix-ui/react-use-size@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/react-use-size@npm:1.1.1" + dependencies: + "@radix-ui/react-use-layout-effect": "npm:1.1.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/851d09a816f44282e0e9e2147b1b571410174cc048703a50c4fa54d672de994fd1dfff1da9d480ecfd12c77ae8f48d74f01adaf668f074156b8cd0043c6c21d8 + languageName: node + linkType: hard + +"@radix-ui/react-visually-hidden@npm:1.2.3": + version: 1.2.3 + resolution: "@radix-ui/react-visually-hidden@npm:1.2.3" + dependencies: + "@radix-ui/react-primitive": "npm:2.1.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/cf86a37f1cbee50a964056f3dc4f6bb1ee79c76daa321f913aa20ff3e1ccdfafbf2b114d7bb616aeefc7c4b895e6ca898523fdb67710d89bd5d8edb739a0d9b6 + languageName: node + linkType: hard + +"@radix-ui/rect@npm:1.1.1": + version: 1.1.1 + resolution: "@radix-ui/rect@npm:1.1.1" + checksum: 10c0/0dac4f0f15691199abe6a0e067821ddd9d0349c0c05f39834e4eafc8403caf724106884035ae91bbc826e10367e6a5672e7bec4d4243860fa7649de246b1f60b + languageName: node + linkType: hard + "@reduxjs/toolkit@npm:^2.12.0": version: 2.12.0 resolution: "@reduxjs/toolkit@npm:2.12.0" @@ -1393,6 +1848,15 @@ __metadata: languageName: node linkType: hard +"aria-hidden@npm:^1.2.4": + version: 1.2.6 + resolution: "aria-hidden@npm:1.2.6" + dependencies: + tslib: "npm:^2.0.0" + checksum: 10c0/7720cb539497a9f760f68f98a4b30f22c6767aa0e72fa7d58279f7c164e258fc38b2699828f8de881aab0fc8e9c56d1313a3f1a965046fc0381a554dbc72b54a + languageName: node + linkType: hard + "aria-query@npm:^5.3.2": version: 5.3.2 resolution: "aria-query@npm:5.3.2" @@ -1890,6 +2354,13 @@ __metadata: languageName: node linkType: hard +"detect-node-es@npm:^1.1.0": + version: 1.1.0 + resolution: "detect-node-es@npm:1.1.0" + checksum: 10c0/e562f00de23f10c27d7119e1af0e7388407eb4b06596a25f6d79a360094a109ff285de317f02b090faae093d314cf6e73ac3214f8a5bb3a0def5bece94557fbe + languageName: node + linkType: hard + "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -2574,6 +3045,13 @@ __metadata: languageName: node linkType: hard +"get-nonce@npm:^1.0.0": + version: 1.0.1 + resolution: "get-nonce@npm:1.0.1" + checksum: 10c0/2d7df55279060bf0568549e1ffc9b84bc32a32b7541675ca092dce56317cdd1a59a98dcc4072c9f6a980779440139a3221d7486f52c488e69dc0fd27b1efb162 + languageName: node + linkType: hard + "get-proto@npm:^1.0.0, get-proto@npm:^1.0.1": version: 1.0.1 resolution: "get-proto@npm:1.0.1" @@ -3171,6 +3649,8 @@ __metadata: version: 0.0.0-use.local resolution: "kham-benh-mo-pho@workspace:." dependencies: + "@radix-ui/react-popover": "npm:^1.1.15" + "@radix-ui/react-tooltip": "npm:^1.2.8" "@reduxjs/toolkit": "npm:^2.12.0" "@tailwindcss/postcss": "npm:^4" "@tanstack/react-query": "npm:^5.100.11" @@ -3192,6 +3672,7 @@ __metadata: nprogress: "npm:^0.2.0" react: "npm:19.2.4" react-dom: "npm:19.2.4" + react-moment: "npm:^2.0.2" react-redux: "npm:^9.3.0" react-toastify: "npm:^11.1.0" tailwindcss: "npm:^4" @@ -3881,6 +4362,20 @@ __metadata: languageName: node linkType: hard +"react-moment@npm:^2.0.2": + version: 2.0.2 + resolution: "react-moment@npm:2.0.2" + peerDependencies: + moment: ^2.29.0 + moment-duration-format: ^2.2.2 + react: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + moment-duration-format: + optional: true + checksum: 10c0/6e3c3261284376a03ccd9de807aa81737468a2384a75f25d33a1a5cbe90d352b995f72efe77370b1525be756ab7b7f65ef81706bce7b3fd9058b19fb57f93fb9 + languageName: node + linkType: hard + "react-redux@npm:^9.3.0": version: 9.3.0 resolution: "react-redux@npm:9.3.0" @@ -3900,6 +4395,57 @@ __metadata: languageName: node linkType: hard +"react-remove-scroll-bar@npm:^2.3.7": + version: 2.3.8 + resolution: "react-remove-scroll-bar@npm:2.3.8" + dependencies: + react-style-singleton: "npm:^2.2.2" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/9a0675c66cbb52c325bdbfaed80987a829c4504cefd8ff2dd3b6b3afc9a1500b8ec57b212e92c1fb654396d07bbe18830a8146fe77677d2a29ce40b5e1f78654 + languageName: node + linkType: hard + +"react-remove-scroll@npm:^2.6.3": + version: 2.7.2 + resolution: "react-remove-scroll@npm:2.7.2" + dependencies: + react-remove-scroll-bar: "npm:^2.3.7" + react-style-singleton: "npm:^2.2.3" + tslib: "npm:^2.1.0" + use-callback-ref: "npm:^1.3.3" + use-sidecar: "npm:^1.1.3" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/b5f3315bead75e72853f492c0b51ba8fb4fa09a4534d7fc42d6fcd59ca3e047cf213279ffc1e35b337e314ef5a04cb2b12544c85e0078802271731c27c09e5aa + languageName: node + linkType: hard + +"react-style-singleton@npm:^2.2.2, react-style-singleton@npm:^2.2.3": + version: 2.2.3 + resolution: "react-style-singleton@npm:2.2.3" + dependencies: + get-nonce: "npm:^1.0.0" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/841938ff16d16a6b76895f4cb2e1fea957e5fe3b30febbf03a54892dae1c9153f2383e231dea0b3ba41192ad2f2849448fa859caccd288943bce32639e971bee + languageName: node + linkType: hard + "react-toastify@npm:^11.1.0": version: 11.1.0 resolution: "react-toastify@npm:11.1.0" @@ -4482,7 +5028,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.4.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": +"tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.8.0, tslib@npm:^2.8.1": version: 2.8.1 resolution: "tslib@npm:2.8.1" checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 @@ -4704,6 +5250,37 @@ __metadata: languageName: node linkType: hard +"use-callback-ref@npm:^1.3.3": + version: 1.3.3 + resolution: "use-callback-ref@npm:1.3.3" + dependencies: + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/f887488c6e6075cdad4962979da1714b217bcb1ee009a9e57ce9a844bcfc4c3a99e93983dfc2e5af9e0913824d24e730090ff255e902c516dcb58d2d3837e01c + languageName: node + linkType: hard + +"use-sidecar@npm:^1.1.3": + version: 1.1.3 + resolution: "use-sidecar@npm:1.1.3" + dependencies: + detect-node-es: "npm:^1.1.0" + tslib: "npm:^2.0.0" + peerDependencies: + "@types/react": "*" + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/161599bf921cfaa41c85d2b01c871975ee99260f3e874c2d41c05890d41170297bdcf314bc5185e7a700de2034ac5b888e3efc8e9f35724f4918f53538d717c9 + languageName: node + linkType: hard + "use-sync-external-store@npm:^1.4.0": version: 1.6.0 resolution: "use-sync-external-store@npm:1.6.0"