feat(13-protection-packages): Pakiety ochronne SOFT/PREMIUM z panelu WP

- Panel admina (wp-admin > Rezerwacje > Pakiety ochronne) do zarzadzania
  nazwami, cenami za dobe, aktywnoscia i opisami pakietow SOFT i PREMIUM
  (zapis w wp_options carei_protection_packages)
- REST endpoint GET /carei/v1/protection-packages zwracajacy aktywne pakiety
- Radio cards SOFT/PREMIUM w modalu rezerwacji nad pozycjami "Pakiety ochronne"
  z API (osobne zrodlo danych, separator wizualny)
- Radio z deselect (klik zaznaczonego odznacza), natywny input z accent-color
- Pakiet NIE wysylany w priceItems Softra (powodowalo HTTP 400) - zamiast tego
  doklejany do comments booking i zapisywany w _carei_protection_package meta
- Summary frontend dokorysowuje wiersz pakietu w tabeli cen i dolicza do
  total gross (grandGross = softraGross + protectionTotal)
- Plan 13-01 oznaczony jako superseded (klient zmienil zrodlo danych)
- Phase 13 Complete, Milestone v0.5 Complete

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-20 00:45:24 +02:00
parent 9e4f47de25
commit 42efe93cdd
13 changed files with 987 additions and 25 deletions

View File

@@ -60,9 +60,9 @@ Plugin Elementor do rezerwacji samochodu na stronie carei.pagedev.pl, zintegrowa
## Validated Requirements (Milestone v0.5) ## Validated Requirements (Milestone v0.5)
- ✓ Modal rezerwacji działa na mobile/tablet — Phase 14 - ✓ 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) ## Out of Scope (backlog)
- Ubezpieczenie (pakiet Soft/Premium) — czeka na potwierdzenie klienta (źródło danych)
- Eksport CSV/PDF rezerwacji - Eksport CSV/PDF rezerwacji
- Email notyfikacje - Email notyfikacje

View File

@@ -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 ### Phase 13: Pakiety ochronne — kafelki z ceną za dobę z panelu WP ✅ Complete
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. 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).
**Blocker:** Czekamy na potwierdzenie klienta — źródło danych cenowych (API Softra vs panel WP).
### Phase 14: Mobile modal fix ✅ Complete ### 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. Fix: modal rezerwacji nie otwierał się na mobile/tablet — sekcja Elementor miała elementor-hidden-mobile. Przeniesienie overlay do document.body.

View File

@@ -2,19 +2,19 @@
## Current Position ## Current Position
Milestone: v0.5 Pakiety Ochronne + Poprawki — In progress Milestone: v0.5 Pakiety Ochronne + Poprawki — Complete ✅
Phase: 14 of 14 (Mobile modal fix) — Complete ✅ Phase: 13 of 14 (Pakiety ochronne) — Complete ✅
Plan: 14-01 complete Plan: 13-02 complete
Status: Phase 14 closed, Phase 13 BLOCKED Status: Milestone v0.5 closed
Last activity: 2026-04-10 — Phase 14 unified, SUMMARY written Last activity: 2026-04-20 — Phase 13 unified, SUMMARY written, milestone closed
Progress: Progress:
- Milestone v0.1: [██████████] 100% ✅ - Milestone v0.1: [██████████] 100% ✅
- Milestone v0.2: [██████████] 100% ✅ - Milestone v0.2: [██████████] 100% ✅
- Milestone v0.3: [██████████] 100% ✅ - Milestone v0.3: [██████████] 100% ✅
- Milestone v0.4: [██████████] 100% ✅ - Milestone v0.4: [██████████] 100% ✅
- Milestone v0.5: [██░░░░░░░░] 20% - Milestone v0.5: [██████████] 100% ✅
- Phase 13: BLOCKED — czeka na klienta - Phase 13: [██████████] 100% ✅
- Phase 14: [██████████] 100% ✅ - Phase 14: [██████████] 100% ✅
## Loop Position ## Loop Position
@@ -22,13 +22,12 @@ Progress:
Current loop state: Current loop state:
``` ```
PLAN ──▶ APPLY ──▶ UNIFY PLAN ──▶ APPLY ──▶ UNIFY
✓ ✓ ✓ [Phase 14 loop closed] ✓ ✓ ✓ [Phase 13 loop closed, milestone v0.5 complete]
``` ```
## Session Continuity ## Session Continuity
Last session: 2026-04-10 Last session: 2026-04-20
Stopped at: Phase 14 unified, Phase 13 BLOCKED Stopped at: Milestone v0.5 complete (Phase 13 + Phase 14)
Next action: Czekamy na odpowiedź klienta — źródło danych cenowych SOFT/PREMIUM (API Softra vs panel WP) Next action: Rozmowa o następnym milestone (v0.6) lub zamknięcie projektu — /paul:discuss-milestone lub /paul:complete-milestone
Blocker: Phase 13 — potwierdzenie klienta Resume file: .paul/phases/13-protection-packages/13-02-SUMMARY.md
Resume file: .paul/phases/13-protection-packages/13-01-PLAN.md

View File

@@ -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`

View File

@@ -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"}

View File

@@ -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)*

View File

@@ -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
---
<objective>
## 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
</objective>
<context>
@.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.
</context>
<acceptance_criteria>
## 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ł"
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Backend — admin settings page + REST endpoint</name>
<files>wp-content/plugins/carei-reservation/includes/class-admin-panel.php, wp-content/plugins/carei-reservation/includes/class-rest-proxy.php</files>
<action>
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.
</action>
<verify>
- `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)
</verify>
<done>AC-1, AC-2 satisfied: Admin może edytować pakiety, endpoint REST eksponuje dane.</done>
</task>
<task type="auto">
<name>Task 2: Frontend HTML — kontener kafelków pakietów z separatorem</name>
<files>wp-content/plugins/carei-reservation/includes/class-elementor-widget.php, wp-content/plugins/carei-reservation/assets/css/carei-reservation.css</files>
<action>
A. W `class-elementor-widget.php`, w sekcji `<div class="carei-form__divider"><span>Pakiety ochronne</span></div>`:
- BEZPOŚREDNIO nad istniejącym `<div class="carei-form__row" id="carei-insurance-container">` dodać:
```html
<div class="carei-form__row carei-form__row--protection-packages" id="carei-protection-packages-container">
<!-- Dynamicznie wstrzykiwane: 2 kafelki SOFT/PREMIUM z panelu WP -->
</div>
<div class="carei-form__protection-divider" aria-hidden="true"></div>
```
- 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).
</action>
<verify>
- 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
</verify>
<done>AC-3 satisfied (HTML skeleton + separator) — JS render w Task 3</done>
</task>
<task type="auto">
<name>Task 3: Frontend JS — render, radio behavior, dynamic recalculation, submission</name>
<files>wp-content/plugins/carei-reservation/assets/js/carei-reservation.js</files>
<action>
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:
```
<label class="carei-form__protection-package" data-key="soft" data-price="X">
<input type="radio" name="protectionPackage" value="soft" class="carei-form__protection-package__input" hidden>
<span class="carei-form__protection-package__name">{name}</span>
<span class="carei-form__protection-package__price">{pricePerDay} zł<span class="carei-form__protection-package__unit">/doba</span></span>
<span class="carei-form__protection-package__total" data-role="total"></span>
<span class="carei-form__protection-package__desc">{description}</span>
</label>
```
- 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).
</action>
<verify>
- 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
</verify>
<done>AC-3, AC-4, AC-5 satisfied: Kafelki działają w pełnym cyklu (render → wybór → kalkulacja → submission → zapis).</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
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.
</what-built>
<how-to-verify>
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.
</how-to-verify>
<resume-signal>Napisz "approved" aby kontynuować do UNIFY, lub opisz problemy do naprawy.</resume-signal>
</task>
</tasks>
<boundaries>
## 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
</boundaries>
<verification>
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
</verification>
<success_criteria>
- Wszystkie taski zakończone
- Wszystkie checki weryfikacji przechodzą
- Brak błędów w konsoli przeglądarki i logach PHP
- Human-verify checkpoint zatwierdzony
</success_criteria>
<output>
After completion, create `.paul/phases/13-protection-packages/13-02-SUMMARY.md`
</output>

View File

@@ -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*

View File

@@ -597,6 +597,89 @@ button.carei-reservation-trigger:hover {
color: inherit; 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 { .carei-form__checkbox-label--abroad {
align-items: center; align-items: center;
font-weight: 600; font-weight: 600;

View File

@@ -83,6 +83,9 @@
var summaryOverlay, summaryDetails, summaryTable, summaryTotal, summaryError; var summaryOverlay, summaryDetails, summaryTable, summaryTotal, summaryError;
var summaryBack, summaryConfirm; var summaryBack, summaryConfirm;
var successView, successNumber, successClose; var successView, successNumber, successClose;
var protectionContainer;
var protectionPackages = { soft: null, premium: null };
var selectedProtectionKey = null;
function initRefs() { function initRefs() {
overlay = document.querySelector('[data-carei-modal]'); overlay = document.querySelector('[data-carei-modal]');
@@ -102,6 +105,7 @@
extrasWrapper = document.getElementById('carei-extras-wrapper'); extrasWrapper = document.getElementById('carei-extras-wrapper');
extrasContainer = document.getElementById('carei-extras-container'); extrasContainer = document.getElementById('carei-extras-container');
insuranceContainer = document.getElementById('carei-insurance-container'); insuranceContainer = document.getElementById('carei-insurance-container');
protectionContainer = document.getElementById('carei-protection-packages-container');
abroadSection = document.getElementById('carei-abroad-section'); abroadSection = document.getElementById('carei-abroad-section');
abroadToggle = document.getElementById('carei-abroad-toggle'); abroadToggle = document.getElementById('carei-abroad-toggle');
abroadSearch = document.getElementById('carei-abroad-search'); abroadSearch = document.getElementById('carei-abroad-search');
@@ -419,6 +423,69 @@
daysCount.innerHTML = 'Wybrano: <strong>' + diff + ' ' + (diff === 1 ? 'dzień' : 'dni') + '</strong>'; daysCount.innerHTML = 'Wybrano: <strong>' + diff + ' ' + (diff === 1 ? 'dzień' : 'dni') + '</strong>';
} }
// ─── 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 ? '<span class="carei-form__protection-package__desc">' + escHtml(pkg.description) + '</span>' : '';
var card = document.createElement('label');
card.className = 'carei-form__protection-package';
card.setAttribute('data-key', key);
card.setAttribute('data-price', price);
card.innerHTML =
'<span class="carei-form__protection-package__row">' +
'<input type="radio" name="protectionPackage" value="' + escAttr(key) + '" class="carei-form__protection-package__input">' +
'<span class="carei-form__protection-package__name">' + escHtml(pkg.name || '') + '</span>' +
'<span class="carei-form__protection-package__price"><strong>' + escHtml(priceLabel) + '</strong></span>' +
'</span>' +
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 ─────────────────────────────── // ─── Load Extras from Pricelist ───────────────────────────────
function loadExtras() { function loadExtras() {
@@ -875,9 +942,33 @@
priceAfterDiscount: price 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; 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 ────────────────────────────────────────── // ─── Step Transitions ──────────────────────────────────────────
function hideStep(el) { function hideStep(el) {
@@ -961,7 +1052,8 @@
// Selected extras // Selected extras
var selectedExtras = getSelectedExtrasForApi(); var selectedExtras = getSelectedExtrasForApi();
if (selectedExtras.length > 0) { var pkgForDetails = getSelectedProtectionPayload();
if (selectedExtras.length > 0 || pkgForDetails) {
html += '<div style="margin-top:8px"><strong>Wybrane opcje:</strong></div><ul style="margin:4px 0 0 16px;padding:0;list-style:disc;">'; html += '<div style="margin-top:8px"><strong>Wybrane opcje:</strong></div><ul style="margin:4px 0 0 16px;padding:0;list-style:disc;">';
selectedExtras.forEach(function (ex) { selectedExtras.forEach(function (ex) {
var totalPrice = ex.priceAfterDiscount * (ex.amount || 1); var totalPrice = ex.priceAfterDiscount * (ex.amount || 1);
@@ -970,6 +1062,12 @@
: fmtPrice(totalPrice) + ' zł'; : fmtPrice(totalPrice) + ' zł';
html += '<li>' + escHtml(toSentenceCase(ex.name)) + ' — ' + priceInfo + '</li>'; html += '<li>' + escHtml(toSentenceCase(ex.name)) + ' — ' + priceInfo + '</li>';
}); });
if (pkgForDetails) {
var pkgInfo = pkgForDetails.days > 1
? fmtPrice(pkgForDetails.pricePerDay) + ' zł/doba × ' + pkgForDetails.days + ' = ' + fmtPrice(pkgForDetails.total) + ' zł'
: fmtPrice(pkgForDetails.total) + ' zł';
html += '<li>' + escHtml(pkgForDetails.name) + ' — ' + pkgInfo + '</li>';
}
html += '</ul>'; html += '</ul>';
} }
@@ -980,6 +1078,8 @@
summaryDetails.innerHTML = html; summaryDetails.innerHTML = html;
} }
var protectionPayload = getSelectedProtectionPayload();
// Price table // Price table
if (summaryTable && summary.pricelist) { if (summaryTable && summary.pricelist) {
var html = '<table><thead><tr><th>Nazwa</th><th>Ilość</th><th>Netto</th><th>Brutto</th></tr></thead><tbody>'; var html = '<table><thead><tr><th>Nazwa</th><th>Ilość</th><th>Netto</th><th>Brutto</th></tr></thead><tbody>';
@@ -991,16 +1091,30 @@
'<td>' + fmtPrice(item.netValue) + '</td>' + '<td>' + fmtPrice(item.netValue) + '</td>' +
'<td>' + fmtPrice(item.grossValue) + '</td></tr>'; '<td>' + fmtPrice(item.grossValue) + '</td></tr>';
}); });
if (protectionPayload) {
html += '<tr class="carei-summary__protection-row">' +
'<td>' + escHtml(protectionPayload.name) + ' <small>(do doliczenia)</small></td>' +
'<td>' + protectionPayload.days + ' doba</td>' +
'<td>—</td>' +
'<td>' + fmtPrice(protectionPayload.total) + '</td></tr>';
}
html += '</tbody></table>'; html += '</tbody></table>';
summaryTable.innerHTML = html; summaryTable.innerHTML = html;
} }
// Totals // Totals
if (summaryTotal) { if (summaryTotal) {
summaryTotal.innerHTML = var softraGross = parseFloat(summary.totalGrossValue || 0);
var protectionTotal = protectionPayload ? protectionPayload.total : 0;
var grandGross = softraGross + protectionTotal;
var totalsHtml =
'<div class="carei-summary__total-row"><span class="carei-summary__total-label">Netto:</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalNetValue) + '</span></div>' + '<div class="carei-summary__total-row"><span class="carei-summary__total-label">Netto:</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalNetValue) + '</span></div>' +
'<div class="carei-summary__total-row"><span class="carei-summary__total-label">VAT:</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalVatValue) + '</span></div>' + '<div class="carei-summary__total-row"><span class="carei-summary__total-label">VAT:</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalVatValue) + '</span></div>';
'<div class="carei-summary__total-row carei-summary__total-row--gross"><span class="carei-summary__total-label">Do zapłaty:</span><span class="carei-summary__total-value">' + fmtPrice(summary.totalGrossValue) + ' zł</span></div>'; if (protectionPayload) {
totalsHtml += '<div class="carei-summary__total-row"><span class="carei-summary__total-label">Pakiet ochronny:</span><span class="carei-summary__total-value">' + fmtPrice(protectionPayload.total) + '</span></div>';
}
totalsHtml += '<div class="carei-summary__total-row carei-summary__total-row--gross"><span class="carei-summary__total-label">Do zapłaty:</span><span class="carei-summary__total-value">' + fmtPrice(grandGross) + ' zł</span></div>';
summaryTotal.innerHTML = totalsHtml;
} }
} }
@@ -1047,7 +1161,8 @@
phone: fd.phone, phone: fd.phone,
email: fd.email email: fd.email
}], }],
comments: fd.message || '' comments: buildBookingComments(fd.message || ''),
protectionPackage: getSelectedProtectionPayload()
}; };
// Add agreement items // Add agreement items
@@ -1344,6 +1459,7 @@
initAbroad(); initAbroad();
initSubmit(); initSubmit();
initMap(); initMap();
loadProtectionPackages();
} }
/* ═══════════════════════════════════════════ /* ═══════════════════════════════════════════

View File

@@ -8,6 +8,8 @@ class Carei_Admin_Panel {
const POST_TYPE = 'carei_reservation'; const POST_TYPE = 'carei_reservation';
const META_PREFIX = '_carei_'; const META_PREFIX = '_carei_';
const PROTECTION_OPTION = 'carei_protection_packages';
private static $statuses = array( private static $statuses = array(
'nowe' => array( 'label' => 'Nowe', 'color' => '#2F2482' ), 'nowe' => array( 'label' => 'Nowe', 'color' => '#2F2482' ),
'przeczytane' => array( 'label' => 'Przeczytane', 'color' => '#f59e0b' ), '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( '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( 'edit_form_after_title', array( $this, 'auto_mark_read' ) );
add_action( 'admin_head', array( $this, 'admin_styles' ) ); 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';
?>
<div class="wrap">
<h1>Pakiety ochronne</h1>
<p>Konfiguracja pakietów wyświetlanych w sekcji <strong>Pakiety ochronne</strong> formularza rezerwacji. Cena podawana jest za dobę — total = cena × liczba dób rezerwacji.</p>
<?php if ( $saved ) : ?>
<div class="notice notice-success is-dismissible"><p>Zapisano.</p></div>
<?php endif; ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="carei-protection-form">
<input type="hidden" name="action" value="carei_save_protection_packages">
<?php wp_nonce_field( 'carei_protection_packages', 'carei_protection_nonce' ); ?>
<?php foreach ( array( 'soft' => 'SOFT', 'premium' => 'PREMIUM' ) as $key => $label ) :
$pkg = $data[ $key ];
?>
<div class="carei-protection-card">
<h2>Pakiet <?php echo esc_html( $label ); ?></h2>
<table class="form-table">
<tr>
<th><label for="carei_<?php echo esc_attr( $key ); ?>_name">Nazwa wyświetlana</label></th>
<td><input type="text" id="carei_<?php echo esc_attr( $key ); ?>_name" name="packages[<?php echo esc_attr( $key ); ?>][name]" value="<?php echo esc_attr( $pkg['name'] ); ?>" class="regular-text" required></td>
</tr>
<tr>
<th><label for="carei_<?php echo esc_attr( $key ); ?>_price">Cena za dobę (zł)</label></th>
<td><input type="number" id="carei_<?php echo esc_attr( $key ); ?>_price" name="packages[<?php echo esc_attr( $key ); ?>][pricePerDay]" value="<?php echo esc_attr( $pkg['pricePerDay'] ); ?>" min="0" step="0.01" class="small-text" required></td>
</tr>
<tr>
<th>Status</th>
<td><label><input type="checkbox" name="packages[<?php echo esc_attr( $key ); ?>][active]" value="1" <?php checked( $pkg['active'] ); ?>> Aktywny (widoczny w modalu)</label></td>
</tr>
<tr>
<th><label for="carei_<?php echo esc_attr( $key ); ?>_desc">Opis / zakres usług</label></th>
<td><textarea id="carei_<?php echo esc_attr( $key ); ?>_desc" name="packages[<?php echo esc_attr( $key ); ?>][description]" rows="3" cols="60" class="large-text"><?php echo esc_textarea( $pkg['description'] ); ?></textarea></td>
</tr>
</table>
</div>
<?php endforeach; ?>
<?php submit_button( 'Zapisz pakiety' ); ?>
</form>
</div>
<style>
.carei-protection-card { background:#fff; border:1px solid #c3c4c7; border-left:4px solid #2F2482; padding:10px 20px; margin:20px 0; }
.carei-protection-card h2 { margin-top:10px; color:#2F2482; }
</style>
<?php
}
public function handle_protection_packages_save() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'Brak uprawnień.' );
}
if ( ! isset( $_POST['carei_protection_nonce'] ) || ! wp_verify_nonce( $_POST['carei_protection_nonce'], 'carei_protection_packages' ) ) {
wp_die( 'Nieprawidłowy token.' );
}
$input = isset( $_POST['packages'] ) && is_array( $_POST['packages'] ) ? $_POST['packages'] : array();
$defaults = self::get_protection_packages_defaults();
$clean = array();
foreach ( $defaults as $key => $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() { public function register_post_type() {
@@ -190,8 +335,19 @@ class Carei_Admin_Panel {
'extras' => get_post_meta( $post->ID, self::META_PREFIX . 'extras', true ), 'extras' => get_post_meta( $post->ID, self::META_PREFIX . 'extras', true ),
'comments' => get_post_meta( $post->ID, self::META_PREFIX . 'comments', true ), 'comments' => get_post_meta( $post->ID, self::META_PREFIX . 'comments', true ),
'status' => get_post_meta( $post->ID, self::META_PREFIX . 'status', true ) ?: 'nowe', '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 = $meta['address'] ? json_decode( $meta['address'], true ) : null;
$address_str = ''; $address_str = '';
if ( $address ) { if ( $address ) {
@@ -235,6 +391,7 @@ class Carei_Admin_Panel {
<tr><th>Adres</th><td><?php echo esc_html( $address_str ?: '—' ); ?></td></tr> <tr><th>Adres</th><td><?php echo esc_html( $address_str ?: '—' ); ?></td></tr>
<tr class="carei-meta-divider"><td colspan="2"><hr></td></tr> <tr class="carei-meta-divider"><td colspan="2"><hr></td></tr>
<tr><th>Opcje dodatkowe</th><td><?php echo esc_html( $extras_str ?: 'Brak' ); ?></td></tr> <tr><th>Opcje dodatkowe</th><td><?php echo esc_html( $extras_str ?: 'Brak' ); ?></td></tr>
<tr><th>Pakiet ochronny</th><td><?php echo esc_html( $protection_str ?: 'Brak' ); ?></td></tr>
<tr><th>Wiadomość</th><td><?php echo esc_html( $meta['comments'] ?: '—' ); ?></td></tr> <tr><th>Wiadomość</th><td><?php echo esc_html( $meta['comments'] ?: '—' ); ?></td></tr>
<tr class="carei-meta-divider"><td colspan="2"><hr></td></tr> <tr class="carei-meta-divider"><td colspan="2"><hr></td></tr>
<tr> <tr>
@@ -401,6 +558,9 @@ class Carei_Admin_Panel {
'comments' => isset( $booking_data['comments'] ) ? $booking_data['comments'] : '', 'comments' => isset( $booking_data['comments'] ) ? $booking_data['comments'] : '',
'status' => 'nowe', 'status' => 'nowe',
'raw_response' => wp_json_encode( $api_result ), '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 ) { foreach ( $meta as $key => $value ) {

View File

@@ -152,6 +152,10 @@ class Carei_Reservation_Widget extends \Elementor\Widget_Base {
<div class="carei-form__divider"><span>Pakiety ochronne</span></div> <div class="carei-form__divider"><span>Pakiety ochronne</span></div>
<div class="carei-form__section"> <div class="carei-form__section">
<div class="carei-form__row carei-form__row--protection-packages" id="carei-protection-packages-container">
<!-- Dynamicznie z panelu WP (SOFT, PREMIUM) -->
</div>
<div class="carei-form__protection-divider" aria-hidden="true"></div>
<div class="carei-form__row" id="carei-insurance-container"> <div class="carei-form__row" id="carei-insurance-container">
<!-- Dynamicznie z API pricelist --> <!-- Dynamicznie z API pricelist -->
</div> </div>

View File

@@ -130,6 +130,13 @@ class Carei_REST_Proxy {
'callback' => array( $this, 'get_agreements' ), 'callback' => array( $this, 'get_agreements' ),
'permission_callback' => '__return_true', '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() ); 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 );
}
} }