first commit

This commit is contained in:
Roman Pyrih
2026-04-21 15:48:41 +02:00
commit 7483681901
10216 changed files with 3236626 additions and 0 deletions

View 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

View File

@@ -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;
}
}

View File

@@ -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);

File diff suppressed because it is too large Load Diff

View File

@@ -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' );
}
}

View File

@@ -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' );
}
}

View File

@@ -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' );
}
}

View File

@@ -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>

View File

@@ -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 );
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 );
}
}

View File

@@ -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' ) );
}
}

View File

@@ -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 : '';
}
}

View File

@@ -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 );
}
}
}

View 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() );
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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 ),
)
);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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 ) );
}
}
}

View 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' );

View 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' );