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",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,6 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-popover": "^1.1.15",
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@react-three/drei": "^10.7.8",
|
||||||
|
"@react-three/fiber": "^9.7.0",
|
||||||
"@reduxjs/toolkit": "^2.12.0",
|
"@reduxjs/toolkit": "^2.12.0",
|
||||||
"@tanstack/react-query": "^5.100.11",
|
"@tanstack/react-query": "^5.100.11",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
@@ -25,7 +27,10 @@
|
|||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
"react-moment": "^2.0.2",
|
"react-moment": "^2.0.2",
|
||||||
"react-redux": "^9.3.0",
|
"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": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
@@ -34,6 +39,7 @@
|
|||||||
"@types/nprogress": "^0",
|
"@types/nprogress": "^0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"@types/three": "^0",
|
||||||
"babel-plugin-react-compiler": "1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "16.2.6",
|
"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 = {
|
export const metadata: Metadata = {
|
||||||
title: "Khám Bệnh Mơ Phố",
|
title: "Core BIM tedi",
|
||||||
description: "Generated by create next app",
|
description: "Generated by create next app",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -6,9 +6,10 @@ import React from "react";
|
|||||||
|
|
||||||
const page = () => {
|
const page = () => {
|
||||||
return (
|
return (
|
||||||
<BaseLayout title="Dashboard">
|
<BaseLayout title="">
|
||||||
<MainPageHome />
|
<MainPageHome />
|
||||||
</BaseLayout>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { PropsBaseLayout, TContextBaseLayout } from "./interface";
|
import { PropsBaseLayout, TContextBaseLayout } from "./interface";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Header from "./componets/Header";
|
import Header from "./componets/Header";
|
||||||
import Navbar from "./componets/Navbar";
|
|
||||||
import RequireAuth from "@/components/protected/RequiredAuth";
|
import RequireAuth from "@/components/protected/RequiredAuth";
|
||||||
|
|
||||||
export const ContextBaseLayout = React.createContext<TContextBaseLayout>({});
|
export const ContextBaseLayout = React.createContext<TContextBaseLayout>({});
|
||||||
@@ -15,49 +15,22 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
|||||||
return (
|
return (
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<ContextBaseLayout
|
<ContextBaseLayout
|
||||||
value={{ showFull, setShowFull, openMenuMobile, setOpenMenuMobile }}
|
value={{
|
||||||
|
showFull,
|
||||||
|
setShowFull,
|
||||||
|
openMenuMobile,
|
||||||
|
setOpenMenuMobile,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="min-h-screen bg-[#f8f8f8]">
|
<div className="min-h-screen w-full overflow-x-hidden 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>
|
|
||||||
|
|
||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<header
|
<header
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"fixed top-0 right-0 h-[68px] z-[11] bg-white transition-all duration-300 border-b border-[#f4f7fa]",
|
"top-0 right-0 z-[11] h-[68px]",
|
||||||
|
"w-full",
|
||||||
/* DESKTOP */
|
"border-b border-[#f4f7fa]",
|
||||||
showFull
|
"bg-white",
|
||||||
? "xl:left-[240px] xl:w-[calc(100%-240px)]"
|
"transition-all duration-300",
|
||||||
: "xl:left-0 xl:w-full",
|
|
||||||
|
|
||||||
/* MOBILE */
|
|
||||||
"left-0 w-full",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Header title={title} breadcrumb={breadcrumb} />
|
<Header title={title} breadcrumb={breadcrumb} />
|
||||||
@@ -66,12 +39,10 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
|||||||
{/* MAIN CONTENT */}
|
{/* MAIN CONTENT */}
|
||||||
<main
|
<main
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"pt-[92px] pb-6 px-6 transition-all duration-300 ease-in-out",
|
"w-full",
|
||||||
|
"min-w-0",
|
||||||
/* DESKTOP */
|
"overflow-x-hidden",
|
||||||
showFull ? "xl:ml-[240px]" : "xl:ml-0",
|
"transition-all duration-300 ease-in-out",
|
||||||
|
|
||||||
/* MOBILE */
|
|
||||||
"ml-0",
|
"ml-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { ChevronDown } from "lucide-react";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import MenuProfile from "../MenuProfile";
|
import MenuProfile from "../MenuProfile";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import profileServices from "@/services/profileServices";
|
// import profileServices from "@/services/profileServices";
|
||||||
import { QUERY_KEY } from "@/constant/config/enum";
|
import { QUERY_KEY } from "@/constant/config/enum";
|
||||||
import { httpRequest } from "@/services";
|
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
|
CALL API GET LIST PROFILE & KHỚP NỐI THEO CHUẨN DATA MỚI
|
||||||
========================================================= */
|
========================================================= */
|
||||||
const profileQuery = useQuery<any>({
|
// const profileQuery = useQuery<any>({
|
||||||
queryKey: [QUERY_KEY.table_list_profile],
|
// queryKey: [QUERY_KEY.table_list_profile],
|
||||||
queryFn: async () => {
|
// queryFn: async () => {
|
||||||
const res = await httpRequest<any>({
|
// const res = await httpRequest<any>({
|
||||||
showMessageFailed: true,
|
// showMessageFailed: true,
|
||||||
http: profileServices.getProfile(), // Đảm bảo endpoint trỏ về /api/v1/UserProfile/list
|
// 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
|
// // Trả về đúng object chứa items theo cấu trúc của API
|
||||||
return (
|
// return (
|
||||||
res || {
|
// res || {
|
||||||
items: [],
|
// items: [],
|
||||||
page: 1,
|
// page: 1,
|
||||||
pageSize: 10,
|
// pageSize: 10,
|
||||||
total: 0,
|
// total: 0,
|
||||||
totalPages: 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
|
// 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
|
// eslint-disable-next-line react-hooks/preserve-manual-memoization
|
||||||
const currentUserProfile = useMemo(() => {
|
// const currentUserProfile = useMemo(() => {
|
||||||
const listItems = profileQuery.data?.items || [];
|
// const listItems = profileQuery.data?.items || [];
|
||||||
if (!infoUser?.userId) return null;
|
// if (!infoUser?.userId) return null;
|
||||||
|
|
||||||
// So khớp accountId của API trả về với userId trong Redux (infoUser)
|
// // So khớp accountId của API trả về với userId trong Redux (infoUser)
|
||||||
return (
|
// return (
|
||||||
listItems.find((item: any) => item.accountId === infoUser.userId) || null
|
// listItems.find((item: any) => item.accountId === infoUser.userId) || null
|
||||||
);
|
// );
|
||||||
}, [profileQuery.data, infoUser?.userId]);
|
// }, [profileQuery.data, infoUser?.userId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full px-6 flex items-center justify-between bg-white relative">
|
<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 */}
|
{/* Nút bấm Mobile: Mở menu mobile */}
|
||||||
<div
|
<div
|
||||||
className="xl:hidden cursor-pointer"
|
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>
|
</div>
|
||||||
{/* Nút bấm Desktop: Thu gọn/Mở rộng sidebar */}
|
{/* Nút bấm Desktop: Thu gọn/Mở rộng sidebar */}
|
||||||
<div
|
<div
|
||||||
className="hidden xl:block cursor-pointer"
|
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>
|
</div>
|
||||||
{breadcrumb ? (
|
{breadcrumb ? (
|
||||||
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>
|
</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">
|
<div className="w-10 h-10 rounded-full border-2 border-blue-500 overflow-hidden relative">
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={icons.avatar}
|
||||||
currentUserProfile?.avatarUrl
|
|
||||||
? `${process.env.NEXT_PUBLIC_IMAGE}${currentUserProfile.avatarUrl}`
|
|
||||||
: icons.avatar
|
|
||||||
}
|
|
||||||
alt="avatar"
|
alt="avatar"
|
||||||
width={40}
|
width={40}
|
||||||
height={40}
|
height={40}
|
||||||
@@ -122,7 +118,7 @@ const Header = ({ title, breadcrumb }: PropsHeader) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="hidden md:block text-sm font-semibold text-[#171832]">
|
<p className="hidden md:block text-sm font-semibold text-[#171832]">
|
||||||
{currentUserProfile?.fullName || "User admin"}
|
{"User admin"}
|
||||||
</p>
|
</p>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
size={16}
|
size={16}
|
||||||
|
|||||||
@@ -51,21 +51,6 @@ const MenuProfile = ({ onClose }: PropsMenuProfile) => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-[260px] bg-white rounded-xl shadow-md p-2">
|
<div className="w-[260px] bg-white rounded-xl shadow-md p-2">
|
||||||
{/* PROFILE */}
|
{/* PROFILE */}
|
||||||
<Link
|
|
||||||
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 */}
|
{/* LOGOUT */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,340 +1,27 @@
|
|||||||
"use client";
|
"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 dynamic from "next/dynamic";
|
||||||
import { httpRequest } from "@/services";
|
import BeamForm from "./components/BeamForm";
|
||||||
import consultationServices, {
|
import { BridgeTreeSidebar } from "@/components/utils/BridgeTreeSidebar";
|
||||||
ConsultationItem,
|
|
||||||
ConsultationResponse,
|
|
||||||
ReportResponse,
|
|
||||||
} from "@/services/consultationServices";
|
|
||||||
import specialtyServices, { SpecialtyItem } from "@/services/specialtyServices";
|
|
||||||
import DataWrapper from "@/components/customs/DataWrapper";
|
|
||||||
|
|
||||||
const MainPageHome = () => {
|
const Canvas3D = dynamic(
|
||||||
const today = moment().format("YYYY-MM-DD");
|
() => import("@/components/viewer3d/Canvas3D").then((m) => m.Canvas3D),
|
||||||
|
{ ssr: false },
|
||||||
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,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
res || {
|
<main className="w-screen h-screen flex overflow-hidden bg-slate-950">
|
||||||
items: [],
|
{/* 1. Cột Menu cây thư mục (Trái) */}
|
||||||
page: 1,
|
<BridgeTreeSidebar />
|
||||||
pageSize: 100,
|
|
||||||
total: 0,
|
{/* 2. Cột Form nhập liệu theo Tab (Giữa) */}
|
||||||
totalPages: 0,
|
<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";
|
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 full_screen from "../../../public/static/images/full-screen.svg";
|
||||||
import avatar from "../../../public/static/images/avatar-default.png";
|
import avatar from "../../../public/static/images/avatar-default.png";
|
||||||
import tickCircle from "../../../public/static/images/tick-circle.svg";
|
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";
|
import emptyFile from "../../../public/static/images/emptyFile.svg";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
hamburger,
|
LogoTedi,
|
||||||
full_screen,
|
full_screen,
|
||||||
avatar,
|
avatar,
|
||||||
tickCircle,
|
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
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@babel/template@npm:^7.28.6":
|
||||||
version: 7.28.6
|
version: 7.28.6
|
||||||
resolution: "@babel/template@npm:7.28.6"
|
resolution: "@babel/template@npm:7.28.6"
|
||||||
@@ -187,6 +194,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@emnapi/core@npm:1.10.0, @emnapi/core@npm:^1.10.0":
|
||||||
version: 1.10.0
|
version: 1.10.0
|
||||||
resolution: "@emnapi/core@npm:1.10.0"
|
resolution: "@emnapi/core@npm:1.10.0"
|
||||||
@@ -653,6 +667,24 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@napi-rs/wasm-runtime@npm:^1.1.4":
|
||||||
version: 1.1.4
|
version: 1.1.4
|
||||||
resolution: "@napi-rs/wasm-runtime@npm:1.1.4"
|
resolution: "@napi-rs/wasm-runtime@npm:1.1.4"
|
||||||
@@ -1188,6 +1220,83 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@reduxjs/toolkit@npm:^2.12.0":
|
||||||
version: 2.12.0
|
version: 2.12.0
|
||||||
resolution: "@reduxjs/toolkit@npm:2.12.0"
|
resolution: "@reduxjs/toolkit@npm:2.12.0"
|
||||||
@@ -1422,6 +1531,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@tybys/wasm-util@npm:^0.10.1":
|
||||||
version: 0.10.2
|
version: 0.10.2
|
||||||
resolution: "@tybys/wasm-util@npm:0.10.2"
|
resolution: "@tybys/wasm-util@npm:0.10.2"
|
||||||
@@ -1431,6 +1547,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/estree@npm:^1.0.6":
|
||||||
version: 1.0.9
|
version: 1.0.9
|
||||||
resolution: "@types/estree@npm:1.0.9"
|
resolution: "@types/estree@npm:1.0.9"
|
||||||
@@ -1475,6 +1598,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/react-dom@npm:^19":
|
||||||
version: 19.2.3
|
version: 19.2.3
|
||||||
resolution: "@types/react-dom@npm:19.2.3"
|
resolution: "@types/react-dom@npm:19.2.3"
|
||||||
@@ -1484,6 +1614,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/react@npm:^19":
|
||||||
version: 19.2.15
|
version: 19.2.15
|
||||||
resolution: "@types/react@npm:19.2.15"
|
resolution: "@types/react@npm:19.2.15"
|
||||||
@@ -1493,6 +1632,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/use-sync-external-store@npm:^0.0.6":
|
||||||
version: 0.0.6
|
version: 0.0.6
|
||||||
resolution: "@types/use-sync-external-store@npm:0.0.6"
|
resolution: "@types/use-sync-external-store@npm:0.0.6"
|
||||||
@@ -1500,6 +1660,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@typescript-eslint/eslint-plugin@npm:8.59.4":
|
||||||
version: 8.59.4
|
version: 8.59.4
|
||||||
resolution: "@typescript-eslint/eslint-plugin@npm:8.59.4"
|
resolution: "@typescript-eslint/eslint-plugin@npm:8.59.4"
|
||||||
@@ -1793,6 +1960,65 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"acorn-jsx@npm:^5.3.2":
|
||||||
version: 5.3.2
|
version: 5.3.2
|
||||||
resolution: "acorn-jsx@npm:5.3.2"
|
resolution: "acorn-jsx@npm:5.3.2"
|
||||||
@@ -2057,6 +2283,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"baseline-browser-mapping@npm:^2.10.12, baseline-browser-mapping@npm:^2.9.19":
|
||||||
version: 2.10.31
|
version: 2.10.31
|
||||||
resolution: "baseline-browser-mapping@npm:2.10.31"
|
resolution: "baseline-browser-mapping@npm:2.10.31"
|
||||||
@@ -2066,6 +2299,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"brace-expansion@npm:^1.1.7":
|
||||||
version: 1.1.14
|
version: 1.1.14
|
||||||
resolution: "brace-expansion@npm:1.1.14"
|
resolution: "brace-expansion@npm:1.1.14"
|
||||||
@@ -2109,6 +2351,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2":
|
||||||
version: 1.0.2
|
version: 1.0.2
|
||||||
resolution: "call-bind-apply-helpers@npm:1.0.2"
|
resolution: "call-bind-apply-helpers@npm:1.0.2"
|
||||||
@@ -2148,6 +2400,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001782":
|
||||||
version: 1.0.30001793
|
version: 1.0.30001793
|
||||||
resolution: "caniuse-lite@npm:1.0.30001793"
|
resolution: "caniuse-lite@npm:1.0.30001793"
|
||||||
@@ -2225,7 +2486,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 7.0.6
|
||||||
resolution: "cross-spawn@npm:7.0.6"
|
resolution: "cross-spawn@npm:7.0.6"
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -2347,6 +2620,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"detect-libc@npm:^2.0.3, detect-libc@npm:^2.1.2":
|
||||||
version: 2.1.2
|
version: 2.1.2
|
||||||
resolution: "detect-libc@npm:2.1.2"
|
resolution: "detect-libc@npm:2.1.2"
|
||||||
@@ -2370,6 +2652,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "dunder-proto@npm:1.0.1"
|
resolution: "dunder-proto@npm:1.0.1"
|
||||||
@@ -2905,6 +3194,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"file-entry-cache@npm:^8.0.0":
|
||||||
version: 8.0.0
|
version: 8.0.0
|
||||||
resolution: "file-entry-cache@npm:8.0.0"
|
resolution: "file-entry-cache@npm:8.0.0"
|
||||||
@@ -3124,6 +3427,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"gopd@npm:^1.0.1, gopd@npm:^1.2.0":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "gopd@npm:1.2.0"
|
resolution: "gopd@npm:1.2.0"
|
||||||
@@ -3211,6 +3521,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"https-proxy-agent@npm:^5.0.1":
|
||||||
version: 5.0.1
|
version: 5.0.1
|
||||||
resolution: "https-proxy-agent@npm:5.0.1"
|
resolution: "https-proxy-agent@npm:5.0.1"
|
||||||
@@ -3221,6 +3538,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"ignore@npm:^5.2.0":
|
||||||
version: 5.3.2
|
version: 5.3.2
|
||||||
resolution: "ignore@npm:5.3.2"
|
resolution: "ignore@npm:5.3.2"
|
||||||
@@ -3235,6 +3559,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"immer@npm:^11.0.0":
|
||||||
version: 11.1.8
|
version: 11.1.8
|
||||||
resolution: "immer@npm:11.1.8"
|
resolution: "immer@npm:11.1.8"
|
||||||
@@ -3435,6 +3766,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"is-regex@npm:^1.2.1":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "is-regex@npm:1.2.1"
|
resolution: "is-regex@npm:1.2.1"
|
||||||
@@ -3547,6 +3885,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"jiti@npm:^2.6.1":
|
||||||
version: 2.7.0
|
version: 2.7.0
|
||||||
resolution: "jiti@npm:2.7.0"
|
resolution: "jiti@npm:2.7.0"
|
||||||
@@ -3645,41 +3994,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"language-subtag-registry@npm:^0.3.20":
|
||||||
version: 0.3.23
|
version: 0.3.23
|
||||||
resolution: "language-subtag-registry@npm:0.3.23"
|
resolution: "language-subtag-registry@npm:0.3.23"
|
||||||
@@ -3706,6 +4020,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"lightningcss-android-arm64@npm:1.32.0":
|
||||||
version: 1.32.0
|
version: 1.32.0
|
||||||
resolution: "lightningcss-android-arm64@npm:1.32.0"
|
resolution: "lightningcss-android-arm64@npm:1.32.0"
|
||||||
@@ -3890,6 +4213,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"magic-string@npm:^0.30.21":
|
||||||
version: 0.30.21
|
version: 0.30.21
|
||||||
resolution: "magic-string@npm:0.30.21"
|
resolution: "magic-string@npm:0.30.21"
|
||||||
@@ -3924,6 +4257,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"micromatch@npm:^4.0.4":
|
||||||
version: 4.0.8
|
version: 4.0.8
|
||||||
resolution: "micromatch@npm:4.0.8"
|
resolution: "micromatch@npm:4.0.8"
|
||||||
@@ -4305,6 +4654,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"prelude-ls@npm:^1.2.1":
|
||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
resolution: "prelude-ls@npm:1.2.1"
|
resolution: "prelude-ls@npm:1.2.1"
|
||||||
@@ -4312,6 +4668,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"prop-types@npm:^15.8.1":
|
||||||
version: 15.8.1
|
version: 15.8.1
|
||||||
resolution: "prop-types@npm:15.8.1"
|
resolution: "prop-types@npm:15.8.1"
|
||||||
@@ -4458,6 +4824,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react@npm:19.2.4":
|
||||||
version: 19.2.4
|
version: 19.2.4
|
||||||
resolution: "react@npm:19.2.4"
|
resolution: "react@npm:19.2.4"
|
||||||
@@ -4511,6 +4890,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"reselect@npm:^5.1.0":
|
||||||
version: 5.2.0
|
version: 5.2.0
|
||||||
resolution: "reselect@npm:5.2.0"
|
resolution: "reselect@npm:5.2.0"
|
||||||
@@ -4838,6 +5224,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"stop-iteration-iterator@npm:^1.1.0":
|
||||||
version: 1.1.0
|
version: 1.1.0
|
||||||
resolution: "stop-iteration-iterator@npm:1.1.0"
|
resolution: "stop-iteration-iterator@npm:1.1.0"
|
||||||
@@ -4974,6 +5380,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"tailwindcss@npm:4.3.0, tailwindcss@npm:^4":
|
||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
resolution: "tailwindcss@npm:4.3.0"
|
resolution: "tailwindcss@npm:4.3.0"
|
||||||
@@ -4988,6 +5403,45 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.15":
|
||||||
version: 0.2.16
|
version: 0.2.16
|
||||||
resolution: "tinyglobby@npm:0.2.16"
|
resolution: "tinyglobby@npm:0.2.16"
|
||||||
@@ -5007,6 +5461,36 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"ts-api-utils@npm:^2.5.0":
|
||||||
version: 2.5.0
|
version: 2.5.0
|
||||||
resolution: "ts-api-utils@npm:2.5.0"
|
resolution: "ts-api-utils@npm:2.5.0"
|
||||||
@@ -5035,6 +5519,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"type-check@npm:^0.4.0, type-check@npm:~0.4.0":
|
||||||
version: 0.4.0
|
version: 0.4.0
|
||||||
resolution: "type-check@npm:0.4.0"
|
resolution: "type-check@npm:0.4.0"
|
||||||
@@ -5281,7 +5774,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 1.6.0
|
||||||
resolution: "use-sync-external-store@npm:1.6.0"
|
resolution: "use-sync-external-store@npm:1.6.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5290,6 +5783,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "which-boxed-primitive@npm:1.1.1"
|
resolution: "which-boxed-primitive@npm:1.1.1"
|
||||||
@@ -5392,9 +5906,50 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 4.4.3
|
||||||
resolution: "zod@npm:4.4.3"
|
resolution: "zod@npm:4.4.3"
|
||||||
checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3
|
checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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