--- phase: 09-finalizacja plan: 06 type: execute wave: 1 depends_on: ["09-05"] 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/yacht-booking-system.php autonomous: false delegation: off --- ## Goal Ograniczyć nawigację na kalendarzu zbiorczym `[yacht_calendar_all]` (`/rezerwacja/`): - Przycisk **"prev"** zablokowany gdy aktualny widok = bieżący miesiąc (nie da się cofnąć w przeszłość). - Przycisk **"next"** zablokowany gdy nie istnieje żadna rezerwacja w miesiącach późniejszych niż widok bieżący — efektywnie: max widok = miesiąc, w którym kończy się ostatnia rezerwacja. ## Purpose Klient nie chce, żeby odwiedzający stronę `/rezerwacja/` przeglądali historię rezerwacji ani puste przyszłe miesiące — kalendarz ma pokazywać tylko sensowny przedział "od dziś do ostatniej znanej rezerwacji". ## Output - Nowy REST endpoint `GET /yacht-booking/v1/availability/bounds` zwracający `{ max_booking_date: 'YYYY-MM-DD' | null }` (najpóźniejsza data zakończenia rezerwacji confirmed/pending). - JS `calendar-all.js` przed inicjalizacją FullCalendar fetchuje bounds i ustawia `validRange` blokujący nawigację. - Plugin bumped → 1.2.1 (cache busting). - **Nawigacja w przód** — Jak ma działać blokada w przód? → Odpowiedź: Do miesiąca ostatniej rezerwacji (next aktywny tylko gdy istnieje rezerwacja w późniejszym miesiącu). - **Zakres** — Który kalendarz dotyczy tej zmiany? → Odpowiedź: Tylko widget zbiorczy `[yacht_calendar_all]` na `/rezerwacja/`. Single-yacht (`/rezerwacja-maja/` itp.) bez zmian. - **Granica wstecz** — Od kiedy nie można się cofać? → Odpowiedź: Bieżący miesiąc (dziś) — prev wyłączony gdy widok = aktualny miesiąc. ## Project Context @.paul/PROJECT.md @.paul/STATE.md @.paul/codebase/architecture.md ## 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/yacht-booking-system.php ## AC-1: REST endpoint zwraca max booking date ```gherkin Given baza zawiera rezerwacje confirmed/pending z `_booking_end_date` od 2026-05-15 do 2026-08-20 When frontend wywołuje GET /wp-json/yacht-booking/v1/availability/bounds Then odpowiedź = `{ "max_booking_date": "2026-08-20" }` (HTTP 200, public, brak nonce required) ``` ## AC-2: Brak rezerwacji → null ```gherkin Given baza nie zawiera żadnych rezerwacji confirmed/pending z `_booking_end_date >= dziś` When frontend wywołuje GET /availability/bounds Then odpowiedź = `{ "max_booking_date": null }` ``` ## AC-3: Prev disabled w bieżącym miesiącu ```gherkin Given strona /rezerwacja/ wczytana, widok = aktualny miesiąc (dziś) When kalendarz wyrenderowany Then przycisk "prev" w toolbar FC ma atrybut `disabled` (lub klasę `fc-button-disabled`); klik nie zmienia widoku And przycisk "today" również zachowuje się normalnie (jest na bieżącym miesiącu — nic się nie zmienia) ``` ## AC-4: Next disabled na/po miesiącu ostatniej rezerwacji ```gherkin Given max_booking_date = "2026-08-20", widok bieżący = sierpień 2026 When kalendarz wyrenderowany Then przycisk "next" wyłączony (validRange.end = 2026-09-01) ``` ## AC-5: Next disabled gdy brak przyszłych rezerwacji ```gherkin Given max_booking_date = null When kalendarz wyrenderowany na bieżącym miesiącu Then przyciski "prev" i "next" są wyłączone (validRange = bieżący miesiąc tylko) ``` ## AC-6: Nawigacja w obrębie zakresu działa ```gherkin Given max_booking_date = "2026-08-20", aktualny miesiąc = maj 2026, widok = maj 2026 When user klika "next" 3 razy Then widok zmienia się czerwiec → lipiec → sierpień; w sierpniu "next" jest wyłączony ``` Task 1: REST endpoint /availability/bounds wp-content/plugins/yacht-booking-system/api/class-rest-controller.php W `Rest_Controller::register_rest_routes()` dodaj rejestrację nowego route'u (obok istniejącego `/availability/all`): ```php register_rest_route( self::NAMESPACE, '/availability/bounds', array( 'methods' => 'GET', 'callback' => array( $this, 'get_availability_bounds' ), 'permission_callback' => '__return_true', ) ); ``` Dodaj nową metodę publiczną `get_availability_bounds( $request )`: - Zapytanie WP_Query lub `get_posts` o `post_type = yacht_booking`, `post_status = publish`, status confirmed/pending (`_booking_status` IN), `posts_per_page = 1`, sortowane malejąco po `_booking_end_date` (meta_key + orderby=meta_value + meta_type=DATE). - Pobierz `_booking_end_date` z pierwszego wyniku. - Walidacja: jeśli niepuste i pasuje do `^\d{4}-\d{2}-\d{2}$` → zwróć `array( 'max_booking_date' => $date )`. W przeciwnym razie `array( 'max_booking_date' => null )`. - Filtruj tylko rezerwacje z `_booking_end_date >= dziś` (gmdate('Y-m-d')) — historyczne nie powinny rozszerzać zakresu. Avoid: dotykać innych endpointów, nie zmieniaj `get_all_availability`. Brak `permission_callback => 'admin'` — endpoint publiczny (potrzebny przy renderze widgetu dla anonimowych userów). 1. `php -l class-rest-controller.php` → no syntax errors. 2. Po deploy: `curl 'https://jachty3.pagedev.pl/wp-json/yacht-booking/v1/availability/bounds'` → JSON z polem `max_booking_date`. 3. Wartość powinna pasować do najpóźniejszej rezerwacji w bazie (confirmed/pending, end_date >= dziś). AC-1, AC-2 spełnione. Task 2: JS bootstrap bounds + validRange wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar-all.js Zmień `initCalendar(wrapper)` tak, by przed `new FullCalendar.Calendar(...)` wykonał `$.getJSON` do `/availability/bounds` i ustawił `validRange` na bazie odpowiedzi: 1. Helper na początku pliku (poza initCalendar): ```js function firstOfMonth(d) { return new Date(d.getFullYear(), d.getMonth(), 1); } function nextMonthFirst(d) { return new Date(d.getFullYear(), d.getMonth() + 1, 1); } ``` 2. W `initCalendar`: wyciągnij `restBase` z `restUrl` (zamień `/availability/all` na bazową ścieżkę REST namespace, czyli najprościej: zbuduj `boundsUrl` przez `restUrl.replace(/\/availability\/all.*$/, '/availability/bounds')`). 3. Wykonaj `$.getJSON(boundsUrl)` PRZED instancjacją FC. W `done` callback: - `var today = new Date();` - `var rangeStart = firstOfMonth(today);` - `var maxDate = data && data.max_booking_date ? new Date(data.max_booking_date + 'T00:00:00') : null;` - `var rangeEnd;` - Jeśli `maxDate && maxDate >= today`: `rangeEnd = nextMonthFirst(maxDate);` (validRange.end jest exclusive — daje cały miesiąc maxDate) - W przeciwnym wypadku (null lub przeszłość): `rangeEnd = nextMonthFirst(today);` (tylko bieżący miesiąc dostępny) - Następnie wywołaj funkcję `buildCalendar(wrapper, $cal, restUrl, heightPx, rangeStart, rangeEnd)` która zawiera dotychczasową logikę inicjalizacji FC z dodanym `validRange: { start: rangeStart, end: rangeEnd }` w opcjach Calendar. - W `fail` callback: zbuduj kalendarz bez `validRange` (graceful degradation — lepsze niż brak kalendarza). 4. Pozostała logika (events fetch, eventDidMount, eventContent, form submit) bez zmian. Avoid: nie usuwaj istniejących handlerów (form submit, half-day gradient, tooltip). Nie zmieniaj `eventContent` ani `applyHalfDayGradient`. 1. W przeglądarce na /rezerwacja/: DevTools Network — request do `/availability/bounds` przed/równolegle do `/availability/all`. 2. Klik "prev" w bieżącym miesiącu — brak akcji; przycisk wizualnie disabled. 3. Klik "next" wielokrotny — kalendarz dochodzi do miesiąca ostatniej rezerwacji i tam się zatrzymuje. AC-3, AC-4, AC-5, AC-6 spełnione. Task 3: Bump wersji pluginu wp-content/plugins/yacht-booking-system/yacht-booking-system.php Zmień `Version: 1.2.0` → `Version: 1.2.1` i `define( 'YACHT_BOOKING_VERSION', '1.2.0' )` → `'1.2.1'` (2 wystąpienia). `grep -n "1.2.1"` → 2 trafienia. Wersja podbita; assets cache busted po deploy. - REST `/availability/bounds` zwraca `max_booking_date` (data ostatniej rezerwacji confirmed/pending). - JS przed init kalendarza pobiera bounds i ustawia `validRange` (start = pierwszy dzień bieżącego miesiąca, end = pierwszy dzień miesiąca PO miesiącu max_booking_date — exclusive). - Prev disabled na bieżącym miesiącu, next disabled na/po miesiącu ostatniej rezerwacji. - Plugin v1.2.1. 1. Wgraj 3 zmienione pliki przez ftp-kr. 2. Otwórz https://jachty3.pagedev.pl/rezerwacja/ z Ctrl+F5. 3. Zweryfikuj: - Prev przycisk wyglądnięci (disabled / wyszarzony) bo widok = bieżący miesiąc. - Klikaj "next" — kalendarz przechodzi przez miesiące tylko do miesiąca, w którym kończy się ostatnia rezerwacja. - W ostatnim miesiącu z rezerwacjami next staje się disabled. 4. DevTools → Network → request `/availability/bounds` zwraca poprawny JSON. 5. Edge case (jeśli możliwe do przetestowania): wyłącz wszystkie przyszłe rezerwacje → reload → kalendarz pokazuje tylko bieżący miesiąc, oba przyciski disabled. Wpisz "approved" lub opisz problemy. ## DO NOT CHANGE - `get_all_availability()` w REST — kontrakt eventów per-day allDay zachowany. - Single-yacht widget (`frontend/class-calendar-widget.php`, `calendar.js`, `calendar.css`) — out of scope. - Inquiry form i jego submit handler — out of scope. - `eventContent`, `applyHalfDayGradient`, `eventDidMount` — bez zmian. ## SCOPE LIMITS - Plan dotyczy tylko widgetu zbiorczego na `/rezerwacja/`. - NIE dodajemy admin-only logiki (publiczny endpoint). - NIE cache'ujemy odpowiedzi `/bounds` długoterminowo (jeden fetch per page load wystarczy; dane zmieniają się rzadko). - NIE zmieniamy nawigacji per-rok (tylko miesiąc-po-miesiącu). - [ ] `php -l` na class-rest-controller.php pass - [ ] curl `/bounds` zwraca poprawny JSON - [ ] Prev disabled w bieżącym miesiącu - [ ] Next disabled na miesiącu max_booking_date - [ ] Wszystkie AC spełnione - 3 zadania auto + checkpoint zatwierdzone - Brak fatal errors / warnings PHP - Klient zaakceptował zachowanie nawigacji - Plugin v1.2.1 deployed After completion, create `.paul/phases/09-finalizacja/09-06-SUMMARY.md`