import { formatTime } from '../../lib/utils'; import { CANCELLED_STATUSES } from './turnStatus'; import type { Appointment } from '../../types'; import type { TimelineSlot } from './types'; /** یک بازهٔ کاری: شیفتِ منبع یا سشنِ برنامهٔ هفتگیِ پزشک. */ export interface WorkingWindow { start: number; end: number; } /** * تایم‌لاینِ نوبت‌دهی **سرویسی**: نوبت‌های رزروشده + بازه‌های خالیِ بینشان. * * حالت سرویسی اسلاتِ ثابت ندارد — طول هر نوبت از سرویس‌هایش می‌آید — پس ردیف‌ها از * روی بازهٔ کاری و نوبت‌های واقعی ساخته می‌شوند، نه از شبکهٔ اسلات. * * پزشک و منبع همین یک الگوریتم را دارند: بازهٔ کاری یکی از برنامهٔ هفتگی می‌آید و * دیگری از تقویم منبع، ولی چیدنِ کارت‌ها فرقی نمی‌کند و دو نسخه‌اش یعنی دو رفتار. */ export function buildServiceTimeline( windows: WorkingWindow[], appointments: Appointment[], now: number = Math.floor(Date.now() / 1000), ): TimelineSlot[] { const booked = appointments .filter(a => !CANCELLED_STATUSES.has(a.status) && !a.is_reserve) .map(a => ({ appointment: a, start: Number(a.slot_start), end: Number(a.slot_end), })) .sort((a, b) => a.start - b.start); const out: TimelineSlot[] = []; /** بازهٔ خالی؛ تکهٔ گذشته‌اش بریده می‌شود چون قابل رزرو نیست. */ const pushFree = (start: number, end: number) => { const from = start < now ? now : start; if (end <= from) return; out.push({ start: from, end, start_time: formatTime(from), end_time: formatTime(end), is_available: true, appointment: null, cancelled_appointment: null, }); }; windows.forEach(({ start: winStart, end: winEnd }) => { let cursor = winStart; booked .filter(b => b.start >= winStart && b.start < winEnd) .forEach(({ appointment, start, end }) => { if (start > cursor) pushFree(cursor, start); out.push({ start, end, start_time: formatTime(start), end_time: formatTime(end), is_available: false, appointment, cancelled_appointment: null, }); cursor = Math.max(cursor, end); }); if (cursor < winEnd) pushFree(cursor, winEnd); }); return out; }