import apiFetch from '@wordpress/api-fetch'; import { addMonths, eachDayOfInterval, endOfMonth, format, getDate, getDay, isBefore, parse, startOfDay, startOfMonth, subMonths, } from 'date-fns'; interface SlotData { start_time: string; end_time: string; available: boolean; } interface SlotsResponse { data: { slots: SlotData[]; duration_minutes: number }; } let enrollmentId = 0; let programTitle = ''; let availableDays: number[] = []; let durationMinutes = 30; let calDate = new Date(); // tracks the current calendar month let selectedDate = ''; let selectedSlot: SlotData | null = null; export function initBooking(): void { const dataEl = document.getElementById('ac-booking-data'); if (!dataEl) return; enrollmentId = parseInt(dataEl.dataset.enrollmentId || '0', 10); programTitle = dataEl.dataset.programTitle || ''; availableDays = JSON.parse(dataEl.dataset.availableDays || '[]'); durationMinutes = parseInt(dataEl.dataset.durationMinutes || '30', 10); if (!enrollmentId) return; renderDow(); renderCalendar(); bindEvents(); } async function fetchDateSlots(date: string): Promise { try { const { data } = await apiFetch({ path: `allcoach/v1/client/date-slots?enrollment_id=${enrollmentId}&date=${date}`, }); durationMinutes = data.duration_minutes || durationMinutes; return data.slots || []; } catch { return []; } } async function bookSession(): Promise { if (!selectedSlot) return; const confirmBtn = document.getElementById( 'ac-book-confirm', ) as HTMLButtonElement; const errorEl = document.getElementById('ac-book-error')!; confirmBtn.disabled = true; errorEl.style.display = 'none'; try { await apiFetch({ path: 'allcoach/v1/client/book-session', method: 'POST', data: { enrollment_id: enrollmentId, scheduled_date: selectedDate, start_time: selectedSlot.start_time, end_time: selectedSlot.end_time, }, }); showSuccess(); } catch (err) { const e = err as { message?: string; data?: { additional_data?: { errors?: Record } }; }; const errors = e?.data?.additional_data?.errors; const message = errors && typeof errors === 'object' ? Object.values(errors).join(', ') : e?.message || 'Something went wrong.'; errorEl.textContent = message; errorEl.style.display = 'block'; confirmBtn.disabled = false; } } // --------------------------------------------------------------------------- // Calendar rendering // --------------------------------------------------------------------------- function renderDow(): void { const el = document.getElementById('ac-cal-dow'); if (!el) return; const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; el.innerHTML = days.map((d) => `
${d}
`).join(''); } function renderCalendar(): void { const daysEl = document.getElementById('ac-cal-days'); const monthEl = document.getElementById('ac-cal-month'); if (!daysEl || !monthEl) return; monthEl.textContent = format(calDate, 'MMMM yyyy'); const monthStart = startOfMonth(calDate); const monthEnd = endOfMonth(calDate); const firstDow = getDay(monthStart); const today = startOfDay(new Date()); const allDays = eachDayOfInterval({ start: monthStart, end: monthEnd }); let html = ''; // Empty cells before the first day of the month. for (let i = 0; i < firstDow; i++) { html += ''; } for (const day of allDays) { const dateStr = format(day, 'yyyy-MM-dd'); const dow = getDay(day); const isPast = isBefore(day, today); const isAvailable = !isPast && availableDays.includes(dow); const isSelected = dateStr === selectedDate; const classes = ['ac-cal-day']; if (!isAvailable) classes.push('ac-cal-day--disabled'); if (isSelected) classes.push('ac-cal-day--selected'); html += ``; } daysEl.innerHTML = html; daysEl .querySelectorAll( '.ac-cal-day:not(.ac-cal-day--disabled):not(.ac-cal-day--empty)', ) .forEach((btn) => { btn.addEventListener('click', () => { daysEl .querySelectorAll('.ac-cal-day--selected') .forEach((el) => el.classList.remove('ac-cal-day--selected')); btn.classList.add('ac-cal-day--selected'); selectedDate = btn.dataset.date || ''; const nextBtn = document.getElementById( 'ac-book-next-date', ) as HTMLButtonElement; nextBtn.disabled = false; }); }); } async function renderSlots(): Promise { const grid = document.getElementById('ac-slots-grid')!; const empty = document.getElementById('ac-slots-empty')!; const loading = document.getElementById('ac-slots-loading')!; grid.innerHTML = ''; grid.style.display = 'none'; empty.style.display = 'none'; loading.style.display = 'block'; const slots = await fetchDateSlots(selectedDate); console.log(slots); loading.style.display = 'none'; if (slots.length === 0) { empty.style.display = 'block'; return; } grid.style.display = 'grid'; grid.innerHTML = slots .map((slot) => { const label = formatTime(slot.start_time); if (!slot.available) { return ``; } return ``; }) .join(''); selectedSlot = null; (document.getElementById('ac-book-next-time') as HTMLButtonElement).disabled = true; grid .querySelectorAll('.ac-slot:not(.ac-slot--taken)') .forEach((btn) => { btn.addEventListener('click', () => { grid .querySelectorAll('.ac-slot--selected') .forEach((el) => el.classList.remove('ac-slot--selected')); btn.classList.add('ac-slot--selected'); selectedSlot = { start_time: btn.dataset.start || '', end_time: btn.dataset.end || '', available: true, }; ( document.getElementById('ac-book-next-time') as HTMLButtonElement ).disabled = false; }); }); } function showStep(step: number): void { for (let i = 1; i <= 3; i++) { const el = document.getElementById(`ac-book-step-${i}`); if (el) el.style.display = i === step ? 'block' : 'none'; } const successEl = document.getElementById('ac-book-step-success'); if (successEl) successEl.style.display = 'none'; document .querySelectorAll('#ac-book-steps .ac-step') .forEach((el) => { const s = parseInt(el.dataset.step || '0', 10); el.classList.remove('ac-step--active', 'ac-step--done'); if (s < step) { el.classList.add('ac-step--done'); el.querySelector('.ac-step__num')!.innerHTML = ''; } else if (s === step) { el.classList.add('ac-step--active'); el.querySelector('.ac-step__num')!.textContent = String(s); } else { el.querySelector('.ac-step__num')!.textContent = String(s); } }); document .querySelectorAll('#ac-book-steps .ac-step__line') .forEach((line, i) => { line.classList.toggle('ac-step__line--done', i < step - 1); }); } function showConfirmation(): void { const dateObj = parseDate(selectedDate); const dateLabel = format(dateObj, 'EEE, MMM d, yyyy'); document.getElementById('ac-confirm-program')!.textContent = programTitle; document.getElementById('ac-confirm-date')!.textContent = dateLabel; document.getElementById('ac-confirm-time')!.textContent = selectedSlot ? `${formatTime(selectedSlot.start_time)} – ${formatTime(selectedSlot.end_time)}` : ''; document.getElementById('ac-confirm-duration')!.textContent = `${durationMinutes} min`; showStep(3); } function showSuccess(): void { const stepsEl = document.getElementById('ac-book-steps'); if (stepsEl) stepsEl.style.display = 'none'; for (let i = 1; i <= 3; i++) { const el = document.getElementById(`ac-book-step-${i}`); if (el) el.style.display = 'none'; } const dateObj = parseDate(selectedDate); const dateLabel = format(dateObj, 'MMM d, yyyy'); const timeLabel = selectedSlot ? formatTime(selectedSlot.start_time) : ''; document.getElementById('ac-success-program')!.textContent = programTitle; document.getElementById('ac-success-datetime')!.textContent = `${dateLabel} at ${timeLabel}`; document.getElementById('ac-success-summary')!.textContent = `${programTitle} · ${dateLabel} at ${timeLabel}`; document.getElementById('ac-book-step-success')!.style.display = 'block'; } function bindEvents(): void { document.getElementById('ac-cal-prev')?.addEventListener('click', () => { calDate = subMonths(calDate, 1); renderCalendar(); }); document.getElementById('ac-cal-next')?.addEventListener('click', () => { calDate = addMonths(calDate, 1); renderCalendar(); }); document .getElementById('ac-book-next-date') ?.addEventListener('click', () => { if (!selectedDate) return; const dateObj = parseDate(selectedDate); const label = format(dateObj, 'EEE, MMM d, yyyy'); document.getElementById('ac-slot-date-label')!.textContent = label; showStep(2); renderSlots(); }); document .getElementById('ac-book-back-date') ?.addEventListener('click', () => showStep(1)); document .getElementById('ac-book-next-time') ?.addEventListener('click', () => { if (!selectedSlot) return; showConfirmation(); }); document .getElementById('ac-book-back-time') ?.addEventListener('click', () => showStep(2)); document .getElementById('ac-book-confirm') ?.addEventListener('click', () => bookSession()); } function parseDate(s: string): Date { return parse(s, 'yyyy-MM-dd', new Date()); } function formatTime(t: string): string { const date = parse(t, 'HH:mm', new Date()); return format(date, 'h:mm a'); }