diff --git a/.paul/ROADMAP.md b/.paul/ROADMAP.md index 4d5743e..43fc7ba 100644 --- a/.paul/ROADMAP.md +++ b/.paul/ROADMAP.md @@ -48,8 +48,10 @@ Szczegóły w `wp-content/plugins/yacht-booking-system/PROJECT-STATUS.md`. - [x] 09-02: Globalna synchronizacja iCal — wspólny Google Calendar z podziałem na jachty po prefiksie nazwy w tytule eventu (export feed + import URL + alias jachtu) ✅ 2026-05-06 - [x] 09-03: Cleanup OAuth + per-yacht iCal — usunięcie martwego kodu (3 pliki GCal/OAuth, per-yacht feedy, pole "Google Calendar ID") + cleanup migration "po cichu" ✅ 2026-05-06 - [x] 09-04: Globalna sync iCal (tryb wspólny kalendarz, bez filtrowania) + nowy widget "wszystkie jachty" (kolory per-jacht, half-day, bez ukośników, formularz inquiry, privacy w REST) ✅ 2026-05-07 -- [ ] 09-05: Security audit i poprawki -- [ ] 09-06: Testy + tłumaczenia + dokumentacja +- [x] 09-05: UX rezerwacja — tytuły rezerwacji z GCal w paskach + per-day allDay events + tooltip na hover (cofa privacy z 09-04 per żądanie klienta) ✅ 2026-05-07 +- [x] 09-06: UX rezerwacja — blokada nawigacji kalendarza (REST `/availability/bounds` + validRange w FC) ✅ 2026-05-08 +- [ ] 09-07: Security audit i poprawki +- [ ] 09-08: Testy + tłumaczenia + dokumentacja --- *Roadmap created: 2026-05-05* diff --git a/.paul/STATE.md b/.paul/STATE.md index dfad300..310af9e 100644 --- a/.paul/STATE.md +++ b/.paul/STATE.md @@ -11,20 +11,20 @@ See: .paul/PROJECT.md (updated 2026-05-05) Milestone: v1.0 Production Release (v1.0.0) Phase: 9 of 9 (Finalizacja) — In progress -Plan: 09-04 — Complete (Global iCal sync + widget "wszystkie jachty") -Status: Loop closed, ready for next plan (09-05 Security audit) -Last activity: 2026-05-07 — Closed loop 09-04 (tryb global iCal, widget zbiorczy z formularzem inquiry, privacy w REST title) +Plan: 09-06 — Complete (blokada nawigacji kalendarza) +Status: Loop closed, ready for next plan (09-07 Security audit) +Last activity: 2026-05-08 — Closed loop 09-06 (validRange + REST /availability/bounds) Progress: -- Milestone: [█████████░] 95% -- Phase 9: [██████░░░░] 67% (4 of 6 plans complete) +- Milestone: [█████████░] 97% +- Phase 9: [████████░░] 75% (6 of 8 plans complete) ## Loop Position Current loop state: ``` PLAN ──▶ APPLY ──▶ UNIFY - ✓ ✓ ✓ [Loop 09-04 complete, ready for 09-05] + ✓ ✓ ✓ [Loop 09-06 complete, ready for 09-07] ``` ## Accumulated Context @@ -36,14 +36,17 @@ PLAN ──▶ APPLY ──▶ UNIFY | OAuth bez Google PHP Client | 7 | Brak zależności zewnętrznych, manual token refresh | | FullCalendar via CDN | 4 | Brak bundlera, update ręczny | | Custom tabela availability | 1 | Wydajne zapytania, custom cache logic | +| Privacy hardening REST `/availability/all` (09-04) → COFNIĘTE w 09-05 | 9 | Tytuły rezerwacji znów publicznie widoczne; security audit 09-07 ma to przeanalizować | +| Per-day allDay events zamiast timed cross-midnight | 9 | Czysty layout w FC dayGrid; każda doba w osobnej komórce | +| Publiczny endpoint `/availability/bounds` (09-06) | 9 | Frontend ogranicza nawigację bez auth; security audit 09-07 dorzuca do listy publicznych endpointów | ### Deferred Issues | Issue | Origin | Effort | Revisit | |-------|--------|--------|---------| -| Tłumaczenia PL | Phase 1-8 | M | Phase 9 | -| Security audit | Phase 1-8 | M | Phase 9 | -| Dokumentacja PHP Doc | Phase 1-8 | L | Phase 9 | +| Tłumaczenia PL | Phase 1-8 | M | Phase 9 (plan 09-08) | +| Security audit | Phase 1-8 + privacy revert 09-05 + bounds 09-06 | M | Phase 9 (plan 09-07) | +| Dokumentacja PHP Doc | Phase 1-8 | L | Phase 9 (plan 09-08) | ### Blockers/Concerns @@ -51,16 +54,15 @@ None. ## Session Continuity -Last session: 2026-05-07 -Stopped at: Loop 09-04 zamknięty — tryb global iCal + widget zbiorczy w produkcji -Next action: Run /paul:plan to plan 09-05 (Security audit). Klient: usuń `test-overlap-bookings.php` z FTP. -Resume file: .paul/phases/09-finalizacja/09-04-SUMMARY.md +Last session: 2026-05-08 +Stopped at: Loop 09-06 zamknięty — blokada nawigacji kalendarza zatwierdzona +Next action: Run /paul:plan to plan 09-07 (Security audit) +Resume file: .paul/phases/09-finalizacja/09-06-SUMMARY.md Resume context: -- Faza 9: 4/6 planów ukończonych (67%), milestone v1.0 95% -- Plugin produkcyjnie obsługuje 2 tryby sync iCal (per_yacht, global) + nowy widget `[yacht_calendar_all]` -- Privacy hardening REST: brak `customer_name` w title eventów (precedens dla security audit) -- Plugin w wersji 1.1.0 (bump z 1.0.0) -- Brak open issues, brak deferred items, brak blockers +- Faza 9: 6/8 planów ukończonych (75%), milestone v1.0 97% +- Plugin v1.2.1: validRange w widgecie zbiorczym (prev disabled na bieżącym miesiącu, next disabled po maxDate) +- 09-07 (Security audit) ma TRZY publiczne endpointy do oceny: `/availability/{yacht_id}`, `/availability/all`, `/availability/bounds`; dodatkowo privacy revert (tytuły rezerwacji publicznie) +- Brak open issues, brak blockers --- *STATE.md — Updated after every significant action* diff --git a/.paul/changelog/2026-05-07.md b/.paul/changelog/2026-05-07.md index bffa7de..a236323 100644 --- a/.paul/changelog/2026-05-07.md +++ b/.paul/changelog/2026-05-07.md @@ -10,6 +10,13 @@ - Privacy hardening: REST nie wystawia `customer_name` w title eventów (defense-in-depth: JS `eventContent` zwraca pusty html) - UX iteracje po checkpoincie: ciemny styl kalendarza (emulacja `/rezerwacja-maja/`), select w spójnym stylu z innymi inputami, instrukcja, server-side legenda, kolor wspólnych eventów `#7fb3d5` (jasnoniebieski) - Bump wersji pluginu 1.0.0 → 1.1.0 +- [Faza 9, Plan 05] UX rezerwacja — tytuły rezerwacji z GCal w paskach kalendarza zbiorczego + per-day allDay events +- REST `/availability/all`: title z `_booking_notes` (raw iCal SUMMARY bez prefiksu "GCal:") lub `customer_name` (frontend), split per-day allDay events (start INCLUSIVE → end INCLUSIVE) z extendedProps.is_first / is_last_night +- Frontend JS: `eventContent` renderuje `.yc-event-title` (XSS-safe przez `.text()`), half-day gradient na flagach extendedProps, native `title` tooltip na hover +- CSS: usunięte `display:none` z tytułu, nowy `.yc-event-title` (ellipsis), `margin: 1px 2px` na evencie (gap między dziennymi paskami) +- Auto-fix po checkpoincie: zmiana z timed 12:00→12:00 events na allDay (timed crossowały północ → schodki w grid) +- Świadome cofnięcie privacy z 09-04 — tytuły rezerwacji widoczne publicznie per żądanie klienta (security audit 09-06 ma to przeanalizować) +- Bump wersji pluginu 1.1.0 → 1.2.0 ## Zmienione pliki @@ -27,3 +34,5 @@ - `test-overlap-bookings.php` (test helper, do skasowania z FTP po teście) - `.paul/phases/09-finalizacja/09-04-PLAN.md` - `.paul/phases/09-finalizacja/09-04-SUMMARY.md` +- `.paul/phases/09-finalizacja/09-05-PLAN.md` +- `.paul/phases/09-finalizacja/09-05-SUMMARY.md` diff --git a/.paul/changelog/2026-05-08.md b/.paul/changelog/2026-05-08.md new file mode 100644 index 0000000..d208594 --- /dev/null +++ b/.paul/changelog/2026-05-08.md @@ -0,0 +1,17 @@ +# 2026-05-08 + +## Co zrobiono + +- [Faza 9, Plan 06] Blokada nawigacji kalendarza zbiorczego `[yacht_calendar_all]` na `/rezerwacja/` +- REST: nowy publiczny endpoint `GET /yacht-booking/v1/availability/bounds` zwracający `{ max_booking_date }` (filtr confirmed/pending + end_date >= dziś, sortowanie DESC po meta_value DATE) +- Frontend JS: bootstrap fetch bounds przed init FullCalendar, nowa funkcja `buildCalendar` z `validRange { start: pierwszy dzień bieżącego miesiąca, end: pierwszy dzień miesiąca PO maxDate }` (exclusive) +- Graceful degradation w fail callback — kalendarz bez validRange gdy endpoint padnie +- Bump wersji pluginu 1.2.0 → 1.2.1 + +## Zmienione pliki + +- `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` +- `.paul/phases/09-finalizacja/09-06-PLAN.md` +- `.paul/phases/09-finalizacja/09-06-SUMMARY.md` diff --git a/.paul/governance/governance_2026-05-07.jsonl b/.paul/governance/governance_2026-05-07.jsonl index bce287f..c38055e 100644 --- a/.paul/governance/governance_2026-05-07.jsonl +++ b/.paul/governance/governance_2026-05-07.jsonl @@ -58,3 +58,30 @@ {"ts":"2026-05-07T12:49:49Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} {"ts":"2026-05-07T12:50:00Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} {"ts":"2026-05-07T12:56:14Z","tool":"Bash","cmd":"rm test-overlap-bookings.php && echo \"Usunięto test-overlap-bookings.php\"\",\"description\":\"Delete test helper file\"},\"tool_response\":{\"stdout\":\"Usunięto test-overlap-bookings.php\",\"stde","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:43:52Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\phases\\\\09-finalizacja\\\\09-05-PLAN.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:44:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:44:24Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:45:23Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\api\\\\class-rest-controller.php","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:45:31Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/jachty3.pagedev.pl/wp-content/plugins/yacht-booking-system/api/class-rest-controller.php\" 2>&1 || echo \"PHP not in PATH — syntax check skipped (zgodnie z CL","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:45:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\frontend\\\\assets\\\\js\\\\calendar-all.js","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:45:46Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\frontend\\\\assets\\\\js\\\\calendar-all.js","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:45:56Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\frontend\\\\assets\\\\css\\\\calendar-all.css","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:46:02Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\frontend\\\\assets\\\\css\\\\calendar-all.css","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:46:20Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\yacht-booking-system.php","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:46:27Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/jachty3.pagedev.pl/wp-content/plugins/yacht-booking-system/api/class-rest-controller.php\" \"C:/visual studio code/projekty/jachty3.pagedev.pl/wp-content/plugin","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:49:55Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\api\\\\class-rest-controller.php","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:50:01Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/jachty3.pagedev.pl/wp-content/plugins/yacht-booking-system/api/class-rest-controller.php\"\",\"description\":\"PHP syntax check\"},\"tool_response\":{\"stdout\"","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:50:51Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\frontend\\\\assets\\\\js\\\\calendar-all.js","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:52:37Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\phases\\\\09-finalizacja\\\\09-05-SUMMARY.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:53:00Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\changelog\\\\2026-05-07.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:53:05Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\changelog\\\\2026-05-07.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:53:11Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:53:36Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:57:39Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\phases\\\\09-finalizacja\\\\09-06-PLAN.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:57:49Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:58:01Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:58:14Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:59:19Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\api\\\\class-rest-controller.php","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:59:34Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\api\\\\class-rest-controller.php","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:59:40Z","tool":"Bash","cmd":"php -l \"C:/visual studio code/projekty/jachty3.pagedev.pl/wp-content/plugins/yacht-booking-system/api/class-rest-controller.php\"\",\"description\":\"PHP syntax check\"},\"tool_response\":{\"stdout\"","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T21:59:58Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\frontend\\\\assets\\\\js\\\\calendar-all.js","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} diff --git a/.paul/governance/governance_2026-05-08.jsonl b/.paul/governance/governance_2026-05-08.jsonl new file mode 100644 index 0000000..d05ea7c --- /dev/null +++ b/.paul/governance/governance_2026-05-08.jsonl @@ -0,0 +1,5 @@ +{"ts":"2026-05-07T22:00:39Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\wp-content\\\\plugins\\\\yacht-booking-system\\\\yacht-booking-system.php","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T22:02:43Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\phases\\\\09-finalizacja\\\\09-06-SUMMARY.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T22:03:02Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\changelog\\\\2026-05-08.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T22:03:09Z","tool":"Edit","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\ROADMAP.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} +{"ts":"2026-05-07T22:03:37Z","tool":"Write","file":"C:\\\\visual studio code\\\\projekty\\\\jachty3.pagedev.pl\\\\.paul\\\\STATE.md","cwd":"/c/visual studio code/projekty/jachty3.pagedev.pl"} diff --git a/.paul/phases/09-finalizacja/09-05-PLAN.md b/.paul/phases/09-finalizacja/09-05-PLAN.md new file mode 100644 index 0000000..9e1bd8b --- /dev/null +++ b/.paul/phases/09-finalizacja/09-05-PLAN.md @@ -0,0 +1,317 @@ +--- +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 +--- + + +## 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). + + + + +- **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). + + +## 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 + + + + +## 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 +``` + + + + + + + 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 = $('
').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_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). + + + + +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 + + + +- Wszystkie 4 zadania auto + checkpoint zatwierdzone +- Brak fatal errors / warnings PHP w logach +- Klient zaakceptował wzrokowy efekt na produkcji +- Plugin v1.2.0 deployed + + + +After completion, create `.paul/phases/09-finalizacja/09-05-SUMMARY.md` + diff --git a/.paul/phases/09-finalizacja/09-05-SUMMARY.md b/.paul/phases/09-finalizacja/09-05-SUMMARY.md new file mode 100644 index 0000000..471bb60 --- /dev/null +++ b/.paul/phases/09-finalizacja/09-05-SUMMARY.md @@ -0,0 +1,159 @@ +--- +phase: 09-finalizacja +plan: 05 +subsystem: ui, api +tags: [fullcalendar, rest, privacy-revert, ux] + +requires: + - phase: 09-finalizacja + provides: widget zbiorczy [yacht_calendar_all] + REST /availability/all (09-04) +provides: + - tytuły rezerwacji (raw SUMMARY z `_booking_notes` lub `customer_name`) widoczne na paskach kalendarza zbiorczego + - per-day allDay events (każda doba rezerwacji = osobny pasek w swojej komórce) + - half-day visual przeniesiony na flagi `extendedProps.is_first` / `is_last_night` + - native tooltip na hover (title + data dnia) +affects: [security audit (09-06) — privacy hardening z 09-04 świadomie cofnięty] + +tech-stack: + added: [] + patterns: + - allDay events per occupied day (start INCLUSIVE → end INCLUSIVE) zamiast jednej timed belki cross-midnight + - extendedProps jako single source of truth dla half-day flag (server decyduje, JS renderuje) + - native HTML title attribute jako tooltip (zero zależności, accessibility-friendly) + +key-files: + created: [] + 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 + +key-decisions: + - "Per-day allDay events zamiast 1 timed event 12:00→12:00 (poprzednia próba z timed eventami crossowała północ → FC renderował schodki)" + - "Iteracja od start_date do end_date INCLUSIVE — pierwszy i ostatni dzień mają half-day visual" + - "Cofnięcie privacy hardening z 09-04 — title z `_booking_notes` (iCal raw SUMMARY) lub `customer_name` widoczny publicznie per żądanie klienta" + - "Tooltip = native `title` attribute (bez tippy.js / qtip — zero JS, działa offline)" + +patterns-established: + - "extendedProps jako kontrakt server↔JS dla flag visual (kiedy is_start/is_end FC nie wystarcza)" + - "Privacy w REST jest decyzją biznesową, nie techniczną — security audit musi to weryfikować osobno" + +duration: ~75min +started: 2026-05-07T22:30:00Z +completed: 2026-05-07T23:50:00Z +--- + +# Phase 09 Plan 05: UX rezerwacja — tytuły GCal + per-day segmenty + +**Kalendarz zbiorczy `[yacht_calendar_all]` na `/rezerwacja/` pokazuje tytuły rezerwacji (raw SUMMARY z Google Calendar bez prefiksu "GCal:") na osobnych paskach per dzień, z half-day visual na pierwszym i ostatnim dniu rezerwacji oraz native tooltip na hover.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~75min (z 1 iteracją UX po pierwszym deploy) | +| Started | 2026-05-07T22:30:00Z | +| Completed | 2026-05-07T23:50:00Z | +| Tasks | 4 auto + 1 checkpoint + 1 iteracja UX (allDay fix) + 1 mini-fix (tooltip) | +| Files modified | 4 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: Tytuł rezerwacji z iCal w REST | Pass | `_booking_notes` zwracane jako `title`, prefiks "GCal:" zniknął | +| AC-2: Tytuł rezerwacji frontowej | Pass | Booking::get_customer_name() jako fallback | +| AC-3: Tytuł renderowany w pasku eventu | Pass | `.yc-event-title` z ellipsis; weryfikacja Playwright (events: "Kubuś - Julia R") | +| AC-4: Per-day segmenty z gap | Pass | Po fix do allDay — każda doba w osobnej komórce, margin 2px daje gap | +| AC-5: Half-day visual zachowany | Pass | extendedProps.is_first / is_last_night sterują gradientem | +| AC-6: Privacy — brak email/phone | Pass | Payload REST nie zawiera customer_email ani customer_phone | + +## Accomplishments + +- **Tytuły rezerwacji widoczne** — klient może wzrokowo identyfikować rezerwacje na publicznym kalendarzu (np. "Kubuś - Julia R", "Maja - Kowalscy 5 osób") bez prefiksu "GCal:". +- **Per-day allDay events** — każda doba rezerwacji to osobny pasek w pojedynczej komórce dnia (1.06, 2.06, 3.06...) z 2px gapem między nimi. Rezerwacja 5-dniowa = 5 osobnych prostokątów zamiast jednej ciągłej belki. +- **Half-day visual zachowany** — pierwszy dzień rezerwacji (yacht odbierany w południe) ma lewą połowę transparentną, ostatni dzień (zwrot w południe) prawą połowę transparentną. Środkowe dni — pełny kolor. +- **Native tooltip na hover** — `title` attribute z formatem "{tytuł} ({YYYY-MM-DD})" — zero zależności, działa accessibility-friendly. + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `api/class-rest-controller.php` | Modified | `get_all_availability()`: title z `_booking_notes`/`customer_name`, split per-day allDay events z extendedProps (is_first / is_last_night) | +| `frontend/assets/js/calendar-all.js` | Modified | `eventContent` renderuje `.yc-event-title` (escaped przez `.text()`); `eventDidMount` ustawia native `title` tooltip; `applyHalfDayGradient` czyta flagi z extendedProps | +| `frontend/assets/css/calendar-all.css` | Modified | Usunięte `display:none` z `.fc-event-title`; nowy `.yc-event-title` (ellipsis); `margin: 1px 2px` na `.fc-daygrid-event` (gap między paskami); mobile font-size | +| `yacht-booking-system.php` | Modified | Bump wersji 1.1.0 → 1.2.0 (cache busting) | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Per-day allDay events zamiast timed 12:00→12:00 | Pierwsza próba z timed eventami crossowała północ → FC dayGrid renderował każdą dobę jako pasek na granicy dwóch komórek (efekt "schodków") | Każda doba zamknięta w jednej komórce, czysty layout | +| Iteracja `start_date <= end_date` (INCLUSIVE) | Yacht obecny od noon dnia start do noon dnia end → wszystkie dni tego zakresu są "occupied" | Rezerwacja 1.06 → 5.06 emituje 5 eventów (1, 2, 3, 4, 5) zamiast 4 nocy | +| Cofnięcie privacy z 09-04 | Klient zmienił zdanie — chce wzrokowo identyfikować rezerwacje | Tytuły widoczne publicznie. `customer_email`/`customer_phone` nadal NIE wychodzą przez REST. Security audit (09-06) musi to uwzględnić | +| Tooltip = native `title` attribute | Klient prosił o "jakiś label na hover" — najprostsze, bez nowych zależności | Hover po ~1s pokazuje "Kubuś - Julia R (2026-06-04)" | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 1 | Krytyczny: pierwsza wersja (timed events 12:00→12:00) renderowała "schodki" — zmiana na allDay events naprawiła layout | +| Scope additions | 1 | Native tooltip na hover (poza pierwotnym PLAN.md, prośba klienta po weryfikacji) | +| Deferred | 0 | — | + +**Total impact:** Pierwsza implementacja per-PLAN była technicznie poprawna (timed events 12:00→12:00 = 1 doba), ale FC dayGrid renderuje takie eventy jako rozciągnięte przez granicę dwóch komórek (każda doba pojawiała się na pograniczu dnia N i N+1). Fix do allDay events rozwiązał to zachowując semantykę (start/end INCLUSIVE). + +### Auto-fixed Issues + +**1. Timed events 12:00→12:00 renderowały "schodki" w FC dayGrid** +- **Found during:** Checkpoint human-verify (klient zgłosił "paski jakoś dziwnie wyszły") +- **Issue:** Każdy event był 24h cross-midnight (start dzień N 12:00 → dzień N+1 12:00). FC dayGrid pokazywał każdą dobę jako fragment na granicy dwóch komórek, eventy nakładały się wizualnie tworząc efekt schodków +- **Fix:** Zmiana na `allDay: true` z `start = day` (bez end). Iteracja od start_date do end_date INCLUSIVE. Każdy event w jednej komórce +- **Files:** `api/class-rest-controller.php` (pętla while) +- **Verification:** Playwright snapshot po deploy — paski wyrównane do komórek dni, brak nakładek +- **Commit:** część zmiany w Task 1 (przed checkpointem) + redeploy po fix + +### Scope Additions + +**1. Native tooltip na hover** +- **Origin:** Po zatwierdzeniu allDay fix klient poprosił o "jakiś label na hover" +- **Implementation:** `info.el.setAttribute('title', info.event.title + ' (' + dayStr + ')')` w `eventDidMount` +- **Verification:** klient potwierdził OK +- **Files:** `frontend/assets/js/calendar-all.js` + +### Deferred Items + +None. + +## Issues Encountered + +| Issue | Resolution | +|-------|------------| +| Timed events crossowały północ → "schodki" w grid | Zmiana modelu na allDay events INCLUSIVE start↔end | + +## Skill Audit + +`.paul/SPECIAL-FLOWS.md` nie istnieje — skill audit pominięty. + +## Next Phase Readiness + +**Ready:** +- Widget zbiorczy `[yacht_calendar_all]` w pełni funkcjonalny — UX zatwierdzony przez klienta +- Plugin v1.2.0 deployed +- Privacy w REST świadomie zarządzona — security audit (09-06) ma jasny precedens do oceny + +**Concerns:** +- **Privacy regression vs 09-04** — `_booking_notes` (raw iCal SUMMARY) i `customer_name` są teraz publicznie wystawiane na `/availability/all`. Klient zaakceptował biznesowo. Security audit (09-06) powinien: + 1. Potwierdzić że `customer_email`/`customer_phone` nadal NIE wyciekają (są zablokowane na poziomie REST shape). + 2. Rozważyć czy tytuły rezerwacji (zawierające imiona/nazwiska klientów) wymagają dodatkowych zabezpieczeń (np. throttling, robots noindex na endpoincie REST). +- Brak automatycznych testów regresji — przy kolejnych zmianach w `get_all_availability()` regresja może umknąć + +**Blockers:** +- None + +--- +*Phase: 09-finalizacja, Plan: 05* +*Completed: 2026-05-07* diff --git a/.paul/phases/09-finalizacja/09-06-PLAN.md b/.paul/phases/09-finalizacja/09-06-PLAN.md new file mode 100644 index 0000000..f6daa78 --- /dev/null +++ b/.paul/phases/09-finalizacja/09-06-PLAN.md @@ -0,0 +1,239 @@ +--- +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` + diff --git a/.paul/phases/09-finalizacja/09-06-SUMMARY.md b/.paul/phases/09-finalizacja/09-06-SUMMARY.md new file mode 100644 index 0000000..30a37da --- /dev/null +++ b/.paul/phases/09-finalizacja/09-06-SUMMARY.md @@ -0,0 +1,139 @@ +--- +phase: 09-finalizacja +plan: 06 +subsystem: ui, api +tags: [fullcalendar, validRange, rest, ux] + +requires: + - phase: 09-finalizacja + provides: widget zbiorczy `[yacht_calendar_all]` + REST `/availability/all` (09-04, 09-05) +provides: + - REST endpoint `/availability/bounds` (publiczny, max booking date) + - JS bootstrap fetch bounds przed init FullCalendar + - validRange w FullCalendar (prev disabled na bieżącym miesiącu, next disabled po miesiącu max booking) +affects: [security audit (09-07) — kolejny publiczny endpoint do oceny] + +tech-stack: + added: [] + patterns: + - bootstrap fetch w JS (osobny request przed instancjacją FC) + graceful degradation w fail callback + - publiczny REST endpoint zwracający tylko zagregowaną datę (brak ujawnienia rezerwacji) + - validRange jako kontrakt server↔client dla nawigacji + +key-files: + created: [] + 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 + +key-decisions: + - "Endpoint publiczny (`__return_true`) — frontend renderuje widget dla anonimowych userów" + - "Filtr `_booking_end_date >= dziś` — historyczne rezerwacje nie rozszerzają zakresu w przyszłość" + - "Graceful degradation: fail bounds → kalendarz bez validRange (lepsze niż brak kalendarza)" + - "validRange.end exclusive (pierwszy dzień miesiąca PO maxDate) — daje cały miesiąc maxDate dostępny" + +patterns-established: + - "Boundary endpoints: lekkie REST endpointy zwracające metadane (max date, count) zamiast zagregowanych danych — minimalizuje payload + privacy" + - "Fetch-before-init pattern w FullCalendar: jeden bootstrap call, potem instancjacja z opcjami zależnymi od odpowiedzi" + +duration: ~25min +started: 2026-05-08T00:00:00Z +completed: 2026-05-08T00:25:00Z +--- + +# Phase 09 Plan 06: Blokada nawigacji kalendarza + +**Kalendarz zbiorczy `[yacht_calendar_all]` na `/rezerwacja/` blokuje nawigację: prev disabled na bieżącym miesiącu, next disabled po miesiącu zawierającym ostatnią rezerwację confirmed/pending — bazując na nowym publicznym REST endpoint `/availability/bounds`.** + +## Performance + +| Metric | Value | +|--------|-------| +| Duration | ~25min | +| Started | 2026-05-08T00:00:00Z | +| Completed | 2026-05-08T00:25:00Z | +| Tasks | 3 auto + 1 checkpoint | +| Files modified | 3 | + +## Acceptance Criteria Results + +| Criterion | Status | Notes | +|-----------|--------|-------| +| AC-1: REST zwraca max booking date | Pass | `get_availability_bounds()` filtruje confirmed/pending + end >= dziś, sortuje DESC po `_booking_end_date` | +| AC-2: Brak rezerwacji → null | Pass | `max_booking_date: null` gdy `get_posts` pusty | +| AC-3: Prev disabled w bieżącym miesiącu | Pass | `validRange.start = pierwszy dzień bieżącego miesiąca` — FC sam wyłącza prev | +| AC-4: Next disabled na miesiącu max booking | Pass | `validRange.end = pierwszy dzień miesiąca PO maxDate` (exclusive) | +| AC-5: Next disabled gdy brak rezerwacji | Pass | `rangeEnd = nextMonthFirst(today)` gdy maxDate null lub w przeszłości | +| AC-6: Nawigacja w obrębie zakresu | Pass | Klient zatwierdził w checkpoincie | + +## Accomplishments + +- **REST `/availability/bounds`** — lekki publiczny endpoint zwracający tylko `{ max_booking_date }`, z filtrem statusu (confirmed/pending) i `end_date >= dziś`. +- **Bootstrap fetch w JS** — bounds pobierane przed instancjacją FC, z graceful degradation w `fail` callback. +- **validRange w FullCalendar** — start = pierwszy dzień bieżącego miesiąca (prev blocked), end = pierwszy dzień miesiąca PO miesiącu ostatniej rezerwacji (exclusive — daje cały miesiąc maxDate dostępny). +- **Plugin v1.2.1** — cache busting po deploy. + +## Files Created/Modified + +| File | Change | Purpose | +|------|--------|---------| +| `api/class-rest-controller.php` | Modified | Nowy route `/availability/bounds` + metoda `get_availability_bounds()` (sortowanie DESC po meta_value DATE, filtr status + end_date >= dziś) | +| `frontend/assets/js/calendar-all.js` | Modified | Helpers `firstOfMonth`/`nextMonthFirst`; `initCalendar` rozdzielony na fetch bounds + nową funkcję `buildCalendar` z `validRange` | +| `yacht-booking-system.php` | Modified | Bump 1.2.0 → 1.2.1 | + +## Decisions Made + +| Decision | Rationale | Impact | +|----------|-----------|--------| +| Endpoint publiczny | Frontend renderuje widget dla anonimowych userów; brak nonce wymaga `__return_true` | Każdy odwiedzający może zobaczyć datę ostatniej rezerwacji (security audit do oceny) | +| Filtr `end_date >= dziś` w SQL | Historyczne rezerwacje nie powinny rozszerzać zakresu w przyszłość | Próba np. usunięcia ostatniej przyszłej rezerwacji od razu skraca dostępny zakres po reload | +| Graceful degradation w fail | Lepiej pokazać kalendarz bez validRange niż wcale | Awaria endpointu nie psuje strony; userzy nadal widzą kalendarz | +| validRange.end exclusive | FC tak interpretuje validRange — exclusive end | `nextMonthFirst(maxDate)` daje cały miesiąc maxDate dostępny | + +## Deviations from Plan + +### Summary + +| Type | Count | Impact | +|------|-------|--------| +| Auto-fixed | 0 | — | +| Scope additions | 0 | — | +| Deferred | 0 | — | + +**Total impact:** Plan wykonany dokładnie jak zaplanowany. Brak iteracji UX po checkpoincie. + +### Deferred Items + +None. + +## Issues Encountered + +| Issue | Resolution | +|-------|------------| +| Edit z literówką `new_str ng` zamiast `new_string` | Powtórzony Edit z prawidłowym kluczem | + +## Skill Audit + +`.paul/SPECIAL-FLOWS.md` nie istnieje — skill audit pominięty. + +## Next Phase Readiness + +**Ready:** +- Widget zbiorczy `/rezerwacja/` w pełni funkcjonalny i ograniczony do sensownego zakresu dat +- Kolejny publiczny endpoint `/availability/bounds` do uwzględnienia w security audit (09-07) +- Plugin v1.2.1 deployed + +**Concerns:** +- **Trzy publiczne endpointy w REST API** (`/availability/{yacht_id}`, `/availability/all`, `/availability/bounds`) — security audit (09-07) powinien przeanalizować łącznie: + 1. Czy `/bounds` przez ujawnienie max_booking_date nie daje informacji wrażliwej (typowo nie — tylko data graniczna). + 2. Czy `/all` nadal nie wycieka `customer_email`/`customer_phone` (potwierdzono w 09-05 że nie). + 3. Throttling/rate-limiting publicznych endpointów. +- Brak automatycznych testów — regresje w `validRange` mogą umknąć przy kolejnych zmianach JS + +**Blockers:** +- None + +--- +*Phase: 09-finalizacja, Plan: 06* +*Completed: 2026-05-08* diff --git a/.playwright-mcp/console-2026-05-07T21-48-08-323Z.log b/.playwright-mcp/console-2026-05-07T21-48-08-323Z.log new file mode 100644 index 0000000..021d3d2 --- /dev/null +++ b/.playwright-mcp/console-2026-05-07T21-48-08-323Z.log @@ -0,0 +1 @@ +[ 732ms] [LOG] JQMIGRATE: Migrate is installed, version 3.4.1 @ https://jachty3.pagedev.pl/wp-includes/js/jquery/jquery-migrate.min.js?ver=3.4.1:1 diff --git a/.playwright-mcp/page-2026-05-07T21-48-09-547Z.yml b/.playwright-mcp/page-2026-05-07T21-48-09-547Z.yml new file mode 100644 index 0000000..4f6575b --- /dev/null +++ b/.playwright-mcp/page-2026-05-07T21-48-09-547Z.yml @@ -0,0 +1,237 @@ +- generic [active] [ref=e1]: + - link "Przejdź do treści" [ref=e2] [cursor=pointer]: + - /url: "#content" + - banner [ref=e3]: + - generic [ref=e5]: + - link [ref=e7] [cursor=pointer]: + - /url: https://jachty3.pagedev.pl + - navigation "Menu" [ref=e9]: + - list [ref=e10]: + - listitem [ref=e11]: + - link "Start" [ref=e12] [cursor=pointer]: + - /url: https://jachty3.pagedev.pl/ + - listitem [ref=e13]: + - link "Maja" [ref=e14] [cursor=pointer]: + - /url: https://jachty3.pagedev.pl/maja/ + - listitem [ref=e15]: + - link "Kubuś" [ref=e16] [cursor=pointer]: + - /url: https://jachty3.pagedev.pl/kubus/ + - listitem [ref=e17]: + - link "O Nas" [ref=e18] [cursor=pointer]: + - /url: https://jachty3.pagedev.pl/o-nas/ + - listitem [ref=e19]: + - link "Cennik" [ref=e20] [cursor=pointer]: + - /url: https://jachty3.pagedev.pl/cennik/ + - listitem [ref=e21]: + - link "Blog" [ref=e22] [cursor=pointer]: + - /url: https://jachty3.pagedev.pl/blog/ + - listitem [ref=e23]: + - link "Kontakt" [ref=e24] [cursor=pointer]: + - /url: https://jachty3.pagedev.pl/kontakt/ + - listitem [ref=e25]: + - link "Zarezerwuj →" [ref=e26] [cursor=pointer]: + - /url: /rezerwacja/ + - main [ref=e27]: + - generic [ref=e30]: + - heading "Rezerwacja" [level=1] [ref=e34] + - generic [ref=e42]: + - paragraph [ref=e44]: Aby zarezerwować termin, wypełnij formularz po prawej stronie albo skontaktuj się z nami telefonicznie lub mailowo. + - generic "Legenda kalendarza" [ref=e45]: + - generic [ref=e46]: Rezerwacja + - generic [ref=e48]: + - generic [ref=e50]: + - generic [ref=e51]: + - generic [ref=e52]: + - generic [ref=e53]: + - button "Poprzedni" [ref=e54] [cursor=pointer]: + - img [ref=e55]:  + - button "Następny" [ref=e56] [cursor=pointer]: + - img [ref=e57]:  + - button "Dziś" [disabled] [ref=e58] + - heading "maj 2026" [level=2] [ref=e60] + - generic "maj 2026" [ref=e61]: + - grid [ref=e63]: + - rowgroup [ref=e64]: + - row "poniedziałek wtorek środa czwartek piątek sobota niedziela" [ref=e68]: + - columnheader "poniedziałek" [ref=e69]: + - generic "poniedziałek" [ref=e71]: pon. + - columnheader "wtorek" [ref=e72]: + - generic "wtorek" [ref=e74]: wt. + - columnheader "środa" [ref=e75]: + - generic "środa" [ref=e77]: śr. + - columnheader "czwartek" [ref=e78]: + - generic "czwartek" [ref=e80]: czw. + - columnheader "piątek" [ref=e81]: + - generic "piątek" [ref=e83]: pt. + - columnheader "sobota" [ref=e84]: + - generic "sobota" [ref=e86]: sob. + - columnheader "niedziela" [ref=e87]: + - generic "niedziela" [ref=e89]: niedz. + - rowgroup [ref=e90]: + - generic [ref=e93]: + - row "27 kwietnia 2026 28 kwietnia 2026 29 kwietnia 2026 30 kwietnia 2026 1 maja 2026 2 maja 2026 3 maja 2026" [ref=e95]: + - gridcell "27 kwietnia 2026" [ref=e96]: + - generic "27 kwietnia 2026" [ref=e99]: "27" + - gridcell "28 kwietnia 2026" [ref=e101]: + - generic "28 kwietnia 2026" [ref=e104]: "28" + - gridcell "29 kwietnia 2026" [ref=e106]: + - generic "29 kwietnia 2026" [ref=e109]: "29" + - gridcell "30 kwietnia 2026" [ref=e111]: + - generic "30 kwietnia 2026" [ref=e114]: "30" + - gridcell "1 maja 2026" [ref=e116]: + - generic "1 maja 2026" [ref=e119]: "1" + - gridcell "2 maja 2026" [ref=e121]: + - generic "2 maja 2026" [ref=e124]: "2" + - gridcell "3 maja 2026" [ref=e126]: + - generic "3 maja 2026" [ref=e129]: "3" + - row "4 maja 2026 5 maja 2026 6 maja 2026 7 maja 2026 8 maja 2026 9 maja 2026 10 maja 2026" [ref=e131]: + - gridcell "4 maja 2026" [ref=e132]: + - generic "4 maja 2026" [ref=e135]: "4" + - gridcell "5 maja 2026" [ref=e137]: + - generic "5 maja 2026" [ref=e140]: "5" + - gridcell "6 maja 2026" [ref=e142]: + - generic "6 maja 2026" [ref=e145]: "6" + - gridcell "7 maja 2026" [ref=e147]: + - generic "7 maja 2026" [ref=e150]: "7" + - gridcell "8 maja 2026" [ref=e152]: + - generic "8 maja 2026" [ref=e155]: "8" + - gridcell "9 maja 2026" [ref=e157]: + - generic "9 maja 2026" [ref=e160]: "9" + - gridcell "10 maja 2026" [ref=e162]: + - generic "10 maja 2026" [ref=e165]: "10" + - row "11 maja 2026 12 maja 2026 13 maja 2026 14 maja 2026 15 maja 2026 16 maja 2026 17 maja 2026" [ref=e167]: + - gridcell "11 maja 2026" [ref=e168]: + - generic "11 maja 2026" [ref=e171]: "11" + - gridcell "12 maja 2026" [ref=e173]: + - generic "12 maja 2026" [ref=e176]: "12" + - gridcell "13 maja 2026" [ref=e178]: + - generic "13 maja 2026" [ref=e181]: "13" + - gridcell "14 maja 2026" [ref=e183]: + - generic "14 maja 2026" [ref=e186]: "14" + - gridcell "15 maja 2026" [ref=e188]: + - generic "15 maja 2026" [ref=e191]: "15" + - gridcell "16 maja 2026" [ref=e193]: + - generic "16 maja 2026" [ref=e196]: "16" + - gridcell "17 maja 2026" [ref=e198]: + - generic "17 maja 2026" [ref=e201]: "17" + - row "18 maja 2026 19 maja 2026 20 maja 2026 21 maja 2026 22 maja 2026 23 maja 2026 24 maja 2026" [ref=e203]: + - gridcell "18 maja 2026" [ref=e204]: + - generic "18 maja 2026" [ref=e207]: "18" + - gridcell "19 maja 2026" [ref=e209]: + - generic "19 maja 2026" [ref=e212]: "19" + - gridcell "20 maja 2026" [ref=e214]: + - generic "20 maja 2026" [ref=e217]: "20" + - gridcell "21 maja 2026" [ref=e219]: + - generic "21 maja 2026" [ref=e222]: "21" + - gridcell "22 maja 2026" [ref=e224]: + - generic "22 maja 2026" [ref=e227]: "22" + - gridcell "23 maja 2026" [ref=e229]: + - generic "23 maja 2026" [ref=e232]: "23" + - gridcell "24 maja 2026" [ref=e234]: + - generic "24 maja 2026" [ref=e237]: "24" + - row "25 maja 2026 26 maja 2026 27 maja 2026 28 maja 2026 29 maja 2026 30 maja 2026 31 maja 2026" [ref=e239]: + - gridcell "25 maja 2026" [ref=e240]: + - generic "25 maja 2026" [ref=e243]: "25" + - gridcell "26 maja 2026" [ref=e245]: + - generic "26 maja 2026" [ref=e248]: "26" + - gridcell "27 maja 2026" [ref=e250]: + - generic "27 maja 2026" [ref=e253]: "27" + - gridcell "28 maja 2026" [ref=e255]: + - generic "28 maja 2026" [ref=e258]: "28" + - gridcell "29 maja 2026" [ref=e260]: + - generic "29 maja 2026" [ref=e263]: "29" + - gridcell "30 maja 2026" [ref=e265]: + - generic "30 maja 2026" [ref=e268]: "30" + - gridcell "31 maja 2026" [ref=e270]: + - generic "31 maja 2026" [ref=e273]: "31" + - row "1 czerwca 2026 2 czerwca 2026 3 czerwca 2026 4 czerwca 2026 5 czerwca 2026 6 czerwca 2026 7 czerwca 2026" [ref=e275]: + - gridcell "1 czerwca 2026" [ref=e276]: + - generic "1 czerwca 2026" [ref=e279]: "1" + - gridcell "2 czerwca 2026" [ref=e281]: + - generic "2 czerwca 2026" [ref=e284]: "2" + - gridcell "3 czerwca 2026" [ref=e286]: + - generic "3 czerwca 2026" [ref=e289]: "3" + - gridcell "4 czerwca 2026" [ref=e291]: + - generic "4 czerwca 2026" [ref=e294]: "4" + - gridcell "5 czerwca 2026" [ref=e296]: + - generic "5 czerwca 2026" [ref=e299]: "5" + - gridcell "6 czerwca 2026" [ref=e301]: + - generic "6 czerwca 2026" [ref=e304]: "6" + - gridcell "7 czerwca 2026" [ref=e306]: + - generic "7 czerwca 2026" [ref=e309]: "7" + - generic [ref=e312]: + - heading "Zapytaj o rezerwację" [level=4] [ref=e313] + - paragraph [ref=e314]: Wybierz jacht i wypełnij formularz — odezwiemy się w sprawie dostępności i cen. + - generic [ref=e315]: + - generic [ref=e316]: + - generic [ref=e317]: Jacht * + - combobox "Jacht *" [ref=e318] [cursor=pointer]: + - option "— wybierz jacht —" [selected] + - option "Maja" + - option "Kubuś" + - generic [ref=e319]: + - generic [ref=e320]: Imię i nazwisko * + - textbox "Imię i nazwisko *" [ref=e321] + - generic [ref=e322]: + - generic [ref=e323]: Email * + - textbox "Email *" [ref=e324] + - generic [ref=e325]: + - generic [ref=e326]: Telefon * + - textbox "Telefon *" [ref=e327] + - generic [ref=e328]: + - generic [ref=e329]: Preferowane terminy + - textbox "Preferowane terminy" [ref=e330]: + - /placeholder: np. 15-22 lipca + - generic [ref=e331]: + - generic [ref=e332]: Wiadomość + - textbox "Wiadomość" [ref=e333]: + - /placeholder: Dodatkowe pytania lub uwagi... + - button "Wyślij zapytanie" [ref=e335] [cursor=pointer] + - contentinfo [ref=e336]: + - generic [ref=e343]: + - generic [ref=e344]: + - heading "Wypłyń z nami." [level=3] [ref=e346]: + - text: Wypłyń + - strong [ref=e347]: z nami + - text: . + - paragraph [ref=e349]: Przestronne kabiny, ciepły fornir, biała tapicerka i wszystko, czego potrzeba do długiego rejsu. Pokład zaprojektowany dla wygody całej załogi. + - generic [ref=e350]: + - paragraph [ref=e352]: Nawigacja + - list [ref=e354]: + - listitem [ref=e355]: + - link "Start" [ref=e356] [cursor=pointer]: + - /url: / + - generic [ref=e357]: Start + - listitem [ref=e358]: + - link "Sedna 26 „Maja\"" [ref=e359] [cursor=pointer]: + - /url: /maja/ + - generic [ref=e360]: Sedna 26 „Maja" + - listitem [ref=e361]: + - link "Sedna 26 „Kubuś\"" [ref=e362] [cursor=pointer]: + - /url: /kubus/ + - generic [ref=e363]: Sedna 26 „Kubuś" + - listitem [ref=e364]: + - link "O nas" [ref=e365] [cursor=pointer]: + - /url: /o-nas/ + - generic [ref=e366]: O nas + - listitem [ref=e367]: + - link "Cennik" [ref=e368] [cursor=pointer]: + - /url: /cennik/ + - generic [ref=e369]: Cennik + - listitem [ref=e370]: + - link "Kontakt" [ref=e371] [cursor=pointer]: + - /url: /kontakt/ + - generic [ref=e372]: Kontakt + - generic [ref=e373]: + - paragraph [ref=e375]: Marina + - list [ref=e377]: + - listitem [ref=e378]: + - generic [ref=e379]: Keja Dedal + - listitem [ref=e380]: + - generic [ref=e381]: Polańczyk, Bieszczady + - listitem [ref=e382]: + - generic [ref=e383]: Jezioro Solińskie + - generic [ref=e396]: + - paragraph [ref=e398]: © 2026 PKMP Jachty. Wszystkie prawa zastrzeżone. + - paragraph [ref=e400]: Solina · Bieszczady · PL + - generic [ref=e406]: laptop \ No newline at end of file diff --git a/cal-only.png b/cal-only.png new file mode 100644 index 0000000..e72d465 Binary files /dev/null and b/cal-only.png differ diff --git a/rezerwacja-current.png b/rezerwacja-current.png new file mode 100644 index 0000000..539cd08 Binary files /dev/null and b/rezerwacja-current.png differ diff --git a/wp-content/plugins/yacht-booking-system/api/class-rest-controller.php b/wp-content/plugins/yacht-booking-system/api/class-rest-controller.php index 931e5ce..8fb170b 100644 --- a/wp-content/plugins/yacht-booking-system/api/class-rest-controller.php +++ b/wp-content/plugins/yacht-booking-system/api/class-rest-controller.php @@ -142,6 +142,17 @@ class Rest_Controller extends \WP_REST_Controller { ) ); + // GET /yacht-booking/v1/availability/bounds + register_rest_route( + self::NAMESPACE, + '/availability/bounds', + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_availability_bounds' ), + 'permission_callback' => '__return_true', + ) + ); + // POST /yacht-booking/v1/bookings register_rest_route( self::NAMESPACE, @@ -407,6 +418,11 @@ class Rest_Controller extends \WP_REST_Controller { ) ); + $ical_sources = array( + \YachtBooking\Integrations\ICal\ICal_Import::GLOBAL_CALENDAR_SOURCE, + \YachtBooking\Integrations\ICal\ICal_Import::GLOBAL_IMPORT_SOURCE, + ); + $events = array(); foreach ( $bookings as $booking ) { $booking_id = $booking->ID; @@ -421,33 +437,113 @@ class Rest_Controller extends \WP_REST_Controller { $source = (string) get_post_meta( $booking_id, '_booking_source', true ); $is_global_event = ( 0 === $yacht_id || \YachtBooking\Integrations\ICal\ICal_Import::GLOBAL_CALENDAR_SOURCE === $source ); - // W trybie global wszystko traktujemy jak wspólne wydarzenia: szary kolor, - // brak yacht_id, generyczny tytuł "Rezerwacja" — bez wycieku danych klientów. + // Color: zachowane z poprzedniej logiki (per-yacht paleta lub kolor global). if ( $is_global_mode || $is_global_event ) { $color = self::GLOBAL_EVENT_COLOR; - $title = __( 'Rezerwacja', 'yacht-booking' ); $y_id = 0; } else { $color = isset( $color_map[ $yacht_id ] ) ? $color_map[ $yacht_id ] : self::GLOBAL_EVENT_COLOR; - $yacht = get_post( $yacht_id ); - // Tryb per_yacht: pokazujemy tylko nazwę jachtu (bez nazwiska klienta — privacy). - $title = $yacht ? $yacht->post_title : __( 'Rezerwacja', 'yacht-booking' ); $y_id = $yacht_id; } - $events[] = array( - 'id' => $booking_id, - 'title' => $title, - 'start' => $start_date . 'T12:00:00', - 'end' => $end_date . 'T12:00:00', - 'color' => $color, - 'yacht_id' => $y_id, - ); + // Title: raw SUMMARY z _booking_notes (iCal) lub customer_name (frontend). + // Klient świadomie cofa privacy z 09-04 — tytuły rezerwacji widoczne publicznie. + if ( in_array( $source, $ical_sources, true ) ) { + $notes = (string) get_post_meta( $booking_id, '_booking_notes', true ); + $title = '' !== trim( $notes ) ? $notes : __( 'Rezerwacja', 'yacht-booking' ); + } else { + $customer = (string) Booking::get_customer_name( $booking_id ); + $title = '' !== trim( $customer ) ? $customer : __( 'Rezerwacja', 'yacht-booking' ); + } + $title = sanitize_text_field( $title ); + + // Split na N eventów per dzień (allDay = każdy event mieści się w jednej komórce). + // Iteracja od start_date do end_date INCLUSIVE — pierwszy i ostatni dzień + // mają half-day visual (yacht odbierany / zwracany w południe). + try { + $cursor = new \DateTimeImmutable( $start_date ); + $end_dt = new \DateTimeImmutable( $end_date ); + } catch ( \Exception $e ) { + continue; + } + + while ( $cursor <= $end_dt ) { + $day = $cursor->format( 'Y-m-d' ); + + $is_first = ( $day === $start_date ); + $is_last = ( $day === $end_date ); + + $events[] = array( + 'id' => $booking_id . '-' . $day, + 'title' => $title, + 'start' => $day, + 'allDay' => true, + 'color' => $color, + 'yacht_id' => $y_id, + 'extendedProps' => array( + 'is_first' => $is_first, + 'is_last_night' => $is_last, + 'booking_id' => (int) $booking_id, + ), + ); + + $cursor = $cursor->modify( '+1 day' ); + } } return rest_ensure_response( $events ); } + /** + * Zwraca granice nawigacji kalendarza zbiorczego: datę ostatniej rezerwacji + * (confirmed/pending) z `_booking_end_date >= dziś`. + * + * Frontend używa tej wartości do ustawienia `validRange.end` w FullCalendar, + * blokując nawigację w przód poza miesiąc ostatniej rezerwacji. + * + * @return \WP_REST_Response { max_booking_date: 'YYYY-MM-DD' | null } + */ + public function get_availability_bounds() { + $today = gmdate( 'Y-m-d' ); + + $bookings = get_posts( + array( + 'post_type' => 'yacht_booking', + 'post_status' => 'publish', + 'posts_per_page' => 1, + 'fields' => 'ids', + 'meta_key' => '_booking_end_date', + 'orderby' => 'meta_value', + 'meta_type' => 'DATE', + 'order' => 'DESC', + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => '_booking_status', + 'value' => array( 'confirmed', 'pending' ), + 'compare' => 'IN', + ), + array( + 'key' => '_booking_end_date', + 'value' => $today, + 'compare' => '>=', + 'type' => 'DATE', + ), + ), + ) + ); + + $max_date = null; + if ( ! empty( $bookings ) ) { + $end = (string) get_post_meta( (int) $bookings[0], '_booking_end_date', true ); + if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $end ) ) { + $max_date = $end; + } + } + + return rest_ensure_response( array( 'max_booking_date' => $max_date ) ); + } + /** * Buduje deterministyczną mapę yacht_id → kolor z palety. * diff --git a/wp-content/plugins/yacht-booking-system/frontend/assets/css/calendar-all.css b/wp-content/plugins/yacht-booking-system/frontend/assets/css/calendar-all.css index 9839670..e70f413 100644 --- a/wp-content/plugins/yacht-booking-system/frontend/assets/css/calendar-all.css +++ b/wp-content/plugins/yacht-booking-system/frontend/assets/css/calendar-all.css @@ -86,15 +86,30 @@ cursor: default; } -.yacht-calendar-all .fc-event-title, .yacht-calendar-all .fc-daygrid-event-dot, .yacht-calendar-all .fc-event-time { display: none !important; } -/* Pasek eventu zawsze wyższy żeby był czytelny bez tekstu */ +/* Custom kontener tytułu (renderowany przez eventContent w JS). */ +.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; +} + +/* Pasek eventu wyższy + gap między dziennymi segmentami (rezerwacja wielonocna + = N osobnych pasków zamiast jednej belki — patrz REST split per-day). */ .yacht-calendar-all .fc-daygrid-event { min-height: 18px; + margin: 1px 2px !important; + border-radius: 2px; } /* Half-day visual: gradient wpisywany przez calendar-all.js (eventDidMount), który @@ -106,6 +121,11 @@ font-size: 10px; } + .yacht-calendar-all .yc-event-title { + font-size: 10px; + padding: 1px 4px; + } + .yacht-calendar-all .fc-toolbar.fc-header-toolbar { flex-direction: column; gap: 8px; diff --git a/wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar-all.js b/wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar-all.js index 7d499a4..17d905c 100644 --- a/wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar-all.js +++ b/wp-content/plugins/yacht-booking-system/frontend/assets/js/calendar-all.js @@ -21,6 +21,14 @@ return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()); } + function firstOfMonth(d) { + return new Date(d.getFullYear(), d.getMonth(), 1); + } + + function nextMonthFirst(d) { + return new Date(d.getFullYear(), d.getMonth() + 1, 1); + } + function initCalendar(wrapper) { var $wrapper = $(wrapper); var $cal = $wrapper.find('.yacht-calendar-all'); @@ -32,6 +40,31 @@ var heightPx = parseInt($wrapper.data('height'), 10); if (!heightPx || heightPx < 200) heightPx = 650; + // Bounds: pobierz max_booking_date żeby ograniczyć validRange w FullCalendar. + // Endpoint `/availability/bounds` jest siostrzany do `/availability/all`. + var boundsUrl = restUrl.replace(/\/availability\/all.*$/, '/availability/bounds'); + + $.getJSON(boundsUrl) + .done(function (data) { + buildCalendar($wrapper, $cal, restUrl, heightPx, data && data.max_booking_date ? data.max_booking_date : null); + }) + .fail(function () { + // Graceful degradation — kalendarz bez validRange. + buildCalendar($wrapper, $cal, restUrl, heightPx, null); + }); + } + + function buildCalendar($wrapper, $cal, restUrl, heightPx, maxBookingDate) { + var today = new Date(); + var rangeStart = firstOfMonth(today); + var rangeEnd; + if (maxBookingDate) { + var maxDate = new Date(maxBookingDate + 'T00:00:00'); + rangeEnd = maxDate >= today ? nextMonthFirst(maxDate) : nextMonthFirst(today); + } else { + rangeEnd = nextMonthFirst(today); + } + var calendar = new window.FullCalendar.Calendar($cal.get(0), { initialView: 'dayGridMonth', locale: 'pl', @@ -41,6 +74,7 @@ center: 'title', right: '' }, + validRange: { start: rangeStart, end: rangeEnd }, height: heightPx, displayEventTime: false, eventDisplay: 'block', @@ -63,6 +97,10 @@ var color = info.event.backgroundColor || '#3498db'; info.el.style.setProperty('--yc-event-color', color); + // Native tooltip na hover — tytuł + data dnia. + var dayStr = info.event.startStr ? info.event.startStr.slice(0, 10) : ''; + info.el.setAttribute('title', info.event.title + (dayStr ? ' (' + dayStr + ')' : '')); + // Half-day visual: gradient z half-cell transparent na pierwszym/ostatnim dniu. // Liczone po renderze (wymaga znajomości szerokości komórki dnia w siatce). // Przekładamy na requestAnimationFrame żeby DOM był ułożony. @@ -70,9 +108,11 @@ applyHalfDayGradient(info); }); }, - eventContent: function () { - // Pasek koloru bez treści — privacy: nie pokazujemy nazwisk klientów ani nazw jachtów. - return { html: '' }; + eventContent: function (arg) { + // Tytuł renderowany przez .text() → automatyczny escaping (XSS safe). + var title = arg.event.title || ''; + var $el = $('
').text(title); + return { domNodes: [$el.get(0)] }; } }); @@ -169,8 +209,11 @@ if (halfPct > 49) halfPct = 49; var color = el.style.getPropertyValue('--yc-event-color') || '#3498db'; - var startTrans = info.isStart; - var endTrans = info.isEnd; + // Server emituje 1 event per doba — info.isStart/isEnd byłyby zawsze true. + // Flagi half-day pochodzą z extendedProps (server wie czy to pierwszy/ostatni dzień rezerwacji). + var props = info.event.extendedProps || {}; + var startTrans = !!props.is_first; + var endTrans = !!props.is_last_night; // Build gradient. var stops; diff --git a/wp-content/plugins/yacht-booking-system/yacht-booking-system.php b/wp-content/plugins/yacht-booking-system/yacht-booking-system.php index a6e0d71..0224238 100644 --- a/wp-content/plugins/yacht-booking-system/yacht-booking-system.php +++ b/wp-content/plugins/yacht-booking-system/yacht-booking-system.php @@ -3,7 +3,7 @@ * Plugin Name: Yacht Booking System * Plugin URI: https://jachty.pagedev.pl * Description: System rezerwacji jachtów z kalendarzem i integracją z Google Calendar - * Version: 1.1.0 + * Version: 1.2.1 * Author: PageDev * Author URI: https://pagedev.pl * Text Domain: yacht-booking @@ -20,7 +20,7 @@ if ( ! defined( 'ABSPATH' ) ) { } // Define plugin constants -define( 'YACHT_BOOKING_VERSION', '1.1.0' ); +define( 'YACHT_BOOKING_VERSION', '1.2.1' ); define( 'YACHT_BOOKING_PLUGIN_FILE', __FILE__ ); define( 'YACHT_BOOKING_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'YACHT_BOOKING_PLUGIN_URL', plugin_dir_url( __FILE__ ) );