--- phase: 09-finalizacja plan: 05 type: execute wave: 1 depends_on: ["09-04"] files_modified: - wp-content/plugins/yacht-booking-system/api/class-rest-controller.php - wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar-all.js - wp-content/plugins/yacht-booking-system/frontend/assets/css/calendar-all.css - wp-content/plugins/yacht-booking-system/yacht-booking-system.php autonomous: false delegation: off --- ## Goal Pokazać tytuł rezerwacji (z Google Calendar bez prefiksu "GCal:" / "GCal (wspólny):") na paskach kalendarza zbiorczego `[yacht_calendar_all]` na stronie `/rezerwacja/` oraz rozbić ciągłą belkę wielodniową na osobne segmenty per dzień z widocznym odstępem. ## Purpose Klient zmienił zdanie po wdrożeniu 09-04 — chce identyfikować rezerwacje wzrokowo bez wchodzenia w szczegóły, w formacie zgodnym z tym co wpisuje w Google Calendar (np. "Maja - Kowalscy 5 osób"). Dodatkowo prosi o czytelniejszy podział wizualny gdy jedna rezerwacja zajmuje kilka dni — żeby na pierwszy rzut oka było widać że to oddzielne noce, a nie jedna ciągła sesja. ## Output - REST `/availability/all` zwraca w `title` raw SUMMARY (z `_booking_notes` dla iCal) lub `customer_name` (dla rezerwacji frontowych) — bez prefiksu "GCal". - Frontend renderuje tytuł w pasku eventu (eventContent zwraca tekst zamiast pustego HTML). - Każdy event z REST jest rozbity na N eventów per dzień (1 event = 1 dzień), więc FullCalendar renderuje N osobnych pasków zamiast jednej belki. - CSS dodaje pionowy gap (margin/padding) między dziennymi paskami. - Pierwszy/ostatni dzień rezerwacji zachowuje half-day gradient (yacht odbierany/zwracany w południe). - **Co pokazać** — Co wyświetlić jako tytuł rezerwacji w pasku? → Odpowiedź: Tytuł zaimportowany z Google Calendar bez prefiksu "GCal:" (czyli raw SUMMARY z `_booking_notes`). - **Widoczność** — Dla kogo widoczne mają być te dane? → Odpowiedź: Publicznie (dla wszystkich odwiedzających). - **Global eventy** — Co z eventami z trybu global iCal (yacht_id=0)? → Odpowiedź: Pokazać tytuł z iCal (raw SUMMARY z `_booking_notes`) — to co właściciel wpisuje w GCal. - **Styl belki** — Jak rozdzielić dni rezerwacji wizualnie? → Odpowiedź: Osobny segment per dzień z gap (każdy dzień = osobny pasek, między nimi odstęp). ## Project Context @.paul/PROJECT.md @.paul/STATE.md @.paul/codebase/architecture.md ## Prior Work @.paul/phases/09-finalizacja/09-04-SUMMARY.md (wprowadziło privacy hardening — w tym planie świadomie cofamy ukrycie tytułu, ALE bez ujawniania `customer_name` dla rezerwacji frontowych… patrz boundaries) ## Source Files @wp-content/plugins/yacht-booking-system/api/class-rest-controller.php @wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar-all.js @wp-content/plugins/yacht-booking-system/frontend/assets/css/calendar-all.css @wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php @wp-content/plugins/yacht-booking-system/includes/class-booking.php ## AC-1: Tytuł rezerwacji z iCal w REST ```gherkin Given booking utworzony przez import iCal (`_booking_source` = `ical_global_calendar` lub `ical_import_global`) z `_booking_notes` = "Maja - Kowalscy 5 osób" When frontend wywołuje GET /wp-json/yacht-booking/v1/availability/all Then odpowiedź zawiera event z `title` = "Maja - Kowalscy 5 osób" (bez prefiksu "GCal:" / "GCal (wspólny):") ``` ## AC-2: Tytuł rezerwacji frontowej ```gherkin Given booking utworzony przez frontend POST /bookings (`_booking_source` puste, `_booking_customer_name` = "Jan Kowalski") When frontend wywołuje GET /availability/all Then event ma `title` = "Jan Kowalski" (customer_name) ``` ## AC-3: Tytuł renderowany w pasku eventu ```gherkin Given strona /rezerwacja/ z widgetem [yacht_calendar_all] i co najmniej jednym eventem w bieżącym miesiącu When kalendarz się wyrenderuje Then pasek eventu zawiera tekst tytułu (nie pusty); tekst nie jest obcinany na wąskich segmentach (ellipsis OK) ``` ## AC-4: Per-day segmenty z gap ```gherkin Given rezerwacja trwająca 4 dni (np. 2026-06-01 → 2026-06-05) When kalendarz wyrenderuje ten miesiąc Then w siatce widoczne są 4 osobne paski (po jednym w każdej z 4 komórek dni — bez ostatniego dnia checkout 5go), z widocznym pionowym odstępem między dolną krawędzią paska dnia N a górną krawędzią paska dnia N+1 w sąsiadujących wierszach (lub wewnątrz wiersza — komórki separowane gridem) And FullCalendar nie renderuje jednej ciągłej belki spanning 4 dni ``` ## AC-5: Half-day visual zachowany ```gherkin Given rezerwacja 2026-06-01 → 2026-06-05 (yacht odbierany 1.06 w południe, zwracany 5.06 w południe) When kalendarz się wyrenderuje Then segment 2026-06-01 ma lewą połowę transparentną (gradient), segment 2026-06-04 ma prawą połowę transparentną; środkowe dni (2/06, 3/06) pełny kolor ``` ## AC-6: Privacy — brak ujawnienia `customer_email`/`customer_phone` ```gherkin Given dowolny booking When REST zwraca eventy Then odpowiedź NIE zawiera `customer_email` ani `customer_phone` w żadnym polu ``` Task 1: REST — tytuł z `_booking_notes` lub `customer_name` + split per-day wp-content/plugins/yacht-booking-system/api/class-rest-controller.php W `Rest_Controller::get_all_availability()` (api/class-rest-controller.php:355): 1. Zastąp logikę budowania `$title` (linie ~424-436) nową: - Pobierz `$source = get_post_meta($booking_id, '_booking_source', true)`. - Pobierz `$notes = get_post_meta($booking_id, '_booking_notes', true)`. - Jeśli `$source` jest jednym z `ICal_Import::GLOBAL_CALENDAR_SOURCE` lub `ICal_Import::GLOBAL_IMPORT_SOURCE` → `$title = $notes` jeśli niepuste, fallback `__('Rezerwacja', 'yacht-booking')`. - W przeciwnym razie (booking frontowy) → `$title = Booking::get_customer_name($booking_id)`, fallback `__('Rezerwacja', 'yacht-booking')`. - Sanitize: `$title = sanitize_text_field($title)`. - Usuń obsługę `$is_global_mode` zmieniającą tytuł na "Rezerwacja" — nawet w trybie global pokazujemy raw SUMMARY (per decyzji klienta). 2. Zastąp pojedynczy event `events[] = [...]` z `start = $start_date.'T12:00:00'` / `end = $end_date.'T12:00:00'` na pętlę emitującą JEDEN event per noc rezerwacji: - `$cursor = new DateTimeImmutable($start_date)` ; `$end_dt = new DateTimeImmutable($end_date)`. - Dopóki `$cursor < $end_dt` (bo end_date to dzień checkout — yacht zwracany w południe, pełna noc kończy się dzień wcześniej): - `$day = $cursor->format('Y-m-d')`. - `$is_first = ($day === $start_date)`. - `$next = $cursor->modify('+1 day')->format('Y-m-d')`. - `$is_last_night = ($next === $end_date)`. - Wyemituj event: ```php $events[] = [ 'id' => $booking_id . '-' . $day, 'title' => $title, 'start' => $day . 'T12:00:00', 'end' => $next . 'T12:00:00', 'color' => $color, 'yacht_id' => $y_id, 'extendedProps' => [ 'is_first' => $is_first, 'is_last_night' => $is_last_night, 'booking_id' => $booking_id, ], ]; ``` - `$cursor = $cursor->modify('+1 day')`. - Każdy emitowany event obejmuje DOKŁADNIE jedną dobę (12:00 → następny dzień 12:00). FC dayGrid wyrenderuje go jako blok TYLKO w komórce dnia rozpoczęcia (bo timed event z start.day < end.day i krótszy niż 24h+1 — w praktyce 24h — renderuje się w day cell startu). 3. Boundary: NIE dodawaj `customer_email`, `customer_phone`, `customer_name` do payloadu poza `title` jeśli source = frontend (i to świadomie — klient zaakceptował publiczną widoczność imienia/nazwiska składającego rezerwację). Avoid: usuwania starego endpointu `/availability/{yacht_id}` (out of scope), modyfikacji `Rest_Controller::YACHT_COLOR_PALETTE`, generowania title po stronie JS (single source of truth = REST). 1. `php -l wp-content/plugins/yacht-booking-system/api/class-rest-controller.php` → no syntax errors. 2. Po deploy: `curl 'https://jachty3.pagedev.pl/wp-json/yacht-booking/v1/availability/all?start=2026-05-01&end=2026-09-01' | jq` → - Każdy booking imported z iCal ma N eventów (N = liczba nocy), każdy z title = SUMMARY bez "GCal:". - Brak pól `customer_email`, `customer_phone`. - extendedProps.is_first true tylko na pierwszym evencie z danego booking_id. AC-1, AC-2, AC-6 spełnione; każdy booking emituje N eventów po jednej nocy każdy. Task 2: Frontend JS — render tytułu + half-day per single-day event wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar-all.js W `calendar-all.js`: 1. Zastąp `eventContent` (linie 73-76) implementacją renderującą tytuł: ```js eventContent: function (arg) { var title = arg.event.title || ''; var $el = $('
').text(title); return { domNodes: [ $el.get(0) ] }; } ``` — `$().text()` zapewnia escaping (XSS safe). 2. Zaktualizuj `applyHalfDayGradient(info)` (linia 147+): - Zamiast `info.isStart` / `info.isEnd` (które dla single-day eventów ZAWSZE są oba true → wpadałoby w "single segment containing both start and end") → odczytuj z `extendedProps`: ```js var isFirstDay = info.event.extendedProps && info.event.extendedProps.is_first; var isLastNight = info.event.extendedProps && info.event.extendedProps.is_last_night; ``` - `startTrans = isFirstDay`, `endTrans = isLastNight`. - Jeśli `isFirstDay && isLastNight && (booking 1-nocny)` → tak jak było (gradient przezroczysty po obu stronach). - Pozostała logika gradient stops bez zmian. - Reszta (środkowa noc) → pełny kolor. 3. Boundary: NIE zmieniaj logiki `events:` fetch (REST URL bez zmian — endpoint ten sam, tylko payload zmieniony).
1. W przeglądarce na `/rezerwacja/`: każdy pasek pokazuje tytuł z `_booking_notes`. 2. DevTools: dla rezerwacji wielonocnej pierwszy pasek ma `background-image: linear-gradient` z transparent po lewej, ostatni z transparent po prawej, środkowe pełny kolor. AC-3, AC-5 spełnione.
Task 3: CSS — gap między dziennymi paskami + styl tytułu w pasku wp-content/plugins/yacht-booking-system/frontend/assets/css/calendar-all.css W `calendar-all.css`: 1. Usuń lub przekomentuj regułę ukrywającą tytuł (linie 89-93): ```css .yacht-calendar-all .fc-event-title, .yacht-calendar-all .fc-daygrid-event-dot, .yacht-calendar-all .fc-event-time { display: none !important; } ``` Zostaw `display: none` dla `.fc-daygrid-event-dot` i `.fc-event-time`, zostaw widoczne `.fc-event-title` (lub po prostu nie ruszaj — eventContent zwraca custom DOM, a fc-event-title FC nie wstawi). 2. Dodaj styl dla custom kontenera tytułu: ```css .yacht-calendar-all .yc-event-title { padding: 1px 6px; font-size: 11px; font-weight: 600; color: #fff; text-shadow: 0 1px 1px rgba(0,0,0,0.35); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.3; } ``` 3. Dodaj gap pionowy między dziennymi paskami (per-day segmenty leżą obok siebie w sąsiednich komórkach — gap zapewnia margin): ```css .yacht-calendar-all .fc-daygrid-event { margin: 1px 2px !important; border-radius: 2px; } ``` Komórki dnia w FC są separowane border-collapse — `margin: 1px 2px` na evencie tworzy widoczną przerwę po lewej i prawej krawędzi paska, co wizualnie segmentuje rezerwację wielodniową. 4. Mobile (max-width: 600px): zmniejsz font-size tytułu do 10px (już jest reguła `.fc-event { font-size: 10px }` — uzupełnij `.yc-event-title { font-size: 10px }` w tej media query). 1. W przeglądarce: rezerwacja 4-nocna wygląda jak 4 osobne paski z 4px łączną przerwą między nimi (2px lewa + 2px prawa kolejnego). 2. Tytuł widoczny na każdym pasku z ellipsis przy wąskich kolumnach. AC-4 spełnione (per-day segmenty z gap), AC-3 wzmocnione (czytelny tytuł). Task 4: Bump wersji pluginu (cache busting JS/CSS) wp-content/plugins/yacht-booking-system/yacht-booking-system.php W headerze pluginu i stałej `YACHT_BOOKING_VERSION` zmień `1.1.0` → `1.2.0`. To wymusza odświeżenie cache assets przez `wp_enqueue_scripts` (filemtime fallback nie działa wszędzie po FTP deploy). `grep -n "1.2.0" wp-content/plugins/yacht-booking-system/yacht-booking-system.php` → 2 trafienia (Version: header + define). Wersja pluginu podbita; po deploy klient widzi nowe assety bez ręcznego czyszczenia cache. - REST `/availability/all` zwraca eventy per noc z tytułem z `_booking_notes` (iCal) lub `customer_name` (frontend). - Frontend renderuje tytuł w pasku eventu. - Każda doba rezerwacji to osobny pasek z 2px gapem po bokach (efekt segmentów). - Half-day gradient zachowany na pierwszej/ostatniej dobie rezerwacji. 1. Po deploy FTP odwiedź https://jachty3.pagedev.pl/rezerwacja/ (twardy reload Ctrl+F5). 2. Zweryfikuj wzrokowo: - Każdy pasek rezerwacji pokazuje tytuł zgodny z tym co wpisałeś w Google Calendar (np. "Maja - Kowalscy") bez prefiksu "GCal:". - Rezerwacja wielodniowa wygląda jak osobne kafelki z widocznym odstępem (a nie jedna ciągła belka). - Pierwszy dzień rezerwacji ma lewą połowę "pustą" (gradient), ostatni dzień prawą — to half-day visual. 3. DevTools → Network → `/availability/all`: payload zawiera N eventów per booking, brak `customer_email`/`customer_phone`. 4. Test edge case: jednodniowa rezerwacja (1 noc) — powinna mieć obie połówki transparentne (gradient z obu stron). Wpisz "approved" żeby zamknąć plan, albo opisz co poprawić.
## DO NOT CHANGE - `wp-content/plugins/yacht-booking-system/api/class-rest-controller.php` — endpoint `/availability/{yacht_id}` (per-yacht, używa innego frontu — `[yacht_calendar yacht_id="X"]`). - `wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar.js` (single-yacht widget — out of scope). - `wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php` (`_booking_notes` jest już zapisywany — nie trzeba ruszać). - `Rest_Controller::YACHT_COLOR_PALETTE` i `GLOBAL_EVENT_COLOR` (kolory pozostają, paleta per-yacht działa nadal). - Wszystko związane z formularzem inquiry (right-side panel) — out of scope. ## SCOPE LIMITS - Plan dotyczy WYŁĄCZNIE widgetu zbiorczego `[yacht_calendar_all]` na `/rezerwacja/`. Single-yacht widget pozostaje bez zmian. - NIE rozszerzamy payloadu REST o `customer_email` / `customer_phone` — tylko `title` (klient zaakceptował publiczną widoczność tytułu, ale email/telefon zostają prywatne). - NIE wprowadzamy admin-only fallback dla privacy — zgodnie z odpowiedzią klienta widoczność jest publiczna dla wszystkich. - Rewersal privacy z 09-04: świadoma decyzja klienta. Odnotować w SUMMARY że hardening tytułów został cofnięty per żądanie biznesu (security audit 09-06 powinien to uwzględnić jeśli zmieni zdanie). Before declaring plan complete: - [ ] `php -l class-rest-controller.php` passes - [ ] Deploy via ftp-kr (4 zmodyfikowane pliki) - [ ] curl `/availability/all` zwraca N eventów per booking, title z `_booking_notes` / `customer_name`, brak email/phone - [ ] Strona `/rezerwacja/` (Ctrl+F5): tytuły widoczne, paski per-day rozdzielone gapem, half-day na first/last - [ ] Brak regresji w widgecie single-yacht (np. `/rezerwacja-maja/`) - [ ] Wszystkie acceptance criteria spełnione - Wszystkie 4 zadania auto + checkpoint zatwierdzone - Brak fatal errors / warnings PHP w logach - Klient zaakceptował wzrokowy efekt na produkcji - Plugin v1.2.0 deployed After completion, create `.paul/phases/09-finalizacja/09-05-SUMMARY.md`