Files
clinicpro/assets/admin/components/appointments/serviceTimeline.ts
T
hamed 4f69bc9044 feat: Implement resource booking functionality
- Add service timeline builder for appointments to manage available slots.
- Create a hook to fetch resource booking services with effective durations.
- Develop ResourceBookingSlotController to handle API requests for resource booking slots.
- Implement ResourceBookingSlotService to calculate available time slots based on resource occupancy and service durations.
- Add tests for resource appointment creation and booking slot functionality to ensure correct behavior and edge cases.
2026-08-03 14:34:23 +03:30

68 lines
2.5 KiB
TypeScript

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;
}