From 2978cf2db00c951df5d3166cf71932e163e3e4d7 Mon Sep 17 00:00:00 2001 From: TuanVT Date: Mon, 25 May 2026 17:42:19 +0700 Subject: [PATCH] feate:update --- .../CreatePrescription/CreatePrescription.tsx | 140 +++++++--- .../MainPrescription/MainPrescription.tsx | 2 + .../UpdatePrescription/UpdatePrescription.tsx | 37 ++- .../components/SelectForm/SelectForm.tsx | 2 +- .../utils/UploadImage/UploadImage.tsx | 243 ++++++++++++++++++ src/components/utils/UploadImage/index.ts | 1 + .../utils/UploadImage/interface/index.ts | 17 ++ src/constant/config/index.ts | 12 +- src/services/prescriptionServices.ts | 11 +- 9 files changed, 416 insertions(+), 49 deletions(-) create mode 100644 src/components/utils/UploadImage/UploadImage.tsx create mode 100644 src/components/utils/UploadImage/index.ts create mode 100644 src/components/utils/UploadImage/interface/index.ts diff --git a/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx b/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx index 9b9b72b..06dc099 100644 --- a/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx +++ b/src/components/page/prescription/CreatePrescription/CreatePrescription.tsx @@ -16,8 +16,6 @@ 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"; @@ -31,6 +29,7 @@ import { QUERY_KEY } from "@/constant/config/enum"; import { toastWarn } from "@/common/funcs/toast"; import { PATH } from "@/constant/config"; import { useRouter } from "next/navigation"; +import UploadImage from "@/components/utils/UploadImage"; interface PrescriptionItem { medicineName: string; @@ -43,7 +42,7 @@ interface PrescriptionItem { interface PrescriptionForm { examinationSessionId: string; notes: string; - prescriptionImg: string; + PrescriptionFile: string; items: PrescriptionItem[]; } @@ -62,14 +61,30 @@ const defaultMedicine: PrescriptionItem = { instructions: "", }; +const convertFileToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.readAsDataURL(file); + + reader.onload = () => { + resolve(reader.result as string); + }; + + reader.onerror = (error) => { + reject(error); + }; + }); +}; + const CreatePrescription = () => { const router = useRouter(); - const [images, setImages] = useState([]); + const [file, setFile] = useState(null); const [form, setForm] = useState({ examinationSessionId: "", notes: "", - prescriptionImg: "", + PrescriptionFile: "", items: [defaultMedicine], }); @@ -106,7 +121,7 @@ const CreatePrescription = () => { const examinationSessionOptions = examinationSessionQuery.data?.items || []; const createPrescriptionMutation = useMutation({ - mutationFn: async () => { + mutationFn: async (body: { PrescriptionFile: string }) => { return await httpRequest({ showMessageFailed: true, showMessageSuccess: true, @@ -114,9 +129,20 @@ const CreatePrescription = () => { http: prescriptionServices.createPrescription({ examinationSessionId: form.examinationSessionId, + notes: form.notes, - prescriptionImg: form.prescriptionImg, - items: form.items, + + PrescriptionFile: body.PrescriptionFile, + + items: form.items + .filter((x) => x.medicineName.trim() !== "") + .map((x) => ({ + medicineName: x.medicineName, + dosage: x.dosage, + frequency: x.frequency, + duration: x.duration, + instructions: x.instructions, + })), }), }); }, @@ -193,11 +219,61 @@ const CreatePrescription = () => { })); }; - /** - * SUBMIT - */ - const handleSubmit = () => { - createPrescriptionMutation.mutate(); + const handleSubmit = async () => { + try { + /** + * VALIDATE + */ + if (!form.examinationSessionId) { + return toastWarn({ + msg: "Vui lòng chọn phiên khám", + }); + } + + if (!file) { + return toastWarn({ + msg: "Vui lòng chọn ảnh đơn thuốc", + }); + } + + /** + * UPLOAD IMAGE + */ + let uploadedImage = ""; + + if (file) { + const formData = new FormData(); + + formData.append("file", file); + + /** + * API UPLOAD + * SỬA uploadServices theo project của bạn + */ + const uploadRes: any = await httpRequest({ + showMessageFailed: true, + + http: prescriptionServices.uploadImage(formData), + }); + + /** + * BACKEND RESPONSE + */ + uploadedImage = + uploadRes?.url || uploadRes?.path || uploadRes?.data?.url || ""; + } + + /** + * CREATE PRESCRIPTION + */ + createPrescriptionMutation.mutate({ + PrescriptionFile: uploadedImage, + }); + } catch (error: any) { + toastWarn({ + msg: error?.message || "Upload ảnh thất bại", + }); + } }; return ( @@ -207,7 +283,7 @@ const CreatePrescription = () => {

- Thêm đơn thuốc + Tạo đơn thuốc

@@ -221,8 +297,8 @@ const CreatePrescription = () => { disabled={createPrescriptionMutation.isPending} > {createPrescriptionMutation.isPending - ? "Đang lưu..." - : "Lưu đơn thuốc"} + ? "Đang tạo..." + : "Tạo đơn thuốc"}

@@ -265,22 +341,16 @@ const CreatePrescription = () => { {/* IMAGE */}
- { - setImages(files); - - const imagePath = - files?.[0]?.path || - files?.[0]?.url || - files?.[0]?.img || - ""; - - setForm((prev) => ({ - ...prev, - prescriptionImg: imagePath, - })); - }} + + Hình ảnh đơn thuốc * + + } + name="PrescriptionFile" + file={file} + setFile={setFile} + path={""} />
@@ -292,6 +362,12 @@ const CreatePrescription = () => { value={form.notes} isBlur max={5000} + onChangeValue={(value) => + setForm((prev) => ({ + ...prev, + notes: String(value), + })) + } /> diff --git a/src/components/page/prescription/MainPrescription/MainPrescription.tsx b/src/components/page/prescription/MainPrescription/MainPrescription.tsx index b903e39..20f8beb 100644 --- a/src/components/page/prescription/MainPrescription/MainPrescription.tsx +++ b/src/components/page/prescription/MainPrescription/MainPrescription.tsx @@ -142,6 +142,8 @@ const MainPrescription = () => { prescription ) : ( diff --git a/src/components/page/prescription/UpdatePrescription/UpdatePrescription.tsx b/src/components/page/prescription/UpdatePrescription/UpdatePrescription.tsx index 68be0f2..0e9d1b4 100644 --- a/src/components/page/prescription/UpdatePrescription/UpdatePrescription.tsx +++ b/src/components/page/prescription/UpdatePrescription/UpdatePrescription.tsx @@ -33,6 +33,7 @@ import type { PrescriptionDetail } from "@/services/prescriptionServices"; ========================= */ interface PrescriptionItem { + id: string; medicineName: string; dosage: string; frequency: string; @@ -58,6 +59,7 @@ interface UploadImageItem { ========================= */ const defaultMedicine: PrescriptionItem = { + id: "", medicineName: "", dosage: "", frequency: "", @@ -171,6 +173,7 @@ const UpdatePrescription = () => { items: data.items && data.items.length > 0 ? data.items.map((i) => ({ + id: i.id, medicineName: i.medicineName ?? "", dosage: i.dosage ?? "", frequency: i.frequency ?? "", @@ -224,19 +227,32 @@ const UpdatePrescription = () => { 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, { - // examinationSessionId: form.examinationSessionId, - notes: form.notes, - prescriptionImg: form.prescriptionImg, - items: form.items, - }), + http: prescriptionServices.putPrescription(id, payload), }); }, @@ -245,8 +261,10 @@ const UpdatePrescription = () => { }, onError: (error: any) => { + console.log("UPDATE ERROR:", error); + const message = - error?.message || error?.error?.message || "Có lỗi xảy ra"; + error?.response?.data?.message || error?.message || "Có lỗi xảy ra"; toastWarn({ msg: message, @@ -283,6 +301,7 @@ const UpdatePrescription = () => { items: [ ...prev.items, { + id: "", medicineName: "", dosage: "", frequency: "", diff --git a/src/components/utils/FormCustom/components/SelectForm/SelectForm.tsx b/src/components/utils/FormCustom/components/SelectForm/SelectForm.tsx index 1415731..791eb36 100644 --- a/src/components/utils/FormCustom/components/SelectForm/SelectForm.tsx +++ b/src/components/utils/FormCustom/components/SelectForm/SelectForm.tsx @@ -81,7 +81,7 @@ function SelectForm({ onClick={() => !readOnly && setOpen((prev) => !prev)} className={clsx( "flex items-center justify-between px-4 h-12 border rounded-full cursor-pointer transition", - "border-gray-300 bg-gray-300 hover:border-blue-600", + "border-gray-300 bg-white hover:border-blue-600", open && "border-blue-600", readOnly && "bg-gray-100 border-gray-200 cursor-not-allowed hover:border-gray-200", diff --git a/src/components/utils/UploadImage/UploadImage.tsx b/src/components/utils/UploadImage/UploadImage.tsx new file mode 100644 index 0000000..fb04efb --- /dev/null +++ b/src/components/utils/UploadImage/UploadImage.tsx @@ -0,0 +1,243 @@ +"use client"; + +import React, { useEffect, useMemo, useState } from "react"; + +import Image from "next/image"; + +import clsx from "clsx"; + +import { UploadCloud, X } from "lucide-react"; + +import { toastError, toastWarn } from "@/common/funcs/toast"; + +import { UploadImageProps } from "./interface"; + +const MAXIMUM_FILE = 10; // MB + +const ACCEPT_TYPES = ["image/jpeg", "image/jpg", "image/png"]; + +/* ========================= + COMPONENT +========================= */ + +const UploadImage = ({ + label, + name, + path, + file, + setFile, + resetPath, + isWidthFull = true, + disabled = false, +}: UploadImageProps) => { + const [dragging, setDragging] = useState(false); + + /* ========================= + PREVIEW IMAGE + ========================= */ + + const imagePreview = useMemo(() => { + if (!file) return ""; + + return URL.createObjectURL(file); + }, [file]); + + /* ========================= + CLEANUP OBJECT URL + ========================= */ + + useEffect(() => { + return () => { + if (imagePreview) { + URL.revokeObjectURL(imagePreview); + } + }; + }, [imagePreview]); + + /* ========================= + VALIDATE FILE + ========================= */ + + const validateFile = (selectedFile: File | null | undefined) => { + if (!selectedFile) return; + + const { size, type } = selectedFile; + + /** + * CHECK SIZE + */ + if (size / 1000000 > MAXIMUM_FILE) { + return toastError({ + msg: `Kích thước tối đa của ảnh là ${MAXIMUM_FILE} MB`, + }); + } + + /** + * CHECK TYPE + */ + if (!ACCEPT_TYPES.includes(type)) { + return toastWarn({ + msg: "Định dạng không hợp lệ. Chỉ chấp nhận JPG, JPEG, PNG", + }); + } + + /** + * SET FILE + */ + setFile(selectedFile); + }; + + /* ========================= + DRAG EVENTS + ========================= */ + + const handleDragEnter = (e: React.DragEvent): void => { + e.preventDefault(); + + if (disabled) return; + + setDragging(true); + }; + + const handleDragLeave = (): void => { + setDragging(false); + }; + + const handleDrop = (e: React.DragEvent): void => { + e.preventDefault(); + + if (disabled) return; + + setDragging(false); + + const droppedFile = e.dataTransfer.files?.[0]; + + validateFile(droppedFile); + }; + + /* ========================= + SELECT FILE + ========================= */ + + const handleSelectImage = (e: React.ChangeEvent): void => { + if (disabled) return; + + const selectedFile = e.target.files?.[0]; + + validateFile(selectedFile); + }; + + /* ========================= + REMOVE IMAGE + ========================= */ + + const handleRemoveImage = (): void => { + setFile(null); + + resetPath?.(); + }; + + /* ========================= + IMAGE SOURCE + ========================= */ + + const imageSrc = imagePreview || path || ""; + + /* ========================= + UI + ========================= */ + + return ( +
+ {/* LABEL */} + {label && ( +
+ {typeof label === "string" ? ( +

{label}

+ ) : ( + label + )} +
+ )} + + {/* PREVIEW */} + {imageSrc ? ( +
+ Preview image + + {/* REMOVE BUTTON */} + {!disabled && ( + + )} +
+ ) : ( + + )} +
+ ); +}; + +export default UploadImage; diff --git a/src/components/utils/UploadImage/index.ts b/src/components/utils/UploadImage/index.ts new file mode 100644 index 0000000..87abefe --- /dev/null +++ b/src/components/utils/UploadImage/index.ts @@ -0,0 +1 @@ +export { default } from "./UploadImage"; diff --git a/src/components/utils/UploadImage/interface/index.ts b/src/components/utils/UploadImage/interface/index.ts new file mode 100644 index 0000000..c44672b --- /dev/null +++ b/src/components/utils/UploadImage/interface/index.ts @@ -0,0 +1,17 @@ +export interface UploadImageProps { + isWidthFull?: boolean; + + label?: string | React.ReactNode; + + name: string; + + file: File | null; + + setFile: (file: File | null) => void; + + path?: string; + + resetPath?: () => void; + + disabled?: boolean; +} diff --git a/src/constant/config/index.ts b/src/constant/config/index.ts index afb4da4..ff31e45 100644 --- a/src/constant/config/index.ts +++ b/src/constant/config/index.ts @@ -45,12 +45,12 @@ export const Menus: { path: PATH.CONSULTATION, pathActive: PATH.CONSULTATION, }, - { - title: "Đơn thuốc", - icon: Pill, - path: PATH.PRESCRIPTION, - pathActive: PATH.PRESCRIPTION, - }, + // { + // title: "Đơn thuốc", + // icon: Pill, + // path: PATH.PRESCRIPTION, + // pathActive: PATH.PRESCRIPTION, + // }, ], }, ]; diff --git a/src/services/prescriptionServices.ts b/src/services/prescriptionServices.ts index aebd9a7..0251d40 100644 --- a/src/services/prescriptionServices.ts +++ b/src/services/prescriptionServices.ts @@ -57,7 +57,7 @@ const prescriptionServices = { data: { examinationSessionId: string; notes: string; - prescriptionImg: string; + PrescriptionFile: string; items: { medicineName: string; dosage: string; @@ -72,6 +72,7 @@ const prescriptionServices = { cancelToken: tokenAxios, }); }, + getPrescription: (params?: GetPrescriptionParams, tokenAxios?: any) => { return axiosClient.get(`/Prescription`, { params, @@ -104,6 +105,14 @@ const prescriptionServices = { cancelToken: tokenAxios, }); }, + + uploadImage: (body: FormData) => { + return axiosClient.post("/upload", body, { + headers: { + "Content-Type": "multipart/form-data", + }, + }); + }, }; export default prescriptionServices;