diff --git a/assets/admin/components/appointments/DoctorTabs.tsx b/assets/admin/components/appointments/DoctorTabs.tsx index 4e0e2313..bee9671a 100644 --- a/assets/admin/components/appointments/DoctorTabs.tsx +++ b/assets/admin/components/appointments/DoctorTabs.tsx @@ -19,8 +19,8 @@ export default function DoctorTabs({ const tabs: DoctorTab[] = showAll ? [{ uuid: '', name: 'همه' }, ...doctors] : doctors; return (
{tabs.map((d) => { const active = selected === d.uuid; diff --git a/assets/admin/components/appointments/TurnsTimeline.tsx b/assets/admin/components/appointments/TurnsTimeline.tsx index 1eeb564b..3ec5945a 100644 --- a/assets/admin/components/appointments/TurnsTimeline.tsx +++ b/assets/admin/components/appointments/TurnsTimeline.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { UserIcon, PhoneIcon, PlusIcon } from '@heroicons/react/24/outline'; +import { UserIcon, PhoneIcon, DocumentTextIcon, PlusIcon } from '@heroicons/react/24/outline'; import type { Appointment } from '../../types'; import AppointmentStatusDropdown from '../ui/AppointmentStatusDropdown'; import AppointmentActionsMenu from '../AppointmentActions'; @@ -7,29 +7,56 @@ import { turnStatusConfig, EMPTY_SLOT_CONFIG } from './turnStatus'; import type { TimelineSlot } from './types'; /** - * نمای زمانبندی (زمانبندی) — بازسازیِ `Timeline.jsx` طرح tauri: هر ردیف شامل یک - * «ریل مارکر» (نقطهٔ رنگی + ساعت شروع + خط‌چین اتصال) در راست و یک «کارت نوبت» در - * چپ است (RTL، row-reverse). اسلات خالی → «افزودن نوبت». اسکرول خودکار به اولین - * نوبتِ فعالِ امروز. رنگ‌ها عیناً از طرح مبدأ. + * نمای زمانبندی (زمانبندی) — بازسازیِ `Timeline.jsx` طرح tauri: + * هر ردیف: «ریل مارکر» بیرونی (نقطهٔ رنگی + ساعت شروع + خط‌چین) سمت راست + «کارت + * نوبت» چپِ آن. داخل کارت: مارکر زمانِ داخلی (نقطهٔ حلقه‌ای شروع/پایان)، اطلاعات + * بیمار (نام/تلفن/سرویس) و در سمت چپ وضعیت + عملیات. اسلات خالی → «افزودن نوبت». + * ردیف‌ها با max-width وسط‌چین می‌شوند. رنگ‌ها عیناً از طرح مبدأ. */ -// نقطهٔ رنگی + خط‌چین عمودی (ریل مارکر سمت راست). -function Marker({ color, time, showLine }: { color: string; time: string; showLine: boolean }) { +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} +
+ + {time}
); @@ -42,25 +69,38 @@ function EmptyCard({ slot, onBook }: { slot: TimelineSlot; onBook: (s: TimelineS
!isPast && onBook(slot)} style={{ - minHeight: 64, borderRadius: 8, padding: '12px 10px', - border: `1px dashed ${isPast ? 'var(--border-2)' : cfg.borderColor}`, background: isPast ? 'var(--surface-2)' : cfg.bgColor, - display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, - cursor: isPast ? 'not-allowed' : 'pointer', opacity: isPast ? 0.7 : 1, + 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 ? 'گذشته' : 'افزودن نوبت'} + + {!isPast && } +
+ {/* برچسب «نوبت جدید» سمت چپ (مطابق طرح) */} + {!isPast && ( + + + نوبت جدید + + )}
); } function OccupiedCard({ - appointment: a, slot, queryKey, onView, + appointment: a, queryKey, onView, }: { - appointment: Appointment; slot: TimelineSlot; queryKey: unknown[]; onView: (a: Appointment) => void; + appointment: Appointment; queryKey: unknown[]; onView: (a: Appointment) => void; }) { const cfg = turnStatusConfig(a.status); return ( @@ -68,12 +108,13 @@ function OccupiedCard({ onClick={() => onView(a)} style={{ background: cfg.bgColor, border: `1px solid ${cfg.borderColor}`, borderRadius: 8, - padding: '12px 10px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', - gap: 10, cursor: 'pointer', minHeight: 64, + padding: '12px 14px', display: 'flex', alignItems: 'center', gap: 14, cursor: 'pointer', minHeight: 88, }} > + + {/* اطلاعات بیمار */} -
+
@@ -84,13 +125,16 @@ function OccupiedCard({ {a.patient_mobile}
-
- سرویس: {a.service_item?.name || '—'} +
+ + + سرویس: {a.service_item?.name || '—'} +
{/* وضعیت + عملیات (کلیک روی این ناحیه نباید کارت را باز کند) */} -
e.stopPropagation()} style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 8, flexShrink: 0 }}> +
e.stopPropagation()} style={{ display: 'flex', alignItems: 'center', gap: 8, alignSelf: 'flex-start', flexShrink: 0 }}>
@@ -109,7 +153,6 @@ export default function TurnsTimeline({ }) { const activeRef = useRef(null); const now = Math.floor(Date.now() / 1000); - // اولین نوبتِ فعالِ آینده برای اسکرول خودکار. const activeIndex = slots.findIndex(s => s.appointment && s.end >= now); useEffect(() => { @@ -128,19 +171,19 @@ export default function TurnsTimeline({ ); return ( -
+
{slots.map((slot, i) => { const color = slot.appointment ? turnStatusConfig(slot.appointment.status).dotColor : EMPTY_SLOT_CONFIG.dotColor; return (
- +
{slot.appointment - ? + ? : }
diff --git a/assets/admin/pages/AppointmentsPage.tsx b/assets/admin/pages/AppointmentsPage.tsx index cf4e726f..cee26e9a 100644 --- a/assets/admin/pages/AppointmentsPage.tsx +++ b/assets/admin/pages/AppointmentsPage.tsx @@ -331,26 +331,16 @@ export default function AppointmentsPage() { return (
- {/* عنوان */} -

نوبت‌ ها

+
+ {/* عنوان */} +

نوبت‌ ها

- {/* نوار آمار */} - + {/* نوار آمار */} + - {/* تب دکترها */} - {showDoctorTabs && ( - - )} - - {/* کارت اصلی */} -
- {/* نوار ابزار */} + {/* نوار ابزار (بیرونِ کارت، مطابق طرح) */}
{/* افزودن نوبت → صفحهٔ کامل */} {!isRepresentation && ( @@ -403,39 +393,47 @@ export default function AppointmentsPage() {
- {/* محتوا */} -
- {viewMode === 'table' ? ( - <> - - {filteredAppointments.length > TABLE_PAGE_SIZE && ( -
- -
- )} - - ) : ( - <> - {!selectedDoctorUuid ? ( -
- برای نمایش زمانبندی، ابتدا یک پزشک انتخاب کنید -
- ) : ( - - )} - + {/* کارت اصلی — تب دکترها (هدر) + محتوا */} +
+ {showDoctorTabs && ( + )} +
+ {viewMode === 'table' ? ( + <> + + {filteredAppointments.length > TABLE_PAGE_SIZE && ( +
+ +
+ )} + + ) : ( + <> + {!selectedDoctorUuid ? ( +
+ برای نمایش زمانبندی، ابتدا یک پزشک انتخاب کنید +
+ ) : ( + + )} + + )} +