Files
nobat724_front/components/dashboard/userAccount/sidebars/turns/isTurnsDetails/DetailSm.js
T
hamedandClaude Opus 5 ae958eee73 feat(booking): show services and duration on the user panel appointment
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>
2026-07-30 15:53:18 +03:30

150 lines
6.3 KiB
JavaScript

import RoutingC from "@/components/icons/RoutingC";
import { Button } from "@mui/material";
import React, { useRef } from "react";
import ButtonData from "./ButtonData";
import TextLoading from "@/app/component/loading/Text";
import { convertTimestampToJalali, convertTimestampToTime } from "@/helper";
import html2canvas from "html2canvas";
import jsPDF from "jspdf";
function DetailSm({ 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="mt-[20px] block md:hidden">
<div className="flex items-center justify-start gap-[8px]">
<div className="flex flex-col justify-center items-start gap-[8px]">
<TextLoading loading={loading} width={60} height={14}>
<p className="text-[#616161] text-[14px] font-medium">
{appointmentData?.doctor?.name}
</p>
</TextLoading>
<TextLoading loading={loading} width={70} height={14}>
<p className="text-[#7E7E7E] text-[14px] font-normal">
{appointmentData?.doctor?.specialties?.[0]?.name}
</p>
</TextLoading>
</div>
</div>
<ul className="flex w-full flex-col items-start justify-start mt-[16px]">
<li className="flex items-center w-full justify-between">
<TextLoading loading={loading} width={70} height={16}>
<p className="text-[#7E7E7E] text-[16px] font-normal">
تاریخ نوبت:
</p>
</TextLoading>
<TextLoading loading={loading} width={55} height={16}>
<p className="text-[#525252] text-[16px] font-medium">
{convertTimestampToJalali(appointmentData?.slot_start)}
</p>
</TextLoading>
</li>
<span className="block w-full h-px bg-[#EFEFEF] mt-[12px] mb-[16px]"></span>
<li className="flex flex-wrap items-center w-full justify-between">
<TextLoading loading={loading} width={80} height={16}>
<p className="text-[#7E7E7E] text-[16px] font-normal">ساعت نوبت:</p>
</TextLoading>
<TextLoading loading={loading} width={60} height={16}>
<p className="text-[#525252] text-[16px] font-medium">
{convertTimestampToTime(appointmentData?.slot_start)}
</p>
</TextLoading>
</li>
{/* حالت نوبت‌دهی سرویسی. شرط اجباری: نوبت اسلاتی این فیلدها را ندارد. */}
{appointmentData?.service_items?.length > 0 && (
<li className="flex flex-wrap gap-x-[12px] items-start w-full justify-between mt-[12px]">
<TextLoading loading={loading} width={80} height={16}>
<p className="text-[#7E7E7E] text-[16px] font-normal shrink-0">سرویس:</p>
</TextLoading>
<TextLoading loading={loading} width={140} height={16}>
<p className="text-[#525252] text-[16px] font-medium text-left">
{appointmentData.service_items.map((s) => s.name).join("، ")}
</p>
</TextLoading>
</li>
)}
{!appointmentData?.is_reserve && appointmentData?.service_total_minutes ? (
<li className="flex flex-wrap items-center w-full justify-between mt-[12px]">
<TextLoading loading={loading} width={80} height={16}>
<p className="text-[#7E7E7E] text-[16px] font-normal">مدت نوبت:</p>
</TextLoading>
<TextLoading loading={loading} width={60} height={16}>
<p className="text-[#525252] text-[16px] font-medium">
{appointmentData.service_total_minutes} دقیقه
</p>
</TextLoading>
</li>
) : null}
<span className="block w-full h-px bg-[#EFEFEF] mt-[12px] mb-[16px]"></span>
<li className="flex flex-wrap gap-x-[12px] gap-y-[12px] items-center w-full justify-between">
<TextLoading loading={loading} width={200} height={16}>
<p className="text-[#7E7E7E] text-[14px] font-normal">
آدرس:<span className="text-[#616161]">{appointmentData?.address?.address}</span>
</p>
</TextLoading>
<TextLoading loading={loading} width={130} height={40}>
<Button
className="!py-[8px] !border-[#EFEFEF] !px-[12px] !gap-[8px]"
variant="outlined"
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={100} height={18}>
<p className="text-[#7E7E7E] text-[14px] font-normal mt-[12px]">
تلفن: <span className="text-[#616161]">{appointmentData?.address?.telephone}</span>
</p>
</TextLoading>
</ul>
<ButtonData loading={loading} onDownload={handleDownloadPDF} />
</div>
);
}
export default DetailSm;