/** * Yacht Calendar (All) — wspólny widok wszystkich jachtów. * * Inicjalizuje FullCalendar dayGridMonth dla każdego elementu .yacht-calendar-all-wrapper * na stronie. Pobiera eventy z REST `/availability/all` (timed 12:00→12:00), renderuje * legendę kolorów wyciągniętą z eventów i dodaje klasy half-day na pierwszym/ostatnim * dniu każdej rezerwacji. */ (function ($) { 'use strict'; if (typeof window.FullCalendar === 'undefined') { return; } function pad(n) { return n < 10 ? '0' + n : '' + n; } function ymd(date) { return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()); } function firstOfMonth(d) { return new Date(d.getFullYear(), d.getMonth(), 1); } function nextMonthFirst(d) { return new Date(d.getFullYear(), d.getMonth() + 1, 1); } function initCalendar(wrapper) { var $wrapper = $(wrapper); var $cal = $wrapper.find('.yacht-calendar-all'); if (!$cal.length) return; var restUrl = $wrapper.data('rest'); if (!restUrl) return; var heightPx = parseInt($wrapper.data('height'), 10); if (!heightPx || heightPx < 200) heightPx = 650; // Bounds: pobierz max_booking_date żeby ograniczyć validRange w FullCalendar. // Endpoint `/availability/bounds` jest siostrzany do `/availability/all`. var boundsUrl = restUrl.replace(/\/availability\/all.*$/, '/availability/bounds'); $.getJSON(boundsUrl) .done(function (data) { buildCalendar($wrapper, $cal, restUrl, heightPx, data && data.max_booking_date ? data.max_booking_date : null); }) .fail(function () { // Graceful degradation — kalendarz bez validRange. buildCalendar($wrapper, $cal, restUrl, heightPx, null); }); } function buildCalendar($wrapper, $cal, restUrl, heightPx, maxBookingDate) { var today = new Date(); var rangeStart = firstOfMonth(today); var rangeEnd; if (maxBookingDate) { var maxDate = new Date(maxBookingDate + 'T00:00:00'); rangeEnd = maxDate >= today ? nextMonthFirst(maxDate) : nextMonthFirst(today); } else { rangeEnd = nextMonthFirst(today); } var calendar = new window.FullCalendar.Calendar($cal.get(0), { initialView: 'dayGridMonth', locale: 'pl', firstDay: 1, headerToolbar: { left: 'prev,next today', center: 'title', right: '' }, validRange: { start: rangeStart, end: rangeEnd }, height: heightPx, displayEventTime: false, eventDisplay: 'block', events: function (fetchInfo, successCallback, failureCallback) { var url = restUrl + (restUrl.indexOf('?') === -1 ? '?' : '&') + 'start=' + ymd(fetchInfo.start) + '&end=' + ymd(fetchInfo.end); $.getJSON(url) .done(function (data) { successCallback(data || []); }) .fail(function () { failureCallback(new Error('Failed to fetch /availability/all')); }); }, eventDidMount: function (info) { // Per-event color via CSS variable. var color = info.event.backgroundColor || '#3498db'; info.el.style.setProperty('--yc-event-color', color); // Native tooltip na hover — tytuł + data dnia. var dayStr = info.event.startStr ? info.event.startStr.slice(0, 10) : ''; info.el.setAttribute('title', info.event.title + (dayStr ? ' (' + dayStr + ')' : '')); // Half-day visual: gradient z half-cell transparent na pierwszym/ostatnim dniu. // Liczone po renderze (wymaga znajomości szerokości komórki dnia w siatce). // Przekładamy na requestAnimationFrame żeby DOM był ułożony. window.requestAnimationFrame(function () { applyHalfDayGradient(info); }); }, eventContent: function (arg) { // Tytuł renderowany przez .text() → automatyczny escaping (XSS safe). var title = arg.event.title || ''; var $el = $('
').text(title); return { domNodes: [$el.get(0)] }; } }); calendar.render(); // Inquiry form submission (yacht select dropdown). var $form = $wrapper.find('.yacht-calendar-all-inquiry-form'); if ($form.length) { $form.on('submit', function (e) { e.preventDefault(); var $submitBtn = $form.find('.yacht-booking-submit'); var $response = $form.find('.yacht-calendar-all-inquiry-response'); var originalBtnText = $submitBtn.text(); var i18n = (window.yachtBookingData && window.yachtBookingData.i18n) || {}; var yachtId = parseInt($form.find('[name="yacht_id"]').val(), 10) || 0; if (!yachtId) { $response.html('
' + (i18n.errorTitle || 'Błąd!') + ' Wybierz jacht z listy.
'); return; } $submitBtn.prop('disabled', true).text(i18n.submitting || 'Wysyłanie...'); $response.html(''); var formData = { yacht_id: yachtId, customer_name: $form.find('[name="customer_name"]').val(), customer_email: $form.find('[name="customer_email"]').val(), customer_phone: $form.find('[name="customer_phone"]').val(), preferred_dates: $form.find('[name="preferred_dates"]').val(), message: $form.find('[name="message"]').val() }; $.ajax({ url: window.yachtBookingData.apiUrl + '/inquiries', method: 'POST', beforeSend: function (xhr) { xhr.setRequestHeader('X-WP-Nonce', window.yachtBookingData.nonce); }, contentType: 'application/json', data: JSON.stringify(formData), success: function (resp) { $response.html('
' + (i18n.successTitle || 'Sukces!') + ' ' + ((resp && resp.message) || i18n.inquirySuccess || 'Twoje zapytanie zostało wysłane.') + '
'); $form[0].reset(); }, error: function (xhr) { var msg = i18n.errorMessage || 'Wystąpił błąd. Spróbuj ponownie.'; if (xhr.responseJSON && xhr.responseJSON.message) { msg = xhr.responseJSON.message; } $response.html('
' + (i18n.errorTitle || 'Błąd!') + ' ' + msg + '
'); }, complete: function () { $submitBtn.prop('disabled', false).text(originalBtnText); } }); }); } } /** * Apply half-day gradient to event bar. Pierwsze dni rezerwacji: lewa połowa pierwszej * komórki transparentna (yacht wraca w południe). Ostatnie: prawa połowa ostatniej * komórki transparentna (yacht wypływa w południe). Dla segmentów środkowych — pełny kolor. */ function applyHalfDayGradient(info) { var el = info.el; var rect = el.getBoundingClientRect(); if (!rect.width) return; // Find a sibling day cell to read its width. var dayCell = el.closest('.fc-daygrid-day') || el.closest('td'); // Fallback: scan parent for any .fc-daygrid-day sibling. if (!dayCell) { var grid = el.closest('.fc-daygrid'); if (grid) { dayCell = grid.querySelector('.fc-daygrid-day'); } } if (!dayCell) return; var cellRect = dayCell.getBoundingClientRect(); if (!cellRect.width) return; var halfPct = (cellRect.width / 2) / rect.width * 100; // Clamp to safe bounds. if (halfPct < 1) halfPct = 1; if (halfPct > 49) halfPct = 49; var color = el.style.getPropertyValue('--yc-event-color') || '#3498db'; // Server emituje 1 event per doba — info.isStart/isEnd byłyby zawsze true. // Flagi half-day pochodzą z extendedProps (server wie czy to pierwszy/ostatni dzień rezerwacji). var props = info.event.extendedProps || {}; var startTrans = !!props.is_first; var endTrans = !!props.is_last_night; // Build gradient. var stops; if (startTrans && endTrans) { // Single segment containing both start and end. stops = [ 'transparent 0%', 'transparent ' + halfPct.toFixed(2) + '%', color + ' ' + halfPct.toFixed(2) + '%', color + ' ' + (100 - halfPct).toFixed(2) + '%', 'transparent ' + (100 - halfPct).toFixed(2) + '%', 'transparent 100%' ]; } else if (startTrans) { stops = [ 'transparent 0%', 'transparent ' + halfPct.toFixed(2) + '%', color + ' ' + halfPct.toFixed(2) + '%', color + ' 100%' ]; } else if (endTrans) { stops = [ color + ' 0%', color + ' ' + (100 - halfPct).toFixed(2) + '%', 'transparent ' + (100 - halfPct).toFixed(2) + '%', 'transparent 100%' ]; } else { // Middle segment — pełny kolor. el.style.backgroundColor = color; el.style.backgroundImage = 'none'; return; } el.style.backgroundImage = 'linear-gradient(to right, ' + stops.join(', ') + ')'; el.style.backgroundColor = 'transparent'; } function escapeHtml(str) { return String(str).replace(/[&<>"']/g, function (m) { return ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[m]; }); } $(function () { $('.yacht-calendar-all-wrapper').each(function () { initCalendar(this); }); }); })(jQuery);