/** * Calendar Component * Airbnb-style availability calendar with iCal synchronization */ class AvailabilityCalendar { constructor(containerId, options = {}) { this.container = document.getElementById(containerId); if (!this.container) return; this.options = { apiUrl: 'api/calendar.php', months: 2, locale: 'fr-FR', weekStart: 1, // Monday refreshInterval: 30 * 60 * 1000, // 30 minutes onDateSelect: null, ...options }; this.bookedDates = new Set(); this.currentMonth = new Date(); this.currentMonth.setDate(1); this.selectedStart = null; this.selectedEnd = null; this.isLoading = true; this.lastUpdated = null; this.weekDays = ['L', 'M', 'M', 'J', 'V', 'S', 'D']; this.init(); } async init() { this.render(); await this.fetchBookedDates(); this.render(); this.startAutoRefresh(); } async fetchBookedDates() { this.isLoading = true; this.render(); try { const response = await fetch(this.options.apiUrl); const data = await response.json(); if (data.success) { this.bookedDates = new Set(data.booked_dates || []); this.lastUpdated = data.last_updated; this.isLoading = false; } else { throw new Error(data.error || 'Failed to load calendar'); } } catch (error) { console.error('Calendar fetch error:', error); this.isLoading = false; this.renderError(); return; } } startAutoRefresh() { if (this.refreshTimer) clearInterval(this.refreshTimer); this.refreshTimer = setInterval(() => { this.fetchBookedDates().then(() => this.render()); }, this.options.refreshInterval); } render() { if (!this.container) return; if (this.isLoading) { this.container.innerHTML = `
Chargement du calendrier...
`; return; } const html = `
${this.renderMonth(this.currentMonth)} ${this.renderMonth(this.getNextMonth(this.currentMonth))}
Disponible
Réservé
${this.lastUpdated ? `
Mis à jour : ${this.lastUpdated}
` : ''} `; this.container.innerHTML = html; this.attachEvents(); } renderMonth(date) { const year = date.getFullYear(); const month = date.getMonth(); const monthName = date.toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); const daysInMonth = new Date(year, month + 1, 0).getDate(); const firstDayOfWeek = this.getAdjustedDay(new Date(year, month, 1).getDay()); let daysHtml = ''; // Weekday headers for (const day of this.weekDays) { daysHtml += `
${day}
`; } // Empty cells before first day for (let i = 0; i < firstDayOfWeek; i++) { daysHtml += '
'; } // Day cells const today = new Date(); today.setHours(0, 0, 0, 0); for (let day = 1; day <= daysInMonth; day++) { const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; const dateObj = new Date(year, month, day); const isPast = dateObj < today; const isToday = dateObj.getTime() === today.getTime(); const isBooked = this.bookedDates.has(dateStr); const isSelected = this.isDateSelected(dateStr); const isInRange = this.isDateInRange(dateStr); let classes = ['calendar-day']; if (isPast) classes.push('calendar-day--past'); if (isBooked && !isPast) classes.push('calendar-day--booked'); if (isToday) classes.push('calendar-day--today'); if (isSelected) classes.push('calendar-day--selected'); if (isInRange) classes.push('calendar-day--in-range'); if (isPast || isBooked) classes.push('calendar-day--disabled'); daysHtml += `
${day}
`; } return `
${this.isFirstMonth(date) ? ` ` : '
'} ${monthName.charAt(0).toUpperCase() + monthName.slice(1)} ${this.isSecondMonth(date) ? ` ` : '
'}
${daysHtml}
`; } renderError() { if (!this.container) return; this.container.innerHTML = `

Impossible de charger le calendrier de disponibilité.

`; } attachEvents() { const days = this.container.querySelectorAll('.calendar-day:not(.calendar-day--disabled):not(.calendar-day--empty)'); days.forEach(day => { day.addEventListener('click', (e) => { const dateStr = e.target.dataset.date; if (dateStr) this.selectDate(dateStr); }); }); } selectDate(dateStr) { if (!this.selectedStart || (this.selectedStart && this.selectedEnd)) { // Start new selection this.selectedStart = dateStr; this.selectedEnd = null; } else { // Complete the range if (dateStr < this.selectedStart) { this.selectedEnd = this.selectedStart; this.selectedStart = dateStr; } else if (dateStr === this.selectedStart) { this.selectedStart = null; } else { // Check if any booked date is in the range if (this.hasBookedDateInRange(this.selectedStart, dateStr)) { // Reset and start new selection this.selectedStart = dateStr; this.selectedEnd = null; } else { this.selectedEnd = dateStr; } } } this.render(); // Sync with booking widget if (this.options.onDateSelect) { this.options.onDateSelect(this.selectedStart, this.selectedEnd); } // Update booking widget inputs this.updateBookingWidget(); } updateBookingWidget() { const arrivalInput = document.getElementById('booking-arrival'); const departureInput = document.getElementById('booking-departure'); if (arrivalInput && this.selectedStart) { arrivalInput.value = this.formatDateFr(this.selectedStart); } if (departureInput && this.selectedEnd) { departureInput.value = this.formatDateFr(this.selectedEnd); } } formatDateFr(dateStr) { const date = new Date(dateStr); return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short', year: 'numeric' }); } hasBookedDateInRange(start, end) { const startDate = new Date(start); const endDate = new Date(end); const current = new Date(startDate); while (current <= endDate) { const dateStr = current.toISOString().split('T')[0]; if (this.bookedDates.has(dateStr)) return true; current.setDate(current.getDate() + 1); } return false; } isDateSelected(dateStr) { return dateStr === this.selectedStart || dateStr === this.selectedEnd; } isDateInRange(dateStr) { if (!this.selectedStart || !this.selectedEnd) return false; return dateStr > this.selectedStart && dateStr < this.selectedEnd; } getAdjustedDay(day) { // Convert Sunday=0 to Monday=0 system return (day + 6) % 7; } getNextMonth(date) { const next = new Date(date); next.setMonth(next.getMonth() + 1); return next; } isFirstMonth(date) { return date.getMonth() === this.currentMonth.getMonth() && date.getFullYear() === this.currentMonth.getFullYear(); } isSecondMonth(date) { const next = this.getNextMonth(this.currentMonth); return date.getMonth() === next.getMonth() && date.getFullYear() === next.getFullYear(); } prevMonth() { const today = new Date(); today.setDate(1); today.setHours(0, 0, 0, 0); const prev = new Date(this.currentMonth); prev.setMonth(prev.getMonth() - 1); if (prev >= today) { this.currentMonth = prev; } else { this.currentMonth = today; } this.render(); } nextMonth() { this.currentMonth.setMonth(this.currentMonth.getMonth() + 1); this.render(); } async retry() { await this.fetchBookedDates(); this.render(); } getSelectedDates() { return { start: this.selectedStart, end: this.selectedEnd }; } destroy() { if (this.refreshTimer) { clearInterval(this.refreshTimer); } } } // Global calendar instance let calendar; document.addEventListener('DOMContentLoaded', function() { calendar = new AvailabilityCalendar('availability-calendar', { apiUrl: 'api/calendar.php', onDateSelect: function(start, end) { // Update booking widget when dates are selected const arrivalInput = document.getElementById('booking-arrival'); const departureInput = document.getElementById('booking-departure'); if (arrivalInput) { arrivalInput.value = start ? formatDateShort(start) : ''; arrivalInput.placeholder = start ? '' : 'Ajouter un...'; } if (departureInput) { departureInput.value = end ? formatDateShort(end) : ''; departureInput.placeholder = end ? '' : 'Ajouter un...'; } } }); }); function formatDateShort(dateStr) { const date = new Date(dateStr); return date.toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }); }