This commit is contained in:
2026-03-25 00:41:16 +01:00
parent 1739f354d1
commit a82ec90a51
48 changed files with 4019 additions and 0 deletions

View File

@@ -0,0 +1,541 @@
/* ═══════════════════════════════════════════
Carei Reservation — Design Tokens
═══════════════════════════════════════════ */
@import url('https://fonts.googleapis.com/css2?family=Albert+Sans:wght@400;500;600;700&display=swap');
:root {
--carei-blue: #2F2482;
--carei-red: #FF0000;
--carei-red-hover: #D60000;
--carei-bg: #EDEDF3;
--carei-gray: #505050;
--carei-placeholder: #C7C7C7;
--carei-border: #D0D0D0;
--carei-white: #FFFFFF;
--carei-radius: 8px;
--carei-radius-lg: 16px;
--carei-input-h: 48px;
--carei-font: 'Albert Sans', sans-serif;
--carei-gap-section: 24px;
--carei-gap-inner: 16px;
}
/* ═══════════════════════════════════════════
Trigger Button
═══════════════════════════════════════════ */
.carei-reservation-trigger {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 14px 28px;
background-color: var(--carei-red);
color: var(--carei-white);
font-family: var(--carei-font);
font-weight: 600;
font-size: 14px;
border: none;
border-radius: var(--carei-radius);
cursor: pointer;
transition: background-color 0.2s ease;
text-decoration: none;
line-height: 1;
}
.carei-reservation-trigger:hover {
background-color: var(--carei-red-hover);
color: var(--carei-white);
}
.carei-reservation-trigger svg {
width: 16px;
height: 16px;
fill: currentColor;
}
/* ═══════════════════════════════════════════
Modal Overlay & Container
═══════════════════════════════════════════ */
.carei-modal-overlay {
display: none;
position: fixed;
inset: 0;
z-index: 100000;
background: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
padding: 20px;
}
.carei-modal-overlay.is-open {
display: flex;
}
.carei-modal {
background: var(--carei-bg);
border-radius: var(--carei-radius-lg);
max-width: 800px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
padding: 40px 48px;
position: relative;
font-family: var(--carei-font);
}
.carei-modal-close {
position: absolute;
top: 16px;
left: 16px;
background: none;
border: none;
cursor: pointer;
color: var(--carei-gray);
line-height: 1;
padding: 4px;
transition: color 0.2s;
}
.carei-modal-close:hover {
color: var(--carei-blue);
}
.carei-modal-title {
font-family: var(--carei-font);
font-weight: 700;
font-size: 20px;
color: var(--carei-blue);
text-align: center;
margin: 0 0 var(--carei-gap-section) 0;
}
.carei-modal-title span {
color: var(--carei-red);
}
/* Scrollbar styling */
.carei-modal::-webkit-scrollbar {
width: 6px;
}
.carei-modal::-webkit-scrollbar-track {
background: transparent;
}
.carei-modal::-webkit-scrollbar-thumb {
background: var(--carei-border);
border-radius: 3px;
}
/* ═══════════════════════════════════════════
Form Layout
═══════════════════════════════════════════ */
.carei-form {
display: flex;
flex-direction: column;
gap: var(--carei-gap-section);
}
.carei-form__section {
display: flex;
flex-direction: column;
gap: var(--carei-gap-inner);
}
.carei-form__row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--carei-gap-inner);
}
.carei-form__row--dates {
grid-template-columns: 1fr 1fr;
}
.carei-form__field {
display: flex;
flex-direction: column;
gap: 4px;
}
.carei-form__field--full {
grid-column: 1 / -1;
}
/* ═══════════════════════════════════════════
Inputs & Selects
═══════════════════════════════════════════ */
.carei-form__input,
.carei-form__textarea,
.carei-form__select-wrap select {
height: var(--carei-input-h);
padding: 0 16px;
border: 1px solid transparent;
border-radius: var(--carei-radius);
background: var(--carei-white);
font-family: var(--carei-font);
font-weight: 600;
font-size: 15px;
color: var(--carei-blue);
outline: none;
transition: border-color 0.2s;
width: 100%;
box-sizing: border-box;
}
.carei-form__input::placeholder,
.carei-form__textarea::placeholder {
color: var(--carei-gray);
font-weight: 400;
}
.carei-form__input:focus,
.carei-form__textarea:focus,
.carei-form__select-wrap select:focus {
border-color: var(--carei-blue);
}
.carei-form__textarea {
height: 143px;
padding: 16px;
resize: vertical;
line-height: 1.5;
}
.carei-form__label-small {
font-family: var(--carei-font);
font-weight: 400;
font-size: 12px;
color: var(--carei-gray);
display: flex;
align-items: center;
gap: 4px;
}
.carei-form__icon-calendar {
color: var(--carei-gray);
}
/* Select wrapper */
.carei-form__select-wrap {
position: relative;
display: flex;
align-items: center;
}
.carei-form__select-wrap select {
appearance: none;
-webkit-appearance: none;
padding-right: 40px;
cursor: pointer;
}
.carei-form__select-wrap--icon select {
padding-left: 40px;
}
.carei-form__select-arrow {
position: absolute;
right: 16px;
pointer-events: none;
color: var(--carei-blue);
}
.carei-form__icon-pin {
position: absolute;
left: 14px;
pointer-events: none;
color: var(--carei-gray);
z-index: 1;
}
/* Days count */
.carei-form__days-count {
font-family: var(--carei-font);
font-size: 13px;
color: var(--carei-gray);
text-align: right;
}
.carei-form__days-count strong {
color: var(--carei-blue);
font-weight: 700;
}
/* ═══════════════════════════════════════════
Phone Input
═══════════════════════════════════════════ */
.carei-form__phone-wrap {
display: flex;
align-items: center;
background: var(--carei-white);
border-radius: var(--carei-radius);
border: 1px solid transparent;
transition: border-color 0.2s;
height: var(--carei-input-h);
}
.carei-form__phone-wrap:focus-within {
border-color: var(--carei-blue);
}
.carei-form__phone-prefix {
display: flex;
align-items: center;
gap: 4px;
padding: 0 8px 0 12px;
border-right: 1px solid var(--carei-border);
height: 60%;
flex-shrink: 0;
font-size: 14px;
color: var(--carei-gray);
}
.carei-form__phone-flag {
font-size: 18px;
line-height: 1;
}
.carei-form__phone-code {
font-family: var(--carei-font);
font-weight: 500;
font-size: 14px;
color: var(--carei-gray);
}
.carei-form__input--phone {
border: none;
background: transparent;
height: 100%;
padding-left: 12px;
}
.carei-form__input--phone::placeholder {
color: var(--carei-placeholder);
letter-spacing: 2px;
}
.carei-form__input--phone:focus {
border: none;
}
/* ═══════════════════════════════════════════
Checkboxes (custom)
═══════════════════════════════════════════ */
.carei-form__checkbox-label {
display: flex;
align-items: flex-start;
gap: 10px;
cursor: pointer;
font-family: var(--carei-font);
font-size: 14px;
color: var(--carei-blue);
user-select: none;
}
.carei-form__checkbox-label input[type="checkbox"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
pointer-events: none;
}
.carei-form__checkbox-box {
width: 24px;
height: 24px;
min-width: 24px;
border: 2px solid var(--carei-border);
border-radius: var(--carei-radius);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
background: var(--carei-white);
margin-top: 1px;
}
.carei-form__checkbox-box svg {
opacity: 0;
transition: opacity 0.15s;
}
.carei-form__checkbox-label input:checked + .carei-form__checkbox-box {
background: var(--carei-blue);
border-color: var(--carei-blue);
}
.carei-form__checkbox-label input:checked + .carei-form__checkbox-box svg {
opacity: 1;
}
.carei-form__checkbox-text {
line-height: 1.6;
padding-top: 2px;
}
.carei-form__checkbox-text a {
color: var(--carei-blue);
text-decoration: underline;
font-weight: 600;
}
.carei-form__checkbox-text a:hover {
color: var(--carei-red);
}
/* ═══════════════════════════════════════════
Dividers
═══════════════════════════════════════════ */
.carei-form__divider {
display: flex;
align-items: center;
gap: 16px;
font-family: 'Roboto', var(--carei-font), sans-serif;
font-size: 12px;
color: var(--carei-gray);
text-transform: none;
}
.carei-form__divider::before,
.carei-form__divider::after {
content: '';
flex: 1;
height: 1px;
background: var(--carei-border);
}
.carei-form__divider span {
white-space: nowrap;
}
/* ═══════════════════════════════════════════
Extra Cards (Ubezpieczenie, Fotelik)
═══════════════════════════════════════════ */
.carei-form__extra-card {
border: 1px solid var(--carei-border);
border-radius: var(--carei-radius);
padding: 16px;
background: var(--carei-white);
transition: border-color 0.2s;
}
.carei-form__extra-card:has(input:checked) {
border-color: var(--carei-blue);
}
.carei-form__checkbox-label--card {
align-items: flex-start;
}
.carei-form__extra-content {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.carei-form__extra-content strong {
font-weight: 700;
font-size: 14px;
color: var(--carei-blue);
}
.carei-form__extra-desc {
font-size: 12px;
color: var(--carei-gray);
font-weight: 400;
line-height: 1.4;
}
.carei-form__extra-price {
font-weight: 700;
font-size: 15px;
color: var(--carei-red);
margin-top: 4px;
}
/* ═══════════════════════════════════════════
Footer (Privacy + Submit)
═══════════════════════════════════════════ */
.carei-form__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--carei-gap-inner);
padding-top: 8px;
}
.carei-form__checkbox-label--privacy {
flex: 1;
}
.carei-form__submit {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
background-color: var(--carei-red);
color: var(--carei-white);
font-family: var(--carei-font);
font-weight: 600;
font-size: 14px;
border: none;
border-radius: var(--carei-radius);
cursor: pointer;
transition: background-color 0.2s;
white-space: nowrap;
line-height: 1;
flex-shrink: 0;
}
.carei-form__submit:hover {
background-color: var(--carei-red-hover);
}
.carei-form__submit:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.carei-form__submit svg {
width: 16px;
height: 16px;
}
/* ═══════════════════════════════════════════
Validation
═══════════════════════════════════════════ */
.carei-form__field--error .carei-form__input,
.carei-form__field--error .carei-form__textarea,
.carei-form__field--error .carei-form__select-wrap select,
.carei-form__field--error .carei-form__phone-wrap {
border-color: var(--carei-red) !important;
}
.carei-form__checkbox-label--error .carei-form__checkbox-box {
border-color: var(--carei-red) !important;
}
.carei-form__error-msg {
font-family: var(--carei-font);
font-size: 11px;
color: var(--carei-red);
margin-top: 2px;
}
.carei-form__error-summary {
font-family: var(--carei-font);
font-size: 13px;
color: var(--carei-red);
text-align: center;
padding: 8px;
background: rgba(255, 0, 0, 0.05);
border-radius: var(--carei-radius);
}
/* Loading state for selects */
.carei-form__select-wrap--loading select {
color: var(--carei-placeholder);
pointer-events: none;
}
/* Return branch slide */
.carei-form__return-wrap {
overflow: hidden;
transition: max-height 0.3s ease, opacity 0.3s ease;
max-height: 0;
opacity: 0;
}
.carei-form__return-wrap.is-visible {
max-height: 80px;
opacity: 1;
display: block !important;
}
/* ═══════════════════════════════════════════
Mobile Responsive (<768px)
═══════════════════════════════════════════ */
@media (max-width: 768px) {
.carei-modal-overlay {
padding: 0;
align-items: flex-end;
}
.carei-modal {
width: 100%;
max-width: 100%;
max-height: 100vh;
height: 100%;
border-radius: var(--carei-radius-lg) var(--carei-radius-lg) 0 0;
padding: 32px 24px;
border-radius: 0;
}
.carei-form__row {
grid-template-columns: 1fr;
}
.carei-form__row--dates {
grid-template-columns: 1fr 1fr;
}
.carei-form__footer {
flex-direction: column;
align-items: stretch;
}
.carei-form__submit {
width: 100%;
justify-content: center;
padding: 16px;
}
}
/* Very small screens */
@media (max-width: 480px) {
.carei-form__row--dates {
grid-template-columns: 1fr;
}
.carei-modal {
padding: 24px 16px;
}
}

View File

@@ -0,0 +1,591 @@
(function () {
'use strict';
var REST_URL = (window.careiReservation && window.careiReservation.restUrl) || '/wp-json/carei/v1/';
var NONCE = (window.careiReservation && window.careiReservation.nonce) || '';
// ─── API Helpers ──────────────────────────────────────────────
function apiGet(endpoint) {
return fetch(REST_URL + endpoint, {
method: 'GET',
headers: {
'X-WP-Nonce': NONCE,
'Content-Type': 'application/json'
}
}).then(function (r) {
if (!r.ok) throw new Error('API error: ' + r.status);
return r.json();
});
}
function apiPost(endpoint, data) {
return fetch(REST_URL + endpoint, {
method: 'POST',
headers: {
'X-WP-Nonce': NONCE,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}).then(function (r) {
if (!r.ok) throw new Error('API error: ' + r.status);
return r.json();
});
}
// ─── DOM Refs ─────────────────────────────────────────────────
var overlay, form, segmentSelect, dateFrom, dateTo, daysCount;
var pickupSelect, returnSelect, returnWrap, sameReturnCheck;
var extrasContainer, errorSummary;
function initRefs() {
overlay = document.querySelector('[data-carei-modal]');
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');
extrasContainer = document.getElementById('carei-extras-container');
errorSummary = document.getElementById('carei-error-summary');
}
// ─── Modal Open/Close ─────────────────────────────────────────
function initModal() {
var triggers = document.querySelectorAll('[data-carei-open-modal]');
var closeBtns = document.querySelectorAll('[data-carei-close-modal]');
triggers.forEach(function (btn) {
btn.addEventListener('click', function (e) {
e.preventDefault();
openModal();
});
});
closeBtns.forEach(function (btn) {
btn.addEventListener('click', closeModal);
});
if (overlay) {
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeModal();
});
}
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && overlay && overlay.classList.contains('is-open')) {
closeModal();
}
});
}
var dataLoaded = false;
function openModal() {
if (!overlay) return;
overlay.classList.add('is-open');
document.body.style.overflow = 'hidden';
if (!dataLoaded) {
loadBranches();
loadAllCarClasses();
setDefaultDates();
dataLoaded = true;
}
}
function closeModal() {
if (!overlay) return;
overlay.classList.remove('is-open');
document.body.style.overflow = '';
}
// ─── Default Dates ────────────────────────────────────────────
function setDefaultDates() {
if (!dateFrom || !dateTo) return;
var tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(10, 0, 0, 0);
var dayAfter = new Date();
dayAfter.setDate(dayAfter.getDate() + 2);
dayAfter.setHours(10, 0, 0, 0);
dateFrom.value = formatDatetimeLocal(tomorrow);
dateTo.value = formatDatetimeLocal(dayAfter);
updateDaysCount();
}
function formatDatetimeLocal(d) {
var y = d.getFullYear();
var m = String(d.getMonth() + 1).padStart(2, '0');
var day = String(d.getDate()).padStart(2, '0');
var h = String(d.getHours()).padStart(2, '0');
var min = String(d.getMinutes()).padStart(2, '0');
return y + '-' + m + '-' + day + 'T' + h + ':' + min;
}
// ─── Days Count ───────────────────────────────────────────────
function updateDaysCount() {
if (!dateFrom || !dateTo || !daysCount) return;
var from = new Date(dateFrom.value);
var 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) / (1000 * 60 * 60 * 24));
var label = diff === 1 ? 'dzień' : 'dni';
daysCount.innerHTML = 'Wybrano: <strong>' + diff + ' ' + label + '</strong>';
}
// ─── Load Branches ────────────────────────────────────────────
function loadBranches() {
if (!pickupSelect) return;
setSelectLoading(pickupSelect, true);
apiGet('branches')
.then(function (branches) {
if (!Array.isArray(branches)) {
branches = [];
}
populateSelect(pickupSelect, branches.map(function (b) {
var label = b.description || b.name;
if (b.city) label += ' — ' + b.city;
return { value: b.name, label: label };
}), 'Miejsce odbioru');
// Copy to return branch
if (returnSelect) {
populateSelect(returnSelect, branches.map(function (b) {
var label = b.description || b.name;
if (b.city) label += ' — ' + b.city;
return { value: b.name, label: label };
}), 'Miejsce zwrotu');
}
setSelectLoading(pickupSelect, false);
})
.catch(function (err) {
console.error('Failed to load branches:', err);
setSelectLoading(pickupSelect, false);
});
}
// ─── Load All Car Classes (on modal open) ────────────────────
function loadAllCarClasses() {
if (!segmentSelect) return;
setSelectLoading(segmentSelect, true);
apiGet('car-classes-all')
.then(function (classes) {
if (!Array.isArray(classes) || classes.length === 0) {
populateSelect(segmentSelect, [], 'Brak segmentów');
setSelectLoading(segmentSelect, false);
return;
}
populateSelect(segmentSelect, 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 };
}), 'Wybierz segment pojazdu');
setSelectLoading(segmentSelect, false);
})
.catch(function (err) {
console.error('Failed to load car classes:', err);
populateSelect(segmentSelect, [], 'Błąd ładowania');
setSelectLoading(segmentSelect, false);
});
}
// ─── Load Available Car Classes (filtered by dates+branch) ───
function loadCarClasses() {
if (!segmentSelect || !dateFrom || !dateTo || !pickupSelect) return;
var fromVal = dateFrom.value;
var toVal = dateTo.value;
var branch = pickupSelect.value;
if (!fromVal || !toVal || !branch) return;
// Convert datetime-local to API format: YYYY-MM-DDTHH:MM:SS
var apiFrom = fromVal.replace('T', 'T') + ':00';
var apiTo = toVal.replace('T', 'T') + ':00';
setSelectLoading(segmentSelect, true);
apiPost('car-classes', {
dateFrom: apiFrom,
dateTo: apiTo,
branchName: branch
})
.then(function (classes) {
if (!Array.isArray(classes) || classes.length === 0) {
populateSelect(segmentSelect, [], 'Brak dostępnych klas');
setSelectLoading(segmentSelect, false);
return;
}
populateSelect(segmentSelect, classes.map(function (c) {
// classes might be strings or objects
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 };
}), 'Wybierz segment pojazdu');
setSelectLoading(segmentSelect, false);
})
.catch(function (err) {
console.error('Failed to load car classes:', err);
populateSelect(segmentSelect, [], 'Błąd ładowania');
setSelectLoading(segmentSelect, false);
});
}
// ─── Load Extras from Pricelist ───────────────────────────────
function loadExtras() {
if (!segmentSelect || !dateFrom || !dateTo || !pickupSelect || !extrasContainer) return;
var category = segmentSelect.value;
var fromVal = dateFrom.value;
var toVal = dateTo.value;
var branch = pickupSelect.value;
if (!category || !fromVal || !toVal || !branch) return;
var apiFrom = fromVal + ':00';
var apiTo = toVal + ':00';
apiPost('pricelist', {
category: category,
dateFrom: apiFrom,
dateTo: apiTo,
pickUpLocation: branch
})
.then(function (pricelists) {
if (!Array.isArray(pricelists) || pricelists.length === 0) return;
var pricelist = pricelists[0];
var items = pricelist.additionalItems;
if (!Array.isArray(items) || items.length === 0) return;
extrasContainer.innerHTML = '';
items.forEach(function (item) {
var price = parseFloat(item.price || item.minPrice || 0);
var priceLabel = price > 0 ? (price.toFixed(0) + ' zł') : '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 + '">' +
'<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>' +
'<span class="carei-form__extra-price">' + escHtml(priceLabel) + '</span>' +
'</span>' +
'</label>';
extrasContainer.appendChild(card);
});
})
.catch(function (err) {
console.error('Failed to load pricelist:', err);
// Keep static fallback extras
});
}
// ─── Select Helpers ───────────────────────────────────────────
function populateSelect(select, options, placeholder) {
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) {
var wrap = select.closest('.carei-form__select-wrap');
if (!wrap) return;
if (loading) {
wrap.classList.add('carei-form__select-wrap--loading');
} else {
wrap.classList.remove('carei-form__select-wrap--loading');
}
}
// ─── Same Return Location ─────────────────────────────────────
function initSameReturn() {
if (!sameReturnCheck || !returnWrap) return;
sameReturnCheck.addEventListener('change', function () {
if (sameReturnCheck.checked) {
returnWrap.classList.remove('is-visible');
} else {
returnWrap.classList.add('is-visible');
}
});
}
// ─── 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;
// Clear previous errors
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') {
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!el.value.trim() || !emailRegex.test(el.value.trim())) hasError = true;
} else if (f.type === 'phone') {
var digits = el.value.replace(/\D/g, '');
if (digits.length < 9) 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);
}
});
// Check dateTo > dateFrom
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');
}
}
// Return branch required if different location
if (sameReturnCheck && !sameReturnCheck.checked && returnSelect) {
if (!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) {
// For phone in wrap
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();
}
// Checkbox
var label = e.target.closest('.carei-form__checkbox-label');
if (label) label.classList.remove('carei-form__checkbox-label--error');
});
// Also on change for checkboxes
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 ──────────────────────────────────────────────
function initSubmit() {
if (!form) return;
form.addEventListener('submit', function (e) {
e.preventDefault();
if (!validateForm()) return;
var formData = collectFormData();
console.log('Carei Reservation — Form data:', formData);
// Phase 3 will handle actual API submission
var submitBtn = form.querySelector('.carei-form__submit');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.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> Wysyłanie...';
// Re-enable after 2s (temporary — Phase 3 will handle properly)
setTimeout(function () {
submitBtn.disabled = false;
submitBtn.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';
}, 2000);
}
});
}
function collectFormData() {
var extras = [];
form.querySelectorAll('input[name="extras[]"]:checked').forEach(function (cb) {
extras.push({
id: cb.value,
price: parseFloat(cb.getAttribute('data-price') || 0)
});
});
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 : '',
extras: extras,
firstName: val('carei-firstname'),
lastName: val('carei-lastname'),
email: val('carei-email'),
phone: '+48' + (document.getElementById('carei-phone') ? document.getElementById('carei-phone').value.replace(/\D/g, '') : ''),
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() : '';
}
// ─── Event Listeners for Dynamic Loading ──────────────────────
function initDynamicLoading() {
// Date change → update days count
if (dateFrom) dateFrom.addEventListener('change', updateDaysCount);
if (dateTo) dateTo.addEventListener('change', updateDaysCount);
// When segment + dates + branch all set → load extras from pricelist
if (segmentSelect) segmentSelect.addEventListener('change', loadExtras);
if (pickupSelect) pickupSelect.addEventListener('change', loadExtras);
if (dateFrom) dateFrom.addEventListener('change', loadExtras);
if (dateTo) dateTo.addEventListener('change', loadExtras);
}
// ─── HTML Escape Helpers ──────────────────────────────────────
function escHtml(str) {
var div = document.createElement('div');
div.textContent = str || '';
return div.innerHTML;
}
function escAttr(str) {
return (str || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// ─── Init ─────────────────────────────────────────────────────
function init() {
initRefs();
if (!overlay || !form) return;
initModal();
initSameReturn();
initDynamicLoading();
initClearErrors();
initSubmit();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@@ -0,0 +1,107 @@
<?php
/**
* Plugin Name: Carei Reservation
* Description: Formularz rezerwacji samochodu zintegrowany z Softra Rent API, jako widget Elementor.
* Version: 1.0.0
* Author: Carei
* Text Domain: carei-reservation
* Requires PHP: 7.4
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'CAREI_RESERVATION_VERSION', '1.0.0' );
define( 'CAREI_RESERVATION_PATH', plugin_dir_path( __FILE__ ) );
define( 'CAREI_RESERVATION_URL', plugin_dir_url( __FILE__ ) );
/**
* Parse .env file (format: "key: value")
*/
function carei_parse_env() {
$env_path = ABSPATH . '.env';
if ( ! file_exists( $env_path ) ) {
return array();
}
$lines = file( $env_path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
$env = array();
foreach ( $lines as $line ) {
$line = trim( $line );
if ( '' === $line || '#' === $line[0] ) {
continue;
}
$pos = strpos( $line, ':' );
if ( false === $pos ) {
continue;
}
$key = trim( substr( $line, 0, $pos ) );
$value = trim( substr( $line, $pos + 1 ) );
$env[ $key ] = $value;
}
return $env;
}
/**
* Load includes
*/
require_once CAREI_RESERVATION_PATH . 'includes/class-softra-api.php';
require_once CAREI_RESERVATION_PATH . 'includes/class-rest-proxy.php';
/**
* Initialize plugin on plugins_loaded
*/
add_action( 'plugins_loaded', function () {
$env = carei_parse_env();
$api_url = isset( $env['url'] ) ? $env['url'] : '';
$username = isset( $env['username'] ) ? $env['username'] : '';
$password = isset( $env['password'] ) ? $env['password'] : '';
if ( empty( $api_url ) || empty( $username ) || empty( $password ) ) {
add_action( 'admin_notices', function () {
echo '<div class="notice notice-error"><p><strong>Carei Reservation:</strong> Brak konfiguracji API w pliku .env (url, username, password).</p></div>';
} );
return;
}
// Initialize Softra API singleton
Carei_Softra_API::init( $api_url, $username, $password );
// Initialize REST proxy
new Carei_REST_Proxy();
} );
/**
* Register Elementor widget
*/
add_action( 'elementor/widgets/register', function ( $widgets_manager ) {
require_once CAREI_RESERVATION_PATH . 'includes/class-elementor-widget.php';
$widgets_manager->register( new Carei_Reservation_Widget() );
} );
/**
* Enqueue frontend assets
*/
add_action( 'wp_enqueue_scripts', function () {
wp_register_style(
'carei-reservation-css',
CAREI_RESERVATION_URL . 'assets/css/carei-reservation.css',
array(),
CAREI_RESERVATION_VERSION
);
wp_register_script(
'carei-reservation-js',
CAREI_RESERVATION_URL . 'assets/js/carei-reservation.js',
array(),
CAREI_RESERVATION_VERSION,
true
);
wp_localize_script( 'carei-reservation-js', 'careiReservation', array(
'restUrl' => esc_url_raw( rest_url( 'carei/v1/' ) ),
'nonce' => wp_create_nonce( 'wp_rest' ),
) );
} );

View File

@@ -0,0 +1,221 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Elementor Widget: Carei Reservation form in modal.
*/
class Carei_Reservation_Widget extends \Elementor\Widget_Base {
public function get_name() {
return 'carei-reservation';
}
public function get_title() {
return 'Carei Reservation';
}
public function get_icon() {
return 'eicon-form-horizontal';
}
public function get_categories() {
return array( 'general' );
}
public function get_style_depends() {
return array( 'carei-reservation-css' );
}
public function get_script_depends() {
return array( 'carei-reservation-js' );
}
protected function register_controls() {
$this->start_controls_section( 'content_section', array(
'label' => 'Przycisk rezerwacji',
'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
) );
$this->add_control( 'button_text', array(
'label' => 'Tekst przycisku',
'type' => \Elementor\Controls_Manager::TEXT,
'default' => 'Złóż zapytanie o rezerwację',
) );
$this->end_controls_section();
}
protected function render() {
$settings = $this->get_settings_for_display();
$button_text = esc_html( $settings['button_text'] );
?>
<button type="button" class="carei-reservation-trigger" data-carei-open-modal>
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
<?php echo $button_text; ?>
</button>
<div class="carei-modal-overlay" data-carei-modal>
<div class="carei-modal">
<button type="button" class="carei-modal-close" data-carei-close-modal>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<h2 class="carei-modal-title">Wypełnij formularz rezerwacji<span>.</span></h2>
<form class="carei-form" id="carei-reservation-form" novalidate>
<!-- Dane wynajmu -->
<div class="carei-form__section">
<div class="carei-form__field carei-form__field--full">
<div class="carei-form__select-wrap">
<select id="carei-segment" name="segment" required>
<option value="" disabled selected>Wybierz segment pojazdu</option>
</select>
<svg class="carei-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</div>
</div>
<div class="carei-form__row carei-form__row--dates">
<div class="carei-form__field">
<label class="carei-form__label-small" for="carei-date-from">Od kiedy?</label>
<input type="datetime-local" id="carei-date-from" name="dateFrom" class="carei-form__input" required>
</div>
<div class="carei-form__field">
<label class="carei-form__label-small" for="carei-date-to">
<svg class="carei-form__icon-calendar" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="3" width="12" height="11" rx="1" stroke="currentColor" stroke-width="1.5"/><path d="M2 6h12M5 1v3M11 1v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
Do kiedy?
</label>
<input type="datetime-local" id="carei-date-to" name="dateTo" class="carei-form__input" required>
</div>
</div>
<div class="carei-form__days-count" id="carei-days-count">Wybrano: <strong>0 dni</strong></div>
<div class="carei-form__field carei-form__field--full">
<div class="carei-form__select-wrap carei-form__select-wrap--icon">
<svg class="carei-form__icon-pin" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1C5.24 1 3 3.24 3 6c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 7a2 2 0 110-4 2 2 0 010 4z" fill="currentColor"/></svg>
<select id="carei-pickup-branch" name="pickupBranch" required>
<option value="" disabled selected>Miejsce odbioru</option>
</select>
<svg class="carei-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</div>
</div>
<label class="carei-form__checkbox-label">
<input type="checkbox" id="carei-same-return" name="sameReturn" checked>
<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__checkbox-text">Zwrot w tej samej lokalizacji</span>
</label>
<div class="carei-form__field carei-form__field--full carei-form__return-wrap" id="carei-return-wrap" style="display:none;">
<div class="carei-form__select-wrap carei-form__select-wrap--icon">
<svg class="carei-form__icon-pin" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1C5.24 1 3 3.24 3 6c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 7a2 2 0 110-4 2 2 0 010 4z" fill="currentColor"/></svg>
<select id="carei-return-branch" name="returnBranch">
<option value="" disabled selected>Miejsce zwrotu</option>
</select>
<svg class="carei-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</div>
</div>
</div>
<!-- Opcje dodatkowe -->
<div class="carei-form__divider"><span>Opcje dodatkowe</span></div>
<div class="carei-form__section">
<div class="carei-form__row" id="carei-extras-container">
<div class="carei-form__extra-card">
<label class="carei-form__checkbox-label carei-form__checkbox-label--card">
<input type="checkbox" name="extras[]" value="insurance">
<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>Rozszerzone ubezpieczenie</strong>
<span class="carei-form__extra-desc">Obejmuje brak odpowiedzialności najemcy za wszelki szkody poniesione na aucie.</span>
<span class="carei-form__extra-price">300 zł</span>
</span>
</label>
</div>
<div class="carei-form__extra-card">
<label class="carei-form__checkbox-label carei-form__checkbox-label--card">
<input type="checkbox" name="extras[]" value="child-seat">
<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>Fotelik dla dziecka</strong>
<span class="carei-form__extra-desc">Prosimy zawrzeć wiek dziecka w wiadomości.</span>
<span class="carei-form__extra-price">50 zł</span>
</span>
</label>
</div>
</div>
</div>
<!-- Dane najemcy -->
<div class="carei-form__divider"><span>Dane najemcy</span></div>
<div class="carei-form__section">
<div class="carei-form__row">
<div class="carei-form__field">
<label class="carei-form__label-small" for="carei-firstname">Imię</label>
<input type="text" id="carei-firstname" name="firstName" class="carei-form__input" required>
</div>
<div class="carei-form__field">
<label class="carei-form__label-small" for="carei-lastname">Nazwisko</label>
<input type="text" id="carei-lastname" name="lastName" class="carei-form__input" required>
</div>
</div>
<div class="carei-form__row">
<div class="carei-form__field">
<label class="carei-form__label-small" for="carei-email">Adres e-mail</label>
<input type="email" id="carei-email" name="email" class="carei-form__input" required>
</div>
<div class="carei-form__field">
<label class="carei-form__label-small" for="carei-phone">Nr telefonu</label>
<div class="carei-form__phone-wrap">
<div class="carei-form__phone-prefix">
<span class="carei-form__phone-flag">🇵🇱</span>
<span class="carei-form__phone-code">+48</span>
</div>
<input type="tel" id="carei-phone" name="phone" class="carei-form__input carei-form__input--phone" placeholder="___ ___ ___" required>
</div>
</div>
</div>
<div class="carei-form__field carei-form__field--full">
<textarea id="carei-message" name="message" class="carei-form__textarea" placeholder="Twoja wiadomość dotycząca rezerwacji" rows="4"></textarea>
</div>
</div>
<!-- Stopka -->
<div class="carei-form__footer">
<label class="carei-form__checkbox-label carei-form__checkbox-label--privacy">
<input type="checkbox" id="carei-privacy" name="privacy" required>
<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__checkbox-text">Zgadzam się na <a href="/polityka-prywatnosci/" target="_blank">Politykę Prywatności</a></span>
</label>
<button type="submit" class="carei-form__submit">
<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
</button>
</div>
<div class="carei-form__error-summary" id="carei-error-summary" style="display:none;">
Uzupełnij wymagane pola zaznaczone na czerwono.
</div>
</form>
</div>
</div>
<?php
}
}

View File

@@ -0,0 +1,242 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* WP REST API proxy to Softra Rent API.
* Namespace: carei/v1
*/
class Carei_REST_Proxy {
const NAMESPACE = 'carei/v1';
public function __construct() {
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}
public function register_routes() {
// GET /branches
register_rest_route( self::NAMESPACE, '/branches', array(
'methods' => 'GET',
'callback' => array( $this, 'get_branches' ),
'permission_callback' => '__return_true',
) );
// GET /car-classes-all (all defined classes, no params needed)
register_rest_route( self::NAMESPACE, '/car-classes-all', array(
'methods' => 'GET',
'callback' => array( $this, 'get_all_car_classes' ),
'permission_callback' => '__return_true',
) );
// POST /car-classes
register_rest_route( self::NAMESPACE, '/car-classes', array(
'methods' => 'POST',
'callback' => array( $this, 'get_car_classes' ),
'permission_callback' => array( $this, 'check_nonce' ),
'args' => array(
'dateFrom' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
'dateTo' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
'branchName' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
),
) );
// POST /car-models
register_rest_route( self::NAMESPACE, '/car-models', array(
'methods' => 'POST',
'callback' => array( $this, 'get_car_models' ),
'permission_callback' => array( $this, 'check_nonce' ),
'args' => array(
'dateFrom' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
'dateTo' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
'branchName' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
'category' => array( 'required' => false, 'sanitize_callback' => 'sanitize_text_field', 'default' => '' ),
),
) );
// POST /pricelist
register_rest_route( self::NAMESPACE, '/pricelist', array(
'methods' => 'POST',
'callback' => array( $this, 'get_pricelist' ),
'permission_callback' => array( $this, 'check_nonce' ),
'args' => array(
'category' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
'dateFrom' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
'dateTo' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
'pickUpLocation' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
),
) );
// POST /pricing-summary
register_rest_route( self::NAMESPACE, '/pricing-summary', array(
'methods' => 'POST',
'callback' => array( $this, 'get_pricing_summary' ),
'permission_callback' => array( $this, 'check_nonce' ),
) );
// POST /customer
register_rest_route( self::NAMESPACE, '/customer', array(
'methods' => 'POST',
'callback' => array( $this, 'add_customer' ),
'permission_callback' => array( $this, 'check_nonce' ),
) );
// POST /booking
register_rest_route( self::NAMESPACE, '/booking', array(
'methods' => 'POST',
'callback' => array( $this, 'make_booking' ),
'permission_callback' => array( $this, 'check_nonce' ),
) );
// POST /booking/confirm
register_rest_route( self::NAMESPACE, '/booking/confirm', array(
'methods' => 'POST',
'callback' => array( $this, 'confirm_booking' ),
'permission_callback' => array( $this, 'check_nonce' ),
'args' => array(
'reservationId' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
),
) );
// GET /agreements
register_rest_route( self::NAMESPACE, '/agreements', array(
'methods' => 'GET',
'callback' => array( $this, 'get_agreements' ),
'permission_callback' => '__return_true',
) );
}
/**
* Verify WP REST nonce for POST requests.
*/
public function check_nonce( WP_REST_Request $request ) {
$nonce = $request->get_header( 'X-WP-Nonce' );
if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
return new WP_Error( 'rest_forbidden', 'Invalid nonce.', array( 'status' => 403 ) );
}
return true;
}
/**
* Get API instance or return error.
*/
private function api() {
$api = Carei_Softra_API::get_instance();
if ( null === $api ) {
return new WP_Error( 'carei_not_configured', 'Softra API not configured.', array( 'status' => 500 ) );
}
return $api;
}
/**
* Wrap API response.
*/
private function respond( $result ) {
if ( is_wp_error( $result ) ) {
return $result;
}
return new WP_REST_Response( $result, 200 );
}
// ─── Callbacks ────────────────────────────────────────────────
public function get_all_car_classes( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
return $this->respond( $api->get_all_car_classes() );
}
public function get_branches( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
return $this->respond( $api->get_branches() );
}
public function get_car_classes( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
return $this->respond( $api->get_car_classes(
$request->get_param( 'dateFrom' ),
$request->get_param( 'dateTo' ),
$request->get_param( 'branchName' )
) );
}
public function get_car_models( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
return $this->respond( $api->get_car_models(
$request->get_param( 'dateFrom' ),
$request->get_param( 'dateTo' ),
$request->get_param( 'branchName' ),
$request->get_param( 'category' )
) );
}
public function get_pricelist( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
return $this->respond( $api->get_pricelist(
$request->get_param( 'category' ),
$request->get_param( 'dateFrom' ),
$request->get_param( 'dateTo' ),
$request->get_param( 'pickUpLocation' )
) );
}
public function get_pricing_summary( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
$params = $request->get_json_params();
return $this->respond( $api->get_pricing_summary( $params ) );
}
public function add_customer( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
$data = $request->get_json_params();
return $this->respond( $api->add_customer( $data ) );
}
public function make_booking( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
$data = $request->get_json_params();
return $this->respond( $api->make_booking( $data ) );
}
public function confirm_booking( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
return $this->respond( $api->confirm_booking(
$request->get_param( 'reservationId' )
) );
}
public function get_agreements( WP_REST_Request $request ) {
$api = $this->api();
if ( is_wp_error( $api ) ) {
return $api;
}
return $this->respond( $api->get_agreements() );
}
}

View File

@@ -0,0 +1,209 @@
<?php
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Softra Rent API client with JWT token caching.
* Uses native cURL (matching softra-test.php) instead of WP HTTP API.
*/
class Carei_Softra_API {
private static $instance = null;
private $base_url;
private $username;
private $password;
const TOKEN_TRANSIENT = 'carei_softra_token';
const TOKEN_EXPIRY = 3000; // 50 minutes (token valid 60 min)
private function __construct( $base_url, $username, $password ) {
$this->base_url = rtrim( $base_url, '/' );
$this->username = $username;
$this->password = $password;
}
public static function init( $base_url, $username, $password ) {
if ( null === self::$instance ) {
self::$instance = new self( $base_url, $username, $password );
}
return self::$instance;
}
public static function get_instance() {
return self::$instance;
}
/**
* Native cURL request — mirrors softra-test.php requestJson().
*/
private function curl_request( $method, $url, $payload = null, $extra_headers = array() ) {
$ch = curl_init( $url );
if ( false === $ch ) {
return new WP_Error( 'carei_curl_init', 'cURL init failed' );
}
$headers = array_merge( array( 'Accept: application/json' ), $extra_headers );
if ( null !== $payload ) {
$headers[] = 'Content-Type: application/json';
}
curl_setopt_array( $ch, array(
CURLOPT_CUSTOMREQUEST => strtoupper( $method ),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 15,
) );
if ( null !== $payload ) {
curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( $payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) );
}
$raw = curl_exec( $ch );
$err = curl_error( $ch );
$status = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( false === $raw ) {
return new WP_Error( 'carei_curl_error', 'cURL error: ' . $err );
}
$decoded = json_decode( $raw, true );
if ( ! is_array( $decoded ) ) {
$decoded = null;
}
return array(
'status' => $status,
'body' => $decoded,
'raw' => $raw,
);
}
/**
* Get JWT token (cached in WP transient).
*/
public function get_token() {
$token = get_transient( self::TOKEN_TRANSIENT );
if ( false !== $token ) {
return $token;
}
$res = $this->curl_request( 'POST', $this->base_url . '/account/auth', array(
'login' => $this->username,
'password' => $this->password,
) );
if ( is_wp_error( $res ) ) {
return new WP_Error( 'carei_auth_failed', 'Softra API auth failed: ' . $res->get_error_message() );
}
if ( 200 !== $res['status'] || empty( $res['body']['token'] ) ) {
$detail = isset( $res['raw'] ) ? $res['raw'] : '';
return new WP_Error( 'carei_auth_failed', 'Softra API auth failed (HTTP ' . $res['status'] . '): ' . $detail );
}
$token = $res['body']['token'];
set_transient( self::TOKEN_TRANSIENT, $token, self::TOKEN_EXPIRY );
return $token;
}
/**
* Generic API request with auth.
*/
public function request( $method, $endpoint, $body = null, $query_params = array() ) {
$token = $this->get_token();
if ( is_wp_error( $token ) ) {
return $token;
}
$url = $this->base_url . $endpoint;
if ( ! empty( $query_params ) ) {
$url .= '?' . http_build_query( $query_params );
}
$res = $this->curl_request(
$method,
$url,
$body,
array( 'Authorization: Bearer ' . $token )
);
if ( is_wp_error( $res ) ) {
return $res;
}
if ( $res['status'] < 200 || $res['status'] >= 300 ) {
return new WP_Error(
'carei_api_error',
'Softra API error: HTTP ' . $res['status'],
array( 'status' => $res['status'], 'body' => $res['body'] )
);
}
return $res['body'];
}
// ─── Public API Methods ───────────────────────────────────────
public function get_branches() {
return $this->request( 'GET', '/branch/list' );
}
public function get_all_car_classes() {
return $this->request( 'GET', '/car/class/listAll' );
}
public function get_car_classes( $date_from, $date_to, $branch_name ) {
return $this->request( 'POST', '/car/class/list', array(
'dateFrom' => $date_from,
'dateTo' => $date_to,
'branchName' => $branch_name,
) );
}
public function get_car_models( $date_from, $date_to, $branch_name, $category = '' ) {
return $this->request( 'POST', '/car/model/list', array(
'dateFrom' => $date_from,
'dateTo' => $date_to,
'branchName' => $branch_name,
'category' => $category,
), array( 'includeBrandDetails' => 'true' ) );
}
public function get_pricelist( $category, $date_from, $date_to, $pickup_location, $language = 'pl', $currency = 'PLN' ) {
return $this->request( 'POST', '/pricelist/list', array(
'category' => $category,
'dateFrom' => $date_from,
'dateTo' => $date_to,
'pickUpLocation' => $pickup_location,
'language' => $language,
'currency' => $currency,
) );
}
public function get_pricing_summary( $params ) {
return $this->request( 'POST', '/rent/princingSummary', $params );
}
public function add_customer( $data ) {
return $this->request( 'POST', '/customer/add', $data );
}
public function make_booking( $data ) {
return $this->request( 'POST', '/rent/makebooking', $data );
}
public function confirm_booking( $reservation_id ) {
return $this->request( 'POST', '/rent/confirm', array(
'reservationId' => $reservation_id,
) );
}
public function get_agreements() {
return $this->request( 'GET', '/agreement/def/list' );
}
}