import { useEffect, useRef, useState } from 'react'; import { UserIcon, PhoneIcon, DocumentTextIcon, PlusIcon } from '@heroicons/react/24/outline'; import type { Appointment } from '../../types'; import AppointmentStatusDropdown from '../ui/AppointmentStatusDropdown'; import AppointmentActionsMenu from '../AppointmentActions'; import ConfirmAppointmentModal from './ConfirmAppointmentModal'; import { turnStatusConfig, EMPTY_SLOT_CONFIG } from './turnStatus'; import type { TimelineSlot } from './types'; /** * نمای زمانبندی (زمانبندی) — بازسازیِ `Timeline.jsx` طرح tauri: * هر ردیف: «ریل مارکر» بیرونی (نقطهٔ رنگی + ساعت شروع + خط‌چین) سمت راست + «کارت * نوبت» چپِ آن. داخل کارت: مارکر زمانِ داخلی (نقطهٔ حلقه‌ای شروع/پایان)، اطلاعات * بیمار (نام/تلفن/سرویس) و در سمت چپ وضعیت + عملیات. اسلات خالی → «افزودن نوبت». * ردیف‌ها با max-width وسط‌چین می‌شوند. رنگ‌ها عیناً از طرح مبدأ. */ const ROW_MAX = 760; // نقطهٔ حلقه‌ای (حلقهٔ بیرونی + نقطهٔ داخلی) — مثل TimelineElement طرح tauri. function RingDot({ color }: { color: string }) { return ( ); } // مارکر زمانِ داخلِ کارت (ساعت شروع بالا، ساعت پایان پایین + خط‌چین بین آن‌ها). function InnerTimes({ start, end, color }: { start: string; end: string; color: string }) { return (
{start}
{end}
); } // ریل مارکر بیرونی (سمت راستِ ردیف): نقطهٔ رنگی + ساعت + خط‌چین عمودی. function OuterMarker({ color, time, showLine }: { color: string; time: string; showLine: boolean }) { return (
{showLine && (
)}
{time}
); } function EmptyCard({ slot, onBook }: { slot: TimelineSlot; onBook: (s: TimelineSlot) => void }) { const cfg = EMPTY_SLOT_CONFIG; const isPast = slot.start < Math.floor(Date.now() / 1000); return (
!isPast && onBook(slot)} style={{ background: isPast ? 'var(--surface-2)' : cfg.bgColor, border: `1px dashed ${isPast ? 'var(--border-2)' : cfg.borderColor}`, borderRadius: 8, padding: '12px 14px', display: 'flex', alignItems: 'center', gap: 14, cursor: isPast ? 'not-allowed' : 'pointer', opacity: isPast ? 0.7 : 1, minHeight: 76, }} >
{isPast ? 'گذشته' : 'افزودن نوبت سریع'} {!isPast && }
{/* برچسب «نوبت جدید» سمت چپ (مطابق طرح) */} {!isPast && ( نوبت جدید )}
); } function OccupiedCard({ appointment: a, queryKey, onView, }: { appointment: Appointment; queryKey: unknown[]; onView: (a: Appointment) => void; }) { const cfg = turnStatusConfig(a.status); const [confirmOpen, setConfirmOpen] = useState(false); return (
onView(a)} style={{ background: cfg.bgColor, border: `1px solid ${cfg.borderColor}`, borderRadius: 8, padding: '12px 14px', display: 'flex', alignItems: 'center', gap: 14, cursor: 'pointer', minHeight: 88, }} > {/* اطلاعات بیمار */}
{a.patient_name || '—'}
{a.patient_mobile}
سرویس: {a.service_item?.name || '—'}
{/* وضعیت + عملیات (کلیک روی این ناحیه نباید کارت را باز کند) */}
e.stopPropagation()} style={{ display: 'flex', alignItems: 'center', gap: 8, alignSelf: 'flex-start', flexShrink: 0 }}> {a.status === 'pending' && ( <> setConfirmOpen(false)} queryKey={queryKey} /> )}
); } /** * چرا این روز اسلاتی ندارد — از `empty_reason` پاسخ appointment-slots. * خالی‌بودن لزوماً تعطیلی نیست. */ const EMPTY_REASON_TEXT: Record = { no_schedule: { title: 'برنامهٔ نوبت‌دهی ثبت نشده', hint: 'برای این محل هنوز برنامهٔ کاری تعریف نشده است' }, holiday: { title: 'این روز تعطیل است', hint: 'در تقویم تعطیلات، این روز برای پزشک تعطیل ثبت شده' }, day_off: { title: 'این روز شیفت کاری ندارد', hint: 'در برنامهٔ هفتگی، برای این روز شیفتی تعریف نشده است' }, outside_window: { title: 'خارج از بازهٔ نوبت‌دهی', hint: 'این تاریخ از بازهٔ مجاز رزرو گذشته یا نوبت‌دهی آنلاین خاموش است' }, }; export default function TurnsTimeline({ slots, loading, queryKey, onView, onBook, emptyReason, errorMessage, }: { slots: TimelineSlot[]; emptyReason?: string | null; errorMessage?: string | null; loading: boolean; queryKey: unknown[]; onView: (a: Appointment) => void; onBook: (s: TimelineSlot) => void; }) { const activeRef = useRef(null); const now = Math.floor(Date.now() / 1000); const activeIndex = slots.findIndex(s => s.appointment && s.end >= now); useEffect(() => { if (!activeRef.current) return; const el = activeRef.current; const top = el.getBoundingClientRect().top + window.scrollY - window.innerHeight * 0.4; window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' }); }, [activeIndex]); if (loading) return
در حال بارگذاری...
; if (errorMessage) { return (
خطا در دریافت برنامهٔ این روز
{errorMessage}
); } if (!slots.length) { // «شیفت کاری ندارد» فقط وقتی که backend صریحاً day_off گفته باشد؛ // دلیل ناشناخته/غایب نباید به تعطیلی تفسیر شود. const reason = EMPTY_REASON_TEXT[emptyReason ?? ''] ?? { title: 'برنامهٔ این روز در دسترس نیست', hint: 'اطلاعات برنامهٔ کاری برای این روز دریافت نشد' }; return (
{reason.title}
{reason.hint}
); } return (
{slots.map((slot, i) => { const color = slot.appointment ? turnStatusConfig(slot.appointment.status).dotColor : EMPTY_SLOT_CONFIG.dotColor; return (
{slot.appointment ? : }
); })}
); }