fix(14-mobile-modal-fix): Modal rezerwacji działa na mobile/tablet

Sekcja Elementor zawierająca modal miała elementor-hidden-mobile/tablet,
co powodowało display:none na rodzicu. Modal position:fixed wewnątrz
ukrytego elementu miał zerowe wymiary.

Fix: przeniesienie overlay do document.body w initRefs().
Plan Phase 13 (pakiety ochronne) utworzony, BLOCKED — czeka na klienta.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 00:05:53 +02:00
parent 9b36f8fec3
commit 6f6c1fcf17
8 changed files with 554 additions and 31 deletions

View File

@@ -0,0 +1,204 @@
---
phase: 13-protection-packages
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
- wp-content/plugins/carei-reservation/assets/css/carei-reservation.css
- wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
autonomous: false
delegation: off
---
<objective>
## Goal
Wyświetlanie pakietów ochronnych SOFT i PREMIUM jako dedykowanych kafelków z ceną obliczaną dynamicznie na podstawie liczby dni wynajmu (3 progi cenowe: minimalna 1-3 dni, za dobę 4-15 dni, maksymalna 16+ dni).
## Purpose
Klient chce prezentować pakiety ochronne w zrozumiały sposób — zamiast generycznego "od X do Y zł" użytkownik widzi jedną konkretną cenę dopasowaną do długości jego rezerwacji. Wybór pakietu jest opcjonalny i wzajemnie wykluczający (SOFT lub PREMIUM, nie oba).
## Output
- Sekcja "Pakiety ochronne" z dwoma kafelkami (SOFT, PREMIUM) z dynamiczną ceną
- Cena przeliczana automatycznie przy zmianie dat
- Wybrany pakiet uwzględniany w podsumowaniu i booking submission
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Source Files
@wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
@wp-content/plugins/carei-reservation/assets/css/carei-reservation.css
@wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
</context>
<skills>
No SPECIAL-FLOWS.md — skills section omitted.
</skills>
<acceptance_criteria>
## AC-1: Tiered price calculation
```gherkin
Given insurance items from API with minPrice, price (per day), and maxPrice
When the user has selected rental dates spanning N days
Then each protection package shows:
- minPrice (flat) when N <= 3
- price × N when 4 <= N <= 15
- maxPrice (flat) when N > 15
```
## AC-2: Mutually exclusive selection
```gherkin
Given two protection package cards (SOFT, PREMIUM) are displayed
When the user clicks on one package
Then the other package is deselected (radio behavior)
And the user can also deselect to choose no package
```
## AC-3: Dynamic price update on date change
```gherkin
Given protection packages are displayed with calculated prices
When the user changes pickup or return date
Then the prices on both cards recalculate immediately
And the correct tier (flat/per-day/flat) is applied based on new duration
```
## AC-4: Package included in booking submission
```gherkin
Given the user has selected a protection package (SOFT or PREMIUM)
When the booking is submitted
Then the selected package is included in priceItems with correct calculated price
And the package appears in the summary overlay with its name and total price
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Render protection packages as radio-style cards with tiered pricing</name>
<files>wp-content/plugins/carei-reservation/assets/js/carei-reservation.js, wp-content/plugins/carei-reservation/assets/css/carei-reservation.css</files>
<action>
1. In the pricelist loading section (~line 432-458), after filtering insuranceItems:
- Create a new function `buildProtectionCard(item, days)` that:
- Calculates price based on days: `days <= 3` → item.minPrice, `days >= 4 && days <= 15` → item.price * days, `days > 15` → item.maxPrice
- Renders a card with radio behavior (use checkbox with JS toggle for deselect capability)
- Shows price label: "180 zł" (flat) or "60 zł/doba = 420 zł" (per-day with total)
- Card design: prominent tile with package name, price, and a brief description if available from API
- Replace current `insuranceContainer` rendering: instead of `buildExtraCard` for each item, use `buildProtectionCard`
- Use `name="protection"` with type="radio" BUT wrap in a click handler that allows deselection (click selected = deselect)
2. Add function `updateProtectionPrices()` that:
- Reads current days count from the date fields (reuse existing daysCount logic)
- Recalculates prices for both cards
- Updates the displayed price labels
- Call this function when dates change (hook into existing date change handler)
3. CSS for protection cards in carei-reservation.css:
- Two cards side by side (flex row, gap)
- Active/selected state with border highlight (#2F2482)
- Responsive: stack vertically on mobile
- Style consistent with existing extra cards but visually distinct (larger, more prominent)
Avoid: Changing the existing `buildExtraCard` function — it still serves regular extras.
Avoid: Hardcoding prices — all pricing comes from API item fields (minPrice, price, maxPrice).
</action>
<verify>
- Open reservation form, select dates for 2 days → packages show flat minPrice
- Change dates to 7 days → packages show per-day price × 7
- Change dates to 20 days → packages show flat maxPrice
- Click SOFT → SOFT selected, click PREMIUM → SOFT deselected + PREMIUM selected
- Click selected card again → deselected (no package chosen)
</verify>
<done>AC-1, AC-2, AC-3 satisfied: Protection packages render with correct tiered pricing, mutually exclusive selection, and dynamic price updates</done>
</task>
<task type="auto">
<name>Task 2: Wire protection package into booking submission and summary</name>
<files>wp-content/plugins/carei-reservation/assets/js/carei-reservation.js</files>
<action>
1. In the booking submission logic (where extras[] checkboxes are collected):
- Add collection of selected protection package: query `input[name="protection"]:checked`
- Build priceItem with: id, name, unit='szt.', amount=1, priceBeforeDiscount = calculated total price, discount=0, priceAfterDiscount = same
- Append to the priceItems array sent to API
2. In the summary overlay rendering:
- Add a row for the selected protection package showing package name and total price
- If no package selected, don't show a row
3. Ensure the protection price is included in the total calculation in the summary.
Avoid: Changing the admin panel storage logic — protection items go through as regular priceItems, same as extras.
Avoid: Duplicating protection item if it also appears in extras — ensure the filtering separates them cleanly.
</action>
<verify>
- Select SOFT package + submit → summary shows "Pakiet ochrony Soft — XXX zł"
- Submit without package → no protection row in summary
- Full booking flow completes with package included in API call
</verify>
<done>AC-4 satisfied: Selected package included in booking submission and visible in summary</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>Protection package cards with tiered pricing, radio selection, date-driven recalculation, and booking integration</what-built>
<how-to-verify>
1. Otwórz stronę carei.pagedev.pl i kliknij rezerwację
2. Wybierz segment, lokalizację i daty na 2 dni → sprawdź cenę pakietów (powinna być cena minimalna)
3. Zmień daty na 7 dni → cena powinna się przeliczyć (cena za dobę × 7)
4. Zmień daty na 20 dni → cena powinna być stała (maksymalna)
5. Kliknij SOFT → zaznaczony, kliknij PREMIUM → SOFT odznaczony, PREMIUM zaznaczony
6. Kliknij zaznaczony ponownie → odznaczony (brak pakietu)
7. Wybierz pakiet i przejdź do podsumowania → pakiet widoczny z ceną
8. Sprawdź na mobile → karty jedna pod drugą
</how-to-verify>
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- class-rest-proxy.php (API proxy unchanged — data comes from existing pricelist endpoint)
- class-admin-panel.php (admin storage — protection items stored same as extras via priceItems)
- class-softra-api.php (Softra API client — no changes needed)
- Abroad section logic (wyjazd zagraniczny)
- Hero search form widget
## SCOPE LIMITS
- No new API endpoints — protection packages come from existing pricelist/additionalItems
- No backend pricing logic — all tier calculation is client-side based on API item fields
- No new admin UI for managing packages — data managed in Softra system
- Description/zakres usług text comes from API item.description — no hardcoded descriptions
</boundaries>
<verification>
Before declaring plan complete:
- [ ] Protection packages display with correct tiered pricing for all 3 duration ranges
- [ ] Selection is mutually exclusive with deselect capability
- [ ] Prices recalculate on date change
- [ ] Selected package included in booking submission priceItems
- [ ] Summary overlay shows selected package with price
- [ ] No regressions in existing extras or abroad sections
- [ ] Responsive layout works on mobile
- [ ] All acceptance criteria met
</verification>
<success_criteria>
- All tasks completed
- All verification checks pass
- No errors or warnings introduced
- Protection packages display correctly for all 3 pricing tiers
- Booking flow works end-to-end with selected package
</success_criteria>
<output>
After completion, create `.paul/phases/13-protection-packages/13-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,95 @@
---
phase: 14-mobile-modal-fix
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
autonomous: true
delegation: off
---
<objective>
## Goal
Fix: modal rezerwacji nie otwiera się na mobile/tablet — przenieść overlay do document.body aby uniezależnić od ukrytych sekcji Elementor.
## Purpose
Formularz rezerwacji był niefunkcjonalny na urządzeniach mobilnych i tabletach — klienci nie mogli złożyć rezerwacji z telefonu.
## Output
- Modal otwiera się poprawnie na mobile i tablet
- Brak regresji na desktop
</objective>
<context>
## Project Context
@.paul/PROJECT.md
## Source Files
@wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
</context>
<acceptance_criteria>
## AC-1: Modal otwiera się na mobile
```gherkin
Given strona carei.pagedev.pl otwarta na urządzeniu mobilnym (375px)
When użytkownik wypełni hero search form i kliknie "Złóż zapytanie o rezerwację"
Then modal rezerwacji otwiera się na pełnym ekranie z pre-wypełnionymi danymi
```
## AC-2: Brak regresji desktop
```gherkin
Given strona otwarta na desktopie
When użytkownik kliknie przycisk otwarcia modala
Then modal otwiera się jak dotychczas (centered overlay)
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Przenieś overlay do document.body w initRefs()</name>
<files>wp-content/plugins/carei-reservation/assets/js/carei-reservation.js</files>
<action>
W funkcji initRefs(), po querySelector overlay, dodać:
if (overlay && overlay.parentElement !== document.body) {
document.body.appendChild(overlay);
}
Przyczyna: sekcja Elementor (elementor-element-a7629b1) ma klasy
elementor-hidden-tablet elementor-hidden-mobile, co ustawia display:none
na rodzicu. Modal position:fixed wewnątrz ukrytego rodzica ma zerowe wymiary.
</action>
<verify>Otworzyć stronę na mobile (375px), wypełnić formularz, kliknąć submit — modal się otwiera</verify>
<done>AC-1, AC-2 satisfied</done>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- CSS modala (carei-reservation.css)
- Elementor widget PHP
- Logika formularza i API
## SCOPE LIMITS
- Tylko fix JS — jedna linijka appendChild
</boundaries>
<verification>
- [ ] Modal otwiera się na mobile (375px)
- [ ] Modal otwiera się na tablet (768px)
- [ ] Modal otwiera się na desktop (1440px)
- [ ] Pre-fill z hero search form działa na mobile
</verification>
<success_criteria>
- Modal działa na wszystkich breakpointach
- Brak regresji
</success_criteria>
<output>
After completion, create `.paul/phases/14-mobile-modal-fix/14-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,108 @@
---
phase: 14-mobile-modal-fix
plan: 01
subsystem: ui
tags: [mobile, elementor, modal, responsive]
requires:
- phase: none
provides: n/a
provides:
- Modal rezerwacji działa na mobile/tablet
affects: []
tech-stack:
added: []
patterns: [overlay-to-body pattern for Elementor hidden sections]
key-files:
created: []
modified:
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
key-decisions:
- "appendChild do body zamiast CSS override — niezależne od przyszłych zmian widoczności sekcji Elementor"
patterns-established:
- "Modal overlay musi być dzieckiem body, nie sekcji Elementor (unika problemów z hidden responsive)"
duration: 15min
started: 2026-04-09T21:50:00Z
completed: 2026-04-09T22:05:00Z
---
# Phase 14 Plan 01: Mobile Modal Fix Summary
**Fix: modal rezerwacji przeniesiony do document.body — działa na mobile/tablet niezależnie od widoczności sekcji Elementor.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~15min |
| Started | 2026-04-09 |
| Completed | 2026-04-09 |
| Tasks | 1 completed |
| Files modified | 1 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Modal otwiera się na mobile | Pass | Zweryfikowane Playwright 375×812 |
| AC-2: Brak regresji desktop | Pass | Desktop bez zmian — overlay i tak jest position:fixed |
## Accomplishments
- Modal rezerwacji działa na mobile i tablet — klienci mogą składać rezerwacje z telefonu
- Pre-fill z hero search form działa poprawnie na mobile po fixie
## Root Cause
Sekcja Elementor (`elementor-element-a7629b1`, klasa `header-box`) miała ustawione `elementor-hidden-tablet elementor-hidden-mobile` w edytorze Elementor. To powodowało `display:none` na rodzicu modala. Modal `position:fixed` wewnątrz ukrytego elementu miał `width:0, height:0` — był w DOM, miał klasę `is-open`, ale był niewidoczny.
## Fix
Jedna linijka w `initRefs()` (carei-reservation.js:90):
```js
if (overlay && overlay.parentElement !== document.body) {
document.body.appendChild(overlay);
}
```
Przeniesienie overlay do `<body>` gwarantuje że `position:fixed` działa względem viewport, niezależnie od struktury Elementor.
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `wp-content/plugins/carei-reservation/assets/js/carei-reservation.js` | Modified | appendChild overlay do body w initRefs() |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| appendChild do body (nie CSS override) | Niezależne od przyszłych zmian responsive w Elementor | Trwały fix, zero ryzyka regresji |
## Deviations from Plan
None — plan wykonany dokładnie jak opisany.
## Issues Encountered
None.
## Next Phase Readiness
**Ready:**
- Phase 13 (Pakiety ochronne) — BLOCKED, czeka na potwierdzenie klienta
**Concerns:**
- None
**Blockers:**
- Phase 13 zablokowana — potrzebna odpowiedź klienta o źródle danych cenowych
---
*Phase: 14-mobile-modal-fix, Plan: 01*
*Completed: 2026-04-09*