Back to Home

Book Your Reservation

Complete the form below to book your stay or membership at April Royals Resorts

Personal Information

Select Property / Service

Fetching available properties…
Property
Admin Price
Duration
Quoted Total

Reservation Receipt

/* ── STATE ───────────────────────────────────────────────────────── */ // All properties from the API, grouped by category let apiData = { apartment: [], // { _id, title, slug, price, pricePeriod, variants: [{label, regularPrice, vipPrice, cautionDeposit}] } gym: [], tennis: [], basketball: [], facilities: [], }; let currentReceiptData = null; /* ── DOM ─────────────────────────────────────────────────────────── */ const $ = id => document.getElementById(id); const reservationType = $('reservationType'); const selectLoading = $('selectLoading'); const priceSummary = $('price-summary'); const receiptModal = $('receipt-modal'); const receiptContent = $('receipt-content'); const formAlert = $('formAlert'); /* ── HELPERS ─────────────────────────────────────────────────────── */ function naira(n) { if (!n && n !== 0) return '—'; return '₦' + parseInt(n).toLocaleString('en-NG'); } function showAlert(msg) { formAlert.className = 'form-alert error show'; $('formAlertMsg').textContent = msg; formAlert.scrollIntoView({ behavior: 'smooth', block: 'center' }); } function clearAlert() { formAlert.className = 'form-alert'; } $('checkInDate').min = new Date().toISOString().split('T')[0]; /* ── CATEGORY MAPPING ────────────────────────────────────────────── */ // Maps API property category/slug keywords → which plan section to show const CATEGORY_MAP = { apartment: ['apartment', 'studio', 'standard', 'deluxe', 'premiere', 'room', 'suite'], gym: ['gym', 'fitness'], tennis: ['tennis', 'lawn'], basketball: ['basketball', 'volleyball'], facilities: ['facilities', 'facility', 'football', 'pool', 'playground', 'game'], }; function detectCategory(prop) { const haystack = `${prop.category || ''} ${prop.slug || ''} ${prop.title || ''}`.toLowerCase(); for (const [cat, keywords] of Object.entries(CATEGORY_MAP)) { if (keywords.some(k => haystack.includes(k))) return cat; } return 'facilities'; // fallback } /* ── FETCH PROPERTIES FROM API ───────────────────────────────────── */ async function loadProperties() { selectLoading.classList.add('show'); reservationType.disabled = true; try { const res = await fetch(`${API_BASE}/properties`); const data = await res.json(); if (!data.success || !data.properties?.length) { throw new Error('No services available at the moment.'); } // Group properties by detected category data.properties.forEach(p => { const cat = detectCategory(p); if (apiData[cat]) apiData[cat].push(p); }); // Build the top-level service dropdown // One option per category that has at least one property const categoryLabels = { apartment: 'Apartment Booking', gym: 'Gym Membership', tennis: 'Lawn Tennis', basketball: 'Basketball / Volleyball', facilities: 'Facilities (Game Payment)', }; reservationType.innerHTML = ''; Object.entries(categoryLabels).forEach(([cat, label]) => { if (apiData[cat].length > 0) { const opt = document.createElement('option'); opt.value = cat; opt.textContent = label; reservationType.appendChild(opt); } }); // Populate the sub-plan selects from API data populateSubPlans(); reservationType.disabled = false; // Auto-select if ?service= param present const urlService = new URLSearchParams(window.location.search).get('service'); if (urlService && reservationType.querySelector(`option[value="${urlService}"]`)) { reservationType.value = urlService; reservationType.dispatchEvent(new Event('change')); } } catch (err) { reservationType.innerHTML = ``; } finally { selectLoading.classList.remove('show'); } } /* ── POPULATE SUB-PLAN SELECTS ───────────────────────────────────── */ function populateSubPlans() { // APARTMENT — each property becomes a unit type option // Each property should have price for Regular and optionally VIP stored in its data // We store as data attributes so calculatePrice() can read them const aptSel = $('apartmentType'); aptSel.innerHTML = ''; apiData.apartment.forEach(p => { const opt = document.createElement('option'); opt.value = p.slug; // Prices: the API property has p.price (base) and optionally p.vipPrice, p.cautionDeposit // Fall back gracefully if not present opt.dataset.slug = p.slug; opt.dataset.regular = p.price || 0; opt.dataset.vip = p.vipPrice || p.price || 0; opt.dataset.caution = p.cautionDeposit || 0; opt.textContent = p.title; aptSel.appendChild(opt); }); // GYM — each property is a plan const gymSel = $('gymPlan'); gymSel.innerHTML = ''; apiData.gym.forEach(p => { const opt = document.createElement('option'); opt.value = p.slug; opt.dataset.price = p.price || 0; opt.textContent = `${p.title}${p.price ? ' — ' + naira(p.price) : ''}`; gymSel.appendChild(opt); }); // TENNIS const tennisSel = $('tennisPlan'); tennisSel.innerHTML = ''; apiData.tennis.forEach(p => { const opt = document.createElement('option'); opt.value = p.slug; opt.dataset.price = p.price || 0; opt.textContent = `${p.title}${p.price ? ' — ' + naira(p.price) : ''}`; tennisSel.appendChild(opt); }); // BASKETBALL const bballSel = $('basketballPlan'); bballSel.innerHTML = ''; apiData.basketball.forEach(p => { const opt = document.createElement('option'); opt.value = p.slug; opt.dataset.price = p.price || 0; opt.textContent = `${p.title}${p.price ? ' — ' + naira(p.price) : ''}`; bballSel.appendChild(opt); }); // FACILITIES const facSel = $('facilityType'); facSel.innerHTML = ''; apiData.facilities.forEach(p => { const opt = document.createElement('option'); opt.value = p.slug; opt.dataset.price = p.price || 0; opt.textContent = `${p.title}${p.price ? ' — ' + naira(p.price) + ' per person' : ''}`; facSel.appendChild(opt); }); } /* ── SHOW / HIDE PLAN SECTIONS ───────────────────────────────────── */ reservationType.addEventListener('change', function () { const cat = this.value; document.querySelectorAll('.plan-section').forEach(s => s.style.display = 'none'); priceSummary.classList.remove('active'); clearAlert(); if (!cat) return; $(`plan-${cat}`).style.display = 'block'; $('date-section').style.display = 'block'; calculatePrice(); }); /* ── PRICE CALCULATION ───────────────────────────────────────────── */ function calculatePrice() { const cat = reservationType.value; if (!cat) return; let packageName = ''; let durationLabel = ''; let caution = 0; let total = 0; let valid = false; if (cat === 'apartment') { const aptOpt = $('apartmentType').selectedOptions[0]; const pkg = $('packageType').value; const months = parseInt($('aptDuration').value) || 1; if (aptOpt?.value && pkg) { const unitPrice = parseInt(pkg === 'VIP' ? aptOpt.dataset.vip : aptOpt.dataset.regular) || 0; caution = parseInt(aptOpt.dataset.caution) || 0; total = unitPrice * months + caution; packageName = `${aptOpt.text} (${pkg})`; durationLabel = `${months} month${months > 1 ? 's' : ''}`; valid = true; } } else if (cat === 'gym') { const opt = $('gymPlan').selectedOptions[0]; if (opt?.value) { total = parseInt(opt.dataset.price) || 0; packageName = opt.text; valid = true; } } else if (cat === 'tennis') { const opt = $('tennisPlan').selectedOptions[0]; if (opt?.value) { total = parseInt(opt.dataset.price) || 0; packageName = opt.text; valid = true; } } else if (cat === 'basketball') { const opt = $('basketballPlan').selectedOptions[0]; if (opt?.value) { total = parseInt(opt.dataset.price) || 0; packageName = opt.text; valid = true; } } else if (cat === 'facilities') { const facOpt = $('facilityType').selectedOptions[0]; const session = $('sessionTime').value; const players = parseInt($('numPlayers').value) || 1; if (facOpt?.value && session) { total = (parseInt(facOpt.dataset.price) || 0) * players; packageName = `${facOpt.text.split('—')[0].trim()} (${session})`; durationLabel = `${players} player${players > 1 ? 's' : ''}`; valid = true; } } if (valid) { $('summary-package').textContent = packageName; $('summary-duration').textContent = durationLabel || '—'; $('summary-caution-row').style.display = caution > 0 ? '' : 'none'; $('summary-caution').textContent = caution > 0 ? naira(caution) : '—'; $('summary-total').textContent = naira(total); priceSummary.classList.add('active'); return { packageName, durationLabel, caution, total }; } priceSummary.classList.remove('active'); return null; } // Attach change/input listeners ['apartmentType','packageType','aptDuration','gymPlan','tennisPlan', 'basketballPlan','facilityType','sessionTime','numPlayers'].forEach(id => { const el = $(id); if (el) { el.addEventListener('change', calculatePrice); if (el.type === 'number') el.addEventListener('input', calculatePrice); } }); /* ── FORM SUBMIT → POST TO API ───────────────────────────────────── */ $('reservation-form').addEventListener('submit', async function (e) { e.preventDefault(); clearAlert(); const fullName = $('fullName').value.trim(); const email = $('email').value.trim(); const phone = $('phone').value.trim(); const altPhone = $('altPhone').value.trim(); const checkInDate = $('checkInDate').value; const cat = reservationType.value; if (!fullName || !email || !phone) { showAlert('Please fill in your name, email and phone number.'); return; } if (!cat) { showAlert('Please select a service type.'); return; } if (!checkInDate) { showAlert('Please choose a check-in / start date.'); return; } const priceData = calculatePrice(); if (!priceData) { showAlert('Please complete your plan selection.'); return; } // Determine the propertySlug to send — use the selected sub-plan's slug if available let propertySlug = cat; if (cat === 'apartment') propertySlug = $('apartmentType').value || cat; else if (cat === 'gym') propertySlug = $('gymPlan').value || cat; else if (cat === 'tennis') propertySlug = $('tennisPlan').value || cat; else if (cat === 'basketball') propertySlug = $('basketballPlan').value|| cat; else if (cat === 'facilities') propertySlug = $('facilityType').value || cat; const noteParts = [ `Plan: ${priceData.packageName}`, priceData.durationLabel ? `Duration: ${priceData.durationLabel}` : '', priceData.caution > 0 ? `Caution deposit: ${naira(priceData.caution)}` : '', `Quoted total: ${naira(priceData.total)}`, altPhone ? `Alt phone: ${altPhone}` : '', ].filter(Boolean).join(' | '); const payload = { fullName, email, phone, propertySlug, checkInDate, durationType: priceData.durationLabel || cat, notes: noteParts, }; const submitBtn = $('submitBtn'); submitBtn.classList.add('loading'); submitBtn.disabled = true; try { const res = await fetch(`${API_BASE}/reservations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await res.json(); if (!res.ok || !data.success) throw new Error(data.message || `Server error ${res.status}`); currentReceiptData = { reservationId: data.reservationId, fullName, email, phone, altPhone, checkInDate, ...priceData, }; receiptContent.innerHTML = buildReceipt(currentReceiptData); receiptModal.classList.add('active'); document.body.style.overflow = 'hidden'; this.reset(); document.querySelectorAll('.plan-section').forEach(s => s.style.display = 'none'); priceSummary.classList.remove('active'); } catch (err) { console.error('[reservation]', err); showAlert(err.message || 'Something went wrong. Please try again.'); } finally { submitBtn.classList.remove('loading'); submitBtn.disabled = false; } }); /* ── BUILD RECEIPT ───────────────────────────────────────────────── */ function buildReceipt(d) { const now = new Date(); const date = now.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }); const time = now.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }); const rid = d.reservationId || ('ARR-' + Date.now().toString(36).toUpperCase()); return `
APRIL ROYALS RESORTS
Reservation Confirmation Receipt
Reservation ID
${rid}
Full Name${d.fullName}
Email${d.email}
Phone${d.phone}
${d.altPhone ? `
Alt. Phone${d.altPhone}
` : ''}
Package${d.packageName}
${d.durationLabel ? `
Duration${d.durationLabel}
` : ''} ${d.caution > 0 ? `
Caution Deposit${naira(d.caution)}
` : ''}
Check-in Date${d.checkInDate}
Date & Time${date}, ${time}
Total Amount ${naira(d.total)}
`; } /* ── WHATSAPP ────────────────────────────────────────────────────── */ function sendWhatsApp() { if (!currentReceiptData) return; const d = currentReceiptData; const rid = d.reservationId || 'Pending'; const msg = `*APRIL ROYALS RESORTS — RESERVATION*%0A%0A` + `*Reservation ID:* ${rid}%0A%0A` + `*Name:* ${d.fullName}%0A` + `*Email:* ${d.email}%0A` + `*Phone:* ${d.phone}%0A` + (d.altPhone ? `*Alt Phone:* ${d.altPhone}%0A` : '') + `%0A*Package:* ${d.packageName}%0A` + (d.durationLabel ? `*Duration:* ${d.durationLabel}%0A` : '') + (d.caution > 0 ? `*Caution Deposit:* ${naira(d.caution)}%0A` : '') + `*Check-in Date:* ${d.checkInDate}%0A` + `%0A*Total Amount:* ${naira(d.total)}%0A%0A` + `Status: Pending confirmation`; window.open(`https://wa.me/${WHATSAPP_NUMBER}?text=${msg}`, '_blank'); } /* ── RECEIPT MODAL CONTROLS ──────────────────────────────────────── */ function closeReceipt() { receiptModal.classList.remove('active'); document.body.style.overflow = ''; } $('receipt-overlay').addEventListener('click', closeReceipt); $('receipt-close').addEventListener('click', closeReceipt); $('print-receipt-btn').addEventListener('click', () => window.print()); $('whatsapp-btn').addEventListener('click', sendWhatsApp); document.addEventListener('keydown', e => { if (e.key === 'Escape' && receiptModal.classList.contains('active')) closeReceipt(); }); /* ── INIT ────────────────────────────────────────────────────────── */ loadProperties(); })();