update
This commit is contained in:
parent
56e2733d8f
commit
62ca6dd0f9
14
src/app/consultation/update-specialty/page.tsx
Normal file
14
src/app/consultation/update-specialty/page.tsx
Normal file
@ -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 (
|
||||||
|
<BaseLayout title="Phiên khám">
|
||||||
|
<UpdateSpecialtyClinicId />
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
14
src/app/statistical/page.tsx
Normal file
14
src/app/statistical/page.tsx
Normal file
@ -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 (
|
||||||
|
<BaseLayout title="Thống kê chuyên khoa">
|
||||||
|
<MainPageStatistical />
|
||||||
|
</BaseLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
194
src/components/customs/FilterCustom.tsx
Normal file
194
src/components/customs/FilterCustom.tsx
Normal file
@ -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<T extends string | number | null> {
|
||||||
|
styleRounded?: boolean;
|
||||||
|
showOptionAll?: boolean;
|
||||||
|
name: string;
|
||||||
|
listOption: OptionItem[];
|
||||||
|
value: T;
|
||||||
|
onChange: (value: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilterCustom<T extends string | number | null>({
|
||||||
|
styleRounded = false,
|
||||||
|
showOptionAll = true,
|
||||||
|
name,
|
||||||
|
listOption,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: PropsFilterCustom<T>) {
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(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 (
|
||||||
|
<div ref={wrapperRef} className="relative">
|
||||||
|
{/* Trigger */}
|
||||||
|
<div
|
||||||
|
onClick={() => 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]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center overflow-hidden">
|
||||||
|
<span className="text-sm font-medium text-[#777e90]">{name}:</span>
|
||||||
|
|
||||||
|
<span className="ml-2 truncate text-sm font-medium">
|
||||||
|
{selectedLabel}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className={clsx(
|
||||||
|
"transition-transform duration-200",
|
||||||
|
open && "rotate-180",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dropdown */}
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"absolute left-0 top-full z-50 mt-2",
|
||||||
|
"min-w-[240px] overflow-hidden",
|
||||||
|
"rounded-2xl bg-white shadow-xl",
|
||||||
|
styleRounded && "min-w-[200px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="p-3">
|
||||||
|
{/* Search */}
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
value={keyword}
|
||||||
|
onChange={(e) => 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 */}
|
||||||
|
<div className="max-h-[320px] overflow-y-auto">
|
||||||
|
{showOptionAll && (
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
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]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>Tất cả</span>
|
||||||
|
|
||||||
|
{(value === null || value === "") && (
|
||||||
|
<Check size={18} className="text-[#5755FF]" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filteredOptions.map((item) => {
|
||||||
|
const isActive = item.uuid === value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.uuid}
|
||||||
|
onClick={() => {
|
||||||
|
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]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
|
||||||
|
{isActive && <Check size={18} className="text-[#5755FF]" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{filteredOptions.length === 0 && (
|
||||||
|
<div className="py-3 text-center text-sm text-gray-400">
|
||||||
|
Không tìm thấy dữ liệu
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterCustom;
|
||||||
205
src/components/customs/FilterMany.tsx
Normal file
205
src/components/customs/FilterMany.tsx
Normal file
@ -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<T> {
|
||||||
|
uuid: T;
|
||||||
|
name: string;
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PropsFilterMany<T extends string | number> {
|
||||||
|
styleRounded?: boolean;
|
||||||
|
small?: boolean;
|
||||||
|
name: string;
|
||||||
|
listOption: OptionItem<T>[];
|
||||||
|
value: T[];
|
||||||
|
setValue: Dispatch<SetStateAction<T[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilterMany<T extends string | number>({
|
||||||
|
styleRounded = false,
|
||||||
|
small = false,
|
||||||
|
name,
|
||||||
|
listOption,
|
||||||
|
value,
|
||||||
|
setValue,
|
||||||
|
}: PropsFilterMany<T>) {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [keyword, setKeyword] = useState("");
|
||||||
|
const [tempSelected, setTempSelected] = useState<T[]>([]);
|
||||||
|
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(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 (
|
||||||
|
<div ref={wrapperRef} className="relative inline-block">
|
||||||
|
{/* TRIGGER */}
|
||||||
|
<div
|
||||||
|
onClick={handleOpen}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center justify-between px-4 cursor-pointer border transition",
|
||||||
|
"border-gray-200 bg-white hover:border-blue-600",
|
||||||
|
small ? "h-10 text-sm" : "h-12",
|
||||||
|
styleRounded ? "rounded-full bg-gray-100" : "rounded-md",
|
||||||
|
open && "border-blue-600",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<p className="text-gray-400 text-sm font-medium">{name}:</p>
|
||||||
|
|
||||||
|
<p className="ml-2 text-sm font-medium">
|
||||||
|
{value.length === 0 || value.length === listOption.length
|
||||||
|
? "Tất cả"
|
||||||
|
: `${value.length} ${name.toLowerCase()}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ChevronDown
|
||||||
|
size={16}
|
||||||
|
className={clsx("transition duration-300", open && "rotate-180")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* DROPDOWN */}
|
||||||
|
{open && (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"absolute left-0 top-full z-50 mt-2",
|
||||||
|
"w-[280px] bg-white border border-gray-200 shadow-lg p-3 space-y-3",
|
||||||
|
styleRounded && "rounded-2xl",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* SEARCH */}
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
value={keyword}
|
||||||
|
onChange={(e) => 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 */}
|
||||||
|
<div className="max-h-60 overflow-y-auto space-y-1">
|
||||||
|
{/* ALL */}
|
||||||
|
<div
|
||||||
|
onClick={() => setTempSelected([])}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center justify-between px-3 py-2 cursor-pointer rounded-lg hover:bg-gray-100",
|
||||||
|
isAllSelected && "bg-blue-50",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p>Tất cả</p>
|
||||||
|
|
||||||
|
{isAllSelected && <Check size={16} className="text-blue-600" />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OPTIONS */}
|
||||||
|
{filteredList.map((item) => {
|
||||||
|
const active = tempSelected.includes(item.uuid);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={String(item.uuid)}
|
||||||
|
onClick={() => {
|
||||||
|
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",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p>{item.name}</p>
|
||||||
|
|
||||||
|
{active && <Check size={16} className="text-blue-600" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ACTION */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="grey"
|
||||||
|
rounded="full"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
Hủy bỏ
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="midnightBlue"
|
||||||
|
rounded="full"
|
||||||
|
onClick={() => {
|
||||||
|
setValue(tempSelected);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Xác nhận
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterMany;
|
||||||
@ -9,18 +9,9 @@ import RequireAuth from "@/components/protected/RequiredAuth";
|
|||||||
export const ContextBaseLayout = React.createContext<TContextBaseLayout>({});
|
export const ContextBaseLayout = React.createContext<TContextBaseLayout>({});
|
||||||
|
|
||||||
const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
||||||
const [showFull, setShowFull] = React.useState(false);
|
const [showFull, setShowFull] = React.useState(true);
|
||||||
const [openMenuMobile, setOpenMenuMobile] = React.useState(false);
|
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 (
|
return (
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<ContextBaseLayout
|
<ContextBaseLayout
|
||||||
@ -39,11 +30,17 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
|||||||
{/* SIDEBAR (Dùng chung cho cả Desktop & Mobile) */}
|
{/* SIDEBAR (Dùng chung cho cả Desktop & Mobile) */}
|
||||||
<nav
|
<nav
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"fixed top-0 left-0 h-full w-[240px] z-[21] bg-white transition-all duration-300 border-r border-[#f4f7fa]",
|
"fixed top-0 left-0 h-screen bg-white border-r border-[#f4f7fa] z-[30] transition-all duration-300 ease-in-out",
|
||||||
// Desktop logic
|
|
||||||
showFull ? "xl:translate-x-0" : "xl:-translate-x-full",
|
/* DESKTOP */
|
||||||
// Mobile logic
|
showFull
|
||||||
openMenuMobile ? "translate-x-0" : "-translate-x-full",
|
? "xl:w-[240px] xl:translate-x-0"
|
||||||
|
: "xl:w-[240px] xl:-translate-x-full",
|
||||||
|
|
||||||
|
/* MOBILE + TABLET */
|
||||||
|
openMenuMobile
|
||||||
|
? "translate-x-0 w-[240px]"
|
||||||
|
: "-translate-x-full w-[240px]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
@ -53,10 +50,14 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
|||||||
<header
|
<header
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"fixed top-0 right-0 h-[68px] z-[11] bg-white transition-all duration-300 border-b border-[#f4f7fa]",
|
"fixed top-0 right-0 h-[68px] z-[11] bg-white transition-all duration-300 border-b border-[#f4f7fa]",
|
||||||
|
|
||||||
|
/* DESKTOP */
|
||||||
showFull
|
showFull
|
||||||
? "xl:left-[240px] xl:w-[calc(100%-240px)]"
|
? "xl:left-[240px] xl:w-[calc(100%-240px)]"
|
||||||
: "left-0 w-full",
|
: "xl:left-0 xl:w-full",
|
||||||
"left-0 w-full", // Mặc định full width trên mobile
|
|
||||||
|
/* MOBILE */
|
||||||
|
"left-0 w-full",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Header title={title} breadcrumb={breadcrumb} />
|
<Header title={title} breadcrumb={breadcrumb} />
|
||||||
@ -65,9 +66,13 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
|||||||
{/* MAIN CONTENT */}
|
{/* MAIN CONTENT */}
|
||||||
<main
|
<main
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"pt-[92px] pb-6 px-6 transition-all duration-300",
|
"pt-[92px] pb-6 px-6 transition-all duration-300 ease-in-out",
|
||||||
showFull ? "xl:pl-[264px]" : "xl:pl-6",
|
|
||||||
"pl-6", // Mặc định padding trên mobile
|
/* DESKTOP */
|
||||||
|
showFull ? "xl:ml-[240px]" : "xl:ml-0",
|
||||||
|
|
||||||
|
/* MOBILE */
|
||||||
|
"ml-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -20,8 +20,10 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
|||||||
|
|
||||||
//Đóng menu mobile khi đổi route
|
//Đóng menu mobile khi đổi route
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
context.setOpenMenuMobile?.(false);
|
if (window.innerWidth < 1280) {
|
||||||
}, [pathname, context]);
|
context.setOpenMenuMobile?.(false);
|
||||||
|
}
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
const toggleFullScreen = () => {
|
const toggleFullScreen = () => {
|
||||||
if (!document.fullscreenElement) {
|
if (!document.fullscreenElement) {
|
||||||
@ -37,7 +39,7 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
|||||||
{/* Nút bấm Mobile: Mở menu mobile */}
|
{/* Nút bấm Mobile: Mở menu mobile */}
|
||||||
<div
|
<div
|
||||||
className="xl:hidden cursor-pointer"
|
className="xl:hidden cursor-pointer"
|
||||||
onClick={() => context?.setOpenMenuMobile?.(true)}
|
onClick={() => context?.setOpenMenuMobile?.(!context?.openMenuMobile)}
|
||||||
>
|
>
|
||||||
<Image src={icons.hamburger} alt="menu" width={20} height={20} />
|
<Image src={icons.hamburger} alt="menu" width={20} height={20} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -51,7 +51,7 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-[260px] bg-white rounded-xl shadow-md p-2">
|
<div className="w-[260px] bg-white rounded-xl shadow-md p-2">
|
||||||
{/* PROFILE */}
|
{/* PROFILE */}
|
||||||
{/* <Link
|
<Link
|
||||||
href={PATH.PROFILE}
|
href={PATH.PROFILE}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@ -65,7 +65,7 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
|||||||
<p className="text-xs text-gray-500">Chi tiết tài khoản</p>
|
<p className="text-xs text-gray-500">Chi tiết tài khoản</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="h-px bg-gray-200 my-1" /> */}
|
<div className="h-px bg-gray-200 my-1" />
|
||||||
|
|
||||||
{/* CHANGE PASSWORD */}
|
{/* CHANGE PASSWORD */}
|
||||||
{/* <Link
|
{/* <Link
|
||||||
|
|||||||
@ -22,13 +22,17 @@ import patientServices, {
|
|||||||
import { QUERY_KEY } from "@/constant/config/enum";
|
import { QUERY_KEY } from "@/constant/config/enum";
|
||||||
|
|
||||||
import CustomButton from "@/components/customs/custom-button";
|
import CustomButton from "@/components/customs/custom-button";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||||
import GridColumn from "@/components/layouts/GridColumn";
|
import GridColumn from "@/components/layouts/GridColumn";
|
||||||
import { toastWarn } from "@/common/funcs/toast";
|
import { toastWarn } from "@/common/funcs/toast";
|
||||||
|
import specialtyServices, { SpecialtyItem } from "@/services/specialtyServices";
|
||||||
|
import SelectMany from "@/components/utils/FormCustom/components/SelectMany";
|
||||||
|
|
||||||
const CreateConsultation = () => {
|
const CreateConsultation = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const patientId = searchParams.get("patientId");
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [form, setForm] = useState<{
|
const [form, setForm] = useState<{
|
||||||
@ -36,17 +40,13 @@ const CreateConsultation = () => {
|
|||||||
chiefComplaint: string;
|
chiefComplaint: string;
|
||||||
symptomsText: string;
|
symptomsText: string;
|
||||||
vitalSigns: string;
|
vitalSigns: string;
|
||||||
diagnosis: string;
|
specialtyIds: string[];
|
||||||
treatmentPlan: string;
|
|
||||||
doctorNotes: string;
|
|
||||||
}>({
|
}>({
|
||||||
patientId: "",
|
patientId: patientId || "",
|
||||||
chiefComplaint: "",
|
chiefComplaint: "",
|
||||||
symptomsText: "",
|
symptomsText: "",
|
||||||
vitalSigns: "",
|
vitalSigns: "",
|
||||||
diagnosis: "",
|
specialtyIds: [],
|
||||||
treatmentPlan: "",
|
|
||||||
doctorNotes: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@ -80,6 +80,24 @@ const CreateConsultation = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
GET LIST SPECIALTY
|
||||||
|
========================= */
|
||||||
|
const specialtyQuery = useQuery<SpecialtyItem[]>({
|
||||||
|
queryKey: [QUERY_KEY.table_list_specialty],
|
||||||
|
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await httpRequest<SpecialtyItem[]>({
|
||||||
|
showMessageFailed: true,
|
||||||
|
http: specialtyServices.getSpecialty(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res || [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const specialties: SpecialtyItem[] = specialtyQuery.data || [];
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
OPTIONS SELECT
|
OPTIONS SELECT
|
||||||
========================= */
|
========================= */
|
||||||
@ -99,6 +117,10 @@ const CreateConsultation = () => {
|
|||||||
|
|
||||||
http: consultationServices.createConsultations({
|
http: consultationServices.createConsultations({
|
||||||
patientId: form.patientId,
|
patientId: form.patientId,
|
||||||
|
chiefComplaint: form.chiefComplaint,
|
||||||
|
symptomsText: form.symptomsText,
|
||||||
|
vitalSigns: form.vitalSigns,
|
||||||
|
specialtyIds: form.specialtyIds,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -123,7 +145,6 @@ const CreateConsultation = () => {
|
|||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
max-w-[95vw]
|
max-w-[95vw]
|
||||||
h-[485px]
|
|
||||||
border
|
border
|
||||||
rounded-lg
|
rounded-lg
|
||||||
bg-white
|
bg-white
|
||||||
@ -158,6 +179,7 @@ const CreateConsultation = () => {
|
|||||||
<span className="text-red-500">*</span>
|
<span className="text-red-500">*</span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
readOnly={!!patientId}
|
||||||
placeholder="Chọn bệnh nhân"
|
placeholder="Chọn bệnh nhân"
|
||||||
value={form.patientId}
|
value={form.patientId}
|
||||||
options={patientOptions}
|
options={patientOptions}
|
||||||
@ -180,6 +202,63 @@ const CreateConsultation = () => {
|
|||||||
getOptionValue={(item: PatientItem) => item.id}
|
getOptionValue={(item: PatientItem) => item.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-5 mb-2">
|
||||||
|
<GridColumn col={3}>
|
||||||
|
<InputForm
|
||||||
|
label="Lý do khám"
|
||||||
|
name="chiefComplaint"
|
||||||
|
type="text"
|
||||||
|
value={form.chiefComplaint}
|
||||||
|
placeholder="Nhập lý do khám"
|
||||||
|
isRequired
|
||||||
|
onChangeValue={(v) =>
|
||||||
|
setForm((prev) => ({ ...prev, chiefComplaint: String(v) }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputForm
|
||||||
|
type="text"
|
||||||
|
label="Triệu chứng"
|
||||||
|
name="symptomsText"
|
||||||
|
placeholder="Nhập triệu chứng"
|
||||||
|
isRequired
|
||||||
|
value={form.symptomsText}
|
||||||
|
onChangeValue={(v) =>
|
||||||
|
setForm((prev) => ({ ...prev, symptomsText: String(v) }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputForm
|
||||||
|
type="text"
|
||||||
|
label="Chỉ số huyết áp, chỉ số mạch"
|
||||||
|
name="vitalSigns"
|
||||||
|
placeholder="Nhập chỉ số huyết áp, chỉ số mạch"
|
||||||
|
value={form.vitalSigns}
|
||||||
|
isRequired
|
||||||
|
onChangeValue={(v) =>
|
||||||
|
setForm((prev) => ({ ...prev, vitalSigns: String(v) }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</GridColumn>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-5 mb2">
|
||||||
|
<SelectMany<SpecialtyItem>
|
||||||
|
label="Chuyên khoa"
|
||||||
|
text="chuyên khoa"
|
||||||
|
title="Chọn chuyên khoa"
|
||||||
|
placeholder="Chọn nhiều chuyên khoa..."
|
||||||
|
options={specialties}
|
||||||
|
selectedItems={form.specialtyIds}
|
||||||
|
setSelectedItems={(value) =>
|
||||||
|
setForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
specialtyIds: value as string[],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
showSelectedItems="default"
|
||||||
|
getOptionLabel={(item) => item.name}
|
||||||
|
getOptionValue={(item) => item.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* FOOTER */}
|
{/* FOOTER */}
|
||||||
@ -207,12 +286,10 @@ const CreateConsultation = () => {
|
|||||||
<CustomButton
|
<CustomButton
|
||||||
disabled={
|
disabled={
|
||||||
!form.patientId ||
|
!form.patientId ||
|
||||||
// !form.chiefComplaint ||
|
!form.chiefComplaint ||
|
||||||
// !form.diagnosis ||
|
!form.symptomsText ||
|
||||||
// !form.doctorNotes ||
|
!form.vitalSigns ||
|
||||||
// !form.symptomsText ||
|
!form.specialtyIds ||
|
||||||
// !form.treatmentPlan ||
|
|
||||||
// !form.vitalSigns ||
|
|
||||||
createConsultationMutation.isPending
|
createConsultationMutation.isPending
|
||||||
}
|
}
|
||||||
variant="midnightBlue"
|
variant="midnightBlue"
|
||||||
|
|||||||
@ -134,10 +134,10 @@ const DetailConsultation = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* DẤU HIỆU SINH TỒN */}
|
{/* CHỈ SỐ HUYẾT ÁP, CHỈ SỐ MẠCH */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-[14px] font-medium text-[#697586]">
|
<p className="text-[14px] font-medium text-[#697586]">
|
||||||
DẤU HIỆU SINH TỒN
|
CHỈ SỐ HUYẾT ÁP, CHỈ SỐ MẠCH
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-[15px] font-medium text-[#202939]">
|
<p className="text-[15px] font-medium text-[#202939]">
|
||||||
@ -189,6 +189,32 @@ const DetailConsultation = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[14px] font-medium text-[#697586]">
|
||||||
|
CÁC CHUYÊN KHOA ĐANG CHỜ KHÁM
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-[15px] font-medium text-[#202939]">
|
||||||
|
{consultation.specialtyClinics
|
||||||
|
?.filter((specialty) => specialty.specialtyStatus === 1)
|
||||||
|
.map((specialty) => specialty.specialtyName)
|
||||||
|
.join(", ") || "---"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[14px] font-medium text-[#697586]">
|
||||||
|
CÁC CHUYÊN KHOA ĐÃ KHÁM
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-[15px] font-medium text-[#202939]">
|
||||||
|
{consultation.specialtyClinics
|
||||||
|
?.filter((specialty) => specialty.specialtyStatus === 3)
|
||||||
|
.map((specialty) => specialty.specialtyName)
|
||||||
|
.join(", ") || "---"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* TRẠNG THÁI */}
|
{/* TRẠNG THÁI */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-[14px] font-medium text-[#697586]">
|
<p className="text-[14px] font-medium text-[#697586]">
|
||||||
@ -200,29 +226,44 @@ const DetailConsultation = () => {
|
|||||||
listState={[
|
listState={[
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Draft,
|
state: TYPE_STATUS.Draft,
|
||||||
text: "Đang chờ",
|
text: "Đang chờ khám",
|
||||||
textColor: "#000",
|
textColor: "#92400E",
|
||||||
backgroundColor: "#FFE4C4",
|
backgroundColor: "#FEF3C7",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.InProgress,
|
state: TYPE_STATUS.InProgress,
|
||||||
text: "Đang thực hiện",
|
text: "Đang khám tổng quát",
|
||||||
textColor: "#000",
|
textColor: "#1E3A8A",
|
||||||
backgroundColor: "#ff3300",
|
backgroundColor: "#DBEAFE",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
state: TYPE_STATUS.ToSpecialty,
|
||||||
|
text: "Chờ khám chuyên khoa",
|
||||||
|
textColor: "#6B21A8",
|
||||||
|
backgroundColor: "#E9D5FF",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
state: TYPE_STATUS.PendingPrescription,
|
||||||
|
text: "Chờ nhận thuốc/kính",
|
||||||
|
textColor: "#9A3412",
|
||||||
|
backgroundColor: "#FED7AA",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Completed,
|
state: TYPE_STATUS.Completed,
|
||||||
text: "Hoàn thành",
|
text: "Hoàn thành",
|
||||||
textColor: "#000",
|
textColor: "#166534",
|
||||||
backgroundColor: "#3d69eb",
|
backgroundColor: "#DCFCE7",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Cancelled,
|
state: TYPE_STATUS.Cancelled,
|
||||||
text: "Đã hủy",
|
text: "Đã hủy",
|
||||||
textColor: "#000",
|
textColor: "#991B1B",
|
||||||
backgroundColor: "#cccc",
|
backgroundColor: "#FEE2E2",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import CustomButton from "@/components/customs/custom-button";
|
import CustomButton from "@/components/customs/custom-button";
|
||||||
import CustomDialog from "@/components/customs/custom-dialog";
|
import CustomDialog from "@/components/customs/custom-dialog";
|
||||||
import CustomPopup from "@/components/customs/custom-popup";
|
|
||||||
import DataWrapper from "@/components/customs/DataWrapper";
|
import DataWrapper from "@/components/customs/DataWrapper";
|
||||||
import Search from "@/components/customs/custom-search";
|
import Search from "@/components/customs/custom-search";
|
||||||
import StateActive from "@/components/customs/StateActive";
|
import StateActive from "@/components/customs/StateActive";
|
||||||
@ -27,8 +26,11 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|||||||
|
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
|
|
||||||
import { PATH } from "@/constant/config";
|
|
||||||
import Pagination from "@/components/customs/custom-pagination";
|
import Pagination from "@/components/customs/custom-pagination";
|
||||||
|
import moment from "moment";
|
||||||
|
import specialtyServices, { SpecialtyItem } from "@/services/specialtyServices";
|
||||||
|
|
||||||
|
import FilterCustom from "@/components/customs/FilterCustom";
|
||||||
|
|
||||||
const MainPageConsultation = () => {
|
const MainPageConsultation = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -49,6 +51,10 @@ const MainPageConsultation = () => {
|
|||||||
|
|
||||||
const _keyword = searchParams.get("_keyword");
|
const _keyword = searchParams.get("_keyword");
|
||||||
|
|
||||||
|
const _status = searchParams.get("_status");
|
||||||
|
|
||||||
|
const _specialtyId = searchParams.get("_specialtyId");
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
STATE
|
STATE
|
||||||
========================= */
|
========================= */
|
||||||
@ -78,7 +84,7 @@ const MainPageConsultation = () => {
|
|||||||
|
|
||||||
Search: _keyword || "",
|
Search: _keyword || "",
|
||||||
|
|
||||||
SortBy: "id",
|
SortBy: _specialtyId || undefined,
|
||||||
|
|
||||||
Desc: true,
|
Desc: true,
|
||||||
}),
|
}),
|
||||||
@ -100,9 +106,35 @@ const MainPageConsultation = () => {
|
|||||||
DATA
|
DATA
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
|
const specialtyQuery = useQuery({
|
||||||
|
queryKey: [QUERY_KEY.list_specialty_lookup],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await httpRequest<SpecialtyItem[]>({
|
||||||
|
http: specialtyServices.getSpecialty(),
|
||||||
|
showMessageFailed: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res || [];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const consultationData = useMemo(() => {
|
const consultationData = useMemo(() => {
|
||||||
return consultationQuery.data?.items || [];
|
let data = consultationQuery.data?.items || [];
|
||||||
}, [consultationQuery.data]);
|
|
||||||
|
if (_status) {
|
||||||
|
data = data.filter((item) => item.status === Number(_status));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_specialtyId) {
|
||||||
|
data = data.filter((item) =>
|
||||||
|
item.specialtyClinics?.some(
|
||||||
|
(specialty) => specialty.specialtyId === _specialtyId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, [consultationQuery.data, _status, _specialtyId]);
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
UPDATE QUERY
|
UPDATE QUERY
|
||||||
@ -114,13 +146,18 @@ const MainPageConsultation = () => {
|
|||||||
const updateQuery = (key: string, value: string | number) => {
|
const updateQuery = (key: string, value: string | number) => {
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
|
||||||
if (!value || value === "" || (key === "_page" && value === 1)) {
|
if (!value || value === "") {
|
||||||
params.delete(key);
|
params.delete(key);
|
||||||
} else {
|
} else {
|
||||||
params.set(key, String(value));
|
params.set(key, String(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === "_pageSize") {
|
if (
|
||||||
|
key === "_pageSize" ||
|
||||||
|
key === "_keyword" ||
|
||||||
|
key === "_status" ||
|
||||||
|
key === "_specialtyId"
|
||||||
|
) {
|
||||||
params.delete("_page");
|
params.delete("_page");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,6 +296,51 @@ const MainPageConsultation = () => {
|
|||||||
placeholder="Tìm kiếm phiên khám..."
|
placeholder="Tìm kiếm phiên khám..."
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FilterCustom<string | null>
|
||||||
|
name="Chuyên khoa"
|
||||||
|
value={_specialtyId}
|
||||||
|
onChange={(value) => updateQuery("_specialtyId", value ?? "")}
|
||||||
|
listOption={useMemo(() => {
|
||||||
|
return (
|
||||||
|
specialtyQuery.data?.map((item) => ({
|
||||||
|
uuid: item.id,
|
||||||
|
name: item.name,
|
||||||
|
})) || []
|
||||||
|
);
|
||||||
|
}, [specialtyQuery.data])}
|
||||||
|
/>
|
||||||
|
<FilterCustom<number | null>
|
||||||
|
name="Trạng thái"
|
||||||
|
value={_status ? Number(_status) : null}
|
||||||
|
onChange={(value) => updateQuery("_status", value ?? "")}
|
||||||
|
listOption={[
|
||||||
|
{
|
||||||
|
uuid: TYPE_STATUS.Draft,
|
||||||
|
name: "Đang chờ khám",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: TYPE_STATUS.InProgress,
|
||||||
|
name: "Đang khám tổng quát",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: TYPE_STATUS.ToSpecialty,
|
||||||
|
name: "Chờ khám chuyên khoa",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: TYPE_STATUS.PendingPrescription,
|
||||||
|
name: "Chờ nhận thuốc/kính",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: TYPE_STATUS.Completed,
|
||||||
|
name: "Hoàn thành",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
uuid: TYPE_STATUS.Cancelled,
|
||||||
|
name: "Đã hủy",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* TABLE */}
|
{/* TABLE */}
|
||||||
@ -295,8 +377,13 @@ const MainPageConsultation = () => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
title: "NGÀY KHÁM",
|
title: "NGÀY KHÁM",
|
||||||
|
render: (item: ConsultationItem) => (
|
||||||
render: (item: ConsultationItem) => <span>{item.visitDate}</span>,
|
<span>
|
||||||
|
{item.visitDate
|
||||||
|
? moment(item.visitDate).format("DD/MM/YYYY")
|
||||||
|
: "---"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -330,7 +417,30 @@ const MainPageConsultation = () => {
|
|||||||
<span>{item.doctorNotes || "---"}</span>
|
<span>{item.doctorNotes || "---"}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "CÁC CHUYÊN KHOA ĐANG CHỜ KHÁM",
|
||||||
|
|
||||||
|
render: (item: ConsultationItem) => {
|
||||||
|
const waitingSpecialties = item.specialtyClinics
|
||||||
|
?.filter((specialty) => specialty.specialtyStatus === 1)
|
||||||
|
.map((specialty) => specialty.specialtyName)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
return <span>{waitingSpecialties || "---"}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "CÁC CHUYÊN KHOA ĐÃ KHÁM",
|
||||||
|
|
||||||
|
render: (item: ConsultationItem) => {
|
||||||
|
const completedSpecialties = item.specialtyClinics
|
||||||
|
?.filter((specialty) => specialty.specialtyStatus === 3)
|
||||||
|
.map((specialty) => specialty.specialtyName)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
return <span>{completedSpecialties || "---"}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "TRẠNG THÁI",
|
title: "TRẠNG THÁI",
|
||||||
|
|
||||||
@ -340,29 +450,44 @@ const MainPageConsultation = () => {
|
|||||||
listState={[
|
listState={[
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Draft,
|
state: TYPE_STATUS.Draft,
|
||||||
text: "Đang chờ",
|
text: "Đang chờ khám",
|
||||||
textColor: "#000",
|
textColor: "#92400E",
|
||||||
backgroundColor: "#FFE4C4",
|
backgroundColor: "#FEF3C7",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.InProgress,
|
state: TYPE_STATUS.InProgress,
|
||||||
text: "Đang thực hiện",
|
text: "Đang khám tổng quát",
|
||||||
textColor: "#000",
|
textColor: "#1E3A8A",
|
||||||
backgroundColor: "#ff3300",
|
backgroundColor: "#DBEAFE",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
state: TYPE_STATUS.ToSpecialty,
|
||||||
|
text: "Chờ khám chuyên khoa",
|
||||||
|
textColor: "#6B21A8",
|
||||||
|
backgroundColor: "#E9D5FF",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
state: TYPE_STATUS.PendingPrescription,
|
||||||
|
text: "Chờ nhận thuốc/kính",
|
||||||
|
textColor: "#9A3412",
|
||||||
|
backgroundColor: "#FED7AA",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Completed,
|
state: TYPE_STATUS.Completed,
|
||||||
text: "Hoàn thành",
|
text: "Hoàn thành",
|
||||||
textColor: "#000",
|
textColor: "#166534",
|
||||||
backgroundColor: "#3d69eb",
|
backgroundColor: "#DCFCE7",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Cancelled,
|
state: TYPE_STATUS.Cancelled,
|
||||||
text: "Đã hủy",
|
text: "Đã hủy",
|
||||||
textColor: "#000",
|
textColor: "#991B1B",
|
||||||
backgroundColor: "#cccc",
|
backgroundColor: "#FEE2E2",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@ -381,15 +506,32 @@ const MainPageConsultation = () => {
|
|||||||
|
|
||||||
const isDisabled = isCancelled || isCompleted;
|
const isDisabled = isCancelled || isCompleted;
|
||||||
|
|
||||||
|
// Tìm chuyên khoa có specialtyStatus = 1
|
||||||
|
const specialtyClinicActive = item.specialtyClinics?.find(
|
||||||
|
(specialty) => specialty.specialtyStatus === 1,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* UPDATE */}
|
{/* UPDATE */}
|
||||||
<CustomButton
|
<CustomButton
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
icon={<Pencil className="text-blue-400" />}
|
icon={
|
||||||
href={`/consultation/update?_id=${item?.id}`}
|
specialtyClinicActive ? undefined : (
|
||||||
/>
|
<Pencil className="text-blue-400" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
href={
|
||||||
|
specialtyClinicActive
|
||||||
|
? `/consultation/update-specialty?_id=${item.id}&specialtyClinicId=${specialtyClinicActive.id}`
|
||||||
|
: `/consultation/update?_id=${item.id}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{specialtyClinicActive
|
||||||
|
? `Cập nhật ${specialtyClinicActive.specialtyName}`
|
||||||
|
: undefined}
|
||||||
|
</CustomButton>
|
||||||
|
|
||||||
{/* CANCEL */}
|
{/* CANCEL */}
|
||||||
<CustomButton
|
<CustomButton
|
||||||
@ -423,7 +565,7 @@ const MainPageConsultation = () => {
|
|||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
onSetPage={(value) => updateQuery("_page", value)}
|
onSetPage={(value) => updateQuery("_page", value)}
|
||||||
onSetPageSize={(value) => updateQuery("_pageSize", value)}
|
onSetPageSize={(value) => updateQuery("_pageSize", value)}
|
||||||
dependencies={[_pageSize, _keyword]}
|
dependencies={[_pageSize, _keyword, _specialtyId, _status]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* CANCEL DIALOG */}
|
{/* CANCEL DIALOG */}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import Table from "@/components/customs/custom-table";
|
|||||||
import StateActive from "@/components/customs/StateActive";
|
import StateActive from "@/components/customs/StateActive";
|
||||||
import Pagination from "@/components/customs/custom-pagination";
|
import Pagination from "@/components/customs/custom-pagination";
|
||||||
import Search from "@/components/customs/custom-search";
|
import Search from "@/components/customs/custom-search";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
const TableHistoryPatient = () => {
|
const TableHistoryPatient = () => {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
@ -129,7 +130,13 @@ const TableHistoryPatient = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "NGÀY KHÁM",
|
title: "NGÀY KHÁM",
|
||||||
render: (item: ConsultationItem) => <span>{item.visitDate}</span>,
|
render: (item: ConsultationItem) => (
|
||||||
|
<span>
|
||||||
|
{item.visitDate
|
||||||
|
? moment(item.visitDate).format("DD/MM/YYYY")
|
||||||
|
: "---"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "DẤU HIỆU",
|
title: "DẤU HIỆU",
|
||||||
@ -151,27 +158,44 @@ const TableHistoryPatient = () => {
|
|||||||
listState={[
|
listState={[
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Draft,
|
state: TYPE_STATUS.Draft,
|
||||||
text: "Đang chờ",
|
text: "Đang chờ khám",
|
||||||
textColor: "#000",
|
textColor: "#92400E",
|
||||||
backgroundColor: "#FFE4C4",
|
backgroundColor: "#FEF3C7",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.InProgress,
|
state: TYPE_STATUS.InProgress,
|
||||||
text: "Đang thực hiện",
|
text: "Đang khám tổng quát",
|
||||||
textColor: "#000",
|
textColor: "#1E3A8A",
|
||||||
backgroundColor: "#ff3300",
|
backgroundColor: "#DBEAFE",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
state: TYPE_STATUS.ToSpecialty,
|
||||||
|
text: "Chờ khám chuyên khoa",
|
||||||
|
textColor: "#6B21A8",
|
||||||
|
backgroundColor: "#E9D5FF",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
state: TYPE_STATUS.PendingPrescription,
|
||||||
|
text: "Chờ nhận thuốc/kính",
|
||||||
|
textColor: "#9A3412",
|
||||||
|
backgroundColor: "#FED7AA",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Completed,
|
state: TYPE_STATUS.Completed,
|
||||||
text: "Hoàn thành",
|
text: "Hoàn thành",
|
||||||
textColor: "#000",
|
textColor: "#166534",
|
||||||
backgroundColor: "#3d69eb",
|
backgroundColor: "#DCFCE7",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Cancelled,
|
state: TYPE_STATUS.Cancelled,
|
||||||
text: "Đã hủy",
|
text: "Đã hủy",
|
||||||
textColor: "#000",
|
textColor: "#991B1B",
|
||||||
backgroundColor: "#cccccc",
|
backgroundColor: "#FEE2E2",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -27,6 +27,7 @@ interface IUpdateConsul {
|
|||||||
diagnosis: string;
|
diagnosis: string;
|
||||||
treatmentPlan: string;
|
treatmentPlan: string;
|
||||||
doctorNotes: string;
|
doctorNotes: string;
|
||||||
|
isGlassesDelivered: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@ -39,6 +40,7 @@ const defaultForm: IUpdateConsul = {
|
|||||||
diagnosis: "",
|
diagnosis: "",
|
||||||
treatmentPlan: "",
|
treatmentPlan: "",
|
||||||
doctorNotes: "",
|
doctorNotes: "",
|
||||||
|
isGlassesDelivered: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@ -103,6 +105,7 @@ const UpdateConsultation = () => {
|
|||||||
diagnosis: consul.diagnosis || "",
|
diagnosis: consul.diagnosis || "",
|
||||||
treatmentPlan: consul.treatmentPlan || "",
|
treatmentPlan: consul.treatmentPlan || "",
|
||||||
doctorNotes: consul.doctorNotes || "",
|
doctorNotes: consul.doctorNotes || "",
|
||||||
|
isGlassesDelivered: consul.isGlassesDelivered,
|
||||||
});
|
});
|
||||||
}, [consulDetailQuery.data]);
|
}, [consulDetailQuery.data]);
|
||||||
|
|
||||||
@ -126,6 +129,7 @@ const UpdateConsultation = () => {
|
|||||||
diagnosis: form.diagnosis,
|
diagnosis: form.diagnosis,
|
||||||
treatmentPlan: form.treatmentPlan,
|
treatmentPlan: form.treatmentPlan,
|
||||||
doctorNotes: form.doctorNotes,
|
doctorNotes: form.doctorNotes,
|
||||||
|
isGlassesDelivered: form?.isGlassesDelivered,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -0,0 +1,341 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import CustomButton from "@/components/customs/custom-button";
|
||||||
|
import GridColumn from "@/components/layouts/GridColumn";
|
||||||
|
import FormCustom from "@/components/utils/FormCustom";
|
||||||
|
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||||
|
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||||
|
import { PATH } from "@/constant/config";
|
||||||
|
import { QUERY_KEY } from "@/constant/config/enum";
|
||||||
|
import { httpRequest } from "@/services";
|
||||||
|
import consultationServices, {
|
||||||
|
ConsultationDetail,
|
||||||
|
} from "@/services/consultationServices";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const UpdateSpecialtyClinicId = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const consultationId = searchParams.get("_id") || "";
|
||||||
|
const specialtyClinicId = searchParams.get("specialtyClinicId") || "";
|
||||||
|
|
||||||
|
const [form, setForm] = useState<{
|
||||||
|
specialtyAssignmentId: string;
|
||||||
|
specialtyNotes: string;
|
||||||
|
diagnosis: string;
|
||||||
|
procedureNotes: string;
|
||||||
|
glassRequired: boolean;
|
||||||
|
glassType: string;
|
||||||
|
}>({
|
||||||
|
specialtyAssignmentId: "",
|
||||||
|
specialtyNotes: "",
|
||||||
|
diagnosis: "",
|
||||||
|
procedureNotes: "",
|
||||||
|
glassRequired: false,
|
||||||
|
glassType: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
GET DETAIL API
|
||||||
|
========================= */
|
||||||
|
const consulDetailQuery = useQuery<ConsultationDetail>({
|
||||||
|
queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId],
|
||||||
|
enabled: !!consultationId,
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await httpRequest<ConsultationDetail>({
|
||||||
|
showMessageFailed: true,
|
||||||
|
http: consultationServices.detailConsultations(consultationId),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
res || {
|
||||||
|
id: "",
|
||||||
|
patientId: "",
|
||||||
|
sessionCode: "",
|
||||||
|
visitDate: "",
|
||||||
|
status: 1,
|
||||||
|
chiefComplaint: null,
|
||||||
|
symptomsText: null,
|
||||||
|
vitalSigns: null,
|
||||||
|
diagnosis: null,
|
||||||
|
treatmentPlan: null,
|
||||||
|
doctorNotes: null,
|
||||||
|
createdBy: "",
|
||||||
|
isDeleted: false,
|
||||||
|
specialtyClinics: [
|
||||||
|
{
|
||||||
|
id: "",
|
||||||
|
specialtyId: "",
|
||||||
|
specialtyName: "",
|
||||||
|
specialtyNotes: null,
|
||||||
|
diagnosis: null,
|
||||||
|
procedureNotes: null,
|
||||||
|
glassRequired: null,
|
||||||
|
glassType: null,
|
||||||
|
examinerName: "",
|
||||||
|
specialtyStatus: 1,
|
||||||
|
sharedItems: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
aggregatedPrescriptionItems: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tìm chuyên khoa hiện tại từ data trả về
|
||||||
|
const selectedSpecialtyClinic =
|
||||||
|
consulDetailQuery.data?.specialtyClinics?.find(
|
||||||
|
(item) => item.id === specialtyClinicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ĐIỀU KIỆN KIỂM TRA KHOA MẮT
|
||||||
|
* Bạn hãy thay đổi điều kiện này tùy thuộc vào database của bạn.
|
||||||
|
* Ví dụ: selectedSpecialtyClinic?.specialtyId === "MẮT" hoặc kiểm tra theo tên.
|
||||||
|
*/
|
||||||
|
const isEyeClinic =
|
||||||
|
selectedSpecialtyClinic?.specialtyName?.toLowerCase().includes("mắt") ||
|
||||||
|
selectedSpecialtyClinic?.specialtyId?.toLowerCase().includes("mat");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const consul = consulDetailQuery.data;
|
||||||
|
if (!consul) return;
|
||||||
|
|
||||||
|
const specialtyClinic = consul.specialtyClinics?.find(
|
||||||
|
(item) => item.id === specialtyClinicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!specialtyClinic) return;
|
||||||
|
|
||||||
|
setForm({
|
||||||
|
specialtyAssignmentId: specialtyClinic.id || "",
|
||||||
|
specialtyNotes: specialtyClinic.specialtyNotes || "",
|
||||||
|
diagnosis: specialtyClinic.diagnosis || "",
|
||||||
|
procedureNotes: specialtyClinic.procedureNotes || "",
|
||||||
|
glassRequired: specialtyClinic.glassRequired || false,
|
||||||
|
glassType: specialtyClinic.glassType || "",
|
||||||
|
});
|
||||||
|
}, [consulDetailQuery.data, specialtyClinicId]);
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
UPDATE MUTATION
|
||||||
|
========================= */
|
||||||
|
const specialtyResultsMutation = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
if (!consultationId) {
|
||||||
|
throw new Error("Không tìm thấy ID phiên khám!");
|
||||||
|
}
|
||||||
|
|
||||||
|
return httpRequest({
|
||||||
|
showMessageFailed: true,
|
||||||
|
showMessageSuccess: true,
|
||||||
|
msgSuccess: "Cập nhật phiên khám thành công!",
|
||||||
|
http: consultationServices.specialtyResults(
|
||||||
|
form.specialtyAssignmentId,
|
||||||
|
{
|
||||||
|
specialtyNotes: form.specialtyNotes,
|
||||||
|
diagnosis: form.diagnosis,
|
||||||
|
procedureNotes: form.procedureNotes,
|
||||||
|
// Nếu không phải khoa mắt, gán mặc định false và chuỗi rỗng khi gửi API
|
||||||
|
glassRequired: isEyeClinic ? form.glassRequired : false,
|
||||||
|
glassType: isEyeClinic ? form.glassType : "",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
if (!data) return;
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [QUERY_KEY.table_list_consultation],
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId],
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push(PATH.CONSULTATION || "/consultation");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
specialtyResultsMutation.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
consulDetailQuery.isLoading || specialtyResultsMutation.isPending;
|
||||||
|
|
||||||
|
if (consulDetailQuery.isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center p-10 font-medium text-gray-500">
|
||||||
|
Đang tải dữ liệu phiên khám...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col gap-6 rounded-2xl bg-white p-6 shadow-xl">
|
||||||
|
{/* HEADER */}
|
||||||
|
<div className="flex items-center justify-between border-b pb-5">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-[24px] font-bold text-[#111827]">
|
||||||
|
Phiên khám chuyên khoa
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-[#697586]">
|
||||||
|
Cập nhật kết quả chi tiết quá trình khám bệnh của bệnh nhân
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CHUYÊN KHOA INFO */}
|
||||||
|
<InputForm
|
||||||
|
label="Chuyên khoa đảm nhận"
|
||||||
|
name="specialtyAssignmentId"
|
||||||
|
type="text"
|
||||||
|
value={
|
||||||
|
selectedSpecialtyClinic
|
||||||
|
? `${selectedSpecialtyClinic.specialtyName}`
|
||||||
|
: "Chưa có thông tin chuyên khoa"
|
||||||
|
}
|
||||||
|
placeholder="Thông tin chuyên khoa"
|
||||||
|
isRequired
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* KHỐI THÔNG TIN KHÁM CHUNG (Luôn cố định 3 cột đẹp mắt) */}
|
||||||
|
<div className="mt-2">
|
||||||
|
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400">
|
||||||
|
Thông tin khám chung
|
||||||
|
</h3>
|
||||||
|
<GridColumn col={3}>
|
||||||
|
<InputForm
|
||||||
|
label="Ghi chú chuyên khoa"
|
||||||
|
name="specialtyNotes"
|
||||||
|
type="text"
|
||||||
|
value={form.specialtyNotes}
|
||||||
|
placeholder="Nhập ghi chú chuyên khoa"
|
||||||
|
isRequired
|
||||||
|
onChangeValue={(v) =>
|
||||||
|
setForm((prev) => ({ ...prev, specialtyNotes: String(v) }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputForm
|
||||||
|
type="text"
|
||||||
|
label="Chuẩn đoán chuyên khoa"
|
||||||
|
name="diagnosis"
|
||||||
|
placeholder="Nhập Chuẩn đoán"
|
||||||
|
isRequired
|
||||||
|
value={form.diagnosis}
|
||||||
|
onChangeValue={(v) =>
|
||||||
|
setForm((prev) => ({ ...prev, diagnosis: String(v) }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InputForm
|
||||||
|
type="text"
|
||||||
|
label="Ghi chú thủ thuật / can thiệp"
|
||||||
|
name="procedureNotes"
|
||||||
|
placeholder="Nhập ghi chú thủ thuật / can thiệp"
|
||||||
|
value={form.procedureNotes}
|
||||||
|
isRequired
|
||||||
|
onChangeValue={(v) =>
|
||||||
|
setForm((prev) => ({ ...prev, procedureNotes: String(v) }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</GridColumn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* KHỐI THÔNG TIN KHÁM MẮT (Chỉ hiển thị khi là khoa mắt) */}
|
||||||
|
{isEyeClinic && (
|
||||||
|
<div className="mt-2 rounded-xl border border-blue-100 bg-[#f8fafc] p-5 transition-all">
|
||||||
|
<h3 className="mb-4 text-sm font-bold uppercase tracking-wider text-blue-600">
|
||||||
|
Chỉ định thị lực (Khoa Mắt)
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
{/* Radio Chọn Có/Không */}
|
||||||
|
<div>
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
Bệnh nhân có chỉ định đeo kính không?
|
||||||
|
</label>
|
||||||
|
<div className="mt-2.5 flex items-center gap-8">
|
||||||
|
<label className="flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-900">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="glassRequired"
|
||||||
|
className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
checked={form.glassRequired === true}
|
||||||
|
onChange={() =>
|
||||||
|
setForm((prev) => ({ ...prev, glassRequired: true }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Có chỉ định
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className="flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-900">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="glassRequired"
|
||||||
|
className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
checked={form.glassRequired === false}
|
||||||
|
onChange={() =>
|
||||||
|
setForm((prev) => ({ ...prev, glassRequired: false }))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
Không chỉ định
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TextArea Loại Kính (Trải rộng ra 100% dòng tạo cảm giác thoáng đãng) */}
|
||||||
|
{form.glassRequired && (
|
||||||
|
<div className="mt-2 animate-fadeIn ">
|
||||||
|
<TextArea
|
||||||
|
name="glassType"
|
||||||
|
placeholder="Nhập thông số chi tiết loại kính được chỉ định (Cận, viễn, loạn, độ kính...)"
|
||||||
|
label="Thông số / Loại kính chỉ định"
|
||||||
|
value={form.glassType}
|
||||||
|
isRequired
|
||||||
|
isBlur
|
||||||
|
max={5000}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* BUTTON ACTION */}
|
||||||
|
<div className="mt-4 flex items-center justify-end gap-3 border-t pt-5">
|
||||||
|
<CustomButton
|
||||||
|
type="button"
|
||||||
|
variant="grey"
|
||||||
|
rounded="md"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
className="min-w-[120px]"
|
||||||
|
>
|
||||||
|
Hủy bỏ
|
||||||
|
</CustomButton>
|
||||||
|
<CustomButton
|
||||||
|
type="submit"
|
||||||
|
variant="midnightBlue"
|
||||||
|
rounded="md"
|
||||||
|
disabled={isLoading || !consultationId}
|
||||||
|
className="min-w-[140px]"
|
||||||
|
>
|
||||||
|
{specialtyResultsMutation.isPending
|
||||||
|
? "Đang cập nhật..."
|
||||||
|
: "Cập nhật kết quả"}
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormCustom>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateSpecialtyClinicId;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./UpdateSpecialtyClinicId";
|
||||||
@ -20,18 +20,16 @@ import CustomButton from "@/components/customs/custom-button";
|
|||||||
import StateActive from "@/components/customs/StateActive";
|
import StateActive from "@/components/customs/StateActive";
|
||||||
import DataWrapper from "@/components/customs/DataWrapper";
|
import DataWrapper from "@/components/customs/DataWrapper";
|
||||||
import Table from "@/components/customs/custom-table";
|
import Table from "@/components/customs/custom-table";
|
||||||
import CustomDialog from "@/components/customs/custom-dialog";
|
import moment from "moment";
|
||||||
|
// import CustomDialog from "@/components/customs/custom-dialog";
|
||||||
|
|
||||||
const DetailPatient = () => {
|
const DetailPatient = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
// const queryClient = useQueryClient();
|
||||||
|
|
||||||
const patientId = params?.id as string;
|
const patientId = params?.id as string;
|
||||||
|
|
||||||
// State quản lý đóng/mở Dialog xác nhận tạo phiên khám
|
|
||||||
const [openCreateDialog, setOpenCreateDialog] = useState<boolean>(false);
|
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
GET DETAIL PATIENT
|
GET DETAIL PATIENT
|
||||||
========================= */
|
========================= */
|
||||||
@ -95,54 +93,9 @@ const DetailPatient = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/* =========================
|
|
||||||
CREATE MUTATION
|
|
||||||
========================= */
|
|
||||||
const createConsultationMutation = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
return httpRequest({
|
|
||||||
showMessageFailed: true,
|
|
||||||
showMessageSuccess: true,
|
|
||||||
msgSuccess: "Tạo phiên khám mới thành công!",
|
|
||||||
// Gọi API với payload chứa patientId của bệnh nhân hiện tại
|
|
||||||
http: consultationServices.createConsultations({
|
|
||||||
patientId: patientId,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess: (res: any) => {
|
|
||||||
// Đóng dialog
|
|
||||||
setOpenCreateDialog(false);
|
|
||||||
|
|
||||||
// Làm mới danh sách lịch sử phiên khám của bệnh nhân này
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: [QUERY_KEY.table_list_consultation, patientId],
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res?.id) {
|
|
||||||
router.push(`${PATH.CONSULTATION}/${res.id}`); // detail
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const patient = patientDetailQuery.data;
|
const patient = patientDetailQuery.data;
|
||||||
const consultationData = consultationQuery.data?.items || [];
|
const consultationData = consultationQuery.data?.items || [];
|
||||||
|
|
||||||
/* =========================
|
|
||||||
HANDLERS DIALOG
|
|
||||||
========================= */
|
|
||||||
const handleOpenCreate = () => {
|
|
||||||
setOpenCreateDialog(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCloseDialog = () => {
|
|
||||||
setOpenCreateDialog(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateDialog = () => {
|
|
||||||
createConsultationMutation.mutate();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (patientDetailQuery.isLoading) {
|
if (patientDetailQuery.isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center p-10 font-medium text-gray-500">
|
<div className="flex items-center justify-center p-10 font-medium text-gray-500">
|
||||||
@ -169,8 +122,11 @@ const DetailPatient = () => {
|
|||||||
variant="midnightBlue"
|
variant="midnightBlue"
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
icon={<ClipboardPlus size={18} />}
|
icon={<ClipboardPlus size={18} />}
|
||||||
onClick={handleOpenCreate}
|
onClick={() =>
|
||||||
disabled={createConsultationMutation.isPending}
|
router.push(
|
||||||
|
`${PATH.CONSULTATION}/create?patientId=${patient?.id}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Tạo phiên khám
|
Tạo phiên khám
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
@ -209,7 +165,9 @@ const DetailPatient = () => {
|
|||||||
Ngày sinh
|
Ngày sinh
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[15px] font-medium text-[#202939]">
|
<p className="text-[15px] font-medium text-[#202939]">
|
||||||
{patient?.dateOfBirth}
|
{patient?.dateOfBirth
|
||||||
|
? moment(patient?.dateOfBirth).format("DD/MM/YYYY")
|
||||||
|
: "---"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -289,7 +247,7 @@ const DetailPatient = () => {
|
|||||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-xl font-semibold text-black">
|
<h2 className="text-xl font-semibold text-black">
|
||||||
Lịch sử phiên khám
|
Lịch sử phiên khám của {patient?.fullName}
|
||||||
</h2>
|
</h2>
|
||||||
<Link
|
<Link
|
||||||
href={`/consultation/history?_id=${patientId}`}
|
href={`/consultation/history?_id=${patientId}`}
|
||||||
@ -308,6 +266,14 @@ const DetailPatient = () => {
|
|||||||
rowKey={(item) => item.id}
|
rowKey={(item) => item.id}
|
||||||
data={consultationData}
|
data={consultationData}
|
||||||
column={[
|
column={[
|
||||||
|
{
|
||||||
|
title: "TÊN BỆNH NHÂN",
|
||||||
|
|
||||||
|
render: (item: ConsultationItem) => (
|
||||||
|
<span>{item.patientName}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "MÃ PHIÊN KHÁM",
|
title: "MÃ PHIÊN KHÁM",
|
||||||
fixedLeft: true,
|
fixedLeft: true,
|
||||||
@ -320,65 +286,120 @@ const DetailPatient = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "NGÀY KHÁM",
|
title: "NGÀY KHÁM",
|
||||||
render: (item: ConsultationItem) => (
|
render: (item: ConsultationItem) => (
|
||||||
<span>{item.visitDate}</span>
|
<span>
|
||||||
|
{item.visitDate
|
||||||
|
? moment(item.visitDate).format("DD/MM/YYYY")
|
||||||
|
: "---"}
|
||||||
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "DẤU HIỆU",
|
title: "DẤU HIỆU",
|
||||||
|
|
||||||
render: (item: ConsultationItem) => (
|
render: (item: ConsultationItem) => (
|
||||||
<span>{item.symptomsText || "---"}</span>
|
<span>{item.symptomsText || "---"}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "CHUẨN ĐOÁN",
|
title: "CHUẨN ĐOÁN",
|
||||||
|
|
||||||
render: (item: ConsultationItem) => (
|
render: (item: ConsultationItem) => (
|
||||||
<span>{item.diagnosis || "---"}</span>
|
<span>{item.diagnosis || "---"}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "KẾ HOẠCH ĐIỀU TRỊ",
|
title: "KẾ HOẠCH ĐIỀU TRỊ",
|
||||||
|
|
||||||
render: (item: ConsultationItem) => (
|
render: (item: ConsultationItem) => (
|
||||||
<span>{item.treatmentPlan || "---"}</span>
|
<span>{item.treatmentPlan || "---"}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "GHI CHÚ BÁC SĨ",
|
title: "GHI CHÚ BÁC SĨ",
|
||||||
|
|
||||||
render: (item: ConsultationItem) => (
|
render: (item: ConsultationItem) => (
|
||||||
<span>{item.doctorNotes || "---"}</span>
|
<span>{item.doctorNotes || "---"}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "CÁC CHUYÊN KHOA ĐANG CHỜ KHÁM",
|
||||||
|
|
||||||
|
render: (item: ConsultationItem) => {
|
||||||
|
const waitingSpecialties = item.specialtyClinics
|
||||||
|
?.filter((specialty) => specialty.specialtyStatus === 1)
|
||||||
|
.map((specialty) => specialty.specialtyName)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
return <span>{waitingSpecialties || "---"}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "CÁC CHUYÊN KHOA ĐÃ KHÁM",
|
||||||
|
|
||||||
|
render: (item: ConsultationItem) => {
|
||||||
|
const completedSpecialties = item.specialtyClinics
|
||||||
|
?.filter((specialty) => specialty.specialtyStatus === 3)
|
||||||
|
.map((specialty) => specialty.specialtyName)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
return <span>{completedSpecialties || "---"}</span>;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "TRẠNG THÁI",
|
title: "TRẠNG THÁI",
|
||||||
|
fixedRight: true,
|
||||||
render: (item: ConsultationItem) => (
|
render: (item: ConsultationItem) => (
|
||||||
<StateActive
|
<StateActive
|
||||||
stateActive={item.status}
|
stateActive={item.status}
|
||||||
listState={[
|
listState={[
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Draft,
|
state: TYPE_STATUS.Draft,
|
||||||
text: "Đang chờ",
|
text: "Đang chờ khám",
|
||||||
textColor: "#000",
|
textColor: "#92400E",
|
||||||
backgroundColor: "#FFE4C4",
|
backgroundColor: "#FEF3C7",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.InProgress,
|
state: TYPE_STATUS.InProgress,
|
||||||
text: "Đang thực hiện",
|
text: "Đang khám tổng quát",
|
||||||
textColor: "#000",
|
textColor: "#1E3A8A",
|
||||||
backgroundColor: "#ff3300",
|
backgroundColor: "#DBEAFE",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
state: TYPE_STATUS.ToSpecialty,
|
||||||
|
text: "Chờ khám chuyên khoa",
|
||||||
|
textColor: "#6B21A8",
|
||||||
|
backgroundColor: "#E9D5FF",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
state: TYPE_STATUS.PendingPrescription,
|
||||||
|
text: "Chờ nhận thuốc/kính",
|
||||||
|
textColor: "#9A3412",
|
||||||
|
backgroundColor: "#FED7AA",
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Completed,
|
state: TYPE_STATUS.Completed,
|
||||||
text: "Hoàn thành",
|
text: "Hoàn thành",
|
||||||
textColor: "#000",
|
textColor: "#166534",
|
||||||
backgroundColor: "#3d69eb",
|
backgroundColor: "#DCFCE7",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
state: TYPE_STATUS.Cancelled,
|
state: TYPE_STATUS.Cancelled,
|
||||||
text: "Đã hủy",
|
text: "Đã hủy",
|
||||||
textColor: "#000",
|
textColor: "#991B1B",
|
||||||
backgroundColor: "#cccc",
|
backgroundColor: "#FEE2E2",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@ -388,20 +409,6 @@ const DetailPatient = () => {
|
|||||||
/>
|
/>
|
||||||
</DataWrapper>
|
</DataWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* DIALOG XÁC NHẬN */}
|
|
||||||
<CustomDialog
|
|
||||||
open={openCreateDialog}
|
|
||||||
title="Tạo phiên khám"
|
|
||||||
note={`Bạn có chắc chắn muốn tạo phiên khám cho bệnh nhân ${patient?.fullName} không?`}
|
|
||||||
type="successPlay" // Giữ nguyên type của bạn, hoặc đổi thành "warning"/"info" tùy UI component quy định
|
|
||||||
onClose={handleCloseDialog}
|
|
||||||
onSubmit={handleCreateDialog}
|
|
||||||
titleCancel="Đóng"
|
|
||||||
titleSubmit={
|
|
||||||
createConsultationMutation.isPending ? "Đang xử lý..." : "Xác nhận"
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -221,7 +221,7 @@ const MainPagePatient = () => {
|
|||||||
href={`/patient/${item.id}`}
|
href={`/patient/${item.id}`}
|
||||||
className="text-blue-500 hover:underline"
|
className="text-blue-500 hover:underline"
|
||||||
>
|
>
|
||||||
{item.fullName}
|
{item.fullName || "---"}
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -230,26 +230,28 @@ const MainPagePatient = () => {
|
|||||||
title: "MÃ ĐỊNH DANH",
|
title: "MÃ ĐỊNH DANH",
|
||||||
|
|
||||||
render: (item: PatientItem) => (
|
render: (item: PatientItem) => (
|
||||||
<span>{item.identificationNumber}</span>
|
<span>{item.identificationNumber || "---"}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "SỐ ĐIỆN THOẠI",
|
title: "SỐ ĐIỆN THOẠI",
|
||||||
|
|
||||||
render: (item: PatientItem) => <span>{item.phone}</span>,
|
render: (item: PatientItem) => <span>{item.phone || "---"}</span>,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "EMAIL",
|
title: "EMAIL",
|
||||||
|
|
||||||
render: (item: PatientItem) => <span>{item.email}</span>,
|
render: (item: PatientItem) => <span>{item.email || "---"}</span>,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: "GIỚI TÍNH",
|
title: "GIỚI TÍNH",
|
||||||
|
|
||||||
render: (item: PatientItem) => <span>{item.gender}</span>,
|
render: (item: PatientItem) => (
|
||||||
|
<span>{item.gender || "---"}</span>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -286,7 +286,7 @@ const PopupUpdatePatient = ({ onClose, id }: PopupUpdatePatientProps) => {
|
|||||||
}
|
}
|
||||||
placeholder="Số điện thoại"
|
placeholder="Số điện thoại"
|
||||||
name="phone"
|
name="phone"
|
||||||
type="number"
|
type="phone"
|
||||||
isPhone
|
isPhone
|
||||||
value={form.phone}
|
value={form.phone}
|
||||||
isRequired
|
isRequired
|
||||||
@ -324,7 +324,7 @@ const PopupUpdatePatient = ({ onClose, id }: PopupUpdatePatientProps) => {
|
|||||||
label="SĐT khẩn cấp"
|
label="SĐT khẩn cấp"
|
||||||
placeholder="SĐT khẩn cấp"
|
placeholder="SĐT khẩn cấp"
|
||||||
name="emergencyContactPhone"
|
name="emergencyContactPhone"
|
||||||
type="number"
|
type="phone"
|
||||||
isRequired
|
isRequired
|
||||||
isPhone
|
isPhone
|
||||||
value={form.emergencyContactPhone}
|
value={form.emergencyContactPhone}
|
||||||
|
|||||||
@ -1,44 +1,26 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { Plus, Trash2 } from "lucide-react";
|
import { Plus, Trash2 } from "lucide-react";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import CustomButton from "@/components/customs/custom-button";
|
import CustomButton from "@/components/customs/custom-button";
|
||||||
|
|
||||||
import FormCustom from "@/components/utils/FormCustom";
|
import FormCustom from "@/components/utils/FormCustom";
|
||||||
|
|
||||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||||
|
|
||||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||||
|
|
||||||
import SelectForm from "@/components/utils/FormCustom/components/SelectForm";
|
import SelectForm from "@/components/utils/FormCustom/components/SelectForm";
|
||||||
|
|
||||||
import UploadImage from "@/components/utils/UploadImage";
|
import UploadImage from "@/components/utils/UploadImage";
|
||||||
|
|
||||||
import { httpRequest } from "@/services";
|
import { httpRequest } from "@/services";
|
||||||
|
|
||||||
import prescriptionServices from "@/services/prescriptionServices";
|
import prescriptionServices from "@/services/prescriptionServices";
|
||||||
|
|
||||||
import consultationServices, {
|
import consultationServices, {
|
||||||
ConsultationItem,
|
|
||||||
ConsultationResponse,
|
ConsultationResponse,
|
||||||
} from "@/services/consultationServices";
|
} from "@/services/consultationServices";
|
||||||
|
import { QUERY_KEY } from "@/constant/config/enum";
|
||||||
import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
|
|
||||||
|
|
||||||
import { toastWarn } from "@/common/funcs/toast";
|
import { toastWarn } from "@/common/funcs/toast";
|
||||||
|
|
||||||
import { PATH } from "@/constant/config";
|
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
TYPES
|
TYPES
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
interface PrescriptionItem {
|
interface PrescriptionItem {
|
||||||
medicineName: string;
|
medicineName: string;
|
||||||
dosage: string;
|
dosage: string;
|
||||||
@ -48,15 +30,15 @@ interface PrescriptionItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface PrescriptionForm {
|
interface PrescriptionForm {
|
||||||
examinationSessionId: string;
|
examinationSessionSpecialtyId: string;
|
||||||
notes: string;
|
notes: string;
|
||||||
|
prescriptionImg: string;
|
||||||
items: PrescriptionItem[];
|
items: PrescriptionItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
DEFAULT
|
DEFAULT
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
const defaultMedicine: PrescriptionItem = {
|
const defaultMedicine: PrescriptionItem = {
|
||||||
medicineName: "",
|
medicineName: "",
|
||||||
dosage: "",
|
dosage: "",
|
||||||
@ -68,29 +50,28 @@ const defaultMedicine: PrescriptionItem = {
|
|||||||
/* =========================
|
/* =========================
|
||||||
COMPONENT
|
COMPONENT
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
const CreatePrescription = () => {
|
const CreatePrescription = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const queryKeys = [QUERY_KEY.table_list_consultation];
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
const [form, setForm] = useState<PrescriptionForm>({
|
const [form, setForm] = useState<PrescriptionForm>({
|
||||||
examinationSessionId: "",
|
examinationSessionSpecialtyId: "",
|
||||||
notes: "",
|
notes: "",
|
||||||
|
prescriptionImg: "",
|
||||||
items: [defaultMedicine],
|
items: [defaultMedicine],
|
||||||
});
|
});
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
GET EXAMINATION SESSION
|
GET EXAMINATION SESSION
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
const examinationSessionQuery = useQuery<ConsultationResponse>({
|
const examinationSessionQuery = useQuery<ConsultationResponse>({
|
||||||
queryKey: [QUERY_KEY.table_list_consultation],
|
queryKey: [QUERY_KEY.table_list_consultation],
|
||||||
|
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await httpRequest<ConsultationResponse>({
|
const res = await httpRequest<ConsultationResponse>({
|
||||||
showMessageFailed: true,
|
showMessageFailed: true,
|
||||||
|
|
||||||
http: consultationServices.getConsultations({
|
http: consultationServices.getConsultations({
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PageSize: 100,
|
PageSize: 100,
|
||||||
@ -112,92 +93,82 @@ const CreatePrescription = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const examinationSessionOptions =
|
const examinationSessionOptions =
|
||||||
examinationSessionQuery.data?.items?.filter(
|
examinationSessionQuery.data?.items
|
||||||
(item) =>
|
?.filter((session) => {
|
||||||
item.status === TYPE_STATUS.Draft ||
|
const isWaitingPrescription = session.status === 4;
|
||||||
item.status === TYPE_STATUS.InProgress,
|
const allSpecialtyCompleted = session.specialtyClinics?.every(
|
||||||
) || [];
|
(specialty) => specialty.specialtyStatus === 3,
|
||||||
|
);
|
||||||
|
return isWaitingPrescription && allSpecialtyCompleted;
|
||||||
|
})
|
||||||
|
.flatMap((session) =>
|
||||||
|
session.specialtyClinics
|
||||||
|
.filter(
|
||||||
|
(specialty) =>
|
||||||
|
specialty.specialtyStatus === 3 &&
|
||||||
|
specialty.sharedItems.length === 0,
|
||||||
|
)
|
||||||
|
.map((specialty) => ({
|
||||||
|
id: specialty.id,
|
||||||
|
specialtyName: specialty.specialtyName,
|
||||||
|
sessionCode: session.sessionCode,
|
||||||
|
patientName: session.patientName,
|
||||||
|
})),
|
||||||
|
) || [];
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
CREATE MUTATION
|
CREATE MUTATION
|
||||||
========================= */
|
========================= */
|
||||||
|
const funcCreatePrescription = useMutation({
|
||||||
|
mutationFn: (payload: any) => {
|
||||||
|
console.log("=== GỬI PAYLOAD LÊN API TẠO ĐƠN THUỐC ===", payload);
|
||||||
|
|
||||||
const createPrescriptionMutation = useMutation({
|
return httpRequest({
|
||||||
mutationFn: async () => {
|
|
||||||
/**
|
|
||||||
* CREATE FORMDATA
|
|
||||||
*/
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BASIC INFO
|
|
||||||
*/
|
|
||||||
formData.append("ExaminationSessionId", form.examinationSessionId);
|
|
||||||
|
|
||||||
formData.append("Notes", form.notes);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* FILE
|
|
||||||
*/
|
|
||||||
if (file) {
|
|
||||||
formData.append("PrescriptionFile", file);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ITEMS
|
|
||||||
* BACKEND EXPECT:
|
|
||||||
* Items = JSON.stringify([...])
|
|
||||||
*/
|
|
||||||
const validItems = form.items.filter((x) => x.medicineName.trim() !== "");
|
|
||||||
|
|
||||||
formData.append("Items", JSON.stringify(validItems));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API
|
|
||||||
*/
|
|
||||||
return await httpRequest({
|
|
||||||
showMessageFailed: true,
|
showMessageFailed: true,
|
||||||
|
|
||||||
showMessageSuccess: true,
|
showMessageSuccess: true,
|
||||||
|
|
||||||
msgSuccess: "Tạo đơn thuốc thành công!",
|
msgSuccess: "Tạo đơn thuốc thành công!",
|
||||||
|
http: prescriptionServices.createPrescription(payload),
|
||||||
http: prescriptionServices.createPrescription(formData),
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onSuccess: () => {
|
onSuccess(data) {
|
||||||
router.push(PATH.PRESCRIPTION);
|
console.log("TẠO ĐƠN THUỐC THÀNH CÔNG:", data);
|
||||||
},
|
|
||||||
|
|
||||||
onError: (error: any) => {
|
queryKeys.forEach((key) => {
|
||||||
const message =
|
queryClient.invalidateQueries({
|
||||||
error?.response?.data?.message || error?.message || "Có lỗi xảy ra";
|
queryKey: [key],
|
||||||
|
});
|
||||||
toastWarn({
|
|
||||||
msg: message,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("CREATE PRESCRIPTION ERROR:", error);
|
setForm({
|
||||||
|
examinationSessionSpecialtyId: "",
|
||||||
|
notes: "",
|
||||||
|
prescriptionImg: "",
|
||||||
|
items: [defaultMedicine],
|
||||||
|
});
|
||||||
|
setFile(null);
|
||||||
|
|
||||||
|
// Điều hướng quay lại danh sách
|
||||||
|
router.back();
|
||||||
|
},
|
||||||
|
onError(error: any) {
|
||||||
|
console.error("LỖI HỆ THỐNG API TẠO ĐƠN THUỐC:", error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
CHANGE MEDICINE FIELD
|
CHANGE MEDICINE FIELD
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
const handleChangeMedicine = (
|
const handleChangeMedicine = (
|
||||||
index: number,
|
index: number,
|
||||||
key: keyof PrescriptionItem,
|
key: keyof PrescriptionItem,
|
||||||
value: string,
|
value: string,
|
||||||
) => {
|
) => {
|
||||||
const cloneItems = [...form.items];
|
const cloneItems = [...form.items];
|
||||||
|
|
||||||
cloneItems[index] = {
|
cloneItems[index] = {
|
||||||
...cloneItems[index],
|
...cloneItems[index],
|
||||||
[key]: value,
|
[key]: value,
|
||||||
};
|
};
|
||||||
|
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
items: cloneItems,
|
items: cloneItems,
|
||||||
@ -205,33 +176,20 @@ const CreatePrescription = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
ADD MEDICINE
|
ADD MEDICINE
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
const handleAddMedicine = () => {
|
const handleAddMedicine = () => {
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
items: [...prev.items, { ...defaultMedicine }],
|
||||||
items: [
|
|
||||||
...prev.items,
|
|
||||||
{
|
|
||||||
medicineName: "",
|
|
||||||
dosage: "",
|
|
||||||
frequency: "",
|
|
||||||
duration: "",
|
|
||||||
instructions: "",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
REMOVE MEDICINE
|
REMOVE MEDICINE
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
const handleRemoveMedicine = (index: number) => {
|
const handleRemoveMedicine = (index: number) => {
|
||||||
const newItems = form.items.filter((_, i) => i !== index);
|
const newItems = form.items.filter((_, i) => i !== index);
|
||||||
|
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
items: newItems,
|
items: newItems,
|
||||||
@ -239,44 +197,103 @@ const CreatePrescription = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
SUBMIT
|
SUBMIT VALIDATION
|
||||||
========================= */
|
========================= */
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
// Kiểm tra dữ liệu đầu vào phía client
|
||||||
|
if (!form.examinationSessionSpecialtyId) {
|
||||||
|
return toastWarn({
|
||||||
|
msg: "Vui lòng chọn phiên khám chuyên khoa",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
if (!file) {
|
||||||
/**
|
return toastWarn({
|
||||||
* VALIDATE
|
msg: "Vui lòng chọn ảnh đơn thuốc",
|
||||||
*/
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!form.examinationSessionId) {
|
const validMedicineList = form.items.filter(
|
||||||
return toastWarn({
|
(item) => item.medicineName && item.medicineName.trim() !== "",
|
||||||
msg: "Vui lòng chọn phiên khám",
|
);
|
||||||
|
|
||||||
|
if (validMedicineList.length === 0) {
|
||||||
|
return toastWarn({
|
||||||
|
msg: "Vui lòng nhập ít nhất 1 loại thuốc hợp lệ",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Thực hiện gọi API upload ảnh
|
||||||
|
setLoading(true);
|
||||||
|
const uploadResponse: any = await prescriptionServices.uploadImage(
|
||||||
|
file,
|
||||||
|
"prescriptions",
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("KẾT QUẢ PHẢN HỒI GỐC TỪ API UPLOAD:", uploadResponse);
|
||||||
|
|
||||||
|
// Trích xuất URL linh hoạt tùy thuộc vào bộ bọc interceptor của axiosClient
|
||||||
|
let imageUrl = "";
|
||||||
|
|
||||||
|
// Trường hợp 1: axiosClient trả về thẳng dữ liệu (hoặc chuỗi URL) từ server
|
||||||
|
if (typeof uploadResponse === "string") {
|
||||||
|
imageUrl = uploadResponse;
|
||||||
|
} else if (uploadResponse?.fileUrl) {
|
||||||
|
imageUrl = uploadResponse.fileUrl;
|
||||||
|
} else if (uploadResponse?.url) {
|
||||||
|
imageUrl = uploadResponse.url;
|
||||||
|
}
|
||||||
|
// Trường hợp 2: axiosClient trả về nguyên bản cấu trúc AxiosResponse chứa thuộc tính .data
|
||||||
|
else if (uploadResponse?.data) {
|
||||||
|
const innerData = uploadResponse.data;
|
||||||
|
if (typeof innerData === "string") {
|
||||||
|
imageUrl = innerData;
|
||||||
|
} else {
|
||||||
|
imageUrl =
|
||||||
|
innerData?.fileUrl ||
|
||||||
|
innerData?.url ||
|
||||||
|
innerData?.data?.fileUrl ||
|
||||||
|
innerData?.data?.url ||
|
||||||
|
"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("ĐƯỜNG DẪN ẢNH CUỐI CÙNG TRÍCH XUẤT ĐƯỢC:", imageUrl);
|
||||||
|
|
||||||
|
if (!imageUrl) {
|
||||||
|
setLoading(false);
|
||||||
|
return toastWarn({
|
||||||
|
msg: "Không tìm thấy đường dẫn ảnh từ phản hồi của máy chủ",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Thiết lập cấu trúc dữ liệu Payload chuẩn xác theo interface của createPrescription
|
||||||
|
const finalPayload = {
|
||||||
|
examinationSessionSpecialtyId: form.examinationSessionSpecialtyId,
|
||||||
|
notes: form.notes || "",
|
||||||
|
prescriptionImg: imageUrl,
|
||||||
|
items: validMedicineList.map((item) => ({
|
||||||
|
medicineName: item.medicineName.trim(),
|
||||||
|
dosage: item.dosage || "",
|
||||||
|
frequency: item.frequency || "",
|
||||||
|
duration: item.duration || "",
|
||||||
|
instructions: item.instructions || "",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3. Kích hoạt gọi Mutation gửi yêu cầu tạo đơn thuốc mới lên Server
|
||||||
|
funcCreatePrescription.mutate(finalPayload);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("LỖI XỬ LÝ SUBMIT HOẶC UPLOAD ĐƠN THUỐC:", error);
|
||||||
|
toastWarn({
|
||||||
|
msg: "Có lỗi xảy ra trong quá trình xử lý tải ảnh lên hoặc lưu đơn thuốc",
|
||||||
});
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
return toastWarn({
|
|
||||||
msg: "Vui lòng chọn ảnh đơn thuốc",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const validMedicine = form.items.some((x) => x.medicineName.trim() !== "");
|
|
||||||
|
|
||||||
if (!validMedicine) {
|
|
||||||
return toastWarn({
|
|
||||||
msg: "Vui lòng nhập ít nhất 1 loại thuốc",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MUTATE
|
|
||||||
*/
|
|
||||||
createPrescriptionMutation.mutate();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* =========================
|
|
||||||
UI
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||||
<div className="flex flex-col gap-6 rounded-2xl bg-white p-6 shadow">
|
<div className="flex flex-col gap-6 rounded-2xl bg-white p-6 shadow">
|
||||||
@ -286,7 +303,6 @@ const CreatePrescription = () => {
|
|||||||
<h2 className="text-[28px] font-semibold text-[#111827]">
|
<h2 className="text-[28px] font-semibold text-[#111827]">
|
||||||
Tạo đơn thuốc
|
Tạo đơn thuốc
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p className="mt-1 text-sm text-[#697586]">
|
<p className="mt-1 text-sm text-[#697586]">
|
||||||
Tạo mới đơn thuốc cho bệnh nhân
|
Tạo mới đơn thuốc cho bệnh nhân
|
||||||
</p>
|
</p>
|
||||||
@ -296,11 +312,11 @@ const CreatePrescription = () => {
|
|||||||
<CustomButton
|
<CustomButton
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="midnightBlue"
|
variant="midnightBlue"
|
||||||
disabled={createPrescriptionMutation.isPending}
|
disabled={funcCreatePrescription.isPending || loading}
|
||||||
className="min-w-[180px]"
|
className="min-w-[180px]"
|
||||||
>
|
>
|
||||||
{createPrescriptionMutation.isPending
|
{funcCreatePrescription.isPending || loading
|
||||||
? "Đang tạo..."
|
? "Đang tạo đơn..."
|
||||||
: "Tạo đơn thuốc"}
|
: "Tạo đơn thuốc"}
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</div>
|
</div>
|
||||||
@ -317,30 +333,32 @@ const CreatePrescription = () => {
|
|||||||
<SelectForm
|
<SelectForm
|
||||||
label={
|
label={
|
||||||
<span className="text-[14px] font-medium text-[#374151]">
|
<span className="text-[14px] font-medium text-[#374151]">
|
||||||
Phiên khám
|
Phiên khám chuyên khoa
|
||||||
<span className="text-red-500">*</span>
|
<span className="text-red-500">*</span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
placeholder="Chọn phiên khám"
|
placeholder="Chọn phiên khám chuyên khoa"
|
||||||
value={form.examinationSessionId}
|
value={form.examinationSessionSpecialtyId}
|
||||||
options={examinationSessionOptions}
|
options={examinationSessionOptions}
|
||||||
onSelect={(item: ConsultationItem) =>
|
onSelect={(item: any) =>
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
examinationSessionId: item.id,
|
examinationSessionSpecialtyId: item.id,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
onClean={() =>
|
onClean={() =>
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
examinationSessionId: "",
|
examinationSessionSpecialtyId: "",
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
getOptionLabel={(item: ConsultationItem) => `${item.sessionCode}`}
|
getOptionLabel={(item: any) =>
|
||||||
getOptionValue={(item: ConsultationItem) => item.id}
|
`${item.sessionCode} - ${item.specialtyName} (${item.patientName})`
|
||||||
|
}
|
||||||
|
getOptionValue={(item: any) => item.id}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* IMAGE */}
|
{/* IMAGE UPLOAD */}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<UploadImage
|
<UploadImage
|
||||||
label={
|
label={
|
||||||
@ -348,10 +366,10 @@ const CreatePrescription = () => {
|
|||||||
Hình ảnh đơn thuốc <span style={{ color: "red" }}>*</span>
|
Hình ảnh đơn thuốc <span style={{ color: "red" }}>*</span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
name="PrescriptionFile"
|
name="prescriptionImg"
|
||||||
file={file}
|
file={file}
|
||||||
setFile={setFile}
|
setFile={setFile}
|
||||||
path=""
|
path={""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -78,7 +78,7 @@ const DetailPrescription = () => {
|
|||||||
{data.prescriptionImg ? (
|
{data.prescriptionImg ? (
|
||||||
<>
|
<>
|
||||||
<Image
|
<Image
|
||||||
src={`${process.env.NEXT_PUBLIC_IMAGE}${data.prescriptionImg}`}
|
src={`${data.prescriptionImg}`}
|
||||||
alt="prescription"
|
alt="prescription"
|
||||||
width={140}
|
width={140}
|
||||||
height={140}
|
height={140}
|
||||||
|
|||||||
@ -142,7 +142,8 @@ const MainPrescription = () => {
|
|||||||
render: (item: any) =>
|
render: (item: any) =>
|
||||||
item.prescriptionImg ? (
|
item.prescriptionImg ? (
|
||||||
<Image
|
<Image
|
||||||
src={`${process.env.NEXT_PUBLIC_IMAGE}${item.prescriptionImg}`}
|
// src={`${process.env.NEXT_PUBLIC_IMAGE}${item.prescriptionImg}`}
|
||||||
|
src={item.prescriptionImg}
|
||||||
alt="prescription"
|
alt="prescription"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
|
|||||||
@ -0,0 +1,107 @@
|
|||||||
|
"use client";
|
||||||
|
import Table from "@/components/customs/custom-table";
|
||||||
|
import DataWrapper from "@/components/customs/DataWrapper";
|
||||||
|
import { QUERY_KEY } 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";
|
||||||
|
|
||||||
|
const MainPageStatistical = () => {
|
||||||
|
// const router = useRouter();
|
||||||
|
// const pathname = usePathname();
|
||||||
|
// const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
// const _page = searchParams.get("_page");
|
||||||
|
|
||||||
|
// const _pageSize = searchParams.get("_pageSize");
|
||||||
|
const reportQuery = useQuery<ReportResponse>({
|
||||||
|
queryKey: [QUERY_KEY.table_list_report],
|
||||||
|
|
||||||
|
queryFn: async () => {
|
||||||
|
const toDate = moment().format("YYYY-MM-DD");
|
||||||
|
const fromDate = moment().subtract(14, "days").format("YYYY-MM-DD");
|
||||||
|
|
||||||
|
const res = await httpRequest<ReportResponse>({
|
||||||
|
showMessageFailed: true,
|
||||||
|
http: consultationServices.getConsultationsReport({
|
||||||
|
FormDate: fromDate,
|
||||||
|
ToDate: toDate,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res as ReportResponse;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className="flex flex-col gap-4 rounded-lg bg-white p-6 shadow">
|
||||||
|
{/* HEADER */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-black">
|
||||||
|
Thống kê các phiên khám
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<DataWrapper
|
||||||
|
data={reportData}
|
||||||
|
loading={reportQuery.isLoading}
|
||||||
|
title="Không có dữ liệu"
|
||||||
|
note="Vui lòng thử lại sau"
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
rowKey={(item) => item.specialtyId}
|
||||||
|
data={reportData}
|
||||||
|
column={[
|
||||||
|
{
|
||||||
|
title: "MÃ CHUYÊN KHOA",
|
||||||
|
render: (item) => <span>{item.specialtyId}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "TÊN CHUYÊN KHOA",
|
||||||
|
render: (item) => <span>{item.specialtyName}</span>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "SỐ CA KHÁM",
|
||||||
|
render: (item) => <span>{item.totalCases}</span>,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DataWrapper>
|
||||||
|
{/* <Pagination
|
||||||
|
total={reportData.length}
|
||||||
|
page={page}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onSetPage={(value) => updateQuery("_page", value)}
|
||||||
|
onSetPageSize={(value) => updateQuery("_pageSize", value)}
|
||||||
|
dependencies={[_pageSize]}
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainPageStatistical;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default } from "./MainPageStatistical";
|
||||||
@ -5,7 +5,7 @@ export interface PropsSelectForm<OptionType> {
|
|||||||
isSearch?: boolean;
|
isSearch?: boolean;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
||||||
value: string | number;
|
value: string | number | React.ReactNode;
|
||||||
options: OptionType[];
|
options: OptionType[];
|
||||||
|
|
||||||
onClean?: () => void;
|
onClean?: () => void;
|
||||||
|
|||||||
@ -2,17 +2,29 @@ export enum QUERY_KEY {
|
|||||||
table_list_patient,
|
table_list_patient,
|
||||||
table_list_consultation,
|
table_list_consultation,
|
||||||
table_list_patientrescription,
|
table_list_patientrescription,
|
||||||
|
table_list_specialty,
|
||||||
|
table_list_report,
|
||||||
|
|
||||||
|
list_specialty_lookup,
|
||||||
|
|
||||||
chi_tiet_benh_nhan,
|
chi_tiet_benh_nhan,
|
||||||
chi_tiet_don_thuoc,
|
chi_tiet_don_thuoc,
|
||||||
chi_tiet_phien_kham,
|
chi_tiet_phien_kham,
|
||||||
|
|
||||||
|
user_me,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draft = 1,
|
||||||
|
// InProgress = 2,
|
||||||
|
// Completed = 3,
|
||||||
|
// Cancelled = 4,
|
||||||
export enum TYPE_STATUS {
|
export enum TYPE_STATUS {
|
||||||
Draft = 1,
|
Draft = 1, // Phiên tạm (mới tiếp nhận, chưa khám)
|
||||||
InProgress = 2,
|
InProgress = 2, // Đang khám tổng quát
|
||||||
Completed = 3,
|
ToSpecialty = 3, // Đã chỉ định & Đang chờ khám chuyên khoa 💡 (MỚI)
|
||||||
Cancelled = 4,
|
PendingPrescription = 4, // Đã khám xong hết, Chờ quầy dược phát thuốc/kính 💡 (MỚI)
|
||||||
|
Completed = 5, // Đã hoàn thành hoàn toàn (Đã khám + Đã nhận thuốc)
|
||||||
|
Cancelled = 6, // Phiên khám bị hủy
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TYPE_GENDER {
|
export enum TYPE_GENDER {
|
||||||
|
|||||||
@ -1,4 +1,10 @@
|
|||||||
import { CircleGauge, HeartPulse, Pill, Users } from "lucide-react";
|
import {
|
||||||
|
ChartNoAxesCombined,
|
||||||
|
CircleGauge,
|
||||||
|
HeartPulse,
|
||||||
|
Pill,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
export enum PATH {
|
export enum PATH {
|
||||||
HOME = "/",
|
HOME = "/",
|
||||||
@ -8,6 +14,8 @@ export enum PATH {
|
|||||||
PRESCRIPTION = "/prescription",
|
PRESCRIPTION = "/prescription",
|
||||||
CREATE_PRESCRIPTION = "/prescription/create",
|
CREATE_PRESCRIPTION = "/prescription/create",
|
||||||
|
|
||||||
|
STATISTICAL = "/statistical",
|
||||||
|
|
||||||
LOGIN = "/auth/login",
|
LOGIN = "/auth/login",
|
||||||
REGISTER = "/auth/register",
|
REGISTER = "/auth/register",
|
||||||
FORGOT_PASSWORD = "/auth/forgot-password",
|
FORGOT_PASSWORD = "/auth/forgot-password",
|
||||||
@ -51,6 +59,12 @@ export const Menus: {
|
|||||||
path: PATH.PRESCRIPTION,
|
path: PATH.PRESCRIPTION,
|
||||||
pathActive: PATH.PRESCRIPTION,
|
pathActive: PATH.PRESCRIPTION,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Thống kê chuyên khoa",
|
||||||
|
icon: ChartNoAxesCombined,
|
||||||
|
path: PATH.STATISTICAL,
|
||||||
|
pathActive: PATH.STATISTICAL,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@ -9,7 +9,7 @@ const authServices = {
|
|||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
return axiosClient.post(`/Auth/login`, data, {
|
return axiosClient.post(`/api/v1/Auth/login`, data, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -22,7 +22,7 @@ const authServices = {
|
|||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
return axiosClient.post(`/Auth/register`, data, {
|
return axiosClient.post(`/api/v1/Auth/register`, data, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -32,13 +32,13 @@ const authServices = {
|
|||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
return axiosClient.post(`/Auth/refresh`, data, {
|
return axiosClient.post(`/api/v1/Auth/refresh`, data, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
logout: (tokenAxios?: any) => {
|
logout: (tokenAxios?: any) => {
|
||||||
return axiosClient.post(
|
return axiosClient.post(
|
||||||
`/Auth/logout`,
|
`/api/v1/Auth/logout`,
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
|
|||||||
@ -24,6 +24,20 @@ export interface ConsultationItem {
|
|||||||
doctorNotes: string | null;
|
doctorNotes: string | null;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
|
specialtyClinics: {
|
||||||
|
id: string;
|
||||||
|
specialtyId: string;
|
||||||
|
specialtyName: string;
|
||||||
|
specialtyNotes: string | null;
|
||||||
|
diagnosis: string | null;
|
||||||
|
procedureNotes: string | null;
|
||||||
|
glassRequired: string | null;
|
||||||
|
glassType: string | null;
|
||||||
|
examinerName: string;
|
||||||
|
specialtyStatus: number;
|
||||||
|
sharedItems: any[];
|
||||||
|
}[];
|
||||||
|
aggregatedPrescriptionItems: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConsultationDetail {
|
export interface ConsultationDetail {
|
||||||
@ -39,8 +53,23 @@ export interface ConsultationDetail {
|
|||||||
diagnosis: string | null;
|
diagnosis: string | null;
|
||||||
treatmentPlan: string | null;
|
treatmentPlan: string | null;
|
||||||
doctorNotes: string | null;
|
doctorNotes: string | null;
|
||||||
|
isGlassesDelivered: boolean;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
|
specialtyClinics: {
|
||||||
|
id: string;
|
||||||
|
specialtyId: string;
|
||||||
|
specialtyName: string;
|
||||||
|
specialtyNotes: string | null;
|
||||||
|
diagnosis: string | null;
|
||||||
|
procedureNotes: string | null;
|
||||||
|
glassRequired: boolean | null;
|
||||||
|
glassType: string | null;
|
||||||
|
examinerName: string;
|
||||||
|
specialtyStatus: number;
|
||||||
|
sharedItems: any[];
|
||||||
|
}[];
|
||||||
|
aggregatedPrescriptionItems: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConsultationResponse {
|
export interface ConsultationResponse {
|
||||||
@ -51,23 +80,63 @@ export interface ConsultationResponse {
|
|||||||
totalPages: number;
|
totalPages: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ReportResponse {
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
totalSessions: number;
|
||||||
|
totalSpecialtySubSessions: number;
|
||||||
|
specialtyDetails: {
|
||||||
|
specialtyId: string;
|
||||||
|
specialtyName: string;
|
||||||
|
totalCases: number;
|
||||||
|
}[];
|
||||||
|
totalPrescriptionsIssued: number;
|
||||||
|
}
|
||||||
|
|
||||||
const consultationServices = {
|
const consultationServices = {
|
||||||
getConsultations: (params?: GetConsultationParams, tokenAxios?: any) => {
|
getConsultations: (params?: GetConsultationParams, tokenAxios?: any) => {
|
||||||
return axiosClient.get<ConsultationResponse>(`/ExaminationSession/list`, {
|
return axiosClient.get<ConsultationResponse>(
|
||||||
params,
|
`/api/v1/ExaminationSession/list`,
|
||||||
cancelToken: tokenAxios,
|
{
|
||||||
});
|
params,
|
||||||
|
cancelToken: tokenAxios,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
createConsultations: (data?: { patientId: string }, tokenAxios?: any) => {
|
getConsultationsReport: (
|
||||||
return axiosClient.post(`/ExaminationSession`, data, {
|
params: { FormDate: string; ToDate: string },
|
||||||
|
tokenAxios?: any,
|
||||||
|
) => {
|
||||||
|
return axiosClient.get<ReportResponse>(
|
||||||
|
`/api/v1/ExaminationSession/report`,
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
cancelToken: tokenAxios,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
createConsultations: (
|
||||||
|
data?: {
|
||||||
|
patientId: string;
|
||||||
|
chiefComplaint: string;
|
||||||
|
symptomsText: string;
|
||||||
|
vitalSigns: string;
|
||||||
|
specialtyIds: string[];
|
||||||
|
},
|
||||||
|
tokenAxios?: any,
|
||||||
|
) => {
|
||||||
|
return axiosClient.post(`/api/v1/ExaminationSession`, data, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
detailConsultations: (id: string, tokenAxios?: any) => {
|
detailConsultations: (id: string, tokenAxios?: any) => {
|
||||||
return axiosClient.get<ConsultationDetail>(`/ExaminationSession/${id}`, {
|
return axiosClient.get<ConsultationDetail>(
|
||||||
cancelToken: tokenAxios,
|
`/api/v1/ExaminationSession/${id}`,
|
||||||
});
|
{
|
||||||
|
cancelToken: tokenAxios,
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
putConsultations: (
|
putConsultations: (
|
||||||
@ -79,25 +148,46 @@ const consultationServices = {
|
|||||||
diagnosis: string | null;
|
diagnosis: string | null;
|
||||||
treatmentPlan: string | null;
|
treatmentPlan: string | null;
|
||||||
doctorNotes: string | null;
|
doctorNotes: string | null;
|
||||||
|
isGlassesDelivered: boolean;
|
||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
return axiosClient.put(`/ExaminationSession/${id}`, data, {
|
return axiosClient.put(`/api/v1/ExaminationSession/${id}`, data, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelConsultations: (id: string, tokenAxios?: any) => {
|
cancelConsultations: (id: string, tokenAxios?: any) => {
|
||||||
return axiosClient.post(`/ExaminationSession/${id}/cancel`, {
|
return axiosClient.post(`/api/v1/ExaminationSession/${id}/cancel`, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
completeConsultations: (id: string, tokenAxios?: any) => {
|
completeConsultations: (id: string, tokenAxios?: any) => {
|
||||||
return axiosClient.post(`/ExaminationSession/${id}/complete`, {
|
return axiosClient.post(`/api/v1/ExaminationSession/${id}/complete`, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
specialtyResults: (
|
||||||
|
specialtyAssignmentId: string,
|
||||||
|
data: {
|
||||||
|
specialtyNotes: string;
|
||||||
|
diagnosis: string;
|
||||||
|
procedureNotes: string;
|
||||||
|
glassRequired: boolean;
|
||||||
|
glassType: string;
|
||||||
|
},
|
||||||
|
tokenAxios?: any,
|
||||||
|
) => {
|
||||||
|
return axiosClient.put(
|
||||||
|
`/api/v1/ExaminationSession/specialty-results/${specialtyAssignmentId}`,
|
||||||
|
data,
|
||||||
|
{
|
||||||
|
cancelToken: tokenAxios,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default consultationServices;
|
export default consultationServices;
|
||||||
|
|||||||
@ -256,6 +256,7 @@ axiosClient.interceptors.response.use(
|
|||||||
|
|
||||||
if ((status === 401 || status === 403) && !data) {
|
if ((status === 401 || status === 403) && !data) {
|
||||||
return Promise.reject({
|
return Promise.reject({
|
||||||
|
status,
|
||||||
success: false,
|
success: false,
|
||||||
|
|
||||||
error: {
|
error: {
|
||||||
@ -265,15 +266,36 @@ axiosClient.interceptors.response.use(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(
|
// If server provided a body (data), forward it and attach HTTP status so
|
||||||
data || {
|
// callers receive the actual error and the numeric status code.
|
||||||
|
const serverBody = error.response?.data;
|
||||||
|
|
||||||
|
let payload: any;
|
||||||
|
|
||||||
|
if (serverBody && typeof serverBody === "object") {
|
||||||
|
payload = { ...serverBody, status };
|
||||||
|
} else if (serverBody) {
|
||||||
|
payload = {
|
||||||
|
status,
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
code: "SERVER_ERROR",
|
code: status ?? "SERVER_ERROR",
|
||||||
message: "Có lỗi xảy ra",
|
message: String(serverBody),
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
);
|
} else {
|
||||||
|
payload = {
|
||||||
|
status,
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
code: status ?? "SERVER_ERROR",
|
||||||
|
message:
|
||||||
|
error.response?.statusText || error.message || "Có lỗi xảy ra",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(payload);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -288,7 +310,7 @@ export const httpRequest = async <T>({
|
|||||||
setLoading,
|
setLoading,
|
||||||
msgSuccess = "Thành công",
|
msgSuccess = "Thành công",
|
||||||
showMessageSuccess = false,
|
showMessageSuccess = false,
|
||||||
showMessageFailed = false,
|
showMessageFailed = true,
|
||||||
onError,
|
onError,
|
||||||
}: HttpRequestOptions<T>): Promise<T> => {
|
}: HttpRequestOptions<T>): Promise<T> => {
|
||||||
try {
|
try {
|
||||||
@ -308,16 +330,20 @@ export const httpRequest = async <T>({
|
|||||||
},
|
},
|
||||||
} as ApiResponse<T>);
|
} as ApiResponse<T>);
|
||||||
|
|
||||||
// ================= SUCCESS =================
|
/* ================= SUCCESS ================= */
|
||||||
|
|
||||||
if (safeRes.success) {
|
if (safeRes.success) {
|
||||||
if (showMessageSuccess) {
|
if (showMessageSuccess) {
|
||||||
toastSuccess({ msg: msgSuccess });
|
toastSuccess({
|
||||||
|
msg: msgSuccess,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return safeRes.data as T;
|
return safeRes.data as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= FAIL =================
|
/* ================= FAIL ================= */
|
||||||
|
|
||||||
const error = {
|
const error = {
|
||||||
message: safeRes.error?.message || "Có lỗi xảy ra",
|
message: safeRes.error?.message || "Có lỗi xảy ra",
|
||||||
code: safeRes.error?.code,
|
code: safeRes.error?.code,
|
||||||
@ -325,9 +351,20 @@ export const httpRequest = async <T>({
|
|||||||
|
|
||||||
onError?.();
|
onError?.();
|
||||||
|
|
||||||
throw error; // 👈 QUAN TRỌNG
|
throw error;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
throw error; // 👈 QUAN TRỌNG: KHÔNG NUỐT
|
console.log("HTTP ERROR:", error);
|
||||||
|
|
||||||
|
const message = error?.error?.message || error?.message || "Có lỗi xảy ra";
|
||||||
|
|
||||||
|
/* SHOW TOAST */
|
||||||
|
if (showMessageFailed) {
|
||||||
|
toastWarn({
|
||||||
|
msg: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading?.(false);
|
setLoading?.(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export interface PatientResponse {
|
|||||||
|
|
||||||
const patientServices = {
|
const patientServices = {
|
||||||
getPatient: (params?: GetPatientParams, tokenAxios?: any) => {
|
getPatient: (params?: GetPatientParams, tokenAxios?: any) => {
|
||||||
return axiosClient.get<PatientResponse>(`/Patient/list`, {
|
return axiosClient.get<PatientResponse>(`/api/v1/Patient/list`, {
|
||||||
params,
|
params,
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
@ -75,7 +75,7 @@ const patientServices = {
|
|||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
return axiosClient.post(`/Patient`, data, {
|
return axiosClient.post(`/api/v1/Patient`, data, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -97,19 +97,19 @@ const patientServices = {
|
|||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
return axiosClient.put(`/Patient/${id}`, data, {
|
return axiosClient.put(`/api/v1/Patient/${id}`, data, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
detailPatient: (id: string, tokenAxios?: any) => {
|
detailPatient: (id: string, tokenAxios?: any) => {
|
||||||
return axiosClient.get<PatientDetail>(`/Patient/${id}`, {
|
return axiosClient.get<PatientDetail>(`/api/v1/Patient/${id}`, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
deletePatient: (id: string, tokenAxios?: any) => {
|
deletePatient: (id: string, tokenAxios?: any) => {
|
||||||
return axiosClient.delete(`/Patient/${id}`, {
|
return axiosClient.delete(`/api/v1/Patient/${id}`, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@ -56,12 +56,22 @@ const prescriptionServices = {
|
|||||||
/**
|
/**
|
||||||
* CREATE
|
* CREATE
|
||||||
*/
|
*/
|
||||||
createPrescription: (data: FormData, tokenAxios?: any) => {
|
createPrescription: (
|
||||||
return axiosClient.post(`/Prescription`, data, {
|
data: {
|
||||||
headers: {
|
examinationSessionSpecialtyId: string;
|
||||||
"Content-Type": "multipart/form-data",
|
notes: string;
|
||||||
},
|
prescriptionImg: string;
|
||||||
|
items: {
|
||||||
|
medicineName: string;
|
||||||
|
dosage: string;
|
||||||
|
frequency: string;
|
||||||
|
duration: string;
|
||||||
|
instructions: string;
|
||||||
|
}[];
|
||||||
|
},
|
||||||
|
tokenAxios?: any,
|
||||||
|
) => {
|
||||||
|
return axiosClient.post(`/api/v1/Prescription`, data, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -70,7 +80,7 @@ const prescriptionServices = {
|
|||||||
* GET LIST
|
* GET LIST
|
||||||
*/
|
*/
|
||||||
getPrescription: (params?: GetPrescriptionParams, tokenAxios?: any) => {
|
getPrescription: (params?: GetPrescriptionParams, tokenAxios?: any) => {
|
||||||
return axiosClient.get<PrescriptionResponse>(`/Prescription`, {
|
return axiosClient.get<PrescriptionResponse>(`/api/v1/Prescription`, {
|
||||||
params,
|
params,
|
||||||
|
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
@ -81,7 +91,7 @@ const prescriptionServices = {
|
|||||||
* UPDATE
|
* UPDATE
|
||||||
*/
|
*/
|
||||||
putPrescription: (id: string, data: FormData, tokenAxios?: any) => {
|
putPrescription: (id: string, data: FormData, tokenAxios?: any) => {
|
||||||
return axiosClient.put(`/Prescription/${id}`, data, {
|
return axiosClient.put(`/api/v1/Prescription/${id}`, data, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
@ -94,7 +104,7 @@ const prescriptionServices = {
|
|||||||
* DETAIL
|
* DETAIL
|
||||||
*/
|
*/
|
||||||
detailPrescription: (id: string, tokenAxios?: any) => {
|
detailPrescription: (id: string, tokenAxios?: any) => {
|
||||||
return axiosClient.get<PrescriptionDetail>(`/Prescription/${id}`, {
|
return axiosClient.get<PrescriptionDetail>(`/api/v1/Prescription/${id}`, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -102,8 +112,22 @@ const prescriptionServices = {
|
|||||||
/**
|
/**
|
||||||
* UPLOAD IMAGE
|
* UPLOAD IMAGE
|
||||||
*/
|
*/
|
||||||
uploadImage: (body: FormData) => {
|
/**
|
||||||
return axiosClient.post("/upload", body, {
|
* 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();
|
||||||
|
|
||||||
|
// Key phải viết hoa chữ cái đầu đúng như Swagger yêu cầu (File và FolderName)
|
||||||
|
formData.append("File", file);
|
||||||
|
|
||||||
|
if (folderName) {
|
||||||
|
formData.append("FolderName", folderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return axiosClient.post("/api/Media/upload", formData, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
|
|||||||
16
src/services/specialtyServices.ts
Normal file
16
src/services/specialtyServices.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import axiosClient from ".";
|
||||||
|
|
||||||
|
export interface SpecialtyItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const specialtyServices = {
|
||||||
|
getSpecialty: (tokenAxios?: any) => {
|
||||||
|
return axiosClient.get<SpecialtyItem[]>("/api/v1/Specialty/lookup", {
|
||||||
|
cancelToken: tokenAxios,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default specialtyServices;
|
||||||
@ -10,9 +10,20 @@ interface GetUserParams {
|
|||||||
Desc?: boolean;
|
Desc?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserMe {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
role: number;
|
||||||
|
roleType: number | string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
fullName: string | null;
|
||||||
|
email: string | null;
|
||||||
|
phone: string | number | null;
|
||||||
|
}
|
||||||
|
|
||||||
const userServices = {
|
const userServices = {
|
||||||
getUser: (params?: GetUserParams, tokenAxios?: any) => {
|
getUser: (params?: GetUserParams, tokenAxios?: any) => {
|
||||||
return axiosClient.get(`/User/list`, {
|
return axiosClient.get(`/api/v1/User/list`, {
|
||||||
params,
|
params,
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
@ -29,7 +40,7 @@ const userServices = {
|
|||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
return axiosClient.post(`/Users/`, data, {
|
return axiosClient.post(`/api/v1/Users/`, data, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -44,22 +55,22 @@ const userServices = {
|
|||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
return axiosClient.put(`/Users/${id}`, data, {
|
return axiosClient.put(`/api/v1/Users/${id}`, data, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteUser: (id: string, tokenAxios?: any) => {
|
deleteUser: (id: string, tokenAxios?: any) => {
|
||||||
return axiosClient.delete(`/Users/${id}`, {
|
return axiosClient.delete(`/api/v1/Users/${id}`, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
detailUser: (id: string, tokenAxios?: any) => {
|
detailUser: (id: string, tokenAxios?: any) => {
|
||||||
return axiosClient.get(`/Users/${id}`, {
|
return axiosClient.get(`/api/v1/Users/${id}`, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
meUser: (tokenAxios?: any) => {
|
meUser: (tokenAxios?: any) => {
|
||||||
return axiosClient.get(`/Users/me`, {
|
return axiosClient.get<UserMe>(`/api/v1/Users/me`, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user