This commit is contained in:
TuanVT
2026-05-31 09:38:10 +07:00
parent 56e2733d8f
commit 62ca6dd0f9
32 changed files with 1785 additions and 381 deletions
@@ -1,44 +1,26 @@
"use client";
import React, { useState } from "react";
import { Plus, Trash2 } from "lucide-react";
import { useMutation, useQuery } from "@tanstack/react-query";
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, {
ConsultationItem,
ConsultationResponse,
} from "@/services/consultationServices";
import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
import { QUERY_KEY } from "@/constant/config/enum";
import { toastWarn } from "@/common/funcs/toast";
import { PATH } from "@/constant/config";
/* =========================
TYPES
========================= */
interface PrescriptionItem {
medicineName: string;
dosage: string;
@@ -48,15 +30,15 @@ interface PrescriptionItem {
}
interface PrescriptionForm {
examinationSessionId: string;
examinationSessionSpecialtyId: string;
notes: string;
prescriptionImg: string;
items: PrescriptionItem[];
}
/* =========================
DEFAULT
========================= */
const defaultMedicine: PrescriptionItem = {
medicineName: "",
dosage: "",
@@ -68,29 +50,28 @@ const defaultMedicine: PrescriptionItem = {
/* =========================
COMPONENT
========================= */
const CreatePrescription = () => {
const router = useRouter();
const queryClient = useQueryClient();
const [loading, setLoading] = useState<boolean>(false);
const queryKeys = [QUERY_KEY.table_list_consultation];
const [file, setFile] = useState<File | null>(null);
const [form, setForm] = useState<PrescriptionForm>({
examinationSessionId: "",
examinationSessionSpecialtyId: "",
notes: "",
prescriptionImg: "",
items: [defaultMedicine],
});
/* =========================
GET EXAMINATION SESSION
GET EXAMINATION SESSION
========================= */
const examinationSessionQuery = useQuery<ConsultationResponse>({
queryKey: [QUERY_KEY.table_list_consultation],
queryFn: async () => {
const res = await httpRequest<ConsultationResponse>({
showMessageFailed: true,
http: consultationServices.getConsultations({
Page: 1,
PageSize: 100,
@@ -112,92 +93,82 @@ const CreatePrescription = () => {
});
const examinationSessionOptions =
examinationSessionQuery.data?.items?.filter(
(item) =>
item.status === TYPE_STATUS.Draft ||
item.status === TYPE_STATUS.InProgress,
) || [];
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
CREATE MUTATION
========================= */
const funcCreatePrescription = useMutation({
mutationFn: (payload: any) => {
console.log("=== GỬI PAYLOAD LÊN API TẠO ĐƠN THUỐC ===", payload);
const createPrescriptionMutation = useMutation({
mutationFn: async () => {
/**
* CREATE FORMDATA
*/
const formData = new FormData();
/**
* BASIC INFO
*/
formData.append("ExaminationSessionId", form.examinationSessionId);
formData.append("Notes", form.notes);
/**
* FILE
*/
if (file) {
formData.append("PrescriptionFile", file);
}
/**
* ITEMS
* BACKEND EXPECT:
* Items = JSON.stringify([...])
*/
const validItems = form.items.filter((x) => x.medicineName.trim() !== "");
formData.append("Items", JSON.stringify(validItems));
/**
* API
*/
return await httpRequest({
return httpRequest({
showMessageFailed: true,
showMessageSuccess: true,
msgSuccess: "Tạo đơn thuốc thành công!",
http: prescriptionServices.createPrescription(formData),
http: prescriptionServices.createPrescription(payload),
});
},
onSuccess: () => {
router.push(PATH.PRESCRIPTION);
},
onSuccess(data) {
console.log("TẠO ĐƠN THUỐC THÀNH CÔNG:", data);
onError: (error: any) => {
const message =
error?.response?.data?.message || error?.message || "Có lỗi xảy ra";
toastWarn({
msg: message,
queryKeys.forEach((key) => {
queryClient.invalidateQueries({
queryKey: [key],
});
});
console.log("CREATE PRESCRIPTION ERROR:", error);
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
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,
@@ -205,33 +176,20 @@ const CreatePrescription = () => {
};
/* =========================
ADD MEDICINE
ADD MEDICINE
========================= */
const handleAddMedicine = () => {
setForm((prev) => ({
...prev,
items: [
...prev.items,
{
medicineName: "",
dosage: "",
frequency: "",
duration: "",
instructions: "",
},
],
items: [...prev.items, { ...defaultMedicine }],
}));
};
/* =========================
REMOVE MEDICINE
REMOVE MEDICINE
========================= */
const handleRemoveMedicine = (index: number) => {
const newItems = form.items.filter((_, i) => i !== index);
setForm((prev) => ({
...prev,
items: newItems,
@@ -239,44 +197,103 @@ const CreatePrescription = () => {
};
/* =========================
SUBMIT
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",
});
}
const handleSubmit = () => {
/**
* VALIDATE
*/
if (!file) {
return toastWarn({
msg: "Vui lòng chọn ảnh đơn thuốc",
});
}
if (!form.examinationSessionId) {
return toastWarn({
msg: "Vui lòng chọn phiên khám",
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ệ",
});
}
// 1. Thực hiện gọi API upload ảnh
setLoading(true);
const uploadResponse: any = await prescriptionServices.uploadImage(
file,
"prescriptions",
);
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;
}
// 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?.fileUrl ||
innerData?.url ||
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);
}
if (!file) {
return toastWarn({
msg: "Vui lòng chọn ảnh đơn thuốc",
});
}
const validMedicine = form.items.some((x) => x.medicineName.trim() !== "");
if (!validMedicine) {
return toastWarn({
msg: "Vui lòng nhập ít nhất 1 loại thuốc",
});
}
/**
* MUTATE
*/
createPrescriptionMutation.mutate();
};
/* =========================
UI
========================= */
return (
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
<div className="flex flex-col gap-6 rounded-2xl bg-white p-6 shadow">
@@ -286,7 +303,6 @@ const CreatePrescription = () => {
<h2 className="text-[28px] font-semibold text-[#111827]">
Tạo đơn thuốc
</h2>
<p className="mt-1 text-sm text-[#697586]">
Tạo mới đơn thuốc cho bệnh nhân
</p>
@@ -296,11 +312,11 @@ const CreatePrescription = () => {
<CustomButton
type="submit"
variant="midnightBlue"
disabled={createPrescriptionMutation.isPending}
disabled={funcCreatePrescription.isPending || loading}
className="min-w-[180px]"
>
{createPrescriptionMutation.isPending
? "Đang tạo..."
{funcCreatePrescription.isPending || loading
? "Đang tạo đơn..."
: "Tạo đơn thuốc"}
</CustomButton>
</div>
@@ -317,30 +333,32 @@ const CreatePrescription = () => {
<SelectForm
label={
<span className="text-[14px] font-medium text-[#374151]">
Phiên khám
Phiên khám chuyên khoa
<span className="text-red-500">*</span>
</span>
}
placeholder="Chọn phiên khám"
value={form.examinationSessionId}
placeholder="Chọn phiên khám chuyên khoa"
value={form.examinationSessionSpecialtyId}
options={examinationSessionOptions}
onSelect={(item: ConsultationItem) =>
onSelect={(item: any) =>
setForm((prev) => ({
...prev,
examinationSessionId: item.id,
examinationSessionSpecialtyId: item.id,
}))
}
onClean={() =>
setForm((prev) => ({
...prev,
examinationSessionId: "",
examinationSessionSpecialtyId: "",
}))
}
getOptionLabel={(item: ConsultationItem) => `${item.sessionCode}`}
getOptionValue={(item: ConsultationItem) => item.id}
getOptionLabel={(item: any) =>
`${item.sessionCode} - ${item.specialtyName} (${item.patientName})`
}
getOptionValue={(item: any) => item.id}
/>
{/* IMAGE */}
{/* IMAGE UPLOAD */}
<div className="flex flex-col gap-2">
<UploadImage
label={
@@ -348,10 +366,10 @@ const CreatePrescription = () => {
Hình nh đơn thuốc <span style={{ color: "red" }}>*</span>
</span>
}
name="PrescriptionFile"
name="prescriptionImg"
file={file}
setFile={setFile}
path=""
path={""}
/>
</div>