273 lines
9.0 KiB
JavaScript
273 lines
9.0 KiB
JavaScript
/**
|
|
* 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 = $('<div class="yc-event-title"></div>').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('<div class="booking-error" style="padding: 12px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px;"><strong>' +
|
|
(i18n.errorTitle || 'Błąd!') + '</strong> Wybierz jacht z listy.</div>');
|
|
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('<div class="booking-success" style="padding: 12px; background: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 4px;"><strong>' +
|
|
(i18n.successTitle || 'Sukces!') + '</strong> ' +
|
|
((resp && resp.message) || i18n.inquirySuccess || 'Twoje zapytanie zostało wysłane.') +
|
|
'</div>');
|
|
$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('<div class="booking-error" style="padding: 12px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px;"><strong>' +
|
|
(i18n.errorTitle || 'Błąd!') + '</strong> ' + msg + '</div>');
|
|
},
|
|
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);
|