From 8cd1253b81416831a2fe57d381958880a4598d50 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Thu, 16 Jul 2026 00:16:37 +0330 Subject: [PATCH] feat(appointment): enhance service mode to display available booking slots between appointments --- assets/admin/pages/AppointmentsPage.tsx | 51 +++++++++++++++++-------- 1 file changed, 36 insertions(+), 15 deletions(-) diff --git a/assets/admin/pages/AppointmentsPage.tsx b/assets/admin/pages/AppointmentsPage.tsx index fc8d66d8..be9df220 100644 --- a/assets/admin/pages/AppointmentsPage.tsx +++ b/assets/admin/pages/AppointmentsPage.tsx @@ -429,24 +429,45 @@ export default function AppointmentsPage() { } }); - // حالت سرویسی: اسلات ثابت وجود ندارد — تایم‌لاین از خودِ نوبت‌های رزروشده - // (با مدت واقعی) ساخته می‌شود، مرتب بر اساس زمان شروع. + // حالت سرویسی: اسلات ثابت وجود ندارد — برای هر شیفتِ کاری، نوبت‌های رزروشده + // نمایش داده می‌شوند و باقیِ زمان به‌صورت بازه‌(های) خالیِ قابل‌رزرو بین آن‌ها. if (serviceMode) { const fmt = (ts: number) => new Date(ts * 1000).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' }); - return [...activeByStart.values()] - .sort((a, b) => Number(a.slot_start) - Number(b.slot_start)) - .map(a => { - const start = Number(a.slot_start); - const end = Number(a.slot_end); - return { - start, end, - start_time: fmt(start), - end_time: fmt(end), - is_available: false, - appointment: a, - cancelled_appointment: null, - } as TimelineSlot; + const parseHM = (t: string) => { const [h, m] = (t ?? '00:00').split(':').map(Number); return (h * 3600) + (m * 60); }; + const rawSessions: any[] = (slotsQuery.data?.data as any)?.sessions ?? []; + const booked = [...activeByStart.values()].sort((a, b) => Number(a.slot_start) - Number(b.slot_start)); + const now = Math.floor(Date.now() / 1000); + + const out: TimelineSlot[] = []; + // بازهٔ خالیِ قابل‌رزرو؛ اگر روی «اکنون» بیفتد به اکنون بریده می‌شود و بازه‌های + // کاملاً گذشته حذف می‌شوند تا فقط زمان قابل‌رزرو باقی بماند. + const pushFree = (start: number, end: number) => { + const s = start < now ? now : start; + if (end <= s) return; + out.push({ start: s, end, start_time: fmt(s), end_time: fmt(end), is_available: true, appointment: null, cancelled_appointment: null }); + }; + + rawSessions.forEach((session: any) => { + const slots = (session.slots as any[]) ?? []; + if (!slots.length) return; + const dayStart = Number(slots[0].start) - parseHM(session.start_time); + const winStart = dayStart + parseHM(session.start_time); + const winEnd = dayStart + parseHM(session.end_time); + + const inWin = booked.filter(a => Number(a.slot_start) >= winStart && Number(a.slot_start) < winEnd); + let cursor = winStart; + inWin.forEach(a => { + const s = Number(a.slot_start), e = Number(a.slot_end); + if (s > cursor) pushFree(cursor, s); + out.push({ + start: s, end: e, start_time: fmt(s), end_time: fmt(e), + is_available: false, appointment: a, cancelled_appointment: null, + }); + cursor = Math.max(cursor, e); }); + if (cursor < winEnd) pushFree(cursor, winEnd); + }); + return out; } const rawSessions: any[] = (slotsQuery.data?.data as any)?.sessions ?? [];