Sekcja Elementor zawierająca modal miała elementor-hidden-mobile/tablet, co powodowało display:none na rodzicu. Modal position:fixed wewnątrz ukrytego elementu miał zerowe wymiary. Fix: przeniesienie overlay do document.body w initRefs(). Plan Phase 13 (pakiety ochronne) utworzony, BLOCKED — czeka na klienta. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1458 lines
65 KiB
JavaScript
1458 lines
65 KiB
JavaScript
(function () {
|
||
'use strict';
|
||
|
||
var REST_URL = (window.careiReservation && window.careiReservation.restUrl) || '/wp-json/carei/v1/';
|
||
var NONCE = (window.careiReservation && window.careiReservation.nonce) || '';
|
||
|
||
// ─── API Helpers ──────────────────────────────────────────────
|
||
|
||
var API_TIMEOUT_MS = 15000;
|
||
|
||
function apiGet(endpoint, isRetry) {
|
||
var controller = new AbortController();
|
||
var timer = setTimeout(function () { controller.abort(); }, API_TIMEOUT_MS);
|
||
return fetch(REST_URL + endpoint, {
|
||
method: 'GET',
|
||
headers: { 'X-WP-Nonce': NONCE, 'Content-Type': 'application/json' },
|
||
signal: controller.signal
|
||
}).then(function (r) {
|
||
clearTimeout(timer);
|
||
return handleResponse(r);
|
||
}).catch(function (err) {
|
||
clearTimeout(timer);
|
||
return handleFetchError(err, function () { return apiGet(endpoint, true); }, isRetry);
|
||
});
|
||
}
|
||
|
||
function apiPost(endpoint, data, isRetry) {
|
||
var controller = new AbortController();
|
||
var timer = setTimeout(function () { controller.abort(); }, API_TIMEOUT_MS);
|
||
return fetch(REST_URL + endpoint, {
|
||
method: 'POST',
|
||
headers: { 'X-WP-Nonce': NONCE, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data),
|
||
signal: controller.signal
|
||
}).then(function (r) {
|
||
clearTimeout(timer);
|
||
if ((r.status === 401 || r.status === 403) && !isRetry) {
|
||
return apiPost(endpoint, data, true);
|
||
}
|
||
return handleResponse(r);
|
||
}).catch(function (err) {
|
||
clearTimeout(timer);
|
||
return handleFetchError(err, function () { return apiPost(endpoint, data, true); }, isRetry);
|
||
});
|
||
}
|
||
|
||
function handleResponse(r) {
|
||
if (!r.ok) {
|
||
var status = r.status;
|
||
return r.json().then(function (body) {
|
||
var msg = (body && body.message) || (body && body.detail) || ('Błąd API: HTTP ' + status);
|
||
var err = new Error(msg);
|
||
err.httpStatus = status;
|
||
throw err;
|
||
}).catch(function (parseErr) {
|
||
if (parseErr.httpStatus) throw parseErr;
|
||
var err = new Error('Błąd API: HTTP ' + status);
|
||
err.httpStatus = status;
|
||
throw err;
|
||
});
|
||
}
|
||
return r.json();
|
||
}
|
||
|
||
function handleFetchError(err, retryFn, isRetry) {
|
||
if (err.name === 'AbortError') {
|
||
throw new Error('Przekroczono czas oczekiwania. Spróbuj ponownie.');
|
||
}
|
||
if (err instanceof TypeError && !isRetry) {
|
||
return retryFn();
|
||
}
|
||
if (err instanceof TypeError) {
|
||
throw new Error('Brak połączenia z serwerem. Sprawdź internet i spróbuj ponownie.');
|
||
}
|
||
throw err;
|
||
}
|
||
|
||
// ─── DOM Refs ─────────────────────────────────────────────────
|
||
|
||
var overlay, form, segmentSelect, dateFrom, dateTo, daysCount;
|
||
var pickupSelect, returnSelect, returnWrap, sameReturnCheck;
|
||
var extrasWrapper, extrasContainer, insuranceContainer, abroadSection, abroadToggle, abroadSearch, abroadInput, abroadResults, abroadSelected, errorSummary;
|
||
var summaryOverlay, summaryDetails, summaryTable, summaryTotal, summaryError;
|
||
var summaryBack, summaryConfirm;
|
||
var successView, successNumber, successClose;
|
||
|
||
function initRefs() {
|
||
overlay = document.querySelector('[data-carei-modal]');
|
||
// Move overlay to body so it's not trapped inside Elementor hidden sections on mobile/tablet
|
||
if (overlay && overlay.parentElement !== document.body) {
|
||
document.body.appendChild(overlay);
|
||
}
|
||
form = document.getElementById('carei-reservation-form');
|
||
segmentSelect = document.getElementById('carei-segment');
|
||
dateFrom = document.getElementById('carei-date-from');
|
||
dateTo = document.getElementById('carei-date-to');
|
||
daysCount = document.getElementById('carei-days-count');
|
||
pickupSelect = document.getElementById('carei-pickup-branch');
|
||
returnSelect = document.getElementById('carei-return-branch');
|
||
returnWrap = document.getElementById('carei-return-wrap');
|
||
sameReturnCheck = document.getElementById('carei-same-return');
|
||
extrasWrapper = document.getElementById('carei-extras-wrapper');
|
||
extrasContainer = document.getElementById('carei-extras-container');
|
||
insuranceContainer = document.getElementById('carei-insurance-container');
|
||
abroadSection = document.getElementById('carei-abroad-section');
|
||
abroadToggle = document.getElementById('carei-abroad-toggle');
|
||
abroadSearch = document.getElementById('carei-abroad-search');
|
||
abroadInput = document.getElementById('carei-abroad-input');
|
||
abroadResults = document.getElementById('carei-abroad-results');
|
||
abroadSelected = document.getElementById('carei-abroad-selected');
|
||
errorSummary = document.getElementById('carei-error-summary');
|
||
// Summary overlay
|
||
summaryOverlay = document.getElementById('carei-summary-overlay');
|
||
summaryDetails = document.getElementById('carei-summary-details');
|
||
summaryTable = document.getElementById('carei-summary-table');
|
||
summaryTotal = document.getElementById('carei-summary-total');
|
||
summaryError = document.getElementById('carei-summary-error');
|
||
summaryBack = document.getElementById('carei-summary-back');
|
||
summaryConfirm = document.getElementById('carei-summary-confirm');
|
||
// Success view
|
||
successView = document.getElementById('carei-success-view');
|
||
successNumber = document.getElementById('carei-success-number');
|
||
successClose = document.getElementById('carei-success-close');
|
||
}
|
||
|
||
// ─── State ────────────────────────────────────────────────────
|
||
|
||
var mapData = null;
|
||
var allSegments = [];
|
||
var currentPriceListId = null;
|
||
var currentCustomerId = null;
|
||
var currentReservationId = null;
|
||
var agreementDefs = [];
|
||
var lastFocusedElement = null;
|
||
var abroadItems = [];
|
||
var selectedCountries = {};
|
||
|
||
// ─── Modal Open/Close ─────────────────────────────────────────
|
||
|
||
function initModal() {
|
||
document.querySelectorAll('[data-carei-open-modal]').forEach(function (btn) {
|
||
btn.addEventListener('click', function (e) {
|
||
e.preventDefault();
|
||
openModal(btn);
|
||
});
|
||
});
|
||
document.querySelectorAll('[data-carei-close-modal]').forEach(function (btn) {
|
||
btn.addEventListener('click', closeModal);
|
||
});
|
||
if (overlay) {
|
||
overlay.addEventListener('click', function (e) {
|
||
if (e.target === overlay) closeModal();
|
||
});
|
||
overlay.addEventListener('keydown', handleFocusTrap);
|
||
}
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape' && overlay && overlay.classList.contains('is-open')) closeModal();
|
||
});
|
||
}
|
||
|
||
var dataLoaded = false;
|
||
|
||
function openModal(triggerBtn) {
|
||
if (!overlay) return;
|
||
lastFocusedElement = triggerBtn || document.activeElement;
|
||
overlay.classList.add('is-open');
|
||
document.body.style.overflow = 'hidden';
|
||
if (!dataLoaded) {
|
||
loadInitialData();
|
||
initDateLabels();
|
||
dataLoaded = true;
|
||
}
|
||
enforceDateMin();
|
||
|
||
// Pre-select segment from trigger attribute
|
||
var presetSegment = triggerBtn && triggerBtn.getAttribute('segment');
|
||
if (presetSegment && segmentSelect) {
|
||
var trySet = function () {
|
||
if (segmentSelect.querySelector('option[value="' + presetSegment + '"]')) {
|
||
segmentSelect.value = presetSegment;
|
||
segmentSelect.dispatchEvent(new Event('change'));
|
||
return true;
|
||
}
|
||
return false;
|
||
};
|
||
if (!trySet()) {
|
||
var attempts = 0;
|
||
var iv = setInterval(function () {
|
||
if (trySet() || ++attempts > 20) clearInterval(iv);
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
setTimeout(function () {
|
||
if (segmentSelect) segmentSelect.focus();
|
||
}, 350);
|
||
}
|
||
|
||
function closeModal() {
|
||
if (!overlay) return;
|
||
overlay.classList.remove('is-open');
|
||
document.body.style.overflow = '';
|
||
if (lastFocusedElement && lastFocusedElement.focus) {
|
||
setTimeout(function () { lastFocusedElement.focus(); }, 300);
|
||
}
|
||
}
|
||
|
||
// ─── Focus Trap ─────────────────────────────────────────────
|
||
|
||
var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||
|
||
function handleFocusTrap(e) {
|
||
if (e.key !== 'Tab') return;
|
||
var modal = overlay.querySelector('.carei-modal');
|
||
if (!modal) return;
|
||
var focusable = Array.prototype.slice.call(modal.querySelectorAll(FOCUSABLE_SELECTOR)).filter(function (el) {
|
||
return el.offsetParent !== null;
|
||
});
|
||
if (focusable.length === 0) return;
|
||
var first = focusable[0];
|
||
var last = focusable[focusable.length - 1];
|
||
if (e.shiftKey) {
|
||
if (document.activeElement === first) { e.preventDefault(); last.focus(); }
|
||
} else {
|
||
if (document.activeElement === last) { e.preventDefault(); first.focus(); }
|
||
}
|
||
}
|
||
|
||
// ─── Initial Data Loading ─────────────────────────────────────
|
||
|
||
function loadInitialData() {
|
||
if (segmentSelect) setSelectLoading(segmentSelect, true);
|
||
|
||
Promise.all([
|
||
apiGet('car-classes-all'),
|
||
apiGet('segments-branches-map'),
|
||
apiGet('agreements')
|
||
]).then(function (results) {
|
||
var classes = results[0];
|
||
mapData = results[1];
|
||
agreementDefs = Array.isArray(results[2]) ? results[2] : [];
|
||
|
||
if (Array.isArray(classes) && classes.length > 0) {
|
||
allSegments = classes.map(function (c) {
|
||
var val = typeof c === 'string' ? c : (c.name || c);
|
||
var label = typeof c === 'string' ? ('Segment ' + c) : (c.description || c.name || c);
|
||
return { value: val, label: label };
|
||
});
|
||
populateSelect(segmentSelect, allSegments, 'Wybierz segment pojazdu');
|
||
} else {
|
||
populateSelect(segmentSelect, [], 'Brak segmentów');
|
||
}
|
||
if (segmentSelect) setSelectLoading(segmentSelect, false);
|
||
|
||
if (pickupSelect) {
|
||
populateSelect(pickupSelect, [], 'Najpierw wybierz segment');
|
||
pickupSelect.disabled = true;
|
||
}
|
||
}).catch(function (err) {
|
||
console.error('Failed to load initial data:', err);
|
||
if (segmentSelect) {
|
||
populateSelect(segmentSelect, [], 'Błąd ładowania');
|
||
setSelectLoading(segmentSelect, false);
|
||
}
|
||
});
|
||
}
|
||
|
||
// ─── Segment Change → Filter Pickup Locations ─────────────────
|
||
|
||
function onSegmentChange() {
|
||
var selectedSegment = segmentSelect ? segmentSelect.value : '';
|
||
if (!selectedSegment || !mapData || !pickupSelect) {
|
||
if (pickupSelect) { populateSelect(pickupSelect, [], 'Najpierw wybierz segment'); pickupSelect.disabled = true; }
|
||
hideExtras();
|
||
return;
|
||
}
|
||
var segBranches = mapData.segmentToBranches[selectedSegment] || [];
|
||
var allBranches = mapData.branches || [];
|
||
var filteredOptions = [];
|
||
allBranches.forEach(function (b) {
|
||
if (segBranches.indexOf(b.name || '') !== -1) {
|
||
var label = b.description || b.name;
|
||
if (b.city) label += ' — ' + b.city;
|
||
filteredOptions.push({ value: b.name, label: label });
|
||
}
|
||
});
|
||
if (filteredOptions.length > 0) {
|
||
populateSelect(pickupSelect, filteredOptions, 'Miejsce odbioru');
|
||
pickupSelect.disabled = false;
|
||
} else {
|
||
populateSelect(pickupSelect, [], 'Brak lokalizacji dla tego segmentu');
|
||
pickupSelect.disabled = true;
|
||
}
|
||
if (returnSelect) {
|
||
var returnOptions = allBranches.map(function (b) {
|
||
var label = b.description || b.name;
|
||
if (b.city) label += ' — ' + b.city;
|
||
return { value: b.name, label: label };
|
||
});
|
||
populateSelect(returnSelect, returnOptions, 'Miejsce zwrotu');
|
||
}
|
||
hideExtras();
|
||
}
|
||
|
||
function onPickupChange() {
|
||
maybeShowExtras();
|
||
}
|
||
|
||
function maybeShowExtras() {
|
||
var segment = segmentSelect ? segmentSelect.value : '';
|
||
var pickup = pickupSelect ? pickupSelect.value : '';
|
||
var from = dateFrom ? dateFrom.value : '';
|
||
var to = dateTo ? dateTo.value : '';
|
||
if (segment && pickup && from && to) { showExtras(); loadExtras(); } else { hideExtras(); }
|
||
}
|
||
|
||
function showExtras() { if (extrasWrapper) extrasWrapper.style.display = ''; }
|
||
function hideExtras() { if (extrasWrapper) extrasWrapper.style.display = 'none'; }
|
||
|
||
// ─── Past Date Prevention ────────────────────────────────────
|
||
|
||
function getNowLocal() {
|
||
var d = new Date();
|
||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes());
|
||
}
|
||
|
||
var dateMinListenersBound = false;
|
||
|
||
function warnPastDate(input, msg) {
|
||
var wrap = input.closest('.carei-form__date-wrap') || input.parentNode;
|
||
var existing = wrap.querySelector('.carei-form__error-msg');
|
||
if (existing) existing.remove();
|
||
var span = document.createElement('span');
|
||
span.className = 'carei-form__error-msg';
|
||
span.textContent = msg;
|
||
wrap.appendChild(span);
|
||
input.classList.add('carei-form__field--error');
|
||
input.value = '';
|
||
updateDateEmpty(input);
|
||
}
|
||
|
||
function clearDateError(input) {
|
||
var wrap = input.closest('.carei-form__date-wrap') || input.parentNode;
|
||
var existing = wrap.querySelector('.carei-form__error-msg');
|
||
if (existing) existing.remove();
|
||
input.classList.remove('carei-form__field--error');
|
||
}
|
||
|
||
function checkPastAndWarn(input, label) {
|
||
if (!input || !input.value) return;
|
||
if (input.value < getNowLocal()) {
|
||
warnPastDate(input, label + ' — data lub godzina już minęły');
|
||
} else {
|
||
clearDateError(input);
|
||
}
|
||
}
|
||
|
||
function enforceDateMin() {
|
||
var now = getNowLocal();
|
||
if (dateFrom) dateFrom.setAttribute('min', now);
|
||
if (dateTo) dateTo.setAttribute('min', now);
|
||
|
||
if (!dateMinListenersBound) {
|
||
if (dateFrom) {
|
||
dateFrom.addEventListener('change', function () {
|
||
checkPastAndWarn(dateFrom, 'Rozpoczęcie');
|
||
if (dateTo && dateFrom.value) dateTo.setAttribute('min', dateFrom.value);
|
||
});
|
||
}
|
||
if (dateTo) {
|
||
dateTo.addEventListener('change', function () {
|
||
checkPastAndWarn(dateTo, 'Zakończenie');
|
||
});
|
||
}
|
||
dateMinListenersBound = true;
|
||
}
|
||
}
|
||
|
||
// ─── Date Labels ──────────────────────────────────────────────
|
||
|
||
function initDateLabels() {
|
||
[dateFrom, dateTo].forEach(function (input) {
|
||
if (!input) return;
|
||
updateDateEmpty(input);
|
||
input.addEventListener('change', function () { updateDateEmpty(input); updateDaysCount(); });
|
||
input.addEventListener('input', function () { updateDateEmpty(input); });
|
||
});
|
||
}
|
||
|
||
function updateDateEmpty(input) {
|
||
var wrap = input.closest('.carei-form__date-wrap');
|
||
if (input.value) {
|
||
input.classList.remove('is-empty');
|
||
if (wrap) wrap.classList.add('has-value');
|
||
} else {
|
||
input.classList.add('is-empty');
|
||
if (wrap) wrap.classList.remove('has-value');
|
||
}
|
||
}
|
||
|
||
function pad(n) { return String(n).padStart(2, '0'); }
|
||
|
||
// ─── Days Count ───────────────────────────────────────────────
|
||
|
||
function getRentalDays() {
|
||
if (!dateFrom || !dateTo) return 1;
|
||
var from = new Date(dateFrom.value), to = new Date(dateTo.value);
|
||
if (isNaN(from.getTime()) || isNaN(to.getTime()) || to <= from) return 1;
|
||
return Math.ceil((to - from) / 86400000);
|
||
}
|
||
|
||
function updateDaysCount() {
|
||
if (!dateFrom || !dateTo || !daysCount) return;
|
||
var from = new Date(dateFrom.value), to = new Date(dateTo.value);
|
||
if (isNaN(from.getTime()) || isNaN(to.getTime()) || to <= from) {
|
||
daysCount.innerHTML = 'Wybrano: <strong>0 dni</strong>'; return;
|
||
}
|
||
var diff = Math.ceil((to - from) / 86400000);
|
||
daysCount.innerHTML = 'Wybrano: <strong>' + diff + ' ' + (diff === 1 ? 'dzień' : 'dni') + '</strong>';
|
||
}
|
||
|
||
// ─── Load Extras from Pricelist ───────────────────────────────
|
||
|
||
function loadExtras() {
|
||
if (!segmentSelect || !dateFrom || !dateTo || !pickupSelect) return;
|
||
var category = segmentSelect.value, fromVal = dateFrom.value, toVal = dateTo.value, branch = pickupSelect.value;
|
||
if (!category || !fromVal || !toVal || !branch) return;
|
||
|
||
apiPost('pricelist', {
|
||
category: category, dateFrom: fromVal + ':00', dateTo: toVal + ':00', pickUpLocation: branch
|
||
}).then(function (pricelists) {
|
||
if (!Array.isArray(pricelists) || pricelists.length === 0) return;
|
||
var pricelist = pricelists[0];
|
||
currentPriceListId = pricelist.id;
|
||
var items = pricelist.additionalItems;
|
||
var insuranceItems = [], extraItems = [];
|
||
abroadItems = [];
|
||
selectedCountries = {};
|
||
if (Array.isArray(items)) {
|
||
items.forEach(function (item) {
|
||
var name = (item.name || '').toLowerCase();
|
||
// Skip penalty/return items
|
||
var code = (item.code || '').toUpperCase();
|
||
if (code.indexOf('BRAK') === 0 || code.indexOf('BRUD') === 0 || code.indexOf('KARA') === 0 ||
|
||
code.indexOf('MYCIE USŁU') === 0 || code === 'MYJ WEW') return;
|
||
if (name.indexOf('wyjazd za granic') !== -1) {
|
||
item._countryName = parseCountryName(item.name);
|
||
item._countryFlag = getCountryFlag(item._countryName);
|
||
abroadItems.push(item);
|
||
} else if (name.indexOf('ubezp') !== -1 || name.indexOf('ochrony') !== -1 ||
|
||
name.indexOf('zniesienie') !== -1 || name.indexOf('insurance') !== -1) {
|
||
insuranceItems.push(item);
|
||
} else {
|
||
extraItems.push(item);
|
||
}
|
||
});
|
||
}
|
||
if (insuranceContainer) { insuranceContainer.innerHTML = ''; insuranceItems.forEach(function (item) { insuranceContainer.appendChild(buildExtraCard(item)); }); }
|
||
if (extrasContainer) { extrasContainer.innerHTML = ''; extraItems.forEach(function (item) { extrasContainer.appendChild(buildExtraCard(item)); }); }
|
||
if (abroadSection) { abroadSection.style.display = abroadItems.length > 0 ? '' : 'none'; }
|
||
renderAbroadSelected();
|
||
}).catch(function (err) { console.error('Failed to load pricelist:', err); });
|
||
}
|
||
|
||
function buildExtraCard(item) {
|
||
var price = parseFloat(item.price || item.minPrice || 0);
|
||
var maxPrice = parseFloat(item.maxPrice || 0);
|
||
var priceLabel = (maxPrice > 0 && maxPrice !== price)
|
||
? 'od ' + price.toFixed(0) + ' do ' + maxPrice.toFixed(0) + ' zł'
|
||
: (price > 0 ? price.toFixed(0) + ' zł' + (item.unit === 'doba' ? '/doba' : '') : 'Gratis');
|
||
var card = document.createElement('div');
|
||
card.className = 'carei-form__extra-card';
|
||
card.innerHTML =
|
||
'<label class="carei-form__checkbox-label carei-form__checkbox-label--card">' +
|
||
'<input type="checkbox" name="extras[]" value="' + escAttr(item.id || item.code) + '" data-price="' + price + '" data-name="' + escAttr(toSentenceCase(item.name)) + '" data-unit="' + escAttr(item.unit || 'szt.') + '">' +
|
||
'<span class="carei-form__checkbox-box"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>' +
|
||
'<span class="carei-form__extra-content"><strong>' + escHtml(toSentenceCase(item.name)) + '</strong>' +
|
||
(item.description ? '<span class="carei-form__extra-desc">' + escHtml(toSentenceCase(item.description)) + '</span>' : '') +
|
||
'<span class="carei-form__extra-price"><strong>' + escHtml(priceLabel) + '</strong></span></span></label>';
|
||
return card;
|
||
}
|
||
|
||
// ─── Abroad Country Search ─────────────────────────────────────
|
||
|
||
var COUNTRY_FLAGS = {
|
||
'niemcy': '\u{1F1E9}\u{1F1EA}', 'czechy': '\u{1F1E8}\u{1F1FF}', 'słowacja': '\u{1F1F8}\u{1F1F0}',
|
||
'austria': '\u{1F1E6}\u{1F1F9}', 'francja': '\u{1F1EB}\u{1F1F7}', 'włochy': '\u{1F1EE}\u{1F1F9}',
|
||
'hiszpania': '\u{1F1EA}\u{1F1F8}', 'holandia': '\u{1F1F3}\u{1F1F1}', 'belgia': '\u{1F1E7}\u{1F1EA}',
|
||
'dania': '\u{1F1E9}\u{1F1F0}', 'szwecja': '\u{1F1F8}\u{1F1EA}', 'norwegia': '\u{1F1F3}\u{1F1F4}',
|
||
'finlandia': '\u{1F1EB}\u{1F1EE}', 'szwajcaria': '\u{1F1E8}\u{1F1ED}', 'węgry': '\u{1F1ED}\u{1F1FA}',
|
||
'chorwacja': '\u{1F1ED}\u{1F1F7}', 'słowenia': '\u{1F1F8}\u{1F1EE}', 'litwa': '\u{1F1F1}\u{1F1F9}',
|
||
'łotwa': '\u{1F1F1}\u{1F1FB}', 'estonia': '\u{1F1EA}\u{1F1EA}', 'rumunia': '\u{1F1F7}\u{1F1F4}',
|
||
'bułgaria': '\u{1F1E7}\u{1F1EC}', 'portugalia': '\u{1F1F5}\u{1F1F9}', 'grecja': '\u{1F1EC}\u{1F1F7}',
|
||
'wielka brytania': '\u{1F1EC}\u{1F1E7}', 'irlandia': '\u{1F1EE}\u{1F1EA}', 'luksemburg': '\u{1F1F1}\u{1F1FA}',
|
||
'serbia': '\u{1F1F7}\u{1F1F8}', 'czarnogóra': '\u{1F1F2}\u{1F1EA}', 'albania': '\u{1F1E6}\u{1F1F1}',
|
||
'turcja': '\u{1F1F9}\u{1F1F7}', 'ukraina': '\u{1F1FA}\u{1F1E6}', 'mołdawia': '\u{1F1F2}\u{1F1E9}'
|
||
};
|
||
|
||
function parseCountryName(name) {
|
||
var raw = (name || '').replace(/wyjazd za granic[eę]\s*[-–—]?\s*/i, '').trim();
|
||
if (!raw) return name || '';
|
||
return raw.split(/\s+/).map(function (w) {
|
||
return w.charAt(0).toUpperCase() + w.slice(1).toLowerCase();
|
||
}).join(' ');
|
||
}
|
||
|
||
function getCountryFlag(countryName) {
|
||
var key = (countryName || '').toLowerCase();
|
||
return COUNTRY_FLAGS[key] || '\u{1F3F3}\u{FE0F}';
|
||
}
|
||
|
||
function initAbroad() {
|
||
if (!abroadToggle || !abroadSearch) return;
|
||
abroadToggle.addEventListener('change', function () {
|
||
if (abroadToggle.checked) {
|
||
abroadSearch.style.display = '';
|
||
} else {
|
||
abroadSearch.style.display = 'none';
|
||
selectedCountries = {};
|
||
if (abroadInput) abroadInput.value = '';
|
||
renderAbroadResults([]);
|
||
renderAbroadSelected();
|
||
}
|
||
});
|
||
var abroadClear = document.getElementById('carei-abroad-clear');
|
||
if (abroadInput) {
|
||
abroadInput.addEventListener('input', function () {
|
||
updateAbroadClear();
|
||
var query = abroadInput.value.trim().toLowerCase();
|
||
if (!query) { renderAbroadResults([]); return; }
|
||
var filtered = abroadItems.filter(function (item) {
|
||
return item._countryName.toLowerCase().indexOf(query) !== -1;
|
||
});
|
||
renderAbroadResults(filtered);
|
||
});
|
||
abroadInput.addEventListener('focus', function () {
|
||
var query = abroadInput.value.trim().toLowerCase();
|
||
if (query) {
|
||
var filtered = abroadItems.filter(function (item) {
|
||
return item._countryName.toLowerCase().indexOf(query) !== -1;
|
||
});
|
||
renderAbroadResults(filtered);
|
||
}
|
||
});
|
||
}
|
||
if (abroadClear) {
|
||
abroadClear.addEventListener('click', function () {
|
||
if (abroadInput) { abroadInput.value = ''; abroadInput.focus(); }
|
||
renderAbroadResults([]);
|
||
updateAbroadClear();
|
||
});
|
||
}
|
||
|
||
function updateAbroadClear() {
|
||
var wrap = abroadInput ? abroadInput.closest('.carei-abroad__input-wrap') : null;
|
||
if (wrap) {
|
||
if (abroadInput.value.trim()) { wrap.classList.add('has-text'); }
|
||
else { wrap.classList.remove('has-text'); }
|
||
}
|
||
}
|
||
}
|
||
|
||
function renderAbroadResults(items) {
|
||
if (!abroadResults) return;
|
||
abroadResults.innerHTML = '';
|
||
if (!items || items.length === 0) return;
|
||
items.forEach(function (item) {
|
||
var id = item.id || item.code;
|
||
if (selectedCountries[id]) return; // already selected — show only in "added" section
|
||
abroadResults.appendChild(buildCountryCard(item, false));
|
||
});
|
||
}
|
||
|
||
function renderAbroadSelected() {
|
||
if (!abroadSelected) return;
|
||
abroadSelected.innerHTML = '';
|
||
var keys = Object.keys(selectedCountries);
|
||
if (keys.length === 0) return;
|
||
keys.forEach(function (id) {
|
||
var item = selectedCountries[id];
|
||
abroadSelected.appendChild(buildCountryCard(item, true));
|
||
});
|
||
}
|
||
|
||
function buildCountryCard(item, isSelected) {
|
||
var id = item.id || item.code;
|
||
var price = parseFloat(item.price || item.minPrice || 0);
|
||
var priceHtml = '<span class="carei-abroad__price-val">' + (price > 0 ? price.toFixed(0) + ' zł' : 'Gratis') + '</span>';
|
||
|
||
var card = document.createElement('div');
|
||
card.className = 'carei-abroad__card' + (isSelected ? ' carei-abroad__card--selected' : '');
|
||
card.innerHTML =
|
||
'<span class="carei-abroad__flag">' + escHtml(item._countryFlag) + '</span>' +
|
||
'<span class="carei-abroad__name">' + escHtml(item._countryName) + '</span>' +
|
||
'<span class="carei-abroad__price">' + priceHtml + '</span>' +
|
||
'<span role="button" tabindex="0" class="carei-abroad__action" data-abroad-id="' + escAttr(id) + '" title="' + (isSelected ? 'Usuń' : 'Dodaj') + '">' +
|
||
(isSelected
|
||
? '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M3 3l8 8M11 3l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>'
|
||
: '<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M7 2v10M2 7h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>'
|
||
) + '</span>';
|
||
|
||
card.querySelector('.carei-abroad__action').addEventListener('click', function (e) {
|
||
e.preventDefault();
|
||
if (selectedCountries[id]) {
|
||
delete selectedCountries[id];
|
||
} else {
|
||
selectedCountries[id] = item;
|
||
}
|
||
var query = abroadInput ? abroadInput.value.trim().toLowerCase() : '';
|
||
if (query) {
|
||
var filtered = abroadItems.filter(function (it) {
|
||
return it._countryName.toLowerCase().indexOf(query) !== -1;
|
||
});
|
||
renderAbroadResults(filtered);
|
||
} else {
|
||
renderAbroadResults([]);
|
||
}
|
||
renderAbroadSelected();
|
||
});
|
||
|
||
return card;
|
||
}
|
||
|
||
// ─── Select Helpers ───────────────────────────────────────────
|
||
|
||
function populateSelect(select, options, placeholder) {
|
||
if (!select) return;
|
||
select.innerHTML = '';
|
||
var ph = document.createElement('option');
|
||
ph.value = ''; ph.disabled = true; ph.selected = true; ph.textContent = placeholder || 'Wybierz...';
|
||
select.appendChild(ph);
|
||
options.forEach(function (opt) {
|
||
var o = document.createElement('option');
|
||
o.value = opt.value; o.textContent = opt.label; select.appendChild(o);
|
||
});
|
||
}
|
||
|
||
function setSelectLoading(select, loading) {
|
||
if (!select) return;
|
||
var wrap = select.closest('.carei-form__select-wrap');
|
||
if (wrap) wrap.classList.toggle('carei-form__select-wrap--loading', loading);
|
||
}
|
||
|
||
// ─── Same Return Location ─────────────────────────────────────
|
||
|
||
function initSameReturn() {
|
||
if (!sameReturnCheck || !returnWrap) return;
|
||
sameReturnCheck.addEventListener('change', function () {
|
||
returnWrap.classList.toggle('is-visible', !sameReturnCheck.checked);
|
||
});
|
||
}
|
||
|
||
// ─── Validation ───────────────────────────────────────────────
|
||
|
||
var requiredFields = [
|
||
{ id: 'carei-segment', type: 'select', msg: 'Wybierz segment pojazdu' },
|
||
{ id: 'carei-date-from', type: 'input', msg: 'Podaj datę rozpoczęcia' },
|
||
{ id: 'carei-date-to', type: 'input', msg: 'Podaj datę zakończenia' },
|
||
{ id: 'carei-pickup-branch', type: 'select', msg: 'Wybierz miejsce odbioru' },
|
||
{ id: 'carei-firstname', type: 'input', msg: 'Podaj imię' },
|
||
{ id: 'carei-lastname', type: 'input', msg: 'Podaj nazwisko' },
|
||
{ id: 'carei-email', type: 'email', msg: 'Podaj poprawny adres e-mail' },
|
||
{ id: 'carei-phone', type: 'phone', msg: 'Podaj numer telefonu (min. 9 cyfr)' },
|
||
{ id: 'carei-privacy', type: 'checkbox', msg: 'Wymagana zgoda na Politykę Prywatności' }
|
||
];
|
||
|
||
function validateForm() {
|
||
var valid = true;
|
||
form.querySelectorAll('.carei-form__field--error').forEach(function (el) { el.classList.remove('carei-form__field--error'); });
|
||
form.querySelectorAll('.carei-form__checkbox-label--error').forEach(function (el) { el.classList.remove('carei-form__checkbox-label--error'); });
|
||
form.querySelectorAll('.carei-form__error-msg').forEach(function (el) { el.remove(); });
|
||
|
||
requiredFields.forEach(function (f) {
|
||
var el = document.getElementById(f.id);
|
||
if (!el) return;
|
||
var hasError = false;
|
||
if (f.type === 'checkbox') { if (!el.checked) hasError = true; }
|
||
else if (f.type === 'email') { if (!el.value.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(el.value.trim())) hasError = true; }
|
||
else if (f.type === 'phone') { if (el.value.replace(/\D/g, '').length < 9) hasError = true; }
|
||
else if (f.type === 'pesel') { if (!/^\d{11}$/.test(el.value.trim())) hasError = true; }
|
||
else if (f.type === 'select') { if (!el.value) hasError = true; }
|
||
else { if (!el.value.trim()) hasError = true; }
|
||
if (hasError) { valid = false; markFieldError(el, f.msg, f.type); }
|
||
});
|
||
|
||
var now = new Date();
|
||
if (dateFrom && dateFrom.value && new Date(dateFrom.value) < now) {
|
||
valid = false; markFieldError(dateFrom, 'Data lub godzina rozpoczęcia już minęły', 'input');
|
||
}
|
||
if (dateTo && dateTo.value && new Date(dateTo.value) < now) {
|
||
valid = false; markFieldError(dateTo, 'Data lub godzina zakończenia już minęły', 'input');
|
||
}
|
||
if (dateFrom && dateTo && dateFrom.value && dateTo.value) {
|
||
if (new Date(dateTo.value) <= new Date(dateFrom.value)) {
|
||
valid = false; markFieldError(dateTo, 'Data zakończenia musi być po dacie rozpoczęcia', 'input');
|
||
}
|
||
}
|
||
if (sameReturnCheck && !sameReturnCheck.checked && returnSelect && !returnSelect.value) {
|
||
valid = false; markFieldError(returnSelect, 'Wybierz miejsce zwrotu', 'select');
|
||
}
|
||
if (errorSummary) errorSummary.style.display = valid ? 'none' : 'block';
|
||
return valid;
|
||
}
|
||
|
||
function markFieldError(el, msg, type) {
|
||
if (type === 'checkbox') {
|
||
var label = el.closest('.carei-form__checkbox-label');
|
||
if (label) label.classList.add('carei-form__checkbox-label--error');
|
||
} else {
|
||
var field = el.closest('.carei-form__field');
|
||
if (!field) { field = el.closest('.carei-form__phone-wrap'); if (field) field = field.closest('.carei-form__field'); }
|
||
if (field) {
|
||
field.classList.add('carei-form__field--error');
|
||
var errEl = document.createElement('span');
|
||
errEl.className = 'carei-form__error-msg'; errEl.textContent = msg;
|
||
field.appendChild(errEl);
|
||
}
|
||
}
|
||
}
|
||
|
||
function initClearErrors() {
|
||
if (!form) return;
|
||
form.addEventListener('focusin', function (e) {
|
||
var field = e.target.closest('.carei-form__field');
|
||
if (field && field.classList.contains('carei-form__field--error')) {
|
||
field.classList.remove('carei-form__field--error');
|
||
var errMsg = field.querySelector('.carei-form__error-msg'); if (errMsg) errMsg.remove();
|
||
}
|
||
var label = e.target.closest('.carei-form__checkbox-label');
|
||
if (label) label.classList.remove('carei-form__checkbox-label--error');
|
||
});
|
||
form.addEventListener('change', function (e) {
|
||
if (e.target.type === 'checkbox') {
|
||
var label = e.target.closest('.carei-form__checkbox-label');
|
||
if (label) label.classList.remove('carei-form__checkbox-label--error');
|
||
}
|
||
});
|
||
}
|
||
|
||
// ─── Form Submit → Booking Flow ───────────────────────────────
|
||
|
||
function initSubmit() {
|
||
if (!form) return;
|
||
form.addEventListener('submit', function (e) {
|
||
e.preventDefault();
|
||
if (!validateForm()) return;
|
||
setSubmitState('loading');
|
||
createCustomerAndShowSummary();
|
||
});
|
||
|
||
if (summaryBack) summaryBack.addEventListener('click', handleSummaryBack);
|
||
if (summaryConfirm) summaryConfirm.addEventListener('click', handleSummaryConfirm);
|
||
if (successClose) successClose.addEventListener('click', handleSuccessClose);
|
||
}
|
||
|
||
function setSubmitState(state) {
|
||
var btn = form ? form.querySelector('.carei-form__submit') : null;
|
||
if (!btn) return;
|
||
if (state === 'loading') {
|
||
btn.disabled = true;
|
||
btn.setAttribute('aria-busy', 'true');
|
||
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> Przetwarzanie...';
|
||
} else {
|
||
btn.disabled = false;
|
||
btn.setAttribute('aria-busy', 'false');
|
||
btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> Wyślij';
|
||
}
|
||
}
|
||
|
||
function collectFormData() {
|
||
var phoneRaw = document.getElementById('carei-phone') ? document.getElementById('carei-phone').value.replace(/\D/g, '') : '';
|
||
return {
|
||
segment: segmentSelect ? segmentSelect.value : '',
|
||
dateFrom: dateFrom ? dateFrom.value : '',
|
||
dateTo: dateTo ? dateTo.value : '',
|
||
pickupBranch: pickupSelect ? pickupSelect.value : '',
|
||
sameReturn: sameReturnCheck ? sameReturnCheck.checked : true,
|
||
returnBranch: (sameReturnCheck && !sameReturnCheck.checked && returnSelect) ? returnSelect.value : '',
|
||
firstName: val('carei-firstname'),
|
||
lastName: val('carei-lastname'),
|
||
email: val('carei-email'),
|
||
phone: '+48' + phoneRaw,
|
||
message: val('carei-message'),
|
||
privacy: document.getElementById('carei-privacy') ? document.getElementById('carei-privacy').checked : false
|
||
};
|
||
}
|
||
|
||
function val(id) { var el = document.getElementById(id); return el ? el.value.trim() : ''; }
|
||
|
||
// ─── Step 1: Create Customer ──────────────────────────────────
|
||
|
||
function createCustomerAndShowSummary() {
|
||
var fd = collectFormData();
|
||
hideSummaryError();
|
||
announce('Ładowanie podsumowania...');
|
||
|
||
apiPost('customer', {
|
||
firstName: fd.firstName,
|
||
lastName: fd.lastName,
|
||
name: fd.firstName + ' ' + fd.lastName,
|
||
isCompany: false,
|
||
address: { city: '-', zipCode: '00-000', street: '-', homeNo: '-' },
|
||
pesel: '00000000000',
|
||
email: fd.email,
|
||
phoneMobile: fd.phone,
|
||
paymentMethod: 'GOTÓWKA',
|
||
skipAccountCreate: true,
|
||
emailVerified: true
|
||
}).then(function (res) {
|
||
if (res && res.customerId) {
|
||
currentCustomerId = res.customerId;
|
||
return loadPricingSummary(fd);
|
||
}
|
||
throw new Error(res.rejectReason || 'Nie udało się utworzyć klienta');
|
||
}).catch(function (err) {
|
||
console.error('Customer creation failed:', err);
|
||
setSubmitState('ready');
|
||
showFormError('Błąd tworzenia klienta: ' + err.message);
|
||
});
|
||
}
|
||
|
||
// ─── Step 2: Pricing Summary ──────────────────────────────────
|
||
|
||
function loadPricingSummary(fd) {
|
||
var returnBranch = fd.sameReturn ? fd.pickupBranch : fd.returnBranch;
|
||
|
||
return apiPost('pricing-summary', {
|
||
dateFrom: fd.dateFrom + ':00',
|
||
dateTo: fd.dateTo + ':00',
|
||
customerId: currentCustomerId,
|
||
pickUpLocation: { branchName: fd.pickupBranch, outOfBranch: 'N' },
|
||
returnLocation: { branchName: returnBranch, outOfBranch: 'N' },
|
||
carParameters: { categoryName: fd.segment },
|
||
priceListId: currentPriceListId,
|
||
priceItems: getSelectedExtrasForApi()
|
||
}).then(function (summary) {
|
||
showSummaryOverlay(summary, fd);
|
||
}).catch(function (err) {
|
||
console.error('Pricing summary failed:', err);
|
||
setSubmitState('ready');
|
||
showFormError('Błąd pobierania podsumowania: ' + err.message);
|
||
});
|
||
}
|
||
|
||
function getSelectedExtrasForApi() {
|
||
var items = [];
|
||
if (!form) return items;
|
||
var days = getRentalDays();
|
||
form.querySelectorAll('input[name="extras[]"]:checked').forEach(function (cb) {
|
||
var price = parseFloat(cb.getAttribute('data-price') || 0);
|
||
var unit = cb.getAttribute('data-unit') || 'szt.';
|
||
var amount = unit === 'doba' ? days : 1;
|
||
items.push({
|
||
id: cb.value,
|
||
name: cb.getAttribute('data-name') || '',
|
||
unit: unit,
|
||
amount: amount,
|
||
priceBeforeDiscount: price,
|
||
discount: 0,
|
||
priceAfterDiscount: price
|
||
});
|
||
});
|
||
Object.keys(selectedCountries).forEach(function (id) {
|
||
var item = selectedCountries[id];
|
||
var price = parseFloat(item.price || item.minPrice || 0);
|
||
var unit = item.unit || 'szt.';
|
||
var amount = unit === 'doba' ? days : 1;
|
||
items.push({
|
||
id: item.id || item.code,
|
||
name: item.name,
|
||
unit: unit,
|
||
amount: amount,
|
||
priceBeforeDiscount: price,
|
||
discount: 0,
|
||
priceAfterDiscount: price
|
||
});
|
||
});
|
||
return items;
|
||
}
|
||
|
||
// ─── Step Transitions ──────────────────────────────────────────
|
||
|
||
function hideStep(el) {
|
||
if (!el) return;
|
||
el.style.display = 'none';
|
||
el.classList.remove('carei-step--entering', 'carei-step--exiting');
|
||
}
|
||
|
||
function showStep(el) {
|
||
if (!el) return;
|
||
el.style.display = '';
|
||
el.classList.remove('carei-step--hidden');
|
||
}
|
||
|
||
function transitionStep(outEl, inEl, callback) {
|
||
if (outEl) {
|
||
outEl.classList.add('carei-step--exiting');
|
||
setTimeout(function () {
|
||
hideStep(outEl);
|
||
if (inEl) {
|
||
inEl.classList.add('carei-step--entering');
|
||
showStep(inEl);
|
||
requestAnimationFrame(function () {
|
||
requestAnimationFrame(function () {
|
||
inEl.classList.remove('carei-step--entering');
|
||
if (callback) callback();
|
||
});
|
||
});
|
||
} else if (callback) { callback(); }
|
||
}, 250);
|
||
} else if (inEl) {
|
||
inEl.classList.add('carei-step--entering');
|
||
showStep(inEl);
|
||
requestAnimationFrame(function () {
|
||
requestAnimationFrame(function () {
|
||
inEl.classList.remove('carei-step--entering');
|
||
if (callback) callback();
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
// ─── Step 3: Show Summary Overlay ─────────────────────────────
|
||
|
||
function showSummaryOverlay(summary, fd) {
|
||
if (!summaryOverlay) return;
|
||
|
||
hideSummaryError();
|
||
|
||
// Populate content before transition
|
||
populateSummaryContent(summary, fd);
|
||
|
||
// Animated transition: form → summary
|
||
transitionStep(form, summaryOverlay, function () {
|
||
announce('Podsumowanie rezerwacji');
|
||
var title = summaryOverlay.querySelector('.carei-summary__title');
|
||
if (title) title.focus();
|
||
});
|
||
}
|
||
|
||
function populateSummaryContent(summary, fd) {
|
||
// Details
|
||
if (summaryDetails) {
|
||
var segLabel = segmentSelect ? segmentSelect.options[segmentSelect.selectedIndex].text : fd.segment;
|
||
var pickupLabel = pickupSelect ? pickupSelect.options[pickupSelect.selectedIndex].text : fd.pickupBranch;
|
||
var returnLabel = '';
|
||
if (!fd.sameReturn && returnSelect) {
|
||
returnLabel = returnSelect.options[returnSelect.selectedIndex].text;
|
||
}
|
||
var html =
|
||
'<div><strong>Segment:</strong> ' + escHtml(segLabel) + '</div>' +
|
||
'<div><strong>Od:</strong> ' + escHtml(fd.dateFrom.replace('T', ' ')) + '</div>' +
|
||
'<div><strong>Do:</strong> ' + escHtml(fd.dateTo.replace('T', ' ')) + '</div>' +
|
||
'<div><strong>Miejsce odbioru:</strong> ' + escHtml(pickupLabel) + '</div>';
|
||
if (returnLabel) {
|
||
html += '<div><strong>Miejsce zwrotu:</strong> ' + escHtml(returnLabel) + '</div>';
|
||
}
|
||
html += '<div><strong>Najemca:</strong> ' + escHtml(fd.firstName + ' ' + fd.lastName) + '</div>' +
|
||
'<div><strong>Email:</strong> ' + escHtml(fd.email) + '</div>' +
|
||
'<div><strong>Telefon:</strong> ' + escHtml(fd.phone) + '</div>';
|
||
|
||
// Selected extras
|
||
var selectedExtras = getSelectedExtrasForApi();
|
||
if (selectedExtras.length > 0) {
|
||
html += '<div style="margin-top:8px"><strong>Wybrane opcje:</strong></div><ul style="margin:4px 0 0 16px;padding:0;list-style:disc;">';
|
||
selectedExtras.forEach(function (ex) {
|
||
var totalPrice = ex.priceAfterDiscount * (ex.amount || 1);
|
||
var priceInfo = ex.unit === 'doba' && ex.amount > 1
|
||
? fmtPrice(ex.priceAfterDiscount) + ' zł/doba × ' + ex.amount + ' = ' + fmtPrice(totalPrice) + ' zł'
|
||
: fmtPrice(totalPrice) + ' zł';
|
||
html += '<li>' + escHtml(toSentenceCase(ex.name)) + ' — ' + priceInfo + '</li>';
|
||
});
|
||
html += '</ul>';
|
||
}
|
||
|
||
if (fd.message) {
|
||
html += '<div style="margin-top:8px"><strong>Wiadomość:</strong> ' + escHtml(fd.message) + '</div>';
|
||
}
|
||
|
||
summaryDetails.innerHTML = html;
|
||
}
|
||
|
||
// Price table
|
||
if (summaryTable && summary.pricelist) {
|
||
var html = '<table><thead><tr><th>Nazwa</th><th>Ilość</th><th>Netto</th><th>Brutto</th></tr></thead><tbody>';
|
||
summary.pricelist.forEach(function (item) {
|
||
var rowClass = item.addedBySystem ? ' class="carei-summary__auto-item"' : '';
|
||
html += '<tr' + rowClass + '>' +
|
||
'<td>' + escHtml(toSentenceCase(item.name)) + (item.addedBySystem ? ' <small>(auto)</small>' : '') + '</td>' +
|
||
'<td>' + (item.amount || 1) + ' ' + escHtml(item.unit || '') + '</td>' +
|
||
'<td>' + fmtPrice(item.netValue) + '</td>' +
|
||
'<td>' + fmtPrice(item.grossValue) + '</td></tr>';
|
||
});
|
||
html += '</tbody></table>';
|
||
summaryTable.innerHTML = html;
|
||
}
|
||
|
||
// Totals
|
||
if (summaryTotal) {
|
||
summaryTotal.innerHTML =
|
||
'<div class="carei-summary__total-row"><span class="carei-summary__total-label">Netto:</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalNetValue) + '</span></div>' +
|
||
'<div class="carei-summary__total-row"><span class="carei-summary__total-label">VAT:</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalVatValue) + '</span></div>' +
|
||
'<div class="carei-summary__total-row carei-summary__total-row--gross"><span class="carei-summary__total-label">Do zapłaty:</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalGrossValue) + ' zł</span></div>';
|
||
}
|
||
}
|
||
|
||
function fmtPrice(val) {
|
||
if (val === null || val === undefined) return '-';
|
||
return parseFloat(val).toFixed(2).replace('.', ',');
|
||
}
|
||
|
||
// ─── Summary Actions ──────────────────────────────────────────
|
||
|
||
function handleSummaryBack() {
|
||
transitionStep(summaryOverlay, form, function () {
|
||
setSubmitState('ready');
|
||
if (segmentSelect) segmentSelect.focus();
|
||
});
|
||
}
|
||
|
||
function handleSummaryConfirm() {
|
||
if (summaryConfirm) {
|
||
summaryConfirm.disabled = true;
|
||
summaryConfirm.setAttribute('aria-busy', 'true');
|
||
summaryConfirm.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> Rezerwuję...';
|
||
}
|
||
hideSummaryError();
|
||
|
||
var fd = collectFormData();
|
||
var returnBranch = fd.sameReturn ? fd.pickupBranch : fd.returnBranch;
|
||
|
||
var bookingData = {
|
||
dateFrom: fd.dateFrom + ':00',
|
||
dateTo: fd.dateTo + ':00',
|
||
customerId: currentCustomerId,
|
||
pickUpLocation: { branchName: fd.pickupBranch, outOfBranch: 'N' },
|
||
returnLocation: { branchName: returnBranch, outOfBranch: 'N' },
|
||
carParameters: { categoryName: fd.segment },
|
||
priceListId: currentPriceListId,
|
||
validTime: 1440,
|
||
priceItems: getSelectedExtrasForApi(),
|
||
drivers: [{
|
||
firstName: fd.firstName,
|
||
lastName: fd.lastName,
|
||
address: { city: '-', zipCode: '00-000', street: '-' },
|
||
pesel: '00000000000',
|
||
phone: fd.phone,
|
||
email: fd.email
|
||
}],
|
||
comments: fd.message || ''
|
||
};
|
||
|
||
// Add agreement items
|
||
if (agreementDefs.length > 0) {
|
||
bookingData.agreementItems = agreementDefs.map(function (a) {
|
||
return { id: a.id, value: true };
|
||
});
|
||
}
|
||
|
||
apiPost('booking', bookingData).then(function (res) {
|
||
if (res && res.success && res.reservationId) {
|
||
currentReservationId = res.reservationId;
|
||
showSuccessView(res.reservationNo || res.reservationId);
|
||
return;
|
||
}
|
||
throw new Error(translateRejectReason(res.rejectReason) || 'Rezerwacja nie powiodła się');
|
||
}).catch(function (err) {
|
||
console.error('Booking failed:', err);
|
||
showSummaryError(err.message);
|
||
resetConfirmBtn();
|
||
});
|
||
}
|
||
|
||
function translateRejectReason(reason) {
|
||
if (!reason) return null;
|
||
var map = {
|
||
'CAR_NOT_FOUND': 'Brak dostępnego pojazdu w wybranym terminie. Zmień daty lub segment.',
|
||
'INVALID_DATE_RANGE': 'Nieprawidłowy zakres dat',
|
||
'BRANCH_NOT_FOUND': 'Nie znaleziono oddziału',
|
||
'CUSTOMER_ALREADY_EXISTS': 'Klient o tych danych już istnieje w systemie',
|
||
'INVALID_PESEL': 'Nieprawidłowy numer PESEL',
|
||
'PRICE_LIST_EXPIRED': 'Cennik wygasł. Odśwież formularz i spróbuj ponownie.'
|
||
};
|
||
return map[reason] || reason;
|
||
}
|
||
|
||
function resetConfirmBtn() {
|
||
if (summaryConfirm) {
|
||
summaryConfirm.disabled = false;
|
||
summaryConfirm.setAttribute('aria-busy', 'false');
|
||
summaryConfirm.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg> Potwierdź rezerwację';
|
||
}
|
||
}
|
||
|
||
// ─── Success View ─────────────────────────────────────────────
|
||
|
||
function showSuccessView(reservationNo) {
|
||
if (successNumber) successNumber.textContent = 'Nr zamówienia: ' + reservationNo;
|
||
transitionStep(summaryOverlay, successView, function () {
|
||
announce('Rezerwacja potwierdzona');
|
||
var title = successView.querySelector('.carei-success__title');
|
||
if (title) title.focus();
|
||
});
|
||
}
|
||
|
||
function handleSuccessClose() {
|
||
hideStep(successView);
|
||
if (form) { showStep(form); form.reset(); }
|
||
setSubmitState('ready');
|
||
resetConfirmBtn();
|
||
currentCustomerId = null;
|
||
currentReservationId = null;
|
||
hideExtras();
|
||
if (pickupSelect) { populateSelect(pickupSelect, [], 'Najpierw wybierz segment'); pickupSelect.disabled = true; }
|
||
closeModal();
|
||
}
|
||
|
||
// ─── Error Helpers ────────────────────────────────────────────
|
||
|
||
function showFormError(msg) {
|
||
announce(msg);
|
||
if (errorSummary) { errorSummary.textContent = msg; errorSummary.style.display = 'block'; }
|
||
}
|
||
|
||
function showSummaryError(msg) {
|
||
announce(msg);
|
||
if (summaryError) { summaryError.textContent = msg; summaryError.style.display = 'block'; }
|
||
}
|
||
|
||
function hideSummaryError() {
|
||
if (summaryError) { summaryError.textContent = ''; summaryError.style.display = 'none'; }
|
||
}
|
||
|
||
// ─── Event Listeners ──────────────────────────────────────────
|
||
|
||
function initDynamicLoading() {
|
||
if (dateFrom) dateFrom.addEventListener('change', updateDaysCount);
|
||
if (dateTo) dateTo.addEventListener('change', updateDaysCount);
|
||
if (segmentSelect) segmentSelect.addEventListener('change', onSegmentChange);
|
||
if (pickupSelect) pickupSelect.addEventListener('change', onPickupChange);
|
||
if (dateFrom) dateFrom.addEventListener('change', maybeShowExtras);
|
||
if (dateTo) dateTo.addEventListener('change', maybeShowExtras);
|
||
}
|
||
|
||
// ─── Aria Live Announcements ───────────────────────────────────
|
||
|
||
var liveRegion = null;
|
||
|
||
function initLiveRegion() {
|
||
liveRegion = document.createElement('div');
|
||
liveRegion.setAttribute('aria-live', 'polite');
|
||
liveRegion.setAttribute('role', 'status');
|
||
liveRegion.className = 'carei-sr-only';
|
||
if (overlay) overlay.appendChild(liveRegion);
|
||
}
|
||
|
||
function announce(msg) {
|
||
if (!liveRegion) return;
|
||
liveRegion.textContent = '';
|
||
setTimeout(function () { liveRegion.textContent = msg; }, 100);
|
||
}
|
||
|
||
// ─── HTML Escape Helpers ──────────────────────────────────────
|
||
|
||
function escHtml(str) { var d = document.createElement('div'); d.textContent = str || ''; return d.innerHTML; }
|
||
function escAttr(str) { return (str || '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>'); }
|
||
function toSentenceCase(str) { if (!str) return ''; var s = str.toLowerCase(); return s.charAt(0).toUpperCase() + s.slice(1); }
|
||
|
||
// ─── Search Form (Hero Mini Form) ──────────────────────────────
|
||
|
||
function initSearchForm() {
|
||
var searchForm = document.querySelector('.carei-search-form');
|
||
if (!searchForm) return;
|
||
|
||
var searchSegment = document.getElementById('carei-search-segment');
|
||
var searchDateFrom = document.getElementById('carei-search-date-from');
|
||
var searchDateTo = document.getElementById('carei-search-date-to');
|
||
var searchPickup = document.getElementById('carei-search-pickup');
|
||
var searchSameReturn = document.getElementById('carei-search-same-return');
|
||
var searchSubmit = document.getElementById('carei-search-submit');
|
||
|
||
var searchMapData = null;
|
||
|
||
// Enforce min dates on search form (no error messages — compact UI)
|
||
var now = getNowLocal();
|
||
if (searchDateFrom) {
|
||
searchDateFrom.setAttribute('min', now);
|
||
searchDateFrom.addEventListener('change', function () {
|
||
if (searchDateFrom.value && searchDateFrom.value < getNowLocal()) searchDateFrom.value = '';
|
||
if (searchDateTo && searchDateFrom.value) searchDateTo.setAttribute('min', searchDateFrom.value);
|
||
});
|
||
}
|
||
if (searchDateTo) {
|
||
searchDateTo.setAttribute('min', now);
|
||
searchDateTo.addEventListener('change', function () {
|
||
if (searchDateTo.value && searchDateTo.value < getNowLocal()) searchDateTo.value = '';
|
||
});
|
||
}
|
||
|
||
// Ładowanie danych do mini formularza
|
||
function loadSearchData() {
|
||
Promise.all([
|
||
apiGet('car-classes-all'),
|
||
apiGet('segments-branches-map')
|
||
]).then(function (results) {
|
||
var classes = results[0];
|
||
searchMapData = results[1];
|
||
|
||
if (Array.isArray(classes) && classes.length > 0) {
|
||
var segments = classes.map(function (c) {
|
||
var val = typeof c === 'string' ? c : (c.name || c);
|
||
var label = typeof c === 'string' ? ('Segment ' + c) : (c.description || c.name || c);
|
||
return { value: val, label: label };
|
||
});
|
||
populateSelect(searchSegment, segments, 'Wybierz segment');
|
||
}
|
||
if (searchPickup) {
|
||
populateSelect(searchPickup, [], 'Najpierw wybierz segment');
|
||
searchPickup.disabled = true;
|
||
}
|
||
}).catch(function (err) {
|
||
console.error('Search form: failed to load data:', err);
|
||
});
|
||
}
|
||
|
||
// Zmiana segmentu → filtr lokalizacji
|
||
if (searchSegment) {
|
||
searchSegment.addEventListener('change', function () {
|
||
var sel = searchSegment.value;
|
||
if (!sel || !searchMapData || !searchPickup) return;
|
||
var segBranches = searchMapData.segmentToBranches[sel] || [];
|
||
var allBranches = searchMapData.branches || [];
|
||
var opts = [];
|
||
allBranches.forEach(function (b) {
|
||
if (segBranches.indexOf(b.name || '') !== -1) {
|
||
var label = b.description || b.name;
|
||
if (b.city) label += ' — ' + b.city;
|
||
opts.push({ value: b.name, label: label });
|
||
}
|
||
});
|
||
if (opts.length > 0) {
|
||
populateSelect(searchPickup, opts, 'Miejsce odbioru');
|
||
searchPickup.disabled = false;
|
||
} else {
|
||
populateSelect(searchPickup, [], 'Brak lokalizacji');
|
||
searchPickup.disabled = true;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Date label behavior
|
||
[searchDateFrom, searchDateTo].forEach(function (input) {
|
||
if (!input) return;
|
||
var wrap = input.closest('.carei-search-form__date-wrap');
|
||
function updateLabel() {
|
||
if (wrap) wrap.classList.toggle('has-value', !!input.value);
|
||
}
|
||
updateLabel();
|
||
input.addEventListener('change', updateLabel);
|
||
input.addEventListener('input', updateLabel);
|
||
});
|
||
|
||
// Submit → otwórz modal z pre-fill
|
||
if (searchSubmit) {
|
||
searchSubmit.addEventListener('click', function () {
|
||
if (!overlay) return;
|
||
|
||
var valSegment = searchSegment ? searchSegment.value : '';
|
||
var valDateFrom = searchDateFrom ? searchDateFrom.value : '';
|
||
var valDateTo = searchDateTo ? searchDateTo.value : '';
|
||
var valPickup = searchPickup ? searchPickup.value : '';
|
||
var valSameReturn = searchSameReturn ? searchSameReturn.checked : true;
|
||
|
||
// Otwórz modal
|
||
openModal(searchSubmit);
|
||
|
||
// Pre-fill po załadowaniu danych modala
|
||
function prefillModal() {
|
||
// Segment
|
||
if (segmentSelect && valSegment) {
|
||
segmentSelect.value = valSegment;
|
||
segmentSelect.dispatchEvent(new Event('change'));
|
||
}
|
||
|
||
// Daty
|
||
if (dateFrom && valDateFrom) {
|
||
dateFrom.value = valDateFrom;
|
||
dateFrom.dispatchEvent(new Event('change'));
|
||
}
|
||
if (dateTo && valDateTo) {
|
||
dateTo.value = valDateTo;
|
||
dateTo.dispatchEvent(new Event('change'));
|
||
}
|
||
|
||
// Checkbox zwrotu
|
||
if (sameReturnCheck) {
|
||
sameReturnCheck.checked = valSameReturn;
|
||
sameReturnCheck.dispatchEvent(new Event('change'));
|
||
}
|
||
|
||
// Pickup — poczekaj aż lokalizacje się załadują po change segmentu
|
||
if (valPickup && pickupSelect) {
|
||
var attempts = 0;
|
||
var pickupInterval = setInterval(function () {
|
||
attempts++;
|
||
// Sprawdź czy opcja jest dostępna
|
||
var optExists = Array.prototype.slice.call(pickupSelect.options).some(function (o) {
|
||
return o.value === valPickup;
|
||
});
|
||
if (optExists) {
|
||
clearInterval(pickupInterval);
|
||
pickupSelect.value = valPickup;
|
||
pickupSelect.dispatchEvent(new Event('change'));
|
||
} else if (attempts > 30) {
|
||
clearInterval(pickupInterval);
|
||
}
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
// Daj czas na loadInitialData w openModal
|
||
setTimeout(prefillModal, 400);
|
||
});
|
||
}
|
||
|
||
loadSearchData();
|
||
}
|
||
|
||
// ─── Init ─────────────────────────────────────────────────────
|
||
|
||
function init() {
|
||
initRefs();
|
||
|
||
// Inicjalizuj search form niezależnie od modala
|
||
initSearchForm();
|
||
|
||
if (!overlay || !form) return;
|
||
|
||
initModal();
|
||
initLiveRegion();
|
||
initSameReturn();
|
||
initDynamicLoading();
|
||
initClearErrors();
|
||
initAbroad();
|
||
initSubmit();
|
||
initMap();
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════
|
||
Carei Map — dynamic pins & tooltips
|
||
═══════════════════════════════════════════ */
|
||
|
||
function initMap() {
|
||
var mapEl = document.querySelector('.carei-map');
|
||
if (!mapEl) return;
|
||
|
||
var pins;
|
||
try {
|
||
pins = JSON.parse(mapEl.getAttribute('data-pins') || '[]');
|
||
} catch (e) {
|
||
return;
|
||
}
|
||
if (!pins.length) return;
|
||
|
||
var svg = mapEl.querySelector('.carei-map__svg');
|
||
var pinsGroup = svg.querySelector('.carei-map__pins');
|
||
var tooltip = mapEl.querySelector('.carei-map__tooltip');
|
||
var tooltipContent = mapEl.querySelector('.carei-map__tooltip-content');
|
||
if (!svg || !pinsGroup || !tooltip || !tooltipContent) return;
|
||
|
||
var activePin = null;
|
||
var SVG_NS = 'http://www.w3.org/2000/svg';
|
||
|
||
pins.forEach(function (pin) {
|
||
var circle = document.createElementNS(SVG_NS, 'circle');
|
||
circle.setAttribute('cx', pin.x);
|
||
circle.setAttribute('cy', pin.y);
|
||
circle.setAttribute('r', '6');
|
||
circle.setAttribute('fill', '#FF0000');
|
||
circle.setAttribute('class', 'carei-map__pin');
|
||
circle.setAttribute('data-city', pin.city);
|
||
circle.setAttribute('data-address', pin.address || '');
|
||
|
||
circle.addEventListener('mouseenter', function () {
|
||
showTooltip(pin, circle);
|
||
});
|
||
circle.addEventListener('mouseleave', function () {
|
||
if (activePin !== circle) {
|
||
hideTooltip();
|
||
}
|
||
});
|
||
circle.addEventListener('click', function (e) {
|
||
e.stopPropagation();
|
||
if (activePin === circle) {
|
||
activePin = null;
|
||
circle.classList.remove('carei-map__pin--active');
|
||
hideTooltip();
|
||
} else {
|
||
if (activePin) activePin.classList.remove('carei-map__pin--active');
|
||
activePin = circle;
|
||
circle.classList.add('carei-map__pin--active');
|
||
showTooltip(pin, circle);
|
||
}
|
||
});
|
||
|
||
pinsGroup.appendChild(circle);
|
||
});
|
||
|
||
// Click outside closes tooltip
|
||
document.addEventListener('click', function () {
|
||
if (activePin) {
|
||
activePin.classList.remove('carei-map__pin--active');
|
||
activePin = null;
|
||
hideTooltip();
|
||
}
|
||
});
|
||
|
||
function showTooltip(pin, circle) {
|
||
// Build tooltip text: address lines + bold city
|
||
var addr = pin.address || '';
|
||
var lines = addr ? addr.replace(/\\n/g, '\n') : pin.city;
|
||
tooltipContent.textContent = '';
|
||
|
||
// If address has postal code, format nicely
|
||
if (addr) {
|
||
tooltipContent.innerHTML = addr.replace(/\n/g, '<br>');
|
||
} else {
|
||
tooltipContent.textContent = pin.city;
|
||
}
|
||
|
||
// Position tooltip relative to map container
|
||
var svgRect = svg.getBoundingClientRect();
|
||
var mapRect = mapEl.getBoundingClientRect();
|
||
var svgWidth = svg.viewBox.baseVal.width || 600;
|
||
var svgHeight = svg.viewBox.baseVal.height || 570;
|
||
var scaleX = svgRect.width / svgWidth;
|
||
var scaleY = svgRect.height / svgHeight;
|
||
|
||
var left = (svgRect.left - mapRect.left) + pin.x * scaleX;
|
||
var top = (svgRect.top - mapRect.top) + pin.y * scaleY;
|
||
|
||
tooltip.style.display = 'block';
|
||
tooltip.style.left = left + 'px';
|
||
tooltip.style.top = top + 'px';
|
||
}
|
||
|
||
function hideTooltip() {
|
||
tooltip.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|