This commit is contained in:
2026-04-22 22:00:50 +02:00
parent 16be247ce1
commit e979fbe755
46 changed files with 5302 additions and 274 deletions

View File

@@ -1647,3 +1647,94 @@ button.carei-reservation-trigger:hover {
grid-template-columns: 1fr;
}
}
/* ──────────────────────────────────────────────────────────────
Flatpickr — compact theme + Carei colors
────────────────────────────────────────────────────────────── */
.flatpickr-calendar {
width: 260px !important;
font-size: 13px !important;
box-shadow: 0 6px 20px rgba(47, 36, 130, 0.15);
border-radius: 8px;
}
.flatpickr-calendar.open { z-index: 100000; }
.flatpickr-months { padding: 4px 0; }
.flatpickr-month { height: 28px; }
.flatpickr-current-month {
font-size: 13px;
padding: 4px 0 0 0;
height: 24px;
}
.flatpickr-current-month .flatpickr-monthDropdown-months,
.flatpickr-current-month input.cur-year {
font-size: 13px;
font-weight: 600;
}
.flatpickr-prev-month,
.flatpickr-next-month {
padding: 4px 8px;
height: 28px;
}
.flatpickr-prev-month svg,
.flatpickr-next-month svg { width: 12px; height: 12px; }
.flatpickr-weekdays { height: 24px; }
.flatpickr-weekday {
font-size: 11px !important;
font-weight: 600;
color: #6b6b8a !important;
}
.dayContainer {
padding: 2px;
width: 252px;
min-width: 252px;
max-width: 252px;
}
.flatpickr-day {
height: 30px;
line-height: 30px;
font-size: 12px;
max-width: 32px;
border-radius: 4px;
margin: 1px 0;
}
.flatpickr-day.today { border-color: #2F2482; }
.flatpickr-day.selected,
.flatpickr-day.startRange,
.flatpickr-day.endRange,
.flatpickr-day.selected:hover {
background: #2F2482 !important;
border-color: #2F2482 !important;
color: #fff !important;
}
.flatpickr-day:hover,
.flatpickr-day.prevMonthDay:hover,
.flatpickr-day.nextMonthDay:hover {
background: rgba(47, 36, 130, 0.08);
}
.flatpickr-time {
height: 32px;
border-top: 1px solid rgba(47, 36, 130, 0.12);
}
.flatpickr-time input {
font-size: 13px;
height: 32px;
}
.flatpickr-time .flatpickr-time-separator,
.flatpickr-time .flatpickr-am-pm {
height: 32px;
line-height: 32px;
font-size: 13px;
}
.flatpickr-time input:hover,
.flatpickr-time input:focus {
background: rgba(47, 36, 130, 0.05);
}
.flatpickr-time .numInputWrapper span {
width: 12px;
height: 50%;
}
@media (max-width: 380px) {
.flatpickr-calendar { width: calc(100vw - 24px) !important; }
.dayContainer { width: 100%; min-width: 0; max-width: 100%; }
.flatpickr-day { max-width: none; }
}

View File

@@ -1,6 +1,26 @@
(function () {
'use strict';
// ─── i18n Helpers ─────────────────────────────────────────────
var I18N = (window.careiI18n || {});
function t(key, fallback) {
return (key in I18N) ? I18N[key] : (fallback || key);
}
function tFmt(key, params, fallback) {
var str = t(key, fallback);
if (params && typeof str === 'string') {
Object.keys(params).forEach(function (k) {
str = str.replace(new RegExp('%' + k + '%', 'g'), params[k]);
});
}
return str;
}
function pluralPl(n, one, few, many) {
if (n === 1) return one;
if (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 12 || n % 100 > 14)) return few;
return many;
}
var REST_URL = (window.careiReservation && window.careiReservation.restUrl) || '/wp-json/carei/v1/';
var NONCE = (window.careiReservation && window.careiReservation.nonce) || '';
@@ -48,13 +68,13 @@
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 msg = (body && body.message) || (body && body.detail) || tFmt('errorApiHttp', {status: status}, '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);
var err = new Error(tFmt('errorApiHttp', {status: status}, 'Błąd API: HTTP %status%'));
err.httpStatus = status;
throw err;
});
@@ -64,13 +84,13 @@
function handleFetchError(err, retryFn, isRetry) {
if (err.name === 'AbortError') {
throw new Error('Przekroczono czas oczekiwania. Spróbuj ponownie.');
throw new Error(t('errorTimeout', '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 new Error(t('errorNetwork', 'Brak połączenia z serwerem. Sprawdź internet i spróbuj ponownie.'));
}
throw err;
}
@@ -79,7 +99,7 @@
var overlay, form, segmentSelect, dateFrom, dateTo, daysCount;
var pickupSelect, returnSelect, returnWrap, sameReturnCheck;
var extrasWrapper, extrasContainer, insuranceContainer, abroadSection, abroadToggle, abroadSearch, abroadInput, abroadResults, abroadSelected, errorSummary;
var extrasWrapper, extrasContainer, abroadSection, abroadToggle, abroadSearch, abroadInput, abroadResults, abroadSelected, errorSummary;
var summaryOverlay, summaryDetails, summaryTable, summaryTotal, summaryError;
var summaryBack, summaryConfirm;
var successView, successNumber, successClose;
@@ -104,7 +124,6 @@
sameReturnCheck = document.getElementById('carei-same-return');
extrasWrapper = document.getElementById('carei-extras-wrapper');
extrasContainer = document.getElementById('carei-extras-container');
insuranceContainer = document.getElementById('carei-insurance-container');
protectionContainer = document.getElementById('carei-protection-packages-container');
abroadSection = document.getElementById('carei-abroad-section');
abroadToggle = document.getElementById('carei-abroad-toggle');
@@ -127,6 +146,89 @@
successClose = document.getElementById('carei-success-close');
}
// ─── Flatpickr date pickers (cross-browser, locale-aware) ─────
// If Flatpickr fails to load (CDN blocked, network, etc.), fall back gracefully
// to native <input type="datetime-local"> — never break date picking.
function initDatePickers() {
if (typeof window.flatpickr !== 'function') {
console.warn('[carei] Flatpickr not loaded — keeping native datetime-local picker.');
return;
}
var isEn = (document.documentElement.lang || '').toLowerCase().indexOf('en') === 0;
var locale = 'default';
if (!isEn && window.flatpickr.l10ns && window.flatpickr.l10ns.pl) {
locale = window.flatpickr.l10ns.pl;
}
var baseOpts = {
enableTime: true,
time_24hr: true,
dateFormat: 'Y-m-d\\TH:i',
altInput: true,
altFormat: isEn ? 'Y-m-d H:i' : 'd.m.Y H:i',
minuteIncrement: 15,
minDate: 'today',
locale: locale,
allowInput: false,
// Force Flatpickr on mobile too (otherwise falls back to native iOS/Android picker — inconsistent look, locale = OS)
disableMobile: true,
// With enableTime, flatpickr doesn't auto-close on date click (time still editable).
// Auto-close 1.5s after last change — gives user time to adjust time, then dismisses.
onChange: function (selectedDates, dateStr, instance) {
if (instance._careiCloseTimer) clearTimeout(instance._careiCloseTimer);
instance._careiCloseTimer = setTimeout(function () {
instance.close();
}, 1500);
}
};
// Modal inputs — use static:true so popup renders INSIDE input container
// (bypasses focus trap + z-index conflicts with modal overlay).
var modalTargets = [dateFrom, dateTo];
// Hero search inputs — render popup to body (default behavior is fine, no modal).
var heroTargets = [
document.getElementById('carei-search-date-from'),
document.getElementById('carei-search-date-to')
];
function attach(el, opts) {
if (!el) return;
// If already initialized, destroy previous instance (idempotent re-init)
if (el._flatpickr) {
try { el._flatpickr.destroy(); } catch (e) {}
}
try {
el.setAttribute('type', 'text');
window.flatpickr(el, opts);
// Bind click on the whole field container — any click (icon, label, padding, input)
// opens the flatpickr. Simplest UX.
var field = el.closest('.carei-form__field--date') || el.closest('.carei-search-form__field');
if (field && !field.dataset.careiFpBound) {
field.dataset.careiFpBound = '1';
field.style.cursor = 'pointer';
var lastCloseAt = 0;
if (el._flatpickr) {
var origOnClose = el._flatpickr.config.onClose || [];
el._flatpickr.config.onClose = (Array.isArray(origOnClose) ? origOnClose.slice() : [origOnClose]).concat([function () { lastCloseAt = Date.now(); }]);
}
field.addEventListener('click', function (e) {
// Ignore clicks that originated inside the flatpickr popup itself.
if (e.target.closest && e.target.closest('.flatpickr-calendar')) return;
// Debounce: if we just closed (within 300ms, e.g. via date selection), don't reopen.
if (Date.now() - lastCloseAt < 300) return;
if (el._flatpickr) el._flatpickr.open();
});
}
} catch (err) {
console.error('[carei] Flatpickr init failed for', el.id, err);
try { el.setAttribute('type', 'datetime-local'); } catch (e) {}
}
}
var modalOpts = Object.assign({}, baseOpts, { static: true });
modalTargets.forEach(function (el) { attach(el, modalOpts); });
heroTargets.forEach(function (el) { attach(el, baseOpts); });
}
// ─── State ────────────────────────────────────────────────────
var mapData = null;
@@ -247,23 +349,23 @@
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);
var label = typeof c === 'string' ? tFmt('segmentLabel', {name: c}, 'Segment %name%') : (c.description || c.name || c);
return { value: val, label: label };
});
populateSelect(segmentSelect, allSegments, 'Wybierz segment pojazdu');
populateSelect(segmentSelect, allSegments, t('selectSegment', 'Wybierz segment pojazdu'));
} else {
populateSelect(segmentSelect, [], 'Brak segmentów');
populateSelect(segmentSelect, [], t('noSegments', 'Brak segmentów'));
}
if (segmentSelect) setSelectLoading(segmentSelect, false);
if (pickupSelect) {
populateSelect(pickupSelect, [], 'Najpierw wybierz segment');
populateSelect(pickupSelect, [], t('pickupPlaceholder', 'Najpierw wybierz segment'));
pickupSelect.disabled = true;
}
}).catch(function (err) {
console.error('Failed to load initial data:', err);
if (segmentSelect) {
populateSelect(segmentSelect, [], 'Błąd ładowania');
populateSelect(segmentSelect, [], t('errorLoading', 'Błąd ładowania'));
setSelectLoading(segmentSelect, false);
}
});
@@ -274,7 +376,7 @@
function onSegmentChange() {
var selectedSegment = segmentSelect ? segmentSelect.value : '';
if (!selectedSegment || !mapData || !pickupSelect) {
if (pickupSelect) { populateSelect(pickupSelect, [], 'Najpierw wybierz segment'); pickupSelect.disabled = true; }
if (pickupSelect) { populateSelect(pickupSelect, [], t('pickupPlaceholder', 'Najpierw wybierz segment')); pickupSelect.disabled = true; }
hideExtras();
return;
}
@@ -289,10 +391,10 @@
}
});
if (filteredOptions.length > 0) {
populateSelect(pickupSelect, filteredOptions, 'Miejsce odbioru');
populateSelect(pickupSelect, filteredOptions, t('pickupLabel', 'Miejsce odbioru'));
pickupSelect.disabled = false;
} else {
populateSelect(pickupSelect, [], 'Brak lokalizacji dla tego segmentu');
populateSelect(pickupSelect, [], t('noPickupForSegment', 'Brak lokalizacji dla tego segmentu'));
pickupSelect.disabled = true;
}
if (returnSelect) {
@@ -301,7 +403,7 @@
if (b.city) label += ' — ' + b.city;
return { value: b.name, label: label };
});
populateSelect(returnSelect, returnOptions, 'Miejsce zwrotu');
populateSelect(returnSelect, returnOptions, t('returnLabel', 'Miejsce zwrotu'));
}
hideExtras();
}
@@ -353,7 +455,7 @@
function checkPastAndWarn(input, label) {
if (!input || !input.value) return;
if (input.value < getNowLocal()) {
warnPastDate(input, label + ' — data lub godzina już minęły');
warnPastDate(input, tFmt('warnPastDate', {label: label}, '%label% — data lub godzina już minęły'));
} else {
clearDateError(input);
}
@@ -367,13 +469,13 @@
if (!dateMinListenersBound) {
if (dateFrom) {
dateFrom.addEventListener('change', function () {
checkPastAndWarn(dateFrom, 'Rozpoczęcie');
checkPastAndWarn(dateFrom, t('dateStart', 'Rozpoczęcie'));
if (dateTo && dateFrom.value) dateTo.setAttribute('min', dateFrom.value);
});
}
if (dateTo) {
dateTo.addEventListener('change', function () {
checkPastAndWarn(dateTo, 'Zakończenie');
checkPastAndWarn(dateTo, t('dateEnd', 'Zakończenie'));
});
}
dateMinListenersBound = true;
@@ -417,16 +519,18 @@
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;
daysCount.innerHTML = tFmt('daysCount', {count: 0, unit: t('dayMany', 'dni')}, 'Wybrano: <strong>%count% %unit%</strong>'); return;
}
var diff = Math.ceil((to - from) / 86400000);
daysCount.innerHTML = 'Wybrano: <strong>' + diff + ' ' + (diff === 1 ? 'dzień' : 'dni') + '</strong>';
var dayUnit = diff === 1 ? t('dayWord', 'dzień') : t('daysWord', 'dni');
daysCount.innerHTML = tFmt('daysCount', {count: diff, unit: dayUnit}, 'Wybrano: <strong>%count% %unit%</strong>');
}
// ─── Protection Packages (WP-managed: SOFT, PREMIUM) ──────────
function loadProtectionPackages() {
return fetch(REST_URL + 'protection-packages', {
var lang = (document.documentElement.lang || '').toLowerCase().indexOf('en') === 0 ? 'en' : 'pl';
return fetch(REST_URL + 'protection-packages?lang=' + lang, {
credentials: 'same-origin'
}).then(function (r) { return r.ok ? r.json() : null; })
.then(function (data) {
@@ -444,7 +548,7 @@
var pkg = protectionPackages[key];
if (!pkg) return;
var price = parseFloat(pkg.pricePerDay || 0);
var priceLabel = price > 0 ? price.toFixed(0) + ' zł/doba' : 'Gratis';
var priceLabel = price > 0 ? tFmt('pricePerDay', {price: price.toFixed(0)}, '%price% zł/doba') : t('free', 'Gratis');
var descHtml = pkg.description ? '<span class="carei-form__protection-package__desc">' + escHtml(pkg.description) + '</span>' : '';
var card = document.createElement('label');
card.className = 'carei-form__protection-package';
@@ -500,7 +604,7 @@
var pricelist = pricelists[0];
currentPriceListId = pricelist.id;
var items = pricelist.additionalItems;
var insuranceItems = [], extraItems = [];
var extraItems = [];
abroadItems = [];
selectedCountries = {};
if (Array.isArray(items)) {
@@ -510,19 +614,20 @@
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;
// Drop Softra-insurance items — pakiety ochronne są zarządzane w panelu WP (SOFT/PREMIUM).
if (name.indexOf('ubezp') !== -1 || name.indexOf('ochrony') !== -1 ||
name.indexOf('zniesienie') !== -1 || name.indexOf('insurance') !== -1) {
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();
@@ -533,8 +638,12 @@
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');
? tFmt('priceRange', {min: price.toFixed(0), max: maxPrice.toFixed(0)}, 'od %min% do %max% zł')
: (price > 0
? (item.unit === 'doba'
? tFmt('pricePerDay', {price: price.toFixed(0)}, '%price% zł/doba')
: tFmt('priceSimple', {price: price.toFixed(0)}, '%price% zł'))
: t('free', 'Gratis'));
var card = document.createElement('div');
card.className = 'carei-form__extra-card';
card.innerHTML =
@@ -652,7 +761,7 @@
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 priceHtml = '<span class="carei-abroad__price-val">' + (price > 0 ? tFmt('priceSimple', {price: price.toFixed(0)}, '%price% zł') : t('free', 'Gratis')) + '</span>';
var card = document.createElement('div');
card.className = 'carei-abroad__card' + (isSelected ? ' carei-abroad__card--selected' : '');
@@ -660,7 +769,7 @@
'<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') + '">' +
'<span role="button" tabindex="0" class="carei-abroad__action" data-abroad-id="' + escAttr(id) + '" title="' + (isSelected ? t('btnRemove', 'Usuń') : t('btnAdd', '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>'
@@ -694,7 +803,7 @@
if (!select) return;
select.innerHTML = '';
var ph = document.createElement('option');
ph.value = ''; ph.disabled = true; ph.selected = true; ph.textContent = placeholder || 'Wybierz...';
ph.value = ''; ph.disabled = true; ph.selected = true; ph.textContent = placeholder || t('selectPlaceholder', 'Wybierz...');
select.appendChild(ph);
options.forEach(function (opt) {
var o = document.createElement('option');
@@ -720,15 +829,15 @@
// ─── 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' }
{ id: 'carei-segment', type: 'select', msgKey: 'selectSegment', msgFallback: 'Wybierz segment pojazdu' },
{ id: 'carei-date-from', type: 'input', msgKey: 'enterDateFrom', msgFallback: 'Podaj datę rozpoczęcia' },
{ id: 'carei-date-to', type: 'input', msgKey: 'enterDateTo', msgFallback: 'Podaj datę zakończenia' },
{ id: 'carei-pickup-branch', type: 'select', msgKey: 'selectPickup', msgFallback: 'Wybierz miejsce odbioru' },
{ id: 'carei-firstname', type: 'input', msgKey: 'enterFirstName', msgFallback: 'Podaj imię' },
{ id: 'carei-lastname', type: 'input', msgKey: 'enterLastName', msgFallback: 'Podaj nazwisko' },
{ id: 'carei-email', type: 'email', msgKey: 'enterEmail', msgFallback: 'Podaj poprawny adres e-mail' },
{ id: 'carei-phone', type: 'phone', msgKey: 'enterPhone', msgFallback: 'Podaj numer telefonu (min. 9 cyfr)' },
{ id: 'carei-privacy', type: 'checkbox', msgKey: 'privacyRequired', msgFallback: 'Wymagana zgoda na Politykę Prywatności' }
];
function validateForm() {
@@ -747,23 +856,23 @@
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); }
if (hasError) { valid = false; markFieldError(el, t(f.msgKey, f.msgFallback), 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');
valid = false; markFieldError(dateFrom, t('dateStartPast', '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');
valid = false; markFieldError(dateTo, t('dateEndPast', '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');
valid = false; markFieldError(dateTo, t('dateEndAfterStart', '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');
valid = false; markFieldError(returnSelect, t('selectReturn', 'Wybierz miejsce zwrotu'), 'select');
}
if (errorSummary) errorSummary.style.display = valid ? 'none' : 'block';
return valid;
@@ -826,11 +935,11 @@
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...';
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> ' + escHtml(t('btnProcessing', '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';
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> ' + escHtml(t('btnSubmit', 'Wyślij'));
}
}
@@ -859,7 +968,7 @@
function createCustomerAndShowSummary() {
var fd = collectFormData();
hideSummaryError();
announce('Ładowanie podsumowania...');
announce(t('loadingSummary', 'Ładowanie podsumowania...'));
apiPost('customer', {
firstName: fd.firstName,
@@ -878,11 +987,11 @@
currentCustomerId = res.customerId;
return loadPricingSummary(fd);
}
throw new Error(res.rejectReason || 'Nie udało się utworzyć klienta');
throw new Error(res.rejectReason || t('errorCustomerCreate', 'Nie udało się utworzyć klienta'));
}).catch(function (err) {
console.error('Customer creation failed:', err);
setSubmitState('ready');
showFormError('Błąd tworzenia klienta: ' + err.message);
showFormError(tFmt('errorCustomerCreatePrefix', {msg: err.message}, 'Błąd tworzenia klienta: %msg%'));
});
}
@@ -905,7 +1014,7 @@
}).catch(function (err) {
console.error('Pricing summary failed:', err);
setSubmitState('ready');
showFormError('Błąd pobierania podsumowania: ' + err.message);
showFormError(tFmt('errorSummaryPrefix', {msg: err.message}, 'Błąd pobierania podsumowania: %msg%'));
});
}
@@ -965,7 +1074,12 @@
function buildBookingComments(userMessage) {
var pkg = getSelectedProtectionPayload();
if (!pkg) return userMessage || '';
var pkgLine = 'Pakiet ochronny: ' + pkg.name + ' — ' + pkg.pricePerDay.toFixed(2) + ' zł/doba × ' + pkg.days + ' = ' + pkg.total.toFixed(2) + ' zł (do doliczenia poza systemem)';
var pkgLine = tFmt('protectionComment', {
name: pkg.name,
perDay: pkg.pricePerDay.toFixed(2),
days: pkg.days,
total: pkg.total.toFixed(2)
}, 'Pakiet ochronny: %name% — %perDay% zł/doba × %days% = %total% zł (do doliczenia poza systemem)');
return userMessage ? (pkgLine + '\n\n' + userMessage) : pkgLine;
}
@@ -1023,7 +1137,7 @@
// Animated transition: form → summary
transitionStep(form, summaryOverlay, function () {
announce('Podsumowanie rezerwacji');
announce(t('announceSummary', 'Podsumowanie rezerwacji'));
var title = summaryOverlay.querySelector('.carei-summary__title');
if (title) title.focus();
});
@@ -1039,40 +1153,40 @@
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>';
'<div><strong>' + escHtml(t('labelSegment', 'Segment')) + ':</strong> ' + escHtml(segLabel) + '</div>' +
'<div><strong>' + escHtml(t('labelFrom', 'Od')) + ':</strong> ' + escHtml(fd.dateFrom.replace('T', ' ')) + '</div>' +
'<div><strong>' + escHtml(t('labelTo', 'Do')) + ':</strong> ' + escHtml(fd.dateTo.replace('T', ' ')) + '</div>' +
'<div><strong>' + escHtml(t('labelPickup', 'Miejsce odbioru')) + ':</strong> ' + escHtml(pickupLabel) + '</div>';
if (returnLabel) {
html += '<div><strong>Miejsce zwrotu:</strong> ' + escHtml(returnLabel) + '</div>';
html += '<div><strong>' + escHtml(t('labelReturn', '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>';
html += '<div><strong>' + escHtml(t('labelRenter', 'Najemca')) + ':</strong> ' + escHtml(fd.firstName + ' ' + fd.lastName) + '</div>' +
'<div><strong>' + escHtml(t('labelEmail', 'Email')) + ':</strong> ' + escHtml(fd.email) + '</div>' +
'<div><strong>' + escHtml(t('labelPhone', 'Telefon')) + ':</strong> ' + escHtml(fd.phone) + '</div>';
// Selected extras
var selectedExtras = getSelectedExtrasForApi();
var pkgForDetails = getSelectedProtectionPayload();
if (selectedExtras.length > 0 || pkgForDetails) {
html += '<div style="margin-top:8px"><strong>Wybrane opcje:</strong></div><ul style="margin:4px 0 0 16px;padding:0;list-style:disc;">';
html += '<div style="margin-top:8px"><strong>' + escHtml(t('labelSelectedOptions', '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ł';
? tFmt('priceLineDaily', {perDay: fmtPrice(ex.priceAfterDiscount), days: ex.amount, total: fmtPrice(totalPrice)}, '%perDay% zł/doba × %days% = %total% zł')
: tFmt('priceSimpleFmt', {price: fmtPrice(totalPrice)}, '%price% zł');
html += '<li>' + escHtml(toSentenceCase(ex.name)) + ' — ' + priceInfo + '</li>';
});
if (pkgForDetails) {
var pkgInfo = pkgForDetails.days > 1
? fmtPrice(pkgForDetails.pricePerDay) + ' zł/doba × ' + pkgForDetails.days + ' = ' + fmtPrice(pkgForDetails.total) + ' zł'
: fmtPrice(pkgForDetails.total) + ' zł';
? tFmt('priceLineDaily', {perDay: fmtPrice(pkgForDetails.pricePerDay), days: pkgForDetails.days, total: fmtPrice(pkgForDetails.total)}, '%perDay% zł/doba × %days% = %total% zł')
: tFmt('priceSimpleFmt', {price: fmtPrice(pkgForDetails.total)}, '%price% zł');
html += '<li>' + escHtml(pkgForDetails.name) + ' — ' + pkgInfo + '</li>';
}
html += '</ul>';
}
if (fd.message) {
html += '<div style="margin-top:8px"><strong>Wiadomość:</strong> ' + escHtml(fd.message) + '</div>';
html += '<div style="margin-top:8px"><strong>' + escHtml(t('labelMessage', 'Wiadomość')) + ':</strong> ' + escHtml(fd.message) + '</div>';
}
summaryDetails.innerHTML = html;
@@ -1082,19 +1196,19 @@
// 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>';
var html = '<table><thead><tr><th>' + escHtml(t('thName', 'Nazwa')) + '</th><th>' + escHtml(t('thQuantity', 'Ilość')) + '</th><th>' + escHtml(t('thNet', 'Netto')) + '</th><th>' + escHtml(t('thGross', '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>' + escHtml(toSentenceCase(item.name)) + (item.addedBySystem ? ' <small>(' + escHtml(t('labelAuto', 'auto')) + ')</small>' : '') + '</td>' +
'<td>' + (item.amount || 1) + ' ' + escHtml(item.unit || '') + '</td>' +
'<td>' + fmtPrice(item.netValue) + '</td>' +
'<td>' + fmtPrice(item.grossValue) + '</td></tr>';
});
if (protectionPayload) {
html += '<tr class="carei-summary__protection-row">' +
'<td>' + escHtml(protectionPayload.name) + ' <small>(do doliczenia)</small></td>' +
'<td>' + protectionPayload.days + ' doba</td>' +
'<td>' + escHtml(protectionPayload.name) + ' <small>(' + escHtml(t('labelExtraCharge', 'do doliczenia')) + ')</small></td>' +
'<td>' + protectionPayload.days + ' ' + escHtml(pluralPl(protectionPayload.days, t('dayOne', 'doba'), t('dayFew', 'doby'), t('dayMany', 'dób'))) + '</td>' +
'<td>—</td>' +
'<td>' + fmtPrice(protectionPayload.total) + '</td></tr>';
}
@@ -1108,12 +1222,12 @@
var protectionTotal = protectionPayload ? protectionPayload.total : 0;
var grandGross = softraGross + protectionTotal;
var totalsHtml =
'<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"><span class="carei-summary__total-label">' + escHtml(t('thNet', '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">' + escHtml(t('labelVat', 'VAT')) + ':</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalVatValue) + '</span></div>';
if (protectionPayload) {
totalsHtml += '<div class="carei-summary__total-row"><span class="carei-summary__total-label">Pakiet ochronny:</span><span class="carei-summary__total-value">' + fmtPrice(protectionPayload.total) + '</span></div>';
totalsHtml += '<div class="carei-summary__total-row"><span class="carei-summary__total-label">' + escHtml(t('labelProtectionPackage', 'Pakiet ochronny')) + ':</span><span class="carei-summary__total-value">' + fmtPrice(protectionPayload.total) + '</span></div>';
}
totalsHtml += '<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(grandGross) + '</span></div>';
totalsHtml += '<div class="carei-summary__total-row carei-summary__total-row--gross"><span class="carei-summary__total-label">' + escHtml(t('labelToPay', 'Do zapłaty')) + ':</span><span class="carei-summary__total-value">' + tFmt('priceSimpleFmt', {price: fmtPrice(grandGross)}, '%price% zł') + '</span></div>';
summaryTotal.innerHTML = totalsHtml;
}
}
@@ -1136,7 +1250,7 @@
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ę...';
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> ' + escHtml(t('btnBookingInProgress', 'Rezerwuję...'));
}
hideSummaryError();
@@ -1178,7 +1292,7 @@
showSuccessView(res.reservationNo || res.reservationId);
return;
}
throw new Error(translateRejectReason(res.rejectReason) || 'Rezerwacja nie powiodła się');
throw new Error(translateRejectReason(res.rejectReason) || t('errorBookingFailed', 'Rezerwacja nie powiodła się'));
}).catch(function (err) {
console.error('Booking failed:', err);
showSummaryError(err.message);
@@ -1189,12 +1303,12 @@
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.'
'CAR_NOT_FOUND': t('rejectCarNotFound', 'Brak dostępnego pojazdu w wybranym terminie. Zmień daty lub segment.'),
'INVALID_DATE_RANGE': t('rejectInvalidDateRange', 'Nieprawidłowy zakres dat'),
'BRANCH_NOT_FOUND': t('rejectBranchNotFound', 'Nie znaleziono oddziału'),
'CUSTOMER_ALREADY_EXISTS': t('rejectCustomerExists', 'Klient o tych danych już istnieje w systemie'),
'INVALID_PESEL': t('rejectInvalidPesel', 'Nieprawidłowy numer PESEL'),
'PRICE_LIST_EXPIRED': t('rejectPriceListExpired', 'Cennik wygasł. Odśwież formularz i spróbuj ponownie.')
};
return map[reason] || reason;
}
@@ -1203,16 +1317,16 @@
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ę';
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> ' + escHtml(t('btnConfirmBooking', 'Potwierdź rezerwację'));
}
}
// ─── Success View ─────────────────────────────────────────────
function showSuccessView(reservationNo) {
if (successNumber) successNumber.textContent = 'Nr zamówienia: ' + reservationNo;
if (successNumber) successNumber.textContent = tFmt('orderNumber', {no: reservationNo}, 'Nr zamówienia: %no%');
transitionStep(summaryOverlay, successView, function () {
announce('Rezerwacja potwierdzona');
announce(t('announceBookingConfirmed', 'Rezerwacja potwierdzona'));
var title = successView.querySelector('.carei-success__title');
if (title) title.focus();
});
@@ -1226,7 +1340,7 @@
currentCustomerId = null;
currentReservationId = null;
hideExtras();
if (pickupSelect) { populateSelect(pickupSelect, [], 'Najpierw wybierz segment'); pickupSelect.disabled = true; }
if (pickupSelect) { populateSelect(pickupSelect, [], t('pickupPlaceholder', 'Najpierw wybierz segment')); pickupSelect.disabled = true; }
closeModal();
}
@@ -1324,13 +1438,13 @@
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);
var label = typeof c === 'string' ? tFmt('segmentLabel', {name: c}, 'Segment %name%') : (c.description || c.name || c);
return { value: val, label: label };
});
populateSelect(searchSegment, segments, 'Wybierz segment');
populateSelect(searchSegment, segments, t('selectSegmentShort', 'Wybierz segment'));
}
if (searchPickup) {
populateSelect(searchPickup, [], 'Najpierw wybierz segment');
populateSelect(searchPickup, [], t('pickupPlaceholder', 'Najpierw wybierz segment'));
searchPickup.disabled = true;
}
}).catch(function (err) {
@@ -1354,10 +1468,10 @@
}
});
if (opts.length > 0) {
populateSelect(searchPickup, opts, 'Miejsce odbioru');
populateSelect(searchPickup, opts, t('pickupLabel', 'Miejsce odbioru'));
searchPickup.disabled = false;
} else {
populateSelect(searchPickup, [], 'Brak lokalizacji');
populateSelect(searchPickup, [], t('noLocations', 'Brak lokalizacji'));
searchPickup.disabled = true;
}
});
@@ -1449,6 +1563,9 @@
// Inicjalizuj search form niezależnie od modala
initSearchForm();
// Flatpickr on both modal + hero inputs — safe to call even if modal absent
initDatePickers();
if (!overlay || !form) return;
initModal();