507 lines
16 KiB
TypeScript
507 lines
16 KiB
TypeScript
"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";
|
|
|
|
/* =========================
|
|
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<boolean>(false);
|
|
const queryKeys = [QUERY_KEY.table_list_consultation];
|
|
const [file, setFile] = useState<File | null>(null);
|
|
|
|
const [form, setForm] = useState<PrescriptionForm>({
|
|
examinationSessionSpecialtyId: "",
|
|
notes: "",
|
|
prescriptionImg: "",
|
|
items: [defaultMedicine],
|
|
});
|
|
|
|
/* =========================
|
|
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,
|
|
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: [...prev.items, { ...defaultMedicine }],
|
|
}));
|
|
};
|
|
|
|
/* =========================
|
|
REMOVE MEDICINE
|
|
========================= */
|
|
const handleRemoveMedicine = (index: number) => {
|
|
const newItems = form.items.filter((_, i) => i !== index);
|
|
setForm((prev) => ({
|
|
...prev,
|
|
items: newItems,
|
|
}));
|
|
};
|
|
|
|
/* =========================
|
|
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ệ",
|
|
});
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<FormCustom form={form} setForm={setForm} onSubmit={handleSubmit}>
|
|
<div className="flex flex-col gap-6 rounded-2xl bg-white p-6 shadow">
|
|
{/* HEADER */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<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>
|
|
</div>
|
|
|
|
<div>
|
|
<CustomButton
|
|
type="submit"
|
|
variant="midnightBlue"
|
|
disabled={funcCreatePrescription.isPending || loading}
|
|
className="min-w-[180px]"
|
|
>
|
|
{funcCreatePrescription.isPending || loading
|
|
? "Đang tạo đơn..."
|
|
: "Tạo đơn thuốc"}
|
|
</CustomButton>
|
|
</div>
|
|
</div>
|
|
|
|
{/* GENERAL INFO */}
|
|
<div className="rounded-2xl border border-gray-200 p-5">
|
|
<h3 className="mb-5 text-lg font-semibold text-[#111827]">
|
|
Thông tin chung
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 gap-5">
|
|
{/* EXAMINATION SESSION */}
|
|
<SelectForm
|
|
label={
|
|
<span className="text-[14px] font-medium text-[#374151]">
|
|
Phiên khám chuyên khoa
|
|
<span className="text-red-500">*</span>
|
|
</span>
|
|
}
|
|
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 */}
|
|
<div className="flex flex-col gap-2">
|
|
<UploadImage
|
|
label={
|
|
<span>
|
|
Hình ảnh đơn thuốc <span style={{ color: "red" }}>*</span>
|
|
</span>
|
|
}
|
|
name="prescriptionImg"
|
|
file={file}
|
|
setFile={setFile}
|
|
path={""}
|
|
/>
|
|
</div>
|
|
|
|
{/* NOTES */}
|
|
<TextArea
|
|
label="Ghi chú"
|
|
name="notes"
|
|
placeholder="Nhập ghi chú"
|
|
value={form.notes}
|
|
isBlur
|
|
max={5000}
|
|
onChangeValue={(value) =>
|
|
setForm((prev) => ({
|
|
...prev,
|
|
notes: String(value),
|
|
}))
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* MEDICINE LIST */}
|
|
<div className="rounded-2xl border border-gray-200 p-5">
|
|
<div className="mb-5 flex items-center justify-between">
|
|
<h3 className="text-lg font-semibold text-[#111827]">
|
|
Danh sách thuốc
|
|
</h3>
|
|
|
|
<div>
|
|
<CustomButton
|
|
type="button"
|
|
variant="midnightBlue"
|
|
icon={<Plus size={18} />}
|
|
onClick={handleAddMedicine}
|
|
>
|
|
Thêm thuốc
|
|
</CustomButton>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-6">
|
|
{form.items.map((medicine, index) => (
|
|
<div
|
|
key={index}
|
|
className="rounded-2xl border border-gray-200 p-5"
|
|
>
|
|
{/* TITLE */}
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h4 className="font-semibold text-[#111827]">
|
|
Thuốc #{index + 1}
|
|
</h4>
|
|
|
|
{form.items.length > 1 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRemoveMedicine(index)}
|
|
className="text-red-500 transition hover:text-red-600"
|
|
>
|
|
<Trash2 size={18} />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* INPUTS */}
|
|
<div className="grid grid-cols-2 gap-5">
|
|
<InputForm
|
|
label="Tên thuốc"
|
|
name={`items.${index}.medicineName`}
|
|
placeholder="Nhập tên thuốc"
|
|
value={medicine.medicineName}
|
|
type="text"
|
|
onChangeValue={(value) =>
|
|
handleChangeMedicine(index, "medicineName", String(value))
|
|
}
|
|
/>
|
|
|
|
<InputForm
|
|
label="Liều lượng"
|
|
name={`items.${index}.dosage`}
|
|
placeholder="Ví dụ: 500mg"
|
|
value={medicine.dosage}
|
|
type="text"
|
|
onChangeValue={(value) =>
|
|
handleChangeMedicine(index, "dosage", String(value))
|
|
}
|
|
/>
|
|
|
|
<InputForm
|
|
label="Tần suất"
|
|
name={`items.${index}.frequency`}
|
|
placeholder="Ví dụ: 2 lần/ngày"
|
|
value={medicine.frequency}
|
|
type="text"
|
|
onChangeValue={(value) =>
|
|
handleChangeMedicine(index, "frequency", String(value))
|
|
}
|
|
/>
|
|
|
|
<InputForm
|
|
label="Thời gian dùng"
|
|
name={`items.${index}.duration`}
|
|
placeholder="Ví dụ: 7 ngày"
|
|
value={medicine.duration}
|
|
type="text"
|
|
onChangeValue={(value) =>
|
|
handleChangeMedicine(index, "duration", String(value))
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{/* INSTRUCTION */}
|
|
<div className="mt-5">
|
|
<TextArea
|
|
label="Hướng dẫn sử dụng"
|
|
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))
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</FormCustom>
|
|
);
|
|
};
|
|
|
|
export default CreatePrescription;
|