document.addEventListener('DOMContentLoaded', function() { // Configuration - replace with your actual Cloudflare worker URL const CLOUDFLARE_WORKER_URL = 'https://oniri.beveillard.workers.dev'; // Store unavailable dates for the calendar let unavailableDates = []; let flatpickrInstance = null; // Only check availability if dates are provided in URL parameters const urlParams = new URLSearchParams(window.location.search); const dateParam = urlParams.get('date'); console.log('🔍 apart-template-new.js initialized'); console.log('🔍 URL params:', { date: dateParam, adults: urlParams.get('adults'), children: urlParams.get('children'), babies: urlParams.get('babies') }); if (dateParam && dateParam.includes(' to ')) { console.log('📅 Found dates in URL, checking availability:', dateParam); // Show loader before initial availability check showLoader(); checkAvailability(); } else { console.log('📅 No dates in URL, waiting for user to select dates'); // Show a message to select dates instead of default pricing showSelectDatesMessage(); } const dateInput = document.querySelector('[data-input="date"]'); if (dateInput) { if (typeof flatpickr !== 'undefined') { // Initialize flatpickr with disabled dates support flatpickrInstance = flatpickr(dateInput, { mode: "range", minDate: "today", disable: unavailableDates, onChange: function(selectedDates, dateStr) { console.log('Flatpickr onChange:', selectedDates.length, 'dates selected'); // Only update URL and make API call when we have BOTH dates selected if (selectedDates.length === 2) { console.log('✅ Complete date range selected, updating URL and checking availability'); updateDateInUrl(dateStr); updateDateElements(dateStr); } else { console.log('⏳ Waiting for complete date range (only', selectedDates.length, 'date selected)'); // Just update the visual elements without making API calls updateDateElementsOnly(dateStr); } } }); // Fetch availability map to disable unavailable dates fetchAvailabilityMap(); } dateInput.addEventListener('change', function() { updateDateInUrl(this.value); updateDateElements(this.value); }); fillDateFromUrl(); updateDateElementsFromUrl(); } else { updateDateElementsFromUrl(); } /** * Get apartment ID from the page * @returns {string} Apartment ID */ function getApartmentId() { // Try multiple methods to find the apartment ID // Method 1: data-booking-id attribute const bookingElement = document.querySelector('[data-booking-id]'); if (bookingElement) { return bookingElement.getAttribute('data-booking-id'); } // Method 2: data-apartment-id attribute const apartmentElement = document.querySelector('[data-apartment-id]'); if (apartmentElement) { return apartmentElement.getAttribute('data-apartment-id'); } // Method 3: Check if ID is in URL path const pathSegments = window.location.pathname.split('/'); const potentialId = pathSegments[pathSegments.length - 1]; if (/^\d+$/.test(potentialId)) { return potentialId; } // Fallback to default ID if nothing found console.warn('No apartment ID found on page, using default ID'); return "209948"; } /** * Get Guesty listing ID from the page * @returns {string|null} Guesty listing ID or null if not found */ function getGuestyListingId() { // Try to find the Guesty listing ID from data attribute const guestyElement = document.querySelector('[data-guesty-id]'); if (guestyElement) { const id = guestyElement.getAttribute('data-guesty-id'); console.log('📅 Found Guesty ID from data-guesty-id:', id); return id; } // Fallback: try data-listing-id const listingElement = document.querySelector('[data-listing-id]'); if (listingElement) { const id = listingElement.getAttribute('data-listing-id'); console.log('📅 Found Guesty ID from data-listing-id:', id); return id; } // Fallback: try data-booking-id (might contain Guesty ID) const bookingElement = document.querySelector('[data-booking-id]'); if (bookingElement) { const id = bookingElement.getAttribute('data-booking-id'); // Check if it looks like a Guesty ID (24 character hex string) if (id && /^[a-f0-9]{24}$/i.test(id)) { console.log('📅 Found Guesty ID from data-booking-id:', id); return id; } } console.warn('📅 No Guesty listing ID found on page. Available data attributes:'); console.warn(' - data-guesty-id:', document.querySelector('[data-guesty-id]')); console.warn(' - data-listing-id:', document.querySelector('[data-listing-id]')); console.warn(' - data-booking-id:', document.querySelector('[data-booking-id]')); console.warn(' - data-apartment-id:', document.querySelector('[data-apartment-id]')); return null; } /** * Fetch availability map from Cloudflare worker and update calendar */ function fetchAvailabilityMap() { const guestyListingId = getGuestyListingId(); if (!guestyListingId) { console.warn('📅 Cannot fetch availability map: no Guesty listing ID found'); return; } // Get guest counts from URL or defaults const urlParams = new URLSearchParams(window.location.search); const adultsParam = urlParams.get('adults') || 1; const childrenParam = urlParams.get('children') || 0; const babiesParam = urlParams.get('babies') || 0; // Use Cloudflare worker to proxy the availability_map request const availabilityUrl = `${CLOUDFLARE_WORKER_URL}/availability_map/${guestyListingId}`; console.log('📅 Fetching availability map from:', availabilityUrl); console.log('👥 Guest counts:', { adults: adultsParam, children: childrenParam, babies: babiesParam }); fetch(availabilityUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nb_adults: parseInt(adultsParam), nb_children: parseInt(childrenParam), nb_babies: parseInt(babiesParam) || 0 }) }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { console.log('📅 Availability map received:', data); if (data.availability_map && data.start_date) { const disabledDates = parseAvailabilityMap(data.availability_map, data.start_date); updateCalendarWithDisabledDates(disabledDates); } }) .catch(error => { console.error('❌ Error fetching availability map:', error); }); } /** * Parse the availability map string into an array of disabled date strings * @param {string} availabilityMap - String of 0s and 1s (0=available, 1=unavailable) * @param {string} startDate - Start date in YYYY-MM-DD format * @returns {string[]} Array of date strings in YYYY-MM-DD format for unavailable dates */ function parseAvailabilityMap(availabilityMap, startDate) { const disabledDates = []; const start = new Date(startDate + 'T12:00:00'); // Add noon to avoid timezone issues for (let i = 0; i < availabilityMap.length; i++) { if (availabilityMap[i] === '1') { // Day is unavailable const unavailableDate = new Date(start); unavailableDate.setDate(start.getDate() + i); // Format as YYYY-MM-DD string for flatpickr const dateStr = unavailableDate.toISOString().split('T')[0]; disabledDates.push(dateStr); } } console.log(`📅 Parsed ${disabledDates.length} unavailable dates from availability map`); console.log('📅 First 5 disabled dates:', disabledDates.slice(0, 5)); return disabledDates; } /** * Update flatpickr calendar with disabled dates * @param {string[]} disabledDates - Array of date strings to disable */ function updateCalendarWithDisabledDates(disabledDates) { // Store the disabled dates unavailableDates = disabledDates; // Try to get flatpickr instance from the input element if not stored const dateInput = document.querySelector('[data-input="date"]'); if (!flatpickrInstance && dateInput && dateInput._flatpickr) { flatpickrInstance = dateInput._flatpickr; console.log('📅 Retrieved flatpickr instance from input element'); } if (!flatpickrInstance) { console.warn('📅 Flatpickr instance not available yet, will retry in 200ms...'); // Retry after a short delay (max 10 retries) if (!updateCalendarWithDisabledDates.retryCount) { updateCalendarWithDisabledDates.retryCount = 0; } updateCalendarWithDisabledDates.retryCount++; if (updateCalendarWithDisabledDates.retryCount < 10) { setTimeout(() => updateCalendarWithDisabledDates(disabledDates), 200); } else { console.error('📅 Failed to find flatpickr instance after 10 retries'); } return; } try { // Get the input element to destroy and recreate flatpickr with disable option const dateInput = document.querySelector('[data-input="date"]'); // Store current selected dates if any const currentDates = flatpickrInstance.selectedDates || []; // Destroy the existing instance flatpickrInstance.destroy(); // Recreate flatpickr with the disable option flatpickrInstance = flatpickr(dateInput, { mode: "range", minDate: "today", disable: disabledDates, defaultDate: currentDates.length > 0 ? currentDates : undefined, onChange: function(selectedDates, dateStr) { console.log('Flatpickr onChange:', selectedDates.length, 'dates selected'); if (selectedDates.length === 2) { console.log('✅ Complete date range selected, updating URL and checking availability'); updateDateInUrl(dateStr); updateDateElements(dateStr); } else { console.log('⏳ Waiting for complete date range (only', selectedDates.length, 'date selected)'); updateDateElementsOnly(dateStr); } } }); console.log('📅 Calendar recreated with', disabledDates.length, 'disabled dates'); // Reset retry count on success updateCalendarWithDisabledDates.retryCount = 0; } catch (error) { console.error('📅 Error setting disabled dates:', error); console.log('📅 flatpickrInstance:', flatpickrInstance); console.log('📅 disabledDates sample:', disabledDates.slice(0, 3)); } } function calculateNights(startDateStr, endDateStr) { const startDate = new Date(startDateStr); const endDate = new Date(endDateStr); const diffTime = Math.abs(endDate - startDate); return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); } function checkAvailability() { console.log('🔍 checkAvailability() called'); const params = new URLSearchParams(window.location.search); const dateParam = params.get('date'); const adultsParam = params.get('adults') || 1; const childrenParam = params.get('children') || 0; const babiesParam = params.get('babies') || 0; // Only proceed if we have valid date parameters if (!dateParam || !dateParam.includes(' to ')) { console.log('❌ No valid dates found, cannot check availability'); showSelectDatesMessage(); return; } const dateParts = dateParam.split(' to '); if (dateParts.length !== 2) { console.log('❌ Invalid date format, cannot check availability'); showSelectDatesMessage(); return; } const checkinDate = dateParts[0].replace(/\+/g, ' ').trim(); const checkoutDate = dateParts[1].replace(/\+/g, ' ').trim(); const calculatedNights = calculateNights(checkinDate, checkoutDate); console.log('✅ Valid dates found - Checking availability:'); console.log('📅 Dates:', checkinDate, 'to', checkoutDate); console.log('🌙 Nights:', calculatedNights); console.log('👥 Guests:', { adults: adultsParam, children: childrenParam, babies: babiesParam }); // Get the apartment ID dynamically const apartmentId = getApartmentId(); console.log('🏠 Apartment ID:', apartmentId); // Use our new worker endpoint structure with dynamic ID const proxyUrl = `${CLOUDFLARE_WORKER_URL}/availability/${apartmentId}`; console.log('🌐 API URL:', proxyUrl); console.log('👥 Guest counts:', { adults: adultsParam, children: childrenParam, babies: babiesParam }); const requestData = { checkin_date: checkinDate, checkout_date: checkoutDate, nb_adults: parseInt(adultsParam), nb_children: parseInt(childrenParam), nb_babies: parseInt(babiesParam) || 0 }; console.log('📤 Request data:', requestData); fetch(proxyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestData) }) .then(response => response.json()) .then(data => { // Hide loader when API call completes successfully hideLoader(); if (data && data.rentals && data.rentals.length > 0) { const rentalInfo = data.rentals[0]; console.log('🏠 Rental info received:', rentalInfo); console.log('📍 Is available:', rentalInfo.is_available); console.log('💰 Pricing array:', rentalInfo.pricing); // Check if apartment is actually available if (rentalInfo.is_available !== false) { // Hide not-available elements when apartment is available document.querySelectorAll('.not-available').forEach(el => { el.style.display = 'none'; }); } else { console.log('❌ Apartment marked as not available in API response'); // Show not-available elements document.querySelectorAll('.not-available').forEach(el => { el.style.display = 'block'; }); // Hide booking component when not available document.querySelectorAll('.item_apartment-book-component').forEach(el => { el.style.display = 'none'; }); return; } if (rentalInfo.pricing && rentalInfo.pricing.length > 0) { const nights = calculatedNights; // Find flexible and non-flexible rates const flexibleRate = rentalInfo.pricing.find(p => p.short_name === 'tarif-flexible' || p.display_name?.toLowerCase().includes('flexible') ); const nonFlexibleRate = rentalInfo.pricing.find(p => p.short_name === 'tarif-non-remboursable' || (p.display_name?.toLowerCase().includes('non') && p.display_name?.toLowerCase().includes('remboursable')) ); console.log('💰 Pricing options found:', { flexible: flexibleRate ? { name: flexibleRate.display_name, short_name: flexibleRate.short_name, total: flexibleRate.total_amount } : null, nonFlexible: nonFlexibleRate ? { name: nonFlexibleRate.display_name, short_name: nonFlexibleRate.short_name, total: nonFlexibleRate.total_amount } : null }); // Store both pricing options globally for later use window.pricingOptions = { flexible: flexibleRate, nonFlexible: nonFlexibleRate, nights: nights }; // Populate both pricing options in the UI if (flexibleRate) { const nightsSpan = document.querySelector('[data-book-flexible="nights-number"]'); const priceSpan = document.querySelector('[data-book-flexible="total-price"]'); const pricePerNightSpan = document.querySelector('[data-book-flexible="total-price-per-night"]'); if (nightsSpan) nightsSpan.textContent = nights; if (priceSpan) priceSpan.textContent = `${flexibleRate.total_amount}€`; if (pricePerNightSpan) { const pricePerNight = Math.round(flexibleRate.total_amount / nights); pricePerNightSpan.textContent = `${pricePerNight}€`; } // Check the flexible radio button by default const noFlexibleRadio = document.querySelector('input[name="type"][value="no-flexible"]'); if (noFlexibleRadio && !document.querySelector('input[name="type"]:checked')) { noFlexibleRadio.checked = true; console.log('✅ Pre-selected no-flexible rate radio button'); } console.log('✅ Populated flexible rate:', flexibleRate.total_amount); } if (nonFlexibleRate) { const nightsSpan = document.querySelector('[data-book-no-flexible="nights-number"]'); const priceSpan = document.querySelector('[data-book-no-flexible="total-price"]'); const pricePerNightSpan = document.querySelector('[data-book-no-flexible="total-price-per-night"]'); if (nightsSpan) nightsSpan.textContent = nights; if (priceSpan) priceSpan.textContent = `${nonFlexibleRate.total_amount}€`; if (pricePerNightSpan) { const pricePerNight = Math.round(nonFlexibleRate.total_amount / nights); pricePerNightSpan.textContent = `${pricePerNight}€`; } console.log('✅ Populated non-flexible rate:', nonFlexibleRate.total_amount); } // Show booking component and hide its loader const bookingComponent = document.querySelector('.item_apartment-book-component'); if (bookingComponent) { bookingComponent.style.display = 'block'; // Hide the small loader inside the booking component const bookingLoader = bookingComponent.querySelector('[data-wf--loader--variant="extra-small"]'); if (bookingLoader) { bookingLoader.style.display = 'none'; console.log('✅ Hiding booking component loader'); } console.log('✅ Showing booking component'); } // Default to flexible rate for calculations (keep existing logic for backward compatibility) const totalAmount = flexibleRate ? flexibleRate.total_amount : rentalInfo.pricing[0].total_amount; if (!totalAmount || totalAmount <= 0) { console.log('❌ Invalid total_amount in pricing:', totalAmount); // Show not-available elements if pricing is invalid document.querySelectorAll('.not-available').forEach(el => { el.style.display = 'block'; }); return; } const actualPricePerNight = totalAmount / nights; // Keep decimals for exact calculation console.log('=== PRICE CALCULATION METHOD ==='); console.log('API avg_per_night (not reliable):', rentalInfo.pricing[0].avg_per_night); console.log('API total_amount (source of truth):', totalAmount); console.log('Calculated nights:', nights); console.log('Actual price per night (with decimals):', actualPricePerNight, '=', totalAmount, '/', nights); console.log('Formatted price display:', actualPricePerNight.toFixed(2), '€'); console.log('================================'); // Extract fees from the API response let taxesFees = 0; let cleaningFees = 0; if (rentalInfo.pricing[0].included_fees && rentalInfo.pricing[0].included_fees.length > 0) { // Find occupancy tax (taxe de séjour) const taxesFeeWrapper = rentalInfo.pricing[0].included_fees.find(fee => fee.name && (fee.name.fr?.toLowerCase().includes('taxe') || fee.name.en?.toLowerCase().includes('tax')) ); if (taxesFeeWrapper) { taxesFees = parseFloat(taxesFeeWrapper.price) || 0; } // Find cleaning fees (frais de ménage) const cleaningFeeWrapper = rentalInfo.pricing[0].included_fees.find(fee => fee.name && (fee.name.fr?.toLowerCase().includes('ménage') || fee.name.en?.toLowerCase().includes('cleaning')) ); if (cleaningFeeWrapper) { cleaningFees = parseFloat(cleaningFeeWrapper.price) || 0; } } // Calculate the base price using the actual price per night (derived from total) const basePrice = actualPricePerNight * nights; // The total from API is our base amount, and we need to ADD fees to get final total const finalTotalRaw = totalAmount + taxesFees + cleaningFees; // Dynamic rounding logic - try different rounding methods to match API expectations // Use more precise rounding to avoid floating point issues const roundedUp = Math.ceil(Math.round(finalTotalRaw * 100) / 100); const roundedDown = Math.floor(Math.round(finalTotalRaw * 100) / 100); const roundedNearest = Math.round(Math.round(finalTotalRaw * 100) / 100); // For now, use ceil (round up) but we'll make this configurable const finalTotal = roundedUp; console.log('🔄 Rounding options:', { raw: finalTotalRaw, ceil: roundedUp, floor: roundedDown, round: roundedNearest, selected: finalTotal }); console.log('=== FINAL CALCULATIONS ==='); console.log('Actual price per night:', actualPricePerNight); console.log('Number of nights:', nights); console.log('Base price (actual price × nights):', basePrice); console.log('Total amount from API (base):', totalAmount); console.log('Taxes fees:', taxesFees); console.log('Cleaning fees:', cleaningFees); console.log('Final total (raw calculation):', finalTotalRaw, '=', totalAmount, '+', taxesFees, '+', cleaningFees); console.log('Final total (rounded for API):', finalTotal); console.log('=== BOOKING API PRICE ==='); console.log('Sending to booking API:', finalTotal, '(rounded total with fees)'); console.log('API expects rounded UP amount:', finalTotalRaw, '→', finalTotal); console.log('=== MATH VERIFICATION ==='); console.log(`${actualPricePerNight.toFixed(2)} × ${nights} = ${(actualPricePerNight * nights).toFixed(2)} (should equal API total: ${totalAmount})`); console.log(`Perfect match: ${actualPricePerNight * nights === totalAmount ? '✅ EXACT MATCH' : '❌ MISMATCH'}`); console.log(`Final customer total: ${totalAmount} + ${taxesFees + cleaningFees} = ${finalTotal}€`); console.log('=========================='); // Format price with appropriate decimals (2 decimals or just 1 if needed) const formattedPrice = actualPricePerNight.toLocaleString('fr-FR', { style: 'currency', currency: 'EUR', minimumFractionDigits: 2, maximumFractionDigits: 2 }); // Update price displays with the actual price per night document.querySelectorAll('[data-price]').forEach(el => { if (el.tagName === 'INPUT') { el.value = actualPricePerNight; } else { el.textContent = formattedPrice; } el.setAttribute('data-price', actualPricePerNight); }); // Mettre à jour les nuits avec notre valeur calculée document.querySelectorAll('[data-nombre-nuit]').forEach(el => { if (el.tagName === 'INPUT') { el.value = nights; } else { el.textContent = nights; } el.setAttribute('data-nombre-nuit', nights); }); // ✅ Mettre à jour le sous-total et le total avec les calculs corrects updateSubtotalAndTotalPrices(basePrice, totalAmount, finalTotal, taxesFees, cleaningFees); } else { // No pricing data found - show not-available and hide booking component console.log('❌ No pricing data found in rental info'); document.querySelectorAll('.not-available').forEach(el => { el.style.display = 'block'; }); document.querySelectorAll('.item_apartment-book-component').forEach(el => { el.style.display = 'none'; }); } } else { // No rental data found - show not-available and hide booking component console.log('❌ No rental data found for these dates'); document.querySelectorAll('.not-available').forEach(el => { el.style.display = 'block'; }); document.querySelectorAll('.item_apartment-book-component').forEach(el => { el.style.display = 'none'; }); } }) .catch(error => { // Hide loader on error hideLoader(); console.error('❌ Error during availability check:', error); // Show not-available elements and hide booking component on error document.querySelectorAll('.not-available').forEach(el => { el.style.display = 'block'; }); document.querySelectorAll('.item_apartment-book-component').forEach(el => { el.style.display = 'none'; }); }); } function updateSubtotalAndTotalPrices(basePrice, subtotalAmount, finalTotal, taxesFees, cleaningFees) { // Update the fee elements const taxesElement = document.querySelector('[data-taxes-fees]'); const cleaningElement = document.querySelector('[data-cleaning-fees]'); if (taxesElement) { if (taxesElement.tagName === 'INPUT') { taxesElement.value = taxesFees; } else { taxesElement.textContent = taxesFees; } taxesElement.setAttribute('data-taxes-fees', taxesFees); } if (cleaningElement) { if (cleaningElement.tagName === 'INPUT') { cleaningElement.value = cleaningFees; } else { cleaningElement.textContent = cleaningFees } cleaningElement.setAttribute('data-cleaning-fees', cleaningFees); } // Store the subtotal amount for booking API // The booking API expects the base price (subtotalAmount), and it will add fees const bookingPriceElement = document.getElementById('price-booking'); if (bookingPriceElement) { if (bookingPriceElement.tagName === 'INPUT') { bookingPriceElement.value = finalTotal; } else { bookingPriceElement.textContent = finalTotal + " €"; } // Store the final total for the booking API (API expects total with fees) bookingPriceElement.setAttribute('data-price-booking', finalTotal); bookingPriceElement.setAttribute('data-total-amount', finalTotal); // Also store the raw total_amount from API (without fees added by us) bookingPriceElement.setAttribute('data-total-amount-base', subtotalAmount); } // Subtotal is the amount from API (before fees) const subtotal = subtotalAmount; // Total is the final amount (subtotal + fees) const total = finalTotal; // Mettre à jour tous les éléments [data-subtotal-price] document.querySelectorAll('[data-subtotal-price]').forEach(el => { if (el.tagName === 'INPUT') { el.value = subtotal; } else { el.textContent = subtotal + " €"; } el.setAttribute('data-subtotal-price', subtotal); }); // Mettre à jour tous les éléments [data-total-price] document.querySelectorAll('[data-total-price]').forEach(el => { if (el.tagName === 'INPUT') { el.value = total; } else { el.textContent = total + " €"; } el.setAttribute('data-total-price', total); }); } function updateDateInUrl(dateValue) { const url = new URL(window.location.href); const params = new URLSearchParams(url.search); params.delete('date'); if (dateValue && dateValue.trim() !== '') { params.set('date', dateValue); } url.search = params.toString(); window.history.replaceState({}, '', url); // Show loader before making API call console.log('🔄 Starting availability check...'); showLoader(); checkAvailability(); } function fillDateFromUrl() { const params = new URLSearchParams(window.location.search); const dateParam = params.get('date'); if (dateParam && dateInput) { dateInput.value = dateParam; if (dateInput._flatpickr) { const dates = dateParam.split(' to '); if (dates.length === 2) { const startDate = new Date(dates[0]); const endDate = new Date(dates[1]); dateInput._flatpickr.setDate([startDate, endDate]); } } } } function updateDateElements(dateValue) { if (dateValue) { const dateParts = dateValue.split(' to '); if (dateParts.length === 2) { const dateFrom = dateParts[0].replace(/\+/g, ' ').trim(); const dateTo = dateParts[1].replace(/\+/g, ' ').trim(); const nights = calculateNights(dateFrom, dateTo); updateDateElementsWithValues(dateFrom, dateTo, nights); } } } // Update visual elements only (no API calls) function updateDateElementsOnly(dateValue) { if (dateValue) { const dateParts = dateValue.split(' to '); if (dateParts.length === 2) { const dateFrom = dateParts[0].replace(/\+/g, ' ').trim(); const dateTo = dateParts[1].replace(/\+/g, ' ').trim(); const nights = calculateNights(dateFrom, dateTo); // Only update visual date displays, don't trigger availability check console.log('📅 Updating date displays only (no API call)'); updateDateElementsWithValues(dateFrom, dateTo, nights); } else if (dateParts.length === 1 && dateParts[0].trim()) { // Single date selected, show only the start date const dateFrom = dateParts[0].replace(/\+/g, ' ').trim(); console.log('📅 Single date selected:', dateFrom); // Update only the from date, clear the to date const dateFromElements = document.querySelectorAll('[js-form-usequery=\"date-from\"]'); const dateToElements = document.querySelectorAll('[js-form-usequery=\"date-to\"]'); dateFromElements.forEach(el => el.tagName === 'INPUT' ? el.value = dateFrom : el.textContent = dateFrom); dateToElements.forEach(el => el.tagName === 'INPUT' ? el.value = '' : el.textContent = 'Select end date'); } } } function updateDateElementsFromUrl() { const params = new URLSearchParams(window.location.search); const dateParam = params.get('date'); if (dateParam) { updateDateElements(dateParam); } } function updateDateElementsWithValues(dateFrom, dateTo, nights) { const dateFromElements = document.querySelectorAll('[js-form-usequery="date-from"]'); const dateToElements = document.querySelectorAll('[js-form-usequery="date-to"]'); const nightsElements = document.querySelectorAll('[data-nombre-nuit]'); // Format dates to French format const formatDateToFrench = (dateStr) => { const date = new Date(dateStr + 'T12:00:00'); // Add noon to avoid timezone issues return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }); }; const frenchDateFrom = formatDateToFrench(dateFrom); const frenchDateTo = formatDateToFrench(dateTo); console.log('📅 Formatting dates to French:', { original_from: dateFrom, original_to: dateTo, french_from: frenchDateFrom, french_to: frenchDateTo }); dateFromElements.forEach(el => el.tagName === 'INPUT' ? el.value = frenchDateFrom : el.textContent = frenchDateFrom); dateToElements.forEach(el => el.tagName === 'INPUT' ? el.value = frenchDateTo : el.textContent = frenchDateTo); nightsElements.forEach(el => el.tagName === 'INPUT' ? el.value = nights : el.textContent = nights); nightsElements.forEach(el => el.setAttribute('data-nombre-nuit', nights)); // Prices will be updated when availability is checked } // Guest counter change listener - refresh availability when guest count changes const adultsInput = document.querySelector('#adults'); const childrenInput = document.querySelector('#children'); const babiesInput = document.querySelector('#babies'); const guestToggle = document.querySelector('#w-dropdown-toggle-3 > div:first-child'); const guestForm = document.querySelector('[data-guest-max]'); // Function to update guest display on dropdown toggle function updateGuestDisplay() { const adults = parseInt(adultsInput?.value) || 1; const children = parseInt(childrenInput?.value) || 0; const babies = parseInt(babiesInput?.value) || 0; const totalGuests = adults + children + babies; if (guestToggle && totalGuests > 0) { guestToggle.textContent = `${totalGuests} guest${totalGuests > 1 ? 's' : ''}`; } } // Get max guests from the form's data-guest-max attribute let maxGuests = 20; // Default fallback if (guestForm) { const maxGuestsAttr = guestForm.getAttribute('data-guest-max'); if (maxGuestsAttr && !isNaN(parseInt(maxGuestsAttr))) { maxGuests = parseInt(maxGuestsAttr); console.log('🔢 Maximum guests allowed for this apartment:', maxGuests); } } // Fill guest inputs from URL parameters on page load const urlParams2 = new URLSearchParams(window.location.search); const adultsParam = urlParams2.get('adults'); const childrenParam = urlParams2.get('children'); const babiesParam = urlParams2.get('babies'); if (adultsParam && adultsInput) adultsInput.value = adultsParam; if (childrenParam && childrenInput) childrenInput.value = childrenParam; if (babiesParam && babiesInput) babiesInput.value = babiesParam; // Update guest display on page load updateGuestDisplay(); console.log('👥 Filled guest inputs from URL parameters:', { adults: adultsParam, children: childrenParam, babies: babiesParam }); // Function to get total guests function getTotalGuests() { const adults = parseInt(adultsInput?.value) || 1; const children = parseInt(childrenInput?.value) || 0; const babies = parseInt(babiesInput?.value) || 0; return adults + children + babies; } // Function to enforce guest limit based on total const enforceGuestLimit = function() { const totalGuests = getTotalGuests(); if (totalGuests > maxGuests) { console.log('⚠️ Total guests exceeded maximum:', totalGuests, '>', maxGuests); // We don't auto-correct here, just warn - the API will handle validation return false; } return true; }; // Function to handle guest count change const handleGuestCountChange = function() { // First, enforce the limit enforceGuestLimit(); const adults = adultsInput?.value || '1'; const children = childrenInput?.value || '0'; const babies = babiesInput?.value || '0'; const totalGuests = getTotalGuests(); console.log('👥 Guest count changed:', { adults, children, babies, total: totalGuests }); // Update URL with new guest counts const url = new URL(window.location.href); const params = new URLSearchParams(url.search); params.delete('guest'); // Remove old param if exists params.set('adults', adults); if (children !== '0') params.set('children', children); else params.delete('children'); if (babies !== '0') params.set('babies', babies); else params.delete('babies'); url.search = params.toString(); window.history.replaceState({}, '', url); // Update guest display updateGuestDisplay(); // Refresh availability map with new guest count console.log('🔄 Refreshing availability map with new guest count'); fetchAvailabilityMap(); // Recheck availability with new guest count if dates are selected const dateParam = params.get('date'); if (dateParam && dateParam.includes(' to ')) { console.log('🔄 Refreshing availability with new guest count'); // Show the small loader inside booking component const bookingLoader = document.querySelector('[data-wf--loader--variant="extra-small"]'); if (bookingLoader) { bookingLoader.style.display = 'flex'; console.log('✅ Showing booking component loader'); } checkAvailability(); } }; // Add listeners to all guest inputs [adultsInput, childrenInput, babiesInput].forEach(input => { if (input) { input.addEventListener('change', handleGuestCountChange); input.addEventListener('input', function() { // Debounce the API call slightly to avoid too many requests clearTimeout(window.guestChangeTimeout); window.guestChangeTimeout = setTimeout(handleGuestCountChange, 500); }); } }); console.log('✅ Guest counter listeners added'); // Attach listeners to all increment/decrement buttons const guestCounterButtons = [ { increment: '[fs-inputcounter-element="increment-2"]', decrement: '[fs-inputcounter-element="decrement-2"]' }, // Adults { increment: '[fs-inputcounter-element="increment-3"]', decrement: '[fs-inputcounter-element="decrement-3"]' }, // Children { increment: '[fs-inputcounter-element="increment-4"]', decrement: '[fs-inputcounter-element="decrement-4"]' } // Babies ]; guestCounterButtons.forEach(({ increment, decrement }) => { const incrementBtn = document.querySelector(increment); const decrementBtn = document.querySelector(decrement); if (incrementBtn) { incrementBtn.addEventListener('click', function() { setTimeout(handleGuestCountChange, 100); }); } if (decrementBtn) { decrementBtn.addEventListener('click', function() { setTimeout(handleGuestCountChange, 100); }); } }); console.log('✅ Increment/decrement button listeners added'); // Booking functionality const bookButton = document.getElementById('cta-book'); if (bookButton) { bookButton.addEventListener('click', (event) => { event.preventDefault(); console.log('Bouton de réservation cliqué, lancement de la requête...'); makeBooking(); }); console.log('Écouteur d\'événements ajouté au bouton de réservation'); } else { console.warn('Le bouton avec l\'ID "cta-book" n\'a pas été trouvé dans la page'); } /** * Function to make a booking */ function makeBooking() { // Show the loader const loader = document.getElementById('loader'); if (loader) { loader.style.display = 'flex'; } // Récupérer les dates et informations depuis l'URL ou les éléments de la page const params = new URLSearchParams(window.location.search); // Dates const dateParam = params.get('date'); let checkinDate = "2025-10-24"; let checkoutDate = "2025-10-29"; if (dateParam) { const dateParts = dateParam.split(' to '); if (dateParts.length === 2) { checkinDate = dateParts[0].replace(/\+/g, ' ').trim(); checkoutDate = dateParts[1].replace(/\+/g, ' ').trim(); } } // Nombre d'invités const adultsParam = params.get('adults'); const childrenParam = params.get('children'); const babiesParam = params.get('babies'); console.log('📋 makeBooking - URL params:', { adultsParam, childrenParam, babiesParam }); const nbAdults = adultsParam ? parseInt(adultsParam) : 1; const nbChildren = childrenParam ? parseInt(childrenParam) : 0; const nbBabies = babiesParam ? parseInt(babiesParam) : 0; console.log('📋 makeBooking - Guest counts:', { nbAdults, nbChildren, nbBabies }); // Check which rate is selected from radio buttons const selectedRateRadio = document.querySelector('input[name="type"]:checked'); const selectedRateType = selectedRateRadio ? selectedRateRadio.value : 'flexible'; console.log('📻 Selected rate type:', selectedRateType); // Get pricing from stored options let priceBooking = '10'; // Default fallback let rateName = 'tarif-flexible'; // Default rate name if (window.pricingOptions) { const selectedRate = selectedRateType === 'flexible' ? window.pricingOptions.flexible : window.pricingOptions.nonFlexible; if (selectedRate) { priceBooking = selectedRate.total_amount.toString(); rateName = selectedRate.short_name; console.log('✅ Using selected rate:', { type: selectedRateType, rateName: rateName, price: priceBooking }); } else { console.warn('⚠️ Selected rate not found in pricing options, using fallback'); } } else { console.warn('⚠️ No pricing options stored, using fallback price extraction'); } // Fallback: try to get price from old price-booking element if pricing options not available const priceBookingElement = document.getElementById('price-booking'); if (priceBookingElement) { // PRIORITY 1: Use data-total-amount-base (original API price without added fees) // This is what PickAFlat expects - the exact total_amount they returned const totalAmountBase = priceBookingElement.getAttribute('data-total-amount-base'); const dataBookingPrice = priceBookingElement.getAttribute('data-price-booking'); const totalAmount = priceBookingElement.getAttribute('data-total-amount'); console.log('🔍 Price attributes found:', { totalAmountBase, dataBookingPrice, totalAmount }); if (totalAmountBase && !isNaN(parseFloat(totalAmountBase))) { // Use the original API price (what PickAFlat expects) priceBooking = parseFloat(totalAmountBase).toString(); console.log('🏷️ Using data-total-amount-base (original API price):', priceBooking); } else if (dataBookingPrice && !isNaN(parseFloat(dataBookingPrice))) { priceBooking = parseFloat(dataBookingPrice).toString(); console.log('🏷️ Using data-price-booking attribute:', priceBooking); } else if (totalAmount && !isNaN(parseFloat(totalAmount))) { priceBooking = parseFloat(totalAmount).toString(); console.log('🏷️ Using data-total-amount attribute:', priceBooking); } else { // PRIORITY 3: Robust fallback with improved mobile compatibility console.log('⚠️ Using fallback price extraction (mobile-safe)'); // Get raw text content let rawPrice = priceBookingElement.textContent || priceBookingElement.value || priceBookingElement.innerText || '10'; console.log('Raw price text:', rawPrice); // Mobile-safe price extraction: more conservative approach // Remove all non-numeric characters except dots and commas let cleanPrice = rawPrice.replace(/[^\d.,]/g, ''); console.log('After removing non-numeric:', cleanPrice); // Handle European format (comma as decimal separator) if (cleanPrice.includes(',') && cleanPrice.includes('.')) { // Format like "1.234,56" - comma is decimal separator cleanPrice = cleanPrice.replace(/\./g, '').replace(',', '.'); } else if (cleanPrice.includes(',') && !cleanPrice.includes('.')) { // Format like "1234,56" - comma is decimal separator cleanPrice = cleanPrice.replace(',', '.'); } // If only dots, assume US format (dots as decimal separator) console.log('After decimal handling:', cleanPrice); // Parse and validate const parsedPrice = parseFloat(cleanPrice); if (!isNaN(parsedPrice) && parsedPrice > 0) { priceBooking = parsedPrice.toString(); console.log('✅ Successfully parsed fallback price:', priceBooking); } else { console.warn('❌ Failed to parse price, using default'); priceBooking = '10'; } } console.log('💰 Final booking price to send to API:', priceBooking); } else { console.warn('Élément price-booking non trouvé, utilisation de la valeur par défaut'); } // Get apartment ID dynamically const apartmentId = getApartmentId(); const appartTestMathias = 209948; if(apartmentId === appartTestMathias) { priceBooking = 6; } // Use the exact price from the UI - round to integer as PickAFlat expects const finalPrice = Math.round(parseFloat(priceBooking)); console.log('💰 Booking with price:', finalPrice, '(raw:', priceBooking, ')'); // Nouvelle URL de l'endpoint booking avec l'ID dynamique const bookingUrl = `${CLOUDFLARE_WORKER_URL}/booking/${apartmentId}`; // Create booking data const bookingData = { checkin_date: checkinDate, checkout_date: checkoutDate, nb_adults: nbAdults, nb_children: nbChildren, nb_babies: nbBabies, rate_name: rateName, price: finalPrice }; console.log('🚀 Sending booking request:', bookingData); // Single booking request fetch(bookingUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(bookingData) }) .then(response => { if (response.ok) { return response.json(); } else { return response.json().then(errorData => { throw new Error(errorData.message || errorData.error || 'Booking failed'); }); } }) .then(data => { console.log('✅ Booking successful!'); console.log('=== DÉTAILS DE LA RÉSERVATION ==='); console.log(data); // Hide the loader if (loader) { loader.style.display = 'none'; } // Créer l'URL avec les paramètres client if (data.payment_url) { const paymentUrlObj = new URL(data.payment_url); // Ajouter les paramètres client sans le numéro de téléphone if (data.customer) { paymentUrlObj.searchParams.set('email', data.customer.email_address); paymentUrlObj.searchParams.set('first_name', data.customer.first_name); paymentUrlObj.searchParams.set('last_name', data.customer.last_name); } // Ajouter le paramètre price-booking paymentUrlObj.searchParams.set('price-booking', finalPrice); // Construction manuelle de l'URL pour éviter le double encodage let redirectUrl = paymentUrlObj.origin + paymentUrlObj.pathname; let params = []; for (const [key, value] of paymentUrlObj.searchParams.entries()) { params.push(`${key}=${value}`); } if (params.length > 0) { redirectUrl += '?' + params.join('&'); } console.log('🔗 Redirection vers:', redirectUrl); // Stocker les informations de réservation dans sessionStorage sessionStorage.setItem('lastBooking', JSON.stringify(data)); // Rediriger vers l'URL de paiement setTimeout(() => { window.location.href = redirectUrl; }, 200); } else { console.warn('⚠️ Aucune URL de paiement trouvée dans la réponse'); alert('Réservation créée mais aucune URL de paiement trouvée. Contactez le support.'); } }) .catch(error => { // Hide the loader in case of error if (loader) { loader.style.display = 'none'; } console.error('❌ ERREUR DE RÉSERVATION:', error.message); alert(`Erreur lors de la réservation: ${error.message}. Veuillez réessayer.`); }); } // Loader management functions function showLoader() { const loader = document.getElementById('loader'); if (loader) { loader.style.display = 'flex'; console.log('💫 Showing loader'); } // Hide the waiting-api element during API requests const waitingApiElement = document.getElementById('waiting-api'); if (waitingApiElement) { waitingApiElement.style.display = 'none'; console.log('🔒 Hiding waiting-api element during API request'); } } function hideLoader() { const loader = document.getElementById('loader'); if (loader) { loader.style.display = 'none'; console.log('✅ Hiding loader'); } // Show the waiting-api element when API request completes const waitingApiElement = document.getElementById('waiting-api'); if (waitingApiElement) { waitingApiElement.style.display = 'block'; console.log('🔓 Showing waiting-api element after API request'); } } function showSelectDatesMessage() { // Hide not-available elements when no dates are selected document.querySelectorAll('.not-available').forEach(el => { el.style.display = 'none'; }); // Hide booking component when no dates are selected document.querySelectorAll('.item_apartment-book-component').forEach(el => { el.style.display = 'none'; }); console.log('🚫 Hiding booking component - no dates selected'); // Show a message encouraging users to select dates const priceElements = document.querySelectorAll('[data-price]'); priceElements.forEach(el => { if (el.tagName === 'INPUT') { el.value = ''; } else { el.textContent = 'Select dates to see pricing'; } }); // Clear other pricing displays document.querySelectorAll('[data-subtotal-price]').forEach(el => { if (el.tagName === 'INPUT') { el.value = ''; } else { el.textContent = 'Select dates'; } }); document.querySelectorAll('[data-total-price]').forEach(el => { if (el.tagName === 'INPUT') { el.value = ''; } else { el.textContent = 'Select dates'; } }); console.log('💡 Showing select dates message - no pricing displayed'); } });