/* ── 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
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();
})();