240 lines
11 KiB
Markdown
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>
|