feat update
This commit is contained in:
+8
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "kham-benh-mo-pho",
|
||||
"name": "TEDI-DDP",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -11,6 +11,8 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
"@radix-ui/react-tooltip": "^1.2.8",
|
||||
"@react-three/drei": "^10.7.8",
|
||||
"@react-three/fiber": "^9.7.0",
|
||||
"@reduxjs/toolkit": "^2.12.0",
|
||||
"@tanstack/react-query": "^5.100.11",
|
||||
"axios": "^1.16.1",
|
||||
@@ -25,7 +27,10 @@
|
||||
"react-dom": "19.2.4",
|
||||
"react-moment": "^2.0.2",
|
||||
"react-redux": "^9.3.0",
|
||||
"react-toastify": "^11.1.0"
|
||||
"react-toastify": "^11.1.0",
|
||||
"three": "^0.185.1",
|
||||
"zod": "^4.4.3",
|
||||
"zustand": "^5.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
@@ -34,6 +39,7 @@
|
||||
"@types/nprogress": "^0",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"@types/three": "^0",
|
||||
"babel-plugin-react-compiler": "1.0.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.6",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 531 KiB After Width: | Height: | Size: 295 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@@ -1,16 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import DetailConsultation from "@/components/page/consultation/DetailConsultation";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<BaseLayout title="Chi tiết phiên khám">
|
||||
<DetailConsultation />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import CreateConsultation from "@/components/page/consultation/CreateConsultation";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Tạo phiên khám">
|
||||
<CreateConsultation />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import TableHistoryPatient from "@/components/page/consultation/TableHistoryPatient";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Lịch sử các phiên khám">
|
||||
<TableHistoryPatient />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import MainPageConsultation from "@/components/page/consultation/MainPageConsultation";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Phiên khám">
|
||||
<MainPageConsultation />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@@ -1,14 +0,0 @@
|
||||
"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;
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import UpdateConsultation from "@/components/page/consultation/UpdateConsultation";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Cập nhật phiên khám">
|
||||
<UpdateConsultation />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@@ -1,15 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import BaseLayout from "@/components/layouts/BaseLayout/BaseLayout";
|
||||
import MainPageHome from "@/components/page/Home/MainPageHome";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Dashboard">
|
||||
<MainPageHome />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 84 KiB |
+1
-1
@@ -15,7 +15,7 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Khám Bệnh Mơ Phố",
|
||||
title: "Core BIM tedi",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
|
||||
+2
-1
@@ -6,9 +6,10 @@ import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Dashboard">
|
||||
<BaseLayout title="">
|
||||
<MainPageHome />
|
||||
</BaseLayout>
|
||||
// <h1>Home</h1>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
|
||||
import DetailPatient from "@/components/page/patient/DetailPatient";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<BaseLayout title="Chi tiết bệnh nhân">
|
||||
<DetailPatient />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,17 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import MainPagePatient from "@/components/page/patient/MainPagePatient/MainPagePatient";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div>
|
||||
<BaseLayout title="Bệnh nhân">
|
||||
<MainPagePatient />
|
||||
</BaseLayout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import DetailPrescription from "@/components/page/prescription/DetailPrescription";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Chi tiết đơn thuốc">
|
||||
<DetailPrescription />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import CreatePrescription from "@/components/page/prescription/CreatePrescription";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Tạo đơn thuốc">
|
||||
<CreatePrescription />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import MainPrescription from "@/components/page/prescription/MainPrescription";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Đơn thuốc">
|
||||
<MainPrescription />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import MainPageProfile from "@/components/page/profile/MainPageProfile";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Thông tin cá nhân">
|
||||
<MainPageProfile />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
||||
import MainUpdateProfile from "@/components/page/profile/MainUpdateProfile";
|
||||
import React from "react";
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<BaseLayout title="Cập nhật thông tin cá nhân">
|
||||
<MainUpdateProfile />
|
||||
</BaseLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
@@ -1,14 +0,0 @@
|
||||
"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;
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const SuperTBeamSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
code: z.string().min(1, "Mã dầm không được để trống"),
|
||||
// Thông số hình học dầm (mm)
|
||||
length: z
|
||||
.number()
|
||||
.min(12000, "Chiều dài dầm tối thiểu 12,000mm")
|
||||
.max(40000, "Chiều dài dầm tối đa 40,000mm"),
|
||||
height: z
|
||||
.number()
|
||||
.min(1000, "Chiều cao tối thiểu 1,000mm")
|
||||
.max(2500, "Chiều cao tối đa 2,500mm"),
|
||||
topFlangeWidth: z.number().min(1500).max(2500),
|
||||
topFlangeThickness: z.number().min(150).max(400),
|
||||
webThickness: z.number().min(100).max(300),
|
||||
bottomFlangeWidth: z.number().min(500).max(1200),
|
||||
bottomFlangeThickness: z.number().min(200).max(500),
|
||||
|
||||
// Cấu kiện kèm theo
|
||||
diaphragmPositions: z.array(z.number()), // Các vị trí dầm ngang tính từ đầu dầm (mm)
|
||||
strandCount: z.number().int().min(2).max(60), // Số lượng tao cáp DƯL
|
||||
|
||||
// Vị trí lắp đặt kết cấu nhịp
|
||||
placement: z.object({
|
||||
station: z.number(), // Lý trình (m)
|
||||
skewAngle: z.number().min(-45).max(45), // Góc chéo (độ)
|
||||
offsetY: z.number(),
|
||||
offsetZ: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type SuperTBeamData = z.infer<typeof SuperTBeamSchema>;
|
||||
@@ -0,0 +1,51 @@
|
||||
import { SuperTBeamData } from "../schema/superT.schema";
|
||||
|
||||
export interface ValidationIssue {
|
||||
type: "error" | "warning";
|
||||
field: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export function validateSuperTEngineering(
|
||||
data: SuperTBeamData,
|
||||
): ValidationIssue[] {
|
||||
const issues: ValidationIssue[] = [];
|
||||
|
||||
// Rule 1: Tỷ lệ chiều cao / chiều dài dầm (H/L) nằm trong khoảng 1/15 đến 1/25
|
||||
const ratio = data.height / data.length;
|
||||
if (ratio < 1 / 25) {
|
||||
issues.push({
|
||||
type: "warning",
|
||||
field: "height",
|
||||
message: `Tỷ lệ H/L = (1/${(1 / ratio).toFixed(1)}) nhỏ hơn 1/25. Dầm có nguy cơ bị võng quá mức cho phép!`,
|
||||
});
|
||||
} else if (ratio > 1 / 15) {
|
||||
issues.push({
|
||||
type: "warning",
|
||||
field: "height",
|
||||
message: `Tỷ lệ H/L = (1/${(1 / ratio).toFixed(1)}) lớn hơn 1/15. Dầm quá cứng, lãng phí vật liệu!`,
|
||||
});
|
||||
}
|
||||
|
||||
// Rule 2: Kiểm tra bề dày sườn dầm với bề rộng cánh dưới
|
||||
if (data.webThickness * 2 >= data.bottomFlangeWidth) {
|
||||
issues.push({
|
||||
type: "error",
|
||||
field: "webThickness",
|
||||
message: "Bề dày sườn dầm quá lớn so với cánh dưới!",
|
||||
});
|
||||
}
|
||||
|
||||
// Rule 3: Kiểm tra vị trí dầm ngang không vượt quá chiều dài dầm
|
||||
data.diaphragmPositions.forEach((pos, idx) => {
|
||||
if (pos < 0 || pos > data.length) {
|
||||
issues.push({
|
||||
type: "error",
|
||||
field: `diaphragmPositions[${idx}]`,
|
||||
message: `Dầm ngang thứ ${idx + 1} ở vị trí ${pos}mm nằm ngoài phạm vi chiều dài dầm!`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return issues;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { PropsBaseLayout, TContextBaseLayout } from "./interface";
|
||||
import clsx from "clsx";
|
||||
import Header from "./componets/Header";
|
||||
import Navbar from "./componets/Navbar";
|
||||
import RequireAuth from "@/components/protected/RequiredAuth";
|
||||
|
||||
export const ContextBaseLayout = React.createContext<TContextBaseLayout>({});
|
||||
@@ -15,49 +15,22 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
||||
return (
|
||||
<RequireAuth>
|
||||
<ContextBaseLayout
|
||||
value={{ showFull, setShowFull, openMenuMobile, setOpenMenuMobile }}
|
||||
value={{
|
||||
showFull,
|
||||
setShowFull,
|
||||
openMenuMobile,
|
||||
setOpenMenuMobile,
|
||||
}}
|
||||
>
|
||||
<div className="min-h-screen bg-[#f8f8f8]">
|
||||
{/* OVERLAY cho Mobile */}
|
||||
<div
|
||||
className={clsx(
|
||||
"fixed inset-0 bg-black/50 backdrop-blur-sm z-[20] transition-opacity duration-300 xl:hidden",
|
||||
openMenuMobile ? "opacity-100 visible" : "opacity-0 invisible",
|
||||
)}
|
||||
onClick={() => setOpenMenuMobile(false)}
|
||||
/>
|
||||
|
||||
{/* SIDEBAR (Dùng chung cho cả Desktop & Mobile) */}
|
||||
<nav
|
||||
className={clsx(
|
||||
"fixed top-0 left-0 h-screen bg-white border-r border-[#f4f7fa] z-[30] transition-all duration-300 ease-in-out",
|
||||
|
||||
/* DESKTOP */
|
||||
showFull
|
||||
? "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 />
|
||||
</nav>
|
||||
|
||||
<div className="min-h-screen w-full overflow-x-hidden bg-[#f8f8f8]">
|
||||
{/* HEADER */}
|
||||
<header
|
||||
className={clsx(
|
||||
"fixed top-0 right-0 h-[68px] z-[11] bg-white transition-all duration-300 border-b border-[#f4f7fa]",
|
||||
|
||||
/* DESKTOP */
|
||||
showFull
|
||||
? "xl:left-[240px] xl:w-[calc(100%-240px)]"
|
||||
: "xl:left-0 xl:w-full",
|
||||
|
||||
/* MOBILE */
|
||||
"left-0 w-full",
|
||||
"top-0 right-0 z-[11] h-[68px]",
|
||||
"w-full",
|
||||
"border-b border-[#f4f7fa]",
|
||||
"bg-white",
|
||||
"transition-all duration-300",
|
||||
)}
|
||||
>
|
||||
<Header title={title} breadcrumb={breadcrumb} />
|
||||
@@ -66,12 +39,10 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
||||
{/* MAIN CONTENT */}
|
||||
<main
|
||||
className={clsx(
|
||||
"pt-[92px] pb-6 px-6 transition-all duration-300 ease-in-out",
|
||||
|
||||
/* DESKTOP */
|
||||
showFull ? "xl:ml-[240px]" : "xl:ml-0",
|
||||
|
||||
/* MOBILE */
|
||||
"w-full",
|
||||
"min-w-0",
|
||||
"overflow-x-hidden",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
"ml-0",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ChevronDown } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import MenuProfile from "../MenuProfile";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import profileServices from "@/services/profileServices";
|
||||
// import profileServices from "@/services/profileServices";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
@@ -41,38 +41,38 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
||||
/* =========================================================
|
||||
CALL API GET LIST PROFILE & KHỚP NỐI THEO CHUẨN DATA MỚI
|
||||
========================================================= */
|
||||
const profileQuery = useQuery<any>({
|
||||
queryKey: [QUERY_KEY.table_list_profile],
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<any>({
|
||||
showMessageFailed: true,
|
||||
http: profileServices.getProfile(), // Đảm bảo endpoint trỏ về /api/v1/UserProfile/list
|
||||
});
|
||||
// const profileQuery = useQuery<any>({
|
||||
// queryKey: [QUERY_KEY.table_list_profile],
|
||||
// queryFn: async () => {
|
||||
// const res = await httpRequest<any>({
|
||||
// showMessageFailed: true,
|
||||
// http: profileServices.getProfile(), // Đảm bảo endpoint trỏ về /api/v1/UserProfile/list
|
||||
// });
|
||||
|
||||
// Trả về đúng object chứa items theo cấu trúc của API
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
// // Trả về đúng object chứa items theo cấu trúc của API
|
||||
// return (
|
||||
// res || {
|
||||
// items: [],
|
||||
// page: 1,
|
||||
// pageSize: 10,
|
||||
// total: 0,
|
||||
// totalPages: 0,
|
||||
// }
|
||||
// );
|
||||
// },
|
||||
// });
|
||||
|
||||
// Tìm kiếm thông tin profile của user hiện tại đang đăng nhập bằng useMemo
|
||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
||||
const currentUserProfile = useMemo(() => {
|
||||
const listItems = profileQuery.data?.items || [];
|
||||
if (!infoUser?.userId) return null;
|
||||
// const currentUserProfile = useMemo(() => {
|
||||
// const listItems = profileQuery.data?.items || [];
|
||||
// if (!infoUser?.userId) return null;
|
||||
|
||||
// So khớp accountId của API trả về với userId trong Redux (infoUser)
|
||||
return (
|
||||
listItems.find((item: any) => item.accountId === infoUser.userId) || null
|
||||
);
|
||||
}, [profileQuery.data, infoUser?.userId]);
|
||||
// // So khớp accountId của API trả về với userId trong Redux (infoUser)
|
||||
// return (
|
||||
// listItems.find((item: any) => item.accountId === infoUser.userId) || null
|
||||
// );
|
||||
// }, [profileQuery.data, infoUser?.userId]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full px-6 flex items-center justify-between bg-white relative">
|
||||
@@ -80,21 +80,21 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
||||
{/* Nút bấm Mobile: Mở menu mobile */}
|
||||
<div
|
||||
className="xl:hidden cursor-pointer"
|
||||
onClick={() => context?.setOpenMenuMobile?.(!context?.openMenuMobile)}
|
||||
// onClick={() => context?.setOpenMenuMobile?.(!context?.openMenuMobile)}
|
||||
>
|
||||
<Image src={icons.hamburger} alt="menu" width={20} height={20} />
|
||||
<Image src={icons.LogoTedi} alt="menu" width={20} height={20} />
|
||||
</div>
|
||||
{/* Nút bấm Desktop: Thu gọn/Mở rộng sidebar */}
|
||||
<div
|
||||
className="hidden xl:block cursor-pointer"
|
||||
onClick={() => context?.setShowFull?.(!context?.showFull)}
|
||||
// onClick={() => context?.setShowFull?.(!context?.showFull)}
|
||||
>
|
||||
<Image src={icons.hamburger} alt="menu" width={20} height={20} />
|
||||
<Image src={icons.LogoTedi} alt="menu" width={20} height={20} />
|
||||
</div>
|
||||
{breadcrumb ? (
|
||||
breadcrumb
|
||||
) : (
|
||||
<h4 className="text-[18px] font-bold text-[#141416]">{title}</h4>
|
||||
<h4 className="text-[18px] font-bold text-[#141416]">Công ty Tedi</h4>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -110,11 +110,7 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full border-2 border-blue-500 overflow-hidden relative">
|
||||
<Image
|
||||
src={
|
||||
currentUserProfile?.avatarUrl
|
||||
? `${process.env.NEXT_PUBLIC_IMAGE}${currentUserProfile.avatarUrl}`
|
||||
: icons.avatar
|
||||
}
|
||||
src={icons.avatar}
|
||||
alt="avatar"
|
||||
width={40}
|
||||
height={40}
|
||||
@@ -122,7 +118,7 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
||||
/>
|
||||
</div>
|
||||
<p className="hidden md:block text-sm font-semibold text-[#171832]">
|
||||
{currentUserProfile?.fullName || "User admin"}
|
||||
{"User admin"}
|
||||
</p>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
|
||||
@@ -51,21 +51,6 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
||||
return (
|
||||
<div className="w-[260px] bg-white rounded-xl shadow-md p-2">
|
||||
{/* PROFILE */}
|
||||
<Link
|
||||
href={PATH.PROFILE}
|
||||
onClick={onClose}
|
||||
className={clsx(
|
||||
"flex items-center gap-3 p-3 rounded-lg hover:bg-gray-100 transition",
|
||||
checkActive(PATH.PROFILE) && "bg-gray-100",
|
||||
)}
|
||||
>
|
||||
<ShieldUser size={20} />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Thông tin cá nhân</p>
|
||||
<p className="text-xs text-gray-500">Chi tiết tài khoản</p>
|
||||
</div>
|
||||
</Link>
|
||||
<div className="h-px bg-gray-200 my-1" />
|
||||
|
||||
{/* LOGOUT */}
|
||||
<div
|
||||
|
||||
@@ -1,340 +1,27 @@
|
||||
"use client";
|
||||
import React, { useMemo } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import moment from "moment";
|
||||
import {
|
||||
Activity,
|
||||
CalendarDays,
|
||||
ClipboardList,
|
||||
Hospital,
|
||||
Stethoscope,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import consultationServices, {
|
||||
ConsultationItem,
|
||||
ConsultationResponse,
|
||||
ReportResponse,
|
||||
} from "@/services/consultationServices";
|
||||
import specialtyServices, { SpecialtyItem } from "@/services/specialtyServices";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import dynamic from "next/dynamic";
|
||||
import BeamForm from "./components/BeamForm";
|
||||
import { BridgeTreeSidebar } from "@/components/utils/BridgeTreeSidebar";
|
||||
|
||||
const MainPageHome = () => {
|
||||
const today = moment().format("YYYY-MM-DD");
|
||||
|
||||
const consultationQuery = useQuery<ConsultationResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_consultation, "dashboard"],
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationResponse>({
|
||||
showMessageFailed: true,
|
||||
http: consultationServices.getConsultations({
|
||||
Page: 1,
|
||||
PageSize: 100,
|
||||
SortBy: "visitDate",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
const Canvas3D = dynamic(
|
||||
() => import("@/components/viewer3d/Canvas3D").then((m) => m.Canvas3D),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
<main className="w-screen h-screen flex overflow-hidden bg-slate-950">
|
||||
{/* 1. Cột Menu cây thư mục (Trái) */}
|
||||
<BridgeTreeSidebar />
|
||||
|
||||
{/* 2. Cột Form nhập liệu theo Tab (Giữa) */}
|
||||
<BeamForm />
|
||||
|
||||
{/* 3. Cột Viewport 3D Focus theo Cấu kiện (Phải) */}
|
||||
<div className="flex-1 h-full relative">
|
||||
<Canvas3D />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const reportQuery = useQuery<ReportResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_report, today],
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ReportResponse>({
|
||||
showMessageFailed: true,
|
||||
http: consultationServices.getConsultationsReport({
|
||||
FromDate: today,
|
||||
ToDate: today,
|
||||
}),
|
||||
});
|
||||
|
||||
return res as ReportResponse;
|
||||
},
|
||||
});
|
||||
|
||||
const specialtyQuery = useQuery<SpecialtyItem[]>({
|
||||
queryKey: [QUERY_KEY.list_specialty_lookup],
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<SpecialtyItem[]>({
|
||||
showMessageFailed: true,
|
||||
http: specialtyServices.getSpecialty(),
|
||||
});
|
||||
|
||||
return res || [];
|
||||
},
|
||||
});
|
||||
|
||||
const consultationItems = consultationQuery.data?.items || [];
|
||||
const consultationReport = reportQuery.data;
|
||||
const specialtyItems = specialtyQuery.data || [];
|
||||
|
||||
const todayConsultations = useMemo(() => {
|
||||
return consultationItems.filter((item) => {
|
||||
if (!item.visitDate) return false;
|
||||
|
||||
return moment(item.visitDate).format("YYYY-MM-DD") === today;
|
||||
});
|
||||
}, [consultationItems, today]);
|
||||
|
||||
const statusCounts = useMemo(() => {
|
||||
return {
|
||||
total: todayConsultations.length,
|
||||
|
||||
waiting: todayConsultations.filter((item) => item.status === 1).length,
|
||||
|
||||
inProgress: todayConsultations.filter((item) => item.status === 2).length,
|
||||
|
||||
completed: todayConsultations.filter((item) => item.status === 5).length,
|
||||
|
||||
cancelled: todayConsultations.filter((item) => item.status === 6).length,
|
||||
};
|
||||
}, [todayConsultations]);
|
||||
|
||||
const specialtyActiveCounts = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
|
||||
todayConsultations.forEach((item) => {
|
||||
item.specialtyClinics?.forEach((specialty) => {
|
||||
const count = map.get(specialty.specialtyId) || 0;
|
||||
|
||||
map.set(specialty.specialtyId, count + 1);
|
||||
});
|
||||
});
|
||||
|
||||
return specialtyItems.map((specialty) => ({
|
||||
...specialty,
|
||||
active: map.get(specialty.id) || 0,
|
||||
}));
|
||||
}, [todayConsultations, specialtyItems]);
|
||||
|
||||
const summaryCards = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: "Tất cả đợt khám",
|
||||
value: statusCounts.total.toString(),
|
||||
subtitle: "Số đợt khám trong ngày",
|
||||
icon: CalendarDays,
|
||||
color: "bg-blue-50 text-blue-600",
|
||||
},
|
||||
{
|
||||
title: "Đang khám",
|
||||
value: statusCounts.inProgress.toString(),
|
||||
subtitle: "Đang khám tổng quát",
|
||||
icon: Stethoscope,
|
||||
color: "bg-emerald-50 text-emerald-600",
|
||||
},
|
||||
{
|
||||
title: "Hoàn tất",
|
||||
value: statusCounts.completed.toString(),
|
||||
subtitle: "Đã hoàn thành",
|
||||
icon: ClipboardList,
|
||||
color: "bg-sky-50 text-sky-600",
|
||||
},
|
||||
{
|
||||
title: "Chờ khám",
|
||||
value: statusCounts.waiting.toString(),
|
||||
subtitle: "Đang chờ bác sĩ",
|
||||
icon: Activity,
|
||||
color: "bg-orange-50 text-orange-600",
|
||||
},
|
||||
],
|
||||
[statusCounts],
|
||||
);
|
||||
|
||||
const generalInfo = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: "Tổng đợt khám",
|
||||
value: statusCounts.total.toString(),
|
||||
icon: Users,
|
||||
color: "bg-violet-50 text-violet-600",
|
||||
},
|
||||
{
|
||||
label: "Đợt khám chuyên khoa",
|
||||
value: specialtyActiveCounts
|
||||
.reduce((sum, item) => sum + item.active, 0)
|
||||
.toString(),
|
||||
icon: Hospital,
|
||||
color: "bg-cyan-50 text-cyan-600",
|
||||
},
|
||||
{
|
||||
label: "Đơn thuốc",
|
||||
value: consultationReport?.totalPrescriptionsIssued?.toString() || "0",
|
||||
icon: ClipboardList,
|
||||
color: "bg-amber-50 text-amber-600",
|
||||
},
|
||||
{
|
||||
label: "Chuyên khoa",
|
||||
value: specialtyItems.length.toString(),
|
||||
icon: Activity,
|
||||
color: "bg-teal-50 text-teal-600",
|
||||
},
|
||||
],
|
||||
[
|
||||
consultationReport,
|
||||
statusCounts.total,
|
||||
specialtyActiveCounts,
|
||||
specialtyItems.length,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="space-y-6">
|
||||
<div className="rounded-[20px] bg-white p-6 shadow-sm border border-gray-100">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-slate-900">
|
||||
Dashboard khám bệnh
|
||||
</h1>
|
||||
<p className="mt-2 text-sm text-slate-500">
|
||||
Theo dõi nhanh các đợt khám, chuyên khoa và thông tin tổng quát
|
||||
trong ngày.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-full bg-slate-100 px-4 py-2 text-sm text-slate-700">
|
||||
Cập nhật: {moment().format("DD/MM/YYYY")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataWrapper
|
||||
loading={
|
||||
consultationQuery.isLoading ||
|
||||
reportQuery.isLoading ||
|
||||
specialtyQuery.isLoading
|
||||
}
|
||||
data={summaryCards}
|
||||
title="Không có dữ liệu"
|
||||
note="Vui lòng thử lại sau"
|
||||
>
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{summaryCards.map((item) => {
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.title}
|
||||
className="rounded-[20px] border border-slate-200 bg-slate-50 p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium text-slate-500">
|
||||
{item.title}
|
||||
</div>
|
||||
<div
|
||||
className={`inline-flex h-10 w-10 items-center justify-center rounded-2xl ${item.color}`}
|
||||
>
|
||||
<Icon size={20} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-end justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-3xl font-semibold text-slate-900">
|
||||
{item.value}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{item.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</DataWrapper>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[1.4fr_1fr]">
|
||||
<div className="rounded-[20px] bg-white p-6 shadow-sm border border-gray-100">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900">
|
||||
Chuyên khoa hiện có
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-slate-500">
|
||||
Danh sách chuyên khoa.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-full bg-slate-100 px-3 py-1 text-sm text-slate-700">
|
||||
{specialtyItems.length} chuyên khoa
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 divide-y divide-slate-200">
|
||||
{specialtyActiveCounts.map((specialty, idx) => (
|
||||
<div
|
||||
key={specialty.id}
|
||||
className={`flex items-center justify-between py-4 ${idx !== 0 ? "border-t border-slate-200" : ""}`}
|
||||
>
|
||||
<div>
|
||||
<p className="text-base font-medium text-slate-900">
|
||||
{specialty.name}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
Số chuyên khoa: {specialty.active} ca khám hôm nay
|
||||
</p>
|
||||
</div>
|
||||
<span className="rounded-full bg-slate-100 px-3 py-1 text-sm font-semibold text-slate-700">
|
||||
{specialty.active}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="rounded-[20px] bg-white p-6 shadow-sm border border-gray-100">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900">
|
||||
Thông tin khám tổng quát
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-slate-500">
|
||||
Tổng số đợt khám, ca khám chuyên khoa và đơn thuốc trong ngày.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-4 sm:grid-cols-2">
|
||||
{generalInfo.map((item) => {
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.label}
|
||||
className="rounded-[18px] border border-slate-200 bg-slate-50 p-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={`inline-flex h-11 w-11 items-center justify-center rounded-2xl ${item.color}`}
|
||||
>
|
||||
<Icon size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-500">
|
||||
{item.label}
|
||||
</p>
|
||||
<p className="mt-1 text-2xl font-semibold text-slate-900">
|
||||
{item.value}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPageHome;
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Download, AlertTriangle, CheckCircle2, Layers } from "lucide-react";
|
||||
import { useSuperTStore } from "@/hooks/useSuperTStore";
|
||||
|
||||
export function BeamForm() {
|
||||
const {
|
||||
activeTab,
|
||||
beamData,
|
||||
setBeamData,
|
||||
bridgeData,
|
||||
setBridgeData,
|
||||
isAssemblyMode,
|
||||
setIsAssemblyMode,
|
||||
validationIssues,
|
||||
exportJSONContract,
|
||||
} = useSuperTStore();
|
||||
|
||||
const handleExportContract = () => {
|
||||
const jsonString = exportJSONContract();
|
||||
const blob = new Blob([jsonString], { type: "application/json" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `TEDI_DDP_${activeTab}_${Date.now()}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-[360px] h-full bg-slate-900 border-r border-slate-800 text-slate-100 p-4 overflow-y-auto flex flex-col gap-5 shrink-0 select-none">
|
||||
{/* Nút bật/tắt Chế độ Ghép Cầu */}
|
||||
<button
|
||||
onClick={() => setIsAssemblyMode(!isAssemblyMode)}
|
||||
className={`w-full py-2 px-3 rounded text-xs font-semibold flex items-center justify-center gap-2 transition-all border ${
|
||||
isAssemblyMode
|
||||
? "bg-emerald-600 hover:bg-emerald-500 border-emerald-400 text-white shadow-lg shadow-emerald-600/20"
|
||||
: "bg-slate-800 hover:bg-slate-700 border-slate-700 text-slate-300"
|
||||
}`}
|
||||
>
|
||||
<Layers className="w-4 h-4 shrink-0" />
|
||||
<span>
|
||||
{isAssemblyMode
|
||||
? "Chế độ: Ghép toàn bộ cầu"
|
||||
: "Xem riêng lẻ cấu kiện"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Dynamic Form Content */}
|
||||
<div className="flex-1 space-y-4">
|
||||
{/* 1.1 DẦM SUPER-T */}
|
||||
{activeTab === "superTBeam" && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
||||
1.1 Thông số Dầm Super-T
|
||||
</h2>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 block mb-1">
|
||||
Chiều dài L (mm):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={beamData.length}
|
||||
onChange={(e) =>
|
||||
setBeamData({ length: Number(e.target.value) })
|
||||
}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 block mb-1">
|
||||
Chiều cao H (mm):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={beamData.height}
|
||||
onChange={(e) =>
|
||||
setBeamData({ height: Number(e.target.value) })
|
||||
}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Validation Panel */}
|
||||
<div className="pt-2">
|
||||
{validationIssues && validationIssues.length > 0 ? (
|
||||
<div className="p-2.5 bg-rose-500/10 border border-rose-500/30 rounded text-rose-400 text-xs space-y-1">
|
||||
<div className="flex items-center gap-1.5 font-semibold">
|
||||
<AlertTriangle className="w-3.5 h-3.5" />
|
||||
<span>Cảnh báo kỹ thuật:</span>
|
||||
</div>
|
||||
<ul className="list-disc list-inside text-[11px] space-y-0.5 text-rose-300">
|
||||
{validationIssues.map((issue, idx) => (
|
||||
<li key={idx}>{issue.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-2 bg-emerald-500/10 border border-emerald-500/30 rounded text-emerald-400 text-xs flex items-center gap-1.5">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
<span>Kích thước dầm hợp lệ (AASHTO / TCVN)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 1.2 BẢN MẶT CẦU */}
|
||||
{activeTab === "deck" && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
||||
1.2 Bản Mặt Cầu
|
||||
</h2>
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 block mb-1">
|
||||
Bề rộng bản mặt cầu (mm):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={bridgeData.deck.width}
|
||||
onChange={(e) =>
|
||||
setBridgeData("deck", { width: Number(e.target.value) })
|
||||
}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 block mb-1">
|
||||
Chiều dày bản (mm):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={bridgeData.deck.thickness}
|
||||
onChange={(e) =>
|
||||
setBridgeData("deck", { thickness: Number(e.target.value) })
|
||||
}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 1.3 DẦM NGANG */}
|
||||
{activeTab === "diaphragm" && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
||||
1.3 Dầm Ngang / Diaphragms
|
||||
</h2>
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 block mb-1">
|
||||
Chiều dày dầm ngang (mm):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={bridgeData.diaphragm.thickness}
|
||||
onChange={(e) =>
|
||||
setBridgeData("diaphragm", {
|
||||
thickness: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 1.4 LAN CAN */}
|
||||
{activeTab === "railing" && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
||||
1.4 Lan Can & Gờ Chắn
|
||||
</h2>
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 block mb-1">
|
||||
Chiều cao lan can (mm):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={bridgeData.railing.height}
|
||||
onChange={(e) =>
|
||||
setBridgeData("railing", { height: Number(e.target.value) })
|
||||
}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 2.1 GỐI CẦU */}
|
||||
{activeTab === "bearing" && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
||||
2.1 Gối Cầu
|
||||
</h2>
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 block mb-1">
|
||||
Chiều cao gối (mm):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={bridgeData.bearing.height}
|
||||
onChange={(e) =>
|
||||
setBridgeData("bearing", { height: Number(e.target.value) })
|
||||
}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3.1 MỐ CẦU */}
|
||||
{activeTab === "abutment" && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
||||
3.1 Mố Cầu
|
||||
</h2>
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 block mb-1">
|
||||
Chiều cao mố H (mm):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={bridgeData.abutment.height}
|
||||
onChange={(e) =>
|
||||
setBridgeData("abutment", { height: Number(e.target.value) })
|
||||
}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3.2 TRỤ CẦU */}
|
||||
{activeTab === "pier" && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
||||
3.2 Trụ Cầu
|
||||
</h2>
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 block mb-1">
|
||||
Chiều cao thân trụ (mm):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={bridgeData.pier.height}
|
||||
onChange={(e) =>
|
||||
setBridgeData("pier", { height: Number(e.target.value) })
|
||||
}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 3.3 CỌC / MÓNG CỌC */}
|
||||
{activeTab === "pile" && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
||||
3.3 Cọc / Móng Cọc
|
||||
</h2>
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 block mb-1">
|
||||
Đường kính cọc D (mm):
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={bridgeData.pile.diameter}
|
||||
onChange={(e) =>
|
||||
setBridgeData("pile", { diameter: Number(e.target.value) })
|
||||
}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Button Xuất dữ liệu Contract */}
|
||||
<button
|
||||
onClick={handleExportContract}
|
||||
className="mt-auto w-full bg-sky-600 hover:bg-sky-500 active:bg-sky-700 text-white font-medium py-2.5 px-3 rounded text-xs flex items-center justify-center gap-2 transition-all duration-150 shadow-lg shadow-sky-600/20"
|
||||
>
|
||||
<Download className="w-4 h-4 shrink-0" />
|
||||
<span className="truncate">Xuất Data Contract ({activeTab})</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BeamForm;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "./BeamForm";
|
||||
@@ -1,320 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import FormCustom from "@/components/utils/FormCustom/FormCustom";
|
||||
import SelectForm from "@/components/utils/FormCustom/components/SelectForm";
|
||||
import { httpRequest } from "@/services";
|
||||
import consultationServices from "@/services/consultationServices";
|
||||
import patientServices, {
|
||||
PatientItem,
|
||||
PatientResponse,
|
||||
} from "@/services/patientServices";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import GridColumn from "@/components/layouts/GridColumn";
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
import specialtyServices, { SpecialtyItem } from "@/services/specialtyServices";
|
||||
import SelectMany from "@/components/utils/FormCustom/components/SelectMany";
|
||||
|
||||
const CreateConsultation = () => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const patientId = searchParams.get("patientId");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [form, setForm] = useState<{
|
||||
patientId: string;
|
||||
chiefComplaint: string;
|
||||
symptomsText: string;
|
||||
vitalSigns: string;
|
||||
specialtyIds: string[];
|
||||
}>({
|
||||
patientId: patientId || "",
|
||||
chiefComplaint: "",
|
||||
symptomsText: "",
|
||||
vitalSigns: "",
|
||||
specialtyIds: [],
|
||||
});
|
||||
|
||||
/* =========================
|
||||
GET LIST PATIENT
|
||||
========================= */
|
||||
const patientQuery = useQuery<PatientResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_patient],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: patientServices.getPatient({
|
||||
Page: 1,
|
||||
PageSize: 1000,
|
||||
Search: "",
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
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
|
||||
========================= */
|
||||
const patientOptions = useMemo(() => {
|
||||
return patientQuery.data?.items || [];
|
||||
}, [patientQuery.data]);
|
||||
|
||||
/* =========================
|
||||
CREATE CONSULTATION
|
||||
========================= */
|
||||
const createConsultationMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Tạo phiên khám thành công!",
|
||||
|
||||
http: consultationServices.createConsultations({
|
||||
patientId: form.patientId,
|
||||
chiefComplaint: form.chiefComplaint,
|
||||
symptomsText: form.symptomsText,
|
||||
vitalSigns: form.vitalSigns,
|
||||
specialtyIds: form.specialtyIds,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
router.back();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!form?.patientId) {
|
||||
return toastWarn({ msg: "Vui lòng chọn bệnh nhân" });
|
||||
}
|
||||
if (!form?.specialtyIds) {
|
||||
return toastWarn({ msg: "Vui lòng chọn chuyên khoa" });
|
||||
}
|
||||
createConsultationMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="
|
||||
max-w-[95vw]
|
||||
border
|
||||
rounded-lg
|
||||
bg-white
|
||||
shadow-xl
|
||||
flex
|
||||
flex-col
|
||||
"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-b
|
||||
px-7
|
||||
py-5
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<h2 className="text-[30px] font-semibold text-[#111827]">
|
||||
Tạo phiên khám
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div className="flex-1 px-7 py-5">
|
||||
<div className="space-y-5 mb-2">
|
||||
<SelectForm
|
||||
label={
|
||||
<span>
|
||||
Bệnh nhân
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
readOnly={!!patientId}
|
||||
placeholder="Chọn bệnh nhân"
|
||||
value={form.patientId}
|
||||
options={patientOptions}
|
||||
// loading={patientQuery.isLoading}
|
||||
onSelect={(item: PatientItem) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
patientId: item.id,
|
||||
}))
|
||||
}
|
||||
onClean={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
patientId: "",
|
||||
}))
|
||||
}
|
||||
getOptionLabel={(item: PatientItem) =>
|
||||
`${item.fullName} - ${item.identificationNumber}`
|
||||
}
|
||||
getOptionValue={(item: PatientItem) => item.id}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-5 mb-2">
|
||||
<GridColumn col={3}>
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Lý do khám <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
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={
|
||||
<span>
|
||||
Triệu chứng <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
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={
|
||||
<span>
|
||||
Chỉ số huyết áp, chỉ số mạch{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
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={
|
||||
<span>
|
||||
Chuyên khoa
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
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>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-t
|
||||
bg-white
|
||||
px-7
|
||||
py-5
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-3
|
||||
"
|
||||
>
|
||||
<CustomButton
|
||||
variant="grey"
|
||||
rounded="md"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
|
||||
<CustomButton
|
||||
disabled={
|
||||
!form.patientId ||
|
||||
!form.chiefComplaint ||
|
||||
!form.symptomsText ||
|
||||
!form.vitalSigns ||
|
||||
!form.specialtyIds ||
|
||||
createConsultationMutation.isPending
|
||||
}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{createConsultationMutation.isPending
|
||||
? "Đang tạo..."
|
||||
: "Tạo phiên khám"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateConsultation;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./CreateConsultation";
|
||||
@@ -1,728 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
|
||||
|
||||
import { ArrowLeft, Pencil } from "lucide-react";
|
||||
|
||||
import consultationServices, {
|
||||
AttachmentFile,
|
||||
ConsultationDetail,
|
||||
} from "@/services/consultationServices";
|
||||
|
||||
import StateActive from "@/components/customs/StateActive";
|
||||
import TabNavLink from "@/components/customs/custom-tabs";
|
||||
import moment from "moment";
|
||||
import GridColumn from "@/components/layouts/GridColumn";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import Pagination from "@/components/customs/custom-pagination";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@radix-ui/react-tooltip";
|
||||
|
||||
const DetailConsultation = () => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const specialtyId = searchParams.get("_type");
|
||||
|
||||
const consultationId = params?.id as string;
|
||||
|
||||
const [prescriptionPage, setPrescriptionPage] = useState<number>(1);
|
||||
const [prescriptionPageSize, setPrescriptionPageSize] = useState<number>(10);
|
||||
|
||||
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: [],
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const consultation = consulDetailQuery.data;
|
||||
|
||||
const canUpdateConsultation =
|
||||
consultation?.status !== TYPE_STATUS.Completed &&
|
||||
consultation?.status !== TYPE_STATUS.Cancelled;
|
||||
|
||||
const specialtyTabs = useMemo(() => {
|
||||
return (
|
||||
consultation?.specialtyClinics?.map((item) => ({
|
||||
pathname: `/consultation/${consultationId}`,
|
||||
query: item.id,
|
||||
title: item.specialtyName,
|
||||
})) || []
|
||||
);
|
||||
}, [consultation?.specialtyClinics, consultationId]);
|
||||
// Tính toán cắt mảng dữ liệu thuốc để hiển thị phân trang trên Client
|
||||
const paginatedPrescriptionItems = useMemo(() => {
|
||||
const items = consultation?.aggregatedPrescriptionItems || [];
|
||||
const startIndex = (prescriptionPage - 1) * prescriptionPageSize;
|
||||
const endIndex = startIndex + prescriptionPageSize;
|
||||
return items.slice(startIndex, endIndex);
|
||||
}, [
|
||||
consultation?.aggregatedPrescriptionItems,
|
||||
prescriptionPage,
|
||||
prescriptionPageSize,
|
||||
]);
|
||||
|
||||
const specialtyClinics = consultation?.specialtyClinics;
|
||||
const selectedSpecialty = useMemo(() => {
|
||||
if (!specialtyClinics?.length) return null;
|
||||
|
||||
return (
|
||||
specialtyClinics.find((item) => item.id === specialtyId) ||
|
||||
specialtyClinics[0]
|
||||
);
|
||||
}, [specialtyClinics, specialtyId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (consultation?.specialtyClinics?.length && !specialtyId) {
|
||||
router.replace(`?&_type=${consultation.specialtyClinics[0].id}`);
|
||||
}
|
||||
}, [consultation?.specialtyClinics, specialtyId, router]);
|
||||
|
||||
if (consulDetailQuery.isLoading) {
|
||||
return <div>Đang tải dữ liệu...</div>;
|
||||
}
|
||||
|
||||
if (!consultation) {
|
||||
return <div>Không tìm thấy dữ liệu phiên khám</div>;
|
||||
}
|
||||
type PrescriptionItem =
|
||||
ConsultationDetail["aggregatedPrescriptionItems"][number];
|
||||
|
||||
// const getFileInfo = (file: AttachmentFile | string, index: number) => {
|
||||
// if (typeof file === "string") {
|
||||
// return {
|
||||
// fileName: file.split("/").pop() || `File ${index + 1}`,
|
||||
// path: file,
|
||||
// };
|
||||
// }
|
||||
|
||||
// return file;
|
||||
// };
|
||||
|
||||
const getFileInfo = (file: AttachmentFile | string, index: number) => {
|
||||
if (typeof file === "string") {
|
||||
return {
|
||||
fileName: file.split("/").pop() || `File ${index + 1}`,
|
||||
path: `${process.env.NEXT_PUBLIC_IMAGE}${file}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...file,
|
||||
path: `${process.env.NEXT_PUBLIC_IMAGE}${file.path}`,
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-row gap-4">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div
|
||||
className="flex cursor-pointer items-center gap-3"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft className="h-6 w-6" />
|
||||
|
||||
<h2 className="text-[28px] font-semibold text-[#111827]">
|
||||
Chi tiết phiên khám
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{consultation.status === TYPE_STATUS.PendingPrescription &&
|
||||
canUpdateConsultation && (
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<Pencil />}
|
||||
href={`/consultation/update?_id=${consultation.id}`}
|
||||
>
|
||||
Cập nhật tổng quát
|
||||
</CustomButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<GridColumn col={3}>
|
||||
{/* ID PHIÊN KHÁM */}
|
||||
{/* <div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
ID PHIÊN KHÁM
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.id}
|
||||
</p>
|
||||
</div> */}
|
||||
|
||||
{/* MÃ PHIÊN KHÁM */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
MÃ PHIÊN KHÁM
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.sessionCode}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* TÊN BỆNH NHÂN */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
TÊN BỆNH NHÂN
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.patientName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* NGÀY GIỜ KHÁM */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
NGÀY, GIỜ KHÁM
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.visitDate
|
||||
? moment(consultation.visitDate).format("DD/MM/YYYY")
|
||||
: "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* TRIỆU CHỨNG */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
DẤU HIỆU
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.symptomsText || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* KHIẾU NẠI CHÍNH */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
KHIẾU NẠI CHÍNH
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.chiefComplaint || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CHỈ SỐ HUYẾT ÁP, CHỈ SỐ MẠCH */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
CHỈ SỐ HUYẾT ÁP, CHỈ SỐ MẠCH
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.vitalSigns || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* CHẨN ĐOÁN */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
CHUẨN ĐOÁN
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.diagnosis || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* KẾ HOẠCH ĐIỀU TRỊ */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
KẾ HOẠCH ĐIỀU TRỊ
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.treatmentPlan || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* GHI CHÚ BÁC SĨ */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
GHI CHÚ BÁC SĨ
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.doctorNotes || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* NGƯỜI TẠO */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
NGƯỜI TẠO
|
||||
</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{consultation.createdByName || "---"}
|
||||
</p>
|
||||
</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>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
FILE UPLOAD
|
||||
</p>
|
||||
{consultation.generalAttachmentUrls?.length > 0 ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-blue-100 bg-blue-50 px-3 py-2 text-sm font-medium text-blue-600 transition hover:bg-blue-100">
|
||||
<span>📎</span>
|
||||
<span>Chi tiết các file</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="w-[350px] rounded-xl border bg-white p-4 shadow-xl"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-gray-800">
|
||||
Danh sách file đính kèm
|
||||
</h4>
|
||||
|
||||
<div className="max-h-[300px] space-y-2 overflow-y-auto">
|
||||
{consultation.generalAttachmentUrls.map(
|
||||
(item, index) => {
|
||||
const file = getFileInfo(item, index);
|
||||
|
||||
const extension =
|
||||
file.fileName
|
||||
.split(".")
|
||||
.pop()
|
||||
?.toLowerCase() || "";
|
||||
|
||||
const isImage = [
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
"bmp",
|
||||
"svg",
|
||||
].includes(extension);
|
||||
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={file.path}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between rounded-lg border border-gray-200 p-3 transition hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="text-xl">
|
||||
{isImage ? "🖼️" : "📄"}
|
||||
</span>
|
||||
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-gray-800">
|
||||
{file.fileName}
|
||||
</p>
|
||||
|
||||
<p className="text-xs text-gray-500 uppercase">
|
||||
{extension || "FILE"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-blue-500">
|
||||
Mở
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<div>Không có file nào được tải lên</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
TRẠNG THÁI
|
||||
</p>
|
||||
|
||||
<StateActive
|
||||
stateActive={consultation.status}
|
||||
listState={[
|
||||
{
|
||||
state: TYPE_STATUS.Draft,
|
||||
text: "Đang chờ khám",
|
||||
textColor: "#92400E",
|
||||
backgroundColor: "#FEF3C7",
|
||||
},
|
||||
{
|
||||
state: TYPE_STATUS.InProgress,
|
||||
text: "Đang khám tổng quát",
|
||||
textColor: "#1E3A8A",
|
||||
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",
|
||||
textColor: "#9A3412",
|
||||
backgroundColor: "#FED7AA",
|
||||
},
|
||||
{
|
||||
state: TYPE_STATUS.Completed,
|
||||
text: "Hoàn thành",
|
||||
textColor: "#166534",
|
||||
backgroundColor: "#DCFCE7",
|
||||
},
|
||||
{
|
||||
state: TYPE_STATUS.Cancelled,
|
||||
text: "Đã hủy",
|
||||
textColor: "#991B1B",
|
||||
backgroundColor: "#FEE2E2",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</GridColumn>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<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">
|
||||
Danh sách đơn thuốc
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<DataWrapper
|
||||
data={consultation.aggregatedPrescriptionItems}
|
||||
loading={consulDetailQuery.isLoading}
|
||||
title="Không có dữ liệu"
|
||||
note="Vui lòng thử lại sau"
|
||||
>
|
||||
<Table
|
||||
// Nếu item không có thuộc tính id, bạn có thể đổi thành (item, index) => String(index)
|
||||
rowKey={(item: PrescriptionItem) => {
|
||||
// Tìm vị trí của item hiện tại trong mảng paginatedPrescriptionItems
|
||||
const idx = paginatedPrescriptionItems.indexOf(item);
|
||||
return item.id || String(idx);
|
||||
}}
|
||||
data={paginatedPrescriptionItems} // Hiển thị dữ liệu trang hiện tại
|
||||
column={[
|
||||
{
|
||||
title: "Tên thuốc",
|
||||
render: (item: PrescriptionItem) => (
|
||||
<span className="font-medium text-gray-900">
|
||||
{item.medicineName}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Liều dùng",
|
||||
render: (item: PrescriptionItem) => (
|
||||
<span>{item.dosage}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Tần suất",
|
||||
render: (item: PrescriptionItem) => (
|
||||
<span>{item.frequency}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Thời gian",
|
||||
render: (item: PrescriptionItem) => (
|
||||
<span>{item.duration}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Hướng dẫn",
|
||||
render: (item: PrescriptionItem) => (
|
||||
<span className="text-sm text-gray-500">
|
||||
{item.instructions || "---"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
|
||||
{/* Chỉ hiển thị phân trang khi tổng số lượng thuốc lớn hơn 10 phần tử */}
|
||||
{(consultation.aggregatedPrescriptionItems?.length || 0) > 10 && (
|
||||
<Pagination
|
||||
total={consultation.aggregatedPrescriptionItems?.length || 0}
|
||||
page={prescriptionPage}
|
||||
pageSize={prescriptionPageSize}
|
||||
onSetPage={(value) => setPrescriptionPage(value)}
|
||||
onSetPageSize={(value) => {
|
||||
setPrescriptionPageSize(value);
|
||||
setPrescriptionPage(1); // Reset về trang 1 khi thay đổi kích thước hiển thị hàng
|
||||
}}
|
||||
dependencies={[prescriptionPage, prescriptionPageSize]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* TAB CHUYÊN KHOA */}
|
||||
{specialtyTabs.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<TabNavLink query="_type" listHref={specialtyTabs} />
|
||||
</div>
|
||||
)}
|
||||
{/* CHI TIẾT CHUYÊN KHOA */}
|
||||
{selectedSpecialty && (
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="mb-6 text-xl font-semibold">
|
||||
Thông tin chuyên khoa: {selectedSpecialty.specialtyName}
|
||||
</h3>
|
||||
{canUpdateConsultation && (
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<Pencil />}
|
||||
href={`/consultation/update-specialty?_id=${consultation.id}&specialtyClinicId=${selectedSpecialty.id}`}
|
||||
>
|
||||
Cập nhật chuyên khoa
|
||||
</CustomButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<GridColumn col={3}>
|
||||
<div>
|
||||
<p className="text-sm text-[#697586]">CHUYÊN KHOA</p>
|
||||
{/* Thêm class để bọc text */}
|
||||
<p className="break-words overflow-hidden">
|
||||
{selectedSpecialty.specialtyName || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-[#697586]">CHẨN ĐOÁN</p>
|
||||
{/* Thêm class để bọc text */}
|
||||
<p className="break-words overflow-hidden">
|
||||
{selectedSpecialty.diagnosis || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-[#697586]">GHI CHÚ KHÁM</p>
|
||||
{/* Thêm class để bọc text */}
|
||||
<p className="break-words overflow-hidden">
|
||||
{selectedSpecialty.specialtyNotes || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-[#697586]">THỦ THUẬT</p>
|
||||
{/* Thêm class để bọc text */}
|
||||
<p className="break-words overflow-hidden">
|
||||
{selectedSpecialty.procedureNotes || "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium uppercase tracking-wide text-[#697586]">
|
||||
FILE UPLOAD
|
||||
</p>
|
||||
{selectedSpecialty.attachmentUrls?.length > 0 ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-blue-100 bg-blue-50 px-3 py-2 text-sm font-medium text-blue-600 transition hover:bg-blue-100">
|
||||
<span>📎</span>
|
||||
<span>Chi tiết các file</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent
|
||||
side="bottom"
|
||||
align="start"
|
||||
className="w-[350px] rounded-xl border bg-white p-4 shadow-xl"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-semibold text-gray-800">
|
||||
Danh sách file đính kèm
|
||||
</h4>
|
||||
|
||||
<div className="max-h-[300px] space-y-2 overflow-y-auto">
|
||||
{selectedSpecialty.attachmentUrls.map(
|
||||
(item, index) => {
|
||||
const file = getFileInfo(item, index);
|
||||
|
||||
const extension =
|
||||
file.fileName.split(".").pop()?.toLowerCase() ||
|
||||
"";
|
||||
|
||||
const isImage = [
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"webp",
|
||||
"bmp",
|
||||
"svg",
|
||||
].includes(extension);
|
||||
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={file.path}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between rounded-lg border border-gray-200 p-3 transition hover:bg-gray-50"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="text-xl">
|
||||
{isImage ? "🖼️" : "📄"}
|
||||
</span>
|
||||
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium text-gray-800">
|
||||
{file.fileName}
|
||||
</p>
|
||||
|
||||
<p className="text-xs text-gray-500 uppercase">
|
||||
{extension || "FILE"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-blue-500">
|
||||
Mở
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<div>Không có file nào được tải lên</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedSpecialty.glassRequired && (
|
||||
<>
|
||||
<div>
|
||||
<p className="text-sm text-[#697586]">CẦN ĐEO KÍNH</p>
|
||||
|
||||
<p>Có</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-[#697586]">LOẠI KÍNH</p>
|
||||
|
||||
<p>{selectedSpecialty.glassType || "---"}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-sm text-[#697586]">ĐÃ PHÁT KÍNH CHƯA</p>
|
||||
|
||||
{selectedSpecialty.isGlassesDelivered ? (
|
||||
<p className="bg-[#DCFCE7] text-[#166534] p-2 border rounded-full w-fit">
|
||||
Đã phát kính
|
||||
</p>
|
||||
) : (
|
||||
<p className="bg-[#fee2e2] text-[#991B1B] p-2 border rounded-full w-fit">
|
||||
Chưa phát kính
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</GridColumn>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailConsultation;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./DetailConsultation";
|
||||
@@ -1,725 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import CustomDialog from "@/components/customs/custom-dialog";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import Search from "@/components/customs/custom-search";
|
||||
import StateActive from "@/components/customs/StateActive";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
|
||||
import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationItem,
|
||||
ConsultationResponse,
|
||||
} from "@/services/consultationServices";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
import { Pencil, ClipboardPlus, Ellipsis } from "lucide-react";
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import Pagination from "@/components/customs/custom-pagination";
|
||||
import moment from "moment";
|
||||
import specialtyServices, { SpecialtyItem } from "@/services/specialtyServices";
|
||||
|
||||
import FilterCustom from "@/components/customs/FilterCustom";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@radix-ui/react-tooltip";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import clsx from "clsx";
|
||||
import TabNavLink from "@/components/customs/custom-tabs";
|
||||
import { removeVietnameseTones } from "@/common/funcs/optionConvert";
|
||||
|
||||
const MainPageConsultation = () => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
/* =========================
|
||||
QUERY PARAMS
|
||||
========================= */
|
||||
|
||||
const _page = searchParams.get("_page");
|
||||
const _pageSize = searchParams.get("_pageSize");
|
||||
const _keyword = searchParams.get("_keyword");
|
||||
const _status = searchParams.get("_status");
|
||||
const _specialtyId = searchParams.get("_specialtyId");
|
||||
const _type = searchParams.get("_type");
|
||||
const isAllTab = !_type || _type === "all";
|
||||
|
||||
const [selectedConsultationId, setSelectedConsultationId] =
|
||||
useState<string>("");
|
||||
|
||||
const [openCancelDialog, setOpenCancelDialog] = useState(false);
|
||||
|
||||
const [openCompleteDialog, setOpenCompleteDialog] = useState(false);
|
||||
|
||||
const consultationQuery = useQuery<ConsultationResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_consultation, _page, _pageSize, _keyword],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: consultationServices.getConsultations({
|
||||
Page: Number(_page || 1),
|
||||
|
||||
PageSize: Number(_pageSize || 10),
|
||||
|
||||
Search: _keyword || "",
|
||||
|
||||
SortBy: _specialtyId || undefined,
|
||||
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
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(() => {
|
||||
let data = consultationQuery.data?.items || [];
|
||||
|
||||
// =========================
|
||||
// TAB CHUYÊN KHOA
|
||||
// =========================
|
||||
if (_type && _type !== "all") {
|
||||
data = data.filter((item) =>
|
||||
item.specialtyClinics?.some(
|
||||
(specialty) => specialty.specialtyId === _type,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// FILTER CHUYÊN KHOA
|
||||
// =========================
|
||||
if (_specialtyId) {
|
||||
data = data.filter((item) =>
|
||||
item.specialtyClinics?.some(
|
||||
(specialty) => specialty.specialtyId === _specialtyId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// =========================
|
||||
// FILTER TRẠNG THÁI
|
||||
// =========================
|
||||
if (_status) {
|
||||
data = data.filter((item) => item.status === Number(_status));
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [consultationQuery.data, _type, _specialtyId, _status]); // Bỏ _keyword khỏi dependencies vì không dùng lọc client nữa
|
||||
|
||||
const page = _page ? Number(_page) : 1;
|
||||
const pageSize = _pageSize ? Number(_pageSize) : 10;
|
||||
|
||||
useEffect(() => {
|
||||
if (!_type) {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.set("_type", "all");
|
||||
|
||||
router.replace(`${pathname}?${params.toString()}`);
|
||||
}
|
||||
}, [_type, pathname, router, searchParams]);
|
||||
|
||||
const updateQuery = (key: string, value: string | number) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
if (!value || value === "") {
|
||||
params.delete(key);
|
||||
} else {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
|
||||
if (
|
||||
key === "_pageSize" ||
|
||||
key === "_keyword" ||
|
||||
key === "_status" ||
|
||||
key === "_specialtyId"
|
||||
) {
|
||||
params.delete("_page");
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
const consultation = consultationQuery.data;
|
||||
|
||||
const specialtyTabs = useMemo(() => {
|
||||
const specialtyMap = new Map();
|
||||
|
||||
consultation?.items?.forEach((consultationItem) => {
|
||||
consultationItem.specialtyClinics?.forEach((specialty) => {
|
||||
if (!specialtyMap.has(specialty.specialtyId)) {
|
||||
specialtyMap.set(specialty.specialtyId, {
|
||||
pathname: "/consultation",
|
||||
query: specialty.specialtyId,
|
||||
title: specialty.specialtyName,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return [
|
||||
{
|
||||
pathname: "/consultation",
|
||||
query: "all",
|
||||
title: "Tất cả",
|
||||
},
|
||||
...Array.from(specialtyMap.values()),
|
||||
];
|
||||
}, [consultation?.items]);
|
||||
|
||||
const handleOpenCancel = (item: ConsultationItem) => {
|
||||
setSelectedConsultationId(item.id);
|
||||
|
||||
setOpenCancelDialog(true);
|
||||
};
|
||||
|
||||
const handleOpenComplete = (item: ConsultationItem) => {
|
||||
setSelectedConsultationId(item.id);
|
||||
|
||||
setOpenCompleteDialog(true);
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setOpenCancelDialog(false);
|
||||
|
||||
setOpenCompleteDialog(false);
|
||||
};
|
||||
|
||||
const cancelConsultationMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
|
||||
showMessageSuccess: true,
|
||||
|
||||
msgSuccess: "Hủy phiên khám thành công!",
|
||||
|
||||
http: consultationServices.cancelConsultations(selectedConsultationId),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const completeConsultationMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
|
||||
showMessageSuccess: true,
|
||||
|
||||
msgSuccess: "Hoàn thành phiên khám thành công!",
|
||||
|
||||
http: consultationServices.completeConsultations(
|
||||
selectedConsultationId,
|
||||
),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
|
||||
handleCloseDialog();
|
||||
},
|
||||
});
|
||||
|
||||
const handleConfirmCancel = () => {
|
||||
cancelConsultationMutation.mutate();
|
||||
};
|
||||
|
||||
const handleConfirmComplete = () => {
|
||||
completeConsultationMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="rounded-lg bg-white shadow mb-6 p-6">
|
||||
<TabNavLink query="_type" listHref={specialtyTabs} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 rounded-lg bg-white shadow p-6">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-black">Phiên khám</h2>
|
||||
{isAllTab && (
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<ClipboardPlus />}
|
||||
// onClick={handleOpenCreate}
|
||||
href="/consultation/create"
|
||||
>
|
||||
Tạo phiên khám
|
||||
</CustomButton>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* SEARCH */}
|
||||
<div
|
||||
className={clsx(
|
||||
"flex gap-4",
|
||||
isAllTab
|
||||
? "flex-col lg:flex-row lg:items-center lg:justify-between"
|
||||
: "flex-col",
|
||||
)}
|
||||
>
|
||||
<div className="w-full lg:min-w-[400px]">
|
||||
<Search
|
||||
keyword={_keyword ?? ""}
|
||||
setKeyword={(value) => updateQuery("_keyword", value)}
|
||||
placeholder="Tìm kiếm phiên khám..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isAllTab && (
|
||||
<div className="flex flex-col gap-4 sm:flex-row">
|
||||
<FilterCustom<string | null>
|
||||
name="Chuyên khoa"
|
||||
value={_specialtyId}
|
||||
onChange={(value) => updateQuery("_specialtyId", value ?? "")}
|
||||
listOption={
|
||||
specialtyQuery.data?.map((item) => ({
|
||||
uuid: item.id,
|
||||
name: item.name,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
|
||||
<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",
|
||||
},
|
||||
{
|
||||
uuid: TYPE_STATUS.Completed,
|
||||
name: "Hoàn thành",
|
||||
},
|
||||
{
|
||||
uuid: TYPE_STATUS.Cancelled,
|
||||
name: "Đã hủy",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* TABLE */}
|
||||
<DataWrapper
|
||||
data={consultationData}
|
||||
loading={consultationQuery.isLoading}
|
||||
title="Không có dữ liệu"
|
||||
note="Vui lòng thử lại sau"
|
||||
>
|
||||
<Table
|
||||
rowKey={(item) => item.id}
|
||||
data={consultationData}
|
||||
column={[
|
||||
{
|
||||
title: "TÊN BỆNH NHÂN",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={`/patient/${item.patientId}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.patientName}
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
<p className="p-2 bg-gray-400 border rounded-full">
|
||||
Chi tiết bệnh nhân
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "MÃ PHIÊN KHÁM",
|
||||
fixedLeft: true,
|
||||
render: (item: ConsultationItem) => {
|
||||
const specialty = item.specialtyClinics.find(
|
||||
(x) => x.specialtyId === _type,
|
||||
);
|
||||
|
||||
const detailHref = isAllTab
|
||||
? `/consultation/${item.id}`
|
||||
: `/consultation/${item.id}?_type=${specialty?.id ?? ""}`;
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={detailHref}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.sessionCode}
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
<p className="rounded-full border bg-gray-400 p-2">
|
||||
Chi tiết phiên khám
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
title: "NGÀY KHÁM",
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>
|
||||
{item.visitDate
|
||||
? moment(item.visitDate).format("DD/MM/YYYY")
|
||||
: "---"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "DẤU HIỆU",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.symptomsText || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "CHUẨN ĐOÁN",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.diagnosis || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "KẾ HOẠCH ĐIỀU TRỊ",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.treatmentPlan || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "GHI CHÚ BÁC SĨ",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.doctorNotes || "---"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "TRẠNG THÁI",
|
||||
render: (item: ConsultationItem) => {
|
||||
// ===== TAB CHUYÊN KHOA =====
|
||||
if (!isAllTab) {
|
||||
const specialty = item.specialtyClinics.find(
|
||||
(x) => x.specialtyId === _type,
|
||||
);
|
||||
|
||||
return (
|
||||
<StateActive
|
||||
stateActive={specialty?.specialtyStatus}
|
||||
listState={[
|
||||
{
|
||||
state: 1,
|
||||
text: "Mới được chỉ định",
|
||||
textColor: "#92400E",
|
||||
backgroundColor: "#FEF3C7",
|
||||
},
|
||||
{
|
||||
state: 2,
|
||||
text: "Đang khám",
|
||||
textColor: "#1E3A8A",
|
||||
backgroundColor: "#DBEAFE",
|
||||
},
|
||||
{
|
||||
state: 3,
|
||||
text: "Đã Hoàn thành",
|
||||
textColor: "#166534",
|
||||
backgroundColor: "#DCFCE7",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== TAB TẤT CẢ =====
|
||||
return (
|
||||
<StateActive
|
||||
stateActive={item.status}
|
||||
listState={[
|
||||
{
|
||||
state: TYPE_STATUS.Draft,
|
||||
text: "Đang chờ khám",
|
||||
textColor: "#92400E",
|
||||
backgroundColor: "#FEF3C7",
|
||||
},
|
||||
{
|
||||
state: TYPE_STATUS.InProgress,
|
||||
text: "Đang khám tổng quát",
|
||||
textColor: "#1E3A8A",
|
||||
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",
|
||||
textColor: "#9A3412",
|
||||
backgroundColor: "#FED7AA",
|
||||
},
|
||||
{
|
||||
state: TYPE_STATUS.Completed,
|
||||
text: "Hoàn thành",
|
||||
textColor: "#166534",
|
||||
backgroundColor: "#DCFCE7",
|
||||
},
|
||||
{
|
||||
state: TYPE_STATUS.Cancelled,
|
||||
text: "Đã hủy",
|
||||
textColor: "#991B1B",
|
||||
backgroundColor: "#FEE2E2",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
fixedRight: true,
|
||||
|
||||
title: "ACTION",
|
||||
|
||||
render: (item: ConsultationItem) => {
|
||||
const isCancelled = item.status === TYPE_STATUS.Cancelled;
|
||||
|
||||
const isCompleted = item.status === TYPE_STATUS.Completed;
|
||||
|
||||
const isDisabled = isCancelled || isCompleted;
|
||||
|
||||
// ===== TAB CHUYÊN KHOA =====
|
||||
if (!isAllTab) {
|
||||
const specialty = item.specialtyClinics.find(
|
||||
(x) => x.specialtyId === _type,
|
||||
);
|
||||
|
||||
if (!specialty) return null;
|
||||
|
||||
return (
|
||||
<CustomButton
|
||||
size="sm"
|
||||
variant="midnightBlue"
|
||||
disabled={isDisabled}
|
||||
href={
|
||||
!isDisabled
|
||||
? `/consultation/update-specialty?_id=${item.id}&specialtyClinicId=${specialty.id}`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Cập nhật
|
||||
</CustomButton>
|
||||
);
|
||||
}
|
||||
|
||||
// ===== TAB TẤT CẢ =====
|
||||
return (
|
||||
/*
|
||||
Chỉnh sửa tại đây:
|
||||
- flex-col: xếp dọc trên màn mobile
|
||||
- sm:flex-row: quay lại xếp ngang trên tablet / desktop (>= 640px)
|
||||
- w-full sm:w-auto: mở rộng full chiều rộng ô khi ở màn mobile cho cân đối
|
||||
*/
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-2 w-full sm:w-auto">
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
disabled={isDisabled}
|
||||
className={clsx(
|
||||
"flex h-8 w-8 items-center justify-center rounded-full border border-gray-200 shrink-0",
|
||||
isDisabled
|
||||
? "cursor-not-allowed opacity-50"
|
||||
: "hover:bg-gray-100",
|
||||
)}
|
||||
>
|
||||
<Ellipsis size={20} />
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
|
||||
{!isDisabled && (
|
||||
<Popover.Content
|
||||
side="bottom"
|
||||
align="end"
|
||||
sideOffset={8}
|
||||
className="
|
||||
z-50
|
||||
min-w-[300px]
|
||||
rounded-xl
|
||||
border
|
||||
bg-white
|
||||
p-2
|
||||
shadow-xl
|
||||
"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
{item.specialtyClinics.map((specialty) => (
|
||||
<Link
|
||||
key={specialty.id}
|
||||
href={`/consultation/update-specialty?_id=${item.id}&specialtyClinicId=${specialty.id}`}
|
||||
className="
|
||||
flex items-center gap-2
|
||||
rounded-lg px-3 py-2
|
||||
text-sm
|
||||
hover:bg-blue-50
|
||||
"
|
||||
>
|
||||
<Pencil size={15} />
|
||||
<span>{specialty.specialtyName}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
)}
|
||||
</Popover.Root>
|
||||
|
||||
<CustomButton
|
||||
size="sm"
|
||||
variant="red"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleOpenCancel(item)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Hủy
|
||||
</CustomButton>
|
||||
|
||||
<CustomButton
|
||||
size="sm"
|
||||
variant="green"
|
||||
disabled={isDisabled}
|
||||
onClick={() => handleOpenComplete(item)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Hoàn thành
|
||||
</CustomButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
<Pagination
|
||||
total={consultationData.length}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
onSetPage={(value) => updateQuery("_page", value)}
|
||||
onSetPageSize={(value) => updateQuery("_pageSize", value)}
|
||||
dependencies={[_pageSize, _keyword, _specialtyId, _status]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* CANCEL DIALOG */}
|
||||
<CustomDialog
|
||||
open={openCancelDialog}
|
||||
title="Hủy phiên khám"
|
||||
note="Bạn có chắc chắn muốn hủy phiên khám này không?"
|
||||
type="error"
|
||||
onClose={handleCloseDialog}
|
||||
onSubmit={handleConfirmCancel}
|
||||
titleCancel="Đóng"
|
||||
titleSubmit="Xác nhận"
|
||||
/>
|
||||
|
||||
{/* COMPLETE DIALOG */}
|
||||
<CustomDialog
|
||||
open={openCompleteDialog}
|
||||
title="Hoàn thành phiên khám"
|
||||
note="Bạn có chắc chắn muốn hoàn thành phiên khám này không?"
|
||||
type="success"
|
||||
onClose={handleCloseDialog}
|
||||
onSubmit={handleConfirmComplete}
|
||||
titleCancel="Đóng"
|
||||
titleSubmit="Xác nhận"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPageConsultation;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./MainPageConsultation";
|
||||
@@ -1,220 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
|
||||
import consultationServices, {
|
||||
ConsultationItem,
|
||||
ConsultationResponse,
|
||||
} from "@/services/consultationServices";
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
|
||||
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
import StateActive from "@/components/customs/StateActive";
|
||||
import Pagination from "@/components/customs/custom-pagination";
|
||||
import Search from "@/components/customs/custom-search";
|
||||
import moment from "moment";
|
||||
|
||||
const TableHistoryPatient = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const patientId = searchParams.get("_id");
|
||||
|
||||
const consultationQuery = useQuery<ConsultationResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_consultation, patientId, "full-history"],
|
||||
enabled: !!patientId,
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationResponse>({
|
||||
showMessageFailed: true,
|
||||
http: consultationServices.getConsultations({
|
||||
Page: 1,
|
||||
PageSize: 1000,
|
||||
PatientId: patientId || "",
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const _page = searchParams.get("_page");
|
||||
|
||||
const _pageSize = searchParams.get("_pageSize");
|
||||
|
||||
const _keyword = searchParams.get("_keyword");
|
||||
|
||||
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 || value === "" || (key === "_page" && value === 1)) {
|
||||
params.delete(key);
|
||||
} else {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
|
||||
if (key === "_pageSize") {
|
||||
params.delete("_page");
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
const data = consultationQuery.data?.items || [];
|
||||
|
||||
// lấy tên bệnh nhân từ record đầu tiên (nếu backend có trả)
|
||||
const patientName = data?.[0]?.patientName || "Bệnh nhân";
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold">
|
||||
Lịch sử phiên khám của {patientName}
|
||||
</h2>
|
||||
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
← Quay lại
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="w-full md:min-w-[400px]">
|
||||
<Search
|
||||
keyword={_keyword ?? ""}
|
||||
setKeyword={(value) => updateQuery("_keyword", value)}
|
||||
placeholder="Tìm kiếm phiên khám..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataWrapper
|
||||
data={data}
|
||||
loading={consultationQuery.isLoading}
|
||||
title="Không có dữ liệu"
|
||||
note="Bệnh nhân chưa có lịch sử khám"
|
||||
>
|
||||
<Table
|
||||
rowKey={(item: ConsultationItem) => item.id}
|
||||
data={data}
|
||||
column={[
|
||||
{
|
||||
title: "MÃ PHIÊN KHÁM",
|
||||
render: (item: ConsultationItem) => (
|
||||
<Link
|
||||
href={`/consultation/${item.id}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.sessionCode}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "NGÀY KHÁM",
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>
|
||||
{item.visitDate
|
||||
? moment(item.visitDate).format("DD/MM/YYYY")
|
||||
: "---"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "DẤU HIỆU",
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.symptomsText || "---"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "CHẨN ĐOÁN",
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.diagnosis || "---"}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "TRẠNG THÁI",
|
||||
render: (item: ConsultationItem) => (
|
||||
<StateActive
|
||||
stateActive={item.status}
|
||||
listState={[
|
||||
{
|
||||
state: TYPE_STATUS.Draft,
|
||||
text: "Đang chờ khám",
|
||||
textColor: "#92400E",
|
||||
backgroundColor: "#FEF3C7",
|
||||
},
|
||||
|
||||
{
|
||||
state: TYPE_STATUS.InProgress,
|
||||
text: "Đang khám tổng quát",
|
||||
textColor: "#1E3A8A",
|
||||
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,
|
||||
text: "Hoàn thành",
|
||||
textColor: "#166534",
|
||||
backgroundColor: "#DCFCE7",
|
||||
},
|
||||
|
||||
{
|
||||
state: TYPE_STATUS.Cancelled,
|
||||
text: "Đã hủy",
|
||||
textColor: "#991B1B",
|
||||
backgroundColor: "#FEE2E2",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
|
||||
<Pagination
|
||||
total={data.length}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
onSetPage={(value) => updateQuery("_page", value)}
|
||||
onSetPageSize={(value) => updateQuery("_pageSize", value)}
|
||||
dependencies={[_pageSize, _keyword]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableHistoryPatient;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./TableHistoryPatient";
|
||||
@@ -1,469 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import GridColumn from "@/components/layouts/GridColumn";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { PATH } from "@/constant/config";
|
||||
import { httpRequest } from "@/services";
|
||||
import consultationServices, {
|
||||
ConsultationDetail,
|
||||
} from "@/services/consultationServices";
|
||||
import uploadServices from "@/services/uploadServices";
|
||||
import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
|
||||
/* =========================
|
||||
TYPES
|
||||
========================= */
|
||||
interface IUpdateConsul {
|
||||
chiefComplaint: string;
|
||||
symptomsText: string;
|
||||
vitalSigns: string;
|
||||
diagnosis: string;
|
||||
treatmentPlan: string;
|
||||
doctorNotes: string;
|
||||
isGlassesDelivered: boolean;
|
||||
glassType: string;
|
||||
generalAttachmentUrls: string[];
|
||||
}
|
||||
|
||||
/* =========================
|
||||
DEFAULT
|
||||
========================= */
|
||||
const defaultForm: IUpdateConsul = {
|
||||
chiefComplaint: "",
|
||||
symptomsText: "",
|
||||
vitalSigns: "",
|
||||
diagnosis: "",
|
||||
treatmentPlan: "",
|
||||
doctorNotes: "",
|
||||
isGlassesDelivered: false,
|
||||
glassType: "",
|
||||
generalAttachmentUrls: [],
|
||||
};
|
||||
|
||||
/* =========================
|
||||
COMPONENT
|
||||
========================= */
|
||||
const UpdateConsultation = () => {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Hỗ trợ lấy ID từ cả /consultation/update/[id] hoặc /consultation/update?_id=...
|
||||
const idFromParams = params?.id as string | undefined;
|
||||
const idFromSearch = searchParams?.get("_id") ?? undefined;
|
||||
const id = idFromParams || idFromSearch || "";
|
||||
|
||||
const [images, setImages] = useState<any[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const [form, setForm] = useState<IUpdateConsul>(defaultForm);
|
||||
|
||||
/* =========================
|
||||
GET DETAIL API
|
||||
========================= */
|
||||
const consulDetailQuery = useQuery<ConsultationDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_phien_kham, id],
|
||||
enabled: !!id,
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationDetail>({
|
||||
showMessageFailed: true,
|
||||
http: consultationServices.detailConsultations(id),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
patientId: "",
|
||||
sessionCode: "",
|
||||
visitDate: "",
|
||||
status: 1,
|
||||
chiefComplaint: null,
|
||||
symptomsText: null,
|
||||
vitalSigns: null,
|
||||
diagnosis: null,
|
||||
treatmentPlan: null,
|
||||
doctorNotes: null,
|
||||
createdBy: "",
|
||||
isDeleted: false,
|
||||
generalAttachmentUrls: [],
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
MAP & SET FORM
|
||||
========================= */
|
||||
const consul = consulDetailQuery.data;
|
||||
useEffect(() => {
|
||||
if (!consul) return;
|
||||
const eyeClinic = consul?.specialtyClinics?.find(
|
||||
(item) =>
|
||||
item.specialtyName?.toLowerCase().includes("mắt") &&
|
||||
item.glassRequired === true,
|
||||
);
|
||||
|
||||
setForm({
|
||||
chiefComplaint: consul.chiefComplaint || "",
|
||||
symptomsText: consul.symptomsText || "",
|
||||
vitalSigns: consul.vitalSigns || "",
|
||||
diagnosis: consul.diagnosis || "",
|
||||
treatmentPlan: consul.treatmentPlan || "",
|
||||
doctorNotes: consul.doctorNotes || "",
|
||||
isGlassesDelivered: !!eyeClinic,
|
||||
glassType: eyeClinic?.glassType || "",
|
||||
generalAttachmentUrls: consul.generalAttachmentUrls || [],
|
||||
});
|
||||
|
||||
setImages(
|
||||
(consul.generalAttachmentUrls || []).map((url) => ({
|
||||
file: null,
|
||||
img: url,
|
||||
path: url,
|
||||
fileName: url.split("/").pop(),
|
||||
})),
|
||||
);
|
||||
}, [consulDetailQuery.data]);
|
||||
|
||||
/* =========================
|
||||
UPDATE MUTATION
|
||||
========================= */
|
||||
|
||||
const updateConsultationMutation = useMutation({
|
||||
mutationFn: async (body: { paths: string[] }) => {
|
||||
return httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Cập nhật phiên khám thành công!",
|
||||
http: consultationServices.putConsultations(id, {
|
||||
chiefComplaint: form.chiefComplaint,
|
||||
symptomsText: form.symptomsText,
|
||||
vitalSigns: form.vitalSigns,
|
||||
diagnosis: form.diagnosis,
|
||||
treatmentPlan: form.treatmentPlan,
|
||||
doctorNotes: form.doctorNotes,
|
||||
isGlassesDelivered: form.isGlassesDelivered,
|
||||
fileUrls: body.paths,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.chi_tiet_phien_kham, id],
|
||||
});
|
||||
|
||||
router.back();
|
||||
|
||||
router.push(PATH.CONSULTATION);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const currentImage = images
|
||||
.filter((item) => !!item.img)
|
||||
.map((item) => item.img);
|
||||
|
||||
const files = images
|
||||
.filter((item) => !!item.file)
|
||||
.map((item) => item.file);
|
||||
|
||||
if (files.length > 0) {
|
||||
const uploadResult: any = await uploadServices.uploadImage({
|
||||
Files: files,
|
||||
SessionCode: consulDetailQuery.data?.sessionCode,
|
||||
});
|
||||
|
||||
console.log("Cấu trúc log kiểm tra uploadResult:", uploadResult);
|
||||
|
||||
// CẬP NHẬT Ở ĐÂY: Lấy trực tiếp từ uploadResult hoặc bóc tách đúng tầng data
|
||||
const uploadedUrls =
|
||||
uploadResult?.fileUrls || uploadResult?.data?.fileUrls || [];
|
||||
|
||||
return updateConsultationMutation.mutate({
|
||||
paths: [...currentImage, ...uploadedUrls],
|
||||
});
|
||||
}
|
||||
|
||||
return updateConsultationMutation.mutate({
|
||||
paths: currentImage,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
consulDetailQuery.isLoading || updateConsultationMutation.isPending;
|
||||
|
||||
/* =========================
|
||||
LOADING UI
|
||||
========================= */
|
||||
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>
|
||||
);
|
||||
}
|
||||
const eyeClinic = consul?.specialtyClinics?.find(
|
||||
(item) =>
|
||||
item.specialtyName?.toLowerCase().includes("mắt") &&
|
||||
item.glassRequired === true,
|
||||
);
|
||||
|
||||
/* =========================
|
||||
MAIN UI
|
||||
========================= */
|
||||
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-[28px] font-semibold text-[#111827]">
|
||||
Phiên khám
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-[#697586]">
|
||||
Cập nhật thông tin chi tiết quá trình khám bệnh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GENERAL INFO (PATIENT) */}
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Bệnh nhân <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
name="patientInfo"
|
||||
type="text"
|
||||
// Hiển thị Tên bệnh nhân - SĐT hoặc CCCD dựa trên API detail trả về
|
||||
value={
|
||||
consulDetailQuery.data?.patientName
|
||||
? `${consulDetailQuery.data.patientName} - ${
|
||||
consulDetailQuery.data.sessionCode ||
|
||||
// consulDetailQuery.data.identityCard ||
|
||||
""
|
||||
}`
|
||||
: "Chưa có thông tin bệnh nhân"
|
||||
}
|
||||
placeholder="Thông tin bệnh nhân"
|
||||
isRequired
|
||||
readOnly
|
||||
/>
|
||||
{/* </div> */}
|
||||
|
||||
<GridColumn col={3}>
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Lý do khám <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
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={
|
||||
<span>
|
||||
Triệu chứng <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
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={
|
||||
<span>
|
||||
Chỉ số huyết áp, chỉ số mạch{" "}
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
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>
|
||||
<GridColumn col={2}>
|
||||
<InputForm
|
||||
type="text"
|
||||
label={
|
||||
<span>
|
||||
Chẩn đoán <span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
name="diagnosis"
|
||||
placeholder="Nhập chẩn đoán"
|
||||
value={form.diagnosis}
|
||||
isRequired
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, diagnosis: String(v) }))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
type="text"
|
||||
label={<span>Ghi chú bác sĩ</span>}
|
||||
name="doctorNotes"
|
||||
placeholder="Nhập ghi chú"
|
||||
value={form.doctorNotes}
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, doctorNotes: String(v) }))
|
||||
}
|
||||
/>
|
||||
</GridColumn>
|
||||
<TextArea
|
||||
name="treatmentPlan"
|
||||
placeholder="Nhập thông số chi tiết loại kính được chỉ định"
|
||||
label={
|
||||
<span>
|
||||
Kế hoạch điều trị
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
value={form.treatmentPlan}
|
||||
isRequired
|
||||
// readOnly
|
||||
isBlur
|
||||
max={5000}
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
treatmentPlan: String(v),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
{eyeClinic && (
|
||||
<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">
|
||||
<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.isGlassesDelivered === true}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
isGlassesDelivered: true,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
Có chỉ định
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
name="glassType"
|
||||
placeholder="Nhập thông số chi tiết loại kính được chỉ định"
|
||||
label={
|
||||
<span>
|
||||
Thông số / loại kính chỉ định
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
value={form.glassType}
|
||||
isRequired
|
||||
readOnly
|
||||
isBlur
|
||||
max={5000}
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
glassType: String(v),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
<label className="text-sm font-medium">Tài liệu đính kèm</label>
|
||||
|
||||
<UploadMultipleFile images={images} setImages={setImages} />
|
||||
|
||||
{uploading && (
|
||||
<p className="text-sm text-blue-500">Đang upload file...</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<CustomButton
|
||||
type="button"
|
||||
variant="grey"
|
||||
rounded="md"
|
||||
onClick={() => router.back()}
|
||||
className="min-w-[120px]"
|
||||
>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
onClick={handleSubmit}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
disabled={
|
||||
!form.chiefComplaint ||
|
||||
!form.symptomsText ||
|
||||
!form.vitalSigns ||
|
||||
!form.diagnosis ||
|
||||
!form.treatmentPlan ||
|
||||
updateConsultationMutation.isPending
|
||||
}
|
||||
className="min-w-[140px]"
|
||||
>
|
||||
{updateConsultationMutation.isPending
|
||||
? "Đang cập nhật..."
|
||||
: "Cập nhật"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateConsultation;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./UpdateConsultation";
|
||||
@@ -1,482 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
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 UploadMultipleFile from "@/components/utils/UploadMultipleFile";
|
||||
import { PATH } from "@/constant/config";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import consultationServices, {
|
||||
ConsultationDetail,
|
||||
} from "@/services/consultationServices";
|
||||
import uploadServices from "@/services/uploadServices";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import moment from "moment";
|
||||
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 [images, setImages] = useState<any[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
const [form, setForm] = useState<{
|
||||
specialtyAssignmentId: string;
|
||||
specialtyNotes: string;
|
||||
diagnosis: string;
|
||||
procedureNotes: string;
|
||||
glassRequired: boolean;
|
||||
glassType: string;
|
||||
isGlassesDelivered: boolean;
|
||||
fileUrls: string[];
|
||||
}>({
|
||||
specialtyAssignmentId: "",
|
||||
specialtyNotes: "",
|
||||
diagnosis: "",
|
||||
procedureNotes: "",
|
||||
glassRequired: false,
|
||||
glassType: "",
|
||||
isGlassesDelivered: false,
|
||||
fileUrls: [],
|
||||
});
|
||||
|
||||
/* =========================
|
||||
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: "",
|
||||
patientName: "",
|
||||
sessionCode: "",
|
||||
visitDate: "",
|
||||
status: 1,
|
||||
chiefComplaint: null,
|
||||
symptomsText: null,
|
||||
vitalSigns: null,
|
||||
diagnosis: null,
|
||||
treatmentPlan: null,
|
||||
doctorNotes: null,
|
||||
isGlassesDelivered: false,
|
||||
createdBy: "",
|
||||
createdByName: "",
|
||||
isDeleted: false,
|
||||
generalAttachmentUrls: [],
|
||||
specialtyClinics: [],
|
||||
aggregatedPrescriptionItems: [],
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Khai báo dữ liệu lấy từ Query phục vụ hiển thị
|
||||
const generalData = consulDetailQuery.data;
|
||||
|
||||
// Tìm chuyên khoa hiện tại từ data trả về
|
||||
const selectedSpecialtyClinic = generalData?.specialtyClinics?.find(
|
||||
(item) => item.id === specialtyClinicId,
|
||||
);
|
||||
|
||||
const isEyeClinic =
|
||||
selectedSpecialtyClinic?.specialtyName?.toLowerCase().includes("mắt") ||
|
||||
selectedSpecialtyClinic?.specialtyId?.toLowerCase().includes("mat");
|
||||
|
||||
useEffect(() => {
|
||||
if (!generalData) return;
|
||||
|
||||
const specialtyClinic = generalData.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 || "",
|
||||
isGlassesDelivered: specialtyClinic.isGlassesDelivered || false,
|
||||
fileUrls: specialtyClinic.attachmentUrls || [],
|
||||
});
|
||||
setImages(
|
||||
(specialtyClinic.attachmentUrls || []).map((url) => ({
|
||||
file: null,
|
||||
img: url,
|
||||
path: url,
|
||||
})),
|
||||
);
|
||||
}, [generalData, specialtyClinicId]);
|
||||
|
||||
/* =========================
|
||||
UPDATE MUTATION
|
||||
========================= */
|
||||
const specialtyResultsMutation = useMutation({
|
||||
mutationFn: async (body: { paths: string[] }) => {
|
||||
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 kết quả khám chuyên khoa thành công!",
|
||||
http: consultationServices.specialtyResults(
|
||||
form.specialtyAssignmentId,
|
||||
{
|
||||
specialtyNotes: form.specialtyNotes,
|
||||
diagnosis: form.diagnosis,
|
||||
procedureNotes: form.procedureNotes,
|
||||
glassRequired: isEyeClinic ? form.glassRequired : false,
|
||||
glassType: isEyeClinic ? form.glassType : "",
|
||||
isGlassesDelivered: isEyeClinic ? form.isGlassesDelivered : false,
|
||||
fileUrls: body.paths,
|
||||
},
|
||||
),
|
||||
});
|
||||
},
|
||||
onSuccess(data) {
|
||||
if (!data) return;
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId],
|
||||
});
|
||||
|
||||
router.back();
|
||||
router.push(PATH.CONSULTATION || "/consultation");
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!form.specialtyNotes || !form.diagnosis || !form.procedureNotes) {
|
||||
return toastWarn({ msg: "Vui lòng nhập đầy đủ các trường!" });
|
||||
}
|
||||
|
||||
if (isEyeClinic && form.glassRequired === true && !form.glassType) {
|
||||
return toastWarn({
|
||||
msg: "Vui lòng nhập thông số / Loại kính chỉ định!",
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const currentImage = images
|
||||
.filter((item) => !!item.img)
|
||||
.map((item) => item.img);
|
||||
|
||||
const files = images
|
||||
.filter((item) => !!item.file)
|
||||
.map((item) => item.file);
|
||||
|
||||
if (files.length > 0) {
|
||||
const uploadResult: any = await uploadServices.uploadImage({
|
||||
Files: files,
|
||||
SessionCode: generalData?.sessionCode,
|
||||
});
|
||||
|
||||
const uploadedUrls =
|
||||
uploadResult?.fileUrls || uploadResult?.data?.fileUrls || [];
|
||||
|
||||
return specialtyResultsMutation.mutate({
|
||||
paths: [...currentImage, ...uploadedUrls],
|
||||
});
|
||||
}
|
||||
|
||||
return specialtyResultsMutation.mutate({
|
||||
paths: currentImage,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
{/* THÔNG TIN HÀNH CHÍNH & PHIÊN KHÁM TỔNG QUÁT */}
|
||||
<div className="rounded-xl border border-gray-200 bg-[#f8fafc] p-5">
|
||||
<h3 className="mb-4 text-xs font-bold uppercase tracking-wider text-gray-500">
|
||||
Thông tin tổng quan phiên khám
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-400 block uppercase">
|
||||
Tên bệnh nhân
|
||||
</span>
|
||||
<span className="text-base font-bold text-gray-700 mt-0.5 block">
|
||||
{generalData?.patientName || "Chưa có thông tin"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-400 block uppercase">
|
||||
Ngày sinh
|
||||
</span>
|
||||
<span className="text-base font-medium text-gray-700 mt-0.5 block">
|
||||
{generalData?.patientDateOfBirth
|
||||
? moment(generalData?.patientDateOfBirth).format("DD/MM/YYYY")
|
||||
: "---"}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs font-medium text-gray-400 block uppercase">
|
||||
Mã phiên khám tổng quát
|
||||
</span>
|
||||
<span className="text-base font-semibold text-blue-600 mt-0.5 block">
|
||||
#{generalData?.sessionCode || "Chưa có mã"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KHỐI THÔNG TIN KHÁM CHUYÊN KHOA */}
|
||||
<div className="mt-2">
|
||||
<div className="mb-4 flex items-center gap-3">
|
||||
<span className="text-xs font-semibold uppercase text-slate-500">
|
||||
Chuyên khoa tiếp nhận:
|
||||
</span>
|
||||
|
||||
<div className="inline-flex items-center rounded-full bg-blue-50 px-4 py-2 text-sm font-semibold text-blue-700 border border-blue-200">
|
||||
{selectedSpecialtyClinic?.specialtyName || "Chưa có thông tin"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GridColumn col={isEyeClinic ? 4 : 3}>
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Ghi chú chuyên khoa
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
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={
|
||||
<span>
|
||||
Chẩn đoán chuyên khoa
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
name="diagnosis"
|
||||
placeholder="Nhập chẩn đoán chuyên khoa"
|
||||
isRequired
|
||||
value={form.diagnosis}
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, diagnosis: String(v) }))
|
||||
}
|
||||
/>
|
||||
<InputForm
|
||||
type="text"
|
||||
label={
|
||||
<span>
|
||||
Ghi chú thủ thuật / can thiệp
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
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-blue-50 p-4 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">
|
||||
<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>
|
||||
|
||||
{form.glassRequired && (
|
||||
<div>
|
||||
<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={
|
||||
<span>
|
||||
Thông số / Loại kính chỉ định
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
value={form.glassType}
|
||||
isRequired
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Đã phát kính chưa
|
||||
</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="isGlassesDelivered"
|
||||
className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
checked={form.isGlassesDelivered === true}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
isGlassesDelivered: true,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
Đã phát kính
|
||||
</label>
|
||||
|
||||
<label className="flex cursor-pointer items-center gap-2 text-sm font-medium text-gray-900">
|
||||
<input
|
||||
type="radio"
|
||||
name="isGlassesDelivered"
|
||||
className="h-4 w-4 border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
checked={form.isGlassesDelivered === false}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
isGlassesDelivered: false,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
Chưa phát kính
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="text-sm font-medium">
|
||||
Tài liệu đính kèm chuyên khoa
|
||||
</label>
|
||||
<UploadMultipleFile images={images} setImages={setImages} />
|
||||
{uploading && (
|
||||
<p className="text-sm text-blue-500">Đang upload file...</p>
|
||||
)}
|
||||
</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
|
||||
onClick={handleSubmit}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
disabled={specialtyResultsMutation.isPending}
|
||||
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;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./UpdateSpecialtyClinicId";
|
||||
@@ -1,433 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, ClipboardPlus } from "lucide-react";
|
||||
|
||||
import patientServices, { PatientDetail } from "@/services/patientServices";
|
||||
import consultationServices, {
|
||||
ConsultationItem,
|
||||
ConsultationResponse,
|
||||
} from "@/services/consultationServices";
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
|
||||
import { PATH } from "@/constant/config";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import StateActive from "@/components/customs/StateActive";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
import moment from "moment";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@radix-ui/react-tooltip";
|
||||
// import CustomDialog from "@/components/customs/custom-dialog";
|
||||
|
||||
const DetailPatient = () => {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
// const queryClient = useQueryClient();
|
||||
|
||||
const patientId = params?.id as string;
|
||||
|
||||
/* =========================
|
||||
GET DETAIL PATIENT
|
||||
========================= */
|
||||
const patientDetailQuery = useQuery<PatientDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, patientId],
|
||||
enabled: !!patientId,
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientDetail>({
|
||||
showMessageFailed: true,
|
||||
http: patientServices.detailPatient(patientId),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
patientCode: "",
|
||||
identificationNumber: "",
|
||||
fullName: "",
|
||||
dateOfBirth: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
emergencyContactName: null,
|
||||
emergencyContactPhone: null,
|
||||
allergyNotes: null,
|
||||
chronicDiseaseNotes: null,
|
||||
insuranceNumber: null,
|
||||
notes: null,
|
||||
isActive: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================
|
||||
GET CONSULTATION HISTORY
|
||||
========================= */
|
||||
const consultationQuery = useQuery<ConsultationResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_consultation, patientId],
|
||||
enabled: !!patientId,
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationResponse>({
|
||||
showMessageFailed: true,
|
||||
http: consultationServices.getConsultations({
|
||||
Page: 1,
|
||||
PageSize: 5,
|
||||
PatientId: patientId,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const patient = patientDetailQuery.data;
|
||||
const consultationData = consultationQuery.data?.items || [];
|
||||
|
||||
if (patientDetailQuery.isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-10 font-medium text-gray-500">
|
||||
Đang tải dữ liệu...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2
|
||||
className="text-[28px] flex items-center gap-2 font-semibold text-[#111827] cursor-pointer"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft /> {patient?.fullName}
|
||||
</h2>
|
||||
<div>
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<ClipboardPlus size={18} />}
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`${PATH.CONSULTATION}/create?patientId=${patient?.id}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
Tạo phiên khám
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thông tin chi tiết bệnh nhân */}
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Mã bệnh nhân
|
||||
</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.patientCode}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Họ tên</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.fullName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Mã định danh
|
||||
</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.identificationNumber}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Ngày sinh
|
||||
</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.dateOfBirth
|
||||
? moment(patient?.dateOfBirth).format("DD/MM/YYYY")
|
||||
: "---"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Giới tính
|
||||
</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.gender}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Số điện thoại
|
||||
</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.phone}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Email</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.email}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Địa chỉ
|
||||
</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.address || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Người liên hệ khẩn cấp
|
||||
</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.emergencyContactName || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
SĐT khẩn cấp
|
||||
</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.emergencyContactPhone || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">Dị ứng</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.allergyNotes || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<p className="text-[14px] font-medium text-[#697586]">
|
||||
Bệnh nền
|
||||
</p>
|
||||
<p className="text-[15px] font-medium text-[#202939]">
|
||||
{patient?.chronicDiseaseNotes || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lịch sử phiên khám */}
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-black">
|
||||
Lịch sử phiên khám của {patient?.fullName}
|
||||
</h2>
|
||||
<Link
|
||||
href={`/consultation/history?_id=${patientId}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
Tất cả
|
||||
</Link>
|
||||
</div>
|
||||
<DataWrapper
|
||||
data={consultationData}
|
||||
loading={consultationQuery.isLoading}
|
||||
title="Không có dữ liệu"
|
||||
note="Bệnh nhân chưa có lịch sử khám"
|
||||
>
|
||||
<Table
|
||||
rowKey={(item) => item.id}
|
||||
data={consultationData}
|
||||
column={[
|
||||
{
|
||||
title: "TÊN BỆNH NHÂN",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.patientName}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "MÃ PHIÊN KHÁM",
|
||||
fixedLeft: true,
|
||||
render: (item: ConsultationItem) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={`/consultation/${item.id}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.sessionCode}
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
<p className="p-2 bg-gray-400 border rounded-full">
|
||||
Chi tiết phiên khám
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "NGÀY KHÁM",
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>
|
||||
{item.visitDate
|
||||
? moment(item.visitDate).format("DD/MM/YYYY")
|
||||
: "---"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "DẤU HIỆU",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.symptomsText || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "CHUẨN ĐOÁN",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.diagnosis || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "KẾ HOẠCH ĐIỀU TRỊ",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<span>{item.treatmentPlan || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "GHI CHÚ BÁC SĨ",
|
||||
|
||||
render: (item: ConsultationItem) => (
|
||||
<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",
|
||||
fixedRight: true,
|
||||
render: (item: ConsultationItem) => (
|
||||
<StateActive
|
||||
stateActive={item.status}
|
||||
listState={[
|
||||
{
|
||||
state: TYPE_STATUS.Draft,
|
||||
text: "Đang chờ khám",
|
||||
textColor: "#92400E",
|
||||
backgroundColor: "#FEF3C7",
|
||||
},
|
||||
|
||||
{
|
||||
state: TYPE_STATUS.InProgress,
|
||||
text: "Đang khám tổng quát",
|
||||
textColor: "#1E3A8A",
|
||||
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,
|
||||
text: "Hoàn thành",
|
||||
textColor: "#166534",
|
||||
backgroundColor: "#DCFCE7",
|
||||
},
|
||||
|
||||
{
|
||||
state: TYPE_STATUS.Cancelled,
|
||||
text: "Đã hủy",
|
||||
textColor: "#991B1B",
|
||||
backgroundColor: "#FEE2E2",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailPatient;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./DetailPatient";
|
||||
@@ -1,362 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import CustomDialog from "@/components/customs/custom-dialog";
|
||||
import CustomPopup from "@/components/customs/custom-popup";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import Search from "@/components/customs/custom-search";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import patientServices, {
|
||||
PatientItem,
|
||||
PatientResponse,
|
||||
} from "@/services/patientServices";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Pencil, Trash2, Users } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import PopupCreatePatient from "../PopupCreatePatient";
|
||||
import PopupUpdatePatient from "../PopupUpdatePatient";
|
||||
import Pagination from "@/components/customs/custom-pagination";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@radix-ui/react-tooltip";
|
||||
import moment from "moment";
|
||||
|
||||
const MainPagePatient = () => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const [loadingDelete, setLoadingDelete] = useState(false);
|
||||
|
||||
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||
const [deleteItemId, setDeleteItemId] = useState<string>("");
|
||||
|
||||
const _page = searchParams.get("_page");
|
||||
const _pageSize = searchParams.get("_pageSize");
|
||||
const _keyword = searchParams.get("_keyword");
|
||||
|
||||
/**
|
||||
* PARAMS
|
||||
*/
|
||||
const createParam = searchParams.get("_create");
|
||||
const updateParam = searchParams.get("_update");
|
||||
|
||||
/**
|
||||
* OPEN POPUP
|
||||
*/
|
||||
const openCreatePopup = createParam !== null;
|
||||
const openUpdatePopup = !!updateParam;
|
||||
|
||||
/**
|
||||
* QUERY LIST PATIENT
|
||||
*/
|
||||
const patientQuery = useQuery<PatientResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_patient, _page, _pageSize, _keyword],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: patientServices.getPatient({
|
||||
Page: Number(_page || 1),
|
||||
PageSize: Number(_pageSize || 10),
|
||||
Search: _keyword || "",
|
||||
// IdentificationNumber: _keyword || "",
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* UPDATE QUERY PARAMS
|
||||
*/
|
||||
|
||||
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 || value === "") {
|
||||
params.delete(key);
|
||||
} else {
|
||||
params.set(key, String(value));
|
||||
}
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `${pathname}?${queryString}` : pathname);
|
||||
};
|
||||
|
||||
/**
|
||||
* OPEN CREATE
|
||||
*/
|
||||
const handleOpenCreate = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.set("_create", "");
|
||||
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* OPEN UPDATE
|
||||
*/
|
||||
const handleOpenUpdate = (id: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.delete("_create");
|
||||
params.set("_update", id);
|
||||
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* CLOSE ALL POPUP
|
||||
*/
|
||||
const handleClosePopup = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
|
||||
params.delete("_create");
|
||||
params.delete("_update");
|
||||
|
||||
const queryString = params.toString();
|
||||
|
||||
router.push(queryString ? `${pathname}?${queryString}` : pathname);
|
||||
|
||||
setOpenDeleteDialog(false);
|
||||
setDeleteItemId("");
|
||||
};
|
||||
|
||||
/**
|
||||
* DELETE
|
||||
*/
|
||||
const handleOpenDelete = (id: string) => {
|
||||
setDeleteItemId(id);
|
||||
setOpenDeleteDialog(true);
|
||||
};
|
||||
|
||||
const handleDeletePatient = async () => {
|
||||
if (!deleteItemId) return;
|
||||
|
||||
try {
|
||||
setLoadingDelete(true);
|
||||
|
||||
await patientServices.deletePatient(deleteItemId);
|
||||
|
||||
// đóng dialog + reset state
|
||||
handleClosePopup();
|
||||
|
||||
// refetch list
|
||||
patientQuery.refetch();
|
||||
} catch (error) {
|
||||
console.error("Delete patient error:", error);
|
||||
} finally {
|
||||
setLoadingDelete(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* DATA
|
||||
*/
|
||||
const patientData = useMemo(() => {
|
||||
const items = patientQuery.data?.items || [];
|
||||
|
||||
if (!_keyword) return items;
|
||||
|
||||
const keyword = _keyword.toLowerCase().trim();
|
||||
|
||||
return items.filter((item) => {
|
||||
return (
|
||||
item.fullName?.toLowerCase().includes(keyword) ||
|
||||
item.identificationNumber?.toLowerCase().includes(keyword) ||
|
||||
item.phone?.toLowerCase().includes(keyword) ||
|
||||
item.email?.toLowerCase().includes(keyword)
|
||||
);
|
||||
});
|
||||
}, [patientQuery.data, _keyword]);
|
||||
|
||||
// const isReturnWithin14Days = (lastVisitDate?: string | null) => {
|
||||
// if (!lastVisitDate) return false;
|
||||
|
||||
// const diffDays = moment().diff(moment(lastVisitDate), "days");
|
||||
|
||||
// return diffDays <= 14;
|
||||
// };
|
||||
|
||||
return (
|
||||
<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">Bệnh nhân</h2>
|
||||
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<Users />}
|
||||
onClick={handleOpenCreate}
|
||||
>
|
||||
Tạo mới
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{/* SEARCH */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="w-full md:min-w-[400px]">
|
||||
<Search
|
||||
keyword={_keyword ?? ""}
|
||||
setKeyword={(value) => updateQuery("_keyword", value)}
|
||||
placeholder="Tìm kiếm theo tên bệnh nhân, ID"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TABLE */}
|
||||
<DataWrapper
|
||||
data={patientData}
|
||||
loading={patientQuery.isLoading}
|
||||
title="No New Requests"
|
||||
note="Please check back later or view your schedule"
|
||||
>
|
||||
<Table
|
||||
rowKey={(item) => item.id}
|
||||
data={patientData}
|
||||
column={[
|
||||
{
|
||||
fixedLeft: true,
|
||||
title: "TÊN BỆNH NHÂN",
|
||||
|
||||
render: (item: PatientItem) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={`/patient/${item.id}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.fullName}
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
<p className="p-2 bg-gray-400 border rounded-full">
|
||||
Chi tiết bệnh nhân
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "MÃ ĐỊNH DANH",
|
||||
|
||||
render: (item: PatientItem) => (
|
||||
<span>{item.identificationNumber || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: "SỐ ĐIỆN THOẠI",
|
||||
|
||||
render: (item: PatientItem) => <span>{item.phone || "---"}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
title: "EMAIL",
|
||||
|
||||
render: (item: PatientItem) => <span>{item.email || "---"}</span>,
|
||||
},
|
||||
|
||||
{
|
||||
title: "GIỚI TÍNH",
|
||||
|
||||
render: (item: PatientItem) => (
|
||||
<span>{item.gender || "---"}</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
fixedRight: true,
|
||||
title: "ACTION",
|
||||
|
||||
render: (item: PatientItem) => (
|
||||
/*
|
||||
- flex-col: Xếp dọc các icon trên mobile
|
||||
- sm:flex-row: Xếp ngang lại trên màn lớn (>= 640px)
|
||||
- items-center justify-center: Căn giữa các nút khi xếp dọc
|
||||
*/
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-2">
|
||||
{/* UPDATE */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
icon={<Pencil className="text-blue-400" />}
|
||||
onClick={() => handleOpenUpdate(item.id)}
|
||||
/>
|
||||
|
||||
{/* DELETE */}
|
||||
<CustomButton
|
||||
size="sm"
|
||||
icon={<Trash2 className="text-red-400" />}
|
||||
onClick={() => handleOpenDelete(item.id)}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
<Pagination
|
||||
total={patientData.length}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
onSetPage={(value) => updateQuery("_page", value)}
|
||||
onSetPageSize={(value) => updateQuery("_pageSize", value)}
|
||||
dependencies={[_pageSize, _keyword]}
|
||||
/>
|
||||
|
||||
{/* DELETE DIALOG */}
|
||||
<CustomDialog
|
||||
open={openDeleteDialog}
|
||||
title="Xóa bệnh nhân"
|
||||
note="Bạn có chắc chắn muốn xóa bệnh nhân này?"
|
||||
type="error"
|
||||
onClose={handleClosePopup}
|
||||
onSubmit={handleDeletePatient}
|
||||
titleCancel="Hủy"
|
||||
titleSubmit="Xác nhận"
|
||||
/>
|
||||
|
||||
{/* CREATE POPUP */}
|
||||
<CustomPopup open={openCreatePopup} onClose={handleClosePopup}>
|
||||
<PopupCreatePatient onClose={handleClosePopup} />
|
||||
</CustomPopup>
|
||||
|
||||
{/* UPDATE POPUP */}
|
||||
<CustomPopup open={openUpdatePopup} onClose={handleClosePopup}>
|
||||
<PopupUpdatePatient onClose={handleClosePopup} id={updateParam || ""} />
|
||||
</CustomPopup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPagePatient;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./MainPagePatient";
|
||||
@@ -1,356 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||
import { QUERY_KEY, TYPE_GENDER } from "@/constant/config/enum";
|
||||
import { X } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import patientServices from "@/services/patientServices";
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
import {
|
||||
formatDateOfBirth,
|
||||
parseDateOfBirth,
|
||||
formatToISODate,
|
||||
} from "@/common/funcs/optionConvert";
|
||||
|
||||
export interface ICreatePatient {
|
||||
identificationNumber: string;
|
||||
fullName: string;
|
||||
dateOfBirth: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface PropsPopupCreatePatient {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const PopupCreatePatient = ({ onClose }: PropsPopupCreatePatient) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [form, setForm] = useState<ICreatePatient>({
|
||||
identificationNumber: "",
|
||||
fullName: "",
|
||||
dateOfBirth: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
});
|
||||
|
||||
const createPatientMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return await httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Tạo bệnh nhân thành công!",
|
||||
|
||||
http: patientServices.createPatient({
|
||||
identificationNumber: form.identificationNumber,
|
||||
fullName: form.fullName,
|
||||
dateOfBirth:
|
||||
formatToISODate(parseDateOfBirth(form.dateOfBirth)) || "",
|
||||
gender: form.gender,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
address: form.address,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onError(error: any) {
|
||||
// Lấy message lỗi từ backend trả về
|
||||
const message = error?.response?.data?.error?.message || error?.message;
|
||||
|
||||
// Bật thông báo Toast dạng cảnh báo
|
||||
toastWarn({ msg: message });
|
||||
},
|
||||
|
||||
onSuccess() {
|
||||
/* REFRESH LIST */
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_patient],
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!form.dateOfBirth) {
|
||||
return toastWarn({ msg: "Vui lòng nhập ngày sinh!" });
|
||||
}
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const birthDay = parseDateOfBirth(form.dateOfBirth);
|
||||
|
||||
if (!birthDay) {
|
||||
return toastWarn({
|
||||
msg: "Ngày sinh phải theo định dạng ngày/tháng/năm!",
|
||||
});
|
||||
}
|
||||
|
||||
birthDay.setHours(0, 0, 0, 0);
|
||||
|
||||
if (birthDay.getTime() > today.getTime()) {
|
||||
return toastWarn({ msg: "Ngày sinh không hợp lệ!" });
|
||||
}
|
||||
|
||||
if (!form.gender) {
|
||||
return toastWarn({ msg: "Vui lòng chọn giới tính!" });
|
||||
}
|
||||
|
||||
createPatientMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
h-[680px]
|
||||
w-[640px]
|
||||
max-w-[95vw]
|
||||
rounded-3xl
|
||||
bg-white
|
||||
shadow-xl
|
||||
flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
{/* CLOSE */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="
|
||||
absolute
|
||||
right-5
|
||||
top-5
|
||||
z-30
|
||||
text-gray-400
|
||||
transition
|
||||
hover:text-gray-600
|
||||
"
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
|
||||
{/* HEADER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-b
|
||||
px-7
|
||||
py-5
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<h2 className="text-[30px] font-semibold text-[#111827]">
|
||||
Thêm bệnh nhân
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div
|
||||
className="
|
||||
flex-1
|
||||
overflow-y-auto
|
||||
px-7
|
||||
py-5
|
||||
"
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Mã định danh
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Mã định danh"
|
||||
name="identificationNumber"
|
||||
type="number"
|
||||
isNumber
|
||||
min={12}
|
||||
max={12}
|
||||
value={form.identificationNumber}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Tên bệnh nhân
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Tên bệnh nhân"
|
||||
name="fullName"
|
||||
type="text"
|
||||
value={form.fullName}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<div className="space-y-3">
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Giới tính <span className="text-red-500">*</span>
|
||||
</label>
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
{/* NAM */}
|
||||
<label className="flex cursor-pointer select-none items-center gap-2.5">
|
||||
<input
|
||||
id="male"
|
||||
type="radio"
|
||||
name="gender"
|
||||
value={form.gender}
|
||||
checked={form.gender === TYPE_GENDER.MALE}
|
||||
onChange={() =>
|
||||
setForm((prev: any) => ({
|
||||
...prev,
|
||||
gender: TYPE_GENDER.MALE,
|
||||
}))
|
||||
}
|
||||
className="h-5 w-5 cursor-pointer"
|
||||
/>
|
||||
|
||||
<span className="cursor-pointer text-sm font-medium text-gray-700">
|
||||
Nam
|
||||
</span>
|
||||
</label>
|
||||
|
||||
{/* NỮ */}
|
||||
<label className="flex cursor-pointer select-none items-center gap-2.5">
|
||||
<input
|
||||
id="female"
|
||||
type="radio"
|
||||
name="gender"
|
||||
value={form.gender}
|
||||
checked={form.gender === TYPE_GENDER.FEMALE}
|
||||
onChange={() =>
|
||||
setForm((prev: any) => ({
|
||||
...prev,
|
||||
gender: TYPE_GENDER.FEMALE,
|
||||
}))
|
||||
}
|
||||
className="h-5 w-5 cursor-pointer"
|
||||
/>
|
||||
|
||||
<span className="cursor-pointer text-sm font-medium text-gray-700">
|
||||
Nữ
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Ngày sinh
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="DD/MM/YYYY"
|
||||
name="dateOfBirth"
|
||||
type="text"
|
||||
value={form.dateOfBirth}
|
||||
onChangeValue={(value) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
dateOfBirth: formatDateOfBirth(String(value)),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Số điện thoại
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Số điện thoại"
|
||||
name="phone"
|
||||
type="number"
|
||||
isNumber
|
||||
min={8}
|
||||
max={11}
|
||||
value={form.phone}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={<span>Email</span>}
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
type="email"
|
||||
isEmail
|
||||
value={form.email}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="address"
|
||||
placeholder="Địa chỉ"
|
||||
label={
|
||||
<span>
|
||||
Địa chỉ
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
isRequired
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-t
|
||||
bg-white
|
||||
px-7
|
||||
py-5
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-3
|
||||
"
|
||||
>
|
||||
<CustomButton variant="grey" rounded="md" onClick={onClose}>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
|
||||
<ContextFormCustom.Consumer>
|
||||
{({ isDone }) => (
|
||||
<CustomButton
|
||||
disabled={!isDone || createPatientMutation.isPending}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
type="submit"
|
||||
>
|
||||
{createPatientMutation.isPending ? "Đang tạo..." : "Tạo"}
|
||||
</CustomButton>
|
||||
)}
|
||||
</ContextFormCustom.Consumer>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupCreatePatient;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./PopupCreatePatient";
|
||||
@@ -1,449 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { X } from "lucide-react";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import CustomLoading from "@/components/customs/custom-loading";
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||
import { QUERY_KEY, TYPE_GENDER } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import patientServices, { PatientDetail } from "@/services/patientServices";
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
import { timeSubmit } from "@/common/funcs/optionConvert";
|
||||
interface PopupUpdatePatientProps {
|
||||
onClose: () => void;
|
||||
id: string;
|
||||
}
|
||||
interface IUpdatePatient {
|
||||
fullName: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
emergencyContactName: string;
|
||||
emergencyContactPhone: string;
|
||||
allergyNotes: string;
|
||||
chronicDiseaseNotes: string;
|
||||
insuranceNumber: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
const defaultForm: IUpdatePatient = {
|
||||
fullName: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
emergencyContactName: "",
|
||||
emergencyContactPhone: "",
|
||||
allergyNotes: "",
|
||||
chronicDiseaseNotes: "",
|
||||
insuranceNumber: "",
|
||||
notes: "",
|
||||
};
|
||||
|
||||
const PopupUpdatePatient = ({ onClose, id }: PopupUpdatePatientProps) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [form, setForm] = useState<IUpdatePatient>(defaultForm);
|
||||
|
||||
const patientDetailQuery = useQuery<PatientDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, id],
|
||||
enabled: !!id,
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PatientDetail>({
|
||||
showMessageFailed: true,
|
||||
http: patientServices.detailPatient(id),
|
||||
});
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
patientCode: "",
|
||||
identificationNumber: "",
|
||||
fullName: "",
|
||||
dateOfBirth: "",
|
||||
gender: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
emergencyContactName: null,
|
||||
emergencyContactPhone: null,
|
||||
allergyNotes: null,
|
||||
chronicDiseaseNotes: null,
|
||||
insuranceNumber: null,
|
||||
notes: null,
|
||||
isActive: false,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const patient = patientDetailQuery.data;
|
||||
if (!patient) return;
|
||||
setForm((prev) => {
|
||||
/**
|
||||
* tránh render loop
|
||||
*/
|
||||
if (
|
||||
prev.fullName === patient.fullName &&
|
||||
prev.phone === patient.phone &&
|
||||
prev.email === patient.email
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
fullName: patient.fullName || "",
|
||||
gender: patient.gender || TYPE_GENDER.MALE,
|
||||
phone: patient.phone || "",
|
||||
email: patient.email || "",
|
||||
address: patient.address || "",
|
||||
emergencyContactName: patient.emergencyContactName || "",
|
||||
emergencyContactPhone: patient.emergencyContactPhone || "",
|
||||
allergyNotes: patient.allergyNotes || "",
|
||||
chronicDiseaseNotes: patient.chronicDiseaseNotes || "",
|
||||
insuranceNumber: patient.insuranceNumber || "",
|
||||
notes: patient.notes || "",
|
||||
};
|
||||
});
|
||||
}, [patientDetailQuery.data]);
|
||||
|
||||
const updatePatientMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Cập nhật bệnh nhân thành công!",
|
||||
http: patientServices.putPatient(id, {
|
||||
fullName: form.fullName,
|
||||
gender: form.gender,
|
||||
phone: form.phone,
|
||||
email: form.email,
|
||||
address: form.address,
|
||||
emergencyContactName: form.emergencyContactName,
|
||||
emergencyContactPhone: form.emergencyContactPhone,
|
||||
allergyNotes: form.allergyNotes,
|
||||
chronicDiseaseNotes: form.chronicDiseaseNotes,
|
||||
insuranceNumber: form.insuranceNumber,
|
||||
notes: form.notes,
|
||||
}),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess(data) {
|
||||
if (!data) return;
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_patient],
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.chi_tiet_benh_nhan, id],
|
||||
});
|
||||
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!form.gender) {
|
||||
return toastWarn({ msg: "Vui lòng chọn giới tính!" });
|
||||
}
|
||||
|
||||
updatePatientMutation.mutate();
|
||||
};
|
||||
|
||||
if (patientDetailQuery.isLoading) {
|
||||
return (
|
||||
<div className="flex h-[400px] items-center justify-center">
|
||||
<CustomLoading />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
h-[680px]
|
||||
w-[640px]
|
||||
max-w-[95vw]
|
||||
rounded-3xl
|
||||
bg-white
|
||||
shadow-xl
|
||||
flex
|
||||
flex-col
|
||||
overflow-hidden
|
||||
"
|
||||
>
|
||||
{/* CLOSE */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="
|
||||
absolute
|
||||
right-5
|
||||
top-5
|
||||
z-10
|
||||
text-gray-400
|
||||
transition
|
||||
hover:text-gray-600
|
||||
"
|
||||
>
|
||||
<X size={22} />
|
||||
</button>
|
||||
|
||||
{/* HEADER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-b
|
||||
px-7
|
||||
py-5
|
||||
bg-white
|
||||
"
|
||||
>
|
||||
<h2 className="text-[30px] font-semibold text-[#111827]">
|
||||
Cập nhật thông tin bệnh nhân
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* BODY */}
|
||||
<div
|
||||
className="
|
||||
flex-1
|
||||
overflow-y-auto
|
||||
px-7
|
||||
py-5
|
||||
"
|
||||
>
|
||||
<div className="space-y-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Tên bệnh nhân
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Tên bệnh nhân"
|
||||
name="fullName"
|
||||
type="text"
|
||||
value={form.fullName}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
{/* GENDER */}
|
||||
<div>
|
||||
<label className="text-base font-medium">
|
||||
Giới tính <span className="text-red-500">*</span>
|
||||
</label>
|
||||
|
||||
<div className="mt-2 flex items-center gap-6">
|
||||
{/* NAM */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="male"
|
||||
checked={form.gender === TYPE_GENDER.MALE}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
gender: TYPE_GENDER.MALE,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<label htmlFor="male">Nam</label>
|
||||
</div>
|
||||
|
||||
{/* NỮ */}
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="female"
|
||||
checked={form.gender === TYPE_GENDER.FEMALE}
|
||||
onChange={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
gender: TYPE_GENDER.FEMALE,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<label htmlFor="female">Nữ</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Số điện thoại
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Số điện thoại"
|
||||
name="phone"
|
||||
type="phone"
|
||||
isPhone
|
||||
value={form.phone}
|
||||
isRequired
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Email<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Email"
|
||||
name="email"
|
||||
type="email"
|
||||
isEmail
|
||||
value={form.email}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="address"
|
||||
placeholder="Địa chỉ"
|
||||
label={
|
||||
<span>
|
||||
Địa chỉ
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
value={form.address}
|
||||
isRequired
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
isRequired
|
||||
label={
|
||||
<span>
|
||||
Người liên hệ khẩn cấp
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Người liên hệ khẩn cấp"
|
||||
name="emergencyContactName"
|
||||
type="text"
|
||||
value={form.emergencyContactName}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
SĐT khẩn cấp
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="SĐT khẩn cấp"
|
||||
name="emergencyContactPhone"
|
||||
type="number"
|
||||
min={8}
|
||||
max={11}
|
||||
isRequired
|
||||
// isPhone
|
||||
value={form.emergencyContactPhone}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="allergyNotes"
|
||||
label={
|
||||
<span>
|
||||
Dị ứng
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
isRequired
|
||||
placeholder="Dị ứng"
|
||||
value={form.allergyNotes}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="chronicDiseaseNotes"
|
||||
placeholder="Bệnh nền"
|
||||
label={
|
||||
<span>
|
||||
Bệnh nền
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
isRequired
|
||||
value={form.chronicDiseaseNotes}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Số bảo hiểm
|
||||
<span className="text-red-500"> *</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Số bảo hiểm"
|
||||
name="insuranceNumber"
|
||||
type="text"
|
||||
isRequired
|
||||
value={form.insuranceNumber}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
name="notes"
|
||||
label={<span>Ghi chú</span>}
|
||||
placeholder="Ghi chú"
|
||||
value={form.notes}
|
||||
isBlur
|
||||
max={5000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div
|
||||
className="
|
||||
shrink-0
|
||||
border-t
|
||||
bg-white
|
||||
px-7
|
||||
py-5
|
||||
flex
|
||||
items-center
|
||||
justify-end
|
||||
gap-3
|
||||
"
|
||||
>
|
||||
<CustomButton variant="grey" rounded="md" onClick={onClose}>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
|
||||
<ContextFormCustom.Consumer>
|
||||
{({ isDone }) => (
|
||||
<CustomButton
|
||||
disabled={!isDone || updatePatientMutation.isPending}
|
||||
variant="midnightBlue"
|
||||
rounded="md"
|
||||
type="submit"
|
||||
>
|
||||
{updatePatientMutation.isPending
|
||||
? "Đang cập nhật..."
|
||||
: "Cập nhật"}
|
||||
</CustomButton>
|
||||
)}
|
||||
</ContextFormCustom.Consumer>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopupUpdatePatient;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./PopupUpdatePatient";
|
||||
@@ -1,554 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { Plus, Trash2 } from "lucide-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import FormCustom from "@/components/utils/FormCustom";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||
import SelectForm from "@/components/utils/FormCustom/components/SelectForm";
|
||||
import UploadImage from "@/components/utils/UploadImage";
|
||||
import { httpRequest } from "@/services";
|
||||
import prescriptionServices from "@/services/prescriptionServices";
|
||||
import consultationServices, {
|
||||
ConsultationResponse,
|
||||
} from "@/services/consultationServices";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
import uploadServices from "@/services/uploadServices";
|
||||
|
||||
/* =========================
|
||||
TYPES
|
||||
========================= */
|
||||
interface PrescriptionItem {
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}
|
||||
|
||||
interface PrescriptionForm {
|
||||
examinationSessionSpecialtyId: string;
|
||||
notes: string;
|
||||
prescriptionImg: string;
|
||||
items: PrescriptionItem[];
|
||||
}
|
||||
|
||||
/* =========================
|
||||
DEFAULT
|
||||
========================= */
|
||||
const defaultMedicine: PrescriptionItem = {
|
||||
medicineName: "",
|
||||
dosage: "",
|
||||
frequency: "",
|
||||
duration: "",
|
||||
instructions: "",
|
||||
};
|
||||
|
||||
/* =========================
|
||||
COMPONENT
|
||||
========================= */
|
||||
const CreatePrescription = () => {
|
||||
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 [form, setForm] = useState<PrescriptionForm>({
|
||||
examinationSessionSpecialtyId: "",
|
||||
notes: "",
|
||||
prescriptionImg: "",
|
||||
items: [defaultMedicine],
|
||||
});
|
||||
|
||||
/* =========================
|
||||
GET EXAMINATION SESSION
|
||||
========================= */
|
||||
const examinationSessionQuery = useQuery<ConsultationResponse>({
|
||||
queryKey: [QUERY_KEY.table_list_consultation],
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<ConsultationResponse>({
|
||||
showMessageFailed: true,
|
||||
http: consultationServices.getConsultations({
|
||||
Page: 1,
|
||||
PageSize: 100,
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const examinationSessionOptions =
|
||||
examinationSessionQuery.data?.items
|
||||
?.filter((session) => {
|
||||
const isWaitingPrescription = session.status === 4;
|
||||
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
|
||||
========================= */
|
||||
const funcCreatePrescription = useMutation({
|
||||
mutationFn: (payload: any) => {
|
||||
console.log("=== GỬI PAYLOAD LÊN API TẠO ĐƠN THUỐC ===", payload);
|
||||
|
||||
return httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Tạo đơn thuốc thành công!",
|
||||
http: prescriptionServices.createPrescription(payload),
|
||||
});
|
||||
},
|
||||
|
||||
onSuccess(data) {
|
||||
console.log("TẠO ĐƠN THUỐC THÀNH CÔNG:", data);
|
||||
|
||||
queryKeys.forEach((key) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [key],
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
========================= */
|
||||
const handleChangeMedicine = (
|
||||
index: number,
|
||||
key: keyof PrescriptionItem,
|
||||
value: string,
|
||||
) => {
|
||||
const cloneItems = [...form.items];
|
||||
cloneItems[index] = {
|
||||
...cloneItems[index],
|
||||
[key]: value,
|
||||
};
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
items: cloneItems,
|
||||
}));
|
||||
};
|
||||
|
||||
/* =========================
|
||||
ADD MEDICINE
|
||||
========================= */
|
||||
const handleAddMedicine = () => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
items: [{ ...defaultMedicine }, ...prev.items],
|
||||
}));
|
||||
};
|
||||
|
||||
/* =========================
|
||||
REMOVE MEDICINE
|
||||
========================= */
|
||||
const handleRemoveMedicine = (index: number) => {
|
||||
const newItems = form.items.filter((_, i) => i !== index);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
items: newItems,
|
||||
}));
|
||||
};
|
||||
|
||||
/* =========================
|
||||
SUBMIT VALIDATION
|
||||
========================= */
|
||||
/* =========================
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
return toastWarn({
|
||||
msg: "Vui lòng chọn ảnh đơn thuốc",
|
||||
});
|
||||
}
|
||||
|
||||
const validMedicineList = form.items.filter(
|
||||
(item) => item.medicineName && item.medicineName.trim() !== "",
|
||||
);
|
||||
|
||||
if (validMedicineList.length === 0) {
|
||||
return toastWarn({
|
||||
msg: "Vui lòng nhập ít nhất 1 loại thuốc hợp lệ",
|
||||
});
|
||||
}
|
||||
|
||||
// Lấy thông tin phiên khám đang được chọn để lấy SessionCode (Mã phiên khám)
|
||||
const selectedSession = examinationSessionOptions.find(
|
||||
(item) => item.id === form.examinationSessionSpecialtyId,
|
||||
);
|
||||
|
||||
// 1. Thực hiện gọi API upload ảnh với cấu trúc chính xác
|
||||
setLoading(true);
|
||||
|
||||
// SỬA TẠI ĐÂY: Truyền đúng định dạng Object chứa mảng Files giống như trang cập nhật
|
||||
const uploadResponse: any = await uploadServices.uploadImage({
|
||||
Files: [file], // Bọc file đơn lẻ vào mảng vì API yêu cầu danh sách file
|
||||
SessionCode: selectedSession?.sessionCode || "", // Truyền mã phiên khám tương ứng
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
// Hỗ trợ trường hợp API trả về mảng fileUrls giống như kết quả POST /api/Media/upload bạn cung cấp
|
||||
else if (uploadResponse?.fileUrls && uploadResponse.fileUrls.length > 0) {
|
||||
imageUrl = uploadResponse.fileUrls[0];
|
||||
}
|
||||
// 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?.fileUrls?.[0] || // Đọc mảng fileUrls bên trong data
|
||||
innerData?.fileUrl ||
|
||||
innerData?.url ||
|
||||
innerData?.data?.fileUrls?.[0] ||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col gap-6 rounded-2xl bg-white p-6 shadow">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-[28px] font-semibold text-[#111827]">
|
||||
Tạo đơn thuốc
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-[#697586]">
|
||||
Tạo mới đơn thuốc cho bệnh nhân
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
variant="midnightBlue"
|
||||
disabled={funcCreatePrescription.isPending || loading}
|
||||
className="min-w-[180px]"
|
||||
>
|
||||
{funcCreatePrescription.isPending || loading
|
||||
? "Đang tạo đơn..."
|
||||
: "Tạo đơn thuốc"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GENERAL INFO */}
|
||||
<div className="rounded-2xl border border-gray-200 p-5">
|
||||
<h3 className="mb-5 text-lg font-semibold text-[#111827]">
|
||||
Thông tin chung
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 gap-5">
|
||||
{/* EXAMINATION SESSION */}
|
||||
<SelectForm
|
||||
label={
|
||||
<span className="text-[14px] font-medium text-[#374151]">
|
||||
Phiên khám chuyên khoa
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
placeholder="Chọn phiên khám chuyên khoa"
|
||||
value={form.examinationSessionSpecialtyId}
|
||||
options={examinationSessionOptions}
|
||||
onSelect={(item: any) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
examinationSessionSpecialtyId: item.id,
|
||||
}))
|
||||
}
|
||||
onClean={() =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
examinationSessionSpecialtyId: "",
|
||||
}))
|
||||
}
|
||||
getOptionLabel={(item: any) =>
|
||||
`${item.sessionCode} - ${item.specialtyName} (${item.patientName})`
|
||||
}
|
||||
getOptionValue={(item: any) => item.id}
|
||||
/>
|
||||
|
||||
{/* IMAGE UPLOAD */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<UploadImage
|
||||
label={
|
||||
<span>
|
||||
Hình ảnh đơn thuốc <span style={{ color: "red" }}>*</span>
|
||||
</span>
|
||||
}
|
||||
name="prescriptionImg"
|
||||
file={file}
|
||||
setFile={setFile}
|
||||
path={""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* NOTES */}
|
||||
<TextArea
|
||||
label={
|
||||
<span>
|
||||
Ghi chú<span style={{ color: "red" }}>*</span>
|
||||
</span>
|
||||
}
|
||||
name="notes"
|
||||
placeholder="Nhập ghi chú"
|
||||
value={form.notes}
|
||||
isBlur
|
||||
isRequired
|
||||
max={5000}
|
||||
onChangeValue={(value) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
notes: String(value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MEDICINE LIST */}
|
||||
<div className="rounded-2xl border border-gray-200 p-5">
|
||||
<div className="mb-5 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-[#111827]">
|
||||
Danh sách thuốc
|
||||
</h3>
|
||||
|
||||
<div>
|
||||
<CustomButton
|
||||
type="button"
|
||||
variant="midnightBlue"
|
||||
icon={<Plus size={18} />}
|
||||
onClick={handleAddMedicine}
|
||||
>
|
||||
Thêm thuốc
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{form.items.map((medicine, index) => (
|
||||
// Lưu ý: Nếu có trường id duy nhất thì nên dùng thay cho index ở đây
|
||||
<div
|
||||
key={`${index}-${form.items.length}`}
|
||||
className="rounded-2xl border border-gray-200 p-5"
|
||||
>
|
||||
{/* TITLE */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h4 className="font-semibold text-[#111827]">
|
||||
Thuốc #{index + 1}
|
||||
</h4>
|
||||
|
||||
{form.items.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveMedicine(index)}
|
||||
className="text-red-500 transition hover:text-red-600"
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* INPUTS */}
|
||||
<div className="grid grid-cols-2 gap-5">
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Tên thuốc<span style={{ color: "red" }}>*</span>
|
||||
</span>
|
||||
}
|
||||
name={`items.${index}.medicineName`}
|
||||
placeholder="Nhập tên thuốc"
|
||||
value={medicine.medicineName}
|
||||
isRequired
|
||||
type="text"
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "medicineName", String(value))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Liều lượng<span style={{ color: "red" }}>*</span>
|
||||
</span>
|
||||
}
|
||||
name={`items.${index}.dosage`}
|
||||
placeholder="Ví dụ: 500mg"
|
||||
value={medicine.dosage}
|
||||
type="text"
|
||||
isRequired
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "dosage", String(value))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Tần xuất<span style={{ color: "red" }}>*</span>
|
||||
</span>
|
||||
}
|
||||
isRequired
|
||||
name={`items.${index}.frequency`}
|
||||
placeholder="Ví dụ: 2 lần/ngày"
|
||||
value={medicine.frequency}
|
||||
type="text"
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "frequency", String(value))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label={
|
||||
<span>
|
||||
Thời gian dùng<span style={{ color: "red" }}>*</span>
|
||||
</span>
|
||||
}
|
||||
isRequired
|
||||
name={`items.${index}.duration`}
|
||||
placeholder="Ví dụ: 7 ngày"
|
||||
value={medicine.duration}
|
||||
type="text"
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "duration", String(value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* INSTRUCTION */}
|
||||
<div className="mt-5">
|
||||
<TextArea
|
||||
label={
|
||||
<span>
|
||||
Hướng dẫn sử dụng<span style={{ color: "red" }}>*</span>
|
||||
</span>
|
||||
}
|
||||
isRequired
|
||||
name={`items.${index}.instructions`}
|
||||
placeholder="Nhập hướng dẫn sử dụng thuốc"
|
||||
value={medicine.instructions}
|
||||
isBlur
|
||||
max={5000}
|
||||
onChangeValue={(value) =>
|
||||
handleChangeMedicine(index, "instructions", String(value))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePrescription;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./CreatePrescription";
|
||||
@@ -1,192 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import prescriptionServices, {
|
||||
PrescriptionDetail,
|
||||
} from "@/services/prescriptionServices";
|
||||
import consultationServices, {
|
||||
ConsultationItem,
|
||||
} from "@/services/consultationServices";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import Image from "next/image";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import CustomPopup from "@/components/customs/custom-popup";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
const DetailPrescription = () => {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const PrescriptionId = params?.id as string;
|
||||
const [openImagePreview, setOpenImagePreview] = useState(false);
|
||||
|
||||
const prescriptionDetailQuery = useQuery<PrescriptionDetail>({
|
||||
queryKey: [QUERY_KEY.chi_tiet_don_thuoc, PrescriptionId],
|
||||
|
||||
enabled: !!PrescriptionId,
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PrescriptionDetail>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: prescriptionServices.detailPrescription(PrescriptionId),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
id: "",
|
||||
examinationSessionSpecialtyId: "",
|
||||
prescriptionCode: "",
|
||||
prescriptionImg: null,
|
||||
notes: "",
|
||||
createdBy: "",
|
||||
items: [],
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const data = prescriptionDetailQuery.data;
|
||||
|
||||
const specialtyListQuery = useQuery<ConsultationItem[]>({
|
||||
queryKey: [QUERY_KEY.examination_specialty_list],
|
||||
enabled: !!data?.examinationSessionSpecialtyId,
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<{
|
||||
items: ConsultationItem[];
|
||||
}>({
|
||||
showMessageFailed: false,
|
||||
http: consultationServices.getConsultations({ PageSize: 1000 }),
|
||||
});
|
||||
|
||||
return res?.items || [];
|
||||
},
|
||||
});
|
||||
|
||||
const specialtyName = React.useMemo(() => {
|
||||
const specialtyId = data?.examinationSessionSpecialtyId;
|
||||
if (!specialtyId) return "";
|
||||
|
||||
const items = specialtyListQuery.data || [];
|
||||
for (const session of items) {
|
||||
const found = session.specialtyClinics?.find((s) => s.id === specialtyId);
|
||||
if (found) return found.specialtyName || "";
|
||||
}
|
||||
|
||||
return "";
|
||||
}, [data?.examinationSessionSpecialtyId, specialtyListQuery.data]);
|
||||
|
||||
return (
|
||||
<div className="p-6 bg-white rounded-lg shadow">
|
||||
<div
|
||||
className="flex items-center gap-1 cursor-pointer"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ArrowLeft />
|
||||
|
||||
<h2 className="text-xl font-semibold text-[#111827]">
|
||||
Chi tiết đơn thuốc
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* LOADING */}
|
||||
{prescriptionDetailQuery.isLoading && <p>Đang tải...</p>}
|
||||
|
||||
{/* ERROR */}
|
||||
{prescriptionDetailQuery.isError && (
|
||||
<p className="text-red-500">Lỗi tải dữ liệu</p>
|
||||
)}
|
||||
|
||||
{/* CONTENT */}
|
||||
{data && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<strong>Mã đơn thuốc:</strong> {data.prescriptionCode}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Chuyên khoa:</strong>{" "}
|
||||
{specialtyName || data.examinationSessionSpecialtyId || "---"}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<strong>Ghi chú:</strong> {data.notes || "---"}
|
||||
</div>
|
||||
|
||||
{/* IMAGE */}
|
||||
<div>
|
||||
<strong>Ảnh đơn thuốc:</strong>
|
||||
<div className="mt-2">
|
||||
{data.prescriptionImg ? (
|
||||
<>
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_IMAGE}/${data.prescriptionImg}`}
|
||||
alt="prescription"
|
||||
width={140}
|
||||
height={140}
|
||||
className="h-10 w-10 rounded object-cover cursor-pointer hover:opacity-80 transition-opacity"
|
||||
onClick={() => setOpenImagePreview(true)}
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
{/* IMAGE PREVIEW POPUP */}
|
||||
<CustomPopup
|
||||
open={openImagePreview}
|
||||
onClose={() => setOpenImagePreview(false)}
|
||||
>
|
||||
<div className="relative rounded-lg p-4 max-w-[90vw] max-h-[90vh] flex items-center justify-center">
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_IMAGE}${data.prescriptionImg}`}
|
||||
alt="prescription preview"
|
||||
width={600}
|
||||
height={600}
|
||||
className="max-w-full max-h-full object-contain"
|
||||
unoptimized
|
||||
/>
|
||||
</div>
|
||||
</CustomPopup>
|
||||
</>
|
||||
) : (
|
||||
<span className="text-gray-400">Không có ảnh</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ITEMS */}
|
||||
<div>
|
||||
<strong>Danh sách thuốc:</strong>
|
||||
|
||||
<div className="mt-2 space-y-2">
|
||||
{data.items?.length ? (
|
||||
data.items.map((item, index) => (
|
||||
<div key={item.id || index} className="p-3 border rounded">
|
||||
<p>
|
||||
<b>Tên thuốc:</b> {item.medicineName}
|
||||
</p>
|
||||
<p>
|
||||
<b>Liều lượng:</b> {item.dosage}
|
||||
</p>
|
||||
<p>
|
||||
<b>Tần suất:</b> {item.frequency}
|
||||
</p>
|
||||
<p>
|
||||
<b>Thời gian:</b> {item.duration}
|
||||
</p>
|
||||
<p>
|
||||
<b>Hướng dẫn:</b> {item.instructions}
|
||||
</p>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-gray-400">Không có thuốc</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailPrescription;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./DetailPrescription";
|
||||
@@ -1,212 +0,0 @@
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import Pagination from "@/components/customs/custom-pagination";
|
||||
import Search from "@/components/customs/custom-search";
|
||||
import Table from "@/components/customs/custom-table";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
import { PATH } from "@/constant/config";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
import { httpRequest } from "@/services";
|
||||
import prescriptionServices, {
|
||||
PrescriptionItem,
|
||||
PrescriptionResponse,
|
||||
} from "@/services/prescriptionServices";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@radix-ui/react-tooltip";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Eye, Pencil, Pill, Trash2 } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
const MainPrescription = () => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const _page = searchParams.get("_page");
|
||||
const _pageSize = searchParams.get("_pageSize");
|
||||
const _keyword = searchParams.get("_keyword");
|
||||
|
||||
const prescriptionQuery = useQuery<PrescriptionResponse>({
|
||||
queryKey: [
|
||||
QUERY_KEY.table_list_patientrescription,
|
||||
_page,
|
||||
_pageSize,
|
||||
_keyword,
|
||||
],
|
||||
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<PrescriptionResponse>({
|
||||
showMessageFailed: true,
|
||||
|
||||
http: prescriptionServices.getPrescription({
|
||||
Page: Number(_page || 1),
|
||||
PageSize: Number(_pageSize || 10),
|
||||
Search: _keyword || "",
|
||||
SortBy: "id",
|
||||
Desc: true,
|
||||
}),
|
||||
});
|
||||
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const prescriptionData = useMemo(() => {
|
||||
return prescriptionQuery.data?.items || [];
|
||||
}, [prescriptionQuery.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">Đơn thuốc</h2>
|
||||
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
fullWidth={false}
|
||||
icon={<Pill />}
|
||||
href={PATH.CREATE_PRESCRIPTION}
|
||||
>
|
||||
Tạo mới
|
||||
</CustomButton>
|
||||
</div>
|
||||
|
||||
{/* SEARCH */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="w-full md:min-w-[400px]">
|
||||
<Search
|
||||
keyword={_keyword ?? ""}
|
||||
setKeyword={(value) => updateQuery("_keyword", value)}
|
||||
placeholder="Tìm theo mã đơn thuốc"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataWrapper
|
||||
data={prescriptionData || []}
|
||||
loading={prescriptionQuery.isLoading}
|
||||
title="No Data"
|
||||
note="Không có đơn thuốc nào"
|
||||
>
|
||||
<Table
|
||||
rowKey={(item: any) => item.id}
|
||||
data={prescriptionData || []}
|
||||
column={[
|
||||
// ================= ID / CODE =================
|
||||
{
|
||||
title: "MÃ ĐƠN THUỐC",
|
||||
render: (item: any) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link
|
||||
href={`/prescription/${item.id}`}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
{item.prescriptionCode}
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
<p className="p-2 bg-gray-400 border rounded-full">
|
||||
Chi tiết đơn thuốc
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
),
|
||||
},
|
||||
|
||||
// ================= NOTES =================
|
||||
{
|
||||
title: "GHI CHÚ",
|
||||
render: (item: any) => <span>{item.notes || "-"}</span>,
|
||||
},
|
||||
|
||||
// ================= IMAGE =================
|
||||
{
|
||||
title: "ẢNH ĐƠN THUỐC",
|
||||
render: (item: any) =>
|
||||
item.prescriptionImg ? (
|
||||
<Image
|
||||
src={`${process.env.NEXT_PUBLIC_IMAGE}${item.prescriptionImg}`}
|
||||
// src={item.prescriptionImg}
|
||||
alt="prescription"
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-10 rounded object-cover"
|
||||
unoptimized
|
||||
/>
|
||||
) : (
|
||||
<span className="text-gray-400">Không có ảnh</span>
|
||||
),
|
||||
},
|
||||
|
||||
// ================= ITEMS COUNT =================
|
||||
{
|
||||
title: "SỐ THUỐC",
|
||||
render: (item: any) => <span>{item.items?.length || 0}</span>,
|
||||
},
|
||||
|
||||
// ================= ACTION =================
|
||||
{
|
||||
fixedRight: true,
|
||||
title: "ACTION",
|
||||
|
||||
render: (item: any) => (
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-2">
|
||||
<CustomButton
|
||||
size="sm"
|
||||
icon={<Eye className="text-blue-400" />}
|
||||
href={`${PATH.PRESCRIPTION}/${item.id}`}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
<Pagination
|
||||
total={prescriptionData?.length}
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
onSetPage={(value) => updateQuery("_page", value)}
|
||||
onSetPageSize={(value) => updateQuery("_pageSize", value)}
|
||||
dependencies={[_pageSize, _keyword]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPrescription;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./MainPrescription";
|
||||
@@ -1,175 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useMemo } from "react";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import icons from "@/constant/images/icons";
|
||||
import { PATH } from "@/constant/config";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import Moment from "react-moment";
|
||||
import {
|
||||
Cake,
|
||||
Phone,
|
||||
MapPin,
|
||||
Mail,
|
||||
SquareUser,
|
||||
ShieldUser,
|
||||
} from "lucide-react";
|
||||
|
||||
import userServices from "@/services/userServices";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
import profileServices, { ProfileResponse } from "@/services/profileServices";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
|
||||
function MainPageProfile() {
|
||||
const router = useRouter();
|
||||
const { infoUser } = useSelector((state: RootState) => state.user);
|
||||
|
||||
const profileQuery = useQuery<any>({
|
||||
queryKey: [QUERY_KEY.table_list_profile],
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<any>({
|
||||
showMessageFailed: true,
|
||||
http: profileServices.getProfile(), // Đảm bảo endpoint trỏ về /api/v1/UserProfile/list
|
||||
});
|
||||
|
||||
// Trả về đúng object chứa items theo cấu trúc của API
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Tìm kiếm thông tin profile của user hiện tại đang đăng nhập bằng useMemo
|
||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
||||
const currentUserProfile = useMemo(() => {
|
||||
const listItems = profileQuery.data?.items || [];
|
||||
if (!infoUser?.userId) return null;
|
||||
|
||||
// So khớp accountId của API trả về với userId trong Redux (infoUser)
|
||||
return (
|
||||
listItems.find((item: any) => item.accountId === infoUser.userId) || null
|
||||
);
|
||||
}, [profileQuery.data, infoUser?.userId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col p-6 bg-white/80 rounded-2xl">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<h2 className="text-xl font-semibold">Thông tin tài khoản</h2>
|
||||
|
||||
<div className="flex gap-2.5">
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
size="md"
|
||||
rounded="full"
|
||||
fullWidth={false}
|
||||
href={PATH.UPDATEPROFILE}
|
||||
>
|
||||
Chỉnh sửa
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AVATAR */}
|
||||
<div className="flex gap-4 mt-6 mb-4">
|
||||
<Image
|
||||
src={
|
||||
currentUserProfile?.avatarUrl
|
||||
? `${process.env.NEXT_PUBLIC_IMAGE}${currentUserProfile.avatarUrl}`
|
||||
: icons.avatar
|
||||
}
|
||||
alt="avatar"
|
||||
width={120}
|
||||
height={120}
|
||||
className="object-cover rounded-lg "
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* MAIN */}
|
||||
<div className="flex gap-4 mt-6 flex-col md:flex-row">
|
||||
{/* BASIC INFO */}
|
||||
<div className="flex flex-col border border-gray-200 bg-gray-50 rounded-xl w-full md:w-1/2 p-5 text-gray-600">
|
||||
<div className="text-xl font-semibold mb-2 text-gray-800">
|
||||
Thông tin cơ bản
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* NAME */}
|
||||
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
|
||||
<p className="flex gap-2">
|
||||
<SquareUser size={24} />
|
||||
Họ và tên
|
||||
</p>
|
||||
<span className="text-gray-900">
|
||||
{currentUserProfile?.fullName || "---"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* DOB */}
|
||||
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
|
||||
<p className="flex gap-2">
|
||||
<Cake size={24} />
|
||||
Ngày sinh
|
||||
</p>
|
||||
<span className="text-gray-900">
|
||||
{currentUserProfile?.birthDate ? (
|
||||
<Moment format="DD/MM/YYYY">
|
||||
{currentUserProfile.birthDate}
|
||||
</Moment>
|
||||
) : (
|
||||
"---"
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CONTACT */}
|
||||
<div className="flex flex-col border border-gray-200 bg-gray-50 rounded-xl w-full md:w-1/2 p-5 text-gray-600">
|
||||
<div className="text-xl font-semibold mb-2 text-gray-800">
|
||||
Liên hệ
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* EMAIL */}
|
||||
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
|
||||
<p className="flex gap-2">
|
||||
<Mail size={24} />
|
||||
Email
|
||||
</p>
|
||||
<span className="text-gray-900">
|
||||
{currentUserProfile?.email || "---"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* PHONE */}
|
||||
<div className="grid grid-cols-[180px_1fr] items-center font-semibold">
|
||||
<p className="flex gap-2">
|
||||
<Phone size={24} />
|
||||
Số điện thoại
|
||||
</p>
|
||||
<span className="text-gray-900">
|
||||
{currentUserProfile?.phone || "---"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MainPageProfile);
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./MainPageProfile";
|
||||
@@ -1,332 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Fragment, memo, useEffect, useMemo, useState } from "react";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import Button from "@/components/customs/custom-button";
|
||||
import UploadAvatar from "@/components/utils/UploadAvatar";
|
||||
import GridColumn from "@/components/layouts/GridColumn";
|
||||
import Loading from "@/components/customs/custom-loading";
|
||||
|
||||
import { PATH } from "@/constant/config";
|
||||
import { QUERY_KEY } from "@/constant/config/enum";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { httpRequest } from "@/services";
|
||||
import uploadServices from "@/services/uploadServices";
|
||||
import profileServices from "@/services/profileServices";
|
||||
|
||||
import { toastWarn } from "@/common/funcs/toast";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
import icons from "@/constant/images/icons";
|
||||
|
||||
import { ContextFormCustom } from "@/components/utils/FormCustom/contexts";
|
||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||
import FormCustom from "@/components/utils/FormCustom/FormCustom";
|
||||
import moment from "moment";
|
||||
import {
|
||||
formatDateOfBirth,
|
||||
formatToISODate,
|
||||
parseDateOfBirth,
|
||||
timeSubmit,
|
||||
} from "@/common/funcs/optionConvert";
|
||||
import CustomButton from "@/components/customs/custom-button";
|
||||
|
||||
function MainUpdateProfile() {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { infoUser } = useSelector((state: RootState) => state.user);
|
||||
const [fileAvatar, setFileAvatar] = useState<File | null>(null);
|
||||
|
||||
// Khởi tạo form dựa hoàn toàn trên các trường Backend trả về
|
||||
const [form, setForm] = useState<{
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
birthDate: string;
|
||||
avatarUrl: string;
|
||||
}>({
|
||||
fullName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
birthDate: "",
|
||||
avatarUrl: "",
|
||||
});
|
||||
|
||||
/* =========================================================
|
||||
1. GỌI API LIST PROFILE & LỌC USER ĐANG ĐĂNG NHẬP
|
||||
========================================================= */
|
||||
const profileQuery = useQuery<any>({
|
||||
queryKey: [QUERY_KEY.table_list_profile],
|
||||
queryFn: async () => {
|
||||
const res = await httpRequest<any>({
|
||||
showMessageFailed: true,
|
||||
http: profileServices.getProfile(),
|
||||
});
|
||||
return (
|
||||
res || {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
||||
const currentUserProfile = useMemo(() => {
|
||||
const listItems = profileQuery.data?.items || [];
|
||||
if (!infoUser?.userId) return null;
|
||||
return (
|
||||
listItems.find((item: any) => item.accountId === infoUser.userId) || null
|
||||
);
|
||||
}, [profileQuery.data, infoUser?.userId]);
|
||||
|
||||
/* =========================================================
|
||||
2. ĐỔ DỮ LIỆU TỪ PROFILE VÀO FORM KHI TẢI TRANG
|
||||
========================================================= */
|
||||
useEffect(() => {
|
||||
if (currentUserProfile) {
|
||||
setForm({
|
||||
fullName: currentUserProfile.fullName || "",
|
||||
email: currentUserProfile.email || "",
|
||||
phone: currentUserProfile.phone || "",
|
||||
// Định dạng input date HTML yêu cầu format YYYY-MM-DD
|
||||
birthDate: currentUserProfile.birthDate
|
||||
? moment(currentUserProfile.birthDate).format("DD/MM/YYYY")
|
||||
: "",
|
||||
avatarUrl: currentUserProfile.avatarUrl || "",
|
||||
});
|
||||
}
|
||||
}, [currentUserProfile]);
|
||||
|
||||
/* =========================================================
|
||||
3. API MUTATION UPDATE PROFILE (Thay thế API userServices cũ)
|
||||
========================================================= */
|
||||
const funcUpdateProfile = useMutation({
|
||||
mutationFn: (body: { path: string }) =>
|
||||
httpRequest({
|
||||
showMessageFailed: true,
|
||||
showMessageSuccess: true,
|
||||
msgSuccess: "Chỉnh sửa thông tin thành công!",
|
||||
// Bạn có thể đổi sang endpoint PATCH/PUT tùy thuộc vào route Update của bạn
|
||||
http: profileServices.putProfile(currentUserProfile?.id, {
|
||||
fullName: form.fullName,
|
||||
email: form.email,
|
||||
phone: form.phone,
|
||||
birthDate: formatToISODate(parseDateOfBirth(form.birthDate)),
|
||||
avatarUrl: body.path || form.avatarUrl,
|
||||
}), // Lưu ý: Hãy cập nhật đúng service update của user profile tại đây
|
||||
}),
|
||||
onSuccess() {
|
||||
// Làm mới dữ liệu list profile trên toàn hệ thống (bao gồm cả Header và Profile Page)
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [QUERY_KEY.table_list_profile],
|
||||
});
|
||||
router.push(PATH.PROFILE || "/profile");
|
||||
},
|
||||
onError() {
|
||||
toastWarn({ msg: "Cập nhật thông tin thất bại!" });
|
||||
},
|
||||
});
|
||||
|
||||
/* =========================================================
|
||||
4. XỬ LÝ SUBMIT (UPLOAD AVATAR TRƯỚC - UPDATE PROFILE SAU)
|
||||
========================================================= */
|
||||
const handleSubmit = async () => {
|
||||
const today = new Date(timeSubmit(new Date())!);
|
||||
const birthDay = new Date(form.birthDate);
|
||||
|
||||
if (today < birthDay) {
|
||||
return toastWarn({ msg: "Ngày sinh không hợp lệ!" });
|
||||
}
|
||||
try {
|
||||
// Nếu người dùng chọn file avatar mới
|
||||
if (fileAvatar) {
|
||||
const uploadResult: any = await uploadServices.uploadImage({
|
||||
Files: [fileAvatar],
|
||||
});
|
||||
|
||||
const uploadedUrl =
|
||||
uploadResult?.fileUrls?.[0] ||
|
||||
uploadResult?.data?.fileUrls?.[0] ||
|
||||
"";
|
||||
|
||||
return funcUpdateProfile.mutate({ path: uploadedUrl });
|
||||
}
|
||||
|
||||
// Nếu không thay đổi ảnh, dùng lại link avatar cũ
|
||||
return funcUpdateProfile.mutate({ path: form.avatarUrl });
|
||||
} catch (error) {
|
||||
console.error("Lỗi cập nhật ảnh đại diện:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Loading
|
||||
loading={funcUpdateProfile.isPending || profileQuery.isLoading}
|
||||
/>
|
||||
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
||||
<div className="flex flex-col p-6 bg-white/80 rounded-2xl gap-6 shadow-sm">
|
||||
{/* HEADER */}
|
||||
<div className="flex items-center justify-between w-full border-b pb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-800">
|
||||
Chỉnh sửa thông tin tài khoản
|
||||
</h2>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<CustomButton
|
||||
variant="grey"
|
||||
rounded="full"
|
||||
size="md"
|
||||
fullWidth={false}
|
||||
type="button"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Hủy bỏ
|
||||
</CustomButton>
|
||||
<ContextFormCustom.Consumer>
|
||||
{({ isDone }) => {
|
||||
const hasRememberedData = !!(
|
||||
form.fullName &&
|
||||
form.email &&
|
||||
form.birthDate &&
|
||||
form.phone
|
||||
);
|
||||
const canSubmit = isDone || hasRememberedData;
|
||||
return (
|
||||
<CustomButton
|
||||
variant="midnightBlue"
|
||||
rounded="full"
|
||||
size="md"
|
||||
fullWidth={false}
|
||||
type="button"
|
||||
disabled={!canSubmit}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{funcUpdateProfile.isPending ? "Đang lưu..." : "Cập nhật"}
|
||||
</CustomButton>
|
||||
);
|
||||
}}
|
||||
</ContextFormCustom.Consumer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MAIN FORM */}
|
||||
<div className="space-y-6">
|
||||
{/* KHỐI AVATAR */}
|
||||
<div className="flex justify-start">
|
||||
<UploadAvatar
|
||||
path={
|
||||
form?.avatarUrl
|
||||
? `${process.env.NEXT_PUBLIC_IMAGE}${form?.avatarUrl}`
|
||||
: icons.avatar
|
||||
}
|
||||
name="avatar"
|
||||
onSetFile={(file) => setFileAvatar(file)}
|
||||
resetPath={() => {
|
||||
setFileAvatar(null);
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
avatarUrl: "",
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* KHỐI THÔNG TIN CÁ NHÂN CHUẨN BACKEND */}
|
||||
<div className="mt-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
{/* Cột trái */}
|
||||
<div className="space-y-4">
|
||||
<InputForm
|
||||
type="text"
|
||||
placeholder="Nhập họ và tên"
|
||||
name="fullName"
|
||||
value={form.fullName}
|
||||
label={
|
||||
<span>
|
||||
Họ Và Tên
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
isRequired
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, fullName: String(v) }))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
type="text"
|
||||
placeholder="Nhập ngày sinh"
|
||||
name="birthDate"
|
||||
isRequired
|
||||
label={
|
||||
<span>
|
||||
Ngày sinh
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
value={form.birthDate}
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
birthDate: formatDateOfBirth(String(v)),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Cột phải */}
|
||||
<div className="space-y-4">
|
||||
<InputForm
|
||||
placeholder="Nhập email"
|
||||
name="email"
|
||||
type="email"
|
||||
isEmail
|
||||
value={form.email}
|
||||
label={
|
||||
<span>
|
||||
Email
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
isRequired
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, email: String(v) }))
|
||||
}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
placeholder="Nhập số điện thoại"
|
||||
name="phone"
|
||||
type="number"
|
||||
value={form.phone}
|
||||
isRequired
|
||||
min={8}
|
||||
max={11}
|
||||
label={
|
||||
<span>
|
||||
Số điện thoại
|
||||
<span className="text-red-500">*</span>
|
||||
</span>
|
||||
}
|
||||
onChangeValue={(v) =>
|
||||
setForm((prev) => ({ ...prev, phone: String(v) }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormCustom>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(MainUpdateProfile);
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./MainUpdateProfile";
|
||||
@@ -1,202 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo, useState } from "react";
|
||||
import moment from "moment";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Users, Stethoscope, Pill, Glasses } from "lucide-react";
|
||||
|
||||
import Table from "@/components/customs/custom-table";
|
||||
import DataWrapper from "@/components/customs/DataWrapper";
|
||||
|
||||
import { QUERY_KEY, TYPE_DATE } from "@/constant/config/enum";
|
||||
|
||||
import { httpRequest } from "@/services";
|
||||
|
||||
import consultationServices, {
|
||||
ReportResponse,
|
||||
} from "@/services/consultationServices";
|
||||
import FilterDateRange from "@/components/utils/FilterDateRage";
|
||||
|
||||
const MainPageStatistical = () => {
|
||||
const [date, setDate] = useState<{
|
||||
from: Date | null;
|
||||
to: Date | null;
|
||||
} | null>(null);
|
||||
const [typeDate, setTypeDate] = useState<TYPE_DATE>(TYPE_DATE.TODAY);
|
||||
|
||||
const isInvalidDate =
|
||||
!!date?.from && moment(date.from).isAfter(moment().endOf("day"));
|
||||
|
||||
const reportQuery = useQuery<ReportResponse>({
|
||||
queryKey: [
|
||||
QUERY_KEY.table_list_report,
|
||||
date?.from?.toISOString(),
|
||||
date?.to?.toISOString(),
|
||||
],
|
||||
|
||||
enabled: !!date?.from && !!date?.to && !isInvalidDate,
|
||||
|
||||
queryFn: async () => {
|
||||
const fromDate = moment(date?.from).format("YYYY-MM-DD");
|
||||
const toDate = moment(date?.to).format("YYYY-MM-DD");
|
||||
|
||||
console.log({
|
||||
FromDate: fromDate,
|
||||
ToDate: toDate,
|
||||
});
|
||||
|
||||
const res = await httpRequest<ReportResponse>({
|
||||
showMessageFailed: true,
|
||||
http: consultationServices.getConsultationsReport({
|
||||
FromDate: fromDate,
|
||||
ToDate: toDate,
|
||||
}),
|
||||
});
|
||||
|
||||
return res as ReportResponse;
|
||||
},
|
||||
});
|
||||
|
||||
const report = reportQuery.data;
|
||||
|
||||
const reportData = useMemo(() => {
|
||||
return report?.specialtyDetails || [];
|
||||
}, [report]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* HEADER */}
|
||||
<div className="rounded-xl bg-white p-6 shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold">Thống kê phiên khám</h2>
|
||||
|
||||
<FilterDateRange
|
||||
date={date}
|
||||
setDate={setDate}
|
||||
typeDate={typeDate}
|
||||
setTypeDate={setTypeDate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CARDS */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div className="rounded-xl bg-white p-5 shadow">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<Users size={22} />
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-500">Tổng lượt khám</p>
|
||||
|
||||
<h3 className="mt-2 text-3xl font-bold">
|
||||
{report?.totalSessions || 0}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-white p-5 shadow">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<Stethoscope size={22} />
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-500">Lượt khám chuyên khoa</p>
|
||||
|
||||
<h3 className="mt-2 text-3xl font-bold">
|
||||
{report?.totalSpecialtySubSessions || 0}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-white p-5 shadow">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<Pill size={22} />
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-500">Đơn thuốc đã cấp</p>
|
||||
|
||||
<h3 className="mt-2 text-3xl font-bold">
|
||||
{report?.totalPrescriptionsIssued || 0}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl bg-white p-5 shadow">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<Glasses size={22} />
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-500">Số kính đã phát</p>
|
||||
|
||||
<h3 className="mt-2 text-3xl font-bold">
|
||||
{report?.totalGlassesDelivered || 0}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BẢNG THỐNG KÊ */}
|
||||
<div className="rounded-xl bg-white p-6 shadow">
|
||||
<h3 className="mb-4 text-lg font-semibold">
|
||||
Thống kê theo chuyên khoa
|
||||
</h3>
|
||||
|
||||
<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.specialtyCode}</span>,
|
||||
},
|
||||
{
|
||||
title: "TÊN CHUYÊN KHOA",
|
||||
render: (item) => <span>{item.specialtyName}</span>,
|
||||
},
|
||||
{
|
||||
title: "SỐ CA KHÁM",
|
||||
render: (item) => (
|
||||
<span className="font-semibold">{item.totalCases}</span>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DataWrapper>
|
||||
</div>
|
||||
|
||||
{/* BÁO CÁO TỔNG HỢP */}
|
||||
<div className="rounded-xl bg-white p-6 shadow">
|
||||
<h3 className="mb-4 text-lg font-semibold">Báo cáo tổng hợp</h3>
|
||||
|
||||
<div className="space-y-3 text-[15px]">
|
||||
<p>
|
||||
Số lượt khám bệnh nhân đạo:
|
||||
<strong> {report?.totalSpecialtySubSessions || 0}</strong> lượt
|
||||
người dân được khám chuyên khoa, cụ thể như sau:
|
||||
</p>
|
||||
|
||||
<ul className="list-disc space-y-2 pl-6">
|
||||
{reportData.map((item) => (
|
||||
<li key={item.specialtyId}>
|
||||
<strong>{item.specialtyName}</strong>: {item.totalCases} ca
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
Tổng số đơn thuốc đã cấp:
|
||||
<strong> {report?.totalPrescriptionsIssued || 0}</strong> đơn.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Tổng số kính đã phát:
|
||||
<strong> {report?.totalGlassesDelivered || 0}</strong> cái.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainPageStatistical;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./MainPageStatistical";
|
||||
@@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useSuperTStore, BridgeComponentType } from "@/hooks/useSuperTStore";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Layers,
|
||||
Link2,
|
||||
Building2,
|
||||
MapPin,
|
||||
} from "lucide-react";
|
||||
|
||||
interface MenuNode {
|
||||
id: string;
|
||||
label: string;
|
||||
tabKey?: BridgeComponentType;
|
||||
children?: MenuNode[];
|
||||
}
|
||||
|
||||
const bridgeTreeData: MenuNode[] = [
|
||||
{
|
||||
id: "superstructure",
|
||||
label: "1. Kết cấu phần trên",
|
||||
children: [
|
||||
{ id: "beam", label: "Dầm Super-T", tabKey: "superTBeam" },
|
||||
{ id: "deck", label: "Bản mặt cầu", tabKey: "deck" },
|
||||
{ id: "diaphragm", label: "Dầm ngang / Diaphragms", tabKey: "diaphragm" },
|
||||
{ id: "railing", label: "Lan can", tabKey: "railing" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "connections",
|
||||
label: "2. Liên kết",
|
||||
children: [
|
||||
{ id: "bearing", label: "Gối cầu", tabKey: "bearing" },
|
||||
{
|
||||
id: "beamDeckJoint",
|
||||
label: "Liên kết dầm - bản",
|
||||
tabKey: "beamDeckJoint",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "substructure",
|
||||
label: "3. Kết cấu phần dưới",
|
||||
children: [
|
||||
{ id: "abutment", label: "Mố cầu", tabKey: "abutment" },
|
||||
{ id: "pier", label: "Trụ cầu", tabKey: "pier" },
|
||||
{ id: "pile", label: "Cọc / Móng cọc", tabKey: "pile" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "geometry",
|
||||
label: "4. Dữ liệu hình học",
|
||||
children: [
|
||||
{ id: "alignment", label: "Tim cầu & Lý trình", tabKey: "alignment" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const BridgeTreeSidebar: React.FC = () => {
|
||||
const { activeTab, setActiveTab } = useSuperTStore();
|
||||
const [openNodes, setOpenNodes] = useState<Record<string, boolean>>({
|
||||
superstructure: true,
|
||||
substructure: true,
|
||||
});
|
||||
|
||||
const toggleNode = (nodeId: string) => {
|
||||
setOpenNodes((prev) => ({ ...prev, [nodeId]: !prev[nodeId] }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 h-full bg-slate-900 border-r border-slate-700 p-3 text-white overflow-y-auto shrink-0 select-none">
|
||||
<div className="mb-4 px-2">
|
||||
<h1 className="text-base font-bold text-sky-400">TEDI DDP - Super-T</h1>
|
||||
<p className="text-[10px] text-slate-400">Mô hình cấu kiện Cầu KTS</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{bridgeTreeData.map((group) => (
|
||||
<div key={group.id} className="text-xs">
|
||||
{/* Header nhóm chính */}
|
||||
<button
|
||||
onClick={() => toggleNode(group.id)}
|
||||
className="w-full flex items-center justify-between p-2 hover:bg-slate-800 rounded font-semibold text-slate-200 transition-colors"
|
||||
>
|
||||
<span>{group.label}</span>
|
||||
{openNodes[group.id] ? (
|
||||
<ChevronDown className="w-4 h-4 text-slate-400" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 text-slate-400" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Danh sách nút cấu kiện con */}
|
||||
{openNodes[group.id] && group.children && (
|
||||
<div className="ml-3 pl-2 border-l border-slate-700 space-y-1 mt-1">
|
||||
{group.children.map((child) => {
|
||||
const isActive = activeTab === child.tabKey;
|
||||
return (
|
||||
<button
|
||||
key={child.id}
|
||||
onClick={() => child.tabKey && setActiveTab(child.tabKey)}
|
||||
className={`w-full text-left p-1.5 rounded transition-all flex items-center gap-2 ${
|
||||
isActive
|
||||
? "bg-sky-600/30 text-sky-400 font-medium border border-sky-500/50"
|
||||
: "text-slate-400 hover:bg-slate-800 hover:text-slate-200"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full ${isActive ? "bg-sky-400" : "bg-slate-600"}`}
|
||||
/>
|
||||
{child.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Canvas } from "@react-three/fiber";
|
||||
import { OrbitControls, Grid, Bounds, useBounds } from "@react-three/drei";
|
||||
import { SuperTBeamMesh } from "./SuperTBeamMesh";
|
||||
import { useSuperTStore } from "@/hooks/useSuperTStore";
|
||||
|
||||
const SelectFocus: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const api = useBounds();
|
||||
const activeTab = useSuperTStore((state) => state.activeTab);
|
||||
const isAssemblyMode = useSuperTStore((state) => state.isAssemblyMode);
|
||||
|
||||
React.useEffect(() => {
|
||||
api.refresh().fit();
|
||||
}, [activeTab, isAssemblyMode, api]);
|
||||
|
||||
return <group>{children}</group>;
|
||||
};
|
||||
|
||||
export const Canvas3D: React.FC = () => {
|
||||
const { activeTab, beamData, bridgeData, isAssemblyMode } = useSuperTStore();
|
||||
|
||||
// Khai báo các mesh bộ phận
|
||||
const renderSuperTBeam = () => <SuperTBeamMesh data={beamData} />;
|
||||
|
||||
const renderDeck = () => (
|
||||
<mesh position={[0, beamData.height + bridgeData.deck.thickness / 2, 0]}>
|
||||
<boxGeometry
|
||||
args={[
|
||||
bridgeData.deck.width,
|
||||
bridgeData.deck.thickness,
|
||||
beamData.length,
|
||||
]}
|
||||
/>
|
||||
<meshStandardMaterial color="#64748b" />
|
||||
</mesh>
|
||||
);
|
||||
|
||||
const renderPile = () => (
|
||||
<mesh position={[0, (-bridgeData.pile.length * 1000) / 2, 0]}>
|
||||
<cylinderGeometry
|
||||
args={[
|
||||
bridgeData.pile.diameter / 2,
|
||||
bridgeData.pile.diameter / 2,
|
||||
bridgeData.pile.length * 1000,
|
||||
32,
|
||||
]}
|
||||
/>
|
||||
<meshStandardMaterial color="#38bdf8" />
|
||||
</mesh>
|
||||
);
|
||||
|
||||
const renderPier = () => (
|
||||
<mesh position={[0, -bridgeData.pier.height / 2, 0]}>
|
||||
<cylinderGeometry
|
||||
args={[
|
||||
bridgeData.pier.diameter / 2,
|
||||
bridgeData.pier.diameter / 2,
|
||||
bridgeData.pier.height,
|
||||
32,
|
||||
]}
|
||||
/>
|
||||
<meshStandardMaterial color="#94a3b8" />
|
||||
</mesh>
|
||||
);
|
||||
|
||||
const renderAbutment = () => (
|
||||
<mesh position={[0, -bridgeData.abutment.height / 2, beamData.length / 2]}>
|
||||
<boxGeometry
|
||||
args={[
|
||||
bridgeData.deck.width,
|
||||
bridgeData.abutment.height,
|
||||
bridgeData.abutment.topWidth,
|
||||
]}
|
||||
/>
|
||||
<meshStandardMaterial color="#475569" />
|
||||
</mesh>
|
||||
);
|
||||
|
||||
const renderBearing = () => (
|
||||
<mesh
|
||||
position={[0, -bridgeData.bearing.height / 2, beamData.length / 2 - 500]}
|
||||
>
|
||||
<boxGeometry args={[600, bridgeData.bearing.height, 600]} />
|
||||
<meshStandardMaterial color="#0f172a" />
|
||||
</mesh>
|
||||
);
|
||||
|
||||
const renderRailing = () => (
|
||||
<group>
|
||||
<mesh
|
||||
position={[
|
||||
-bridgeData.deck.width / 2 + 200,
|
||||
beamData.height +
|
||||
bridgeData.deck.thickness +
|
||||
bridgeData.railing.height / 2,
|
||||
0,
|
||||
]}
|
||||
>
|
||||
<boxGeometry args={[100, bridgeData.railing.height, beamData.length]} />
|
||||
<meshStandardMaterial color="#cbd5e1" />
|
||||
</mesh>
|
||||
<mesh
|
||||
position={[
|
||||
bridgeData.deck.width / 2 - 200,
|
||||
beamData.height +
|
||||
bridgeData.deck.thickness +
|
||||
bridgeData.railing.height / 2,
|
||||
0,
|
||||
]}
|
||||
>
|
||||
<boxGeometry args={[100, bridgeData.railing.height, beamData.length]} />
|
||||
<meshStandardMaterial color="#cbd5e1" />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
|
||||
const renderDiaphragm = () => (
|
||||
<mesh position={[0, beamData.height / 2, 0]}>
|
||||
<boxGeometry
|
||||
args={[
|
||||
bridgeData.deck.width - 1000,
|
||||
beamData.height * 0.8,
|
||||
bridgeData.diaphragm.thickness,
|
||||
]}
|
||||
/>
|
||||
<meshStandardMaterial color="#e2e8f0" />
|
||||
</mesh>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<Canvas
|
||||
camera={{ position: [15000, 10000, 25000], fov: 45, far: 500000 }}
|
||||
>
|
||||
<ambientLight intensity={1.2} />
|
||||
<directionalLight position={[20000, 30000, 20000]} intensity={1.5} />
|
||||
|
||||
<Bounds fit clip observe margin={1.2}>
|
||||
<SelectFocus>
|
||||
{isAssemblyMode ? (
|
||||
/* Ghép toàn bộ các cấu kiện lại thành cầu */
|
||||
<group>
|
||||
{renderSuperTBeam()}
|
||||
{renderDeck()}
|
||||
{renderPile()}
|
||||
{renderPier()}
|
||||
{renderAbutment()}
|
||||
{renderBearing()}
|
||||
{renderRailing()}
|
||||
{renderDiaphragm()}
|
||||
</group>
|
||||
) : (
|
||||
/* Hiển thị đơn lẻ theo Tab đang chọn */
|
||||
<>
|
||||
{activeTab === "superTBeam" && renderSuperTBeam()}
|
||||
{activeTab === "deck" && renderDeck()}
|
||||
{activeTab === "pile" && renderPile()}
|
||||
{activeTab === "pier" && renderPier()}
|
||||
{activeTab === "abutment" && renderAbutment()}
|
||||
{activeTab === "bearing" && renderBearing()}
|
||||
{activeTab === "railing" && renderRailing()}
|
||||
{activeTab === "diaphragm" && renderDiaphragm()}
|
||||
</>
|
||||
)}
|
||||
</SelectFocus>
|
||||
</Bounds>
|
||||
|
||||
<Grid
|
||||
args={[200000, 200000]}
|
||||
sectionColor="#38bdf8"
|
||||
cellColor="#334155"
|
||||
infiniteGrid
|
||||
/>
|
||||
<OrbitControls makeDefault />
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { SuperTBeamData } from "../core/schema/superT.schema";
|
||||
|
||||
// Component Dầm Ngang
|
||||
export const DiaphragmsMesh: React.FC<{ data: SuperTBeamData }> = ({
|
||||
data,
|
||||
}) => {
|
||||
return (
|
||||
<group position={[0, 0, -data.length / 2]}>
|
||||
{data.diaphragmPositions.map((pos, index) => (
|
||||
<mesh key={index} position={[0, data.height / 2, pos]} castShadow>
|
||||
<boxGeometry
|
||||
args={[data.topFlangeWidth - 200, data.height - 300, 300]}
|
||||
/>
|
||||
<meshStandardMaterial color="#4A5568" />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import { SuperTBeamData } from "@/constant/types/bridge/superT";
|
||||
import React from "react";
|
||||
|
||||
// Component Tao Cáp Dự Ứng Lực
|
||||
export const StrandsMesh: React.FC<{ data: SuperTBeamData }> = ({ data }) => {
|
||||
const strandRows = Math.ceil(data.strandCount / 4);
|
||||
const strands = [];
|
||||
|
||||
for (let i = 0; i < strandRows; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
if (strands.length < data.strandCount) {
|
||||
strands.push({
|
||||
x: (j - 1.5) * 60,
|
||||
y: 80 + i * 50,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<group position={[0, 0, -data.length / 2]}>
|
||||
{strands.map((s, idx) => (
|
||||
<mesh key={idx} position={[s.x, s.y, data.length / 2]}>
|
||||
<cylinderGeometry args={[7, 7, data.length, 8]} />
|
||||
<meshStandardMaterial
|
||||
color="#E53E3E"
|
||||
metalness={0.8}
|
||||
roughness={0.2}
|
||||
/>
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import React, { useMemo } from "react";
|
||||
import * as THREE from "three";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const SuperTBeamMesh: React.FC<{ data: any }> = ({ data }) => {
|
||||
const beamShape = useMemo(() => {
|
||||
const shape = new THREE.Shape();
|
||||
const halfTop = (data.topFlangeWidth || 2100) / 2;
|
||||
const h = data.height || 1752;
|
||||
|
||||
// Vẽ hình chữ nhật đơn giản để test trước
|
||||
shape.moveTo(-halfTop, 0);
|
||||
shape.lineTo(halfTop, 0);
|
||||
shape.lineTo(halfTop, h);
|
||||
shape.lineTo(-halfTop, h);
|
||||
shape.closePath();
|
||||
|
||||
return shape;
|
||||
}, [data]);
|
||||
|
||||
const extrudeSettings = useMemo(
|
||||
() => ({
|
||||
depth: data.length || 33000,
|
||||
bevelEnabled: false,
|
||||
}),
|
||||
[data.length],
|
||||
);
|
||||
|
||||
return (
|
||||
<mesh position={[0, 0, -(data.length || 33000) / 2]}>
|
||||
<extrudeGeometry args={[beamShape, extrudeSettings]} />
|
||||
<meshStandardMaterial color="#00ffff" roughness={0.3} />
|
||||
</mesh>
|
||||
);
|
||||
};
|
||||
@@ -113,4 +113,47 @@ export const ListOptionFilterDate: {
|
||||
},
|
||||
];
|
||||
|
||||
export const SPECIALTY_ORDER = [
|
||||
"Khám Nội",
|
||||
"Khám Răng",
|
||||
"Khám Tai Mũi Họng",
|
||||
"Khám Mắt",
|
||||
"Khám Sản phụ khoa",
|
||||
"Xét nghiệm máu",
|
||||
"Xét nghiệm nước tiểu",
|
||||
"X-quang",
|
||||
"Điện tim",
|
||||
"Siêu Âm",
|
||||
];
|
||||
|
||||
export const SPECIALTY_DISPLAY_NAME: Record<string, string> = {
|
||||
Nội: "Khám Nội",
|
||||
"Khám Nội": "Khám Nội",
|
||||
|
||||
Răng: "Khám Răng",
|
||||
"Khám Răng": "Khám Răng",
|
||||
|
||||
"Tai Mũi Họng": "Khám Tai Mũi Họng",
|
||||
"Khám Tai Mũi Họng": "Khám Tai Mũi Họng",
|
||||
|
||||
Mắt: "Khám Mắt",
|
||||
"Khám Mắt": "Khám Mắt",
|
||||
|
||||
"Sản Phụ Khoa": "Khám Sản phụ khoa",
|
||||
"Sản phụ khoa": "Khám Sản phụ khoa",
|
||||
"Khám Sản phụ khoa": "Khám Sản phụ khoa",
|
||||
|
||||
"Xét nghiệm máu": "Xét nghiệm máu",
|
||||
|
||||
"Xét nghiệm nước tiểu": "Xét nghiệm nước tiểu",
|
||||
|
||||
"X-Quang": "X-quang",
|
||||
"X-quang": "X-quang",
|
||||
|
||||
"Điện Tim": "Điện tim",
|
||||
"Điện tim": "Điện tim",
|
||||
|
||||
"Siêu Âm": "Siêu Âm",
|
||||
};
|
||||
|
||||
export const KEY_STORE = "management-kham-benh";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import hamburger from "../../../public/static/images/icon-hamburger.svg";
|
||||
import LogoTedi from "../../../public/static/images/logo-tedi.png";
|
||||
import full_screen from "../../../public/static/images/full-screen.svg";
|
||||
import avatar from "../../../public/static/images/avatar-default.png";
|
||||
import tickCircle from "../../../public/static/images/tick-circle.svg";
|
||||
@@ -10,7 +10,7 @@ import emptyTable from "../../../public/static/images/empty_table.png";
|
||||
import emptyFile from "../../../public/static/images/emptyFile.svg";
|
||||
|
||||
export default {
|
||||
hamburger,
|
||||
LogoTedi,
|
||||
full_screen,
|
||||
avatar,
|
||||
tickCircle,
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { create } from "zustand";
|
||||
import { SuperTBeamData } from "@/components/core/schema/superT.schema";
|
||||
import {
|
||||
validateSuperTEngineering,
|
||||
ValidationIssue,
|
||||
} from "@/components/core/validators/superTValidator";
|
||||
|
||||
export type BridgeComponentType =
|
||||
| "superTBeam"
|
||||
| "deck"
|
||||
| "diaphragm"
|
||||
| "asphalt"
|
||||
| "railing"
|
||||
| "expansionJoint"
|
||||
| "subComponents"
|
||||
| "bearing"
|
||||
| "beamDeckJoint"
|
||||
| "diaphragmJoint"
|
||||
| "substructureJoint"
|
||||
| "abutment"
|
||||
| "pier"
|
||||
| "footing"
|
||||
| "pile"
|
||||
| "alignment";
|
||||
|
||||
interface BridgeFullData {
|
||||
deck: { width: number; thickness: number; slope: number };
|
||||
diaphragm: { thickness: number; count: number };
|
||||
railing: { height: number; curbWidth: number };
|
||||
bearing: { type: string; height: number };
|
||||
beamDeckJoint: { rebarDiameter: number; spacing: number };
|
||||
abutment: { height: number; topWidth: number };
|
||||
pier: { height: number; diameter: number };
|
||||
pile: { diameter: number; length: number; count: number };
|
||||
}
|
||||
|
||||
interface SuperTStore {
|
||||
activeTab: BridgeComponentType;
|
||||
setActiveTab: (tab: BridgeComponentType) => void;
|
||||
isAssemblyMode: boolean;
|
||||
setIsAssemblyMode: (val: boolean) => void;
|
||||
|
||||
beamData: SuperTBeamData;
|
||||
validationIssues: ValidationIssue[];
|
||||
setBeamData: (data: Partial<SuperTBeamData>) => void;
|
||||
|
||||
bridgeData: BridgeFullData;
|
||||
setBridgeData: <K extends keyof BridgeFullData>(
|
||||
component: K,
|
||||
data: Partial<BridgeFullData[K]>,
|
||||
) => void;
|
||||
|
||||
exportJSONContract: () => string;
|
||||
}
|
||||
|
||||
const initialData: SuperTBeamData = {
|
||||
id: "550e8400-e29b-41d4-a716-446655440000",
|
||||
code: "SUPER-T-33M-01",
|
||||
length: 33000,
|
||||
height: 1750,
|
||||
topFlangeWidth: 2100,
|
||||
topFlangeThickness: 250,
|
||||
webThickness: 150,
|
||||
bottomFlangeWidth: 750,
|
||||
bottomFlangeThickness: 300,
|
||||
diaphragmPositions: [0, 16500, 33000],
|
||||
strandCount: 24,
|
||||
placement: {
|
||||
station: 1025.5,
|
||||
skewAngle: 0,
|
||||
offsetY: 0,
|
||||
offsetZ: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const initialBridgeData: BridgeFullData = {
|
||||
deck: { width: 12000, thickness: 200, slope: 2 },
|
||||
diaphragm: { thickness: 300, count: 3 },
|
||||
railing: { height: 1100, curbWidth: 500 },
|
||||
bearing: { type: "elastomeric", height: 80 },
|
||||
beamDeckJoint: { rebarDiameter: 16, spacing: 200 },
|
||||
abutment: { height: 7500, topWidth: 1800 },
|
||||
pier: { height: 12000, diameter: 1800 },
|
||||
pile: { diameter: 1200, length: 30, count: 6 },
|
||||
};
|
||||
|
||||
export const useSuperTStore = create<SuperTStore>((set, get) => ({
|
||||
activeTab: "superTBeam",
|
||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
||||
isAssemblyMode: false,
|
||||
setIsAssemblyMode: (val) => set({ isAssemblyMode: val }),
|
||||
|
||||
beamData: initialData,
|
||||
bridgeData: initialBridgeData,
|
||||
validationIssues: validateSuperTEngineering(initialData),
|
||||
|
||||
setBeamData: (updated) => {
|
||||
set((state) => {
|
||||
const newBeamData = {
|
||||
...state.beamData,
|
||||
...updated,
|
||||
placement: {
|
||||
...state.beamData.placement,
|
||||
...(updated.placement || {}),
|
||||
},
|
||||
};
|
||||
const issues = validateSuperTEngineering(newBeamData);
|
||||
return { beamData: newBeamData, validationIssues: issues };
|
||||
});
|
||||
},
|
||||
|
||||
setBridgeData: (component, updated) => {
|
||||
set((state) => ({
|
||||
bridgeData: {
|
||||
...state.bridgeData,
|
||||
[component]: {
|
||||
...state.bridgeData[component],
|
||||
...updated,
|
||||
},
|
||||
},
|
||||
}));
|
||||
},
|
||||
|
||||
exportJSONContract: () => {
|
||||
const { beamData, bridgeData } = get();
|
||||
return JSON.stringify(
|
||||
{
|
||||
header: {
|
||||
schemaVersion: "1.0",
|
||||
generator: "TEDI DDP Web Module",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
payload: { beamData, bridgeData },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
},
|
||||
}));
|
||||
@@ -1,50 +0,0 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
const accountServices = {
|
||||
sendOTP: (
|
||||
data: {
|
||||
email: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.post(`/Account/send-otp-email`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
enterOTP: (
|
||||
data: {
|
||||
email: string;
|
||||
otp: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.post(`/Account/enter-otp`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
changePassForget: (
|
||||
data: {
|
||||
email: string;
|
||||
otp: string;
|
||||
newPass: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.post(`/Account/reset-password`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
changePassword: (
|
||||
data: {
|
||||
oldPass: string;
|
||||
newPass: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.post(`/Account/change-password`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default accountServices;
|
||||
@@ -1,240 +0,0 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
export interface GetConsultationParams {
|
||||
Page?: number;
|
||||
PageSize?: number;
|
||||
Search?: string;
|
||||
PatientId?: string;
|
||||
SortBy?: string;
|
||||
Desc?: boolean;
|
||||
}
|
||||
|
||||
export interface ConsultationItem {
|
||||
id: string;
|
||||
patientId: string;
|
||||
patientName: string;
|
||||
sessionCode: string;
|
||||
visitDate: string;
|
||||
status: number;
|
||||
chiefComplaint: string | null;
|
||||
symptomsText: string | null;
|
||||
vitalSigns: string | null;
|
||||
diagnosis: string | null;
|
||||
treatmentPlan: string | null;
|
||||
doctorNotes: string | null;
|
||||
createdBy: string;
|
||||
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: string | number;
|
||||
sharedItems: {
|
||||
id: string;
|
||||
prescriptionId: string;
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}[];
|
||||
}[];
|
||||
aggregatedPrescriptionItems: {
|
||||
id: string;
|
||||
prescriptionId: string;
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface AttachmentFile {
|
||||
fileName: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ConsultationDetail {
|
||||
id: string;
|
||||
patientId: string;
|
||||
patientName: string;
|
||||
patientDateOfBirth: string | null;
|
||||
sessionCode: string;
|
||||
visitDate: string;
|
||||
status: string | number;
|
||||
chiefComplaint: string | null;
|
||||
symptomsText: string | null;
|
||||
vitalSigns: string | null;
|
||||
diagnosis: string | null;
|
||||
treatmentPlan: string | null;
|
||||
doctorNotes: string | null;
|
||||
isGlassesDelivered: boolean;
|
||||
createdBy: string;
|
||||
createdByName: string;
|
||||
isDeleted: boolean;
|
||||
generalAttachmentUrls: string[];
|
||||
specialtyClinics: {
|
||||
id: string;
|
||||
specialtyId: string;
|
||||
specialtyName: string;
|
||||
specialtyNotes: string | null;
|
||||
diagnosis: string | null;
|
||||
procedureNotes: string | null;
|
||||
glassRequired: boolean | null;
|
||||
glassType: string | null;
|
||||
isGlassesDelivered: boolean;
|
||||
examinerName: string;
|
||||
specialtyStatus: string | number | undefined;
|
||||
attachmentUrls: string[];
|
||||
sharedItems: {
|
||||
id: string;
|
||||
prescriptionId: string;
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}[];
|
||||
}[];
|
||||
aggregatedPrescriptionItems: {
|
||||
id: string;
|
||||
prescriptionId: string;
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ConsultationResponse {
|
||||
items: ConsultationItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface ReportResponse {
|
||||
fromDate: string;
|
||||
toDate: string;
|
||||
totalSessions: number;
|
||||
totalSpecialtySubSessions: number;
|
||||
specialtyDetails: {
|
||||
specialtyId: string;
|
||||
specialtyCode: string;
|
||||
specialtyName: string;
|
||||
totalCases: number;
|
||||
}[];
|
||||
totalPrescriptionsIssued: number;
|
||||
totalGlassesDelivered: number;
|
||||
}
|
||||
|
||||
const consultationServices = {
|
||||
getConsultations: (params?: GetConsultationParams, tokenAxios?: any) => {
|
||||
return axiosClient.get<ConsultationResponse>(
|
||||
`/api/v1/ExaminationSession/list`,
|
||||
{
|
||||
params,
|
||||
cancelToken: tokenAxios,
|
||||
},
|
||||
);
|
||||
},
|
||||
getConsultationsReport: (
|
||||
params: { FromDate: 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,
|
||||
});
|
||||
},
|
||||
|
||||
detailConsultations: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.get<ConsultationDetail>(
|
||||
`/api/v1/ExaminationSession/${id}`,
|
||||
{
|
||||
cancelToken: tokenAxios,
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
putConsultations: (
|
||||
id: string,
|
||||
data: {
|
||||
chiefComplaint: string | null;
|
||||
symptomsText: string | null;
|
||||
vitalSigns: string | null;
|
||||
diagnosis: string | null;
|
||||
treatmentPlan: string | null;
|
||||
doctorNotes: string | null;
|
||||
isGlassesDelivered: boolean;
|
||||
fileUrls: string[];
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.put(`/api/v1/ExaminationSession/${id}`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
cancelConsultations: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.post(`/api/v1/ExaminationSession/${id}/cancel`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
completeConsultations: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.post(`/api/v1/ExaminationSession/${id}/complete`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
specialtyResults: (
|
||||
specialtyAssignmentId: string,
|
||||
data: {
|
||||
specialtyNotes: string;
|
||||
diagnosis: string;
|
||||
procedureNotes: string;
|
||||
glassRequired: boolean;
|
||||
glassType: string;
|
||||
isGlassesDelivered: boolean;
|
||||
fileUrls: string[];
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.put(
|
||||
`/api/v1/ExaminationSession/specialty-results/${specialtyAssignmentId}`,
|
||||
data,
|
||||
{
|
||||
cancelToken: tokenAxios,
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default consultationServices;
|
||||
@@ -1,118 +0,0 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
export interface PatientItem {
|
||||
id: string;
|
||||
patientCode: string;
|
||||
identificationNumber: string;
|
||||
fullName: string;
|
||||
dateOfBirth: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
emergencyContactName?: string;
|
||||
emergencyContactPhone?: string;
|
||||
allergyNotes?: string;
|
||||
chronicDiseaseNotes?: string;
|
||||
insuranceNumber?: string;
|
||||
notes?: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export interface PatientDetail {
|
||||
id: string;
|
||||
patientCode: string;
|
||||
identificationNumber: string;
|
||||
fullName: string;
|
||||
dateOfBirth: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
emergencyContactName: string | null;
|
||||
emergencyContactPhone: string | null;
|
||||
allergyNotes: string | null;
|
||||
chronicDiseaseNotes: string | null;
|
||||
insuranceNumber: string | null;
|
||||
notes: string | null;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export interface GetPatientParams {
|
||||
Page?: number;
|
||||
PageSize?: number;
|
||||
Search?: string;
|
||||
IdentificationNumber?: string;
|
||||
SortBy?: string;
|
||||
Desc?: boolean;
|
||||
}
|
||||
|
||||
export interface PatientResponse {
|
||||
items: PatientItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const patientServices = {
|
||||
getPatient: (params?: GetPatientParams, tokenAxios?: any) => {
|
||||
return axiosClient.get<PatientResponse>(`/api/v1/Patient/list`, {
|
||||
params,
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
createPatient: (
|
||||
data: {
|
||||
identificationNumber: string;
|
||||
fullName: string;
|
||||
dateOfBirth: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.post(`/api/v1/Patient`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
putPatient: (
|
||||
id: string,
|
||||
data: {
|
||||
fullName: string;
|
||||
gender: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
emergencyContactName: string;
|
||||
emergencyContactPhone: string;
|
||||
allergyNotes: string;
|
||||
chronicDiseaseNotes: string;
|
||||
insuranceNumber: string;
|
||||
notes: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.put(`/api/v1/Patient/${id}`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
detailPatient: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.get<PatientDetail>(`/api/v1/Patient/${id}`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
deletePatient: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.delete(`/api/v1/Patient/${id}`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default patientServices;
|
||||
@@ -1,117 +0,0 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
export interface GetPrescriptionParams {
|
||||
Page?: number;
|
||||
PageSize?: number;
|
||||
Search?: string;
|
||||
SortBy?: string;
|
||||
Desc?: boolean;
|
||||
}
|
||||
|
||||
export interface PrescriptionItem {
|
||||
id: string;
|
||||
examinationSessionId: string;
|
||||
prescriptionCode: string;
|
||||
prescriptionImg: string | null;
|
||||
notes: string;
|
||||
createdBy: string;
|
||||
items: {
|
||||
id: string;
|
||||
prescriptionId: string;
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface PrescriptionDetail {
|
||||
id: string;
|
||||
examinationSessionSpecialtyId: string;
|
||||
prescriptionCode: string;
|
||||
prescriptionImg: string;
|
||||
notes: string;
|
||||
createdBy: string;
|
||||
items: {
|
||||
id: string;
|
||||
prescriptionId: string;
|
||||
medicineName: string;
|
||||
dosage: string;
|
||||
frequency: string;
|
||||
duration: string;
|
||||
instructions: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface PrescriptionResponse {
|
||||
items: PrescriptionItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const prescriptionServices = {
|
||||
/**
|
||||
* CREATE
|
||||
*/
|
||||
createPrescription: (
|
||||
data: {
|
||||
examinationSessionSpecialtyId: string;
|
||||
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,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* GET LIST
|
||||
*/
|
||||
getPrescription: (params?: GetPrescriptionParams, tokenAxios?: any) => {
|
||||
return axiosClient.get<PrescriptionResponse>(`/api/v1/Prescription`, {
|
||||
params,
|
||||
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* UPDATE
|
||||
*/
|
||||
putPrescription: (id: string, data: FormData, tokenAxios?: any) => {
|
||||
return axiosClient.put(`/api/v1/Prescription/${id}`, data, {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* DETAIL
|
||||
*/
|
||||
detailPrescription: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.get<PrescriptionDetail>(`/api/v1/Prescription/${id}`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* UPLOAD IMAGE
|
||||
*/
|
||||
};
|
||||
|
||||
export default prescriptionServices;
|
||||
@@ -1,50 +0,0 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
export interface GetProfileParams {
|
||||
Page?: number;
|
||||
PageSize?: number;
|
||||
Search?: string;
|
||||
SortBy?: string;
|
||||
Desc?: boolean;
|
||||
}
|
||||
|
||||
export interface ProfileResponse {
|
||||
items: ProfileItem[];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export interface ProfileItem {
|
||||
id: string;
|
||||
accountId: string;
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string | null;
|
||||
dateOfBirth: string | null;
|
||||
avatarUrl: string | null;
|
||||
}
|
||||
const profileServices = {
|
||||
getProfile: (params?: GetProfileParams, tokenAxios?: any) => {
|
||||
return axiosClient.get<ProfileResponse>(`/api/v1/UserProfile/list`, {
|
||||
params,
|
||||
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
putProfile: (
|
||||
id: string,
|
||||
data: {
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
birthDate: string;
|
||||
avatarUrl: string;
|
||||
},
|
||||
) => {
|
||||
return axiosClient.put<ProfileItem>(`/api/v1/UserProfile/${id}`, data);
|
||||
},
|
||||
};
|
||||
|
||||
export default profileServices;
|
||||
@@ -1,17 +0,0 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
export interface SpecialtyItem {
|
||||
id: string;
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const specialtyServices = {
|
||||
getSpecialty: (tokenAxios?: any) => {
|
||||
return axiosClient.get<SpecialtyItem[]>("/api/v1/Specialty/lookup", {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default specialtyServices;
|
||||
@@ -1,50 +0,0 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
type UploadPayload = {
|
||||
Files: File[];
|
||||
SessionCode?: string;
|
||||
};
|
||||
|
||||
const uploadServices = {
|
||||
// Accept either: (filesArray, sessionCode?) OR ({ Files, SessionCode })
|
||||
uploadImage: (
|
||||
filesOrPayload: File[] | File | UploadPayload,
|
||||
sessionCode?: string,
|
||||
) => {
|
||||
let files: File[] = [];
|
||||
let session: string | undefined = sessionCode;
|
||||
|
||||
if (!filesOrPayload) return Promise.reject(new Error("No files provided"));
|
||||
|
||||
if (Array.isArray(filesOrPayload)) {
|
||||
files = filesOrPayload as File[];
|
||||
} else if ((filesOrPayload as UploadPayload).Files) {
|
||||
const p = filesOrPayload as UploadPayload;
|
||||
files = p.Files || [];
|
||||
session = p.SessionCode || session;
|
||||
} else {
|
||||
files = [filesOrPayload as File];
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
files.forEach((file) => {
|
||||
formData.append("Files", file);
|
||||
});
|
||||
|
||||
if (session) {
|
||||
formData.append("SessionCode", session);
|
||||
}
|
||||
|
||||
// Override content-type for this request so multipart boundary is set correctly
|
||||
return axiosClient.post("/api/Media/upload", formData, {
|
||||
headers: {
|
||||
// Let browser/axios set the correct boundary; setting to multipart/form-data
|
||||
// overrides the default 'application/json' header defined on axiosClient.
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default uploadServices;
|
||||
@@ -1,90 +0,0 @@
|
||||
import axiosClient from ".";
|
||||
|
||||
interface GetUserParams {
|
||||
Page?: number;
|
||||
PageSize?: number;
|
||||
Search?: string;
|
||||
Role?: number;
|
||||
IsActive?: boolean;
|
||||
SortBy?: string;
|
||||
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;
|
||||
}
|
||||
|
||||
export interface UserDetail {
|
||||
id: string;
|
||||
username: string;
|
||||
role?: number;
|
||||
roleType?: number | string | null;
|
||||
isActive?: boolean;
|
||||
fullName: string | null;
|
||||
email: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
const userServices = {
|
||||
getUser: (params?: GetUserParams, tokenAxios?: any) => {
|
||||
return axiosClient.get(`/api/v1/User/list`, {
|
||||
params,
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
postUser: (
|
||||
data: {
|
||||
username: string;
|
||||
password: string;
|
||||
role: number;
|
||||
roleType: number;
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.post(`/api/v1/Users/`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
putUser: (
|
||||
id: string,
|
||||
data: {
|
||||
role: number;
|
||||
roleType: number;
|
||||
fullName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
},
|
||||
tokenAxios?: any,
|
||||
) => {
|
||||
return axiosClient.put(`/api/v1/Users/${id}`, data, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
deleteUser: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.delete(`/api/v1/Users/${id}`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
detailUser: (id: string, tokenAxios?: any) => {
|
||||
return axiosClient.get(`/api/v1/Users/${id}`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
meUser: (tokenAxios?: any) => {
|
||||
return axiosClient.get<UserMe>(`/api/v1/Users/me`, {
|
||||
cancelToken: tokenAxios,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default userServices;
|
||||
@@ -151,6 +151,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.26.0":
|
||||
version: 7.29.7
|
||||
resolution: "@babel/runtime@npm:7.29.7"
|
||||
checksum: 10c0/ca11572f7146b21e0bde6a9ed4bb6a89eafbee5f0944c7eb54d0d8a2dac962c33638a1d611e14faa71dfbb92b4b5f9236232208568a6b7d5c6f3f39ddb91771e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/template@npm:^7.28.6":
|
||||
version: 7.28.6
|
||||
resolution: "@babel/template@npm:7.28.6"
|
||||
@@ -187,6 +194,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@dimforge/rapier3d-compat@npm:~0.12.0":
|
||||
version: 0.12.0
|
||||
resolution: "@dimforge/rapier3d-compat@npm:0.12.0"
|
||||
checksum: 10c0/c66c24f90649c0fc870679c12e7fec1a111080d44450169b57561f957d7b6b284ad8a3ceeba95533e213176ea171351acebd3dd43885fafb33f18bfbd9d507db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@emnapi/core@npm:1.10.0, @emnapi/core@npm:^1.10.0":
|
||||
version: 1.10.0
|
||||
resolution: "@emnapi/core@npm:1.10.0"
|
||||
@@ -653,6 +667,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mediapipe/tasks-vision@npm:0.10.17":
|
||||
version: 0.10.17
|
||||
resolution: "@mediapipe/tasks-vision@npm:0.10.17"
|
||||
checksum: 10c0/f2f5dd9ca39d562b902a6d964f8b786f17a143f42776d0d26b8c79632b10cf210e28d98ec92de757b7bb2e6595ed4f9bbf5e49f1709b09dc08709bd276ced442
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@monogrid/gainmap-js@npm:^3.0.6":
|
||||
version: 3.4.0
|
||||
resolution: "@monogrid/gainmap-js@npm:3.4.0"
|
||||
dependencies:
|
||||
promise-worker-transferable: "npm:^1.0.4"
|
||||
peerDependencies:
|
||||
three: ">= 0.159.0"
|
||||
checksum: 10c0/06104767a0f542b36e1e203fe569f78996babcdd52661f42db4f079f372b272e0849b86f84f7690b2c165a8ba931f77ad324fe82a09c9b23e26d20e76cabd3e7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@napi-rs/wasm-runtime@npm:^1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "@napi-rs/wasm-runtime@npm:1.1.4"
|
||||
@@ -1188,6 +1220,83 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-three/drei@npm:^10.7.8":
|
||||
version: 10.7.8
|
||||
resolution: "@react-three/drei@npm:10.7.8"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.26.0"
|
||||
"@mediapipe/tasks-vision": "npm:0.10.17"
|
||||
"@monogrid/gainmap-js": "npm:^3.0.6"
|
||||
"@use-gesture/react": "npm:^10.3.1"
|
||||
camera-controls: "npm:^3.1.0"
|
||||
cross-env: "npm:^7.0.3"
|
||||
detect-gpu: "npm:^5.0.56"
|
||||
glsl-noise: "npm:^0.0.0"
|
||||
hls.js: "npm:^1.5.17"
|
||||
maath: "npm:^0.10.8"
|
||||
meshline: "npm:^3.3.1"
|
||||
stats-gl: "npm:^2.2.8"
|
||||
stats.js: "npm:^0.17.0"
|
||||
suspend-react: "npm:^0.1.3"
|
||||
three-mesh-bvh: "npm:^0.8.3"
|
||||
three-stdlib: "npm:^2.35.6"
|
||||
troika-three-text: "npm:^0.52.4"
|
||||
tunnel-rat: "npm:^0.1.2"
|
||||
use-sync-external-store: "npm:^1.4.0"
|
||||
utility-types: "npm:^3.11.0"
|
||||
zustand: "npm:^5.0.1"
|
||||
peerDependencies:
|
||||
"@react-three/fiber": ^9.0.0
|
||||
react: ^19
|
||||
react-dom: ^19
|
||||
three: ">=0.159"
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: 10c0/b4103a79f2cebfd6ff00a869de38e602ad670df14bd08c2fa0d44b71ebebe81b000b523077df01df8c358fe406a4e1ff75c44eb727bca216d3407c1d7e91192e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-three/fiber@npm:^9.7.0":
|
||||
version: 9.7.0
|
||||
resolution: "@react-three/fiber@npm:9.7.0"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.17.8"
|
||||
"@types/webxr": "npm:*"
|
||||
base64-js: "npm:^1.5.1"
|
||||
buffer: "npm:^6.0.3"
|
||||
its-fine: "npm:^2.0.0"
|
||||
react-use-measure: "npm:^2.1.7"
|
||||
scheduler: "npm:^0.27.0"
|
||||
suspend-react: "npm:^0.1.3"
|
||||
use-sync-external-store: "npm:^1.4.0"
|
||||
zustand: "npm:^5.0.3"
|
||||
peerDependencies:
|
||||
expo: ">=43.0"
|
||||
expo-asset: ">=8.4"
|
||||
expo-file-system: ">=11.0"
|
||||
expo-gl: ">=11.0"
|
||||
react: ">=19 <19.3"
|
||||
react-dom: ">=19 <19.3"
|
||||
react-native: ">=0.78"
|
||||
three: ">=0.156"
|
||||
peerDependenciesMeta:
|
||||
expo:
|
||||
optional: true
|
||||
expo-asset:
|
||||
optional: true
|
||||
expo-file-system:
|
||||
optional: true
|
||||
expo-gl:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
checksum: 10c0/7ad36da135b5105e379c4984455dab1bd7cfabfad7548e4ff87ee161f8265be5d9e9af7f00a761c7f3a4252dc2db7c7ea8b9bfd9db7f601dd9759ee1179dc533
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@reduxjs/toolkit@npm:^2.12.0":
|
||||
version: 2.12.0
|
||||
resolution: "@reduxjs/toolkit@npm:2.12.0"
|
||||
@@ -1422,6 +1531,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tweenjs/tween.js@npm:~23.1.3":
|
||||
version: 23.1.3
|
||||
resolution: "@tweenjs/tween.js@npm:23.1.3"
|
||||
checksum: 10c0/811b30f5f0e7409fb41833401c501c2d6f600eb5f43039dd9067a7f70aff6dad5f5ce1233848e13f0b33a269a160d9c133f344d986cbff4f1f6b72ddecd06c89
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tybys/wasm-util@npm:^0.10.1":
|
||||
version: 0.10.2
|
||||
resolution: "@tybys/wasm-util@npm:0.10.2"
|
||||
@@ -1431,6 +1547,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/draco3d@npm:^1.4.0":
|
||||
version: 1.4.10
|
||||
resolution: "@types/draco3d@npm:1.4.10"
|
||||
checksum: 10c0/431e333b2fd67e2b081e8697e71dfb82a125fc04b2cbfa0205e4d521719d3749c964b2bf82bb7944ba0a12e5bc3d9afe387a58e68b195d838fcdbc65c8572a35
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/estree@npm:^1.0.6":
|
||||
version: 1.0.9
|
||||
resolution: "@types/estree@npm:1.0.9"
|
||||
@@ -1475,6 +1598,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/offscreencanvas@npm:^2019.6.4":
|
||||
version: 2019.7.3
|
||||
resolution: "@types/offscreencanvas@npm:2019.7.3"
|
||||
checksum: 10c0/6d1dfae721d321cd2b5435f347a0e53b09f33b2f9e9333396480f592823bc323847b8169f7d251d2285cb93dbc1ba2e30741ac5cf4b1c003d660fd4c24526963
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-dom@npm:^19":
|
||||
version: 19.2.3
|
||||
resolution: "@types/react-dom@npm:19.2.3"
|
||||
@@ -1484,6 +1614,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-reconciler@npm:^0.28.9":
|
||||
version: 0.28.9
|
||||
resolution: "@types/react-reconciler@npm:0.28.9"
|
||||
peerDependencies:
|
||||
"@types/react": "*"
|
||||
checksum: 10c0/9fe71fa856aab2cd4742fe0416bdd4f5c82ecc05ef6451ee7fcb65a5efdf5fa588f5820fbe2a665b15371b0da3bfc4097f28bb6d450b9a834af2d0fc00f403bd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react@npm:^19":
|
||||
version: 19.2.15
|
||||
resolution: "@types/react@npm:19.2.15"
|
||||
@@ -1493,6 +1632,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/stats.js@npm:*":
|
||||
version: 0.17.4
|
||||
resolution: "@types/stats.js@npm:0.17.4"
|
||||
checksum: 10c0/4fe0429998519f0476f03a25b4900b4d4a1474606478657271e40a884f7936ba902ea564b1c95cfd33a8e84af46cef6e1e98bb23e86fd3b6676cd5b974987151
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/three@npm:*, @types/three@npm:^0":
|
||||
version: 0.185.4
|
||||
resolution: "@types/three@npm:0.185.4"
|
||||
dependencies:
|
||||
"@dimforge/rapier3d-compat": "npm:~0.12.0"
|
||||
"@tweenjs/tween.js": "npm:~23.1.3"
|
||||
"@types/stats.js": "npm:*"
|
||||
"@types/webxr": "npm:>=0.5.17"
|
||||
fflate: "npm:~0.8.2"
|
||||
meshoptimizer: "npm:~1.1.1"
|
||||
checksum: 10c0/f9cc28d3f343067e56741017bb0ac0257d8d2994707c2ee69512b668a57c8cd8892798978e54b2224920e9495eebe0878fcd8ea18b9a5ab555a7d445c689f804
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/use-sync-external-store@npm:^0.0.6":
|
||||
version: 0.0.6
|
||||
resolution: "@types/use-sync-external-store@npm:0.0.6"
|
||||
@@ -1500,6 +1660,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/webxr@npm:*, @types/webxr@npm:>=0.5.17, @types/webxr@npm:^0.5.2":
|
||||
version: 0.5.24
|
||||
resolution: "@types/webxr@npm:0.5.24"
|
||||
checksum: 10c0/ff59ffd390d06ca3f89ab2531d583ac10bc5e2ab82e5a01ecc40fbc365224a3375e7caa5b14649cf6141db21fb024940da7ad2bd8faa3cd497a6665257cb53b5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@typescript-eslint/eslint-plugin@npm:8.59.4":
|
||||
version: 8.59.4
|
||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.59.4"
|
||||
@@ -1793,6 +1960,65 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@use-gesture/core@npm:10.3.1":
|
||||
version: 10.3.1
|
||||
resolution: "@use-gesture/core@npm:10.3.1"
|
||||
checksum: 10c0/2e3b5c0f7fe26cdb47be3a9c2a58a6a9edafc5b2895b07d2898eda9ab5a2b29fb0098b15597baa0856907b593075cd44cc69bba4785c9cfb7b6fabaa3b52cd3e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@use-gesture/react@npm:^10.3.1":
|
||||
version: 10.3.1
|
||||
resolution: "@use-gesture/react@npm:10.3.1"
|
||||
dependencies:
|
||||
"@use-gesture/core": "npm:10.3.1"
|
||||
peerDependencies:
|
||||
react: ">= 16.8.0"
|
||||
checksum: 10c0/978da66e4e7c424866ad52eba8fdf0ce93a4c8fc44f8837c7043e68c6a6107cd67e817fffb27f7db2ae871ef2f6addb0c8ddf1586f24c67b7e6aef1646c668cf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"TEDI-DDP@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "TEDI-DDP@workspace:."
|
||||
dependencies:
|
||||
"@radix-ui/react-popover": "npm:^1.1.15"
|
||||
"@radix-ui/react-tooltip": "npm:^1.2.8"
|
||||
"@react-three/drei": "npm:^10.7.8"
|
||||
"@react-three/fiber": "npm:^9.7.0"
|
||||
"@reduxjs/toolkit": "npm:^2.12.0"
|
||||
"@tailwindcss/postcss": "npm:^4"
|
||||
"@tanstack/react-query": "npm:^5.100.11"
|
||||
"@types/md5": "npm:^2"
|
||||
"@types/node": "npm:^20"
|
||||
"@types/nprogress": "npm:^0"
|
||||
"@types/react": "npm:^19"
|
||||
"@types/react-dom": "npm:^19"
|
||||
"@types/three": "npm:^0"
|
||||
axios: "npm:^1.16.1"
|
||||
babel-plugin-react-compiler: "npm:1.0.0"
|
||||
clsx: "npm:^2.1.1"
|
||||
eslint: "npm:^9"
|
||||
eslint-config-next: "npm:16.2.6"
|
||||
lottie-react: "npm:^2.4.1"
|
||||
lucide-react: "npm:^1.16.0"
|
||||
md5: "npm:^2.3.0"
|
||||
moment: "npm:^2.30.1"
|
||||
next: "npm:16.2.6"
|
||||
nprogress: "npm:^0.2.0"
|
||||
react: "npm:19.2.4"
|
||||
react-dom: "npm:19.2.4"
|
||||
react-moment: "npm:^2.0.2"
|
||||
react-redux: "npm:^9.3.0"
|
||||
react-toastify: "npm:^11.1.0"
|
||||
tailwindcss: "npm:^4"
|
||||
three: "npm:^0.185.1"
|
||||
typescript: "npm:^5"
|
||||
zod: "npm:^4.4.3"
|
||||
zustand: "npm:^5.0.14"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"acorn-jsx@npm:^5.3.2":
|
||||
version: 5.3.2
|
||||
resolution: "acorn-jsx@npm:5.3.2"
|
||||
@@ -2057,6 +2283,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64-js@npm:^1.3.1, base64-js@npm:^1.5.1":
|
||||
version: 1.5.1
|
||||
resolution: "base64-js@npm:1.5.1"
|
||||
checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"baseline-browser-mapping@npm:^2.10.12, baseline-browser-mapping@npm:^2.9.19":
|
||||
version: 2.10.31
|
||||
resolution: "baseline-browser-mapping@npm:2.10.31"
|
||||
@@ -2066,6 +2299,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bidi-js@npm:^1.0.2":
|
||||
version: 1.0.3
|
||||
resolution: "bidi-js@npm:1.0.3"
|
||||
dependencies:
|
||||
require-from-string: "npm:^2.0.2"
|
||||
checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^1.1.7":
|
||||
version: 1.1.14
|
||||
resolution: "brace-expansion@npm:1.1.14"
|
||||
@@ -2109,6 +2351,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer@npm:^6.0.3":
|
||||
version: 6.0.3
|
||||
resolution: "buffer@npm:6.0.3"
|
||||
dependencies:
|
||||
base64-js: "npm:^1.3.1"
|
||||
ieee754: "npm:^1.2.1"
|
||||
checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "call-bind-apply-helpers@npm:1.0.2"
|
||||
@@ -2148,6 +2400,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"camera-controls@npm:^3.1.0":
|
||||
version: 3.1.2
|
||||
resolution: "camera-controls@npm:3.1.2"
|
||||
peerDependencies:
|
||||
three: ">=0.126.1"
|
||||
checksum: 10c0/2d42162efd6789e713332a72212819fb04b261f7babc45b5794cd902997e4a4826c9401b672ffab2e09f9808fc2b615d9154d67c57f38a7c90487d4cf5429655
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001782":
|
||||
version: 1.0.30001793
|
||||
resolution: "caniuse-lite@npm:1.0.30001793"
|
||||
@@ -2225,7 +2486,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cross-spawn@npm:^7.0.6":
|
||||
"cross-env@npm:^7.0.3":
|
||||
version: 7.0.3
|
||||
resolution: "cross-env@npm:7.0.3"
|
||||
dependencies:
|
||||
cross-spawn: "npm:^7.0.1"
|
||||
bin:
|
||||
cross-env: src/bin/cross-env.js
|
||||
cross-env-shell: src/bin/cross-env-shell.js
|
||||
checksum: 10c0/f3765c25746c69fcca369655c442c6c886e54ccf3ab8c16847d5ad0e91e2f337d36eedc6599c1227904bf2a228d721e690324446876115bc8e7b32a866735ecf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.6":
|
||||
version: 7.0.6
|
||||
resolution: "cross-spawn@npm:7.0.6"
|
||||
dependencies:
|
||||
@@ -2347,6 +2620,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-gpu@npm:^5.0.56":
|
||||
version: 5.0.70
|
||||
resolution: "detect-gpu@npm:5.0.70"
|
||||
dependencies:
|
||||
webgl-constants: "npm:^1.1.1"
|
||||
checksum: 10c0/5a2053d4779edb3490821599b316b5362c8b55c30b82806c64bcfd21f40c67e2debfd677937899080aea2b27765471552bafeeae9c9f1cf63bdd07461a7e2163
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-libc@npm:^2.0.3, detect-libc@npm:^2.1.2":
|
||||
version: 2.1.2
|
||||
resolution: "detect-libc@npm:2.1.2"
|
||||
@@ -2370,6 +2652,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"draco3d@npm:^1.4.1":
|
||||
version: 1.5.7
|
||||
resolution: "draco3d@npm:1.5.7"
|
||||
checksum: 10c0/4419509bb93c31560a22a1a54e1c394a5b5017cab39941120c75151d941c11dec05925abf31f597ac2694c570b78c04f82aa3d69e5311f8f8e71fc8b9d92d12f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "dunder-proto@npm:1.0.1"
|
||||
@@ -2905,6 +3194,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fflate@npm:^0.6.9":
|
||||
version: 0.6.11
|
||||
resolution: "fflate@npm:0.6.11"
|
||||
checksum: 10c0/5bbf4a7557db6f65b25c7bb12621008c400a116ec1cc65bea78abd75c0c049d8a22fb92f0a59fbf9a7ad1a24a47d2112ce10fd817d08db0e1bf7dc82e780faa1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fflate@npm:~0.8.2":
|
||||
version: 0.8.3
|
||||
resolution: "fflate@npm:0.8.3"
|
||||
checksum: 10c0/eab181ca37f5348ae76d4b6f840e0026e30220e33153289ac942222d8b9638237d486507dbcc09878d724095bd354993a2ee48bbee99c8f2c6440d4448719aa7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"file-entry-cache@npm:^8.0.0":
|
||||
version: 8.0.0
|
||||
resolution: "file-entry-cache@npm:8.0.0"
|
||||
@@ -3124,6 +3427,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glsl-noise@npm:^0.0.0":
|
||||
version: 0.0.0
|
||||
resolution: "glsl-noise@npm:0.0.0"
|
||||
checksum: 10c0/442630e86ed109079f4ed92f3b59d24d0b9da60c03cb8ac9313a056996bf4b5d696b4c6121ee29952e8a19c5cf174a999cfe672464959db36fc75334c3134677
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"gopd@npm:^1.0.1, gopd@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "gopd@npm:1.2.0"
|
||||
@@ -3211,6 +3521,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hls.js@npm:^1.5.17":
|
||||
version: 1.6.18
|
||||
resolution: "hls.js@npm:1.6.18"
|
||||
checksum: 10c0/f1601ed4da27e09341f2dd2ac29951d30fb0c78581c405217a8286ec3962e50a181eb82e362dccf828acd364deae1fe0236fc8e6aeed9b2e990ac61e2bd82a77
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"https-proxy-agent@npm:^5.0.1":
|
||||
version: 5.0.1
|
||||
resolution: "https-proxy-agent@npm:5.0.1"
|
||||
@@ -3221,6 +3538,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ieee754@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "ieee754@npm:1.2.1"
|
||||
checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ignore@npm:^5.2.0":
|
||||
version: 5.3.2
|
||||
resolution: "ignore@npm:5.3.2"
|
||||
@@ -3235,6 +3559,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"immediate@npm:~3.0.5":
|
||||
version: 3.0.6
|
||||
resolution: "immediate@npm:3.0.6"
|
||||
checksum: 10c0/f8ba7ede69bee9260241ad078d2d535848745ff5f6995c7c7cb41cfdc9ccc213f66e10fa5afb881f90298b24a3f7344b637b592beb4f54e582770cdce3f1f039
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"immer@npm:^11.0.0":
|
||||
version: 11.1.8
|
||||
resolution: "immer@npm:11.1.8"
|
||||
@@ -3435,6 +3766,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-promise@npm:^2.1.0":
|
||||
version: 2.2.2
|
||||
resolution: "is-promise@npm:2.2.2"
|
||||
checksum: 10c0/2dba959812380e45b3df0fb12e7cb4d4528c989c7abb03ececb1d1fd6ab1cbfee956ca9daa587b9db1d8ac3c1e5738cf217bdb3dfd99df8c691be4c00ae09069
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"is-regex@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "is-regex@npm:1.2.1"
|
||||
@@ -3547,6 +3885,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"its-fine@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "its-fine@npm:2.0.0"
|
||||
dependencies:
|
||||
"@types/react-reconciler": "npm:^0.28.9"
|
||||
peerDependencies:
|
||||
react: ^19.0.0
|
||||
checksum: 10c0/1ff1ff3257c0c7eb115c9c26cf0506eb84162edc1a63d3136780d146f7c7833298b240d0fcb46888909773f1a7d16539e0c03f2482cff1a5a502d6436686fe21
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jiti@npm:^2.6.1":
|
||||
version: 2.7.0
|
||||
resolution: "jiti@npm:2.7.0"
|
||||
@@ -3645,41 +3994,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"kham-benh-mo-pho@workspace:.":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "kham-benh-mo-pho@workspace:."
|
||||
dependencies:
|
||||
"@radix-ui/react-popover": "npm:^1.1.15"
|
||||
"@radix-ui/react-tooltip": "npm:^1.2.8"
|
||||
"@reduxjs/toolkit": "npm:^2.12.0"
|
||||
"@tailwindcss/postcss": "npm:^4"
|
||||
"@tanstack/react-query": "npm:^5.100.11"
|
||||
"@types/md5": "npm:^2"
|
||||
"@types/node": "npm:^20"
|
||||
"@types/nprogress": "npm:^0"
|
||||
"@types/react": "npm:^19"
|
||||
"@types/react-dom": "npm:^19"
|
||||
axios: "npm:^1.16.1"
|
||||
babel-plugin-react-compiler: "npm:1.0.0"
|
||||
clsx: "npm:^2.1.1"
|
||||
eslint: "npm:^9"
|
||||
eslint-config-next: "npm:16.2.6"
|
||||
lottie-react: "npm:^2.4.1"
|
||||
lucide-react: "npm:^1.16.0"
|
||||
md5: "npm:^2.3.0"
|
||||
moment: "npm:^2.30.1"
|
||||
next: "npm:16.2.6"
|
||||
nprogress: "npm:^0.2.0"
|
||||
react: "npm:19.2.4"
|
||||
react-dom: "npm:19.2.4"
|
||||
react-moment: "npm:^2.0.2"
|
||||
react-redux: "npm:^9.3.0"
|
||||
react-toastify: "npm:^11.1.0"
|
||||
tailwindcss: "npm:^4"
|
||||
typescript: "npm:^5"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"language-subtag-registry@npm:^0.3.20":
|
||||
version: 0.3.23
|
||||
resolution: "language-subtag-registry@npm:0.3.23"
|
||||
@@ -3706,6 +4020,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lie@npm:^3.0.2":
|
||||
version: 3.3.0
|
||||
resolution: "lie@npm:3.3.0"
|
||||
dependencies:
|
||||
immediate: "npm:~3.0.5"
|
||||
checksum: 10c0/56dd113091978f82f9dc5081769c6f3b947852ecf9feccaf83e14a123bc630c2301439ce6182521e5fbafbde88e88ac38314327a4e0493a1bea7e0699a7af808
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lightningcss-android-arm64@npm:1.32.0":
|
||||
version: 1.32.0
|
||||
resolution: "lightningcss-android-arm64@npm:1.32.0"
|
||||
@@ -3890,6 +4213,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"maath@npm:^0.10.8":
|
||||
version: 0.10.8
|
||||
resolution: "maath@npm:0.10.8"
|
||||
peerDependencies:
|
||||
"@types/three": ">=0.134.0"
|
||||
three: ">=0.134.0"
|
||||
checksum: 10c0/fb61faac89567595f7e25f261f8a9750d0ced674fd16c77c6e486a0c4a00c572fa37969d613d017380f8ea4e8ff357d481dcd94240a104a7969bc7e95b9fe99e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"magic-string@npm:^0.30.21":
|
||||
version: 0.30.21
|
||||
resolution: "magic-string@npm:0.30.21"
|
||||
@@ -3924,6 +4257,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"meshline@npm:^3.3.1":
|
||||
version: 3.3.1
|
||||
resolution: "meshline@npm:3.3.1"
|
||||
peerDependencies:
|
||||
three: ">=0.137"
|
||||
checksum: 10c0/95d5c7c90aca350530f37f9ea5c5e333a66ea0c50416fa6372c24a23321837a4c70d92d6a9a5942ee124484855a2d3fc8ca0656305feb8d8f86aa10513e324e4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"meshoptimizer@npm:~1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "meshoptimizer@npm:1.1.1"
|
||||
checksum: 10c0/aacd816cbe206e49f16c88ed8b13deafec40cdb8b844080b6d614b4d945dfc7242d5dccfbc1269375a5d4dce2f354261c107d3ce6e564961bd9a220e4616dafb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"micromatch@npm:^4.0.4":
|
||||
version: 4.0.8
|
||||
resolution: "micromatch@npm:4.0.8"
|
||||
@@ -4305,6 +4654,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"potpack@npm:^1.0.1":
|
||||
version: 1.0.2
|
||||
resolution: "potpack@npm:1.0.2"
|
||||
checksum: 10c0/670c23898a4257130858b960c2e654d3327c0f6a7e7091ff5846f213e65af8f9476320b995b8ad561a47a4d1c359c7ef347de57d22e7b02597051abb52bc85c4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prelude-ls@npm:^1.2.1":
|
||||
version: 1.2.1
|
||||
resolution: "prelude-ls@npm:1.2.1"
|
||||
@@ -4312,6 +4668,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"promise-worker-transferable@npm:^1.0.4":
|
||||
version: 1.0.4
|
||||
resolution: "promise-worker-transferable@npm:1.0.4"
|
||||
dependencies:
|
||||
is-promise: "npm:^2.1.0"
|
||||
lie: "npm:^3.0.2"
|
||||
checksum: 10c0/110d273cbba2631c318762f02c159531f1e05e4af91080368b9d4c02b880824ae4c49a8ec7094c6434989ea2ee8de913b21c809279babeaf447fc90a682dcd7c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prop-types@npm:^15.8.1":
|
||||
version: 15.8.1
|
||||
resolution: "prop-types@npm:15.8.1"
|
||||
@@ -4458,6 +4824,19 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-use-measure@npm:^2.1.7":
|
||||
version: 2.1.7
|
||||
resolution: "react-use-measure@npm:2.1.7"
|
||||
peerDependencies:
|
||||
react: ">=16.13"
|
||||
react-dom: ">=16.13"
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: 10c0/ff24130e6f95e853feb6892fb74af08dbc5aae3574b701169e3bc3adb392c3162f51a58ddfe39bb7337db13ae609bbec0bb51a9de8b5fae5420f9d17e1f8b542
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react@npm:19.2.4":
|
||||
version: 19.2.4
|
||||
resolution: "react@npm:19.2.4"
|
||||
@@ -4511,6 +4890,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"require-from-string@npm:^2.0.2":
|
||||
version: 2.0.2
|
||||
resolution: "require-from-string@npm:2.0.2"
|
||||
checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"reselect@npm:^5.1.0":
|
||||
version: 5.2.0
|
||||
resolution: "reselect@npm:5.2.0"
|
||||
@@ -4838,6 +5224,26 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stats-gl@npm:^2.2.8":
|
||||
version: 2.4.2
|
||||
resolution: "stats-gl@npm:2.4.2"
|
||||
dependencies:
|
||||
"@types/three": "npm:*"
|
||||
three: "npm:^0.170.0"
|
||||
peerDependencies:
|
||||
"@types/three": "*"
|
||||
three: "*"
|
||||
checksum: 10c0/cff079103d4004cdfd72d65c977b7237a929b02f57b275081188e9313d74620b70ea0a57e0561fb6a43b1597830068b4adb2625dbc08b99799fd12a71ec54729
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stats.js@npm:^0.17.0":
|
||||
version: 0.17.0
|
||||
resolution: "stats.js@npm:0.17.0"
|
||||
checksum: 10c0/03392b4b9eb15cb285b8cf46c5dd68e4e7b556d8e4c72a3a48e6bd2ab6f2e72a02b145d465f594450df081d8f94a45e8ac1112a0d507a200a5768504082ca080
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stop-iteration-iterator@npm:^1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "stop-iteration-iterator@npm:1.1.0"
|
||||
@@ -4974,6 +5380,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"suspend-react@npm:^0.1.3":
|
||||
version: 0.1.3
|
||||
resolution: "suspend-react@npm:0.1.3"
|
||||
peerDependencies:
|
||||
react: ">=17.0"
|
||||
checksum: 10c0/c35436f492a5cab85a2aac8f5ee5af8f67edbab3a49e12d15c7f770aae4cf09fe2925b3b8c122929210c1f28aa0958f91684a687d6608f116f7da8406d8a2f1c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tailwindcss@npm:4.3.0, tailwindcss@npm:^4":
|
||||
version: 4.3.0
|
||||
resolution: "tailwindcss@npm:4.3.0"
|
||||
@@ -4988,6 +5403,45 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"three-mesh-bvh@npm:^0.8.3":
|
||||
version: 0.8.3
|
||||
resolution: "three-mesh-bvh@npm:0.8.3"
|
||||
peerDependencies:
|
||||
three: ">= 0.159.0"
|
||||
checksum: 10c0/d1f052d4606498acf57e08e28d01c84157eb388d46be8e03bc839630dc4eb919d5bb5e2ed4e9113d39cbc3cda29f09212e3450e1bb21de6615ec330f2575fc28
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"three-stdlib@npm:^2.35.6":
|
||||
version: 2.36.1
|
||||
resolution: "three-stdlib@npm:2.36.1"
|
||||
dependencies:
|
||||
"@types/draco3d": "npm:^1.4.0"
|
||||
"@types/offscreencanvas": "npm:^2019.6.4"
|
||||
"@types/webxr": "npm:^0.5.2"
|
||||
draco3d: "npm:^1.4.1"
|
||||
fflate: "npm:^0.6.9"
|
||||
potpack: "npm:^1.0.1"
|
||||
peerDependencies:
|
||||
three: ">=0.128.0"
|
||||
checksum: 10c0/6d04a3e6e1cf5a23e1d8807085609e5cfa999d342a8f4a3168432c4152e99ab555f7fd7ab22903d9639f33d8d19d2183c62931be34ad6c3ac8bd3ed879d973c2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"three@npm:^0.170.0":
|
||||
version: 0.170.0
|
||||
resolution: "three@npm:0.170.0"
|
||||
checksum: 10c0/eeaa1eccb31467654a044fb90634b3b6bb8d0768a4f029c082fb6aa22aec0d6362d0e3af74938caa1db512b7823a031b54a76e88fba6cfd535054165ea9667c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"three@npm:^0.185.1":
|
||||
version: 0.185.1
|
||||
resolution: "three@npm:0.185.1"
|
||||
checksum: 10c0/d15d3fa934f53c9fd6706965ff9946f55653bc728cf582e68acb03421c0179d7662f7d270e14bcf72923e140c36779e7fba6b0f6e542e07deade7342f63c8bdf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.15":
|
||||
version: 0.2.16
|
||||
resolution: "tinyglobby@npm:0.2.16"
|
||||
@@ -5007,6 +5461,36 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"troika-three-text@npm:^0.52.4":
|
||||
version: 0.52.5
|
||||
resolution: "troika-three-text@npm:0.52.5"
|
||||
dependencies:
|
||||
bidi-js: "npm:^1.0.2"
|
||||
troika-three-utils: "npm:^0.52.5"
|
||||
troika-worker-utils: "npm:^0.52.0"
|
||||
webgl-sdf-generator: "npm:1.1.1"
|
||||
peerDependencies:
|
||||
three: ">=0.125.0"
|
||||
checksum: 10c0/0c3dbe6bc0addf43dbbec2a5d2deafe18775d3971df745655b86ed08341940ab6fed6a2e2082779656709081fd5113cc6deb1f4808aa10dc6e913827d2f1ea4a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"troika-three-utils@npm:^0.52.5":
|
||||
version: 0.52.5
|
||||
resolution: "troika-three-utils@npm:0.52.5"
|
||||
peerDependencies:
|
||||
three: ">=0.125.0"
|
||||
checksum: 10c0/97e5bdf78f8633c42f0665b39320c890fa898ff6e9f521a06e3dd1f0b97c235b201e0ab0c3813c9e0a49e2be82f3d14509a02ef1d830b2190d244d2fdc48f0bb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"troika-worker-utils@npm:^0.52.0":
|
||||
version: 0.52.0
|
||||
resolution: "troika-worker-utils@npm:0.52.0"
|
||||
checksum: 10c0/bcd776324ce3e941c1a913531cee6022c867b71bc2d6bb67f1eadf6df96fd4f865c51b90ac58fc5c0ae3346a4bd8360b0a52398864800a7a9c60473151d452d5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ts-api-utils@npm:^2.5.0":
|
||||
version: 2.5.0
|
||||
resolution: "ts-api-utils@npm:2.5.0"
|
||||
@@ -5035,6 +5519,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tunnel-rat@npm:^0.1.2":
|
||||
version: 0.1.2
|
||||
resolution: "tunnel-rat@npm:0.1.2"
|
||||
dependencies:
|
||||
zustand: "npm:^4.3.2"
|
||||
checksum: 10c0/93cd50c7c9141e2662707602a21401145092e5a3c815b57a752937419ab6187a2ff36fa7e0f65e0c587022149bf2d323ace07dff61106511b7d4845e53390cc9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-check@npm:^0.4.0, type-check@npm:~0.4.0":
|
||||
version: 0.4.0
|
||||
resolution: "type-check@npm:0.4.0"
|
||||
@@ -5281,7 +5774,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"use-sync-external-store@npm:^1.4.0":
|
||||
"use-sync-external-store@npm:^1.2.2, use-sync-external-store@npm:^1.4.0":
|
||||
version: 1.6.0
|
||||
resolution: "use-sync-external-store@npm:1.6.0"
|
||||
peerDependencies:
|
||||
@@ -5290,6 +5783,27 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"utility-types@npm:^3.11.0":
|
||||
version: 3.11.0
|
||||
resolution: "utility-types@npm:3.11.0"
|
||||
checksum: 10c0/2f1580137b0c3e6cf5405f37aaa8f5249961a76d26f1ca8efc0ff49a2fc0e0b2db56de8e521a174d075758e0c7eb3e590edec0832eb44478b958f09914920f19
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webgl-constants@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "webgl-constants@npm:1.1.1"
|
||||
checksum: 10c0/525a9838b0cbcbab902d1bc5457882424a9c146f851042c901ae457123193f5c5e47846b47c1c999e4f3abc24322cac494cea0cc52fec28bd8eb3a05cd79d573
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webgl-sdf-generator@npm:1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "webgl-sdf-generator@npm:1.1.1"
|
||||
checksum: 10c0/769bfab22a9485a59bda9448d1d8684228adc9ebdac2cc69012a0854282385d1bef319f703758829e2c647e7532a9844e62977110e825bd75a3e0b8a0ee276ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "which-boxed-primitive@npm:1.1.1"
|
||||
@@ -5392,9 +5906,50 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zod@npm:^3.25.0 || ^4.0.0":
|
||||
"zod@npm:^3.25.0 || ^4.0.0, zod@npm:^4.4.3":
|
||||
version: 4.4.3
|
||||
resolution: "zod@npm:4.4.3"
|
||||
checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zustand@npm:^4.3.2":
|
||||
version: 4.5.7
|
||||
resolution: "zustand@npm:4.5.7"
|
||||
dependencies:
|
||||
use-sync-external-store: "npm:^1.2.2"
|
||||
peerDependencies:
|
||||
"@types/react": ">=16.8"
|
||||
immer: ">=9.0.6"
|
||||
react: ">=16.8"
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
checksum: 10c0/55559e37a82f0c06cadc61cb08f08314c0fe05d6a93815e41e3376130c13db22a5017cbb0cd1f018c82f2dad0051afe3592561d40f980bd4082e32005e8a950c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"zustand@npm:^5.0.1, zustand@npm:^5.0.14, zustand@npm:^5.0.3":
|
||||
version: 5.0.14
|
||||
resolution: "zustand@npm:5.0.14"
|
||||
peerDependencies:
|
||||
"@types/react": ">=18.0.0"
|
||||
immer: ">=9.0.6"
|
||||
react: ">=18.0.0"
|
||||
use-sync-external-store: ">=1.2.0"
|
||||
peerDependenciesMeta:
|
||||
"@types/react":
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
use-sync-external-store:
|
||||
optional: true
|
||||
checksum: 10c0/89de0c8119f1e0861b2f896a0b4df0d67994a373ecee02628d4e7b97cee1885207f646917d3ede395c3a5083316d48846855cfbc84aced7cd23a6db3238930a8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
Reference in New Issue
Block a user