/* global usScheduler */ (function () { 'use strict'; const app = document.getElementById('us-booking-app'); if (!app) return; const slotList = document.getElementById('us-slot-list'); const myLessons = document.getElementById('us-my-lessons'); const confirm = document.getElementById('us-booking-confirmation'); const errorBox = document.getElementById('us-booking-error'); const { restUrl, nonce } = usScheduler; function apiFetch(path, options = {}) { return fetch(restUrl + path, { ...options, headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce, ...(options.headers || {}), }, }).then(async (res) => { const data = await res.json(); if (!res.ok) throw new Error(data.message || 'Request failed'); return data; }); } function showError(message) { errorBox.textContent = message; errorBox.style.display = 'block'; } function clearError() { errorBox.style.display = 'none'; } function escHtml(str) { return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } const dayKey = (dt) => String(dt).slice(0, 10); // "2026-07-06 14:30:00" → "2:30 PM" function timeOf(dt) { const hours = Number(String(dt).slice(11, 13)); const minutes = String(dt).slice(14, 16); return `${hours % 12 || 12}:${minutes} ${hours < 12 ? 'AM' : 'PM'}`; } function dayLabel(key) { const date = new Date(key + 'T00:00:00'); if (Number.isNaN(date.getTime())) return key; return date.toLocaleDateString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); } function shortDayLabel(key) { const date = new Date(key + 'T00:00:00'); if (Number.isNaN(date.getTime())) return key; return date.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); } function groupByDay(slots) { const groups = new Map(); slots.forEach((slot) => { const key = dayKey(slot.start_dt); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(slot); }); return [...groups.entries()].sort((a, b) => a[0].localeCompare(b[0])); } // --- calendar view state (list is the default; week keeps its position) --- let allSlots = []; let view = 'list'; let weekStart = null; const pad = (n) => String(n).padStart(2, '0'); const toKey = (d) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; function addDays(key, days) { const date = new Date(key + 'T00:00:00'); date.setDate(date.getDate() + days); return toKey(date); } // First day of the week containing `key`, honouring the site's // start-of-week setting (0 = Sunday … 6 = Saturday). function weekStartOf(key) { const startOfWeek = Number(usScheduler.startOfWeek) || 0; const date = new Date(key + 'T00:00:00'); return addDays(key, -((date.getDay() - startOfWeek + 7) % 7)); } function toggleHtml() { return `
`; } // Agenda-style calendar: available slots grouped by day. function listHtml() { return groupByDay(allSlots).map(([key, daySlots]) => `

${escHtml(dayLabel(key))}

${daySlots.map((slot) => `
${escHtml(timeOf(slot.start_dt))}–${escHtml(timeOf(slot.end_dt))} (${escHtml(String(slot.duration_minutes))} min)
`).join('')}
`).join(''); } // Weekly calendar: seven day columns with a bookable button per slot. function weekHtml() { const byDay = new Map(groupByDay(allSlots)); const days = [...Array(7).keys()].map((i) => addDays(weekStart, i)); const columns = days.map((key) => { const daySlots = byDay.get(key) || []; const buttons = daySlots.map((slot) => ` `).join(''); return `

${escHtml(shortDayLabel(key))}

${buttons || ''}
`; }).join(''); return `
Week of ${escHtml(shortDayLabel(weekStart))}
${columns}
`; } function render() { if (!allSlots.length) { slotList.innerHTML = '

No available lesson slots at this time.

'; return; } slotList.innerHTML = toggleHtml() + (view === 'week' ? weekHtml() : listHtml()); wireCalendarEvents(); } function wireCalendarEvents() { document.getElementById('us-view-list').addEventListener('click', () => { view = 'list'; render(); }); document.getElementById('us-view-week').addEventListener('click', () => { view = 'week'; // Default to the week of the earliest open slot (the API returns // slots ordered by start), so the first look is never empty. if (!weekStart) weekStart = weekStartOf(dayKey(allSlots[0].start_dt)); render(); }); const prev = document.getElementById('us-week-prev'); const next = document.getElementById('us-week-next'); if (prev) prev.addEventListener('click', () => { weekStart = addDays(weekStart, -7); render(); }); if (next) next.addEventListener('click', () => { weekStart = addDays(weekStart, 7); render(); }); slotList.querySelectorAll('.us-book-btn[data-slot-id]').forEach((btn) => { const slot = allSlots.find((s) => String(s.id) === btn.dataset.slotId); if (slot) btn.addEventListener('click', () => openRegistration(slot)); }); } function questionField(q) { const name = `q_${q.id}`; const required = q.is_required ? 'required' : ''; let input; if (q.field_type === 'textarea') { input = ``; } else if (q.field_type === 'select') { const opts = (q.options || []).map((o) => ``).join(''); input = ``; } else if (q.field_type === 'checkbox') { input = ``; } else { input = ``; } return `

`; } function policyField(p) { return `

${escHtml(p.title)}

${p.body || ''}
`; } function openRegistration(slot) { clearError(); const offeringId = Number(slot.offering_id) || 0; const qPath = offeringId ? `offerings/${offeringId}/questions` : null; Promise.all([ qPath ? apiFetch(qPath) : Promise.resolve([]), apiFetch('policies?scope=booking'), ]) .then(([questions, policies]) => { renderRegistration(slot, offeringId, questions, policies); }) .catch((err) => showError(err.message)); } function renderRegistration(slot, offeringId, questions, policies) { const weekly = slot.recurrence_group ? `

` : ''; slotList.innerHTML = `

${escHtml(dayLabel(dayKey(slot.start_dt)))} · ${escHtml(timeOf(slot.start_dt))}–${escHtml(timeOf(slot.end_dt))}

${questions.map(questionField).join('')} ${policies.map(policyField).join('')} ${weekly}

`; document.getElementById('us-cancel').addEventListener('click', loadSlots); document.getElementById('us-register-form').addEventListener('submit', (e) => { e.preventDefault(); submitBooking(e.target, slot, offeringId, questions); }); } function submitBooking(form, slot, offeringId, questions) { clearError(); const answers = {}; questions.forEach((q) => { const field = form.elements[`q_${q.id}`]; if (!field) return; answers[q.id] = field.type === 'checkbox' ? (field.checked ? '1' : '0') : field.value; }); const accepted = [...form.querySelectorAll('.us-policy-accept:checked')].map((c) => Number(c.value)); const weeklyEl = document.getElementById('us-weekly'); apiFetch('bookings', { method: 'POST', body: JSON.stringify({ slot_id: slot.id, offering_id: offeringId, recurrence: weeklyEl && weeklyEl.checked ? 'weekly' : 'single', answers, accepted_policy_version_ids: accepted, }), }) // A booking with nothing owed has no payment, so there is no payment // step to run — the booking is already confirmed server-side. .then((res) => (res.payment ? window.usPayment.collect('lesson', (res.ids || [])[0], slotList) : null)) .then((result) => { loadMyLessons(); showConfirmation(window.usPayment.message(result)); }) .catch((err) => showError(err.message)); } function lessonStatusLabel(status) { if (status === 'pending') return 'Pending payment'; if (status === 'confirmed') return 'Confirmed'; return status.charAt(0).toUpperCase() + status.slice(1); } function renderMyLessons(lessons) { const upcoming = lessons.filter((l) => l.start_dt); if (!upcoming.length) { myLessons.innerHTML = ''; return; } myLessons.innerHTML = `

Your upcoming lessons

${upcoming.map((l) => `
${escHtml(dayLabel(dayKey(l.start_dt)))} · ${escHtml(timeOf(l.start_dt))}–${escHtml(timeOf(l.end_dt))} ${escHtml(lessonStatusLabel(String(l.status)))}
`).join('')}
`; } function loadMyLessons() { if (!myLessons) return; // The lesson list is a bonus panel: never let it break slot browsing. apiFetch('bookings') .then(renderMyLessons) .catch(() => { myLessons.innerHTML = ''; }); } function showConfirmation(message) { confirm.textContent = message; slotList.style.display = 'none'; confirm.style.display = 'block'; } function loadSlots() { clearError(); slotList.style.display = 'block'; confirm.style.display = 'none'; loadMyLessons(); apiFetch('availability') .then((slots) => { allSlots = slots; render(); }) .catch((err) => showError(err.message)); } loadSlots(); }());