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:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
25
.paul/changelog/2026-04-20.md
Normal file
25
.paul/changelog/2026-04-20.md
Normal 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`
|
||||
42
.paul/governance/governance_2026-04-20.jsonl
Normal file
42
.paul/governance/governance_2026-04-20.jsonl
Normal 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"}
|
||||
21
.paul/phases/13-protection-packages/13-01-SUMMARY.md
Normal file
21
.paul/phases/13-protection-packages/13-01-SUMMARY.md
Normal 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)*
|
||||
317
.paul/phases/13-protection-packages/13-02-PLAN.md
Normal file
317
.paul/phases/13-protection-packages/13-02-PLAN.md
Normal 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>
|
||||
173
.paul/phases/13-protection-packages/13-02-SUMMARY.md
Normal file
173
.paul/phases/13-protection-packages/13-02-SUMMARY.md
Normal 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*
|
||||
@@ -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;
|
||||
|
||||
@@ -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: <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 ───────────────────────────────
|
||||
|
||||
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 += '<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) {
|
||||
var totalPrice = ex.priceAfterDiscount * (ex.amount || 1);
|
||||
@@ -970,6 +1062,12 @@
|
||||
: fmtPrice(totalPrice) + ' zł';
|
||||
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>';
|
||||
}
|
||||
|
||||
@@ -980,6 +1078,8 @@
|
||||
summaryDetails.innerHTML = html;
|
||||
}
|
||||
|
||||
var protectionPayload = getSelectedProtectionPayload();
|
||||
|
||||
// Price table
|
||||
if (summaryTable && summary.pricelist) {
|
||||
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.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>';
|
||||
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 =
|
||||
'<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 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>';
|
||||
'<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>';
|
||||
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,
|
||||
email: fd.email
|
||||
}],
|
||||
comments: fd.message || ''
|
||||
comments: buildBookingComments(fd.message || ''),
|
||||
protectionPackage: getSelectedProtectionPayload()
|
||||
};
|
||||
|
||||
// Add agreement items
|
||||
@@ -1344,6 +1459,7 @@
|
||||
initAbroad();
|
||||
initSubmit();
|
||||
initMap();
|
||||
loadProtectionPackages();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
|
||||
@@ -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';
|
||||
?>
|
||||
<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() {
|
||||
@@ -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 {
|
||||
<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><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 class="carei-meta-divider"><td colspan="2"><hr></td></tr>
|
||||
<tr>
|
||||
@@ -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 ) {
|
||||
|
||||
@@ -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__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">
|
||||
<!-- Dynamicznie z API pricelist -->
|
||||
</div>
|
||||
|
||||
@@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user