From 7791e1d6a85c72f923e30138542cbdbab69bf2b7 Mon Sep 17 00:00:00 2001
From: TuanVT
Date: Thu, 4 Jun 2026 15:59:39 +0700
Subject: [PATCH] feat:update
---
src/app/prescription/update/page.tsx | 14 -
src/common/funcs/selectData.ts | 3 -
src/components/customs/FilterMany.tsx | 9 +-
.../page/Home/MainPageHome/MainPageHome.tsx | 2 +-
.../DetailConsultation/DetailConsultation.tsx | 190 ++++++-
.../UpdateConsultation/UpdateConsultation.tsx | 82 ++-
.../UpdateSpecialtyClinicId.tsx | 63 ++-
.../CreatePrescription/CreatePrescription.tsx | 27 +-
.../UpdatePrescription/UpdatePrescription.tsx | 533 ------------------
.../prescription/UpdatePrescription/index.ts | 1 -
.../MainPageStatistical.tsx | 4 +-
.../components/DateOption/DateOption.tsx | 4 +-
.../RangeDatePicker/RangeDatePicker.tsx | 9 +-
.../UploadMultipleFile/UploadMultipleFile.tsx | 213 +++++--
.../UploadMultipleFile/interface/index.ts | 14 +-
src/constant/config/index.ts | 4 -
src/services/consultationServices.ts | 11 +-
src/services/prescriptionServices.ts | 17 -
src/services/uploadServices.ts | 50 ++
19 files changed, 572 insertions(+), 678 deletions(-)
delete mode 100644 src/app/prescription/update/page.tsx
delete mode 100644 src/components/page/prescription/UpdatePrescription/UpdatePrescription.tsx
delete mode 100644 src/components/page/prescription/UpdatePrescription/index.ts
create mode 100644 src/services/uploadServices.ts
diff --git a/src/app/prescription/update/page.tsx b/src/app/prescription/update/page.tsx
deleted file mode 100644
index 615386c..0000000
--- a/src/app/prescription/update/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-"use client";
-import BaseLayout from "@/components/layouts/BaseLayout";
-import UpdatePrescription from "@/components/page/prescription/UpdatePrescription";
-import React from "react";
-
-const page = () => {
- return (
-
-
-
- );
-};
-
-export default page;
diff --git a/src/common/funcs/selectData.ts b/src/common/funcs/selectData.ts
index 459cd88..88cb2d6 100644
--- a/src/common/funcs/selectData.ts
+++ b/src/common/funcs/selectData.ts
@@ -8,9 +8,6 @@ export function getDateRange(range: number): {
today.setHours(0, 0, 0, 0);
switch (range) {
- case TYPE_DATE.ALL:
- return { from: null, to: null };
-
// Tuần này
case TYPE_DATE.TODAY:
return { from: new Date(), to: new Date() };
diff --git a/src/components/customs/FilterMany.tsx b/src/components/customs/FilterMany.tsx
index b289c66..8f94976 100644
--- a/src/components/customs/FilterMany.tsx
+++ b/src/components/customs/FilterMany.tsx
@@ -11,6 +11,7 @@ import clsx from "clsx";
import { Check, ChevronDown } from "lucide-react";
import Button from "./custom-button";
import { removeVietnameseTones } from "@/common/funcs/optionConvert";
+import CustomButton from "./custom-button";
interface OptionItem {
uuid: T;
@@ -177,15 +178,15 @@ function FilterMany({
{/* ACTION */}
-
+
-
+
)}
diff --git a/src/components/page/Home/MainPageHome/MainPageHome.tsx b/src/components/page/Home/MainPageHome/MainPageHome.tsx
index def620f..497b8f4 100644
--- a/src/components/page/Home/MainPageHome/MainPageHome.tsx
+++ b/src/components/page/Home/MainPageHome/MainPageHome.tsx
@@ -55,7 +55,7 @@ const MainPageHome = () => {
const res = await httpRequest({
showMessageFailed: true,
http: consultationServices.getConsultationsReport({
- FormDate: today,
+ FromDate: today,
ToDate: today,
}),
});
diff --git a/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx b/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx
index 385ccd0..11692a4 100644
--- a/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx
+++ b/src/components/page/consultation/DetailConsultation/DetailConsultation.tsx
@@ -1,6 +1,6 @@
"use client";
-import React, { useEffect, useMemo } from "react";
+import React, { useEffect, useMemo, useState } from "react";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import { useQuery } from "@tanstack/react-query";
@@ -10,6 +10,7 @@ import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
import { ArrowLeft, Pencil } from "lucide-react";
import consultationServices, {
+ AttachmentFile,
ConsultationDetail,
} from "@/services/consultationServices";
@@ -20,6 +21,7 @@ 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";
const DetailConsultation = () => {
const params = useParams();
@@ -29,6 +31,9 @@ const DetailConsultation = () => {
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],
@@ -77,6 +82,17 @@ const DetailConsultation = () => {
})) || []
);
}, [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(() => {
@@ -104,6 +120,17 @@ const DetailConsultation = () => {
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;
+ };
+
return (
@@ -122,16 +149,17 @@ const DetailConsultation = () => {
- {consultation.status === TYPE_STATUS.PendingPrescription && canUpdateConsultation && (
-
}
- href={`/consultation/update?_id=${consultation.id}`}
- >
- Cập nhật tổng quát
-
- )}
+ {consultation.status === TYPE_STATUS.PendingPrescription &&
+ canUpdateConsultation && (
+
}
+ href={`/consultation/update?_id=${consultation.id}`}
+ >
+ Cập nhật tổng quát
+
+ )}
@@ -283,6 +311,61 @@ const DetailConsultation = () => {
.join(", ") || "---"}
+
+
+
+ FILE UPLOAD
+
+
+ {consultation.generalAttachmentUrls?.length > 0 ? (
+
+ {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}
+
+ ) : (
+
+ 📄 {file.fileName}
+
+ )}
+
+ );
+ })}
+
+ ) : (
+
+ Không có file nào được tải lên.
+
+ )}
+
+
TRẠNG THÁI
@@ -341,6 +424,7 @@ const DetailConsultation = () => {
Danh sách đơn thuốc
+
{
note="Vui lòng thử lại sau"
>
item.id}
- data={consultation.aggregatedPrescriptionItems}
+ // Nếu item không có thuộc tính id, bạn có thể đổi thành (item, index) => String(index)
+ rowKey={(item: PrescriptionItem) => {
+ // Tìm vị trí của item hiện tại trong mảng paginatedPrescriptionItems
+ const idx = paginatedPrescriptionItems.indexOf(item);
+ return item.id || String(idx);
+ }}
+ data={paginatedPrescriptionItems} // Hiển thị dữ liệu trang hiện tại
column={[
{
title: "Tên thuốc",
render: (item: PrescriptionItem) => (
- {item.medicineName}
+
+ {item.medicineName}
+
),
},
{
@@ -378,12 +469,29 @@ const DetailConsultation = () => {
{
title: "Hướng dẫn",
render: (item: PrescriptionItem) => (
- {item.instructions || "---"}
+
+ {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]}
+ />
+ )}
@@ -437,6 +545,58 @@ const DetailConsultation = () => {
{selectedSpecialty.procedureNotes || "---"}
+
+
+ FILE UPLOAD
+
+
+ {selectedSpecialty.attachmentUrls?.length > 0 ? (
+
+ {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}
+
+ ) : (
+
+ 📄 {file.fileName}
+
+ )}
+
+ );
+ })}
+
+ ) : (
+
Không có file nào được tải lên.
+ )}
+
+
{selectedSpecialty.glassRequired && (
<>
diff --git a/src/components/page/consultation/UpdateConsultation/UpdateConsultation.tsx b/src/components/page/consultation/UpdateConsultation/UpdateConsultation.tsx
index 624d912..8652e4b 100644
--- a/src/components/page/consultation/UpdateConsultation/UpdateConsultation.tsx
+++ b/src/components/page/consultation/UpdateConsultation/UpdateConsultation.tsx
@@ -7,7 +7,6 @@ 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 TextArea from "@/components/utils/FormCustom/components/TextArea";
import GridColumn from "@/components/layouts/GridColumn";
import { QUERY_KEY } from "@/constant/config/enum";
@@ -16,6 +15,8 @@ import { httpRequest } from "@/services";
import consultationServices, {
ConsultationDetail,
} from "@/services/consultationServices";
+import uploadServices from "@/services/uploadServices";
+import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
/* =========================
TYPES
@@ -28,6 +29,7 @@ interface IUpdateConsul {
treatmentPlan: string;
doctorNotes: string;
isGlassesDelivered: boolean;
+ generalAttachmentUrls: string[];
}
/* =========================
@@ -41,6 +43,7 @@ const defaultForm: IUpdateConsul = {
treatmentPlan: "",
doctorNotes: "",
isGlassesDelivered: false,
+ generalAttachmentUrls: [],
};
/* =========================
@@ -57,6 +60,9 @@ const UpdateConsultation = () => {
const idFromSearch = searchParams?.get("_id") ?? undefined;
const id = idFromParams || idFromSearch || "";
+ const [images, setImages] = useState
([]);
+ const [uploading, setUploading] = useState(false);
+
const [form, setForm] = useState(defaultForm);
/* =========================
@@ -86,6 +92,7 @@ const UpdateConsultation = () => {
doctorNotes: null,
createdBy: "",
isDeleted: false,
+ generalAttachmentUrls: [],
}
);
},
@@ -106,18 +113,25 @@ const UpdateConsultation = () => {
treatmentPlan: consul.treatmentPlan || "",
doctorNotes: consul.doctorNotes || "",
isGlassesDelivered: consul.isGlassesDelivered,
+ 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 () => {
- if (!id || id === "undefined") {
- throw new Error("Không tìm thấy ID phiên khám!");
- }
+ const updateConsultationMutation = useMutation({
+ mutationFn: async (body: { paths: string[] }) => {
return httpRequest({
showMessageFailed: true,
showMessageSuccess: true,
@@ -129,31 +143,60 @@ const UpdateConsultation = () => {
diagnosis: form.diagnosis,
treatmentPlan: form.treatmentPlan,
doctorNotes: form.doctorNotes,
- isGlassesDelivered: form?.isGlassesDelivered,
+ isGlassesDelivered: form.isGlassesDelivered,
+ fileUrls: body.paths,
}),
});
},
- onSuccess(data) {
- if (!data) return;
-
- // Làm mới danh sách và chi tiết phiên khám trong cache
+ onSuccess() {
queryClient.invalidateQueries({
queryKey: [QUERY_KEY.table_list_consultation],
});
+
queryClient.invalidateQueries({
queryKey: [QUERY_KEY.chi_tiet_phien_kham, id],
});
router.back();
- // Quay lại trang quản lý danh sách phiên khám
- router.push(PATH.CONSULTATION || "/consultation");
+ router.push(PATH.CONSULTATION);
},
});
- const handleSubmit = () => {
- updateConsultationMutation.mutate();
+ 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 =
@@ -281,6 +324,15 @@ const UpdateConsultation = () => {
}
/>
+
+
+
+
+
+ {uploading && (
+
Đang upload file...
+ )}
+
{
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;
@@ -31,6 +35,7 @@ const UpdateSpecialtyClinicId = () => {
procedureNotes: string;
glassRequired: boolean;
glassType: string;
+ fileUrls: string[];
}>({
specialtyAssignmentId: "",
specialtyNotes: "",
@@ -38,6 +43,7 @@ const UpdateSpecialtyClinicId = () => {
procedureNotes: "",
glassRequired: false,
glassType: "",
+ fileUrls: [],
});
/* =========================
@@ -79,6 +85,7 @@ const UpdateSpecialtyClinicId = () => {
glassType: null,
examinerName: "",
specialtyStatus: 1,
+ attachmentUrls: [],
sharedItems: [],
},
],
@@ -120,14 +127,23 @@ const UpdateSpecialtyClinicId = () => {
procedureNotes: specialtyClinic.procedureNotes || "",
glassRequired: specialtyClinic.glassRequired || false,
glassType: specialtyClinic.glassType || "",
+ fileUrls: specialtyClinic.attachmentUrls || [],
});
+ setImages(
+ (consul.generalAttachmentUrls || []).map((url) => ({
+ file: null,
+ img: url,
+ path: url,
+ })),
+ );
}, [consulDetailQuery.data, specialtyClinicId]);
/* =========================
UPDATE MUTATION
========================= */
+
const specialtyResultsMutation = useMutation({
- mutationFn: async () => {
+ mutationFn: async (body: { paths: string[] }) => {
if (!consultationId) {
throw new Error("Không tìm thấy ID phiên khám!");
}
@@ -145,6 +161,7 @@ const UpdateSpecialtyClinicId = () => {
// Nếu không phải khoa mắt, gán mặc định false và chuỗi rỗng khi gửi API
glassRequired: isEyeClinic ? form.glassRequired : false,
glassType: isEyeClinic ? form.glassType : "",
+ fileUrls: body.paths,
},
),
});
@@ -165,8 +182,39 @@ const UpdateSpecialtyClinicId = () => {
},
});
- const handleSubmit = () => {
- specialtyResultsMutation.mutate();
+ 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 specialtyResultsMutation.mutate({
+ paths: [...currentImage, ...uploadedUrls],
+ });
+ }
+
+ return specialtyResultsMutation.mutate({
+ paths: currentImage,
+ });
+ } catch (error) {
+ console.error(error);
+ }
};
const isLoading =
@@ -311,6 +359,15 @@ const UpdateSpecialtyClinicId = () => {
)}
+
+
+
+
+
+ {uploading && (
+
Đang upload file...
+ )}
+
{/* BUTTON ACTION */}
diff --git a/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx b/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx
index 919e77d..dc33c2f 100644
--- a/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx
+++ b/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx
@@ -17,6 +17,7 @@ import consultationServices, {
} from "@/services/consultationServices";
import { QUERY_KEY } from "@/constant/config/enum";
import { toastWarn } from "@/common/funcs/toast";
+import uploadServices from "@/services/uploadServices";
/* =========================
TYPES
@@ -200,6 +201,9 @@ const CreatePrescription = () => {
/* =========================
SUBMIT VALIDATION
========================= */
+ /* =========================
+ SUBMIT VALIDATION
+========================= */
const handleSubmit = async () => {
try {
// Kiểm tra dữ liệu đầu vào phía client
@@ -225,13 +229,20 @@ const CreatePrescription = () => {
});
}
- // 1. Thực hiện gọi API upload ảnh
- setLoading(true);
- const uploadResponse: any = await prescriptionServices.uploadImage(
- file,
- folderName,
+ // 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
@@ -245,6 +256,10 @@ const CreatePrescription = () => {
} 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;
@@ -252,8 +267,10 @@ const CreatePrescription = () => {
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 ||
"";
diff --git a/src/components/page/prescription/UpdatePrescription/UpdatePrescription.tsx b/src/components/page/prescription/UpdatePrescription/UpdatePrescription.tsx
deleted file mode 100644
index 0e9d1b4..0000000
--- a/src/components/page/prescription/UpdatePrescription/UpdatePrescription.tsx
+++ /dev/null
@@ -1,533 +0,0 @@
-"use client";
-
-import React, { useEffect, useMemo, useState } from "react";
-
-import { Plus, Trash2 } from "lucide-react";
-import { useMutation, useQuery } 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 TextArea from "@/components/utils/FormCustom/components/TextArea";
-import SelectForm from "@/components/utils/FormCustom/components/SelectForm";
-import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
-
-import { httpRequest } from "@/services";
-import prescriptionServices from "@/services/prescriptionServices";
-
-import consultationServices, {
- ConsultationItem,
- ConsultationResponse,
-} from "@/services/consultationServices";
-
-import { QUERY_KEY } from "@/constant/config/enum";
-import { PATH } from "@/constant/config";
-
-import { toastWarn } from "@/common/funcs/toast";
-
-import type { PrescriptionDetail } from "@/services/prescriptionServices";
-
-/* =========================
- TYPES
-========================= */
-
-interface PrescriptionItem {
- id: string;
- medicineName: string;
- dosage: string;
- frequency: string;
- duration: string;
- instructions: string;
-}
-
-interface PrescriptionForm {
- examinationSessionId: string;
- notes: string;
- prescriptionImg: string;
- items: PrescriptionItem[];
-}
-
-interface UploadImageItem {
- url?: string;
- path?: string;
- img?: string;
-}
-
-/* =========================
- DEFAULT
-========================= */
-
-const defaultMedicine: PrescriptionItem = {
- id: "",
- medicineName: "",
- dosage: "",
- frequency: "",
- duration: "",
- instructions: "",
-};
-
-/* =========================
- COMPONENT
-========================= */
-
-const UpdatePrescription = () => {
- const router = useRouter();
-
- const params = useParams();
-
- const searchParams = useSearchParams();
-
- const idFromParams = params?.id as string | undefined;
-
- const idFromSearch = searchParams?.get("_id") ?? undefined;
-
- const id = idFromParams || idFromSearch || "";
-
- /* =========================
- 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 || [];
-
- /* =========================
- GET DETAIL PRESCRIPTION
- ========================= */
-
- const detailQuery = useQuery({
- queryKey: [QUERY_KEY.chi_tiet_don_thuoc, id],
-
- enabled: !!id,
-
- queryFn: async () => {
- const res = await httpRequest({
- showMessageFailed: true,
-
- http: prescriptionServices.detailPrescription(id),
- });
-
- return (
- res || {
- id: "",
- examinationSessionId: "",
- prescriptionCode: "",
- prescriptionImg: null,
- notes: "",
- createdBy: "",
- items: [],
- }
- );
- },
- });
-
- /* =========================
- MAP DATA
- ========================= */
-
- const mappedForm: PrescriptionForm = useMemo(() => {
- const data = detailQuery.data;
-
- if (!data) {
- return {
- examinationSessionId: "",
- notes: "",
- prescriptionImg: "",
- items: [defaultMedicine],
- };
- }
-
- return {
- examinationSessionId: data.examinationSessionId ?? "",
-
- notes: data.notes ?? "",
-
- prescriptionImg: data.prescriptionImg ?? "",
-
- items:
- data.items && data.items.length > 0
- ? data.items.map((i) => ({
- id: i.id,
- medicineName: i.medicineName ?? "",
- dosage: i.dosage ?? "",
- frequency: i.frequency ?? "",
- duration: i.duration ?? "",
- instructions: i.instructions ?? "",
- }))
- : [defaultMedicine],
- };
- }, [detailQuery.data]);
-
- /* =========================
- FORM STATE
- ========================= */
-
- const [form, setForm] = useState({
- examinationSessionId: "",
- notes: "",
- prescriptionImg: "",
- items: [defaultMedicine],
- });
-
- const [images, setImages] = useState([]);
-
- const [initialized, setInitialized] = useState(false);
-
- /* =========================
- INIT FORM
- ========================= */
-
- useEffect(() => {
- if (!detailQuery.data || initialized) return;
-
- queueMicrotask(() => {
- setForm(mappedForm);
-
- setImages(
- mappedForm.prescriptionImg ? [{ url: mappedForm.prescriptionImg }] : [],
- );
-
- setInitialized(true);
- });
- }, [detailQuery.data, initialized, mappedForm]);
-
- /* =========================
- UPDATE MUTATION
- ========================= */
-
- const updateMutation = useMutation({
- mutationFn: async () => {
- if (!id || id === "undefined") {
- throw new Error("Không tìm thấy ID đơn thuốc!");
- }
-
- const payload = {
- examinationSessionId: form.examinationSessionId,
- notes: form.notes,
- prescriptionImg: form.prescriptionImg,
-
- items: form.items
- .filter((item) => item.medicineName.trim() !== "")
- .map((item) => ({
- ...(item.id ? { id: item.id } : {}),
-
- medicineName: item.medicineName,
- dosage: item.dosage,
- frequency: item.frequency,
- duration: item.duration,
- instructions: item.instructions,
- })),
- };
-
- console.log("PAYLOAD UPDATE:", payload);
-
- return await httpRequest({
- showMessageFailed: true,
- showMessageSuccess: true,
- msgSuccess: "Cập nhật đơn thuốc thành công!",
-
- http: prescriptionServices.putPrescription(id, payload),
- });
- },
-
- onSuccess: () => {
- router.push(PATH.PRESCRIPTION);
- },
-
- onError: (error: any) => {
- console.log("UPDATE ERROR:", error);
-
- const message =
- error?.response?.data?.message || error?.message || "Có lỗi xảy ra";
-
- toastWarn({
- msg: message,
- });
- },
- });
-
- /* =========================
- HANDLERS
- ========================= */
-
- const handleChangeMedicine = (
- index: number,
- key: keyof PrescriptionItem,
- value: string,
- ) => {
- const clone = [...form.items];
-
- clone[index] = {
- ...clone[index],
- [key]: value,
- };
-
- setForm((prev) => ({
- ...prev,
- items: clone,
- }));
- };
-
- const handleAddMedicine = () => {
- setForm((prev) => ({
- ...prev,
-
- items: [
- ...prev.items,
- {
- id: "",
- medicineName: "",
- dosage: "",
- frequency: "",
- duration: "",
- instructions: "",
- },
- ],
- }));
- };
-
- const handleRemoveMedicine = (index: number) => {
- setForm((prev) => ({
- ...prev,
-
- items: prev.items.filter((_, i) => i !== index),
- }));
- };
-
- const handleSubmit = () => {
- updateMutation.mutate();
- };
-
- const isLoading = updateMutation.isPending || detailQuery.isLoading;
-
- /* =========================
- UI
- ========================= */
-
- return (
-
-
- {/* HEADER */}
-
-
-
- Cập nhật đơn thuốc
-
-
-
- Chỉnh sửa đơn thuốc cho bệnh nhân
-
-
-
-
- {isLoading ? "Đang cập nhật..." : "Lưu đơn thuốc"}
-
-
-
-
- {/* GENERAL INFO */}
-
-
- Thông tin chung
-
-
-
- {/* EXAM SESSION */}
-
- Phiên khám *
-
- }
- placeholder="Chọn phiên khám"
- value={form.examinationSessionId}
- readOnly
- options={examinationSessionOptions}
- onSelect={(item: ConsultationItem) =>
- setForm((prev) => ({
- ...prev,
- examinationSessionId: item.id,
- }))
- }
- onClean={() =>
- setForm((prev) => ({
- ...prev,
- examinationSessionId: "",
- }))
- }
- getOptionLabel={(item: ConsultationItem) => item.sessionCode}
- getOptionValue={(item: ConsultationItem) => item.id}
- />
-
- {/* IMAGE */}
- {
- setImages(files);
-
- const imagePath =
- files?.[0]?.path || files?.[0]?.url || files?.[0]?.img || "";
-
- setForm((prev) => ({
- ...prev,
- prescriptionImg: imagePath,
- }));
- }}
- />
-
- {/* NOTES */}
-
-
-
- {/* MEDICINE LIST */}
-
-
-
- Danh sách thuốc
-
-
- }
- onClick={handleAddMedicine}
- >
- Thêm thuốc
-
-
-
-
-
- {form.items.map((medicine, index) => (
-
- {/* TITLE */}
-
-
- Thuốc #{index + 1}
-
-
- {form.items.length > 1 && (
-
- )}
-
-
- {/* INPUTS */}
-
-
- handleChangeMedicine(index, "medicineName", String(v))
- }
- />
-
-
- handleChangeMedicine(index, "dosage", String(v))
- }
- />
-
-
- handleChangeMedicine(index, "frequency", String(v))
- }
- />
-
-
- handleChangeMedicine(index, "duration", String(v))
- }
- />
-
-
- {/* INSTRUCTIONS */}
-
-
-
- ))}
-
-
-
-
- );
-};
-
-export default UpdatePrescription;
diff --git a/src/components/page/prescription/UpdatePrescription/index.ts b/src/components/page/prescription/UpdatePrescription/index.ts
deleted file mode 100644
index dbbd9e4..0000000
--- a/src/components/page/prescription/UpdatePrescription/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./UpdatePrescription";
diff --git a/src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx b/src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx
index b43cf6f..78a4321 100644
--- a/src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx
+++ b/src/components/page/statistical/MainPageStatistical/MainPageStatistical.tsx
@@ -41,14 +41,14 @@ const MainPageStatistical = () => {
const toDate = moment(date?.to).format("YYYY-MM-DD");
console.log({
- FormDate: fromDate,
+ FromDate: fromDate,
ToDate: toDate,
});
const res = await httpRequest({
showMessageFailed: true,
http: consultationServices.getConsultationsReport({
- FormDate: fromDate,
+ FromDate: fromDate,
ToDate: toDate,
}),
});
diff --git a/src/components/utils/FilterDateRage/components/DateOption/DateOption.tsx b/src/components/utils/FilterDateRage/components/DateOption/DateOption.tsx
index 1ad3a9d..fd12983 100644
--- a/src/components/utils/FilterDateRage/components/DateOption/DateOption.tsx
+++ b/src/components/utils/FilterDateRage/components/DateOption/DateOption.tsx
@@ -36,7 +36,9 @@ export default function DateOption({
>
{(showOptionAll
? ListOptionFilterDate
- : ListOptionFilterDate.filter((item) => item.value !== TYPE_DATE.ALL)
+ : ListOptionFilterDate.filter(
+ (item) => item.value !== TYPE_DATE.TODAY,
+ )
).map((item) => {
const active = item.value === typeDate;
diff --git a/src/components/utils/FilterDateRage/components/RangeDatePicker/RangeDatePicker.tsx b/src/components/utils/FilterDateRage/components/RangeDatePicker/RangeDatePicker.tsx
index 5a5bb84..4021770 100644
--- a/src/components/utils/FilterDateRage/components/RangeDatePicker/RangeDatePicker.tsx
+++ b/src/components/utils/FilterDateRage/components/RangeDatePicker/RangeDatePicker.tsx
@@ -7,6 +7,7 @@ import { ChevronLeft, MoveRight, ChevronRight } from "lucide-react";
import CalendarMain from "../CalendarMain";
import Button from "@/components/customs/custom-button";
import Moment from "react-moment";
+import CustomButton from "@/components/customs/custom-button";
const daysOfWeek = ["CN", "T2", "T3", "T4", "T5", "T6", "T7"];
@@ -203,13 +204,13 @@ function RangeDatePicker({
{/* BUTTON */}
-
-
+
diff --git a/src/components/utils/UploadMultipleFile/UploadMultipleFile.tsx b/src/components/utils/UploadMultipleFile/UploadMultipleFile.tsx
index 1fd594c..8f91117 100644
--- a/src/components/utils/UploadMultipleFile/UploadMultipleFile.tsx
+++ b/src/components/utils/UploadMultipleFile/UploadMultipleFile.tsx
@@ -2,69 +2,167 @@
import React from "react";
import Image from "next/image";
-import { X, CirclePlus } from "lucide-react";
+import {
+ X,
+ CirclePlus,
+ FileText,
+ FileSpreadsheet,
+ FileType,
+ FileImage,
+} from "lucide-react";
+
import clsx from "clsx";
-import { PropsUploadMultipleFile } from "./interface";
+import { PropsUploadMultipleFile, UploadFileItem } from "./interface";
export default function UploadMultipleFile({
images = [],
setImages,
isDisableDelete = false,
}: PropsUploadMultipleFile) {
- // 👉 upload files
- const handleFileChange = (event: React.ChangeEvent) => {
- const files = event.target.files;
- if (!files) return;
+ /* =========================
+ CHECK IMAGE FILE
+ ========================= */
+ const isImageFile = (item: UploadFileItem) => {
+ const type = item?.fileType || "";
- const newImages = Array.from(files).map((file) => ({
- url: URL.createObjectURL(file),
- file,
- }));
-
- setImages((prev: any) => [...prev, ...newImages]);
+ return (
+ type.startsWith("image/") ||
+ /\.(jpg|jpeg|png|gif|webp)$/i.test(item?.path || "")
+ );
};
- // 👉 delete file
- const handleDelete = (index: number) => {
- setImages((prev: any) => {
- const target = prev[index];
- if (target?.url) URL.revokeObjectURL(target.url);
+ /* =========================
+ GET FILE ICON
+ ========================= */
+ const renderFileIcon = (item: UploadFileItem) => {
+ const fileName = item?.fileName || item?.path?.split("/").pop() || "";
- return prev.filter((_: any, i: number) => i !== index);
+ const ext = fileName.split(".").pop()?.toLowerCase();
+
+ switch (ext) {
+ case "jpg":
+ case "jpeg":
+ case "png":
+ case "gif":
+ case "webp":
+ return ;
+
+ case "pdf":
+ return ;
+
+ case "xls":
+ case "xlsx":
+ return ;
+
+ case "doc":
+ case "docx":
+ return ;
+
+ case "ppt":
+ case "pptx":
+ return ;
+
+ case "txt":
+ return ;
+
+ default:
+ return ;
+ }
+ };
+
+ /* =========================
+ UPLOAD FILES
+ ========================= */
+ const handleFileChange = (event: React.ChangeEvent) => {
+ const files = event.target.files;
+
+ if (!files) return;
+
+ const newFiles: UploadFileItem[] = Array.from(files).map((file) => ({
+ file,
+ url: URL.createObjectURL(file),
+ fileName: file.name,
+ fileType: file.type,
+ }));
+
+ setImages((prev) => [...prev, ...newFiles]);
+ };
+
+ /* =========================
+ DELETE FILE
+ ========================= */
+ const handleDelete = (index: number) => {
+ setImages((prev) => {
+ const target = prev[index];
+
+ if (target?.url) {
+ URL.revokeObjectURL(target.url);
+ }
+
+ return prev.filter((_, i) => i !== index);
});
};
return (
-
- {/* LIST IMAGE */}
+
+ {/* FILE LIST */}
{images?.length > 0 && (
-
- {images.map((image, index) => (
-
-
+
+ {images.map((item, index) => {
+ const fileUrl =
+ item?.url ||
+ (item?.path
+ ? `${process.env.NEXT_PUBLIC_IMAGE}/${item.path}`
+ : "");
- {/* DELETE BUTTON */}
- {isDisableDelete && !image?.file && image?.img ? null : (
-
handleDelete(index)}
- className="absolute top-[2px] right-[2px] w-[18px] h-[18px] bg-white rounded-sm flex items-center justify-center cursor-pointer transition active:scale-90"
- >
-
-
- )}
-
- ))}
+ return (
+
+ );
+ })}
)}
@@ -72,18 +170,27 @@ export default function UploadMultipleFile({
- {/* NOTE */}
Upload file
+
- File không vượt quá 50MB
+ JPG, PNG, PDF, Word, Excel, PPT (≤ 50MB)
diff --git a/src/components/utils/UploadMultipleFile/interface/index.ts b/src/components/utils/UploadMultipleFile/interface/index.ts
index adcf026..e44b849 100644
--- a/src/components/utils/UploadMultipleFile/interface/index.ts
+++ b/src/components/utils/UploadMultipleFile/interface/index.ts
@@ -1,5 +1,15 @@
+export interface UploadFileItem {
+ file?: File | null;
+ url?: string;
+ path?: string;
+ img?: string;
+
+ fileName?: string;
+ fileType?: string;
+}
+
export interface PropsUploadMultipleFile {
- images: any[];
- setImages: (any: any) => void;
+ images: UploadFileItem[];
+ setImages: React.Dispatch
>;
isDisableDelete?: boolean;
}
diff --git a/src/constant/config/index.ts b/src/constant/config/index.ts
index 9194234..36a35a9 100644
--- a/src/constant/config/index.ts
+++ b/src/constant/config/index.ts
@@ -74,10 +74,6 @@ export const ListOptionFilterDate: {
name: string;
value: number;
}[] = [
- {
- name: "Tất cả",
- value: TYPE_DATE.ALL,
- },
{
name: "Hôm nay",
value: TYPE_DATE.TODAY,
diff --git a/src/services/consultationServices.ts b/src/services/consultationServices.ts
index 4f45b7d..0734dcd 100644
--- a/src/services/consultationServices.ts
+++ b/src/services/consultationServices.ts
@@ -56,6 +56,11 @@ export interface ConsultationItem {
}[];
}
+export interface AttachmentFile {
+ fileName: string;
+ path: string;
+}
+
export interface ConsultationDetail {
id: string;
patientId: string;
@@ -72,6 +77,7 @@ export interface ConsultationDetail {
isGlassesDelivered: boolean;
createdBy: string;
isDeleted: boolean;
+ generalAttachmentUrls: string[];
specialtyClinics: {
id: string;
specialtyId: string;
@@ -83,6 +89,7 @@ export interface ConsultationDetail {
glassType: string | null;
examinerName: string;
specialtyStatus: string | number | undefined;
+ attachmentUrls: string[];
sharedItems: {
id: string;
prescriptionId: string;
@@ -136,7 +143,7 @@ const consultationServices = {
);
},
getConsultationsReport: (
- params: { FormDate: string; ToDate: string },
+ params: { FromDate: string; ToDate: string },
tokenAxios?: any,
) => {
return axiosClient.get(
@@ -181,6 +188,7 @@ const consultationServices = {
treatmentPlan: string | null;
doctorNotes: string | null;
isGlassesDelivered: boolean;
+ fileUrls: string[];
},
tokenAxios?: any,
) => {
@@ -209,6 +217,7 @@ const consultationServices = {
procedureNotes: string;
glassRequired: boolean;
glassType: string;
+ fileUrls: string[];
},
tokenAxios?: any,
) => {
diff --git a/src/services/prescriptionServices.ts b/src/services/prescriptionServices.ts
index 454ead0..c970a98 100644
--- a/src/services/prescriptionServices.ts
+++ b/src/services/prescriptionServices.ts
@@ -112,23 +112,6 @@ const prescriptionServices = {
/**
* UPLOAD IMAGE
*/
-
- uploadImage: (file: File, folderName?: string) => {
- const formData = new FormData();
-
- // Key phải viết hoa chữ cái đầu đúng như Swagger yêu cầu (File và FolderName)
- formData.append("File", file);
-
- if (folderName) {
- formData.append("FolderName", folderName);
- }
-
- return axiosClient.post("/api/Media/upload", formData, {
- headers: {
- "Content-Type": "multipart/form-data",
- },
- });
- },
};
export default prescriptionServices;
diff --git a/src/services/uploadServices.ts b/src/services/uploadServices.ts
new file mode 100644
index 0000000..1751362
--- /dev/null
+++ b/src/services/uploadServices.ts
@@ -0,0 +1,50 @@
+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;