Files
2026-05-08 00:12:37 +02:00

318 lines
17 KiB
Markdown

---
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
---
<objective>
## 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).
</objective>
<context>
<clarifications>
- **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).
</clarifications>
## 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
</context>
<acceptance_criteria>
## 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
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: REST — tytuł z `_booking_notes` lub `customer_name` + split per-day</name>
<files>wp-content/plugins/yacht-booking-system/api/class-rest-controller.php</files>
<action>
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).
</action>
<verify>
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.
</verify>
<done>AC-1, AC-2, AC-6 spełnione; każdy booking emituje N eventów po jednej nocy każdy.</done>
</task>
<task type="auto">
<name>Task 2: Frontend JS — render tytułu + half-day per single-day event</name>
<files>wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar-all.js</files>
<action>
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 = $('<div class="yc-event-title"></div>').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).
</action>
<verify>
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.
</verify>
<done>AC-3, AC-5 spełnione.</done>
</task>
<task type="auto">
<name>Task 3: CSS — gap między dziennymi paskami + styl tytułu w pasku</name>
<files>wp-content/plugins/yacht-booking-system/frontend/assets/css/calendar-all.css</files>
<action>
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).
</action>
<verify>
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.
</verify>
<done>AC-4 spełnione (per-day segmenty z gap), AC-3 wzmocnione (czytelny tytuł).</done>
</task>
<task type="auto">
<name>Task 4: Bump wersji pluginu (cache busting JS/CSS)</name>
<files>wp-content/plugins/yacht-booking-system/yacht-booking-system.php</files>
<action>
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).
</action>
<verify>
`grep -n "1.2.0" wp-content/plugins/yacht-booking-system/yacht-booking-system.php` → 2 trafienia (Version: header + define).
</verify>
<done>Wersja pluginu podbita; po deploy klient widzi nowe assety bez ręcznego czyszczenia cache.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
- 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.
</what-built>
<how-to-verify>
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).
</how-to-verify>
<resume-signal>Wpisz "approved" żeby zamknąć plan, albo opisz co poprawić.</resume-signal>
</task>
</tasks>
<boundaries>
## 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).
</boundaries>
<verification>
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
</verification>
<success_criteria>
- Wszystkie 4 zadania auto + checkpoint zatwierdzone
- Brak fatal errors / warnings PHP w logach
- Klient zaakceptował wzrokowy efekt na produkcji
- Plugin v1.2.0 deployed
</success_criteria>
<output>
After completion, create `.paul/phases/09-finalizacja/09-05-SUMMARY.md`
</output>