17 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, delegation
| phase | plan | type | wave | depends_on | files_modified | autonomous | delegation | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09-finalizacja | 05 | execute | 1 |
|
|
false | off |
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/allzwraca wtitleraw SUMMARY (z_booking_notesdla iCal) lubcustomer_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).
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
<acceptance_criteria>
AC-1: Tytuł rezerwacji z iCal w REST
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
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
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
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
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
Given dowolny booking
When REST zwraca eventy
Then odpowiedź NIE zawiera `customer_email` ani `customer_phone` w żadnym polu
</acceptance_criteria>
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 = $('<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).
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_notesjest już zapisywany — nie trzeba ruszać).Rest_Controller::YACHT_COLOR_PALETTEiGLOBAL_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— tylkotitle(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).
<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>