A patient who booked in service mode could not see which services they had reserved or how long the appointment was. Both now appear on the list card and in both detail layouts, reading service_items and service_total_minutes from GET /api/v1/appointments/user. Every field is behind an explicit guard. Slot-mode appointments carry none of them, and an unguarded map would crash the card for every slot-mode appointment, taking the whole panel with it. A reserve entry shows its services but not a duration, because it has no time. Covered by Card.test.jsx: slot-mode appointments render unchanged, absent fields (older backend) do not crash, reserve hides the duration, and a zero duration produces no row. Task: clinicpro/docs/new_feture/taskes/task-00b-nobat724-service-mode/ Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
175 lines
7.4 KiB
JavaScript
175 lines
7.4 KiB
JavaScript
import TextLoading from "@/app/component/loading/Text";
|
|
import CalendarD from "@/components/icons/CalendarD";
|
|
import CallD from "@/components/icons/CallD";
|
|
import ClockClinic from "@/components/icons/ClockClinic";
|
|
import DownloadD from "@/components/icons/DownloadD";
|
|
import LocationClinic from "@/components/icons/LocationClinic";
|
|
import RoutingC from "@/components/icons/RoutingC";
|
|
import { Button } from "@mui/material";
|
|
import React, { useRef } from "react";
|
|
import { convertTimestampToJalali, convertTimestampToTime } from "@/helper";
|
|
import html2canvas from "html2canvas";
|
|
import jsPDF from "jspdf";
|
|
|
|
function DetailLg({ appointmentData, loading }) {
|
|
const detailsRef = useRef(null);
|
|
|
|
const handleDownloadPDF = async () => {
|
|
if (!detailsRef.current) return;
|
|
|
|
try {
|
|
const canvas = await html2canvas(detailsRef.current, {
|
|
scale: 2,
|
|
useCORS: true,
|
|
logging: false,
|
|
backgroundColor: '#ffffff',
|
|
});
|
|
|
|
const imgData = canvas.toDataURL("image/png");
|
|
const pdf = new jsPDF({
|
|
orientation: "portrait",
|
|
unit: "mm",
|
|
format: "a4",
|
|
});
|
|
|
|
const pdfWidth = pdf.internal.pageSize.getWidth();
|
|
const pdfHeight = pdf.internal.pageSize.getHeight();
|
|
|
|
const imgWidth = pdfWidth * 0.9; // 90% of page width
|
|
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
|
|
|
// Center horizontally and vertically
|
|
const xPosition = (pdfWidth - imgWidth) / 2;
|
|
const yPosition = (pdfHeight - imgHeight) / 2;
|
|
|
|
pdf.addImage(imgData, "PNG", xPosition, yPosition, imgWidth, imgHeight);
|
|
pdf.save(`appointment_${appointmentData?.uuid || Date.now()}.pdf`);
|
|
} catch (error) {
|
|
console.error("Error generating PDF:", error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div ref={detailsRef} className="lg:mt-[18px] hidden md:block">
|
|
<div className="flex md:mt-[24px] lg:mt-0 items-center justify-start gap-[8px] md:gap-[12px] lg:gap-[16px]">
|
|
<div className="flex flex-col items-start justify-center gap-[20px]">
|
|
<TextLoading loading={loading} width={70} height={18}>
|
|
<p className="text-[#3B3B3B] text-[14px] sm:text-[16px] md:text-[18px] lg:text-[20px] font-medium">
|
|
{appointmentData?.doctor?.name}
|
|
</p>
|
|
</TextLoading>
|
|
<TextLoading loading={loading} width={80} height={18}>
|
|
<p className="text-[#616161] text-[14px] md:text-[15px] lg:text-[16px] font-medium">
|
|
تخصص: {appointmentData?.doctor?.specialties?.[0]?.name}
|
|
</p>
|
|
</TextLoading>
|
|
</div>
|
|
</div>
|
|
<ul className="flex mt-[16px] sm:mt-[21px] md:mt-[27px] lg:mt-[32px] mb-[24px] flex-col gap-[24px] items-start justify-start">
|
|
<li className="flex items-center justify-start flex-wrap gap-[16px] lg:gap-[56px]">
|
|
<TextLoading loading={loading} width={100} height={20}>
|
|
<div className="flex items-start justify-start gap-[4px]">
|
|
<CalendarD />
|
|
<p className="text-[#616161] text-[14px] lg:text-[20px] font-medium">
|
|
تاریخ نوبت:{" "}
|
|
<span className="text-[#2F2F2F] text-[14px] lg:text-[20px] font-bold">
|
|
{convertTimestampToJalali(appointmentData?.slot_start)}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</TextLoading>
|
|
<TextLoading loading={loading} width={100} height={20}>
|
|
<div className="flex items-start justify-start gap-[4px]">
|
|
<ClockClinic />
|
|
<p className="text-[#616161] text-[14px] lg:text-[20px] font-medium">
|
|
زمان نوبت:{" "}
|
|
<span className="text-[#2F2F2F] text-[14px] lg:text-[20px] font-bold">
|
|
ساعت {convertTimestampToTime(appointmentData?.slot_start)}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</TextLoading>
|
|
</li>
|
|
{/* حالت نوبتدهی سرویسی. شرط اجباری: نوبت اسلاتی این فیلدها را ندارد. */}
|
|
{appointmentData?.service_items?.length > 0 && (
|
|
<li className="flex flex-wrap items-center justify-start gap-y-[12px] gap-[32px]">
|
|
<TextLoading loading={loading} width={200} height={20}>
|
|
<p className="text-[#616161] text-[14px] lg:text-[16px] font-normal">
|
|
سرویس:{" "}
|
|
<span className="text-[#2F2F2F] text-[14px] lg:text-[16px] font-medium">
|
|
{appointmentData.service_items.map((s) => s.name).join("، ")}
|
|
</span>
|
|
</p>
|
|
</TextLoading>
|
|
{!appointmentData.is_reserve && appointmentData.service_total_minutes ? (
|
|
<TextLoading loading={loading} width={110} height={20}>
|
|
<p className="text-[#616161] text-[14px] lg:text-[16px] font-normal">
|
|
مدت نوبت:{" "}
|
|
<span className="text-[#2F2F2F] text-[14px] lg:text-[16px] font-medium">
|
|
{appointmentData.service_total_minutes} دقیقه
|
|
</span>
|
|
</p>
|
|
</TextLoading>
|
|
) : null}
|
|
</li>
|
|
)}
|
|
<li className="flex flex-wrap items-center justify-start gap-y-[12px] gap-[32px]">
|
|
<TextLoading loading={loading} width={250} height={20}>
|
|
<div className="flex items-start justify-start gap-[4px]">
|
|
<LocationClinic />
|
|
<p className="text-[#616161] text-[14px] lg:text-[16px] font-normal">
|
|
آدرس:{" "}
|
|
<span className="text-[#2F2F2F] text-[14px] lg:text-[16px] font-normal">
|
|
{appointmentData?.address?.address}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
</TextLoading>
|
|
<TextLoading loading={loading} width={120} height={30}>
|
|
<Button
|
|
className="!py-[8px] !px-[12px] !gap-[8px]"
|
|
variant="text"
|
|
onClick={() => {
|
|
const map = appointmentData?.address?.map;
|
|
if (map?.latitude && map?.longitude) {
|
|
window.open(`https://www.google.com/maps/dir/?api=1&destination=${map.latitude},${map.longitude}`, '_blank');
|
|
}
|
|
}}
|
|
>
|
|
<RoutingC />
|
|
<p className="text-[#5559CE] text-[14px] font-medium">
|
|
مسیریابی آدرس
|
|
</p>
|
|
</Button>
|
|
</TextLoading>
|
|
</li>
|
|
<TextLoading loading={loading} width={120} height={18}>
|
|
<li className="flex items-center justify-start gap-[4px]">
|
|
<CallD />
|
|
<p className="text-[#616161] text-[14px] lg:text-[16px] font-normal">
|
|
تلفن:{" "}
|
|
<span className="text-[#2F2F2F] text-[14px] lg:text-[16px] font-normal">
|
|
{appointmentData?.address?.telephone}
|
|
</span>
|
|
</p>
|
|
</li>
|
|
</TextLoading>
|
|
</ul>
|
|
{/* <Button
|
|
className="!px-[12px] !hidden md:!flex !mt-[24px] !py-[8px] !border !border-solid !border-[#D7D7D7] !rounded-[4px] !w-fit !items-center !justify-center !gap-[4px]"
|
|
variant="outlined"
|
|
onClick={handleDownloadPDF}
|
|
>
|
|
<TextLoading loading={loading} width={150} height={18}>
|
|
<DownloadD />
|
|
<p className="text-[#616161] text-[16px] font-medium">
|
|
دانلود اطلاعات نوبت
|
|
</p>
|
|
</TextLoading>
|
|
</Button> */}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default DetailLg;
|