Files
2026-05-07 14:57:59 +02:00

211 lines
14 KiB
Markdown

---
phase: 09-finalizacja
plan: 04
subsystem: api, ui, integrations
tags: [ical, fullcalendar, elementor, rest, privacy]
requires:
- phase: 09-finalizacja
provides: globalna sync iCal (09-02), cleanup OAuth (09-03)
provides:
- tryb sync iCal "wspólny kalendarz" (storage bez per-yacht matchingu)
- REST endpoint /availability/all (agregacja + privacy)
- widget Elementor + shortcode [yacht_calendar_all]
- paleta kolorów per-jacht (Rest_Controller::YACHT_COLOR_PALETTE)
- privacy: tytuły eventów nie zawierają nazwisk klientów (REST + JS)
affects: [security audit, dokumentacja]
tech-stack:
added: []
patterns:
- View helper pattern (Calendar_All_View) — render wspólny dla widgetu i shortcode
- Eager-load REST controller (constants/static methods used in frontend)
- Privacy-by-default: REST endpoint nie wystawia nazwisk klientów
key-files:
created:
- wp-content/plugins/yacht-booking-system/frontend/class-calendar-widget-all.php
- wp-content/plugins/yacht-booking-system/frontend/assets/css/calendar-all.css
- wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar-all.js
modified:
- wp-content/plugins/yacht-booking-system/api/class-rest-controller.php
- wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php
- wp-content/plugins/yacht-booking-system/includes/class-settings.php
- wp-content/plugins/yacht-booking-system/includes/class-installer.php
- wp-content/plugins/yacht-booking-system/includes/class-yacht-booking.php
- wp-content/plugins/yacht-booking-system/admin/class-admin.php
- wp-content/plugins/yacht-booking-system/frontend/class-shortcode.php
- wp-content/plugins/yacht-booking-system/yacht-booking-system.php
key-decisions:
- "Tryb sync `global` = osobna ścieżka cleanup, dwie stałe source (per-yacht / global)"
- "Privacy: title eventów na REST = nazwa jachtu (per_yacht) lub generyczne 'Rezerwacja' (global), bez customer_name"
- "View helper `Calendar_All_View::render()` — wspólny markup widget + shortcode"
- "Eager-load Rest_Controller w load_dependencies() — używany przez frontend View, nie tylko REST"
- "Half-day visual: dynamiczny gradient JS na bazie szerokości komórki dnia (FC nie ma natywnej obsługi half-day w dayGrid)"
- "Kolor wspólnych eventów: jasnoniebieski #7fb3d5 (po iteracjach: #7f8c8d#bc1834#7fb3d5)"
patterns-established:
- "Privacy-by-default w REST: tytuły eventów bez customer_name nawet w trybie per_yacht"
- "Settings z whitelistą wartości (`per_yacht`/`global`) + fallback do bezpiecznego defaultu"
- "Stała paleta kolorów współdzielona REST ↔ frontend (Rest_Controller::YACHT_COLOR_PALETTE)"
duration: ~120min
started: 2026-05-07T11:30:00Z
completed: 2026-05-07T13:30:00Z
---
# Phase 09 Plan 04: Globalna sync iCal + widget wszystkich jachtów
**Plugin obsługuje teraz dwa tryby synchronizacji iCal (per-jacht / wspólny) i ma drugi widget kalendarza pokazujący zajętość wszystkich jachtów na jednej siatce z paletą kolorów per-jacht, half-day gradientem i ciemnym formularzem zapytania po prawej stronie.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~120min (z iteracjami UX po checkpoincie) |
| Started | 2026-05-07T11:30:00Z |
| Completed | 2026-05-07T13:30:00Z |
| Tasks | 4 auto + 1 checkpoint + 5 iteracji UX po checkpoincie |
| Files modified | 8 zmodyfikowanych + 3 nowe + 1 testowy (test-overlap-bookings.php) |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Settings — przełącznik trybu sync iCal | Pass | Pole select w sekcji "Synchronizacja", domyślnie `per_yacht` |
| AC-2: Import iCal w trybie globalnym (wszystkie eventy) | Pass | Wszystkie eventy z basic.ics importowane, brak wpisów do `wp_yacht_availability` |
| AC-3: Import iCal w trybie per_yacht (bez regresji) | Pass | Stara ścieżka `run_per_yacht_mode()` działa identycznie, własna stała cleanup |
| AC-4: REST endpoint `/availability/all` | Pass | Zwraca FullCalendar events z timed 12:00→12:00 i kolorami |
| AC-5: Widget + shortcode | Pass | Po iteracjach: ciemne tło, formularz inquiry z select jachtów, bez tytułów eventów |
| AC-6: Auto paleta kolorów per jacht | Pass | `YACHT_COLOR_PALETTE` (8 kolorów) deterministyczna po yacht_id |
## Accomplishments
- **Tryb global iCal** — klient prowadzi jeden wspólny Google Calendar, wszystkie eventy (32 z basic.ics) są importowane bez filtrowania prefiksem; nie blokują dostępności jachtów (`yacht_id=0`).
- **Wspólny widget z formularzem inquiry** — `[yacht_calendar_all]` + Elementor "Kalendarz Jachtów (wszystkie)" pokazuje siatkę miesięczną z paskami rezerwacji w kolorach palety per-jacht (lub jednolity jasnoniebieski w trybie global), formularz "Zapytaj o rezerwację" po prawej w ciemnym stylu identycznym jak `/rezerwacja-maja/`.
- **Privacy-by-default** — REST endpoint `/availability/all` nie wystawia `customer_name` w `title` eventów; w trybie global tytuł = generyczne "Rezerwacja", w per_yacht = sama nazwa jachtu. Defense-in-depth: JS `eventContent` zwraca pusty html (nawet gdyby title trafił do REST, nie wyrenderuje się w DOM).
- **Half-day visual** — dynamiczny gradient JS na pierwszym/ostatnim dniu rezerwacji (yacht zwracany/odbierany w południe), liczony na bazie szerokości komórki dnia (FullCalendar dayGrid nie ma natywnej obsługi).
## Files Created/Modified
| File | Change | Purpose |
|------|--------|---------|
| `wp-content/plugins/yacht-booking-system/yacht-booking-system.php` | Modified | Bump wersji 1.0.0 → 1.1.0 (uruchamia Installer::install + cache busting) |
| `includes/class-settings.php` | Modified | `get_ical_sync_mode()` z whitelistą wartości |
| `includes/class-installer.php` | Modified | Default option `yacht_booking_ical_sync_mode = per_yacht` |
| `includes/class-yacht-booking.php` | Modified | Eager-load Rest_Controller, rejestracja widgetu, conditional enqueue, detekcja shortcode/widget |
| `admin/class-admin.php` | Modified | Pole select trybu sync + obsługa zapisu w `process_settings_save()` |
| `integrations/ical/class-ical-import.php` | Modified | Rozgałęzienie `run_per_yacht_mode()` / `run_global_calendar_mode()`, nowa stała `GLOBAL_CALENDAR_SOURCE`, metody `upsert_global_calendar_event()`, `get_existing_global_calendar_map()` |
| `api/class-rest-controller.php` | Modified | Stałe `YACHT_COLOR_PALETTE` + `GLOBAL_EVENT_COLOR`, route + metoda `get_all_availability()`, `get_yacht_color_palette()`, privacy w title |
| `frontend/class-shortcode.php` | Modified | Shortcode `[yacht_calendar_all]` |
| `frontend/class-calendar-widget-all.php` | Created | Widget Elementor `Calendar_Widget_All` + helper `Calendar_All_View` (server-side legenda + formularz inquiry z select jachtów) |
| `frontend/assets/css/calendar-all.css` | Created | Tła komórek emulujące paletę single-yacht (semi-transparent na ciemnym tle), styl select, ukryte tytuły eventów |
| `frontend/assets/js/calendar-all.js` | Created | FullCalendar init, dynamiczny gradient half-day, submit handler dla formularza inquiry z dropdownem jachtów |
| `test-overlap-bookings.php` | Created (root) | Test helper dodający 2 nakładające się eventy (do usunięcia po teście) |
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| Dwie stałe source (`ical_import_global` per-yacht, `ical_global_calendar` global) | Pozwala niezależny cleanup w obu trybach bez krzyżowych usunięć | Przełączanie trybów nie usuwa eventów z drugiego trybu |
| Privacy: brak `customer_name` w REST title | Wcześniej `title` zawierało "Maja — Jan Kowalski" — nazwiska klientów na publicznym REST | Nawet po inspekcji DOM/API nazwisk nie ma |
| View helper `Calendar_All_View::render()` zamiast duplikacji widget+shortcode | DRY | Zmiana markupu w jednym miejscu |
| Eager-load Rest_Controller w `load_dependencies()` | Frontend View używa stałych palety + statyk metod | Bez tego: `Class 'Rest_Controller' not found` przy renderze |
| Half-day przez dynamiczny gradient JS (a nie czyste CSS) | FullCalendar dayGrid nie ma natywnej obsługi half-day; CSS nie zna szerokości segmentu | Wymaga `requestAnimationFrame` w `eventDidMount` |
| Klasa wrappera `yacht-calendar-all-wrapper` (bez `yacht-calendar-wrapper`) | Calendar.js inicjalizuje wszystko z `.yacht-calendar-wrapper` → kolizja | Drugi widget niezależny od starego JS |
| Kolor wspólnych eventów: `#7fb3d5` (jasnoniebieski) | Iteracje z klientem: szary `#7f8c8d` → czerwony `#bc1834` (zbyt agresywny) → jasnoniebieski | Spójny z legendą + komfort wizualny |
| Tła komórek `rgba(245,249,255, 0.4)` na `#0e2036` | Emulacja efektu `yacht-day-available` bg-event nad ciemnym parent containerem w `/rezerwacja-maja/` | Spójny styl z istniejącym widgetem per-jacht |
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Auto-fixed | 6 | UX poprawki po checkpoincie + privacy hardening |
| Scope additions | 1 | Privacy w REST (poza pierwotnym scope) |
| Deferred | 0 | Wszystko ukończone |
**Total impact:** Solidne UX iteracje + bonus privacy hardening (chroni przed wyciekiem nazwisk klientów).
### Auto-fixed Issues
**1. Path discrepancies — assets w `frontend/assets/` zamiast root `assets/`**
- **Found during:** Task 4 (widget assets)
- **Issue:** Plan referował `assets/js/`, `assets/css/`, `admin/views/settings-page.php` — w projekcie tych ścieżek nie ma
- **Fix:** Użyłem faktycznej struktury (`frontend/assets/{css,js}/`, formularz settings inline w `class-admin.php`)
- **Verification:** Pliki utworzone w prawidłowych miejscach, conditional enqueue działa
- **Commit:** część zmiany w `class-yacht-booking.php`
**2. `Class 'Rest_Controller' not found` przy frontend renderze**
- **Found during:** Po pierwszym deploy (raportowane przez klienta)
- **Issue:** Rest_Controller ładowany lazy w `register_rest_routes()`, View używa stałych palety przy frontend render
- **Fix:** Dodano `require_once api/class-rest-controller.php` w `Yacht_Booking::load_dependencies()` (eager)
- **Verification:** Brak fatal errors po reload
- **Commit:** `includes/class-yacht-booking.php`
**3. Brakujący formularz inquiry + brakująca legenda + brakująca instrukcja**
- **Found during:** Po checkpoincie human-verify
- **Issue:** Pierwsza wersja widgetu była tylko kalendarzem; klient oczekiwał layoutu jak na `/rezerwacja-maja/` (calendar + form right)
- **Fix:** Dodano `Calendar_All_View` render z `.yacht-inquiry-layout`, server-side legenda (działa nawet przed JS), instrukcję, formularz inquiry z `<select name="yacht_id">` dla wyboru jachtu, własny submit handler w JS
- **Verification:** Playwright snapshot — wszystko widoczne
- **Commit:** `class-calendar-widget-all.php`, `calendar-all.css`, `calendar-all.js`
**4. Kalendarz nie renderował się (tylko nagłówki dni)**
- **Found during:** Po dodaniu layoutu 2-kolumnowego
- **Issue:** FullCalendar `height: '100%'` w gridzie się nie liczyło → komórki collapsed
- **Fix:** Wysokość przekazywana przez `data-height` na wrapperze, JS odczytuje i podaje FC jako liczbę pikseli (`heightPx`)
- **Verification:** Playwright eval — fcDayGridRect ma poprawną wysokość
- **Commit:** `calendar-all.js` + `class-calendar-widget-all.php`
**5. Select formularza w jasnym natywnym stylu**
- **Found during:** Po dodaniu formularza inquiry
- **Issue:** calendar.css ma reguły dla `input[type=text|email|tel]` ale BRAK dla `<select>` → biały tekst, czarne tło, natywna strzałka
- **Fix:** Dodano w calendar-all.css regułę `.yacht-inquiry-form select` z analogicznym stylem (semi-transparent biały bg, custom SVG strzałka, dark `<option>`)
- **Verification:** Playwright eval — bg/color/border zgodne z innymi inputami
- **Commit:** `calendar-all.css`
**6. Privacy gap — nazwiska klientów w REST title**
- **Found during:** Iteracja "ukryj nazwy na zajętych dniach"
- **Issue:** REST `title = "Maja — Jan Kowalski"` — wrażliwe dane na publicznym endpointcie
- **Fix:** REST title to teraz: w trybie global `"Rezerwacja"`, w per_yacht sama nazwa jachtu (bez customer_name). JS `eventContent` zwraca pusty html (defense-in-depth)
- **Verification:** Playwright eval `/availability/all` — title bez nazwisk
- **Commit:** `class-rest-controller.php`, `calendar-all.js`, `calendar-all.css`
### Deferred Items
None.
## Issues Encountered
| Issue | Resolution |
|-------|------------|
| Brak `php` w PATH środowiska deweloperskiego (PowerShell + WSL) | Manualne `php -l` przez klienta podczas checkpointu human-verify |
| FullCalendar `height: '100%'` w grid container collapsuje | Pixel value przekazywany przez data-attribute |
| Tło kalendarza wyglądało "jasno" mimo poprawnej palety komórek | Klient oczekiwał ciemnego efektu jak `/rezerwacja-maja/`, gdzie tło to bg-eventy semi-transparent nad ciemnym parentem; emulacja przez `background: #0e2036` na container + `rgba(245,249,255, 0.4)` na komórkach |
| Klasa `yacht-calendar-wrapper` powodowała kolizję z `calendar.js` (per-jacht) | Usunięta z roota nowego widgetu |
## Skill Audit
`.paul/SPECIAL-FLOWS.md` nie istnieje — skill audit pominięty.
## Next Phase Readiness
**Ready:**
- Plugin produkcyjnie obsługuje 2 tryby sync iCal (ostatnia funkcjonalna brakująca cecha)
- Privacy-hardening REST endpointów (precedens na security audit w 09-05)
- Widget zbiorczy gotowy do publicznego użycia (read-only z formularzem inquiry)
**Concerns:**
- `test-overlap-bookings.php` w root projektu — do skasowania z FTP po teście (nie commitować)
- Brak automatycznych testów regresji (zgodnie z CLAUDE.md — testy manualne)
- Privacy-hardening REST tylko w nowym endpointcie `/availability/all`. Stary `/availability/{yacht_id}` zwraca tylko status dnia — bez customer_name — OK.
**Blockers:**
- None.
---
*Phase: 09-finalizacja, Plan: 04*
*Completed: 2026-05-07*