Add MPHB iCal Sync plugin for automatic iCal synchronization and booking management
- Implement SyncCron class for scheduling iCal sync every 15 minutes. - Create main plugin file with initialization and activation hooks. - Add admin booking modal for managing reservations, including AJAX functionality for searching available rooms and creating bookings. - Include necessary CSS and JS for modal functionality and user interaction. - Ensure compatibility with MotoPress Hotel Booking plugin.
This commit is contained in:
8
.claude/settings.local.json
Normal file
8
.claude/settings.local.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__serena__activate_project",
|
||||
"mcp__serena__check_onboarding_performed"
|
||||
]
|
||||
}
|
||||
}
|
||||
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
||||
126
.serena/project.yml
Normal file
126
.serena/project.yml
Normal file
@@ -0,0 +1,126 @@
|
||||
# the name by which the project can be referenced within Serena
|
||||
project_name: "wrapartamenty.pl"
|
||||
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# matlab nix pascal perl php
|
||||
# php_phpactor powershell python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# swift terraform toml typescript typescript_vts
|
||||
# vue yaml zig
|
||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some languages require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- php
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names to that are always to be included in the set of active modes
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to [] to disable base modes for this project.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
default_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
3
.vscode/ftp-kr.json
vendored
3
.vscode/ftp-kr.json
vendored
@@ -14,6 +14,7 @@
|
||||
".git",
|
||||
"/.vscode",
|
||||
"/.serena",
|
||||
"/.claude"
|
||||
"/.claude",
|
||||
"/docs"
|
||||
]
|
||||
}
|
||||
50
.vscode/ftp-kr.sync.cache.json
vendored
50
.vscode/ftp-kr.sync.cache.json
vendored
@@ -9,6 +9,12 @@
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"PLAN-click-to-block-calendar.md": {
|
||||
"type": "-",
|
||||
"size": 4095,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"archive.zip": {
|
||||
"type": "-",
|
||||
"size": 547064861,
|
||||
@@ -29,9 +35,9 @@
|
||||
},
|
||||
"license.txt": {
|
||||
"type": "-",
|
||||
"size": 19915,
|
||||
"size": 19903,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"phpinfo.php": {
|
||||
"type": "-",
|
||||
@@ -41,15 +47,15 @@
|
||||
},
|
||||
"readme.html": {
|
||||
"type": "-",
|
||||
"size": 7409,
|
||||
"size": 7425,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"wp-activate.php": {
|
||||
"type": "-",
|
||||
"size": 7387,
|
||||
"size": 7349,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"wp-admin": {},
|
||||
"wp-blog-header.php": {
|
||||
@@ -66,9 +72,9 @@
|
||||
},
|
||||
"wp-config-sample.php": {
|
||||
"type": "-",
|
||||
"size": 3336,
|
||||
"size": 3339,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"wp-config.php": {
|
||||
"type": "-",
|
||||
@@ -86,9 +92,9 @@
|
||||
"wp-includes": {},
|
||||
"wp-links-opml.php": {
|
||||
"type": "-",
|
||||
"size": 2502,
|
||||
"size": 2493,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"wp-load.php": {
|
||||
"type": "-",
|
||||
@@ -98,39 +104,39 @@
|
||||
},
|
||||
"wp-login.php": {
|
||||
"type": "-",
|
||||
"size": 51367,
|
||||
"size": 51437,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"wp-mail.php": {
|
||||
"type": "-",
|
||||
"size": 8543,
|
||||
"size": 8727,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"wp-settings.php": {
|
||||
"type": "-",
|
||||
"size": 29032,
|
||||
"size": 31055,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"wp-signup.php": {
|
||||
"type": "-",
|
||||
"size": 34385,
|
||||
"size": 34516,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"wp-trackback.php": {
|
||||
"type": "-",
|
||||
"size": 5102,
|
||||
"size": 5214,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
},
|
||||
"xmlrpc.php": {
|
||||
"type": "-",
|
||||
"size": 3246,
|
||||
"size": 3205,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
"modified": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
docs/changelog.md
Normal file
37
docs/changelog.md
Normal file
@@ -0,0 +1,37 @@
|
||||
## 2026-03-02
|
||||
|
||||
### Admin - MPHB Calendar (Dodaj rezerwacje)
|
||||
- Wdrożono nowy moduł "Dodaj rezerwację" na stronie `admin.php?page=mphb_calendar` (flow 2-krokowy: wyszukanie dostępnych pokoi + zapis właściwej rezerwacji).
|
||||
- Naprawiono obsluge bledow AJAX w modalu tworzenia rezerwacji na stronie `admin.php?page=mphb_calendar`.
|
||||
- Przy wyszukiwaniu wolnych pokoi ("Szukaj wolnych pokoi") komunikaty backendu sa teraz poprawnie wyswietlane w UI zamiast ogolnego "Blad polaczenia z serwerem".
|
||||
- Dla przypadku "brak wynikow" endpoint nie zwraca juz kodu HTTP 404; zwracany jest standardowy blad JSON z czytelnym komunikatem ("Brak dostepnych pokoi dla podanych kryteriow.").
|
||||
- Plik: `wp-content/themes/hello-elementor/includes/wrap-mphb-admin-booking-modal.php`.
|
||||
|
||||
---
|
||||
## 2026-03-02
|
||||
|
||||
### Nowy plugin: MPHB iCal Sync (`wp-content/plugins/mphb-ical-sync/`)
|
||||
- Dodano własny plugin do dwukierunkowej synchronizacji kalendarzy iCal z Booking.com (i innymi kanałami OTA).
|
||||
- **Eksport (MPHB → Booking.com):** endpoint `/?mphb_ical_export=ROOM_ID&token=TOKEN` generuje plik `.ics` z potwierdzonymi rezerwacjami pokoju. Token = `md5(roomId . AUTH_KEY . 'mphb-ical')`.
|
||||
- **Import (Booking.com → MPHB):** pobiera feed iCal z Booking.com i tworzy blokady dat jako posty `mphb_booking` (status `confirmed`) z powiązanym `mphb_reserved_room`. Logika idempotentna — identyfikacja przez meta `_mphb_ical_uid`.
|
||||
- **Cron:** automatyczna synchronizacja co 15 minut przez WP-Cron.
|
||||
- **Panel admin:** strona `admin.php?page=mphb_ical` zastąpiona tabelą wszystkich pokoi z linkami eksportu, URL-ami importu i statusem ostatniej synchronizacji. Metabox „iCal Synchronizacja" na stronie edycji każdego pokoju.
|
||||
- Pliki: `mphb-ical-sync.php`, `includes/Exporter.php`, `includes/Parser.php`, `includes/Importer.php`, `includes/FeedEndpoint.php`, `includes/SyncCron.php`, `includes/AdminUI.php`.
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-02
|
||||
|
||||
### Bugfix
|
||||
- Naprawiono błędy PHP Warning na stronie Raportów (`admin.php?page=mphb_reports`): `Undefined array key "potwierdzone"` i podobne. Przyczyną było iterowanie wartości (przetłumaczonych nazw statusów) zamiast kluczy tablicy `dataTypes` w `wp-content/plugins/motopress-hotel-booking-lite/includes/reports/data/report-earnings-by-dates-data.php:78`. Zmieniono `foreach ($this->getDataTypes() as $dataType)` na `foreach (array_keys($this->getDataTypes()) as $dataType)`.
|
||||
|
||||
---
|
||||
|
||||
### Admin
|
||||
- Zmieniono nazwę pozycji menu "Customers" na "Klienci" (`admin.php?page=mphb_customers`) przez hook `admin_menu` w `wp-content/themes/hello-elementor/functions.php`.
|
||||
|
||||
---
|
||||
|
||||
### Admin
|
||||
- Ukryto podmenu-zaślepkę "Atrybuty" (`edit.php?post_type=mphb_room_type&page=mphb_room_attribute`) z panelu administratora. Strona była nieaktywną pozostałością po mechanizmie premium-gate pluginu MotoPress Hotel Booking Lite (przycisk "Dodaj atrybut" był celowo wyłączony). Zmiana dodana przez hook `admin_menu` w `wp-content/themes/hello-elementor/functions.php`.
|
||||
|
||||
102
docs/intrukcja.md
Normal file
102
docs/intrukcja.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Instrukcja obsługi — wrapartamenty.pl
|
||||
|
||||
---
|
||||
|
||||
## Aktualizacja 2026-03-02 - MPHB Calendar (Dodaj rezerwacje)
|
||||
|
||||
Wdrożono nowy moduł **"Dodaj rezerwację"** na stronie kalendarza MPHB (`admin.php?page=mphb_calendar`) w modelu 2-krokowym:
|
||||
- krok 1: wyszukanie dostępnych pokoi,
|
||||
- krok 2: właściwy formularz i zapis rezerwacji.
|
||||
|
||||
Dodatkowo naprawiono obsluge bledow podczas kroku **"Szukaj wolnych pokoi"**.
|
||||
|
||||
### Co sie zmienilo
|
||||
- Komunikaty bledow z backendu sa teraz pokazywane bezposrednio w oknie modalnym.
|
||||
- Uzytkownik nie widzi juz domyslnego komunikatu "Blad polaczenia z serwerem" dla bledow biznesowych.
|
||||
- Gdy nie ma dostepnych pokoi, wyswietlany jest czytelny komunikat: "Brak dostepnych pokoi dla podanych kryteriow.".
|
||||
|
||||
### Uwagi operacyjne
|
||||
- Po wdrozeniu zmian warto wykonac twarde odswiezenie panelu administracyjnego (`Ctrl+F5`), aby przeladowac zaktualizowany skrypt JavaScript.
|
||||
|
||||
---
|
||||
## Synchronizacja kalendarzy iCal z Booking.com
|
||||
|
||||
Plugin **MPHB iCal Sync** synchronizuje kalendarz dostępności między MPHB a Booking.com (i innymi kanałami OTA) w obu kierunkach.
|
||||
|
||||
---
|
||||
|
||||
### Jak to działa
|
||||
|
||||
| Kierunek | Opis | Częstotliwość |
|
||||
|---|---|---|
|
||||
| MPHB → Booking.com | Plugin generuje URL z plikiem `.ics`. Booking.com pobiera go samodzielnie. | Co kilka godzin (strona Booking.com) |
|
||||
| Booking.com → MPHB | Plugin pobiera feed iCal z Booking.com i tworzy blokady dat w MPHB. | Co 15 minut (WP-Cron) |
|
||||
|
||||
---
|
||||
|
||||
### Konfiguracja — krok po kroku
|
||||
|
||||
#### 1. Eksport: MPHB → Booking.com
|
||||
|
||||
Jednorazowa konfiguracja — Booking.com musi wiedzieć, skąd pobierać nasz kalendarz.
|
||||
|
||||
1. Wejdź na **WP Admin → Zakwaterowania → Synchronizacja kalendarzy** (`admin.php?page=mphb_ical`)
|
||||
2. Znajdź pokój, kliknij **Kopiuj** przy linku eksportu
|
||||
3. Zaloguj się na [extranet.booking.com](https://extranet.booking.com)
|
||||
4. Przejdź do: **Nieruchomość → Synchronizacja kalendarza → Eksportuj do zewnętrznego kalendarza**
|
||||
5. Wklej skopiowany URL i zapisz
|
||||
6. Booking.com będzie od tej pory automatycznie pobierał aktualny kalendarz
|
||||
|
||||
#### 2. Import: Booking.com → MPHB
|
||||
|
||||
Jednorazowa konfiguracja — plugin musi wiedzieć, skąd pobierać kalendarz Booking.com.
|
||||
|
||||
1. Zaloguj się na [extranet.booking.com](https://extranet.booking.com)
|
||||
2. Przejdź do: **Nieruchomość → Synchronizacja kalendarza → Importuj z zewnętrznego kalendarza**
|
||||
3. Skopiuj URL iCal podany przez Booking.com
|
||||
4. Wejdź na **WP Admin → Zakwaterowania → edycja pokoju**
|
||||
5. W metaboxie **„iCal Synchronizacja"** wklej URL w polu „URL importu z Booking.com"
|
||||
6. Zapisz pokój
|
||||
7. Kliknij **„Synchronizuj teraz"** żeby od razu pobrać rezerwacje (nie czekając na cron)
|
||||
|
||||
---
|
||||
|
||||
### Podgląd stanu synchronizacji
|
||||
|
||||
Strona **WP Admin → Zakwaterowania → Synchronizacja kalendarzy** (`admin.php?page=mphb_ical`) pokazuje tabelę wszystkich pokoi:
|
||||
|
||||
- **Link eksportu** — URL do wklejenia w Booking.com (z przyciskiem Kopiuj)
|
||||
- **URL importu** — skonfigurowany feed z Booking.com (lub info o braku)
|
||||
- **Ostatnia synchronizacja** — data, godzina i wynik (✓ OK / ✗ Błąd)
|
||||
- **Przycisk Sync** — ręczna synchronizacja bez czekania na cron
|
||||
|
||||
---
|
||||
|
||||
### Jak działają blokady z Booking.com
|
||||
|
||||
Rezerwacje zaimportowane z Booking.com są zapisywane jako specjalne **blokady dat** w MPHB:
|
||||
- Widoczne w kalendarzu MPHB tak samo jak normalne rezerwacje
|
||||
- Oznaczone jako `_mphb_ical_source = booking.com`
|
||||
- Automatycznie **usuwane** jeśli rezerwacja zniknie z feedu Booking.com (anulowanie)
|
||||
- Nie są pełnymi rezerwacjami — nie mają danych gościa, płatności itp.
|
||||
|
||||
---
|
||||
|
||||
### Rozwiązywanie problemów
|
||||
|
||||
**Zmiany w MPHB nie pojawiają się w Booking.com**
|
||||
→ Booking.com pobiera nasz kalendarz co kilka godzin. Zmiany pojawią się z opóźnieniem. Można wymusić odświeżenie ręcznie w Booking.com extranet.
|
||||
|
||||
**Rezerwacje z Booking.com nie pojawiają się w MPHB**
|
||||
→ Sprawdź czy URL importu jest poprawnie wklejony w edycji pokoju. Kliknij „Synchronizuj teraz" i sprawdź status.
|
||||
|
||||
**WP-Cron nie działa**
|
||||
→ Sprawdź przez WP Crontrol (`Narzędzia → Cron Events`) czy event `mphb_ical_sync_cron` jest zaplanowany. Jeśli serwer wyłącza WP-Cron, dodaj do `wp-config.php`:
|
||||
```php
|
||||
define('DISABLE_WP_CRON', true);
|
||||
```
|
||||
i skonfiguruj prawdziwy cron systemowy:
|
||||
```
|
||||
*/15 * * * * wget -q -O - https://wrapartamenty.pl/wp-cron.php?doing_wp_cron > /dev/null 2>&1
|
||||
```
|
||||
|
||||
@@ -75,7 +75,7 @@ class ReportEarningsByDatesData extends ReportByDatesData {
|
||||
* @return array
|
||||
*/
|
||||
private function requestBookings( $args ) {
|
||||
foreach ( $this->getDataTypes() as $dataType ) {
|
||||
foreach ( array_keys( $this->getDataTypes() ) as $dataType ) {
|
||||
$bookingsData[ $dataType ] = array();
|
||||
}
|
||||
|
||||
|
||||
386
wp-content/plugins/mphb-ical-sync/includes/AdminUI.php
Normal file
386
wp-content/plugins/mphb-ical-sync/includes/AdminUI.php
Normal file
@@ -0,0 +1,386 @@
|
||||
<?php
|
||||
|
||||
namespace MphbIcalSync;
|
||||
|
||||
/**
|
||||
* Panel administracyjny iCal:
|
||||
* – metabox na stronie edycji pokoju (mphb_room)
|
||||
* – pełna strona pod admin.php?page=mphb_ical (przejmuje hook MPHB)
|
||||
*/
|
||||
class AdminUI {
|
||||
|
||||
const NONCE_FIELD = 'mphb_ical_sync_nonce';
|
||||
const NONCE_ACTION = 'mphb_ical_sync_save';
|
||||
const NONCE_AJAX = 'mphb_ical_sync_ajax';
|
||||
|
||||
public function __construct() {
|
||||
// Metabox na stronie pokoju
|
||||
add_action( 'add_meta_boxes', [ $this, 'addMetaBox' ] );
|
||||
add_action( 'save_post_mphb_room', [ $this, 'saveMetaBox' ], 10, 1 );
|
||||
|
||||
// AJAX sync
|
||||
add_action( 'wp_ajax_mphb_ical_sync_room', [ $this, 'ajaxSyncRoom' ] );
|
||||
|
||||
// Przejęcie strony mphb_ical — bardzo późny priorytet (po MPHB)
|
||||
add_action( 'admin_menu', [ $this, 'overrideIcalPage' ], 9999 );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Przejęcie strony admin.php?page=mphb_ical
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function overrideIcalPage(): void {
|
||||
// WordPress rejestruje callback strony jako akcję o nazwie zwróconej
|
||||
// przez add_submenu_page(). Dla rodzica 'mphb_booking_menu' i sluga 'mphb_ical'
|
||||
// ta nazwa to wynik get_plugin_page_hookname().
|
||||
$hookname = get_plugin_page_hookname( 'mphb_ical', 'mphb_booking_menu' );
|
||||
|
||||
if ( ! $hookname ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Usuń MPHB-owy render (który jest pusty), dodaj nasz
|
||||
remove_all_actions( $hookname );
|
||||
add_action( $hookname, [ $this, 'renderIcalPage' ] );
|
||||
}
|
||||
|
||||
public function renderIcalPage(): void {
|
||||
$rooms = get_posts( [
|
||||
'post_type' => 'mphb_room',
|
||||
'post_status' => 'publish',
|
||||
'numberposts' => -1,
|
||||
'no_found_rows' => true,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
] );
|
||||
$syncRepo = MPHB()->getSyncUrlsRepository();
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1 class="wp-heading-inline">Synchronizacja kalendarzy iCal</h1>
|
||||
<hr class="wp-header-end" />
|
||||
|
||||
<?php if ( empty( $rooms ) ) : ?>
|
||||
<p>Brak opublikowanych pokoi.</p>
|
||||
<?php else : ?>
|
||||
|
||||
<style>
|
||||
#mphb-ical-rooms-table { border-collapse: collapse; width: 100%; margin-top: 16px; background: #fff; }
|
||||
#mphb-ical-rooms-table th,
|
||||
#mphb-ical-rooms-table td { padding: 9px 13px; border: 1px solid #dcdcde; vertical-align: middle; font-size: 13px; }
|
||||
#mphb-ical-rooms-table th { background: #f6f7f7; font-weight: 600; text-align: left; }
|
||||
#mphb-ical-rooms-table tr:hover td { background: #f9f9f9; }
|
||||
#mphb-ical-rooms-table .url-mono { font-family: monospace; font-size: 11px; word-break: break-all; }
|
||||
#mphb-ical-rooms-table .no-val { color: #999; font-style: italic; }
|
||||
.mphb-ical-status-ok { color: #1a7a1a; font-weight: 600; }
|
||||
.mphb-ical-status-err { color: #c33; font-weight: 600; }
|
||||
</style>
|
||||
|
||||
<p>
|
||||
<strong>Eksport:</strong> skopiuj link i wklej w Booking.com → Ekstranet → Nieruchomość → Synchronizacja kalendarza → Eksportuj.<br>
|
||||
<strong>Import:</strong> URL iCal z Booking.com wklej na stronie edycji pokoju (metabox „iCal Synchronizacja").
|
||||
</p>
|
||||
|
||||
<table id="mphb-ical-rooms-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:200px;">Pokój</th>
|
||||
<th>Link eksportu (→ Booking.com)</th>
|
||||
<th>URL importu (← Booking.com)</th>
|
||||
<th style="width:170px;">Ostatnia synchronizacja</th>
|
||||
<th style="width:120px;">Akcje</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $rooms as $room ) :
|
||||
$roomId = $room->ID;
|
||||
$exportUrl = FeedEndpoint::getExportUrl( $roomId );
|
||||
$importUrls = array_values( $syncRepo->getUrls( $roomId ) );
|
||||
$editUrl = get_edit_post_link( $roomId );
|
||||
$lastSync = get_post_meta( $roomId, '_mphb_ical_last_sync', true );
|
||||
$lastStatus = get_post_meta( $roomId, '_mphb_ical_last_status', true );
|
||||
$lastResult = get_post_meta( $roomId, '_mphb_ical_last_result', true );
|
||||
$inputId = 'mphb-ical-exp-' . $roomId;
|
||||
$nonce = wp_create_nonce( self::NONCE_AJAX );
|
||||
?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="<?php echo esc_url( $editUrl ); ?>" style="font-weight:600;">
|
||||
<?php echo esc_html( $room->post_title ); ?>
|
||||
</a>
|
||||
<br><small style="color:#999;">ID: <?php echo $roomId; ?></small>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div style="display:flex;gap:6px;align-items:flex-start;">
|
||||
<input type="text"
|
||||
id="<?php echo esc_attr( $inputId ); ?>"
|
||||
value="<?php echo esc_attr( $exportUrl ); ?>"
|
||||
readonly onclick="this.select()"
|
||||
class="url-mono"
|
||||
style="flex:1;padding:3px 6px;" />
|
||||
<button type="button" class="button button-small"
|
||||
onclick="(function(id){var el=document.getElementById(id);el.select();navigator.clipboard.writeText(el.value).then(function(){});})('<?php echo esc_js( $inputId ); ?>')">
|
||||
Kopiuj
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<?php if ( empty( $importUrls ) ) : ?>
|
||||
<span class="no-val">— brak —
|
||||
<a href="<?php echo esc_url( $editUrl ); ?>" style="font-size:11px;">dodaj w edycji pokoju</a>
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<?php foreach ( $importUrls as $url ) : ?>
|
||||
<div class="url-mono"><?php echo esc_html( $url ); ?></div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<?php if ( $lastSync ) :
|
||||
$timeStr = wp_date( 'd.m.Y H:i', (int) $lastSync );
|
||||
$cls = $lastStatus === 'ok' ? 'mphb-ical-status-ok' : 'mphb-ical-status-err';
|
||||
$icon = $lastStatus === 'ok' ? '✓' : '✗';
|
||||
?>
|
||||
<span class="<?php echo esc_attr( $cls ); ?>">
|
||||
<?php echo esc_html( $icon . ' ' . $timeStr ); ?>
|
||||
</span>
|
||||
<?php if ( is_array( $lastResult ) ) : ?>
|
||||
<br><small style="color:#888;">
|
||||
+<?php echo (int) ( $lastResult['created'] ?? 0 ); ?> nowych,
|
||||
−<?php echo (int) ( $lastResult['deleted'] ?? 0 ); ?> usuniętych
|
||||
</small>
|
||||
<?php endif; ?>
|
||||
<?php else : ?>
|
||||
<span class="no-val">nie uruchamiano</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<?php if ( ! empty( $importUrls ) ) : ?>
|
||||
<button type="button" class="button mphb-ical-sync-btn"
|
||||
data-room-id="<?php echo esc_attr( $roomId ); ?>"
|
||||
data-nonce="<?php echo esc_attr( $nonce ); ?>">
|
||||
🔄 Sync
|
||||
</button>
|
||||
<span class="mphb-ical-row-status" style="display:block;font-size:11px;margin-top:4px;"></span>
|
||||
<?php else : ?>
|
||||
<span class="no-val">—</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p style="margin-top:12px;color:#666;font-size:12px;">
|
||||
⏰ Automatyczna synchronizacja uruchamia się co 15 minut przez WP-Cron.
|
||||
</p>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
document.querySelectorAll('.mphb-ical-sync-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
var status = btn.nextElementSibling;
|
||||
status.style.color = '#888';
|
||||
status.textContent = 'Synchronizuję\u2026';
|
||||
btn.disabled = true;
|
||||
|
||||
var fd = new FormData();
|
||||
fd.append('action', 'mphb_ical_sync_room');
|
||||
fd.append('room_id', btn.dataset.roomId);
|
||||
fd.append('nonce', btn.dataset.nonce);
|
||||
|
||||
fetch(ajaxurl, { method: 'POST', body: fd })
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(r) {
|
||||
if (r.success) {
|
||||
status.style.color = '#1a7a1a';
|
||||
status.textContent = '\u2713 +' + r.data.created + ' / \u2212' + r.data.deleted;
|
||||
} else {
|
||||
status.style.color = '#c33';
|
||||
status.textContent = '\u2717 ' + (r.data || 'b\u0142\u0105d');
|
||||
}
|
||||
btn.disabled = false;
|
||||
})
|
||||
.catch(function() {
|
||||
status.style.color = '#c33';
|
||||
status.textContent = '\u2717 b\u0142\u0105d po\u0142\u0105czenia';
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Metabox na stronie edycji pokoju
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function addMetaBox(): void {
|
||||
add_meta_box(
|
||||
'mphb_ical_sync',
|
||||
'iCal Synchronizacja (Booking.com)',
|
||||
[ $this, 'renderMetaBox' ],
|
||||
'mphb_room',
|
||||
'normal',
|
||||
'default'
|
||||
);
|
||||
}
|
||||
|
||||
public function renderMetaBox( \WP_Post $post ): void {
|
||||
$roomId = $post->ID;
|
||||
$exportUrl = FeedEndpoint::getExportUrl( $roomId );
|
||||
$syncRepo = MPHB()->getSyncUrlsRepository();
|
||||
$urls = $syncRepo->getUrls( $roomId );
|
||||
$importUrls = implode( "\n", array_values( $urls ) );
|
||||
|
||||
$lastSync = get_post_meta( $roomId, '_mphb_ical_last_sync', true );
|
||||
$lastStatus = get_post_meta( $roomId, '_mphb_ical_last_status', true );
|
||||
$lastResult = get_post_meta( $roomId, '_mphb_ical_last_result', true );
|
||||
|
||||
wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD );
|
||||
|
||||
echo '<style>
|
||||
#mphb_ical_sync .mphb-section { margin-bottom: 14px; }
|
||||
#mphb_ical_sync .mphb-section h4 { margin: 0 0 5px; font-size: 13px; font-weight: 600; }
|
||||
#mphb_ical_sync .mphb-export-row { display: flex; gap: 8px; align-items: center; }
|
||||
#mphb_ical_sync .mphb-export-row input { flex: 1; font-family: monospace; font-size: 11px; }
|
||||
#mphb_ical_sync .description { color: #666; font-size: 12px; margin-top: 4px; }
|
||||
</style>';
|
||||
|
||||
// Eksport
|
||||
echo '<div class="mphb-section">';
|
||||
echo '<h4>📤 Link eksportu (dla Booking.com)</h4>';
|
||||
echo '<div class="mphb-export-row">';
|
||||
$inputId = 'mphb-ical-exp-meta-' . $roomId;
|
||||
echo '<input type="text" readonly id="' . esc_attr( $inputId ) . '" value="' . esc_attr( $exportUrl ) . '" onclick="this.select()" />';
|
||||
echo '<button type="button" class="button" onclick="(function(id){var el=document.getElementById(id);el.select();navigator.clipboard.writeText(el.value).then(function(){});})(' . wp_json_encode( $inputId ) . ')">Kopiuj</button>';
|
||||
echo '</div>';
|
||||
echo '<p class="description">Wklej w Booking.com: Ekstranet → Nieruchomość → Synchronizacja kalendarza → Eksportuj.</p>';
|
||||
echo '</div>';
|
||||
|
||||
// Import
|
||||
echo '<div class="mphb-section">';
|
||||
echo '<h4>📥 URL importu z Booking.com</h4>';
|
||||
echo '<textarea name="mphb_ical_import_urls" rows="3" class="large-text" style="font-family:monospace;font-size:11px;">' . esc_textarea( $importUrls ) . '</textarea>';
|
||||
echo '<p class="description">URL iCal z Booking.com (jeden na linię). Znajdziesz go w Booking.com: Ekstranet → Synchronizacja kalendarza → Importuj.</p>';
|
||||
echo '</div>';
|
||||
|
||||
// Status
|
||||
echo '<div class="mphb-section">';
|
||||
if ( $lastSync ) {
|
||||
$timeStr = wp_date( 'd.m.Y H:i', (int) $lastSync );
|
||||
$icon = $lastStatus === 'ok' ? '✓ OK' : '✗ Błąd';
|
||||
echo '<p style="margin:0 0 6px;">Ostatnia sync: <strong>' . esc_html( $timeStr ) . '</strong> — ' . esc_html( $icon );
|
||||
if ( is_array( $lastResult ) ) {
|
||||
echo ' (+' . (int) ( $lastResult['created'] ?? 0 ) . ' / −' . (int) ( $lastResult['deleted'] ?? 0 ) . ')';
|
||||
}
|
||||
echo '</p>';
|
||||
} else {
|
||||
echo '<p style="margin:0 0 6px;color:#666;">Synchronizacja jeszcze nie była uruchamiana.</p>';
|
||||
}
|
||||
$nonce = wp_create_nonce( self::NONCE_AJAX );
|
||||
echo '<button type="button" class="button" id="mphb-ical-meta-sync" data-room-id="' . esc_attr( $roomId ) . '" data-nonce="' . esc_attr( $nonce ) . '">🔄 Synchronizuj teraz</button>';
|
||||
echo ' <span id="mphb-ical-meta-status" style="font-size:12px;margin-left:8px;"></span>';
|
||||
echo '</div>';
|
||||
|
||||
?>
|
||||
<script>
|
||||
(function() {
|
||||
var btn = document.getElementById('mphb-ical-meta-sync');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', function() {
|
||||
var status = document.getElementById('mphb-ical-meta-status');
|
||||
status.textContent = 'Synchronizuję\u2026';
|
||||
btn.disabled = true;
|
||||
var fd = new FormData();
|
||||
fd.append('action', 'mphb_ical_sync_room');
|
||||
fd.append('room_id', btn.dataset.roomId);
|
||||
fd.append('nonce', btn.dataset.nonce);
|
||||
fetch(ajaxurl, {method:'POST',body:fd})
|
||||
.then(function(r){return r.json();})
|
||||
.then(function(r){
|
||||
if(r.success){
|
||||
status.style.color='#1a7a1a';
|
||||
status.textContent='\u2713 +'+r.data.created+' / \u2212'+r.data.deleted;
|
||||
} else {
|
||||
status.style.color='#c33';
|
||||
status.textContent='\u2717 '+(r.data||'b\u0142\u0105d');
|
||||
}
|
||||
btn.disabled=false;
|
||||
})
|
||||
.catch(function(){
|
||||
status.style.color='#c33';
|
||||
status.textContent='\u2717 b\u0142\u0105d po\u0142\u0105czenia';
|
||||
btn.disabled=false;
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<?php
|
||||
}
|
||||
|
||||
public function saveMetaBox( int $postId ): void {
|
||||
if ( ! isset( $_POST[ self::NONCE_FIELD ] ) ) {
|
||||
return;
|
||||
}
|
||||
if ( ! wp_verify_nonce(
|
||||
sanitize_text_field( wp_unslash( $_POST[ self::NONCE_FIELD ] ) ),
|
||||
self::NONCE_ACTION
|
||||
) ) {
|
||||
return;
|
||||
}
|
||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||||
return;
|
||||
}
|
||||
if ( ! current_user_can( 'edit_post', $postId ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$raw = isset( $_POST['mphb_ical_import_urls'] )
|
||||
? sanitize_textarea_field( wp_unslash( $_POST['mphb_ical_import_urls'] ) )
|
||||
: '';
|
||||
|
||||
$validUrls = [];
|
||||
foreach ( array_filter( array_map( 'trim', explode( "\n", $raw ) ) ) as $url ) {
|
||||
$url = esc_url_raw( $url );
|
||||
if ( filter_var( $url, FILTER_VALIDATE_URL ) ) {
|
||||
$validUrls[] = $url;
|
||||
}
|
||||
}
|
||||
|
||||
MPHB()->getSyncUrlsRepository()->updateUrls( $postId, $validUrls );
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// AJAX
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public function ajaxSyncRoom(): void {
|
||||
check_ajax_referer( self::NONCE_AJAX, 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
wp_send_json_error( 'Brak uprawnień' );
|
||||
}
|
||||
|
||||
$roomId = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||
if ( ! $roomId ) {
|
||||
wp_send_json_error( 'Nieprawidłowy ID pokoju' );
|
||||
}
|
||||
|
||||
$result = SyncCron::syncRoom( $roomId );
|
||||
|
||||
if ( isset( $result['error'] ) ) {
|
||||
wp_send_json_error( $result['error'] );
|
||||
}
|
||||
|
||||
wp_send_json_success( $result );
|
||||
}
|
||||
}
|
||||
103
wp-content/plugins/mphb-ical-sync/includes/Exporter.php
Normal file
103
wp-content/plugins/mphb-ical-sync/includes/Exporter.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace MphbIcalSync;
|
||||
|
||||
/**
|
||||
* Generuje plik VCALENDAR z potwierdzonymi rezerwacjami danego pokoju.
|
||||
*/
|
||||
class Exporter {
|
||||
|
||||
public function export( int $roomId ): string {
|
||||
$events = $this->getEventsForRoom( $roomId );
|
||||
return $this->buildIcal( $roomId, $events );
|
||||
}
|
||||
|
||||
private function getEventsForRoom( int $roomId ): array {
|
||||
// Znajdź wszystkie mphb_reserved_room powiązane z tym pokojem
|
||||
$reservedRoomIds = get_posts( [
|
||||
'post_type' => 'mphb_reserved_room',
|
||||
'meta_key' => '_mphb_room_id',
|
||||
'meta_value' => $roomId,
|
||||
'fields' => 'ids',
|
||||
'numberposts' => -1,
|
||||
'post_status' => 'any',
|
||||
'no_found_rows' => true,
|
||||
] );
|
||||
|
||||
if ( empty( $reservedRoomIds ) ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Zbierz unikalne booking IDs
|
||||
$bookingIds = [];
|
||||
foreach ( $reservedRoomIds as $reservedRoomId ) {
|
||||
$bookingId = wp_get_post_parent_id( $reservedRoomId );
|
||||
if ( $bookingId && ! in_array( $bookingId, $bookingIds, true ) ) {
|
||||
$bookingIds[] = $bookingId;
|
||||
}
|
||||
}
|
||||
|
||||
$host = parse_url( home_url(), PHP_URL_HOST );
|
||||
$events = [];
|
||||
|
||||
foreach ( $bookingIds as $bookingId ) {
|
||||
$booking = get_post( $bookingId );
|
||||
if ( ! $booking || $booking->post_status !== 'confirmed' ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$checkIn = get_post_meta( $bookingId, 'mphb_check_in_date', true );
|
||||
$checkOut = get_post_meta( $bookingId, 'mphb_check_out_date', true );
|
||||
|
||||
if ( ! $checkIn || ! $checkOut ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$events[] = [
|
||||
'uid' => $bookingId . '@' . $host,
|
||||
'dtstart' => $checkIn,
|
||||
'dtend' => $checkOut,
|
||||
'summary' => 'Zarezerwowane',
|
||||
];
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
private function buildIcal( int $roomId, array $events ): string {
|
||||
$now = gmdate( 'Ymd\THis\Z' );
|
||||
$lines = [
|
||||
'BEGIN:VCALENDAR',
|
||||
'VERSION:2.0',
|
||||
'PRODID:-//wrapartamenty.pl//MPHB iCal Sync ' . MPHB_ICAL_SYNC_VERSION . '//PL',
|
||||
'CALSCALE:GREGORIAN',
|
||||
'METHOD:PUBLISH',
|
||||
'X-WR-CALNAME:Room ' . $roomId,
|
||||
];
|
||||
|
||||
foreach ( $events as $event ) {
|
||||
$dtstart = str_replace( '-', '', $event['dtstart'] );
|
||||
$dtend = str_replace( '-', '', $event['dtend'] );
|
||||
|
||||
$lines[] = 'BEGIN:VEVENT';
|
||||
$lines[] = 'UID:' . $event['uid'];
|
||||
$lines[] = 'DTSTAMP:' . $now;
|
||||
$lines[] = 'DTSTART;VALUE=DATE:' . $dtstart;
|
||||
$lines[] = 'DTEND;VALUE=DATE:' . $dtend;
|
||||
$lines[] = 'SUMMARY:' . $this->escapeIcalText( $event['summary'] );
|
||||
$lines[] = 'END:VEVENT';
|
||||
}
|
||||
|
||||
$lines[] = 'END:VCALENDAR';
|
||||
|
||||
return implode( "\r\n", $lines ) . "\r\n";
|
||||
}
|
||||
|
||||
private function escapeIcalText( string $text ): string {
|
||||
$text = str_replace( '\\', '\\\\', $text );
|
||||
$text = str_replace( ';', '\;', $text );
|
||||
$text = str_replace( ',', '\,', $text );
|
||||
$text = str_replace( "\n", '\\n', $text );
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
85
wp-content/plugins/mphb-ical-sync/includes/FeedEndpoint.php
Normal file
85
wp-content/plugins/mphb-ical-sync/includes/FeedEndpoint.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace MphbIcalSync;
|
||||
|
||||
/**
|
||||
* Obsługuje endpoint eksportu iCal: /?mphb_ical_export=ROOM_ID&token=TOKEN
|
||||
*/
|
||||
class FeedEndpoint {
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'template_redirect', [ $this, 'handleRequest' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generuje token bezpieczeństwa dla danego pokoju.
|
||||
* Token jest deterministyczny — można go odtworzyć znając room_id.
|
||||
*/
|
||||
public static function getToken( int $roomId ): string {
|
||||
return md5( $roomId . AUTH_KEY . 'mphb-ical' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca pełny URL eksportu dla danego pokoju.
|
||||
*/
|
||||
public static function getExportUrl( int $roomId ): string {
|
||||
return add_query_arg(
|
||||
[
|
||||
'mphb_ical_export' => $roomId,
|
||||
'token' => self::getToken( $roomId ),
|
||||
],
|
||||
home_url( '/' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obsługuje żądanie eksportu — wywołuje eksporter i wysyła plik .ics.
|
||||
*/
|
||||
public function handleRequest(): void {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification
|
||||
$roomId = isset( $_GET['mphb_ical_export'] ) ? absint( $_GET['mphb_ical_export'] ) : 0;
|
||||
|
||||
if ( ! $roomId ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Weryfikacja tokenu
|
||||
// phpcs:ignore WordPress.Security.NonceVerification
|
||||
$token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : '';
|
||||
$expected = self::getToken( $roomId );
|
||||
|
||||
if ( ! hash_equals( $expected, $token ) ) {
|
||||
status_header( 403 );
|
||||
nocache_headers();
|
||||
exit( 'Forbidden: invalid token.' );
|
||||
}
|
||||
|
||||
// Sprawdź czy pokój istnieje
|
||||
$room = get_post( $roomId );
|
||||
if ( ! $room || $room->post_type !== 'mphb_room' ) {
|
||||
status_header( 404 );
|
||||
exit( 'Room not found.' );
|
||||
}
|
||||
|
||||
$exporter = new Exporter();
|
||||
$ical = $exporter->export( $roomId );
|
||||
|
||||
// DEBUG: ?debug=1 wyświetla zawartość zamiast pobierać plik
|
||||
// phpcs:ignore WordPress.Security.NonceVerification
|
||||
if ( isset( $_GET['debug'] ) ) {
|
||||
nocache_headers();
|
||||
header( 'Content-Type: text/plain; charset=utf-8' );
|
||||
echo "=== DEBUG: iCal export dla room_id={$roomId} ===\n\n";
|
||||
echo $ical;
|
||||
exit;
|
||||
}
|
||||
|
||||
nocache_headers();
|
||||
header( 'Content-Type: text/calendar; charset=utf-8' );
|
||||
header( 'Content-Disposition: attachment; filename="room-' . $roomId . '.ics"' );
|
||||
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $ical;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
154
wp-content/plugins/mphb-ical-sync/includes/Importer.php
Normal file
154
wp-content/plugins/mphb-ical-sync/includes/Importer.php
Normal file
@@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace MphbIcalSync;
|
||||
|
||||
/**
|
||||
* Tworzy i usuwa blokady dat w MPHB na podstawie eventów z iCal.
|
||||
* Logika jest idempotentna — każdy event identyfikowany jest przez UID.
|
||||
*/
|
||||
class Importer {
|
||||
|
||||
/**
|
||||
* Synchronizuje eventy z feedu z istniejącymi blokadami dla danego pokoju.
|
||||
*
|
||||
* @param int $roomId ID pokoju (mphb_room)
|
||||
* @param array[] $events Sparsowane eventy z Parser::parse()
|
||||
* @return array Statystyki: created, deleted, skipped
|
||||
*/
|
||||
public function sync( int $roomId, array $events ): array {
|
||||
$result = [
|
||||
'created' => 0,
|
||||
'deleted' => 0,
|
||||
'skipped' => 0,
|
||||
];
|
||||
|
||||
$eventUids = array_column( $events, 'uid' );
|
||||
$existingBlocks = $this->getExistingBlocks( $roomId );
|
||||
$existingUids = array_keys( $existingBlocks );
|
||||
|
||||
// Usuń blokady których UID zniknął z feedu (anulowane/usunięte)
|
||||
$toDelete = array_diff( $existingUids, $eventUids );
|
||||
foreach ( $toDelete as $uid ) {
|
||||
$this->deleteBlock( $existingBlocks[ $uid ] );
|
||||
$result['deleted']++;
|
||||
}
|
||||
|
||||
// Utwórz nowe blokady dla nowych UID-ów
|
||||
$toCreate = array_flip( array_diff( $eventUids, $existingUids ) );
|
||||
foreach ( $events as $event ) {
|
||||
if ( ! isset( $toCreate[ $event['uid'] ] ) ) {
|
||||
$result['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$created = $this->createBlock( $roomId, $event );
|
||||
if ( $created ) {
|
||||
$result['created']++;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca istniejące blokady importu dla pokoju: [uid => booking_id]
|
||||
*/
|
||||
private function getExistingBlocks( int $roomId ): array {
|
||||
// Znajdź wszystkie rezerwacje z flagą importu powiązane z tym pokojem
|
||||
$reservedRooms = get_posts( [
|
||||
'post_type' => 'mphb_reserved_room',
|
||||
'meta_key' => '_mphb_room_id',
|
||||
'meta_value' => $roomId,
|
||||
'fields' => 'ids',
|
||||
'numberposts' => -1,
|
||||
'post_status' => 'any',
|
||||
'no_found_rows' => true,
|
||||
] );
|
||||
|
||||
$blocks = [];
|
||||
|
||||
foreach ( $reservedRooms as $reservedRoomId ) {
|
||||
$bookingId = wp_get_post_parent_id( $reservedRoomId );
|
||||
if ( ! $bookingId ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Sprawdź czy to jest import (a nie oryginalna rezerwacja MPHB)
|
||||
$isImport = get_post_meta( $bookingId, '_mphb_ical_import', true );
|
||||
if ( ! $isImport ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$uid = get_post_meta( $bookingId, '_mphb_ical_uid', true );
|
||||
if ( $uid ) {
|
||||
$blocks[ $uid ] = $bookingId;
|
||||
}
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tworzy blokadę dat: mphb_booking + mphb_reserved_room (child).
|
||||
*/
|
||||
private function createBlock( int $roomId, array $event ): bool {
|
||||
// 1. Utwórz post mphb_booking
|
||||
$bookingId = wp_insert_post( [
|
||||
'post_type' => 'mphb_booking',
|
||||
'post_status' => 'confirmed',
|
||||
'post_title' => 'Booking.com – ' . $event['uid'],
|
||||
'post_author' => 0,
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $bookingId ) || ! $bookingId ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Daty rezerwacji (format Y-m-d, bez prefiksu _)
|
||||
update_post_meta( $bookingId, 'mphb_check_in_date', sanitize_text_field( $event['dtstart'] ) );
|
||||
update_post_meta( $bookingId, 'mphb_check_out_date', sanitize_text_field( $event['dtend'] ) );
|
||||
|
||||
// Metadane importu
|
||||
update_post_meta( $bookingId, '_mphb_ical_import', '1' );
|
||||
update_post_meta( $bookingId, '_mphb_ical_uid', sanitize_text_field( $event['uid'] ) );
|
||||
update_post_meta( $bookingId, '_mphb_ical_source', 'booking.com' );
|
||||
|
||||
// 2. Utwórz post mphb_reserved_room jako dziecko bookingu
|
||||
$reservedRoomId = wp_insert_post( [
|
||||
'post_type' => 'mphb_reserved_room',
|
||||
'post_status' => 'confirmed',
|
||||
'post_title' => 'Reserved Room',
|
||||
'post_parent' => $bookingId,
|
||||
'post_author' => 0,
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $reservedRoomId ) || ! $reservedRoomId ) {
|
||||
wp_delete_post( $bookingId, true );
|
||||
return false;
|
||||
}
|
||||
|
||||
update_post_meta( $reservedRoomId, '_mphb_room_id', $roomId );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Usuwa blokadę: najpierw reserved_rooms, potem booking.
|
||||
*/
|
||||
private function deleteBlock( int $bookingId ): void {
|
||||
$reservedRooms = get_posts( [
|
||||
'post_type' => 'mphb_reserved_room',
|
||||
'post_parent' => $bookingId,
|
||||
'fields' => 'ids',
|
||||
'numberposts' => -1,
|
||||
'post_status' => 'any',
|
||||
'no_found_rows' => true,
|
||||
] );
|
||||
|
||||
foreach ( $reservedRooms as $reservedRoomId ) {
|
||||
wp_delete_post( $reservedRoomId, true );
|
||||
}
|
||||
|
||||
wp_delete_post( $bookingId, true );
|
||||
}
|
||||
}
|
||||
144
wp-content/plugins/mphb-ical-sync/includes/Parser.php
Normal file
144
wp-content/plugins/mphb-ical-sync/includes/Parser.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace MphbIcalSync;
|
||||
|
||||
/**
|
||||
* Parser plików iCal (.ics) zgodny z RFC 5545.
|
||||
* Nie wymaga zewnętrznych bibliotek.
|
||||
*/
|
||||
class Parser {
|
||||
|
||||
/**
|
||||
* Parsuje zawartość pliku .ics i zwraca tablicę eventów.
|
||||
*
|
||||
* @param string $icsContent Zawartość pliku .ics
|
||||
* @return array[] Tablica eventów: [uid, dtstart, dtend, summary, status]
|
||||
*/
|
||||
public function parse( string $icsContent ): array {
|
||||
// Unfold lines (RFC 5545: linia złożona z CRLF + spacja/tab jest kontynuacją)
|
||||
$icsContent = preg_replace( "/\r\n[ \t]/", '', $icsContent );
|
||||
$icsContent = preg_replace( "/\n[ \t]/", '', $icsContent );
|
||||
|
||||
$lines = preg_split( "/\r\n|\n|\r/", $icsContent );
|
||||
|
||||
$events = [];
|
||||
$inEvent = false;
|
||||
$current = [];
|
||||
|
||||
foreach ( $lines as $line ) {
|
||||
$line = trim( $line );
|
||||
|
||||
if ( $line === 'BEGIN:VEVENT' ) {
|
||||
$inEvent = true;
|
||||
$current = [];
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $line === 'END:VEVENT' ) {
|
||||
$inEvent = false;
|
||||
if ( ! empty( $current ) ) {
|
||||
$event = $this->parseEvent( $current );
|
||||
if ( $event !== null ) {
|
||||
$events[] = $event;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $inEvent && $line !== '' ) {
|
||||
$current[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
return $events;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $lines Linie z wnętrza VEVENT
|
||||
* @return array|null Sparsowany event lub null (jeśli CANCELLED/niepełny)
|
||||
*/
|
||||
private function parseEvent( array $lines ): ?array {
|
||||
$event = [
|
||||
'uid' => '',
|
||||
'dtstart' => '',
|
||||
'dtend' => '',
|
||||
'summary' => '',
|
||||
'status' => '',
|
||||
];
|
||||
|
||||
foreach ( $lines as $line ) {
|
||||
$colonPos = strpos( $line, ':' );
|
||||
if ( $colonPos === false ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = strtoupper( substr( $line, 0, $colonPos ) );
|
||||
$value = substr( $line, $colonPos + 1 );
|
||||
|
||||
// Oddziel parametry od nazwy właściwości (DTSTART;VALUE=DATE → DTSTART)
|
||||
$semiPos = strpos( $key, ';' );
|
||||
$baseKey = $semiPos !== false ? substr( $key, 0, $semiPos ) : $key;
|
||||
|
||||
switch ( $baseKey ) {
|
||||
case 'UID':
|
||||
$event['uid'] = trim( $value );
|
||||
break;
|
||||
case 'DTSTART':
|
||||
$event['dtstart'] = $this->parseDate( $value );
|
||||
break;
|
||||
case 'DTEND':
|
||||
$event['dtend'] = $this->parseDate( $value );
|
||||
break;
|
||||
case 'SUMMARY':
|
||||
$event['summary'] = $this->unescapeIcalText( $value );
|
||||
break;
|
||||
case 'STATUS':
|
||||
$event['status'] = strtoupper( trim( $value ) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Pomiń anulowane eventy
|
||||
if ( $event['status'] === 'CANCELLED' ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pomiń eventy bez wymaganych pól
|
||||
if ( empty( $event['uid'] ) || empty( $event['dtstart'] ) || empty( $event['dtend'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konwertuje datę iCal do formatu Y-m-d.
|
||||
* Obsługuje: YYYYMMDD, YYYYMMDDTHHmmss, YYYYMMDDTHHmmssZ
|
||||
*/
|
||||
private function parseDate( string $value ): string {
|
||||
$value = trim( $value );
|
||||
|
||||
// Usuń część czasową i strefę czasową (Z lub identyfikator)
|
||||
$tPos = strpos( $value, 'T' );
|
||||
if ( $tPos !== false ) {
|
||||
$value = substr( $value, 0, $tPos );
|
||||
}
|
||||
|
||||
// Usuń myślniki jeśli są
|
||||
$value = str_replace( '-', '', $value );
|
||||
|
||||
if ( strlen( $value ) === 8 && ctype_digit( $value ) ) {
|
||||
return substr( $value, 0, 4 ) . '-' . substr( $value, 4, 2 ) . '-' . substr( $value, 6, 2 );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function unescapeIcalText( string $text ): string {
|
||||
$text = str_replace( '\\n', "\n", $text );
|
||||
$text = str_replace( '\,', ',', $text );
|
||||
$text = str_replace( '\;', ';', $text );
|
||||
$text = str_replace( '\\\\', '\\', $text );
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
153
wp-content/plugins/mphb-ical-sync/includes/SyncCron.php
Normal file
153
wp-content/plugins/mphb-ical-sync/includes/SyncCron.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace MphbIcalSync;
|
||||
|
||||
/**
|
||||
* Zarządza automatyczną synchronizacją iCal co 15 minut.
|
||||
*/
|
||||
class SyncCron {
|
||||
|
||||
const HOOK = 'mphb_ical_sync_cron';
|
||||
const INTERVAL = 'mphb_ical_15min';
|
||||
|
||||
public function __construct() {
|
||||
add_filter( 'cron_schedules', [ $this, 'addInterval' ] );
|
||||
add_action( self::HOOK, [ $this, 'doSync' ] );
|
||||
}
|
||||
|
||||
public function addInterval( array $schedules ): array {
|
||||
$schedules[ self::INTERVAL ] = [
|
||||
'interval' => 900,
|
||||
'display' => 'Co 15 minut',
|
||||
];
|
||||
return $schedules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rejestruje cron przy aktywacji pluginu.
|
||||
*/
|
||||
public static function activate(): void {
|
||||
if ( ! wp_next_scheduled( self::HOOK ) ) {
|
||||
wp_schedule_event( time(), self::INTERVAL, self::HOOK );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Usuwa cron przy deaktywacji pluginu.
|
||||
*/
|
||||
public static function deactivate(): void {
|
||||
wp_clear_scheduled_hook( self::HOOK );
|
||||
}
|
||||
|
||||
/**
|
||||
* Główna funkcja crona — synchronizuje wszystkie pokoje z przypisanymi URL-ami iCal.
|
||||
*/
|
||||
public function doSync(): void {
|
||||
if ( ! function_exists( 'MPHB' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$syncRepo = MPHB()->getSyncUrlsRepository();
|
||||
$roomIds = $syncRepo->getAllRoomIds();
|
||||
|
||||
if ( empty( $roomIds ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parser = new Parser();
|
||||
$importer = new Importer();
|
||||
|
||||
foreach ( $roomIds as $roomId ) {
|
||||
$urls = $syncRepo->getUrls( $roomId );
|
||||
|
||||
if ( empty( $urls ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allEvents = [];
|
||||
$hasError = false;
|
||||
|
||||
foreach ( $urls as $url ) {
|
||||
$response = wp_remote_get( $url, [
|
||||
'timeout' => 30,
|
||||
'headers' => [ 'User-Agent' => 'MPHB-iCal-Sync/' . MPHB_ICAL_SYNC_VERSION ],
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$hasError = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
|
||||
$hasError = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$allEvents = array_merge( $allEvents, $parser->parse( $body ) );
|
||||
}
|
||||
|
||||
$result = $importer->sync( $roomId, $allEvents );
|
||||
|
||||
update_post_meta( $roomId, '_mphb_ical_last_sync', time() );
|
||||
update_post_meta( $roomId, '_mphb_ical_last_status', $hasError ? 'error' : 'ok' );
|
||||
update_post_meta( $roomId, '_mphb_ical_last_result', $result );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ręczna synchronizacja konkretnego pokoju (z AJAX).
|
||||
*
|
||||
* @param int $roomId
|
||||
* @return array Statystyki lub ['error' => string]
|
||||
*/
|
||||
public static function syncRoom( int $roomId ): array {
|
||||
if ( ! function_exists( 'MPHB' ) ) {
|
||||
return [ 'error' => 'MPHB not available' ];
|
||||
}
|
||||
|
||||
$syncRepo = MPHB()->getSyncUrlsRepository();
|
||||
$urls = $syncRepo->getUrls( $roomId );
|
||||
|
||||
if ( empty( $urls ) ) {
|
||||
return [ 'error' => 'Brak skonfigurowanych URL-ów importu dla tego pokoju' ];
|
||||
}
|
||||
|
||||
$parser = new Parser();
|
||||
$importer = new Importer();
|
||||
$allEvents = [];
|
||||
$hasError = false;
|
||||
|
||||
foreach ( $urls as $url ) {
|
||||
$response = wp_remote_get( $url, [
|
||||
'timeout' => 30,
|
||||
'headers' => [ 'User-Agent' => 'MPHB-iCal-Sync/' . MPHB_ICAL_SYNC_VERSION ],
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$hasError = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( wp_remote_retrieve_response_code( $response ) !== 200 ) {
|
||||
$hasError = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$allEvents = array_merge( $allEvents, $parser->parse( $body ) );
|
||||
}
|
||||
|
||||
$result = $importer->sync( $roomId, $allEvents );
|
||||
|
||||
update_post_meta( $roomId, '_mphb_ical_last_sync', time() );
|
||||
update_post_meta( $roomId, '_mphb_ical_last_status', $hasError ? 'error' : 'ok' );
|
||||
update_post_meta( $roomId, '_mphb_ical_last_result', $result );
|
||||
|
||||
if ( $hasError ) {
|
||||
$result['warning'] = 'Jeden lub więcej URL-ów nie odpowiedział poprawnie';
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
45
wp-content/plugins/mphb-ical-sync/mphb-ical-sync.php
Normal file
45
wp-content/plugins/mphb-ical-sync/mphb-ical-sync.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: MPHB iCal Sync
|
||||
* Description: Synchronizacja iCal dla MotoPress Hotel Booking — eksport do Booking.com i import blokad dat
|
||||
* Version: 1.0.0
|
||||
* Author: wrapartamenty.pl
|
||||
* Requires at least: 5.0
|
||||
* Requires PHP: 7.4
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'MPHB_ICAL_SYNC_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'MPHB_ICAL_SYNC_VERSION', '1.0.0' );
|
||||
|
||||
require_once MPHB_ICAL_SYNC_DIR . 'includes/Exporter.php';
|
||||
require_once MPHB_ICAL_SYNC_DIR . 'includes/Parser.php';
|
||||
require_once MPHB_ICAL_SYNC_DIR . 'includes/Importer.php';
|
||||
require_once MPHB_ICAL_SYNC_DIR . 'includes/FeedEndpoint.php';
|
||||
require_once MPHB_ICAL_SYNC_DIR . 'includes/SyncCron.php';
|
||||
require_once MPHB_ICAL_SYNC_DIR . 'includes/AdminUI.php';
|
||||
|
||||
function mphb_ical_sync_init() {
|
||||
if ( ! function_exists( 'MPHB' ) ) {
|
||||
add_action( 'admin_notices', function () {
|
||||
echo '<div class="notice notice-error"><p><strong>MPHB iCal Sync:</strong> Plugin MotoPress Hotel Booking nie jest aktywny.</p></div>';
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
new \MphbIcalSync\FeedEndpoint();
|
||||
new \MphbIcalSync\SyncCron();
|
||||
new \MphbIcalSync\AdminUI();
|
||||
}
|
||||
add_action( 'plugins_loaded', 'mphb_ical_sync_init' );
|
||||
|
||||
register_activation_hook( __FILE__, function () {
|
||||
\MphbIcalSync\SyncCron::activate();
|
||||
} );
|
||||
|
||||
register_deactivation_hook( __FILE__, function () {
|
||||
\MphbIcalSync\SyncCron::deactivate();
|
||||
} );
|
||||
@@ -463,3 +463,22 @@ function add_custom_js() {
|
||||
wp_enqueue_script('custom-script', get_template_directory_uri() . '/assets/js/custom.js', array('jquery'), null, true);
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'add_custom_js');
|
||||
|
||||
add_action('admin_menu', function() {
|
||||
remove_submenu_page('edit.php?post_type=mphb_room_type', 'mphb_room_attribute');
|
||||
}, 999);
|
||||
|
||||
add_action('admin_menu', function() {
|
||||
global $menu;
|
||||
foreach ($menu as $key => $item) {
|
||||
if (isset($item[2]) && $item[2] === 'mphb_customers') {
|
||||
$menu[$key][0] = 'Klienci';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, 999);
|
||||
|
||||
$wrap_mphb_admin_booking_modal = get_template_directory() . '/includes/wrap-mphb-admin-booking-modal.php';
|
||||
if ( file_exists( $wrap_mphb_admin_booking_modal ) ) {
|
||||
require_once $wrap_mphb_admin_booking_modal;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,791 @@
|
||||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'Wrap_MPHB_Admin_Booking_Modal' ) ) {
|
||||
class Wrap_MPHB_Admin_Booking_Modal {
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance() {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
|
||||
add_action( 'admin_footer', array( $this, 'render_modal' ) );
|
||||
|
||||
add_action( 'wp_ajax_wrap_mphb_search_available_rooms', array( $this, 'ajax_search_available_rooms' ) );
|
||||
add_action( 'wp_ajax_wrap_mphb_create_booking', array( $this, 'ajax_create_booking' ) );
|
||||
}
|
||||
|
||||
private function is_calendar_page() {
|
||||
if ( ! is_admin() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isset( $_GET['page'] ) && 'mphb_calendar' === sanitize_key( wp_unslash( $_GET['page'] ) );
|
||||
}
|
||||
|
||||
public function enqueue_assets() {
|
||||
if ( ! $this->is_calendar_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_script( 'jquery' );
|
||||
|
||||
$nonce = wp_create_nonce( 'wrap_mphb_calendar_booking' );
|
||||
|
||||
$config_script = 'window.WrapMphbCalendarBooking = ' . wp_json_encode(
|
||||
array(
|
||||
'nonce' => $nonce,
|
||||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||||
)
|
||||
) . ';';
|
||||
|
||||
wp_add_inline_script( 'jquery', $config_script, 'before' );
|
||||
|
||||
wp_add_inline_style( 'common', $this->get_inline_css() );
|
||||
wp_add_inline_script( 'jquery', $this->get_inline_js() );
|
||||
}
|
||||
|
||||
private function get_inline_css() {
|
||||
return '
|
||||
#wrap-mphb-booking-modal{position:fixed;inset:0;display:none;z-index:100000}
|
||||
#wrap-mphb-booking-modal.is-open{display:block}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-modal-backdrop{position:absolute;inset:0;background:rgba(0,0,0,.5)}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-modal{position:relative;background:#fff;max-width:920px;margin:40px auto;padding:0;border-radius:8px;box-shadow:0 8px 30px rgba(0,0,0,.25);max-height:calc(100vh - 80px);overflow:auto}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-header{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid #e2e2e2}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-title{margin:0;font-size:20px}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-close{border:0;background:none;font-size:24px;line-height:1;cursor:pointer}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-body{padding:20px}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-grid-1{grid-column:1/-1}
|
||||
#wrap-mphb-booking-modal label{display:block;font-weight:600;margin-bottom:6px}
|
||||
#wrap-mphb-booking-modal input[type=text],
|
||||
#wrap-mphb-booking-modal input[type=email],
|
||||
#wrap-mphb-booking-modal input[type=date],
|
||||
#wrap-mphb-booking-modal input[type=number],
|
||||
#wrap-mphb-booking-modal textarea,
|
||||
#wrap-mphb-booking-modal select{width:100%}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-step{display:none}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-step.is-active{display:block}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-results{margin-top:16px;border:1px solid #e2e2e2;border-radius:6px}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-results-header{padding:10px 12px;border-bottom:1px solid #e2e2e2;font-weight:600;background:#f9f9f9}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-result-item{display:flex;align-items:flex-start;gap:10px;padding:10px 12px;border-bottom:1px solid #f0f0f0}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-result-item:last-child{border-bottom:0}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-result-meta{font-size:12px;color:#555;margin-top:4px}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-error{color:#b32d2e;margin-top:10px;display:none}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-success{color:#06752f;margin-top:10px;display:none}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-loading{opacity:.6;pointer-events:none}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-summary{background:#f6f7f7;border:1px solid #dcdcde;padding:12px;border-radius:6px;margin-bottom:14px}
|
||||
@media (max-width: 782px){
|
||||
#wrap-mphb-booking-modal .wrap-mphb-modal{margin:20px}
|
||||
#wrap-mphb-booking-modal .wrap-mphb-grid{grid-template-columns:1fr}
|
||||
}
|
||||
';
|
||||
}
|
||||
|
||||
private function get_inline_js() {
|
||||
return <<<'JS'
|
||||
(function($){
|
||||
function getModal(){
|
||||
return $('#wrap-mphb-booking-modal');
|
||||
}
|
||||
|
||||
function setError($modal, message){
|
||||
$modal.find('.wrap-mphb-error').text(message).show();
|
||||
}
|
||||
|
||||
function getAjaxErrorMessage(xhr, fallback){
|
||||
if(xhr && xhr.responseJSON && xhr.responseJSON.data && xhr.responseJSON.data.message){
|
||||
return xhr.responseJSON.data.message;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function clearMessages($modal){
|
||||
$modal.find('.wrap-mphb-error').hide().text('');
|
||||
$modal.find('.wrap-mphb-success').hide().text('');
|
||||
}
|
||||
|
||||
function formatSummary(data){
|
||||
return '' +
|
||||
'<strong>Wybrany pokoj:</strong> ' + data.roomName + ' (' + data.roomTypeName + ')<br>' +
|
||||
'<strong>Stawka:</strong> ' + data.rateName + '<br>' +
|
||||
'<strong>Termin:</strong> ' + data.checkIn + ' - ' + data.checkOut + '<br>' +
|
||||
'<strong>Goscie:</strong> ' + data.adults + ' doroslych, ' + data.children + ' dzieci<br>' +
|
||||
'<strong>Cena:</strong> ' + data.price;
|
||||
}
|
||||
|
||||
function openModal(){
|
||||
var $modal = getModal();
|
||||
clearMessages($modal);
|
||||
$modal.addClass('is-open');
|
||||
$modal.find('.wrap-mphb-step').removeClass('is-active');
|
||||
$modal.find('.wrap-mphb-step-1').addClass('is-active');
|
||||
$modal.find('.wrap-mphb-results').hide();
|
||||
$modal.find('.wrap-mphb-results-list').empty();
|
||||
}
|
||||
|
||||
function closeModal(){
|
||||
getModal().removeClass('is-open');
|
||||
}
|
||||
|
||||
function renderResults($modal, results, requestData){
|
||||
var $list = $modal.find('.wrap-mphb-results-list').empty();
|
||||
|
||||
results.forEach(function(item, index){
|
||||
var id = 'wrap-mphb-room-option-' + index;
|
||||
var $row = $('<label class="wrap-mphb-result-item" for="'+id+'"></label>');
|
||||
var $radio = $('<input type="radio" name="wrap_mphb_room_option">')
|
||||
.attr('id', id)
|
||||
.attr('data-room-id', item.room_id)
|
||||
.attr('data-room-name', item.room_name)
|
||||
.attr('data-room-type-id', item.room_type_id)
|
||||
.attr('data-room-type-name', item.room_type_name)
|
||||
.attr('data-rate-id', item.rate_id)
|
||||
.attr('data-rate-name', item.rate_name)
|
||||
.attr('data-price', item.price)
|
||||
.attr('data-check-in', requestData.check_in)
|
||||
.attr('data-check-out', requestData.check_out)
|
||||
.attr('data-adults', requestData.adults)
|
||||
.attr('data-children', requestData.children)
|
||||
.prop('checked', index === 0);
|
||||
var $text = $('<div></div>');
|
||||
$text.append('<strong>' + item.room_name + '</strong> <span>(' + item.room_type_name + ')</span>');
|
||||
$text.append('<div class="wrap-mphb-result-meta">Stawka: ' + item.rate_name + ' | Cena: ' + item.price + '</div>');
|
||||
$row.append($radio).append($text);
|
||||
$list.append($row);
|
||||
});
|
||||
|
||||
$modal.find('.wrap-mphb-results').show();
|
||||
}
|
||||
|
||||
function moveToStep2($modal){
|
||||
var $selected = $modal.find('input[name="wrap_mphb_room_option"]:checked');
|
||||
if(!$selected.length){
|
||||
setError($modal, 'Wybierz pokoj z listy.');
|
||||
return;
|
||||
}
|
||||
|
||||
var payload = {
|
||||
roomId: $selected.data('room-id'),
|
||||
roomName: $selected.data('room-name'),
|
||||
roomTypeId: $selected.data('room-type-id'),
|
||||
roomTypeName: $selected.data('room-type-name'),
|
||||
rateId: $selected.data('rate-id'),
|
||||
rateName: $selected.data('rate-name'),
|
||||
price: $selected.data('price'),
|
||||
checkIn: $selected.data('check-in'),
|
||||
checkOut: $selected.data('check-out'),
|
||||
adults: $selected.data('adults'),
|
||||
children: $selected.data('children')
|
||||
};
|
||||
|
||||
$modal.find('[name="room_id"]').val(payload.roomId);
|
||||
$modal.find('[name="room_type_id"]').val(payload.roomTypeId);
|
||||
$modal.find('[name="rate_id"]').val(payload.rateId);
|
||||
$modal.find('[name="check_in"]').val(payload.checkIn);
|
||||
$modal.find('[name="check_out"]').val(payload.checkOut);
|
||||
$modal.find('[name="adults"]').val(payload.adults);
|
||||
$modal.find('[name="children"]').val(payload.children);
|
||||
$modal.find('.wrap-mphb-summary').html(formatSummary(payload));
|
||||
|
||||
$modal.find('.wrap-mphb-step').removeClass('is-active');
|
||||
$modal.find('.wrap-mphb-step-2').addClass('is-active');
|
||||
}
|
||||
|
||||
$(function(){
|
||||
var $title = $('.mphb-booking-calendar-title');
|
||||
if($title.length && !$('#wrap-mphb-add-booking').length){
|
||||
var $btn = $('<a href="#" class="page-title-action" id="wrap-mphb-add-booking">Dodaj rezerwację</a>');
|
||||
$btn.insertAfter($title);
|
||||
}
|
||||
|
||||
$(document).on('click', '#wrap-mphb-add-booking', function(e){
|
||||
e.preventDefault();
|
||||
openModal();
|
||||
});
|
||||
|
||||
$(document).on('click', '.wrap-mphb-close, .wrap-mphb-modal-backdrop', function(){
|
||||
closeModal();
|
||||
});
|
||||
|
||||
$(document).on('click', '.wrap-mphb-to-step-1', function(){
|
||||
var $modal = getModal();
|
||||
clearMessages($modal);
|
||||
$modal.find('.wrap-mphb-step').removeClass('is-active');
|
||||
$modal.find('.wrap-mphb-step-1').addClass('is-active');
|
||||
});
|
||||
|
||||
$(document).on('click', '.wrap-mphb-to-step-2', function(){
|
||||
var $modal = getModal();
|
||||
clearMessages($modal);
|
||||
moveToStep2($modal);
|
||||
});
|
||||
|
||||
$(document).on('submit', '#wrap-mphb-step-1-form', function(e){
|
||||
e.preventDefault();
|
||||
var $modal = getModal();
|
||||
clearMessages($modal);
|
||||
|
||||
var requestData = {
|
||||
action: 'wrap_mphb_search_available_rooms',
|
||||
nonce: window.WrapMphbCalendarBooking.nonce,
|
||||
check_in: $modal.find('[name="search_check_in"]').val(),
|
||||
check_out: $modal.find('[name="search_check_out"]').val(),
|
||||
adults: $modal.find('[name="search_adults"]').val(),
|
||||
children: $modal.find('[name="search_children"]').val(),
|
||||
room_type_id: $modal.find('[name="search_room_type_id"]').val()
|
||||
};
|
||||
|
||||
$modal.addClass('wrap-mphb-loading');
|
||||
$.post(window.WrapMphbCalendarBooking.ajaxUrl, requestData)
|
||||
.done(function(response){
|
||||
if(!response || !response.success){
|
||||
setError($modal, (response && response.data && response.data.message) ? response.data.message : 'Nie udalo sie wyszukac wolnych pokoi.');
|
||||
return;
|
||||
}
|
||||
renderResults($modal, response.data.results, requestData);
|
||||
})
|
||||
.fail(function(xhr){
|
||||
setError($modal, getAjaxErrorMessage(xhr, 'Blad polaczenia z serwerem.'));
|
||||
})
|
||||
.always(function(){
|
||||
$modal.removeClass('wrap-mphb-loading');
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('submit', '#wrap-mphb-step-2-form', function(e){
|
||||
e.preventDefault();
|
||||
var $modal = getModal();
|
||||
clearMessages($modal);
|
||||
|
||||
var requestData = {
|
||||
action: 'wrap_mphb_create_booking',
|
||||
nonce: window.WrapMphbCalendarBooking.nonce,
|
||||
room_id: $modal.find('[name="room_id"]').val(),
|
||||
room_type_id: $modal.find('[name="room_type_id"]').val(),
|
||||
rate_id: $modal.find('[name="rate_id"]').val(),
|
||||
check_in: $modal.find('[name="check_in"]').val(),
|
||||
check_out: $modal.find('[name="check_out"]').val(),
|
||||
adults: $modal.find('[name="adults"]').val(),
|
||||
children: $modal.find('[name="children"]').val(),
|
||||
guest_name: $modal.find('[name="guest_name"]').val(),
|
||||
first_name: $modal.find('[name="first_name"]').val(),
|
||||
last_name: $modal.find('[name="last_name"]').val(),
|
||||
email: $modal.find('[name="email"]').val(),
|
||||
phone: $modal.find('[name="phone"]').val(),
|
||||
country: $modal.find('[name="country"]').val(),
|
||||
city: $modal.find('[name="city"]').val(),
|
||||
address1: $modal.find('[name="address1"]').val(),
|
||||
zip: $modal.find('[name="zip"]').val(),
|
||||
state: $modal.find('[name="state"]').val(),
|
||||
note: $modal.find('[name="note"]').val(),
|
||||
status: $modal.find('[name="status"]').val()
|
||||
};
|
||||
|
||||
$modal.addClass('wrap-mphb-loading');
|
||||
$.post(window.WrapMphbCalendarBooking.ajaxUrl, requestData)
|
||||
.done(function(response){
|
||||
if(!response || !response.success){
|
||||
setError($modal, (response && response.data && response.data.message) ? response.data.message : 'Nie udalo sie zapisac rezerwacji.');
|
||||
return;
|
||||
}
|
||||
|
||||
var text = response.data.message + ' #' + response.data.booking_id;
|
||||
$modal.find('.wrap-mphb-success').text(text).show();
|
||||
setTimeout(function(){
|
||||
window.location.reload();
|
||||
}, 900);
|
||||
})
|
||||
.fail(function(xhr){
|
||||
setError($modal, getAjaxErrorMessage(xhr, 'Blad polaczenia z serwerem.'));
|
||||
})
|
||||
.always(function(){
|
||||
$modal.removeClass('wrap-mphb-loading');
|
||||
});
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
JS;
|
||||
}
|
||||
|
||||
public function render_modal() {
|
||||
if ( ! $this->is_calendar_page() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$room_types = array();
|
||||
$countries = array();
|
||||
|
||||
if ( function_exists( 'MPHB' ) ) {
|
||||
$room_types = MPHB()->getRoomTypePersistence()->getPosts( array( 'fields' => 'all' ) );
|
||||
$countries = MPHB()->settings()->main()->getCountriesBundle()->getCountriesList();
|
||||
}
|
||||
?>
|
||||
<div id="wrap-mphb-booking-modal" aria-hidden="true">
|
||||
<div class="wrap-mphb-modal-backdrop"></div>
|
||||
<div class="wrap-mphb-modal" role="dialog" aria-modal="true">
|
||||
<div class="wrap-mphb-header">
|
||||
<h2 class="wrap-mphb-title">Dodaj rezerwację</h2>
|
||||
<button type="button" class="wrap-mphb-close" aria-label="Zamknij">×</button>
|
||||
</div>
|
||||
<div class="wrap-mphb-body">
|
||||
<div class="wrap-mphb-error"></div>
|
||||
<div class="wrap-mphb-success"></div>
|
||||
|
||||
<div class="wrap-mphb-step wrap-mphb-step-1 is-active">
|
||||
<form id="wrap-mphb-step-1-form">
|
||||
<div class="wrap-mphb-grid">
|
||||
<div>
|
||||
<label for="wrap-mphb-search-check-in">Data przyjazdu</label>
|
||||
<input id="wrap-mphb-search-check-in" type="date" name="search_check_in" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-search-check-out">Data wyjazdu</label>
|
||||
<input id="wrap-mphb-search-check-out" type="date" name="search_check_out" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-search-adults">Dorosli</label>
|
||||
<input id="wrap-mphb-search-adults" type="number" min="1" value="2" name="search_adults" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-search-children">Dzieci</label>
|
||||
<input id="wrap-mphb-search-children" type="number" min="0" value="0" name="search_children" required>
|
||||
</div>
|
||||
<div class="wrap-mphb-grid-1">
|
||||
<label for="wrap-mphb-search-room-type">Typ noclegu</label>
|
||||
<select id="wrap-mphb-search-room-type" name="search_room_type_id">
|
||||
<option value="0">Wszystkie typy</option>
|
||||
<?php foreach ( $room_types as $room_type ) : ?>
|
||||
<option value="<?php echo esc_attr( $room_type->ID ); ?>"><?php echo esc_html( $room_type->post_title ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrap-mphb-actions">
|
||||
<button type="submit" class="button button-primary">Szukaj wolnych pokoi</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="wrap-mphb-results" style="display:none;">
|
||||
<div class="wrap-mphb-results-header">Dostepne pokoje</div>
|
||||
<div class="wrap-mphb-results-list"></div>
|
||||
<div class="wrap-mphb-actions">
|
||||
<button type="button" class="button button-primary wrap-mphb-to-step-2">Przejdz dalej</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wrap-mphb-step wrap-mphb-step-2">
|
||||
<div class="wrap-mphb-summary"></div>
|
||||
<form id="wrap-mphb-step-2-form">
|
||||
<input type="hidden" name="room_id">
|
||||
<input type="hidden" name="room_type_id">
|
||||
<input type="hidden" name="rate_id">
|
||||
<input type="hidden" name="check_in">
|
||||
<input type="hidden" name="check_out">
|
||||
<input type="hidden" name="adults">
|
||||
<input type="hidden" name="children">
|
||||
|
||||
<div class="wrap-mphb-grid">
|
||||
<div>
|
||||
<label for="wrap-mphb-first-name">Imie</label>
|
||||
<input id="wrap-mphb-first-name" type="text" name="first_name" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-last-name">Nazwisko</label>
|
||||
<input id="wrap-mphb-last-name" type="text" name="last_name" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-email">E-mail</label>
|
||||
<input id="wrap-mphb-email" type="email" name="email" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-phone">Telefon</label>
|
||||
<input id="wrap-mphb-phone" type="text" name="phone">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-country">Kraj</label>
|
||||
<select id="wrap-mphb-country" name="country">
|
||||
<option value="">- Wybierz -</option>
|
||||
<?php foreach ( $countries as $code => $name ) : ?>
|
||||
<option value="<?php echo esc_attr( $code ); ?>"><?php echo esc_html( $name ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-city">Miasto</label>
|
||||
<input id="wrap-mphb-city" type="text" name="city">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-address1">Adres</label>
|
||||
<input id="wrap-mphb-address1" type="text" name="address1">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-zip">Kod pocztowy</label>
|
||||
<input id="wrap-mphb-zip" type="text" name="zip">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-state">Wojewodztwo / region</label>
|
||||
<input id="wrap-mphb-state" type="text" name="state">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-guest-name">Imie i nazwisko goscia</label>
|
||||
<input id="wrap-mphb-guest-name" type="text" name="guest_name">
|
||||
</div>
|
||||
<div>
|
||||
<label for="wrap-mphb-status">Status rezerwacji</label>
|
||||
<select id="wrap-mphb-status" name="status" required>
|
||||
<option value="confirmed">Potwierdzona</option>
|
||||
<option value="pending">Oczekujaca (admin)</option>
|
||||
<option value="pending-user">Oczekujaca (klient)</option>
|
||||
<option value="pending-payment">Oczekujaca platnosc</option>
|
||||
<option value="cancelled">Anulowana</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="wrap-mphb-grid-1">
|
||||
<label for="wrap-mphb-note">Notatka klienta</label>
|
||||
<textarea id="wrap-mphb-note" name="note" rows="4"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wrap-mphb-actions">
|
||||
<button type="button" class="button wrap-mphb-to-step-1">Wroc</button>
|
||||
<button type="submit" class="button button-primary">Zapisz rezerwacje</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function parse_date_ymd( $raw_value ) {
|
||||
$date_value = sanitize_text_field( wp_unslash( (string) $raw_value ) );
|
||||
$date = \DateTime::createFromFormat( 'Y-m-d', $date_value );
|
||||
|
||||
if ( ! $date || $date->format( 'Y-m-d' ) !== $date_value ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
private function get_room_type_original_id( $room_type ) {
|
||||
if ( is_object( $room_type ) && method_exists( $room_type, 'getOriginalId' ) ) {
|
||||
return (int) $room_type->getOriginalId();
|
||||
}
|
||||
|
||||
if ( is_object( $room_type ) && method_exists( $room_type, 'getId' ) ) {
|
||||
return (int) $room_type->getId();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function validate_common_booking_data( $check_in, $check_out, $adults, $children ) {
|
||||
if ( is_null( $check_in ) || is_null( $check_out ) ) {
|
||||
return new WP_Error( 'invalid_dates', 'Podaj poprawne daty pobytu.' );
|
||||
}
|
||||
|
||||
if ( $check_out <= $check_in ) {
|
||||
return new WP_Error( 'invalid_dates_order', 'Data wyjazdu musi byc pozniejsza od daty przyjazdu.' );
|
||||
}
|
||||
|
||||
if ( $adults < 1 || $children < 0 ) {
|
||||
return new WP_Error( 'invalid_guests', 'Liczba gosci jest niepoprawna.' );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function ajax_search_available_rooms() {
|
||||
if ( ! current_user_can( 'edit_mphb_bookings' ) && ! current_user_can( 'edit_posts' ) ) {
|
||||
wp_send_json_error( array( 'message' => 'Brak uprawnien do tworzenia rezerwacji.' ), 403 );
|
||||
}
|
||||
|
||||
check_ajax_referer( 'wrap_mphb_calendar_booking', 'nonce' );
|
||||
|
||||
if ( ! function_exists( 'MPHB' ) ) {
|
||||
wp_send_json_error( array( 'message' => 'Modul Hotel Booking nie jest dostepny.' ), 500 );
|
||||
}
|
||||
|
||||
$check_in = $this->parse_date_ymd( $_POST['check_in'] ?? '' );
|
||||
$check_out = $this->parse_date_ymd( $_POST['check_out'] ?? '' );
|
||||
$adults = isset( $_POST['adults'] ) ? absint( $_POST['adults'] ) : 1;
|
||||
$children = isset( $_POST['children'] ) ? absint( $_POST['children'] ) : 0;
|
||||
$room_type_id = isset( $_POST['room_type_id'] ) ? absint( $_POST['room_type_id'] ) : 0;
|
||||
|
||||
$validation = $this->validate_common_booking_data( $check_in, $check_out, $adults, $children );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
wp_send_json_error( array( 'message' => $validation->get_error_message() ), 400 );
|
||||
}
|
||||
|
||||
$is_ignore_rules = MPHB()->settings()->main()->isBookingRulesForAdminDisabled();
|
||||
|
||||
$room_type_ids = array();
|
||||
if ( $room_type_id > 0 ) {
|
||||
$room_type_ids[] = $room_type_id;
|
||||
} else {
|
||||
$room_types = MPHB()->getRoomTypePersistence()->getPosts();
|
||||
$room_type_ids = array_map( 'intval', $room_types );
|
||||
}
|
||||
|
||||
$results = array();
|
||||
|
||||
foreach ( $room_type_ids as $current_room_type_id ) {
|
||||
$room_type = MPHB()->getRoomTypeRepository()->findById( $current_room_type_id );
|
||||
if ( ! $room_type ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $adults > (int) $room_type->getAdultsCapacity() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $children > (int) $room_type->getChildrenCapacity() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $room_type->hasLimitedTotalCapacity() && ( $adults + $children ) > (int) $room_type->getTotalCapacity() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$room_type_original_id = $this->get_room_type_original_id( $room_type );
|
||||
if ( $room_type_original_id <= 0 ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( mphb_availability_facade()->isBookingRulesViolated( $room_type_original_id, $check_in, $check_out, $is_ignore_rules ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$allowed_rates = mphb_prices_facade()->getActiveRates( $room_type_original_id, $check_in, $check_out );
|
||||
if ( empty( $allowed_rates ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$unavailable_rooms = mphb_availability_facade()->getUnavailableRoomIds( $room_type_original_id, $check_in, $check_out, $is_ignore_rules );
|
||||
|
||||
$found_rooms = MPHB()->getRoomPersistence()->searchRooms(
|
||||
array(
|
||||
'availability' => 'free',
|
||||
'from_date' => $check_in,
|
||||
'to_date' => $check_out,
|
||||
'count' => 10,
|
||||
'room_type_id' => $room_type_original_id,
|
||||
'exclude_rooms' => $unavailable_rooms,
|
||||
'skip_buffer_rules' => false,
|
||||
)
|
||||
);
|
||||
|
||||
if ( empty( $found_rooms ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $found_rooms as $room_id ) {
|
||||
$room_id = (int) $room_id;
|
||||
$room = MPHB()->getRoomRepository()->findById( $room_id );
|
||||
if ( ! $room ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$selected_rate = reset( $allowed_rates );
|
||||
if ( ! $selected_rate ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reserved_room = \MPHB\Entities\ReservedRoom::create(
|
||||
array(
|
||||
'room_id' => $room_id,
|
||||
'rate_id' => (int) $selected_rate->getId(),
|
||||
'adults' => $adults,
|
||||
'children' => $children,
|
||||
)
|
||||
);
|
||||
|
||||
$raw_price = $reserved_room->calcRoomPrice( $check_in, $check_out );
|
||||
$price = mphb_prices_facade()->formatPrice( (float) $raw_price );
|
||||
|
||||
$results[] = array(
|
||||
'room_type_id' => $room_type_original_id,
|
||||
'room_type_name' => $room_type->getTitle(),
|
||||
'room_id' => $room_id,
|
||||
'room_name' => $room->getTitle(),
|
||||
'rate_id' => (int) $selected_rate->getId(),
|
||||
'rate_name' => $selected_rate->getTitle(),
|
||||
'price' => wp_strip_all_tags( (string) $price ),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $results ) ) {
|
||||
wp_send_json_error( array( 'message' => 'Brak dostepnych pokoi dla podanych kryteriow.' ) );
|
||||
}
|
||||
|
||||
wp_send_json_success( array( 'results' => $results ) );
|
||||
}
|
||||
|
||||
public function ajax_create_booking() {
|
||||
if ( ! current_user_can( 'edit_mphb_bookings' ) && ! current_user_can( 'edit_posts' ) ) {
|
||||
wp_send_json_error( array( 'message' => 'Brak uprawnien do tworzenia rezerwacji.' ), 403 );
|
||||
}
|
||||
|
||||
check_ajax_referer( 'wrap_mphb_calendar_booking', 'nonce' );
|
||||
|
||||
if ( ! function_exists( 'MPHB' ) ) {
|
||||
wp_send_json_error( array( 'message' => 'Modul Hotel Booking nie jest dostepny.' ), 500 );
|
||||
}
|
||||
|
||||
$room_id = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||||
$rate_id = isset( $_POST['rate_id'] ) ? absint( $_POST['rate_id'] ) : 0;
|
||||
$check_in = $this->parse_date_ymd( $_POST['check_in'] ?? '' );
|
||||
$check_out = $this->parse_date_ymd( $_POST['check_out'] ?? '' );
|
||||
$adults = isset( $_POST['adults'] ) ? absint( $_POST['adults'] ) : 1;
|
||||
$children = isset( $_POST['children'] ) ? absint( $_POST['children'] ) : 0;
|
||||
$status = isset( $_POST['status'] ) ? sanitize_key( wp_unslash( $_POST['status'] ) ) : 'confirmed';
|
||||
|
||||
$validation = $this->validate_common_booking_data( $check_in, $check_out, $adults, $children );
|
||||
if ( is_wp_error( $validation ) ) {
|
||||
wp_send_json_error( array( 'message' => $validation->get_error_message() ), 400 );
|
||||
}
|
||||
|
||||
if ( $room_id <= 0 || $rate_id <= 0 ) {
|
||||
wp_send_json_error( array( 'message' => 'Nie wybrano poprawnego pokoju.' ), 400 );
|
||||
}
|
||||
|
||||
$allowed_statuses = array( 'confirmed', 'pending', 'pending-user', 'pending-payment', 'cancelled' );
|
||||
if ( ! in_array( $status, $allowed_statuses, true ) ) {
|
||||
$status = 'confirmed';
|
||||
}
|
||||
|
||||
$room = MPHB()->getRoomRepository()->findById( $room_id );
|
||||
if ( ! $room ) {
|
||||
wp_send_json_error( array( 'message' => 'Wybrany pokoj nie istnieje.' ), 404 );
|
||||
}
|
||||
|
||||
$room_type = MPHB()->getRoomTypeRepository()->findById( $room->getRoomTypeId() );
|
||||
if ( ! $room_type ) {
|
||||
wp_send_json_error( array( 'message' => 'Nie mozna odnalezc typu pokoju.' ), 404 );
|
||||
}
|
||||
|
||||
if ( $adults > (int) $room_type->getAdultsCapacity() ) {
|
||||
wp_send_json_error( array( 'message' => 'Liczba doroslych przekracza pojemnosc pokoju.' ), 400 );
|
||||
}
|
||||
|
||||
if ( $children > (int) $room_type->getChildrenCapacity() ) {
|
||||
wp_send_json_error( array( 'message' => 'Liczba dzieci przekracza pojemnosc pokoju.' ), 400 );
|
||||
}
|
||||
|
||||
if ( $room_type->hasLimitedTotalCapacity() && ( $adults + $children ) > (int) $room_type->getTotalCapacity() ) {
|
||||
wp_send_json_error( array( 'message' => 'Laczna liczba gosci przekracza pojemnosc pokoju.' ), 400 );
|
||||
}
|
||||
|
||||
$room_type_original_id = $this->get_room_type_original_id( $room_type );
|
||||
$is_ignore_rules = MPHB()->settings()->main()->isBookingRulesForAdminDisabled();
|
||||
|
||||
if ( mphb_availability_facade()->isBookingRulesViolated( $room_type_original_id, $check_in, $check_out, $is_ignore_rules ) ) {
|
||||
wp_send_json_error( array( 'message' => 'Wybrany termin narusza zasady rezerwacji.' ), 400 );
|
||||
}
|
||||
|
||||
$unavailable_rooms = mphb_availability_facade()->getUnavailableRoomIds( $room_type_original_id, $check_in, $check_out, $is_ignore_rules );
|
||||
if ( in_array( $room_id, array_map( 'intval', $unavailable_rooms ), true ) ) {
|
||||
wp_send_json_error( array( 'message' => 'Pokoj nie jest juz dostepny w podanym terminie.' ), 409 );
|
||||
}
|
||||
|
||||
$allowed_rates = mphb_prices_facade()->getActiveRates( $room_type_original_id, $check_in, $check_out );
|
||||
$allowed_rate_ids = array_map(
|
||||
function ( $rate ) {
|
||||
return (int) $rate->getId();
|
||||
},
|
||||
$allowed_rates
|
||||
);
|
||||
|
||||
if ( ! in_array( $rate_id, $allowed_rate_ids, true ) ) {
|
||||
wp_send_json_error( array( 'message' => 'Wybrana stawka nie jest dostepna dla tego terminu.' ), 400 );
|
||||
}
|
||||
|
||||
$first_name = isset( $_POST['first_name'] ) ? sanitize_text_field( wp_unslash( $_POST['first_name'] ) ) : '';
|
||||
$last_name = isset( $_POST['last_name'] ) ? sanitize_text_field( wp_unslash( $_POST['last_name'] ) ) : '';
|
||||
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
|
||||
|
||||
if ( '' === $first_name || '' === $last_name || '' === $email ) {
|
||||
wp_send_json_error( array( 'message' => 'Wypelnij wymagane dane klienta (imie, nazwisko, e-mail).' ), 400 );
|
||||
}
|
||||
|
||||
$customer = new \MPHB\Entities\Customer(
|
||||
array(
|
||||
'first_name' => $first_name,
|
||||
'last_name' => $last_name,
|
||||
'email' => $email,
|
||||
'phone' => isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : '',
|
||||
'country' => isset( $_POST['country'] ) ? sanitize_text_field( wp_unslash( $_POST['country'] ) ) : '',
|
||||
'city' => isset( $_POST['city'] ) ? sanitize_text_field( wp_unslash( $_POST['city'] ) ) : '',
|
||||
'address1' => isset( $_POST['address1'] ) ? sanitize_text_field( wp_unslash( $_POST['address1'] ) ) : '',
|
||||
'zip' => isset( $_POST['zip'] ) ? sanitize_text_field( wp_unslash( $_POST['zip'] ) ) : '',
|
||||
'state' => isset( $_POST['state'] ) ? sanitize_text_field( wp_unslash( $_POST['state'] ) ) : '',
|
||||
)
|
||||
);
|
||||
|
||||
$reserved_room = \MPHB\Entities\ReservedRoom::create(
|
||||
array(
|
||||
'room_id' => $room_id,
|
||||
'rate_id' => $rate_id,
|
||||
'adults' => $adults,
|
||||
'children' => $children,
|
||||
'guest_name' => isset( $_POST['guest_name'] ) ? sanitize_text_field( wp_unslash( $_POST['guest_name'] ) ) : '',
|
||||
)
|
||||
);
|
||||
|
||||
$booking = \MPHB\Entities\Booking::create(
|
||||
array(
|
||||
'check_in_date' => $check_in,
|
||||
'check_out_date' => $check_out,
|
||||
'customer' => $customer,
|
||||
'note' => isset( $_POST['note'] ) ? sanitize_textarea_field( wp_unslash( $_POST['note'] ) ) : '',
|
||||
'status' => $status,
|
||||
'reserved_rooms' => array( $reserved_room ),
|
||||
'checkout_id' => wp_generate_uuid4(),
|
||||
)
|
||||
);
|
||||
|
||||
$booking->getPriceBreakdown();
|
||||
|
||||
$created_customer_id = MPHB()->customers()->createCustomerOnBooking( $booking, true );
|
||||
if ( ! is_wp_error( $created_customer_id ) ) {
|
||||
$customer->setCustomerId( $created_customer_id );
|
||||
$booking->setCustomer( $customer );
|
||||
}
|
||||
|
||||
$is_saved = MPHB()->getBookingRepository()->save( $booking );
|
||||
if ( ! $is_saved || ! $booking->getId() ) {
|
||||
wp_send_json_error( array( 'message' => 'Nie udalo sie zapisac rezerwacji.' ), 500 );
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'booking_id' => (int) $booking->getId(),
|
||||
'edit_link' => get_edit_post_link( $booking->getId(), '' ),
|
||||
'message' => 'Rezerwacja zostala zapisana.',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add_action(
|
||||
'after_setup_theme',
|
||||
function () {
|
||||
if ( is_admin() ) {
|
||||
Wrap_MPHB_Admin_Booking_Modal::instance();
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user