feat(07-hero-search-form): Mini formularz rezerwacji w hero z pre-fill do modala

Phase 7 complete:
- Nowy widget Elementor "Carei Search Form" do osadzenia w hero
- Pola: segment, daty od/do, lokalizacja, checkbox zwrotu
- Po kliknięciu przycisku otwiera modal z pre-wypełnionymi danymi
- Design zgodny z Figmą (tło #EDEDF3, przycisk czerwony, tytuł fioletowy)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 13:50:22 +02:00
parent 1dbb8ab5da
commit 92a58cb2e2
9 changed files with 945 additions and 39 deletions

View File

@@ -388,11 +388,11 @@
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(item.name) + '" data-unit="' + escAttr(item.unit || 'szt.') + '">' +
'<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(item.name) + '</strong>' +
(item.description ? '<span class="carei-form__extra-desc">' + escHtml(item.description) + '</span>' : '') +
'<span class="carei-form__extra-price">' + escHtml(priceLabel) + '</span></span></label>';
'<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;
}
@@ -881,7 +881,11 @@
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) {
html += '<li>' + escHtml(ex.name) + ' — ' + fmtPrice(ex.priceAfterDiscount) + ' zł</li>';
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>';
}
@@ -899,7 +903,7 @@
summary.pricelist.forEach(function (item) {
var rowClass = item.addedBySystem ? ' class="carei-summary__auto-item"' : '';
html += '<tr' + rowClass + '>' +
'<td>' + escHtml(item.name) + (item.addedBySystem ? ' <small>(auto)</small>' : '') + '</td>' +
'<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>';
@@ -1078,11 +1082,160 @@
function escHtml(str) { var d = document.createElement('div'); d.textContent = str || ''; return d.innerHTML; }
function escAttr(str) { return (str || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
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;
// Ł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();