diff --git a/.paul/PROJECT.md b/.paul/PROJECT.md index a3bf150..d073e83 100644 --- a/.paul/PROJECT.md +++ b/.paul/PROJECT.md @@ -60,9 +60,9 @@ Plugin Elementor do rezerwacji samochodu na stronie carei.pagedev.pl, zintegrowa ## Validated Requirements (Milestone v0.5) - ✓ Modal rezerwacji działa na mobile/tablet — Phase 14 +- ✓ Pakiety ochronne SOFT+PREMIUM z ceną/dobę zarządzaną w panelu WP — Phase 13 (plan 13-02) ## Out of Scope (backlog) -- Ubezpieczenie (pakiet Soft/Premium) — czeka na potwierdzenie klienta (źródło danych) - Eksport CSV/PDF rezerwacji - Email notyfikacje diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index e2bcb57..956ccb6 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -46,15 +46,14 @@ Dwa widgety Elementor: (1) mapa Polski SVG z dynamicznymi pinami oddziałów i t --- -## Milestone v0.5: Pakiety Ochronne +## Milestone v0.5: Pakiety Ochronne + Poprawki -**Goal:** Wyświetlanie pakietów ochronnych SOFT i PREMIUM z dynamiczną ceną zależną od liczby dni wynajmu (3 progi: minimalna 1-3 dni, za dobę 4-15 dni, maksymalna 16+ dni). +**Goal:** Wyświetlanie dwóch pakietów ochronnych (SOFT i PREMIUM) ze stałą ceną/dobę zarządzaną w panelu administratora WP. Plus poprawki UX (mobile modal fix). -**Status:** In progress +**Status:** Complete ✅ -### Phase 13: Pakiety ochronne — kafelki z ceną progową 🔄 BLOCKED -Dedykowane kafelki SOFT/PREMIUM w sekcji "Pakiety ochronne" z ceną obliczaną dynamicznie na podstawie długości rezerwacji. Wybór wzajemnie wykluczający (radio). Dane z istniejącego API pricelist/additionalItems. -**Blocker:** Czekamy na potwierdzenie klienta — źródło danych cenowych (API Softra vs panel WP). +### Phase 13: Pakiety ochronne — kafelki z ceną za dobę z panelu WP ✅ Complete +Dedykowane kafelki SOFT/PREMIUM w sekcji "Pakiety ochronne" z ceną za dobę z panelu WP. Radio z możliwością odznaczenia. Panel admina (wp-admin → Rezerwacje → Pakiety ochronne), REST endpoint /protection-packages. Pakiet poza priceItems Softra — wysyłany w comments + zapisany w meta CPT _carei_protection_package. Summary frontend dokłada wiersz pakietu i grandTotal. Plan 13-02 ukończony (13-01 superseded). ### Phase 14: Mobile modal fix ✅ Complete Fix: modal rezerwacji nie otwierał się na mobile/tablet — sekcja Elementor miała elementor-hidden-mobile. Przeniesienie overlay do document.body. diff --git a/.paul/STATE.md b/.paul/STATE.md index 1968df8..873bc75 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -2,19 +2,19 @@ ## Current Position -Milestone: v0.5 Pakiety Ochronne + Poprawki — In progress -Phase: 14 of 14 (Mobile modal fix) — Complete ✅ -Plan: 14-01 complete -Status: Phase 14 closed, Phase 13 BLOCKED -Last activity: 2026-04-10 — Phase 14 unified, SUMMARY written +Milestone: v0.5 Pakiety Ochronne + Poprawki — Complete ✅ +Phase: 13 of 14 (Pakiety ochronne) — Complete ✅ +Plan: 13-02 complete +Status: Milestone v0.5 closed +Last activity: 2026-04-20 — Phase 13 unified, SUMMARY written, milestone closed Progress: - Milestone v0.1: [██████████] 100% ✅ - Milestone v0.2: [██████████] 100% ✅ - Milestone v0.3: [██████████] 100% ✅ - Milestone v0.4: [██████████] 100% ✅ -- Milestone v0.5: [██░░░░░░░░] 20% - - Phase 13: BLOCKED — czeka na klienta +- Milestone v0.5: [██████████] 100% ✅ + - Phase 13: [██████████] 100% ✅ - Phase 14: [██████████] 100% ✅ ## Loop Position @@ -22,13 +22,12 @@ Progress: Current loop state: ``` PLAN ──▶ APPLY ──▶ UNIFY - ✓ ✓ ✓ [Phase 14 loop closed] + ✓ ✓ ✓ [Phase 13 loop closed, milestone v0.5 complete] ``` ## Session Continuity -Last session: 2026-04-10 -Stopped at: Phase 14 unified, Phase 13 BLOCKED -Next action: Czekamy na odpowiedź klienta — źródło danych cenowych SOFT/PREMIUM (API Softra vs panel WP) -Blocker: Phase 13 — potwierdzenie klienta -Resume file: .paul/phases/13-protection-packages/13-01-PLAN.md +Last session: 2026-04-20 +Stopped at: Milestone v0.5 complete (Phase 13 + Phase 14) +Next action: Rozmowa o następnym milestone (v0.6) lub zamknięcie projektu — /paul:discuss-milestone lub /paul:complete-milestone +Resume file: .paul/phases/13-protection-packages/13-02-SUMMARY.md diff --git a/.paul/changelog/2026-04-20.md b/.paul/changelog/2026-04-20.md new file mode 100644 index 0000000..c9b6097 --- /dev/null +++ b/.paul/changelog/2026-04-20.md @@ -0,0 +1,25 @@ +# 2026-04-20 + +## Co zrobiono + +- [Phase 13, Plan 02] Pakiety ochronne SOFT + PREMIUM — zarządzanie cenami/dobę w panelu WP (wp-admin → Rezerwacje → Pakiety ochronne) +- REST endpoint GET /carei/v1/protection-packages (public, filtr active) +- Radio cards w modalu rezerwacji nad sekcją pozycji API z wizualnym separatorem +- Fix HTTP 400 Softra: pakiet wyłączony z priceItems, doklejany do comments booking oraz dorysowany w summary frontend (details list + price table + totals) +- Zapis wybranego pakietu w post_meta _carei_protection_package + widoczność w meta boxie CPT carei_reservation +- Plan 13-01 oznaczony jako superseded (klient zmienił źródło danych na panel WP) +- Milestone v0.5 zamknięty (Phase 13 ✅ + Phase 14 ✅) + +## Zmienione pliki + +- `wp-content/plugins/carei-reservation/includes/class-admin-panel.php` +- `wp-content/plugins/carei-reservation/includes/class-rest-proxy.php` +- `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` +- `.paul/PROJECT.md` +- `.paul/ROADMAP.md` +- `.paul/STATE.md` +- `.paul/phases/13-protection-packages/13-01-SUMMARY.md` (superseded note) +- `.paul/phases/13-protection-packages/13-02-PLAN.md` +- `.paul/phases/13-protection-packages/13-02-SUMMARY.md` diff --git a/.paul/governance/governance_2026-04-20.jsonl b/.paul/governance/governance_2026-04-20.jsonl new file mode 100644 index 0000000..1c27ce5 --- /dev/null +++ b/.paul/governance/governance_2026-04-20.jsonl @@ -0,0 +1,42 @@ +{"ts":"2026-04-19T22:25:36Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\13-protection-packages\\\\13-02-PLAN.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:25:54Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:26:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:26:15Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\PROJECT.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:28:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:28:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:28:22Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:28:28Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-admin-panel.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:28:33Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-rest-proxy.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:28:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\includes\\\\class-rest-proxy.php","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:28:48Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/includes/class-admin-panel.php\" && php -l \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/pl","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:29:24Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\css\\\\carei-reservation.css","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:29:29Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/includes/class-elementor-widget.php\"\",\"description\":\"PHP lint widget\"},\"tool_response\":{\"stdout\"","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:30:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:30:16Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:30:37Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:30:57Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:31:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:31:29Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:31:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:31:46Z","tool":"Bash","cmd":"node --check \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/assets/js/carei-reservation.js\" 2>&1 && echo \"JS syntax OK\"\",\"description\":\"Node syntax check","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:34:07Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:34:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:34:18Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:34:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\css\\\\carei-reservation.css","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:34:37Z","tool":"Bash","cmd":"node --check \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/assets/js/carei-reservation.js\" && echo \"OK\"\",\"description\":\"JS syntax check\"},\"tool_respon","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:35:27Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\css\\\\carei-reservation.css","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:36:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:36:10Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\css\\\\carei-reservation.css","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:36:14Z","tool":"Bash","cmd":"node --check \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/assets/js/carei-reservation.js\" && echo OK\",\"description\":\"JS syntax check\"},\"tool_response\"","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:37:21Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:37:32Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:37:40Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:37:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:38:03Z","tool":"Bash","cmd":"node --check \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/assets/js/carei-reservation.js\" && echo OK\",\"description\":\"JS syntax check\"},\"tool_response\"","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:40:22Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\wp-content\\\\plugins\\\\carei-reservation\\\\assets\\\\js\\\\carei-reservation.js","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:40:26Z","tool":"Bash","cmd":"node --check \"C:/visual studio code/projekty/carei.pagedev.pl/wp-content/plugins/carei-reservation/assets/js/carei-reservation.js\" && echo OK\",\"description\":\"JS syntax check\"},\"tool_response\"","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:43:15Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\13-protection-packages\\\\13-02-SUMMARY.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:43:28Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\phases\\\\13-protection-packages\\\\13-01-SUMMARY.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:43:45Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\changelog\\\\2026-04-20.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:44:08Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\carei.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} +{"ts":"2026-04-19T22:45:06Z","tool":"Bash","cmd":"git -C \"C:/visual studio code/projekty/carei.pagedev.pl\" status --short\",\"description\":\"Git status\"},\"tool_response\":{\"stdout\":\" M .paul/PROJECT.md\\n M .paul/ROADMAP.md\\n M .paul/STATE.m","cwd":"/c/visual studio code/projekty/carei.pagedev.pl"} diff --git a/.paul/phases/13-protection-packages/13-01-SUMMARY.md b/.paul/phases/13-protection-packages/13-01-SUMMARY.md new file mode 100644 index 0000000..c21d73e --- /dev/null +++ b/.paul/phases/13-protection-packages/13-01-SUMMARY.md @@ -0,0 +1,21 @@ +--- +phase: 13-protection-packages +plan: 01 +status: superseded +superseded_by: 13-02-PLAN.md +completed: 2026-04-20 +--- + +# Phase 13 Plan 01: Superseded + +Plan 13-01 (pricing progowy min/doba/max z Softra API) **nie został wdrożony** — klient po audycie wybrał prostsze źródło danych: stała cena/dobę zarządzana w panelu WP. + +**Supersession decision (2026-04-20):** +- Plan 13-01 zakładał 3 progi cenowe pobierane z `additionalItems` API Softra +- Klient potwierdził, że ceny pakietów ochronnych mają być zarządzane w WP (nie wyeksponowane w Softra) +- Uproszczenie: jedna cena/dobę × liczba dób + +**Plan wdrażający wymagania klienta:** [13-02-PLAN.md](./13-02-PLAN.md) — ukończony, SUMMARY: [13-02-SUMMARY.md](./13-02-SUMMARY.md). + +--- +*Phase: 13-protection-packages, Plan: 01 (superseded)* diff --git a/.paul/phases/13-protection-packages/13-02-PLAN.md b/.paul/phases/13-protection-packages/13-02-PLAN.md new file mode 100644 index 0000000..58c3b5d --- /dev/null +++ b/.paul/phases/13-protection-packages/13-02-PLAN.md @@ -0,0 +1,317 @@ +--- +phase: 13-protection-packages +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: + - wp-content/plugins/carei-reservation/includes/class-admin-panel.php + - wp-content/plugins/carei-reservation/includes/class-rest-proxy.php + - 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 +autonomous: false +delegation: off +--- + + +## Goal +Dodać dwa stałe pakiety ochronne (SOFT, PREMIUM) w sekcji "Pakiety ochronne" modalu rezerwacji, z ceną za dobę zarządzaną w panelu administratora WordPress. Kalkulacja dzienna (price × liczba_dób). Wizualne oddzielenie od pozostałych pozycji API w tej sekcji. + +## Purpose +Klient potwierdził, że źródłem danych cenowych dla pakietów ochronnych jest panel WP (nie API Softra). Stary plan 13-01 (pricing progowy min/doba/max z Softra) został odrzucony. Nowe wymaganie upraszcza model: stała cena/dobę × liczba dób rezerwacji, zarządzana przez admina w WP. + +## Output +- Nowa podstrona w menu "Rezerwacje" → "Pakiety ochronne" (name, price/doba, aktywne, opis dla SOFT i PREMIUM) +- Endpoint `GET /carei/v1/protection-packages` dla frontendu +- Dwa kafelki w modalu nad/pod istniejącymi opcjami "Pakiety ochronne" z delikatnym separatorem +- Cena wyświetlana: "X zł/doba" + wyliczony total dla wybranych dat +- Wybrany pakiet uwzględniony w `priceItems` booking submission i zapisany w CPT + + + +@.paul/PROJECT.md +@.paul/ROADMAP.md +@.paul/STATE.md + +## Source Files +@wp-content/plugins/carei-reservation/includes/class-admin-panel.php +@wp-content/plugins/carei-reservation/includes/class-rest-proxy.php +@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 + +## Prior Work +Plan 13-01 (BLOCKED) zakładał pricing progowy z pola `additionalItems` Softra API — **odrzucony**. Ten plan (13-02) zastępuje 13-01. Zbiór SCOPE LIMITS odwraca się: backend+admin SĄ w zakresie, pricing tierowy poza zakresem. + + + + +## AC-1: Panel administratora — konfiguracja pakietów +```gherkin +Given admin otwiera menu "Rezerwacje" w WP +When klika podstronę "Pakiety ochronne" +Then widzi formularz z dwoma sekcjami (SOFT i PREMIUM) zawierającymi: + - nazwa wyświetlana (text, default "Ubezpieczenie SOFT" / "Ubezpieczenie PREMIUM") + - cena za dobę (number, min 0, step 0.01) + - aktywne (checkbox, default on) + - opis/zakres usług (textarea, opcjonalny) +And zapis formularza trwale przechowuje wartości w wp_options (carei_protection_packages) +``` + +## AC-2: Endpoint REST z danymi pakietów +```gherkin +Given w panelu admina są zapisane pakiety +When frontend wywołuje GET /wp-json/carei/v1/protection-packages +Then endpoint zwraca JSON { soft: {name, pricePerDay, active, description}, premium: {...} } +And zwraca tylko pakiety z active=true (nieaktywne pomijane lub flagowane jako inactive) +``` + +## AC-3: Render kafelków w modalu z separatorem +```gherkin +Given otwarto modal rezerwacji, wybrano segment + oddział + daty +When sekcja "Pakiety ochronne" renderuje się +Then nad listą opcji API pojawiają się 2 kafelki SOFT + PREMIUM (tylko te z active=true) +And kafelki są oddzielone delikatną linią/marginesem od pozostałych pozycji "Pakiety ochronne" pochodzących z pricelist API +And każdy kafelek pokazuje: nazwę, cenę "X zł/doba", wyliczony total "= Y zł" dla aktualnej liczby dób, opis (jeśli ustawiony) +``` + +## AC-4: Wybór i dynamiczne przeliczanie +```gherkin +Given użytkownik widzi oba kafelki pakietów +When klika SOFT +Then SOFT jest zaznaczony (radio-style), PREMIUM odznaczony +When następnie klika PREMIUM +Then SOFT jest odznaczony, PREMIUM zaznaczony +When klika ponownie zaznaczony pakiet +Then zostaje odznaczony (dozwolony brak wybranego pakietu) +When zmienia daty rezerwacji +Then wyliczony total (price × days) aktualizuje się automatycznie na obu kafelkach +``` + +## AC-5: Pakiet w booking submission i CPT +```gherkin +Given użytkownik wybrał pakiet SOFT +When klika "Pokaż podsumowanie" → "Zarezerwuj" +Then wybrany pakiet jest dołączony do priceItems w zapytaniu do Softra (price, priceBeforeDiscount, name, quantity=days) +And po zapisie rezerwacji widoczny w CPT carei_reservation w polu "extras" lub dedykowanym polu meta +And w summary overlay pojawia się linia "Ubezpieczenie SOFT — Y zł" +``` + + + + + + + Task 1: Backend — admin settings page + REST endpoint + wp-content/plugins/carei-reservation/includes/class-admin-panel.php, wp-content/plugins/carei-reservation/includes/class-rest-proxy.php + + A. W `class-admin-panel.php`: + - Dodać metodę `register_protection_packages_page()` hookowaną przez `admin_menu`: `add_submenu_page('edit.php?post_type=carei_reservation', 'Pakiety ochronne', 'Pakiety ochronne', 'manage_options', 'carei-protection-packages', [$this, 'render_protection_packages_page'])` + - Dodać `render_protection_packages_page()`: formularz POST z nonce `carei_protection_packages`, dwie sekcje (SOFT, PREMIUM) z polami: `name`, `pricePerDay`, `active`, `description`. + - Dodać obsługę POST: walidacja nonce + `manage_options`, sanityzacja (`sanitize_text_field` dla name, `floatval` + clamp >=0 dla price, `boolean` checkbox, `sanitize_textarea_field`), zapis do `update_option('carei_protection_packages', $data)`. + - Defaulty (przy pierwszym wczytaniu / pustym option): + `soft = { name: 'Ubezpieczenie SOFT', pricePerDay: 0, active: true, description: '' }` + `premium = { name: 'Ubezpieczenie PREMIUM', pricePerDay: 0, active: true, description: '' }` + - Dodać helper statyczny `public static function get_protection_packages()` zwracający aktualną tablicę (merge z defaultami). + - Dodatkowo dopisać obsługę zapisu wybranego pakietu do post_meta w `save_reservation()` (jeśli `$data['protectionPackage']` podane — zapisać jako `_carei_protection_package` JSON: `{key, name, pricePerDay, days, total}`) oraz wyświetlić w meta boxie pod sekcją extras. + + B. W `class-rest-proxy.php`: + - Zarejestrować nowy route w `register_routes()`: + ```php + register_rest_route( self::NAMESPACE, '/protection-packages', array( + 'methods' => 'GET', + 'callback' => array( $this, 'get_protection_packages' ), + 'permission_callback' => '__return_true', + ) ); + ``` + - Dodać metodę `get_protection_packages()`: odczyt `Carei_Admin_Panel::get_protection_packages()`, filtrowanie tylko `active === true`, zwrot jako `rest_ensure_response({ soft: {...}|null, premium: {...}|null })`. + + Avoid: Nie tworzyć custom table — wystarczy wp_options (spójne z MVP). Nie dodawać JS-a do panelu admina (formularz statyczny, native WP). Nie przenosić `save_reservation()` logic — tylko dopisać pole. + + + - `wp-admin/edit.php?post_type=carei_reservation&page=carei-protection-packages` — strona renderuje formularz + - Zmiana ceny + zapis → komunikat "Zapisano" i wartości persystują po reload + - `curl https://carei.pagedev.pl/wp-json/carei/v1/protection-packages` → JSON z oboma pakietami + - Ustawienie SOFT inactive → endpoint zwraca `soft: null` (lub bez klucza) + + AC-1, AC-2 satisfied: Admin może edytować pakiety, endpoint REST eksponuje dane. + + + + Task 2: Frontend HTML — kontener kafelków pakietów z separatorem + wp-content/plugins/carei-reservation/includes/class-elementor-widget.php, wp-content/plugins/carei-reservation/assets/css/carei-reservation.css + + A. W `class-elementor-widget.php`, w sekcji `
Pakiety ochronne
`: + - BEZPOŚREDNIO nad istniejącym `
` dodać: + ```html +
+ +
+ + ``` + - Pozostawić istniejący `#carei-insurance-container` bez zmian (nadal renderuje pozycje z API Softra, jeśli są). + + B. W `carei-reservation.css`: + - `.carei-form__row--protection-packages` — grid/flex z 2 kolumnami (desktop), wrap na mobile. + - `.carei-form__protection-package` — karta: padding, border 2px solid transparent, border-radius 12px, background #f8f8fb, cursor pointer, tranzycja. + - `.carei-form__protection-package.is-selected` — border #2F2482, background #fff z subtelnym shadow. + - `.carei-form__protection-package__name` — bold, color #2F2482. + - `.carei-form__protection-package__price` — większy font, strong. + - `.carei-form__protection-package__unit` — mały font szary "/doba". + - `.carei-form__protection-package__total` — color #FF0000 dla total. + - `.carei-form__protection-package__desc` — mały font, szary. + - `.carei-form__protection-divider` — margin-top/bottom, border-bottom 1px dashed rgba(47,36,130,0.15), height 0. Stanowi delikatny wizualny separator między pakietami WP a opcjami API. + - Responsive: `@media (max-width: 640px)` → kolumna, pełna szerokość. + + Avoid: Nie ruszać istniejącego `#carei-insurance-container` ani `.carei-form__extra-card` (służą pozostałym pozycjom API i extras). + + + - W edytorze Elementor / na stronie: DOM zawiera nowy `#carei-protection-packages-container` nad `#carei-insurance-container` wewnątrz sekcji "Pakiety ochronne" + - Separator `.carei-form__protection-divider` widoczny w DOM + - Style zaczytywane — brak błędów w DevTools + + AC-3 satisfied (HTML skeleton + separator) — JS render w Task 3 + + + + Task 3: Frontend JS — render, radio behavior, dynamic recalculation, submission + wp-content/plugins/carei-reservation/assets/js/carei-reservation.js + + 1. **Wczytanie pakietów (przy init lub przy otwarciu modala):** + - Dodać stan modułu: `var protectionPackages = { soft: null, premium: null };` oraz `var selectedProtectionKey = null;`. + - Dodać referencję: `protectionContainer = document.getElementById('carei-protection-packages-container');` w sekcji `cacheDom()` (~linie 100-120). + - Dodać funkcję `loadProtectionPackages()` wywołującą `fetch('/wp-json/carei/v1/protection-packages')` (REST nie wymaga nonce dla GET). Po otrzymaniu: zapis do `protectionPackages`, wywołanie `renderProtectionPackages()`. + - Wywołać `loadProtectionPackages()` jednorazowo (np. w `init()` obok innych API calls, niezależnie od pricelist). + + 2. **Render `renderProtectionPackages()`:** + - Jeśli kontener nie istnieje — return. + - Wyczyścić container. + - Dla każdego klucza `['soft','premium']`, jeśli pakiet istnieje i `active`: + - Zbudować kafelek zgodnie z markup CSS z Task 2: + ``` + + ``` + - Podłączyć listener click do każdego kafelka: toggle `is-selected`, aktualizacja `selectedProtectionKey`, wywołanie `updateProtectionTotals()`. + - Wywołać `updateProtectionTotals()` bezpośrednio po render. + + 3. **`updateProtectionTotals()`:** + - Obliczyć `days` = liczba dób (wyznaczyć z dat `#carei-date-from` + `#carei-date-to` — użyć istniejącej funkcji/logiki; jeśli brak helper — obliczyć: `Math.max(1, Math.ceil((dateTo - dateFrom) / 86400000))`). + - Dla każdego kafelka: aktualizacja `[data-role="total"]` → `= {price*days} zł`. + - Jeśli daty nie są jeszcze ustawione (days === NaN) — total pusty. + + 4. **Hook w zmianę dat:** + - Zlokalizować istniejący handler `change`/`input` na polach `#carei-date-from` i `#carei-date-to` (lub common `onDateChange()`) i dodać wywołanie `updateProtectionTotals()`. Alternatywnie dodać niezależne listenery. + + 5. **Radio deselect behavior:** + - W click handlerze: jeśli `selectedProtectionKey === clickedKey` → odznacz (pusty stan, `selectedProtectionKey = null`). Inaczej ustaw jako wybrany, odznacz drugi kafelek. + + 6. **Submission + summary:** + - W `getSelectedExtrasForApi()` (~linia 849): po zebraniu extras dodać warunkowo wybrany pakiet: + ``` + if (selectedProtectionKey && protectionPackages[selectedProtectionKey]) { + var pkg = protectionPackages[selectedProtectionKey]; + var days = calculateDays(); + var total = pkg.pricePerDay * days; + extrasArr.push({ + id: 'protection_' + selectedProtectionKey, + name: pkg.name, + price: total, + priceBeforeDiscount: total, + quantity: days, + unit: 'doba' + }); + } + ``` + - W payloadzie do `makeBooking`: dodać pole `protectionPackage: { key, name, pricePerDay, days, total }` (obok `priceItems`) — admin zapisze w meta. + + Avoid: Nie modyfikować `buildExtraCard` ani logiki `extras[]` API Softra. Nie renderować pakietów jeśli `pricePerDay === 0` (opcjonalne — dopisać validation w admin, ale render sam skip tych z 0 jest też ok). + + + - Network tab: request do `/carei/v1/protection-packages` zwraca dane + - Modal: w sekcji "Pakiety ochronne" widoczne 2 kafelki SOFT+PREMIUM nad separatorem + - Kliknięcie SOFT: `is-selected`, kliknięcie PREMIUM: SOFT traci selekcję, PREMIUM dostaje + - Kliknięcie zaznaczonego pakietu: odznaczenie + - Zmiana dat 3→7 dni: total pakietu aktualizuje się (price × 7) + - Wybór pakietu → podsumowanie → payload `priceItems` zawiera pakiet, `protectionPackage` przekazany do backendu + - Zapisana rezerwacja w CPT zawiera wybrany pakiet w meta boxie + + AC-3, AC-4, AC-5 satisfied: Kafelki działają w pełnym cyklu (render → wybór → kalkulacja → submission → zapis). + + + + + Panel admina "Pakiety ochronne" (wp-admin), endpoint REST, kafelki SOFT/PREMIUM w modalu z separatorem od opcji API, radio selection z deselect, dynamiczne przeliczanie price×days, pakiet w booking submission + zapis w CPT. + + + 1. **Admin:** Zaloguj się do WP → menu "Rezerwacje" → "Pakiety ochronne". + - Ustaw SOFT: nazwa "Ubezpieczenie SOFT", cena 25 zł/dobę, aktywne, opis krótki + - Ustaw PREMIUM: nazwa "Ubezpieczenie PREMIUM", cena 50 zł/dobę, aktywne, opis krótki + - Zapisz, przeładuj — wartości zostały + 2. **Frontend:** Otwórz carei.pagedev.pl → kliknij przycisk rezerwacji. + - Wybierz segment, oddział, daty (3 dni) + - Sekcja "Pakiety ochronne" — widzisz u góry 2 kafelki SOFT+PREMIUM, pod nimi linia separatora, następnie pozostałe opcje API (jeśli są) + - SOFT pokazuje "25 zł/doba = 75 zł", PREMIUM "50 zł/doba = 150 zł" + 3. **Interakcja:** Kliknij SOFT → podświetlony. Kliknij PREMIUM → SOFT traci selekcję, PREMIUM zaznaczony. Kliknij PREMIUM ponownie → odznaczony. + 4. **Recalc:** Zmień daty na 7 dni → totale: SOFT 175 zł, PREMIUM 350 zł. + 5. **Submit:** Wybierz SOFT, dokończ flow → podsumowanie pokazuje "Ubezpieczenie SOFT — 175 zł" jako pozycję. Potwierdź rezerwację. + 6. **CPT:** wp-admin → Rezerwacje → otwórz nową pozycję → meta box zawiera informację o wybranym pakiecie. + 7. **Inactive test:** Ustaw SOFT active=false w adminie → modal pokazuje tylko PREMIUM. + 8. **Mobile (360×640):** Kafelki stackują się pionowo, czytelne, klikalne. + + Napisz "approved" aby kontynuować do UNIFY, lub opisz problemy do naprawy. + + + + + + +## DO NOT CHANGE +- `class-softra-api.php` — klient Softra API bez zmian +- `buildExtraCard()` / `#carei-extras-container` / `input[name="extras[]"]` — logika regular extras pozostaje +- `#carei-insurance-container` (istniejąca logika filtrowania pozycji ubezpieczeniowych z pricelist API) — pozostaje jako fallback dla pozycji z Softra, gdyby były +- Sekcja "Wyjazd zagraniczny" i "Opcje dodatkowe" +- Hero search form widget +- Flow booking (customer/add → makebooking → confirm) poza dołączeniem pakietu do payload + +## SCOPE LIMITS +- Brak pricing progowego (min/per-day/max) — ten pomysł z 13-01 jest odrzucony +- Brak integracji z Softra API dla pakietów — dane tylko z panelu WP +- Brak tłumaczeń multi-lang — hardcoded polski (spójne z resztą pluginu) +- Brak custom DB table — wp_options wystarczy +- Brak więcej niż 2 pakietów w UI adminu — tylko SOFT i PREMIUM (fixed) +- Brak refaktoryzacji `getSelectedExtrasForApi()` — tylko dopisanie warunkowej gałęzi + + + + +Before declaring plan complete: +- [ ] Panel admina "Pakiety ochronne" dostępny i zapisuje dane do wp_options +- [ ] `GET /wp-json/carei/v1/protection-packages` zwraca poprawny JSON (z filtrem active) +- [ ] Modal renderuje 2 kafelki nad istniejącym kontenerem insurance, z separatorem +- [ ] Radio behavior: exclusive + deselect działają +- [ ] Total przelicza się przy zmianie dat +- [ ] Wybrany pakiet dołączany do priceItems w payloadzie booking +- [ ] Pakiet widoczny w summary overlay i w CPT carei_reservation +- [ ] Inactive pakiet ukryty w UI +- [ ] Brak regresji w extras, abroad, reszcie formularza +- [ ] Responsywność mobile (< 640px) działa +- [ ] Wszystkie AC-1 – AC-5 spełnione + + + +- Wszystkie taski zakończone +- Wszystkie checki weryfikacji przechodzą +- Brak błędów w konsoli przeglądarki i logach PHP +- Human-verify checkpoint zatwierdzony + + + +After completion, create `.paul/phases/13-protection-packages/13-02-SUMMARY.md` + diff --git a/.paul/phases/13-protection-packages/13-02-SUMMARY.md b/.paul/phases/13-protection-packages/13-02-SUMMARY.md new file mode 100644 index 0000000..b4f0c16 --- /dev/null +++ b/.paul/phases/13-protection-packages/13-02-SUMMARY.md @@ -0,0 +1,173 @@ +--- +phase: 13-protection-packages +plan: 02 +subsystem: ui +tags: [wordpress, rest-api, elementor, admin-panel, vanilla-js, insurance] + +requires: + - phase: 05-admin-panel + provides: CPT carei_reservation, save_reservation() static, meta box infrastructure + - phase: 03-booking-flow + provides: booking payload pipeline, priceItems convention, summary overlay + +provides: + - Panel admina "Pakiety ochronne" (wp-admin → Rezerwacje → Pakiety ochronne) + - REST endpoint GET /carei/v1/protection-packages (public) + - Radio cards SOFT/PREMIUM w modalu z wizualnym separatorem od pozycji API + - Integracja z summary overlay (details list + price table + totals) + - Zapis wybranego pakietu w post_meta _carei_protection_package + - Info o pakiecie doklejane do comments booking (rezerwacja Softra widzi pakiet w uwagach) + +affects: [future-insurance-adjustments, pricing-customizations, admin-ux] + +tech-stack: + added: [] + patterns: + - "WP options + REST proxy jako źródło danych niezależne od Softra" + - "Hybrydowe podsumowanie: pozycje z Softra + lokalne dodatki łączone w summary overlay" + - "Rozszerzenie comments Softra o strukturalne info pakietu (fallback dla braku dedicated field)" + +key-files: + created: + - ".paul/phases/13-protection-packages/13-02-PLAN.md" + - ".paul/phases/13-protection-packages/13-02-SUMMARY.md" + modified: + - "wp-content/plugins/carei-reservation/includes/class-admin-panel.php" + - "wp-content/plugins/carei-reservation/includes/class-rest-proxy.php" + - "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" + +key-decisions: + - "Dane pakietów w wp_options (opcja carei_protection_packages) — nie CPT ani custom table" + - "Pakiet poza Softra priceItems — HTTP 400 przy próbie wysłania fałszywych ID" + - "Pakiet w comments booking + osobny protectionPackage w payloadzie dla CPT" + - "Summary frontend dokłada wiersz pakietu i oblicza grandTotal = softraGross + protectionTotal" + - "Plan 13-01 superseded — pricing progowy odrzucony przez klienta na rzecz prostej ceny/dobę z WP" + +patterns-established: + - "REST route dla danych WP-managed: __return_true permission (publiczne read-only)" + - "Filtrowanie active=true po stronie endpointu, frontend dostaje tylko aktywne" + - "Radio z deselect przez JS click handler (natywne radio nie odznacza)" + - "accent-color: var(--carei-blue) dla natywnych radio spójnych z design systemem" + +duration: ~3h +started: 2026-04-20T12:00:00Z +completed: 2026-04-20T15:30:00Z +--- + +# Phase 13 Plan 02: Pakiety ochronne SOFT+PREMIUM — Summary + +**Dwa pakiety ochronne z ceną/doba zarządzane w panelu WP (wp_options), renderowane jako radio cards w modalu nad opcjami API, doliczane lokalnie w summary i przesyłane do Softra przez comments + lokalny meta CPT.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~3h | +| Started | 2026-04-20T12:00:00Z | +| Completed | 2026-04-20T15:30:00Z | +| Tasks | 3 auto + 1 checkpoint (human-verify approved) | +| Files modified | 5 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Panel administratora — konfiguracja pakietów | ✅ Pass | Submenu "Pakiety ochronne" w edit.php?post_type=carei_reservation, formularz z nazwą/ceną/aktywnym/opisem dla SOFT+PREMIUM, zapis w carei_protection_packages | +| AC-2: Endpoint REST z danymi pakietów | ✅ Pass | GET /wp-json/carei/v1/protection-packages zwraca {soft, premium} z filtrem active=true | +| AC-3: Render kafelków z separatorem | ✅ Pass | #carei-protection-packages-container nad #carei-insurance-container, .carei-form__protection-divider oddziela wizualnie | +| AC-4: Wybór i dynamiczne przeliczanie | ✅ Pass (zmieniono UX) | Radio z deselect działa; sumowanie × days wyłączone w kafelku (decyzja UX z klientem — tylko "X zł/doba" szare po prawej, total dopiero w summary) | +| AC-5: Pakiet w booking submission i CPT | ✅ Pass (rozwiązanie hybrydowe) | Pakiet w comments booking + protectionPackage w payloadzie + _carei_protection_package meta CPT + wiersz w summary overlay (details list + price table + totals) | + +## Accomplishments + +- **Zarządzanie niezależne od Softra** — admin może zmieniać ceny pakietów bez dotykania API wynajmowanego przez zewnętrznego dostawcę +- **Rozwiązanie konfliktu API** — po pierwszej próbie wysłania pakietów jako `priceItems` Softra zwracała HTTP 400; refactor w locie: pakiet wyłączony z priceItems, doklejony do comments + zobaczony w summary po stronie frontu +- **Spójność wizualna** — kafelki pakietów identyczne stylem z `.carei-form__extra-card` (nazwa blue, cena szara po prawej, bold, natywny radio z accent-color) +- **Pełny cykl życia rezerwacji** — pakiet od momentu wyboru (radio) → comments Softra → CPT meta → meta box admina → wyświetlenie w formacie "nazwa — cena/doba × dni = total" + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `.paul/phases/13-protection-packages/13-02-PLAN.md` | Created | Plan zastępujący 13-01 (nowe założenia klienta) | +| `.paul/phases/13-protection-packages/13-02-SUMMARY.md` | Created | Ten dokument | +| `wp-content/plugins/carei-reservation/includes/class-admin-panel.php` | Modified | Submenu page, handler POST, get_protection_packages() static, rozszerzenie save_reservation() o protection_package meta, wiersz w meta boxie | +| `wp-content/plugins/carei-reservation/includes/class-rest-proxy.php` | Modified | Rejestracja GET /protection-packages, callback get_protection_packages() z filtrem active | +| `wp-content/plugins/carei-reservation/includes/class-elementor-widget.php` | Modified | Nowy #carei-protection-packages-container + .carei-form__protection-divider nad #carei-insurance-container | +| `wp-content/plugins/carei-reservation/assets/js/carei-reservation.js` | Modified | protectionPackages state, loadProtectionPackages(), renderProtectionPackages() z radio + deselect, getSelectedProtectionPayload(), buildBookingComments(), integracja w showSummaryOverlay (details + table + totals) | +| `wp-content/plugins/carei-reservation/assets/css/carei-reservation.css` | Modified | Style kafelków pakietów (flex row, name blue, price gray right, radio 16×16 accent-color, separator dashed), mobile stack | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Pakiet NIE w priceItems Softra | HTTP 400 przy próbie wysłania fałszywego ID — Softra waliduje wobec swojej bazy | Wymagane osobne kanały: comments (widoczne pracownikom wypożyczalni), protectionPackage payload (zapis WP), summary UI (widoczne klientowi) | +| UX: brak totala × days w kafelku | Klient poprosił o spójność z extras (tylko "X zł/doba" szary po prawej), total dopiero w summary | Prostszy kafelek, `updateProtectionTotals()` usunięty jako martwy kod, listenery dat oczyszczone | +| Native radio + accent-color | Spójne z reszta formularza, user prosił o widoczny wybór | Minimalny CSS, natywna accessibility, JS tylko dla deselect | +| Plan 13-01 superseded zamiast skasowany | Historia decyzji klienta zachowana dla przyszłego audytu | 13-01-PLAN.md pozostaje w repo z notatką supersession w ROADMAP | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 2 | Konieczne korekty po błędach runtime | +| Scope additions | 1 | Feedback UX w trakcie: prostszy kafelek | +| Deferred | 0 | — | + +**Total impact:** Wszystkie deviations wynikły z feedback usera w trakcie APPLY (3 iteracje UI + 1 fix błędu API). Żadna nie wychodziła poza cel planu. + +### Auto-fixed Issues + +**1. [API] HTTP 400 z Softra przy wysyłaniu pakietu jako priceItem** +- **Found during:** Task 3 (frontend JS) po wdrożeniu i kliknięciu "Pokaż podsumowanie" +- **Issue:** `priceItems` zawierał `id: 'protection_soft'` — Softra nie zna tego ID i zwracała 400 Bad Request +- **Fix:** Usunięto pakiet z `getSelectedExtrasForApi()`, dodano jako osobną linię w `comments` booking (`buildBookingComments()`) oraz jako wiersz w summary frontend (details list + price table + totals), grandGross = softraGross + protectionTotal +- **Files:** `carei-reservation.js` +- **Verification:** User potwierdził brak błędu + widoczność pakietu w summary + +**2. [UX] Pakiet niewidoczny w liście "Wybrane opcje" summary** +- **Found during:** Weryfikacja po fixie HTTP 400 +- **Issue:** `summaryDetails` iterował po `selectedExtras` z `getSelectedExtrasForApi()`, który nie zawiera już pakietu +- **Fix:** Dodany warunkowy wiersz w liście używający `getSelectedProtectionPayload()` z formatem analogicznym do extras (`X zł/doba × N = total zł`) +- **Files:** `carei-reservation.js` +- **Verification:** User potwierdził widoczność + +### Scope Additions + +**1. [UX] Uproszczenie kafelka (usunięcie total × days z popupu)** +- User request podczas APPLY: "pokazuj tak jak ceny pozostałych elementów, czyli po prawej wszystko szarym kolorem i bez sumowania za liczbę dni, to dopiero w kolejnym widoku podsumowania" +- Efekt: `updateProtectionTotals()` usunięte jako martwy kod, listenery dat oczyszczone, CSS kafelków dostosowany do wzorca `.carei-form__extra-card` + +### Deferred Items + +None. + +## Issues Encountered + +| Issue | Resolution | +|-------|------------| +| Softra HTTP 400 | Refactor w locie — pakiet wyłączony z priceItems, dostarczany przez comments + meta + summary UI (patrz Auto-fixed #1) | +| Radio niewidoczny (pierwotnie hidden) | User request: "brakuje checkboxa wyboru wersji ubezpieczenia" → native radio w row z accent-color | +| Cena nie pogrubiona | User request → usunięcie override `font-weight: 400` na `.carei-form__protection-package__price strong` | + +## Next Phase Readiness + +**Ready:** +- Milestone v0.5 zamykany (Phase 13 ✅ + Phase 14 ✅ = 100%) +- Wzorzec "WP-managed dane + REST public endpoint + integracja z summary" gotowy do reuse w kolejnych niezależnych od Softra customizacjach +- Admin ma pełną kontrolę nad cenami bez wymagań modyfikacji Softra + +**Concerns:** +- VAT dla pakietu traktowany jako brutto bez rozbicia netto/VAT — wystarczy dla MVP, ale jeśli klient będzie wymagał księgowego rozbicia, trzeba dodać `vatRate` w adminie i kalkulację +- Comments Softra ma limit długości nieznany — w przyszłości rozważyć dedykowany field w integracji, jeśli Softra doda taki + +**Blockers:** +- None + +--- +*Phase: 13-protection-packages, Plan: 02* +*Completed: 2026-04-20* 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 a04ecab..7c6fdb7 100644 --- a/wp-content/plugins/carei-reservation/assets/css/carei-reservation.css +++ b/wp-content/plugins/carei-reservation/assets/css/carei-reservation.css @@ -597,6 +597,89 @@ button.carei-reservation-trigger:hover { color: inherit; } +/* ═══════════════════════════════════════════ + Protection Packages (SOFT / PREMIUM — zarządzane w panelu WP) + ═══════════════════════════════════════════ */ +.carei-form__row--protection-packages { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} +.carei-form__protection-package { + display: flex; + flex-direction: column; + gap: 6px; + padding: 16px; + border: 1px solid var(--carei-border); + border-radius: var(--carei-radius); + background: var(--carei-white); + cursor: pointer; + transition: border-color 0.2s; + min-width: 0; + overflow: hidden; +} +.carei-form__protection-package:hover { + border-color: rgba(47, 36, 130, 0.4); +} +.carei-form__protection-package.is-selected { + border-color: var(--carei-blue); +} +.carei-form__protection-package__row { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} +.carei-form__protection-package__input { + flex-shrink: 0; + width: 16px; + height: 16px; + accent-color: var(--carei-blue); + cursor: pointer; + margin: 0; +} +.carei-form__protection-package__name { + font-weight: 600; + font-size: 15px; + color: var(--carei-blue); + flex: 1; + min-width: 0; + word-break: break-word; +} +.carei-form__protection-package__price { + font-weight: 400; + font-size: 14px; + color: #505050; + margin-left: auto; + white-space: nowrap; + flex-shrink: 0; +} +.carei-form__protection-package__price strong { + color: inherit; +} +.carei-form__protection-package__desc { + font-size: 12px; + color: #666; + line-height: 1.4; +} +.carei-form__protection-package__desc:empty { + display: none; +} +.carei-form__protection-divider { + margin: 14px 0; + border-top: 1px dashed rgba(47, 36, 130, 0.18); + height: 0; +} +.carei-form__row--protection-packages:empty + .carei-form__protection-divider { + display: none; +} + +@media (max-width: 640px) { + .carei-form__row--protection-packages { + grid-template-columns: 1fr; + } +} + .carei-form__checkbox-label--abroad { align-items: center; font-weight: 600; 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 8d6bc67..a8d94b1 100644 --- a/wp-content/plugins/carei-reservation/assets/js/carei-reservation.js +++ b/wp-content/plugins/carei-reservation/assets/js/carei-reservation.js @@ -83,6 +83,9 @@ var summaryOverlay, summaryDetails, summaryTable, summaryTotal, summaryError; var summaryBack, summaryConfirm; var successView, successNumber, successClose; + var protectionContainer; + var protectionPackages = { soft: null, premium: null }; + var selectedProtectionKey = null; function initRefs() { overlay = document.querySelector('[data-carei-modal]'); @@ -102,6 +105,7 @@ extrasWrapper = document.getElementById('carei-extras-wrapper'); extrasContainer = document.getElementById('carei-extras-container'); insuranceContainer = document.getElementById('carei-insurance-container'); + protectionContainer = document.getElementById('carei-protection-packages-container'); abroadSection = document.getElementById('carei-abroad-section'); abroadToggle = document.getElementById('carei-abroad-toggle'); abroadSearch = document.getElementById('carei-abroad-search'); @@ -419,6 +423,69 @@ daysCount.innerHTML = 'Wybrano: ' + diff + ' ' + (diff === 1 ? 'dzień' : 'dni') + ''; } + // ─── Protection Packages (WP-managed: SOFT, PREMIUM) ────────── + + function loadProtectionPackages() { + return fetch(REST_URL + 'protection-packages', { + credentials: 'same-origin' + }).then(function (r) { return r.ok ? r.json() : null; }) + .then(function (data) { + if (!data || typeof data !== 'object') return; + protectionPackages.soft = data.soft || null; + protectionPackages.premium = data.premium || null; + renderProtectionPackages(); + }).catch(function (err) { console.error('Failed to load protection packages:', err); }); + } + + function renderProtectionPackages() { + if (!protectionContainer) return; + protectionContainer.innerHTML = ''; + ['soft', 'premium'].forEach(function (key) { + var pkg = protectionPackages[key]; + if (!pkg) return; + var price = parseFloat(pkg.pricePerDay || 0); + var priceLabel = price > 0 ? price.toFixed(0) + ' zł/doba' : 'Gratis'; + var descHtml = pkg.description ? '' + escHtml(pkg.description) + '' : ''; + var card = document.createElement('label'); + card.className = 'carei-form__protection-package'; + card.setAttribute('data-key', key); + card.setAttribute('data-price', price); + card.innerHTML = + '' + + '' + + '' + escHtml(pkg.name || '') + '' + + '' + escHtml(priceLabel) + '' + + '' + + descHtml; + card.addEventListener('click', function (e) { + e.preventDefault(); + onProtectionCardClick(key); + }); + protectionContainer.appendChild(card); + }); + } + + function onProtectionCardClick(key) { + if (!protectionContainer) return; + if (selectedProtectionKey === key) { + selectedProtectionKey = null; + } else { + selectedProtectionKey = key; + } + var cards = protectionContainer.querySelectorAll('.carei-form__protection-package'); + cards.forEach(function (card) { + var k = card.getAttribute('data-key'); + var input = card.querySelector('input'); + if (k === selectedProtectionKey) { + card.classList.add('is-selected'); + if (input) input.checked = true; + } else { + card.classList.remove('is-selected'); + if (input) input.checked = false; + } + }); + } + // ─── Load Extras from Pricelist ─────────────────────────────── function loadExtras() { @@ -875,9 +942,33 @@ priceAfterDiscount: price }); }); + // UWAGA: pakiet ochronny (SOFT/PREMIUM) zarządzany w panelu WP + // NIE jest dołączany do priceItems — Softra nie zna tych ID (zwróciłaby HTTP 400). + // Jest liczony osobno w getSelectedProtectionPayload() i dodawany do podsumowania po stronie frontu. return items; } + function getSelectedProtectionPayload() { + if (!selectedProtectionKey || !protectionPackages[selectedProtectionKey]) return null; + var pkg = protectionPackages[selectedProtectionKey]; + var pricePerDay = parseFloat(pkg.pricePerDay || 0); + var days = getRentalDays(); + return { + key: selectedProtectionKey, + name: pkg.name, + pricePerDay: pricePerDay, + days: days, + total: pricePerDay * days + }; + } + + function buildBookingComments(userMessage) { + var pkg = getSelectedProtectionPayload(); + if (!pkg) return userMessage || ''; + var pkgLine = 'Pakiet ochronny: ' + pkg.name + ' — ' + pkg.pricePerDay.toFixed(2) + ' zł/doba × ' + pkg.days + ' = ' + pkg.total.toFixed(2) + ' zł (do doliczenia poza systemem)'; + return userMessage ? (pkgLine + '\n\n' + userMessage) : pkgLine; + } + // ─── Step Transitions ────────────────────────────────────────── function hideStep(el) { @@ -961,7 +1052,8 @@ // Selected extras var selectedExtras = getSelectedExtrasForApi(); - if (selectedExtras.length > 0) { + var pkgForDetails = getSelectedProtectionPayload(); + if (selectedExtras.length > 0 || pkgForDetails) { html += '
Wybrane opcje:
    '; selectedExtras.forEach(function (ex) { var totalPrice = ex.priceAfterDiscount * (ex.amount || 1); @@ -970,6 +1062,12 @@ : fmtPrice(totalPrice) + ' zł'; html += '
  • ' + escHtml(toSentenceCase(ex.name)) + ' — ' + priceInfo + '
  • '; }); + if (pkgForDetails) { + var pkgInfo = pkgForDetails.days > 1 + ? fmtPrice(pkgForDetails.pricePerDay) + ' zł/doba × ' + pkgForDetails.days + ' = ' + fmtPrice(pkgForDetails.total) + ' zł' + : fmtPrice(pkgForDetails.total) + ' zł'; + html += '
  • ' + escHtml(pkgForDetails.name) + ' — ' + pkgInfo + '
  • '; + } html += '
'; } @@ -980,6 +1078,8 @@ summaryDetails.innerHTML = html; } + var protectionPayload = getSelectedProtectionPayload(); + // Price table if (summaryTable && summary.pricelist) { var html = ''; @@ -991,16 +1091,30 @@ '' + ''; }); + if (protectionPayload) { + html += '' + + '' + + '' + + '' + + ''; + } html += '
NazwaIlośćNettoBrutto
' + fmtPrice(item.netValue) + '' + fmtPrice(item.grossValue) + '
' + escHtml(protectionPayload.name) + ' (do doliczenia)' + protectionPayload.days + ' doba' + fmtPrice(protectionPayload.total) + '
'; summaryTable.innerHTML = html; } // Totals if (summaryTotal) { - summaryTotal.innerHTML = + var softraGross = parseFloat(summary.totalGrossValue || 0); + var protectionTotal = protectionPayload ? protectionPayload.total : 0; + var grandGross = softraGross + protectionTotal; + var totalsHtml = '
Netto:' + fmtPrice(summary.totalNetValue) + '
' + - '
VAT:' + fmtPrice(summary.totalVatValue) + '
' + - '
Do zapłaty:' + fmtPrice(summary.totalGrossValue) + ' zł
'; + '
VAT:' + fmtPrice(summary.totalVatValue) + '
'; + if (protectionPayload) { + totalsHtml += '
Pakiet ochronny:' + fmtPrice(protectionPayload.total) + '
'; + } + totalsHtml += '
Do zapłaty:' + fmtPrice(grandGross) + ' zł
'; + summaryTotal.innerHTML = totalsHtml; } } @@ -1047,7 +1161,8 @@ phone: fd.phone, email: fd.email }], - comments: fd.message || '' + comments: buildBookingComments(fd.message || ''), + protectionPackage: getSelectedProtectionPayload() }; // Add agreement items @@ -1344,6 +1459,7 @@ initAbroad(); initSubmit(); initMap(); + loadProtectionPackages(); } /* ═══════════════════════════════════════════ diff --git a/wp-content/plugins/carei-reservation/includes/class-admin-panel.php b/wp-content/plugins/carei-reservation/includes/class-admin-panel.php index f849264..1d4d500 100644 --- a/wp-content/plugins/carei-reservation/includes/class-admin-panel.php +++ b/wp-content/plugins/carei-reservation/includes/class-admin-panel.php @@ -8,6 +8,8 @@ class Carei_Admin_Panel { const POST_TYPE = 'carei_reservation'; const META_PREFIX = '_carei_'; + const PROTECTION_OPTION = 'carei_protection_packages'; + private static $statuses = array( 'nowe' => array( 'label' => 'Nowe', 'color' => '#2F2482' ), 'przeczytane' => array( 'label' => 'Przeczytane', 'color' => '#f59e0b' ), @@ -24,6 +26,149 @@ class Carei_Admin_Panel { add_action( 'save_post_' . self::POST_TYPE, array( $this, 'save_meta_box' ), 10, 2 ); add_action( 'edit_form_after_title', array( $this, 'auto_mark_read' ) ); add_action( 'admin_head', array( $this, 'admin_styles' ) ); + add_action( 'admin_menu', array( $this, 'register_protection_packages_page' ) ); + add_action( 'admin_post_carei_save_protection_packages', array( $this, 'handle_protection_packages_save' ) ); + } + + // ─── Protection Packages (SOFT / PREMIUM) ──────────────────── + + public static function get_protection_packages_defaults() { + return array( + 'soft' => array( + 'name' => 'Ubezpieczenie SOFT', + 'pricePerDay' => 0, + 'active' => true, + 'description' => '', + ), + 'premium' => array( + 'name' => 'Ubezpieczenie PREMIUM', + 'pricePerDay' => 0, + 'active' => true, + 'description' => '', + ), + ); + } + + public static function get_protection_packages() { + $defaults = self::get_protection_packages_defaults(); + $stored = get_option( self::PROTECTION_OPTION, array() ); + if ( ! is_array( $stored ) ) { + $stored = array(); + } + $out = array(); + foreach ( $defaults as $key => $def ) { + $item = isset( $stored[ $key ] ) && is_array( $stored[ $key ] ) ? $stored[ $key ] : array(); + $out[ $key ] = array( + 'name' => isset( $item['name'] ) && $item['name'] !== '' ? (string) $item['name'] : $def['name'], + 'pricePerDay' => isset( $item['pricePerDay'] ) ? (float) $item['pricePerDay'] : (float) $def['pricePerDay'], + 'active' => isset( $item['active'] ) ? (bool) $item['active'] : (bool) $def['active'], + 'description' => isset( $item['description'] ) ? (string) $item['description'] : $def['description'], + ); + } + return $out; + } + + public function register_protection_packages_page() { + add_submenu_page( + 'edit.php?post_type=' . self::POST_TYPE, + 'Pakiety ochronne', + 'Pakiety ochronne', + 'manage_options', + 'carei-protection-packages', + array( $this, 'render_protection_packages_page' ) + ); + } + + public function render_protection_packages_page() { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( 'Brak uprawnień.' ); + } + $data = self::get_protection_packages(); + $saved = isset( $_GET['carei_saved'] ) && $_GET['carei_saved'] === '1'; + ?> +
+

Pakiety ochronne

+

Konfiguracja pakietów wyświetlanych w sekcji Pakiety ochronne formularza rezerwacji. Cena podawana jest za dobę — total = cena × liczba dób rezerwacji.

+ +

Zapisano.

+ +
+ + + + 'SOFT', 'premium' => 'PREMIUM' ) as $key => $label ) : + $pkg = $data[ $key ]; + ?> +
+

Pakiet

+ + + + + + + + + + + + + + + + + +
Status
+
+ + + +
+
+ + $def ) { + $raw = isset( $input[ $key ] ) && is_array( $input[ $key ] ) ? $input[ $key ] : array(); + $name = isset( $raw['name'] ) ? sanitize_text_field( wp_unslash( $raw['name'] ) ) : $def['name']; + $price = isset( $raw['pricePerDay'] ) ? (float) $raw['pricePerDay'] : 0; + if ( $price < 0 ) { $price = 0; } + $active = ! empty( $raw['active'] ); + $desc = isset( $raw['description'] ) ? sanitize_textarea_field( wp_unslash( $raw['description'] ) ) : ''; + $clean[ $key ] = array( + 'name' => $name !== '' ? $name : $def['name'], + 'pricePerDay' => $price, + 'active' => $active, + 'description' => $desc, + ); + } + update_option( self::PROTECTION_OPTION, $clean ); + + $redirect = add_query_arg( + array( + 'post_type' => self::POST_TYPE, + 'page' => 'carei-protection-packages', + 'carei_saved' => '1', + ), + admin_url( 'edit.php' ) + ); + wp_safe_redirect( $redirect ); + exit; } public function register_post_type() { @@ -190,8 +335,19 @@ class Carei_Admin_Panel { 'extras' => get_post_meta( $post->ID, self::META_PREFIX . 'extras', true ), 'comments' => get_post_meta( $post->ID, self::META_PREFIX . 'comments', true ), 'status' => get_post_meta( $post->ID, self::META_PREFIX . 'status', true ) ?: 'nowe', + 'protection' => get_post_meta( $post->ID, self::META_PREFIX . 'protection_package', true ), ); + $protection = $meta['protection'] ? json_decode( $meta['protection'], true ) : null; + $protection_str = ''; + if ( is_array( $protection ) && ! empty( $protection['name'] ) ) { + $name = isset( $protection['name'] ) ? $protection['name'] : ''; + $price = isset( $protection['pricePerDay'] ) ? (float) $protection['pricePerDay'] : 0; + $days = isset( $protection['days'] ) ? (int) $protection['days'] : 0; + $total = isset( $protection['total'] ) ? (float) $protection['total'] : ( $price * $days ); + $protection_str = sprintf( '%s — %s zł/doba × %d = %s zł', $name, number_format( $price, 2, ',', ' ' ), $days, number_format( $total, 2, ',', ' ' ) ); + } + $address = $meta['address'] ? json_decode( $meta['address'], true ) : null; $address_str = ''; if ( $address ) { @@ -235,6 +391,7 @@ class Carei_Admin_Panel { Adres
Opcje dodatkowe + Pakiet ochronny Wiadomość
@@ -401,6 +558,9 @@ class Carei_Admin_Panel { 'comments' => isset( $booking_data['comments'] ) ? $booking_data['comments'] : '', 'status' => 'nowe', 'raw_response' => wp_json_encode( $api_result ), + 'protection_package' => ( isset( $booking_data['protectionPackage'] ) && is_array( $booking_data['protectionPackage'] ) ) + ? wp_json_encode( $booking_data['protectionPackage'] ) + : '', ); foreach ( $meta as $key => $value ) { 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 1170ca2..66c7347 100644 --- a/wp-content/plugins/carei-reservation/includes/class-elementor-widget.php +++ b/wp-content/plugins/carei-reservation/includes/class-elementor-widget.php @@ -152,6 +152,10 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
Pakiety ochronne
+
+ +
+
diff --git a/wp-content/plugins/carei-reservation/includes/class-rest-proxy.php b/wp-content/plugins/carei-reservation/includes/class-rest-proxy.php index 1745bb7..121a29a 100644 --- a/wp-content/plugins/carei-reservation/includes/class-rest-proxy.php +++ b/wp-content/plugins/carei-reservation/includes/class-rest-proxy.php @@ -130,6 +130,13 @@ class Carei_REST_Proxy { 'callback' => array( $this, 'get_agreements' ), 'permission_callback' => '__return_true', ) ); + + // GET /protection-packages + register_rest_route( self::NAMESPACE, '/protection-packages', array( + 'methods' => 'GET', + 'callback' => array( $this, 'get_protection_packages' ), + 'permission_callback' => '__return_true', + ) ); } /** @@ -298,4 +305,20 @@ class Carei_REST_Proxy { } return $this->respond( $api->get_agreements() ); } + + public function get_protection_packages( WP_REST_Request $request ) { + $all = Carei_Admin_Panel::get_protection_packages(); + $out = array( 'soft' => null, 'premium' => null ); + foreach ( array( 'soft', 'premium' ) as $key ) { + if ( isset( $all[ $key ] ) && ! empty( $all[ $key ]['active'] ) ) { + $out[ $key ] = array( + 'key' => $key, + 'name' => $all[ $key ]['name'], + 'pricePerDay' => (float) $all[ $key ]['pricePerDay'], + 'description' => $all[ $key ]['description'], + ); + } + } + return rest_ensure_response( $out ); + } }