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

240 lines
11 KiB
Markdown

---
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
---
<objective>
## 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).
</objective>
<context>
<clarifications>
- **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.
</clarifications>
## 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
</context>
<acceptance_criteria>
## 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
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: REST endpoint /availability/bounds</name>
<files>wp-content/plugins/yacht-booking-system/api/class-rest-controller.php</files>
<action>
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).
</action>
<verify>
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ś).
</verify>
<done>AC-1, AC-2 spełnione.</done>
</task>
<task type="auto">
<name>Task 2: JS bootstrap bounds + validRange</name>
<files>wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar-all.js</files>
<action>
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`.
</action>
<verify>
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.
</verify>
<done>AC-3, AC-4, AC-5, AC-6 spełnione.</done>
</task>
<task type="auto">
<name>Task 3: Bump wersji pluginu</name>
<files>wp-content/plugins/yacht-booking-system/yacht-booking-system.php</files>
<action>
Zmień `Version: 1.2.0` → `Version: 1.2.1` i `define( 'YACHT_BOOKING_VERSION', '1.2.0' )` → `'1.2.1'` (2 wystąpienia).
</action>
<verify>`grep -n "1.2.1"` → 2 trafienia.</verify>
<done>Wersja podbita; assets cache busted po deploy.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
- 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.
</what-built>
<how-to-verify>
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.
</how-to-verify>
<resume-signal>Wpisz "approved" lub opisz problemy.</resume-signal>
</task>
</tasks>
<boundaries>
## 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).
</boundaries>
<verification>
- [ ] `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
</verification>
<success_criteria>
- 3 zadania auto + checkpoint zatwierdzone
- Brak fatal errors / warnings PHP
- Klient zaakceptował zachowanie nawigacji
- Plugin v1.2.1 deployed
</success_criteria>
<output>
After completion, create `.paul/phases/09-finalizacja/09-06-SUMMARY.md`
</output>