Capilla del Monte - Córdoba
✨ Escapada perfecta en las sierras cordobesas
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cabañas Lomas del Uritorco - Sistema de Reservas</title>
<style>
/* CSS Completo para el Sistema de Reservas */
.cabanas-container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.cabanas-header {
text-align: center;
margin-bottom: 30px;
}
.cabanas-title {
font-size: 2rem;
font-weight: bold;
color: #1f2937;
margin: 0 0 10px 0;
}
.cabanas-subtitle {
font-size: 1.1rem;
color: #6b7280;
margin: 0 0 5px 0;
}
.cabanas-info {
font-size: 0.9rem;
color: #9ca3af;
margin: 0;
}
.cabanas-form-container {
background: linear-gradient(135deg, #dbeafe 0%, #e0e7ff 100%);
padding: 30px;
border-radius: 12px;
margin-bottom: 30px;
.cabanas-form-title {
font-size: 1.3rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 25px 0;
display: flex;
align-items: center;
}
.cabanas-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.cabanas-dates-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
@media (max-width: 640px) {
.cabanas-dates-row {
grid-template-columns: 1fr;
}
}
.cabanas-field {
display: flex;
flex-direction: column;
gap: 8px;
}
.cabanas-field label {
font-size: 0.9rem;
font-weight: 500;
color: #374151;
}
.cabanas-field input,
.cabanas-field select {
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
transition: all 0.2s ease;
background: #ffffff;
}
.cabanas-field input:focus,
.cabanas-field select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.cabanas-field small {
font-size: 0.8rem;
color: #6b7280;
margin-top: 4px;
}
.cabanas-submit-btn {
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
color: white;
border: none;
border-radius: 8px;
padding: 16px 24px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 10px;
}
.cabanas-submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.3);
}
.cabanas-submit-btn:active {
transform: translateY(0);
}
.cabanas-submit-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.cabanas-result {
background: #ffffff;
border-radius: 12px;
padding: 30px;
border: 1px solid #e5e7eb;
margin-top: 20px;
}
.cabanas-success {
border-color: #10b981;
background: linear-gradient(135deg, #f0fdfa 0%, #ecfdf5 100%);
}
.cabanas-error {
border-color: #ef4444;
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
}
.result-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.result-icon {
font-size: 2rem;
}
.result-header h3 {
margin: 0;
font-size: 1.5rem;
color: #1f2937;
}
.result-summary {
background: #ffffff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #e5e7eb;
}
.result-summary div {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.95rem;
}
.price-breakdown {
background: #f9fafb;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
}
.price-breakdown h4 {
margin: 0 0 10px 0;
font-size: 1rem;
color: #374151;
}
.breakdown-day {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #e5e7eb;
font-size: 0.9rem;
}
.breakdown-day:last-child {
border-bottom: none;
}
.breakdown-date {
font-weight: 500;
}
.breakdown-type {
color: #6b7280;
font-size: 0.8rem;
}
.breakdown-price {
font-weight: 600;
color: #1f2937;
}
.total-price {
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 50%, #6d28d9 100%);
color: white;
padding: 15px;
border-radius: 8px;
text-align: center;
font-size: 1.2rem;
margin: 20px 0;
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 20px;
}
.btn-reserve,
.btn-info {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-block;
text-align: center;
flex: 1;
}
.btn-reserve {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
.btn-reserve:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.btn-info {
background: #ffffff;
color: #3b82f6;
border: 2px solid #3b82f6;
}
.btn-info:hover {
background: #3b82f6;
color: white;
}
.suggestion {
background: #fffbeb;
border: 1px solid #fbbf24;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
}
.suggestion p {
margin: 0;
font-size: 0.9rem;
color: #92400e;
}
.cabanas-info-section {
background: #f9fafb;
border-radius: 12px;
padding: 25px;
}
.cabanas-info-section h3 {
margin: 0 0 20px 0;
color: #1f2937;
}
.cabanas-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.cabin-card {
background: #ffffff;
border-radius: 8px;
padding: 20px;
border: 1px solid #e5e7eb;
}
.cabin-card h4 {
margin: 0 0 10px 0;
color: #1f2937;
font-size: 1.1rem;
}
.cabin-card p {
margin: 0;
color: #6b7280;
font-size: 0.9rem;
}
.assignment-note {
text-align: center;
font-style: italic;
color: #6b7280;
font-size: 0.9rem;
margin-bottom: 20px;
}
.contact-info {
background: #ffffff;
border-radius: 8px;
padding: 20px;
border: 1px solid #e5e7eb;
}
.contact-info h4 {
margin: 0 0 15px 0;
color: #1f2937;
}
.contact-info p {
margin: 8px 0;
font-size: 0.9rem;
color: #374151;
}
.contact-info a {
color: #3b82f6;
text-decoration: none;
font-weight: 500;
}
.contact-info a:hover {
text-decoration: underline;
}
.result-message {
font-size: 1rem;
color: #374151;
margin-bottom: 15px;
line-height: 1.5;
}
.available-cabins {
background: #f0fdfa;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
border: 1px solid #10b981;
}
.available-cabins h4 {
margin: 0 0 12px 0;
font-size: 1rem;
color: #065f46;
}
.cabins-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.available-cabin {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: #ffffff;
border-radius: 6px;
border: 1px solid #d1fae5;
}
.cabin-emoji {
font-size: 1.2rem;
}
.cabin-name {
font-weight: 500;
color: #065f46;
flex: 1;
}
/* Colores específicos para cada cabaña */
.cabin-lomas-apart {
color: #10B981 !important; /* Verde para Lomas Apart */
}
.cabin-uritorco {
color: #8B5CF6 !important; /* Violeta para Uritorco */
}
.cabin-capacity {
font-size: 0.8rem;
color: #059669;
background: #ecfdf5;
padding: 2px 8px;
border-radius: 4px;
}
.cabin-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.cabin-calendar-info small {
color: #9ca3af;
font-size: 0.7rem;
}
.advance-payment {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
text-align: center;
}
.advance-payment h4 {
margin: 0 0 8px 0;
color: #92400e;
}
.advance-payment p {
margin: 0;
font-size: 0.9rem;
color: #92400e;
}
.btn-mercadopago {
background: linear-gradient(135deg, #009ee3 0%, #0084d4 100%) !important;
color: white !important;
}
.btn-mercadopago:hover {
background: linear-gradient(135deg, #0084d4 0%, #006bb3 100%) !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 158, 227, 0.3);
}
.action-buttons {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
margin-top: 20px;
}
@media (min-width: 640px) {
.action-buttons {
grid-template-columns: 1fr 1fr 1fr;
}
}
@media (max-width: 640px) {
.cabanas-container {
padding: 15px;
}
.cabanas-form-container {
padding: 20px;
}
.action-buttons {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="cabanas-container">
<div class="cabanas-header">
<h1 class="cabanas-title">🏞️ Cabañas Lomas del Uritorco</h1>
<p class="cabanas-subtitle">Capilla del Monte - Córdoba</p>
<p class="cabanas-info">✨ Escapada perfecta en las sierras cordobesas</p>
</div>
<div class="cabanas-form-container">
<h3 class="cabanas-form-title">📅 Consulta tu reserva</h3>
<form id="cabanas-booking-form" class="cabanas-form">
<div class="cabanas-dates-row">
<div class="cabanas-field">
<label for="check_in">Fecha de llegada</label>
<input type="date" id="check_in" name="check_in" required>
<small>Día que llegás a la cabaña</small>
</div>
<div class="cabanas-field">
<label for="check_out">Fecha de salida</label>
<input type="date" id="check_out" name="check_out" required>
<small>Día que dejás libre la cabaña (checkout mañana)</small>
</div>
</div>
<div class="cabanas-field">
<label for="guests">👤 Cantidad de adultos</label>
<select id="guests" name="guests" required>
<option value="1">1 adulto</option>
<option value="2">2 adultos</option>
<option value="3" selected>3 adultos</option>
<option value="4">4 adultos</option>
<option value="5">5 adultos</option>
<option value="6">6 adultos</option>
</select>
<small>Mencione la cantidad de adultos</small>
</div>
<div class="cabanas-field">
<label for="minors">🧒 ¿Trae menores?</label>
<select id="minors" name="minors">
<option value="no">No</option>
<option value="1">1 menor</option>
<option value="2">2 menores</option>
<option value="3">3 menores</option>
</select>
<small>Menores de 12 años - 50% de descuento por menor</small>
</div>
<div class="cabanas-field">
<label for="pets">🐶 ¿Trae mascotas?</label>
<select id="pets" name="pets">
<option value="no">No</option>
<option value="yes">Sí</option>
</select>
<small>Consultar políticas de mascotas (opcional)</small>
</div>
<button type="submit" id="check-availability-btn" class="cabanas-submit-btn">
<span class="btn-text">🔍 Consultar Disponibilidad</span>
<span class="btn-loading" style="display:none;">⏳</span>
</button>
</form>
</div>
</div>
<!-- ...existing code UI y formulario... -->
<!-- El bloque JS y todas las funciones van al final, dentro de <script> -->
function findSeason(dateStr) {
const date = new Date(dateStr);
for (const season of PRICING_CONFIG.seasons) {
const startDate = new Date(season.start);
const endDate = new Date(season.end);
if (date >= startDate && date <= endDate) {
return season;
}
}
// Si no encuentra temporada, usar temporada normal como fallback
return PRICING_CONFIG.seasons[1];
}
// Función para detectar días "sandwich" entre reservas (DESACTIVADA)
// Esta función se usará en el futuro cuando tengamos datos reales de reservas anteriores
function detectSandwichDates(blockedDates) {
// DESACTIVADO: No aplicar descuentos sandwich hasta tener datos reales
return [];
/*
const sandwichDates = [];
// Ordenar fechas bloqueadas
const sortedBlockedDates = blockedDates.sort();
console.log('🥪 Detectando días sandwich entre reservas:', sortedBlockedDates);
// Buscar huecos de 1-2 días entre reservas
for (let i = 0; i < sortedBlockedDates.length - 1; i++) {
const currentDate = new Date(sortedBlockedDates[i]);
const nextDate = new Date(sortedBlockedDates[i + 1]);
// Calcular días entre esta fecha y la siguiente
const daysBetween = Math.ceil((nextDate - currentDate) / (1000 60 60 * 24)) - 1;
// Si hay 1 o 2 días libres entre reservas, son días sandwich
if (daysBetween > 0 && daysBetween <= 2) {
const sandwichStart = new Date(currentDate);
sandwichStart.setDate(currentDate.getDate() + 1);
console.log(`🥪 Encontrado hueco sandwich: ${daysBetween} días entre ${sortedBlockedDates[i]} y ${sortedBlockedDates[i + 1]}`);
for (let j = 0; j < daysBetween; j++) {
const sandwichDay = new Date(sandwichStart);
sandwichDay.setDate(sandwichStart.getDate() + j);
const sandwichDateStr = sandwichDay.toISOString().split('T')[0];
sandwichDates.push(sandwichDateStr);
console.log(`🥪 Día sandwich agregado: ${sandwichDateStr}`);
}
}
}
console.log('🥪 Total días sandwich detectados:', sandwichDates);
return sandwichDates;
*/
}
// Función para verificar si una fecha está en período sandwich (DESACTIVADA)
function isSandwichDate(dateStr, cabinId, allBlockedDates) {
// DESACTIVADO: No aplicar descuentos sandwich hasta tener datos reales
return false;
// const sandwichDates = detectSandwichDates(allBlockedDates);
// return sandwichDates.includes(dateStr);
}
// Función para verificar si es fin de semana largo (usa feriados reales de Google Calendar - OPTIMIZADA)
async function isLongWeekend(date, realHolidays = null) {
const dateStr = date.toISOString().split('T')[0];
const dayOfWeek = date.getDay(); // 0 = Domingo, 6 = Sábado
// Obtener feriados reales si no se proporcionaron (solo del mes relevante)
let holidays = realHolidays;
if (!holidays) {
// Optimizar: solo buscar feriados del mes de la fecha consultada
const monthStart = new Date(date.getFullYear(), date.getMonth(), 1).toISOString().split('T')[0];
const monthEnd = new Date(date.getFullYear(), date.getMonth() + 1, 0).toISOString().split('T')[0];
holidays = await fetchArgentineHolidays(monthStart, monthEnd);
}
// Si es feriado oficial, aplicar tarifa de fin de semana largo
if (holidays.includes(dateStr)) {
return true;
}
// Verificar feriados puente (lunes después de domingo feriado, viernes antes de domingo feriado)
if (dayOfWeek === 1) { // Lunes
const sunday = new Date(date);
sunday.setDate(date.getDate() - 1);
if (holidays.includes(sunday.toISOString().split('T')[0])) {
return true;
}
}
if (dayOfWeek === 5) { // Viernes
const sunday = new Date(date);
sunday.setDate(date.getDate() + 2);
if (holidays.includes(sunday.toISOString().split('T')[0])) {
return true;
}
}
return false;
}
// Función para generar rango de fechas (CORREGIDO para evitar overbooking)
function getDateRange(startDate, endDate) {
const dates = [];
const currentDate = new Date(startDate);
const lastDate = new Date(endDate);
// IMPORTANTE: El checkout NO debe estar incluido en las fechas ocupadas
// Si checkout es 30/7, significa que liberan la cabaña el 30/7 por la mañana
// Por tanto, las fechas ocupadas son solo hasta el 29/7 (la última noche)
while (currentDate < lastDate) {
dates.push(currentDate.toISOString().split('T')[0]);
currentDate.setDate(currentDate.getDate() + 1);
}
return dates;
}
// Función para formatear fecha en español
function formatDateSpanish(dateStr) {
const date = new Date(dateStr + 'T12:00:00'); // Agregar hora para evitar problemas de zona horaria
const options = {
weekday: 'short',
day: 'numeric',
month: 'short'
};
return date.toLocaleDateString('es-AR', options);
}
// Función para generar URL de MercadoPago con datos de la reserva
function generateMercadoPagoURL(data) {
// URL base de MercadoPago
const baseURL = 'https://link.mercadopago.com.ar/lomasdeluritorco';
// Preparar datos de la reserva para n8n (se pasarán como parámetros)
const reservationData = {
// Datos de la reserva
checkin: data.check_in,
checkout: data.check_out,
guests: data.guests,
nights: data.nights,
// Precios
total_ars: data.total_price_ars,
total_usd: data.total_price_usd,
anticipo_ars: data.advance_payment_ars,
anticipo_usd: data.advance_payment_usd,
// Detalles
season: data.season,
cabins: data.available_cabins.map(c => c.name).join(' o '),
// Timestamp
timestamp: Date.now()
};
// Construir URL con parámetros para n8n (opcional, para tracking)
const urlParams = new URLSearchParams({
// Parámetros básicos para referencia
amount: data.advance_payment_ars,
reference: `RES-${reservationData.timestamp}`,
checkin: data.check_in,
checkout: data.check_out,
guests: data.guests
});
// En una integración completa con n8n, podrías usar:
// return `${baseURL}?${urlParams.toString()}`;
// Por ahora, URL directa (n8n capturará los datos después del pago)
return baseURL;
}
// Event listener principal
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('cabanas-booking-form');
const checkInInput = document.getElementById('check_in');
const checkOutInput = document.getElementById('check_out');
const submitBtn = document.getElementById('check-availability-btn');
const btnText = submitBtn.querySelector('.btn-text');
const btnLoading = submitBtn.querySelector('.btn-loading');
const resultDiv = document.getElementById('availability-result');
// Establecer fecha mínima como hoy
const today = new Date().toISOString().split('T')[0];
checkInInput.min = today;
// Actualizar fecha mínima de check-out cuando cambia check-in
checkInInput.addEventListener('change', function() {
const checkInDate = new Date(this.value);
const nextDay = new Date(checkInDate);
nextDay.setDate(checkInDate.getDate() + 1);
checkOutInput.min = nextDay.toISOString().split('T')[0];
if (checkOutInput.value && checkOutInput.value <= this.value) {
checkOutInput.value = '';
}
});
form.addEventListener('submit', async function(e) {
e.preventDefault();
const checkIn = checkInInput.value;
const checkOut = checkOutInput.value;
const guests = parseInt(document.getElementById('guests').value);
// Debug para verificar fechas capturadas
console.log(`📝 FORMULARIO ENVIADO:`);
console.log(` Check-in capturado: ${checkIn}`);
console.log(` Check-out capturado: ${checkOut}`);
console.log(` Huéspedes: ${guests}`);
// Validaciones
if (!checkIn || !checkOut) {
showError('Por favor selecciona las fechas de llegada y salida');
return;
}
if (checkIn >= checkOut) {
showError('La fecha de salida debe ser posterior a la de llegada');
return;
}
if (checkIn < today) {
showError('No puedes seleccionar fechas pasadas');
return;
}
// Mostrar loading
submitBtn.disabled = true;
btnText.style.display = 'none';
btnLoading.style.display = 'inline';
resultDiv.style.display = 'none';
try {
// Verificar disponibilidad (ahora incluye calendarios externos)
const result = await checkAvailability(checkIn, checkOut, guests);
// Ocultar loading
submitBtn.disabled = false;
btnText.style.display = 'inline';
btnLoading.style.display = 'none';
resultDiv.style.display = 'block';
if (result.available) {
resultDiv.innerHTML = generateSuccessHTML(result);
resultDiv.className = 'cabanas-result cabanas-success';
} else {
resultDiv.innerHTML = generateErrorHTML(result.message, result.suggestions, result.unavailableReasons);
resultDiv.className = 'cabanas-result cabanas-error';
}
} catch (error) {
console.error('Error verificando disponibilidad:', error);
// Ocultar loading
submitBtn.disabled = false;
btnText.style.display = 'inline';
btnLoading.style.display = 'none';
showError('Error al verificar disponibilidad. Por favor intenta nuevamente.');
}
});
async function checkAvailability(checkIn, checkOut, guests) {
// Verificar disponibilidad de todas las cabañas
const availabilityResult = await checkAllCabinsAvailability(checkIn, checkOut, guests);
console.log('Resultado de disponibilidad:', availabilityResult); // Debug
if (availabilityResult.availableCabins.length === 0) {
// Buscar fechas alternativas
const suggestions = await suggestAlternativeDates(checkIn, checkOut, guests);
let message = `Lo sentimos, no hay cabañas disponibles para ${guests} persona${guests > 1 ? 's' : ''} del ${formatDateSpanish(checkIn)} al ${formatDateSpanish(checkOut)}.`;
if (availabilityResult.unavailableReasons.length > 0) {
message += `\n\nMotivos:\n${availabilityResult.unavailableReasons.join('\n')}`;
}
return {
available: false,
message: message,
suggestions: suggestions,
unavailableReasons: availabilityResult.unavailableReasons
};
}
// Calcular pricing (usar la primera cabaña disponible para el cálculo)
const pricing = await calculatePricing(checkIn, checkOut, guests);
return {
available: true,
message: `¡Excelente! Tenemos ${availabilityResult.availableCabins.length} cabaña${availabilityResult.availableCabins.length > 1 ? 's' : ''} disponible${availabilityResult.availableCabins.length > 1 ? 's' : ''} para tu estadía`,
nights: pricing.nights,
total_price_usd: pricing.total_usd,
total_price_ars: pricing.total_ars,
advance_payment_usd: pricing.advance_payment_usd,
advance_payment_ars: pricing.advance_payment_ars,
price_breakdown: pricing.breakdown,
season: pricing.season,
available_cabins: availabilityResult.availableCabins,
check_in: checkIn,
check_out: checkOut,
guests: guests
};
}
async function calculatePricing(checkIn, checkOut, guests) {
const startDate = new Date(checkIn);
const endDate = new Date(checkOut);
const nights = Math.ceil((endDate - startDate) / (1000 60 60 * 24));
console.log(`🎯 CALCULANDO PRECIOS: ${checkIn} a ${checkOut} (${nights} noches, ${guests} huéspedes)`);
// Obtener feriados reales SOLO del período de la estadía (OPTIMIZADO)
const realHolidays = await fetchArgentineHolidays(checkIn, checkOut);
console.log(`📅 Feriados oficiales para el período ${checkIn}-${checkOut}:`, realHolidays);
let total = 0;
const breakdown = [];
let seasonInfo = '';
// Detectar días especiales ANTES del loop (solo para logs, sin descuentos)
console.log(`📅 Calculando precios estándar basados en temporada y fines de semana largos`);
for (let i = 0; i < nights; i++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateStr = currentDate.toISOString().split('T')[0];
// Buscar temporada
const season = findSeason(dateStr);
if (!season) continue;
// Guardar info de temporada para mostrar
if (seasonInfo === '') {
seasonInfo = season.name;
}
const basePriceUSD = season.prices_usd[guests];
const basePriceARS = convertUSDtoARS(basePriceUSD);
let dayTotalUSD = basePriceUSD;
let surchargeUSD = 0;
let type = 'Día normal';
// Verificar tipo de día
const dayOfWeek = currentDate.getDay(); // 0 = Domingo, 6 = Sábado
const isWeekend = (dayOfWeek === 0 || dayOfWeek === 6);
const isHoliday = realHolidays.includes(dateStr);
const isLongWeekendCheck = await isLongWeekend(currentDate, realHolidays);
console.log(`📅 ${dateStr}: Base $${basePriceUSD}USD | Weekend: ${isWeekend ? 'SÍ' : 'NO'} | Holiday: ${isHoliday ? 'SÍ' : 'NO'} | LongWeekend: ${isLongWeekendCheck ? 'SÍ' : 'NO'}`);
// Aplicar recargos según tipo de día
if (isHoliday || isLongWeekendCheck) {
surchargeUSD = season.long_weekend_surcharge_usd;
dayTotalUSD += surchargeUSD;
type = 'Fin de semana largo';
console.log(` ➕ Recargo fin de semana largo: +$${surchargeUSD}USD`);
} else if (isWeekend) {
surchargeUSD = season.weekend_surcharge_usd;
dayTotalUSD += surchargeUSD;
type = 'Fin de semana';
console.log(` ➕ Recargo fin de semana: +$${surchargeUSD}USD`);
}
console.log(` 🎯 Total día: $${dayTotalUSD}USD (${type})`);
console.log(` 💰 En pesos: $${convertUSDtoARS(dayTotalUSD).toLocaleString()}ARS`);
console.log(` ---`);
breakdown.push({
date: dateStr,
base_price_usd: basePriceUSD,
base_price_ars: basePriceARS,
surcharge_usd: surchargeUSD,
surcharge_ars: convertUSDtoARS(surchargeUSD),
discount_usd: 0, // Sin descuentos por ahora
discount_ars: 0,
total_usd: dayTotalUSD,
total_ars: convertUSDtoARS(dayTotalUSD),
type: type,
season: season.name,
is_special: false // Removido lógica sandwich
});
total += dayTotalUSD;
}
return {
nights: nights,
total_usd: total,
total_ars: convertUSDtoARS(total),
breakdown: breakdown,
season: seasonInfo,
advance_payment_usd: Math.round(total * 0.5), // 50% anticipo
advance_payment_ars: convertUSDtoARS(Math.round(total * 0.5))
};
}
function generateSuccessHTML(data) {
const checkInFormatted = formatDateSpanish(data.check_in);
const checkOutFormatted = formatDateSpanish(data.check_out);
// Debug para verificar fechas
console.log(`🗓️ DEBUG FECHAS:`);
console.log(` Fecha llegada original: ${data.check_in}`);
console.log(` Fecha salida original: ${data.check_out}`);
console.log(` Fecha llegada formateada: ${checkInFormatted}`);
console.log(` Fecha salida formateada: ${checkOutFormatted}`);
// Generar lista de cabañas disponibles
const cabinsList = data.available_cabins.map(cabin =>
`${cabin.emoji} ${cabin.name}`
).join(' o ');
const whatsappMessage = `Hola! Me interesa reservar una cabaña del ${checkInFormatted} al ${checkOutFormatted} para ${data.guests} persona${data.guests > 1 ? 's' : ''}. Cabañas disponibles: ${cabinsList}. El total sería $${data.total_price_ars.toLocaleString('es-AR')} ARS. ¿Está disponible?`;
const whatsappURL = `https://wa.me/5493548436436?text=${encodeURIComponent(whatsappMessage)}`;
const emailSubject = `Consulta Reserva - ${checkInFormatted} al ${checkOutFormatted}`;
const emailBody = `Hola,\n\nMe interesa reservar una cabaña con los siguientes detalles:\n\nFecha llegada: ${checkInFormatted}\nFecha salida: ${checkOutFormatted}\nHuéspedes: ${data.guests}\nCabañas disponibles: ${cabinsList}\nTotal: $${data.total_price_ars.toLocaleString('es-AR')} ARS\n\n¿Está disponible?\n\nGracias!`;
const emailURL = `mailto:[email protected]?subject=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailBody)}`;
// URL de MercadoPago para pago del 50% (necesitarás proporcionar la URL real)
const mercadoPagoURL = generateMercadoPagoURL(data);
// Generar sección de cabañas disponibles con detalles específicos
const availableCabinsHTML = data.available_cabins.length > 0 ? `
<div class="available-cabins">
<h4>🏠 Cabañas disponibles para tu reserva:</h4>
<div class="cabins-list">
${data.available_cabins.map(cabin => `
// ...existing code...
// Solo la versión funcional y corregida debe permanecer aquí
// Si hay duplicados, se eliminan y se mantiene la versión actualizada
// ...existing code...
function generateErrorHTML(message, suggestions = [], unavailableReasons = []) {
let suggestionsHTML = '';
if (suggestions && suggestions.length > 0) {
suggestionsHTML = `
<div class="suggestion">
<h4 style="margin: 0 0 10px 0; color: #92400e;">💡 Fechas alternativas disponibles:</h4>
${suggestions.map(suggestion => `
<div style="background: white; padding: 10px; margin: 8px 0; border-radius: 6px; border: 1px solid #f59e0b;">
<strong>${formatDateSpanish(suggestion.checkIn)} al ${formatDateSpanish(suggestion.checkOut)}</strong><br>
<small>${suggestion.availableCabins} cabaña${suggestion.availableCabins > 1 ? 's' : ''} disponible${suggestion.availableCabins > 1 ? 's' : ''} - $${suggestion.totalARS.toLocaleString('es-AR')} ARS</small>
</div>
`).join('')}
</div>
`;
}
let reasonsHTML = '';
if (unavailableReasons && unavailableReasons.length > 0) {
reasonsHTML = `
<div class="suggestion">
<h4 style="margin: 0 0 10px 0; color: #92400e;">ℹ️ Detalles de disponibilidad:</h4>
${unavailableReasons.map(reason => `
<p style="margin: 4px 0; font-size: 0.9rem;">${reason}</p>
`).join('')}
</div>
`;
}
return `
<div class="result-header">
<span class="result-icon">❌</span>
<h3>No disponible</h3>
</div>
<p class="result-message">${message}</p>
${reasonsHTML}
${suggestionsHTML}
${!suggestionsHTML ? `
<div class="suggestion">
<p><strong>💡 Sugerencia:</strong> Prueba con otras fechas o reduce el número de huéspedes.</p>
</div>
` : ''}
<div class="contact-info">
<h4>📞 ¿Necesitas ayuda?</h4>
<p>Contáctanos para encontrar fechas alternativas:</p>
<p>WhatsApp: <a href="https://wa.me/5493548436436?text=Hola!%20Necesito%20ayuda%20con%20las%20fechas%20de%20reserva" target="_blank">+54 9 354 843-6436</a></p>
<p>Email: <a href="mailto:[email protected]">[email protected]</a></p>
</div>
`;
}
function showError(message) {
resultDiv.style.display = 'block';
resultDiv.innerHTML = generateErrorHTML(message, [], []);
resultDiv.className = 'cabanas-result cabanas-error';
}
});
</body>
<script>
// TODO: Mueve aquí todo el JS y funciones del sistema de reservas
// Ejemplo:
// function parseICalData(...) {...}
// function checkAllCabinsAvailability(...) {...}
// ...etc...
</script>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Caba�as Lomas del Uritorco - Sistema de Reservas</title>
<style>
/* CSS Completo para el Sistema de Reservas */
.cabanas-container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.cabanas-header {
text-align: center;
margin-bottom: 30px;
}
.cabanas-title {
font-size: 2rem;
font-weight: bold;
color: #1f2937;
margin: 0 0 10px 0;
}
.cabanas-subtitle {
font-size: 1.1rem;
color: #6b7280;
margin: 0 0 5px 0;
}
.cabanas-info {
font-size: 0.9rem;
color: #9ca3af;
margin: 0;
}
.cabanas-form-container {
background: linear-gradient(135deg, #dbeafe 0%, #e0e7ff 100%);
padding: 30px;
border-radius: 12px;
margin-bottom: 30px;
}
.cabanas-form-title {
font-size: 1.3rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 25px 0;
display: flex;
align-items: center;
}
.cabanas-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.cabanas-dates-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
@media (max-width: 640px) {
.cabanas-dates-row {
grid-template-columns: 1fr;
}
}
.cabanas-field {
display: flex;
flex-direction: column;
gap: 8px;
}
.cabanas-field label {
font-size: 0.9rem;
font-weight: 500;
color: #374151;
}
.cabanas-field input,
.cabanas-field select {
padding: 12px 16px;
border: 2px solid #e5e7eb;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
transition: all 0.2s ease;
background: #ffffff;
}
.cabanas-field input:focus,
.cabanas-field select:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.cabanas-field small {
font-size: 0.8rem;
color: #6b7280;
margin-top: 4px;
}
.cabanas-submit-btn {
background: linear-gradient(135deg, #3b82f6 0%, #1e40af 100%);
color: white;
border: none;
border-radius: 8px;
padding: 16px 24px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 10px;
}
.cabanas-submit-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.3);
}
.cabanas-submit-btn:active {
transform: translateY(0);
}
.cabanas-submit-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.cabanas-result {
background: #ffffff;
border-radius: 12px;
padding: 30px;
border: 1px solid #e5e7eb;
margin-top: 20px;
}
.cabanas-success {
border-color: #10b981;
background: linear-gradient(135deg, #f0fdfa 0%, #ecfdf5 100%);
}
.cabanas-error {
border-color: #ef4444;
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
}
.result-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.result-icon {
font-size: 2rem;
}
.result-header h3 {
margin: 0;
font-size: 1.5rem;
color: #1f2937;
}
.result-summary {
background: #ffffff;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #e5e7eb;
}
.result-summary div {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 0.95rem;
}
.price-breakdown {
background: #f9fafb;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
}
.price-breakdown h4 {
margin: 0 0 10px 0;
font-size: 1rem;
color: #374151;
}
.breakdown-day {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #e5e7eb;
font-size: 0.9rem;
}
.breakdown-day:last-child {
border-bottom: none;
}
.breakdown-date {
font-weight: 500;
}
.breakdown-type {
color: #6b7280;
font-size: 0.8rem;
}
.breakdown-price {
font-weight: 600;
color: #1f2937;
}
.total-price {
background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 50%, #6d28d9 100%);
color: white;
padding: 15px;
border-radius: 8px;
text-align: center;
font-size: 1.2rem;
margin: 20px 0;
box-shadow: 0 4px 15px rgba(139, 92, 246, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.action-buttons {
display: flex;
gap: 12px;
margin-top: 20px;
}
.btn-reserve,
.btn-info {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
display: inline-block;
text-align: center;
flex: 1;
}
.btn-reserve {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
}
.btn-reserve:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.btn-share-whatsapp:hover {
transform: translateY(-1px);
box-shadow: 0 8px 25px rgba(37, 211, 102, 0.6);
background: linear-gradient(135deg, #1cb55c 0%, #128c43 50%, #0f7c3c 100%);
}
.btn-generate-image:hover {
transform: translateY(-1px);
box-shadow: 0 8px 25px rgba(245, 158, 11, 0.6);
background: linear-gradient(135deg, #d97706 0%, #b45309 50%, #92400e 100%);
}
.btn-info {
background: #ffffff;
color: #3b82f6;
border: 2px solid #3b82f6;
}
.btn-info:hover {
background: #3b82f6;
color: white;
}
.suggestion {
background: #fffbeb;
border: 1px solid #fbbf24;
border-radius: 8px;
padding: 15px;
margin-top: 15px;
}
.suggestion p {
margin: 0;
font-size: 0.9rem;
color: #92400e;
}
.cabanas-info-section {
background: #f9fafb;
border-radius: 12px;
padding: 25px;
}
.cabanas-info-section h3 {
margin: 0 0 20px 0;
color: #1f2937;
}
.cabanas-info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.cabin-card {
background: #ffffff;
border-radius: 8px;
padding: 20px;
border: 1px solid #e5e7eb;
}
.cabin-card h4 {
margin: 0 0 10px 0;
color: #1f2937;
font-size: 1.1rem;
}
.cabin-card p {
margin: 0;
color: #6b7280;
font-size: 0.9rem;
}
.assignment-note {
text-align: center;
font-style: italic;
color: #6b7280;
font-size: 0.9rem;
margin-bottom: 20px;
}
.contact-info {
background: #ffffff;
border-radius: 8px;
padding: 20px;
border: 1px solid #e5e7eb;
}
.contact-info h4 {
margin: 0 0 15px 0;
color: #1f2937;
}
.contact-info p {
margin: 8px 0;
font-size: 0.9rem;
color: #374151;
}
.contact-info a {
color: #3b82f6;
text-decoration: none;
font-weight: 500;
}
.contact-info a:hover {
text-decoration: underline;
}
.result-message {
font-size: 1rem;
color: #374151;
margin-bottom: 15px;
line-height: 1.5;
}
.available-cabins {
background: #f0fdfa;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
border: 1px solid #10b981;
}
.available-cabins h4 {
margin: 0 0 12px 0;
font-size: 1rem;
color: #065f46;
}
.cabins-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.available-cabin {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: #ffffff;
border-radius: 6px;
border: 1px solid #d1fae5;
}
.cabin-emoji {
font-size: 1.2rem;
}
.cabin-name {
font-weight: 500;
color: #065f46;
flex: 1;
}
/* Colores espec�ficos para cada caba�a */
.cabin-lomas-apart {
color: #10B981 !important; /* Verde para Lomas Apart */
}
.cabin-uritorco {
color: #8B5CF6 !important; /* Violeta para Uritorco */
}
.cabin-capacity {
font-size: 0.8rem;
color: #059669;
background: #ecfdf5;
padding: 2px 8px;
border-radius: 4px;
}
.cabin-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.cabin-calendar-info small {
color: #9ca3af;
font-size: 0.7rem;
}
.advance-payment {
background: #fef3c7;
border: 1px solid #f59e0b;
border-radius: 8px;
padding: 15px;
margin: 15px 0;
text-align: center;
}
.advance-payment h4 {
margin: 0 0 8px 0;
color: #92400e;
}
.advance-payment p {
margin: 0;
font-size: 0.9rem;
color: #92400e;
}
.btn-mercadopago {
background: linear-gradient(135deg, #009ee3 0%, #0084d4 100%) !important;
color: white !important;
}
.btn-mercadopago:hover {
background: linear-gradient(135deg, #0084d4 0%, #006bb3 100%) !important;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 158, 227, 0.3);
}
.action-buttons {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
margin-top: 20px;
}
@media (min-width: 640px) {
.action-buttons {
grid-template-columns: 1fr 1fr 1fr;
}
}
@media (max-width: 640px) {
.cabanas-container {
padding: 15px;
}
.cabanas-form-container {
padding: 20px;
}
.action-buttons {
flex-direction: column;
}
}
/* Estilos para secci�n de descuentos */
.cabanas-discount-section {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
border: 2px solid #f59e0b;
border-radius: 12px;
padding: 20px;
margin-top: 20px;
}
.cabanas-discount-section h4 {
margin: 0 0 15px 0;
color: #92400e;
font-size: 1.1rem;
font-weight: 600;
}
.discount-input-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
.discount-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #f59e0b;
border-radius: 8px;
font-size: 1rem;
background: #ffffff;
color: #92400e;
}
.discount-input:focus {
outline: none;
border-color: #d97706;
box-shadow: 0 0 0 3px rgba(217, 119, 6, 0.1);
}
.discount-apply-btn {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
color: white;
border: none;
border-radius: 8px;
padding: 12px 20px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.discount-apply-btn:hover {
background: linear-gradient(135deg, #d97706 0%, #b45309 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
}
.discount-apply-btn:disabled {
background: #9ca3af;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.discount-result {
padding: 12px 16px;
border-radius: 8px;
font-weight: 600;
margin-top: 10px;
}
.discount-success {
background: #d1fae5;
color: #065f46;
border: 2px solid #10b981;
}
.discount-error {
background: #fee2e2;
color: #991b1b;
border: 2px solid #ef4444;
}
@media (max-width: 640px) {
.discount-input-row {
flex-direction: column;
}
.discount-apply-btn {
padding: 14px 20px;
}
}
</style>
</head>
<body>
<div class="cabanas-container">
<div class="cabanas-header">
<h1 class="cabanas-title">??? Caba�as Lomas del Uritorco</h1>
<p class="cabanas-subtitle">Capilla del Monte - C�rdoba</p>
<p class="cabanas-info">? Escapada perfecta en las sierras cordobesas</p>
</div>
<div class="cabanas-form-container">
<h3 class="cabanas-form-title">?? Consulta tu reserva</h3>
<form id="cabanas-booking-form" class="cabanas-form">
<div class="cabanas-dates-row">
<div class="cabanas-field">
<label for="check_in">Fecha de llegada</label>
<input type="date" id="check_in" name="check_in" required>
<small>D�a que lleg�s a la caba�a</small>
</div>
<div class="cabanas-field">
<label for="check_out">Fecha de salida</label>
<input type="date" id="check_out" name="check_out" required>
<small>D�a que dej�s libre la caba�a</small>
</div>
</div>
<div class="cabanas-field">
<label for="guests">?? Cantidad de hu�spedes</label>
<select id="guests" name="guests" required>
<option value="1">1 adulto</option>
<option value="2">2 adultos</option>
<option value="3" selected>3 adultos</option>
<option value="4">4 adultos</option>
<option value="5">5 adultos</option>
<option value="6">6 adultos</option>
</select>
<small>Mencione la cantidad de adultos</small>
</div>
<div class="cabanas-field">
<label for="minors">?? �Trae menores?</label>
<select id="minors" name="minors">
<option value="no">No</option>
<option value="1">1 menor</option>
<option value="2">2 menores</option>
<option value="3">3 menores</option>
</select>
<small>Menores de 12 a�os - 50% de descuento por menor</small>
</div>
<div class="cabanas-field">
<label for="pets">?? �Trae mascotas?</label>
<select id="pets" name="pets">
<option value="no">No</option>
<option value="yes">S�</option>
</select>
<small>Consultar pol�ticas de mascotas (opcional)</small>
</div>
<button type="submit" id="check-availability-btn" class="cabanas-submit-btn">
<span class="btn-text">🔍 Consultar Disponibilidad</span>
<span class="btn-loading" style="display:none;">⏳</span>
</button>
</form>
</div>
</div>
<!-- Todo el JS debe ir dentro del bloque <script> al final del archivo -->
<h4>??? �Tienes un C�digo de Descuento?</h4>
<div class="discount-input-row">
<input type="text" id="discount_code" name="discount_code" placeholder="Ej: PRIMERAVEZ10, VERANO20" class="discount-input">
<button type="button" id="apply-discount-btn" class="discount-apply-btn">
<span class="btn-text">Aplicar</span>
<span class="btn-loading" style="display: none;">?</span>
</button>
</div>
<div id="discount-result" class="discount-result" style="display: none;"></div>
</div>
</div>
<div id="availability-result" class="cabanas-result" style="display: none;"></div>
</div>
<script>
// Configuraci�n completa STANDALONE - Con calendarios por caba�a
const PRICING_CONFIG = {
// Paridad USD/ARS (configurable desde admin)
usd_to_ars_rate: 1300, // Valor por defecto, se puede modificar desde admin
// C�DIGOS DE DESCUENTO CONFIGURABLES DESDE ADMIN
discount_codes: {
// Descuentos por porcentaje
'PRIMERAVEZ10': {
type: 'percentage',
value: 10,
admin_reason: 'Descuento primera vez',
client_display: 'Descuento especial',
active: true,
min_nights: 1,
max_uses: null,
valid_from: null,
valid_until: null
},
'VERANO15': {
type: 'percentage',
value: 15,
admin_reason: 'Promoci�n temporada verano',
client_display: 'Descuento especial',
active: true,
min_nights: 3,
max_uses: 50,
valid_from: '2025-12-01',
valid_until: '2026-03-31'
},
'FINDESEMANA20': {
type: 'percentage',
value: 20,
admin_reason: 'Descuento fin de semana para ocupar',
client_display: 'Descuento especial',
active: true,
min_nights: 2,
max_uses: null,
valid_from: null,
valid_until: null
},
// Descuentos por monto fijo
'FIJO500': {
type: 'fixed_ars',
value: 500,
admin_reason: 'Descuento fijo cliente frecuente',
client_display: 'Descuento especial',
active: true,
min_nights: 1,
max_uses: null,
valid_from: null,
valid_until: null
},
'FIJO20USD': {
type: 'fixed_usd',
value: 20,
admin_reason: 'Descuento fijo en USD para extranjeros',
client_display: 'Descuento especial',
active: true,
min_nights: 2,
max_uses: null,
valid_from: null,
valid_until: null
},
// Descuentos especiales
'CLIENTEVIP': {
type: 'percentage',
value: 25,
admin_reason: 'Cliente VIP - descuento especial',
client_display: 'Descuento especial',
active: true,
min_nights: 1,
max_uses: null,
valid_from: null,
valid_until: null
},
'ULTIMAHORA': {
type: 'percentage',
value: 30,
admin_reason: 'Descuento �ltima hora - ocupar caba�a',
client_display: 'Descuento especial',
active: false, // Se activa manualmente
min_nights: 1,
max_uses: 5,
valid_from: null,
valid_until: null
},
// C�DIGOS ESPEC�FICOS PARA FINES DE SEMANA LARGOS
'AGOSTO2NOCHES': {
type: 'percentage',
value: 15,
admin_reason: 'Compensaci�n por 2 noches en fin de semana largo agosto (15-19/8)',
client_display: 'Descuento especial',
active: true,
min_nights: 2,
max_nights: 2, // Solo para exactly 2 noches
date_restrictions: {
valid_date_ranges: [
{ start: '2025-08-15', end: '2025-08-19' }
]
},
special_pricing: {
charge_as_minimum_nights: 3, // Cobra como si fueran 3 noches
message: 'Fin de semana largo: se cobra como m�nimo 3 noches o aplic� descuento por 2 noches'
}
},
'FINDELARGO3': {
type: 'percentage',
value: 5,
admin_reason: 'Incentivo para quedarse 3+ noches en fines de semana largos',
client_display: 'Descuento especial',
active: true,
min_nights: 3,
date_restrictions: {
long_weekends_only: true
}
},
'COMPENSACION2NOCHES': {
type: 'fixed_percentage_or_charge_as_minimum',
value: 20,
admin_reason: 'Compensaci�n por estad�a corta en per�odo de alta demanda',
client_display: 'Descuento especial',
active: true,
min_nights: 1,
max_nights: 2,
special_logic: {
strategy: 'choice',
option_1: {
type: 'percentage_discount',
value: 20,
message: 'Descuento del 20% por estad�a corta'
},
option_2: {
type: 'charge_as_minimum',
minimum_nights: 3,
message: 'Se cobra como m�nimo 3 noches'
}
}
}
},
// CALENDARIO DE FERIADOS ARGENTINOS (REAL)
// Utilizando el calendario oficial de Google para Argentina
holidays_calendar: {
name: 'Feriados Argentinos (Google Calendar)',
ical_url: 'https://calendar.google.com/calendar/ical/es.ar%23holiday%40group.v.calendar.google.com/public/basic.ics',
timezone: 'America/Argentina/Cordoba'
}, // SITUACI�N REAL EXACTA (22/7/2025):
//
// ??? CABA�A LOMAS APART (VERDE):
// - Booking.com: "closed - not available" del 21/7 al 2/8 (checkout 2/8 = libera ma�ana 2/8)
// - Noches ocupadas: 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 1/8
// - Estado: NO DISPONIBLE hasta el 2/8
//
// ?? CABA�A URITORCO (VIOLETA):
// - Silvia: 23-28/7 (checkout 28/7 = libera ma�ana 28/7)
// - Noches ocupadas por Silvia: 23, 24, 25, 26, 27/7
// - Flia Delgado: 30/7-4/8 (checkout 4/8 = libera ma�ana 4/8)
// - Noches ocupadas por Flia Delgado: 30, 31/7, 1, 2, 3/8
// - SOLO DISPONIBLE: 28/7 y 29/7 (2 noches libres entre reservas)
// - Estado: DISPONIBLE solo para estancias que no incluyan fechas ocupadas
//
// EJEMPLOS DE RESERVAS:
// ? 28/7-30/7 (2 noches: 28, 29) = DISPONIBLE
// ? 30/7-31/7 (1 noche: 30) = NO DISPONIBLE (Flia Delgado ya est�)
// ? 29/7-31/7 (2 noches: 29, 30) = NO DISPONIBLE (30/7 ocupada)
// ? 4/8-6/8 (2 noches: 4, 5) = DISPONIBLE (despu�s del checkout de Flia Delgado)
//
// RESULTADO: Solo Caba�a Uritorco disponible para fechas espec�ficas (28-29/7)
// CALENDARIOS REALES CONFIGURADOS:
// - Google Calendar: Reservas directas de cada caba�a
// - Booking.com: Reservas sincronizadas autom�ticamente desde Booking.com
// - Manual blocks: Fechas de mantenimiento o bloqueos especiales
// Configuraci�n de caba�as individuales con Google Calendar + Booking.com
cabins: {
'lomas_apart': {
name: 'Caba�a Lomas Apart',
emoji: '???',
color: '#10B981', // Verde para Lomas Apart
max_guests: 3, // M�ximo 3 personas
// Google Calendar principal de Lomas Apart
google_calendar_id: '[email protected]',
google_calendar_url: 'https://calendar.google.com/calendar/ical/mmonsdcsflpe9p1igm08klllh8%40group.calendar.google.com/public/basic.ics',
// Booking.com calendar para Lomas Apart (REAL - sincronizado autom�ticamente)
booking_calendar_id: '[email protected]', // ID real de Booking
booking_calendar_url: 'https://ical.booking.com/v1/export?t=b9307149-21c2-4e9d-a33a-47fa20f6a5b6', // URL real iCal de Booking para Lomas Apart
// Fechas bloqueadas manualmente (mantenimiento, etc.)
manual_blocks: [
// BOOKING.COM REAL: "closed - not available" del 21/7 al 2/8 (checkout 2/8)
'2025-07-21', '2025-07-22', '2025-07-23', '2025-07-24', '2025-07-25',
'2025-07-26', '2025-07-27', '2025-07-28', '2025-07-29', '2025-07-30',
'2025-07-31', '2025-08-01', // Ocupada hasta el 2/8
// Reservas futuras
'2025-08-15', '2025-08-16', '2025-08-17',
'2025-09-15', '2025-09-16',
'2025-09-25', '2025-09-26', '2025-09-27'
]
},
'uritorco': {
name: 'Caba�a Uritorco',
emoji: '??',
color: '#8B5CF6', // Violeta para Uritorco
max_guests: 3,
// Google Calendar principal de Caba�a Uritorco
google_calendar_id: '[email protected]',
google_calendar_url: 'https://calendar.google.com/calendar/ical/em4ht6t4uqlr4edra90bsrqkls%40group.calendar.google.com/public/basic.ics',
// Booking.com calendar para Uritorco (REAL - sincronizado autom�ticamente)
booking_calendar_id: '[email protected]', // ID real de Booking
booking_calendar_url: 'https://ical.booking.com/v1/export?t=1d615de0-eb38-481c-bc35-4a80a09ccd9c', // URL real iCal de Booking para Uritorco
manual_blocks: [
// SILVIA: 23-28/7 (checkout 28/7 = libera ma�ana 28/7)
'2025-07-23', '2025-07-24', '2025-07-25', '2025-07-26', '2025-07-27',
// FLIA DELGADO: 30/7-4/8 (checkout 4/8 = libera ma�ana 4/8)
// ?? CR�TICO: Estas noches est�n ocupadas
'2025-07-30', '2025-07-31', '2025-08-01', '2025-08-02', '2025-08-03',
// Reservas futuras
'2025-08-10', '2025-08-11', '2025-08-12',
'2025-08-20', '2025-08-21', '2025-08-22',
'2025-10-05', '2025-10-06',
'2025-12-28', '2025-12-29', '2025-12-30'
]
}
},
// Temporadas globales seg�n vacaciones argentinas (precios base en USD, se convierten a ARS)
seasons: [
{
name: 'Temporada Alta - Vacaciones de Verano',
end: '2025-02-28',
prices_usd: { 1: 50, 2: 60, 3: 80, 4: 90 }, // Precios base en USD
weekend_surcharge_usd: 20,
long_weekend_surcharge_usd: 30
},
{
name: 'Temporada Normal',
start: '2025-03-01',
end: '2025-06-30',
prices_usd: { 1: 35, 2: 40, 3: 55, 4: 65 },
weekend_surcharge_usd: 10,
long_weekend_surcharge_usd: 15
},
{
name: 'Temporada Alta - Vacaciones de Invierno',
start: '2025-07-01',
end: '2025-07-31',
prices_usd: { 1: 45, 2: 55, 3: 75, 4: 85 },
weekend_surcharge_usd: 15,
long_weekend_surcharge_usd: 25
},
{
name: 'Temporada Normal',
start: '2025-08-01',
end: '2025-12-31',
prices_usd: { 1: 35, 2: 40, 3: 55, 4: 65 },
weekend_surcharge_usd: 10,
long_weekend_surcharge_usd: 15
}
],
// Feriados argentinos (FALLBACK - se actualizan autom�ticamente desde Google Calendar)
holidays: [
'2025-07-09', // D�a de la Independencia (ya pas�)
'2025-08-17', // Paso a la Inmortalidad del General San Mart�n (domingo)
'2025-08-18', // Feriado puente (lunes)
'2025-10-12', // D�a del Respeto a la Diversidad Cultural
'2025-10-13', // Feriado puente (lunes)
'2025-11-20', // D�a de la Soberan�a Nacional
'2025-12-08', // Inmaculada Concepci�n de Mar�a
'2025-12-25', // Navidad
'2026-01-01', // A�o Nuevo
'2026-02-24', '2026-02-25' // Carnaval
],
// Cache para feriados obtenidos del calendario real
holidays_cache: {
data: [],
last_updated: null,
expiry_hours: 24 // Cache por 24 horas
},
// Sistema de c�digos de descuento
discount_codes: {
// C�digos porcentuales
'PRIMERAVEZ10': {
type: 'percentage',
value: 10,
description: 'Primera vez - 10% de descuento',
min_nights: 2,
max_uses: 100,
used_count: 0,
valid_until: '2025-12-31',
active: true
},
'VERANO20': {
type: 'percentage',
value: 20,
description: 'Promoci�n verano - 20% de descuento',
min_nights: 3,
max_uses: 50,
used_count: 0,
valid_until: '2025-02-28',
active: true
},
'INVIERNO15': {
type: 'percentage',
value: 15,
description: 'Promoci�n invierno - 15% de descuento',
min_nights: 2,
max_uses: 75,
used_count: 0,
valid_until: '2025-07-31',
active: true
},
// C�digos de descuento fijo
'DESCUENTO5000': {
type: 'fixed_ars',
value: 5000,
description: 'Descuento fijo $5.000 ARS',
min_nights: 2,
max_uses: 25,
used_count: 0,
valid_until: '2025-12-31',
active: true
},
'ULTIMOMOMENTO': {
type: 'percentage',
value: 25,
description: '�ltimo momento - 25% de descuento',
min_nights: 1,
max_uses: 20,
used_count: 0,
valid_until: '2025-12-31',
active: true,
conditions: {
advance_booking_max_days: 7 // Solo v�lido si se reserva con menos de 7 d�as de anticipaci�n
}
}
}
};
// Funciones para manejo de monedas
function convertUSDtoARS(usdAmount) {
return Math.round(usdAmount * PRICING_CONFIG.usd_to_ars_rate);
}
function formatPrice(usdAmount) {
const arsAmount = convertUSDtoARS(usdAmount);
return {
usd: usdAmount,
ars: arsAmount,
formatted: `$${arsAmount.toLocaleString('es-AR')} ARS`
};
}
// Funci�n para obtener fechas bloqueadas desde Google Calendar + Booking.com por caba�a (INTEGRACI�N REAL)
async function fetchCabinBlockedDates(cabinId) {
const cabin = PRICING_CONFIG.cabins[cabinId];
if (!cabin) return [];
try {
const blockedDates = [...cabin.manual_blocks]; // Empezar con fechas manuales
// Consultar Google Calendar principal (REAL)
if (cabin.google_calendar_url) {
const googleDates = await fetchRealCalendarData(cabin.google_calendar_url, cabin.name, 'Google Calendar');
blockedDates.push(...googleDates);
}
// Consultar Booking.com Calendar (REAL - iCal desde Booking.com)
if (cabin.booking_calendar_url) {
const bookingDates = await fetchRealCalendarData(cabin.booking_calendar_url, cabin.name, 'Booking.com');
blockedDates.push(...bookingDates);
}
// Remover duplicados
const uniqueBlockedDates = [...new Set(blockedDates)];
return uniqueBlockedDates;
} catch (error) {
return cabin.manual_blocks; // Fallback a fechas manuales
}
}
// INTEGRACI�N REAL CON CALENDARIO DE FERIADOS ARGENTINOS (LIMITADA A 6 MESES)
async function fetchArgentineHolidays(startDate = null, endDate = null) {
try {
// LIMITAR B�SQUEDA A 6 MESES M�XIMO
const now = new Date();
const maxDate = new Date(now);
maxDate.setMonth(now.getMonth() + 6);
// Si no se especifican fechas, usar desde hoy hasta 6 meses
if (!startDate) {
startDate = now.toISOString().split('T')[0];
}
if (!endDate) {
endDate = maxDate.toISOString().split('T')[0];
}
// Forzar l�mite de 6 meses m�ximo
const requestedEndDate = new Date(endDate);
if (requestedEndDate > maxDate) {
endDate = maxDate.toISOString().split('T')[0];
}
// Verificar cache primero
const cache = PRICING_CONFIG.holidays_cache;
if (cache.data.length > 0 && cache.last_updated) {
const hoursAgo = (now - new Date(cache.last_updated)) / (1000 60 60);
if (hoursAgo < cache.expiry_hours && cache.period) {
// Verificar si el cache cubre el per�odo solicitado
if (cache.period.startDate <= startDate && cache.period.endDate >= endDate) {
const relevantHolidays = cache.data.filter(holiday =>
holiday >= startDate && holiday <= endDate
);
return relevantHolidays;
}
}
}
// Usar fallback hardcoded en lugar de consultar APIs externas
// Filtrar feriados hardcoded para el per�odo limitado
const relevantHolidays = PRICING_CONFIG.holidays.filter(holiday => {
return holiday >= startDate && holiday <= endDate;
});
// Actualizar cache con per�odo limitado
cache.data = relevantHolidays;
cache.last_updated = now.toISOString();
cache.period = { startDate, endDate };
return relevantHolidays;
} catch (error) {
// Aplicar l�mite de 6 meses al fallback tambi�n
const now = new Date();
const maxDate = new Date(now);
maxDate.setMonth(now.getMonth() + 6);
const maxDateStr = maxDate.toISOString().split('T')[0];
const limitedFallback = PRICING_CONFIG.holidays.filter(holiday =>
holiday >= (startDate || now.toISOString().split('T')[0]) &&
holiday <= (endDate || maxDateStr)
);
return limitedFallback;
}
}
// INTEGRACI�N REAL CON CALENDARIOS - NO SIMULACI�N (CON TIMEOUT)
// Esta funci�n hace fetch REAL a los calendarios iCal
async function fetchRealCalendarData(icalUrl, cabinName, calendarType = 'Google Calendar') {
try {
// CORS Proxy para poder acceder a los calendarios desde el navegador
// En producci�n, esto deber�a manejarse desde el backend
const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(icalUrl)}`;
// Agregar timeout de 5 segundos para evitar demoras
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 segundos
const response = await fetch(proxyUrl, {
signal: controller.signal
});
clearTimeout(timeoutId); // Limpiar timeout si la respuesta llega a tiempo
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const icalData = await response.text();
// Parser b�sico de iCal para extraer fechas bloqueadas
const blockedDates = parseICalData(icalData);
return blockedDates;
} catch (error) {
// FALLBACK: Si falla la consulta real, usar datos simulados como backup
return await simulateGoogleCalendarFetch(icalUrl, cabinName, calendarType);
}
}
// Parser de datos iCal para extraer fechas de eventos/reservas
function parseICalData(icalData) {
const blockedDates = [];
const lines = icalData.split('\n');
let currentEvent = {};
let inEvent = false;
for (let line of lines) {
line = line.trim();
if (line === 'BEGIN:VEVENT') {
inEvent = true;
currentEvent = {};
} else if (line === 'END:VEVENT' && inEvent) {
// Procesar evento completado
if (currentEvent.dtstart && currentEvent.dtend) {
const startDate = parseICalDate(currentEvent.dtstart);
const endDate = parseICalDate(currentEvent.dtend);
if (startDate && endDate) {
// Generar todas las fechas entre start y end (excluyendo end como checkout)
const dateRange = getDateRange(startDate, endDate);
blockedDates.push(...dateRange);
}
}
inEvent = false;
} else if (inEvent) {
// Extraer propiedades del evento
if (line.startsWith('DTSTART')) {
currentEvent.dtstart = line.split(':')[1];
} else if (line.startsWith('DTEND')) {
currentEvent.dtend = line.split(':')[1];
} else if (line.startsWith('SUMMARY')) {
currentEvent.summary = line.split(':')[1];
}
}
}
// Remover duplicados y ordenar
return [...new Set(blockedDates)].sort();
}
// Parser de fechas iCal (maneja diferentes formatos)
function parseICalDate(icalDate) {
if (!icalDate) return null;
// Formato t�pico: 20250721T000000Z o 20250721
let dateStr = icalDate.replace(/[TZ]/g, '').substring(0, 8);
if (dateStr.length === 8) {
// Convertir YYYYMMDD a YYYY-MM-DD
const year = dateStr.substring(0, 4);
const month = dateStr.substring(4, 6);
const day = dateStr.substring(6, 8);
return `${year}-${month}-${day}`;
}
return null;
}
async function simulateGoogleCalendarFetch(icalUrl, cabinName, calendarType = 'Google Calendar') {
return new Promise((resolve) => {
setTimeout(() => {
// SITUACI�N REAL ACTUAL (22/7/2025):
// - Lomas Apart: ESTAD�A EN CURSO (verde) - bloqueada actualmente
// - Uritorco: DISPONIBLE (violeta) hasta el 30/7 cuando entra Flia Delgado
if (cabinName.includes('Lomas Apart')) {
if (calendarType === 'Google Calendar') {
// LOMAS APART: Reservas directas de Google Calendar
resolve([
// Reservas futuras de Google Calendar (despu�s de la estad�a de Booking.com)
'2025-08-15', '2025-08-16', '2025-08-17', // Reserva directa agosto
'2025-09-15', '2025-09-16', // Septiembre
'2025-12-20', '2025-12-21', '2025-12-22' // Diciembre
]);
} else if (calendarType === 'Booking.com') {
// BOOKING.COM REAL: "closed - not available" del 21/7 al 2/8 (checkout 2/8 = libera ma�ana 2/8)
// Las noches ocupadas son: 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 1 (hasta checkout 2/8)
resolve([
'2025-07-21', '2025-07-22', '2025-07-23', '2025-07-24', '2025-07-25',
'2025-07-26', '2025-07-27', '2025-07-28', '2025-07-29', '2025-07-30',
'2025-07-31', '2025-08-01', // Booking.com: 21/7 al 2/8 (checkout 2/8)
// Otras reservas de Booking.com futuras
'2025-09-25', '2025-09-26', '2025-09-27', // Septiembre Booking
'2025-11-10', '2025-11-11' // Noviembre Booking
]);
}
} else if (cabinName.includes('Uritorco')) {
if (calendarType === 'Google Calendar') {
// CABA�A URITORCO: Reservas directas del Google Calendar
resolve([
// SILVIA: 23-28/7 (checkout 28/7 = libera ma�ana 28/7)
// Noches ocupadas: 23, 24, 25, 26, 27
'2025-07-23', '2025-07-24', '2025-07-25', '2025-07-26', '2025-07-27', // Silvia: 23-28/7
// FLIA DELGADO: 30/7-4/8 (checkout 4/8 = libera ma�ana 4/8)
// ?? CR�TICO: Noches ocupadas son 30, 31/7 y 1, 2, 3/8
// CUALQUIER reserva que incluya estas noches ser� RECHAZADA
'2025-07-30', '2025-07-31', '2025-08-01', '2025-08-02', '2025-08-03', // Flia Delgado: 30/7-4/8
// RESULTADO REAL: Solo 28/7 y 29/7 est�n libres entre reservas
// Una reserva 30/7-31/7 ser� RECHAZADA porque 30/7 est� ocupado por Flia Delgado
// Reservas futuras
'2025-08-10', '2025-08-11', '2025-08-12', // Agosto adicional
'2025-10-05', '2025-10-06', // Octubre
'2025-12-28', '2025-12-29', '2025-12-30' // Diciembre
]);
} else if (calendarType === 'Booking.com') {
// Reservas desde Booking.com sincronizadas para Uritorco
resolve([
'2025-08-20', '2025-08-21', '2025-08-22', // Reserva Booking agosto
'2025-09-05', '2025-09-06', '2025-09-07', // Septiembre Booking
'2025-10-15', '2025-10-16', '2025-10-17', // Octubre Booking
'2025-11-20', '2025-11-21' // Noviembre Booking
]);
}
}
resolve([]);
}, Math.random() * 500 + 200); // Simular latencia del calendario
});
}
// Funci�n para sugerir fechas alternativas cuando no hay disponibilidad
async function suggestAlternativeDates(originalCheckIn, originalCheckOut, totalGuests) {
const suggestions = [];
const nights = Math.ceil((new Date(originalCheckOut) - new Date(originalCheckIn)) / (1000 60 60 * 24));
// Buscar en los pr�ximos 60 d�as
const startSearch = new Date();
const endSearch = new Date();
endSearch.setDate(startSearch.getDate() + 60);
let currentDate = new Date(startSearch);
while (currentDate <= endSearch && suggestions.length < 3) {
const checkInStr = currentDate.toISOString().split('T')[0];
const checkOutDate = new Date(currentDate);
checkOutDate.setDate(currentDate.getDate() + nights);
const checkOutStr = checkOutDate.toISOString().split('T')[0];
// No sugerir la misma fecha que ya prob�
if (checkInStr !== originalCheckIn) {
const availability = await checkAllCabinsAvailability(checkInStr, checkOutStr, totalGuests);
if (availability.availableCabins.length > 0) {
const pricing = await calculatePricing(checkInStr, checkOutStr, totalGuests);
let accommodationType = '';
if (availability.isMultipleCabins) {
accommodationType = ` (${availability.availableCabins.length} caba�as)`;
}
suggestions.push({
checkIn: checkInStr,
checkOut: checkOutStr,
availableCabins: availability.availableCabins.length,
totalARS: pricing.total_ars,
season: pricing.season,
accommodationType: accommodationType
});
}
}
// Avanzar al siguiente d�a
currentDate.setDate(currentDate.getDate() + 1);
}
return suggestions;
}
// Funci�n para verificar disponibilidad de todas las caba�as
async function checkAllCabinsAvailability(checkIn, checkOut, totalGuests) {
const availableCabins = [];
const unavailableReasons = [];
const cabinIds = Object.keys(PRICING_CONFIG.cabins);
// Primero verificar caba�as que pueden alojar todo el grupo
for (const cabinId of cabinIds) {
const cabin = PRICING_CONFIG.cabins[cabinId];
// Obtener fechas bloqueadas para esta caba�a espec�fica
const blockedDates = await fetchCabinBlockedDates(cabinId);
// Verificar si hay conflicto con las fechas solicitadas
const dateRange = getDateRange(checkIn, checkOut);
const conflictingDates = dateRange.filter(date => blockedDates.includes(date));
const hasConflict = conflictingDates.length > 0;
if (hasConflict) {
unavailableReasons.push(`${cabin.emoji} ${cabin.name}: Ocupada en ${conflictingDates.map(d => formatDateSpanish(d)).join(', ')}`);
} else {
// Verificar capacidad
if (totalGuests <= cabin.max_guests) {
availableCabins.push({
id: cabinId,
name: cabin.name,
emoji: cabin.emoji,
max_guests: cabin.max_guests,
blocked_dates: blockedDates,
accommodation_type: 'single_cabin',
guests_capacity: totalGuests
});
} else {
}
}
}
// Si no hay caba�as que puedan alojar todo el grupo, verificar combinaci�n de caba�as
if (availableCabins.length === 0 && totalGuests > 3) {
// Obtener todas las caba�as disponibles (sin restricci�n de capacidad)
const allAvailableCabins = [];
for (const cabinId of cabinIds) {
const cabin = PRICING_CONFIG.cabins[cabinId];
const blockedDates = await fetchCabinBlockedDates(cabinId);
const dateRange = getDateRange(checkIn, checkOut);
const conflictingDates = dateRange.filter(date => blockedDates.includes(date));
if (conflictingDates.length === 0) {
allAvailableCabins.push({
id: cabinId,
name: cabin.name,
emoji: cabin.emoji,
max_guests: cabin.max_guests,
blocked_dates: blockedDates
});
}
}
// Si hay al menos 2 caba�as disponibles, ofrecer alojamiento separado
if (allAvailableCabins.length >= 2) {
const totalCapacity = allAvailableCabins.reduce((sum, cabin) => sum + cabin.max_guests, 0);
if (totalGuests <= totalCapacity) {
// Retornar todas las caba�as disponibles con indicaci�n de alojamiento separado
return {
availableCabins: allAvailableCabins.map(cabin => ({
...cabin,
accommodation_type: 'multiple_cabins',
guests_capacity: totalGuests,
total_cabins_needed: allAvailableCabins.length
})),
unavailableReasons: [],
isMultipleCabins: true,
totalCapacity: totalCapacity
};
}
}
}
return {
availableCabins,
unavailableReasons,
isMultipleCabins: false
};
}
function findSeason(dateStr) {
const date = new Date(dateStr);
for (const season of PRICING_CONFIG.seasons) {
const startDate = new Date(season.start);
const endDate = new Date(season.end);
if (date >= startDate && date <= endDate) {
return season;
}
}
// Si no encuentra temporada, usar temporada normal como fallback
return PRICING_CONFIG.seasons[1];
}
// Funci�n para detectar d�as "sandwich" entre reservas (DESACTIVADA)
// Esta funci�n se usar� en el futuro cuando tengamos datos reales de reservas anteriores
function detectSandwichDates(blockedDates) {
// DESACTIVADO: No aplicar descuentos sandwich hasta tener datos reales
return [];
/*
const sandwichDates = [];
// Ordenar fechas bloqueadas
const sortedBlockedDates = blockedDates.sort();
// Buscar huecos de 1-2 d�as entre reservas
for (let i = 0; i < sortedBlockedDates.length - 1; i++) {
const currentDate = new Date(sortedBlockedDates[i]);
const nextDate = new Date(sortedBlockedDates[i + 1]);
// Calcular d�as entre esta fecha y la siguiente
const daysBetween = Math.ceil((nextDate - currentDate) / (1000 60 60 * 24)) - 1;
// Si hay 1 o 2 d�as libres entre reservas, son d�as sandwich
if (daysBetween > 0 && daysBetween <= 2) {
const sandwichStart = new Date(currentDate);
sandwichStart.setDate(currentDate.getDate() + 1);
for (let j = 0; j < daysBetween; j++) {
const sandwichDay = new Date(sandwichStart);
sandwichDay.setDate(sandwichStart.getDate() + j);
const sandwichDateStr = sandwichDay.toISOString().split('T')[0];
sandwichDates.push(sandwichDateStr);
}
}
}
return sandwichDates;
*/
}
// Funci�n para verificar si una fecha est� en per�odo sandwich (DESACTIVADA)
function isSandwichDate(dateStr, cabinId, allBlockedDates) {
// DESACTIVADO: No aplicar descuentos sandwich hasta tener datos reales
return false;
// const sandwichDates = detectSandwichDates(allBlockedDates);
// return sandwichDates.includes(dateStr);
}
// Funci�n para verificar si es fin de semana largo (usa feriados reales de Google Calendar - OPTIMIZADA)
async function isLongWeekend(date, realHolidays = null) {
const dateStr = date.toISOString().split('T')[0];
const dayOfWeek = date.getDay(); // 0 = Domingo, 6 = S�bado
// Obtener feriados reales si no se proporcionaron (solo del mes relevante)
let holidays = realHolidays;
if (!holidays) {
// Optimizar: solo buscar feriados del mes de la fecha consultada
const monthStart = new Date(date.getFullYear(), date.getMonth(), 1).toISOString().split('T')[0];
const monthEnd = new Date(date.getFullYear(), date.getMonth() + 1, 0).toISOString().split('T')[0];
holidays = await fetchArgentineHolidays(monthStart, monthEnd);
}
// Si es feriado oficial, aplicar tarifa de fin de semana largo
if (holidays.includes(dateStr)) {
return true;
}
// Verificar feriados puente (lunes despu�s de domingo feriado, viernes antes de domingo feriado)
if (dayOfWeek === 1) { // Lunes
const sunday = new Date(date);
sunday.setDate(date.getDate() - 1);
if (holidays.includes(sunday.toISOString().split('T')[0])) {
return true;
}
}
if (dayOfWeek === 5) { // Viernes
const sunday = new Date(date);
sunday.setDate(date.getDate() + 2);
if (holidays.includes(sunday.toISOString().split('T')[0])) {
return true;
}
}
return false;
}
// Funci�n para generar rango de fechas (CORREGIDO para evitar overbooking)
function getDateRange(startDate, endDate) {
const dates = [];
const currentDate = new Date(startDate);
const lastDate = new Date(endDate);
// IMPORTANTE: El checkout NO debe estar incluido en las fechas ocupadas
// Si checkout es 30/7, significa que liberan la caba�a el 30/7 por la ma�ana
// Por tanto, las fechas ocupadas son solo hasta el 29/7 (la �ltima noche)
while (currentDate < lastDate) {
dates.push(currentDate.toISOString().split('T')[0]);
currentDate.setDate(currentDate.getDate() + 1);
}
return dates;
}
// Funci�n para formatear fecha en espa�ol
function formatDateSpanish(dateStr) {
const date = new Date(dateStr + 'T12:00:00'); // Agregar hora para evitar problemas de zona horaria
const options = {
weekday: 'short',
day: 'numeric',
month: 'short'
};
return date.toLocaleDateString('es-AR', options);
}
// Funci�n para generar URL de MercadoPago con datos de la reserva
function generateMercadoPagoURL(data) {
// URL base de MercadoPago
const baseURL = 'https://link.mercadopago.com.ar/lomasdeluritorco';
// Preparar datos de la reserva para n8n (se pasar�n como par�metros)
const reservationData = {
// Datos de la reserva
checkin: data.check_in,
checkout: data.check_out,
totalGuests: data.totalGuests,
adults: data.adults,
minorsCount: data.minorsCount,
minors: data.minors || 'no',
pets: data.pets || 'no',
isMultipleCabins: data.isMultipleCabins || false,
nights: data.nights,
// Precios
total_ars: data.total_price_ars,
total_usd: data.total_price_usd,
anticipo_ars: data.advance_payment_ars,
anticipo_usd: data.advance_payment_usd,
// Detalles
season: data.season,
cabins: data.available_cabins.map(c => c.name).join(data.isMultipleCabins ? ' + ' : ' o '),
// Timestamp
timestamp: Date.now()
};
// Construir URL con par�metros para n8n (opcional, para tracking)
const urlParams = new URLSearchParams({
// Par�metros b�sicos para referencia
amount: data.advance_payment_ars,
reference: `RES-${reservationData.timestamp}`,
checkin: data.check_in,
checkout: data.check_out,
totalGuests: data.totalGuests,
adults: data.adults,
minors: data.minors || 'no',
pets: data.pets || 'no',
isMultipleCabins: data.isMultipleCabins || false
});
// En una integraci�n completa con n8n, podr�as usar:
// return `${baseURL}?${urlParams.toString()}`;
// Por ahora, URL directa (n8n capturar� los datos despu�s del pago)
return baseURL;
}
// Event listener principal
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('cabanas-booking-form');
const checkInInput = document.getElementById('check_in');
const checkOutInput = document.getElementById('check_out');
const submitBtn = document.getElementById('check-availability-btn');
const btnText = submitBtn.querySelector('.btn-text');
const btnLoading = submitBtn.querySelector('.btn-loading');
const resultDiv = document.getElementById('availability-result');
// ===== FUNCIONES GLOBALES PARA COMPARTIR Y GENERAR IMAGEN =====
function shareQuoteOnWhatsApp() {
if (!currentReservationData) {
alert('Primero debes verificar disponibilidad para compartir la cotizaci�n');
return;
}
const data = currentReservationData;
// Formatear fechas
const checkInFormatted = formatDate(data.check_in);
const checkOutFormatted = formatDate(data.check_out);
const nights = calculateNights(data.check_in, data.check_out);
// Informaci�n de hu�spedes
const guestInfo = `${data.adults} adulto${data.adults > 1 ? 's' : ''}` +
(data.minors > 0 ? ` + ${data.minors} menor${data.minors > 1 ? 'es' : ''}` : '');
const totalGuestText = `${data.totalGuests} persona${data.totalGuests > 1 ? 's' : ''} total`;
// Informaci�n de caba�as
const cabinsList = data.available_cabins.map(cabin =>
`${cabin.emoji} ${cabin.name}`
).join(' + ');
let accommodationText = '';
if (data.isMultipleCabins) {
accommodationText = `Se alojar�n en ${data.available_cabins.length} caba�as separadas: ${cabinsList}`;
} else {
accommodationText = `Caba�as disponibles: ${cabinsList}`;
}
// Informaci�n de descuento (si aplica)
let discountInfo = '';
if (data.discount_applied && data.discount_applied.amount > 0) {
discountInfo = `\n?? Descuento especial aplicado: -$${data.discount_applied.amount.toLocaleString('es-AR')} ARS`;
}
// Construir mensaje
const message = `??? COTIZACI�N CABA�AS LOMAS DEL URITORCO
?? Check-in: ${checkInFormatted}
?? Check-out: ${checkOutFormatted}
?? Noches: ${nights}
?? Hu�spedes: ${guestInfo} (${totalGuestText})
?? Alojamiento:
${accommodationText}${discountInfo}
?? TOTAL: $${data.total_price_ars.toLocaleString('es-AR')} ARS
?? Se�a (30%): $${data.advance_payment_ars.toLocaleString('es-AR')} ARS
?? �Confirmamos la reserva? �Esperamos tu respuesta!`;
// URL de WhatsApp
const whatsappURL = `https://wa.me/5493548479302?text=${encodeURIComponent(message)}`;
// Abrir WhatsApp
window.open(whatsappURL, '_blank');
}
function generateQuoteImage() {
if (!currentReservationData) {
alert('Primero debes verificar disponibilidad para generar la imagen');
return;
}
const data = currentReservationData;
// Crear canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Configurar tama�o del canvas (formato �ptimo para WhatsApp)
canvas.width = 800;
canvas.height = 1000;
// Fondo degradado
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#1e3a8a');
gradient.addColorStop(0.5, '#3730a3');
gradient.addColorStop(1, '#581c87');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Configuraci�n de texto
ctx.textAlign = 'center';
ctx.fillStyle = '#ffffff';
// T�tulo principal
ctx.font = 'bold 36px Arial';
ctx.fillText('CABA�AS LOMAS', canvas.width / 2, 80);
ctx.fillText('DEL URITORCO', canvas.width / 2, 120);
// Subt�tulo
ctx.font = '20px Arial';
ctx.fillStyle = '#fbbf24';
ctx.fillText('COTIZACI�N PERSONALIZADA', canvas.width / 2, 160);
// L�nea decorativa
ctx.strokeStyle = '#fbbf24';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(canvas.width * 0.2, 180);
ctx.lineTo(canvas.width * 0.8, 180);
ctx.stroke();
// Fechas y noches
ctx.font = 'bold 24px Arial';
ctx.fillStyle = '#ffffff';
ctx.fillText(`?? ${formatDate(data.check_in)} ? ${formatDate(data.check_out)}`, canvas.width / 2, 240);
ctx.fillText(`?? ${data.nights} noche${data.nights > 1 ? 's' : ''}`, canvas.width / 2, 280);
// Hu�spedes
const guestInfo = `?? ${data.adults} adulto${data.adults > 1 ? 's' : ''}` +
(data.minors > 0 ? ` + ${data.minors} menor${data.minors > 1 ? 'es' : ''}` : '');
ctx.fillText(guestInfo, canvas.width / 2, 320);
// Caba�as disponibles
ctx.font = '18px Arial';
ctx.fillStyle = '#d1d5db';
ctx.fillText('?? CABA�AS DISPONIBLES:', canvas.width / 2, 380);
let yPos = 420;
data.available_cabins.forEach(cabin => {
ctx.font = 'bold 20px Arial';
ctx.fillStyle = '#fbbf24';
ctx.fillText(`${cabin.emoji} ${cabin.name}`, canvas.width / 2, yPos);
yPos += 40;
});
// Informaci�n de descuento (si aplica)
if (data.discount_applied && data.discount_applied.amount > 0) {
ctx.font = 'bold 22px Arial';
ctx.fillStyle = '#10b981';
ctx.fillText(`?? Descuento: -$${data.discount_applied.amount.toLocaleString('es-AR')} ARS`, canvas.width / 2, yPos + 40);
yPos += 60;
}
// Precios
ctx.font = 'bold 32px Arial';
ctx.fillStyle = '#fbbf24';
ctx.fillText('?? TOTAL', canvas.width / 2, yPos + 80);
ctx.font = 'bold 40px Arial';
ctx.fillStyle = '#ffffff';
ctx.fillText(`$${data.total_price_ars.toLocaleString('es-AR')} ARS`, canvas.width / 2, yPos + 130);
ctx.font = '24px Arial';
ctx.fillStyle = '#d1d5db';
ctx.fillText(`(USD $${data.total_price_usd.toLocaleString('en-US')})`, canvas.width / 2, yPos + 170);
// Se�a
ctx.font = 'bold 28px Arial';
ctx.fillStyle = '#ef4444';
ctx.fillText('?? SE�A (30%)', canvas.width / 2, yPos + 230);
ctx.font = 'bold 32px Arial';
ctx.fillStyle = '#ffffff';
ctx.fillText(`$${data.advance_payment_ars.toLocaleString('es-AR')} ARS`, canvas.width / 2, yPos + 270);
// Contacto
ctx.font = '18px Arial';
ctx.fillStyle = '#fbbf24';
ctx.fillText('?? WhatsApp: +54 9 3548 479302', canvas.width / 2, canvas.height - 60);
ctx.fillText('?? Capilla del Monte, C�rdoba', canvas.width / 2, canvas.height - 30);
// Convertir a imagen y mostrar
canvas.toBlob(function(blob) {
const url = URL.createObjectURL(blob);
// Crear modal para mostrar la imagen
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.9); display: flex; flex-direction: column;
justify-content: center; align-items: center; z-index: 10000;
`;
const img = document.createElement('img');
img.src = url;
img.style.cssText = 'max-width: 90%; max-height: 80%; border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.5);';
const closeBtn = document.createElement('button');
closeBtn.textContent = 'Cerrar Vista Previa';
closeBtn.style.cssText = `
margin-top: 20px; padding: 12px 24px; background: #ef4444;
color: white; border: none; border-radius: 8px; cursor: pointer;
font-size: 16px; font-weight: 600;
`;
closeBtn.onclick = () => document.body.removeChild(modal);
modal.appendChild(img);
modal.appendChild(closeBtn);
document.body.appendChild(modal);
});
}
// ? VARIABLE GLOBAL PARA ALMACENAR DATOS DE RESERVA
let currentReservationData = null;
// Establecer fecha m�nima como hoy
const today = new Date().toISOString().split('T')[0];
checkInInput.min = today;
// Actualizar fecha m�nima de check-out cuando cambia check-in
checkInInput.addEventListener('change', function() {
const checkInDate = new Date(this.value);
const nextDay = new Date(checkInDate);
nextDay.setDate(checkInDate.getDate() + 1);
checkOutInput.min = nextDay.toISOString().split('T')[0];
if (checkOutInput.value && checkOutInput.value <= this.value) {
checkOutInput.value = '';
}
});
form.addEventListener('submit', async function(e) {
e.preventDefault();
const checkIn = checkInInput.value;
const checkOut = checkOutInput.value;
const adults = parseInt(document.getElementById('guests').value);
const minorsCount = document.getElementById('minors').value === 'no' ? 0 : parseInt(document.getElementById('minors').value);
const totalGuests = adults + minorsCount; // Total de personas (adultos + menores)
const minors = document.getElementById('minors').value;
const pets = document.getElementById('pets').value;
// Debug para verificar fechas capturadas
// Validaciones
if (!checkIn || !checkOut) {
showError('Por favor selecciona las fechas de llegada y salida');
return;
}
if (checkIn >= checkOut) {
showError('La fecha de salida debe ser posterior a la de llegada');
return;
}
if (checkIn < today) {
showError('No puedes seleccionar fechas pasadas');
return;
}
// Mostrar loading
submitBtn.disabled = true;
btnText.style.display = 'none';
btnLoading.style.display = 'inline';
resultDiv.style.display = 'none';
try {
// Verificar disponibilidad (ahora incluye calendarios externos)
const result = await checkAvailability(checkIn, checkOut, totalGuests, adults, minorsCount, minors, pets);
// Ocultar loading
submitBtn.disabled = false;
btnText.style.display = 'inline';
btnLoading.style.display = 'none';
resultDiv.style.display = 'block';
if (result.available) {
resultDiv.innerHTML = generateSuccessHTML(result);
resultDiv.className = 'cabanas-result cabanas-success';
// ? GUARDAR DATOS PARA WHATSAPP Y DESCUENTOS
currentReservationData = result;
// Mostrar secci�n de descuentos
document.getElementById('discount-section').style.display = 'block';
} else {
resultDiv.innerHTML = generateErrorHTML(result.message, result.suggestions, result.unavailableReasons);
resultDiv.className = 'cabanas-result cabanas-error';
// Ocultar secci�n de descuentos
document.getElementById('discount-section').style.display = 'none';
}
} catch (error) {
// Ocultar loading
submitBtn.disabled = false;
btnText.style.display = 'inline';
btnLoading.style.display = 'none';
showError('Error al verificar disponibilidad. Por favor intenta nuevamente.');
}
});
async function checkAvailability(checkIn, checkOut, totalGuests, adults, minorsCount, minors, pets) {
// Verificar disponibilidad de todas las caba�as
const availabilityResult = await checkAllCabinsAvailability(checkIn, checkOut, totalGuests);
// Debug
if (availabilityResult.availableCabins.length === 0) {
// Buscar fechas alternativas
const suggestions = await suggestAlternativeDates(checkIn, checkOut, totalGuests);
let message = `Lo sentimos, no hay caba�as disponibles para ${totalGuests} persona${totalGuests > 1 ? 's' : ''} del ${formatDateSpanish(checkIn)} al ${formatDateSpanish(checkOut)}.`;
if (availabilityResult.unavailableReasons.length > 0) {
message += `\n\nMotivos:\n${availabilityResult.unavailableReasons.join('\n')}`;
}
return {
available: false,
message: message,
suggestions: suggestions,
unavailableReasons: availabilityResult.unavailableReasons
};
}
// Calcular pricing (usar la primera caba�a disponible para el c�lculo)
const pricing = await calculatePricing(checkIn, checkOut, totalGuests, adults, minorsCount);
// Generar mensaje apropiado seg�n el tipo de alojamiento
let message;
if (availabilityResult.isMultipleCabins) {
message = `�Excelente! Para ${totalGuests} personas, tenemos ${availabilityResult.availableCabins.length} caba�as disponibles. Se alojar�n separados para mayor comodidad.`;
} else {
message = `�Excelente! Tenemos ${availabilityResult.availableCabins.length} caba�a${availabilityResult.availableCabins.length > 1 ? 's' : ''} disponible${availabilityResult.availableCabins.length > 1 ? 's' : ''} para tu estad�a de ${totalGuests} persona${totalGuests > 1 ? 's' : ''}.`;
}
return {
available: true,
message: message,
nights: pricing.nights,
total_price_usd: pricing.total_usd,
total_price_ars: pricing.total_ars,
advance_payment_usd: pricing.advance_payment_usd,
advance_payment_ars: pricing.advance_payment_ars,
price_breakdown: pricing.breakdown,
season: pricing.season,
available_cabins: availabilityResult.availableCabins,
check_in: checkIn,
check_out: checkOut,
totalGuests: totalGuests,
adults: adults,
minorsCount: minorsCount,
minors: minors,
pets: pets,
isMultipleCabins: availabilityResult.isMultipleCabins || false
};
}
// ===== SISTEMA DE ESTANCIAS M�NIMAS DIN�MICAS =====
function checkMinimumStayRequirements(checkIn, checkOut, totalGuests, adults, minors) {
const startDate = new Date(checkIn);
const endDate = new Date(checkOut);
const nights = Math.ceil((endDate - startDate) / (1000 60 60 * 24));
// Verificar si est� en per�odo de estancia m�nima
const minimumStayInfo = getMinimumStayForPeriod(checkIn, checkOut);
if (!minimumStayInfo.hasRequirement) {
return {
isValid: true,
actualNights: nights,
chargeNights: nights,
penalty: null,
suggestions: []
};
}
if (nights >= minimumStayInfo.minimumNights) {
return {
isValid: true,
actualNights: nights,
chargeNights: nights,
penalty: null,
suggestions: []
};
}
// Aplicar penalizaci�n
const penalty = calculateMinimumStayPenalty(minimumStayInfo, nights, checkIn, checkOut);
// Generar sugerencias
const suggestions = generateMinimumStaySuggestions(checkIn, checkOut, minimumStayInfo.minimumNights);
// Estancia corta detectada
// Estancia corta detectada
// Si necesitas log, usa console.log o return
// Si esto es parte de un objeto, debe ir dentro de return {...}
// Ejemplo:
// return {
// required: minimumStayInfo.minimumNights,
// requested: nights,
// penalty: penalty,
// suggestions: suggestions.length
// };
return {
isValid: false,
actualNights: nights,
chargeNights: penalty.chargeAsNights || nights,
minimumRequired: minimumStayInfo.minimumNights,
penalty: penalty,
reason: minimumStayInfo.reason,
suggestions: suggestions
};
}
function getMinimumStayForPeriod(checkIn, checkOut) {
const startDate = new Date(checkIn);
const endDate = new Date(checkOut);
// Verificar fines de semana largos
const longWeekendPeriods = [
{start: '2025-05-01', end: '2025-05-04', name: 'D�a del Trabajador (4 d�as)', minNights: 3},
{start: '2025-07-18', end: '2025-07-21', name: 'Independencia (4 d�as)', minNights: 3},
{start: '2025-08-15', end: '2025-08-18', name: 'San Mart�n (4 d�as)', minNights: 3},
{start: '2025-12-25', end: '2025-12-29', name: 'Navidad (5 d�as)', minNights: 3},
{start: '2025-12-31', end: '2026-01-05', name: 'A�o Nuevo (6 d�as)', minNights: 3}
];
for (const period of longWeekendPeriods) {
const periodStart = new Date(period.start);
const periodEnd = new Date(period.end);
// Verificar si la estad�a overlap con el per�odo
if ((startDate >= periodStart && startDate <= periodEnd) ||
(endDate >= periodStart && endDate <= periodEnd) ||
(startDate <= periodStart && endDate >= periodEnd)) {
return {
hasRequirement: true,
minimumNights: period.minNights,
reason: period.name,
type: 'long_weekend',
period: period
};
}
}
// Verificar per�odos de vacaciones
const vacationPeriods = [
{start: '2025-07-15', end: '2025-07-31', name: 'Vacaciones de Invierno', minNights: 3},
{start: '2025-12-20', end: '2026-01-10', name: 'Vacaciones de Verano', minNights: 3}
];
for (const period of vacationPeriods) {
const periodStart = new Date(period.start);
const periodEnd = new Date(period.end);
if ((startDate >= periodStart && startDate <= periodEnd) ||
(endDate >= periodStart && endDate <= periodEnd)) {
return {
hasRequirement: true,
minimumNights: period.minNights,
reason: period.name,
type: 'vacation',
period: period
};
}
}
// Verificar fines de semana regulares (solo para 1 noche)
const isWeekendStay = isWeekendOnlyStay(startDate, endDate);
if (isWeekendStay) {
return {
hasRequirement: true,
minimumNights: 2,
reason: 'Fin de semana regular',
type: 'weekend',
period: null
};
}
return {
hasRequirement: false,
minimumNights: 1,
reason: 'Sin restricciones',
type: 'normal'
};
}
function isWeekendOnlyStay(startDate, endDate) {
const nights = Math.ceil((endDate - startDate) / (1000 60 60 * 24));
if (nights !== 1) return false; // Solo aplica para 1 noche
const dayOfWeek = startDate.getDay(); // 0 = Domingo, 6 = S�bado
return dayOfWeek === 5 || dayOfWeek === 6; // Viernes o S�bado
}
function calculateMinimumStayPenalty(minimumStayInfo, actualNights, checkIn, checkOut) {
const shortfall = minimumStayInfo.minimumNights - actualNights;
if (minimumStayInfo.type === 'long_weekend') {
return {
type: 'charge_as_minimum_or_discount',
chargeAsNights: minimumStayInfo.minimumNights,
alternativeDiscount: 20,
message: `Para estad�as de ${actualNights} noche${actualNights > 1 ? 's' : ''} en ${minimumStayInfo.reason.toLowerCase()}:
Opci�n 1: Se cobra como ${minimumStayInfo.minimumNights} noches
Opci�n 2: Descuento del 20% sobre el total actual`,
adminReason: `Estancia corta en ${minimumStayInfo.reason}: ${actualNights}/${minimumStayInfo.minimumNights} noches`
};
}
if (minimumStayInfo.type === 'vacation') {
return {
type: 'charge_as_minimum',
chargeAsNights: minimumStayInfo.minimumNights,
message: `Durante ${minimumStayInfo.reason.toLowerCase()} el m�nimo son ${minimumStayInfo.minimumNights} noches. Se cobra como ${minimumStayInfo.minimumNights} noches.`,
adminReason: `Estancia corta en per�odo de vacaciones: ${actualNights}/${minimumStayInfo.minimumNights} noches`
};
}
if (minimumStayInfo.type === 'weekend') {
return {
type: 'percentage_surcharge',
surchargePercent: 25,
message: `Para estad�as de 1 noche en fin de semana se aplica un recargo del 25% por alta demanda`,
adminReason: `Estancia de 1 noche en fin de semana - recargo por demanda`
};
}
return {
type: 'none',
message: 'Sin penalizaci�n',
adminReason: 'Estancia v�lida'
};
}
function generateMinimumStaySuggestions(checkIn, checkOut, minimumNights) {
const suggestions = [];
const startDate = new Date(checkIn);
// Sugerencia 1: Extender estad�a actual
const extendedCheckOut = new Date(startDate);
extendedCheckOut.setDate(startDate.getDate() + minimumNights);
suggestions.push({
type: 'extend_current',
checkIn: checkIn,
checkOut: extendedCheckOut.toISOString().split('T')[0],
nights: minimumNights,
message: `Extender hasta ${formatDateSpanish(extendedCheckOut.toISOString().split('T')[0])} (${minimumNights} noches)`
});
// Sugerencia 2: Mover fechas hacia adelante (1 semana)
const futureStartDate = new Date(startDate);
futureStartDate.setDate(startDate.getDate() + 7);
const futureEndDate = new Date(futureStartDate);
futureEndDate.setDate(futureStartDate.getDate() + minimumNights);
suggestions.push({
type: 'alternative_dates',
checkIn: futureStartDate.toISOString().split('T')[0],
checkOut: futureEndDate.toISOString().split('T')[0],
nights: minimumNights,
message: `${formatDateSpanish(futureStartDate.toISOString().split('T')[0])} a ${formatDateSpanish(futureEndDate.toISOString().split('T')[0])} (sin restricciones)`
});
return suggestions;
}
// ===== FIN SISTEMA DE ESTANCIAS M�NIMAS =====
async function calculatePricing(checkIn, checkOut, guests, adults = null, minors = null) {
const startDate = new Date(checkIn);
const endDate = new Date(checkOut);
const nights = Math.ceil((endDate - startDate) / (1000 60 60 * 24));
// Si no se especifican adultos/menores, asumimos que todos son adultos (retrocompatibilidad)
if (adults === null) {
adults = guests;
minors = 0;
}
// ===== VERIFICAR ESTANCIAS M�NIMAS =====
const minimumStayCheck = checkMinimumStayRequirements(checkIn, checkOut, guests, adults, minors);
// Si hay penalizaci�n por estancia corta, calculamos con las noches penalizadas
const effectiveNights = minimumStayCheck.chargeNights;
const actualNights = nights;
if (!minimumStayCheck.isValid) {
}
// ===== FIN VERIFICACI�N ESTANCIAS M�NIMAS =====
// Obtener feriados reales SOLO del per�odo de la estad�a (OPTIMIZADO)
const realHolidays = await fetchArgentineHolidays(checkIn, checkOut);
let total = 0;
const breakdown = [];
let seasonInfo = '';
// Detectar d�as especiales ANTES del loop (solo para logs, sin descuentos)
// Usar effectiveNights para el c�lculo (puede ser mayor que nights si hay penalizaci�n)
for (let i = 0; i < effectiveNights; i++) {
const currentDate = new Date(startDate);
currentDate.setDate(startDate.getDate() + i);
const dateStr = currentDate.toISOString().split('T')[0];
// Buscar temporada
const season = findSeason(dateStr);
if (!season) continue;
// Guardar info de temporada para mostrar
if (seasonInfo === '') {
seasonInfo = season.name;
}
// Calcular precio base por adultos
const basePriceUSD = season.prices_usd[adults] || season.prices_usd[3];
// Agregar 50% del precio por cada menor
let minorsSurchargeUSD = 0;
if (minors > 0) {
const singleAdultPrice = season.prices_usd[1] || season.prices_usd[3];
minorsSurchargeUSD = (singleAdultPrice 0.5) minors;
}
const basePriceARS = convertUSDtoARS(basePriceUSD + minorsSurchargeUSD);
let dayTotalUSD = basePriceUSD + minorsSurchargeUSD;
let surchargeUSD = 0;
let type = 'D�a normal';
// Verificar tipo de d�a
const dayOfWeek = currentDate.getDay(); // 0 = Domingo, 6 = S�bado
const isWeekend = (dayOfWeek === 0 || dayOfWeek === 6);
const isHoliday = realHolidays.includes(dateStr);
const isLongWeekendCheck = await isLongWeekend(currentDate, realHolidays);
// Aplicar recargos seg�n tipo de d�a
if (isHoliday || isLongWeekendCheck) {
surchargeUSD = season.long_weekend_surcharge_usd;
dayTotalUSD += surchargeUSD;
type = 'Fin de semana largo';
} else if (isWeekend) {
surchargeUSD = season.weekend_surcharge_usd;
dayTotalUSD += surchargeUSD;
type = 'Fin de semana';
}
if (minors > 0) {
}
breakdown.push({
date: dateStr,
base_price_usd: basePriceUSD,
base_price_ars: convertUSDtoARS(basePriceUSD),
minors_surcharge_usd: minorsSurchargeUSD,
minors_surcharge_ars: convertUSDtoARS(minorsSurchargeUSD),
surcharge_usd: surchargeUSD,
surcharge_ars: convertUSDtoARS(surchargeUSD),
discount_usd: 0, // Sin descuentos por ahora
discount_ars: 0,
total_usd: dayTotalUSD,
total_ars: convertUSDtoARS(dayTotalUSD),
type: type,
season: season.name,
is_special: false // Removido l�gica sandwich
});
total += dayTotalUSD;
}
// Aplicar penalizaci�n si es necesario
let finalTotal = total;
let penaltyApplied = null;
if (!minimumStayCheck.isValid && minimumStayCheck.penalty) {
if (minimumStayCheck.penalty.type === 'percentage_surcharge') {
const surcharge = total * (minimumStayCheck.penalty.surchargePercent / 100);
finalTotal = total + surcharge;
penaltyApplied = {
type: 'surcharge',
amount_usd: surcharge,
amount_ars: convertUSDtoARS(surcharge),
percentage: minimumStayCheck.penalty.surchargePercent,
reason: minimumStayCheck.penalty.adminReason,
message: minimumStayCheck.penalty.message
};
} else if (minimumStayCheck.penalty.type === 'charge_as_minimum_or_discount') {
// Ya se calcul� con effectiveNights, no se aplica recargo adicional
penaltyApplied = {
type: 'charged_as_minimum',
effective_nights: effectiveNights,
actual_nights: actualNights,
reason: minimumStayCheck.penalty.adminReason,
message: minimumStayCheck.penalty.message,
alternative_available: true,
alternative_discount: minimumStayCheck.penalty.alternativeDiscount
};
}
}
return {
nights: actualNights, // Noches reales
effective_nights: effectiveNights, // Noches que se cobran
total_usd: finalTotal,
total_ars: convertUSDtoARS(finalTotal),
breakdown: breakdown,
season: seasonInfo,
advance_payment_usd: Math.round(finalTotal * 0.5), // 50% anticipo
advance_payment_ars: convertUSDtoARS(Math.round(finalTotal * 0.5)),
minimum_stay_info: minimumStayCheck,
penalty_applied: penaltyApplied
};
}
function generateSuccessHTML(data) {
const checkInFormatted = formatDateSpanish(data.check_in);
const checkOutFormatted = formatDateSpanish(data.check_out);
// Debug para verificar fechas
// Generar informaci�n de hu�spedes
let guestInfo = `${data.adults} adulto${data.adults > 1 ? 's' : ''}`;
if (data.minorsCount > 0) {
guestInfo += ` + ${data.minorsCount} menor${data.minorsCount > 1 ? 'es' : ''}`;
}
const totalGuestText = `${data.totalGuests} persona${data.totalGuests > 1 ? 's' : ''} total`;
// Generar lista de caba�as disponibles
const cabinsList = data.available_cabins.map(cabin =>
`${cabin.emoji} ${cabin.name}`
).join(' + ');
// Mensaje para comunicaciones externas (si las hubiera)
let accommodationText = '';
if (data.isMultipleCabins) {
accommodationText = ` Se alojar�n en ${data.available_cabins.length} caba�as separadas: ${cabinsList}`;
} else {
accommodationText = ` Caba�as disponibles: ${cabinsList}`;
}
const whatsappMessage = `Hola! Me interesa reservar del ${checkInFormatted} al ${checkOutFormatted} para ${guestInfo} (${totalGuestText}).${accommodationText}. El total ser�a $${data.total_price_ars.toLocaleString('es-AR')} ARS. �Est� disponible?`;
const emailSubject = `Consulta Reserva - ${checkInFormatted} al ${checkOutFormatted}`;
const emailBody = `Hola,\n\nMe interesa reservar con los siguientes detalles:\n\nFecha llegada: ${checkInFormatted}\nFecha salida: ${checkOutFormatted}\nHu�spedes: ${guestInfo} (${totalGuestText})\n${accommodationText}\nTotal: $${data.total_price_ars.toLocaleString('es-AR')} ARS\n\n�Est� disponible?\n\nGracias!`;
const emailURL = `mailto:[email protected]?subject=${encodeURIComponent(emailSubject)}&body=${encodeURIComponent(emailBody)}`;
// URL de MercadoPago para pago del 50% (necesitar�s proporcionar la URL real)
const mercadoPagoURL = generateMercadoPagoURL(data);
// Generar secci�n de caba�as disponibles con detalles espec�ficos
// COMENTADO: No mostrar detalles de caba�as en frontend para evitar confusi�n con "�ltima caba�a"
const availableCabinsHTML = ''; /* data.available_cabins.length > 0 ? `
<div class="available-cabins">
<h4>?? Caba�as disponibles para tu reserva:</h4>
<div class="cabins-list">
${data.available_cabins.map(cabin => `
<div class="available-cabin">
<span class="cabin-emoji">${cabin.emoji}</span>
<div class="cabin-details">
<span class="cabin-name" style="color: ${PRICING_CONFIG.cabins[cabin.id]?.color || '#065f46'};">${cabin.name}</span>
<span class="cabin-capacity">Hasta ${cabin.max_guests} personas</span>
<div class="cabin-calendar-info">
<small>?? Google Calendar + ?? Booking.com sincronizados</small>
</div>
</div>
</div>
`).join('')}
</div>
<p class="assignment-note">Se asignar� autom�ticamente una caba�a disponible seg�n orden de reserva</p>
</div>
` : ''; */
// Generar desglose de precios por d�a
// COMENTADO: No mostrar detalle por d�a para simplificar la experiencia del usuario
let breakdownHTML = ''; /*
if (data.price_breakdown && data.price_breakdown.length > 0) {
breakdownHTML = `
<div class="price-breakdown">
<h4>?? Detalle por d�a</h4>
${data.price_breakdown.map(day => `
<div class="breakdown-day">
<div>
<div class="breakdown-date">${formatDateSpanish(day.date)}</div>
<div class="breakdown-type">${day.type}</div>
</div>
<div class="breakdown-price">
$${day.total_ars.toLocaleString('es-AR')} ARS
</div>
</div>
`).join('')}
</div>
`;
} */
// Generar informaci�n de estancia m�nima si aplica
let minimumStayHTML = '';
if (data.minimum_stay_info && !data.minimum_stay_info.isValid) {
const penaltyInfo = data.penalty_applied;
if (penaltyInfo) {
if (penaltyInfo.type === 'charged_as_minimum') {
minimumStayHTML = `
<div style="background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); border: 2px solid #f59e0b; border-radius: 12px; padding: 20px; margin: 20px 0; font-size: 0.95rem;">
<h4 style="margin: 0 0 12px 0; color: #92400e; font-size: 1.1rem;">?? Informaci�n sobre Estancia M�nima</h4>
<div style="background: rgba(255, 255, 255, 0.9); padding: 15px; border-radius: 8px; margin: 12px 0;">
<p style="margin: 8px 0; color: #92400e; line-height: 1.5; font-weight: 600;">
<strong>Per�odo:</strong> ${data.minimum_stay_info.reason}<br>
<strong>Estancia m�nima:</strong> ${data.minimum_stay_info.minimumRequired} noches<br>
<strong>Tu estad�a:</strong> ${data.minimum_stay_info.actualNights} noche${data.minimum_stay_info.actualNights > 1 ? 's' : ''}
</p>
<p style="margin: 8px 0; color: #92400e; font-weight: 600;">
Se cobra como ${penaltyInfo.effective_nights} noches (m�nimo del per�odo)
</p>
${penaltyInfo.alternative_available ? `
<div style="background: #e0f2fe; padding: 12px; border-radius: 6px; margin: 12px 0; border-left: 4px solid #0ea5e9;">
<p style="margin: 4px 0; color: #0c4a6e; font-weight: 600;">?? Alternativa disponible:</p>
<p style="margin: 4px 0; color: #0c4a6e;">Pod�s aplicar el c�digo <strong>AGOSTO2NOCHES</strong> para obtener un descuento del ${penaltyInfo.alternative_discount}% sobre el precio actual (${data.minimum_stay_info.actualNights} noches)</p>
</div>
` : ''}
</div>
</div>
`;
} else if (penaltyInfo.type === 'surcharge') {
minimumStayHTML = `
<div style="background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); border: 2px solid #f59e0b; border-radius: 12px; padding: 20px; margin: 20px 0; font-size: 0.95rem;">
<h4 style="margin: 0 0 12px 0; color: #92400e; font-size: 1.1rem;">?? Recargo por Alta Demanda</h4>
<div style="background: rgba(255, 255, 255, 0.9); padding: 15px; border-radius: 8px; margin: 12px 0;">
<p style="margin: 8px 0; color: #92400e; line-height: 1.5;">
<strong>Motivo:</strong> ${penaltyInfo.message}<br>
<strong>Recargo aplicado:</strong> +$${penaltyInfo.amount_ars.toLocaleString('es-AR')} ARS (${penaltyInfo.percentage}%)
</p>
</div>
</div>
`;
}
}
}
// Generar sugerencias de estancia m�nima si hay
let minimumStaySuggestionsHTML = '';
if (data.minimum_stay_info && data.minimum_stay_info.suggestions && data.minimum_stay_info.suggestions.length > 0) {
minimumStaySuggestionsHTML = `
<div style="background: #e0f2fe; border: 2px solid #0ea5e9; border-radius: 12px; padding: 20px; margin: 20px 0; font-size: 0.95rem;">
<h4 style="margin: 0 0 12px 0; color: #0c4a6e; font-size: 1.1rem;">?? Sugerencias para optimizar tu estad�a</h4>
${data.minimum_stay_info.suggestions.map(suggestion => `
<div style="background: rgba(255, 255, 255, 0.9); padding: 15px; border-radius: 8px; margin: 12px 0; border-left: 4px solid #0ea5e9;">
<p style="margin: 4px 0; color: #0c4a6e; font-weight: 600;">
${suggestion.type === 'extend_current' ? '?? Extender estad�a actual:' : '?? Fechas alternativas:'}
</p>
<p style="margin: 4px 0; color: #0c4a6e;">
${suggestion.message}
</p>
</div>
`).join('')}
</div>
`;
}
return `
<div class="result-header">
<span class="result-icon">?</span>
<h3>�Disponible!</h3>
</div>
<p class="result-message">${data.message}</p>
<div class="result-summary">
<div><strong>Llegada:</strong> ${checkInFormatted}</div>
<div><strong>Salida:</strong> ${checkOutFormatted} (10:30hs)</div>
<div><strong>Hu�spedes:</strong> ${guestInfo} (${totalGuestText})</div>
${data.pets === 'yes' ? '<div><strong>Mascotas:</strong> S� (consultar pol�ticas)</div>' : ''}
<div><strong>Noches:</strong> ${data.nights}</div>
<div><strong>Temporada:</strong> ${data.season || 'Normal'}</div>
${data.isMultipleCabins ? `<div><strong>Alojamiento:</strong> ${data.available_cabins.length} caba�as separadas</div>` : ''}
</div>
${availableCabinsHTML}
${breakdownHTML}
<div class="total-price">
<div style="margin-bottom: 10px;">
<strong>Total: $${data.total_price_ars.toLocaleString('es-AR')} ARS</strong>
</div>
<div style="font-size: 0.9rem; color: rgba(255, 255, 255, 0.9); font-weight: 500;">
Equivalente a $${data.total_price_usd} USD (Tipo de cambio: $${PRICING_CONFIG.usd_to_ars_rate})
</div>
</div>
${minimumStayHTML}
${minimumStaySuggestionsHTML}
<div style="background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #d97706 100%); border: 2px solid #f59e0b; border-radius: 12px; padding: 20px; margin: 20px 0; font-size: 0.95rem; box-shadow: 0 8px 25px rgba(245, 158, 11, 0.3);">
${data.isMultipleCabins ?
`<h4 style="margin: 0 0 12px 0; color: #92400e; font-size: 1.1rem;">?? ��LTIMA DISPONIBILIDAD PARA ${data.totalGuests} PERSONAS!</h4>
<p style="margin: 8px 0; color: #92400e; line-height: 1.5; font-weight: 600;">
Esta es la <strong>�nica combinaci�n de caba�as disponible</strong> para alojar a todo tu grupo en las fechas seleccionadas.
�No dejes pasar esta oportunidad �nica de disfrutar Lomas del Uritorco juntos!
</p>` :
`<h4 style="margin: 0 0 12px 0; color: #92400e; font-size: 1.1rem;">?? ��LTIMA CABA�A DISPONIBLE!</h4>
<p style="margin: 8px 0; color: #92400e; line-height: 1.5; font-weight: 600;">
Esta es la <strong>�ltima caba�a disponible</strong> para las fechas que seleccionaste.
�No dejes pasar esta oportunidad �nica de disfrutar Lomas del Uritorco!
</p>`
}
<div style="background: rgba(255, 255, 255, 0.9); padding: 15px; border-radius: 8px; margin: 12px 0;">
<h5 style="margin: 0 0 8px 0; color: #92400e;">?? BONIFICACI�N ESPECIAL POR RESERVAR AHORA:</h5>
<p style="margin: 4px 0; color: #92400e; font-weight: 600;"><strong>??? Sesi�n de Terapia de Reiki y Coaching para el Bienestar SIN CARGO</strong></p>
<p style="margin: 4px 0; color: #92400e; font-size: 0.95rem;"><strong>VALOR: $35.000</strong></p>
<p style="margin: 8px 0; color: #92400e; font-size: 0.9rem;">
?? <strong>Durante su estad�a</strong> en Lomas del Uritorco<br>
?? <strong>O por VideoConferencia</strong> seg�n disponibilidad
</p>
<p style="margin: 8px 0 4px 0; color: #92400e; font-size: 0.85rem; font-style: italic;">*Bonificaci�n v�lida solo al reservar ahora</p>
</div>
<p style="margin: 12px 0 0 0; color: #92400e; font-weight: 700; text-align: center; font-size: 1.05rem;">
?? �RESERVA AHORA y obt�n tu sesi�n de bienestar GRATIS!
</p>
</div>
<div style="background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 8px; padding: 15px; margin: 15px 0; font-size: 0.9rem;">
<h4 style="margin: 0 0 12px 0; color: #0c4a6e;">?? Proceso de Reserva Detallado:</h4>
<div style="background: white; padding: 12px; border-radius: 6px; margin: 10px 0;">
<p style="margin: 4px 0; color: #0c4a6e;"><strong>1. Pago Seguro:</strong> Haz clic en "Reservar Ahora" y ser�s dirigido a MercadoPago</p>
<p style="margin: 4px 0; color: #0c4a6e;"><strong>2. Anticipo 50%:</strong> Paga $${data.advance_payment_ars.toLocaleString('es-AR')} ARS de forma segura</p>
<p style="margin: 4px 0; color: #0c4a6e;"><strong>3. Formulario Autom�tico:</strong> Despu�s del pago recibir�s el formulario de check-in</p>
<p style="margin: 4px 0; color: #0c4a6e;"><strong>4. Confirmaci�n Inmediata:</strong> WhatsApp + Email con todos los detalles</p>
<p style="margin: 4px 0; color: #0c4a6e;"><strong>5. Check-in:</strong> El resto se paga al llegar a Lomas del Uritorco</p>
</div>
</div>
<div class="action-buttons">
<a href="${mercadoPagoURL}" target="_blank" class="btn-reserve" style="text-decoration: none; background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 50%, #6d28d9 100%); color: white; padding: 18px 24px; border-radius: 12px; font-weight: 700; text-align: center; display: block; width: 100%; box-sizing: border-box; font-size: 1.2rem; box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4); border: 2px solid rgba(255, 255, 255, 0.3); transition: all 0.3s ease; margin: 20px 0;">
? RESERVAR AHORA
</a>
<button onclick="shareQuoteOnWhatsApp()" class="btn-share-whatsapp" style="background: linear-gradient(135deg, #25d366 0%, #1cb55c 50%, #128c43 100%); color: white; padding: 18px 24px; border: none; border-radius: 12px; font-weight: 700; text-align: center; display: block; width: 100%; box-sizing: border-box; font-size: 1.2rem; box-shadow: 0 6px 20px rgba(37, 211, 102, 0.4); border: 2px solid rgba(255, 255, 255, 0.3); transition: all 0.3s ease; margin: 10px 0; cursor: pointer;">
?? COMPARTIR POR WHATSAPP
</button>
<button onclick="generateQuoteImage()" class="btn-generate-image" style="background: linear-gradient(135deg, #f59e0b 0%, #d97706 50%, #b45309 100%); color: white; padding: 18px 24px; border: none; border-radius: 12px; font-weight: 700; text-align: center; display: block; width: 100%; box-sizing: border-box; font-size: 1.2rem; box-shadow: 0 6px 20px rgba(245, 158, 11, 0.4); border: 2px solid rgba(255, 255, 255, 0.3); transition: all 0.3s ease; margin: 10px 0; cursor: pointer;">
?? GENERAR IMAGEN COTIZACI�N
</button>
</div>
`;
}
function generateErrorHTML(message, suggestions = [], unavailableReasons = []) {
let suggestionsHTML = '';
if (suggestions && suggestions.length > 0) {
suggestionsHTML = `
<div class="suggestion">
<h4 style="margin: 0 0 10px 0; color: #92400e;">?? Fechas alternativas disponibles:</h4>
${suggestions.map(suggestion => `
<div style="background: white; padding: 10px; margin: 8px 0; border-radius: 6px; border: 1px solid #f59e0b;">
<strong>${formatDateSpanish(suggestion.checkIn)} al ${formatDateSpanish(suggestion.checkOut)}</strong><br>
<small>${suggestion.availableCabins} caba�a${suggestion.availableCabins > 1 ? 's' : ''} disponible${suggestion.availableCabins > 1 ? 's' : ''} - $${suggestion.totalARS.toLocaleString('es-AR')} ARS</small>
</div>
`).join('')}
</div>
`;
}
let reasonsHTML = '';
if (unavailableReasons && unavailableReasons.length > 0) {
reasonsHTML = `
<div class="suggestion">
<h4 style="margin: 0 0 10px 0; color: #92400e;">?? Detalles de disponibilidad:</h4>
${unavailableReasons.map(reason => `
<p style="margin: 4px 0; font-size: 0.9rem;">${reason}</p>
`).join('')}
</div>
`;
}
return `
<div class="result-header">
<span class="result-icon">?</span>
<h3>No disponible</h3>
</div>
<p class="result-message">${message}</p>
${reasonsHTML}
${suggestionsHTML}
${!suggestionsHTML ? `
<div class="suggestion">
<h4 style="margin: 0 0 10px 0; color: #92400e;">?? Fechas Alternativas Disponibles:</h4>
<p style="margin: 8px 0; color: #92400e;">Te sugerimos estas fechas pr�ximas disponibles para ${data.guests} hu�sped${data.guests > 1 ? 'es' : ''} y ${data.nights || 'varias'} noche${(data.nights || 2) > 1 ? 's' : ''}:</p>
<div style="background: white; padding: 12px; border-radius: 6px; margin: 10px 0;">
<p style="margin: 4px 0; color: #0c4a6e;"><strong>?? Pr�ximas fechas:</strong> Consulta disponibilidad desde ma�ana en adelante</p>
<p style="margin: 4px 0; color: #0c4a6e;"><strong>??? Fines de semana:</strong> Mayor disponibilidad en d�as de semana</p>
<p style="margin: 4px 0; color: #0c4a6e;"><strong>?? Tip:</strong> Las fechas m�s alejadas suelen tener mejor disponibilidad</p>
</div>
<p style="margin: 8px 0; color: #92400e;"><strong>�Prueba otras fechas y encuentra tu estad�a perfecta!</strong></p>
</div>
` : ''}
<div class="suggestion" style="background: #f0f9ff; border: 1px solid #0ea5e9; border-radius: 8px; padding: 15px; margin: 15px 0;">
<h4 style="margin: 0 0 10px 0; color: #0c4a6e;">? Prueba con Otro Calendario</h4>
<p style="margin: 8px 0; color: #0c4a6e;">Selecciona fechas diferentes arriba en el calendario para encontrar disponibilidad. Tenemos caba�as disponibles durante todo el a�o.</p>
<div style="background: white; padding: 12px; border-radius: 6px; margin: 10px 0;">
<p style="margin: 4px 0; color: #0c4a6e;"><strong>? Consejo:</strong> Intenta con fechas de lunes a jueves para mayor disponibilidad</p>
<p style="margin: 4px 0; color: #0c4a6e;"><strong>??? Flexibilidad:</strong> Cambiar tu fecha de llegada por 1-2 d�as puede abrir nuevas opciones</p>
</div>
</div>
`;
}
function showError(message) {
resultDiv.style.display = 'block';
resultDiv.innerHTML = generateErrorHTML(message, [], []);
resultDiv.className = 'cabanas-result cabanas-error';
}
// ===== SISTEMA DE DESCUENTOS =====
// Aplicar c�digo de descuento
document.getElementById('apply-discount-btn').addEventListener('click', function() {
const discountCode = document.getElementById('discount_code').value.trim().toUpperCase();
if (!discountCode) {
showDiscountResult('error', 'Por favor ingresa un c�digo de descuento');
return;
}
if (!currentReservationData) {
showDiscountResult('error', 'Primero debes verificar disponibilidad');
return;
}
applyDiscountCode(discountCode);
});
// Funci�n para aplicar descuento
function applyDiscountCode(code) {
const discountBtn = document.getElementById('apply-discount-btn');
const btnText = discountBtn.querySelector('.btn-text');
const btnLoading = discountBtn.querySelector('.btn-loading');
// Mostrar loading
btnText.style.display = 'none';
btnLoading.style.display = 'inline';
discountBtn.disabled = true;
// Buscar c�digo en configuraci�n
const discountConfig = PRICING_CONFIG.discount_codes[code];
if (!discountConfig) {
showDiscountResult('error', 'C�digo de descuento no v�lido');
resetDiscountButton();
return;
}
if (!discountConfig.active) {
showDiscountResult('error', 'Este c�digo no est� disponible actualmente');
resetDiscountButton();
return;
}
// Validar n�mero m�nimo de noches
if (discountConfig.min_nights && currentReservationData.nights < discountConfig.min_nights) {
showDiscountResult('error', `Este c�digo requiere m�nimo ${discountConfig.min_nights} noche${discountConfig.min_nights > 1 ? 's' : ''}`);
resetDiscountButton();
return;
}
// Validar fechas de validez
const today = new Date();
if (discountConfig.valid_from) {
const validFrom = new Date(discountConfig.valid_from);
if (today < validFrom) {
showDiscountResult('error', 'Este c�digo a�n no est� disponible');
resetDiscountButton();
return;
}
}
if (discountConfig.valid_until) {
const validUntil = new Date(discountConfig.valid_until);
if (today > validUntil) {
showDiscountResult('error', 'Este c�digo ha expirado');
resetDiscountButton();
return;
}
}
// Calcular descuento
const discountResult = calculateDiscount(discountConfig, currentReservationData);
if (discountResult.success) {
// Aplicar descuento
applyDiscountToReservation(discountResult, discountConfig);
showDiscountResult('success', `? ${discountConfig.client_display} aplicado: -$${discountResult.discount_ars.toLocaleString()} ARS`);
} else {
showDiscountResult('error', discountResult.message);
}
resetDiscountButton();
}
// Calcular monto del descuento
function calculateDiscount(discountConfig, reservationData) {
let discountUSD = 0;
let discountARS = 0;
switch (discountConfig.type) {
case 'percentage':
discountUSD = (reservationData.total_price_usd * discountConfig.value) / 100;
discountARS = (reservationData.total_price_ars * discountConfig.value) / 100;
break;
case 'fixed_usd':
discountUSD = discountConfig.value;
discountARS = convertUSDtoARS(discountConfig.value);
break;
case 'fixed_ars':
discountARS = discountConfig.value;
discountUSD = discountARS / PRICING_CONFIG.usd_to_ars_rate;
break;
default:
return { success: false, message: 'Tipo de descuento no v�lido' };
}
// Validar que el descuento no sea mayor al total
if (discountUSD >= reservationData.total_price_usd) {
discountUSD = reservationData.total_price_usd * 0.95; // M�ximo 95% de descuento
discountARS = reservationData.total_price_ars * 0.95;
}
return {
success: true,
discount_usd: Math.round(discountUSD * 100) / 100,
discount_ars: Math.round(discountARS),
code: discountConfig
};
}
// Aplicar descuento a la reserva
function applyDiscountToReservation(discountResult, discountConfig) {
// Calcular nuevos totales
const newTotalUSD = currentReservationData.total_price_usd - discountResult.discount_usd;
const newTotalARS = currentReservationData.total_price_ars - discountResult.discount_ars;
const newAdvanceUSD = Math.round((newTotalUSD 0.5) 100) / 100;
const newAdvanceARS = Math.round(newTotalARS * 0.5);
// Actualizar datos de reserva
currentReservationData.original_total_usd = currentReservationData.total_price_usd;
currentReservationData.original_total_ars = currentReservationData.total_price_ars;
currentReservationData.discount_applied = {
code: document.getElementById('discount_code').value.toUpperCase(),
admin_reason: discountConfig.admin_reason,
client_display: discountConfig.client_display,
discount_usd: discountResult.discount_usd,
discount_ars: discountResult.discount_ars
};
currentReservationData.total_price_usd = newTotalUSD;
currentReservationData.total_price_ars = newTotalARS;
currentReservationData.advance_payment_usd = newAdvanceUSD;
currentReservationData.advance_payment_ars = newAdvanceARS;
// Actualizar el resumen visual
updateReservationSummaryWithDiscount();
}
// Actualizar resumen visual con descuento
function updateReservationSummaryWithDiscount() {
const resultDiv = document.getElementById('availability-result');
const totalSection = resultDiv.querySelector('.price-total');
if (totalSection && currentReservationData.discount_applied) {
const discount = currentReservationData.discount_applied;
totalSection.innerHTML = `
<div style="margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #e0e0e0;">
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>Subtotal:</span>
<span>$${currentReservationData.original_total_ars.toLocaleString()} ARS</span>
</div>
<div style="display: flex; justify-content: space-between; color: #059669; font-weight: 600;">
<span>??? ${discount.client_display}:</span>
<span>-$${discount.discount_ars.toLocaleString()} ARS</span>
</div>
</div>
<div style="display: flex; justify-content: space-between; font-size: 1.2rem; font-weight: 700; color: #1f2937;">
<span>?? TOTAL:</span>
<span>$${currentReservationData.total_price_ars.toLocaleString()} ARS</span>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 4px; color: #6b7280; font-size: 0.9rem;">
<span>Equivalente:</span>
<span>USD $${currentReservationData.total_price_usd}</span>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 8px; padding-top: 8px; border-top: 1px solid #e0e0e0; color: #059669; font-weight: 600;">
<span>?? Anticipo (50%):</span>
<span>$${currentReservationData.advance_payment_ars.toLocaleString()} ARS</span>
</div>
`;
}
}
// Mostrar resultado del descuento
function showDiscountResult(type, message) {
const resultDiv = document.getElementById('discount-result');
resultDiv.style.display = 'block';
resultDiv.className = `discount-result discount-${type}`;
resultDiv.textContent = message;
// Auto-ocultar despu�s de 5 segundos si es error
if (type === 'error') {
setTimeout(() => {
resultDiv.style.display = 'none';
}, 5000);
}
}
// Resetear bot�n de descuento
function resetDiscountButton() {
const discountBtn = document.getElementById('apply-discount-btn');
const btnText = discountBtn.querySelector('.btn-text');
const btnLoading = discountBtn.querySelector('.btn-loading');
btnText.style.display = 'inline';
btnLoading.style.display = 'none';
discountBtn.disabled = false;
}
// ===== FIN SISTEMA DE DESCUENTOS =====
});
</script>
</body>
</html>