From 62ca6dd0f954bf485061ee1d875dcc49964a3d8f Mon Sep 17 00:00:00 2001 From: TuanVT Date: Sun, 31 May 2026 09:38:10 +0700 Subject: [PATCH] update --- .../consultation/update-specialty/page.tsx | 14 + src/app/statistical/page.tsx | 14 + src/components/customs/FilterCustom.tsx | 194 ++++++++++ src/components/customs/FilterMany.tsx | 205 +++++++++++ .../layouts/BaseLayout/BaseLayout.tsx | 47 +-- .../BaseLayout/componets/Header/Header.tsx | 8 +- .../componets/MenuProfile/MenuProfile.tsx | 4 +- .../CreateConsultation/CreateConsultation.tsx | 107 +++++- .../DetailConsultation/DetailConsultation.tsx | 65 +++- .../MainPageConsultation.tsx | 188 ++++++++-- .../TableHistoryPatient.tsx | 46 ++- .../UpdateConsultation/UpdateConsultation.tsx | 4 + .../UpdateSpecialtyClinicId.tsx | 341 ++++++++++++++++++ .../UpdateSpecialtyClinicId/index.ts | 1 + .../patient/DetailPatient/DetailPatient.tsx | 165 +++++---- .../MainPagePatient/MainPagePatient.tsx | 12 +- .../PopupUpdatePatient/PopupUpdatePatient.tsx | 4 +- .../CreatePrescription/CreatePrescription.tsx | 316 ++++++++-------- .../DetailPrescription/DetailPrescription.tsx | 2 +- .../MainPrescription/MainPrescription.tsx | 3 +- .../MainPageStatistical.tsx | 107 ++++++ .../statistical/MainPageStatistical/index.ts | 1 + .../components/SelectForm/interface/index.ts | 2 +- src/constant/config/enum.ts | 20 +- src/constant/config/index.ts | 16 +- src/services/authServices.ts | 8 +- src/services/consultationServices.ts | 114 +++++- src/services/index.ts | 63 +++- src/services/patientServices.ts | 10 +- src/services/prescriptionServices.ts | 46 ++- src/services/specialtyServices.ts | 16 + src/services/userServices.ts | 23 +- 32 files changed, 1785 insertions(+), 381 deletions(-) create mode 100644 src/app/consultation/update-specialty/page.tsx create mode 100644 src/app/statistical/page.tsx create mode 100644 src/components/customs/FilterCustom.tsx create mode 100644 src/components/customs/FilterMany.tsx create mode 100644 src/components/page/consultation/UpdateSpecialtyClinicId/UpdateSpecialtyClinicId.tsx create mode 100644 src/components/page/consultation/UpdateSpecialtyClinicId/index.ts create mode 100644 src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx create mode 100644 src/components/page/statistical/MainPageStatistical/index.ts create mode 100644 src/services/specialtyServices.ts diff --git a/src/app/consultation/update-specialty/page.tsx b/src/app/consultation/update-specialty/page.tsx new file mode 100644 index 0000000..268f39d --- /dev/null +++ b/src/app/consultation/update-specialty/page.tsx @@ -0,0 +1,14 @@ +"use client"; +import BaseLayout from "@/components/layouts/BaseLayout"; +import UpdateSpecialtyClinicId from "@/components/page/consultation/UpdateSpecialtyClinicId"; +import React from "react"; + +const page = () => { + return ( + + + + ); +}; + +export default page; diff --git a/src/app/statistical/page.tsx b/src/app/statistical/page.tsx new file mode 100644 index 0000000..482ba8e --- /dev/null +++ b/src/app/statistical/page.tsx @@ -0,0 +1,14 @@ +"use client"; +import BaseLayout from "@/components/layouts/BaseLayout"; +import MainPageStatistical from "@/components/page/statistical/MainPageStatistical"; +import React from "react"; + +const page = () => { + return ( + + + + ); +}; + +export default page; diff --git a/src/components/customs/FilterCustom.tsx b/src/components/customs/FilterCustom.tsx new file mode 100644 index 0000000..e8e0ce1 --- /dev/null +++ b/src/components/customs/FilterCustom.tsx @@ -0,0 +1,194 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import clsx from "clsx"; +import { Check, ChevronDown } from "lucide-react"; + +import { removeVietnameseTones } from "@/common/funcs/optionConvert"; + +interface OptionItem { + uuid: string | number; + name: string; + code?: string; +} + +interface PropsFilterCustom { + styleRounded?: boolean; + showOptionAll?: boolean; + name: string; + listOption: OptionItem[]; + value: T; + onChange: (value: T) => void; +} + +function FilterCustom({ + styleRounded = false, + showOptionAll = true, + name, + listOption, + value, + onChange, +}: PropsFilterCustom) { + const wrapperRef = useRef(null); + const inputRef = useRef(null); + + const [open, setOpen] = useState(false); + const [keyword, setKeyword] = useState(""); + + const normalizedKeyword = removeVietnameseTones(keyword); + + useEffect(() => { + if (open) { + requestAnimationFrame(() => { + inputRef.current?.focus(); + }); + } + }, [open]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target as Node) + ) { + setOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + const filteredOptions = listOption.filter((item) => + removeVietnameseTones(item.name).includes(normalizedKeyword), + ); + + const selectedLabel = + listOption.find((item) => item.uuid === value)?.name || "Tất cả"; + + return ( +
+ {/* Trigger */} +
setOpen((prev) => !prev)} + className={clsx( + "flex items-center justify-between gap-2 rounded-full border", + "h-12 min-w-[240px] px-4", + "cursor-pointer select-none", + "border border-[#e1e5ed] bg-white", + "transition-all", + "hover:border-[#0019f6]", + styleRounded ? "rounded-full" : "rounded-md", + open && "border-[#0019f6]", + )} + > +
+ {name}: + + + {selectedLabel} + +
+ + +
+ + {/* Dropdown */} + {open && ( +
+
+ {/* Search */} + setKeyword(e.target.value)} + placeholder="Tìm kiếm..." + className=" + mb-2 w-full + rounded-lg border border-[#e1e5ed] + px-3 py-2 text-sm + outline-none + focus:border-[#0019f6] + " + /> + + {/* Options */} +
+ {showOptionAll && ( +
{ + onChange(null as T); + setOpen(false); + }} + className={clsx( + "flex cursor-pointer items-center justify-between", + "border-b border-[#e2e0ec]", + "px-3 py-2 text-sm", + "hover:text-[#0019f6]", + (value === null || value === "") && "text-[#0019f6]", + )} + > + Tất cả + + {(value === null || value === "") && ( + + )} +
+ )} + + {filteredOptions.map((item) => { + const isActive = item.uuid === value; + + return ( +
{ + onChange(item.uuid as T); + setOpen(false); + }} + className={clsx( + "mt-1 flex cursor-pointer items-center justify-between", + "border-b border-[#e2e0ec]", + "px-3 py-2 text-sm", + "hover:text-[#0019f6]", + "last:border-none", + isActive && "text-[#0019f6]", + )} + > + {item.name} + + {isActive && } +
+ ); + })} + + {filteredOptions.length === 0 && ( +
+ Không tìm thấy dữ liệu +
+ )} +
+
+
+ )} +
+ ); +} + +export default FilterCustom; diff --git a/src/components/customs/FilterMany.tsx b/src/components/customs/FilterMany.tsx new file mode 100644 index 0000000..b289c66 --- /dev/null +++ b/src/components/customs/FilterMany.tsx @@ -0,0 +1,205 @@ +"use client"; + +import React, { + Dispatch, + SetStateAction, + useEffect, + useRef, + useState, +} from "react"; +import clsx from "clsx"; +import { Check, ChevronDown } from "lucide-react"; +import Button from "./custom-button"; +import { removeVietnameseTones } from "@/common/funcs/optionConvert"; + +interface OptionItem { + uuid: T; + name: string; + code?: string; +} + +interface PropsFilterMany { + styleRounded?: boolean; + small?: boolean; + name: string; + listOption: OptionItem[]; + value: T[]; + setValue: Dispatch>; +} + +function FilterMany({ + styleRounded = false, + small = false, + name, + listOption, + value, + setValue, +}: PropsFilterMany) { + const inputRef = useRef(null); + + const [open, setOpen] = useState(false); + const [keyword, setKeyword] = useState(""); + const [tempSelected, setTempSelected] = useState([]); + + const wrapperRef = useRef(null); + + // sync khi mở dropdown + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + wrapperRef.current && + !wrapperRef.current.contains(event.target as Node) + ) { + setOpen(false); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, []); + + const handleOpen = () => { + setOpen((prev) => !prev); + }; + + useEffect(() => { + if (open) { + requestAnimationFrame(() => { + inputRef.current?.focus(); + }); + } + }, [open]); + + const isAllSelected = + tempSelected.length === 0 || tempSelected.length === listOption.length; + + const filteredList = listOption.filter((item) => + removeVietnameseTones(item.name).includes( + removeVietnameseTones(keyword || ""), + ), + ); + + return ( +
+ {/* TRIGGER */} +
+
+

{name}:

+ +

+ {value.length === 0 || value.length === listOption.length + ? "Tất cả" + : `${value.length} ${name.toLowerCase()}`} +

+
+ + +
+ + {/* DROPDOWN */} + {open && ( +
+ {/* SEARCH */} + setKeyword(e.target.value)} + placeholder="Tìm kiếm..." + className=" + w-full px-3 py-2 text-sm + border border-gray-200 rounded-lg outline-none + focus:border-blue-500 + " + /> + + {/* LIST */} +
+ {/* ALL */} +
setTempSelected([])} + className={clsx( + "flex items-center justify-between px-3 py-2 cursor-pointer rounded-lg hover:bg-gray-100", + isAllSelected && "bg-blue-50", + )} + > +

Tất cả

+ + {isAllSelected && } +
+ + {/* OPTIONS */} + {filteredList.map((item) => { + const active = tempSelected.includes(item.uuid); + + return ( +
{ + setTempSelected((prev) => + prev.includes(item.uuid) + ? prev.filter((x) => x !== item.uuid) + : [...prev, item.uuid], + ); + }} + className={clsx( + "flex items-center justify-between px-3 py-2 cursor-pointer rounded-lg hover:bg-gray-100", + active && "bg-blue-50", + )} + > +

{item.name}

+ + {active && } +
+ ); + })} +
+ + {/* ACTION */} +
+ + + +
+
+ )} +
+ ); +} + +export default FilterMany; diff --git a/src/components/layouts/BaseLayout/BaseLayout.tsx b/src/components/layouts/BaseLayout/BaseLayout.tsx index d9c7e6a..e165e55 100644 --- a/src/components/layouts/BaseLayout/BaseLayout.tsx +++ b/src/components/layouts/BaseLayout/BaseLayout.tsx @@ -9,18 +9,9 @@ import RequireAuth from "@/components/protected/RequiredAuth"; export const ContextBaseLayout = React.createContext({}); const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => { - const [showFull, setShowFull] = React.useState(false); + const [showFull, setShowFull] = React.useState(true); const [openMenuMobile, setOpenMenuMobile] = React.useState(false); - // Khi mount trên client, mở sidebar mặc định nếu là màn hình desktop (xl) - // React.useEffect(() => { - // try { - // if (window?.innerWidth >= 1280) { - // setShowFull(true); - // } - // } catch (e) { - // // ignore - // } - // }, []); + return ( { {/* SIDEBAR (Dùng chung cho cả Desktop & Mobile) */}