feat:update web

This commit is contained in:
TuanVT
2026-05-24 23:33:43 +07:00
parent b6ccde2c74
commit b0baa509d9
58 changed files with 4920 additions and 292 deletions
@@ -0,0 +1,409 @@
"use client";
import React, { useState } from "react";
import { Plus, Trash2 } from "lucide-react";
import { useMutation, useQuery } from "@tanstack/react-query";
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 { toastWarn } from "@/common/funcs/toast";
import { PATH } from "@/constant/config";
import { useRouter } from "next/navigation";
interface PrescriptionItem {
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;
file?: File;
img?: string;
}
const defaultMedicine: PrescriptionItem = {
medicineName: "",
dosage: "",
frequency: "",
duration: "",
instructions: "",
};
const CreatePrescription = () => {
const router = useRouter();
const [images, setImages] = useState<UploadImageItem[]>([]);
const [form, setForm] = useState<PrescriptionForm>({
examinationSessionId: "",
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 || [];
const createPrescriptionMutation = useMutation({
mutationFn: async () => {
return await httpRequest({
showMessageFailed: true,
showMessageSuccess: true,
msgSuccess: "Tạo đơn thuốc thành công!",
http: prescriptionServices.createPrescription({
examinationSessionId: form.examinationSessionId,
notes: form.notes,
prescriptionImg: form.prescriptionImg,
items: form.items,
}),
});
},
onSuccess: () => {
router.push(PATH.PRESCRIPTION);
},
onError: (error: any) => {
// 👇 BACKEND ERROR HERE
const message =
error?.message || error?.error?.message || "Có lỗi xảy ra";
const code = error?.code || error?.error?.code;
toastWarn({
msg: message,
});
console.log("ERROR CODE:", code);
},
});
/**
* 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,
{
medicineName: "",
dosage: "",
frequency: "",
duration: "",
instructions: "",
},
],
}));
};
/**
* REMOVE MEDICINE
*/
const handleRemoveMedicine = (index: number) => {
const newItems = form.items.filter((_, i) => i !== index);
setForm((prev) => ({
...prev,
items: newItems,
}));
};
/**
* SUBMIT
*/
const handleSubmit = () => {
createPrescriptionMutation.mutate();
};
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]">
Thêm đơ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={createPrescriptionMutation.isPending}
>
{createPrescriptionMutation.isPending
? "Đang lưu..."
: "Lưu đơ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
<span className="text-red-500">*</span>
</span>
}
placeholder="Chọn phiên khám"
value={form.examinationSessionId}
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 */}
<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,
}));
}}
/>
</div>
{/* NOTES */}
<TextArea
label="Ghi chú"
name="notes"
placeholder="Nhập ghi chú"
value={form.notes}
isBlur
max={5000}
/>
</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>
{/* FORM */}
<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>
<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;