diff --git a/package.json b/package.json index 7678d58..64d46e0 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "kham-benh-mo-pho", + "name": "TEDI-DDP", "version": "0.1.0", "private": true, "scripts": { @@ -11,6 +11,8 @@ "dependencies": { "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-tooltip": "^1.2.8", + "@react-three/drei": "^10.7.8", + "@react-three/fiber": "^9.7.0", "@reduxjs/toolkit": "^2.12.0", "@tanstack/react-query": "^5.100.11", "axios": "^1.16.1", @@ -25,7 +27,10 @@ "react-dom": "19.2.4", "react-moment": "^2.0.2", "react-redux": "^9.3.0", - "react-toastify": "^11.1.0" + "react-toastify": "^11.1.0", + "three": "^0.185.1", + "zod": "^4.4.3", + "zustand": "^5.0.14" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -34,6 +39,7 @@ "@types/nprogress": "^0", "@types/react": "^19", "@types/react-dom": "^19", + "@types/three": "^0", "babel-plugin-react-compiler": "1.0.0", "eslint": "^9", "eslint-config-next": "16.2.6", diff --git a/public/static/images/background_auth.jpg b/public/static/images/background_auth.jpg index 30b2521..6e39f55 100644 Binary files a/public/static/images/background_auth.jpg and b/public/static/images/background_auth.jpg differ diff --git a/public/static/images/logo-tedi.png b/public/static/images/logo-tedi.png new file mode 100644 index 0000000..4cb85ee Binary files /dev/null and b/public/static/images/logo-tedi.png differ diff --git a/src/app/consultation/[id]/page.tsx b/src/app/consultation/[id]/page.tsx deleted file mode 100644 index 1eb43d8..0000000 --- a/src/app/consultation/[id]/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default Page; diff --git a/src/app/consultation/create/page.tsx b/src/app/consultation/create/page.tsx deleted file mode 100644 index 8c5e48e..0000000 --- a/src/app/consultation/create/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/app/consultation/history/page.tsx b/src/app/consultation/history/page.tsx deleted file mode 100644 index 0f10967..0000000 --- a/src/app/consultation/history/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/app/consultation/page.tsx b/src/app/consultation/page.tsx deleted file mode 100644 index 08f9f17..0000000 --- a/src/app/consultation/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/app/consultation/update-specialty/page.tsx b/src/app/consultation/update-specialty/page.tsx deleted file mode 100644 index 268f39d..0000000 --- a/src/app/consultation/update-specialty/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/app/consultation/update/page.tsx b/src/app/consultation/update/page.tsx deleted file mode 100644 index 4c042d8..0000000 --- a/src/app/consultation/update/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx deleted file mode 100644 index 1a7fc19..0000000 --- a/src/app/dashboard/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/app/favicon.ico b/src/app/favicon.ico index 718d6fe..4cab8cb 100644 Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 1cbd287..e6579ac 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -15,7 +15,7 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Khám Bệnh Mơ Phố", + title: "Core BIM tedi", description: "Generated by create next app", }; diff --git a/src/app/page.tsx b/src/app/page.tsx index 1a7fc19..e308c78 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,9 +6,10 @@ import React from "react"; const page = () => { return ( - + + //

Home

); }; diff --git a/src/app/patient/[id]/page.tsx b/src/app/patient/[id]/page.tsx deleted file mode 100644 index 92d7b8a..0000000 --- a/src/app/patient/[id]/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default Page; diff --git a/src/app/patient/page.tsx b/src/app/patient/page.tsx deleted file mode 100644 index 89b4560..0000000 --- a/src/app/patient/page.tsx +++ /dev/null @@ -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 ( -
- - - -
- ); -}; - -export default page; diff --git a/src/app/prescription/[id]/page.tsx b/src/app/prescription/[id]/page.tsx deleted file mode 100644 index 73f81c1..0000000 --- a/src/app/prescription/[id]/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/app/prescription/create/page.tsx b/src/app/prescription/create/page.tsx deleted file mode 100644 index 2c0f5a0..0000000 --- a/src/app/prescription/create/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/app/prescription/page.tsx b/src/app/prescription/page.tsx deleted file mode 100644 index 8a86f47..0000000 --- a/src/app/prescription/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx deleted file mode 100644 index 2fc28d6..0000000 --- a/src/app/profile/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/app/profile/update/page.tsx b/src/app/profile/update/page.tsx deleted file mode 100644 index 4267d9b..0000000 --- a/src/app/profile/update/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/app/statistical/page.tsx b/src/app/statistical/page.tsx deleted file mode 100644 index 482ba8e..0000000 --- a/src/app/statistical/page.tsx +++ /dev/null @@ -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 ( - - - - ); -}; - -export default page; diff --git a/src/components/core/schema/superT.schema.ts b/src/components/core/schema/superT.schema.ts new file mode 100644 index 0000000..751e5cd --- /dev/null +++ b/src/components/core/schema/superT.schema.ts @@ -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; diff --git a/src/components/core/validators/superTValidator.ts b/src/components/core/validators/superTValidator.ts new file mode 100644 index 0000000..43a060c --- /dev/null +++ b/src/components/core/validators/superTValidator.ts @@ -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; +} diff --git a/src/components/layouts/BaseLayout/BaseLayout.tsx b/src/components/layouts/BaseLayout/BaseLayout.tsx index e165e55..cd775ab 100644 --- a/src/components/layouts/BaseLayout/BaseLayout.tsx +++ b/src/components/layouts/BaseLayout/BaseLayout.tsx @@ -1,9 +1,9 @@ "use client"; + import React from "react"; import { PropsBaseLayout, TContextBaseLayout } from "./interface"; import clsx from "clsx"; import Header from "./componets/Header"; -import Navbar from "./componets/Navbar"; import RequireAuth from "@/components/protected/RequiredAuth"; export const ContextBaseLayout = React.createContext({}); @@ -15,49 +15,22 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => { return ( -
- {/* OVERLAY cho Mobile */} -
setOpenMenuMobile(false)} - /> - - {/* SIDEBAR (Dùng chung cho cả Desktop & Mobile) */} - - +
{/* HEADER */}
@@ -66,12 +39,10 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => { {/* MAIN CONTENT */}
diff --git a/src/components/layouts/BaseLayout/componets/Header/Header.tsx b/src/components/layouts/BaseLayout/componets/Header/Header.tsx index af7ba4d..dc9ed94 100644 --- a/src/components/layouts/BaseLayout/componets/Header/Header.tsx +++ b/src/components/layouts/BaseLayout/componets/Header/Header.tsx @@ -12,7 +12,7 @@ import { ChevronDown } from "lucide-react"; import clsx from "clsx"; import MenuProfile from "../MenuProfile"; import { useQuery } from "@tanstack/react-query"; -import profileServices from "@/services/profileServices"; +// import profileServices from "@/services/profileServices"; import { QUERY_KEY } from "@/constant/config/enum"; import { httpRequest } from "@/services"; @@ -41,38 +41,38 @@ const Header = ({ title, breadcrumb }: PropsHeader) => { /* ========================================================= CALL API GET LIST PROFILE & KHỚP NỐI THEO CHUẨN DATA MỚI ========================================================= */ - const profileQuery = useQuery({ - queryKey: [QUERY_KEY.table_list_profile], - queryFn: async () => { - const res = await httpRequest({ - showMessageFailed: true, - http: profileServices.getProfile(), // Đảm bảo endpoint trỏ về /api/v1/UserProfile/list - }); + // const profileQuery = useQuery({ + // queryKey: [QUERY_KEY.table_list_profile], + // queryFn: async () => { + // const res = await httpRequest({ + // showMessageFailed: true, + // http: profileServices.getProfile(), // Đảm bảo endpoint trỏ về /api/v1/UserProfile/list + // }); - // Trả về đúng object chứa items theo cấu trúc của API - return ( - res || { - items: [], - page: 1, - pageSize: 10, - total: 0, - totalPages: 0, - } - ); - }, - }); + // // Trả về đúng object chứa items theo cấu trúc của API + // return ( + // res || { + // items: [], + // page: 1, + // pageSize: 10, + // total: 0, + // totalPages: 0, + // } + // ); + // }, + // }); // Tìm kiếm thông tin profile của user hiện tại đang đăng nhập bằng useMemo // eslint-disable-next-line react-hooks/preserve-manual-memoization - const currentUserProfile = useMemo(() => { - const listItems = profileQuery.data?.items || []; - if (!infoUser?.userId) return null; + // const currentUserProfile = useMemo(() => { + // const listItems = profileQuery.data?.items || []; + // if (!infoUser?.userId) return null; - // So khớp accountId của API trả về với userId trong Redux (infoUser) - return ( - listItems.find((item: any) => item.accountId === infoUser.userId) || null - ); - }, [profileQuery.data, infoUser?.userId]); + // // So khớp accountId của API trả về với userId trong Redux (infoUser) + // return ( + // listItems.find((item: any) => item.accountId === infoUser.userId) || null + // ); + // }, [profileQuery.data, infoUser?.userId]); return (
@@ -80,21 +80,21 @@ const Header = ({ title, breadcrumb }: PropsHeader) => { {/* Nút bấm Mobile: Mở menu mobile */}
context?.setOpenMenuMobile?.(!context?.openMenuMobile)} + // onClick={() => context?.setOpenMenuMobile?.(!context?.openMenuMobile)} > - menu + menu
{/* Nút bấm Desktop: Thu gọn/Mở rộng sidebar */}
context?.setShowFull?.(!context?.showFull)} + // onClick={() => context?.setShowFull?.(!context?.showFull)} > - menu + menu
{breadcrumb ? ( breadcrumb ) : ( -

{title}

+

Công ty Tedi

)}
@@ -110,11 +110,7 @@ const Header = ({ title, breadcrumb }: PropsHeader) => { >
avatar { />

- {currentUserProfile?.fullName || "User admin"} + {"User admin"}

{ return (
{/* PROFILE */} - - -
-

Thông tin cá nhân

-

Chi tiết tài khoản

-
- -
{/* LOGOUT */}
{ - const today = moment().format("YYYY-MM-DD"); - - const consultationQuery = useQuery({ - queryKey: [QUERY_KEY.table_list_consultation, "dashboard"], - queryFn: async () => { - const res = await httpRequest({ - showMessageFailed: true, - http: consultationServices.getConsultations({ - Page: 1, - PageSize: 100, - SortBy: "visitDate", - Desc: true, - }), - }); - - return ( - res || { - items: [], - page: 1, - pageSize: 100, - total: 0, - totalPages: 0, - } - ); - }, - }); - - const reportQuery = useQuery({ - queryKey: [QUERY_KEY.table_list_report, today], - queryFn: async () => { - const res = await httpRequest({ - showMessageFailed: true, - http: consultationServices.getConsultationsReport({ - FromDate: today, - ToDate: today, - }), - }); - - return res as ReportResponse; - }, - }); - - const specialtyQuery = useQuery({ - queryKey: [QUERY_KEY.list_specialty_lookup], - queryFn: async () => { - const res = await httpRequest({ - showMessageFailed: true, - http: specialtyServices.getSpecialty(), - }); - - return res || []; - }, - }); - - const consultationItems = consultationQuery.data?.items || []; - const consultationReport = reportQuery.data; - const specialtyItems = specialtyQuery.data || []; - - const todayConsultations = useMemo(() => { - return consultationItems.filter((item) => { - if (!item.visitDate) return false; - - return moment(item.visitDate).format("YYYY-MM-DD") === today; - }); - }, [consultationItems, today]); - - const statusCounts = useMemo(() => { - return { - total: todayConsultations.length, - - waiting: todayConsultations.filter((item) => item.status === 1).length, - - inProgress: todayConsultations.filter((item) => item.status === 2).length, - - completed: todayConsultations.filter((item) => item.status === 5).length, - - cancelled: todayConsultations.filter((item) => item.status === 6).length, - }; - }, [todayConsultations]); - - const specialtyActiveCounts = useMemo(() => { - const map = new Map(); - - todayConsultations.forEach((item) => { - item.specialtyClinics?.forEach((specialty) => { - const count = map.get(specialty.specialtyId) || 0; - - map.set(specialty.specialtyId, count + 1); - }); - }); - - return specialtyItems.map((specialty) => ({ - ...specialty, - active: map.get(specialty.id) || 0, - })); - }, [todayConsultations, specialtyItems]); - - const summaryCards = useMemo( - () => [ - { - title: "Tất cả đợt khám", - value: statusCounts.total.toString(), - subtitle: "Số đợt khám trong ngày", - icon: CalendarDays, - color: "bg-blue-50 text-blue-600", - }, - { - title: "Đang khám", - value: statusCounts.inProgress.toString(), - subtitle: "Đang khám tổng quát", - icon: Stethoscope, - color: "bg-emerald-50 text-emerald-600", - }, - { - title: "Hoàn tất", - value: statusCounts.completed.toString(), - subtitle: "Đã hoàn thành", - icon: ClipboardList, - color: "bg-sky-50 text-sky-600", - }, - { - title: "Chờ khám", - value: statusCounts.waiting.toString(), - subtitle: "Đang chờ bác sĩ", - icon: Activity, - color: "bg-orange-50 text-orange-600", - }, - ], - [statusCounts], - ); - - const generalInfo = useMemo( - () => [ - { - label: "Tổng đợt khám", - value: statusCounts.total.toString(), - icon: Users, - color: "bg-violet-50 text-violet-600", - }, - { - label: "Đợt khám chuyên khoa", - value: specialtyActiveCounts - .reduce((sum, item) => sum + item.active, 0) - .toString(), - icon: Hospital, - color: "bg-cyan-50 text-cyan-600", - }, - { - label: "Đơn thuốc", - value: consultationReport?.totalPrescriptionsIssued?.toString() || "0", - icon: ClipboardList, - color: "bg-amber-50 text-amber-600", - }, - { - label: "Chuyên khoa", - value: specialtyItems.length.toString(), - icon: Activity, - color: "bg-teal-50 text-teal-600", - }, - ], - [ - consultationReport, - statusCounts.total, - specialtyActiveCounts, - specialtyItems.length, - ], - ); +const Canvas3D = dynamic( + () => import("@/components/viewer3d/Canvas3D").then((m) => m.Canvas3D), + { ssr: false }, +); +export default function Home() { return ( -
-
-
-
-

- Dashboard khám bệnh -

-

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

-
-
- Cập nhật: {moment().format("DD/MM/YYYY")} -
-
+
+ {/* 1. Cột Menu cây thư mục (Trái) */} + - -
- {summaryCards.map((item) => { - const Icon = item.icon; + {/* 2. Cột Form nhập liệu theo Tab (Giữa) */} + - return ( -
-
-
- {item.title} -
-
- -
-
-
-
-

- {item.value} -

-

- {item.subtitle} -

-
-
-
- ); - })} -
-
+ {/* 3. Cột Viewport 3D Focus theo Cấu kiện (Phải) */} +
+
- -
-
-
-
-

- Chuyên khoa hiện có -

-

- Danh sách chuyên khoa. -

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

- {specialty.name} -

-

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

-
- - {specialty.active} - -
- ))} -
-
-
-
-
-

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

-

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

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

- {item.label} -

-

- {item.value} -

-
-
-
- ); - })} -
-
-
-
-
+
); -}; - -export default MainPageHome; +} diff --git a/src/components/page/Home/MainPageHome/components/BeamForm/BeamForm.tsx b/src/components/page/Home/MainPageHome/components/BeamForm/BeamForm.tsx new file mode 100644 index 0000000..7bb5bf1 --- /dev/null +++ b/src/components/page/Home/MainPageHome/components/BeamForm/BeamForm.tsx @@ -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 ( +
+ {/* Nút bật/tắt Chế độ Ghép Cầu */} + + + {/* Dynamic Form Content */} +
+ {/* 1.1 DẦM SUPER-T */} + {activeTab === "superTBeam" && ( +
+

+ 1.1 Thông số Dầm Super-T +

+ +
+
+ + + 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" + /> +
+ +
+ + + 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" + /> +
+
+ + {/* Validation Panel */} +
+ {validationIssues && validationIssues.length > 0 ? ( +
+
+ + Cảnh báo kỹ thuật: +
+
    + {validationIssues.map((issue, idx) => ( +
  • {issue.message}
  • + ))} +
+
+ ) : ( +
+ + Kích thước dầm hợp lệ (AASHTO / TCVN) +
+ )} +
+
+ )} + + {/* 1.2 BẢN MẶT CẦU */} + {activeTab === "deck" && ( +
+

+ 1.2 Bản Mặt Cầu +

+
+ + + 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" + /> +
+
+ + + 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" + /> +
+
+ )} + + {/* 1.3 DẦM NGANG */} + {activeTab === "diaphragm" && ( +
+

+ 1.3 Dầm Ngang / Diaphragms +

+
+ + + 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" + /> +
+
+ )} + + {/* 1.4 LAN CAN */} + {activeTab === "railing" && ( +
+

+ 1.4 Lan Can & Gờ Chắn +

+
+ + + 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" + /> +
+
+ )} + + {/* 2.1 GỐI CẦU */} + {activeTab === "bearing" && ( +
+

+ 2.1 Gối Cầu +

+
+ + + 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" + /> +
+
+ )} + + {/* 3.1 MỐ CẦU */} + {activeTab === "abutment" && ( +
+

+ 3.1 Mố Cầu +

+
+ + + 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" + /> +
+
+ )} + + {/* 3.2 TRỤ CẦU */} + {activeTab === "pier" && ( +
+

+ 3.2 Trụ Cầu +

+
+ + + 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" + /> +
+
+ )} + + {/* 3.3 CỌC / MÓNG CỌC */} + {activeTab === "pile" && ( +
+

+ 3.3 Cọc / Móng Cọc +

+
+ + + 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" + /> +
+
+ )} +
+ + {/* Button Xuất dữ liệu Contract */} + +
+ ); +} + +export default BeamForm; diff --git a/src/components/page/Home/MainPageHome/components/BeamForm/index.ts b/src/components/page/Home/MainPageHome/components/BeamForm/index.ts new file mode 100644 index 0000000..02f1ba6 --- /dev/null +++ b/src/components/page/Home/MainPageHome/components/BeamForm/index.ts @@ -0,0 +1 @@ +export { default } from "./BeamForm"; diff --git a/src/components/page/consultation/CreateConsultation/CreateConsultation.tsx b/src/components/page/consultation/CreateConsultation/CreateConsultation.tsx deleted file mode 100644 index 0247fef..0000000 --- a/src/components/page/consultation/CreateConsultation/CreateConsultation.tsx +++ /dev/null @@ -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({ - queryKey: [QUERY_KEY.table_list_patient], - - queryFn: async () => { - const res = await httpRequest({ - 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({ - queryKey: [QUERY_KEY.table_list_specialty], - - queryFn: async () => { - const res = await httpRequest({ - 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 ( - -
-
-
-

- Tạo phiên khám -

-
-
- - {/* BODY */} -
-
- - Bệnh nhân - * - - } - 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} - /> -
-
- - - Lý do khám * - - } - name="chiefComplaint" - type="text" - value={form.chiefComplaint} - placeholder="Nhập lý do khám" - isRequired - onChangeValue={(v) => - setForm((prev) => ({ ...prev, chiefComplaint: String(v) })) - } - /> - - Triệu chứng * - - } - name="symptomsText" - placeholder="Nhập triệu chứng" - isRequired - value={form.symptomsText} - onChangeValue={(v) => - setForm((prev) => ({ ...prev, symptomsText: String(v) })) - } - /> - - - Chỉ số huyết áp, chỉ số mạch{" "} - * - - } - name="vitalSigns" - placeholder="Nhập chỉ số huyết áp, chỉ số mạch" - value={form.vitalSigns} - isRequired - onChangeValue={(v) => - setForm((prev) => ({ ...prev, vitalSigns: String(v) })) - } - /> - -
-
- - label={ - - Chuyên khoa - * - - } - text="chuyên khoa" - title="Chọn chuyên khoa" - placeholder="Chọn nhiều chuyên khoa..." - options={specialties} - selectedItems={form.specialtyIds} - setSelectedItems={(value) => - setForm((prev) => ({ - ...prev, - specialtyIds: value as string[], - })) - } - showSelectedItems="default" - getOptionLabel={(item) => item.name} - getOptionValue={(item) => item.id} - /> -
-
- - {/* FOOTER */} -
- router.back()} - > - Hủy bỏ - - - - {createConsultationMutation.isPending - ? "Đang tạo..." - : "Tạo phiên khám"} - -
-
-
- ); -}; - -export default CreateConsultation; diff --git a/src/components/page/consultation/CreateConsultation/index.ts b/src/components/page/consultation/CreateConsultation/index.ts deleted file mode 100644 index 294ec6b..0000000 --- a/src/components/page/consultation/CreateConsultation/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./CreateConsultation"; diff --git a/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx b/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx deleted file mode 100644 index ddca924..0000000 --- a/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx +++ /dev/null @@ -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(1); - const [prescriptionPageSize, setPrescriptionPageSize] = useState(10); - - const consulDetailQuery = useQuery({ - queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId], - - enabled: !!consultationId, - - queryFn: async () => { - const res = await httpRequest({ - 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
Đang tải dữ liệu...
; - } - - if (!consultation) { - return
Không tìm thấy dữ liệu phiên khám
; - } - 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 ( -
-
-
-
- {/* HEADER */} -
-
router.back()} - > - - -

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

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

- ID PHIÊN KHÁM -

- -

- {consultation.id} -

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

- MÃ PHIÊN KHÁM -

- -

- {consultation.sessionCode} -

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

- TÊN BỆNH NHÂN -

- -

- {consultation.patientName} -

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

- NGÀY, GIỜ KHÁM -

- -

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

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

- DẤU HIỆU -

- -

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

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

- KHIẾU NẠI CHÍNH -

- -

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

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

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

- -

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

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

- CHUẨN ĐOÁN -

- -

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

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

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

- -

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

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

- GHI CHÚ BÁC SĨ -

- -

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

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

- NGƯỜI TẠO -

- -

- {consultation.createdByName || "---"} -

-
- -
-

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

- -

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

-
- -
-

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

- -

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

-
- -
-

- FILE UPLOAD -

- {consultation.generalAttachmentUrls?.length > 0 ? ( - - - -
- 📎 - Chi tiết các file -
-
- - -
-

- Danh sách file đính kèm -

- -
- {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 ( - -
- - {isImage ? "🖼️" : "📄"} - - -
-

- {file.fileName} -

- -

- {extension || "FILE"} -

-
-
- - - Mở - -
- ); - }, - )} -
-
-
-
-
- ) : ( -
Không có file nào được tải lên
- )} -
- -
-

- TRẠNG THÁI -

- - -
-
-
-
-
-
- {/* HEADER */} -
-

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

-
- - - 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) => ( - - {item.medicineName} - - ), - }, - { - title: "Liều dùng", - render: (item: PrescriptionItem) => ( - {item.dosage} - ), - }, - { - title: "Tần suất", - render: (item: PrescriptionItem) => ( - {item.frequency} - ), - }, - { - title: "Thời gian", - render: (item: PrescriptionItem) => ( - {item.duration} - ), - }, - { - title: "Hướng dẫn", - render: (item: PrescriptionItem) => ( - - {item.instructions || "---"} - - ), - }, - ]} - /> - - - {/* 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 && ( - 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]} - /> - )} - - - - {/* TAB CHUYÊN KHOA */} - {specialtyTabs.length > 0 && ( -
- -
- )} - {/* CHI TIẾT CHUYÊN KHOA */} - {selectedSpecialty && ( -
-
-

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

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

CHUYÊN KHOA

- {/* Thêm class để bọc text */} -

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

-
- -
-

CHẨN ĐOÁN

- {/* Thêm class để bọc text */} -

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

-
- -
-

GHI CHÚ KHÁM

- {/* Thêm class để bọc text */} -

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

-
- -
-

THỦ THUẬT

- {/* Thêm class để bọc text */} -

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

-
- -
-

- FILE UPLOAD -

- {selectedSpecialty.attachmentUrls?.length > 0 ? ( - - - -
- 📎 - Chi tiết các file -
-
- - -
-

- Danh sách file đính kèm -

- -
- {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 ( - -
- - {isImage ? "🖼️" : "📄"} - - -
-

- {file.fileName} -

- -

- {extension || "FILE"} -

-
-
- - - Mở - -
- ); - }, - )} -
-
-
-
-
- ) : ( -
Không có file nào được tải lên
- )} -
- - {selectedSpecialty.glassRequired && ( - <> -
-

CẦN ĐEO KÍNH

- -

-
- -
-

LOẠI KÍNH

- -

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

-
- -
-

ĐÃ PHÁT KÍNH CHƯA

- - {selectedSpecialty.isGlassesDelivered ? ( -

- Đã phát kính -

- ) : ( -

- Chưa phát kính -

- )} -
- - )} -
-
- )} - - ); -}; - -export default DetailConsultation; diff --git a/src/components/page/consultation/DetailConsultation/index.ts b/src/components/page/consultation/DetailConsultation/index.ts deleted file mode 100644 index 2038c73..0000000 --- a/src/components/page/consultation/DetailConsultation/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./DetailConsultation"; diff --git a/src/components/page/consultation/MainPageConsultation/MainPageConsultation.tsx b/src/components/page/consultation/MainPageConsultation/MainPageConsultation.tsx deleted file mode 100644 index 77715c1..0000000 --- a/src/components/page/consultation/MainPageConsultation/MainPageConsultation.tsx +++ /dev/null @@ -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(""); - - const [openCancelDialog, setOpenCancelDialog] = useState(false); - - const [openCompleteDialog, setOpenCompleteDialog] = useState(false); - - const consultationQuery = useQuery({ - queryKey: [QUERY_KEY.table_list_consultation, _page, _pageSize, _keyword], - - queryFn: async () => { - const res = await httpRequest({ - 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({ - 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 ( -
-
- -
-
- {/* HEADER */} -
-

Phiên khám

- {isAllTab && ( - } - // onClick={handleOpenCreate} - href="/consultation/create" - > - Tạo phiên khám - - )} -
- - {/* SEARCH */} -
-
- updateQuery("_keyword", value)} - placeholder="Tìm kiếm phiên khám..." - /> -
- - {isAllTab && ( -
- - name="Chuyên khoa" - value={_specialtyId} - onChange={(value) => updateQuery("_specialtyId", value ?? "")} - listOption={ - specialtyQuery.data?.map((item) => ({ - uuid: item.id, - name: item.name, - })) || [] - } - /> - - - name="Trạng thái" - value={_status ? Number(_status) : null} - onChange={(value) => updateQuery("_status", value ?? "")} - listOption={[ - { - uuid: TYPE_STATUS.Draft, - name: "Đang chờ khám", - }, - { - uuid: TYPE_STATUS.InProgress, - name: "Đang khám tổng quát", - }, - { - uuid: TYPE_STATUS.ToSpecialty, - name: "Chờ khám chuyên khoa", - }, - { - uuid: TYPE_STATUS.PendingPrescription, - name: "Chờ nhận thuốc", - }, - { - uuid: TYPE_STATUS.Completed, - name: "Hoàn thành", - }, - { - uuid: TYPE_STATUS.Cancelled, - name: "Đã hủy", - }, - ]} - /> -
- )} -
- - {/* TABLE */} - -
item.id} - data={consultationData} - column={[ - { - title: "TÊN BỆNH NHÂN", - - render: (item: ConsultationItem) => ( - - - - - {item.patientName} - - - - -

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

-
-
-
- ), - }, - - { - title: "MÃ PHIÊN KHÁM", - fixedLeft: true, - render: (item: ConsultationItem) => { - const specialty = item.specialtyClinics.find( - (x) => x.specialtyId === _type, - ); - - const detailHref = isAllTab - ? `/consultation/${item.id}` - : `/consultation/${item.id}?_type=${specialty?.id ?? ""}`; - - return ( - - - - - {item.sessionCode} - - - - -

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

-
-
-
- ); - }, - }, - - { - title: "NGÀY KHÁM", - render: (item: ConsultationItem) => ( - - {item.visitDate - ? moment(item.visitDate).format("DD/MM/YYYY") - : "---"} - - ), - }, - - { - title: "DẤU HIỆU", - - render: (item: ConsultationItem) => ( - {item.symptomsText || "---"} - ), - }, - - { - title: "CHUẨN ĐOÁN", - - render: (item: ConsultationItem) => ( - {item.diagnosis || "---"} - ), - }, - - { - title: "KẾ HOẠCH ĐIỀU TRỊ", - - render: (item: ConsultationItem) => ( - {item.treatmentPlan || "---"} - ), - }, - - { - title: "GHI CHÚ BÁC SĨ", - - render: (item: ConsultationItem) => ( - {item.doctorNotes || "---"} - ), - }, - { - title: "TRẠNG THÁI", - render: (item: ConsultationItem) => { - // ===== TAB CHUYÊN KHOA ===== - if (!isAllTab) { - const specialty = item.specialtyClinics.find( - (x) => x.specialtyId === _type, - ); - - return ( - - ); - } - - // ===== TAB TẤT CẢ ===== - return ( - - ); - }, - }, - { - fixedRight: true, - - title: "ACTION", - - render: (item: ConsultationItem) => { - const isCancelled = item.status === TYPE_STATUS.Cancelled; - - const isCompleted = item.status === TYPE_STATUS.Completed; - - const isDisabled = isCancelled || isCompleted; - - // ===== TAB CHUYÊN KHOA ===== - if (!isAllTab) { - const specialty = item.specialtyClinics.find( - (x) => x.specialtyId === _type, - ); - - if (!specialty) return null; - - return ( - - Cập nhật - - ); - } - - // ===== TAB TẤT CẢ ===== - return ( - /* - 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 - */ -
- - - - - - {!isDisabled && ( - -
- {item.specialtyClinics.map((specialty) => ( - - - {specialty.specialtyName} - - ))} -
-
- )} -
- - handleOpenCancel(item)} - className="w-full sm:w-auto" - > - Hủy - - - handleOpenComplete(item)} - className="w-full sm:w-auto" - > - Hoàn thành - -
- ); - }, - }, - ]} - /> - - updateQuery("_page", value)} - onSetPageSize={(value) => updateQuery("_pageSize", value)} - dependencies={[_pageSize, _keyword, _specialtyId, _status]} - /> - - - {/* CANCEL DIALOG */} - - - {/* COMPLETE DIALOG */} - - - ); -}; - -export default MainPageConsultation; diff --git a/src/components/page/consultation/MainPageConsultation/index.ts b/src/components/page/consultation/MainPageConsultation/index.ts deleted file mode 100644 index 5f96bbb..0000000 --- a/src/components/page/consultation/MainPageConsultation/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./MainPageConsultation"; diff --git a/src/components/page/consultation/TableHistoryPatient/TableHistoryPatient.tsx b/src/components/page/consultation/TableHistoryPatient/TableHistoryPatient.tsx deleted file mode 100644 index 79f509e..0000000 --- a/src/components/page/consultation/TableHistoryPatient/TableHistoryPatient.tsx +++ /dev/null @@ -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({ - queryKey: [QUERY_KEY.table_list_consultation, patientId, "full-history"], - enabled: !!patientId, - queryFn: async () => { - const res = await httpRequest({ - 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 ( -
-
-

- Lịch sử phiên khám của {patientName} -

- - -
-
-
- updateQuery("_keyword", value)} - placeholder="Tìm kiếm phiên khám..." - /> -
-
- - -
item.id} - data={data} - column={[ - { - title: "MÃ PHIÊN KHÁM", - render: (item: ConsultationItem) => ( - - {item.sessionCode} - - ), - }, - { - title: "NGÀY KHÁM", - render: (item: ConsultationItem) => ( - - {item.visitDate - ? moment(item.visitDate).format("DD/MM/YYYY") - : "---"} - - ), - }, - { - title: "DẤU HIỆU", - render: (item: ConsultationItem) => ( - {item.symptomsText || "---"} - ), - }, - { - title: "CHẨN ĐOÁN", - render: (item: ConsultationItem) => ( - {item.diagnosis || "---"} - ), - }, - { - title: "TRẠNG THÁI", - render: (item: ConsultationItem) => ( - - ), - }, - ]} - /> - - - updateQuery("_page", value)} - onSetPageSize={(value) => updateQuery("_pageSize", value)} - dependencies={[_pageSize, _keyword]} - /> - - ); -}; - -export default TableHistoryPatient; diff --git a/src/components/page/consultation/TableHistoryPatient/index.ts b/src/components/page/consultation/TableHistoryPatient/index.ts deleted file mode 100644 index 2b62655..0000000 --- a/src/components/page/consultation/TableHistoryPatient/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./TableHistoryPatient"; diff --git a/src/components/page/consultation/UpdateConsultation/UpdateConsultation.tsx b/src/components/page/consultation/UpdateConsultation/UpdateConsultation.tsx deleted file mode 100644 index 9255cba..0000000 --- a/src/components/page/consultation/UpdateConsultation/UpdateConsultation.tsx +++ /dev/null @@ -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([]); - const [uploading, setUploading] = useState(false); - - const [form, setForm] = useState(defaultForm); - - /* ========================= - GET DETAIL API - ========================= */ - const consulDetailQuery = useQuery({ - queryKey: [QUERY_KEY.chi_tiet_phien_kham, id], - enabled: !!id, - queryFn: async () => { - const res = await httpRequest({ - 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 ( -
- Đang tải dữ liệu phiên khám... -
- ); - } - const eyeClinic = consul?.specialtyClinics?.find( - (item) => - item.specialtyName?.toLowerCase().includes("mắt") && - item.glassRequired === true, - ); - - /* ========================= - MAIN UI - ========================= */ - return ( - -
- {/* HEADER */} -
-
-

- Phiên khám -

-

- Cập nhật thông tin chi tiết quá trình khám bệnh -

-
-
- - {/* GENERAL INFO (PATIENT) */} - - Bệnh nhân * - - } - 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 - /> - {/*
*/} - - - - Lý do khám * - - } - name="chiefComplaint" - type="text" - value={form.chiefComplaint} - placeholder="Nhập lý do khám" - isRequired - onChangeValue={(v) => - setForm((prev) => ({ ...prev, chiefComplaint: String(v) })) - } - /> - - Triệu chứng * - - } - name="symptomsText" - placeholder="Nhập triệu chứng" - isRequired - value={form.symptomsText} - onChangeValue={(v) => - setForm((prev) => ({ ...prev, symptomsText: String(v) })) - } - /> - - - Chỉ số huyết áp, chỉ số mạch{" "} - * - - } - name="vitalSigns" - placeholder="Nhập chỉ số huyết áp, chỉ số mạch" - value={form.vitalSigns} - isRequired - onChangeValue={(v) => - setForm((prev) => ({ ...prev, vitalSigns: String(v) })) - } - /> - - - - Chẩn đoán * - - } - name="diagnosis" - placeholder="Nhập chẩn đoán" - value={form.diagnosis} - isRequired - onChangeValue={(v) => - setForm((prev) => ({ ...prev, diagnosis: String(v) })) - } - /> - - Ghi chú bác sĩ} - name="doctorNotes" - placeholder="Nhập ghi chú" - value={form.doctorNotes} - onChangeValue={(v) => - setForm((prev) => ({ ...prev, doctorNotes: String(v) })) - } - /> - -