feat:update
This commit is contained in:
parent
77dd05932e
commit
7791e1d6a8
@ -1,14 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import BaseLayout from "@/components/layouts/BaseLayout";
|
|
||||||
import UpdatePrescription from "@/components/page/prescription/UpdatePrescription";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const page = () => {
|
|
||||||
return (
|
|
||||||
<BaseLayout title="Cập nhật đơn thuốc">
|
|
||||||
<UpdatePrescription />
|
|
||||||
</BaseLayout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default page;
|
|
||||||
@ -8,9 +8,6 @@ export function getDateRange(range: number): {
|
|||||||
today.setHours(0, 0, 0, 0);
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
switch (range) {
|
switch (range) {
|
||||||
case TYPE_DATE.ALL:
|
|
||||||
return { from: null, to: null };
|
|
||||||
|
|
||||||
// Tuần này
|
// Tuần này
|
||||||
case TYPE_DATE.TODAY:
|
case TYPE_DATE.TODAY:
|
||||||
return { from: new Date(), to: new Date() };
|
return { from: new Date(), to: new Date() };
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import clsx from "clsx";
|
|||||||
import { Check, ChevronDown } from "lucide-react";
|
import { Check, ChevronDown } from "lucide-react";
|
||||||
import Button from "./custom-button";
|
import Button from "./custom-button";
|
||||||
import { removeVietnameseTones } from "@/common/funcs/optionConvert";
|
import { removeVietnameseTones } from "@/common/funcs/optionConvert";
|
||||||
|
import CustomButton from "./custom-button";
|
||||||
|
|
||||||
interface OptionItem<T> {
|
interface OptionItem<T> {
|
||||||
uuid: T;
|
uuid: T;
|
||||||
@ -177,15 +178,15 @@ function FilterMany<T extends string | number>({
|
|||||||
|
|
||||||
{/* ACTION */}
|
{/* ACTION */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<CustomButton
|
||||||
variant="grey"
|
variant="grey"
|
||||||
rounded="full"
|
rounded="full"
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
Hủy bỏ
|
Hủy bỏ
|
||||||
</Button>
|
</CustomButton>
|
||||||
|
|
||||||
<Button
|
<CustomButton
|
||||||
variant="midnightBlue"
|
variant="midnightBlue"
|
||||||
rounded="full"
|
rounded="full"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -194,7 +195,7 @@ function FilterMany<T extends string | number>({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Xác nhận
|
Xác nhận
|
||||||
</Button>
|
</CustomButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ const MainPageHome = () => {
|
|||||||
const res = await httpRequest<ReportResponse>({
|
const res = await httpRequest<ReportResponse>({
|
||||||
showMessageFailed: true,
|
showMessageFailed: true,
|
||||||
http: consultationServices.getConsultationsReport({
|
http: consultationServices.getConsultationsReport({
|
||||||
FormDate: today,
|
FromDate: today,
|
||||||
ToDate: today,
|
ToDate: today,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useMemo } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
@ -10,6 +10,7 @@ import { QUERY_KEY, TYPE_STATUS } from "@/constant/config/enum";
|
|||||||
import { ArrowLeft, Pencil } from "lucide-react";
|
import { ArrowLeft, Pencil } from "lucide-react";
|
||||||
|
|
||||||
import consultationServices, {
|
import consultationServices, {
|
||||||
|
AttachmentFile,
|
||||||
ConsultationDetail,
|
ConsultationDetail,
|
||||||
} from "@/services/consultationServices";
|
} from "@/services/consultationServices";
|
||||||
|
|
||||||
@ -20,6 +21,7 @@ import GridColumn from "@/components/layouts/GridColumn";
|
|||||||
import DataWrapper from "@/components/customs/DataWrapper";
|
import DataWrapper from "@/components/customs/DataWrapper";
|
||||||
import Table from "@/components/customs/custom-table";
|
import Table from "@/components/customs/custom-table";
|
||||||
import CustomButton from "@/components/customs/custom-button";
|
import CustomButton from "@/components/customs/custom-button";
|
||||||
|
import Pagination from "@/components/customs/custom-pagination";
|
||||||
|
|
||||||
const DetailConsultation = () => {
|
const DetailConsultation = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -29,6 +31,9 @@ const DetailConsultation = () => {
|
|||||||
|
|
||||||
const consultationId = params?.id as string;
|
const consultationId = params?.id as string;
|
||||||
|
|
||||||
|
const [prescriptionPage, setPrescriptionPage] = useState<number>(1);
|
||||||
|
const [prescriptionPageSize, setPrescriptionPageSize] = useState<number>(10);
|
||||||
|
|
||||||
const consulDetailQuery = useQuery<ConsultationDetail>({
|
const consulDetailQuery = useQuery<ConsultationDetail>({
|
||||||
queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId],
|
queryKey: [QUERY_KEY.chi_tiet_phien_kham, consultationId],
|
||||||
|
|
||||||
@ -77,6 +82,17 @@ const DetailConsultation = () => {
|
|||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
}, [consultation?.specialtyClinics, consultationId]);
|
}, [consultation?.specialtyClinics, consultationId]);
|
||||||
|
// Tính toán cắt mảng dữ liệu thuốc để hiển thị phân trang trên Client
|
||||||
|
const paginatedPrescriptionItems = useMemo(() => {
|
||||||
|
const items = consultation?.aggregatedPrescriptionItems || [];
|
||||||
|
const startIndex = (prescriptionPage - 1) * prescriptionPageSize;
|
||||||
|
const endIndex = startIndex + prescriptionPageSize;
|
||||||
|
return items.slice(startIndex, endIndex);
|
||||||
|
}, [
|
||||||
|
consultation?.aggregatedPrescriptionItems,
|
||||||
|
prescriptionPage,
|
||||||
|
prescriptionPageSize,
|
||||||
|
]);
|
||||||
|
|
||||||
const specialtyClinics = consultation?.specialtyClinics;
|
const specialtyClinics = consultation?.specialtyClinics;
|
||||||
const selectedSpecialty = useMemo(() => {
|
const selectedSpecialty = useMemo(() => {
|
||||||
@ -104,6 +120,17 @@ const DetailConsultation = () => {
|
|||||||
type PrescriptionItem =
|
type PrescriptionItem =
|
||||||
ConsultationDetail["aggregatedPrescriptionItems"][number];
|
ConsultationDetail["aggregatedPrescriptionItems"][number];
|
||||||
|
|
||||||
|
const getFileInfo = (file: AttachmentFile | string, index: number) => {
|
||||||
|
if (typeof file === "string") {
|
||||||
|
return {
|
||||||
|
fileName: file.split("/").pop() || `File ${index + 1}`,
|
||||||
|
path: file,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
@ -122,7 +149,8 @@ const DetailConsultation = () => {
|
|||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{consultation.status === TYPE_STATUS.PendingPrescription && canUpdateConsultation && (
|
{consultation.status === TYPE_STATUS.PendingPrescription &&
|
||||||
|
canUpdateConsultation && (
|
||||||
<CustomButton
|
<CustomButton
|
||||||
variant="midnightBlue"
|
variant="midnightBlue"
|
||||||
fullWidth={false}
|
fullWidth={false}
|
||||||
@ -283,6 +311,61 @@ const DetailConsultation = () => {
|
|||||||
.join(", ") || "---"}
|
.join(", ") || "---"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[14px] font-medium text-[#697586]">
|
||||||
|
FILE UPLOAD
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{consultation.generalAttachmentUrls?.length > 0 ? (
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{consultation.generalAttachmentUrls.map((item, index) => {
|
||||||
|
const file = getFileInfo(item, index);
|
||||||
|
|
||||||
|
const extension =
|
||||||
|
file.fileName.split(".").pop()?.toLowerCase() || "";
|
||||||
|
|
||||||
|
const isImage = [
|
||||||
|
"jpg",
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"gif",
|
||||||
|
"webp",
|
||||||
|
"bmp",
|
||||||
|
"svg",
|
||||||
|
].includes(extension);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={index}>
|
||||||
|
{isImage ? (
|
||||||
|
<a
|
||||||
|
href={file.path}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
🖼️ {file.fileName}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={file.path}
|
||||||
|
download={file.fileName}
|
||||||
|
className="text-green-600 hover:underline"
|
||||||
|
>
|
||||||
|
📄 {file.fileName}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">
|
||||||
|
Không có file nào được tải lên.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-[14px] font-medium text-[#697586]">
|
<p className="text-[14px] font-medium text-[#697586]">
|
||||||
TRẠNG THÁI
|
TRẠNG THÁI
|
||||||
@ -341,6 +424,7 @@ const DetailConsultation = () => {
|
|||||||
Danh sách đơn thuốc
|
Danh sách đơn thuốc
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DataWrapper
|
<DataWrapper
|
||||||
data={consultation.aggregatedPrescriptionItems}
|
data={consultation.aggregatedPrescriptionItems}
|
||||||
loading={consulDetailQuery.isLoading}
|
loading={consulDetailQuery.isLoading}
|
||||||
@ -348,13 +432,20 @@ const DetailConsultation = () => {
|
|||||||
note="Vui lòng thử lại sau"
|
note="Vui lòng thử lại sau"
|
||||||
>
|
>
|
||||||
<Table
|
<Table
|
||||||
rowKey={(item) => item.id}
|
// Nếu item không có thuộc tính id, bạn có thể đổi thành (item, index) => String(index)
|
||||||
data={consultation.aggregatedPrescriptionItems}
|
rowKey={(item: PrescriptionItem) => {
|
||||||
|
// Tìm vị trí của item hiện tại trong mảng paginatedPrescriptionItems
|
||||||
|
const idx = paginatedPrescriptionItems.indexOf(item);
|
||||||
|
return item.id || String(idx);
|
||||||
|
}}
|
||||||
|
data={paginatedPrescriptionItems} // Hiển thị dữ liệu trang hiện tại
|
||||||
column={[
|
column={[
|
||||||
{
|
{
|
||||||
title: "Tên thuốc",
|
title: "Tên thuốc",
|
||||||
render: (item: PrescriptionItem) => (
|
render: (item: PrescriptionItem) => (
|
||||||
<span>{item.medicineName}</span>
|
<span className="font-medium text-gray-900">
|
||||||
|
{item.medicineName}
|
||||||
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -378,12 +469,29 @@ const DetailConsultation = () => {
|
|||||||
{
|
{
|
||||||
title: "Hướng dẫn",
|
title: "Hướng dẫn",
|
||||||
render: (item: PrescriptionItem) => (
|
render: (item: PrescriptionItem) => (
|
||||||
<span>{item.instructions || "---"}</span>
|
<span className="text-sm text-gray-500">
|
||||||
|
{item.instructions || "---"}
|
||||||
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</DataWrapper>
|
</DataWrapper>
|
||||||
|
|
||||||
|
{/* Chỉ hiển thị phân trang khi tổng số lượng thuốc lớn hơn 10 phần tử */}
|
||||||
|
{(consultation.aggregatedPrescriptionItems?.length || 0) > 10 && (
|
||||||
|
<Pagination
|
||||||
|
total={consultation.aggregatedPrescriptionItems?.length || 0}
|
||||||
|
page={prescriptionPage}
|
||||||
|
pageSize={prescriptionPageSize}
|
||||||
|
onSetPage={(value) => setPrescriptionPage(value)}
|
||||||
|
onSetPageSize={(value) => {
|
||||||
|
setPrescriptionPageSize(value);
|
||||||
|
setPrescriptionPage(1); // Reset về trang 1 khi thay đổi kích thước hiển thị hàng
|
||||||
|
}}
|
||||||
|
dependencies={[prescriptionPage, prescriptionPageSize]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -437,6 +545,58 @@ const DetailConsultation = () => {
|
|||||||
<p>{selectedSpecialty.procedureNotes || "---"}</p>
|
<p>{selectedSpecialty.procedureNotes || "---"}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[14px] font-medium text-[#697586]">
|
||||||
|
FILE UPLOAD
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{selectedSpecialty.attachmentUrls?.length > 0 ? (
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{selectedSpecialty.attachmentUrls.map((item, index) => {
|
||||||
|
const file = getFileInfo(item, index);
|
||||||
|
|
||||||
|
const extension =
|
||||||
|
file.fileName.split(".").pop()?.toLowerCase() || "";
|
||||||
|
|
||||||
|
const isImage = [
|
||||||
|
"jpg",
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"gif",
|
||||||
|
"webp",
|
||||||
|
"bmp",
|
||||||
|
"svg",
|
||||||
|
].includes(extension);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={index}>
|
||||||
|
{isImage ? (
|
||||||
|
<a
|
||||||
|
href={file.path}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
>
|
||||||
|
🖼️ {file.fileName}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={file.path}
|
||||||
|
download={file.fileName}
|
||||||
|
className="text-green-600 hover:underline"
|
||||||
|
>
|
||||||
|
📄 {file.fileName}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">Không có file nào được tải lên.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{selectedSpecialty.glassRequired && (
|
{selectedSpecialty.glassRequired && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import { useParams, useRouter, useSearchParams } from "next/navigation";
|
|||||||
import CustomButton from "@/components/customs/custom-button";
|
import CustomButton from "@/components/customs/custom-button";
|
||||||
import FormCustom from "@/components/utils/FormCustom";
|
import FormCustom from "@/components/utils/FormCustom";
|
||||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
|
||||||
import GridColumn from "@/components/layouts/GridColumn";
|
import GridColumn from "@/components/layouts/GridColumn";
|
||||||
|
|
||||||
import { QUERY_KEY } from "@/constant/config/enum";
|
import { QUERY_KEY } from "@/constant/config/enum";
|
||||||
@ -16,6 +15,8 @@ import { httpRequest } from "@/services";
|
|||||||
import consultationServices, {
|
import consultationServices, {
|
||||||
ConsultationDetail,
|
ConsultationDetail,
|
||||||
} from "@/services/consultationServices";
|
} from "@/services/consultationServices";
|
||||||
|
import uploadServices from "@/services/uploadServices";
|
||||||
|
import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
TYPES
|
TYPES
|
||||||
@ -28,6 +29,7 @@ interface IUpdateConsul {
|
|||||||
treatmentPlan: string;
|
treatmentPlan: string;
|
||||||
doctorNotes: string;
|
doctorNotes: string;
|
||||||
isGlassesDelivered: boolean;
|
isGlassesDelivered: boolean;
|
||||||
|
generalAttachmentUrls: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@ -41,6 +43,7 @@ const defaultForm: IUpdateConsul = {
|
|||||||
treatmentPlan: "",
|
treatmentPlan: "",
|
||||||
doctorNotes: "",
|
doctorNotes: "",
|
||||||
isGlassesDelivered: false,
|
isGlassesDelivered: false,
|
||||||
|
generalAttachmentUrls: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@ -57,6 +60,9 @@ const UpdateConsultation = () => {
|
|||||||
const idFromSearch = searchParams?.get("_id") ?? undefined;
|
const idFromSearch = searchParams?.get("_id") ?? undefined;
|
||||||
const id = idFromParams || idFromSearch || "";
|
const id = idFromParams || idFromSearch || "";
|
||||||
|
|
||||||
|
const [images, setImages] = useState<any[]>([]);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
const [form, setForm] = useState<IUpdateConsul>(defaultForm);
|
const [form, setForm] = useState<IUpdateConsul>(defaultForm);
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@ -86,6 +92,7 @@ const UpdateConsultation = () => {
|
|||||||
doctorNotes: null,
|
doctorNotes: null,
|
||||||
createdBy: "",
|
createdBy: "",
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
|
generalAttachmentUrls: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -106,18 +113,25 @@ const UpdateConsultation = () => {
|
|||||||
treatmentPlan: consul.treatmentPlan || "",
|
treatmentPlan: consul.treatmentPlan || "",
|
||||||
doctorNotes: consul.doctorNotes || "",
|
doctorNotes: consul.doctorNotes || "",
|
||||||
isGlassesDelivered: consul.isGlassesDelivered,
|
isGlassesDelivered: consul.isGlassesDelivered,
|
||||||
|
generalAttachmentUrls: consul.generalAttachmentUrls || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setImages(
|
||||||
|
(consul.generalAttachmentUrls || []).map((url) => ({
|
||||||
|
file: null,
|
||||||
|
img: url,
|
||||||
|
path: url,
|
||||||
|
fileName: url.split("/").pop(),
|
||||||
|
})),
|
||||||
|
);
|
||||||
}, [consulDetailQuery.data]);
|
}, [consulDetailQuery.data]);
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
UPDATE MUTATION
|
UPDATE MUTATION
|
||||||
========================= */
|
========================= */
|
||||||
const updateConsultationMutation = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
if (!id || id === "undefined") {
|
|
||||||
throw new Error("Không tìm thấy ID phiên khám!");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const updateConsultationMutation = useMutation({
|
||||||
|
mutationFn: async (body: { paths: string[] }) => {
|
||||||
return httpRequest({
|
return httpRequest({
|
||||||
showMessageFailed: true,
|
showMessageFailed: true,
|
||||||
showMessageSuccess: true,
|
showMessageSuccess: true,
|
||||||
@ -129,31 +143,60 @@ const UpdateConsultation = () => {
|
|||||||
diagnosis: form.diagnosis,
|
diagnosis: form.diagnosis,
|
||||||
treatmentPlan: form.treatmentPlan,
|
treatmentPlan: form.treatmentPlan,
|
||||||
doctorNotes: form.doctorNotes,
|
doctorNotes: form.doctorNotes,
|
||||||
isGlassesDelivered: form?.isGlassesDelivered,
|
isGlassesDelivered: form.isGlassesDelivered,
|
||||||
|
fileUrls: body.paths,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onSuccess(data) {
|
onSuccess() {
|
||||||
if (!data) return;
|
|
||||||
|
|
||||||
// Làm mới danh sách và chi tiết phiên khám trong cache
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [QUERY_KEY.table_list_consultation],
|
queryKey: [QUERY_KEY.table_list_consultation],
|
||||||
});
|
});
|
||||||
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [QUERY_KEY.chi_tiet_phien_kham, id],
|
queryKey: [QUERY_KEY.chi_tiet_phien_kham, id],
|
||||||
});
|
});
|
||||||
|
|
||||||
router.back();
|
router.back();
|
||||||
|
|
||||||
// Quay lại trang quản lý danh sách phiên khám
|
router.push(PATH.CONSULTATION);
|
||||||
router.push(PATH.CONSULTATION || "/consultation");
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = async () => {
|
||||||
updateConsultationMutation.mutate();
|
try {
|
||||||
|
const currentImage = images
|
||||||
|
.filter((item) => !!item.img)
|
||||||
|
.map((item) => item.img);
|
||||||
|
|
||||||
|
const files = images
|
||||||
|
.filter((item) => !!item.file)
|
||||||
|
.map((item) => item.file);
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
const uploadResult: any = await uploadServices.uploadImage({
|
||||||
|
Files: files,
|
||||||
|
SessionCode: consulDetailQuery.data?.sessionCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Cấu trúc log kiểm tra uploadResult:", uploadResult);
|
||||||
|
|
||||||
|
// CẬP NHẬT Ở ĐÂY: Lấy trực tiếp từ uploadResult hoặc bóc tách đúng tầng data
|
||||||
|
const uploadedUrls =
|
||||||
|
uploadResult?.fileUrls || uploadResult?.data?.fileUrls || [];
|
||||||
|
|
||||||
|
return updateConsultationMutation.mutate({
|
||||||
|
paths: [...currentImage, ...uploadedUrls],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateConsultationMutation.mutate({
|
||||||
|
paths: currentImage,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLoading =
|
const isLoading =
|
||||||
@ -281,6 +324,15 @@ const UpdateConsultation = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="text-sm font-medium">Tài liệu đính kèm</label>
|
||||||
|
|
||||||
|
<UploadMultipleFile images={images} setImages={setImages} />
|
||||||
|
|
||||||
|
{uploading && (
|
||||||
|
<p className="text-sm text-blue-500">Đang upload file...</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<CustomButton
|
<CustomButton
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@ -5,12 +5,14 @@ import GridColumn from "@/components/layouts/GridColumn";
|
|||||||
import FormCustom from "@/components/utils/FormCustom";
|
import FormCustom from "@/components/utils/FormCustom";
|
||||||
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
import InputForm from "@/components/utils/FormCustom/components/InputForm";
|
||||||
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
import TextArea from "@/components/utils/FormCustom/components/TextArea";
|
||||||
|
import UploadMultipleFile from "@/components/utils/UploadMultipleFile";
|
||||||
import { PATH } from "@/constant/config";
|
import { PATH } from "@/constant/config";
|
||||||
import { QUERY_KEY } from "@/constant/config/enum";
|
import { QUERY_KEY } from "@/constant/config/enum";
|
||||||
import { httpRequest } from "@/services";
|
import { httpRequest } from "@/services";
|
||||||
import consultationServices, {
|
import consultationServices, {
|
||||||
ConsultationDetail,
|
ConsultationDetail,
|
||||||
} from "@/services/consultationServices";
|
} from "@/services/consultationServices";
|
||||||
|
import uploadServices from "@/services/uploadServices";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
@ -23,6 +25,8 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
|
|
||||||
const consultationId = searchParams.get("_id") || "";
|
const consultationId = searchParams.get("_id") || "";
|
||||||
const specialtyClinicId = searchParams.get("specialtyClinicId") || "";
|
const specialtyClinicId = searchParams.get("specialtyClinicId") || "";
|
||||||
|
const [images, setImages] = useState<any[]>([]);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
const [form, setForm] = useState<{
|
const [form, setForm] = useState<{
|
||||||
specialtyAssignmentId: string;
|
specialtyAssignmentId: string;
|
||||||
@ -31,6 +35,7 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
procedureNotes: string;
|
procedureNotes: string;
|
||||||
glassRequired: boolean;
|
glassRequired: boolean;
|
||||||
glassType: string;
|
glassType: string;
|
||||||
|
fileUrls: string[];
|
||||||
}>({
|
}>({
|
||||||
specialtyAssignmentId: "",
|
specialtyAssignmentId: "",
|
||||||
specialtyNotes: "",
|
specialtyNotes: "",
|
||||||
@ -38,6 +43,7 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
procedureNotes: "",
|
procedureNotes: "",
|
||||||
glassRequired: false,
|
glassRequired: false,
|
||||||
glassType: "",
|
glassType: "",
|
||||||
|
fileUrls: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
@ -79,6 +85,7 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
glassType: null,
|
glassType: null,
|
||||||
examinerName: "",
|
examinerName: "",
|
||||||
specialtyStatus: 1,
|
specialtyStatus: 1,
|
||||||
|
attachmentUrls: [],
|
||||||
sharedItems: [],
|
sharedItems: [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -120,14 +127,23 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
procedureNotes: specialtyClinic.procedureNotes || "",
|
procedureNotes: specialtyClinic.procedureNotes || "",
|
||||||
glassRequired: specialtyClinic.glassRequired || false,
|
glassRequired: specialtyClinic.glassRequired || false,
|
||||||
glassType: specialtyClinic.glassType || "",
|
glassType: specialtyClinic.glassType || "",
|
||||||
|
fileUrls: specialtyClinic.attachmentUrls || [],
|
||||||
});
|
});
|
||||||
|
setImages(
|
||||||
|
(consul.generalAttachmentUrls || []).map((url) => ({
|
||||||
|
file: null,
|
||||||
|
img: url,
|
||||||
|
path: url,
|
||||||
|
})),
|
||||||
|
);
|
||||||
}, [consulDetailQuery.data, specialtyClinicId]);
|
}, [consulDetailQuery.data, specialtyClinicId]);
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
UPDATE MUTATION
|
UPDATE MUTATION
|
||||||
========================= */
|
========================= */
|
||||||
|
|
||||||
const specialtyResultsMutation = useMutation({
|
const specialtyResultsMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async (body: { paths: string[] }) => {
|
||||||
if (!consultationId) {
|
if (!consultationId) {
|
||||||
throw new Error("Không tìm thấy ID phiên khám!");
|
throw new Error("Không tìm thấy ID phiên khám!");
|
||||||
}
|
}
|
||||||
@ -145,6 +161,7 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
// Nếu không phải khoa mắt, gán mặc định false và chuỗi rỗng khi gửi API
|
// Nếu không phải khoa mắt, gán mặc định false và chuỗi rỗng khi gửi API
|
||||||
glassRequired: isEyeClinic ? form.glassRequired : false,
|
glassRequired: isEyeClinic ? form.glassRequired : false,
|
||||||
glassType: isEyeClinic ? form.glassType : "",
|
glassType: isEyeClinic ? form.glassType : "",
|
||||||
|
fileUrls: body.paths,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@ -165,8 +182,39 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = async () => {
|
||||||
specialtyResultsMutation.mutate();
|
try {
|
||||||
|
const currentImage = images
|
||||||
|
.filter((item) => !!item.img)
|
||||||
|
.map((item) => item.img);
|
||||||
|
|
||||||
|
const files = images
|
||||||
|
.filter((item) => !!item.file)
|
||||||
|
.map((item) => item.file);
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
const uploadResult: any = await uploadServices.uploadImage({
|
||||||
|
Files: files,
|
||||||
|
SessionCode: consulDetailQuery.data?.sessionCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Cấu trúc log kiểm tra uploadResult:", uploadResult);
|
||||||
|
|
||||||
|
// CẬP NHẬT Ở ĐÂY: Lấy trực tiếp từ uploadResult hoặc bóc tách đúng tầng data
|
||||||
|
const uploadedUrls =
|
||||||
|
uploadResult?.fileUrls || uploadResult?.data?.fileUrls || [];
|
||||||
|
|
||||||
|
return specialtyResultsMutation.mutate({
|
||||||
|
paths: [...currentImage, ...uploadedUrls],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return specialtyResultsMutation.mutate({
|
||||||
|
paths: currentImage,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isLoading =
|
const isLoading =
|
||||||
@ -311,6 +359,15 @@ const UpdateSpecialtyClinicId = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<label className="text-sm font-medium">Tài liệu đính kèm</label>
|
||||||
|
|
||||||
|
<UploadMultipleFile images={images} setImages={setImages} />
|
||||||
|
|
||||||
|
{uploading && (
|
||||||
|
<p className="text-sm text-blue-500">Đang upload file...</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* BUTTON ACTION */}
|
{/* BUTTON ACTION */}
|
||||||
<div className="mt-4 flex items-center justify-end gap-3 border-t pt-5">
|
<div className="mt-4 flex items-center justify-end gap-3 border-t pt-5">
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import consultationServices, {
|
|||||||
} from "@/services/consultationServices";
|
} from "@/services/consultationServices";
|
||||||
import { QUERY_KEY } from "@/constant/config/enum";
|
import { QUERY_KEY } from "@/constant/config/enum";
|
||||||
import { toastWarn } from "@/common/funcs/toast";
|
import { toastWarn } from "@/common/funcs/toast";
|
||||||
|
import uploadServices from "@/services/uploadServices";
|
||||||
|
|
||||||
/* =========================
|
/* =========================
|
||||||
TYPES
|
TYPES
|
||||||
@ -197,6 +198,9 @@ const CreatePrescription = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
SUBMIT VALIDATION
|
||||||
|
========================= */
|
||||||
/* =========================
|
/* =========================
|
||||||
SUBMIT VALIDATION
|
SUBMIT VALIDATION
|
||||||
========================= */
|
========================= */
|
||||||
@ -225,13 +229,20 @@ const CreatePrescription = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Thực hiện gọi API upload ảnh
|
// Lấy thông tin phiên khám đang được chọn để lấy SessionCode (Mã phiên khám)
|
||||||
setLoading(true);
|
const selectedSession = examinationSessionOptions.find(
|
||||||
const uploadResponse: any = await prescriptionServices.uploadImage(
|
(item) => item.id === form.examinationSessionSpecialtyId,
|
||||||
file,
|
|
||||||
folderName,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 1. Thực hiện gọi API upload ảnh với cấu trúc chính xác
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// SỬA TẠI ĐÂY: Truyền đúng định dạng Object chứa mảng Files giống như trang cập nhật
|
||||||
|
const uploadResponse: any = await uploadServices.uploadImage({
|
||||||
|
Files: [file], // Bọc file đơn lẻ vào mảng vì API yêu cầu danh sách file
|
||||||
|
SessionCode: selectedSession?.sessionCode || "", // Truyền mã phiên khám tương ứng
|
||||||
|
});
|
||||||
|
|
||||||
console.log("KẾT QUẢ PHẢN HỒI GỐC TỪ API UPLOAD:", uploadResponse);
|
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
|
// Trích xuất URL linh hoạt tùy thuộc vào bộ bọc interceptor của axiosClient
|
||||||
@ -245,6 +256,10 @@ const CreatePrescription = () => {
|
|||||||
} else if (uploadResponse?.url) {
|
} else if (uploadResponse?.url) {
|
||||||
imageUrl = uploadResponse.url;
|
imageUrl = uploadResponse.url;
|
||||||
}
|
}
|
||||||
|
// Hỗ trợ trường hợp API trả về mảng fileUrls giống như kết quả POST /api/Media/upload bạn cung cấp
|
||||||
|
else if (uploadResponse?.fileUrls && uploadResponse.fileUrls.length > 0) {
|
||||||
|
imageUrl = uploadResponse.fileUrls[0];
|
||||||
|
}
|
||||||
// Trường hợp 2: axiosClient trả về nguyên bản cấu trúc AxiosResponse chứa thuộc tính .data
|
// 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) {
|
else if (uploadResponse?.data) {
|
||||||
const innerData = uploadResponse.data;
|
const innerData = uploadResponse.data;
|
||||||
@ -252,8 +267,10 @@ const CreatePrescription = () => {
|
|||||||
imageUrl = innerData;
|
imageUrl = innerData;
|
||||||
} else {
|
} else {
|
||||||
imageUrl =
|
imageUrl =
|
||||||
|
innerData?.fileUrls?.[0] || // Đọc mảng fileUrls bên trong data
|
||||||
innerData?.fileUrl ||
|
innerData?.fileUrl ||
|
||||||
innerData?.url ||
|
innerData?.url ||
|
||||||
|
innerData?.data?.fileUrls?.[0] ||
|
||||||
innerData?.data?.fileUrl ||
|
innerData?.data?.fileUrl ||
|
||||||
innerData?.data?.url ||
|
innerData?.data?.url ||
|
||||||
"";
|
"";
|
||||||
|
|||||||
@ -1,533 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import { Plus, Trash2 } from "lucide-react";
|
|
||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
||||||
import { useParams, useRouter, useSearchParams } 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 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 { PATH } from "@/constant/config";
|
|
||||||
|
|
||||||
import { toastWarn } from "@/common/funcs/toast";
|
|
||||||
|
|
||||||
import type { PrescriptionDetail } from "@/services/prescriptionServices";
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
TYPES
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
interface PrescriptionItem {
|
|
||||||
id: string;
|
|
||||||
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;
|
|
||||||
img?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
DEFAULT
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
const defaultMedicine: PrescriptionItem = {
|
|
||||||
id: "",
|
|
||||||
medicineName: "",
|
|
||||||
dosage: "",
|
|
||||||
frequency: "",
|
|
||||||
duration: "",
|
|
||||||
instructions: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
COMPONENT
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
const UpdatePrescription = () => {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const params = useParams();
|
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const idFromParams = params?.id as string | undefined;
|
|
||||||
|
|
||||||
const idFromSearch = searchParams?.get("_id") ?? undefined;
|
|
||||||
|
|
||||||
const id = idFromParams || idFromSearch || "";
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
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 || [];
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
GET DETAIL PRESCRIPTION
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
const detailQuery = useQuery<PrescriptionDetail>({
|
|
||||||
queryKey: [QUERY_KEY.chi_tiet_don_thuoc, id],
|
|
||||||
|
|
||||||
enabled: !!id,
|
|
||||||
|
|
||||||
queryFn: async () => {
|
|
||||||
const res = await httpRequest<PrescriptionDetail>({
|
|
||||||
showMessageFailed: true,
|
|
||||||
|
|
||||||
http: prescriptionServices.detailPrescription(id),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
res || {
|
|
||||||
id: "",
|
|
||||||
examinationSessionId: "",
|
|
||||||
prescriptionCode: "",
|
|
||||||
prescriptionImg: null,
|
|
||||||
notes: "",
|
|
||||||
createdBy: "",
|
|
||||||
items: [],
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
MAP DATA
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
const mappedForm: PrescriptionForm = useMemo(() => {
|
|
||||||
const data = detailQuery.data;
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return {
|
|
||||||
examinationSessionId: "",
|
|
||||||
notes: "",
|
|
||||||
prescriptionImg: "",
|
|
||||||
items: [defaultMedicine],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
examinationSessionId: data.examinationSessionId ?? "",
|
|
||||||
|
|
||||||
notes: data.notes ?? "",
|
|
||||||
|
|
||||||
prescriptionImg: data.prescriptionImg ?? "",
|
|
||||||
|
|
||||||
items:
|
|
||||||
data.items && data.items.length > 0
|
|
||||||
? data.items.map((i) => ({
|
|
||||||
id: i.id,
|
|
||||||
medicineName: i.medicineName ?? "",
|
|
||||||
dosage: i.dosage ?? "",
|
|
||||||
frequency: i.frequency ?? "",
|
|
||||||
duration: i.duration ?? "",
|
|
||||||
instructions: i.instructions ?? "",
|
|
||||||
}))
|
|
||||||
: [defaultMedicine],
|
|
||||||
};
|
|
||||||
}, [detailQuery.data]);
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
FORM STATE
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
const [form, setForm] = useState<PrescriptionForm>({
|
|
||||||
examinationSessionId: "",
|
|
||||||
notes: "",
|
|
||||||
prescriptionImg: "",
|
|
||||||
items: [defaultMedicine],
|
|
||||||
});
|
|
||||||
|
|
||||||
const [images, setImages] = useState<UploadImageItem[]>([]);
|
|
||||||
|
|
||||||
const [initialized, setInitialized] = useState(false);
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
INIT FORM
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!detailQuery.data || initialized) return;
|
|
||||||
|
|
||||||
queueMicrotask(() => {
|
|
||||||
setForm(mappedForm);
|
|
||||||
|
|
||||||
setImages(
|
|
||||||
mappedForm.prescriptionImg ? [{ url: mappedForm.prescriptionImg }] : [],
|
|
||||||
);
|
|
||||||
|
|
||||||
setInitialized(true);
|
|
||||||
});
|
|
||||||
}, [detailQuery.data, initialized, mappedForm]);
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
UPDATE MUTATION
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
const updateMutation = useMutation({
|
|
||||||
mutationFn: async () => {
|
|
||||||
if (!id || id === "undefined") {
|
|
||||||
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, payload),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onSuccess: () => {
|
|
||||||
router.push(PATH.PRESCRIPTION);
|
|
||||||
},
|
|
||||||
|
|
||||||
onError: (error: any) => {
|
|
||||||
console.log("UPDATE ERROR:", error);
|
|
||||||
|
|
||||||
const message =
|
|
||||||
error?.response?.data?.message || error?.message || "Có lỗi xảy ra";
|
|
||||||
|
|
||||||
toastWarn({
|
|
||||||
msg: message,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
HANDLERS
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
const handleChangeMedicine = (
|
|
||||||
index: number,
|
|
||||||
key: keyof PrescriptionItem,
|
|
||||||
value: string,
|
|
||||||
) => {
|
|
||||||
const clone = [...form.items];
|
|
||||||
|
|
||||||
clone[index] = {
|
|
||||||
...clone[index],
|
|
||||||
[key]: value,
|
|
||||||
};
|
|
||||||
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
items: clone,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddMedicine = () => {
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
|
|
||||||
items: [
|
|
||||||
...prev.items,
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
medicineName: "",
|
|
||||||
dosage: "",
|
|
||||||
frequency: "",
|
|
||||||
duration: "",
|
|
||||||
instructions: "",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveMedicine = (index: number) => {
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
|
|
||||||
items: prev.items.filter((_, i) => i !== index),
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
updateMutation.mutate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const isLoading = updateMutation.isPending || detailQuery.isLoading;
|
|
||||||
|
|
||||||
/* =========================
|
|
||||||
UI
|
|
||||||
========================= */
|
|
||||||
|
|
||||||
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]">
|
|
||||||
Cập nhật đơn thuốc
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p className="mt-1 text-sm text-[#697586]">
|
|
||||||
Chỉnh sửa đơn thuốc cho bệnh nhân
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<CustomButton
|
|
||||||
type="submit"
|
|
||||||
variant="midnightBlue"
|
|
||||||
// Vừa loading vừa check id, nếu id không tồn tại thì không cho click submit
|
|
||||||
disabled={isLoading || !id || id === "undefined"}
|
|
||||||
>
|
|
||||||
{isLoading ? "Đang cập nhật..." : "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">
|
|
||||||
{/* EXAM 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}
|
|
||||||
readOnly
|
|
||||||
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 */}
|
|
||||||
<UploadMultipleFile
|
|
||||||
images={images}
|
|
||||||
setImages={(files: UploadImageItem[]) => {
|
|
||||||
setImages(files);
|
|
||||||
|
|
||||||
const imagePath =
|
|
||||||
files?.[0]?.path || files?.[0]?.url || files?.[0]?.img || "";
|
|
||||||
|
|
||||||
setForm((prev) => ({
|
|
||||||
...prev,
|
|
||||||
prescriptionImg: imagePath,
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* NOTES */}
|
|
||||||
<TextArea
|
|
||||||
label="Ghi chú"
|
|
||||||
name="notes"
|
|
||||||
placeholder="Nhập ghi chú"
|
|
||||||
value={form.notes}
|
|
||||||
isBlur
|
|
||||||
max={5000}
|
|
||||||
onChangeValue={(v) =>
|
|
||||||
setForm((prev) => ({ ...prev, notes: String(v) }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</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={`medicineName-${index}`}
|
|
||||||
type="text"
|
|
||||||
placeholder="Nhập tên thuốc"
|
|
||||||
value={medicine.medicineName}
|
|
||||||
onChangeValue={(v) =>
|
|
||||||
handleChangeMedicine(index, "medicineName", String(v))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InputForm
|
|
||||||
label="Liều lượng"
|
|
||||||
name={`dosage-${index}`}
|
|
||||||
type="text"
|
|
||||||
placeholder="Nhập liều lượng"
|
|
||||||
value={medicine.dosage}
|
|
||||||
onChangeValue={(v) =>
|
|
||||||
handleChangeMedicine(index, "dosage", String(v))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InputForm
|
|
||||||
label="Tần suất"
|
|
||||||
name={`frequency-${index}`}
|
|
||||||
type="text"
|
|
||||||
placeholder="Nhập tần suất"
|
|
||||||
value={medicine.frequency}
|
|
||||||
onChangeValue={(v) =>
|
|
||||||
handleChangeMedicine(index, "frequency", String(v))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InputForm
|
|
||||||
label="Thời gian"
|
|
||||||
name={`duration-${index}`}
|
|
||||||
type="text"
|
|
||||||
placeholder="Nhập thời gian"
|
|
||||||
value={medicine.duration}
|
|
||||||
onChangeValue={(v) =>
|
|
||||||
handleChangeMedicine(index, "duration", String(v))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* INSTRUCTIONS */}
|
|
||||||
<div className="mt-5">
|
|
||||||
<TextArea
|
|
||||||
label="Hướng dẫn sử dụng"
|
|
||||||
name={`instructions-${index}`}
|
|
||||||
placeholder="Nhập hướng dẫn sử dụng"
|
|
||||||
value={medicine.instructions}
|
|
||||||
onChangeValue={(v) =>
|
|
||||||
handleChangeMedicine(index, "instructions", String(v))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</FormCustom>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UpdatePrescription;
|
|
||||||
@ -1 +0,0 @@
|
|||||||
export { default } from "./UpdatePrescription";
|
|
||||||
@ -41,14 +41,14 @@ const MainPageStatistical = () => {
|
|||||||
const toDate = moment(date?.to).format("YYYY-MM-DD");
|
const toDate = moment(date?.to).format("YYYY-MM-DD");
|
||||||
|
|
||||||
console.log({
|
console.log({
|
||||||
FormDate: fromDate,
|
FromDate: fromDate,
|
||||||
ToDate: toDate,
|
ToDate: toDate,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await httpRequest<ReportResponse>({
|
const res = await httpRequest<ReportResponse>({
|
||||||
showMessageFailed: true,
|
showMessageFailed: true,
|
||||||
http: consultationServices.getConsultationsReport({
|
http: consultationServices.getConsultationsReport({
|
||||||
FormDate: fromDate,
|
FromDate: fromDate,
|
||||||
ToDate: toDate,
|
ToDate: toDate,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -36,7 +36,9 @@ export default function DateOption({
|
|||||||
>
|
>
|
||||||
{(showOptionAll
|
{(showOptionAll
|
||||||
? ListOptionFilterDate
|
? ListOptionFilterDate
|
||||||
: ListOptionFilterDate.filter((item) => item.value !== TYPE_DATE.ALL)
|
: ListOptionFilterDate.filter(
|
||||||
|
(item) => item.value !== TYPE_DATE.TODAY,
|
||||||
|
)
|
||||||
).map((item) => {
|
).map((item) => {
|
||||||
const active = item.value === typeDate;
|
const active = item.value === typeDate;
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { ChevronLeft, MoveRight, ChevronRight } from "lucide-react";
|
|||||||
import CalendarMain from "../CalendarMain";
|
import CalendarMain from "../CalendarMain";
|
||||||
import Button from "@/components/customs/custom-button";
|
import Button from "@/components/customs/custom-button";
|
||||||
import Moment from "react-moment";
|
import Moment from "react-moment";
|
||||||
|
import CustomButton from "@/components/customs/custom-button";
|
||||||
|
|
||||||
const daysOfWeek = ["CN", "T2", "T3", "T4", "T5", "T6", "T7"];
|
const daysOfWeek = ["CN", "T2", "T3", "T4", "T5", "T6", "T7"];
|
||||||
|
|
||||||
@ -203,13 +204,13 @@ function RangeDatePicker({
|
|||||||
|
|
||||||
{/* BUTTON */}
|
{/* BUTTON */}
|
||||||
<div className="grid grid-cols-2 gap-3 p-3 pt-0">
|
<div className="grid grid-cols-2 gap-3 p-3 pt-0">
|
||||||
<Button onClick={onClose}>Hủy bỏ</Button>
|
<CustomButton onClick={onClose}>Hủy bỏ</CustomButton>
|
||||||
<Button
|
<CustomButton
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disable={!datePicker.from || !datePicker.to}
|
disabled={!datePicker.from || !datePicker.to}
|
||||||
>
|
>
|
||||||
Áp dụng
|
Áp dụng
|
||||||
</Button>
|
</CustomButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ContextCalendar.Provider>
|
</ContextCalendar.Provider>
|
||||||
|
|||||||
@ -2,69 +2,167 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { X, CirclePlus } from "lucide-react";
|
import {
|
||||||
|
X,
|
||||||
|
CirclePlus,
|
||||||
|
FileText,
|
||||||
|
FileSpreadsheet,
|
||||||
|
FileType,
|
||||||
|
FileImage,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { PropsUploadMultipleFile } from "./interface";
|
import { PropsUploadMultipleFile, UploadFileItem } from "./interface";
|
||||||
|
|
||||||
export default function UploadMultipleFile({
|
export default function UploadMultipleFile({
|
||||||
images = [],
|
images = [],
|
||||||
setImages,
|
setImages,
|
||||||
isDisableDelete = false,
|
isDisableDelete = false,
|
||||||
}: PropsUploadMultipleFile) {
|
}: PropsUploadMultipleFile) {
|
||||||
// 👉 upload files
|
/* =========================
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
CHECK IMAGE FILE
|
||||||
const files = event.target.files;
|
========================= */
|
||||||
if (!files) return;
|
const isImageFile = (item: UploadFileItem) => {
|
||||||
|
const type = item?.fileType || "";
|
||||||
|
|
||||||
const newImages = Array.from(files).map((file) => ({
|
return (
|
||||||
url: URL.createObjectURL(file),
|
type.startsWith("image/") ||
|
||||||
file,
|
/\.(jpg|jpeg|png|gif|webp)$/i.test(item?.path || "")
|
||||||
}));
|
);
|
||||||
|
|
||||||
setImages((prev: any) => [...prev, ...newImages]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 👉 delete file
|
/* =========================
|
||||||
const handleDelete = (index: number) => {
|
GET FILE ICON
|
||||||
setImages((prev: any) => {
|
========================= */
|
||||||
const target = prev[index];
|
const renderFileIcon = (item: UploadFileItem) => {
|
||||||
if (target?.url) URL.revokeObjectURL(target.url);
|
const fileName = item?.fileName || item?.path?.split("/").pop() || "";
|
||||||
|
|
||||||
return prev.filter((_: any, i: number) => i !== index);
|
const ext = fileName.split(".").pop()?.toLowerCase();
|
||||||
|
|
||||||
|
switch (ext) {
|
||||||
|
case "jpg":
|
||||||
|
case "jpeg":
|
||||||
|
case "png":
|
||||||
|
case "gif":
|
||||||
|
case "webp":
|
||||||
|
return <FileImage size={28} />;
|
||||||
|
|
||||||
|
case "pdf":
|
||||||
|
return <FileText size={28} />;
|
||||||
|
|
||||||
|
case "xls":
|
||||||
|
case "xlsx":
|
||||||
|
return <FileSpreadsheet size={28} />;
|
||||||
|
|
||||||
|
case "doc":
|
||||||
|
case "docx":
|
||||||
|
return <FileType size={28} />;
|
||||||
|
|
||||||
|
case "ppt":
|
||||||
|
case "pptx":
|
||||||
|
return <FileType size={28} />;
|
||||||
|
|
||||||
|
case "txt":
|
||||||
|
return <FileText size={28} />;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <FileText size={28} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
UPLOAD FILES
|
||||||
|
========================= */
|
||||||
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = event.target.files;
|
||||||
|
|
||||||
|
if (!files) return;
|
||||||
|
|
||||||
|
const newFiles: UploadFileItem[] = Array.from(files).map((file) => ({
|
||||||
|
file,
|
||||||
|
url: URL.createObjectURL(file),
|
||||||
|
fileName: file.name,
|
||||||
|
fileType: file.type,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setImages((prev) => [...prev, ...newFiles]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* =========================
|
||||||
|
DELETE FILE
|
||||||
|
========================= */
|
||||||
|
const handleDelete = (index: number) => {
|
||||||
|
setImages((prev) => {
|
||||||
|
const target = prev[index];
|
||||||
|
|
||||||
|
if (target?.url) {
|
||||||
|
URL.revokeObjectURL(target.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
return prev.filter((_, i) => i !== index);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center flex-wrap gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{/* LIST IMAGE */}
|
{/* FILE LIST */}
|
||||||
{images?.length > 0 && (
|
{images?.length > 0 && (
|
||||||
<div className="flex items-center flex-wrap gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{images.map((image, index) => (
|
{images.map((item, index) => {
|
||||||
|
const fileUrl =
|
||||||
|
item?.url ||
|
||||||
|
(item?.path
|
||||||
|
? `${process.env.NEXT_PUBLIC_IMAGE}/${item.path}`
|
||||||
|
: "");
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="relative w-[72px] h-[72px] rounded-md border border-[#99a2b34d] overflow-hidden select-none"
|
className="relative h-[72px] w-[72px] overflow-hidden rounded-md border border-[#99a2b34d]"
|
||||||
|
>
|
||||||
|
{/* IMAGE */}
|
||||||
|
{isImageFile(item) ? (
|
||||||
|
<a
|
||||||
|
href={fileUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="block h-full w-full"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={
|
src={fileUrl}
|
||||||
image?.url ||
|
alt="file"
|
||||||
`${process.env.NEXT_PUBLIC_IMAGE}/${image?.path}`
|
|
||||||
}
|
|
||||||
alt="image"
|
|
||||||
fill
|
fill
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={fileUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="flex h-full w-full flex-col items-center justify-center gap-1 bg-gray-50 p-1"
|
||||||
|
>
|
||||||
|
{renderFileIcon(item)}
|
||||||
|
|
||||||
{/* DELETE BUTTON */}
|
<span className="line-clamp-2 text-center text-[9px]">
|
||||||
{isDisableDelete && !image?.file && image?.img ? null : (
|
{item?.fileName || item?.path?.split("/").pop()}
|
||||||
<div
|
</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* DELETE */}
|
||||||
|
{isDisableDelete && !item?.file && item?.img ? null : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
onClick={() => handleDelete(index)}
|
onClick={() => handleDelete(index)}
|
||||||
className="absolute top-[2px] right-[2px] w-[18px] h-[18px] bg-white rounded-sm flex items-center justify-center cursor-pointer transition active:scale-90"
|
className="absolute right-[2px] top-[2px] flex h-[18px] w-[18px] items-center justify-center rounded-sm bg-white transition active:scale-90"
|
||||||
>
|
>
|
||||||
<X size={14} color="#8496AC" />
|
<X size={14} color="#8496AC" />
|
||||||
</div>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -72,18 +170,27 @@ export default function UploadMultipleFile({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label
|
<label
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-center justify-center",
|
"flex h-[72px] w-[72px] cursor-pointer items-center justify-center rounded-md border bg-white",
|
||||||
"w-[72px] h-[72px] rounded-md border cursor-pointer bg-white",
|
"border-[#99a2b34d] transition hover:border-gray-400",
|
||||||
"border-[#99a2b34d] hover:border-gray-400 transition",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CirclePlus color="rgba(198, 201, 206, 1)" />
|
<CirclePlus color="rgba(198, 201, 206, 1)" />
|
||||||
|
|
||||||
<input
|
<input
|
||||||
hidden
|
hidden
|
||||||
type="file"
|
|
||||||
multiple
|
multiple
|
||||||
accept="image/png, image/jpeg, image/gif"
|
type="file"
|
||||||
|
accept="
|
||||||
|
image/*,
|
||||||
|
.pdf,
|
||||||
|
.doc,
|
||||||
|
.docx,
|
||||||
|
.xls,
|
||||||
|
.xlsx,
|
||||||
|
.ppt,
|
||||||
|
.pptx,
|
||||||
|
.txt
|
||||||
|
"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
(e.target as HTMLInputElement).value = "";
|
(e.target as HTMLInputElement).value = "";
|
||||||
}}
|
}}
|
||||||
@ -91,11 +198,11 @@ export default function UploadMultipleFile({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{/* NOTE */}
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-[14px] font-medium text-[#2f3643]">Upload file</p>
|
<p className="text-[14px] font-medium text-[#2f3643]">Upload file</p>
|
||||||
|
|
||||||
<p className="text-[12px] font-medium text-[#99a2b3]">
|
<p className="text-[12px] font-medium text-[#99a2b3]">
|
||||||
File không vượt quá 50MB
|
JPG, PNG, PDF, Word, Excel, PPT (≤ 50MB)
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,5 +1,15 @@
|
|||||||
|
export interface UploadFileItem {
|
||||||
|
file?: File | null;
|
||||||
|
url?: string;
|
||||||
|
path?: string;
|
||||||
|
img?: string;
|
||||||
|
|
||||||
|
fileName?: string;
|
||||||
|
fileType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PropsUploadMultipleFile {
|
export interface PropsUploadMultipleFile {
|
||||||
images: any[];
|
images: UploadFileItem[];
|
||||||
setImages: (any: any) => void;
|
setImages: React.Dispatch<React.SetStateAction<UploadFileItem[]>>;
|
||||||
isDisableDelete?: boolean;
|
isDisableDelete?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,10 +74,6 @@ export const ListOptionFilterDate: {
|
|||||||
name: string;
|
name: string;
|
||||||
value: number;
|
value: number;
|
||||||
}[] = [
|
}[] = [
|
||||||
{
|
|
||||||
name: "Tất cả",
|
|
||||||
value: TYPE_DATE.ALL,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "Hôm nay",
|
name: "Hôm nay",
|
||||||
value: TYPE_DATE.TODAY,
|
value: TYPE_DATE.TODAY,
|
||||||
|
|||||||
@ -56,6 +56,11 @@ export interface ConsultationItem {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AttachmentFile {
|
||||||
|
fileName: string;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ConsultationDetail {
|
export interface ConsultationDetail {
|
||||||
id: string;
|
id: string;
|
||||||
patientId: string;
|
patientId: string;
|
||||||
@ -72,6 +77,7 @@ export interface ConsultationDetail {
|
|||||||
isGlassesDelivered: boolean;
|
isGlassesDelivered: boolean;
|
||||||
createdBy: string;
|
createdBy: string;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
|
generalAttachmentUrls: string[];
|
||||||
specialtyClinics: {
|
specialtyClinics: {
|
||||||
id: string;
|
id: string;
|
||||||
specialtyId: string;
|
specialtyId: string;
|
||||||
@ -83,6 +89,7 @@ export interface ConsultationDetail {
|
|||||||
glassType: string | null;
|
glassType: string | null;
|
||||||
examinerName: string;
|
examinerName: string;
|
||||||
specialtyStatus: string | number | undefined;
|
specialtyStatus: string | number | undefined;
|
||||||
|
attachmentUrls: string[];
|
||||||
sharedItems: {
|
sharedItems: {
|
||||||
id: string;
|
id: string;
|
||||||
prescriptionId: string;
|
prescriptionId: string;
|
||||||
@ -136,7 +143,7 @@ const consultationServices = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
getConsultationsReport: (
|
getConsultationsReport: (
|
||||||
params: { FormDate: string; ToDate: string },
|
params: { FromDate: string; ToDate: string },
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
return axiosClient.get<ReportResponse>(
|
return axiosClient.get<ReportResponse>(
|
||||||
@ -181,6 +188,7 @@ const consultationServices = {
|
|||||||
treatmentPlan: string | null;
|
treatmentPlan: string | null;
|
||||||
doctorNotes: string | null;
|
doctorNotes: string | null;
|
||||||
isGlassesDelivered: boolean;
|
isGlassesDelivered: boolean;
|
||||||
|
fileUrls: string[];
|
||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
@ -209,6 +217,7 @@ const consultationServices = {
|
|||||||
procedureNotes: string;
|
procedureNotes: string;
|
||||||
glassRequired: boolean;
|
glassRequired: boolean;
|
||||||
glassType: string;
|
glassType: string;
|
||||||
|
fileUrls: string[];
|
||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@ -112,23 +112,6 @@ const prescriptionServices = {
|
|||||||
/**
|
/**
|
||||||
* UPLOAD IMAGE
|
* UPLOAD IMAGE
|
||||||
*/
|
*/
|
||||||
|
|
||||||
uploadImage: (file: File, folderName?: string) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
// Key phải viết hoa chữ cái đầu đúng như Swagger yêu cầu (File và FolderName)
|
|
||||||
formData.append("File", file);
|
|
||||||
|
|
||||||
if (folderName) {
|
|
||||||
formData.append("FolderName", folderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return axiosClient.post("/api/Media/upload", formData, {
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "multipart/form-data",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default prescriptionServices;
|
export default prescriptionServices;
|
||||||
|
|||||||
50
src/services/uploadServices.ts
Normal file
50
src/services/uploadServices.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import axiosClient from ".";
|
||||||
|
|
||||||
|
type UploadPayload = {
|
||||||
|
Files: File[];
|
||||||
|
SessionCode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadServices = {
|
||||||
|
// Accept either: (filesArray, sessionCode?) OR ({ Files, SessionCode })
|
||||||
|
uploadImage: (
|
||||||
|
filesOrPayload: File[] | File | UploadPayload,
|
||||||
|
sessionCode?: string,
|
||||||
|
) => {
|
||||||
|
let files: File[] = [];
|
||||||
|
let session: string | undefined = sessionCode;
|
||||||
|
|
||||||
|
if (!filesOrPayload) return Promise.reject(new Error("No files provided"));
|
||||||
|
|
||||||
|
if (Array.isArray(filesOrPayload)) {
|
||||||
|
files = filesOrPayload as File[];
|
||||||
|
} else if ((filesOrPayload as UploadPayload).Files) {
|
||||||
|
const p = filesOrPayload as UploadPayload;
|
||||||
|
files = p.Files || [];
|
||||||
|
session = p.SessionCode || session;
|
||||||
|
} else {
|
||||||
|
files = [filesOrPayload as File];
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
files.forEach((file) => {
|
||||||
|
formData.append("Files", file);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
formData.append("SessionCode", session);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override content-type for this request so multipart boundary is set correctly
|
||||||
|
return axiosClient.post("/api/Media/upload", formData, {
|
||||||
|
headers: {
|
||||||
|
// Let browser/axios set the correct boundary; setting to multipart/form-data
|
||||||
|
// overrides the default 'application/json' header defined on axiosClient.
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default uploadServices;
|
||||||
Loading…
Reference in New Issue
Block a user