- 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>
18 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, delegation
| phase | plan | type | wave | depends_on | files_modified | autonomous | delegation | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 13-protection-packages | 02 | execute | 1 |
|
false | off |
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-packagesdla 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
priceItemsbooking submission i zapisany w CPT
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.
<acceptance_criteria>
AC-1: Panel administratora — konfiguracja pakietów
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
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
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
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
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>
Task 1: Backend — admin settings page + REST endpoint wp-content/plugins/carei-reservation/includes/class-admin-panel.php, wp-content/plugins/carei-reservation/includes/class-rest-proxy.php A. W `class-admin-panel.php`: - Dodać metodę `register_protection_packages_page()` hookowaną przez `admin_menu`: `add_submenu_page('edit.php?post_type=carei_reservation', 'Pakiety ochronne', 'Pakiety ochronne', 'manage_options', 'carei-protection-packages', [$this, 'render_protection_packages_page'])` - Dodać `render_protection_packages_page()`: formularz POST z nonce `carei_protection_packages`, dwie sekcje (SOFT, PREMIUM) z polami: `name`, `pricePerDay`, `active`, `description`. - Dodać obsługę POST: walidacja nonce + `manage_options`, sanityzacja (`sanitize_text_field` dla name, `floatval` + clamp >=0 dla price, `boolean` checkbox, `sanitize_textarea_field`), zapis do `update_option('carei_protection_packages', $data)`. - Defaulty (przy pierwszym wczytaniu / pustym option): `soft = { name: 'Ubezpieczenie SOFT', pricePerDay: 0, active: true, description: '' }` `premium = { name: 'Ubezpieczenie PREMIUM', pricePerDay: 0, active: true, description: '' }` - Dodać helper statyczny `public static function get_protection_packages()` zwracający aktualną tablicę (merge z defaultami). - Dodatkowo dopisać obsługę zapisu wybranego pakietu do post_meta w `save_reservation()` (jeśli `$data['protectionPackage']` podane — zapisać jako `_carei_protection_package` JSON: `{key, name, pricePerDay, days, total}`) oraz wyświetlić w meta boxie pod sekcją extras.B. W `class-rest-proxy.php`:
- Zarejestrować nowy route w `register_routes()`:
```php
register_rest_route( self::NAMESPACE, '/protection-packages', array(
'methods' => 'GET',
'callback' => array( $this, 'get_protection_packages' ),
'permission_callback' => '__return_true',
) );
```
- Dodać metodę `get_protection_packages()`: odczyt `Carei_Admin_Panel::get_protection_packages()`, filtrowanie tylko `active === true`, zwrot jako `rest_ensure_response({ soft: {...}|null, premium: {...}|null })`.
Avoid: Nie tworzyć custom table — wystarczy wp_options (spójne z MVP). Nie dodawać JS-a do panelu admina (formularz statyczny, native WP). Nie przenosić `save_reservation()` logic — tylko dopisać pole.
- `wp-admin/edit.php?post_type=carei_reservation&page=carei-protection-packages` — strona renderuje formularz
- Zmiana ceny + zapis → komunikat "Zapisano" i wartości persystują po reload
- `curl https://carei.pagedev.pl/wp-json/carei/v1/protection-packages` → JSON z oboma pakietami
- Ustawienie SOFT inactive → endpoint zwraca `soft: null` (lub bez klucza)
AC-1, AC-2 satisfied: Admin może edytować pakiety, endpoint REST eksponuje dane.
Task 2: Frontend HTML — kontener kafelków pakietów z separatorem
wp-content/plugins/carei-reservation/includes/class-elementor-widget.php, wp-content/plugins/carei-reservation/assets/css/carei-reservation.css
A. W `class-elementor-widget.php`, w sekcji `B. W `carei-reservation.css`:
- `.carei-form__row--protection-packages` — grid/flex z 2 kolumnami (desktop), wrap na mobile.
- `.carei-form__protection-package` — karta: padding, border 2px solid transparent, border-radius 12px, background #f8f8fb, cursor pointer, tranzycja.
- `.carei-form__protection-package.is-selected` — border #2F2482, background #fff z subtelnym shadow.
- `.carei-form__protection-package__name` — bold, color #2F2482.
- `.carei-form__protection-package__price` — większy font, strong.
- `.carei-form__protection-package__unit` — mały font szary "/doba".
- `.carei-form__protection-package__total` — color #FF0000 dla total.
- `.carei-form__protection-package__desc` — mały font, szary.
- `.carei-form__protection-divider` — margin-top/bottom, border-bottom 1px dashed rgba(47,36,130,0.15), height 0. Stanowi delikatny wizualny separator między pakietami WP a opcjami API.
- Responsive: `@media (max-width: 640px)` → kolumna, pełna szerokość.
Avoid: Nie ruszać istniejącego `#carei-insurance-container` ani `.carei-form__extra-card` (służą pozostałym pozycjom API i extras).
- W edytorze Elementor / na stronie: DOM zawiera nowy `#carei-protection-packages-container` nad `#carei-insurance-container` wewnątrz sekcji "Pakiety ochronne"
- Separator `.carei-form__protection-divider` widoczny w DOM
- Style zaczytywane — brak błędów w DevTools
AC-3 satisfied (HTML skeleton + separator) — JS render w Task 3
Task 3: Frontend JS — render, radio behavior, dynamic recalculation, submission
wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
1. **Wczytanie pakietów (przy init lub przy otwarciu modala):**
- Dodać stan modułu: `var protectionPackages = { soft: null, premium: null };` oraz `var selectedProtectionKey = null;`.
- Dodać referencję: `protectionContainer = document.getElementById('carei-protection-packages-container');` w sekcji `cacheDom()` (~linie 100-120).
- Dodać funkcję `loadProtectionPackages()` wywołującą `fetch('/wp-json/carei/v1/protection-packages')` (REST nie wymaga nonce dla GET). Po otrzymaniu: zapis do `protectionPackages`, wywołanie `renderProtectionPackages()`.
- Wywołać `loadProtectionPackages()` jednorazowo (np. w `init()` obok innych API calls, niezależnie od pricelist).
2. **Render `renderProtectionPackages()`:**
- Jeśli kontener nie istnieje — return.
- Wyczyścić container.
- Dla każdego klucza `['soft','premium']`, jeśli pakiet istnieje i `active`:
- Zbudować kafelek zgodnie z markup CSS z Task 2:
```
<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).
- Network tab: request do `/carei/v1/protection-packages` zwraca dane
- Modal: w sekcji "Pakiety ochronne" widoczne 2 kafelki SOFT+PREMIUM nad separatorem
- Kliknięcie SOFT: `is-selected`, kliknięcie PREMIUM: SOFT traci selekcję, PREMIUM dostaje
- Kliknięcie zaznaczonego pakietu: odznaczenie
- Zmiana dat 3→7 dni: total pakietu aktualizuje się (price × 7)
- Wybór pakietu → podsumowanie → payload `priceItems` zawiera pakiet, `protectionPackage` przekazany do backendu
- Zapisana rezerwacja w CPT zawiera wybrany pakiet w meta boxie
AC-3, AC-4, AC-5 satisfied: Kafelki działają w pełnym cyklu (render → wybór → kalkulacja → submission → zapis).
Panel admina "Pakiety ochronne" (wp-admin), endpoint REST, kafelki SOFT/PREMIUM w modalu z separatorem od opcji API, radio selection z deselect, dynamiczne przeliczanie price×days, pakiet w booking submission + zapis w CPT.
1. **Admin:** Zaloguj się do WP → menu "Rezerwacje" → "Pakiety ochronne".
- Ustaw SOFT: nazwa "Ubezpieczenie SOFT", cena 25 zł/dobę, aktywne, opis krótki
- Ustaw PREMIUM: nazwa "Ubezpieczenie PREMIUM", cena 50 zł/dobę, aktywne, opis krótki
- Zapisz, przeładuj — wartości zostały
2. **Frontend:** Otwórz carei.pagedev.pl → kliknij przycisk rezerwacji.
- Wybierz segment, oddział, daty (3 dni)
- Sekcja "Pakiety ochronne" — widzisz u góry 2 kafelki SOFT+PREMIUM, pod nimi linia separatora, następnie pozostałe opcje API (jeśli są)
- SOFT pokazuje "25 zł/doba = 75 zł", PREMIUM "50 zł/doba = 150 zł"
3. **Interakcja:** Kliknij SOFT → podświetlony. Kliknij PREMIUM → SOFT traci selekcję, PREMIUM zaznaczony. Kliknij PREMIUM ponownie → odznaczony.
4. **Recalc:** Zmień daty na 7 dni → totale: SOFT 175 zł, PREMIUM 350 zł.
5. **Submit:** Wybierz SOFT, dokończ flow → podsumowanie pokazuje "Ubezpieczenie SOFT — 175 zł" jako pozycję. Potwierdź rezerwację.
6. **CPT:** wp-admin → Rezerwacje → otwórz nową pozycję → meta box zawiera informację o wybranym pakiecie.
7. **Inactive test:** Ustaw SOFT active=false w adminie → modal pokazuje tylko PREMIUM.
8. **Mobile (360×640):** Kafelki stackują się pionowo, czytelne, klikalne.
Napisz "approved" aby kontynuować do UNIFY, lub opisz problemy do naprawy.
DO NOT CHANGE
class-softra-api.php— klient Softra API bez zmianbuildExtraCard()/#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
<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>