From df13b3613c9ced66b5e82cfe4176c6e27b4ec7d3 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Wed, 25 Mar 2026 17:45:13 +0100 Subject: [PATCH] update --- .paul/HANDOFF-2026-03-25.md | 112 -- .../phases/02-form-ui-step1/02-01-SUMMARY.md | 131 +++ .../03-form-submit-booking/03-01-PLAN.md | 395 +++++++ .../03-form-submit-booking/03-01-SUMMARY.md | 130 +++ .paul/phases/04-polish-testing/04-01-PLAN.md | 273 +++++ .../phases/04-polish-testing/04-01-SUMMARY.md | 126 ++ .vscode/ftp-kr.sync.cache.json | 159 ++- .../assets/css/carei-reservation.css | 311 ++++- .../assets/js/carei-reservation.js | 1014 +++++++++++------ .../includes/class-elementor-widget.php | 153 ++- .../includes/class-softra-api.php | 61 + 11 files changed, 2341 insertions(+), 524 deletions(-) delete mode 100644 .paul/HANDOFF-2026-03-25.md create mode 100644 .paul/phases/02-form-ui-step1/02-01-SUMMARY.md create mode 100644 .paul/phases/03-form-submit-booking/03-01-PLAN.md create mode 100644 .paul/phases/03-form-submit-booking/03-01-SUMMARY.md create mode 100644 .paul/phases/04-polish-testing/04-01-PLAN.md create mode 100644 .paul/phases/04-polish-testing/04-01-SUMMARY.md diff --git a/.paul/HANDOFF-2026-03-25.md b/.paul/HANDOFF-2026-03-25.md deleted file mode 100644 index fbbc646..0000000 --- a/.paul/HANDOFF-2026-03-25.md +++ /dev/null @@ -1,112 +0,0 @@ -# PAUL Handoff - -**Date:** 2026-03-25 -**Status:** paused — checkpoint human-verify pending (awaiting deploy + test) - ---- - -## READ THIS FIRST - -You have no prior context. This document tells you everything. - -**Project:** Carei — Formularz rezerwacji samochodu jako plugin Elementor, zintegrowany z Softra Rent API -**Core value:** Klient klika "Złóż zapytanie o rezerwację" → modal z formularzem → dane z API Softra → rezerwacja - ---- - -## Current State - -**Milestone:** v0.1 Formularz Rezerwacji MVP -**Phase:** 2 of 4 — Form UI Krok 1 (Formularz) -**Plan:** 02-01 — APPLY in progress (Tasks 1+2 done, Task 3 checkpoint pending) - -**Loop Position:** -``` -PLAN ──▶ APPLY ──▶ UNIFY - ✓ ◐ ○ [Apply in progress — checkpoint:human-verify waiting] -``` - ---- - -## What Was Done - -**Phase 1 (complete):** -- Plugin `carei-reservation` z natywnym cURL proxy do Softra Rent API -- 10 WP REST endpoints w namespace `carei/v1` -- Widget Elementor z przyciskiem CTA + modal overlay -- Fix: widget require_once przeniesiony do hooka elementor/widgets/register -- Fix: zamiana wp_remote_post na natywny cURL (matching softra-test.php) - -**Phase 2 (in progress — Tasks 1+2 done):** -- HTML formularza z wszystkimi sekcjami z Figmy (widget render) -- CSS 541 linii (responsive, custom checkboxy, karty opcji, walidacja) -- JS 580+ linii (API integration, walidacja, interakcje) -- Nowy endpoint GET /car-classes-all (listAll) — segmenty ładują się od razu bez wymagania oddziału -- Opcje dodatkowe ładowane dynamicznie z cennika API - ---- - -## What's In Progress - -- **Task 3 checkpoint:human-verify** — użytkownik musi wgrać pliki na serwer i przetestować wizualnie -- Pliki do wgrania: - - `includes/class-softra-api.php` (natywny cURL + get_all_car_classes) - - `includes/class-rest-proxy.php` (nowy endpoint car-classes-all) - - `includes/class-elementor-widget.php` (pełny HTML formularza) - - `assets/css/carei-reservation.css` (kompletne style) - - `assets/js/carei-reservation.js` (logika + API) - ---- - -## What's Next - -**Immediate:** Wgrać pliki na serwer → przetestować formularz → "approved" lub opisać problemy - -**After that:** -- Jeśli approved → /paul:unify → Phase 3 (Overlay/podsumowanie + submit rezerwacji do API) -- Jeśli issues → fix → re-verify - -**Remaining phases:** -- Phase 3: Form UI Krok 2 (Overlay z podsumowaniem, tworzenie klienta, booking) -- Phase 4: Polish & integration testing - ---- - -## Key Decisions Made - -| Decision | Rationale | -|----------|-----------| -| Natywny cURL zamiast wp_remote_post | softra-test.php działa z cURL, WP HTTP API timeout | -| GET /car/class/listAll dla segmentów | Segment jest pierwszym polem w Figmie, nie powinien wymagać oddziału | -| Osobny plugin carei-reservation | Czystsza separacja od elementor-addon | -| sslverify nie ustawiane (domyślne cURL) | Tak jak w softra-test.php który działa na produkcji | - ---- - -## Key Files - -| File | Purpose | -|------|---------| -| `.paul/STATE.md` | Live project state | -| `.paul/ROADMAP.md` | Phase overview (Phase 1 ✅, Phase 2-4 ⬜) | -| `.paul/phases/02-form-ui-step1/02-01-PLAN.md` | Current plan | -| `.paul/phases/01-reservation-form-plugin/01-01-SUMMARY.md` | Phase 1 summary | -| `wp-content/plugins/carei-reservation/` | Plugin directory (all code) | -| `docs/figma-formularz/README.md` | Figma design spec | -| `docs/figma-formularz/screenshot-desktop.png` | Desktop reference | -| `docs/figma-formularz/screenshot-mobile.png` | Mobile reference | -| `softra-test.php` | Working Softra API test (reference for cURL config) | -| `.env` | API credentials (url, username, password) | - ---- - -## Resume Instructions - -1. Read `.paul/STATE.md` for latest position -2. User needs to deploy files to server and test -3. After test: "approved" → run `/paul:unify` then `/paul:plan` for Phase 3 -4. Or run `/paul:resume` or `/paul:progress` - ---- - -*Handoff created: 2026-03-25* diff --git a/.paul/phases/02-form-ui-step1/02-01-SUMMARY.md b/.paul/phases/02-form-ui-step1/02-01-SUMMARY.md new file mode 100644 index 0000000..34ac753 --- /dev/null +++ b/.paul/phases/02-form-ui-step1/02-01-SUMMARY.md @@ -0,0 +1,131 @@ +--- +phase: 02-form-ui-step1 +plan: 01 +subsystem: ui +tags: [elementor, modal, form, softra-api, css, javascript] + +requires: + - phase: 01-reservation-form-plugin + provides: Plugin skeleton, REST proxy, Elementor widget mount point +provides: + - Complete reservation form UI (Step 1) in Elementor modal + - Dynamic segment/branch loading from Softra API + - Segment-to-branch filtering with cached mapping + - Form validation and data collection +affects: [03-form-ui-step2, 05-admin-panel] + +tech-stack: + added: [] + patterns: [BEM CSS naming, IIFE JS module, cached API mapping via WP transients] + +key-files: + created: [] + modified: + - wp-content/plugins/carei-reservation/includes/class-elementor-widget.php + - wp-content/plugins/carei-reservation/assets/css/carei-reservation.css + - wp-content/plugins/carei-reservation/assets/js/carei-reservation.js + - wp-content/plugins/carei-reservation/includes/class-softra-api.php + - wp-content/plugins/carei-reservation/includes/class-rest-proxy.php + +key-decisions: + - "Segment shows ALL classes from ALL branches (car-classes-all endpoint)" + - "Pickup location filters by selected segment via cached backend mapping" + - "Extras hidden until segment AND pickup selected" + - "Insurance section skipped — API has no dedicated insurance items" + - "Foreign travel section skipped — API has no country/travel endpoints" + - "Backend segment-branches map cached in WP transient for 6 hours" + +patterns-established: + - "Form field visibility driven by selection state, not page load" + - "Backend mapping endpoint with transient caching for cross-entity relationships" + +duration: ~3h +started: 2025-03-25T08:00:00Z +completed: 2025-03-25T11:00:00Z +--- + +# Phase 2 Plan 01: Form UI — Krok 1 Summary + +**Kompletny formularz rezerwacji w modalu Elementor z dynamicznym ładowaniem segmentów i oddziałów z API Softra, filtrowanie lokalizacji po segmencie, walidacja i zbieranie danych.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~3h (across sessions) | +| Tasks | 3 completed | +| Files modified | 5 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Formularz renderuje się w modalu zgodnie z Figmą | Pass | Layout, kolory, typografia zgodne. Pominięto sekcje ubezpieczenia i wyjazdu zagranicznego (brak w API) | +| AC-2: Dynamiczne dane z API | Pass | Segmenty z car-classes-all, oddziały filtrowane po segmencie, opcje dodatkowe z pricelist. Zmieniono flow vs plan: segment ładuje się pierwszy (ze wszystkich lokalizacji), nie po wyborze oddziału | +| AC-3: Interakcje formularza działają poprawnie | Pass | Checkbox zwrot, walidacja, auto-kalkulacja dni, dynamiczne extras — wszystko działa | + +## Accomplishments + +- Formularz rezerwacji renderuje się w modalu z pełnym stylingiem Figma (desktop + mobile) +- Dynamiczne ładowanie: segmenty (wszystkie), oddziały (filtrowane po segmencie), opcje dodatkowe (z cennika API) +- Nowy endpoint `/segments-branches-map` z cachem 6h — mapuje segmenty na oddziały +- Walidacja formularza z komunikatami, zbieranie danych do console.log (gotowe na Phase 3) + +## Files Modified + +| File | Change | Purpose | +|------|--------|---------| +| `includes/class-elementor-widget.php` | Modified | Pełny HTML formularza: segment+daty w jednej linii, pickup+checkbox w jednej linii, ukrywalny wrapper na extras | +| `assets/css/carei-reservation.css` | Modified | 550+ linii — modal, form layout, inputs, checkboxes, cards, responsive, validation, disabled states | +| `assets/js/carei-reservation.js` | Modified | 450+ linii — API helpers, segment/branch loading, filtering, extras hide/show, validation, form collection | +| `includes/class-softra-api.php` | Modified | Nowa metoda `get_segments_branches_map()` — cached mapping segment→branches | +| `includes/class-rest-proxy.php` | Modified | Nowy endpoint `GET /segments-branches-map` | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Segment ładuje WSZYSTKIE klasy, nie filtrowane po oddziale | Wymaganie klienta — użytkownik widzi pełną ofertę | Odwrócony flow vs plan: segment pierwszy, oddział filtrowany | +| Oddziały filtrowane po segmencie | Klient najpierw wybiera segment, potem widzi tylko pasujące oddziały | Nowy backend endpoint z cachem | +| Pominięto sekcję ubezpieczenia | API nie ma dedykowanych pozycji ubezpieczeniowych (Soft/Premium) | Do backlogu — wrócimy gdy API będzie gotowe | +| Pominięto sekcję wyjazdu zagranicznego | API nie ma endpointów country/travel | Do backlogu | +| Extras ukryte do wybrania segmentu + oddziału | Wymaganie klienta — mniej informacji na start | Lepszy UX, mniej przeładowania | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Scope changes (user-directed) | 3 | Layout i flow zmienione per feedback klienta | +| Deferred features | 2 | Ubezpieczenie + wyjazd zagraniczny — API limitations | + +**Total impact:** Formularz jest prostszy niż w Figmie (brak 2 sekcji), ale flow jest lepszy (segment-first). + +### Scope Changes + +1. **Segment+daty w jednej linii** — klient poprosił o kompaktniejszy layout +2. **Pickup+checkbox w jednej linii** — klient poprosił +3. **Odwrócony flow** — segment pierwszy (z wszystkich lokalizacji), oddziały filtrowane po segmencie + +### Deferred Items + +- Sekcja ubezpieczenia (Pakiet Soft/Premium) — API nie ma tych pozycji, w backlogu +- Sekcja wyjazdu zagranicznego (checkbox + wyszukiwarka krajów) — API nie ma endpointów, w backlogu + +## Next Phase Readiness + +**Ready:** +- Formularz zbiera wszystkie dane i loguje do console +- API proxy obsługuje customer/add, makebooking, confirm +- Cennik (pricelist) ładuje się dynamicznie +- Phase 5 dodana do roadmapy: Admin Panel historia formularzy + +**Concerns:** +- Opcje dodatkowe z API zawierają pozycje karno-zwrotowe (BRAK, BRUD, KARA) — Phase 3 powinien je filtrować przy wysyłce + +**Blockers:** None + +--- +*Phase: 02-form-ui-step1, Plan: 01* +*Completed: 2026-03-25* diff --git a/.paul/phases/03-form-submit-booking/03-01-PLAN.md b/.paul/phases/03-form-submit-booking/03-01-PLAN.md new file mode 100644 index 0000000..688d2ad --- /dev/null +++ b/.paul/phases/03-form-submit-booking/03-01-PLAN.md @@ -0,0 +1,395 @@ +--- +phase: 03-form-submit-booking +plan: 01 +type: execute +wave: 1 +depends_on: ["02-01"] +files_modified: + - wp-content/plugins/carei-reservation/includes/class-elementor-widget.php + - wp-content/plugins/carei-reservation/assets/css/carei-reservation.css + - wp-content/plugins/carei-reservation/assets/js/carei-reservation.js + - wp-content/plugins/carei-reservation/includes/class-softra-api.php + - wp-content/plugins/carei-reservation/includes/class-rest-proxy.php +autonomous: false +--- + + +## Goal +Zbudować kompletny flow wysyłki formularza: tworzenie klienta w API Softra → podsumowanie kosztów w overlay → złożenie rezerwacji → potwierdzenie. Formularz staje się w pełni funkcjonalny. + +## Purpose +Bez tego flow formularz tylko zbiera dane do console.log. To jest core value — klient składa rezerwację która trafia do systemu Softra Rent. + +## Output +Działający submit: formularz → API customer/add → pricing summary overlay → makebooking → confirm → komunikat sukcesu. + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md + +## Prior Work +@.paul/phases/02-form-ui-step1/02-01-SUMMARY.md +- Formularz Krok 1 w modalu z dynamicznym ładowaniem segmentów/oddziałów/extras +- Walidacja + zbieranie danych (console.log) +- Pricelist ID dostępny z odpowiedzi /pricelist endpoint + +## API Documentation +@docs/rent-api-02-klienci-i-konta.md — customer/add wymaga: name, address, paymentMethod, isCompany, account|skipAccountCreate, pesel|passportNo|idCard (dla osób fizycznych) +@docs/rent-api-03-rezerwacje-i-platnosci.md — makebooking wymaga: dateFrom, dateTo, customerId, pickUpLocation, returnLocation, carParameters, priceListId, validTime + +## Source Files +@wp-content/plugins/carei-reservation/includes/class-elementor-widget.php +@wp-content/plugins/carei-reservation/assets/js/carei-reservation.js +@wp-content/plugins/carei-reservation/assets/css/carei-reservation.css +@wp-content/plugins/carei-reservation/includes/class-softra-api.php +@wp-content/plugins/carei-reservation/includes/class-rest-proxy.php + + + + +## AC-1: Formularz zbiera dodatkowe dane wymagane przez API +```gherkin +Given formularz rezerwacji jest otwarty +When użytkownik przewija do sekcji "Dane najemcy" +Then widoczne są dodatkowe pola: Miejscowość, Kod pocztowy, Ulica, PESEL +And pola adresowe i PESEL są wymagane przy walidacji +``` + +## AC-2: Submit formularza tworzy klienta i pokazuje podsumowanie kosztów +```gherkin +Given użytkownik wypełnił wszystkie wymagane pola formularza +When klika "Wyślij" +Then przycisk zmienia się na "Przetwarzanie..." (disabled) +And w tle: tworzony jest klient via /customer/add +And w tle: pobierane jest podsumowanie kosztów via /rent/princingSummary +And wyświetla się overlay z podsumowaniem: lista opłat, wartość netto/brutto +And overlay ma przycisk "Potwierdź rezerwację" i "Wróć do formularza" +``` + +## AC-3: Potwierdzenie rezerwacji kończy flow +```gherkin +Given overlay z podsumowaniem jest wyświetlony +When użytkownik klika "Potwierdź rezerwację" +Then w tle: tworzona jest rezerwacja via /rent/makebooking +And w tle: rezerwacja jest potwierdzana via /rent/confirm +And overlay zamienia się na komunikat sukcesu z numerem rezerwacji +And jest przycisk "Zamknij" który zamyka modal + +When rezerwacja się nie powiedzie (API error) +Then wyświetla się komunikat błędu z przyczyną (rejectReason) +And użytkownik może wrócić do formularza i spróbować ponownie +``` + + + + + + + Task 1: Dodatkowe pola formularza + overlay podsumowania (HTML/CSS) + wp-content/plugins/carei-reservation/includes/class-elementor-widget.php, wp-content/plugins/carei-reservation/assets/css/carei-reservation.css + + **class-elementor-widget.php — dodaj do sekcji "Dane najemcy":** + + Po wierszu Imię/Nazwisko, przed wierszem Email/Telefon, dodaj wiersz adresowy: + ``` + .carei-form__row (3 kolumny: Miejscowość | Kod pocztowy | Ulica) + ├─ input#carei-city (Miejscowość, required) + ├─ input#carei-zipcode (Kod pocztowy, placeholder "00-000", required) + └─ input#carei-street (Ulica i nr domu, required) + ``` + + Po wierszu Email/Telefon dodaj wiersz PESEL: + ``` + .carei-form__field.carei-form__field--half + └─ input#carei-pesel (PESEL, type="text", maxlength="11", required) + ``` + + **Overlay podsumowania — dodaj PO form, ale wewnątrz .carei-modal:** + + ```html + + ``` + + **Widok sukcesu — dodaj PO overlay podsumowania:** + + ```html + + ``` + + **carei-reservation.css — dodaj style:** + + - `.carei-form__row--address`: grid 3 kolumny (1fr auto 1fr), mobile: 1 kolumna + - `.carei-form__field--half`: max-width 50%, mobile: 100% + - `.carei-summary`: pełna wysokość modala, flex column, gap 24px, padding jak modal + - `.carei-summary__title`: jak .carei-modal-title + - `.carei-summary__table`: tabela z opłatami — wiersz: nazwa | ilość | cena | wartość + - `.carei-summary__table table`: width 100%, border-collapse, alternating rows + - `.carei-summary__total`: pogrubiony, wyrównany do prawej, netto/VAT/brutto + - `.carei-summary__actions`: flex, gap 16px, justify-content space-between + - `.carei-summary__btn--back`: border 1px solid carei-blue, bg transparent, color carei-blue + - `.carei-summary__btn--confirm`: bg carei-red, color white (jak submit) + - `.carei-summary__error`: bg rgba(255,0,0,0.05), color red, padding 12px, border-radius + - `.carei-success`: flex column, align-items center, text-align center, gap 16px + - `.carei-success__icon`: 64px circle, bg green, color white, font-size 32px + - `.carei-success__close`: jak .carei-form__submit + + + `php -l wp-content/plugins/carei-reservation/includes/class-elementor-widget.php` — brak syntax errors. + CSS plik zawiera style dla `.carei-summary` i `.carei-success`. + + AC-1 satisfied: dodatkowe pola adresowe i PESEL widoczne, overlay HTML gotowy + + + + Task 2: JS — submit flow (customer → pricing → booking → confirm) + wp-content/plugins/carei-reservation/assets/js/carei-reservation.js, wp-content/plugins/carei-reservation/includes/class-softra-api.php, wp-content/plugins/carei-reservation/includes/class-rest-proxy.php + + **JS — przepisz initSubmit() i dodaj nowe funkcje:** + + **Stan globalny — dodaj zmienne:** + ``` + var currentCustomerId = null; + var currentPriceListId = null; // z odpowiedzi pricelist (już ładowany w loadExtras) + var currentPricingSummary = null; + var currentReservationId = null; + var summaryOverlay, successView, summaryBack, summaryConfirm; + ``` + + **initRefs() — dodaj referencje:** + - summaryOverlay = getElementById('carei-summary-overlay') + - successView = getElementById('carei-success-view') + - summaryBack = getElementById('carei-summary-back') + - summaryConfirm = getElementById('carei-summary-confirm') + - summaryError = getElementById('carei-summary-error') + + **loadExtras() — zapisz priceListId:** + - Przy ładowaniu pricelist, zapisz `currentPriceListId = pricelist.id` z pierwszego elementu odpowiedzi + + **Nowy flow submit:** + + 1. `handleSubmit()`: + - Walidacja (jak teraz) + - Disable submit btn → "Przetwarzanie..." + - Wywołaj `createCustomerAndShowSummary()` + + 2. `createCustomerAndShowSummary()`: + - Zbierz dane formularza + - POST /customer z danymi: + ```json + { + "firstName": formData.firstName, + "lastName": formData.lastName, + "name": firstName + " " + lastName, + "isCompany": "N", + "address": { + "city": formData.city, + "zipCode": formData.zipCode, + "street": formData.street, + "homeNo": "-" + }, + "pesel": formData.pesel, + "email": formData.email, + "phoneMobile": formData.phone, + "paymentMethod": "GOTÓWKA", + "skipAccountCreate": "T", + "emailVerified": true + } + ``` + - On success: `currentCustomerId = response.customerId` + - Wywołaj `loadPricingSummary()` + - On error: pokaż błąd, re-enable submit btn + + 3. `loadPricingSummary()`: + - POST /pricing-summary z danymi: + ```json + { + "dateFrom": formData.dateFrom + ":00", + "dateTo": formData.dateTo + ":00", + "customerId": currentCustomerId, + "pickUpLocation": { "branchName": formData.pickupBranch, "outOfBranch": "N" }, + "returnLocation": { "branchName": returnBranch, "outOfBranch": "N" }, + "carParameters": { "categoryName": formData.segment }, + "priceListId": currentPriceListId, + "priceItems": selectedExtrasAsBookingPriceItems() + } + ``` + - On success: `currentPricingSummary = response` + - Wywołaj `showSummaryOverlay(response)` + - On error: pokaż błąd, re-enable submit btn + + 4. `selectedExtrasAsBookingPriceItems()`: + - Dla każdego zaznaczonego checkbox extras[]: + ```json + { + "id": checkbox.value, + "name": checkbox label text, + "unit": "szt.", + "amount": 1, + "priceBeforeDiscount": checkbox.dataset.price, + "discount": 0, + "priceAfterDiscount": checkbox.dataset.price + } + ``` + + 5. `showSummaryOverlay(summary)`: + - Ukryj form, pokaż summaryOverlay + - Wypełnij #carei-summary-details: segment, daty, oddział + - Wypełnij #carei-summary-table: tabela z summary.pricelist items + - Kolumny: Nazwa | Ilość | Cena netto | Wartość brutto + - Oznacz wiersze addedBySystem=true (np. kursywą + "doliczone automatycznie") + - Wypełnij #carei-summary-total: totalNetValue, totalVatValue, totalGrossValue + + 6. `handleSummaryBack()`: + - Ukryj summaryOverlay, pokaż form + - Re-enable submit btn + + 7. `handleSummaryConfirm()`: + - Disable confirm btn → "Rezerwuję..." + - POST /booking z pełnymi danymi makebooking: + ```json + { + "dateFrom": ..., "dateTo": ..., + "customerId": currentCustomerId, + "pickUpLocation": { "branchName": ..., "outOfBranch": "N" }, + "returnLocation": { "branchName": ..., "outOfBranch": "N" }, + "carParameters": { "categoryName": segment }, + "priceListId": currentPriceListId, + "validTime": 30, + "priceItems": selectedExtrasAsBookingPriceItems(), + "agreementItems": agreementItemsFromForm(), + "comments": formData.message + } + ``` + - On success (response.success=true): + - `currentReservationId = response.reservationId` + - POST /booking/confirm z { reservationId } + - Pokaż success view z response.reservationNo + - On error: pokaż błąd w #carei-summary-error, re-enable btn + + 8. `agreementItemsFromForm()`: + - Privacy checkbox → return [{ id: agreementId, value: true }] + - AgreementId: załaduj raz z GET /agreements na modal open, zapisz w zmiennej + + 9. `showSuccessView(reservationNo)`: + - Ukryj summaryOverlay, pokaż successView + - Wypełnij #carei-success-number: "Nr rezerwacji: " + reservationNo + + 10. `handleSuccessClose()`: + - Zamknij modal (closeModal()) + - Reset formularza + + **Walidacja — dodaj nowe wymagane pola do requiredFields:** + - { id: 'carei-city', type: 'input', msg: 'Podaj miejscowość' } + - { id: 'carei-zipcode', type: 'input', msg: 'Podaj kod pocztowy' } + - { id: 'carei-street', type: 'input', msg: 'Podaj ulicę' } + - { id: 'carei-pesel', type: 'pesel', msg: 'Podaj poprawny PESEL (11 cyfr)' } + - Walidacja PESEL: dokładnie 11 cyfr + + **REST proxy — dodaj brakujący endpoint /cancel:** + W class-rest-proxy.php dodaj route POST /booking/cancel. + W class-softra-api.php metoda cancel_booking już może istnieć, jeśli nie — dodaj. + + **WAŻNE — obsługa błędów:** + - Każdy krok API (customer, pricing, booking, confirm) może failować + - Przy każdym błędzie: wyświetl czytelny komunikat, pozwól wrócić do formularza + - Timeout: jeśli API nie odpowie w 30s → "Serwer nie odpowiada, spróbuj ponownie" + - rejectReason z API: wyświetl użytkownikowi (np. "CAR_NOT_FOUND" → "Brak dostępnego pojazdu w wybranym terminie") + + + JS plik nie zawiera syntax errors. + PHP pliki: `php -l class-softra-api.php`, `php -l class-rest-proxy.php` — brak błędów. + Flow: form submit → customer → pricing summary → display overlay. + + AC-2 (submit tworzy klienta i pokazuje podsumowanie), AC-3 (potwierdzenie kończy flow) + + + + Kompletny flow rezerwacji: formularz → tworzenie klienta → podsumowanie kosztów → rezerwacja → potwierdzenie + + 1. Wgraj pliki na serwer (carei.pagedev.pl) + 2. Otwórz formularz, wypełnij wszystkie pola (w tym adres, PESEL) + 3. Kliknij "Wyślij" — sprawdź: + - Przycisk zmienia się na "Przetwarzanie..." + - Po chwili pojawia się overlay z podsumowaniem kosztów + - Tabela pokazuje opłaty (wynajem + ewentualne dodatki) + - Widoczne wartości netto/VAT/brutto + 4. Kliknij "Wróć do formularza" — formularz wraca + 5. Kliknij "Wyślij" ponownie → overlay → "Potwierdź rezerwację" + - Sprawdź czy pojawia się komunikat sukcesu z numerem rezerwacji + 6. Test błędów: + - Wyślij z niepoprawnym PESEL → walidacja łapie + - Wyślij z datami w przeszłości → sprawdź reakcję API + 7. Na mobile: overlay i success view powinny być responsywne + + Type "approved" to continue, or describe issues to fix + + + + + + +## DO NOT CHANGE +- wp-content/plugins/carei-reservation/carei-reservation.php (main plugin file) +- wp-content/plugins/elementor-addon/* (istniejący plugin) +- wp-content/themes/hello-elementor/* (theme) +- .env, docs/* + +## SCOPE LIMITS +- Ten plan implementuje flow: customer → pricing summary → booking → confirm +- NIE implementuje płatności online (preautoryzacja karty itp.) +- NIE implementuje konta klienta (skipAccountCreate='T') +- NIE buduje panelu admina (Phase 5) +- Wyjazd zagraniczny i ubezpieczenie pominięte (backlog) +- Anulowanie rezerwacji: endpoint dodany ale UI nie (future) + + + + +Before declaring plan complete: +- [ ] `php -l` na wszystkich zmienionych plikach PHP — brak błędów +- [ ] Formularz zawiera pola: adres (city, zip, street), PESEL +- [ ] Submit flow: customer/add → princingSummary → overlay z tabelą kosztów +- [ ] Confirm flow: makebooking → confirm → success view z numerem rezerwacji +- [ ] Error handling: API errors wyświetlane użytkownikowi +- [ ] Responsive: overlay i success view działają na mobile +- [ ] Human verify: pełny test flow na żywym serwerze + + + +- Formularz składa rezerwację w systemie Softra Rent +- Użytkownik widzi podsumowanie kosztów przed potwierdzeniem +- Użytkownik dostaje numer rezerwacji po potwierdzeniu +- Błędy API wyświetlane czytelnie + + + +After completion, create `.paul/phases/03-form-submit-booking/03-01-SUMMARY.md` + diff --git a/.paul/phases/03-form-submit-booking/03-01-SUMMARY.md b/.paul/phases/03-form-submit-booking/03-01-SUMMARY.md new file mode 100644 index 0000000..261d8b9 --- /dev/null +++ b/.paul/phases/03-form-submit-booking/03-01-SUMMARY.md @@ -0,0 +1,130 @@ +--- +phase: 03-form-submit-booking +plan: 01 +subsystem: ui, api +tags: [softra-api, booking, customer, pricing-summary, form, modal] + +requires: + - phase: 02-form-ui-step1 + provides: Form UI with segment/branch filtering, validation, data collection +provides: + - Complete booking flow: customer creation → pricing summary → reservation → confirmation + - Address and PESEL fields for API compliance + - Pricing summary overlay with cost breakdown + - Success view with reservation number +affects: [04-polish-testing, 05-admin-panel] + +tech-stack: + added: [] + patterns: [multi-step API flow with error recovery, boolean API params for Java backend] + +key-files: + modified: + - wp-content/plugins/carei-reservation/includes/class-elementor-widget.php + - wp-content/plugins/carei-reservation/assets/css/carei-reservation.css + - wp-content/plugins/carei-reservation/assets/js/carei-reservation.js + - wp-content/plugins/carei-reservation/includes/class-softra-api.php + - wp-content/plugins/carei-reservation/includes/class-rest-proxy.php + +key-decisions: + - "Boolean values for Java API (true/false not 'T'/'N') despite documentation saying otherwise" + - "Simplified segments-branches-map: 2 API calls instead of ~40 — all branches shown for all segments" + - "drivers[] required in makebooking — populated from form data (same person as customer)" + - "skipAccountCreate: true — no customer account, MVP approach" + - "Penalty items (BRAK, BRUD, KARA) filtered out from extras display" + +patterns-established: + - "Softra API uses Java boolean deserialization — always send true/false, never 'T'/'N'" + - "Multi-step API flow with error recovery at each step" + +duration: ~2h +started: 2026-03-25T11:00:00Z +completed: 2026-03-25T13:00:00Z +--- + +# Phase 3 Plan 01: Submit + Booking Flow Summary + +**Pełny flow rezerwacji: formularz → tworzenie klienta → podsumowanie kosztów w overlay → złożenie rezerwacji → potwierdzenie z numerem.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~2h | +| Tasks | 3 completed | +| Files modified | 5 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Dodatkowe pola (adres, PESEL) | Pass | Miejscowość, kod pocztowy, ulica, PESEL — wymagane, walidowane | +| AC-2: Submit tworzy klienta i pokazuje podsumowanie | Pass | customer/add → princingSummary → overlay z tabelą kosztów i wszystkimi wybranymi opcjami | +| AC-3: Potwierdzenie rezerwacji kończy flow | Pass | makebooking → confirm → success view z numerem rezerwacji | + +## Accomplishments + +- Pełny booking flow działa end-to-end na produkcji +- Overlay podsumowania pokazuje: dane wynajmu, najemcę, wybrane opcje, tabelę kosztów (netto/VAT/brutto) +- Error handling na każdym kroku z czytelnym komunikatem +- Endpoint /booking/cancel dodany (UI do wykorzystania w przyszłości) + +## Files Modified + +| File | Change | Purpose | +|------|--------|---------| +| `includes/class-elementor-widget.php` | Modified | Pola adresowe + PESEL, overlay podsumowania, success view | +| `assets/css/carei-reservation.css` | Modified | 800+ linii — style summary, success, address row | +| `assets/js/carei-reservation.js` | Modified | 730+ linii — pełny booking flow z error handling | +| `includes/class-softra-api.php` | Modified | cancel_booking(), uproszczone segments-branches-map (2 calls) | +| `includes/class-rest-proxy.php` | Modified | Endpoint POST /booking/cancel | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Boolean zamiast string w API | Java deserializacja nie akceptuje 'T'/'N', wymaga true/false | Wszystkie pola boolean muszą być native boolean | +| Uproszczone mapowanie segment→branch | 39 oddziałów × 30s timeout = niemożliwe w jednym request | Wszystkie oddziały dostępne dla każdego segmentu, API weryfikuje przy rezerwacji | +| drivers[] z danymi najemcy | API wymaga (NotNull), dokumentacja nie zaznacza jako required | Kierowca = ten sam co najemca formularza | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 3 | API compatibility fixes discovered during testing | + +### Auto-fixed Issues + +**1. Boolean serialization** +- Issue: isCompany='N', skipAccountCreate='T' → HTTP 400 +- Fix: Changed to isCompany: false, skipAccountCreate: true +- Verification: customer/add returns 200 with customerId + +**2. Segments-branches-map timeout** +- Issue: ~40 sequential API calls → cURL timeout after 30s +- Fix: Simplified to 2 API calls (branches + car-classes-all), all branches shown for all segments +- Verification: Endpoint returns in <2s + +**3. Missing drivers field** +- Issue: makebooking requires drivers[] (NotNull), not documented as required +- Fix: Added drivers array with form data (firstName, lastName, address, pesel, phone, email) +- Verification: makebooking returns success with reservationId + +## Next Phase Readiness + +**Ready:** +- Rezerwacja działa end-to-end +- Wszystkie endpointy API przetestowane na produkcji +- Error handling na każdym kroku + +**Concerns:** +- Penalty items z pricelist filtrowane po kodzie (BRAK/BRUD/KARA) — może nie pokryć wszystkich +- Brak email notification po rezerwacji (zależy od konfiguracji Softra) + +**Blockers:** None + +--- +*Phase: 03-form-submit-booking, Plan: 01* +*Completed: 2026-03-25* diff --git a/.paul/phases/04-polish-testing/04-01-PLAN.md b/.paul/phases/04-polish-testing/04-01-PLAN.md new file mode 100644 index 0000000..6206065 --- /dev/null +++ b/.paul/phases/04-polish-testing/04-01-PLAN.md @@ -0,0 +1,273 @@ +--- +phase: 04-polish-testing +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - wp-content/plugins/carei-reservation/assets/js/carei-reservation.js + - wp-content/plugins/carei-reservation/assets/css/carei-reservation.css + - wp-content/plugins/carei-reservation/includes/class-elementor-widget.php +autonomous: false +--- + + +## Goal +Polish formularza rezerwacji: obsługa edge cases (wygasły token, brak dostępności, timeouty), animacje przejść form↔summary↔success, poprawki a11y (ARIA, focus management, keyboard nav), naprawa bugów CSS. + +## Purpose +Formularz działa end-to-end (Phase 3), ale brak obsługi błędów brzegowych, brak animacji, a11y nie jest wdrożone. Phase 4 doprowadza formularz do jakości produkcyjnej. + +## Output +Zmodyfikowane: JS (error handling + animacje + a11y), CSS (transitions + fix bugs), PHP widget (ARIA attrs). + + + +## Project Context +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Prior Work +@.paul/phases/03-form-submit-booking/03-01-SUMMARY.md +- Phase 3: pełny booking flow działa (customer → pricing → makebooking → confirm → success) +- API discoveries: boolean true/false, drivers[] required, penalty items filtered + +## Source Files +@wp-content/plugins/carei-reservation/assets/js/carei-reservation.js +@wp-content/plugins/carei-reservation/assets/css/carei-reservation.css +@wp-content/plugins/carei-reservation/includes/class-elementor-widget.php + + + + +## AC-1: Obsługa wygasłego tokenu JWT +```gherkin +Given formularz jest otwarty i token JWT serwera wygasł (>1h) +When użytkownik wysyła formularz lub zmienia segment/extras +Then system automatycznie odświeża token i ponawia request (retry 1x) + And użytkownik nie widzi błędu tokenu — flow kontynuuje normalnie +``` + +## AC-2: Obsługa braku dostępności pojazdu +```gherkin +Given użytkownik wypełnił formularz i jest na ekranie podsumowania +When API makebooking zwraca rejectReason CAR_NOT_FOUND +Then wyświetla się czytelny komunikat "Brak dostępnego pojazdu w wybranym terminie. Zmień daty lub segment." + And przycisk "Wróć do formularza" jest aktywny + And użytkownik może wrócić i zmienić dane +``` + +## AC-3: Obsługa timeoutów i błędów sieciowych +```gherkin +Given dowolny request API trwa >15s lub sieć jest niedostępna +When fetch rzuca timeout lub network error +Then wyświetla się komunikat "Wystąpił problem z połączeniem. Spróbuj ponownie." + And przycisk submit/confirm wraca do stanu aktywnego + And formularz nie jest zablokowany +``` + +## AC-4: Animacje przejść między krokami +```gherkin +Given użytkownik jest na formularzu +When przechodzi do podsumowania (submit) lub do success (confirm) +Then przejście jest animowane (fade out → fade in, ~300ms) + And przy powrocie z podsumowania do formularza również jest animacja +``` + +## AC-5: Accessibility — ARIA i focus management +```gherkin +Given użytkownik otwiera modal +When modal się otwiera +Then focus przenosi się na pierwszy interaktywny element modalu + And modal ma role="dialog" i aria-modal="true" + And focus jest trapowany w modalu (Tab nie wychodzi poza modal) + And po zamknięciu modalu focus wraca na przycisk trigger + And przejście form→summary→success przenosi focus na odpowiedni heading +``` + +## AC-6: Fix CSS — orphaned styles w media query +```gherkin +Given plik CSS ma orphaned styles poza media query (linie ~560-570) +When CSS jest naprawiony +Then style .carei-form__row--address i .carei-summary__actions są wewnątrz @media (max-width: 768px) + And nie ma podwójnego zamknięcia } na końcu media query +``` + + + + + + + Task 1: Edge cases — token retry, timeout, lepsze error messages + wp-content/plugins/carei-reservation/assets/js/carei-reservation.js + + 1. **Token retry:** Wrap apiGet/apiPost — jeśli response HTTP 401 lub 403, wywołaj retry 1x (token odświeża się automatycznie po stronie PHP proxy). Dodaj flagę `isRetry` żeby nie zapętlić. + + 2. **Timeout:** Dodaj AbortController z timeout 15s do fetch w apiGet/apiPost. Na timeout rzuć Error z komunikatem "Przekroczono czas oczekiwania. Spróbuj ponownie." + + 3. **Network error:** W handleResponse/catch, rozróżnij TypeError (network) od Error (API). Na TypeError pokaż: "Brak połączenia z serwerem. Sprawdź internet i spróbuj ponownie." + + 4. **Rozszerz translateRejectReason:** Dodaj więcej kodów z API: + - 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." + + 5. **Submit button recovery:** Upewnij się, że KAŻDY catch w createCustomerAndShowSummary i handleSummaryConfirm przywraca przyciski do stanu aktywnego. + + Avoid: Nie zmieniaj endpointów API ani logiki proxy PHP. Retry tylko na 401/403, nie na inne kody. + + + - Symuluj offline: w DevTools Network → Offline → submit → czytelny komunikat, przycisk aktywny + - Symuluj slow: Network → Slow 3G → submit → po 15s timeout message + - Sprawdź że po błędzie można ponownie wysłać formularz + + AC-1, AC-2, AC-3 satisfied: token retry, timeout handling, network errors, rozszerzone komunikaty + + + + Task 2: Animacje przejść + fix CSS bug + wp-content/plugins/carei-reservation/assets/css/carei-reservation.css, wp-content/plugins/carei-reservation/assets/js/carei-reservation.js + + 1. **CSS transitions:** Dodaj klasy animacji: + ```css + .carei-step-enter { opacity: 0; transform: translateY(10px); } + .carei-step-active { opacity: 1; transform: translateY(0); transition: opacity 0.3s ease, transform 0.3s ease; } + .carei-step-exit { opacity: 0; transform: translateY(-10px); transition: opacity 0.2s ease, transform 0.2s ease; } + ``` + + 2. **JS transitions:** W showSummaryOverlay(), handleSummaryBack(), showSuccessView(): + - Dodaj klasę `carei-step-exit` do wychodzącego elementu + - Po 200ms (transitionend lub setTimeout): hide wychodzący, show wchodzący z `carei-step-enter` + - Po 1 frame (requestAnimationFrame): zamień `carei-step-enter` na `carei-step-active` + + 3. **Modal open animation:** Dodaj fade-in na overlay (.carei-modal-overlay) i scale na .carei-modal: + ```css + .carei-modal-overlay { opacity: 0; transition: opacity 0.3s ease; } + .carei-modal-overlay.is-open { opacity: 1; } + .carei-modal { transform: scale(0.95); transition: transform 0.3s ease; } + .carei-modal-overlay.is-open .carei-modal { transform: scale(1); } + ``` + Zmień display:none/flex na visibility+opacity pattern (display:flex zawsze, visibility:hidden gdy nie is-open). + + 4. **Fix CSS bug:** Linie ~558-570 — orphaned styles po zamknięciu media query 768px. Przenieś `.carei-form__row--address`, `.carei-summary__actions`, `.carei-summary__btn` do wnętrza `@media (max-width: 768px)` i usuń dodatkowy `}`. + + Avoid: Nie dodawaj żadnych bibliotek animacji. Czyste CSS transitions + JS class toggle. + + + - Otwórz modal: smooth fade-in + scale + - Submit formularz: form fade out → summary fade in + - "Wróć": summary fade out → form fade in + - "Potwierdź": summary fade out → success fade in + - Sprawdź w DevTools że nie ma CSS parse errors + + AC-4 satisfied: animacje przejść. AC-6 satisfied: CSS bug naprawiony. + + + + Task 3: Accessibility — ARIA, focus trap, focus management + wp-content/plugins/carei-reservation/includes/class-elementor-widget.php, wp-content/plugins/carei-reservation/assets/js/carei-reservation.js + + 1. **PHP widget — ARIA attrs:** + - Modal overlay div: dodaj `role="dialog"` `aria-modal="true"` `aria-labelledby="carei-modal-title"` + - Close button: dodaj `aria-label="Zamknij formularz"` + - Form sections: dodaj `role="group"` z `aria-label` na sekcjach (dane rezerwacji, dane osobowe, opcje) + - Submit button: dodaj `aria-busy="false"` (JS zmieni na true podczas loading) + - Summary confirm: dodaj `aria-busy="false"` + + 2. **JS — Focus management:** + - openModal(): po otwarciu, focus na pierwszy select (segmentSelect) lub na modal-title + - closeModal(): zapisz `lastFocusedElement` przed open, przywróć focus po close + - showSummaryOverlay(): focus na .carei-summary__title + - showSuccessView(): focus na .carei-success__title + - handleSummaryBack(): focus na segmentSelect + + 3. **JS — Focus trap:** + - W openModal(), dodaj keydown listener na modal: + - Zbierz wszystkie focusable elements w modalu + - Na Tab z ostatniego → focus na pierwszy + - Na Shift+Tab z pierwszego → focus na ostatni + - Usuń trap w closeModal() + + 4. **JS — aria-busy:** + - setSubmitState('loading'): dodaj aria-busy="true" na submit button + - setSubmitState('ready'): dodaj aria-busy="false" + - Analogicznie dla summaryConfirm + + 5. **Announcements:** Dodaj aria-live="polite" region (sr-only) do modalu. Announce: + - "Ładowanie podsumowania..." przy submit + - "Rezerwacja potwierdzona" przy success + - Błędy API + + Avoid: Nie dodawaj aria-label do elementów które mają visible text. Nie rób z formularza role="form" (natywny
wystarczy). + + + - Tab przez formularz: focus nie wychodzi poza modal + - Submit → focus na heading podsumowania + - Confirm → focus na heading success + - Close modal → focus wraca na trigger button + - Screen reader: ogłasza "Ładowanie...", "Rezerwacja potwierdzona", błędy + + AC-5 satisfied: ARIA attrs, focus trap, focus management, aria-live announcements + + + + Edge cases, animacje przejść, accessibility — pełny polish formularza + + 1. Otwórz https://carei.pagedev.pl + 2. Kliknij "Złóż zapytanie o rezerwację" — modal fade-in smooth + 3. Tab przez formularz — focus nie wychodzi poza modal + 4. Wypełnij formularz i wyślij — form animuje się do summary + 5. Kliknij "Wróć do formularza" — animacja powrotu + 6. Wyślij ponownie → Potwierdź → success z animacją + 7. Zamknij modal → focus wraca na przycisk trigger + 8. Test offline: DevTools → Network → Offline → submit → komunikat błędu + 9. Test mobile: responsive 375px — form działa, animacje smooth + + Type "approved" to continue, or describe issues to fix + + + + + + +## DO NOT CHANGE +- wp-content/plugins/carei-reservation/includes/class-softra-api.php (API proxy stable) +- wp-content/plugins/carei-reservation/includes/class-rest-proxy.php (REST endpoints stable) +- .env (credentials) +- Logika API endpoints (segments-branches-map, pricelist, customer, booking) + +## SCOPE LIMITS +- Nie dodawaj nowych endpointów API +- Nie zmieniaj flow rezerwacji (customer → pricing → booking → confirm) +- Nie dodawaj bibliotek zewnętrznych +- Nie implementuj e2e test framework — weryfikacja manualna na produkcji +- Nie dodawaj ubezpieczenia ani wyjazdu zagranicznego (backlog) + + + + +Before declaring plan complete: +- [ ] Formularz działa end-to-end bez regresji +- [ ] Token retry: 401 → retry → success +- [ ] Timeout 15s: komunikat, przycisk aktywny +- [ ] Network error: komunikat, przycisk aktywny +- [ ] CAR_NOT_FOUND: czytelny komunikat PL +- [ ] Animacje: modal open, form→summary, summary→form, summary→success +- [ ] Focus: modal open → focus w modalu, close → focus na trigger +- [ ] Focus trap: Tab nie wychodzi poza modal +- [ ] ARIA: role=dialog, aria-modal, aria-busy, aria-live +- [ ] CSS: brak orphaned styles, media queries poprawne +- [ ] Mobile 375px: responsive OK + + + +- All 6 acceptance criteria met +- All verification checks pass +- No regressions in booking flow +- Human verification approved on carei.pagedev.pl + + + +After completion, create `.paul/phases/04-polish-testing/04-01-SUMMARY.md` + diff --git a/.paul/phases/04-polish-testing/04-01-SUMMARY.md b/.paul/phases/04-polish-testing/04-01-SUMMARY.md new file mode 100644 index 0000000..7718e45 --- /dev/null +++ b/.paul/phases/04-polish-testing/04-01-SUMMARY.md @@ -0,0 +1,126 @@ +--- +phase: 04-polish-testing +plan: 01 +subsystem: ui, api +tags: [a11y, animations, error-handling, focus-trap, aria, css-transitions] + +requires: + - phase: 03-form-submit-booking + provides: Complete booking flow end-to-end (customer → pricing → booking → confirm → success) +provides: + - Token retry on 401/403 with auto-recovery + - AbortController timeout 15s with Polish error messages + - Network error detection and user-friendly messaging + - Extended reject reason translations (6 codes) + - Animated step transitions (form ↔ summary ↔ success) + - Modal open/close animations (fade + scale) + - ARIA dialog with focus trap and focus management + - aria-live announcements for screen readers + - CSS bug fix (orphaned media query styles) +affects: [05-admin-panel] + +tech-stack: + added: [] + patterns: [AbortController timeout, CSS class-based step transitions, focus trap pattern, aria-live announcements] + +key-files: + modified: + - wp-content/plugins/carei-reservation/assets/js/carei-reservation.js + - wp-content/plugins/carei-reservation/assets/css/carei-reservation.css + - wp-content/plugins/carei-reservation/includes/class-elementor-widget.php + +key-decisions: + - "Inline style.display for step visibility instead of CSS-only classes — HTML inline display:none overrides classes" + - "Focus trap via keydown listener on overlay, filtering visible focusable elements" + - "aria-live region appended to modal overlay for scoped announcements" + +patterns-established: + - "transitionStep(outEl, inEl, callback) pattern for animated view switching" + - "hideStep/showStep helpers using style.display for reliable visibility control" + - "announce() via aria-live polite region with 100ms delay for screen reader pickup" + +duration: ~45min +started: 2026-03-25T14:00:00Z +completed: 2026-03-25T14:45:00Z +--- + +# Phase 4 Plan 01: Polish & Integration Testing Summary + +**Edge cases (token retry, timeout, network errors), animated step transitions, ARIA dialog with focus trap — formularz gotowy produkcyjnie.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~45min | +| Tasks | 3 auto + 1 checkpoint | +| Files modified | 3 | +| Deviations | 1 (auto-fixed) | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Wygasły token JWT | Pass | 401/403 → auto-retry 1x, user nie widzi błędu | +| AC-2: Brak dostępności pojazdu | Pass | CAR_NOT_FOUND → czytelny komunikat PL z sugestią | +| AC-3: Timeouty i błędy sieciowe | Pass | AbortController 15s, TypeError → retry → komunikat PL | +| AC-4: Animacje przejść | Pass | fade+translate form↔summary↔success, modal scale | +| AC-5: Accessibility | Pass | role=dialog, aria-modal, focus trap, focus management, aria-live | +| AC-6: Fix CSS orphaned styles | Pass | Styles przeniesione do @media (max-width: 768px) | + +## Accomplishments + +- Pełny error recovery: token retry, timeout 15s, network errors — formularz nigdy nie blokuje się na stałe +- Animowane przejścia między krokami: smooth fade+translate (250ms) z callback focus management +- Focus trap w modalu: Tab nie wychodzi poza modal, Escape zamyka, focus wraca na trigger +- aria-live announcements: "Ładowanie podsumowania...", "Rezerwacja potwierdzona", błędy API + +## Files Modified + +| File | Change | Purpose | +|------|--------|---------| +| `assets/js/carei-reservation.js` | Modified | Token retry, timeout, network errors, animacje, focus trap, aria-live | +| `assets/css/carei-reservation.css` | Modified | Modal animations, step transitions, sr-only class, CSS bug fix | +| `includes/class-elementor-widget.php` | Modified | ARIA: role=dialog, aria-modal, aria-labelledby, aria-busy, tabindex=-1 | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| style.display zamiast CSS class dla visibility | HTML inline display:none nadpisuje klasy CSS | hideStep/showStep helpers operują na style.display | +| Focus na segment select po open | Pierwszy interaktywny element, najlogiczniejszy start | 350ms delay po animacji otwarcia | +| aria-live region w overlay | Scoped do modalu, nie zanieczyszcza strony | announce() z 100ms delay dla pickup przez screen readery | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 1 | Konieczna zmiana podejścia do visibility | + +**Total impact:** Essential fix, no scope creep + +### Auto-fixed Issues + +**1. Inline display:none vs CSS class conflict** +- **Found during:** Checkpoint (user reported form title instead of summary) +- **Issue:** HTML elements miały `style="display:none;"` inline, a transitionStep używał klas CSS (`carei-step--hidden`) — inline style nadpisywał klasę +- **Fix:** Zmieniono na hideStep()/showStep() operujące na `el.style.display` +- **Verification:** User potwierdził poprawne działanie po fix + +## Next Phase Readiness + +**Ready:** +- Formularz produkcyjnie gotowy: error handling, animacje, a11y +- Booking flow end-to-end przetestowany na carei.pagedev.pl + +**Concerns:** +- Penalty items filtrowane po kodzie (BRAK/BRUD/KARA) — może nie pokryć przyszłych kodów +- Brak email notification po rezerwacji (zależy od konfiguracji Softra) + +**Blockers:** None + +--- +*Phase: 04-polish-testing, Plan: 01* +*Completed: 2026-03-25* diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index 4da2102..1817d6f 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -102,6 +102,108 @@ "lmtime": 0, "modified": false }, + ".paul": { + "HANDOFF-2026-03-25.md": { + "type": "-", + "size": 3911, + "lmtime": 0, + "modified": false + }, + "HANDOFF-2026-03-25-phase3.md": { + "type": "-", + "size": 3672, + "lmtime": 1774440974369, + "modified": false + }, + "HANDOFF-2026-03-25-phase5.md": { + "type": "-", + "size": 4436, + "lmtime": 1774453504251, + "modified": false + }, + "handoffs": { + "archive": { + "HANDOFF-2026-03-25-phase3.md": { + "type": "-", + "size": 3672, + "lmtime": 1774440974369, + "modified": false + }, + "HANDOFF-2026-03-25.md": { + "type": "-", + "size": 3911, + "lmtime": 1774394933000, + "modified": false + } + } + }, + "phases": { + "01-reservation-form-plugin": {}, + "02-form-ui-step1": { + "02-01-SUMMARY.md": { + "type": "-", + "size": 6067, + "lmtime": 1774426464638, + "modified": false + } + }, + "03-form-submit-booking": { + "03-01-PLAN.md": { + "type": "-", + "size": 17170, + "lmtime": 1774429395933, + "modified": false + }, + "03-01-SUMMARY.md": { + "type": "-", + "size": 5371, + "lmtime": 1774440223363, + "modified": false + } + }, + "04-polish-testing": { + "04-01-PLAN.md": { + "type": "-", + "size": 12463, + "lmtime": 1774444454151, + "modified": false + }, + "04-01-SUMMARY.md": { + "type": "-", + "size": 5317, + "lmtime": 1774447383859, + "modified": false + } + }, + "05-admin-panel": { + "05-01-PLAN.md": { + "type": "-", + "size": 11298, + "lmtime": 1774447717369, + "modified": false + } + } + }, + "PROJECT.md": { + "type": "-", + "size": 2025, + "lmtime": 0, + "modified": false + }, + "ROADMAP.md": { + "type": "-", + "size": 1906, + "lmtime": 1774447405669, + "modified": false + }, + "STATE.md": { + "type": "-", + "size": 1376, + "lmtime": 1774453515616, + "modified": false + } + }, + ".playwright-mcp": {}, "readme.html": { "type": "-", "size": 7425, @@ -169,7 +271,62 @@ "lmtime": 0, "modified": false }, - "wp-content": {}, + "wp-content": { + "plugins": { + "carei-reservation": { + "assets": { + "js": { + "carei-reservation.js": { + "type": "-", + "size": 42643, + "lmtime": 1774446710808, + "modified": false + } + }, + "css": { + "carei-reservation.css": { + "type": "-", + "size": 22586, + "lmtime": 1774445557903, + "modified": false + } + } + }, + "carei-reservation.php": { + "type": "-", + "size": 3199, + "lmtime": 1774448068413, + "modified": false + }, + "includes": { + "class-admin-panel.php": { + "type": "-", + "size": 18994, + "lmtime": 1774448051188, + "modified": false + }, + "class-elementor-widget.php": { + "type": "-", + "size": 16135, + "lmtime": 1774445732326, + "modified": false + }, + "class-rest-proxy.php": { + "type": "-", + "size": 10811, + "lmtime": 1774448090860, + "modified": false + }, + "class-softra-api.php": { + "type": "-", + "size": 8662, + "lmtime": 1774439108636, + "modified": false + } + } + } + } + }, "wp-cron.php": { "type": "-", "size": 5617, diff --git a/wp-content/plugins/carei-reservation/assets/css/carei-reservation.css b/wp-content/plugins/carei-reservation/assets/css/carei-reservation.css index 1929e4b..cad203c 100644 --- a/wp-content/plugins/carei-reservation/assets/css/carei-reservation.css +++ b/wp-content/plugins/carei-reservation/assets/css/carei-reservation.css @@ -54,7 +54,7 @@ Modal Overlay & Container ═══════════════════════════════════════════ */ .carei-modal-overlay { - display: none; + display: flex; position: fixed; inset: 0; z-index: 100000; @@ -62,9 +62,13 @@ justify-content: center; align-items: center; padding: 20px; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; } .carei-modal-overlay.is-open { - display: flex; + opacity: 1; + visibility: visible; } .carei-modal { background: var(--carei-bg); @@ -76,6 +80,11 @@ padding: 40px 48px; position: relative; font-family: var(--carei-font); + transform: scale(0.95) translateY(10px); + transition: transform 0.3s ease; +} +.carei-modal-overlay.is-open .carei-modal { + transform: scale(1) translateY(0); } .carei-modal-close { position: absolute; @@ -134,8 +143,21 @@ grid-template-columns: 1fr 1fr; gap: var(--carei-gap-inner); } -.carei-form__row--dates { - grid-template-columns: 1fr 1fr; +.carei-form__row--top { + grid-template-columns: 1fr 1fr 1fr; +} +.carei-form__row--pickup { + display: flex; + align-items: center; + gap: var(--carei-gap-inner); +} +.carei-form__row--pickup > .carei-form__field { + flex: 1; + min-width: 0; +} +.carei-form__checkbox-label--inline { + white-space: nowrap; + flex-shrink: 0; } .carei-form__field { display: flex; @@ -483,6 +505,20 @@ pointer-events: none; } +/* Disabled select */ +.carei-form__select-wrap select:disabled { + color: var(--carei-placeholder); + cursor: not-allowed; + opacity: 0.6; +} + +/* Extras wrapper — hidden until segment + pickup selected */ +.carei-form__extras-wrapper { + display: flex; + flex-direction: column; + gap: var(--carei-gap-section); +} + /* Return branch slide */ .carei-form__return-wrap { overflow: hidden; @@ -496,6 +532,37 @@ display: block !important; } +/* ═══════════════════════════════════════════ + Step Transitions (form ↔ summary ↔ success) + ═══════════════════════════════════════════ */ +.carei-step { + transition: opacity 0.25s ease, transform 0.25s ease; +} +.carei-step--hidden { + display: none !important; +} +.carei-step--exiting { + opacity: 0; + transform: translateY(-8px); +} +.carei-step--entering { + opacity: 0; + transform: translateY(8px); +} + +/* Screen reader only */ +.carei-sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + /* ═══════════════════════════════════════════ Mobile Responsive (<768px) ═══════════════════════════════════════════ */ @@ -516,7 +583,7 @@ .carei-form__row { grid-template-columns: 1fr; } - .carei-form__row--dates { + .carei-form__row--top { grid-template-columns: 1fr 1fr; } .carei-form__footer { @@ -528,14 +595,246 @@ justify-content: center; padding: 16px; } + .carei-form__row--address { + grid-template-columns: 1fr; + } + .carei-summary__actions { + flex-direction: column; + } + .carei-summary__btn { + width: 100%; + justify-content: center; + } } /* Very small screens */ @media (max-width: 480px) { - .carei-form__row--dates { + .carei-form__row--top { grid-template-columns: 1fr; } .carei-modal { padding: 24px 16px; } } + +/* ═══════════════════════════════════════════ + Address Row + ═══════════════════════════════════════════ */ +.carei-form__row--address { + display: grid; + grid-template-columns: 1fr auto 1fr; + gap: var(--carei-gap-inner); +} +.carei-form__field--zipcode { + width: 120px; +} + +/* ═══════════════════════════════════════════ + Summary Overlay + ═══════════════════════════════════════════ */ +.carei-summary { + display: flex; + flex-direction: column; + gap: var(--carei-gap-section); +} +.carei-summary__title { + font-family: var(--carei-font); + font-weight: 700; + font-size: 20px; + color: var(--carei-blue); + text-align: center; + margin: 0; +} +.carei-summary__title span { + color: var(--carei-red); +} +.carei-summary__details { + font-family: var(--carei-font); + font-size: 14px; + color: var(--carei-gray); + display: flex; + flex-direction: column; + gap: 6px; + background: var(--carei-white); + padding: 16px; + border-radius: var(--carei-radius); +} +.carei-summary__details strong { + color: var(--carei-blue); +} +.carei-summary__table table { + width: 100%; + border-collapse: collapse; + font-family: var(--carei-font); + font-size: 13px; +} +.carei-summary__table th { + text-align: left; + font-weight: 600; + color: var(--carei-blue); + padding: 8px 12px; + border-bottom: 2px solid var(--carei-border); + font-size: 12px; + text-transform: uppercase; +} +.carei-summary__table td { + padding: 8px 12px; + border-bottom: 1px solid var(--carei-border); + color: var(--carei-gray); +} +.carei-summary__table tr:nth-child(even) td { + background: rgba(237, 237, 243, 0.5); +} +.carei-summary__table .carei-summary__auto-item td { + font-style: italic; + color: var(--carei-placeholder); +} +.carei-summary__table td:last-child { + text-align: right; + font-weight: 600; + color: var(--carei-blue); +} +.carei-summary__total { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + font-family: var(--carei-font); + font-size: 14px; + color: var(--carei-gray); + padding: 0 12px; +} +.carei-summary__total-row { + display: flex; + gap: 16px; +} +.carei-summary__total-row--gross { + font-size: 18px; + font-weight: 700; + color: var(--carei-blue); +} +.carei-summary__total-label { + min-width: 120px; + text-align: right; +} +.carei-summary__total-value { + min-width: 100px; + text-align: right; +} +.carei-summary__actions { + display: flex; + justify-content: space-between; + gap: var(--carei-gap-inner); + padding-top: 8px; +} +.carei-summary__btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 14px 28px; + font-family: var(--carei-font); + font-weight: 600; + font-size: 14px; + border-radius: var(--carei-radius); + cursor: pointer; + transition: all 0.2s; + line-height: 1; + white-space: nowrap; +} +.carei-summary__btn svg { + width: 16px; + height: 16px; +} +.carei-summary__btn--back { + background: transparent; + border: 1px solid var(--carei-blue); + color: var(--carei-blue); +} +.carei-summary__btn--back:hover { + background: rgba(47, 36, 130, 0.05); +} +.carei-summary__btn--confirm { + background: var(--carei-red); + border: 1px solid var(--carei-red); + color: var(--carei-white); +} +.carei-summary__btn--confirm:hover { + background: var(--carei-red-hover); + border-color: var(--carei-red-hover); +} +.carei-summary__btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} +.carei-summary__error { + background: rgba(255, 0, 0, 0.05); + color: var(--carei-red); + padding: 12px 16px; + border-radius: var(--carei-radius); + font-family: var(--carei-font); + font-size: 13px; + text-align: center; +} + +/* ═══════════════════════════════════════════ + Success View + ═══════════════════════════════════════════ */ +.carei-success { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 16px; + padding: 40px 0; +} +.carei-success__icon { + width: 72px; + height: 72px; + border-radius: 50%; + background: #22c55e; + display: flex; + align-items: center; + justify-content: center; +} +.carei-success__title { + font-family: var(--carei-font); + font-weight: 700; + font-size: 22px; + color: var(--carei-blue); + margin: 0; +} +.carei-success__number { + font-family: var(--carei-font); + font-weight: 700; + font-size: 16px; + color: var(--carei-blue); + background: var(--carei-white); + padding: 12px 24px; + border-radius: var(--carei-radius); + margin: 0; +} +.carei-success__message { + font-family: var(--carei-font); + font-size: 14px; + color: var(--carei-gray); + margin: 0; +} +.carei-success__close { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 14px 32px; + background: 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; + margin-top: 8px; +} +.carei-success__close:hover { + background: var(--carei-red-hover); +} diff --git a/wp-content/plugins/carei-reservation/assets/js/carei-reservation.js b/wp-content/plugins/carei-reservation/assets/js/carei-reservation.js index ce87cc6..42e20c9 100644 --- a/wp-content/plugins/carei-reservation/assets/js/carei-reservation.js +++ b/wp-content/plugins/carei-reservation/assets/js/carei-reservation.js @@ -6,38 +6,83 @@ // ─── API Helpers ────────────────────────────────────────────── - function apiGet(endpoint) { + var API_TIMEOUT_MS = 15000; + + function apiGet(endpoint, isRetry) { + var controller = new AbortController(); + var timer = setTimeout(function () { controller.abort(); }, API_TIMEOUT_MS); return fetch(REST_URL + endpoint, { method: 'GET', - headers: { - 'X-WP-Nonce': NONCE, - 'Content-Type': 'application/json' - } + headers: { 'X-WP-Nonce': NONCE, 'Content-Type': 'application/json' }, + signal: controller.signal }).then(function (r) { - if (!r.ok) throw new Error('API error: ' + r.status); - return r.json(); + clearTimeout(timer); + return handleResponse(r); + }).catch(function (err) { + clearTimeout(timer); + return handleFetchError(err, function () { return apiGet(endpoint, true); }, isRetry); }); } - function apiPost(endpoint, data) { + function apiPost(endpoint, data, isRetry) { + var controller = new AbortController(); + var timer = setTimeout(function () { controller.abort(); }, API_TIMEOUT_MS); return fetch(REST_URL + endpoint, { method: 'POST', - headers: { - 'X-WP-Nonce': NONCE, - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) + headers: { 'X-WP-Nonce': NONCE, 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + signal: controller.signal }).then(function (r) { - if (!r.ok) throw new Error('API error: ' + r.status); - return r.json(); + clearTimeout(timer); + if ((r.status === 401 || r.status === 403) && !isRetry) { + return apiPost(endpoint, data, true); + } + return handleResponse(r); + }).catch(function (err) { + clearTimeout(timer); + return handleFetchError(err, function () { return apiPost(endpoint, data, true); }, isRetry); }); } + function handleResponse(r) { + if (!r.ok) { + var status = r.status; + return r.json().then(function (body) { + var msg = (body && body.message) || (body && body.detail) || ('Błąd API: HTTP ' + status); + var err = new Error(msg); + err.httpStatus = status; + throw err; + }).catch(function (parseErr) { + if (parseErr.httpStatus) throw parseErr; + var err = new Error('Błąd API: HTTP ' + status); + err.httpStatus = status; + throw err; + }); + } + return r.json(); + } + + function handleFetchError(err, retryFn, isRetry) { + if (err.name === 'AbortError') { + throw new Error('Przekroczono czas oczekiwania. Spróbuj ponownie.'); + } + if (err instanceof TypeError && !isRetry) { + return retryFn(); + } + if (err instanceof TypeError) { + throw new Error('Brak połączenia z serwerem. Sprawdź internet i spróbuj ponownie.'); + } + throw err; + } + // ─── DOM Refs ───────────────────────────────────────────────── var overlay, form, segmentSelect, dateFrom, dateTo, daysCount; var pickupSelect, returnSelect, returnWrap, sameReturnCheck; - var extrasContainer, errorSummary; + var extrasWrapper, extrasContainer, insuranceContainer, errorSummary; + var summaryOverlay, summaryDetails, summaryTable, summaryTotal, summaryError; + var summaryBack, summaryConfirm; + var successView, successNumber, successClose; function initRefs() { overlay = document.querySelector('[data-carei-modal]'); @@ -50,317 +95,300 @@ returnSelect = document.getElementById('carei-return-branch'); returnWrap = document.getElementById('carei-return-wrap'); sameReturnCheck = document.getElementById('carei-same-return'); + extrasWrapper = document.getElementById('carei-extras-wrapper'); extrasContainer = document.getElementById('carei-extras-container'); + insuranceContainer = document.getElementById('carei-insurance-container'); errorSummary = document.getElementById('carei-error-summary'); + // Summary overlay + summaryOverlay = document.getElementById('carei-summary-overlay'); + summaryDetails = document.getElementById('carei-summary-details'); + summaryTable = document.getElementById('carei-summary-table'); + summaryTotal = document.getElementById('carei-summary-total'); + summaryError = document.getElementById('carei-summary-error'); + summaryBack = document.getElementById('carei-summary-back'); + summaryConfirm = document.getElementById('carei-summary-confirm'); + // Success view + successView = document.getElementById('carei-success-view'); + successNumber = document.getElementById('carei-success-number'); + successClose = document.getElementById('carei-success-close'); } + // ─── State ──────────────────────────────────────────────────── + + var mapData = null; + var allSegments = []; + var currentPriceListId = null; + var currentCustomerId = null; + var currentReservationId = null; + var agreementDefs = []; + var lastFocusedElement = null; + // ─── 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) { + document.querySelectorAll('[data-carei-open-modal]').forEach(function (btn) { btn.addEventListener('click', function (e) { e.preventDefault(); - openModal(); + openModal(btn); }); }); - - closeBtns.forEach(function (btn) { + document.querySelectorAll('[data-carei-close-modal]').forEach(function (btn) { btn.addEventListener('click', closeModal); }); - if (overlay) { overlay.addEventListener('click', function (e) { if (e.target === overlay) closeModal(); }); + overlay.addEventListener('keydown', handleFocusTrap); } - document.addEventListener('keydown', function (e) { - if (e.key === 'Escape' && overlay && overlay.classList.contains('is-open')) { - closeModal(); - } + if (e.key === 'Escape' && overlay && overlay.classList.contains('is-open')) closeModal(); }); } var dataLoaded = false; - function openModal() { + function openModal(triggerBtn) { if (!overlay) return; + lastFocusedElement = triggerBtn || document.activeElement; overlay.classList.add('is-open'); document.body.style.overflow = 'hidden'; - if (!dataLoaded) { - loadBranches(); - loadAllCarClasses(); + loadInitialData(); setDefaultDates(); dataLoaded = true; } + setTimeout(function () { + if (segmentSelect) segmentSelect.focus(); + }, 350); } function closeModal() { if (!overlay) return; overlay.classList.remove('is-open'); document.body.style.overflow = ''; + if (lastFocusedElement && lastFocusedElement.focus) { + setTimeout(function () { lastFocusedElement.focus(); }, 300); + } } + // ─── Focus Trap ───────────────────────────────────────────── + + var FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'; + + function handleFocusTrap(e) { + if (e.key !== 'Tab') return; + var modal = overlay.querySelector('.carei-modal'); + if (!modal) return; + var focusable = Array.prototype.slice.call(modal.querySelectorAll(FOCUSABLE_SELECTOR)).filter(function (el) { + return el.offsetParent !== null; + }); + if (focusable.length === 0) return; + var first = focusable[0]; + var last = focusable[focusable.length - 1]; + if (e.shiftKey) { + if (document.activeElement === first) { e.preventDefault(); last.focus(); } + } else { + if (document.activeElement === last) { e.preventDefault(); first.focus(); } + } + } + + // ─── Initial Data Loading ───────────────────────────────────── + + function loadInitialData() { + if (segmentSelect) setSelectLoading(segmentSelect, true); + + Promise.all([ + apiGet('car-classes-all'), + apiGet('segments-branches-map'), + apiGet('agreements') + ]).then(function (results) { + var classes = results[0]; + mapData = results[1]; + agreementDefs = Array.isArray(results[2]) ? results[2] : []; + + if (Array.isArray(classes) && classes.length > 0) { + allSegments = classes.map(function (c) { + var val = typeof c === 'string' ? c : (c.name || c); + var label = typeof c === 'string' ? ('Segment ' + c) : (c.description || c.name || c); + return { value: val, label: label }; + }); + populateSelect(segmentSelect, allSegments, 'Wybierz segment pojazdu'); + } else { + populateSelect(segmentSelect, [], 'Brak segmentów'); + } + if (segmentSelect) setSelectLoading(segmentSelect, false); + + if (pickupSelect) { + populateSelect(pickupSelect, [], 'Najpierw wybierz segment'); + pickupSelect.disabled = true; + } + }).catch(function (err) { + console.error('Failed to load initial data:', err); + if (segmentSelect) { + populateSelect(segmentSelect, [], 'Błąd ładowania'); + setSelectLoading(segmentSelect, false); + } + }); + } + + // ─── Segment Change → Filter Pickup Locations ───────────────── + + function onSegmentChange() { + var selectedSegment = segmentSelect ? segmentSelect.value : ''; + if (!selectedSegment || !mapData || !pickupSelect) { + if (pickupSelect) { populateSelect(pickupSelect, [], 'Najpierw wybierz segment'); pickupSelect.disabled = true; } + hideExtras(); + return; + } + var segBranches = mapData.segmentToBranches[selectedSegment] || []; + var allBranches = mapData.branches || []; + var filteredOptions = []; + allBranches.forEach(function (b) { + if (segBranches.indexOf(b.name || '') !== -1) { + var label = b.description || b.name; + if (b.city) label += ' — ' + b.city; + filteredOptions.push({ value: b.name, label: label }); + } + }); + if (filteredOptions.length > 0) { + populateSelect(pickupSelect, filteredOptions, 'Miejsce odbioru'); + pickupSelect.disabled = false; + } else { + populateSelect(pickupSelect, [], 'Brak lokalizacji dla tego segmentu'); + pickupSelect.disabled = true; + } + if (returnSelect) { + var returnOptions = allBranches.map(function (b) { + var label = b.description || b.name; + if (b.city) label += ' — ' + b.city; + return { value: b.name, label: label }; + }); + populateSelect(returnSelect, returnOptions, 'Miejsce zwrotu'); + } + hideExtras(); + } + + function onPickupChange() { + maybeShowExtras(); + } + + function maybeShowExtras() { + var segment = segmentSelect ? segmentSelect.value : ''; + var pickup = pickupSelect ? pickupSelect.value : ''; + if (segment && pickup) { showExtras(); loadExtras(); } else { hideExtras(); } + } + + function showExtras() { if (extrasWrapper) extrasWrapper.style.display = ''; } + function hideExtras() { if (extrasWrapper) extrasWrapper.style.display = 'none'; } + // ─── 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); - + 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 = fmtDT(tomorrow); + dateTo.value = fmtDT(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; + function fmtDT(d) { + return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + 'T' + pad(d.getHours()) + ':' + pad(d.getMinutes()); } + function pad(n) { return String(n).padStart(2, '0'); } // ─── Days Count ─────────────────────────────────────────────── function updateDaysCount() { if (!dateFrom || !dateTo || !daysCount) return; - - var from = new Date(dateFrom.value); - var to = new Date(dateTo.value); - + var from = new Date(dateFrom.value), to = new Date(dateTo.value); if (isNaN(from.getTime()) || isNaN(to.getTime()) || to <= from) { - daysCount.innerHTML = 'Wybrano: 0 dni'; - return; + daysCount.innerHTML = 'Wybrano: 0 dni'; return; } - - var diff = Math.ceil((to - from) / (1000 * 60 * 60 * 24)); - var label = diff === 1 ? 'dzień' : 'dni'; - daysCount.innerHTML = 'Wybrano: ' + diff + ' ' + label + ''; - } - - // ─── 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); - }); + var diff = Math.ceil((to - from) / 86400000); + daysCount.innerHTML = 'Wybrano: ' + diff + ' ' + (diff === 1 ? 'dzień' : 'dni') + ''; } // ─── 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 (!segmentSelect || !dateFrom || !dateTo || !pickupSelect) return; + var category = segmentSelect.value, fromVal = dateFrom.value, toVal = dateTo.value, 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 = ''; - + category: category, dateFrom: fromVal + ':00', dateTo: toVal + ':00', pickUpLocation: branch + }).then(function (pricelists) { + if (!Array.isArray(pricelists) || pricelists.length === 0) return; + var pricelist = pricelists[0]; + currentPriceListId = pricelist.id; + var items = pricelist.additionalItems; + var insuranceItems = [], extraItems = []; + if (Array.isArray(items)) { 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 = - ''; - - extrasContainer.appendChild(card); + var name = (item.name || '').toLowerCase(); + // Skip penalty/return items + var code = (item.code || '').toUpperCase(); + if (code.indexOf('BRAK') === 0 || code.indexOf('BRUD') === 0 || code.indexOf('KARA') === 0 || + code.indexOf('MYCIE USŁU') === 0 || code === 'MYJ WEW') return; + if (name.indexOf('ubezp') !== -1 || name.indexOf('ochrony') !== -1 || + name.indexOf('zniesienie') !== -1 || name.indexOf('insurance') !== -1) { + insuranceItems.push(item); + } else { + extraItems.push(item); + } }); - }) - .catch(function (err) { - console.error('Failed to load pricelist:', err); - // Keep static fallback extras - }); + } + if (insuranceContainer) { insuranceContainer.innerHTML = ''; insuranceItems.forEach(function (item) { insuranceContainer.appendChild(buildExtraCard(item)); }); } + if (extrasContainer) { extrasContainer.innerHTML = ''; extraItems.forEach(function (item) { extrasContainer.appendChild(buildExtraCard(item)); }); } + }).catch(function (err) { console.error('Failed to load pricelist:', err); }); + } + + function buildExtraCard(item) { + var price = parseFloat(item.price || item.minPrice || 0); + var maxPrice = parseFloat(item.maxPrice || 0); + var priceLabel = (maxPrice > 0 && maxPrice !== price) + ? 'od ' + price.toFixed(0) + ' do ' + maxPrice.toFixed(0) + ' zł' + : (price > 0 ? price.toFixed(0) + ' zł' + (item.unit === 'doba' ? '/doba' : '') : 'Gratis'); + var card = document.createElement('div'); + card.className = 'carei-form__extra-card'; + card.innerHTML = + ''; + return card; } // ─── Select Helpers ─────────────────────────────────────────── function populateSelect(select, options, placeholder) { + if (!select) return; select.innerHTML = ''; - var ph = document.createElement('option'); - ph.value = ''; - ph.disabled = true; - ph.selected = true; - ph.textContent = placeholder || 'Wybierz...'; + 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); + o.value = opt.value; o.textContent = opt.label; select.appendChild(o); }); } function setSelectLoading(select, loading) { + if (!select) return; var wrap = select.closest('.carei-form__select-wrap'); - if (!wrap) return; - if (loading) { - wrap.classList.add('carei-form__select-wrap--loading'); - } else { - wrap.classList.remove('carei-form__select-wrap--loading'); - } + if (wrap) wrap.classList.toggle('carei-form__select-wrap--loading', 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'); - } + returnWrap.classList.toggle('is-visible', !sameReturnCheck.checked); }); } @@ -373,71 +401,43 @@ { 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-city', type: 'input', msg: 'Podaj miejscowość' }, + { id: 'carei-zipcode', type: 'input', msg: 'Podaj kod pocztowy' }, + { id: 'carei-street', type: 'input', msg: 'Podaj ulicę' }, { 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-pesel', type: 'pesel', msg: 'Podaj poprawny PESEL (11 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(); - }); + 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); - } + if (f.type === 'checkbox') { if (!el.checked) hasError = true; } + else if (f.type === 'email') { if (!el.value.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(el.value.trim())) hasError = true; } + else if (f.type === 'phone') { if (el.value.replace(/\D/g, '').length < 9) hasError = true; } + else if (f.type === 'pesel') { if (!/^\d{11}$/.test(el.value.trim())) hasError = true; } + else if (f.type === 'select') { if (!el.value) hasError = true; } + else { if (!el.value.trim()) hasError = true; } + if (hasError) { valid = false; markFieldError(el, f.msg, f.type); } }); - // 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'); + 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 (sameReturnCheck && !sameReturnCheck.checked && returnSelect && !returnSelect.value) { + valid = false; markFieldError(returnSelect, 'Wybierz miejsce zwrotu', 'select'); } - - if (errorSummary) { - errorSummary.style.display = valid ? 'none' : 'block'; - } - + if (errorSummary) errorSummary.style.display = valid ? 'none' : 'block'; return valid; } @@ -447,16 +447,11 @@ 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 = 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; + errEl.className = 'carei-form__error-msg'; errEl.textContent = msg; field.appendChild(errEl); } } @@ -468,14 +463,11 @@ var field = e.target.closest('.carei-form__field'); if (field && field.classList.contains('carei-form__field--error')) { field.classList.remove('carei-form__field--error'); - var errMsg = field.querySelector('.carei-form__error-msg'); - if (errMsg) errMsg.remove(); + var 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'); @@ -484,45 +476,38 @@ }); } - // ─── Form Submit ────────────────────────────────────────────── + // ─── Form Submit → Booking Flow ─────────────────────────────── 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 = - ' Wysyłanie...'; - - // Re-enable after 2s (temporary — Phase 3 will handle properly) - setTimeout(function () { - submitBtn.disabled = false; - submitBtn.innerHTML = - ' Wyślij'; - }, 2000); - } + setSubmitState('loading'); + createCustomerAndShowSummary(); }); + + if (summaryBack) summaryBack.addEventListener('click', handleSummaryBack); + if (summaryConfirm) summaryConfirm.addEventListener('click', handleSummaryConfirm); + if (successClose) successClose.addEventListener('click', handleSuccessClose); + } + + function setSubmitState(state) { + var btn = form ? form.querySelector('.carei-form__submit') : null; + if (!btn) return; + if (state === 'loading') { + btn.disabled = true; + btn.setAttribute('aria-busy', 'true'); + btn.innerHTML = ' Przetwarzanie...'; + } else { + btn.disabled = false; + btn.setAttribute('aria-busy', 'false'); + btn.innerHTML = ' Wyślij'; + } } 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) - }); - }); - + var phoneRaw = document.getElementById('carei-phone') ? document.getElementById('carei-phone').value.replace(/\D/g, '') : ''; return { segment: segmentSelect ? segmentSelect.value : '', dateFrom: dateFrom ? dateFrom.value : '', @@ -530,53 +515,386 @@ 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'), + city: val('carei-city'), + zipCode: val('carei-zipcode'), + street: val('carei-street'), email: val('carei-email'), - phone: '+48' + (document.getElementById('carei-phone') ? document.getElementById('carei-phone').value.replace(/\D/g, '') : ''), + phone: '+48' + phoneRaw, + pesel: val('carei-pesel'), 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() : ''; + function val(id) { var el = document.getElementById(id); return el ? el.value.trim() : ''; } + + // ─── Step 1: Create Customer ────────────────────────────────── + + function createCustomerAndShowSummary() { + var fd = collectFormData(); + hideSummaryError(); + announce('Ładowanie podsumowania...'); + + apiPost('customer', { + firstName: fd.firstName, + lastName: fd.lastName, + name: fd.firstName + ' ' + fd.lastName, + isCompany: false, + address: { city: fd.city, zipCode: fd.zipCode, street: fd.street, homeNo: '-' }, + pesel: fd.pesel, + email: fd.email, + phoneMobile: fd.phone, + paymentMethod: 'GOTÓWKA', + skipAccountCreate: true, + emailVerified: true + }).then(function (res) { + if (res && res.customerId) { + currentCustomerId = res.customerId; + return loadPricingSummary(fd); + } + throw new Error(res.rejectReason || 'Nie udało się utworzyć klienta'); + }).catch(function (err) { + console.error('Customer creation failed:', err); + setSubmitState('ready'); + showFormError('Błąd tworzenia klienta: ' + err.message); + }); } - // ─── Event Listeners for Dynamic Loading ────────────────────── + // ─── Step 2: Pricing Summary ────────────────────────────────── + + function loadPricingSummary(fd) { + var returnBranch = fd.sameReturn ? fd.pickupBranch : fd.returnBranch; + + return apiPost('pricing-summary', { + dateFrom: fd.dateFrom + ':00', + dateTo: fd.dateTo + ':00', + customerId: currentCustomerId, + pickUpLocation: { branchName: fd.pickupBranch, outOfBranch: 'N' }, + returnLocation: { branchName: returnBranch, outOfBranch: 'N' }, + carParameters: { categoryName: fd.segment }, + priceListId: currentPriceListId, + priceItems: getSelectedExtrasForApi() + }).then(function (summary) { + showSummaryOverlay(summary, fd); + }).catch(function (err) { + console.error('Pricing summary failed:', err); + setSubmitState('ready'); + showFormError('Błąd pobierania podsumowania: ' + err.message); + }); + } + + function getSelectedExtrasForApi() { + var items = []; + if (!form) return items; + form.querySelectorAll('input[name="extras[]"]:checked').forEach(function (cb) { + var price = parseFloat(cb.getAttribute('data-price') || 0); + items.push({ + id: cb.value, + name: cb.getAttribute('data-name') || '', + unit: cb.getAttribute('data-unit') || 'szt.', + amount: 1, + priceBeforeDiscount: price, + discount: 0, + priceAfterDiscount: price + }); + }); + return items; + } + + // ─── Step Transitions ────────────────────────────────────────── + + function hideStep(el) { + if (!el) return; + el.style.display = 'none'; + el.classList.remove('carei-step--entering', 'carei-step--exiting'); + } + + function showStep(el) { + if (!el) return; + el.style.display = ''; + el.classList.remove('carei-step--hidden'); + } + + function transitionStep(outEl, inEl, callback) { + if (outEl) { + outEl.classList.add('carei-step--exiting'); + setTimeout(function () { + hideStep(outEl); + if (inEl) { + inEl.classList.add('carei-step--entering'); + showStep(inEl); + requestAnimationFrame(function () { + requestAnimationFrame(function () { + inEl.classList.remove('carei-step--entering'); + if (callback) callback(); + }); + }); + } else if (callback) { callback(); } + }, 250); + } else if (inEl) { + inEl.classList.add('carei-step--entering'); + showStep(inEl); + requestAnimationFrame(function () { + requestAnimationFrame(function () { + inEl.classList.remove('carei-step--entering'); + if (callback) callback(); + }); + }); + } + } + + // ─── Step 3: Show Summary Overlay ───────────────────────────── + + function showSummaryOverlay(summary, fd) { + if (!summaryOverlay) return; + + hideSummaryError(); + + // Populate content before transition + populateSummaryContent(summary, fd); + + // Animated transition: form → summary + transitionStep(form, summaryOverlay, function () { + announce('Podsumowanie rezerwacji'); + var title = summaryOverlay.querySelector('.carei-summary__title'); + if (title) title.focus(); + }); + } + + function populateSummaryContent(summary, fd) { + // Details + if (summaryDetails) { + var segLabel = segmentSelect ? segmentSelect.options[segmentSelect.selectedIndex].text : fd.segment; + var pickupLabel = pickupSelect ? pickupSelect.options[pickupSelect.selectedIndex].text : fd.pickupBranch; + var returnLabel = ''; + if (!fd.sameReturn && returnSelect) { + returnLabel = returnSelect.options[returnSelect.selectedIndex].text; + } + var html = + '
Segment: ' + escHtml(segLabel) + '
' + + '
Od: ' + escHtml(fd.dateFrom.replace('T', ' ')) + '
' + + '
Do: ' + escHtml(fd.dateTo.replace('T', ' ')) + '
' + + '
Miejsce odbioru: ' + escHtml(pickupLabel) + '
'; + if (returnLabel) { + html += '
Miejsce zwrotu: ' + escHtml(returnLabel) + '
'; + } + html += '
Najemca: ' + escHtml(fd.firstName + ' ' + fd.lastName) + '
' + + '
Email: ' + escHtml(fd.email) + '
' + + '
Telefon: ' + escHtml(fd.phone) + '
'; + + // Selected extras + var selectedExtras = getSelectedExtrasForApi(); + if (selectedExtras.length > 0) { + html += '
Wybrane opcje:
    '; + selectedExtras.forEach(function (ex) { + html += '
  • ' + escHtml(ex.name) + ' — ' + fmtPrice(ex.priceAfterDiscount) + ' zł
  • '; + }); + html += '
'; + } + + if (fd.message) { + html += '
Wiadomość: ' + escHtml(fd.message) + '
'; + } + + summaryDetails.innerHTML = html; + } + + // Price table + if (summaryTable && summary.pricelist) { + var html = ''; + summary.pricelist.forEach(function (item) { + var rowClass = item.addedBySystem ? ' class="carei-summary__auto-item"' : ''; + html += '' + + '' + + '' + + '' + + ''; + }); + html += '
NazwaIlośćNettoBrutto
' + escHtml(item.name) + (item.addedBySystem ? ' (auto)' : '') + '' + (item.amount || 1) + ' ' + escHtml(item.unit || '') + '' + fmtPrice(item.netValue) + '' + fmtPrice(item.grossValue) + '
'; + summaryTable.innerHTML = html; + } + + // Totals + if (summaryTotal) { + summaryTotal.innerHTML = + '
Netto:' + fmtPrice(summary.totalNetValue) + '
' + + '
VAT:' + fmtPrice(summary.totalVatValue) + '
' + + '
Do zapłaty:' + fmtPrice(summary.totalGrossValue) + ' zł
'; + } + } + + function fmtPrice(val) { + if (val === null || val === undefined) return '-'; + return parseFloat(val).toFixed(2).replace('.', ','); + } + + // ─── Summary Actions ────────────────────────────────────────── + + function handleSummaryBack() { + transitionStep(summaryOverlay, form, function () { + setSubmitState('ready'); + if (segmentSelect) segmentSelect.focus(); + }); + } + + function handleSummaryConfirm() { + if (summaryConfirm) { + summaryConfirm.disabled = true; + summaryConfirm.setAttribute('aria-busy', 'true'); + summaryConfirm.innerHTML = ' Rezerwuję...'; + } + hideSummaryError(); + + var fd = collectFormData(); + var returnBranch = fd.sameReturn ? fd.pickupBranch : fd.returnBranch; + + var bookingData = { + dateFrom: fd.dateFrom + ':00', + dateTo: fd.dateTo + ':00', + customerId: currentCustomerId, + pickUpLocation: { branchName: fd.pickupBranch, outOfBranch: 'N' }, + returnLocation: { branchName: returnBranch, outOfBranch: 'N' }, + carParameters: { categoryName: fd.segment }, + priceListId: currentPriceListId, + validTime: 30, + priceItems: getSelectedExtrasForApi(), + drivers: [{ + firstName: fd.firstName, + lastName: fd.lastName, + address: { city: fd.city, zipCode: fd.zipCode, street: fd.street }, + pesel: fd.pesel, + phone: fd.phone, + email: fd.email + }], + comments: fd.message || '' + }; + + // Add agreement items + if (agreementDefs.length > 0) { + bookingData.agreementItems = agreementDefs.map(function (a) { + return { id: a.id, value: true }; + }); + } + + apiPost('booking', bookingData).then(function (res) { + if (res && res.success && res.reservationId) { + currentReservationId = res.reservationId; + return apiPost('booking/confirm', { reservationId: res.reservationId }).then(function () { + showSuccessView(res.reservationNo || res.reservationId); + }); + } + throw new Error(translateRejectReason(res.rejectReason) || 'Rezerwacja nie powiodła się'); + }).catch(function (err) { + console.error('Booking failed:', err); + showSummaryError(err.message); + resetConfirmBtn(); + }); + } + + function translateRejectReason(reason) { + if (!reason) return null; + var map = { + 'CAR_NOT_FOUND': 'Brak dostępnego pojazdu w wybranym terminie. Zmień daty lub segment.', + 'INVALID_DATE_RANGE': 'Nieprawidłowy zakres dat', + 'BRANCH_NOT_FOUND': 'Nie znaleziono oddziału', + 'CUSTOMER_ALREADY_EXISTS': 'Klient o tych danych już istnieje w systemie', + 'INVALID_PESEL': 'Nieprawidłowy numer PESEL', + 'PRICE_LIST_EXPIRED': 'Cennik wygasł. Odśwież formularz i spróbuj ponownie.' + }; + return map[reason] || reason; + } + + function resetConfirmBtn() { + if (summaryConfirm) { + summaryConfirm.disabled = false; + summaryConfirm.setAttribute('aria-busy', 'false'); + summaryConfirm.innerHTML = ' Potwierdź rezerwację'; + } + } + + // ─── Success View ───────────────────────────────────────────── + + function showSuccessView(reservationNo) { + if (successNumber) successNumber.textContent = 'Nr rezerwacji: ' + reservationNo; + transitionStep(summaryOverlay, successView, function () { + announce('Rezerwacja potwierdzona'); + var title = successView.querySelector('.carei-success__title'); + if (title) title.focus(); + }); + } + + function handleSuccessClose() { + hideStep(successView); + if (form) { showStep(form); form.reset(); } + setSubmitState('ready'); + resetConfirmBtn(); + currentCustomerId = null; + currentReservationId = null; + hideExtras(); + if (pickupSelect) { populateSelect(pickupSelect, [], 'Najpierw wybierz segment'); pickupSelect.disabled = true; } + closeModal(); + } + + // ─── Error Helpers ──────────────────────────────────────────── + + function showFormError(msg) { + announce(msg); + if (errorSummary) { errorSummary.textContent = msg; errorSummary.style.display = 'block'; } + } + + function showSummaryError(msg) { + announce(msg); + if (summaryError) { summaryError.textContent = msg; summaryError.style.display = 'block'; } + } + + function hideSummaryError() { + if (summaryError) { summaryError.textContent = ''; summaryError.style.display = 'none'; } + } + + // ─── Event Listeners ────────────────────────────────────────── function initDynamicLoading() { - // Date change → update days count if (dateFrom) dateFrom.addEventListener('change', updateDaysCount); if (dateTo) dateTo.addEventListener('change', updateDaysCount); + if (segmentSelect) segmentSelect.addEventListener('change', onSegmentChange); + if (pickupSelect) pickupSelect.addEventListener('change', onPickupChange); + if (dateFrom) dateFrom.addEventListener('change', maybeShowExtras); + if (dateTo) dateTo.addEventListener('change', maybeShowExtras); + } - // 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); + // ─── Aria Live Announcements ─────────────────────────────────── + + var liveRegion = null; + + function initLiveRegion() { + liveRegion = document.createElement('div'); + liveRegion.setAttribute('aria-live', 'polite'); + liveRegion.setAttribute('role', 'status'); + liveRegion.className = 'carei-sr-only'; + if (overlay) overlay.appendChild(liveRegion); + } + + function announce(msg) { + if (!liveRegion) return; + liveRegion.textContent = ''; + setTimeout(function () { liveRegion.textContent = msg; }, 100); } // ─── HTML Escape Helpers ────────────────────────────────────── - function escHtml(str) { - var div = document.createElement('div'); - div.textContent = str || ''; - return div.innerHTML; - } - - function escAttr(str) { - return (str || '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(//g, '>'); - } + function escHtml(str) { var d = document.createElement('div'); d.textContent = str || ''; return d.innerHTML; } + function escAttr(str) { return (str || '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(//g, '>'); } // ─── Init ───────────────────────────────────────────────────── function init() { initRefs(); if (!overlay || !form) return; + initModal(); + initLiveRegion(); initSameReturn(); initDynamicLoading(); initClearErrors(); diff --git a/wp-content/plugins/carei-reservation/includes/class-elementor-widget.php b/wp-content/plugins/carei-reservation/includes/class-elementor-widget.php index 91d5e2d..0c825ab 100644 --- a/wp-content/plugins/carei-reservation/includes/class-elementor-widget.php +++ b/wp-content/plugins/carei-reservation/includes/class-elementor-widget.php @@ -56,29 +56,28 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base { -
+