feate:update
This commit is contained in:
parent
b0baa509d9
commit
2978cf2db0
@ -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<string> => {
|
||||
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<UploadImageItem[]>([]);
|
||||
const [file, setFile] = useState<any>(null);
|
||||
|
||||
const [form, setForm] = useState<PrescriptionForm>({
|
||||
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 = () => {
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-[28px] font-semibold text-[#111827]">
|
||||
Thêm đơn thuốc
|
||||
Tạo đơn thuốc
|
||||
</h2>
|
||||
|
||||
<p className="mt-1 text-sm text-[#697586]">
|
||||
@ -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"}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</div>
|
||||
@ -265,22 +341,16 @@ const CreatePrescription = () => {
|
||||
|
||||
{/* IMAGE */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<UploadMultipleFile
|
||||
images={images}
|
||||
setImages={(files: UploadImageItem[]) => {
|
||||
setImages(files);
|
||||
|
||||
const imagePath =
|
||||
files?.[0]?.path ||
|
||||
files?.[0]?.url ||
|
||||
files?.[0]?.img ||
|
||||
"";
|
||||
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
prescriptionImg: imagePath,
|
||||
}));
|
||||
}}
|
||||
<UploadImage
|
||||
label={
|
||||
<span>
|
||||
Hình ảnh đơn thuốc <span style={{ color: "red" }}>*</span>
|
||||
</span>
|
||||
}
|
||||
name="PrescriptionFile"
|
||||
file={file}
|
||||
setFile={setFile}
|
||||
path={""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -292,6 +362,12 @@ const CreatePrescription = () => {
|
||||
value={form.notes}
|
||||
isBlur
|
||||
max={5000}
|
||||
onChangeValue={(value) =>
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
notes: String(value),
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -142,6 +142,8 @@ const MainPrescription = () => {
|
||||
<Image
|
||||
src={item.prescriptionImg}
|
||||
alt="prescription"
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-10 rounded object-cover"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@ -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: "",
|
||||
|
||||
@ -81,7 +81,7 @@ function SelectForm<OptionType>({
|
||||
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",
|
||||
|
||||
243
src/components/utils/UploadImage/UploadImage.tsx
Normal file
243
src/components/utils/UploadImage/UploadImage.tsx
Normal file
@ -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<HTMLLabelElement>): void => {
|
||||
e.preventDefault();
|
||||
|
||||
if (disabled) return;
|
||||
|
||||
setDragging(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (): void => {
|
||||
setDragging(false);
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLLabelElement>): void => {
|
||||
e.preventDefault();
|
||||
|
||||
if (disabled) return;
|
||||
|
||||
setDragging(false);
|
||||
|
||||
const droppedFile = e.dataTransfer.files?.[0];
|
||||
|
||||
validateFile(droppedFile);
|
||||
};
|
||||
|
||||
/* =========================
|
||||
SELECT FILE
|
||||
========================= */
|
||||
|
||||
const handleSelectImage = (e: React.ChangeEvent<HTMLInputElement>): 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 (
|
||||
<div className="w-full">
|
||||
{/* LABEL */}
|
||||
{label && (
|
||||
<div className="mb-2">
|
||||
{typeof label === "string" ? (
|
||||
<p className="text-sm font-medium text-gray-700">{label}</p>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PREVIEW */}
|
||||
{imageSrc ? (
|
||||
<div
|
||||
className={clsx(
|
||||
"relative overflow-hidden rounded-2xl border border-dashed border-blue-300 bg-blue-50/20",
|
||||
"h-[240px]",
|
||||
isWidthFull ? "w-full" : "w-[600px]",
|
||||
)}
|
||||
>
|
||||
<Image
|
||||
src={imageSrc}
|
||||
alt="Preview image"
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="100vw"
|
||||
/>
|
||||
|
||||
{/* REMOVE BUTTON */}
|
||||
{!disabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRemoveImage}
|
||||
className="absolute right-3 top-3 z-10 flex h-9 w-9 items-center justify-center rounded-full bg-white shadow-md transition hover:bg-gray-100 active:scale-95"
|
||||
>
|
||||
<X size={18} className="text-gray-700" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<label
|
||||
htmlFor={`upload-image-${name}`}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={handleDrop}
|
||||
className={clsx(
|
||||
"flex h-[240px] flex-col items-center justify-center gap-4 rounded-2xl border border-dashed transition",
|
||||
isWidthFull ? "w-full" : "w-[600px]",
|
||||
|
||||
disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer",
|
||||
|
||||
dragging
|
||||
? "border-blue-500 bg-white"
|
||||
: "border-blue-300 bg-blue-50/20",
|
||||
)}
|
||||
>
|
||||
{/* ICON */}
|
||||
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-white shadow-sm">
|
||||
<UploadCloud className="h-8 w-8 text-blue-500" />
|
||||
</div>
|
||||
|
||||
{/* TEXT */}
|
||||
<div className="space-y-1 text-center">
|
||||
<p className="text-sm font-medium text-gray-800">
|
||||
Kéo & thả ảnh vào đây
|
||||
</p>
|
||||
|
||||
<p className="text-xs text-gray-500">
|
||||
Hỗ trợ JPG, JPEG, PNG (Tối đa {MAXIMUM_FILE}MB)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* INPUT */}
|
||||
<input
|
||||
hidden
|
||||
id={`upload-image-${name}`}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/jpg"
|
||||
disabled={disabled}
|
||||
onChange={handleSelectImage}
|
||||
onClick={(e) => {
|
||||
(e.target as HTMLInputElement).value = "";
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadImage;
|
||||
1
src/components/utils/UploadImage/index.ts
Normal file
1
src/components/utils/UploadImage/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default } from "./UploadImage";
|
||||
17
src/components/utils/UploadImage/interface/index.ts
Normal file
17
src/components/utils/UploadImage/interface/index.ts
Normal file
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
// },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@ -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<PrescriptionResponse>(`/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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user