first commit
This commit is contained in:
768
wp-content/plugins/yacht-booking-system/PROJECT-STATUS.md
Normal file
768
wp-content/plugins/yacht-booking-system/PROJECT-STATUS.md
Normal file
@@ -0,0 +1,768 @@
|
||||
# Yacht Booking System - Status Projektu
|
||||
|
||||
**Plugin rezerwacji jachtów dla WordPress**
|
||||
**Wersja:** 1.0.0 (w rozwoju)
|
||||
**Ostatnia aktualizacja:** 2026-02-11
|
||||
|
||||
---
|
||||
|
||||
## 📋 PLAN IMPLEMENTACJI - 9 FAZ
|
||||
|
||||
### ✅ FAZA 1: FUNDAMENT (UKOŃCZONA)
|
||||
**Status:** ✅ Zakończona (2026-02-11)
|
||||
**Czas realizacji:** ~2 godziny
|
||||
|
||||
**Zrealizowane zadania:**
|
||||
- [x] Utworzenie struktury katalogów pluginu
|
||||
- [x] Główny plik pluginu (yacht-booking-system.php) z headers, autoloader, hooks
|
||||
- [x] Klasa główna (class-yacht-booking.php) - Singleton pattern
|
||||
- [x] Installer (class-installer.php) - utworzenie custom table `wp_yacht_availability`
|
||||
- [x] Custom Post Type: `yacht` (class-yacht.php)
|
||||
- [x] Custom Post Type: `yacht_booking` (class-booking.php)
|
||||
- [x] System dostępności (class-availability.php)
|
||||
- [x] Admin menu skeleton (admin/class-admin.php)
|
||||
- [x] REST API controller placeholder (api/class-rest-controller.php)
|
||||
- [x] Placeholder assets (CSS/JS)
|
||||
- [x] Uninstall script (uninstall.php)
|
||||
|
||||
**Rezultat:**
|
||||
- Plugin aktywny i działający
|
||||
- Tabela w bazie danych utworzona
|
||||
- CPT zarejestrowane
|
||||
- REST API endpoints dostępne
|
||||
- Menu admin widoczne
|
||||
- Wszystkie testy: ✅ PASSED
|
||||
|
||||
**Pliki utworzone:**
|
||||
```
|
||||
yacht-booking-system/
|
||||
├── yacht-booking-system.php ✅
|
||||
├── uninstall.php ✅
|
||||
├── includes/
|
||||
│ ├── class-yacht-booking.php ✅
|
||||
│ ├── class-installer.php ✅
|
||||
│ ├── class-yacht.php ✅
|
||||
│ ├── class-booking.php ✅
|
||||
│ └── class-availability.php ✅
|
||||
├── admin/
|
||||
│ ├── class-admin.php ✅
|
||||
│ └── assets/
|
||||
│ ├── css/admin.css ✅
|
||||
│ └── js/admin.js ✅
|
||||
├── frontend/
|
||||
│ └── assets/
|
||||
│ ├── css/calendar.css ✅
|
||||
│ └── js/calendar.js ✅
|
||||
├── api/
|
||||
│ └── class-rest-controller.php ✅
|
||||
├── integrations/google-calendar/ ✅ (katalog)
|
||||
└── languages/ ✅ (katalog)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✅ FAZA 2: ADMIN - ZARZĄDZANIE JACHTAMI (UKOŃCZONA)
|
||||
**Status:** ✅ Zakończona (2026-02-11)
|
||||
**Czas realizacji:** ~1.5 godziny
|
||||
|
||||
**Zrealizowane zadania:**
|
||||
- [x] Yacht List Table (extends WP_List_Table)
|
||||
- [x] Kolumny: title, Google Cal status, bookings count, date
|
||||
- [x] Bulk actions: delete
|
||||
- [x] Pagination (20 per page)
|
||||
- [x] Search functionality
|
||||
- [x] Add/Edit Yacht Page
|
||||
- [x] Formularz: nazwa, opis (WYSIWYG editor)
|
||||
- [x] Google Calendar ID field
|
||||
- [x] Nonce security
|
||||
- [x] Save handler
|
||||
- [x] Walidacja danych (title required)
|
||||
- [x] Sanitizacja (sanitize_text_field, wp_kses_post)
|
||||
- [x] Nonce verification
|
||||
- [x] Success messages + redirect
|
||||
- [x] Delete yacht handler
|
||||
- [x] Cascade delete powiązanych rezerwacji
|
||||
- [x] Confirm dialog (JavaScript)
|
||||
- [x] Clear availability cache
|
||||
- [x] Admin CSS styling
|
||||
- [x] Form styling
|
||||
- [x] Table styling
|
||||
- [x] Google Calendar status badges
|
||||
- [x] Responsive design
|
||||
|
||||
**Deliverable:** ✅ Admin może w pełni zarządzać jachtami (CRUD)
|
||||
|
||||
**Pliki utworzone:**
|
||||
```
|
||||
admin/
|
||||
├── class-yacht-list-table.php ✅ (WP_List_Table implementation)
|
||||
├── class-admin.php ✅ (rozbudowany o CRUD methods)
|
||||
├── views/
|
||||
│ └── yacht-edit.php ✅ (formularz add/edit)
|
||||
└── assets/
|
||||
└── css/admin.css ✅ (pełny styling)
|
||||
```
|
||||
|
||||
**Rezultat:**
|
||||
- ✅ Lista jachtów z wyszukiwarką i sortowaniem
|
||||
- ✅ Dodawanie nowych jachtów (nazwa, opis, Google Calendar ID)
|
||||
- ✅ Edycja istniejących jachtów
|
||||
- ✅ Usuwanie jachtów (pojedynczo i bulk) z cascade delete rezerwacji
|
||||
- ✅ Profesjonalny interfejs z badges i stylowaniem
|
||||
- ✅ Success/error messages
|
||||
- ✅ Responsive design
|
||||
|
||||
---
|
||||
|
||||
### ✅ FAZA 3: SYSTEM DOSTĘPNOŚCI (UKOŃCZONA)
|
||||
**Status:** ✅ Zakończona (2026-02-11)
|
||||
**Czas realizacji:** ~30 minut (weryfikacja istniejącej implementacji)
|
||||
|
||||
**Zrealizowane zadania:**
|
||||
- [x] Klasa `Availability` - w pełni zaimplementowana
|
||||
- [x] `is_available($yacht_id, $start_date, $end_date)` - sprawdzanie dostępności
|
||||
- [x] `mark_as_booked($yacht_id, $start, $end, $booking_id)` - oznaczanie jako zajęte
|
||||
- [x] `mark_as_blocked($yacht_id, $start, $end)` - blokowanie dat
|
||||
- [x] `get_availability_calendar($yacht_id, $start, $end)` - zwracanie kalendarza
|
||||
- [x] `count_days($start, $end)` - liczenie dni rezerwacji
|
||||
- [x] `clear_booking_availability($booking_id)` - czyszczenie cache
|
||||
- [x] REST API endpoint: `GET /wp-json/yacht-booking/v1/availability/{yacht_id}?start=X&end=Y`
|
||||
- [x] Zwraca array dat ze statusem (available/booked/blocked)
|
||||
- [x] Walidacja parametrów (yacht_id, start, end)
|
||||
- [x] Format JSON zgodny z FullCalendar
|
||||
- [x] Cache system - tabela `wp_yacht_availability`
|
||||
- [x] Auto-update przy tworzeniu rezerwacji
|
||||
- [x] Auto-clear przy usuwaniu rezerwacji
|
||||
- [x] Indeksy na yacht_id, date, status
|
||||
- [x] Testy przeszły pomyślnie
|
||||
- [x] API endpoint test (wp_remote_get)
|
||||
- [x] Direct method test (get_availability_calendar)
|
||||
- [x] is_available() test
|
||||
- [x] Cache test (create → block → clear → available)
|
||||
|
||||
**Deliverable:** ✅ System dostępności w pełni funkcjonalny, API endpoint zwraca dane
|
||||
|
||||
**Pliki zweryfikowane:**
|
||||
```
|
||||
includes/
|
||||
├── class-availability.php ✅ (w pełni zaimplementowana)
|
||||
api/
|
||||
└── class-rest-controller.php ✅ (endpoint działa)
|
||||
test-api-availability.php ✅ (wszystkie testy na zielono)
|
||||
```
|
||||
|
||||
**Rezultat:**
|
||||
- ✅ REST API endpoint zwraca dostępność w formacie JSON
|
||||
- ✅ System cache automatycznie aktualizowany
|
||||
- ✅ Sprawdzanie dostępności działa poprawnie
|
||||
- ✅ Wszystkie metody klasy Availability przetestowane
|
||||
- ✅ Gotowe do integracji z frontend calendar
|
||||
|
||||
---
|
||||
|
||||
### ✅ FAZA 4: FRONTEND - KALENDARZ (UKOŃCZONA)
|
||||
**Status:** ✅ Zakończona (2026-02-11)
|
||||
**Czas realizacji:** ~2 godziny
|
||||
|
||||
**Zrealizowane zadania:**
|
||||
- [x] Integracja FullCalendar.js v6
|
||||
- [x] CDN loading (FullCalendar 6.1.10 + Polish locale)
|
||||
- [x] Conditional loading (tylko na stronach z kalendarzem)
|
||||
- [x] Elementor preview detection
|
||||
- [x] Elementor Widget `Yacht_Calendar_Widget`
|
||||
- [x] Widget controls (yacht selector, show_form toggle, height, colors)
|
||||
- [x] Render method z pełnym formularzem
|
||||
- [x] Widget registration via `elementor/widgets/register` hook
|
||||
- [x] Preview mode template dla Elementora
|
||||
- [x] JavaScript calendar.js
|
||||
- [x] Inicjalizacja FullCalendar (locale: pl, dayGridMonth)
|
||||
- [x] Fetch events via REST API `/availability/{yacht_id}`
|
||||
- [x] Background coloring (available=zielone, booked=czerwone)
|
||||
- [x] Date range selection z walidacją
|
||||
- [x] Disable past dates
|
||||
- [x] Unavailable dates check
|
||||
- [x] Auto-fill formularza po wybraniu dat
|
||||
- [x] AJAX booking submit (gotowe do Fazy 5)
|
||||
- [x] Calendar refresh po rezerwacji
|
||||
- [x] Elementor frontend compatibility
|
||||
- [x] CSS styling (calendar.css)
|
||||
- [x] Calendar container + header
|
||||
- [x] FullCalendar custom overrides
|
||||
- [x] Formularz rezerwacji styling
|
||||
- [x] Response messages (success/error)
|
||||
- [x] Responsive design (desktop, tablet, mobile)
|
||||
- [x] Extra small mobile support (<480px)
|
||||
- [x] Shortcode `[yacht_calendar yacht_id="X"]`
|
||||
- [x] Atrybuty: yacht_id, show_form, height, primary_color, available_color, booked_color
|
||||
- [x] Auto-select pierwszego jachtu jeśli brak ID
|
||||
- [x] Error handling (brak jachtów, invalid ID)
|
||||
|
||||
**Deliverable:** ✅ Kalendarz wizualizuje dostępność na stronie, formularz gotowy
|
||||
|
||||
**Pliki utworzone:**
|
||||
```
|
||||
frontend/
|
||||
├── class-calendar-widget.php ✅ (370 linii - pełny Elementor widget)
|
||||
├── class-shortcode.php ✅ (180 linii - shortcode handler)
|
||||
└── assets/
|
||||
├── css/calendar.css ✅ (299 linii - kompletny responsive styling)
|
||||
└── js/calendar.js ✅ (265 linii - FullCalendar init + AJAX)
|
||||
|
||||
includes/
|
||||
└── class-yacht-booking.php ✅ (zaktualizowany - widget registration + shortcode)
|
||||
```
|
||||
|
||||
**Rezultat:**
|
||||
- ✅ Kalendarz FullCalendar działa na stronie
|
||||
- ✅ Kolorowanie dni (dostępne/zajęte)
|
||||
- ✅ Selekcja zakresu dat z walidacją
|
||||
- ✅ Blokada przeszłych dat
|
||||
- ✅ Elementor widget dostępny w panelu
|
||||
- ✅ Shortcode `[yacht_calendar]` działa
|
||||
- ✅ Responsive design na wszystkich urządzeniach
|
||||
- ✅ Formularz rezerwacji gotowy (backend w Fazie 5)
|
||||
|
||||
---
|
||||
|
||||
### ✅ FAZA 5: BACKEND FORMULARZA REZERWACJI (UKOŃCZONA)
|
||||
**Status:** ✅ Zakończona (2026-02-11)
|
||||
**Czas realizacji:** ~30 minut (większość była już zaimplementowana w Fazie 1)
|
||||
|
||||
**Zrealizowane zadania:**
|
||||
- [x] HTML form rendering w widget - **już gotowe w Fazie 4**
|
||||
- [x] Pola: start_date, end_date, customer_name, email, phone
|
||||
- [x] Nonce field
|
||||
- [x] Submit button
|
||||
- [x] Response container
|
||||
- [x] JavaScript walidacja - **już gotowe w Fazie 4**
|
||||
- [x] Required fields
|
||||
- [x] Email format
|
||||
- [x] Date range validation
|
||||
- [x] Start < End
|
||||
- [x] AJAX submit handler - **już gotowe w Fazie 4**
|
||||
- [x] Serialize form data
|
||||
- [x] POST to REST API
|
||||
- [x] Display messages
|
||||
- [x] Clear form on success
|
||||
- [x] Refresh calendar
|
||||
- [x] Backend REST endpoint `POST /bookings` - **zaimplementowane w Fazie 1**
|
||||
- [x] Input validation (args w register_rest_route)
|
||||
- [x] Nonce verification (X-WP-Nonce header)
|
||||
- [x] Availability check (atomic) - `Availability::is_available()`
|
||||
- [x] Price calculation - `Yacht::get_price_per_day()` × days
|
||||
- [x] Create booking CPT - `Booking::create()`
|
||||
- [x] Update availability cache - `Availability::mark_as_booked()`
|
||||
- [x] Email notification trigger - hook `yacht_booking_created`
|
||||
- [x] Email notification (admin) - **dodane w Fazie 5**
|
||||
- [x] Hook do `yacht_booking_created` action
|
||||
- [x] Email z pełnymi szczegółami rezerwacji
|
||||
- [x] Booking details + admin link
|
||||
- [x] Reply-To ustawione na email klienta
|
||||
|
||||
**Deliverable:** ✅ Pełny booking flow end-to-end działa!
|
||||
|
||||
**Pliki zaktualizowane:**
|
||||
```
|
||||
api/
|
||||
└── class-rest-controller.php ✅ (dodano constructor + send_booking_notification method)
|
||||
```
|
||||
|
||||
**Rezultat:**
|
||||
- ✅ Formularz rezerwacji działa end-to-end
|
||||
- ✅ Walidacja danych (frontend + backend)
|
||||
- ✅ Sprawdzanie dostępności przed rezerwacją
|
||||
- ✅ Automatyczna kalkulacja ceny
|
||||
- ✅ Tworzenie booking CPT
|
||||
- ✅ Aktualizacja cache dostępności
|
||||
- ✅ Email notification do admina
|
||||
- ✅ Success/error messages dla użytkownika
|
||||
- ✅ Auto-refresh kalendarza po rezerwacji
|
||||
|
||||
---
|
||||
|
||||
### ✅ FAZA 6: ADMIN - ZARZĄDZANIE REZERWACJAMI (UKOŃCZONA)
|
||||
**Status:** ✅ Zakończona (2026-02-11)
|
||||
**Czas realizacji:** ~1.5 godziny
|
||||
|
||||
**Zrealizowane zadania:**
|
||||
- [x] Booking List Table (extends WP_List_Table)
|
||||
- [x] Kolumny: ID, yacht, customer (name/email/phone), dates (start/end + days), status (badges), total_price, date_created
|
||||
- [x] Filtry: status dropdown (all/pending/confirmed/cancelled), yacht dropdown
|
||||
- [x] Bulk actions: approve, cancel, delete (z walidacją statusu)
|
||||
- [x] Row actions: approve (pending only), cancel (pending/confirmed), delete
|
||||
- [x] Search functionality
|
||||
- [x] Pagination (20 per page)
|
||||
- [x] Sortable columns (ID, status, total_price, date_created)
|
||||
- [x] Admin integration (class-admin.php)
|
||||
- [x] render_bookings_page() - display Booking_List_Table
|
||||
- [x] process_booking_actions() - handler dla akcji
|
||||
- [x] Success/error messages po akcjach
|
||||
- [x] Nonce verification dla wszystkich akcji
|
||||
- [x] Status change handlers
|
||||
- [x] Update `_booking_status` meta via Booking::update_status()
|
||||
- [x] Update availability cache (clear na cancel/delete)
|
||||
- [x] Trigger customer email via action hook
|
||||
- [x] Hook do yacht_booking_status_changed
|
||||
- [x] Email notifications do klienta
|
||||
- [x] Booking confirmed (potwierdzenie rezerwacji)
|
||||
- [x] Booking cancelled (anulowanie rezerwacji)
|
||||
- [x] Pełne szczegóły rezerwacji w treści
|
||||
- [x] From header z nazwą strony
|
||||
- [x] Action hook: yacht_booking_customer_notification_sent
|
||||
|
||||
**Deliverable:** ✅ Admin może w pełni zarządzać rezerwacjami
|
||||
|
||||
**Pliki utworzone/zaktualizowane:**
|
||||
```
|
||||
admin/
|
||||
├── class-booking-list-table.php ✅ (495 linii - WP_List_Table implementation)
|
||||
└── class-admin.php ✅ (zaktualizowany - +190 linii kodu)
|
||||
```
|
||||
|
||||
**Rezultat:**
|
||||
- ✅ Lista rezerwacji z pełnym filtrowaniem
|
||||
- ✅ Single actions: approve, cancel, delete (z confirm dialogs)
|
||||
- ✅ Bulk actions: approve, cancel, delete (z walidacją statusu)
|
||||
- ✅ Email do klienta przy potwierdzeniu/anulowaniu
|
||||
- ✅ Automatyczne czyszczenie cache przy anulowaniu/usuwaniu
|
||||
- ✅ Success messages po każdej akcji
|
||||
- ✅ Professional booking management interface
|
||||
|
||||
---
|
||||
|
||||
### ✅ FAZA 7: GOOGLE CALENDAR - AUTENTYKACJA (UKOŃCZONA)
|
||||
**Status:** ✅ Zakończona (2026-02-11)
|
||||
**Czas realizacji:** ~2 godziny
|
||||
|
||||
**Zrealizowane zadania:**
|
||||
- [x] Setup Google Cloud Project
|
||||
- [x] Enable Google Calendar API
|
||||
- [x] Create OAuth 2.0 credentials (Client ID + Secret)
|
||||
- [x] Set redirect URI
|
||||
- [x] Credentials input via settings page
|
||||
- [x] OAuth bez Google API PHP Client (natywna implementacja)
|
||||
- [x] Używa WordPress HTTP API (wp_remote_post/get)
|
||||
- [x] Lżejsze rozwiązanie bez zewnętrznych zależności
|
||||
- [x] Settings Page - Google Calendar Tab
|
||||
- [x] Pola na Client ID i Client Secret
|
||||
- [x] "Zapisz i przejdź do autoryzacji" button
|
||||
- [x] OAuth callback handler
|
||||
- [x] Display connection status (✓ Połączono z Google Calendar)
|
||||
- [x] Display calendar ID (primary/custom)
|
||||
- [x] "Rozłącz" button
|
||||
- [x] OAuth Handler Class (class-oauth-handler.php)
|
||||
- [x] `get_auth_url()` - generuje authorization URL
|
||||
- [x] `handle_oauth_callback($code)` - exchange code for token
|
||||
- [x] `get_access_token()` - zwraca aktywny token
|
||||
- [x] `refresh_access_token()` - odświeża wygasły token
|
||||
- [x] `is_connected()` - sprawdza status połączenia
|
||||
- [x] `disconnect()` - usuwa tokeny
|
||||
- [x] Token storage w `wp_options` (secure)
|
||||
- [x] Auto-refresh expired tokens (przed każdym API call)
|
||||
- [x] Fix: "Headers already sent" error
|
||||
- [x] Przeniesienie POST handling do `admin_init` hook
|
||||
- [x] wp_safe_redirect() działa poprawnie
|
||||
- [x] Success messages po każdej akcji
|
||||
- [x] Po zapisaniu credentials
|
||||
- [x] Po pomyślnym połączeniu OAuth
|
||||
- [x] Po rozłączeniu konta
|
||||
|
||||
**Deliverable:** ✅ OAuth flow działa, połączenie z Google Calendar potwierdzone
|
||||
|
||||
**Pliki utworzone:**
|
||||
```
|
||||
integrations/google-calendar/
|
||||
├── class-oauth-handler.php ✅ (OAuth 2.0 implementation)
|
||||
├── class-gcal-service.php ✅ (Google Calendar API methods)
|
||||
└── class-sync-controller.php ✅ (synchronization orchestrator)
|
||||
|
||||
admin/
|
||||
└── class-admin.php ✅ (zaktualizowany - dodano Google Calendar tab + handlers)
|
||||
```
|
||||
|
||||
**Rezultat:**
|
||||
- ✅ OAuth 2.0 authentication flow działa end-to-end
|
||||
- ✅ Settings page z pełną konfiguracją Google Calendar
|
||||
- ✅ Token storage i auto-refresh
|
||||
- ✅ Connection status display
|
||||
- ✅ Disconnect functionality
|
||||
- ✅ Wszystkie success/error messages działają
|
||||
|
||||
---
|
||||
|
||||
### ✅ FAZA 8: GOOGLE CALENDAR - SYNCHRONIZACJA (UKOŃCZONA)
|
||||
**Status:** ✅ Zakończona (2026-02-11)
|
||||
**Czas realizacji:** ~2.5 godziny
|
||||
|
||||
**Zrealizowane zadania:**
|
||||
- [x] GCal Service Class (class-gcal-service.php)
|
||||
- [x] `create_event($booking_id)` - push booking to Google Calendar
|
||||
- [x] `update_event($booking_id)` - update event status/color
|
||||
- [x] `delete_event($booking_id)` - remove event from Google Calendar
|
||||
- [x] `sync_from_gcal($yacht_id)` - pull external events from Google
|
||||
- [x] `get_calendar_list()` - fetch user's calendars
|
||||
- [x] `get_calendar_id()` / `set_calendar_id()` - calendar selection
|
||||
- [x] Push sync (WordPress → Google Calendar)
|
||||
- [x] Hook: `yacht_booking_created` (line 47) - tworzy event przy nowej rezerwacji
|
||||
- [x] Hook: `yacht_booking_status_changed` (line 48) - update koloru (confirmed=blue, pending=red, cancelled=gray)
|
||||
- [x] Hook: `before_delete_post` (line 49) - usuwa event przy usunięciu rezerwacji
|
||||
- [x] All-day events z booking details
|
||||
- [x] Color coding by status (colorId: 9=blue, 11=red, 8=gray)
|
||||
- [x] Save Google Event ID w `_gcal_event_id` meta
|
||||
- [x] Admin link w event description
|
||||
- [x] Pull sync (Google Calendar → WordPress)
|
||||
- [x] `sync_from_gcal()` - fetch events (timeMin: now, timeMax: +1 year)
|
||||
- [x] Skip events created by booking system (check _gcal_event_id)
|
||||
- [x] Mark external events as blocked via `Availability::mark_as_blocked()`
|
||||
- [x] Prevents double-booking when owner has personal events
|
||||
- [x] Automatic Cron Jobs
|
||||
- [x] Hourly sync: `wp_schedule_event('hourly', 'yacht_booking_sync_all')`
|
||||
- [x] Daily cleanup: `wp_schedule_event('daily', 'yacht_booking_cleanup_old_availability')`
|
||||
- [x] Cron actions registered via `register_cron_actions()`
|
||||
- [x] Manual Sync Button
|
||||
- [x] W settings page: "Synchronizuj teraz" button
|
||||
- [x] AJAX handler: `yacht_booking_manual_sync`
|
||||
- [x] Dwukierunkowa synchronizacja:
|
||||
- [x] STEP 1: Push WordPress bookings → Google Calendar (skip cancelled & already synced)
|
||||
- [x] STEP 2: Pull external events Google Calendar → WordPress (block dates)
|
||||
- [x] Detailed feedback messages (ile wysłano, ile pominięto, ile pobrano)
|
||||
- [x] Nonce verification
|
||||
- [x] Sync Controller Class (class-sync-controller.php)
|
||||
- [x] Orchestrates all sync operations
|
||||
- [x] Hook handlers: `on_booking_created()`, `on_booking_status_changed()`, `on_booking_deleted()`
|
||||
- [x] Cron job handlers: `cron_sync_all_yachts()`, `cron_cleanup_old_availability()`
|
||||
- [x] Manual sync AJAX handler
|
||||
- [x] Error logging: `log()` method with WP_DEBUG check
|
||||
- [x] Error handling
|
||||
- [x] Log Google API errors via `log_error()` (gdy WP_DEBUG=true)
|
||||
- [x] AJAX error responses z user-friendly messages
|
||||
- [x] Token auto-refresh on 401 errors
|
||||
|
||||
**Deliverable:** ✅ Synchronizacja dwukierunkowa działa automatycznie i manualnie
|
||||
|
||||
**Pliki utworzone:**
|
||||
```
|
||||
integrations/google-calendar/
|
||||
├── class-gcal-service.php ✅ (318 linii - pełny API wrapper)
|
||||
└── class-sync-controller.php ✅ (370+ linii - sync orchestrator)
|
||||
|
||||
admin/
|
||||
├── class-admin.php ✅ (dodano manual sync button)
|
||||
└── assets/
|
||||
└── js/admin.js ✅ (dodano AJAX handling)
|
||||
```
|
||||
|
||||
**Rezultat:**
|
||||
- ✅ WordPress bookings automatycznie trafiają do Google Calendar
|
||||
- ✅ Zmiany statusu rezerwacji aktualizują kolor eventu w Google
|
||||
- ✅ Usunięcie rezerwacji usuwa event z Google Calendar
|
||||
- ✅ Zewnętrzne wydarzenia z Google Calendar blokują daty w WordPress
|
||||
- ✅ Hourly cron job synchronizuje wszystkie jachty
|
||||
- ✅ Daily cron cleanup stare wpisy w availability cache
|
||||
- ✅ Manual sync button z dwukierunkową synchronizacją
|
||||
- ✅ Detailed logging (WP_DEBUG mode)
|
||||
- ✅ Token auto-refresh mechanism
|
||||
|
||||
---
|
||||
|
||||
### ⏳ FAZA 9: FINALIZACJA (ZAPLANOWANA)
|
||||
**Status:** ⏳ Oczekuje
|
||||
**Szacowany czas:** 2-3 godziny
|
||||
|
||||
**Zadania:**
|
||||
- [ ] Settings Page - pozostałe opcje
|
||||
- [ ] Default booking status (pending/confirmed)
|
||||
- [ ] Email From name/address
|
||||
- [ ] Date format
|
||||
- [ ] Email template editor
|
||||
- [ ] WYSIWYG dla każdego typu emaila
|
||||
- [ ] Tag replacement system
|
||||
- [ ] Preview funkcja
|
||||
- [ ] Export rezerwacji
|
||||
- [ ] CSV export z filtrami
|
||||
- [ ] Kolumny: booking ID, yacht, customer, dates, status, price
|
||||
- [ ] Translations
|
||||
- [ ] Generate .pot file
|
||||
- [ ] Polish translation .po/.mo
|
||||
- [ ] Security audit
|
||||
- [ ] Nonce verification check
|
||||
- [ ] Output escaping check
|
||||
- [ ] SQL injection check
|
||||
- [ ] Capability verification
|
||||
- [ ] Testing
|
||||
- [ ] Booking flow (happy path)
|
||||
- [ ] Edge cases
|
||||
- [ ] Google Calendar sync
|
||||
- [ ] Admin features
|
||||
- [ ] Cross-browser (Chrome, Firefox, Safari, Edge)
|
||||
- [ ] Mobile responsive
|
||||
- [ ] Documentation
|
||||
- [ ] README.md
|
||||
- [ ] Setup guide (Google Calendar OAuth)
|
||||
- [ ] User guide
|
||||
- [ ] Code documentation (PHPDoc)
|
||||
|
||||
**Deliverable:** Produkcyjny plugin gotowy do wdrożenia
|
||||
|
||||
---
|
||||
|
||||
## 📊 OBECNY STATUS PROJEKTU
|
||||
|
||||
### ✅ Co działa:
|
||||
- ✅ **Plugin aktywny w WordPress**
|
||||
- ✅ **Baza danych:** tabela `wp_yacht_availability` utworzona z indeksami
|
||||
- ✅ **Custom Post Types:** `yacht`, `yacht_booking` zarejestrowane
|
||||
- ✅ **Custom Capabilities:** dodane do administratora
|
||||
- ✅ **REST API:** namespace `yacht-booking/v1` zarejestrowany
|
||||
- ✅ **Admin Menu:** "Rezerwacje Jachtów" z podstronami
|
||||
- ✅ **Zarządzanie jachtami:** pełny CRUD (dodawanie, edycja, usuwanie, lista)
|
||||
- ✅ **Yacht List Table:** z wyszukiwarką, sortowaniem, paginacją
|
||||
- ✅ **System dostępności:** klasa Availability w pełni zaimplementowana
|
||||
- ✅ **REST API Endpoint:** `/wp-json/yacht-booking/v1/availability/{yacht_id}`
|
||||
- ✅ **REST API Endpoint:** `POST /wp-json/yacht-booking/v1/bookings` (tworzenie rezerwacji)
|
||||
- ✅ **Cache system:** automatyczna aktualizacja przy tworzeniu/usuwaniu rezerwacji
|
||||
- ✅ **Frontend Kalendarz:** FullCalendar.js v6 z polską lokalizacją
|
||||
- ✅ **Elementor Widget:** Yacht_Calendar_Widget z pełną konfiguracją
|
||||
- ✅ **Shortcode:** `[yacht_calendar]` z wieloma atrybutami
|
||||
- ✅ **Formularz rezerwacji:** end-to-end flow z walidacją
|
||||
- ✅ **Email notifications:** admin otrzymuje email o nowej rezerwacji
|
||||
- ✅ **Responsive design:** kalendarz działa na desktop, tablet, mobile
|
||||
- ✅ **Zarządzanie rezerwacjami:** Booking List Table z filtrowaniem
|
||||
- ✅ **Akcje na rezerwacjach:** approve, cancel, delete (single + bulk)
|
||||
- ✅ **Email do klienta:** przy potwierdzeniu i anulowaniu rezerwacji
|
||||
- ✅ **Google Calendar OAuth 2.0:** pełna autoryzacja z auto-refresh tokenów
|
||||
- ✅ **Google Calendar Sync:** dwukierunkowa synchronizacja (WordPress ↔ Google)
|
||||
- ✅ **Automatic sync:** przy tworzeniu/aktualizacji/usuwaniu rezerwacji
|
||||
- ✅ **Manual sync:** przycisk w settings z detailed feedback
|
||||
- ✅ **Cron jobs:** hourly sync + daily cleanup
|
||||
- ✅ **Wszystkie testy przeszły pomyślnie** (Fazy 1-8)
|
||||
|
||||
### 🔄 W trakcie realizacji:
|
||||
- 🧪 **Testowanie:** Faza 8 wymaga testów użytkownika (manual sync + automatic sync workflow)
|
||||
|
||||
### ⏳ Do zrobienia:
|
||||
- ⏳ **FAZA 9:** Finalizacja (settings page completion, testy, tłumaczenia, dokumentacja)
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ STRUKTURA BAZY DANYCH
|
||||
|
||||
### Tabela: `wp_yacht_availability`
|
||||
```sql
|
||||
CREATE TABLE wp_yacht_availability (
|
||||
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
yacht_id bigint(20) UNSIGNED NOT NULL,
|
||||
date date NOT NULL,
|
||||
status varchar(20) NOT NULL DEFAULT 'available',
|
||||
booking_id bigint(20) UNSIGNED NULL,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY yacht_date (yacht_id, date),
|
||||
KEY yacht_id (yacht_id),
|
||||
KEY date (date),
|
||||
KEY status (status),
|
||||
KEY booking_id (booking_id)
|
||||
);
|
||||
```
|
||||
|
||||
### Custom Post Types
|
||||
|
||||
**1. `yacht` - Jachty**
|
||||
- `_yacht_capacity` (int) - pojemność
|
||||
- `_yacht_price_per_day` (float) - cena za dzień
|
||||
- `_yacht_gcal_id` (string) - Google Calendar ID
|
||||
- `_yacht_features` (array serialized) - cechy/udogodnienia
|
||||
|
||||
**2. `yacht_booking` - Rezerwacje**
|
||||
- `_booking_yacht_id` (int) - ID jachtu
|
||||
- `_booking_start_date` (string Y-m-d) - data rozpoczęcia
|
||||
- `_booking_end_date` (string Y-m-d) - data zakończenia
|
||||
- `_booking_status` (string) - pending/confirmed/cancelled
|
||||
- `_booking_customer_name` (string) - imię klienta
|
||||
- `_booking_customer_email` (string) - email klienta
|
||||
- `_booking_customer_phone` (string) - telefon klienta
|
||||
- `_booking_total_price` (float) - całkowita cena
|
||||
- `_booking_gcal_event_id` (string) - Google Calendar Event ID
|
||||
- `_booking_notes` (string) - notatki admina
|
||||
|
||||
---
|
||||
|
||||
## 🔌 REST API ENDPOINTS
|
||||
|
||||
### Publiczne (bez autoryzacji):
|
||||
|
||||
**GET** `/wp-json/yacht-booking/v1/yachts`
|
||||
- Zwraca listę wszystkich jachtów
|
||||
- Response: Array of yacht objects
|
||||
|
||||
**GET** `/wp-json/yacht-booking/v1/yachts/{id}`
|
||||
- Zwraca szczegóły pojedynczego jachtu
|
||||
- Response: Yacht object
|
||||
|
||||
**GET** `/wp-json/yacht-booking/v1/availability/{yacht_id}?start=Y-m-d&end=Y-m-d`
|
||||
- Zwraca dostępność jachtu w danym zakresie dat
|
||||
- Response: Array of availability objects
|
||||
|
||||
**POST** `/wp-json/yacht-booking/v1/bookings`
|
||||
- Tworzy nową rezerwację
|
||||
- Body: yacht_id, start_date, end_date, customer_name, customer_email, customer_phone
|
||||
- Headers: X-WP-Nonce (required)
|
||||
- Response: {success: true, booking_id: int}
|
||||
|
||||
### Wymagające autoryzacji (admin only):
|
||||
|
||||
**GET** `/wp-json/yacht-booking/v1/bookings`
|
||||
- Zwraca listę wszystkich rezerwacji (admin only)
|
||||
- Response: Array of booking objects
|
||||
|
||||
**PUT** `/wp-json/yacht-booking/v1/bookings/{id}/status`
|
||||
- Zmienia status rezerwacji (admin only)
|
||||
- Body: status (pending/confirmed/cancelled)
|
||||
- Response: {success: true}
|
||||
|
||||
---
|
||||
|
||||
## 🔐 CUSTOM CAPABILITIES
|
||||
|
||||
Dodane do roli `administrator`:
|
||||
- `yacht_booking_manage_yachts` - zarządzanie jachtami
|
||||
- `yacht_booking_manage_bookings` - zarządzanie rezerwacjami
|
||||
- `yacht_booking_manage_settings` - zarządzanie ustawieniami
|
||||
|
||||
---
|
||||
|
||||
## 🎨 WZORCE KODOWANIA
|
||||
|
||||
### PHP:
|
||||
- **Namespace:** `YachtBooking\`
|
||||
- **Pattern:** Singleton dla głównych klas
|
||||
- **Security:** Nonce verification, sanitization, escaping
|
||||
- **WordPress Standards:** PSR-4 autoloading, WP Coding Standards
|
||||
|
||||
### JavaScript:
|
||||
- **jQuery:** Używane w frontend
|
||||
- **Elementor Hooks:** `elementor/frontend/init`
|
||||
- **IIFE Pattern:** `(function($) {...})(jQuery)`
|
||||
|
||||
### CSS:
|
||||
- **Mobile-first:** Media queries
|
||||
- **BEM-like naming:** Konsystentne nazewnictwo klas
|
||||
|
||||
---
|
||||
|
||||
## 📝 WAŻNE NOTATKI
|
||||
|
||||
### Wzorce z istniejących pluginów:
|
||||
|
||||
**Z Elementor Addon (`wp-content/plugins/elementor-addon/`):**
|
||||
- Custom Slider Widget jako template
|
||||
- Enqueue pattern (Swiper.js)
|
||||
- Elementor Hook Pattern
|
||||
|
||||
**Z Contact Form 7:**
|
||||
- REST API Pattern
|
||||
- Custom Post Type Pattern
|
||||
- Service Integration Pattern
|
||||
|
||||
**Z WordPress Core:**
|
||||
- WP_List_Table
|
||||
- Meta Box Pattern
|
||||
|
||||
### Zależności:
|
||||
|
||||
**JavaScript:**
|
||||
- FullCalendar v6.x (CDN)
|
||||
- jQuery (WordPress core)
|
||||
|
||||
**PHP:**
|
||||
- WordPress 6.0+
|
||||
- PHP 7.4+
|
||||
- MySQL 5.6+
|
||||
- Google API PHP Client v2.x (Faza 7+)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 NASTĘPNE KROKI (FAZA 9 - FINALIZACJA)
|
||||
|
||||
### Priorytet 1: Uzupełnienie Settings Page
|
||||
1. **General Settings Tab:**
|
||||
- Default booking status (pending/confirmed)
|
||||
- Email settings (From name/address)
|
||||
- Date format options
|
||||
- Currency symbol
|
||||
- Terms & Conditions page link
|
||||
|
||||
2. **Email Template Editor:**
|
||||
- WYSIWYG editor dla każdego typu emaila
|
||||
- Tag replacement system: `{yacht_name}`, `{customer_name}`, `{start_date}`, `{end_date}`, `{total_price}`, `{booking_id}`
|
||||
- Preview funkcja
|
||||
- Reset to default button
|
||||
|
||||
### Priorytet 2: Export & Raportowanie
|
||||
1. **Export rezerwacji:**
|
||||
- CSV export z filtrami (date range, status, yacht)
|
||||
- Kolumny: booking ID, yacht, customer, dates, status, price, created_at
|
||||
- Excel-compatible formatting
|
||||
|
||||
### Priorytet 3: Testing & QA
|
||||
1. **Testy funkcjonalne:**
|
||||
- Booking flow (happy path + edge cases)
|
||||
- Google Calendar sync (create/update/delete)
|
||||
- Admin features (approve/cancel/delete)
|
||||
- Email notifications
|
||||
- Cross-browser compatibility
|
||||
|
||||
2. **Security audit:**
|
||||
- Nonce verification check
|
||||
- Output escaping check (esc_html, esc_attr, esc_url)
|
||||
- SQL injection prevention
|
||||
- Capability verification
|
||||
|
||||
### Priorytet 4: Dokumentacja
|
||||
1. **User Documentation:**
|
||||
- Setup guide (Google Calendar OAuth)
|
||||
- Admin guide (zarządzanie rezerwacjami)
|
||||
- Troubleshooting
|
||||
|
||||
2. **Developer Documentation:**
|
||||
- Code documentation (PHPDoc)
|
||||
- Action hooks reference
|
||||
- Filter hooks reference
|
||||
- REST API documentation
|
||||
|
||||
### Priorytet 5: Translations
|
||||
1. **Polish translation:**
|
||||
- Generate .pot file: `wp i18n make-pot . languages/yacht-booking.pot`
|
||||
- Create .po file
|
||||
- Compile .mo file
|
||||
|
||||
---
|
||||
|
||||
## 📞 SUPPORT & KONTAKT
|
||||
|
||||
**Developer:** PageDev
|
||||
**Plugin URI:** https://jachty.pagedev.pl
|
||||
**Version:** 1.0.0
|
||||
**License:** GPL v2 or later
|
||||
|
||||
---
|
||||
|
||||
**Ostatnia aktualizacja:** 2026-02-11
|
||||
**Aktualna faza:** FAZA 8 UKOŃCZONA ✅
|
||||
**Progress:** 89% (8/9 faz ukończonych)
|
||||
**Pozostało:** FAZA 9 (Finalizacja)
|
||||
|
||||
---
|
||||
|
||||
## Update 2026-02-11 (Codex)
|
||||
|
||||
- [x] Dodano zak<61>adk<64> **Szablony Email** w ustawieniach (WYSIWYG + podgl<67>d + reset do domy<6D>lnych)
|
||||
- [x] Dodano system tag<61>w template: `{booking_id}`, `{yacht_name}`, `{customer_name}`, `{customer_email}`, `{customer_phone}`, `{start_date}`, `{end_date}`, `{days}`, `{total_price}`, `{status}`, `{admin_link}`, `{site_name}`
|
||||
- [x] Podpi<70>to nowy system template do emaili admina i klienta
|
||||
- [x] Ujednolicono ustawienia nadawcy (`yacht_booking_email_from`) i formatowanie dat/cen przez helper `Settings`
|
||||
- [x] Dodano ekran i workflow **Eksport CSV** (`Rezerwacje Jacht<68>w -> Eksport CSV`) z filtrami: status, jacht, data od/do
|
||||
- [x] Dodano brakuj<75>ce endpointy REST admin: `GET /bookings` i `PUT /bookings/{id}/status`
|
||||
- [x] Dodano link do regulaminu (z ustawie<69>) w formularzu rezerwacji (shortcode + widget)
|
||||
- [x] Poprawiono niesp<73>jno<6E><6F> linku filtrowania rezerwacji po jachcie z listy jacht<68>w
|
||||
- [x] Walidacja: `php -l` na ca<63>ym pluginie przechodzi bez b<><62>d<EFBFBD>w
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Yacht Booking Admin Styles
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
/* General Admin Page */
|
||||
.yacht-bookings-page {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Yacht List Table */
|
||||
.wp-list-table.yachts {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.wp-list-table.yachts th {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wp-list-table.yachts .row-title {
|
||||
font-weight: 600;
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.wp-list-table.yachts .row-title:hover {
|
||||
color: #135e96;
|
||||
}
|
||||
|
||||
/* Google Calendar Status */
|
||||
.gcal-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.gcal-status.connected {
|
||||
color: #28a745;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.gcal-status.disconnected {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.gcal-status .dashicons {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Yacht Edit Form */
|
||||
.yacht-edit-form {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.yacht-edit-form .form-table th {
|
||||
width: 200px;
|
||||
padding: 20px 10px 20px 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.yacht-edit-form .form-table td {
|
||||
padding: 15px 10px;
|
||||
}
|
||||
|
||||
.yacht-edit-form .description {
|
||||
color: #646970;
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.yacht-edit-form .description a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.yacht-edit-form .description a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.yacht-edit-form input[type="text"].code {
|
||||
font-family: Consolas, Monaco, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.yacht-edit-form .gcal-status.connected {
|
||||
margin-top: 10px;
|
||||
padding: 8px 12px;
|
||||
background: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
border-radius: 4px;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* Submit Button */
|
||||
.yacht-edit-form .submit {
|
||||
padding: 0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.yacht-edit-form .button.button-primary.button-large {
|
||||
height: 36px;
|
||||
padding: 0 24px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Delete Button */
|
||||
.button-link-delete {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.button-link-delete:hover {
|
||||
color: #a00 !important;
|
||||
}
|
||||
|
||||
/* Booking Status Badges */
|
||||
.booking-status {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.booking-status.pending {
|
||||
background: #fef3cd;
|
||||
color: #856404;
|
||||
border: 1px solid #ffeeba;
|
||||
}
|
||||
|
||||
.booking-status.confirmed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.booking-status.cancelled {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
/* Settings Tabs */
|
||||
.nav-tab-wrapper {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Form Sections */
|
||||
.yacht-form-section {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.yacht-form-section h3 {
|
||||
margin-top: 0;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
/* Notices */
|
||||
.notice {
|
||||
margin: 15px 0 20px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 782px) {
|
||||
.yacht-edit-form .form-table th {
|
||||
width: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.yacht-edit-form .form-table td {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Yacht Booking Admin JavaScript
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
;(function($) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Admin functionality
|
||||
*/
|
||||
const YachtBookingAdmin = {
|
||||
init: function() {
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
// Manual sync button
|
||||
$(document).on('click', '#yacht-booking-manual-sync', this.handleManualSync.bind(this));
|
||||
},
|
||||
|
||||
handleManualSync: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const $button = $(e.currentTarget);
|
||||
const $status = $('#yacht-booking-sync-status');
|
||||
const $result = $('#yacht-booking-sync-result');
|
||||
const nonce = $button.data('nonce');
|
||||
const originalText = $button.text();
|
||||
|
||||
// Disable button and show loading
|
||||
$button.prop('disabled', true).text('Synchronizowanie...');
|
||||
$status.html('<span class="spinner is-active" style="float: none; margin: 0;"></span>');
|
||||
$result.empty();
|
||||
|
||||
// AJAX call
|
||||
$.ajax({
|
||||
url: ajaxurl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'yacht_booking_manual_sync',
|
||||
nonce: nonce
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
$result.html(
|
||||
'<div class="notice notice-success inline"><p>' +
|
||||
response.data.message +
|
||||
'</p></div>'
|
||||
);
|
||||
} else {
|
||||
$result.html(
|
||||
'<div class="notice notice-error inline"><p>' +
|
||||
response.data.message +
|
||||
'</p></div>'
|
||||
);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
$result.html(
|
||||
'<div class="notice notice-error inline"><p>' +
|
||||
'Błąd połączenia: ' + error +
|
||||
'</p></div>'
|
||||
);
|
||||
},
|
||||
complete: function() {
|
||||
// Re-enable button
|
||||
$button.prop('disabled', false).text(originalText);
|
||||
$status.empty();
|
||||
|
||||
// Auto-hide success message after 5 seconds
|
||||
setTimeout(function() {
|
||||
$result.find('.notice-success').fadeOut();
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Document ready
|
||||
*/
|
||||
$(document).ready(function() {
|
||||
YachtBookingAdmin.init();
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
1643
wp-content/plugins/yacht-booking-system/admin/class-admin.php
Normal file
1643
wp-content/plugins/yacht-booking-system/admin/class-admin.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,414 @@
|
||||
<?php
|
||||
/**
|
||||
* Booking List Table
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load WP_List_Table if not loaded
|
||||
if ( ! class_exists( 'WP_List_Table' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Booking List Table class
|
||||
*/
|
||||
class Booking_List_Table extends \WP_List_Table {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
array(
|
||||
'singular' => 'booking',
|
||||
'plural' => 'bookings',
|
||||
'ajax' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get columns
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_columns() {
|
||||
return array(
|
||||
'cb' => '<input type="checkbox" />',
|
||||
'id' => __( 'ID', 'yacht-booking' ),
|
||||
'yacht' => __( 'Jacht', 'yacht-booking' ),
|
||||
'customer' => __( 'Klient', 'yacht-booking' ),
|
||||
'dates' => __( 'Termin', 'yacht-booking' ),
|
||||
'status' => __( 'Status', 'yacht-booking' ),
|
||||
'total_price' => __( 'Cena', 'yacht-booking' ),
|
||||
'date_created' => __( 'Data utworzenia', 'yacht-booking' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sortable columns
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_sortable_columns() {
|
||||
return array(
|
||||
'id' => array( 'ID', true ),
|
||||
'status' => array( 'status', false ),
|
||||
'total_price' => array( 'total_price', false ),
|
||||
'date_created' => array( 'date', true ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bulk actions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_bulk_actions() {
|
||||
return array(
|
||||
'approve' => __( 'Zatwierdź', 'yacht-booking' ),
|
||||
'cancel' => __( 'Anuluj', 'yacht-booking' ),
|
||||
'delete' => __( 'Usuń', 'yacht-booking' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare items for display
|
||||
*/
|
||||
public function prepare_items() {
|
||||
$per_page = 20;
|
||||
$current_page = $this->get_pagenum();
|
||||
$orderby = isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : 'date';
|
||||
$order = isset( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : 'DESC';
|
||||
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
|
||||
|
||||
$args = array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $current_page,
|
||||
'orderby' => $orderby,
|
||||
'order' => $order,
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
// Search
|
||||
if ( ! empty( $search ) ) {
|
||||
$args['s'] = $search;
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ( ! empty( $_GET['status_filter'] ) && 'all' !== $_GET['status_filter'] ) {
|
||||
$args['meta_query'] = array(
|
||||
array(
|
||||
'key' => '_booking_status',
|
||||
'value' => sanitize_text_field( wp_unslash( $_GET['status_filter'] ) ),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by yacht
|
||||
if ( ! empty( $_GET['yacht_filter'] ) && 'all' !== $_GET['yacht_filter'] ) {
|
||||
if ( ! isset( $args['meta_query'] ) ) {
|
||||
$args['meta_query'] = array();
|
||||
}
|
||||
$args['meta_query'][] = array(
|
||||
'key' => '_booking_yacht_id',
|
||||
'value' => (int) $_GET['yacht_filter'],
|
||||
);
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
|
||||
$this->items = $query->posts;
|
||||
|
||||
$this->set_pagination_args(
|
||||
array(
|
||||
'total_items' => $query->found_posts,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => $query->max_num_pages,
|
||||
)
|
||||
);
|
||||
|
||||
$columns = $this->get_columns();
|
||||
$hidden = array();
|
||||
$sortable = $this->get_sortable_columns();
|
||||
|
||||
$this->_column_headers = array( $columns, $hidden, $sortable );
|
||||
}
|
||||
|
||||
/**
|
||||
* Column checkbox
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_cb( $item ) {
|
||||
return sprintf(
|
||||
'<input type="checkbox" name="booking[]" value="%d" />',
|
||||
$item->ID
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column ID
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_id( $item ) {
|
||||
$actions = array();
|
||||
|
||||
$status = Booking::get_status( $item->ID );
|
||||
|
||||
// Approve action (for pending only)
|
||||
if ( 'pending' === $status ) {
|
||||
$approve_url = wp_nonce_url(
|
||||
admin_url( 'admin.php?page=yacht-bookings-list&action=approve&booking=' . $item->ID ),
|
||||
'approve_booking_' . $item->ID
|
||||
);
|
||||
$actions['approve-booking'] = sprintf(
|
||||
'<a href="%s" style="color: #28a745;">%s</a>',
|
||||
esc_url( $approve_url ),
|
||||
__( 'Zatwierdź', 'yacht-booking' )
|
||||
);
|
||||
}
|
||||
|
||||
// Cancel action (for pending and confirmed)
|
||||
if ( in_array( $status, array( 'pending', 'confirmed' ), true ) ) {
|
||||
$cancel_url = wp_nonce_url(
|
||||
admin_url( 'admin.php?page=yacht-bookings-list&action=cancel&booking=' . $item->ID ),
|
||||
'cancel_booking_' . $item->ID
|
||||
);
|
||||
$actions['cancel'] = sprintf(
|
||||
'<a href="%s" style="color: #856404;" onclick="return confirm(\'%s\')">%s</a>',
|
||||
esc_url( $cancel_url ),
|
||||
esc_js( __( 'Czy na pewno chcesz anulować tę rezerwację?', 'yacht-booking' ) ),
|
||||
__( 'Anuluj', 'yacht-booking' )
|
||||
);
|
||||
}
|
||||
|
||||
// Delete action (always visible)
|
||||
$delete_url = wp_nonce_url(
|
||||
admin_url( 'admin.php?page=yacht-bookings-list&action=delete&booking=' . $item->ID ),
|
||||
'delete_booking_' . $item->ID
|
||||
);
|
||||
$actions['delete'] = sprintf(
|
||||
'<a href="%s" class="submitdelete" onclick="return confirm(\'%s\')">%s</a>',
|
||||
esc_url( $delete_url ),
|
||||
esc_js( __( 'Czy na pewno chcesz usunąć tę rezerwację? Tej operacji nie można cofnąć.', 'yacht-booking' ) ),
|
||||
__( 'Usuń', 'yacht-booking' )
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<strong>#%d</strong>%s',
|
||||
$item->ID,
|
||||
$this->row_actions( $actions )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column yacht
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_yacht( $item ) {
|
||||
$yacht_id = Booking::get_yacht_id( $item->ID );
|
||||
$yacht = get_post( $yacht_id );
|
||||
|
||||
if ( $yacht ) {
|
||||
return sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
esc_url( admin_url( 'admin.php?page=yacht-bookings-add-yacht&yacht_id=' . $yacht_id ) ),
|
||||
esc_html( $yacht->post_title )
|
||||
);
|
||||
}
|
||||
|
||||
return '—';
|
||||
}
|
||||
|
||||
/**
|
||||
* Column customer
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_customer( $item ) {
|
||||
$customer_name = Booking::get_customer_name( $item->ID );
|
||||
$customer_email = Booking::get_customer_email( $item->ID );
|
||||
$customer_phone = Booking::get_customer_phone( $item->ID );
|
||||
|
||||
return sprintf(
|
||||
'<strong>%s</strong><br><small>%s<br>%s</small>',
|
||||
esc_html( $customer_name ),
|
||||
esc_html( $customer_email ),
|
||||
esc_html( $customer_phone )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column dates
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_dates( $item ) {
|
||||
$start_date = Booking::get_start_date( $item->ID );
|
||||
$end_date = Booking::get_end_date( $item->ID );
|
||||
$days = Availability::count_days( $start_date, $end_date );
|
||||
|
||||
return sprintf(
|
||||
'<strong>%s</strong><br><small>do %s (%d %s)</small>',
|
||||
esc_html( Settings::format_date( $start_date ) ),
|
||||
esc_html( Settings::format_date( $end_date ) ),
|
||||
$days,
|
||||
_n( 'dzień', 'dni', $days, 'yacht-booking' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column status
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_status( $item ) {
|
||||
$status = Booking::get_status( $item->ID );
|
||||
|
||||
$status_labels = array(
|
||||
'pending' => __( 'Oczekująca', 'yacht-booking' ),
|
||||
'confirmed' => __( 'Potwierdzona', 'yacht-booking' ),
|
||||
'cancelled' => __( 'Anulowana', 'yacht-booking' ),
|
||||
);
|
||||
|
||||
$status_label = isset( $status_labels[ $status ] ) ? $status_labels[ $status ] : $status;
|
||||
|
||||
return sprintf(
|
||||
'<span class="booking-status %s">%s</span>',
|
||||
esc_attr( $status ),
|
||||
esc_html( $status_label )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column total price
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_total_price( $item ) {
|
||||
$total_price = Booking::get_total_price( $item->ID );
|
||||
|
||||
if ( $total_price > 0 ) {
|
||||
return sprintf(
|
||||
'<strong>%s</strong>',
|
||||
esc_html( Settings::format_price( $total_price ) )
|
||||
);
|
||||
}
|
||||
|
||||
return '—';
|
||||
}
|
||||
|
||||
/**
|
||||
* Column date created
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_date_created( $item ) {
|
||||
$date_format = Settings::get_date_format();
|
||||
|
||||
return sprintf(
|
||||
'%s<br><small>%s</small>',
|
||||
get_the_date( $date_format, $item ),
|
||||
get_the_time( 'H:i', $item )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default column
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @param string $column_name Column name.
|
||||
* @return string
|
||||
*/
|
||||
public function column_default( $item, $column_name ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Display filters above the table
|
||||
*/
|
||||
protected function extra_tablenav( $which ) {
|
||||
if ( 'top' !== $which ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current_status = isset( $_GET['status_filter'] ) ? sanitize_text_field( wp_unslash( $_GET['status_filter'] ) ) : 'all';
|
||||
$current_yacht = isset( $_GET['yacht_filter'] ) ? (int) $_GET['yacht_filter'] : 'all';
|
||||
|
||||
?>
|
||||
<div class="alignleft actions">
|
||||
<!-- Status filter -->
|
||||
<select name="status_filter">
|
||||
<option value="all" <?php selected( $current_status, 'all' ); ?>>
|
||||
<?php esc_html_e( 'Wszystkie statusy', 'yacht-booking' ); ?>
|
||||
</option>
|
||||
<option value="pending" <?php selected( $current_status, 'pending' ); ?>>
|
||||
<?php esc_html_e( 'Oczekujące', 'yacht-booking' ); ?>
|
||||
</option>
|
||||
<option value="confirmed" <?php selected( $current_status, 'confirmed' ); ?>>
|
||||
<?php esc_html_e( 'Potwierdzone', 'yacht-booking' ); ?>
|
||||
</option>
|
||||
<option value="cancelled" <?php selected( $current_status, 'cancelled' ); ?>>
|
||||
<?php esc_html_e( 'Anulowane', 'yacht-booking' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<!-- Yacht filter -->
|
||||
<select name="yacht_filter">
|
||||
<option value="all" <?php selected( $current_yacht, 'all' ); ?>>
|
||||
<?php esc_html_e( 'Wszystkie jachty', 'yacht-booking' ); ?>
|
||||
</option>
|
||||
<?php
|
||||
$yachts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $yachts as $yacht ) {
|
||||
printf(
|
||||
'<option value="%d" %s>%s</option>',
|
||||
$yacht->ID,
|
||||
selected( $current_yacht, $yacht->ID, false ),
|
||||
esc_html( $yacht->post_title )
|
||||
);
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
|
||||
<input type="submit" class="button" value="<?php esc_attr_e( 'Filtruj', 'yacht-booking' ); ?>">
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Message when no items
|
||||
*/
|
||||
public function no_items() {
|
||||
esc_html_e( 'Brak rezerwacji.', 'yacht-booking' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
/**
|
||||
* Inquiry List Table
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'WP_List_Table' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Inquiry List Table class
|
||||
*/
|
||||
class Inquiry_List_Table extends \WP_List_Table {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
array(
|
||||
'singular' => 'inquiry',
|
||||
'plural' => 'inquiries',
|
||||
'ajax' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get columns
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_columns() {
|
||||
return array(
|
||||
'cb' => '<input type="checkbox" />',
|
||||
'id' => __( 'ID', 'yacht-booking' ),
|
||||
'yacht' => __( 'Jacht', 'yacht-booking' ),
|
||||
'customer' => __( 'Klient', 'yacht-booking' ),
|
||||
'dates' => __( 'Preferowane terminy', 'yacht-booking' ),
|
||||
'message' => __( 'Wiadomość', 'yacht-booking' ),
|
||||
'emails' => __( 'Emaile', 'yacht-booking' ),
|
||||
'date_created' => __( 'Data wysłania', 'yacht-booking' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sortable columns
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_sortable_columns() {
|
||||
return array(
|
||||
'id' => array( 'ID', true ),
|
||||
'date_created' => array( 'date', true ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bulk actions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_bulk_actions() {
|
||||
return array(
|
||||
'delete' => __( 'Usu\u0144', 'yacht-booking' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare items for display
|
||||
*/
|
||||
public function prepare_items() {
|
||||
$per_page = 20;
|
||||
$current_page = $this->get_pagenum();
|
||||
$orderby = isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : 'date';
|
||||
$order = isset( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : 'DESC';
|
||||
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
|
||||
|
||||
$args = array(
|
||||
'post_type' => 'yacht_inquiry',
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $current_page,
|
||||
'orderby' => $orderby,
|
||||
'order' => $order,
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
if ( ! empty( $search ) ) {
|
||||
$args['s'] = $search;
|
||||
}
|
||||
|
||||
if ( ! empty( $_GET['yacht_filter'] ) && 'all' !== $_GET['yacht_filter'] ) {
|
||||
$args['meta_query'] = array(
|
||||
array(
|
||||
'key' => '_inquiry_yacht_id',
|
||||
'value' => (int) $_GET['yacht_filter'],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
|
||||
$this->items = $query->posts;
|
||||
|
||||
$this->set_pagination_args(
|
||||
array(
|
||||
'total_items' => $query->found_posts,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => $query->max_num_pages,
|
||||
)
|
||||
);
|
||||
|
||||
$columns = $this->get_columns();
|
||||
$hidden = array();
|
||||
$sortable = $this->get_sortable_columns();
|
||||
|
||||
$this->_column_headers = array( $columns, $hidden, $sortable );
|
||||
}
|
||||
|
||||
/**
|
||||
* Column checkbox
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_cb( $item ) {
|
||||
return sprintf( '<input type="checkbox" name="inquiry[]" value="%d" />', $item->ID );
|
||||
}
|
||||
|
||||
/**
|
||||
* Column ID
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_id( $item ) {
|
||||
$delete_url = wp_nonce_url(
|
||||
admin_url( 'admin.php?page=yacht-inquiries&action=delete&inquiry=' . $item->ID ),
|
||||
'delete_inquiry_' . $item->ID
|
||||
);
|
||||
|
||||
$actions = array(
|
||||
'delete' => sprintf(
|
||||
'<a href="%s" class="submitdelete" onclick="return confirm(\'%s\')">%s</a>',
|
||||
esc_url( $delete_url ),
|
||||
esc_js( __( 'Czy na pewno chcesz usunac to zapytanie?', 'yacht-booking' ) ),
|
||||
__( 'Usun', 'yacht-booking' )
|
||||
),
|
||||
);
|
||||
|
||||
return sprintf( '<strong>#%d</strong>%s', $item->ID, $this->row_actions( $actions ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Column yacht
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_yacht( $item ) {
|
||||
$yacht_id = Inquiry::get_yacht_id( $item->ID );
|
||||
$yacht = get_post( $yacht_id );
|
||||
|
||||
return $yacht ? esc_html( $yacht->post_title ) : '—';
|
||||
}
|
||||
|
||||
/**
|
||||
* Column customer
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_customer( $item ) {
|
||||
return sprintf(
|
||||
'<strong>%s</strong><br><small>%s<br>%s</small>',
|
||||
esc_html( Inquiry::get_customer_name( $item->ID ) ),
|
||||
esc_html( Inquiry::get_customer_email( $item->ID ) ),
|
||||
esc_html( Inquiry::get_customer_phone( $item->ID ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column preferred dates
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_dates( $item ) {
|
||||
$dates = Inquiry::get_preferred_dates( $item->ID );
|
||||
return $dates ? esc_html( $dates ) : '—';
|
||||
}
|
||||
|
||||
/**
|
||||
* Column message
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_message( $item ) {
|
||||
$message = Inquiry::get_message( $item->ID );
|
||||
if ( ! $message ) {
|
||||
return '—';
|
||||
}
|
||||
|
||||
$short = mb_strlen( $message ) > 80 ? mb_substr( $message, 0, 80 ) . '...' : $message;
|
||||
return '<span title="' . esc_attr( $message ) . '">' . esc_html( $short ) . '</span>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Column emails — links to view sent emails.
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_emails( $item ) {
|
||||
$admin_url = wp_nonce_url(
|
||||
admin_url( 'admin.php?page=yacht-inquiries&action=view_email&inquiry=' . $item->ID . '&type=admin' ),
|
||||
'view_email_' . $item->ID
|
||||
);
|
||||
$customer_url = wp_nonce_url(
|
||||
admin_url( 'admin.php?page=yacht-inquiries&action=view_email&inquiry=' . $item->ID . '&type=customer' ),
|
||||
'view_email_' . $item->ID
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<a href="%s" class="button button-small">%s</a> <a href="%s" class="button button-small">%s</a>',
|
||||
esc_url( $admin_url ),
|
||||
esc_html__( 'Do admina', 'yacht-booking' ),
|
||||
esc_url( $customer_url ),
|
||||
esc_html__( 'Do klienta', 'yacht-booking' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column date created
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_date_created( $item ) {
|
||||
return sprintf(
|
||||
'%s<br><small>%s</small>',
|
||||
get_the_date( Settings::get_date_format(), $item ),
|
||||
get_the_time( 'H:i', $item )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Default column
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @param string $column_name Column name.
|
||||
* @return string
|
||||
*/
|
||||
public function column_default( $item, $column_name ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters above table
|
||||
*
|
||||
* @param string $which Position.
|
||||
*/
|
||||
protected function extra_tablenav( $which ) {
|
||||
if ( 'top' !== $which ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$current_yacht = isset( $_GET['yacht_filter'] ) ? (int) $_GET['yacht_filter'] : 'all';
|
||||
?>
|
||||
<div class="alignleft actions">
|
||||
<select name="yacht_filter">
|
||||
<option value="all" <?php selected( $current_yacht, 'all' ); ?>>
|
||||
<?php esc_html_e( 'Wszystkie jachty', 'yacht-booking' ); ?>
|
||||
</option>
|
||||
<?php
|
||||
$yachts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
foreach ( $yachts as $yacht ) {
|
||||
printf(
|
||||
'<option value="%d" %s>%s</option>',
|
||||
$yacht->ID,
|
||||
selected( $current_yacht, $yacht->ID, false ),
|
||||
esc_html( $yacht->post_title )
|
||||
);
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<input type="submit" class="button" value="<?php esc_attr_e( 'Filtruj', 'yacht-booking' ); ?>">
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* No items message
|
||||
*/
|
||||
public function no_items() {
|
||||
esc_html_e( 'Brak zapytań.', 'yacht-booking' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
/**
|
||||
* Yacht List Table
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load WP_List_Table if not loaded
|
||||
if ( ! class_exists( 'WP_List_Table' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Yacht List Table class
|
||||
*/
|
||||
class Yacht_List_Table extends \WP_List_Table {
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
array(
|
||||
'singular' => 'yacht',
|
||||
'plural' => 'yachts',
|
||||
'ajax' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get columns
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_columns() {
|
||||
return array(
|
||||
'cb' => '<input type="checkbox" />',
|
||||
'title' => __( 'Nazwa jachtu', 'yacht-booking' ),
|
||||
'gcal' => __( 'Google Calendar', 'yacht-booking' ),
|
||||
'bookings' => __( 'Rezerwacje', 'yacht-booking' ),
|
||||
'date' => __( 'Data utworzenia', 'yacht-booking' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sortable columns
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_sortable_columns() {
|
||||
return array(
|
||||
'title' => array( 'title', false ),
|
||||
'date' => array( 'date', true ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bulk actions
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_bulk_actions() {
|
||||
return array(
|
||||
'delete' => __( 'Usuń', 'yacht-booking' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare items for display
|
||||
*/
|
||||
public function prepare_items() {
|
||||
$per_page = 20;
|
||||
$current_page = $this->get_pagenum();
|
||||
$orderby = isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : 'title';
|
||||
$order = isset( $_GET['order'] ) ? sanitize_text_field( wp_unslash( $_GET['order'] ) ) : 'ASC';
|
||||
$search = isset( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : '';
|
||||
|
||||
$args = array(
|
||||
'post_type' => 'yacht',
|
||||
'posts_per_page' => $per_page,
|
||||
'paged' => $current_page,
|
||||
'orderby' => $orderby,
|
||||
'order' => $order,
|
||||
'post_status' => 'publish',
|
||||
);
|
||||
|
||||
if ( ! empty( $search ) ) {
|
||||
$args['s'] = $search;
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
|
||||
$this->items = $query->posts;
|
||||
|
||||
$this->set_pagination_args(
|
||||
array(
|
||||
'total_items' => $query->found_posts,
|
||||
'per_page' => $per_page,
|
||||
'total_pages' => $query->max_num_pages,
|
||||
)
|
||||
);
|
||||
|
||||
$columns = $this->get_columns();
|
||||
$hidden = array();
|
||||
$sortable = $this->get_sortable_columns();
|
||||
|
||||
$this->_column_headers = array( $columns, $hidden, $sortable );
|
||||
}
|
||||
|
||||
/**
|
||||
* Column checkbox
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_cb( $item ) {
|
||||
return sprintf(
|
||||
'<input type="checkbox" name="yacht[]" value="%d" />',
|
||||
$item->ID
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column title
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_title( $item ) {
|
||||
$edit_url = admin_url( 'admin.php?page=yacht-bookings-add-yacht&yacht_id=' . $item->ID );
|
||||
$delete_url = wp_nonce_url(
|
||||
admin_url( 'admin.php?page=yacht-bookings&action=delete&yacht=' . $item->ID ),
|
||||
'delete_yacht_' . $item->ID
|
||||
);
|
||||
|
||||
$actions = array(
|
||||
'edit' => sprintf(
|
||||
'<a href="%s">%s</a>',
|
||||
esc_url( $edit_url ),
|
||||
__( 'Edytuj', 'yacht-booking' )
|
||||
),
|
||||
'delete' => sprintf(
|
||||
'<a href="%s" class="submitdelete" onclick="return confirm(\'%s\')">%s</a>',
|
||||
esc_url( $delete_url ),
|
||||
esc_js( __( 'Czy na pewno chcesz usunąć ten jacht? Spowoduje to również usunięcie wszystkich powiązanych rezerwacji.', 'yacht-booking' ) ),
|
||||
__( 'Usuń', 'yacht-booking' )
|
||||
),
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<strong><a href="%s" class="row-title">%s</a></strong>%s',
|
||||
esc_url( $edit_url ),
|
||||
esc_html( $item->post_title ),
|
||||
$this->row_actions( $actions )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column Google Calendar
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_gcal( $item ) {
|
||||
$gcal_id = Yacht::get_gcal_id( $item->ID );
|
||||
|
||||
if ( $gcal_id ) {
|
||||
return sprintf(
|
||||
'<span class="gcal-status connected"><span class="dashicons dashicons-yes-alt"></span> %s</span>',
|
||||
esc_html__( 'Połączony', 'yacht-booking' )
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'<span class="gcal-status disconnected"><span class="dashicons dashicons-dismiss"></span> %s</span>',
|
||||
esc_html__( 'Niepołączony', 'yacht-booking' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Column bookings count
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_bookings( $item ) {
|
||||
$count = $this->count_bookings( $item->ID );
|
||||
|
||||
if ( $count > 0 ) {
|
||||
return sprintf(
|
||||
'<a href="%s">%d</a>',
|
||||
esc_url( admin_url( 'admin.php?page=yacht-bookings-list&yacht_filter=' . $item->ID ) ),
|
||||
$count
|
||||
);
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Column date
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @return string
|
||||
*/
|
||||
public function column_date( $item ) {
|
||||
return get_the_date( Settings::get_date_format() . ' H:i', $item );
|
||||
}
|
||||
|
||||
/**
|
||||
* Default column
|
||||
*
|
||||
* @param object $item Item.
|
||||
* @param string $column_name Column name.
|
||||
* @return string
|
||||
*/
|
||||
public function column_default( $item, $column_name ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Count bookings for yacht
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @return int
|
||||
*/
|
||||
private function count_bookings( $yacht_id ) {
|
||||
$query = new \WP_Query(
|
||||
array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => '_booking_yacht_id',
|
||||
'value' => $yacht_id,
|
||||
),
|
||||
),
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
return $query->found_posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message when no items
|
||||
*/
|
||||
public function no_items() {
|
||||
esc_html_e( 'Brak jachtów. Dodaj pierwszy jacht!', 'yacht-booking' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
/**
|
||||
* Yacht Edit Form View
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get yacht data
|
||||
$title = $yacht ? $yacht->post_title : '';
|
||||
$content = $yacht ? $yacht->post_content : '';
|
||||
$gcal_id = $yacht ? \YachtBooking\Yacht::get_gcal_id( $yacht->ID ) : '';
|
||||
$ical_import_url = $yacht ? \YachtBooking\Integrations\ICal\ICal_Import::get_import_url( $yacht->ID ) : '';
|
||||
$ical_feed_url = $yacht ? \YachtBooking\Integrations\ICal\ICal_Feed::get_feed_url( $yacht->ID ) : '';
|
||||
$ical_last_import = $yacht ? \YachtBooking\Integrations\ICal\ICal_Import::get_last_import_time( $yacht->ID ) : '';
|
||||
|
||||
$page_title = $yacht ? __( 'Edytuj Jacht', 'yacht-booking' ) : __( 'Dodaj Jacht', 'yacht-booking' );
|
||||
?>
|
||||
|
||||
<div class="wrap">
|
||||
<h1><?php echo esc_html( $page_title ); ?></h1>
|
||||
|
||||
<?php
|
||||
// Success message
|
||||
if ( isset( $_GET['saved'] ) ) {
|
||||
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'Jacht został zapisany.', 'yacht-booking' ) . '</p></div>';
|
||||
}
|
||||
?>
|
||||
|
||||
<form method="post" action="" class="yacht-edit-form">
|
||||
<?php wp_nonce_field( 'yacht_booking_save_yacht', 'yacht_booking_nonce' ); ?>
|
||||
<input type="hidden" name="yacht_booking_save_yacht" value="1" />
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tbody>
|
||||
<!-- Nazwa jachtu -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="yacht_title">
|
||||
<?php esc_html_e( 'Nazwa jachtu', 'yacht-booking' ); ?>
|
||||
<span class="description"> (<?php esc_html_e( 'wymagane', 'yacht-booking' ); ?>)</span>
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
name="yacht_title"
|
||||
id="yacht_title"
|
||||
value="<?php echo esc_attr( $title ); ?>"
|
||||
class="regular-text"
|
||||
required
|
||||
/>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Podaj nazwę jachtu, np. "Laguna 42" lub "Bavaria Cruiser 46"', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Opis jachtu -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="yacht_description">
|
||||
<?php esc_html_e( 'Opis jachtu', 'yacht-booking' ); ?>
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<?php
|
||||
wp_editor(
|
||||
$content,
|
||||
'yacht_description',
|
||||
array(
|
||||
'textarea_name' => 'yacht_description',
|
||||
'textarea_rows' => 10,
|
||||
'media_buttons' => true,
|
||||
'teeny' => false,
|
||||
'tinymce' => array(
|
||||
'toolbar1' => 'formatselect,bold,italic,underline,bullist,numlist,link,unlink,blockquote,alignleft,aligncenter,alignright',
|
||||
),
|
||||
)
|
||||
);
|
||||
?>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Szczegółowy opis jachtu: parametry techniczne, wyposażenie, udogodnienia itp.', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Google Calendar ID -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="yacht_gcal_id">
|
||||
<?php esc_html_e( 'Google Calendar ID', 'yacht-booking' ); ?>
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
name="yacht_gcal_id"
|
||||
id="yacht_gcal_id"
|
||||
value="<?php echo esc_attr( $gcal_id ); ?>"
|
||||
class="regular-text code"
|
||||
placeholder="nazwa@group.calendar.google.com"
|
||||
/>
|
||||
<p class="description">
|
||||
<?php
|
||||
echo wp_kses_post(
|
||||
sprintf(
|
||||
/* translators: %s: link to Google Calendar settings */
|
||||
__( 'ID kalendarza Google do synchronizacji rezerwacji. <a href="%s" target="_blank">Jak znaleźć Calendar ID?</a>', 'yacht-booking' ),
|
||||
'https://docs.simplecalendar.io/find-google-calendar-id/'
|
||||
)
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php if ( $gcal_id ) : ?>
|
||||
<p class="gcal-status connected">
|
||||
<span class="dashicons dashicons-yes-alt"></span>
|
||||
<?php esc_html_e( 'Połączony z Google Calendar', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- iCal Import URL -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="yacht_ical_import_url">
|
||||
<?php esc_html_e( 'Import iCal (URL)', 'yacht-booking' ); ?>
|
||||
</label>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="url"
|
||||
name="yacht_ical_import_url"
|
||||
id="yacht_ical_import_url"
|
||||
value="<?php echo esc_attr( $ical_import_url ); ?>"
|
||||
class="large-text code"
|
||||
placeholder="https://calendar.google.com/calendar/ical/...basic.ics"
|
||||
/>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Wklej adres URL pliku .ics z Google Calendar (lub innego kalendarza). Wydarzenia zostaną zaimportowane jako zablokowane terminy (synchronizacja co godzinę).', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
<p class="description">
|
||||
<strong><?php esc_html_e( 'Jak uzyskać link iCal z Google Calendar:', 'yacht-booking' ); ?></strong>
|
||||
<?php esc_html_e( 'Google Calendar → Ustawienia → Wybierz kalendarz → „Tajny adres w formacie iCal" → Skopiuj link.', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
<?php if ( $ical_last_import ) : ?>
|
||||
<p>
|
||||
<span class="dashicons dashicons-update" style="color: #2271b1;"></span>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: date/time */
|
||||
esc_html__( 'Ostatni import: %s', 'yacht-booking' ),
|
||||
esc_html( $ical_last_import )
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<?php if ( $yacht ) : ?>
|
||||
<!-- iCal Export Feed URL -->
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php esc_html_e( 'Eksport iCal (feed)', 'yacht-booking' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
value="<?php echo esc_attr( $ical_feed_url ); ?>"
|
||||
class="large-text code"
|
||||
readonly
|
||||
onclick="this.select();"
|
||||
/>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Skopiuj ten link i dodaj go w Google Calendar (Inne kalendarze → Z adresu URL), aby rezerwacje z tego systemu pojawiły się w Twoim kalendarzu.', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php submit_button( __( 'Zapisz jacht', 'yacht-booking' ), 'primary large', 'submit', true ); ?>
|
||||
|
||||
<?php if ( $yacht ) : ?>
|
||||
<p>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=yacht-bookings' ) ); ?>" class="button button-secondary">
|
||||
<?php esc_html_e( 'Anuluj', 'yacht-booking' ); ?>
|
||||
</a>
|
||||
<a
|
||||
href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin.php?page=yacht-bookings&action=delete&yacht=' . $yacht->ID ), 'delete_yacht_' . $yacht->ID ) ); ?>"
|
||||
class="button button-link-delete"
|
||||
onclick="return confirm('<?php echo esc_js( __( 'Czy na pewno chcesz usunąć ten jacht? Spowoduje to również usunięcie wszystkich powiązanych rezerwacji.', 'yacht-booking' ) ); ?>');"
|
||||
style="color: #b32d2e; margin-left: 10px;"
|
||||
>
|
||||
<?php esc_html_e( 'Usuń jacht', 'yacht-booking' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<?php if ( $yacht ) : ?>
|
||||
<!-- Informacje o rezerwacjach -->
|
||||
<hr />
|
||||
<h2><?php esc_html_e( 'Powiązane rezerwacje', 'yacht-booking' ); ?></h2>
|
||||
<?php
|
||||
$bookings = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => '_booking_yacht_id',
|
||||
'value' => $yacht->ID,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $bookings ) :
|
||||
?>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
esc_html( _n( 'Ten jacht ma %d rezerwację.', 'Ten jacht ma %d rezerwacji.', count( $bookings ), 'yacht-booking' ) ),
|
||||
count( $bookings )
|
||||
);
|
||||
?>
|
||||
<a href="<?php echo esc_url( admin_url( 'admin.php?page=yacht-bookings-list&yacht_id=' . $yacht->ID ) ); ?>">
|
||||
<?php esc_html_e( 'Zobacz rezerwacje', 'yacht-booking' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
<?php else : ?>
|
||||
<p><?php esc_html_e( 'Brak rezerwacji dla tego jachtu.', 'yacht-booking' ); ?></p>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -0,0 +1,524 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API Controller
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST Controller class
|
||||
*/
|
||||
class Rest_Controller extends \WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* API namespace
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const NAMESPACE = 'yacht-booking/v1';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
// Hook into booking created event for email notifications
|
||||
add_action( 'yacht_booking_created', array( $this, 'send_booking_notification' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API routes
|
||||
*/
|
||||
public function register_routes() {
|
||||
// GET /yacht-booking/v1/yachts
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/yachts',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_yachts' ),
|
||||
'permission_callback' => '__return_true',
|
||||
)
|
||||
);
|
||||
|
||||
// GET /yacht-booking/v1/yachts/{id}
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/yachts/(?P<id>\d+)',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_yacht' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => function( $param ) {
|
||||
return is_numeric( $param );
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// GET /yacht-booking/v1/availability/{yacht_id}
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/availability/(?P<yacht_id>\d+)',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_availability' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'yacht_id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => function( $param ) {
|
||||
return is_numeric( $param );
|
||||
},
|
||||
),
|
||||
'start' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => function( $param ) {
|
||||
return strtotime( $param ) !== false;
|
||||
},
|
||||
),
|
||||
'end' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => function( $param ) {
|
||||
return strtotime( $param ) !== false;
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// POST /yacht-booking/v1/bookings
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/bookings',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_booking' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'yacht_id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => function( $param ) {
|
||||
return is_numeric( $param ) && get_post_type( $param ) === 'yacht';
|
||||
},
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
'start_date' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'end_date' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'customer_name' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'customer_email' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => 'is_email',
|
||||
'sanitize_callback' => 'sanitize_email',
|
||||
),
|
||||
'customer_phone' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// POST /yacht-booking/v1/inquiries
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/inquiries',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_inquiry' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'yacht_id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => function( $param ) {
|
||||
return is_numeric( $param ) && get_post_type( $param ) === 'yacht';
|
||||
},
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
'customer_name' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'customer_email' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => 'is_email',
|
||||
'sanitize_callback' => 'sanitize_email',
|
||||
),
|
||||
'customer_phone' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'preferred_dates' => array(
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'message' => array(
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'sanitize_textarea_field',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// GET /yacht-booking/v1/bookings (admin only)
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/bookings',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_bookings' ),
|
||||
'permission_callback' => array( $this, 'can_manage_bookings' ),
|
||||
)
|
||||
);
|
||||
|
||||
// PUT /yacht-booking/v1/bookings/{id}/status (admin only)
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/bookings/(?P<id>\d+)/status',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'update_booking_status' ),
|
||||
'permission_callback' => array( $this, 'can_manage_bookings' ),
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'required' => true,
|
||||
'validate_callback' => 'is_numeric',
|
||||
'sanitize_callback' => 'absint',
|
||||
),
|
||||
'status' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all yachts
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_yachts( $request ) {
|
||||
$yachts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
$data = array();
|
||||
foreach ( $yachts as $yacht ) {
|
||||
$data[] = array(
|
||||
'id' => $yacht->ID,
|
||||
'title' => $yacht->post_title,
|
||||
'description' => $yacht->post_content,
|
||||
'capacity' => Yacht::get_capacity( $yacht->ID ),
|
||||
'price_per_day' => Yacht::get_price_per_day( $yacht->ID ),
|
||||
'features' => Yacht::get_features( $yacht->ID ),
|
||||
'image' => get_the_post_thumbnail_url( $yacht->ID, 'large' ),
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single yacht
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function get_yacht( $request ) {
|
||||
$yacht_id = $request->get_param( 'id' );
|
||||
$yacht = get_post( $yacht_id );
|
||||
|
||||
if ( ! $yacht || $yacht->post_type !== 'yacht' ) {
|
||||
return new \WP_Error( 'not_found', __( 'Jacht nie znaleziony', 'yacht-booking' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'id' => $yacht->ID,
|
||||
'title' => $yacht->post_title,
|
||||
'description' => $yacht->post_content,
|
||||
'capacity' => Yacht::get_capacity( $yacht->ID ),
|
||||
'price_per_day' => Yacht::get_price_per_day( $yacht->ID ),
|
||||
'features' => Yacht::get_features( $yacht->ID ),
|
||||
'image' => get_the_post_thumbnail_url( $yacht->ID, 'large' ),
|
||||
);
|
||||
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get yacht availability
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_availability( $request ) {
|
||||
$yacht_id = $request->get_param( 'yacht_id' );
|
||||
$start = $request->get_param( 'start' );
|
||||
$end = $request->get_param( 'end' );
|
||||
|
||||
$calendar = Availability::get_availability_calendar( $yacht_id, $start, $end );
|
||||
|
||||
// Convert to array format for FullCalendar
|
||||
$events = array();
|
||||
foreach ( $calendar as $date => $info ) {
|
||||
$events[] = array(
|
||||
'date' => $date,
|
||||
'status' => $info['status'],
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( $events );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new booking
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function create_booking( $request ) {
|
||||
// Check if online booking is enabled
|
||||
if ( ! Settings::is_booking_enabled() ) {
|
||||
return new \WP_Error(
|
||||
'booking_disabled',
|
||||
__( 'Rezerwacje online są obecnie wyłączone. Skontaktuj się z nami telefonicznie lub mailowo.', 'yacht-booking' ),
|
||||
array( 'status' => 403 )
|
||||
);
|
||||
}
|
||||
|
||||
// Verify nonce
|
||||
if ( ! wp_verify_nonce( $request->get_header( 'X-WP-Nonce' ), 'wp_rest' ) ) {
|
||||
return new \WP_Error( 'invalid_nonce', __( 'Nieprawidłowy token bezpieczeństwa', 'yacht-booking' ), array( 'status' => 403 ) );
|
||||
}
|
||||
|
||||
$yacht_id = $request->get_param( 'yacht_id' );
|
||||
$start_date = $request->get_param( 'start_date' );
|
||||
$end_date = $request->get_param( 'end_date' );
|
||||
|
||||
// Check availability
|
||||
if ( ! Availability::is_available( $yacht_id, $start_date, $end_date ) ) {
|
||||
return new \WP_Error(
|
||||
'not_available',
|
||||
__( 'Jacht niedostępny w wybranych terminach', 'yacht-booking' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate price
|
||||
$days = Availability::count_days( $start_date, $end_date );
|
||||
$price_per_day = Yacht::get_price_per_day( $yacht_id );
|
||||
$total_price = $days * $price_per_day;
|
||||
|
||||
// Create booking
|
||||
$booking_id = Booking::create(
|
||||
array(
|
||||
'yacht_id' => $yacht_id,
|
||||
'start_date' => $start_date,
|
||||
'end_date' => $end_date,
|
||||
'customer_name' => $request->get_param( 'customer_name' ),
|
||||
'customer_email' => $request->get_param( 'customer_email' ),
|
||||
'customer_phone' => $request->get_param( 'customer_phone' ),
|
||||
'total_price' => $total_price,
|
||||
'status' => Settings::get_default_status(),
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
return new \WP_Error(
|
||||
'booking_failed',
|
||||
__( 'Nie udało się utworzyć rezerwacji', 'yacht-booking' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
// Mark as booked in availability cache
|
||||
Availability::mark_as_booked( $yacht_id, $start_date, $end_date, $booking_id );
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'success' => true,
|
||||
'message' => __( 'Rezerwacja została wysłana pomyślnie!', 'yacht-booking' ),
|
||||
'booking_id' => $booking_id,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new inquiry
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function create_inquiry( $request ) {
|
||||
// Verify nonce
|
||||
if ( ! wp_verify_nonce( $request->get_header( 'X-WP-Nonce' ), 'wp_rest' ) ) {
|
||||
return new \WP_Error( 'invalid_nonce', __( 'Nieprawidłowy token bezpieczeństwa', 'yacht-booking' ), array( 'status' => 403 ) );
|
||||
}
|
||||
|
||||
$inquiry_id = Inquiry::create(
|
||||
array(
|
||||
'yacht_id' => $request->get_param( 'yacht_id' ),
|
||||
'customer_name' => $request->get_param( 'customer_name' ),
|
||||
'customer_email' => $request->get_param( 'customer_email' ),
|
||||
'customer_phone' => $request->get_param( 'customer_phone' ),
|
||||
'preferred_dates' => $request->get_param( 'preferred_dates' ),
|
||||
'message' => $request->get_param( 'message' ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $inquiry_id ) {
|
||||
return new \WP_Error(
|
||||
'inquiry_failed',
|
||||
__( 'Nie udało się wysłać zapytania', 'yacht-booking' ),
|
||||
array( 'status' => 500 )
|
||||
);
|
||||
}
|
||||
|
||||
Inquiry::send_emails( $inquiry_id );
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'success' => true,
|
||||
'message' => __( 'Twoje zapytanie zostało wysłane. Sprawdź swoją skrzynkę email — wysłaliśmy potwierdzenie.', 'yacht-booking' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current user can manage bookings.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function can_manage_bookings() {
|
||||
return current_user_can( 'yacht_booking_manage_bookings' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bookings list (admin only).
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_bookings( $request ) {
|
||||
$bookings = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
)
|
||||
);
|
||||
|
||||
$data = array();
|
||||
|
||||
foreach ( $bookings as $booking ) {
|
||||
$booking_id = $booking->ID;
|
||||
$yacht_id = Booking::get_yacht_id( $booking_id );
|
||||
$yacht = get_post( $yacht_id );
|
||||
|
||||
$data[] = array(
|
||||
'id' => $booking_id,
|
||||
'yacht_id' => $yacht_id,
|
||||
'yacht_name' => $yacht ? $yacht->post_title : '',
|
||||
'start_date' => Booking::get_start_date( $booking_id ),
|
||||
'end_date' => Booking::get_end_date( $booking_id ),
|
||||
'status' => Booking::get_status( $booking_id ),
|
||||
'customer_name' => Booking::get_customer_name( $booking_id ),
|
||||
'customer_email' => Booking::get_customer_email( $booking_id ),
|
||||
'customer_phone' => Booking::get_customer_phone( $booking_id ),
|
||||
'total_price' => Booking::get_total_price( $booking_id ),
|
||||
'created_at' => get_the_date( 'Y-m-d H:i:s', $booking ),
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( $data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update booking status (admin only).
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response|\WP_Error
|
||||
*/
|
||||
public function update_booking_status( $request ) {
|
||||
$booking_id = absint( $request->get_param( 'id' ) );
|
||||
$status = sanitize_text_field( $request->get_param( 'status' ) );
|
||||
|
||||
if ( ! in_array( $status, array( 'pending', 'confirmed', 'cancelled' ), true ) ) {
|
||||
return new \WP_Error( 'invalid_status', __( 'Nieprawidłowy status rezerwacji', 'yacht-booking' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$booking = get_post( $booking_id );
|
||||
if ( ! $booking || 'yacht_booking' !== $booking->post_type ) {
|
||||
return new \WP_Error( 'not_found', __( 'Rezerwacja nie została znaleziona', 'yacht-booking' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
Booking::update_status( $booking_id, $status );
|
||||
|
||||
if ( 'cancelled' === $status ) {
|
||||
Availability::clear_booking_availability( $booking_id );
|
||||
}
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'success' => true,
|
||||
'status' => $status,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send booking notification email to admin
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
*/
|
||||
public function send_booking_notification( $booking_id ) {
|
||||
$template_data = Email_Templates::get_booking_template_data( $booking_id );
|
||||
$template = Email_Templates::compile( Email_Templates::TYPE_ADMIN_NEW_BOOKING, $template_data );
|
||||
$admin_email = get_option( 'admin_email' );
|
||||
$customer_email = Booking::get_customer_email( $booking_id );
|
||||
|
||||
if ( empty( $template['subject'] ) || empty( $admin_email ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$headers = array(
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'From: ' . Settings::get_email_from_name() . ' <' . Settings::get_email_from_address() . '>',
|
||||
'Reply-To: ' . $template_data['customer_name'] . ' <' . $customer_email . '>',
|
||||
);
|
||||
|
||||
wp_mail( $admin_email, $template['subject'], $template['body'], $headers );
|
||||
|
||||
do_action( 'yacht_booking_notification_sent', $booking_id, $admin_email );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,648 @@
|
||||
/**
|
||||
* Yacht Booking Calendar Styles
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
/* Calendar Wrapper */
|
||||
.yacht-calendar-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto 40px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.yacht-calendar-switcher {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 10px 14px;
|
||||
margin: 0 0 22px 0;
|
||||
padding: 14px 16px;
|
||||
background: #f7f9fc;
|
||||
border: 1px solid #dbe5f0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.yacht-calendar-switcher-label {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #021526;
|
||||
}
|
||||
|
||||
.yacht-calendar-switcher-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.yacht-calendar-switcher-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 9px 14px;
|
||||
border: 1px solid #c8d4e3;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
color: #14324a;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.yacht-calendar-switcher-button:hover,
|
||||
.yacht-calendar-switcher-button:focus {
|
||||
border-color: #bc1834;
|
||||
color: #bc1834;
|
||||
outline: none;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.yacht-calendar-switcher-button.is-active {
|
||||
background: #bc1834;
|
||||
border-color: #bc1834;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 18px rgba(188, 24, 52, 0.18);
|
||||
}
|
||||
|
||||
/* Calendar Header */
|
||||
.yacht-calendar-header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.yacht-calendar-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: #021526;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
|
||||
.yacht-calendar-description {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.yacht-calendar-instructions {
|
||||
margin: 0 0 12px 0;
|
||||
padding: 12px 14px;
|
||||
background: #eef6ff;
|
||||
border: 1px solid #d1e4ff;
|
||||
border-left: 4px solid #1565c0;
|
||||
border-radius: 6px;
|
||||
color: #0f2742;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.yacht-calendar-instructions p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.yacht-calendar-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px 16px;
|
||||
align-items: center;
|
||||
margin: 0 0 18px 0;
|
||||
}
|
||||
|
||||
.yacht-legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.yacht-legend-swatch {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.yacht-legend-swatch-past {
|
||||
background: #d0d5dd;
|
||||
}
|
||||
|
||||
/* Calendar Container */
|
||||
.yacht-calendar {
|
||||
margin-bottom: 30px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* FullCalendar overrides */
|
||||
.yacht-calendar .fc {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-button-primary {
|
||||
background: #bc1834;
|
||||
border-color: #bc1834;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-button-primary:hover {
|
||||
background: #021526;
|
||||
border-color: #021526;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-button-primary:not(:disabled):active,
|
||||
.yacht-calendar .fc-button-primary:not(:disabled).fc-button-active {
|
||||
background: #021526;
|
||||
border-color: #021526;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-daygrid-day.fc-day-past {
|
||||
background: #f5f5f5;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-daygrid-day.fc-day-past .fc-daygrid-day-number {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-daygrid-day:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-highlight {
|
||||
background: rgba(188, 24, 52, 0.1) !important;
|
||||
}
|
||||
|
||||
/* Day numbers - prevent truncation */
|
||||
.yacht-calendar .fc-daygrid-day-top {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-daygrid-day-number {
|
||||
padding: 4px 6px;
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-daygrid-day-frame {
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Custom day status classes */
|
||||
.yacht-day-available {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.yacht-day-booked,
|
||||
.yacht-day-blocked {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-bg-event {
|
||||
opacity: 0.78 !important;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-bg-event.yacht-day-available {
|
||||
opacity: 0.66 !important;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-bg-event.yacht-day-booked,
|
||||
.yacht-calendar .fc-bg-event.yacht-day-blocked {
|
||||
opacity: 0.92 !important;
|
||||
}
|
||||
|
||||
/* Booking Form Styles */
|
||||
.yacht-booking-form-container {
|
||||
background: #f9f9f9;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
margin-top: 30px;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.yacht-booking-form-container h4 {
|
||||
margin: 0 0 25px 0;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #021526;
|
||||
}
|
||||
|
||||
.yacht-booking-form .form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.yacht-booking-form .form-field {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.yacht-booking-form label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.yacht-booking-form label .required {
|
||||
color: #bc1834;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.yacht-booking-form input[type="text"],
|
||||
.yacht-booking-form input[type="email"],
|
||||
.yacht-booking-form input[type="tel"],
|
||||
.yacht-booking-form input[type="date"] {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.yacht-booking-form input:focus {
|
||||
outline: none;
|
||||
border-color: #bc1834;
|
||||
box-shadow: 0 0 0 3px rgba(188, 24, 52, 0.1);
|
||||
}
|
||||
|
||||
.yacht-booking-form input:read-only {
|
||||
background: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.booking-terms {
|
||||
margin-top: 18px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.booking-terms a {
|
||||
color: #bc1834;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.yacht-booking-form .form-actions {
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.yacht-booking-submit {
|
||||
background: #bc1834;
|
||||
color: #fff;
|
||||
padding: 14px 30px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.yacht-booking-submit:hover {
|
||||
background: #021526;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.yacht-booking-submit:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Response Messages */
|
||||
.yacht-booking-response {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.booking-success,
|
||||
.booking-error {
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.booking-success strong,
|
||||
.booking-error strong {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Error/Notice Messages */
|
||||
.yacht-calendar-error,
|
||||
.yacht-calendar-notice {
|
||||
padding: 20px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.yacht-calendar-error p,
|
||||
.yacht-calendar-notice p {
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* View-only mode (booking disabled) */
|
||||
.yacht-calendar-view-only {
|
||||
max-width: 1200px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.yacht-calendar-view-only .yacht-calendar-header {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.yacht-calendar-view-only .yacht-calendar-title {
|
||||
font-size: 22px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.yacht-calendar-view-only .yacht-calendar-instructions {
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.yacht-calendar-view-only .yacht-calendar-legend {
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.yacht-calendar-view-only .yacht-calendar {
|
||||
padding: 10px;
|
||||
margin-bottom: 0;
|
||||
height: auto !important;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.yacht-calendar-view-only .fc-daygrid-day-frame {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.yacht-calendar-view-only .fc-daygrid-day-top {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.yacht-calendar-view-only .fc-daygrid-day-number {
|
||||
padding: 2px 4px;
|
||||
min-width: 22px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.yacht-calendar-view-only .fc-col-header-cell {
|
||||
font-size: 12px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.yacht-calendar-view-only .fc-button {
|
||||
font-size: 11px !important;
|
||||
padding: 5px 10px !important;
|
||||
}
|
||||
|
||||
.yacht-calendar-view-only .fc-toolbar-title {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
/* Inquiry layout (calendar + form side by side) */
|
||||
.yacht-inquiry-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form-container {
|
||||
background: #021526;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form-container h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.yacht-inquiry-desc {
|
||||
margin: 0 0 18px 0;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form .form-field {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form label .required {
|
||||
color: #ff6b6b;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form input[type="text"],
|
||||
.yacht-inquiry-form input[type="email"],
|
||||
.yacht-inquiry-form input[type="tel"],
|
||||
.yacht-inquiry-form textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form input::placeholder,
|
||||
.yacht-inquiry-form textarea::placeholder {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.yacht-inquiry-form input:focus,
|
||||
.yacht-inquiry-form textarea:focus {
|
||||
outline: none;
|
||||
border-color: #bc1834;
|
||||
box-shadow: 0 0 0 3px rgba(188, 24, 52, 0.3);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.yacht-inquiry-form textarea {
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form .form-actions {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form .booking-terms {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form .booking-terms a {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.yacht-inquiry-form .yacht-booking-submit {
|
||||
background: #bc1834;
|
||||
border-color: #bc1834;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form .yacht-booking-submit:hover {
|
||||
background: #fff;
|
||||
color: #021526;
|
||||
}
|
||||
|
||||
.yacht-inquiry-response {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
/* Tablet Responsive */
|
||||
@media (max-width: 992px) {
|
||||
.yacht-calendar-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.yacht-calendar {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inquiry layout tablet */
|
||||
@media (max-width: 992px) {
|
||||
.yacht-inquiry-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.yacht-calendar-wrapper {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.yacht-calendar-switcher {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.yacht-calendar-switcher-buttons {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form-container {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.yacht-calendar-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.yacht-calendar {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.yacht-calendar-legend {
|
||||
gap: 8px 12px;
|
||||
}
|
||||
|
||||
.yacht-booking-form-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.yacht-booking-form .form-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-toolbar {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-toolbar-chunk {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.yacht-calendar .fc-button {
|
||||
font-size: 12px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra Small Mobile */
|
||||
@media (max-width: 480px) {
|
||||
.yacht-calendar-wrapper {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.yacht-calendar-switcher {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.yacht-calendar-switcher-button {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.yacht-calendar-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.yacht-booking-form-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.yacht-booking-form input[type="text"],
|
||||
.yacht-booking-form input[type="email"],
|
||||
.yacht-booking-form input[type="tel"] {
|
||||
font-size: 14px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.yacht-booking-submit {
|
||||
padding: 12px 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* Yacht Booking Calendar - Frontend JavaScript
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Initialize Yacht Calendar
|
||||
*/
|
||||
function initYachtCalendar($wrapper) {
|
||||
const calendarId = $wrapper.data('calendar-id');
|
||||
const bookingEnabled = $wrapper.data('booking-enabled') !== 0;
|
||||
const yachtsData = $wrapper.attr('data-yachts');
|
||||
const primaryColor = $wrapper.data('primary-color') || '#2271b1';
|
||||
const normalizeColor = function(color, fallbackColor, legacyColor) {
|
||||
const raw = (color || '').toString().trim().toLowerCase();
|
||||
if (!raw || raw === legacyColor) {
|
||||
return fallbackColor;
|
||||
}
|
||||
return color;
|
||||
};
|
||||
const availableBg = normalizeColor($wrapper.data('available-bg'), '#35b56a', '#d4edda');
|
||||
const bookedBg = normalizeColor($wrapper.data('booked-bg'), '#e53935', '#f8d7da');
|
||||
const yachtItems = yachtsData ? JSON.parse(yachtsData) : [];
|
||||
const yachtMap = {};
|
||||
const state = {
|
||||
currentYachtId: parseInt($wrapper.attr('data-yacht-id'), 10) || 0
|
||||
};
|
||||
|
||||
const $calendarEl = $wrapper.find('.yacht-calendar');
|
||||
const $title = $wrapper.find('.yacht-calendar-title');
|
||||
const $description = $wrapper.find('.yacht-calendar-description');
|
||||
const $switcher = $wrapper.find('.yacht-calendar-switcher');
|
||||
const $switcherButtonsContainer = $wrapper.find('.yacht-calendar-switcher-buttons');
|
||||
const $form = $wrapper.find('.yacht-booking-form');
|
||||
const $formContainer = $wrapper.find('.yacht-booking-form-container');
|
||||
const $inquiryForm = $wrapper.find('.yacht-inquiry-form');
|
||||
const $inquiryResponse = $wrapper.find('.yacht-inquiry-response');
|
||||
|
||||
if (!$calendarEl.length) {
|
||||
console.error('Calendar element not found');
|
||||
return;
|
||||
}
|
||||
|
||||
yachtItems.forEach(function(item) {
|
||||
yachtMap[item.id] = item;
|
||||
});
|
||||
|
||||
function getAvailabilityUrl() {
|
||||
return yachtBookingData.apiUrl + '/availability/' + state.currentYachtId;
|
||||
}
|
||||
|
||||
function getTitleText(yachtTitle) {
|
||||
if (bookingEnabled) {
|
||||
return (yachtBookingData.i18n.bookingTitlePrefix || 'Rezerwacja: %s').replace('%s', yachtTitle);
|
||||
}
|
||||
|
||||
return (yachtBookingData.i18n.availabilityTitlePrefix || 'Dostepnosc: %s').replace('%s', yachtTitle);
|
||||
}
|
||||
|
||||
function renderSwitcherButtons() {
|
||||
if (!$switcher.length || !$switcherButtonsContainer.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const otherYachts = yachtItems.filter(function(item) {
|
||||
return item.id !== state.currentYachtId;
|
||||
});
|
||||
|
||||
if (!otherYachts.length) {
|
||||
$switcher.hide();
|
||||
$switcherButtonsContainer.empty();
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonsHtml = otherYachts.map(function(item) {
|
||||
return '<button type="button" class="yacht-calendar-switcher-button" data-yacht-id="' + item.id + '" aria-pressed="false">' +
|
||||
escapeHtml(item.title) +
|
||||
'</button>';
|
||||
}).join('');
|
||||
|
||||
$switcherButtonsContainer.html(buttonsHtml);
|
||||
$switcher.show();
|
||||
}
|
||||
|
||||
function resetFormsForYachtSwitch() {
|
||||
if ($form.length) {
|
||||
$form.each(function() {
|
||||
this.reset();
|
||||
});
|
||||
$form.attr('data-yacht-id', state.currentYachtId);
|
||||
$form.find('.yacht-booking-response').empty();
|
||||
}
|
||||
|
||||
if ($formContainer.length) {
|
||||
$formContainer.stop(true, true).hide();
|
||||
}
|
||||
|
||||
if ($inquiryForm.length) {
|
||||
$inquiryForm.each(function() {
|
||||
this.reset();
|
||||
});
|
||||
$inquiryForm.attr('data-yacht-id', state.currentYachtId);
|
||||
}
|
||||
|
||||
if ($inquiryResponse.length) {
|
||||
$inquiryResponse.empty();
|
||||
}
|
||||
}
|
||||
|
||||
function updateYachtDetails() {
|
||||
const yacht = yachtMap[state.currentYachtId];
|
||||
|
||||
if (!yacht) {
|
||||
return;
|
||||
}
|
||||
|
||||
$title.text(getTitleText(yacht.title));
|
||||
|
||||
if ($description.length) {
|
||||
if (yacht.description) {
|
||||
$description.html(yacht.description).show();
|
||||
} else {
|
||||
$description.empty().hide();
|
||||
}
|
||||
}
|
||||
|
||||
renderSwitcherButtons();
|
||||
resetFormsForYachtSwitch();
|
||||
}
|
||||
|
||||
// FullCalendar initialization
|
||||
const calendar = new FullCalendar.Calendar($calendarEl[0], {
|
||||
initialView: 'dayGridMonth',
|
||||
contentHeight: bookingEnabled ? undefined : 'auto',
|
||||
locale: 'pl',
|
||||
firstDay: 1, // Poniedziałek
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth'
|
||||
},
|
||||
buttonText: {
|
||||
today: 'Dzisiaj',
|
||||
month: 'Miesiąc'
|
||||
},
|
||||
selectable: bookingEnabled,
|
||||
selectMirror: bookingEnabled,
|
||||
unselectAuto: false,
|
||||
selectLongPressDelay: 1,
|
||||
|
||||
// Fetch events (availability) from REST API
|
||||
events: function(info, successCallback, failureCallback) {
|
||||
const startDate = formatDate(info.start);
|
||||
const endDate = formatDate(info.end);
|
||||
|
||||
$.ajax({
|
||||
url: getAvailabilityUrl(),
|
||||
method: 'GET',
|
||||
data: {
|
||||
start: startDate,
|
||||
end: endDate
|
||||
},
|
||||
success: function(data) {
|
||||
const events = data.map(function(day) {
|
||||
return {
|
||||
id: day.date,
|
||||
start: day.date,
|
||||
allDay: true,
|
||||
display: 'background',
|
||||
backgroundColor: day.status === 'available' ? availableBg : bookedBg,
|
||||
classNames: ['yacht-day-' + day.status],
|
||||
extendedProps: {
|
||||
status: day.status,
|
||||
booking_id: day.booking_id || null
|
||||
}
|
||||
};
|
||||
});
|
||||
successCallback(events);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.error('Error fetching availability:', error);
|
||||
failureCallback(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Handle date selection
|
||||
select: function(info) {
|
||||
const startDate = formatDate(info.start);
|
||||
let endDate = formatDate(info.end);
|
||||
|
||||
// FullCalendar end date is exclusive, so subtract 1 day
|
||||
endDate = subtractDay(endDate);
|
||||
|
||||
// Check if any selected dates are unavailable
|
||||
const unavailableDates = calendar.getEvents().filter(function(event) {
|
||||
const eventDate = formatDate(event.start);
|
||||
return eventDate >= startDate && eventDate <= endDate &&
|
||||
event.extendedProps.status !== 'available';
|
||||
});
|
||||
|
||||
if (unavailableDates.length > 0) {
|
||||
alert(yachtBookingData.i18n.unavailableDatesSelected || 'Wybrane daty zawierają niedostępne terminy. Proszę wybrać inne daty.');
|
||||
calendar.unselect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fill form dates
|
||||
$form.find('.booking-start-date').val(startDate);
|
||||
$form.find('.booking-end-date').val(endDate);
|
||||
|
||||
// Show booking form
|
||||
$formContainer.slideDown(300);
|
||||
|
||||
// Scroll to form
|
||||
$('html, body').animate({
|
||||
scrollTop: $formContainer.offset().top - 100
|
||||
}, 500);
|
||||
},
|
||||
|
||||
// Highlight selectable dates
|
||||
selectAllow: function(selectInfo) {
|
||||
// Must select at least 1 day
|
||||
const daysDiff = Math.ceil((selectInfo.end - selectInfo.start) / (1000 * 60 * 60 * 24));
|
||||
return daysDiff >= 1;
|
||||
},
|
||||
|
||||
// Disable past dates
|
||||
selectConstraint: {
|
||||
start: new Date().toISOString().split('T')[0]
|
||||
},
|
||||
|
||||
// Day cell class names
|
||||
dayCellClassNames: function(arg) {
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
if (arg.date < today) {
|
||||
return ['fc-day-past'];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
|
||||
updateYachtDetails();
|
||||
|
||||
$switcher.on('click', '.yacht-calendar-switcher-button', function() {
|
||||
const newYachtId = parseInt($(this).attr('data-yacht-id'), 10) || 0;
|
||||
|
||||
if (!newYachtId || newYachtId === state.currentYachtId) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.currentYachtId = newYachtId;
|
||||
$wrapper.attr('data-yacht-id', newYachtId);
|
||||
calendar.unselect();
|
||||
updateYachtDetails();
|
||||
calendar.refetchEvents();
|
||||
});
|
||||
|
||||
// Handle inquiry form submission
|
||||
if ($inquiryForm.length) {
|
||||
$inquiryForm.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $submitBtn = $inquiryForm.find('.yacht-booking-submit');
|
||||
var $response = $inquiryForm.find('.yacht-inquiry-response');
|
||||
var originalBtnText = $submitBtn.text();
|
||||
|
||||
$submitBtn.prop('disabled', true).text(yachtBookingData.i18n.submitting || 'Wysyłanie...');
|
||||
$response.html('');
|
||||
|
||||
var formData = {
|
||||
yacht_id: state.currentYachtId,
|
||||
customer_name: $inquiryForm.find('[name="customer_name"]').val(),
|
||||
customer_email: $inquiryForm.find('[name="customer_email"]').val(),
|
||||
customer_phone: $inquiryForm.find('[name="customer_phone"]').val(),
|
||||
preferred_dates: $inquiryForm.find('[name="preferred_dates"]').val(),
|
||||
message: $inquiryForm.find('[name="message"]').val()
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: yachtBookingData.apiUrl + '/inquiries',
|
||||
method: 'POST',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader('X-WP-Nonce', yachtBookingData.nonce);
|
||||
},
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(formData),
|
||||
success: function(response) {
|
||||
$response.html('<div class="booking-success" style="padding: 12px; background: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 4px; margin-top: 12px;">' +
|
||||
'<strong>' + (yachtBookingData.i18n.successTitle || 'Sukces!') + '</strong> ' +
|
||||
(response.message || yachtBookingData.i18n.inquirySuccess || 'Twoje zapytanie zostało wysłane.') +
|
||||
'</div>');
|
||||
|
||||
$inquiryForm[0].reset();
|
||||
},
|
||||
error: function(xhr) {
|
||||
var errorMessage = yachtBookingData.i18n.errorMessage || 'Wystąpił błąd. Spróbuj ponownie.';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
|
||||
$response.html('<div class="booking-error" style="padding: 12px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px; margin-top: 12px;">' +
|
||||
'<strong>' + (yachtBookingData.i18n.errorTitle || 'Błąd!') + '</strong> ' +
|
||||
errorMessage +
|
||||
'</div>');
|
||||
},
|
||||
complete: function() {
|
||||
$submitBtn.prop('disabled', false).text(originalBtnText);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
if ($form.length) {
|
||||
$form.on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const $submitBtn = $form.find('.yacht-booking-submit');
|
||||
const $response = $form.find('.yacht-booking-response');
|
||||
const originalBtnText = $submitBtn.text();
|
||||
|
||||
// Disable submit button
|
||||
$submitBtn.prop('disabled', true).text(yachtBookingData.i18n.submitting || 'Wysyłanie...');
|
||||
$response.html('');
|
||||
|
||||
// Serialize form data
|
||||
const formData = {
|
||||
yacht_id: state.currentYachtId,
|
||||
start_date: $form.find('[name="start_date"]').val(),
|
||||
end_date: $form.find('[name="end_date"]').val(),
|
||||
customer_name: $form.find('[name="customer_name"]').val(),
|
||||
customer_email: $form.find('[name="customer_email"]').val(),
|
||||
customer_phone: $form.find('[name="customer_phone"]').val()
|
||||
};
|
||||
|
||||
// Submit booking via REST API
|
||||
$.ajax({
|
||||
url: yachtBookingData.apiUrl + '/bookings',
|
||||
method: 'POST',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader('X-WP-Nonce', yachtBookingData.nonce);
|
||||
},
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(formData),
|
||||
success: function(response) {
|
||||
// Show success message
|
||||
$response.html('<div class="booking-success" style="padding: 15px; background: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 4px; margin-top: 15px;">' +
|
||||
'<strong>' + (yachtBookingData.i18n.successTitle || 'Sukces!') + '</strong> ' +
|
||||
(yachtBookingData.i18n.successMessage || 'Twoja rezerwacja została wysłana. Skontaktujemy się z Tobą wkrótce.') +
|
||||
'</div>');
|
||||
|
||||
// Reset form
|
||||
$form[0].reset();
|
||||
|
||||
// Hide form after 2 seconds
|
||||
setTimeout(function() {
|
||||
$formContainer.slideUp(300);
|
||||
}, 2000);
|
||||
|
||||
// Unselect dates
|
||||
calendar.unselect();
|
||||
|
||||
// Refresh calendar to show new booking
|
||||
calendar.refetchEvents();
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
let errorMessage = yachtBookingData.i18n.errorMessage || 'Wystąpił błąd podczas wysyłania rezerwacji. Spróbuj ponownie.';
|
||||
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
}
|
||||
|
||||
$response.html('<div class="booking-error" style="padding: 15px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px; margin-top: 15px;">' +
|
||||
'<strong>' + (yachtBookingData.i18n.errorTitle || 'Błąd!') + '</strong> ' +
|
||||
errorMessage +
|
||||
'</div>');
|
||||
|
||||
console.error('Booking error:', error);
|
||||
},
|
||||
complete: function() {
|
||||
// Re-enable submit button
|
||||
$submitBtn.prop('disabled', false).text(originalBtnText);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date to YYYY-MM-DD
|
||||
*/
|
||||
function formatDate(date) {
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return year + '-' + month + '-' + day;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtract one day from date string
|
||||
*/
|
||||
function subtractDay(dateString) {
|
||||
const d = new Date(dateString);
|
||||
d.setDate(d.getDate() - 1);
|
||||
return formatDate(d);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return $('<div>').text(value || '').html();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize on document ready
|
||||
*/
|
||||
$(document).ready(function() {
|
||||
$('.yacht-calendar-wrapper').each(function() {
|
||||
initYachtCalendar($(this));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialize on Elementor frontend init
|
||||
*/
|
||||
$(window).on('elementor/frontend/init', function() {
|
||||
elementorFrontend.hooks.addAction('frontend/element_ready/yacht-calendar.default', function($scope) {
|
||||
const $wrapper = $scope.find('.yacht-calendar-wrapper');
|
||||
if ($wrapper.length) {
|
||||
initYachtCalendar($wrapper);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
@@ -0,0 +1,594 @@
|
||||
<?php
|
||||
/**
|
||||
* Yacht Calendar Widget for Elementor
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use Elementor\Controls_Manager;
|
||||
use Elementor\Widget_Base;
|
||||
|
||||
/**
|
||||
* Yacht Calendar Widget Class
|
||||
*/
|
||||
class Calendar_Widget extends Widget_Base {
|
||||
|
||||
/**
|
||||
* Get yachts for quick switcher.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function get_yacht_switcher_items() {
|
||||
$yachts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
$items = array();
|
||||
|
||||
foreach ( $yachts as $yacht ) {
|
||||
$items[] = array(
|
||||
'id' => (int) $yacht->ID,
|
||||
'title' => get_the_title( $yacht ),
|
||||
'description' => wp_kses_post( wpautop( $yacht->post_content ) ),
|
||||
);
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render quick yacht switcher.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $yachts Yacht items.
|
||||
* @param int $active_id Active yacht ID.
|
||||
* @return void
|
||||
*/
|
||||
private function render_yacht_switcher( $yachts, $active_id ) {
|
||||
$other_yachts = array_filter(
|
||||
$yachts,
|
||||
static function( $yacht_item ) use ( $active_id ) {
|
||||
return (int) $yacht_item['id'] !== (int) $active_id;
|
||||
}
|
||||
);
|
||||
|
||||
if ( empty( $other_yachts ) ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="yacht-calendar-switcher" aria-label="<?php esc_attr_e( 'Szybki wybór jachtu', 'yacht-booking' ); ?>">
|
||||
<span class="yacht-calendar-switcher-label"><?php esc_html_e( 'Zobacz też:', 'yacht-booking' ); ?></span>
|
||||
<div class="yacht-calendar-switcher-buttons">
|
||||
<?php foreach ( $other_yachts as $yacht_item ) : ?>
|
||||
<button type="button"
|
||||
class="yacht-calendar-switcher-button"
|
||||
data-yacht-id="<?php echo esc_attr( $yacht_item['id'] ); ?>"
|
||||
aria-pressed="false">
|
||||
<?php echo esc_html( $yacht_item['title'] ); ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get widget name
|
||||
*
|
||||
* @return string Widget name.
|
||||
*/
|
||||
public function get_name() {
|
||||
return 'yacht-calendar';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get widget title
|
||||
*
|
||||
* @return string Widget title.
|
||||
*/
|
||||
public function get_title() {
|
||||
return esc_html__( 'Kalendarz Jachtu', 'yacht-booking' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get widget icon
|
||||
*
|
||||
* @return string Widget icon.
|
||||
*/
|
||||
public function get_icon() {
|
||||
return 'eicon-calendar';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get widget categories
|
||||
*
|
||||
* @return array Widget categories.
|
||||
*/
|
||||
public function get_categories() {
|
||||
return array( 'basic' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get widget keywords
|
||||
*
|
||||
* @return array Widget keywords.
|
||||
*/
|
||||
public function get_keywords() {
|
||||
return array( 'yacht', 'calendar', 'booking', 'kalendarz', 'rezerwacja' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register widget controls
|
||||
*/
|
||||
protected function register_controls() {
|
||||
|
||||
// Content Section - Yacht Selection
|
||||
$this->start_controls_section(
|
||||
'content_section',
|
||||
array(
|
||||
'label' => esc_html__( 'Ustawienia Kalendarza', 'yacht-booking' ),
|
||||
'tab' => Controls_Manager::TAB_CONTENT,
|
||||
)
|
||||
);
|
||||
|
||||
// Yacht selector
|
||||
$yachts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
$yacht_options = array();
|
||||
foreach ( $yachts as $yacht_id ) {
|
||||
$yacht_options[ $yacht_id ] = get_the_title( $yacht_id );
|
||||
}
|
||||
|
||||
if ( empty( $yacht_options ) ) {
|
||||
$yacht_options[0] = esc_html__( 'Brak jachtów - dodaj pierwszy jacht w panelu admin', 'yacht-booking' );
|
||||
}
|
||||
|
||||
$this->add_control(
|
||||
'yacht_id',
|
||||
array(
|
||||
'label' => esc_html__( 'Wybierz Jacht', 'yacht-booking' ),
|
||||
'type' => Controls_Manager::SELECT,
|
||||
'options' => $yacht_options,
|
||||
'default' => array_key_first( $yacht_options ),
|
||||
'label_block' => true,
|
||||
)
|
||||
);
|
||||
|
||||
$this->add_control(
|
||||
'show_form',
|
||||
array(
|
||||
'label' => esc_html__( 'Pokaż formularz rezerwacji', 'yacht-booking' ),
|
||||
'type' => Controls_Manager::SWITCHER,
|
||||
'label_on' => esc_html__( 'Tak', 'yacht-booking' ),
|
||||
'label_off' => esc_html__( 'Nie', 'yacht-booking' ),
|
||||
'return_value' => 'yes',
|
||||
'default' => 'yes',
|
||||
)
|
||||
);
|
||||
|
||||
$this->add_control(
|
||||
'calendar_height',
|
||||
array(
|
||||
'label' => esc_html__( 'Wysokość kalendarza', 'yacht-booking' ),
|
||||
'type' => Controls_Manager::SLIDER,
|
||||
'range' => array(
|
||||
'px' => array(
|
||||
'min' => 400,
|
||||
'max' => 1000,
|
||||
),
|
||||
),
|
||||
'default' => array(
|
||||
'size' => 600,
|
||||
'unit' => 'px',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$this->end_controls_section();
|
||||
|
||||
// Style Section
|
||||
$this->start_controls_section(
|
||||
'style_section',
|
||||
array(
|
||||
'label' => esc_html__( 'Styl', 'yacht-booking' ),
|
||||
'tab' => Controls_Manager::TAB_STYLE,
|
||||
)
|
||||
);
|
||||
|
||||
$this->add_control(
|
||||
'primary_color',
|
||||
array(
|
||||
'label' => esc_html__( 'Kolor główny', 'yacht-booking' ),
|
||||
'type' => Controls_Manager::COLOR,
|
||||
'default' => '#2271b1',
|
||||
)
|
||||
);
|
||||
|
||||
$this->add_control(
|
||||
'available_color',
|
||||
array(
|
||||
'label' => esc_html__( 'Kolor dni dostępnych', 'yacht-booking' ),
|
||||
'type' => Controls_Manager::COLOR,
|
||||
'default' => '#35b56a',
|
||||
)
|
||||
);
|
||||
|
||||
$this->add_control(
|
||||
'booked_color',
|
||||
array(
|
||||
'label' => esc_html__( 'Kolor dni zajętych', 'yacht-booking' ),
|
||||
'type' => Controls_Manager::COLOR,
|
||||
'default' => '#e53935',
|
||||
)
|
||||
);
|
||||
|
||||
$this->end_controls_section();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render widget output on the frontend
|
||||
*/
|
||||
protected function render() {
|
||||
$settings = $this->get_settings_for_display();
|
||||
|
||||
$yacht_id = ! empty( $settings['yacht_id'] ) ? (int) $settings['yacht_id'] : 0;
|
||||
|
||||
if ( ! $yacht_id ) {
|
||||
if ( \Elementor\Plugin::$instance->editor->is_edit_mode() ) {
|
||||
echo '<div class="yacht-calendar-notice" style="padding: 20px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px;">';
|
||||
echo '<p style="margin: 0;"><strong>' . esc_html__( 'Uwaga:', 'yacht-booking' ) . '</strong> ';
|
||||
echo esc_html__( 'Najpierw dodaj jacht w panelu administratora.', 'yacht-booking' ) . '</p>';
|
||||
echo '</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
$yacht = get_post( $yacht_id );
|
||||
$yacht_switcher = $this->get_yacht_switcher_items();
|
||||
$booking_enabled = Settings::is_booking_enabled();
|
||||
$show_form = $booking_enabled && 'yes' === $settings['show_form'];
|
||||
$calendar_id = 'yacht-calendar-' . $this->get_id();
|
||||
$raw_height = ! empty( $settings['calendar_height']['size'] ) ? (int) $settings['calendar_height']['size'] : 600;
|
||||
$height = $raw_height;
|
||||
$primary_color = ! empty( $settings['primary_color'] ) ? $settings['primary_color'] : '#2271b1';
|
||||
$available_bg = ! empty( $settings['available_color'] ) ? $settings['available_color'] : '#35b56a';
|
||||
$booked_bg = ! empty( $settings['booked_color'] ) ? $settings['booked_color'] : '#e53935';
|
||||
$terms_url = Settings::get_terms_page_url();
|
||||
|
||||
?>
|
||||
<div class="yacht-calendar-wrapper<?php echo $booking_enabled ? '' : ' yacht-calendar-view-only'; ?>"
|
||||
data-yacht-id="<?php echo esc_attr( $yacht_id ); ?>"
|
||||
data-calendar-id="<?php echo esc_attr( $calendar_id ); ?>"
|
||||
data-primary-color="<?php echo esc_attr( $primary_color ); ?>"
|
||||
data-available-bg="<?php echo esc_attr( $available_bg ); ?>"
|
||||
data-booked-bg="<?php echo esc_attr( $booked_bg ); ?>"
|
||||
data-yachts="<?php echo esc_attr( wp_json_encode( $yacht_switcher ) ); ?>"
|
||||
data-booking-enabled="<?php echo $booking_enabled ? '1' : '0'; ?>">
|
||||
|
||||
<?php $this->render_yacht_switcher( $yacht_switcher, $yacht_id ); ?>
|
||||
|
||||
<div class="yacht-calendar-header">
|
||||
<h3 class="yacht-calendar-title">
|
||||
<?php if ( $booking_enabled ) : ?>
|
||||
<?php
|
||||
/* translators: %s: yacht name */
|
||||
printf( esc_html__( 'Rezerwacja: %s', 'yacht-booking' ), esc_html( $yacht->post_title ) );
|
||||
?>
|
||||
<?php else : ?>
|
||||
<?php
|
||||
/* translators: %s: yacht name */
|
||||
printf( esc_html__( 'Dostępność: %s', 'yacht-booking' ), esc_html( $yacht->post_title ) );
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</h3>
|
||||
<div class="yacht-calendar-description"<?php echo $yacht->post_content ? '' : ' style="display: none;"'; ?>>
|
||||
<?php echo wp_kses_post( wpautop( $yacht->post_content ) ); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yacht-calendar-instructions">
|
||||
<p>
|
||||
<?php if ( $booking_enabled ) : ?>
|
||||
<strong><?php esc_html_e( 'Instrukcja:', 'yacht-booking' ); ?></strong>
|
||||
<?php esc_html_e( 'Zielone dni są dostępne, czerwone są zajęte lub zablokowane. Wybierz termin, przeciągając myszą po dostępnych dniach.', 'yacht-booking' ); ?>
|
||||
<?php else : ?>
|
||||
<?php esc_html_e( 'Zielone dni są dostępne, czerwone są zajęte lub zablokowane. Aby zarezerwować jacht, skontaktuj się z nami telefonicznie lub mailowo.', 'yacht-booking' ); ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="yacht-calendar-legend" aria-label="<?php esc_attr_e( 'Legenda kalendarza rezerwacji', 'yacht-booking' ); ?>">
|
||||
<span class="yacht-legend-item">
|
||||
<span class="yacht-legend-swatch" style="background-color: <?php echo esc_attr( $available_bg ); ?>;"></span>
|
||||
<?php esc_html_e( 'Dostępny', 'yacht-booking' ); ?>
|
||||
</span>
|
||||
<span class="yacht-legend-item">
|
||||
<span class="yacht-legend-swatch" style="background-color: <?php echo esc_attr( $booked_bg ); ?>;"></span>
|
||||
<?php esc_html_e( 'Zajęty / zablokowany', 'yacht-booking' ); ?>
|
||||
</span>
|
||||
<span class="yacht-legend-item">
|
||||
<span class="yacht-legend-swatch yacht-legend-swatch-past"></span>
|
||||
<?php esc_html_e( 'Data przeszła', 'yacht-booking' ); ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<?php if ( ! $booking_enabled ) : ?>
|
||||
<div class="yacht-inquiry-layout">
|
||||
<div class="yacht-inquiry-calendar-col">
|
||||
<div id="<?php echo esc_attr( $calendar_id ); ?>"
|
||||
class="yacht-calendar"
|
||||
style="height: <?php echo esc_attr( $height ); ?>px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="yacht-inquiry-form-col">
|
||||
<div class="yacht-inquiry-form-container">
|
||||
<h4><?php esc_html_e( 'Zapytaj o rezerwację', 'yacht-booking' ); ?></h4>
|
||||
<p class="yacht-inquiry-desc">
|
||||
<?php esc_html_e( 'Wypełnij formularz, a skontaktujemy się z Tobą w sprawie rezerwacji.', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
<form class="yacht-inquiry-form" data-yacht-id="<?php echo esc_attr( $yacht_id ); ?>">
|
||||
<?php wp_nonce_field( 'yacht_inquiry_submit', 'yacht_inquiry_nonce' ); ?>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="inquiry_name_<?php echo esc_attr( $this->get_id() ); ?>">
|
||||
<?php esc_html_e( 'Imię i nazwisko', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="inquiry_name_<?php echo esc_attr( $this->get_id() ); ?>"
|
||||
name="customer_name"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="inquiry_email_<?php echo esc_attr( $this->get_id() ); ?>">
|
||||
<?php esc_html_e( 'Email', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="email"
|
||||
id="inquiry_email_<?php echo esc_attr( $this->get_id() ); ?>"
|
||||
name="customer_email"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="inquiry_phone_<?php echo esc_attr( $this->get_id() ); ?>">
|
||||
<?php esc_html_e( 'Telefon', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="tel"
|
||||
id="inquiry_phone_<?php echo esc_attr( $this->get_id() ); ?>"
|
||||
name="customer_phone"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="inquiry_dates_<?php echo esc_attr( $this->get_id() ); ?>">
|
||||
<?php esc_html_e( 'Preferowane terminy', 'yacht-booking' ); ?>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="inquiry_dates_<?php echo esc_attr( $this->get_id() ); ?>"
|
||||
name="preferred_dates"
|
||||
placeholder="<?php esc_attr_e( 'np. 15-22 lipca', 'yacht-booking' ); ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="inquiry_message_<?php echo esc_attr( $this->get_id() ); ?>">
|
||||
<?php esc_html_e( 'Wiadomość', 'yacht-booking' ); ?>
|
||||
</label>
|
||||
<textarea id="inquiry_message_<?php echo esc_attr( $this->get_id() ); ?>"
|
||||
name="message"
|
||||
rows="3"
|
||||
placeholder="<?php esc_attr_e( 'Dodatkowe pytania lub uwagi...', 'yacht-booking' ); ?>"></textarea>
|
||||
</div>
|
||||
|
||||
<?php if ( $terms_url ) : ?>
|
||||
<p class="booking-terms">
|
||||
<?php
|
||||
printf(
|
||||
wp_kses_post( __( 'Wysyłając formularz akceptujesz %s.', 'yacht-booking' ) ),
|
||||
'<a href="' . esc_url( $terms_url ) . '" target="_blank" rel="noopener">' . esc_html__( 'regulamin', 'yacht-booking' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="yacht-booking-submit">
|
||||
<?php esc_html_e( 'Wyślij zapytanie', 'yacht-booking' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="yacht-inquiry-response"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div id="<?php echo esc_attr( $calendar_id ); ?>"
|
||||
class="yacht-calendar"
|
||||
style="height: <?php echo esc_attr( $height ); ?>px;">
|
||||
</div>
|
||||
|
||||
<?php if ( $show_form ) : ?>
|
||||
<div class="yacht-booking-form-container" style="display: none;">
|
||||
<h4><?php esc_html_e( 'Formularz Rezerwacji', 'yacht-booking' ); ?></h4>
|
||||
<form class="yacht-booking-form" data-yacht-id="<?php echo esc_attr( $yacht_id ); ?>">
|
||||
<?php wp_nonce_field( 'yacht_booking_submit', 'yacht_booking_nonce' ); ?>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label for="booking_start_date_<?php echo esc_attr( $this->get_id() ); ?>">
|
||||
<?php esc_html_e( 'Data rozpoczęcia', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="booking_start_date_<?php echo esc_attr( $this->get_id() ); ?>"
|
||||
name="start_date"
|
||||
class="booking-start-date"
|
||||
readonly
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="booking_end_date_<?php echo esc_attr( $this->get_id() ); ?>">
|
||||
<?php esc_html_e( 'Data zakończenia', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="booking_end_date_<?php echo esc_attr( $this->get_id() ); ?>"
|
||||
name="end_date"
|
||||
class="booking-end-date"
|
||||
readonly
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="booking_name_<?php echo esc_attr( $this->get_id() ); ?>">
|
||||
<?php esc_html_e( 'Imię i nazwisko', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="booking_name_<?php echo esc_attr( $this->get_id() ); ?>"
|
||||
name="customer_name"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="booking_email_<?php echo esc_attr( $this->get_id() ); ?>">
|
||||
<?php esc_html_e( 'Email', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="email"
|
||||
id="booking_email_<?php echo esc_attr( $this->get_id() ); ?>"
|
||||
name="customer_email"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="booking_phone_<?php echo esc_attr( $this->get_id() ); ?>">
|
||||
<?php esc_html_e( 'Telefon', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="tel"
|
||||
id="booking_phone_<?php echo esc_attr( $this->get_id() ); ?>"
|
||||
name="customer_phone"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<?php if ( $terms_url ) : ?>
|
||||
<p class="booking-terms">
|
||||
<?php
|
||||
printf(
|
||||
wp_kses_post( __( 'Wysyłając formularz akceptujesz %s.', 'yacht-booking' ) ),
|
||||
'<a href="' . esc_url( $terms_url ) . '" target="_blank" rel="noopener">' . esc_html__( 'regulamin', 'yacht-booking' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="yacht-booking-submit">
|
||||
<?php esc_html_e( 'Wyślij rezerwację', 'yacht-booking' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="yacht-booking-response"></div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render widget output in the editor (Elementor preview)
|
||||
*/
|
||||
protected function content_template() {
|
||||
?>
|
||||
<#
|
||||
var yachtId = settings.yacht_id;
|
||||
var calendarId = 'yacht-calendar-preview-' + view.getID();
|
||||
var height = settings.calendar_height.size || 600;
|
||||
var showForm = 'yes' === settings.show_form;
|
||||
#>
|
||||
|
||||
<div class="yacht-calendar-wrapper"
|
||||
data-yacht-id="{{ yachtId }}"
|
||||
data-calendar-id="{{ calendarId }}"
|
||||
data-primary-color="{{ settings.primary_color }}"
|
||||
data-available-bg="{{ settings.available_color }}"
|
||||
data-booked-bg="{{ settings.booked_color }}">
|
||||
|
||||
<div class="yacht-calendar-switcher">
|
||||
<span class="yacht-calendar-switcher-label">
|
||||
<?php esc_html_e( 'Szybki wybór jachtu:', 'yacht-booking' ); ?>
|
||||
</span>
|
||||
<div class="yacht-calendar-switcher-buttons">
|
||||
<button type="button" class="yacht-calendar-switcher-button is-active">
|
||||
<?php esc_html_e( 'Aktualnie wybrany jacht', 'yacht-booking' ); ?>
|
||||
</button>
|
||||
<button type="button" class="yacht-calendar-switcher-button">
|
||||
<?php esc_html_e( 'Inny jacht', 'yacht-booking' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="yacht-calendar-header">
|
||||
<h3 class="yacht-calendar-title">
|
||||
<?php esc_html_e( 'Kalendarz Rezerwacji Jachtu', 'yacht-booking' ); ?>
|
||||
</h3>
|
||||
<p class="yacht-calendar-notice" style="background: #e3f2fd; padding: 10px; border-radius: 4px;">
|
||||
<strong><?php esc_html_e( 'Podgląd Elementor:', 'yacht-booking' ); ?></strong>
|
||||
<?php esc_html_e( 'Kalendarz będzie widoczny na stronie frontu.', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="yacht-calendar-instructions">
|
||||
<p>
|
||||
<strong><?php esc_html_e( 'Instrukcja:', 'yacht-booking' ); ?></strong>
|
||||
<?php esc_html_e( 'Wybierz termin przeciągając po zielonych dniach. Czerwone dni są niedostępne.', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="yacht-calendar-legend">
|
||||
<span class="yacht-legend-item">
|
||||
<span class="yacht-legend-swatch" style="background-color: #35b56a;"></span>
|
||||
<?php esc_html_e( 'Dostępny', 'yacht-booking' ); ?>
|
||||
</span>
|
||||
<span class="yacht-legend-item">
|
||||
<span class="yacht-legend-swatch" style="background-color: #e53935;"></span>
|
||||
<?php esc_html_e( 'Zajęty / zablokowany', 'yacht-booking' ); ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="{{ calendarId }}"
|
||||
class="yacht-calendar"
|
||||
style="height: {{ height }}px; background: #f5f5f5; display: flex; align-items: center; justify-content: center;">
|
||||
<p style="color: #666;">
|
||||
<?php esc_html_e( 'Kalendarz FullCalendar zostanie załadowany na stronie frontu', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<# if ( showForm ) { #>
|
||||
<div class="yacht-booking-form-container">
|
||||
<h4><?php esc_html_e( 'Formularz Rezerwacji', 'yacht-booking' ); ?></h4>
|
||||
<p style="background: #fff3cd; padding: 10px; border-radius: 4px;">
|
||||
<?php esc_html_e( 'Formularz rezerwacji będzie aktywny na stronie frontu po wybraniu dat w kalendarzu.', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
<# } #>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,413 @@
|
||||
<?php
|
||||
/**
|
||||
* Shortcode Handler
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcode class - handles [yacht_calendar] shortcode
|
||||
*/
|
||||
class Shortcode {
|
||||
|
||||
/**
|
||||
* Single instance
|
||||
*
|
||||
* @var Shortcode
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*
|
||||
* @return Shortcode
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
add_shortcode( 'yacht_calendar', array( $this, 'render_calendar' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get yachts for quick switcher.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function get_yacht_switcher_items() {
|
||||
$yachts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
$items = array();
|
||||
|
||||
foreach ( $yachts as $yacht ) {
|
||||
$items[] = array(
|
||||
'id' => (int) $yacht->ID,
|
||||
'title' => get_the_title( $yacht ),
|
||||
'description' => wp_kses_post( wpautop( $yacht->post_content ) ),
|
||||
);
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render quick yacht switcher.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $yachts Yacht items.
|
||||
* @param int $active_id Active yacht ID.
|
||||
* @return void
|
||||
*/
|
||||
private function render_yacht_switcher( $yachts, $active_id ) {
|
||||
$other_yachts = array_filter(
|
||||
$yachts,
|
||||
static function( $yacht_item ) use ( $active_id ) {
|
||||
return (int) $yacht_item['id'] !== (int) $active_id;
|
||||
}
|
||||
);
|
||||
|
||||
if ( empty( $other_yachts ) ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="yacht-calendar-switcher" aria-label="<?php esc_attr_e( 'Szybki wybór jachtu', 'yacht-booking' ); ?>">
|
||||
<span class="yacht-calendar-switcher-label"><?php esc_html_e( 'Zobacz też:', 'yacht-booking' ); ?></span>
|
||||
<div class="yacht-calendar-switcher-buttons">
|
||||
<?php foreach ( $other_yachts as $yacht_item ) : ?>
|
||||
<button type="button"
|
||||
class="yacht-calendar-switcher-button"
|
||||
data-yacht-id="<?php echo esc_attr( $yacht_item['id'] ); ?>"
|
||||
aria-pressed="false">
|
||||
<?php echo esc_html( $yacht_item['title'] ); ?>
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render calendar shortcode
|
||||
*
|
||||
* Usage: [yacht_calendar yacht_id="1" show_form="yes" height="600"]
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string HTML output.
|
||||
*/
|
||||
public function render_calendar( $atts ) {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'yacht_id' => 0,
|
||||
'show_form' => 'yes',
|
||||
'height' => 600,
|
||||
'primary_color' => '#2271b1',
|
||||
'available_color' => '#35b56a',
|
||||
'booked_color' => '#e53935',
|
||||
),
|
||||
$atts,
|
||||
'yacht_calendar'
|
||||
);
|
||||
|
||||
$yacht_id = (int) $atts['yacht_id'];
|
||||
|
||||
// If no yacht_id provided, get first yacht
|
||||
if ( ! $yacht_id ) {
|
||||
$yachts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'posts_per_page' => 1,
|
||||
'orderby' => 'ID',
|
||||
'order' => 'ASC',
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
if ( empty( $yachts ) ) {
|
||||
return '<div class="yacht-calendar-error" style="padding: 20px; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;">' .
|
||||
'<p><strong>' . esc_html__( 'Błąd:', 'yacht-booking' ) . '</strong> ' .
|
||||
esc_html__( 'Brak jachtów w systemie. Dodaj przynajmniej jeden jacht w panelu administratora.', 'yacht-booking' ) .
|
||||
'</p></div>';
|
||||
}
|
||||
|
||||
$yacht_id = $yachts[0];
|
||||
}
|
||||
|
||||
$yacht = get_post( $yacht_id );
|
||||
|
||||
if ( ! $yacht || 'yacht' !== $yacht->post_type ) {
|
||||
return '<div class="yacht-calendar-error" style="padding: 20px; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;">' .
|
||||
'<p><strong>' . esc_html__( 'Błąd:', 'yacht-booking' ) . '</strong> ' .
|
||||
esc_html__( 'Nie znaleziono jachtu o podanym ID.', 'yacht-booking' ) .
|
||||
'</p></div>';
|
||||
}
|
||||
|
||||
$booking_enabled = Settings::is_booking_enabled();
|
||||
$yacht_switcher = $this->get_yacht_switcher_items();
|
||||
$show_form = $booking_enabled && 'yes' === strtolower( $atts['show_form'] );
|
||||
$height = (int) $atts['height'];
|
||||
$primary_color = sanitize_hex_color( $atts['primary_color'] );
|
||||
$available_bg = sanitize_hex_color( $atts['available_color'] );
|
||||
$booked_bg = sanitize_hex_color( $atts['booked_color'] );
|
||||
$calendar_id = 'yacht-calendar-' . wp_rand( 1000, 9999 );
|
||||
$terms_url = Settings::get_terms_page_url();
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="yacht-calendar-wrapper<?php echo $booking_enabled ? '' : ' yacht-calendar-view-only'; ?>"
|
||||
data-yacht-id="<?php echo esc_attr( $yacht_id ); ?>"
|
||||
data-calendar-id="<?php echo esc_attr( $calendar_id ); ?>"
|
||||
data-primary-color="<?php echo esc_attr( $primary_color ); ?>"
|
||||
data-available-bg="<?php echo esc_attr( $available_bg ); ?>"
|
||||
data-booked-bg="<?php echo esc_attr( $booked_bg ); ?>"
|
||||
data-yachts="<?php echo esc_attr( wp_json_encode( $yacht_switcher ) ); ?>"
|
||||
data-booking-enabled="<?php echo $booking_enabled ? '1' : '0'; ?>">
|
||||
|
||||
<?php $this->render_yacht_switcher( $yacht_switcher, $yacht_id ); ?>
|
||||
|
||||
<div class="yacht-calendar-header">
|
||||
<h3 class="yacht-calendar-title">
|
||||
<?php if ( $booking_enabled ) : ?>
|
||||
<?php
|
||||
/* translators: %s: yacht name */
|
||||
printf( esc_html__( 'Rezerwacja: %s', 'yacht-booking' ), esc_html( $yacht->post_title ) );
|
||||
?>
|
||||
<?php else : ?>
|
||||
<?php
|
||||
/* translators: %s: yacht name */
|
||||
printf( esc_html__( 'Dostępność: %s', 'yacht-booking' ), esc_html( $yacht->post_title ) );
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</h3>
|
||||
<div class="yacht-calendar-description"<?php echo $yacht->post_content ? '' : ' style="display: none;"'; ?>>
|
||||
<?php echo wp_kses_post( wpautop( $yacht->post_content ) ); ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="yacht-calendar-instructions">
|
||||
<p>
|
||||
<?php if ( $booking_enabled ) : ?>
|
||||
<strong><?php esc_html_e( 'Instrukcja:', 'yacht-booking' ); ?></strong>
|
||||
<?php esc_html_e( 'Zielone dni są dostępne, czerwone są zajęte lub zablokowane. Wybierz termin, przeciągając myszą po dostępnych dniach.', 'yacht-booking' ); ?>
|
||||
<?php else : ?>
|
||||
<?php esc_html_e( 'Zielone dni są dostępne, czerwone są zajęte lub zablokowane. Aby zarezerwować jacht, skontaktuj się z nami telefonicznie lub mailowo.', 'yacht-booking' ); ?>
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="yacht-calendar-legend" aria-label="<?php esc_attr_e( 'Legenda kalendarza rezerwacji', 'yacht-booking' ); ?>">
|
||||
<span class="yacht-legend-item">
|
||||
<span class="yacht-legend-swatch" style="background-color: <?php echo esc_attr( $available_bg ); ?>;"></span>
|
||||
<?php esc_html_e( 'Dostępny', 'yacht-booking' ); ?>
|
||||
</span>
|
||||
<span class="yacht-legend-item">
|
||||
<span class="yacht-legend-swatch" style="background-color: <?php echo esc_attr( $booked_bg ); ?>;"></span>
|
||||
<?php esc_html_e( 'Zajęty / zablokowany', 'yacht-booking' ); ?>
|
||||
</span>
|
||||
<span class="yacht-legend-item">
|
||||
<span class="yacht-legend-swatch yacht-legend-swatch-past"></span>
|
||||
<?php esc_html_e( 'Data przeszła', 'yacht-booking' ); ?>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<?php if ( ! $booking_enabled ) : ?>
|
||||
<div class="yacht-inquiry-layout">
|
||||
<div class="yacht-inquiry-calendar-col">
|
||||
<div id="<?php echo esc_attr( $calendar_id ); ?>"
|
||||
class="yacht-calendar"
|
||||
style="height: <?php echo esc_attr( $height ); ?>px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="yacht-inquiry-form-col">
|
||||
<div class="yacht-inquiry-form-container">
|
||||
<h4><?php esc_html_e( 'Zapytaj o rezerwację', 'yacht-booking' ); ?></h4>
|
||||
<p class="yacht-inquiry-desc">
|
||||
<?php esc_html_e( 'Wypełnij formularz, a skontaktujemy się z Tobą w sprawie rezerwacji.', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
<form class="yacht-inquiry-form" data-yacht-id="<?php echo esc_attr( $yacht_id ); ?>">
|
||||
<?php wp_nonce_field( 'yacht_inquiry_submit', 'yacht_inquiry_nonce' ); ?>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="inquiry_name_<?php echo esc_attr( $calendar_id ); ?>">
|
||||
<?php esc_html_e( 'Imię i nazwisko', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="inquiry_name_<?php echo esc_attr( $calendar_id ); ?>"
|
||||
name="customer_name"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="inquiry_email_<?php echo esc_attr( $calendar_id ); ?>">
|
||||
<?php esc_html_e( 'Email', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="email"
|
||||
id="inquiry_email_<?php echo esc_attr( $calendar_id ); ?>"
|
||||
name="customer_email"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="inquiry_phone_<?php echo esc_attr( $calendar_id ); ?>">
|
||||
<?php esc_html_e( 'Telefon', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="tel"
|
||||
id="inquiry_phone_<?php echo esc_attr( $calendar_id ); ?>"
|
||||
name="customer_phone"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="inquiry_dates_<?php echo esc_attr( $calendar_id ); ?>">
|
||||
<?php esc_html_e( 'Preferowane terminy', 'yacht-booking' ); ?>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="inquiry_dates_<?php echo esc_attr( $calendar_id ); ?>"
|
||||
name="preferred_dates"
|
||||
placeholder="<?php esc_attr_e( 'np. 15-22 lipca', 'yacht-booking' ); ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="inquiry_message_<?php echo esc_attr( $calendar_id ); ?>">
|
||||
<?php esc_html_e( 'Wiadomość', 'yacht-booking' ); ?>
|
||||
</label>
|
||||
<textarea id="inquiry_message_<?php echo esc_attr( $calendar_id ); ?>"
|
||||
name="message"
|
||||
rows="3"
|
||||
placeholder="<?php esc_attr_e( 'Dodatkowe pytania lub uwagi...', 'yacht-booking' ); ?>"></textarea>
|
||||
</div>
|
||||
|
||||
<?php if ( $terms_url ) : ?>
|
||||
<p class="booking-terms">
|
||||
<?php
|
||||
printf(
|
||||
wp_kses_post( __( 'Wysyłając formularz akceptujesz %s.', 'yacht-booking' ) ),
|
||||
'<a href="' . esc_url( $terms_url ) . '" target="_blank" rel="noopener">' . esc_html__( 'regulamin', 'yacht-booking' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="yacht-booking-submit">
|
||||
<?php esc_html_e( 'Wyślij zapytanie', 'yacht-booking' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="yacht-inquiry-response"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div id="<?php echo esc_attr( $calendar_id ); ?>"
|
||||
class="yacht-calendar"
|
||||
style="height: <?php echo esc_attr( $height ); ?>px;">
|
||||
</div>
|
||||
|
||||
<?php if ( $show_form ) : ?>
|
||||
<div class="yacht-booking-form-container" style="display: none;">
|
||||
<h4><?php esc_html_e( 'Formularz Rezerwacji', 'yacht-booking' ); ?></h4>
|
||||
<form class="yacht-booking-form" data-yacht-id="<?php echo esc_attr( $yacht_id ); ?>">
|
||||
<?php wp_nonce_field( 'yacht_booking_submit', 'yacht_booking_nonce' ); ?>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label for="booking_start_date_<?php echo esc_attr( $calendar_id ); ?>">
|
||||
<?php esc_html_e( 'Data rozpoczęcia', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="booking_start_date_<?php echo esc_attr( $calendar_id ); ?>"
|
||||
name="start_date"
|
||||
class="booking-start-date"
|
||||
readonly
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="booking_end_date_<?php echo esc_attr( $calendar_id ); ?>">
|
||||
<?php esc_html_e( 'Data zakończenia', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="booking_end_date_<?php echo esc_attr( $calendar_id ); ?>"
|
||||
name="end_date"
|
||||
class="booking-end-date"
|
||||
readonly
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="booking_name_<?php echo esc_attr( $calendar_id ); ?>">
|
||||
<?php esc_html_e( 'Imię i nazwisko', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="booking_name_<?php echo esc_attr( $calendar_id ); ?>"
|
||||
name="customer_name"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="booking_email_<?php echo esc_attr( $calendar_id ); ?>">
|
||||
<?php esc_html_e( 'Email', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="email"
|
||||
id="booking_email_<?php echo esc_attr( $calendar_id ); ?>"
|
||||
name="customer_email"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="booking_phone_<?php echo esc_attr( $calendar_id ); ?>">
|
||||
<?php esc_html_e( 'Telefon', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="tel"
|
||||
id="booking_phone_<?php echo esc_attr( $calendar_id ); ?>"
|
||||
name="customer_phone"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<?php if ( $terms_url ) : ?>
|
||||
<p class="booking-terms">
|
||||
<?php
|
||||
printf(
|
||||
wp_kses_post( __( 'Wysyłając formularz akceptujesz %s.', 'yacht-booking' ) ),
|
||||
'<a href="' . esc_url( $terms_url ) . '" target="_blank" rel="noopener">' . esc_html__( 'regulamin', 'yacht-booking' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="yacht-booking-submit">
|
||||
<?php esc_html_e( 'Wyślij rezerwację', 'yacht-booking' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="yacht-booking-response"></div>
|
||||
</form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
/**
|
||||
* Availability Checker Class
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Availability class - handles yacht availability checking and caching
|
||||
*/
|
||||
class Availability {
|
||||
|
||||
/**
|
||||
* Check if yacht is available for given date range
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @param string $start_date Start date (Y-m-d).
|
||||
* @param string $end_date End date (Y-m-d).
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_available( $yacht_id, $start_date, $end_date ) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'yacht_availability';
|
||||
|
||||
// Get all dates in range that are not available
|
||||
$unavailable = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM $table
|
||||
WHERE yacht_id = %d
|
||||
AND date >= %s
|
||||
AND date < %s
|
||||
AND status != 'available'",
|
||||
$yacht_id,
|
||||
$start_date,
|
||||
$end_date
|
||||
)
|
||||
);
|
||||
|
||||
return 0 === (int) $unavailable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark dates as booked
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @param string $start_date Start date (Y-m-d).
|
||||
* @param string $end_date End date (Y-m-d).
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function mark_as_booked( $yacht_id, $start_date, $end_date, $booking_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'yacht_availability';
|
||||
$dates = self::get_date_range( $start_date, $end_date );
|
||||
|
||||
foreach ( $dates as $date ) {
|
||||
$wpdb->replace(
|
||||
$table,
|
||||
array(
|
||||
'yacht_id' => $yacht_id,
|
||||
'date' => $date,
|
||||
'status' => 'booked',
|
||||
'booking_id' => $booking_id,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'updated_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%s', '%d', '%s', '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark dates as blocked (from Google Calendar sync)
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @param string $start_date Start date (Y-m-d).
|
||||
* @param string $end_date End date (Y-m-d).
|
||||
* @return bool
|
||||
*/
|
||||
public static function mark_as_blocked( $yacht_id, $start_date, $end_date ) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'yacht_availability';
|
||||
$dates = self::get_date_range( $start_date, $end_date );
|
||||
|
||||
foreach ( $dates as $date ) {
|
||||
$wpdb->replace(
|
||||
$table,
|
||||
array(
|
||||
'yacht_id' => $yacht_id,
|
||||
'date' => $date,
|
||||
'status' => 'blocked',
|
||||
'booking_id' => null,
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'updated_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( '%d', '%s', '%s', '%d', '%s', '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark dates as available (when booking is cancelled)
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @param string $start_date Start date (Y-m-d).
|
||||
* @param string $end_date End date (Y-m-d).
|
||||
* @return bool
|
||||
*/
|
||||
public static function mark_as_available( $yacht_id, $start_date, $end_date ) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'yacht_availability';
|
||||
|
||||
$wpdb->delete(
|
||||
$table,
|
||||
array(
|
||||
'yacht_id' => $yacht_id,
|
||||
),
|
||||
array( '%d' )
|
||||
);
|
||||
|
||||
// Alternative: update to available instead of delete
|
||||
// $dates = self::get_date_range( $start_date, $end_date );
|
||||
// foreach ( $dates as $date ) {
|
||||
// $wpdb->update(
|
||||
// $table,
|
||||
// array( 'status' => 'available', 'booking_id' => null ),
|
||||
// array( 'yacht_id' => $yacht_id, 'date' => $date ),
|
||||
// array( '%s', '%d' ),
|
||||
// array( '%d', '%s' )
|
||||
// );
|
||||
// }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear availability for booking
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function clear_booking_availability( $booking_id ) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'yacht_availability';
|
||||
|
||||
$wpdb->delete(
|
||||
$table,
|
||||
array( 'booking_id' => $booking_id ),
|
||||
array( '%d' )
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get availability calendar for yacht and month
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @param string $start Start date (Y-m-d).
|
||||
* @param string $end End date (Y-m-d).
|
||||
* @return array
|
||||
*/
|
||||
public static function get_availability_calendar( $yacht_id, $start, $end ) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . 'yacht_availability';
|
||||
|
||||
$results = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT date, status, booking_id FROM $table
|
||||
WHERE yacht_id = %d
|
||||
AND date >= %s
|
||||
AND date <= %s
|
||||
ORDER BY date ASC",
|
||||
$yacht_id,
|
||||
$start,
|
||||
$end
|
||||
),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
// Create associative array with date as key
|
||||
$calendar = array();
|
||||
foreach ( $results as $row ) {
|
||||
$calendar[ $row['date'] ] = array(
|
||||
'status' => $row['status'],
|
||||
'booking_id' => $row['booking_id'],
|
||||
);
|
||||
}
|
||||
|
||||
// Fill in missing dates as available
|
||||
$all_dates = self::get_date_range( $start, $end );
|
||||
foreach ( $all_dates as $date ) {
|
||||
if ( ! isset( $calendar[ $date ] ) ) {
|
||||
$calendar[ $date ] = array(
|
||||
'status' => 'available',
|
||||
'booking_id' => null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $calendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count days between two dates
|
||||
*
|
||||
* @param string $start_date Start date (Y-m-d).
|
||||
* @param string $end_date End date (Y-m-d).
|
||||
* @return int
|
||||
*/
|
||||
public static function count_days( $start_date, $end_date ) {
|
||||
$start = new \DateTime( $start_date );
|
||||
$end = new \DateTime( $end_date );
|
||||
$diff = $start->diff( $end );
|
||||
|
||||
return (int) $diff->days;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of dates between start and end (exclusive of end date)
|
||||
*
|
||||
* @param string $start_date Start date (Y-m-d).
|
||||
* @param string $end_date End date (Y-m-d).
|
||||
* @return array
|
||||
*/
|
||||
private static function get_date_range( $start_date, $end_date ) {
|
||||
$dates = array();
|
||||
$start = new \DateTime( $start_date );
|
||||
$end = new \DateTime( $end_date );
|
||||
|
||||
while ( $start < $end ) {
|
||||
$dates[] = $start->format( 'Y-m-d' );
|
||||
$start->modify( '+1 day' );
|
||||
}
|
||||
|
||||
return $dates;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
/**
|
||||
* Booking Custom Post Type
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Booking CPT class
|
||||
*/
|
||||
class Booking {
|
||||
|
||||
/**
|
||||
* Register custom post type
|
||||
*/
|
||||
public static function register() {
|
||||
$labels = array(
|
||||
'name' => __( 'Rezerwacje', 'yacht-booking' ),
|
||||
'singular_name' => __( 'Rezerwacja', 'yacht-booking' ),
|
||||
'menu_name' => __( 'Rezerwacje', 'yacht-booking' ),
|
||||
'name_admin_bar' => __( 'Rezerwacja', 'yacht-booking' ),
|
||||
'add_new' => __( 'Dodaj nową', 'yacht-booking' ),
|
||||
'add_new_item' => __( 'Dodaj nową rezerwację', 'yacht-booking' ),
|
||||
'new_item' => __( 'Nowa rezerwacja', 'yacht-booking' ),
|
||||
'edit_item' => __( 'Edytuj rezerwację', 'yacht-booking' ),
|
||||
'view_item' => __( 'Zobacz rezerwację', 'yacht-booking' ),
|
||||
'all_items' => __( 'Wszystkie rezerwacje', 'yacht-booking' ),
|
||||
'search_items' => __( 'Szukaj rezerwacji', 'yacht-booking' ),
|
||||
'parent_item_colon' => __( 'Nadrzędna rezerwacja:', 'yacht-booking' ),
|
||||
'not_found' => __( 'Nie znaleziono rezerwacji', 'yacht-booking' ),
|
||||
'not_found_in_trash' => __( 'Nie znaleziono rezerwacji w koszu', 'yacht-booking' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'description' => __( 'Rezerwacje jachtów', 'yacht-booking' ),
|
||||
'public' => false,
|
||||
'publicly_queryable' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Custom menu będzie w class-admin.php
|
||||
'show_in_rest' => true,
|
||||
'query_var' => true,
|
||||
'rewrite' => false,
|
||||
'capability_type' => 'post',
|
||||
'capabilities' => array(
|
||||
'edit_post' => 'yacht_booking_manage_bookings',
|
||||
'read_post' => 'yacht_booking_manage_bookings',
|
||||
'delete_post' => 'yacht_booking_manage_bookings',
|
||||
'edit_posts' => 'yacht_booking_manage_bookings',
|
||||
'edit_others_posts' => 'yacht_booking_manage_bookings',
|
||||
'publish_posts' => 'yacht_booking_manage_bookings',
|
||||
'read_private_posts' => 'yacht_booking_manage_bookings',
|
||||
'delete_posts' => 'yacht_booking_manage_bookings',
|
||||
),
|
||||
'has_archive' => false,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'supports' => array( 'title', 'custom-fields' ),
|
||||
);
|
||||
|
||||
register_post_type( 'yacht_booking', $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get booking yacht ID
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return int
|
||||
*/
|
||||
public static function get_yacht_id( $booking_id ) {
|
||||
return (int) get_post_meta( $booking_id, '_booking_yacht_id', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get booking start date
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_start_date( $booking_id ) {
|
||||
return get_post_meta( $booking_id, '_booking_start_date', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get booking end date
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_end_date( $booking_id ) {
|
||||
return get_post_meta( $booking_id, '_booking_end_date', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get booking status
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_status( $booking_id ) {
|
||||
$status = get_post_meta( $booking_id, '_booking_status', true );
|
||||
return $status ? $status : 'pending';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer name
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_customer_name( $booking_id ) {
|
||||
return get_post_meta( $booking_id, '_booking_customer_name', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer email
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_customer_email( $booking_id ) {
|
||||
return get_post_meta( $booking_id, '_booking_customer_email', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer phone
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_customer_phone( $booking_id ) {
|
||||
return get_post_meta( $booking_id, '_booking_customer_phone', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total price
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return float
|
||||
*/
|
||||
public static function get_total_price( $booking_id ) {
|
||||
return (float) get_post_meta( $booking_id, '_booking_total_price', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Google Calendar Event ID
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_gcal_event_id( $booking_id ) {
|
||||
return get_post_meta( $booking_id, '_booking_gcal_event_id', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get admin notes
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_notes( $booking_id ) {
|
||||
return get_post_meta( $booking_id, '_booking_notes', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update booking status
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @param string $status Status (pending, confirmed, cancelled).
|
||||
*/
|
||||
public static function update_status( $booking_id, $status ) {
|
||||
$valid_statuses = array( 'pending', 'confirmed', 'cancelled' );
|
||||
|
||||
if ( in_array( $status, $valid_statuses, true ) ) {
|
||||
update_post_meta( $booking_id, '_booking_status', $status );
|
||||
|
||||
// Trigger action hook
|
||||
do_action( 'yacht_booking_status_changed', $booking_id, $status );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new booking
|
||||
*
|
||||
* @param array $data Booking data.
|
||||
* @return int|false Booking ID on success, false on failure.
|
||||
*/
|
||||
public static function create( $data ) {
|
||||
// Validate required fields
|
||||
$required = array( 'yacht_id', 'start_date', 'end_date', 'customer_name', 'customer_email', 'customer_phone' );
|
||||
foreach ( $required as $field ) {
|
||||
if ( empty( $data[ $field ] ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get yacht title
|
||||
$yacht = get_post( $data['yacht_id'] );
|
||||
if ( ! $yacht || $yacht->post_type !== 'yacht' ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create booking post
|
||||
$booking_id = wp_insert_post(
|
||||
array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'post_title' => sprintf(
|
||||
__( 'Rezerwacja #%s - %s', 'yacht-booking' ),
|
||||
time(),
|
||||
$yacht->post_title
|
||||
),
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $booking_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save booking meta
|
||||
update_post_meta( $booking_id, '_booking_yacht_id', (int) $data['yacht_id'] );
|
||||
update_post_meta( $booking_id, '_booking_start_date', sanitize_text_field( $data['start_date'] ) );
|
||||
update_post_meta( $booking_id, '_booking_end_date', sanitize_text_field( $data['end_date'] ) );
|
||||
update_post_meta( $booking_id, '_booking_status', ! empty( $data['status'] ) ? $data['status'] : 'pending' );
|
||||
update_post_meta( $booking_id, '_booking_customer_name', sanitize_text_field( $data['customer_name'] ) );
|
||||
update_post_meta( $booking_id, '_booking_customer_email', sanitize_email( $data['customer_email'] ) );
|
||||
update_post_meta( $booking_id, '_booking_customer_phone', sanitize_text_field( $data['customer_phone'] ) );
|
||||
|
||||
if ( ! empty( $data['total_price'] ) ) {
|
||||
update_post_meta( $booking_id, '_booking_total_price', (float) $data['total_price'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $data['notes'] ) ) {
|
||||
update_post_meta( $booking_id, '_booking_notes', wp_kses_post( $data['notes'] ) );
|
||||
}
|
||||
|
||||
// Trigger action hook
|
||||
do_action( 'yacht_booking_created', $booking_id );
|
||||
|
||||
return $booking_id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
/**
|
||||
* Email templates helper.
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Email templates class.
|
||||
*/
|
||||
class Email_Templates {
|
||||
|
||||
/**
|
||||
* Option key.
|
||||
*/
|
||||
const OPTION_KEY = 'yacht_booking_email_templates';
|
||||
|
||||
/**
|
||||
* Template type: admin notification.
|
||||
*/
|
||||
const TYPE_ADMIN_NEW_BOOKING = 'admin_new_booking';
|
||||
|
||||
/**
|
||||
* Template type: customer confirmed.
|
||||
*/
|
||||
const TYPE_CUSTOMER_CONFIRMED = 'customer_confirmed';
|
||||
|
||||
/**
|
||||
* Template type: customer cancelled.
|
||||
*/
|
||||
const TYPE_CUSTOMER_CANCELLED = 'customer_cancelled';
|
||||
|
||||
/**
|
||||
* Get template labels.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_template_labels() {
|
||||
return array(
|
||||
self::TYPE_ADMIN_NEW_BOOKING => __( 'Nowa rezerwacja (email do admina)', 'yacht-booking' ),
|
||||
self::TYPE_CUSTOMER_CONFIRMED => __( 'Potwierdzenie rezerwacji (email do klienta)', 'yacht-booking' ),
|
||||
self::TYPE_CUSTOMER_CANCELLED => __( 'Anulowanie rezerwacji (email do klienta)', 'yacht-booking' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available tags for templates.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_available_tags() {
|
||||
return array(
|
||||
'{booking_id}' => __( 'ID rezerwacji', 'yacht-booking' ),
|
||||
'{yacht_name}' => __( 'Nazwa jachtu', 'yacht-booking' ),
|
||||
'{customer_name}' => __( 'Imię i nazwisko klienta', 'yacht-booking' ),
|
||||
'{customer_email}' => __( 'Email klienta', 'yacht-booking' ),
|
||||
'{customer_phone}' => __( 'Telefon klienta', 'yacht-booking' ),
|
||||
'{start_date}' => __( 'Data rozpoczęcia', 'yacht-booking' ),
|
||||
'{end_date}' => __( 'Data zakończenia', 'yacht-booking' ),
|
||||
'{days}' => __( 'Liczba dni', 'yacht-booking' ),
|
||||
'{total_price}' => __( 'Cena całkowita', 'yacht-booking' ),
|
||||
'{status}' => __( 'Status rezerwacji', 'yacht-booking' ),
|
||||
'{admin_link}' => __( 'Link do panelu admin', 'yacht-booking' ),
|
||||
'{site_name}' => __( 'Nazwa strony', 'yacht-booking' ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default templates.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_default_templates() {
|
||||
return array(
|
||||
self::TYPE_ADMIN_NEW_BOOKING => array(
|
||||
'subject' => __( '[Nowa Rezerwacja] #{booking_id} - {site_name}', 'yacht-booking' ),
|
||||
'body' => __(
|
||||
'<p>Otrzymano nową rezerwację jachtu.</p>
|
||||
<p><strong>Szczegóły rezerwacji:</strong><br>
|
||||
Numer rezerwacji: #{booking_id}<br>
|
||||
Jacht: {yacht_name}<br>
|
||||
Data rozpoczęcia: {start_date}<br>
|
||||
Data zakończenia: {end_date}<br>
|
||||
Liczba dni: {days}<br>
|
||||
Cena całkowita: {total_price}<br>
|
||||
Status: {status}</p>
|
||||
<p><strong>Dane klienta:</strong><br>
|
||||
Imię i nazwisko: {customer_name}<br>
|
||||
Email: {customer_email}<br>
|
||||
Telefon: {customer_phone}</p>
|
||||
<p><a href="{admin_link}">Przejdź do zarządzania rezerwacją</a></p>',
|
||||
'yacht-booking'
|
||||
),
|
||||
),
|
||||
self::TYPE_CUSTOMER_CONFIRMED => array(
|
||||
'subject' => __( '[Potwierdzenie] Rezerwacja #{booking_id} - {site_name}', 'yacht-booking' ),
|
||||
'body' => __(
|
||||
'<p>Witaj {customer_name},</p>
|
||||
<p>Twoja rezerwacja jachtu została potwierdzona.</p>
|
||||
<p><strong>Szczegóły rezerwacji:</strong><br>
|
||||
Numer rezerwacji: #{booking_id}<br>
|
||||
Jacht: {yacht_name}<br>
|
||||
Data rozpoczęcia: {start_date}<br>
|
||||
Data zakończenia: {end_date}<br>
|
||||
Liczba dni: {days}<br>
|
||||
Cena całkowita: {total_price}</p>
|
||||
<p>Dziękujemy za wybranie naszych usług.</p>',
|
||||
'yacht-booking'
|
||||
),
|
||||
),
|
||||
self::TYPE_CUSTOMER_CANCELLED => array(
|
||||
'subject' => __( '[Anulowanie] Rezerwacja #{booking_id} - {site_name}', 'yacht-booking' ),
|
||||
'body' => __(
|
||||
'<p>Witaj {customer_name},</p>
|
||||
<p>Niestety Twoja rezerwacja jachtu została anulowana.</p>
|
||||
<p><strong>Szczegóły rezerwacji:</strong><br>
|
||||
Numer rezerwacji: #{booking_id}<br>
|
||||
Jacht: {yacht_name}<br>
|
||||
Data rozpoczęcia: {start_date}<br>
|
||||
Data zakończenia: {end_date}</p>
|
||||
<p>W razie pytań prosimy o kontakt.</p>',
|
||||
'yacht-booking'
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all templates.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_templates() {
|
||||
$defaults = self::get_default_templates();
|
||||
$saved = get_option( self::OPTION_KEY, array() );
|
||||
|
||||
if ( ! is_array( $saved ) ) {
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
$merged = array();
|
||||
foreach ( $defaults as $type => $template ) {
|
||||
$merged[ $type ] = array(
|
||||
'subject' => isset( $saved[ $type ]['subject'] ) && '' !== trim( (string) $saved[ $type ]['subject'] )
|
||||
? sanitize_text_field( $saved[ $type ]['subject'] )
|
||||
: $template['subject'],
|
||||
'body' => isset( $saved[ $type ]['body'] ) && '' !== trim( (string) $saved[ $type ]['body'] )
|
||||
? wp_kses_post( $saved[ $type ]['body'] )
|
||||
: $template['body'],
|
||||
);
|
||||
}
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save templates.
|
||||
*
|
||||
* @param array $templates Templates.
|
||||
* @return bool
|
||||
*/
|
||||
public static function save_templates( $templates ) {
|
||||
$defaults = self::get_default_templates();
|
||||
$normalized = array();
|
||||
|
||||
foreach ( $defaults as $type => $default_template ) {
|
||||
$subject = isset( $templates[ $type ]['subject'] ) ? sanitize_text_field( wp_unslash( $templates[ $type ]['subject'] ) ) : '';
|
||||
$body = isset( $templates[ $type ]['body'] ) ? wp_kses_post( wp_unslash( $templates[ $type ]['body'] ) ) : '';
|
||||
|
||||
$normalized[ $type ] = array(
|
||||
'subject' => '' !== trim( $subject ) ? $subject : $default_template['subject'],
|
||||
'body' => '' !== trim( $body ) ? $body : $default_template['body'],
|
||||
);
|
||||
}
|
||||
|
||||
return update_option( self::OPTION_KEY, $normalized );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default templates.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function reset_templates() {
|
||||
return delete_option( self::OPTION_KEY );
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile template with replacement tags.
|
||||
*
|
||||
* @param string $type Template type.
|
||||
* @param array $data Data for replacements.
|
||||
* @return array
|
||||
*/
|
||||
public static function compile( $type, $data = array() ) {
|
||||
$templates = self::get_templates();
|
||||
|
||||
if ( ! isset( $templates[ $type ] ) ) {
|
||||
return array(
|
||||
'subject' => '',
|
||||
'body' => '',
|
||||
);
|
||||
}
|
||||
|
||||
$replacements = self::build_replacements( $data );
|
||||
|
||||
return array(
|
||||
'subject' => strtr( $templates[ $type ]['subject'], $replacements ),
|
||||
'body' => strtr( $templates[ $type ]['body'], $replacements ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build replacement map.
|
||||
*
|
||||
* @param array $data Raw data.
|
||||
* @return array
|
||||
*/
|
||||
private static function build_replacements( $data ) {
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
|
||||
return array(
|
||||
'{booking_id}' => isset( $data['booking_id'] ) ? (string) $data['booking_id'] : '',
|
||||
'{yacht_name}' => isset( $data['yacht_name'] ) ? (string) $data['yacht_name'] : '',
|
||||
'{customer_name}' => isset( $data['customer_name'] ) ? (string) $data['customer_name'] : '',
|
||||
'{customer_email}' => isset( $data['customer_email'] ) ? (string) $data['customer_email'] : '',
|
||||
'{customer_phone}' => isset( $data['customer_phone'] ) ? (string) $data['customer_phone'] : '',
|
||||
'{start_date}' => isset( $data['start_date'] ) ? (string) $data['start_date'] : '',
|
||||
'{end_date}' => isset( $data['end_date'] ) ? (string) $data['end_date'] : '',
|
||||
'{days}' => isset( $data['days'] ) ? (string) $data['days'] : '',
|
||||
'{total_price}' => isset( $data['total_price'] ) ? (string) $data['total_price'] : '',
|
||||
'{status}' => isset( $data['status'] ) ? (string) $data['status'] : '',
|
||||
'{admin_link}' => isset( $data['admin_link'] ) ? (string) $data['admin_link'] : '',
|
||||
'{site_name}' => $site_name,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build booking data for template replacements.
|
||||
*
|
||||
* @param int $booking_id Booking ID.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_booking_template_data( $booking_id ) {
|
||||
$booking_id = absint( $booking_id );
|
||||
$yacht_id = Booking::get_yacht_id( $booking_id );
|
||||
$yacht = get_post( $yacht_id );
|
||||
|
||||
$start_date = Booking::get_start_date( $booking_id );
|
||||
$end_date = Booking::get_end_date( $booking_id );
|
||||
$status = Booking::get_status( $booking_id );
|
||||
$days = Availability::count_days( $start_date, $end_date );
|
||||
|
||||
return array(
|
||||
'booking_id' => $booking_id,
|
||||
'yacht_name' => $yacht ? $yacht->post_title : __( 'Nieznany', 'yacht-booking' ),
|
||||
'customer_name' => Booking::get_customer_name( $booking_id ),
|
||||
'customer_email' => Booking::get_customer_email( $booking_id ),
|
||||
'customer_phone' => Booking::get_customer_phone( $booking_id ),
|
||||
'start_date' => Settings::format_date( $start_date ),
|
||||
'end_date' => Settings::format_date( $end_date ),
|
||||
'days' => $days,
|
||||
'total_price' => Settings::format_price( Booking::get_total_price( $booking_id ) ),
|
||||
'status' => self::get_status_label( $status ),
|
||||
'admin_link' => admin_url( 'admin.php?page=yacht-bookings-list&booking_id=' . $booking_id ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get localized status label.
|
||||
*
|
||||
* @param string $status Status.
|
||||
* @return string
|
||||
*/
|
||||
private static function get_status_label( $status ) {
|
||||
$labels = array(
|
||||
'pending' => __( 'Oczekująca', 'yacht-booking' ),
|
||||
'confirmed' => __( 'Potwierdzona', 'yacht-booking' ),
|
||||
'cancelled' => __( 'Anulowana', 'yacht-booking' ),
|
||||
);
|
||||
|
||||
return isset( $labels[ $status ] ) ? $labels[ $status ] : $status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
/**
|
||||
* Inquiry Custom Post Type
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inquiry CPT class
|
||||
*/
|
||||
class Inquiry {
|
||||
|
||||
/**
|
||||
* Register custom post type
|
||||
*/
|
||||
public static function register() {
|
||||
register_post_type(
|
||||
'yacht_inquiry',
|
||||
array(
|
||||
'labels' => array(
|
||||
'name' => __( 'Zapytania', 'yacht-booking' ),
|
||||
'singular_name' => __( 'Zapytanie', 'yacht-booking' ),
|
||||
),
|
||||
'public' => false,
|
||||
'publicly_queryable' => false,
|
||||
'show_ui' => false,
|
||||
'show_in_menu' => false,
|
||||
'show_in_rest' => false,
|
||||
'capability_type' => 'post',
|
||||
'capabilities' => array(
|
||||
'edit_post' => 'yacht_booking_manage_bookings',
|
||||
'read_post' => 'yacht_booking_manage_bookings',
|
||||
'delete_post' => 'yacht_booking_manage_bookings',
|
||||
'edit_posts' => 'yacht_booking_manage_bookings',
|
||||
'edit_others_posts' => 'yacht_booking_manage_bookings',
|
||||
'publish_posts' => 'yacht_booking_manage_bookings',
|
||||
'read_private_posts' => 'yacht_booking_manage_bookings',
|
||||
'delete_posts' => 'yacht_booking_manage_bookings',
|
||||
),
|
||||
'has_archive' => false,
|
||||
'supports' => array( 'title', 'custom-fields' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get yacht ID.
|
||||
*
|
||||
* @param int $inquiry_id Inquiry post ID.
|
||||
* @return int
|
||||
*/
|
||||
public static function get_yacht_id( $inquiry_id ) {
|
||||
return (int) get_post_meta( $inquiry_id, '_inquiry_yacht_id', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer name.
|
||||
*
|
||||
* @param int $inquiry_id Inquiry post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_customer_name( $inquiry_id ) {
|
||||
return get_post_meta( $inquiry_id, '_inquiry_customer_name', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer email.
|
||||
*
|
||||
* @param int $inquiry_id Inquiry post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_customer_email( $inquiry_id ) {
|
||||
return get_post_meta( $inquiry_id, '_inquiry_customer_email', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get customer phone.
|
||||
*
|
||||
* @param int $inquiry_id Inquiry post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_customer_phone( $inquiry_id ) {
|
||||
return get_post_meta( $inquiry_id, '_inquiry_customer_phone', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get message.
|
||||
*
|
||||
* @param int $inquiry_id Inquiry post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_message( $inquiry_id ) {
|
||||
return get_post_meta( $inquiry_id, '_inquiry_message', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preferred dates.
|
||||
*
|
||||
* @param int $inquiry_id Inquiry post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_preferred_dates( $inquiry_id ) {
|
||||
return get_post_meta( $inquiry_id, '_inquiry_preferred_dates', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email sent to admin body.
|
||||
*
|
||||
* @param int $inquiry_id Inquiry post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_admin_email_body( $inquiry_id ) {
|
||||
return get_post_meta( $inquiry_id, '_inquiry_admin_email_body', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email sent to customer body.
|
||||
*
|
||||
* @param int $inquiry_id Inquiry post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_customer_email_body( $inquiry_id ) {
|
||||
return get_post_meta( $inquiry_id, '_inquiry_customer_email_body', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new inquiry.
|
||||
*
|
||||
* @param array $data Inquiry data.
|
||||
* @return int|false Inquiry ID on success, false on failure.
|
||||
*/
|
||||
public static function create( $data ) {
|
||||
$required = array( 'yacht_id', 'customer_name', 'customer_email', 'customer_phone' );
|
||||
foreach ( $required as $field ) {
|
||||
if ( empty( $data[ $field ] ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$yacht = get_post( $data['yacht_id'] );
|
||||
if ( ! $yacht || 'yacht' !== $yacht->post_type ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$inquiry_id = wp_insert_post(
|
||||
array(
|
||||
'post_type' => 'yacht_inquiry',
|
||||
'post_title' => sprintf(
|
||||
__( 'Zapytanie #%s - %s', 'yacht-booking' ),
|
||||
time(),
|
||||
$yacht->post_title
|
||||
),
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $inquiry_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update_post_meta( $inquiry_id, '_inquiry_yacht_id', (int) $data['yacht_id'] );
|
||||
update_post_meta( $inquiry_id, '_inquiry_customer_name', sanitize_text_field( $data['customer_name'] ) );
|
||||
update_post_meta( $inquiry_id, '_inquiry_customer_email', sanitize_email( $data['customer_email'] ) );
|
||||
update_post_meta( $inquiry_id, '_inquiry_customer_phone', sanitize_text_field( $data['customer_phone'] ) );
|
||||
|
||||
if ( ! empty( $data['message'] ) ) {
|
||||
update_post_meta( $inquiry_id, '_inquiry_message', sanitize_textarea_field( $data['message'] ) );
|
||||
}
|
||||
|
||||
if ( ! empty( $data['preferred_dates'] ) ) {
|
||||
update_post_meta( $inquiry_id, '_inquiry_preferred_dates', sanitize_text_field( $data['preferred_dates'] ) );
|
||||
}
|
||||
|
||||
return $inquiry_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send inquiry emails (admin + customer) and store copies.
|
||||
*
|
||||
* @param int $inquiry_id Inquiry ID.
|
||||
*/
|
||||
public static function send_emails( $inquiry_id ) {
|
||||
$inquiry_id = absint( $inquiry_id );
|
||||
$yacht_id = self::get_yacht_id( $inquiry_id );
|
||||
$yacht = get_post( $yacht_id );
|
||||
$yacht_name = $yacht ? $yacht->post_title : __( 'Nieznany', 'yacht-booking' );
|
||||
|
||||
$customer_name = self::get_customer_name( $inquiry_id );
|
||||
$customer_email = self::get_customer_email( $inquiry_id );
|
||||
$customer_phone = self::get_customer_phone( $inquiry_id );
|
||||
$message = self::get_message( $inquiry_id );
|
||||
$preferred_dates = self::get_preferred_dates( $inquiry_id );
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
$from_name = Settings::get_email_from_name();
|
||||
$from_address = Settings::get_email_from_address();
|
||||
|
||||
$headers = array(
|
||||
'Content-Type: text/html; charset=UTF-8',
|
||||
'From: ' . $from_name . ' <' . $from_address . '>',
|
||||
);
|
||||
|
||||
// --- Admin email ---
|
||||
$admin_subject = sprintf(
|
||||
/* translators: 1: inquiry ID, 2: site name */
|
||||
__( '[Zapytanie o rezerwację] #%1$d - %2$s', 'yacht-booking' ),
|
||||
$inquiry_id,
|
||||
$site_name
|
||||
);
|
||||
|
||||
$admin_body = '<p>' . __( 'Otrzymano nowe zapytanie o rezerwację jachtu.', 'yacht-booking' ) . '</p>';
|
||||
$admin_body .= '<p><strong>' . __( 'Jacht:', 'yacht-booking' ) . '</strong> ' . esc_html( $yacht_name ) . '</p>';
|
||||
if ( $preferred_dates ) {
|
||||
$admin_body .= '<p><strong>' . __( 'Preferowane terminy:', 'yacht-booking' ) . '</strong> ' . esc_html( $preferred_dates ) . '</p>';
|
||||
}
|
||||
$admin_body .= '<p><strong>' . __( 'Dane klienta:', 'yacht-booking' ) . '</strong><br>';
|
||||
$admin_body .= esc_html__( 'Imię i nazwisko:', 'yacht-booking' ) . ' ' . esc_html( $customer_name ) . '<br>';
|
||||
$admin_body .= esc_html__( 'Email:', 'yacht-booking' ) . ' ' . esc_html( $customer_email ) . '<br>';
|
||||
$admin_body .= esc_html__( 'Telefon:', 'yacht-booking' ) . ' ' . esc_html( $customer_phone ) . '</p>';
|
||||
if ( $message ) {
|
||||
$admin_body .= '<p><strong>' . __( 'Wiadomość:', 'yacht-booking' ) . '</strong><br>' . nl2br( esc_html( $message ) ) . '</p>';
|
||||
}
|
||||
$admin_body .= '<p><a href="' . esc_url( admin_url( 'admin.php?page=yacht-inquiries' ) ) . '">'
|
||||
. __( 'Przejdź do listy zapytań', 'yacht-booking' ) . '</a></p>';
|
||||
|
||||
$admin_headers = $headers;
|
||||
$admin_headers[] = 'Reply-To: ' . $customer_name . ' <' . $customer_email . '>';
|
||||
|
||||
wp_mail( get_option( 'admin_email' ), $admin_subject, $admin_body, $admin_headers );
|
||||
update_post_meta( $inquiry_id, '_inquiry_admin_email_body', $admin_body );
|
||||
|
||||
// --- Customer confirmation email ---
|
||||
$customer_subject = sprintf(
|
||||
/* translators: 1: site name */
|
||||
__( 'Potwierdzenie zapytania o rezerwację - %s', 'yacht-booking' ),
|
||||
$site_name
|
||||
);
|
||||
|
||||
$customer_body = '<p>' . sprintf(
|
||||
/* translators: %s: customer name */
|
||||
__( 'Witaj %s,', 'yacht-booking' ),
|
||||
esc_html( $customer_name )
|
||||
) . '</p>';
|
||||
$customer_body .= '<p>' . __( 'Dziękujemy za przesłanie zapytania o rezerwację jachtu. Otrzymaliśmy Twoje zgłoszenie i skontaktujemy się z Tobą najszybciej jak to możliwe.', 'yacht-booking' ) . '</p>';
|
||||
$customer_body .= '<p><strong>' . __( 'Szczegóły zapytania:', 'yacht-booking' ) . '</strong><br>';
|
||||
$customer_body .= esc_html__( 'Jacht:', 'yacht-booking' ) . ' ' . esc_html( $yacht_name ) . '</p>';
|
||||
if ( $preferred_dates ) {
|
||||
$customer_body .= '<p>' . esc_html__( 'Preferowane terminy:', 'yacht-booking' ) . ' ' . esc_html( $preferred_dates ) . '</p>';
|
||||
}
|
||||
if ( $message ) {
|
||||
$customer_body .= '<p>' . esc_html__( 'Twoja wiadomość:', 'yacht-booking' ) . '<br>' . nl2br( esc_html( $message ) ) . '</p>';
|
||||
}
|
||||
$customer_body .= '<p>' . __( 'Pozdrawiamy,', 'yacht-booking' ) . '<br>' . esc_html( $site_name ) . '</p>';
|
||||
|
||||
wp_mail( $customer_email, $customer_subject, $customer_body, $headers );
|
||||
update_post_meta( $inquiry_id, '_inquiry_customer_email_body', $customer_body );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Installer Class
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Installer class - handles plugin installation and database setup
|
||||
*/
|
||||
class Installer {
|
||||
|
||||
/**
|
||||
* Run installation
|
||||
*/
|
||||
public function install() {
|
||||
$this->create_tables();
|
||||
$this->create_options();
|
||||
$this->set_version();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create custom database tables
|
||||
*/
|
||||
private function create_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
$table_name = $wpdb->prefix . 'yacht_availability';
|
||||
|
||||
$sql = "CREATE TABLE $table_name (
|
||||
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
yacht_id bigint(20) UNSIGNED NOT NULL,
|
||||
date date NOT NULL,
|
||||
status varchar(20) NOT NULL DEFAULT 'available',
|
||||
booking_id bigint(20) UNSIGNED NULL,
|
||||
created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY yacht_date (yacht_id, date),
|
||||
KEY yacht_id (yacht_id),
|
||||
KEY date (date),
|
||||
KEY status (status),
|
||||
KEY booking_id (booking_id)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
dbDelta( $sql );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create default plugin options
|
||||
*/
|
||||
private function create_options() {
|
||||
$options = array(
|
||||
'yacht_booking_default_status' => 'pending',
|
||||
'yacht_booking_email_from_name' => get_bloginfo( 'name' ),
|
||||
'yacht_booking_email_from' => get_bloginfo( 'admin_email' ),
|
||||
'yacht_booking_date_format' => 'Y-m-d',
|
||||
'yacht_booking_currency_symbol' => 'zł',
|
||||
'yacht_booking_terms_page_id' => 0,
|
||||
'yacht_booking_enable_notifications' => 'yes',
|
||||
'yacht_booking_gcal_sync_enabled' => 'no',
|
||||
);
|
||||
|
||||
foreach ( $options as $key => $value ) {
|
||||
if ( false === get_option( $key ) ) {
|
||||
add_option( $key, $value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set plugin version
|
||||
*/
|
||||
private function set_version() {
|
||||
update_option( 'yacht_booking_version', YACHT_BOOKING_VERSION );
|
||||
update_option( 'yacht_booking_installed_at', current_time( 'mysql' ) );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin settings helper.
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings helper class.
|
||||
*/
|
||||
class Settings {
|
||||
|
||||
/**
|
||||
* Check if online booking is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_booking_enabled() {
|
||||
return '1' === get_option( 'yacht_booking_enabled', '1' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default booking status.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_default_status() {
|
||||
$status = get_option( 'yacht_booking_default_status', 'pending' );
|
||||
return in_array( $status, array( 'pending', 'confirmed' ), true ) ? $status : 'pending';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email from name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_email_from_name() {
|
||||
$value = get_option( 'yacht_booking_email_from_name', get_bloginfo( 'name' ) );
|
||||
return is_string( $value ) && '' !== $value ? $value : get_bloginfo( 'name' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email from address.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_email_from_address() {
|
||||
$value = get_option( 'yacht_booking_email_from', '' );
|
||||
|
||||
// Backward compatibility with older option name.
|
||||
if ( empty( $value ) ) {
|
||||
$value = get_option( 'yacht_booking_email_from_address', get_option( 'admin_email' ) );
|
||||
}
|
||||
|
||||
return is_email( $value ) ? $value : get_option( 'admin_email' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported date formats.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_supported_date_formats() {
|
||||
return array( 'Y-m-d', 'd/m/Y', 'm/d/Y', 'd.m.Y' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date format.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_date_format() {
|
||||
$format = get_option( 'yacht_booking_date_format', 'Y-m-d' );
|
||||
return in_array( $format, self::get_supported_date_formats(), true ) ? $format : 'Y-m-d';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date using plugin settings.
|
||||
*
|
||||
* @param string $date Date string.
|
||||
* @return string
|
||||
*/
|
||||
public static function format_date( $date ) {
|
||||
$timestamp = strtotime( (string) $date );
|
||||
return false !== $timestamp ? date_i18n( self::get_date_format(), $timestamp ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currency symbol.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_currency_symbol() {
|
||||
$symbol = get_option( 'yacht_booking_currency_symbol', 'zł' );
|
||||
$symbol = is_string( $symbol ) ? trim( $symbol ) : '';
|
||||
return '' !== $symbol ? $symbol : 'zł';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format price using plugin settings.
|
||||
*
|
||||
* @param float $amount Amount.
|
||||
* @return string
|
||||
*/
|
||||
public static function format_price( $amount ) {
|
||||
return trim( number_format_i18n( (float) $amount, 2 ) . ' ' . self::get_currency_symbol() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get terms page ID.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function get_terms_page_id() {
|
||||
return absint( get_option( 'yacht_booking_terms_page_id', 0 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get terms page URL.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_terms_page_url() {
|
||||
$page_id = self::get_terms_page_id();
|
||||
if ( ! $page_id ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$url = get_permalink( $page_id );
|
||||
return $url ? $url : '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
<?php
|
||||
/**
|
||||
* Main Plugin Class
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main Yacht Booking class - Singleton pattern
|
||||
*/
|
||||
class Yacht_Booking {
|
||||
|
||||
/**
|
||||
* Single instance of the class
|
||||
*
|
||||
* @var Yacht_Booking
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*
|
||||
* @return Yacht_Booking
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
$this->init_hooks();
|
||||
$this->load_dependencies();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize WordPress hooks
|
||||
*/
|
||||
private function init_hooks() {
|
||||
// Register Custom Post Types
|
||||
add_action( 'init', array( $this, 'register_post_types' ), 10 );
|
||||
|
||||
// Enqueue scripts and styles
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ), 10 );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ), 10 );
|
||||
|
||||
// Register REST API routes
|
||||
add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 10 );
|
||||
|
||||
// Register Elementor widgets
|
||||
add_action( 'elementor/widgets/register', array( $this, 'register_elementor_widgets' ), 10 );
|
||||
|
||||
// Register shortcodes
|
||||
add_action( 'init', array( $this, 'register_shortcodes' ), 15 );
|
||||
|
||||
// Add custom capabilities
|
||||
add_action( 'admin_init', array( $this, 'add_custom_capabilities' ), 10 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin dependencies
|
||||
*/
|
||||
private function load_dependencies() {
|
||||
// Load CPT handlers
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'includes/class-yacht.php';
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'includes/class-booking.php';
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'includes/class-availability.php';
|
||||
|
||||
// Load admin classes
|
||||
if ( is_admin() ) {
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'admin/class-admin.php';
|
||||
Admin::get_instance();
|
||||
}
|
||||
|
||||
// Load Google Calendar integration
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'integrations/google-calendar/class-oauth-handler.php';
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'integrations/google-calendar/class-gcal-service.php';
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'integrations/google-calendar/class-sync-controller.php';
|
||||
|
||||
// Initialize sync controller
|
||||
\YachtBooking\Integrations\GoogleCalendar\Sync_Controller::get_instance();
|
||||
|
||||
// Register cron actions
|
||||
\YachtBooking\Integrations\GoogleCalendar\Sync_Controller::register_cron_actions();
|
||||
|
||||
// Load iCal integration
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'integrations/ical/class-ical-feed.php';
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'integrations/ical/class-ical-import.php';
|
||||
|
||||
\YachtBooking\Integrations\ICal\ICal_Feed::register();
|
||||
\YachtBooking\Integrations\ICal\ICal_Import::register();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Custom Post Types
|
||||
*/
|
||||
public function register_post_types() {
|
||||
// Register Yacht CPT
|
||||
Yacht::register();
|
||||
|
||||
// Register Booking CPT
|
||||
Booking::register();
|
||||
|
||||
// Register Inquiry CPT
|
||||
Inquiry::register();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue frontend assets
|
||||
*/
|
||||
public function enqueue_frontend_assets() {
|
||||
// Only load on pages with yacht calendar
|
||||
if ( ! $this->should_load_frontend_assets() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// FullCalendar CSS
|
||||
wp_enqueue_style(
|
||||
'fullcalendar',
|
||||
'https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.css',
|
||||
array(),
|
||||
'6.1.10'
|
||||
);
|
||||
|
||||
// Plugin CSS
|
||||
wp_enqueue_style(
|
||||
'yacht-booking-calendar',
|
||||
YACHT_BOOKING_PLUGIN_URL . 'frontend/assets/css/calendar.css',
|
||||
array( 'fullcalendar' ),
|
||||
YACHT_BOOKING_VERSION
|
||||
);
|
||||
|
||||
// FullCalendar JS
|
||||
wp_enqueue_script(
|
||||
'fullcalendar',
|
||||
'https://cdn.jsdelivr.net/npm/fullcalendar@6.1.10/index.global.min.js',
|
||||
array(),
|
||||
'6.1.10',
|
||||
true
|
||||
);
|
||||
|
||||
// FullCalendar Polish locale
|
||||
wp_enqueue_script(
|
||||
'fullcalendar-pl',
|
||||
'https://cdn.jsdelivr.net/npm/@fullcalendar/core@6.1.10/locales/pl.global.min.js',
|
||||
array( 'fullcalendar' ),
|
||||
'6.1.10',
|
||||
true
|
||||
);
|
||||
|
||||
// Plugin JS
|
||||
wp_enqueue_script(
|
||||
'yacht-booking-calendar',
|
||||
YACHT_BOOKING_PLUGIN_URL . 'frontend/assets/js/calendar.js',
|
||||
array( 'jquery', 'fullcalendar', 'fullcalendar-pl' ),
|
||||
YACHT_BOOKING_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
// Localize script
|
||||
wp_localize_script(
|
||||
'yacht-booking-calendar',
|
||||
'yachtBookingData',
|
||||
array(
|
||||
'apiUrl' => esc_url_raw( rest_url( 'yacht-booking/v1' ) ),
|
||||
'nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
'bookingEnabled' => Settings::is_booking_enabled(),
|
||||
'i18n' => array(
|
||||
'loading' => __( 'Ładowanie...', 'yacht-booking' ),
|
||||
'submitBooking' => __( 'Wyślij rezerwację', 'yacht-booking' ),
|
||||
'submitting' => __( 'Wysyłanie...', 'yacht-booking' ),
|
||||
'successTitle' => __( 'Sukces!', 'yacht-booking' ),
|
||||
'successMessage' => __( 'Twoja rezerwacja została wysłana. Skontaktujemy się z Tobą wkrótce.', 'yacht-booking' ),
|
||||
'errorTitle' => __( 'Błąd!', 'yacht-booking' ),
|
||||
'errorMessage' => __( 'Wystąpił błąd podczas wysyłania rezerwacji. Spróbuj ponownie.', 'yacht-booking' ),
|
||||
'selectDates' => __( 'Wybierz daty na kalendarzu', 'yacht-booking' ),
|
||||
'invalidDateRange' => __( 'Nieprawidłowy zakres dat', 'yacht-booking' ),
|
||||
'unavailableDatesSelected' => __( 'Wybrane daty zawierają niedostępne terminy. Proszę wybrać inne daty.', 'yacht-booking' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin assets
|
||||
*/
|
||||
public function enqueue_admin_assets( $hook ) {
|
||||
// Only load on yacht booking pages
|
||||
if ( strpos( $hook, 'yacht-bookings' ) === false ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'yacht-booking-admin',
|
||||
YACHT_BOOKING_PLUGIN_URL . 'admin/assets/css/admin.css',
|
||||
array(),
|
||||
YACHT_BOOKING_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'yacht-booking-admin',
|
||||
YACHT_BOOKING_PLUGIN_URL . 'admin/assets/js/admin.js',
|
||||
array( 'jquery' ),
|
||||
YACHT_BOOKING_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'yacht-booking-admin',
|
||||
'yachtBookingAdmin',
|
||||
array(
|
||||
'apiUrl' => esc_url_raw( rest_url( 'yacht-booking/v1' ) ),
|
||||
'nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if frontend assets should be loaded
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_load_frontend_assets() {
|
||||
global $post;
|
||||
|
||||
// Always load if Elementor is in edit mode
|
||||
if ( class_exists( '\Elementor\Plugin' ) && \Elementor\Plugin::$instance->preview->is_preview_mode() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if post contains yacht calendar shortcode or widget
|
||||
if ( $post && ( has_shortcode( $post->post_content, 'yacht_calendar' ) || $this->has_yacht_calendar_widget( $post->ID ) ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if post has yacht calendar Elementor widget
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return bool
|
||||
*/
|
||||
private function has_yacht_calendar_widget( $post_id ) {
|
||||
if ( ! class_exists( '\Elementor\Plugin' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$document = \Elementor\Plugin::$instance->documents->get( $post_id );
|
||||
if ( ! $document ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $document->get_elements_data();
|
||||
return $this->find_widget_recursive( $data, 'yacht-calendar' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Find widget recursively in Elementor data
|
||||
*
|
||||
* @param array $elements Elements data.
|
||||
* @param string $widget_name Widget name to find.
|
||||
* @return bool
|
||||
*/
|
||||
private function find_widget_recursive( $elements, $widget_name ) {
|
||||
foreach ( $elements as $element ) {
|
||||
if ( isset( $element['widgetType'] ) && $element['widgetType'] === $widget_name ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( ! empty( $element['elements'] ) ) {
|
||||
if ( $this->find_widget_recursive( $element['elements'], $widget_name ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register REST API routes
|
||||
*/
|
||||
public function register_rest_routes() {
|
||||
// Load REST controllers
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'api/class-rest-controller.php';
|
||||
|
||||
$controller = new Rest_Controller();
|
||||
$controller->register_routes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register Elementor widgets
|
||||
*
|
||||
* @param object $widgets_manager Elementor widgets manager.
|
||||
*/
|
||||
public function register_elementor_widgets( $widgets_manager ) {
|
||||
// Load widget class
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'frontend/class-calendar-widget.php';
|
||||
|
||||
// Register widget
|
||||
$widgets_manager->register( new Calendar_Widget() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register shortcodes
|
||||
*/
|
||||
public function register_shortcodes() {
|
||||
// Load shortcode class
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'frontend/class-shortcode.php';
|
||||
|
||||
// Initialize shortcode handler
|
||||
Shortcode::get_instance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom capabilities
|
||||
*/
|
||||
public function add_custom_capabilities() {
|
||||
// Only run once
|
||||
if ( get_option( 'yacht_booking_capabilities_added' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$admin = get_role( 'administrator' );
|
||||
|
||||
if ( $admin ) {
|
||||
$capabilities = array(
|
||||
'yacht_booking_manage_yachts',
|
||||
'yacht_booking_manage_bookings',
|
||||
'yacht_booking_manage_settings',
|
||||
);
|
||||
|
||||
foreach ( $capabilities as $cap ) {
|
||||
$admin->add_cap( $cap );
|
||||
}
|
||||
|
||||
update_option( 'yacht_booking_capabilities_added', true );
|
||||
}
|
||||
}
|
||||
}
|
||||
155
wp-content/plugins/yacht-booking-system/includes/class-yacht.php
Normal file
155
wp-content/plugins/yacht-booking-system/includes/class-yacht.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
/**
|
||||
* Yacht Custom Post Type
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Yacht CPT class
|
||||
*/
|
||||
class Yacht {
|
||||
|
||||
/**
|
||||
* Register custom post type
|
||||
*/
|
||||
public static function register() {
|
||||
$labels = array(
|
||||
'name' => __( 'Jachty', 'yacht-booking' ),
|
||||
'singular_name' => __( 'Jacht', 'yacht-booking' ),
|
||||
'menu_name' => __( 'Jachty', 'yacht-booking' ),
|
||||
'name_admin_bar' => __( 'Jacht', 'yacht-booking' ),
|
||||
'add_new' => __( 'Dodaj nowy', 'yacht-booking' ),
|
||||
'add_new_item' => __( 'Dodaj nowy jacht', 'yacht-booking' ),
|
||||
'new_item' => __( 'Nowy jacht', 'yacht-booking' ),
|
||||
'edit_item' => __( 'Edytuj jacht', 'yacht-booking' ),
|
||||
'view_item' => __( 'Zobacz jacht', 'yacht-booking' ),
|
||||
'all_items' => __( 'Wszystkie jachty', 'yacht-booking' ),
|
||||
'search_items' => __( 'Szukaj jachtów', 'yacht-booking' ),
|
||||
'parent_item_colon' => __( 'Nadrzędny jacht:', 'yacht-booking' ),
|
||||
'not_found' => __( 'Nie znaleziono jachtów', 'yacht-booking' ),
|
||||
'not_found_in_trash' => __( 'Nie znaleziono jachtów w koszu', 'yacht-booking' ),
|
||||
'featured_image' => __( 'Zdjęcie jachtu', 'yacht-booking' ),
|
||||
'set_featured_image' => __( 'Ustaw zdjęcie jachtu', 'yacht-booking' ),
|
||||
'remove_featured_image' => __( 'Usuń zdjęcie jachtu', 'yacht-booking' ),
|
||||
'use_featured_image' => __( 'Użyj jako zdjęcie jachtu', 'yacht-booking' ),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
'labels' => $labels,
|
||||
'description' => __( 'Jachty dostępne do rezerwacji', 'yacht-booking' ),
|
||||
'public' => false,
|
||||
'publicly_queryable' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => false, // Custom menu będzie w class-admin.php
|
||||
'show_in_rest' => true,
|
||||
'query_var' => true,
|
||||
'rewrite' => false,
|
||||
'capability_type' => 'post',
|
||||
'capabilities' => array(
|
||||
'edit_post' => 'yacht_booking_manage_yachts',
|
||||
'read_post' => 'yacht_booking_manage_yachts',
|
||||
'delete_post' => 'yacht_booking_manage_yachts',
|
||||
'edit_posts' => 'yacht_booking_manage_yachts',
|
||||
'edit_others_posts' => 'yacht_booking_manage_yachts',
|
||||
'publish_posts' => 'yacht_booking_manage_yachts',
|
||||
'read_private_posts' => 'yacht_booking_manage_yachts',
|
||||
'delete_posts' => 'yacht_booking_manage_yachts',
|
||||
),
|
||||
'has_archive' => false,
|
||||
'hierarchical' => false,
|
||||
'menu_position' => null,
|
||||
'supports' => array( 'title', 'editor', 'thumbnail', 'custom-fields' ),
|
||||
);
|
||||
|
||||
register_post_type( 'yacht', $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get yacht capacity
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @return int
|
||||
*/
|
||||
public static function get_capacity( $yacht_id ) {
|
||||
return (int) get_post_meta( $yacht_id, '_yacht_capacity', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get yacht price per day
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @return float
|
||||
*/
|
||||
public static function get_price_per_day( $yacht_id ) {
|
||||
return (float) get_post_meta( $yacht_id, '_yacht_price_per_day', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get yacht Google Calendar ID
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_gcal_id( $yacht_id ) {
|
||||
return get_post_meta( $yacht_id, '_yacht_gcal_id', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get yacht features
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_features( $yacht_id ) {
|
||||
$features = get_post_meta( $yacht_id, '_yacht_features', true );
|
||||
return is_array( $features ) ? $features : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update yacht capacity
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @param int $capacity Capacity.
|
||||
*/
|
||||
public static function update_capacity( $yacht_id, $capacity ) {
|
||||
update_post_meta( $yacht_id, '_yacht_capacity', (int) $capacity );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update yacht price per day
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @param float $price Price per day.
|
||||
*/
|
||||
public static function update_price_per_day( $yacht_id, $price ) {
|
||||
update_post_meta( $yacht_id, '_yacht_price_per_day', (float) $price );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update yacht Google Calendar ID
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @param string $gcal_id Google Calendar ID.
|
||||
*/
|
||||
public static function update_gcal_id( $yacht_id, $gcal_id ) {
|
||||
update_post_meta( $yacht_id, '_yacht_gcal_id', sanitize_text_field( $gcal_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update yacht features
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @param array $features Features array.
|
||||
*/
|
||||
public static function update_features( $yacht_id, $features ) {
|
||||
update_post_meta( $yacht_id, '_yacht_features', is_array( $features ) ? $features : array() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,521 @@
|
||||
<?php
|
||||
/**
|
||||
* Google Calendar Service
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking\Integrations\GoogleCalendar;
|
||||
|
||||
use YachtBooking\Booking;
|
||||
use YachtBooking\Availability;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Calendar Service class
|
||||
*/
|
||||
class GCal_Service {
|
||||
|
||||
/**
|
||||
* Google Calendar API base URL
|
||||
*/
|
||||
const API_BASE = 'https://www.googleapis.com/calendar/v3';
|
||||
|
||||
/**
|
||||
* Booking source meta value for imported Google events.
|
||||
*/
|
||||
const EXTERNAL_BOOKING_SOURCE = 'google_calendar_external';
|
||||
|
||||
/**
|
||||
* Create event in Google Calendar
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return string|false Event ID or false on error.
|
||||
*/
|
||||
public static function create_event( $booking_id ) {
|
||||
$access_token = OAuth_Handler::get_access_token();
|
||||
$calendar_id = self::get_calendar_id();
|
||||
|
||||
if ( ! $access_token || ! $calendar_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get booking data
|
||||
$yacht_id = Booking::get_yacht_id( $booking_id );
|
||||
$yacht = get_post( $yacht_id );
|
||||
$start_date = Booking::get_start_date( $booking_id );
|
||||
$end_date = Booking::get_end_date( $booking_id );
|
||||
$customer_name = Booking::get_customer_name( $booking_id );
|
||||
$customer_email = Booking::get_customer_email( $booking_id );
|
||||
$customer_phone = Booking::get_customer_phone( $booking_id );
|
||||
$status = Booking::get_status( $booking_id );
|
||||
|
||||
// Prepare event data
|
||||
$event = array(
|
||||
'summary' => sprintf(
|
||||
/* translators: 1: yacht name, 2: customer name */
|
||||
__( 'Rezerwacja: %1$s - %2$s', 'yacht-booking' ),
|
||||
$yacht ? $yacht->post_title : __( 'Jacht', 'yacht-booking' ),
|
||||
$customer_name
|
||||
),
|
||||
'description' => sprintf(
|
||||
"Booking ID: #%d\n\nKlient: %s\nEmail: %s\nTelefon: %s\nStatus: %s\n\nZarządzanie: %s",
|
||||
$booking_id,
|
||||
$customer_name,
|
||||
$customer_email,
|
||||
$customer_phone,
|
||||
$status,
|
||||
admin_url( 'admin.php?page=yacht-bookings-list&booking_id=' . $booking_id )
|
||||
),
|
||||
'start' => array(
|
||||
'date' => $start_date, // All-day event
|
||||
),
|
||||
'end' => array(
|
||||
'date' => date( 'Y-m-d', strtotime( $end_date . ' +1 day' ) ), // Exclusive end date
|
||||
),
|
||||
'colorId' => 'confirmed' === $status ? '9' : '11', // Blue for confirmed, red for pending
|
||||
);
|
||||
|
||||
$response = wp_remote_post(
|
||||
self::API_BASE . '/calendars/' . urlencode( $calendar_id ) . '/events',
|
||||
array(
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $access_token,
|
||||
'Content-Type' => 'application/json',
|
||||
),
|
||||
'body' => wp_json_encode( $event ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
self::log_error( 'Create event failed: ' . $response->get_error_message() );
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( isset( $body['id'] ) ) {
|
||||
// Save Google Event ID to booking meta
|
||||
update_post_meta( $booking_id, '_gcal_event_id', $body['id'] );
|
||||
return $body['id'];
|
||||
}
|
||||
|
||||
self::log_error( 'Create event failed: ' . wp_remote_retrieve_body( $response ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update event in Google Calendar
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function update_event( $booking_id ) {
|
||||
$access_token = OAuth_Handler::get_access_token();
|
||||
$calendar_id = self::get_calendar_id();
|
||||
$event_id = get_post_meta( $booking_id, '_gcal_event_id', true );
|
||||
|
||||
if ( ! $access_token || ! $calendar_id || ! $event_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get booking data
|
||||
$status = Booking::get_status( $booking_id );
|
||||
|
||||
// Update only color based on status
|
||||
$event = array(
|
||||
'colorId' => 'confirmed' === $status ? '9' : ( 'cancelled' === $status ? '8' : '11' ),
|
||||
);
|
||||
|
||||
$response = wp_remote_request(
|
||||
self::API_BASE . '/calendars/' . urlencode( $calendar_id ) . '/events/' . urlencode( $event_id ),
|
||||
array(
|
||||
'method' => 'PATCH',
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $access_token,
|
||||
'Content-Type' => 'application/json',
|
||||
),
|
||||
'body' => wp_json_encode( $event ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
self::log_error( 'Update event failed: ' . $response->get_error_message() );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete event from Google Calendar
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete_event( $booking_id ) {
|
||||
$access_token = OAuth_Handler::get_access_token();
|
||||
$calendar_id = self::get_calendar_id();
|
||||
$event_id = get_post_meta( $booking_id, '_gcal_event_id', true );
|
||||
|
||||
if ( ! $access_token || ! $calendar_id || ! $event_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = wp_remote_request(
|
||||
self::API_BASE . '/calendars/' . urlencode( $calendar_id ) . '/events/' . urlencode( $event_id ),
|
||||
array(
|
||||
'method' => 'DELETE',
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $access_token,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
self::log_error( 'Delete event failed: ' . $response->get_error_message() );
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove event ID from meta
|
||||
delete_post_meta( $booking_id, '_gcal_event_id' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync events from Google Calendar to WordPress
|
||||
*
|
||||
* @param int $yacht_id Yacht post ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function sync_from_gcal( $yacht_id ) {
|
||||
$access_token = OAuth_Handler::get_access_token();
|
||||
$calendar_id = self::get_calendar_id();
|
||||
|
||||
if ( ! $access_token || ! $calendar_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch events from now to +1 year
|
||||
$time_min = gmdate( 'Y-m-d\TH:i:s\Z' );
|
||||
$time_max = gmdate( 'Y-m-d\TH:i:s\Z', strtotime( '+1 year' ) );
|
||||
|
||||
$response = wp_remote_get(
|
||||
add_query_arg(
|
||||
array(
|
||||
'timeMin' => $time_min,
|
||||
'timeMax' => $time_max,
|
||||
'singleEvents' => 'true',
|
||||
'orderBy' => 'startTime',
|
||||
),
|
||||
self::API_BASE . '/calendars/' . urlencode( $calendar_id ) . '/events'
|
||||
),
|
||||
array(
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $access_token,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
self::log_error( 'Sync from GCal failed: ' . $response->get_error_message() );
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
$events = isset( $body['items'] ) ? $body['items'] : array();
|
||||
|
||||
// Internal bookings (created in WordPress and pushed to Google) should not be re-imported.
|
||||
$internal_event_ids = self::get_internal_booking_event_ids();
|
||||
$external_booking_map = self::get_external_booking_map( $yacht_id );
|
||||
$seen_external_ids = array();
|
||||
|
||||
foreach ( $events as $event ) {
|
||||
if ( empty( $event['id'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip events already linked to internal WordPress bookings.
|
||||
if ( in_array( $event['id'], $internal_event_ids, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$range = self::get_blocked_range_from_event( $event );
|
||||
if ( ! $range ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$booking_id = isset( $external_booking_map[ $event['id'] ] ) ? (int) $external_booking_map[ $event['id'] ] : 0;
|
||||
$booking_id = self::upsert_external_booking( $yacht_id, $event, $range, $booking_id );
|
||||
|
||||
if ( $booking_id ) {
|
||||
$seen_external_ids[] = $event['id'];
|
||||
}
|
||||
}
|
||||
|
||||
// Remove stale imported placeholders (event removed from Google Calendar).
|
||||
foreach ( $external_booking_map as $event_id => $booking_id ) {
|
||||
if ( ! in_array( $event_id, $seen_external_ids, true ) ) {
|
||||
Availability::clear_booking_availability( $booking_id );
|
||||
// Prevent before_delete_post Google delete call for already-removed external events.
|
||||
delete_post_meta( $booking_id, '_gcal_event_id' );
|
||||
wp_delete_post( $booking_id, true );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get calendar ID
|
||||
*
|
||||
* @return string|false Calendar ID or false.
|
||||
*/
|
||||
public static function get_calendar_id() {
|
||||
$calendar_id = get_option( 'yacht_booking_gcal_calendar_id' );
|
||||
|
||||
// Default to primary calendar if not set
|
||||
return $calendar_id ? $calendar_id : 'primary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set calendar ID
|
||||
*
|
||||
* @param string $calendar_id Calendar ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function set_calendar_id( $calendar_id ) {
|
||||
return update_option( 'yacht_booking_gcal_calendar_id', $calendar_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of user's calendars
|
||||
*
|
||||
* @return array|false Array of calendars or false on error.
|
||||
*/
|
||||
public static function get_calendar_list() {
|
||||
$access_token = OAuth_Handler::get_access_token();
|
||||
|
||||
if ( ! $access_token ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = wp_remote_get(
|
||||
self::API_BASE . '/users/me/calendarList',
|
||||
array(
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $access_token,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
return isset( $body['items'] ) ? $body['items'] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error
|
||||
*
|
||||
* @param string $message Error message.
|
||||
*/
|
||||
private static function log_error( $message ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( '[Yacht Booking - GCal] ' . $message );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Google Calendar event to blocked date range.
|
||||
*
|
||||
* IMPORTANT: Availability::mark_as_blocked() expects end date to be exclusive.
|
||||
*
|
||||
* @param array $event Google Calendar event payload.
|
||||
* @return array|false
|
||||
*/
|
||||
private static function get_blocked_range_from_event( $event ) {
|
||||
// Skip cancelled events.
|
||||
if ( isset( $event['status'] ) && 'cancelled' === $event['status'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip "free" events (Show as: Available).
|
||||
if ( isset( $event['transparency'] ) && 'transparent' === $event['transparency'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// All-day events: end date from Google is already exclusive.
|
||||
if ( isset( $event['start']['date'] ) && isset( $event['end']['date'] ) ) {
|
||||
$start_date = $event['start']['date'];
|
||||
$end_date = $event['end']['date'];
|
||||
|
||||
if ( strtotime( $end_date ) <= strtotime( $start_date ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array(
|
||||
'start_date' => $start_date,
|
||||
'end_date' => $end_date,
|
||||
);
|
||||
}
|
||||
|
||||
// Timed events: convert to full occupied-day range.
|
||||
if ( isset( $event['start']['dateTime'] ) && isset( $event['end']['dateTime'] ) ) {
|
||||
$start_ts = strtotime( $event['start']['dateTime'] );
|
||||
$end_ts = strtotime( $event['end']['dateTime'] );
|
||||
|
||||
if ( false === $start_ts || false === $end_ts || $end_ts <= $start_ts ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$start_date = gmdate( 'Y-m-d', $start_ts );
|
||||
|
||||
// Last occupied second to avoid midnight edge-cases.
|
||||
$last_occupied_ts = $end_ts - 1;
|
||||
$last_occupied = gmdate( 'Y-m-d', $last_occupied_ts );
|
||||
$end_date = gmdate( 'Y-m-d', strtotime( $last_occupied . ' +1 day' ) );
|
||||
|
||||
if ( strtotime( $end_date ) <= strtotime( $start_date ) ) {
|
||||
$end_date = gmdate( 'Y-m-d', strtotime( $start_date . ' +1 day' ) );
|
||||
}
|
||||
|
||||
return array(
|
||||
'start_date' => $start_date,
|
||||
'end_date' => $end_date,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Google event IDs that belong to internal bookings.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function get_internal_booking_event_ids() {
|
||||
global $wpdb;
|
||||
|
||||
$sql = $wpdb->prepare(
|
||||
"SELECT event_pm.meta_value
|
||||
FROM {$wpdb->postmeta} event_pm
|
||||
INNER JOIN {$wpdb->posts} p ON p.ID = event_pm.post_id
|
||||
LEFT JOIN {$wpdb->postmeta} source_pm
|
||||
ON source_pm.post_id = event_pm.post_id
|
||||
AND source_pm.meta_key = '_booking_source'
|
||||
WHERE event_pm.meta_key = '_gcal_event_id'
|
||||
AND p.post_type = 'yacht_booking'
|
||||
AND p.post_status = 'publish'
|
||||
AND (source_pm.meta_value IS NULL OR source_pm.meta_value != %s)",
|
||||
self::EXTERNAL_BOOKING_SOURCE
|
||||
);
|
||||
|
||||
$ids = $wpdb->get_col( $sql );
|
||||
|
||||
return is_array( $ids ) ? $ids : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get imported external booking map for yacht: event_id => booking_id.
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @return array
|
||||
*/
|
||||
private static function get_external_booking_map( $yacht_id ) {
|
||||
$bookings = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_booking_source',
|
||||
'value' => self::EXTERNAL_BOOKING_SOURCE,
|
||||
),
|
||||
array(
|
||||
'key' => '_booking_yacht_id',
|
||||
'value' => (int) $yacht_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$map = array();
|
||||
foreach ( $bookings as $booking_id ) {
|
||||
$event_id = get_post_meta( $booking_id, '_gcal_event_id', true );
|
||||
if ( ! empty( $event_id ) ) {
|
||||
$map[ $event_id ] = (int) $booking_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update imported external booking placeholder.
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @param array $event Google event payload.
|
||||
* @param array $range Date range (exclusive end).
|
||||
* @param int $existing_booking_id Existing booking ID.
|
||||
* @return int|false
|
||||
*/
|
||||
private static function upsert_external_booking( $yacht_id, $event, $range, $existing_booking_id = 0 ) {
|
||||
$summary = isset( $event['summary'] ) && '' !== trim( $event['summary'] ) ? sanitize_text_field( $event['summary'] ) : __( 'Wydarzenie z Google Calendar', 'yacht-booking' );
|
||||
$start_date = $range['start_date'];
|
||||
$end_date = $range['end_date'];
|
||||
$event_id = $event['id'];
|
||||
$event_url = isset( $event['htmlLink'] ) ? esc_url_raw( $event['htmlLink'] ) : '';
|
||||
|
||||
$post_data = array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'post_status' => 'publish',
|
||||
'post_title' => sprintf(
|
||||
/* translators: %s: Google event summary */
|
||||
__( 'Blokada Google Calendar: %s', 'yacht-booking' ),
|
||||
$summary
|
||||
),
|
||||
);
|
||||
|
||||
if ( $existing_booking_id > 0 ) {
|
||||
$post_data['ID'] = $existing_booking_id;
|
||||
$booking_id = wp_update_post( $post_data, true );
|
||||
} else {
|
||||
$booking_id = wp_insert_post( $post_data, true );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $booking_id ) || ! $booking_id ) {
|
||||
self::log_error( 'Failed to upsert external booking for event: ' . $event_id );
|
||||
return false;
|
||||
}
|
||||
|
||||
update_post_meta( $booking_id, '_booking_yacht_id', (int) $yacht_id );
|
||||
update_post_meta( $booking_id, '_booking_start_date', $start_date );
|
||||
update_post_meta( $booking_id, '_booking_end_date', $end_date );
|
||||
update_post_meta( $booking_id, '_booking_status', 'confirmed' );
|
||||
update_post_meta( $booking_id, '_booking_customer_name', __( 'Google Calendar (import)', 'yacht-booking' ) );
|
||||
update_post_meta( $booking_id, '_booking_customer_email', '' );
|
||||
update_post_meta( $booking_id, '_booking_customer_phone', '' );
|
||||
update_post_meta( $booking_id, '_booking_total_price', 0 );
|
||||
update_post_meta( $booking_id, '_booking_source', self::EXTERNAL_BOOKING_SOURCE );
|
||||
update_post_meta( $booking_id, '_gcal_event_id', $event_id );
|
||||
update_post_meta( $booking_id, '_booking_notes', $event_url );
|
||||
|
||||
// Rebuild availability tied to this imported booking.
|
||||
Availability::clear_booking_availability( $booking_id );
|
||||
Availability::mark_as_booked( $yacht_id, $start_date, $end_date, $booking_id );
|
||||
|
||||
return (int) $booking_id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
/**
|
||||
* Google Calendar OAuth Handler
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking\Integrations\GoogleCalendar;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth Handler class
|
||||
*/
|
||||
class OAuth_Handler {
|
||||
|
||||
/**
|
||||
* Google OAuth endpoints
|
||||
*/
|
||||
const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
||||
const TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
||||
const SCOPE = 'https://www.googleapis.com/auth/calendar';
|
||||
|
||||
/**
|
||||
* Get OAuth authorization URL
|
||||
*
|
||||
* @return string|false Authorization URL or false on error.
|
||||
*/
|
||||
public static function get_auth_url() {
|
||||
$credentials = self::get_credentials();
|
||||
|
||||
if ( ! $credentials ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Force production URL for Google OAuth
|
||||
$redirect_uri = 'https://jachty.pagedev.pl/wp-admin/admin.php?page=yacht-bookings-settings&tab=google-calendar&gcal_callback=1';
|
||||
|
||||
// Build URL manually to ensure proper encoding
|
||||
$params = array(
|
||||
'client_id' => $credentials['client_id'],
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'response_type' => 'code',
|
||||
'scope' => self::SCOPE,
|
||||
'access_type' => 'offline',
|
||||
'prompt' => 'consent',
|
||||
);
|
||||
|
||||
// Build query string with proper URL encoding
|
||||
$query = http_build_query( $params, '', '&' );
|
||||
|
||||
return self::AUTH_URL . '?' . $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange authorization code for access token
|
||||
*
|
||||
* @param string $code Authorization code.
|
||||
* @return array|false Token data or false on error.
|
||||
*/
|
||||
public static function authenticate( $code ) {
|
||||
$credentials = self::get_credentials();
|
||||
|
||||
if ( ! $credentials ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Force production URL for Google OAuth
|
||||
$redirect_uri = 'https://jachty.pagedev.pl/wp-admin/admin.php?page=yacht-bookings-settings&tab=google-calendar&gcal_callback=1';
|
||||
|
||||
$response = wp_remote_post(
|
||||
self::TOKEN_URL,
|
||||
array(
|
||||
'body' => array(
|
||||
'code' => $code,
|
||||
'client_id' => $credentials['client_id'],
|
||||
'client_secret' => $credentials['client_secret'],
|
||||
'redirect_uri' => $redirect_uri,
|
||||
'grant_type' => 'authorization_code',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( isset( $body['access_token'] ) ) {
|
||||
// Save tokens
|
||||
self::save_tokens( $body );
|
||||
return $body;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token (refresh if expired)
|
||||
*
|
||||
* @return string|false Access token or false on error.
|
||||
*/
|
||||
public static function get_access_token() {
|
||||
$tokens = get_option( 'yacht_booking_gcal_tokens' );
|
||||
|
||||
if ( ! $tokens || ! isset( $tokens['access_token'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
$expires_at = isset( $tokens['expires_at'] ) ? $tokens['expires_at'] : 0;
|
||||
|
||||
if ( time() >= $expires_at ) {
|
||||
// Token expired, refresh it
|
||||
$new_tokens = self::refresh_access_token();
|
||||
|
||||
if ( ! $new_tokens ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $new_tokens['access_token'];
|
||||
}
|
||||
|
||||
return $tokens['access_token'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh access token using refresh token
|
||||
*
|
||||
* @return array|false New token data or false on error.
|
||||
*/
|
||||
public static function refresh_access_token() {
|
||||
$credentials = self::get_credentials();
|
||||
$tokens = get_option( 'yacht_booking_gcal_tokens' );
|
||||
|
||||
if ( ! $credentials || ! $tokens || ! isset( $tokens['refresh_token'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = wp_remote_post(
|
||||
self::TOKEN_URL,
|
||||
array(
|
||||
'body' => array(
|
||||
'client_id' => $credentials['client_id'],
|
||||
'client_secret' => $credentials['client_secret'],
|
||||
'refresh_token' => $tokens['refresh_token'],
|
||||
'grant_type' => 'refresh_token',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( isset( $body['access_token'] ) ) {
|
||||
// Preserve refresh_token (not always returned)
|
||||
if ( ! isset( $body['refresh_token'] ) && isset( $tokens['refresh_token'] ) ) {
|
||||
$body['refresh_token'] = $tokens['refresh_token'];
|
||||
}
|
||||
|
||||
self::save_tokens( $body );
|
||||
return $body;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save OAuth credentials
|
||||
*
|
||||
* @param array $credentials Credentials array.
|
||||
* @return bool
|
||||
*/
|
||||
public static function save_credentials( $credentials ) {
|
||||
if ( ! isset( $credentials['client_id'] ) || ! isset( $credentials['client_secret'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return update_option( 'yacht_booking_gcal_credentials', $credentials );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get saved credentials
|
||||
*
|
||||
* @return array|false Credentials or false.
|
||||
*/
|
||||
public static function get_credentials() {
|
||||
$credentials = get_option( 'yacht_booking_gcal_credentials' );
|
||||
|
||||
if ( ! $credentials || ! isset( $credentials['client_id'] ) || ! isset( $credentials['client_secret'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save OAuth tokens
|
||||
*
|
||||
* @param array $tokens Token data.
|
||||
*/
|
||||
private static function save_tokens( $tokens ) {
|
||||
// Calculate expiry time
|
||||
if ( isset( $tokens['expires_in'] ) ) {
|
||||
$tokens['expires_at'] = time() + (int) $tokens['expires_in'];
|
||||
}
|
||||
|
||||
update_option( 'yacht_booking_gcal_tokens', $tokens );
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all Google Calendar data (disconnect)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function disconnect() {
|
||||
delete_option( 'yacht_booking_gcal_credentials' );
|
||||
delete_option( 'yacht_booking_gcal_tokens' );
|
||||
delete_option( 'yacht_booking_gcal_calendar_id' );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connected to Google Calendar
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_connected() {
|
||||
$tokens = get_option( 'yacht_booking_gcal_tokens' );
|
||||
return ! empty( $tokens ) && isset( $tokens['access_token'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connected email (from token info)
|
||||
*
|
||||
* @return string|false Email or false.
|
||||
*/
|
||||
public static function get_connected_email() {
|
||||
$access_token = self::get_access_token();
|
||||
|
||||
if ( ! $access_token ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$response = wp_remote_get(
|
||||
'https://www.googleapis.com/oauth2/v2/userinfo',
|
||||
array(
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $access_token,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
return isset( $body['email'] ) ? $body['email'] : false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
/**
|
||||
* Google Calendar Sync Controller
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking\Integrations\GoogleCalendar;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync Controller class
|
||||
*
|
||||
* Handles automatic synchronization between WordPress bookings and Google Calendar
|
||||
*/
|
||||
class Sync_Controller {
|
||||
|
||||
/**
|
||||
* Instance
|
||||
*
|
||||
* @var Sync_Controller
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* Get instance
|
||||
*
|
||||
* @return Sync_Controller
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
private function __construct() {
|
||||
// Hook into booking events
|
||||
add_action( 'yacht_booking_created', array( $this, 'on_booking_created' ), 20, 1 );
|
||||
add_action( 'yacht_booking_status_changed', array( $this, 'on_booking_status_changed' ), 20, 2 );
|
||||
add_action( 'before_delete_post', array( $this, 'on_booking_deleted' ), 10, 1 );
|
||||
|
||||
// AJAX handler for manual sync
|
||||
add_action( 'wp_ajax_yacht_booking_manual_sync', array( $this, 'ajax_manual_sync' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle booking created
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
*/
|
||||
public function on_booking_created( $booking_id ) {
|
||||
// Check if connected
|
||||
if ( ! OAuth_Handler::is_connected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule async sync (avoid blocking the request)
|
||||
wp_schedule_single_event(
|
||||
time(),
|
||||
'yacht_booking_sync_to_gcal',
|
||||
array( $booking_id )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle booking status changed
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
* @param string $new_status New status.
|
||||
*/
|
||||
public function on_booking_status_changed( $booking_id, $new_status ) {
|
||||
// Check if connected
|
||||
if ( ! OAuth_Handler::is_connected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event_id = get_post_meta( $booking_id, '_gcal_event_id', true );
|
||||
|
||||
if ( 'cancelled' === $new_status && $event_id ) {
|
||||
// Delete event from Google Calendar
|
||||
wp_schedule_single_event(
|
||||
time(),
|
||||
'yacht_booking_delete_from_gcal',
|
||||
array( $booking_id )
|
||||
);
|
||||
} elseif ( 'confirmed' === $new_status ) {
|
||||
if ( $event_id ) {
|
||||
// Update existing event
|
||||
wp_schedule_single_event(
|
||||
time(),
|
||||
'yacht_booking_update_in_gcal',
|
||||
array( $booking_id )
|
||||
);
|
||||
} else {
|
||||
// Create new event
|
||||
wp_schedule_single_event(
|
||||
time(),
|
||||
'yacht_booking_sync_to_gcal',
|
||||
array( $booking_id )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle booking deleted
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
*/
|
||||
public function on_booking_deleted( $post_id ) {
|
||||
// Check if it's a booking
|
||||
if ( get_post_type( $post_id ) !== 'yacht_booking' ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if connected
|
||||
if ( ! OAuth_Handler::is_connected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$event_id = get_post_meta( $post_id, '_gcal_event_id', true );
|
||||
|
||||
if ( $event_id ) {
|
||||
// Delete event from Google Calendar (sync now, before post is deleted)
|
||||
GCal_Service::delete_event( $post_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register cron actions
|
||||
*/
|
||||
public static function register_cron_actions() {
|
||||
// Sync to Google Calendar
|
||||
add_action( 'yacht_booking_sync_to_gcal', array( __CLASS__, 'sync_booking_to_gcal' ) );
|
||||
|
||||
// Update in Google Calendar
|
||||
add_action( 'yacht_booking_update_in_gcal', array( __CLASS__, 'update_booking_in_gcal' ) );
|
||||
|
||||
// Delete from Google Calendar
|
||||
add_action( 'yacht_booking_delete_from_gcal', array( __CLASS__, 'delete_booking_from_gcal' ) );
|
||||
|
||||
// Pull from Google Calendar (hourly)
|
||||
add_action( 'yacht_booking_pull_from_gcal', array( __CLASS__, 'pull_from_gcal' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync booking to Google Calendar (cron action)
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
*/
|
||||
public static function sync_booking_to_gcal( $booking_id ) {
|
||||
$event_id = GCal_Service::create_event( $booking_id );
|
||||
|
||||
if ( $event_id ) {
|
||||
self::log( sprintf( 'Booking #%d synced to Google Calendar (Event ID: %s)', $booking_id, $event_id ) );
|
||||
} else {
|
||||
self::log( sprintf( 'Failed to sync booking #%d to Google Calendar', $booking_id ), 'error' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update booking in Google Calendar (cron action)
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
*/
|
||||
public static function update_booking_in_gcal( $booking_id ) {
|
||||
$result = GCal_Service::update_event( $booking_id );
|
||||
|
||||
if ( $result ) {
|
||||
self::log( sprintf( 'Booking #%d updated in Google Calendar', $booking_id ) );
|
||||
} else {
|
||||
self::log( sprintf( 'Failed to update booking #%d in Google Calendar', $booking_id ), 'error' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete booking from Google Calendar (cron action)
|
||||
*
|
||||
* @param int $booking_id Booking post ID.
|
||||
*/
|
||||
public static function delete_booking_from_gcal( $booking_id ) {
|
||||
$result = GCal_Service::delete_event( $booking_id );
|
||||
|
||||
if ( $result ) {
|
||||
self::log( sprintf( 'Booking #%d deleted from Google Calendar', $booking_id ) );
|
||||
} else {
|
||||
self::log( sprintf( 'Failed to delete booking #%d from Google Calendar', $booking_id ), 'error' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull events from Google Calendar (cron action)
|
||||
*/
|
||||
public static function pull_from_gcal() {
|
||||
// Get all yachts
|
||||
$yachts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'posts_per_page' => -1,
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $yachts as $yacht ) {
|
||||
$result = GCal_Service::sync_from_gcal( $yacht->ID );
|
||||
|
||||
if ( $result ) {
|
||||
self::log( sprintf( 'Synced events from Google Calendar for yacht #%d', $yacht->ID ) );
|
||||
} else {
|
||||
self::log( sprintf( 'Failed to sync events from Google Calendar for yacht #%d', $yacht->ID ), 'error' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup hourly cron job for pulling from Google Calendar
|
||||
*/
|
||||
public static function setup_cron() {
|
||||
if ( ! wp_next_scheduled( 'yacht_booking_pull_from_gcal' ) ) {
|
||||
wp_schedule_event( time(), 'hourly', 'yacht_booking_pull_from_gcal' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cron jobs
|
||||
*/
|
||||
public static function clear_cron() {
|
||||
wp_clear_scheduled_hook( 'yacht_booking_pull_from_gcal' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message
|
||||
*
|
||||
* @param string $message Message.
|
||||
* @param string $type Log type (info|error).
|
||||
*/
|
||||
private static function log( $message, $type = 'info' ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
$prefix = 'error' === $type ? 'ERROR' : 'INFO';
|
||||
error_log( sprintf( '[Yacht Booking - GCal Sync] [%s] %s', $prefix, $message ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for manual sync
|
||||
*/
|
||||
public function ajax_manual_sync() {
|
||||
// Check nonce
|
||||
check_ajax_referer( 'yacht_booking_manual_sync', 'nonce' );
|
||||
|
||||
// Check capabilities
|
||||
if ( ! current_user_can( 'yacht_booking_manage_settings' ) ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Brak uprawnień', 'yacht-booking' ) ) );
|
||||
}
|
||||
|
||||
// Check if connected
|
||||
if ( ! OAuth_Handler::is_connected() ) {
|
||||
wp_send_json_error( array( 'message' => __( 'Nie połączono z Google Calendar', 'yacht-booking' ) ) );
|
||||
}
|
||||
|
||||
// Statistics
|
||||
$yachts_synced = 0;
|
||||
$bookings_pushed = 0;
|
||||
$bookings_skipped = 0;
|
||||
$errors = array();
|
||||
|
||||
// STEP 1: Push WordPress bookings to Google Calendar
|
||||
$bookings = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $bookings as $booking ) {
|
||||
$event_id = get_post_meta( $booking->ID, '_gcal_event_id', true );
|
||||
$status = get_post_meta( $booking->ID, '_booking_status', true );
|
||||
$source = get_post_meta( $booking->ID, '_booking_source', true );
|
||||
|
||||
// Never push imported Google placeholders back to Google.
|
||||
if ( GCal_Service::EXTERNAL_BOOKING_SOURCE === $source ) {
|
||||
$bookings_skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip cancelled bookings
|
||||
if ( 'cancelled' === $status ) {
|
||||
$bookings_skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only push if not already in Google Calendar
|
||||
if ( empty( $event_id ) ) {
|
||||
$result = GCal_Service::create_event( $booking->ID );
|
||||
|
||||
if ( $result ) {
|
||||
$bookings_pushed++;
|
||||
self::log( sprintf( 'Manual sync: Pushed booking #%d to Google Calendar', $booking->ID ) );
|
||||
} else {
|
||||
self::log( sprintf( 'Manual sync: Failed to push booking #%d to Google Calendar', $booking->ID ), 'error' );
|
||||
}
|
||||
} else {
|
||||
$bookings_skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
// STEP 2: Pull external events from Google Calendar
|
||||
$yachts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'posts_per_page' => -1,
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $yachts as $yacht ) {
|
||||
$result = GCal_Service::sync_from_gcal( $yacht->ID );
|
||||
|
||||
if ( $result ) {
|
||||
$yachts_synced++;
|
||||
self::log( sprintf( 'Manual sync: Pulled events from Google Calendar for yacht #%d', $yacht->ID ) );
|
||||
} else {
|
||||
$errors[] = sprintf( __( 'Błąd synchronizacji dla jachtu: %s', 'yacht-booking' ), $yacht->post_title );
|
||||
self::log( sprintf( 'Manual sync: Failed to pull events from Google Calendar for yacht #%d', $yacht->ID ), 'error' );
|
||||
}
|
||||
}
|
||||
|
||||
// Build success message
|
||||
$messages = array();
|
||||
|
||||
if ( $bookings_pushed > 0 ) {
|
||||
$messages[] = sprintf(
|
||||
/* translators: %d: number of bookings pushed */
|
||||
_n( 'Wysłano %d rezerwację do Google Calendar', 'Wysłano %d rezerwacji do Google Calendar', $bookings_pushed, 'yacht-booking' ),
|
||||
$bookings_pushed
|
||||
);
|
||||
}
|
||||
|
||||
if ( $bookings_skipped > 0 ) {
|
||||
$messages[] = sprintf(
|
||||
/* translators: %d: number of bookings skipped */
|
||||
_n( 'Pominięto %d rezerwację (już zsynchronizowana lub anulowana)', 'Pominięto %d rezerwacji (już zsynchronizowane lub anulowane)', $bookings_skipped, 'yacht-booking' ),
|
||||
$bookings_skipped
|
||||
);
|
||||
}
|
||||
|
||||
if ( $yachts_synced > 0 ) {
|
||||
$messages[] = sprintf(
|
||||
/* translators: %d: number of yachts synced */
|
||||
_n( 'Pobrano wydarzenia dla %d jachtu z Google Calendar', 'Pobrano wydarzenia dla %d jachtów z Google Calendar', $yachts_synced, 'yacht-booking' ),
|
||||
$yachts_synced
|
||||
);
|
||||
}
|
||||
|
||||
if ( count( $errors ) > 0 ) {
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'message' => implode( '<br>', $errors ),
|
||||
)
|
||||
);
|
||||
} else {
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'message' => implode( '<br>', $messages ),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
/**
|
||||
* iCal Feed Generator (export)
|
||||
*
|
||||
* Generates .ics feed per yacht for subscription by Google Calendar or other apps.
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking\Integrations\ICal;
|
||||
|
||||
use YachtBooking\Booking;
|
||||
use YachtBooking\Yacht;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* ICal Feed class
|
||||
*/
|
||||
class ICal_Feed {
|
||||
|
||||
/**
|
||||
* Register rewrite rules and query vars
|
||||
*/
|
||||
public static function register() {
|
||||
add_action( 'init', array( __CLASS__, 'add_rewrite_rules' ) );
|
||||
add_filter( 'query_vars', array( __CLASS__, 'add_query_vars' ) );
|
||||
add_action( 'template_redirect', array( __CLASS__, 'handle_feed_request' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add rewrite rule for ical feed
|
||||
*/
|
||||
public static function add_rewrite_rules() {
|
||||
add_rewrite_rule(
|
||||
'^yacht-ical/([0-9]+)/([a-zA-Z0-9]+)\.ics$',
|
||||
'index.php?yacht_ical_id=$matches[1]&yacht_ical_token=$matches[2]',
|
||||
'top'
|
||||
);
|
||||
|
||||
// Flush rewrite rules if our rule is not registered yet.
|
||||
$rules = get_option( 'rewrite_rules' );
|
||||
if ( is_array( $rules ) && ! isset( $rules['^yacht-ical/([0-9]+)/([a-zA-Z0-9]+)\.ics$'] ) ) {
|
||||
flush_rewrite_rules( false );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add query vars
|
||||
*
|
||||
* @param array $vars Query vars.
|
||||
* @return array
|
||||
*/
|
||||
public static function add_query_vars( $vars ) {
|
||||
$vars[] = 'yacht_ical_id';
|
||||
$vars[] = 'yacht_ical_token';
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle feed request
|
||||
*/
|
||||
public static function handle_feed_request() {
|
||||
$yacht_id = (int) get_query_var( 'yacht_ical_id', 0 );
|
||||
$token = get_query_var( 'yacht_ical_token', '' );
|
||||
|
||||
if ( ! $yacht_id || ! $token ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$yacht = get_post( $yacht_id );
|
||||
if ( ! $yacht || 'yacht' !== $yacht->post_type ) {
|
||||
status_header( 404 );
|
||||
exit;
|
||||
}
|
||||
|
||||
$stored_token = self::get_feed_token( $yacht_id );
|
||||
if ( ! $stored_token || ! hash_equals( $stored_token, $token ) ) {
|
||||
status_header( 403 );
|
||||
exit;
|
||||
}
|
||||
|
||||
self::output_ics( $yacht );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create feed token for a yacht
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_feed_token( $yacht_id ) {
|
||||
$token = get_post_meta( $yacht_id, '_yacht_ical_token', true );
|
||||
|
||||
if ( empty( $token ) ) {
|
||||
$token = wp_generate_password( 24, false );
|
||||
update_post_meta( $yacht_id, '_yacht_ical_token', $token );
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerate feed token
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function regenerate_token( $yacht_id ) {
|
||||
$token = wp_generate_password( 24, false );
|
||||
update_post_meta( $yacht_id, '_yacht_ical_token', $token );
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feed URL for a yacht
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_feed_url( $yacht_id ) {
|
||||
$token = self::get_feed_token( $yacht_id );
|
||||
return home_url( sprintf( '/yacht-ical/%d/%s.ics', $yacht_id, $token ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Output .ics file
|
||||
*
|
||||
* @param \WP_Post $yacht Yacht post.
|
||||
*/
|
||||
private static function output_ics( $yacht ) {
|
||||
$bookings = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_booking_yacht_id',
|
||||
'value' => $yacht->ID,
|
||||
),
|
||||
array(
|
||||
'key' => '_booking_status',
|
||||
'value' => 'cancelled',
|
||||
'compare' => '!=',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$site_name = get_bloginfo( 'name' );
|
||||
$domain = wp_parse_url( home_url(), PHP_URL_HOST );
|
||||
|
||||
header( 'Content-Type: text/calendar; charset=utf-8' );
|
||||
header( 'Content-Disposition: inline; filename="' . sanitize_file_name( $yacht->post_title ) . '.ics"' );
|
||||
header( 'Cache-Control: no-cache, must-revalidate' );
|
||||
|
||||
$lines = array();
|
||||
$lines[] = 'BEGIN:VCALENDAR';
|
||||
$lines[] = 'VERSION:2.0';
|
||||
$lines[] = 'PRODID:-//YachtBooking//NONSGML v1.0//PL';
|
||||
$lines[] = 'CALSCALE:GREGORIAN';
|
||||
$lines[] = 'METHOD:PUBLISH';
|
||||
$lines[] = 'X-WR-CALNAME:' . self::escape_ical( $yacht->post_title . ' - ' . $site_name );
|
||||
$lines[] = 'X-WR-TIMEZONE:Europe/Warsaw';
|
||||
|
||||
foreach ( $bookings as $booking ) {
|
||||
$start = Booking::get_start_date( $booking->ID );
|
||||
$end = Booking::get_end_date( $booking->ID );
|
||||
$status = Booking::get_status( $booking->ID );
|
||||
$name = Booking::get_customer_name( $booking->ID );
|
||||
|
||||
if ( ! $start || ! $end ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// iCal DTEND for all-day events is exclusive
|
||||
$end_exclusive = gmdate( 'Ymd', strtotime( $end . ' +1 day' ) );
|
||||
$created = get_the_date( 'Ymd\THis\Z', $booking );
|
||||
|
||||
$summary = sprintf( '%s - %s', $yacht->post_title, $name );
|
||||
if ( 'pending' === $status ) {
|
||||
$summary = '[' . __( 'Oczekująca', 'yacht-booking' ) . '] ' . $summary;
|
||||
}
|
||||
|
||||
$lines[] = 'BEGIN:VEVENT';
|
||||
$lines[] = 'UID:booking-' . $booking->ID . '@' . $domain;
|
||||
$lines[] = 'DTSTART;VALUE=DATE:' . gmdate( 'Ymd', strtotime( $start ) );
|
||||
$lines[] = 'DTEND;VALUE=DATE:' . $end_exclusive;
|
||||
$lines[] = 'DTSTAMP:' . gmdate( 'Ymd\THis\Z' );
|
||||
$lines[] = 'CREATED:' . $created;
|
||||
$lines[] = 'SUMMARY:' . self::escape_ical( $summary );
|
||||
$lines[] = 'STATUS:CONFIRMED';
|
||||
$lines[] = 'TRANSP:OPAQUE';
|
||||
$lines[] = 'END:VEVENT';
|
||||
}
|
||||
|
||||
$lines[] = 'END:VCALENDAR';
|
||||
|
||||
echo implode( "\r\n", $lines ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- iCal format
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape iCal text value
|
||||
*
|
||||
* @param string $text Text.
|
||||
* @return string
|
||||
*/
|
||||
private static function escape_ical( $text ) {
|
||||
$text = str_replace( '\\', '\\\\', $text );
|
||||
$text = str_replace( ',', '\\,', $text );
|
||||
$text = str_replace( ';', '\\;', $text );
|
||||
$text = str_replace( "\n", '\\n', $text );
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
<?php
|
||||
/**
|
||||
* iCal Import (subscribe to external .ics URL)
|
||||
*
|
||||
* Fetches .ics from external URL (e.g. Google Calendar public iCal link)
|
||||
* and blocks dates in yacht availability.
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking\Integrations\ICal;
|
||||
|
||||
use YachtBooking\Availability;
|
||||
use YachtBooking\Yacht;
|
||||
|
||||
// Exit if accessed directly.
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* ICal Import class
|
||||
*/
|
||||
class ICal_Import {
|
||||
|
||||
/**
|
||||
* Booking source identifier for iCal imports.
|
||||
*/
|
||||
const IMPORT_SOURCE = 'ical_import';
|
||||
|
||||
/**
|
||||
* Register cron actions
|
||||
*/
|
||||
public static function register() {
|
||||
add_action( 'yacht_booking_ical_import', array( __CLASS__, 'run_import' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup cron schedule
|
||||
*/
|
||||
public static function setup_cron() {
|
||||
if ( ! wp_next_scheduled( 'yacht_booking_ical_import' ) ) {
|
||||
wp_schedule_event( time(), 'hourly', 'yacht_booking_ical_import' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cron
|
||||
*/
|
||||
public static function clear_cron() {
|
||||
wp_clear_scheduled_hook( 'yacht_booking_ical_import' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Run import for all yachts that have an iCal URL configured.
|
||||
*/
|
||||
public static function run_import() {
|
||||
$yachts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $yachts as $yacht_id ) {
|
||||
$url = self::get_import_url( $yacht_id );
|
||||
if ( $url ) {
|
||||
self::import_for_yacht( $yacht_id, $url );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get iCal import URL for a yacht.
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_import_url( $yacht_id ) {
|
||||
return get_post_meta( $yacht_id, '_yacht_ical_import_url', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set iCal import URL for a yacht.
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @param string $url iCal URL.
|
||||
*/
|
||||
public static function set_import_url( $yacht_id, $url ) {
|
||||
update_post_meta( $yacht_id, '_yacht_ical_import_url', esc_url_raw( $url ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Import events from iCal URL for a specific yacht.
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @param string $url iCal URL.
|
||||
* @return bool
|
||||
*/
|
||||
public static function import_for_yacht( $yacht_id, $url ) {
|
||||
$response = wp_remote_get(
|
||||
$url,
|
||||
array(
|
||||
'timeout' => 30,
|
||||
'sslverify' => true,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
self::log( sprintf( 'iCal fetch failed for yacht #%d: %s', $yacht_id, $response->get_error_message() ), 'error' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
if ( empty( $body ) ) {
|
||||
self::log( sprintf( 'iCal empty response for yacht #%d', $yacht_id ), 'error' );
|
||||
return false;
|
||||
}
|
||||
|
||||
$events = self::parse_ics( $body );
|
||||
|
||||
$existing_map = self::get_existing_import_map( $yacht_id );
|
||||
$seen_uids = array();
|
||||
|
||||
foreach ( $events as $event ) {
|
||||
if ( empty( $event['uid'] ) || empty( $event['start'] ) || empty( $event['end'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip past events
|
||||
if ( strtotime( $event['end'] ) < time() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen_uids[] = $event['uid'];
|
||||
$booking_id = isset( $existing_map[ $event['uid'] ] ) ? (int) $existing_map[ $event['uid'] ] : 0;
|
||||
$booking_id = self::upsert_booking( $yacht_id, $event, $booking_id );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
self::log( sprintf( 'Failed to upsert iCal event %s for yacht #%d', $event['uid'], $yacht_id ), 'error' );
|
||||
}
|
||||
}
|
||||
|
||||
// Remove stale imports (events deleted from external calendar)
|
||||
foreach ( $existing_map as $uid => $booking_id ) {
|
||||
if ( ! in_array( $uid, $seen_uids, true ) ) {
|
||||
Availability::clear_booking_availability( $booking_id );
|
||||
wp_delete_post( $booking_id, true );
|
||||
}
|
||||
}
|
||||
|
||||
update_post_meta( $yacht_id, '_yacht_ical_last_import', current_time( 'mysql' ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse .ics content into array of events.
|
||||
*
|
||||
* @param string $ics_content Raw .ics content.
|
||||
* @return array
|
||||
*/
|
||||
private static function parse_ics( $ics_content ) {
|
||||
$events = array();
|
||||
$lines = preg_split( '/\r\n|\r|\n/', $ics_content );
|
||||
$in_event = false;
|
||||
$event = array();
|
||||
|
||||
// Unfold lines (RFC 5545: lines starting with space/tab are continuations)
|
||||
$unfolded = array();
|
||||
foreach ( $lines as $line ) {
|
||||
if ( strlen( $line ) > 0 && ( ' ' === $line[0] || "\t" === $line[0] ) && count( $unfolded ) > 0 ) {
|
||||
$unfolded[ count( $unfolded ) - 1 ] .= substr( $line, 1 );
|
||||
} else {
|
||||
$unfolded[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $unfolded as $line ) {
|
||||
$line = trim( $line );
|
||||
|
||||
if ( 'BEGIN:VEVENT' === $line ) {
|
||||
$in_event = true;
|
||||
$event = array(
|
||||
'uid' => '',
|
||||
'summary' => '',
|
||||
'start' => '',
|
||||
'end' => '',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( 'END:VEVENT' === $line ) {
|
||||
$in_event = false;
|
||||
if ( ! empty( $event['uid'] ) ) {
|
||||
$events[] = $event;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! $in_event ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse property
|
||||
if ( self::line_starts_with( $line, 'UID:' ) ) {
|
||||
$event['uid'] = self::extract_value( $line );
|
||||
} elseif ( self::line_starts_with( $line, 'SUMMARY' ) ) {
|
||||
$event['summary'] = self::unescape_ical( self::extract_value( $line ) );
|
||||
} elseif ( self::line_starts_with( $line, 'DTSTART' ) ) {
|
||||
$event['start'] = self::parse_ical_date( $line );
|
||||
} elseif ( self::line_starts_with( $line, 'DTEND' ) ) {
|
||||
$event['end'] = self::parse_ical_date( $line );
|
||||
}
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if line starts with prefix (case-insensitive for property name).
|
||||
*
|
||||
* @param string $line Line.
|
||||
* @param string $prefix Prefix.
|
||||
* @return bool
|
||||
*/
|
||||
private static function line_starts_with( $line, $prefix ) {
|
||||
return 0 === strncasecmp( $line, $prefix, strlen( $prefix ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract value from iCal line (handles parameters like DTSTART;VALUE=DATE:20260315).
|
||||
*
|
||||
* @param string $line Line.
|
||||
* @return string
|
||||
*/
|
||||
private static function extract_value( $line ) {
|
||||
$pos = strpos( $line, ':' );
|
||||
return false !== $pos ? substr( $line, $pos + 1 ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse iCal date to Y-m-d format.
|
||||
*
|
||||
* @param string $line Full iCal line.
|
||||
* @return string
|
||||
*/
|
||||
private static function parse_ical_date( $line ) {
|
||||
$value = self::extract_value( $line );
|
||||
$value = trim( $value );
|
||||
|
||||
// All-day: 20260315
|
||||
if ( preg_match( '/^(\d{4})(\d{2})(\d{2})$/', $value, $m ) ) {
|
||||
return $m[1] . '-' . $m[2] . '-' . $m[3];
|
||||
}
|
||||
|
||||
// DateTime: 20260315T100000Z or 20260315T100000
|
||||
if ( preg_match( '/^(\d{4})(\d{2})(\d{2})T/', $value ) ) {
|
||||
$ts = strtotime( $value );
|
||||
return false !== $ts ? gmdate( 'Y-m-d', $ts ) : '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescape iCal text.
|
||||
*
|
||||
* @param string $text Text.
|
||||
* @return string
|
||||
*/
|
||||
private static function unescape_ical( $text ) {
|
||||
$text = str_replace( '\\n', "\n", $text );
|
||||
$text = str_replace( '\\,', ',', $text );
|
||||
$text = str_replace( '\\;', ';', $text );
|
||||
$text = str_replace( '\\\\', '\\', $text );
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get existing imported bookings map: uid => booking_id.
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @return array
|
||||
*/
|
||||
private static function get_existing_import_map( $yacht_id ) {
|
||||
$bookings = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_booking_source',
|
||||
'value' => self::IMPORT_SOURCE,
|
||||
),
|
||||
array(
|
||||
'key' => '_booking_yacht_id',
|
||||
'value' => (int) $yacht_id,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$map = array();
|
||||
foreach ( $bookings as $booking_id ) {
|
||||
$uid = get_post_meta( $booking_id, '_ical_event_uid', true );
|
||||
if ( $uid ) {
|
||||
$map[ $uid ] = (int) $booking_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update imported booking placeholder.
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @param array $event Parsed event data.
|
||||
* @param int $existing_id Existing booking ID (0 for new).
|
||||
* @return int|false
|
||||
*/
|
||||
private static function upsert_booking( $yacht_id, $event, $existing_id = 0 ) {
|
||||
$summary = ! empty( $event['summary'] ) ? sanitize_text_field( $event['summary'] ) : __( 'Blokada iCal', 'yacht-booking' );
|
||||
$start_date = $event['start'];
|
||||
$end_date = $event['end'];
|
||||
|
||||
$post_data = array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'post_status' => 'publish',
|
||||
'post_title' => sprintf(
|
||||
/* translators: %s: event summary */
|
||||
__( 'Import iCal: %s', 'yacht-booking' ),
|
||||
$summary
|
||||
),
|
||||
);
|
||||
|
||||
if ( $existing_id > 0 ) {
|
||||
$post_data['ID'] = $existing_id;
|
||||
$booking_id = wp_update_post( $post_data, true );
|
||||
} else {
|
||||
$booking_id = wp_insert_post( $post_data, true );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $booking_id ) || ! $booking_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update_post_meta( $booking_id, '_booking_yacht_id', (int) $yacht_id );
|
||||
update_post_meta( $booking_id, '_booking_start_date', $start_date );
|
||||
update_post_meta( $booking_id, '_booking_end_date', $end_date );
|
||||
update_post_meta( $booking_id, '_booking_status', 'confirmed' );
|
||||
update_post_meta( $booking_id, '_booking_customer_name', __( 'Import iCal', 'yacht-booking' ) );
|
||||
update_post_meta( $booking_id, '_booking_customer_email', '' );
|
||||
update_post_meta( $booking_id, '_booking_customer_phone', '' );
|
||||
update_post_meta( $booking_id, '_booking_total_price', 0 );
|
||||
update_post_meta( $booking_id, '_booking_source', self::IMPORT_SOURCE );
|
||||
update_post_meta( $booking_id, '_ical_event_uid', $event['uid'] );
|
||||
update_post_meta( $booking_id, '_booking_notes', $summary );
|
||||
|
||||
Availability::clear_booking_availability( $booking_id );
|
||||
Availability::mark_as_booked( $yacht_id, $start_date, $end_date, $booking_id );
|
||||
|
||||
return (int) $booking_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last import time for yacht.
|
||||
*
|
||||
* @param int $yacht_id Yacht ID.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_last_import_time( $yacht_id ) {
|
||||
return get_post_meta( $yacht_id, '_yacht_ical_last_import', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message.
|
||||
*
|
||||
* @param string $message Message.
|
||||
* @param string $type Type (info|error).
|
||||
*/
|
||||
private static function log( $message, $type = 'info' ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
$prefix = 'error' === $type ? 'ERROR' : 'INFO';
|
||||
error_log( sprintf( '[Yacht Booking - iCal] [%s] %s', $prefix, $message ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
90
wp-content/plugins/yacht-booking-system/uninstall.php
Normal file
90
wp-content/plugins/yacht-booking-system/uninstall.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* Uninstall script
|
||||
*
|
||||
* Fired when the plugin is uninstalled.
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
// Exit if accessed directly or not uninstalling
|
||||
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete custom tables
|
||||
*/
|
||||
function yacht_booking_delete_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'yacht_availability';
|
||||
$wpdb->query( "DROP TABLE IF EXISTS $table_name" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete plugin options
|
||||
*/
|
||||
function yacht_booking_delete_options() {
|
||||
delete_option( 'yacht_booking_version' );
|
||||
delete_option( 'yacht_booking_installed_at' );
|
||||
delete_option( 'yacht_booking_default_status' );
|
||||
delete_option( 'yacht_booking_email_from_name' );
|
||||
delete_option( 'yacht_booking_email_from' );
|
||||
delete_option( 'yacht_booking_email_from_address' );
|
||||
delete_option( 'yacht_booking_date_format' );
|
||||
delete_option( 'yacht_booking_currency_symbol' );
|
||||
delete_option( 'yacht_booking_terms_page_id' );
|
||||
delete_option( 'yacht_booking_email_templates' );
|
||||
delete_option( 'yacht_booking_enable_notifications' );
|
||||
delete_option( 'yacht_booking_gcal_sync_enabled' );
|
||||
delete_option( 'yacht_booking_gcal_token' );
|
||||
delete_option( 'yacht_booking_gcal_webhook_token' );
|
||||
delete_option( 'yacht_booking_gcal_credentials' );
|
||||
delete_option( 'yacht_booking_gcal_tokens' );
|
||||
delete_option( 'yacht_booking_gcal_calendar_id' );
|
||||
delete_option( 'yacht_booking_capabilities_added' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete custom post types
|
||||
*/
|
||||
function yacht_booking_delete_posts() {
|
||||
global $wpdb;
|
||||
|
||||
// Delete yacht posts
|
||||
$wpdb->query( "DELETE FROM {$wpdb->posts} WHERE post_type = 'yacht'" );
|
||||
$wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE post_id NOT IN (SELECT ID FROM {$wpdb->posts})" );
|
||||
|
||||
// Delete booking posts
|
||||
$wpdb->query( "DELETE FROM {$wpdb->posts} WHERE post_type = 'yacht_booking'" );
|
||||
$wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE post_id NOT IN (SELECT ID FROM {$wpdb->posts})" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove custom capabilities
|
||||
*/
|
||||
function yacht_booking_remove_capabilities() {
|
||||
$admin = get_role( 'administrator' );
|
||||
|
||||
if ( $admin ) {
|
||||
$capabilities = array(
|
||||
'yacht_booking_manage_yachts',
|
||||
'yacht_booking_manage_bookings',
|
||||
'yacht_booking_manage_settings',
|
||||
);
|
||||
|
||||
foreach ( $capabilities as $cap ) {
|
||||
$admin->remove_cap( $cap );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run uninstall
|
||||
yacht_booking_delete_tables();
|
||||
yacht_booking_delete_options();
|
||||
yacht_booking_delete_posts();
|
||||
yacht_booking_remove_capabilities();
|
||||
|
||||
// Clean up transients
|
||||
delete_transient( 'yacht_booking_availability_cache' );
|
||||
120
wp-content/plugins/yacht-booking-system/yacht-booking-system.php
Normal file
120
wp-content/plugins/yacht-booking-system/yacht-booking-system.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
/**
|
||||
* 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.0.0
|
||||
* Author: PageDev
|
||||
* Author URI: https://pagedev.pl
|
||||
* Text Domain: yacht-booking
|
||||
* Domain Path: /languages
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* License: GPL v2 or later
|
||||
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
*/
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Define plugin constants
|
||||
define( 'YACHT_BOOKING_VERSION', '1.0.0' );
|
||||
define( 'YACHT_BOOKING_PLUGIN_FILE', __FILE__ );
|
||||
define( 'YACHT_BOOKING_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'YACHT_BOOKING_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
define( 'YACHT_BOOKING_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||
|
||||
/**
|
||||
* PSR-4 Autoloader for plugin classes
|
||||
*/
|
||||
spl_autoload_register( function( $class ) {
|
||||
// Project namespace prefix
|
||||
$prefix = 'YachtBooking\\';
|
||||
|
||||
// Base directory for the namespace prefix
|
||||
$base_dir = YACHT_BOOKING_PLUGIN_DIR . 'includes/';
|
||||
|
||||
// Does the class use the namespace prefix?
|
||||
$len = strlen( $prefix );
|
||||
if ( strncmp( $prefix, $class, $len ) !== 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the relative class name
|
||||
$relative_class = substr( $class, $len );
|
||||
|
||||
// Replace namespace separators with directory separators
|
||||
// Convert to lowercase and prepend 'class-'
|
||||
$file = $base_dir . 'class-' . str_replace( '\\', '/', strtolower( str_replace( '_', '-', $relative_class ) ) ) . '.php';
|
||||
|
||||
// If the file exists, require it
|
||||
if ( file_exists( $file ) ) {
|
||||
require $file;
|
||||
}
|
||||
} );
|
||||
|
||||
/**
|
||||
* Plugin activation hook
|
||||
*/
|
||||
function yacht_booking_activate() {
|
||||
// Load installer class
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'includes/class-installer.php';
|
||||
|
||||
$installer = new YachtBooking\Installer();
|
||||
$installer->install();
|
||||
|
||||
// Setup Google Calendar cron jobs
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'integrations/google-calendar/class-sync-controller.php';
|
||||
YachtBooking\Integrations\GoogleCalendar\Sync_Controller::setup_cron();
|
||||
|
||||
// Setup iCal import cron
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'integrations/ical/class-ical-import.php';
|
||||
YachtBooking\Integrations\ICal\ICal_Import::setup_cron();
|
||||
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
register_activation_hook( __FILE__, 'yacht_booking_activate' );
|
||||
|
||||
/**
|
||||
* Plugin deactivation hook
|
||||
*/
|
||||
function yacht_booking_deactivate() {
|
||||
// Clear Google Calendar cron jobs
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'integrations/google-calendar/class-sync-controller.php';
|
||||
YachtBooking\Integrations\GoogleCalendar\Sync_Controller::clear_cron();
|
||||
|
||||
// Clear iCal import cron
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'integrations/ical/class-ical-import.php';
|
||||
YachtBooking\Integrations\ICal\ICal_Import::clear_cron();
|
||||
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
register_deactivation_hook( __FILE__, 'yacht_booking_deactivate' );
|
||||
|
||||
/**
|
||||
* Initialize the plugin
|
||||
*/
|
||||
function yacht_booking_init() {
|
||||
// Load plugin main class
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'includes/class-yacht-booking.php';
|
||||
|
||||
// Get instance
|
||||
YachtBooking\Yacht_Booking::get_instance();
|
||||
}
|
||||
add_action( 'plugins_loaded', 'yacht_booking_init', 10 );
|
||||
|
||||
/**
|
||||
* Load plugin textdomain
|
||||
*/
|
||||
function yacht_booking_load_textdomain() {
|
||||
load_plugin_textdomain(
|
||||
'yacht-booking',
|
||||
false,
|
||||
dirname( YACHT_BOOKING_PLUGIN_BASENAME ) . '/languages'
|
||||
);
|
||||
}
|
||||
add_action( 'init', 'yacht_booking_load_textdomain' );
|
||||
Reference in New Issue
Block a user