import React, { useMemo } from 'react'; import { TrashIcon } from '@heroicons/react/24/outline'; import PageHeader from '../components/ui/PageHeader'; import DataTable, { type Column } from '../components/ui/DataTable'; import SearchableSelect from '../components/ui/SearchableSelect'; import { formatDate, formatNumber, isoToUnix } from '../lib/utils'; import { useUrlState } from '../hooks/useUrlState'; import { usePermissions } from '../hooks/usePermissions'; import { useWaitlist, useWaitlistMatches } from '../hooks/useCancellation'; import { useAllServiceItems } from '../hooks/useServiceCatalog'; import PersianDateInput from '../components/ui/PersianDateInput'; import type { WaitlistEntry } from '../types'; /** * مرزهای ساعتش در بک‌اند است (`WaitlistEntry::DAY_PARTS`)؛ اینجا فقط ترجمهٔ نمایش. * تعریف مرز در دو جا یعنی روزی که یکی عوض شود، بیمار برای ساعتی خبر شود که رد کرده بود. */ const DAY_PART_LABELS: Record = { morning: 'صبح', afternoon: 'بعدازظهر', evening: 'عصر', }; const STATUS: Record = { waiting: { label: 'در انتظار', className: 'badge' }, notified: { label: 'خبر داده شد', className: 'badge amber' }, converted: { label: 'رزرو شد', className: 'badge green' }, expired: { label: 'منقضی', className: 'badge red' }, }; /** * لیست انتظار. * * ستون «تعداد اطلاع» عمداً دیده می‌شود: وقتی ظرفیتی آزاد می‌شود همه خبر می‌گیرند و * اولین رزروکننده می‌برد، پس اپراتور باید بداند چه کسی چند بار خبر شده. */ export default function WaitlistPage() { const [urlState, setUrlState] = useUrlState({ search: '', status: '', match_service: '', match_at: '' }); const { entries, loading, remove } = useWaitlist(urlState.status || undefined); /** * «چه کسی برای **این** وقت منتظر است» — سؤالی که لحظهٔ آزاد شدن ظرفیت پرسیده می‌شود * و فهرست کلی جوابش را نمی‌دهد. */ const { items: services } = useAllServiceItems(); const matchAt = isoToUnix(urlState.match_at) ?? 0; const { matches, loading: matching } = useWaitlistMatches({ serviceUuid: urlState.match_service, start: matchAt, }); const { can } = usePermissions(); const canManage = can('appointment_settings', 'update'); const rows = useMemo(() => { const q = urlState.search.trim(); return entries.filter((e) => q === '' || e.service_name.includes(q)); }, [entries, urlState.search]); const columns: Column[] = [ { key: 'service_name', header: 'خدمت', render: (e) => {e.service_name}, }, { key: 'window', header: 'بازهٔ دلخواه', render: (e) => ( {formatDate(e.desired_from)} تا {formatDate(e.desired_to)} ), }, { key: 'preferred_day_parts', header: 'زمان ترجیحی', render: (e) => ( {e.preferred_day_parts.length === 0 ? 'بی‌تفاوت' : e.preferred_day_parts.map((p) => DAY_PART_LABELS[p] ?? p).join('، ')} ), }, { key: 'notify_count', header: 'تعداد اطلاع', render: (e) => ( {e.notify_count} {e.notified_at !== null && ( · {formatDate(e.notified_at)} )} ), }, { key: 'status', header: 'وضعیت', render: (e) => ( {STATUS[e.status].label} ), }, ]; return (
setUrlState({ match_service: String(v ?? '') })} options={services.map((s) => ({ value: s.uuid, label: s.name }))} placeholder="انتخاب خدمت" isClearable />
setUrlState({ match_at: v })} />
{urlState.match_service === '' || matchAt === 0 ? 'خدمت و تاریخ را انتخاب کنید تا منتظرانِ همان وقت را ببینید.' : matching ? 'در حال بررسی…' : `${formatNumber(matches.length)} نفر برای این وقت منتظرند`}
{urlState.match_service !== '' && matchAt > 0 && matches.length > 0 && (

قابل تطبیق با این وقت

{matches.map((m) => (
{m.service_name} {formatDate(m.desired_from)} تا {formatDate(m.desired_to)} {m.preferred_day_parts.length === 0 ? 'بی‌تفاوت' : m.preferred_day_parts.map((p) => DAY_PART_LABELS[p] ?? p).join('، ')} {STATUS[m.status].label}
))}

لغو نوبت خودش این افراد را خبر می‌کند؛ این فهرست فقط برای دیدنِ پیش از تصمیم است.

)}
setUrlState({ search: v })} searchPlaceholder="جستجو در خدمات..." emptyMessage="کسی در لیست انتظار نیست" headerExtra={
setUrlState({ status: String(v ?? '') })} options={[ { value: '', label: 'همهٔ وضعیت‌ها' }, ...Object.entries(STATUS).map(([value, meta]) => ({ value, label: meta.label })), ]} placeholder="وضعیت" />
} actions={(e) => canManage ? ( ) : null } />
); }