/* booking-engine.jsx — shared booking helpers + price logic Used by mini-selector, home reservar section, and reservar.html wizard. Currency: EUR. All prices VAT-incl. */ const PRICING = { // Base price per night for the entire building (10 guests) base: 420, // weekday (Mon–Thu) weekend: 520, // Fri/Sat night // Direct booking discount applied on top directDiscountPct: 10, // entire building directDiscountRoomPct: 5, // individual rooms // Non-refundable rate extra discount on top nonRefundableExtraPct: 10, // Min stay minStayWeekend: 2, minStayWeekday: 1, // Max guests maxGuests: 10, currency: '€', }; // -------- room inventory -------- // Placeholder prices — these will come from Smoobu API in production. // Sum of rooms (L-J): 90+115+95+105+85 = 490 → > 420 building base, so the // "entire building" is genuinely cheaper than rooms à la carte. ✓ const ROOMS = [ { id:'01', key:'r_01', baseLJ: 90, baseVS:110, capacity:2, bath:'acc', img:'images/cama_habitacion_2.jpeg' }, { id:'02', key:'r_02', baseLJ:115, baseVS:140, capacity:2, bath:'private', img:'images/habitacion_3_lavabo.jpeg' }, { id:'03', key:'r_03', baseLJ: 95, baseVS:115, capacity:2, bath:'shared', img:'images/habitacion_3_escritorio.jpeg' }, { id:'04', key:'r_04', baseLJ:105, baseVS:130, capacity:2, bath:'private', img:'images/habitacion_4_2_camas.jpeg' }, { id:'05', key:'r_05', baseLJ: 85, baseVS:105, capacity:2, bath:'shared', img:'images/habitacion_5.jpeg' }, ]; function getRoom(id) { return ROOMS.find(r => r.id === id) || null; } // -------- date helpers -------- function todayISO() { const d = new Date(); d.setHours(0,0,0,0); return toISO(d); } function toISO(d) { const y = d.getFullYear(), m = String(d.getMonth()+1).padStart(2,'0'), day = String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${day}`; } function fromISO(s) { if (!s) return null; const [y, m, d] = s.split('-').map(Number); return new Date(y, m-1, d); } function addDays(s, n) { const d = fromISO(s); d.setDate(d.getDate()+n); return toISO(d); } function diffNights(a, b) { if (!a || !b) return 0; const da = fromISO(a), db = fromISO(b); return Math.round((db - da) / 86400000); } function isWeekendNight(iso) { // night STARTING on iso. Fri or Sat night. const d = fromISO(iso); const dow = d.getDay(); // 0=Sun..6=Sat return dow === 5 || dow === 6; } function fmtDate(iso, lang) { if (!iso) return '—'; const d = fromISO(iso); const opts = { day:'numeric', month:'short' }; return d.toLocaleDateString(lang === 'en' ? 'en-GB' : lang === 'fr' ? 'fr-FR' : 'es-ES', opts); } function fmtDateLong(iso, lang) { if (!iso) return '—'; const d = fromISO(iso); return d.toLocaleDateString(lang === 'en' ? 'en-GB' : lang === 'fr' ? 'fr-FR' : 'es-ES', { weekday:'short', day:'numeric', month:'long', year:'numeric' }); } function fmtMoney(n) { return `${PRICING.currency}${Math.round(n).toLocaleString('es-ES')}`; } // -------- pricing -------- // Returns total for given dates (excl. discounts), for the ENTIRE BUILDING. function basePriceForRange(checkIn, checkOut) { if (!checkIn || !checkOut) return 0; let total = 0; let cur = checkIn; while (cur < checkOut) { total += isWeekendNight(cur) ? PRICING.weekend : PRICING.base; cur = addDays(cur, 1); } return total; } // Room subtotal across the range (no discount). function roomBasePriceForRange(roomId, checkIn, checkOut) { if (!checkIn || !checkOut) return 0; const r = getRoom(roomId); if (!r) return 0; let total = 0, cur = checkIn; while (cur < checkOut) { total += isWeekendNight(cur) ? r.baseVS : r.baseLJ; cur = addDays(cur, 1); } return total; } // Sum of all 5 rooms across the range. function allRoomsBasePriceForRange(checkIn, checkOut) { return ROOMS.reduce((s, r) => s + roomBasePriceForRange(r.id, checkIn, checkOut), 0); } // What you save by booking the entire building vs all 5 rooms (after discounts). function buildingSavingsVsAllRooms(checkIn, checkOut) { const buildingTotal = priceQuote({ mode:'full', checkIn, checkOut, rate:'flex' }).total; const allRoomsTotal = priceQuote({ mode:'rooms', checkIn, checkOut, rate:'flex', rooms: ROOMS.map(r => r.id) }).total; return Math.max(0, allRoomsTotal - buildingTotal); } // ----- unified price quote ----- // args: { mode:'full'|'rooms', checkIn, checkOut, rate:'flex'|'nr', promoPct?, rooms?:[ids] } function priceQuote(args = {}) { const { mode = 'full', checkIn, checkOut, rate = 'flex', promoPct = 0, rooms = [] } = args; const nights = diffNights(checkIn, checkOut); let subtotal = 0; let directPct = 0; let lineItems = []; if (mode === 'full') { subtotal = basePriceForRange(checkIn, checkOut); directPct = PRICING.directDiscountPct; lineItems = [{ key:'full', label:null, qty:nights, subtotal }]; } else { directPct = PRICING.directDiscountRoomPct; for (const id of rooms) { const sub = roomBasePriceForRange(id, checkIn, checkOut); subtotal += sub; lineItems.push({ key:'room', roomId:id, qty:nights, subtotal:sub }); } } const directDisc = subtotal * (directPct / 100); const afterDirect = subtotal - directDisc; const rateDisc = rate === 'nr' ? afterDirect * (PRICING.nonRefundableExtraPct / 100) : 0; const afterRate = afterDirect - rateDisc; const promoDisc = afterRate * (promoPct / 100); const total = afterRate - promoDisc; return { mode, subtotal, directPct, directDisc, rateDisc, promoDisc, total, nights, perNight: nights ? total / nights : 0, lineItems, }; } // -------- min stay validation -------- function violatesMinStay(checkIn, checkOut) { const nights = diffNights(checkIn, checkOut); if (nights < 1) return false; // any weekend night → 2-night min applies for that part. Simplify: // if range includes a Fri or Sat night and nights < 2 → violate let cur = checkIn, hasWeekend = false; while (cur < checkOut) { if (isWeekendNight(cur)) hasWeekend = true; cur = addDays(cur, 1); } if (hasWeekend && nights < PRICING.minStayWeekend) return true; if (!hasWeekend && nights < PRICING.minStayWeekday) return true; return false; } // -------- mocked unavailability (for the demo, before Smoobu wiring) -------- // Returns true if range overlaps a fake "blocked" period. const MOCK_BLOCKED = [ // Pretend two blocks already taken via Booking { from:'2026-05-15', to:'2026-05-18' }, { from:'2026-06-20', to:'2026-06-24' }, ]; function isRangeAvailable(checkIn, checkOut) { if (!checkIn || !checkOut) return false; for (const b of MOCK_BLOCKED) { if (checkIn < b.to && checkOut > b.from) return false; } return true; } // -------- promo codes (mocked) -------- const PROMO_CODES = { 'CAMINO': { pct: 5, label:'Camino de Santiago' }, 'AMIGOS': { pct: 10, label:'Friends & family' }, 'INVIERNO': { pct: 15, label:'Invierno' }, }; function checkPromo(code) { if (!code) return null; return PROMO_CODES[code.trim().toUpperCase()] || null; } window.BookingEngine = { PRICING, ROOMS, getRoom, todayISO, toISO, fromISO, addDays, diffNights, isWeekendNight, fmtDate, fmtDateLong, fmtMoney, basePriceForRange, roomBasePriceForRange, allRoomsBasePriceForRange, buildingSavingsVsAllRooms, priceQuote, violatesMinStay, isRangeAvailable, checkPromo, };