-
-
-
- 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
+
+
+
+
+ {/* Validation Panel */}
+
+ {validationIssues && validationIssues.length > 0 ? (
+
+
+
+ {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
- )}
-
-
-
-
-
-
-
-
- {/* 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 && (
- <>
-
-
-
-
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) }))
- }
- />
-
-
- );
-};
-
-export default UpdateConsultation;
diff --git a/src/components/page/consultation/UpdateConsultation/index.ts b/src/components/page/consultation/UpdateConsultation/index.ts
deleted file mode 100644
index 73f65dd..0000000
--- a/src/components/page/consultation/UpdateConsultation/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./UpdateConsultation";
diff --git a/src/components/page/consultation/UpdateSpecialtyClinicId/UpdateSpecialtyClinicId.tsx b/src/components/page/consultation/UpdateSpecialtyClinicId/UpdateSpecialtyClinicId.tsx
deleted file mode 100644
index f06c35f..0000000
--- a/src/components/page/consultation/UpdateSpecialtyClinicId/UpdateSpecialtyClinicId.tsx
+++ /dev/null
@@ -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([]);
- 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({
- 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: "",
- 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 (
-
- Đang tải dữ liệu phiên khám...
-
- );
- }
-
- return (
-
-
- {/* HEADER */}
-
-
-
- Phiên khám chuyên khoa
-
-
- Cập nhật kết quả chi tiết quá trình khám bệnh của bệnh nhân
-
-
-
-
- {/* THÔNG TIN HÀNH CHÍNH & PHIÊN KHÁM TỔNG QUÁT */}
-
-
- Thông tin tổng quan phiên khám
-
-
-
-
-
- Tên bệnh nhân
-
-
- {generalData?.patientName || "Chưa có thông tin"}
-
-
-
-
- Ngày sinh
-
-
- {generalData?.patientDateOfBirth
- ? moment(generalData?.patientDateOfBirth).format("DD/MM/YYYY")
- : "---"}
-
-
-
-
- Mã phiên khám tổng quát
-
-
- #{generalData?.sessionCode || "Chưa có mã"}
-
-
-
-
-
- {/* KHỐI THÔNG TIN KHÁM CHUYÊN KHOA */}
-
-
-
- Chuyên khoa tiếp nhận:
-
-
-
- {selectedSpecialtyClinic?.specialtyName || "Chưa có thông tin"}
-
-
-
-
-
- Ghi chú chuyên khoa
- *
-
- }
- name="specialtyNotes"
- type="text"
- value={form.specialtyNotes}
- placeholder="Nhập ghi chú chuyên khoa"
- isRequired
- onChangeValue={(v) =>
- setForm((prev) => ({ ...prev, specialtyNotes: String(v) }))
- }
- />
-
- Chẩn đoán chuyên khoa
- *
-
- }
- name="diagnosis"
- placeholder="Nhập chẩn đoán chuyên khoa"
- isRequired
- value={form.diagnosis}
- onChangeValue={(v) =>
- setForm((prev) => ({ ...prev, diagnosis: String(v) }))
- }
- />
-
- Ghi chú thủ thuật / can thiệp
- *
-
- }
- name="procedureNotes"
- placeholder="Nhập ghi chú thủ thuật / can thiệp"
- value={form.procedureNotes}
- isRequired
- onChangeValue={(v) =>
- setForm((prev) => ({ ...prev, procedureNotes: String(v) }))
- }
- />
-
-
-
- {/* KHỐI THÔNG TIN KHÁM MẮT (Chỉ hiển thị khi là khoa mắt) */}
- {isEyeClinic && (
-
-
- Chỉ định thị lực (Khoa Mắt)
-
-
-
-
-
- {form.glassRequired && (
-
-
-
-
-
- )}
-
-
- )}
-
-
-
-
- {uploading && (
-
Đang upload file...
- )}
-
-
- {/* BUTTON ACTION */}
-
- router.back()}
- className="min-w-[120px]"
- >
- Hủy bỏ
-
-
- {specialtyResultsMutation.isPending
- ? "Đang cập nhật..."
- : "Cập nhật kết quả"}
-
-
-
-
- );
-};
-
-export default UpdateSpecialtyClinicId;
diff --git a/src/components/page/consultation/UpdateSpecialtyClinicId/index.ts b/src/components/page/consultation/UpdateSpecialtyClinicId/index.ts
deleted file mode 100644
index 3d1235d..0000000
--- a/src/components/page/consultation/UpdateSpecialtyClinicId/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./UpdateSpecialtyClinicId";
diff --git a/src/components/page/patient/DetailPatient/DetailPatient.tsx b/src/components/page/patient/DetailPatient/DetailPatient.tsx
deleted file mode 100644
index c1e92e3..0000000
--- a/src/components/page/patient/DetailPatient/DetailPatient.tsx
+++ /dev/null
@@ -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({
- queryKey: [QUERY_KEY.chi_tiet_benh_nhan, patientId],
- enabled: !!patientId,
- queryFn: async () => {
- const res = await httpRequest({
- 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({
- queryKey: [QUERY_KEY.table_list_consultation, patientId],
- enabled: !!patientId,
- queryFn: async () => {
- const res = await httpRequest({
- 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 (
-
- Đang tải dữ liệu...
-
- );
- }
-
- return (
-
-
-
-
-
-
router.back()}
- >
- {patient?.fullName}
-
-
- }
- onClick={() =>
- router.push(
- `${PATH.CONSULTATION}/create?patientId=${patient?.id}`,
- )
- }
- >
- Tạo phiên khám
-
-
-
-
- {/* Thông tin chi tiết bệnh nhân */}
-
-
-
- Mã bệnh nhân
-
-
- {patient?.patientCode}
-
-
-
-
-
Họ tên
-
- {patient?.fullName}
-
-
-
-
-
- Mã định danh
-
-
- {patient?.identificationNumber}
-
-
-
-
-
- Ngày sinh
-
-
- {patient?.dateOfBirth
- ? moment(patient?.dateOfBirth).format("DD/MM/YYYY")
- : "---"}
-
-
-
-
-
- Giới tính
-
-
- {patient?.gender}
-
-
-
-
-
- Số điện thoại
-
-
- {patient?.phone}
-
-
-
-
-
Email
-
- {patient?.email}
-
-
-
-
-
- Địa chỉ
-
-
- {patient?.address || "-"}
-
-
-
-
-
- Người liên hệ khẩn cấp
-
-
- {patient?.emergencyContactName || "-"}
-
-
-
-
-
- SĐT khẩn cấp
-
-
- {patient?.emergencyContactPhone || "-"}
-
-
-
-
-
Dị ứng
-
- {patient?.allergyNotes || "-"}
-
-
-
-
-
- Bệnh nền
-
-
- {patient?.chronicDiseaseNotes || "-"}
-
-
-
-
-
-
-
- {/* Lịch sử phiên khám */}
-
-
-
- Lịch sử phiên khám của {patient?.fullName}
-
-
- Tất cả
-
-
-
- item.id}
- data={consultationData}
- column={[
- {
- title: "TÊN BỆNH NHÂN",
-
- render: (item: ConsultationItem) => (
- {item.patientName}
- ),
- },
-
- {
- title: "MÃ PHIÊN KHÁM",
- fixedLeft: true,
- render: (item: ConsultationItem) => (
-
-
-
-
- {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: "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 {waitingSpecialties || "---"};
- },
- },
- {
- 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 {completedSpecialties || "---"};
- },
- },
- {
- title: "TRẠNG THÁI",
- fixedRight: true,
- render: (item: ConsultationItem) => (
-
- ),
- },
- ]}
- />
-
-
-
- );
-};
-
-export default DetailPatient;
diff --git a/src/components/page/patient/DetailPatient/index.ts b/src/components/page/patient/DetailPatient/index.ts
deleted file mode 100644
index e2ba4d7..0000000
--- a/src/components/page/patient/DetailPatient/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./DetailPatient";
diff --git a/src/components/page/patient/MainPagePatient/MainPagePatient.tsx b/src/components/page/patient/MainPagePatient/MainPagePatient.tsx
deleted file mode 100644
index 6d64eae..0000000
--- a/src/components/page/patient/MainPagePatient/MainPagePatient.tsx
+++ /dev/null
@@ -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("");
-
- 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({
- queryKey: [QUERY_KEY.table_list_patient, _page, _pageSize, _keyword],
-
- queryFn: async () => {
- const res = await httpRequest({
- 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 (
-
- {/* HEADER */}
-
-
Bệnh nhân
-
- }
- onClick={handleOpenCreate}
- >
- Tạo mới
-
-
-
- {/* SEARCH */}
-
-
- updateQuery("_keyword", value)}
- placeholder="Tìm kiếm theo tên bệnh nhân, ID"
- />
-
-
-
- {/* TABLE */}
-
- item.id}
- data={patientData}
- column={[
- {
- fixedLeft: true,
- title: "TÊN BỆNH NHÂN",
-
- render: (item: PatientItem) => (
-
-
-
-
- {item.fullName}
-
-
-
-
-
- Chi tiết bệnh nhân
-
-
-
-
- ),
- },
-
- {
- title: "MÃ ĐỊNH DANH",
-
- render: (item: PatientItem) => (
- {item.identificationNumber || "---"}
- ),
- },
-
- {
- title: "SỐ ĐIỆN THOẠI",
-
- render: (item: PatientItem) => {item.phone || "---"},
- },
-
- {
- title: "EMAIL",
-
- render: (item: PatientItem) => {item.email || "---"},
- },
-
- {
- title: "GIỚI TÍNH",
-
- render: (item: PatientItem) => (
- {item.gender || "---"}
- ),
- },
-
- {
- 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
- */
-
- {/* UPDATE */}
- }
- onClick={() => handleOpenUpdate(item.id)}
- />
-
- {/* DELETE */}
- }
- onClick={() => handleOpenDelete(item.id)}
- />
-
- ),
- },
- ]}
- />
-
- updateQuery("_page", value)}
- onSetPageSize={(value) => updateQuery("_pageSize", value)}
- dependencies={[_pageSize, _keyword]}
- />
-
- {/* DELETE DIALOG */}
-
-
- {/* CREATE POPUP */}
-
-
-
-
- {/* UPDATE POPUP */}
-
-
-
-
- );
-};
-
-export default MainPagePatient;
diff --git a/src/components/page/patient/MainPagePatient/index.ts b/src/components/page/patient/MainPagePatient/index.ts
deleted file mode 100644
index 07c26e4..0000000
--- a/src/components/page/patient/MainPagePatient/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./MainPagePatient";
diff --git a/src/components/page/patient/PopupCreatePatient/PopupCreatePatient.tsx b/src/components/page/patient/PopupCreatePatient/PopupCreatePatient.tsx
deleted file mode 100644
index 9fd217c..0000000
--- a/src/components/page/patient/PopupCreatePatient/PopupCreatePatient.tsx
+++ /dev/null
@@ -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({
- 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 (
-
-
- {/* CLOSE */}
-
-
- {/* HEADER */}
-
-
- Thêm bệnh nhân
-
-
-
- {/* BODY */}
-
-
-
- Mã định danh
- *
-
- }
- placeholder="Mã định danh"
- name="identificationNumber"
- type="number"
- isNumber
- min={12}
- max={12}
- value={form.identificationNumber}
- isRequired
- />
-
-
- Tên bệnh nhân
- *
-
- }
- placeholder="Tên bệnh nhân"
- name="fullName"
- type="text"
- value={form.fullName}
- isRequired
- />
-
-
-
-
- Ngày sinh
- *
-
- }
- placeholder="DD/MM/YYYY"
- name="dateOfBirth"
- type="text"
- value={form.dateOfBirth}
- onChangeValue={(value) =>
- setForm((prev) => ({
- ...prev,
- dateOfBirth: formatDateOfBirth(String(value)),
- }))
- }
- />
-
-
- Số điện thoại
- *
-
- }
- placeholder="Số điện thoại"
- name="phone"
- type="number"
- isNumber
- min={8}
- max={11}
- value={form.phone}
- isRequired
- />
-
- Email}
- placeholder="Email"
- name="email"
- type="email"
- isEmail
- value={form.email}
- />
-
-
-
-
- {/* FOOTER */}
-
-
- Hủy bỏ
-
-
-
- {({ isDone }) => (
-
- {createPatientMutation.isPending ? "Đang tạo..." : "Tạo"}
-
- )}
-
-
-
-
- );
-};
-
-export default PopupCreatePatient;
diff --git a/src/components/page/patient/PopupCreatePatient/index.ts b/src/components/page/patient/PopupCreatePatient/index.ts
deleted file mode 100644
index f4ca8b8..0000000
--- a/src/components/page/patient/PopupCreatePatient/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./PopupCreatePatient";
diff --git a/src/components/page/patient/PopupUpdatePatient/PopupUpdatePatient.tsx b/src/components/page/patient/PopupUpdatePatient/PopupUpdatePatient.tsx
deleted file mode 100644
index 490faf4..0000000
--- a/src/components/page/patient/PopupUpdatePatient/PopupUpdatePatient.tsx
+++ /dev/null
@@ -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(defaultForm);
-
- const patientDetailQuery = useQuery({
- queryKey: [QUERY_KEY.chi_tiet_benh_nhan, id],
- enabled: !!id,
- queryFn: async () => {
- const res = await httpRequest({
- 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 (
-
-
-
- );
- }
-
- return (
-
-
- {/* CLOSE */}
-
-
- {/* HEADER */}
-
-
- Cập nhật thông tin bệnh nhân
-
-
-
- {/* BODY */}
-
-
-
- Tên bệnh nhân
- *
-
- }
- placeholder="Tên bệnh nhân"
- name="fullName"
- type="text"
- value={form.fullName}
- isRequired
- />
-
- {/* GENDER */}
-
-
-
- Số điện thoại
- *
-
- }
- placeholder="Số điện thoại"
- name="phone"
- type="phone"
- isPhone
- value={form.phone}
- isRequired
- />
-
-
- Email *
-
- }
- placeholder="Email"
- name="email"
- type="email"
- isEmail
- value={form.email}
- />
-
-
-
-
- {/* FOOTER */}
-
-
- Hủy bỏ
-
-
-
- {({ isDone }) => (
-
- {updatePatientMutation.isPending
- ? "Đang cập nhật..."
- : "Cập nhật"}
-
- )}
-
-
-
-
- );
-};
-
-export default PopupUpdatePatient;
diff --git a/src/components/page/patient/PopupUpdatePatient/index.ts b/src/components/page/patient/PopupUpdatePatient/index.ts
deleted file mode 100644
index b14b717..0000000
--- a/src/components/page/patient/PopupUpdatePatient/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./PopupUpdatePatient";
diff --git a/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx b/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx
deleted file mode 100644
index 6d2f487..0000000
--- a/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx
+++ /dev/null
@@ -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(false);
- const queryKeys = [QUERY_KEY.table_list_consultation];
- const [file, setFile] = useState(null);
-
- const [form, setForm] = useState({
- examinationSessionSpecialtyId: "",
- notes: "",
- prescriptionImg: "",
- items: [defaultMedicine],
- });
-
- /* =========================
- GET EXAMINATION SESSION
- ========================= */
- const examinationSessionQuery = useQuery({
- queryKey: [QUERY_KEY.table_list_consultation],
- queryFn: async () => {
- const res = await httpRequest({
- 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 (
-
-
- {/* HEADER */}
-
-
-
- Tạo đơn thuốc
-
-
- Tạo mới đơn thuốc cho bệnh nhân
-
-
-
-
-
- {funcCreatePrescription.isPending || loading
- ? "Đang tạo đơn..."
- : "Tạo đơn thuốc"}
-
-
-
-
- {/* GENERAL INFO */}
-
-
- Thông tin chung
-
-
-
- {/* EXAMINATION SESSION */}
-
- Phiên khám chuyên khoa
- *
-
- }
- 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 */}
-
-
- Hình ảnh đơn thuốc *
-
- }
- name="prescriptionImg"
- file={file}
- setFile={setFile}
- path={""}
- />
-
-
- {/* NOTES */}
-
- Ghi chú*
-
- }
- name="notes"
- placeholder="Nhập ghi chú"
- value={form.notes}
- isBlur
- isRequired
- max={5000}
- onChangeValue={(value) =>
- setForm((prev) => ({
- ...prev,
- notes: String(value),
- }))
- }
- />
-
-
-
- {/* MEDICINE LIST */}
-
-
-
- Danh sách thuốc
-
-
-
- }
- onClick={handleAddMedicine}
- >
- Thêm thuốc
-
-
-
-
-
- {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
-
- {/* TITLE */}
-
-
- Thuốc #{index + 1}
-
-
- {form.items.length > 1 && (
-
- )}
-
-
- {/* INPUTS */}
-
-
- Tên thuốc*
-
- }
- name={`items.${index}.medicineName`}
- placeholder="Nhập tên thuốc"
- value={medicine.medicineName}
- isRequired
- type="text"
- onChangeValue={(value) =>
- handleChangeMedicine(index, "medicineName", String(value))
- }
- />
-
-
- Liều lượng*
-
- }
- name={`items.${index}.dosage`}
- placeholder="Ví dụ: 500mg"
- value={medicine.dosage}
- type="text"
- isRequired
- onChangeValue={(value) =>
- handleChangeMedicine(index, "dosage", String(value))
- }
- />
-
-
- Tần xuất*
-
- }
- 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))
- }
- />
-
-
- Thời gian dùng*
-
- }
- isRequired
- name={`items.${index}.duration`}
- placeholder="Ví dụ: 7 ngày"
- value={medicine.duration}
- type="text"
- onChangeValue={(value) =>
- handleChangeMedicine(index, "duration", String(value))
- }
- />
-
-
- {/* INSTRUCTION */}
-
-
- Hướng dẫn sử dụng*
-
- }
- 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))
- }
- />
-
-
- ))}
-
-
-
-
- );
-};
-
-export default CreatePrescription;
diff --git a/src/components/page/prescription/CreatePrescription/index.ts b/src/components/page/prescription/CreatePrescription/index.ts
deleted file mode 100644
index 7acf7f4..0000000
--- a/src/components/page/prescription/CreatePrescription/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./CreatePrescription";
diff --git a/src/components/page/prescription/DetailPrescription/DetailPrescription.tsx b/src/components/page/prescription/DetailPrescription/DetailPrescription.tsx
deleted file mode 100644
index 4e4da0e..0000000
--- a/src/components/page/prescription/DetailPrescription/DetailPrescription.tsx
+++ /dev/null
@@ -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({
- queryKey: [QUERY_KEY.chi_tiet_don_thuoc, PrescriptionId],
-
- enabled: !!PrescriptionId,
-
- queryFn: async () => {
- const res = await httpRequest({
- showMessageFailed: true,
-
- http: prescriptionServices.detailPrescription(PrescriptionId),
- });
-
- return (
- res || {
- id: "",
- examinationSessionSpecialtyId: "",
- prescriptionCode: "",
- prescriptionImg: null,
- notes: "",
- createdBy: "",
- items: [],
- }
- );
- },
- });
-
- const data = prescriptionDetailQuery.data;
-
- const specialtyListQuery = useQuery({
- 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 (
-
-
router.back()}
- >
-
-
-
- Chi tiết đơn thuốc
-
-
-
- {/* LOADING */}
- {prescriptionDetailQuery.isLoading &&
Đang tải...
}
-
- {/* ERROR */}
- {prescriptionDetailQuery.isError && (
-
Lỗi tải dữ liệu
- )}
-
- {/* CONTENT */}
- {data && (
-
-
- Mã đơn thuốc: {data.prescriptionCode}
-
-
-
- Chuyên khoa:{" "}
- {specialtyName || data.examinationSessionSpecialtyId || "---"}
-
-
-
- Ghi chú: {data.notes || "---"}
-
-
- {/* IMAGE */}
-
-
Ảnh đơn thuốc:
-
- {data.prescriptionImg ? (
- <>
-
setOpenImagePreview(true)}
- unoptimized
- />
-
- {/* IMAGE PREVIEW POPUP */}
- setOpenImagePreview(false)}
- >
-
-
-
-
- >
- ) : (
- Không có ảnh
- )}
-
-
-
- {/* ITEMS */}
-
-
Danh sách thuốc:
-
-
- {data.items?.length ? (
- data.items.map((item, index) => (
-
-
- Tên thuốc: {item.medicineName}
-
-
- Liều lượng: {item.dosage}
-
-
- Tần suất: {item.frequency}
-
-
- Thời gian: {item.duration}
-
-
- Hướng dẫn: {item.instructions}
-
-
- ))
- ) : (
-
Không có thuốc
- )}
-
-
-
- )}
-
- );
-};
-
-export default DetailPrescription;
diff --git a/src/components/page/prescription/DetailPrescription/index.ts b/src/components/page/prescription/DetailPrescription/index.ts
deleted file mode 100644
index 5d46b06..0000000
--- a/src/components/page/prescription/DetailPrescription/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./DetailPrescription";
diff --git a/src/components/page/prescription/MainPrescription/MainPrescription.tsx b/src/components/page/prescription/MainPrescription/MainPrescription.tsx
deleted file mode 100644
index 24d5f89..0000000
--- a/src/components/page/prescription/MainPrescription/MainPrescription.tsx
+++ /dev/null
@@ -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({
- queryKey: [
- QUERY_KEY.table_list_patientrescription,
- _page,
- _pageSize,
- _keyword,
- ],
-
- queryFn: async () => {
- const res = await httpRequest({
- 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 (
-
- {/* HEADER */}
-
-
Đơn thuốc
-
- }
- href={PATH.CREATE_PRESCRIPTION}
- >
- Tạo mới
-
-
-
- {/* SEARCH */}
-
-
- updateQuery("_keyword", value)}
- placeholder="Tìm theo mã đơn thuốc"
- />
-
-
-
-
- item.id}
- data={prescriptionData || []}
- column={[
- // ================= ID / CODE =================
- {
- title: "MÃ ĐƠN THUỐC",
- render: (item: any) => (
-
-
-
-
- {item.prescriptionCode}
-
-
-
-
-
- Chi tiết đơn thuốc
-
-
-
-
- ),
- },
-
- // ================= NOTES =================
- {
- title: "GHI CHÚ",
- render: (item: any) => {item.notes || "-"},
- },
-
- // ================= IMAGE =================
- {
- title: "ẢNH ĐƠN THUỐC",
- render: (item: any) =>
- item.prescriptionImg ? (
-
- ) : (
- Không có ảnh
- ),
- },
-
- // ================= ITEMS COUNT =================
- {
- title: "SỐ THUỐC",
- render: (item: any) => {item.items?.length || 0},
- },
-
- // ================= ACTION =================
- {
- fixedRight: true,
- title: "ACTION",
-
- render: (item: any) => (
-
- }
- href={`${PATH.PRESCRIPTION}/${item.id}`}
- />
-
- ),
- },
- ]}
- />
-
- updateQuery("_page", value)}
- onSetPageSize={(value) => updateQuery("_pageSize", value)}
- dependencies={[_pageSize, _keyword]}
- />
-
- );
-};
-
-export default MainPrescription;
diff --git a/src/components/page/prescription/MainPrescription/index.tsx b/src/components/page/prescription/MainPrescription/index.tsx
deleted file mode 100644
index 46b4fe5..0000000
--- a/src/components/page/prescription/MainPrescription/index.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./MainPrescription";
diff --git a/src/components/page/profile/MainPageProfile/MainPageProfile.tsx b/src/components/page/profile/MainPageProfile/MainPageProfile.tsx
deleted file mode 100644
index 2e6ca86..0000000
--- a/src/components/page/profile/MainPageProfile/MainPageProfile.tsx
+++ /dev/null
@@ -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({
- 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,
- }
- );
- },
- });
-
- // 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 (
-
- {/* HEADER */}
-
-
Thông tin tài khoản
-
-
-
- Chỉnh sửa
-
-
-
-
- {/* AVATAR */}
-
-
-
-
- {/* MAIN */}
-
- {/* BASIC INFO */}
-
-
- Thông tin cơ bản
-
-
-
- {/* NAME */}
-
-
-
- Họ và tên
-
-
- {currentUserProfile?.fullName || "---"}
-
-
-
- {/* DOB */}
-
-
-
- Ngày sinh
-
-
- {currentUserProfile?.birthDate ? (
-
- {currentUserProfile.birthDate}
-
- ) : (
- "---"
- )}
-
-
-
-
-
- {/* CONTACT */}
-
-
- Liên hệ
-
-
-
- {/* EMAIL */}
-
-
-
- Email
-
-
- {currentUserProfile?.email || "---"}
-
-
-
- {/* PHONE */}
-
-
-
- Số điện thoại
-
-
- {currentUserProfile?.phone || "---"}
-
-
-
-
-
-
- );
-}
-
-export default memo(MainPageProfile);
diff --git a/src/components/page/profile/MainPageProfile/index.ts b/src/components/page/profile/MainPageProfile/index.ts
deleted file mode 100644
index c08a689..0000000
--- a/src/components/page/profile/MainPageProfile/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./MainPageProfile";
diff --git a/src/components/page/profile/MainUpdateProfile/MainUpdateProfile.tsx b/src/components/page/profile/MainUpdateProfile/MainUpdateProfile.tsx
deleted file mode 100644
index 691ebd3..0000000
--- a/src/components/page/profile/MainUpdateProfile/MainUpdateProfile.tsx
+++ /dev/null
@@ -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(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({
- queryKey: [QUERY_KEY.table_list_profile],
- queryFn: async () => {
- const res = await httpRequest({
- 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 (
-
-
-
-
- {/* HEADER */}
-
-
- Chỉnh sửa thông tin tài khoản
-
-
- router.back()}
- >
- Hủy bỏ
-
-
- {({ isDone }) => {
- const hasRememberedData = !!(
- form.fullName &&
- form.email &&
- form.birthDate &&
- form.phone
- );
- const canSubmit = isDone || hasRememberedData;
- return (
-
- {funcUpdateProfile.isPending ? "Đang lưu..." : "Cập nhật"}
-
- );
- }}
-
-
-
-
- {/* MAIN FORM */}
-
- {/* KHỐI AVATAR */}
-
- setFileAvatar(file)}
- resetPath={() => {
- setFileAvatar(null);
-
- setForm((prev) => ({
- ...prev,
- avatarUrl: "",
- }));
- }}
- />
-
-
- {/* KHỐI THÔNG TIN CÁ NHÂN CHUẨN BACKEND */}
-
-
- {/* Cột trái */}
-
-
- Họ Và Tên
- *
-
- }
- isRequired
- onChangeValue={(v) =>
- setForm((prev) => ({ ...prev, fullName: String(v) }))
- }
- />
-
-
- Ngày sinh
- *
-
- }
- value={form.birthDate}
- onChangeValue={(v) =>
- setForm((prev) => ({
- ...prev,
- birthDate: formatDateOfBirth(String(v)),
- }))
- }
- />
-
-
- {/* Cột phải */}
-
-
- Email
- *
-
- }
- isRequired
- onChangeValue={(v) =>
- setForm((prev) => ({ ...prev, email: String(v) }))
- }
- />
-
-
- Số điện thoại
- *
-
- }
- onChangeValue={(v) =>
- setForm((prev) => ({ ...prev, phone: String(v) }))
- }
- />
-
-
-
-
-
-
-
- );
-}
-
-export default memo(MainUpdateProfile);
diff --git a/src/components/page/profile/MainUpdateProfile/index.ts b/src/components/page/profile/MainUpdateProfile/index.ts
deleted file mode 100644
index 1aa8a77..0000000
--- a/src/components/page/profile/MainUpdateProfile/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./MainUpdateProfile";
diff --git a/src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx b/src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx
deleted file mode 100644
index 183b5dc..0000000
--- a/src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx
+++ /dev/null
@@ -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.TODAY);
-
- const isInvalidDate =
- !!date?.from && moment(date.from).isAfter(moment().endOf("day"));
-
- const reportQuery = useQuery({
- 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({
- 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 (
-
- {/* HEADER */}
-
-
-
Thống kê phiên khám
-
-
-
-
-
- {/* CARDS */}
-
-
-
-
-
-
-
Tổng lượt khám
-
-
- {report?.totalSessions || 0}
-
-
-
-
-
-
-
-
-
Lượt khám chuyên khoa
-
-
- {report?.totalSpecialtySubSessions || 0}
-
-
-
-
-
-
-
Đơn thuốc đã cấp
-
-
- {report?.totalPrescriptionsIssued || 0}
-
-
-
-
-
-
-
-
-
Số kính đã phát
-
-
- {report?.totalGlassesDelivered || 0}
-
-
-
-
- {/* BẢNG THỐNG KÊ */}
-
-
- Thống kê theo chuyên khoa
-
-
-
- item.specialtyId}
- data={reportData}
- column={[
- {
- title: "MÃ CHUYÊN KHOA",
- render: (item) => {item.specialtyCode},
- },
- {
- title: "TÊN CHUYÊN KHOA",
- render: (item) => {item.specialtyName},
- },
- {
- title: "SỐ CA KHÁM",
- render: (item) => (
- {item.totalCases}
- ),
- },
- ]}
- />
-
-
-
- {/* BÁO CÁO TỔNG HỢP */}
-
-
Báo cáo tổng hợp
-
-
-
- Số lượt khám bệnh nhân đạo:
- {report?.totalSpecialtySubSessions || 0} lượt
- người dân được khám chuyên khoa, cụ thể như sau:
-
-
-
- {reportData.map((item) => (
- -
- {item.specialtyName}: {item.totalCases} ca
-
- ))}
-
-
-
- Tổng số đơn thuốc đã cấp:
- {report?.totalPrescriptionsIssued || 0} đơn.
-
-
-
- Tổng số kính đã phát:
- {report?.totalGlassesDelivered || 0} cái.
-
-
-
-
- );
-};
-
-export default MainPageStatistical;
diff --git a/src/components/page/statistical/MainPageStatistical/index.ts b/src/components/page/statistical/MainPageStatistical/index.ts
deleted file mode 100644
index 48c9011..0000000
--- a/src/components/page/statistical/MainPageStatistical/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./MainPageStatistical";
diff --git a/src/components/utils/BridgeTreeSidebar.tsx b/src/components/utils/BridgeTreeSidebar.tsx
new file mode 100644
index 0000000..d6898ab
--- /dev/null
+++ b/src/components/utils/BridgeTreeSidebar.tsx
@@ -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>({
+ superstructure: true,
+ substructure: true,
+ });
+
+ const toggleNode = (nodeId: string) => {
+ setOpenNodes((prev) => ({ ...prev, [nodeId]: !prev[nodeId] }));
+ };
+
+ return (
+
+
+
TEDI DDP - Super-T
+
Mô hình cấu kiện Cầu KTS
+
+
+
+ {bridgeTreeData.map((group) => (
+
+ {/* Header nhóm chính */}
+
+
+ {/* Danh sách nút cấu kiện con */}
+ {openNodes[group.id] && group.children && (
+
+ {group.children.map((child) => {
+ const isActive = activeTab === child.tabKey;
+ return (
+
+ );
+ })}
+
+ )}
+
+ ))}
+
+
+ );
+};
diff --git a/src/components/viewer3d/Canvas3D.tsx b/src/components/viewer3d/Canvas3D.tsx
new file mode 100644
index 0000000..14bfd01
--- /dev/null
+++ b/src/components/viewer3d/Canvas3D.tsx
@@ -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 {children};
+};
+
+export const Canvas3D: React.FC = () => {
+ const { activeTab, beamData, bridgeData, isAssemblyMode } = useSuperTStore();
+
+ // Khai báo các mesh bộ phận
+ const renderSuperTBeam = () => ;
+
+ const renderDeck = () => (
+
+
+
+
+ );
+
+ const renderPile = () => (
+
+
+
+
+ );
+
+ const renderPier = () => (
+
+
+
+
+ );
+
+ const renderAbutment = () => (
+
+
+
+
+ );
+
+ const renderBearing = () => (
+
+
+
+
+ );
+
+ const renderRailing = () => (
+
+
+
+
+
+
+
+
+
+
+ );
+
+ const renderDiaphragm = () => (
+
+
+
+
+ );
+
+ return (
+
+
+
+ );
+};
diff --git a/src/components/viewer3d/DiaphragmsMesh.tsx b/src/components/viewer3d/DiaphragmsMesh.tsx
new file mode 100644
index 0000000..22f931f
--- /dev/null
+++ b/src/components/viewer3d/DiaphragmsMesh.tsx
@@ -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 (
+
+ {data.diaphragmPositions.map((pos, index) => (
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/components/viewer3d/StrandsMesh.tsx b/src/components/viewer3d/StrandsMesh.tsx
new file mode 100644
index 0000000..38ca5ff
--- /dev/null
+++ b/src/components/viewer3d/StrandsMesh.tsx
@@ -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 (
+
+ {strands.map((s, idx) => (
+
+
+
+
+ ))}
+
+ );
+};
diff --git a/src/components/viewer3d/SuperTBeamMesh.tsx b/src/components/viewer3d/SuperTBeamMesh.tsx
new file mode 100644
index 0000000..826a954
--- /dev/null
+++ b/src/components/viewer3d/SuperTBeamMesh.tsx
@@ -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 (
+
+
+
+
+ );
+};
diff --git a/src/constant/config/index.ts b/src/constant/config/index.ts
index 50d4b8c..fb4ed8d 100644
--- a/src/constant/config/index.ts
+++ b/src/constant/config/index.ts
@@ -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 = {
+ 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";
diff --git a/src/constant/images/icons.ts b/src/constant/images/icons.ts
index da6ee4f..84808e0 100644
--- a/src/constant/images/icons.ts
+++ b/src/constant/images/icons.ts
@@ -1,4 +1,4 @@
-import hamburger from "../../../public/static/images/icon-hamburger.svg";
+import LogoTedi from "../../../public/static/images/logo-tedi.png";
import full_screen from "../../../public/static/images/full-screen.svg";
import avatar from "../../../public/static/images/avatar-default.png";
import tickCircle from "../../../public/static/images/tick-circle.svg";
@@ -10,7 +10,7 @@ import emptyTable from "../../../public/static/images/empty_table.png";
import emptyFile from "../../../public/static/images/emptyFile.svg";
export default {
- hamburger,
+ LogoTedi,
full_screen,
avatar,
tickCircle,
diff --git a/src/hooks/useSuperTStore.ts b/src/hooks/useSuperTStore.ts
new file mode 100644
index 0000000..cabe4aa
--- /dev/null
+++ b/src/hooks/useSuperTStore.ts
@@ -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) => void;
+
+ bridgeData: BridgeFullData;
+ setBridgeData: (
+ component: K,
+ data: Partial,
+ ) => 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((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,
+ );
+ },
+}));
diff --git a/src/services/accountServices.ts b/src/services/accountServices.ts
deleted file mode 100644
index 9e1f5c2..0000000
--- a/src/services/accountServices.ts
+++ /dev/null
@@ -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;
diff --git a/src/services/consultationServices.ts b/src/services/consultationServices.ts
deleted file mode 100644
index 120cd63..0000000
--- a/src/services/consultationServices.ts
+++ /dev/null
@@ -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(
- `/api/v1/ExaminationSession/list`,
- {
- params,
- cancelToken: tokenAxios,
- },
- );
- },
- getConsultationsReport: (
- params: { FromDate: string; ToDate: string },
- tokenAxios?: any,
- ) => {
- return axiosClient.get(
- `/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(
- `/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;
diff --git a/src/services/patientServices.ts b/src/services/patientServices.ts
deleted file mode 100644
index 1b648ba..0000000
--- a/src/services/patientServices.ts
+++ /dev/null
@@ -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(`/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(`/api/v1/Patient/${id}`, {
- cancelToken: tokenAxios,
- });
- },
-
- deletePatient: (id: string, tokenAxios?: any) => {
- return axiosClient.delete(`/api/v1/Patient/${id}`, {
- cancelToken: tokenAxios,
- });
- },
-};
-
-export default patientServices;
diff --git a/src/services/prescriptionServices.ts b/src/services/prescriptionServices.ts
deleted file mode 100644
index c970a98..0000000
--- a/src/services/prescriptionServices.ts
+++ /dev/null
@@ -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(`/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(`/api/v1/Prescription/${id}`, {
- cancelToken: tokenAxios,
- });
- },
-
- /**
- * UPLOAD IMAGE
- */
-};
-
-export default prescriptionServices;
diff --git a/src/services/profileServices.ts b/src/services/profileServices.ts
deleted file mode 100644
index 0671a0d..0000000
--- a/src/services/profileServices.ts
+++ /dev/null
@@ -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(`/api/v1/UserProfile/list`, {
- params,
-
- cancelToken: tokenAxios,
- });
- },
- putProfile: (
- id: string,
- data: {
- fullName: string;
- email: string;
- phone: string;
- birthDate: string;
- avatarUrl: string;
- },
- ) => {
- return axiosClient.put(`/api/v1/UserProfile/${id}`, data);
- },
-};
-
-export default profileServices;
diff --git a/src/services/specialtyServices.ts b/src/services/specialtyServices.ts
deleted file mode 100644
index 5590aa3..0000000
--- a/src/services/specialtyServices.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import axiosClient from ".";
-
-export interface SpecialtyItem {
- id: string;
- code: string;
- name: string;
-}
-
-const specialtyServices = {
- getSpecialty: (tokenAxios?: any) => {
- return axiosClient.get("/api/v1/Specialty/lookup", {
- cancelToken: tokenAxios,
- });
- },
-};
-
-export default specialtyServices;
diff --git a/src/services/uploadServices.ts b/src/services/uploadServices.ts
deleted file mode 100644
index 1751362..0000000
--- a/src/services/uploadServices.ts
+++ /dev/null
@@ -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;
diff --git a/src/services/userServices.ts b/src/services/userServices.ts
deleted file mode 100644
index 9e86e35..0000000
--- a/src/services/userServices.ts
+++ /dev/null
@@ -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(`/api/v1/Users/me`, {
- cancelToken: tokenAxios,
- });
- },
-};
-
-export default userServices;
diff --git a/yarn.lock b/yarn.lock
index 1e4f209..742fef0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -151,6 +151,13 @@ __metadata:
languageName: node
linkType: hard
+"@babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.26.0":
+ version: 7.29.7
+ resolution: "@babel/runtime@npm:7.29.7"
+ checksum: 10c0/ca11572f7146b21e0bde6a9ed4bb6a89eafbee5f0944c7eb54d0d8a2dac962c33638a1d611e14faa71dfbb92b4b5f9236232208568a6b7d5c6f3f39ddb91771e
+ languageName: node
+ linkType: hard
+
"@babel/template@npm:^7.28.6":
version: 7.28.6
resolution: "@babel/template@npm:7.28.6"
@@ -187,6 +194,13 @@ __metadata:
languageName: node
linkType: hard
+"@dimforge/rapier3d-compat@npm:~0.12.0":
+ version: 0.12.0
+ resolution: "@dimforge/rapier3d-compat@npm:0.12.0"
+ checksum: 10c0/c66c24f90649c0fc870679c12e7fec1a111080d44450169b57561f957d7b6b284ad8a3ceeba95533e213176ea171351acebd3dd43885fafb33f18bfbd9d507db
+ languageName: node
+ linkType: hard
+
"@emnapi/core@npm:1.10.0, @emnapi/core@npm:^1.10.0":
version: 1.10.0
resolution: "@emnapi/core@npm:1.10.0"
@@ -653,6 +667,24 @@ __metadata:
languageName: node
linkType: hard
+"@mediapipe/tasks-vision@npm:0.10.17":
+ version: 0.10.17
+ resolution: "@mediapipe/tasks-vision@npm:0.10.17"
+ checksum: 10c0/f2f5dd9ca39d562b902a6d964f8b786f17a143f42776d0d26b8c79632b10cf210e28d98ec92de757b7bb2e6595ed4f9bbf5e49f1709b09dc08709bd276ced442
+ languageName: node
+ linkType: hard
+
+"@monogrid/gainmap-js@npm:^3.0.6":
+ version: 3.4.0
+ resolution: "@monogrid/gainmap-js@npm:3.4.0"
+ dependencies:
+ promise-worker-transferable: "npm:^1.0.4"
+ peerDependencies:
+ three: ">= 0.159.0"
+ checksum: 10c0/06104767a0f542b36e1e203fe569f78996babcdd52661f42db4f079f372b272e0849b86f84f7690b2c165a8ba931f77ad324fe82a09c9b23e26d20e76cabd3e7
+ languageName: node
+ linkType: hard
+
"@napi-rs/wasm-runtime@npm:^1.1.4":
version: 1.1.4
resolution: "@napi-rs/wasm-runtime@npm:1.1.4"
@@ -1188,6 +1220,83 @@ __metadata:
languageName: node
linkType: hard
+"@react-three/drei@npm:^10.7.8":
+ version: 10.7.8
+ resolution: "@react-three/drei@npm:10.7.8"
+ dependencies:
+ "@babel/runtime": "npm:^7.26.0"
+ "@mediapipe/tasks-vision": "npm:0.10.17"
+ "@monogrid/gainmap-js": "npm:^3.0.6"
+ "@use-gesture/react": "npm:^10.3.1"
+ camera-controls: "npm:^3.1.0"
+ cross-env: "npm:^7.0.3"
+ detect-gpu: "npm:^5.0.56"
+ glsl-noise: "npm:^0.0.0"
+ hls.js: "npm:^1.5.17"
+ maath: "npm:^0.10.8"
+ meshline: "npm:^3.3.1"
+ stats-gl: "npm:^2.2.8"
+ stats.js: "npm:^0.17.0"
+ suspend-react: "npm:^0.1.3"
+ three-mesh-bvh: "npm:^0.8.3"
+ three-stdlib: "npm:^2.35.6"
+ troika-three-text: "npm:^0.52.4"
+ tunnel-rat: "npm:^0.1.2"
+ use-sync-external-store: "npm:^1.4.0"
+ utility-types: "npm:^3.11.0"
+ zustand: "npm:^5.0.1"
+ peerDependencies:
+ "@react-three/fiber": ^9.0.0
+ react: ^19
+ react-dom: ^19
+ three: ">=0.159"
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ checksum: 10c0/b4103a79f2cebfd6ff00a869de38e602ad670df14bd08c2fa0d44b71ebebe81b000b523077df01df8c358fe406a4e1ff75c44eb727bca216d3407c1d7e91192e
+ languageName: node
+ linkType: hard
+
+"@react-three/fiber@npm:^9.7.0":
+ version: 9.7.0
+ resolution: "@react-three/fiber@npm:9.7.0"
+ dependencies:
+ "@babel/runtime": "npm:^7.17.8"
+ "@types/webxr": "npm:*"
+ base64-js: "npm:^1.5.1"
+ buffer: "npm:^6.0.3"
+ its-fine: "npm:^2.0.0"
+ react-use-measure: "npm:^2.1.7"
+ scheduler: "npm:^0.27.0"
+ suspend-react: "npm:^0.1.3"
+ use-sync-external-store: "npm:^1.4.0"
+ zustand: "npm:^5.0.3"
+ peerDependencies:
+ expo: ">=43.0"
+ expo-asset: ">=8.4"
+ expo-file-system: ">=11.0"
+ expo-gl: ">=11.0"
+ react: ">=19 <19.3"
+ react-dom: ">=19 <19.3"
+ react-native: ">=0.78"
+ three: ">=0.156"
+ peerDependenciesMeta:
+ expo:
+ optional: true
+ expo-asset:
+ optional: true
+ expo-file-system:
+ optional: true
+ expo-gl:
+ optional: true
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+ checksum: 10c0/7ad36da135b5105e379c4984455dab1bd7cfabfad7548e4ff87ee161f8265be5d9e9af7f00a761c7f3a4252dc2db7c7ea8b9bfd9db7f601dd9759ee1179dc533
+ languageName: node
+ linkType: hard
+
"@reduxjs/toolkit@npm:^2.12.0":
version: 2.12.0
resolution: "@reduxjs/toolkit@npm:2.12.0"
@@ -1422,6 +1531,13 @@ __metadata:
languageName: node
linkType: hard
+"@tweenjs/tween.js@npm:~23.1.3":
+ version: 23.1.3
+ resolution: "@tweenjs/tween.js@npm:23.1.3"
+ checksum: 10c0/811b30f5f0e7409fb41833401c501c2d6f600eb5f43039dd9067a7f70aff6dad5f5ce1233848e13f0b33a269a160d9c133f344d986cbff4f1f6b72ddecd06c89
+ languageName: node
+ linkType: hard
+
"@tybys/wasm-util@npm:^0.10.1":
version: 0.10.2
resolution: "@tybys/wasm-util@npm:0.10.2"
@@ -1431,6 +1547,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/draco3d@npm:^1.4.0":
+ version: 1.4.10
+ resolution: "@types/draco3d@npm:1.4.10"
+ checksum: 10c0/431e333b2fd67e2b081e8697e71dfb82a125fc04b2cbfa0205e4d521719d3749c964b2bf82bb7944ba0a12e5bc3d9afe387a58e68b195d838fcdbc65c8572a35
+ languageName: node
+ linkType: hard
+
"@types/estree@npm:^1.0.6":
version: 1.0.9
resolution: "@types/estree@npm:1.0.9"
@@ -1475,6 +1598,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/offscreencanvas@npm:^2019.6.4":
+ version: 2019.7.3
+ resolution: "@types/offscreencanvas@npm:2019.7.3"
+ checksum: 10c0/6d1dfae721d321cd2b5435f347a0e53b09f33b2f9e9333396480f592823bc323847b8169f7d251d2285cb93dbc1ba2e30741ac5cf4b1c003d660fd4c24526963
+ languageName: node
+ linkType: hard
+
"@types/react-dom@npm:^19":
version: 19.2.3
resolution: "@types/react-dom@npm:19.2.3"
@@ -1484,6 +1614,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/react-reconciler@npm:^0.28.9":
+ version: 0.28.9
+ resolution: "@types/react-reconciler@npm:0.28.9"
+ peerDependencies:
+ "@types/react": "*"
+ checksum: 10c0/9fe71fa856aab2cd4742fe0416bdd4f5c82ecc05ef6451ee7fcb65a5efdf5fa588f5820fbe2a665b15371b0da3bfc4097f28bb6d450b9a834af2d0fc00f403bd
+ languageName: node
+ linkType: hard
+
"@types/react@npm:^19":
version: 19.2.15
resolution: "@types/react@npm:19.2.15"
@@ -1493,6 +1632,27 @@ __metadata:
languageName: node
linkType: hard
+"@types/stats.js@npm:*":
+ version: 0.17.4
+ resolution: "@types/stats.js@npm:0.17.4"
+ checksum: 10c0/4fe0429998519f0476f03a25b4900b4d4a1474606478657271e40a884f7936ba902ea564b1c95cfd33a8e84af46cef6e1e98bb23e86fd3b6676cd5b974987151
+ languageName: node
+ linkType: hard
+
+"@types/three@npm:*, @types/three@npm:^0":
+ version: 0.185.4
+ resolution: "@types/three@npm:0.185.4"
+ dependencies:
+ "@dimforge/rapier3d-compat": "npm:~0.12.0"
+ "@tweenjs/tween.js": "npm:~23.1.3"
+ "@types/stats.js": "npm:*"
+ "@types/webxr": "npm:>=0.5.17"
+ fflate: "npm:~0.8.2"
+ meshoptimizer: "npm:~1.1.1"
+ checksum: 10c0/f9cc28d3f343067e56741017bb0ac0257d8d2994707c2ee69512b668a57c8cd8892798978e54b2224920e9495eebe0878fcd8ea18b9a5ab555a7d445c689f804
+ languageName: node
+ linkType: hard
+
"@types/use-sync-external-store@npm:^0.0.6":
version: 0.0.6
resolution: "@types/use-sync-external-store@npm:0.0.6"
@@ -1500,6 +1660,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/webxr@npm:*, @types/webxr@npm:>=0.5.17, @types/webxr@npm:^0.5.2":
+ version: 0.5.24
+ resolution: "@types/webxr@npm:0.5.24"
+ checksum: 10c0/ff59ffd390d06ca3f89ab2531d583ac10bc5e2ab82e5a01ecc40fbc365224a3375e7caa5b14649cf6141db21fb024940da7ad2bd8faa3cd497a6665257cb53b5
+ languageName: node
+ linkType: hard
+
"@typescript-eslint/eslint-plugin@npm:8.59.4":
version: 8.59.4
resolution: "@typescript-eslint/eslint-plugin@npm:8.59.4"
@@ -1793,6 +1960,65 @@ __metadata:
languageName: node
linkType: hard
+"@use-gesture/core@npm:10.3.1":
+ version: 10.3.1
+ resolution: "@use-gesture/core@npm:10.3.1"
+ checksum: 10c0/2e3b5c0f7fe26cdb47be3a9c2a58a6a9edafc5b2895b07d2898eda9ab5a2b29fb0098b15597baa0856907b593075cd44cc69bba4785c9cfb7b6fabaa3b52cd3e
+ languageName: node
+ linkType: hard
+
+"@use-gesture/react@npm:^10.3.1":
+ version: 10.3.1
+ resolution: "@use-gesture/react@npm:10.3.1"
+ dependencies:
+ "@use-gesture/core": "npm:10.3.1"
+ peerDependencies:
+ react: ">= 16.8.0"
+ checksum: 10c0/978da66e4e7c424866ad52eba8fdf0ce93a4c8fc44f8837c7043e68c6a6107cd67e817fffb27f7db2ae871ef2f6addb0c8ddf1586f24c67b7e6aef1646c668cf
+ languageName: node
+ linkType: hard
+
+"TEDI-DDP@workspace:.":
+ version: 0.0.0-use.local
+ resolution: "TEDI-DDP@workspace:."
+ dependencies:
+ "@radix-ui/react-popover": "npm:^1.1.15"
+ "@radix-ui/react-tooltip": "npm:^1.2.8"
+ "@react-three/drei": "npm:^10.7.8"
+ "@react-three/fiber": "npm:^9.7.0"
+ "@reduxjs/toolkit": "npm:^2.12.0"
+ "@tailwindcss/postcss": "npm:^4"
+ "@tanstack/react-query": "npm:^5.100.11"
+ "@types/md5": "npm:^2"
+ "@types/node": "npm:^20"
+ "@types/nprogress": "npm:^0"
+ "@types/react": "npm:^19"
+ "@types/react-dom": "npm:^19"
+ "@types/three": "npm:^0"
+ axios: "npm:^1.16.1"
+ babel-plugin-react-compiler: "npm:1.0.0"
+ clsx: "npm:^2.1.1"
+ eslint: "npm:^9"
+ eslint-config-next: "npm:16.2.6"
+ lottie-react: "npm:^2.4.1"
+ lucide-react: "npm:^1.16.0"
+ md5: "npm:^2.3.0"
+ moment: "npm:^2.30.1"
+ next: "npm:16.2.6"
+ nprogress: "npm:^0.2.0"
+ react: "npm:19.2.4"
+ react-dom: "npm:19.2.4"
+ react-moment: "npm:^2.0.2"
+ react-redux: "npm:^9.3.0"
+ react-toastify: "npm:^11.1.0"
+ tailwindcss: "npm:^4"
+ three: "npm:^0.185.1"
+ typescript: "npm:^5"
+ zod: "npm:^4.4.3"
+ zustand: "npm:^5.0.14"
+ languageName: unknown
+ linkType: soft
+
"acorn-jsx@npm:^5.3.2":
version: 5.3.2
resolution: "acorn-jsx@npm:5.3.2"
@@ -2057,6 +2283,13 @@ __metadata:
languageName: node
linkType: hard
+"base64-js@npm:^1.3.1, base64-js@npm:^1.5.1":
+ version: 1.5.1
+ resolution: "base64-js@npm:1.5.1"
+ checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf
+ languageName: node
+ linkType: hard
+
"baseline-browser-mapping@npm:^2.10.12, baseline-browser-mapping@npm:^2.9.19":
version: 2.10.31
resolution: "baseline-browser-mapping@npm:2.10.31"
@@ -2066,6 +2299,15 @@ __metadata:
languageName: node
linkType: hard
+"bidi-js@npm:^1.0.2":
+ version: 1.0.3
+ resolution: "bidi-js@npm:1.0.3"
+ dependencies:
+ require-from-string: "npm:^2.0.2"
+ checksum: 10c0/fdddea4aa4120a34285486f2267526cd9298b6e8b773ad25e765d4f104b6d7437ab4ba542e6939e3ac834a7570bcf121ee2cf6d3ae7cd7082c4b5bedc8f271e1
+ languageName: node
+ linkType: hard
+
"brace-expansion@npm:^1.1.7":
version: 1.1.14
resolution: "brace-expansion@npm:1.1.14"
@@ -2109,6 +2351,16 @@ __metadata:
languageName: node
linkType: hard
+"buffer@npm:^6.0.3":
+ version: 6.0.3
+ resolution: "buffer@npm:6.0.3"
+ dependencies:
+ base64-js: "npm:^1.3.1"
+ ieee754: "npm:^1.2.1"
+ checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0
+ languageName: node
+ linkType: hard
+
"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2":
version: 1.0.2
resolution: "call-bind-apply-helpers@npm:1.0.2"
@@ -2148,6 +2400,15 @@ __metadata:
languageName: node
linkType: hard
+"camera-controls@npm:^3.1.0":
+ version: 3.1.2
+ resolution: "camera-controls@npm:3.1.2"
+ peerDependencies:
+ three: ">=0.126.1"
+ checksum: 10c0/2d42162efd6789e713332a72212819fb04b261f7babc45b5794cd902997e4a4826c9401b672ffab2e09f9808fc2b615d9154d67c57f38a7c90487d4cf5429655
+ languageName: node
+ linkType: hard
+
"caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001782":
version: 1.0.30001793
resolution: "caniuse-lite@npm:1.0.30001793"
@@ -2225,7 +2486,19 @@ __metadata:
languageName: node
linkType: hard
-"cross-spawn@npm:^7.0.6":
+"cross-env@npm:^7.0.3":
+ version: 7.0.3
+ resolution: "cross-env@npm:7.0.3"
+ dependencies:
+ cross-spawn: "npm:^7.0.1"
+ bin:
+ cross-env: src/bin/cross-env.js
+ cross-env-shell: src/bin/cross-env-shell.js
+ checksum: 10c0/f3765c25746c69fcca369655c442c6c886e54ccf3ab8c16847d5ad0e91e2f337d36eedc6599c1227904bf2a228d721e690324446876115bc8e7b32a866735ecf
+ languageName: node
+ linkType: hard
+
+"cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.6":
version: 7.0.6
resolution: "cross-spawn@npm:7.0.6"
dependencies:
@@ -2347,6 +2620,15 @@ __metadata:
languageName: node
linkType: hard
+"detect-gpu@npm:^5.0.56":
+ version: 5.0.70
+ resolution: "detect-gpu@npm:5.0.70"
+ dependencies:
+ webgl-constants: "npm:^1.1.1"
+ checksum: 10c0/5a2053d4779edb3490821599b316b5362c8b55c30b82806c64bcfd21f40c67e2debfd677937899080aea2b27765471552bafeeae9c9f1cf63bdd07461a7e2163
+ languageName: node
+ linkType: hard
+
"detect-libc@npm:^2.0.3, detect-libc@npm:^2.1.2":
version: 2.1.2
resolution: "detect-libc@npm:2.1.2"
@@ -2370,6 +2652,13 @@ __metadata:
languageName: node
linkType: hard
+"draco3d@npm:^1.4.1":
+ version: 1.5.7
+ resolution: "draco3d@npm:1.5.7"
+ checksum: 10c0/4419509bb93c31560a22a1a54e1c394a5b5017cab39941120c75151d941c11dec05925abf31f597ac2694c570b78c04f82aa3d69e5311f8f8e71fc8b9d92d12f
+ languageName: node
+ linkType: hard
+
"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1":
version: 1.0.1
resolution: "dunder-proto@npm:1.0.1"
@@ -2905,6 +3194,20 @@ __metadata:
languageName: node
linkType: hard
+"fflate@npm:^0.6.9":
+ version: 0.6.11
+ resolution: "fflate@npm:0.6.11"
+ checksum: 10c0/5bbf4a7557db6f65b25c7bb12621008c400a116ec1cc65bea78abd75c0c049d8a22fb92f0a59fbf9a7ad1a24a47d2112ce10fd817d08db0e1bf7dc82e780faa1
+ languageName: node
+ linkType: hard
+
+"fflate@npm:~0.8.2":
+ version: 0.8.3
+ resolution: "fflate@npm:0.8.3"
+ checksum: 10c0/eab181ca37f5348ae76d4b6f840e0026e30220e33153289ac942222d8b9638237d486507dbcc09878d724095bd354993a2ee48bbee99c8f2c6440d4448719aa7
+ languageName: node
+ linkType: hard
+
"file-entry-cache@npm:^8.0.0":
version: 8.0.0
resolution: "file-entry-cache@npm:8.0.0"
@@ -3124,6 +3427,13 @@ __metadata:
languageName: node
linkType: hard
+"glsl-noise@npm:^0.0.0":
+ version: 0.0.0
+ resolution: "glsl-noise@npm:0.0.0"
+ checksum: 10c0/442630e86ed109079f4ed92f3b59d24d0b9da60c03cb8ac9313a056996bf4b5d696b4c6121ee29952e8a19c5cf174a999cfe672464959db36fc75334c3134677
+ languageName: node
+ linkType: hard
+
"gopd@npm:^1.0.1, gopd@npm:^1.2.0":
version: 1.2.0
resolution: "gopd@npm:1.2.0"
@@ -3211,6 +3521,13 @@ __metadata:
languageName: node
linkType: hard
+"hls.js@npm:^1.5.17":
+ version: 1.6.18
+ resolution: "hls.js@npm:1.6.18"
+ checksum: 10c0/f1601ed4da27e09341f2dd2ac29951d30fb0c78581c405217a8286ec3962e50a181eb82e362dccf828acd364deae1fe0236fc8e6aeed9b2e990ac61e2bd82a77
+ languageName: node
+ linkType: hard
+
"https-proxy-agent@npm:^5.0.1":
version: 5.0.1
resolution: "https-proxy-agent@npm:5.0.1"
@@ -3221,6 +3538,13 @@ __metadata:
languageName: node
linkType: hard
+"ieee754@npm:^1.2.1":
+ version: 1.2.1
+ resolution: "ieee754@npm:1.2.1"
+ checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb
+ languageName: node
+ linkType: hard
+
"ignore@npm:^5.2.0":
version: 5.3.2
resolution: "ignore@npm:5.3.2"
@@ -3235,6 +3559,13 @@ __metadata:
languageName: node
linkType: hard
+"immediate@npm:~3.0.5":
+ version: 3.0.6
+ resolution: "immediate@npm:3.0.6"
+ checksum: 10c0/f8ba7ede69bee9260241ad078d2d535848745ff5f6995c7c7cb41cfdc9ccc213f66e10fa5afb881f90298b24a3f7344b637b592beb4f54e582770cdce3f1f039
+ languageName: node
+ linkType: hard
+
"immer@npm:^11.0.0":
version: 11.1.8
resolution: "immer@npm:11.1.8"
@@ -3435,6 +3766,13 @@ __metadata:
languageName: node
linkType: hard
+"is-promise@npm:^2.1.0":
+ version: 2.2.2
+ resolution: "is-promise@npm:2.2.2"
+ checksum: 10c0/2dba959812380e45b3df0fb12e7cb4d4528c989c7abb03ececb1d1fd6ab1cbfee956ca9daa587b9db1d8ac3c1e5738cf217bdb3dfd99df8c691be4c00ae09069
+ languageName: node
+ linkType: hard
+
"is-regex@npm:^1.2.1":
version: 1.2.1
resolution: "is-regex@npm:1.2.1"
@@ -3547,6 +3885,17 @@ __metadata:
languageName: node
linkType: hard
+"its-fine@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "its-fine@npm:2.0.0"
+ dependencies:
+ "@types/react-reconciler": "npm:^0.28.9"
+ peerDependencies:
+ react: ^19.0.0
+ checksum: 10c0/1ff1ff3257c0c7eb115c9c26cf0506eb84162edc1a63d3136780d146f7c7833298b240d0fcb46888909773f1a7d16539e0c03f2482cff1a5a502d6436686fe21
+ languageName: node
+ linkType: hard
+
"jiti@npm:^2.6.1":
version: 2.7.0
resolution: "jiti@npm:2.7.0"
@@ -3645,41 +3994,6 @@ __metadata:
languageName: node
linkType: hard
-"kham-benh-mo-pho@workspace:.":
- version: 0.0.0-use.local
- resolution: "kham-benh-mo-pho@workspace:."
- dependencies:
- "@radix-ui/react-popover": "npm:^1.1.15"
- "@radix-ui/react-tooltip": "npm:^1.2.8"
- "@reduxjs/toolkit": "npm:^2.12.0"
- "@tailwindcss/postcss": "npm:^4"
- "@tanstack/react-query": "npm:^5.100.11"
- "@types/md5": "npm:^2"
- "@types/node": "npm:^20"
- "@types/nprogress": "npm:^0"
- "@types/react": "npm:^19"
- "@types/react-dom": "npm:^19"
- axios: "npm:^1.16.1"
- babel-plugin-react-compiler: "npm:1.0.0"
- clsx: "npm:^2.1.1"
- eslint: "npm:^9"
- eslint-config-next: "npm:16.2.6"
- lottie-react: "npm:^2.4.1"
- lucide-react: "npm:^1.16.0"
- md5: "npm:^2.3.0"
- moment: "npm:^2.30.1"
- next: "npm:16.2.6"
- nprogress: "npm:^0.2.0"
- react: "npm:19.2.4"
- react-dom: "npm:19.2.4"
- react-moment: "npm:^2.0.2"
- react-redux: "npm:^9.3.0"
- react-toastify: "npm:^11.1.0"
- tailwindcss: "npm:^4"
- typescript: "npm:^5"
- languageName: unknown
- linkType: soft
-
"language-subtag-registry@npm:^0.3.20":
version: 0.3.23
resolution: "language-subtag-registry@npm:0.3.23"
@@ -3706,6 +4020,15 @@ __metadata:
languageName: node
linkType: hard
+"lie@npm:^3.0.2":
+ version: 3.3.0
+ resolution: "lie@npm:3.3.0"
+ dependencies:
+ immediate: "npm:~3.0.5"
+ checksum: 10c0/56dd113091978f82f9dc5081769c6f3b947852ecf9feccaf83e14a123bc630c2301439ce6182521e5fbafbde88e88ac38314327a4e0493a1bea7e0699a7af808
+ languageName: node
+ linkType: hard
+
"lightningcss-android-arm64@npm:1.32.0":
version: 1.32.0
resolution: "lightningcss-android-arm64@npm:1.32.0"
@@ -3890,6 +4213,16 @@ __metadata:
languageName: node
linkType: hard
+"maath@npm:^0.10.8":
+ version: 0.10.8
+ resolution: "maath@npm:0.10.8"
+ peerDependencies:
+ "@types/three": ">=0.134.0"
+ three: ">=0.134.0"
+ checksum: 10c0/fb61faac89567595f7e25f261f8a9750d0ced674fd16c77c6e486a0c4a00c572fa37969d613d017380f8ea4e8ff357d481dcd94240a104a7969bc7e95b9fe99e
+ languageName: node
+ linkType: hard
+
"magic-string@npm:^0.30.21":
version: 0.30.21
resolution: "magic-string@npm:0.30.21"
@@ -3924,6 +4257,22 @@ __metadata:
languageName: node
linkType: hard
+"meshline@npm:^3.3.1":
+ version: 3.3.1
+ resolution: "meshline@npm:3.3.1"
+ peerDependencies:
+ three: ">=0.137"
+ checksum: 10c0/95d5c7c90aca350530f37f9ea5c5e333a66ea0c50416fa6372c24a23321837a4c70d92d6a9a5942ee124484855a2d3fc8ca0656305feb8d8f86aa10513e324e4
+ languageName: node
+ linkType: hard
+
+"meshoptimizer@npm:~1.1.1":
+ version: 1.1.1
+ resolution: "meshoptimizer@npm:1.1.1"
+ checksum: 10c0/aacd816cbe206e49f16c88ed8b13deafec40cdb8b844080b6d614b4d945dfc7242d5dccfbc1269375a5d4dce2f354261c107d3ce6e564961bd9a220e4616dafb
+ languageName: node
+ linkType: hard
+
"micromatch@npm:^4.0.4":
version: 4.0.8
resolution: "micromatch@npm:4.0.8"
@@ -4305,6 +4654,13 @@ __metadata:
languageName: node
linkType: hard
+"potpack@npm:^1.0.1":
+ version: 1.0.2
+ resolution: "potpack@npm:1.0.2"
+ checksum: 10c0/670c23898a4257130858b960c2e654d3327c0f6a7e7091ff5846f213e65af8f9476320b995b8ad561a47a4d1c359c7ef347de57d22e7b02597051abb52bc85c4
+ languageName: node
+ linkType: hard
+
"prelude-ls@npm:^1.2.1":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
@@ -4312,6 +4668,16 @@ __metadata:
languageName: node
linkType: hard
+"promise-worker-transferable@npm:^1.0.4":
+ version: 1.0.4
+ resolution: "promise-worker-transferable@npm:1.0.4"
+ dependencies:
+ is-promise: "npm:^2.1.0"
+ lie: "npm:^3.0.2"
+ checksum: 10c0/110d273cbba2631c318762f02c159531f1e05e4af91080368b9d4c02b880824ae4c49a8ec7094c6434989ea2ee8de913b21c809279babeaf447fc90a682dcd7c
+ languageName: node
+ linkType: hard
+
"prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
@@ -4458,6 +4824,19 @@ __metadata:
languageName: node
linkType: hard
+"react-use-measure@npm:^2.1.7":
+ version: 2.1.7
+ resolution: "react-use-measure@npm:2.1.7"
+ peerDependencies:
+ react: ">=16.13"
+ react-dom: ">=16.13"
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ checksum: 10c0/ff24130e6f95e853feb6892fb74af08dbc5aae3574b701169e3bc3adb392c3162f51a58ddfe39bb7337db13ae609bbec0bb51a9de8b5fae5420f9d17e1f8b542
+ languageName: node
+ linkType: hard
+
"react@npm:19.2.4":
version: 19.2.4
resolution: "react@npm:19.2.4"
@@ -4511,6 +4890,13 @@ __metadata:
languageName: node
linkType: hard
+"require-from-string@npm:^2.0.2":
+ version: 2.0.2
+ resolution: "require-from-string@npm:2.0.2"
+ checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2
+ languageName: node
+ linkType: hard
+
"reselect@npm:^5.1.0":
version: 5.2.0
resolution: "reselect@npm:5.2.0"
@@ -4838,6 +5224,26 @@ __metadata:
languageName: node
linkType: hard
+"stats-gl@npm:^2.2.8":
+ version: 2.4.2
+ resolution: "stats-gl@npm:2.4.2"
+ dependencies:
+ "@types/three": "npm:*"
+ three: "npm:^0.170.0"
+ peerDependencies:
+ "@types/three": "*"
+ three: "*"
+ checksum: 10c0/cff079103d4004cdfd72d65c977b7237a929b02f57b275081188e9313d74620b70ea0a57e0561fb6a43b1597830068b4adb2625dbc08b99799fd12a71ec54729
+ languageName: node
+ linkType: hard
+
+"stats.js@npm:^0.17.0":
+ version: 0.17.0
+ resolution: "stats.js@npm:0.17.0"
+ checksum: 10c0/03392b4b9eb15cb285b8cf46c5dd68e4e7b556d8e4c72a3a48e6bd2ab6f2e72a02b145d465f594450df081d8f94a45e8ac1112a0d507a200a5768504082ca080
+ languageName: node
+ linkType: hard
+
"stop-iteration-iterator@npm:^1.1.0":
version: 1.1.0
resolution: "stop-iteration-iterator@npm:1.1.0"
@@ -4974,6 +5380,15 @@ __metadata:
languageName: node
linkType: hard
+"suspend-react@npm:^0.1.3":
+ version: 0.1.3
+ resolution: "suspend-react@npm:0.1.3"
+ peerDependencies:
+ react: ">=17.0"
+ checksum: 10c0/c35436f492a5cab85a2aac8f5ee5af8f67edbab3a49e12d15c7f770aae4cf09fe2925b3b8c122929210c1f28aa0958f91684a687d6608f116f7da8406d8a2f1c
+ languageName: node
+ linkType: hard
+
"tailwindcss@npm:4.3.0, tailwindcss@npm:^4":
version: 4.3.0
resolution: "tailwindcss@npm:4.3.0"
@@ -4988,6 +5403,45 @@ __metadata:
languageName: node
linkType: hard
+"three-mesh-bvh@npm:^0.8.3":
+ version: 0.8.3
+ resolution: "three-mesh-bvh@npm:0.8.3"
+ peerDependencies:
+ three: ">= 0.159.0"
+ checksum: 10c0/d1f052d4606498acf57e08e28d01c84157eb388d46be8e03bc839630dc4eb919d5bb5e2ed4e9113d39cbc3cda29f09212e3450e1bb21de6615ec330f2575fc28
+ languageName: node
+ linkType: hard
+
+"three-stdlib@npm:^2.35.6":
+ version: 2.36.1
+ resolution: "three-stdlib@npm:2.36.1"
+ dependencies:
+ "@types/draco3d": "npm:^1.4.0"
+ "@types/offscreencanvas": "npm:^2019.6.4"
+ "@types/webxr": "npm:^0.5.2"
+ draco3d: "npm:^1.4.1"
+ fflate: "npm:^0.6.9"
+ potpack: "npm:^1.0.1"
+ peerDependencies:
+ three: ">=0.128.0"
+ checksum: 10c0/6d04a3e6e1cf5a23e1d8807085609e5cfa999d342a8f4a3168432c4152e99ab555f7fd7ab22903d9639f33d8d19d2183c62931be34ad6c3ac8bd3ed879d973c2
+ languageName: node
+ linkType: hard
+
+"three@npm:^0.170.0":
+ version: 0.170.0
+ resolution: "three@npm:0.170.0"
+ checksum: 10c0/eeaa1eccb31467654a044fb90634b3b6bb8d0768a4f029c082fb6aa22aec0d6362d0e3af74938caa1db512b7823a031b54a76e88fba6cfd535054165ea9667c9
+ languageName: node
+ linkType: hard
+
+"three@npm:^0.185.1":
+ version: 0.185.1
+ resolution: "three@npm:0.185.1"
+ checksum: 10c0/d15d3fa934f53c9fd6706965ff9946f55653bc728cf582e68acb03421c0179d7662f7d270e14bcf72923e140c36779e7fba6b0f6e542e07deade7342f63c8bdf
+ languageName: node
+ linkType: hard
+
"tinyglobby@npm:^0.2.13, tinyglobby@npm:^0.2.15":
version: 0.2.16
resolution: "tinyglobby@npm:0.2.16"
@@ -5007,6 +5461,36 @@ __metadata:
languageName: node
linkType: hard
+"troika-three-text@npm:^0.52.4":
+ version: 0.52.5
+ resolution: "troika-three-text@npm:0.52.5"
+ dependencies:
+ bidi-js: "npm:^1.0.2"
+ troika-three-utils: "npm:^0.52.5"
+ troika-worker-utils: "npm:^0.52.0"
+ webgl-sdf-generator: "npm:1.1.1"
+ peerDependencies:
+ three: ">=0.125.0"
+ checksum: 10c0/0c3dbe6bc0addf43dbbec2a5d2deafe18775d3971df745655b86ed08341940ab6fed6a2e2082779656709081fd5113cc6deb1f4808aa10dc6e913827d2f1ea4a
+ languageName: node
+ linkType: hard
+
+"troika-three-utils@npm:^0.52.5":
+ version: 0.52.5
+ resolution: "troika-three-utils@npm:0.52.5"
+ peerDependencies:
+ three: ">=0.125.0"
+ checksum: 10c0/97e5bdf78f8633c42f0665b39320c890fa898ff6e9f521a06e3dd1f0b97c235b201e0ab0c3813c9e0a49e2be82f3d14509a02ef1d830b2190d244d2fdc48f0bb
+ languageName: node
+ linkType: hard
+
+"troika-worker-utils@npm:^0.52.0":
+ version: 0.52.0
+ resolution: "troika-worker-utils@npm:0.52.0"
+ checksum: 10c0/bcd776324ce3e941c1a913531cee6022c867b71bc2d6bb67f1eadf6df96fd4f865c51b90ac58fc5c0ae3346a4bd8360b0a52398864800a7a9c60473151d452d5
+ languageName: node
+ linkType: hard
+
"ts-api-utils@npm:^2.5.0":
version: 2.5.0
resolution: "ts-api-utils@npm:2.5.0"
@@ -5035,6 +5519,15 @@ __metadata:
languageName: node
linkType: hard
+"tunnel-rat@npm:^0.1.2":
+ version: 0.1.2
+ resolution: "tunnel-rat@npm:0.1.2"
+ dependencies:
+ zustand: "npm:^4.3.2"
+ checksum: 10c0/93cd50c7c9141e2662707602a21401145092e5a3c815b57a752937419ab6187a2ff36fa7e0f65e0c587022149bf2d323ace07dff61106511b7d4845e53390cc9
+ languageName: node
+ linkType: hard
+
"type-check@npm:^0.4.0, type-check@npm:~0.4.0":
version: 0.4.0
resolution: "type-check@npm:0.4.0"
@@ -5281,7 +5774,7 @@ __metadata:
languageName: node
linkType: hard
-"use-sync-external-store@npm:^1.4.0":
+"use-sync-external-store@npm:^1.2.2, use-sync-external-store@npm:^1.4.0":
version: 1.6.0
resolution: "use-sync-external-store@npm:1.6.0"
peerDependencies:
@@ -5290,6 +5783,27 @@ __metadata:
languageName: node
linkType: hard
+"utility-types@npm:^3.11.0":
+ version: 3.11.0
+ resolution: "utility-types@npm:3.11.0"
+ checksum: 10c0/2f1580137b0c3e6cf5405f37aaa8f5249961a76d26f1ca8efc0ff49a2fc0e0b2db56de8e521a174d075758e0c7eb3e590edec0832eb44478b958f09914920f19
+ languageName: node
+ linkType: hard
+
+"webgl-constants@npm:^1.1.1":
+ version: 1.1.1
+ resolution: "webgl-constants@npm:1.1.1"
+ checksum: 10c0/525a9838b0cbcbab902d1bc5457882424a9c146f851042c901ae457123193f5c5e47846b47c1c999e4f3abc24322cac494cea0cc52fec28bd8eb3a05cd79d573
+ languageName: node
+ linkType: hard
+
+"webgl-sdf-generator@npm:1.1.1":
+ version: 1.1.1
+ resolution: "webgl-sdf-generator@npm:1.1.1"
+ checksum: 10c0/769bfab22a9485a59bda9448d1d8684228adc9ebdac2cc69012a0854282385d1bef319f703758829e2c647e7532a9844e62977110e825bd75a3e0b8a0ee276ce
+ languageName: node
+ linkType: hard
+
"which-boxed-primitive@npm:^1.1.0, which-boxed-primitive@npm:^1.1.1":
version: 1.1.1
resolution: "which-boxed-primitive@npm:1.1.1"
@@ -5392,9 +5906,50 @@ __metadata:
languageName: node
linkType: hard
-"zod@npm:^3.25.0 || ^4.0.0":
+"zod@npm:^3.25.0 || ^4.0.0, zod@npm:^4.4.3":
version: 4.4.3
resolution: "zod@npm:4.4.3"
checksum: 10c0/7ea31b558e88f9faf44f31dd185e2e1cbf51fed3081787fb96cc2534749b50c0acfc6da7f0922a7353ed092dd358c7d50c28ea96c94d04af64191bd33152eca3
languageName: node
linkType: hard
+
+"zustand@npm:^4.3.2":
+ version: 4.5.7
+ resolution: "zustand@npm:4.5.7"
+ dependencies:
+ use-sync-external-store: "npm:^1.2.2"
+ peerDependencies:
+ "@types/react": ">=16.8"
+ immer: ">=9.0.6"
+ react: ">=16.8"
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ checksum: 10c0/55559e37a82f0c06cadc61cb08f08314c0fe05d6a93815e41e3376130c13db22a5017cbb0cd1f018c82f2dad0051afe3592561d40f980bd4082e32005e8a950c
+ languageName: node
+ linkType: hard
+
+"zustand@npm:^5.0.1, zustand@npm:^5.0.14, zustand@npm:^5.0.3":
+ version: 5.0.14
+ resolution: "zustand@npm:5.0.14"
+ peerDependencies:
+ "@types/react": ">=18.0.0"
+ immer: ">=9.0.6"
+ react: ">=18.0.0"
+ use-sync-external-store: ">=1.2.0"
+ peerDependenciesMeta:
+ "@types/react":
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ use-sync-external-store:
+ optional: true
+ checksum: 10c0/89de0c8119f1e0861b2f896a0b4df0d67994a373ecee02628d4e7b97cee1885207f646917d3ede395c3a5083316d48846855cfbc84aced7cd23a6db3238930a8
+ languageName: node
+ linkType: hard