20 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, delegation
| phase | plan | type | wave | depends_on | files_modified | autonomous | delegation | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 09-finalizacja | 02 | execute | 1 |
|
|
false | off |
Purpose
Klient (właściciel wypożyczalni) chce zarządzać rezerwacjami wszystkich jachtów w jednym Google Calendar — jednym widokiem floty. Tworzy ręcznie eventy w GCal (np. blokady, rezerwacje telefoniczne) używając prefiksu z nazwą jachtu, a strona automatycznie przypisuje je do właściwego jachtu. Jednocześnie rezerwacje złożone przez frontend trafiają z powrotem do tego samego kalendarza w spójnym formacie.
Output
- Globalny iCal Export feed (jeden URL z tokenem) — zawiera rezerwacje wszystkich jachtów z prefiksem nazwy jachtu w SUMMARY
- Globalny iCal Import URL — parser dopasowuje prefiks do jachtu, importuje tylko rozpoznane eventy
- Settings page — pola dla globalnego import URL i wyświetlanie globalnego export URL
- Pole
_yacht_gcal_alias(opcjonalny alias dla skróconej nazwy w GCal) - Per-yacht feedy iCal pozostają sprawne (kompatybilność wsteczna)
Source Files
@wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-feed.php @wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php @wp-content/plugins/yacht-booking-system/admin/class-admin.php @wp-content/plugins/yacht-booking-system/includes/class-yacht.php @wp-content/plugins/yacht-booking-system/includes/class-settings.php
- **Mechanizm** — iCal feed (publiczny URL Google Calendar) zamiast OAuth API. Powód: klient nie chce przechodzić przez OAuth, woli dodać URL i mieć synchronizację automatyczną. → Odpowiedź: dwukierunkowo przez iCal — import z GCal feedu + export własnego feedu (admin subskrybuje go w GCal).-
Identyfikacja jachtu — po prefiksie w tytule eventu, separator
" - ". Przykład:"Maja - Kowalski 5 osób","Kubuś - blokada serwisowa". → Odpowiedź: parser wyciąga tekst przed pierwszym" - ", dopasowuje case-insensitive dopost_titlejachtu lub_yacht_gcal_alias(jeśli ustawiony — ma priorytet). -
Eventy bez rozpoznanego jachtu — ignorowane. → Odpowiedź: jeśli prefiks nie pasuje do żadnego jachtu (lub brak separatora
" - "), event pomijany w imporcie. Brak fallbacku na "blokuj wszystkie" — klient nie chce niespodzianek. -
Push do GCal — przez ten sam globalny feed iCal (admin subskrybuje feed pluginu w GCal). → Odpowiedź: rezerwacje ze strony pojawiają się w GCal po następnej refresh subskrypcji (Google odświeża iCal subscription co kilka godzin — limit Google'a, nie pluginu).
-
Frontend filtrowanie — kalendarz per jacht pokazuje tylko eventy danego jachtu. → Odpowiedź: bez zmian w widgecie — działa już teraz przez
_booking_yacht_id. Wystarczy że import poprawnie przypisze yacht_id.
<acceptance_criteria>
AC-1: Globalny iCal Export feed
Given admin ma kilka rezerwacji w bazie dla różnych jachtów (np. "Maja", "Kubuś")
When admin otwiera URL `/yacht-ical-global/{token}.ics`
Then odpowiedź ma Content-Type `text/calendar`
And feed zawiera VEVENT dla każdej niezanullowanej rezerwacji wszystkich jachtów
And SUMMARY każdego eventu ma format `"{nazwa_jachtu} - {imię_klienta}"` (lub z prefiksem `[Oczekująca]` gdy status pending)
And UID jest stabilny (`booking-{id}@{domain}`) — kolejne fetche zwracają ten sam UID
AC-2: Globalny iCal Import — dopasowanie po prefiksie
Given globalny iCal Import URL jest ustawiony w Settings
And kalendarz Google zawiera event "Maja - Test rezerwacja" (jacht "Maja" istnieje w bazie)
And kalendarz Google zawiera event "Nieznany - cokolwiek" (brak takiego jachtu)
And kalendarz Google zawiera event "Brak separatora" (bez " - ")
When uruchamiany jest cron `yacht_booking_ical_import` (lub manualny przycisk)
Then plugin tworzy/aktualizuje booking dla jachtu "Maja" z source `ical_import_global`
And blokuje dostępność `wp_yacht_availability` tylko dla yacht_id jachtu "Maja"
And event "Nieznany" jest pominięty (logowane jako info, brak booking)
And event "Brak separatora" jest pominięty
And kolejny przebieg (nawet bez zmian w GCal) nie tworzy duplikatów (idempotencja po UID)
AC-3: Globalny Import — czyszczenie usuniętych eventów
Given event "Maja - Foo" został zaimportowany przy poprzednim cron'ie
When admin usuwa go z Google Calendar
And uruchamia się kolejny cron import
Then powiązany booking (z source `ical_import_global` i tym UID) jest usuwany
And `Availability::clear_booking_availability()` zwalnia daty
AC-4: Alias jachtu (opcjonalny)
Given jacht ma `post_title="Marina Maja Sailing 35"` i `_yacht_gcal_alias="Maja"`
And event w GCal ma SUMMARY "Maja - Klient X"
When uruchamia się import
Then plugin dopasowuje event do tego jachtu po aliasie
And tworzy booking z `_booking_yacht_id` = ID tego jachtu
AC-5: Frontend filtrowanie (regresja)
Given booking dla jachtu "Maja" zaimportowany przez globalny iCal
When klient otwiera stronę z widgetem `[yacht_calendar yacht_id=ID_MAJA]`
Then kalendarz pokazuje datę rezerwacji jako zablokowaną
And widget jachtu "Kubuś" NIE pokazuje tej daty jako zablokowanej
AC-6: Settings UI
Given admin otwiera Settings → zakładka Google Calendar / iCal
When widzi sekcję "Globalna synchronizacja iCal"
Then widzi pole "iCal Import URL" (input type=url) — globalny URL kalendarza Google
And widzi pole tylko-do-odczytu "iCal Export URL" z linkiem `/yacht-ical-global/{token}.ics` i przyciskiem "Kopiuj"
And widzi przycisk "Wygeneruj nowy token" (regeneruje token, unieważniając poprzedni URL)
And widzi przycisk "Importuj teraz" (manual trigger run_global_import)
And w yacht-edit jest nowe pole "Alias dla Google Calendar" (placeholder: nazwa jachtu) — opcjonalne
</acceptance_criteria>
Task 1: Globalny iCal Export feed + alias jachtu wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-feed.php, wp-content/plugins/yacht-booking-system/includes/class-yacht.php, wp-content/plugins/yacht-booking-system/includes/class-installer.php 1. W `class-ical-feed.php` dodaj nową ścieżkę feed: - rewrite rule `^yacht-ical-global/([a-zA-Z0-9]+)\.ics$` → `index.php?yacht_ical_global=1&yacht_ical_token=$matches[1]` - query var `yacht_ical_global` - handler `handle_global_feed_request()` weryfikuje token przez `hash_equals` względem opcji `yacht_booking_global_ical_token` - metoda `output_global_ics()`: pobiera WSZYSTKIE bookingi (nie filtrowane po yacht), iteruje, dla każdego ustala SUMMARY = `"{nazwa_jachtu} - {imię_klienta}"` (analogicznie do istniejącego `output_ics`, ale z prefiksem nazwy jachtu z `Yacht::get_post_title( yacht_id )` zamiast tylko klienta) - skip bookingi ze statusem `cancelled` oraz z source `ical_import_global` (nie wysyłaj z powrotem do Google tego co stamtąd przyszło — anti-loop) - UID format: `booking-{id}@{domain}` (taki sam jak per-yacht — zachować stabilność) - X-WR-CALNAME: `"Wszystkie jachty - " . get_bloginfo('name')` 2. Helper `get_global_feed_token()` — generuje/zwraca token z opcji, `regenerate_global_token()` — nadpisuje token. 3. Helper `get_global_feed_url()` — zwraca pełny URL. 4. W `class-yacht.php` dodaj akcesory: - `get_gcal_alias( $yacht_id )` → `get_post_meta( $yacht_id, '_yacht_gcal_alias', true )` - `update_gcal_alias( $yacht_id, $alias )` → sanitize_text_field + update_post_meta 5. W `class-installer.php` (jeśli wersja schematu się ewoluuje) dodaj domyślne opcje: - `add_option( 'yacht_booking_global_ical_import_url', '' )` - `add_option( 'yacht_booking_global_ical_token', '' )` (token generowany lazy przy pierwszym fetch URL) - flush_rewrite_rules przy aktywacji (rewrite rule).Avoid: nie modyfikuj istniejących per-yacht feedów — działają jako fallback i są używane przez OTA/iCal subscribers innych systemów.
- `php -l class-ical-feed.php` (syntax OK)
- W przeglądarce: po zapisie pluginu i flush rewrite, URL `/yacht-ical-global/{token}.ics` zwraca .ics z eventami wszystkich jachtów, każdy z prefiksem nazwy jachtu
- `curl /yacht-ical-global/{token}.ics | grep "SUMMARY"` zawiera np. `SUMMARY:Maja - Kowalski`
- URL z błędnym tokenem zwraca 403
AC-1, AC-4 (część dot. aliasu — akcesory) zaspokojone
Task 2: Globalny iCal Import + parser prefiksu
wp-content/plugins/yacht-booking-system/integrations/ical/class-ical-import.php
1. Dodaj stałą `const GLOBAL_IMPORT_SOURCE = 'ical_import_global';` (osobna od per-yacht `IMPORT_SOURCE = 'ical_import'`).
2. Nowa metoda statyczna `run_global_import()`:
- Pobiera URL z `get_option( 'yacht_booking_global_ical_import_url' )`
- Jeśli pusty — return false
- `wp_remote_get` z timeout 30s
- parse_ics (re-use istniejącej metody, jest prywatna — wystawić jako protected lub wynieść do helpera; preferowane: zmienić visibility na `protected` lub utworzyć publiczną wrapping)
- Pre-fetch: lista jachtów + mapa `lowercase_name_or_alias → yacht_id`. Budowa: dla każdego jachtu klucz = `_yacht_gcal_alias` (jeśli != '') albo `post_title`, wszystko `mb_strtolower` + trim.
- Pre-fetch: `existing_global_map` — bookingi z source `GLOBAL_IMPORT_SOURCE`, mapa `uid → booking_id` (analogicznie do `get_existing_import_map` ale bez filtrowania po yacht).
- Iteracja po eventach:
a. Skip jeśli brak uid/start/end lub event w przeszłości (`strtotime($event['end']) < time()`)
b. Parse SUMMARY: `$pos = mb_strpos( $summary, ' - ' )`. Jeśli `$pos === false` → log "skip: no separator", continue.
c. `$prefix = mb_strtolower( trim( mb_substr( $summary, 0, $pos ) ) )`
d. Lookup w mapie. Jeśli brak → log "skip: yacht not found for prefix '{prefix}'", continue.
e. Wywołaj `upsert_global_booking( $yacht_id, $event, $existing_id )` (nowa metoda — jak `upsert_booking` ale z `_booking_source = GLOBAL_IMPORT_SOURCE`, `_booking_customer_name = "Google Calendar (import)"`, post_title `"GCal: {summary}"`).
f. Dodaj uid do `$seen_uids`.
- Po pętli: stale cleanup — dla każdego `$uid => $booking_id` w `$existing_global_map` którego nie ma w `$seen_uids` → `Availability::clear_booking_availability($booking_id)` + `wp_delete_post($booking_id, true)`.
- `update_option( 'yacht_booking_global_ical_last_import', current_time('mysql') )`.
- return true.
3. Hook cron: w `register()` dodać `add_action( 'yacht_booking_ical_global_import', array( __CLASS__, 'run_global_import' ) )` i w `setup_cron()` zarejestruj hourly.
4. Hook do uruchomienia obu importów (per-yacht + global) z jednego cron tick'a — nie zmieniać istniejącego, dodać osobny `wp_schedule_event` dla `yacht_booking_ical_global_import`.
Avoid:
- Nie usuwać per-yacht importu (kompatybilność)
- Nie używać sztywno `strpos` po bajtach — używać `mb_*` (polskie znaki w nazwach jachtów: "Kubuś", "Maja Słoneczna")
- Nie matchować po fragmentach — tylko exact equality po lower-case (inaczej "Maja" pasuje do "Maja Słoneczna" jednocześnie)
- `php -l class-ical-import.php`
- Test ręczny: stworzyć w Google Calendar event "Maja - test", subskrybować feed Google iCal URL w settings, kliknąć "Importuj teraz", sprawdzić że w admin → Bookings pojawia się booking przypisany do jachtu "Maja"
- Event "FooBar - test" (brak takiego jachtu) — nie tworzy bookingu, w error log linia "skip: yacht not found"
- Event "BezSeparatora" — nie tworzy bookingu
- Drugi cron tick — brak duplikatu
- Usunięcie eventu w GCal + cron → booking zniknął, daty zwolnione w `wp_yacht_availability`
AC-2, AC-3, AC-4 (matching po aliasie) zaspokojone
Task 3: Settings UI + yacht-edit alias + admin actions
wp-content/plugins/yacht-booking-system/admin/class-admin.php,
wp-content/plugins/yacht-booking-system/admin/views/settings.php,
wp-content/plugins/yacht-booking-system/admin/views/yacht-edit.php
1. `settings.php` — dodaj sekcję "Globalna synchronizacja iCal":
- Pole `yacht_booking_global_ical_import_url` (input type=url, full width)
- Read-only display: globalny export URL (`ICal_Feed::get_global_feed_url()`) — input readonly + przycisk "Kopiuj" (JS clipboard)
- Przycisk "Wygeneruj nowy token" (form POST z nonce + action `regenerate_global_ical_token`)
- Przycisk "Importuj teraz" (form POST z nonce + action `run_global_ical_import`)
- Wyświetl `yacht_booking_global_ical_last_import` ("Ostatni import: ...")
- Krótka instrukcja: "Skopiuj Export URL i dodaj jako 'Z URL-a' w Google Calendar (Inne kalendarze → Z URL). Eventy w Google nazywaj wg wzorca: 'Nazwa jachtu - opis'. Wklej iCal URL kalendarza Google poniżej."
2. `class-admin.php` — w `process_settings_save()` (lub equivalent) zapisz nowe pole.
3. `class-admin.php` — `process_booking_actions()` (lub nowy `process_ical_actions()`):
- obsłuż `regenerate_global_ical_token` → `ICal_Feed::regenerate_global_token()` + `?notice=token_regenerated` redirect
- obsłuż `run_global_ical_import` → `ICal_Import::run_global_import()` + `?notice=import_done` redirect
4. `yacht-edit.php` — dodaj wiersz tabeli "Alias dla Google Calendar":
- input text `yacht_gcal_alias`, value = `Yacht::get_gcal_alias( $yacht_id )`
- Description: "Opcjonalny krótki alias używany w tytule eventu Google Calendar (np. 'Maja' zamiast 'Marina Maja Sailing 35'). Jeśli puste — używana jest pełna nazwa jachtu."
5. `class-admin.php::process_yacht_save()` — zapisz `yacht_gcal_alias` przez `Yacht::update_gcal_alias()`.
6. Dodaj admin notices dla token_regenerated i import_done.
Avoid:
- Nie usuwaj istniejących pól per-yacht ("Google Calendar ID" i "iCal Import URL") — out of scope. Klient zdecyduje w kolejnym planie czy je sprzątać.
- Nie dotykaj OAuth UI (sekcja Google Calendar OAuth zostaje bez zmian).
- `php -l` na każdym zmienionym pliku
- W panelu admin → Settings: nowa sekcja jest widoczna, zapis URL działa, "Wygeneruj token" zmienia URL, "Importuj teraz" wywołuje import (sprawdzalne w error log lub bookingach)
- W yacht-edit: pole alias zapisuje się i odczytuje
- Token w URL ma >=20 znaków, zmiana tokenu unieważnia stary URL (403)
AC-6 zaspokojone, AC-4 (UI dla aliasu) zaspokojone
Dwukierunkowa synchronizacja iCal z jednym wspólnym Google Calendar:
- Globalny export feed (admin subskrybuje w GCal)
- Globalny import URL (plugin pobiera kalendarz Google)
- Identyfikacja jachtu po prefiksie "Nazwa - opis" w tytule eventu
- Frontend per-jacht pokazuje tylko swoje eventy
1. Settings → skopiuj Export URL.
2. W Google Calendar → "Inne kalendarze" → "Z URL-a" → wklej Export URL → potwierdź że pojawiają się rezerwacje wszystkich jachtów z prefiksem nazwy.
3. W Google Calendar (głównym admina) utwórz event ręczny: tytuł "Maja - Test ręcznie", data za tydzień.
4. Skopiuj URL iCal swojego głównego kalendarza Google (Ustawienia kalendarza → Tajny adres w formacie iCal).
5. Wklej do Settings pluginu → "iCal Import URL".
6. Kliknij "Importuj teraz".
7. Sprawdź: WP Admin → Rezerwacje → pojawia się booking "GCal: Maja - Test ręcznie" przypisany do jachtu "Maja".
8. Otwórz frontend kalendarz jachtu Maja — data zablokowana. Otwórz kalendarz innego jachtu — data wolna.
9. Usuń event w Google → uruchom "Importuj teraz" ponownie → booking znika.
10. Stwórz w GCal "Nieznany - test" → import → brak bookingu (sprawdź WP error log: "skip: yacht not found").
Wpisz "approved" gdy wszystkie 10 punktów weryfikacji przejdzie, lub opisz problemy do poprawy
DO NOT CHANGE
class-availability.php(logika availability stabilna)class-booking.php(CPT booking — nie ruszamy)wp_yacht_availabilityschema (zablokowane)frontend/class-calendar-widget.phpifrontend/class-shortcode.php(widget renderingu — działa już per-yacht)class-rest-controller.php(REST endpointy — bez zmian)- OAuth integration (
class-oauth-handler.php,class-gcal-service.php,class-sync-controller.php) — out of scope tego planu, push przez OAuth zostaje na razie jak jest (drugi mechanizm; klient użyje iCal a OAuth zignoruje, lub w kolejnym planie wyłączymy) - Per-yacht iCal feed (
output_ics) i per-yacht import (run_import+import_for_yacht) — pozostają funkcjonalne dla kompatybilności wstecznej
SCOPE LIMITS
- Plan NIE usuwa pól per-yacht ("Google Calendar ID", "iCal Import URL") z yacht-edit — to decyzja na osobny plan po pozytywnej weryfikacji nowej globalnej synchronizacji
- Plan NIE wyłącza OAuth push do GCal — drugi (równoległy) mechanizm; jeśli klient nie połączy OAuth, nie jest aktywny
- Brak nowych zależności (PHP libs, npm, CDN)
- Brak zmian w bazie poza dodaniem
wp_optionskeys i opcjonalnym yacht meta_yacht_gcal_alias - Brak tłumaczeń .pot w tym planie (Phase 9 osobno)
<success_criteria>
- Klient może zarządzać rezerwacjami całej floty w jednym Google Calendar
- Eventy ręczne w GCal z prefiksem "Nazwa jachtu - ..." automatycznie pojawiają się jako blokady na frontendzie tego jachtu
- Rezerwacje ze strony pojawiają się w GCal admina (po refresh subskrypcji Google)
- Każdy frontend kalendarz pokazuje tylko swoje rezerwacje
- Zachowana kompatybilność wsteczna (per-yacht feedy + OAuth push nadal działają) </success_criteria>