Files
2026-05-07 14:57:59 +02:00

330 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 09-finalizacja
plan: 04
type: execute
wave: 1
depends_on: []
files_modified:
- wp-content/plugins/yacht-booking-system/includes/class-settings.php
- wp-content/plugins/yacht-booking-system/includes/class-installer.php
- wp-content/plugins/yacht-booking-system/admin/class-admin.php
- wp-content/plugins/yacht-booking-system/admin/views/settings-page.php
- wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php
- wp-content/plugins/yacht-booking-system/api/class-rest-controller.php
- wp-content/plugins/yacht-booking-system/includes/class-yacht-booking.php
- wp-content/plugins/yacht-booking-system/frontend/class-calendar-widget-all.php (NEW)
- wp-content/plugins/yacht-booking-system/frontend/class-shortcode.php
- wp-content/plugins/yacht-booking-system/assets/js/calendar-all.js (NEW)
- wp-content/plugins/yacht-booking-system/assets/css/calendar-all.css (NEW)
autonomous: false
delegation: off
---
<objective>
## Goal
Wprowadzić tryb "globalnej" synchronizacji iCal (wszystkie eventy z jednego feedu trafiają do wspólnego storage bez per-yacht matchingu) oraz nowy publiczny widget kalendarza pokazujący zajętość WSZYSTKICH publikowanych jachtów na jednej siatce, z kolorami per-jacht i wizualnym efektem "half-day" na pierwszym/ostatnim dniu rezerwacji.
## Purpose
- Klient prowadzi jeden wspólny Google Calendar do wszystkich jachtów. Obecny tryb wymaga prefiksu w SUMMARY ("Maja - Klient") i wyrzuca nierozpoznane eventy. Nowy tryb pozwala importować je wszystkie bez utraty (np. "Pierwszy dzień szkoły") jako wydarzenia informacyjne na wspólnym widoku.
- Frontend ma jedno miejsce ("kalendarz floty") gdzie potencjalny klient widzi wszystkie zajętości naraz, bez rozpraszającego efektu ukośników z trybu per-jacht. Half-day daje wizualny sygnał, że ktoś może wynająć od/do południa.
## Output
- Settings: nowy select `ical_sync_mode` (`per_yacht` | `global`)
- iCal Import: gałąź `global` zapisuje WSZYSTKIE eventy do wspólnego storage (`_booking_source = 'ical_global_calendar'`, brak yacht_id), bez wpisów do `wp_yacht_availability`
- REST: `GET /availability/all` — zwraca eventy FullCalendar dla wszystkich jachtów + globalne wydarzenia, z kolorami auto-paleta i czasami timed (12:00 → 12:00)
- Nowy widget Elementor `Yacht_Calendar_All_Widget` + shortcode `[yacht_calendar_all]` renderujący FullCalendar timed-grid z legendą jachtów
</objective>
<context>
<clarifications>
- **Unmatched** — Co zrobić z eventami iCal bez prefiksu lub z nieznaną nazwą jachtu?
→ Odpowiedź: Dodatkowa opcja w ustawieniach przełączająca tryb sync. Tryb `per_yacht` (obecny — match po prefiksie) lub `global` (jeden wspólny kalendarz, wszystkie eventy zapisywane bez przypisywania do jachtów; w danym dniu może być kilka wydarzeń).
- **Widget zakres** — Nowy widget pokazuje które jachty?
→ Odpowiedź: Wszystkie publikowane jachty (auto, `post_status = publish`).
- **Rozróżnienie** — Jak odróżnić rezerwacje różnych jachtów?
→ Odpowiedź: Kolor per jacht z auto palety (predefiniowana tablica kolorów indeksowana wg kolejności yacht_id, deterministycznie).
- **Half-day** — Jak pokazać start/koniec w połowie dnia?
→ Odpowiedź: FullCalendar timed events. Dane all-day konwertowane na timed (start = data 12:00, end = data+1 12:00) na poziomie REST endpointu.
</clarifications>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
@.paul/codebase/architecture.md
@.paul/codebase/db_schema.md
## Prior Work
@.paul/phases/09-finalizacja/09-02-SUMMARY.md
@.paul/phases/09-finalizacja/09-03-SUMMARY.md
## Source Files
@wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php
@wp-content/plugins/yacht-booking-system/includes/class-settings.php
@wp-content/plugins/yacht-booking-system/admin/class-admin.php
@wp-content/plugins/yacht-booking-system/admin/views/settings-page.php
@wp-content/plugins/yacht-booking-system/api/class-rest-controller.php
@wp-content/plugins/yacht-booking-system/frontend/class-calendar-widget.php
@wp-content/plugins/yacht-booking-system/frontend/class-shortcode.php
@wp-content/plugins/yacht-booking-system/includes/class-yacht-booking.php
@wp-content/plugins/yacht-booking-system/includes/class-installer.php
</context>
<acceptance_criteria>
## AC-1: Settings — przełącznik trybu synchronizacji iCal
```gherkin
Given admin otwiera "Yacht Booking Settings Synchronizacja"
When zobaczy nowe pole "Tryb synchronizacji iCal" z opcjami "Per jacht" i "Wspólny kalendarz"
And wybiera "Wspólny kalendarz" i zapisuje
Then opcja `yacht_booking_ical_sync_mode` w wp_options = "global"
And formularz pokazuje wybraną wartość po reload
And opis pomocniczy wyjaśnia różnicę między trybami (per_yacht: matching po prefiksie / global: wszystkie eventy bez filtrowania)
```
## AC-2: Import iCal w trybie globalnym — wszystkie eventy bez filtrowania
```gherkin
Given `yacht_booking_ical_sync_mode = "global"` i feed URL ustawiony
And feed zawiera 32 eventy (z prefiksami "Kubuś -", "Maja -", bez prefiksu "Pierwszy dzień szkoły", bez SUMMARY itp.)
When uruchamia się hook `yacht_booking_ical_global_import`
Then powstaje booking dla KAŻDEGO eventu z UID i `start`/`end` (poza past events)
And `_booking_source = "ical_global_calendar"`, `_booking_yacht_id = 0`, `_booking_status = "confirmed"`
And eventy NIE są wpisywane do `wp_yacht_availability` (nie blokują dostępności żadnego jachtu)
And eventy bez SUMMARY otrzymują domyślny tytuł (np. "Wydarzenie kalendarza")
And ponowne uruchomienie cron-u (idempotencja) bookingi z tymi samymi UID są aktualizowane, nie duplikowane
And eventy obecne w DB ale brakujące w nowym feedzie są usuwane (stale cleanup)
```
## AC-3: Import iCal w trybie per-jacht — bez regresji
```gherkin
Given `yacht_booking_ical_sync_mode = "per_yacht"` (domyślne dla istniejących instalacji)
When uruchamia się hook globalnego importu
Then zachowanie jest IDENTYCZNE jak przed tą zmianą (matching po prefiksie SUMMARY, wpisy do `wp_yacht_availability`, stale cleanup ograniczony do `_booking_source = "ical_import_global"`)
And istniejące bookingi importowane wcześniej w trybie per-jacht działają dalej i są nadal aktualizowane
```
## AC-4: REST endpoint `GET /availability/all`
```gherkin
Given istnieje min. 2 publikowane jachty (np. "Kubuś", "Maja") z rezerwacjami
And w trybie globalnym istnieją wydarzenia z `_booking_source = "ical_global_calendar"`
When klient niezalogowany wywołuje `GET /wp-json/yacht-booking/v1/availability/all?start=2026-05-01&end=2026-12-31`
Then odpowiedź to JSON tablica obiektów FullCalendar event:
{ id, title, start (ISO datetime z "T12:00:00"), end (ISO datetime z "T12:00:00"), color, yacht_id }
And bookingi per-jacht mają `color` z deterministycznej palety (tablica 8 kolorów indeksowana wg kolejności yacht_id)
And globalne wydarzenia mają osobny kolor (np. szary `#7f8c8d`) i `yacht_id = 0`
And tylko bookingi `confirmed`/`pending` są zwracane (nie `cancelled`/`rejected`)
And `start <= end` zachowane
```
## AC-5: Nowy widget i shortcode
```gherkin
Given strona z `[yacht_calendar_all]` lub widgetem Elementor "Yacht Calendar (wszystkie jachty)"
When klient otwiera stronę
Then widoczny FullCalendar w widoku miesięcznym (dayGridMonth)
And eventy z `GET /availability/all` są renderowane jako timed events 12:00 12:00 (efekt half-day na pierwszym/ostatnim dniu rezerwacji)
And nad/pod kalendarzem jest legenda kolorów (kropka + nazwa jachtu) auto-generowana z palety
And widget jest read-only (brak formularza rezerwacji, brak klikalności dnia)
And brak ukośników (cell background pełny / pusty)
```
## AC-6: Auto paleta kolorów per jacht
```gherkin
Given lista jachtów posortowana po ID rosnąco
When system mapuje yacht_id kolor
Then używana jest stała tablica min. 8 kolorów (np. #3498db, #e74c3c, #2ecc71, #f39c12, #9b59b6, #1abc9c, #34495e, #d35400)
And mapowanie jest deterministyczne (yacht_id index modulo długość palety) ten sam yacht zawsze ten sam kolor
And paleta jest wspólna dla REST endpointu i frontendowej legendy
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Settings — przełącznik trybu sync iCal + zachowanie per_yacht jako default</name>
<files>
wp-content/plugins/yacht-booking-system/includes/class-settings.php,
wp-content/plugins/yacht-booking-system/includes/class-installer.php,
wp-content/plugins/yacht-booking-system/admin/class-admin.php,
wp-content/plugins/yacht-booking-system/admin/views/settings-page.php
</files>
<action>
1. `Settings`: dodaj typowany getter `get_ical_sync_mode()` zwracający `'per_yacht'` lub `'global'` (default: `'per_yacht'`). Klucz opcji: `yacht_booking_ical_sync_mode`. Dodaj setter w obecnym wzorcu klasy.
2. `Installer`: dopisz default opcji `yacht_booking_ical_sync_mode = 'per_yacht'` do tablicy defaultów (nie nadpisuj istniejącej wartości jeśli jest).
3. `Admin::process_settings_save()`: obsłuż nowe pole formularza (sanitize_text_field + whitelista wartości; nieznane → fallback `per_yacht`).
4. `settings-page.php` (sekcja "Synchronizacja"): dodaj `<select name="ical_sync_mode">` z dwoma opcjami i opisem pomocniczym po polsku wyjaśniającym różnicę. Selected = bieżąca wartość. Zachowaj wzorzec nonce + i18n istniejący w pliku.
Avoid: dodawanie nowej tabeli/migration — wystarczy `wp_options`. Avoid: zmiana default na `global` (regresja dla istniejących klientów).
</action>
<verify>
- W WP Admin → Settings widzę nowe pole, mogę zapisać "Wspólny kalendarz" i po reload jest zaznaczone.
- `wp option get yacht_booking_ical_sync_mode` zwraca `global`.
- `php -l` na każdym zmienionym pliku.
</verify>
<done>AC-1 satisfied.</done>
</task>
<task type="auto">
<name>Task 2: ICal_Import — gałąź global (storage bez per-yacht matchingu, idempotencja, stale cleanup)</name>
<files>
wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php
</files>
<action>
1. W `run_global_import()` po pobraniu i sparsowaniu eventów odczytaj `Settings::get_ical_sync_mode()`.
2. Jeśli `per_yacht` — kod bez zmian (obecna ścieżka match po prefiksie + wpisy do availability).
3. Jeśli `global` — pomiń `build_yacht_lookup_map()` / `match_yacht_by_prefix()`. Dla KAŻDEGO eventu (z UID + start + end, pomijając past events) wywołaj nową metodę `upsert_global_calendar_event($event, $existing_id)`:
- Wstawia/aktualizuje CPT `yacht_booking` z metami: `_booking_source = 'ical_global_calendar'`, `_booking_yacht_id = 0`, `_booking_status = 'confirmed'`, `_ical_event_uid = uid`, `_booking_start_date`, `_booking_end_date`, `_booking_customer_name = 'Google Calendar (kalendarz wspólny)'`, `_booking_total_price = 0`, `_booking_notes = summary`.
- Tytuł postu: `summary` lub fallback `__( 'Wydarzenie kalendarza', 'yacht-booking' )` gdy puste.
- NIE wywołuj `Availability::mark_as_booked()` (wydarzenia globalne nie blokują dostępności jachtów).
4. Stale cleanup: w trybie global pobierz mapę istniejących bookingów po `_booking_source = 'ical_global_calendar'` (nowa stała `GLOBAL_CALENDAR_SOURCE`) i usuń te których UID brakuje w nowym feedzie (`wp_delete_post`, force).
5. Stale cleanup w trybie per_yacht musi pozostać ograniczony do `_booking_source = 'ical_import_global'` (obecna stała) — nie tykać bookingów `ical_global_calendar` w drugą stronę i odwrotnie. Dwie stałe, dwie ścieżki cleanup.
6. Logowanie: w trybie global loguj liczbę zaimportowanych / zaktualizowanych / usuniętych eventów.
Avoid: dotykanie `wp_yacht_availability` w trybie global. Avoid: zmiana sygnatury `run_global_import()` (cron callback).
</action>
<verify>
- Test ręczny: ustaw URL na lokalną kopię `D:\temp\basic.ics` (zhostowaną w WP uploads lub serve), włącz tryb `global`, uruchom `wp cron event run yacht_booking_ical_global_import`.
- W WP Admin → Bookings widzę ~30 nowych pozycji z prefiksem "GCal:" lub tytułem eventu, brak wpływu na dostępność `yacht`.
- Drugie uruchomienie cron-u nie tworzy duplikatów (count się zgadza).
- `wp db query "SELECT COUNT(*) FROM wp_yacht_availability WHERE booking_id IN (SELECT post_id FROM wp_postmeta WHERE meta_key='_booking_source' AND meta_value='ical_global_calendar')"` zwraca 0.
- Przełącz na `per_yacht` i uruchom — nowa logika nie ma efektu, stara działa jak wcześniej.
- `php -l`.
</verify>
<done>AC-2 + AC-3 satisfied.</done>
</task>
<task type="auto">
<name>Task 3: REST endpoint /availability/all — agregacja wszystkich jachtów + globalnych eventów z kolorami i timed</name>
<files>
wp-content/plugins/yacht-booking-system/api/class-rest-controller.php,
wp-content/plugins/yacht-booking-system/includes/class-yacht-booking.php
</files>
<action>
1. W `Rest_Controller`: dodaj nową metodę `get_all_availability( $request )` zarejestrowaną jako `GET /yacht-booking/v1/availability/all` (callback w `register_rest_routes()`, `permission_callback => '__return_true'`).
2. Walidacja parametrów `start`, `end` (Y-m-d, opcjonalne — default: pierwszy dzień bieżącego miesiąca / + 12 mies.).
3. Pobierz publikowane jachty (`get_posts` post_type=yacht). Zbuduj mapę `yacht_id => color` przez nową statyczną metodę `Rest_Controller::get_yacht_color_palette( array $yacht_ids )`:
- Stała tablica 8 kolorów hex.
- Mapuj `yacht_id` (sortowane rosnąco) na `palette[ index % count(palette) ]`.
- Stała kolor globalnego eventu: `#7f8c8d`.
4. Query bookings: `post_type = yacht_booking`, status `confirmed` lub `pending`, daty przecinające `start``end`. Per booking zwróć:
```
{
id: booking_id,
title: '<nazwa_jachtu> — <imię_klienta>' lub tytuł postu dla global,
start: '<start_date>T12:00:00',
end: '<end_date>T12:00:00',
color: <z palety lub szary dla global>,
yacht_id: <id lub 0>
}
```
5. Globalne eventy (`_booking_source = ical_global_calendar`) traktowane jednolicie z yacht_id=0 i kolorem szarym; tytuł = `_booking_notes` lub post_title.
6. Pomiń `cancelled`/`rejected`.
7. W `Yacht_Booking::register_rest_routes()` upewnij się, że nowa trasa się rejestruje (jeśli rejestracja jest delegowana do `Rest_Controller::register_routes()` — bez zmiany).
Avoid: cache na poziomie endpointu (małe wolumeny, prosto). Avoid: dodawanie nowych zależności / SDK.
</action>
<verify>
- `curl 'https://<host>/wp-json/yacht-booking/v1/availability/all?start=2026-05-01&end=2026-12-31'` zwraca tablicę JSON z polami { id, title, start, end, color, yacht_id }.
- Każdy yacht_id ma stały kolor między requestami.
- Bookingi per-yacht mają yacht_id != 0; globalne mają yacht_id = 0 i kolor `#7f8c8d`.
- Daty zawierają `T12:00:00`.
- `php -l`.
</verify>
<done>AC-4 + AC-6 satisfied.</done>
</task>
<task type="auto">
<name>Task 4: Widget Elementor + shortcode "yacht_calendar_all" + JS/CSS</name>
<files>
wp-content/plugins/yacht-booking-system/frontend/class-calendar-widget-all.php (NEW),
wp-content/plugins/yacht-booking-system/frontend/class-shortcode.php,
wp-content/plugins/yacht-booking-system/assets/js/calendar-all.js (NEW),
wp-content/plugins/yacht-booking-system/assets/css/calendar-all.css (NEW),
wp-content/plugins/yacht-booking-system/includes/class-yacht-booking.php
</files>
<action>
1. **Nowy widget** `Yacht_Calendar_All_Widget` (extends `\Elementor\Widget_Base`) — wzorowany na `Calendar_Widget`, name=`yacht-calendar-all`, title=`Yacht Calendar (wszystkie jachty)`, ikona kalendarza. `render()` wypisuje `<div class="yacht-calendar-all" data-rest="<URL>"></div>`.
2. **Shortcode** `[yacht_calendar_all]` w `Shortcode` (lub nowa metoda) — bez atrybutów. Wypisuje ten sam markup co widget.
3. **`Yacht_Booking`**: rejestracja widgetu w `register_elementor_widgets()` (dopisz obok istniejącego). Conditional enqueue assets `calendar-all.js`/`calendar-all.css` gdy w treści jest shortcode lub na stronie z widgetem (rozszerz obecny mechanizm).
4. **`calendar-all.js`** (IIFE jQuery): inicjalizacja FullCalendar v6 (CDN), `initialView: 'dayGridMonth'`, `events: <URL z data-rest>`, `displayEventTime: false`, `eventDisplay: 'block'`. Po `eventDidMount` dodaj klasę `.is-half-day-start` / `.is-half-day-end` do pierwszej/ostatniej komórki eventu (na podstawie hours = 12). Renderuj legendę (lista jachtów + kolorów) pobraną z dodatkowego endpointu `GET /availability/all/legend` LUB wyciągniętą z eventów po pierwszym fetch (wybierz prostsze — preferuj wyciąganie z eventów na froncie).
5. **`calendar-all.css`** — minimalistyczny styl: ukryj kropkę przy timed event, pełne wypełnienie kafelka kolorem eventu, half-day cell renderowany przez gradient `linear-gradient(to right, transparent 50%, var(--color) 50%)` na pierwszym dniu i odwrotnie na ostatnim. Brak ukośników. Mobile-first.
6. Brak modal-a / formularza rezerwacji — read-only.
Avoid: npm/bundler — FullCalendar via CDN jak istniejący widget. Avoid: dotykanie istniejącego widgetu/CSS.
</action>
<verify>
- `php -l` na nowych/zmienionych plikach PHP.
- Strona z `[yacht_calendar_all]` renderuje kalendarz z eventami pobranymi z REST.
- Eventy z różnych jachtów mają różne kolory (paleta).
- Pierwszy/ostatni dzień rezerwacji wizualnie wypełniony do połowy (gradient/half cell).
- Brak konsoli błędów JS.
- Widget pojawia się w panelu Elementor pod kategorią pluginu.
</verify>
<done>AC-5 satisfied (półdniowe rendery + legenda + read-only). AC-6 deterministyczna paleta wspólna z REST.</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
- Settings: nowa opcja "Tryb synchronizacji iCal" (per_yacht / global)
- iCal Import: tryb global importuje wszystkie eventy bez filtrowania, nie blokuje dostępności jachtów
- REST: `GET /availability/all` zwraca wszystkie rezerwacje + globalne eventy z kolorami i timed (12:00)
- Frontend: shortcode `[yacht_calendar_all]` + widget Elementor "Yacht Calendar (wszystkie jachty)" z efektem half-day i legendą kolorów
</what-built>
<how-to-verify>
1. Deploy plików przez FTP (ftp-kr) i ewentualnie aktywuj plugin (jeśli wymagane przez Installer dla nowej opcji).
2. Wejdź w WP Admin → Yacht Booking → Settings → sekcja Synchronizacja: ustaw "Wspólny kalendarz", podaj URL feedu (lub tymczasowo `D:\temp\basic.ics` wystawiony przez serwer testowy), zapisz.
3. Uruchom ręcznie `yacht_booking_ical_global_import` (np. WP-CLI: `wp cron event run yacht_booking_ical_global_import` lub przycisk "Synchronizuj teraz" jeśli istnieje).
4. Sprawdź WP Admin → Bookings: powinny pojawić się ~30 wydarzeń bez yacht_id.
5. Zweryfikuj że dostępność jachtów (kalendarz pojedynczego jachtu) się NIE zmieniła.
6. Stwórz testową stronę z `[yacht_calendar_all]`. Otwórz w przeglądarce.
7. Sprawdź: a) różne kolory dla "Maja" i "Kubuś", b) szare dla globalnych, c) pierwszy/ostatni dzień rezerwacji wypełniony do połowy, d) brak ukośników, e) legenda widoczna.
8. Przełącz tryb na "Per jacht", uruchom cron — sprawdź że stary mechanizm nadal działa (matching po prefiksie, wpisy w availability).
</how-to-verify>
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- `frontend/class-calendar-widget.php` (istniejący widget per-jacht — kopiujemy NOWY plik, nie dotykamy starego)
- `assets/js/calendar.js`, `assets/css/calendar.css` (assety istniejącego widgetu)
- Schema tabeli `wp_yacht_availability` — nie dodajemy kolumn
- `Booking::create()` flow (REST POST /bookings) — flow rezerwacji frontendowej bez zmian
- Settings: nie zmieniamy domyślnej wartości `ical_sync_mode` na `global` (regresja). Default = `per_yacht`.
- Stała `ICal_Import::GLOBAL_IMPORT_SOURCE = 'ical_import_global'` (per-jacht stale cleanup) — dodajemy DRUGĄ stałą `GLOBAL_CALENDAR_SOURCE = 'ical_global_calendar'`, nie zmieniamy istniejącej
## SCOPE LIMITS
- Brak konfigurowalnej palety kolorów (color picker per jacht) — auto paleta z hardcoded tablicy. Color picker w meta jachtu = osobny plan jeśli klient zechce.
- Brak osobnego widoku "tylko globalne wydarzenia" — globalne pokazują się tylko na nowym wspólnym widgecie.
- Brak konfiguracji shortcode (atrybuty `yachts=`, `view=`) — wszystkie publikowane, dayGridMonth.
- Brak klikalności dnia / formularza rezerwacji na nowym widgecie — read-only podgląd.
- Brak migracji istniejących bookingów `_booking_source = ical_import_global` na nowy storage. Klient musi ręcznie usunąć stare jeśli przełączy się trwale na global.
- Bez zmian dla widgetu Elementor istniejącego (per-jacht) — dwa widgety obok siebie.
</boundaries>
<verification>
Before declaring plan complete:
- [ ] `php -l` na każdym zmienionym/nowym pliku PHP — bez błędów
- [ ] `wp option get yacht_booking_ical_sync_mode` zwraca poprawną wartość po zapisie
- [ ] W trybie `global` po cron-imporcie liczba wydarzeń w `Bookings` ≈ liczba VEVENT z feedu (poza past)
- [ ] W trybie `global` brak wpisów do `wp_yacht_availability` dla `_booking_source = ical_global_calendar`
- [ ] W trybie `per_yacht` zachowanie identyczne jak przed planem (regression check)
- [ ] `GET /availability/all` zwraca poprawny JSON dla niezalogowanego klienta
- [ ] Strona z `[yacht_calendar_all]` renderuje się bez błędów JS w konsoli
- [ ] Half-day visual widoczny na pierwszym i ostatnim dniu każdej rezerwacji
- [ ] Legenda kolorów spójna z kolorami eventów
- [ ] Wszystkie acceptance criteria spełnione
</verification>
<success_criteria>
- Klient może przełączyć tryb sync w settings i zobaczyć efekt po cron-imporcie
- Wszystkie eventy z basic.ics (32) są zaimportowane w trybie global (poza past events)
- Nowy widget pokazuje wszystkie jachty + globalne eventy z kolorami i half-day
- Brak regresji w trybie per-jacht
- Brak regresji w istniejącym widgecie per-jacht
- Brak nowych ostrzeżeń PHP / błędów konsoli
</success_criteria>
<output>
After completion, create `.paul/phases/09-finalizacja/09-04-SUMMARY.md`
</output>