From c49d3b39d4e308fa6c77a840aa10ffdc808b7ffb Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Mon, 2 Mar 2026 18:14:50 +0100 Subject: [PATCH] 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. --- .claude/settings.local.json | 8 + .serena/.gitignore | 1 + .serena/project.yml | 126 +++ .vscode/ftp-kr.json | 3 +- .vscode/ftp-kr.sync.cache.json | 50 +- docs/changelog.md | 37 + docs/intrukcja.md | 102 +++ .../data/report-earnings-by-dates-data.php | 2 +- .../mphb-ical-sync/includes/AdminUI.php | 386 +++++++++ .../mphb-ical-sync/includes/Exporter.php | 103 +++ .../mphb-ical-sync/includes/FeedEndpoint.php | 85 ++ .../mphb-ical-sync/includes/Importer.php | 154 ++++ .../mphb-ical-sync/includes/Parser.php | 144 ++++ .../mphb-ical-sync/includes/SyncCron.php | 153 ++++ .../plugins/mphb-ical-sync/mphb-ical-sync.php | 45 + .../themes/hello-elementor/functions.php | 19 + .../wrap-mphb-admin-booking-modal.php | 791 ++++++++++++++++++ 17 files changed, 2185 insertions(+), 24 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .serena/.gitignore create mode 100644 .serena/project.yml create mode 100644 docs/changelog.md create mode 100644 docs/intrukcja.md create mode 100644 wp-content/plugins/mphb-ical-sync/includes/AdminUI.php create mode 100644 wp-content/plugins/mphb-ical-sync/includes/Exporter.php create mode 100644 wp-content/plugins/mphb-ical-sync/includes/FeedEndpoint.php create mode 100644 wp-content/plugins/mphb-ical-sync/includes/Importer.php create mode 100644 wp-content/plugins/mphb-ical-sync/includes/Parser.php create mode 100644 wp-content/plugins/mphb-ical-sync/includes/SyncCron.php create mode 100644 wp-content/plugins/mphb-ical-sync/mphb-ical-sync.php create mode 100644 wp-content/themes/hello-elementor/includes/wrap-mphb-admin-booking-modal.php diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..3acbe521 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "mcp__serena__activate_project", + "mcp__serena__check_onboarding_performed" + ] + } +} diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 00000000..14d86ad6 --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1 @@ +/cache diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 00000000..bfe9daa5 --- /dev/null +++ b/.serena/project.yml @@ -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: diff --git a/.vscode/ftp-kr.json b/.vscode/ftp-kr.json index 90c2c80e..d4846edf 100644 --- a/.vscode/ftp-kr.json +++ b/.vscode/ftp-kr.json @@ -14,6 +14,7 @@ ".git", "/.vscode", "/.serena", - "/.claude" + "/.claude", + "/docs" ] } \ No newline at end of file diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index 82430559..e725ce1b 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -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 } } } diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..8b5e5cd7 --- /dev/null +++ b/docs/changelog.md @@ -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`. + diff --git a/docs/intrukcja.md b/docs/intrukcja.md new file mode 100644 index 00000000..d9cd6ce9 --- /dev/null +++ b/docs/intrukcja.md @@ -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 +``` + diff --git a/wp-content/plugins/motopress-hotel-booking-lite/includes/reports/data/report-earnings-by-dates-data.php b/wp-content/plugins/motopress-hotel-booking-lite/includes/reports/data/report-earnings-by-dates-data.php index c79a961b..521a60d7 100644 --- a/wp-content/plugins/motopress-hotel-booking-lite/includes/reports/data/report-earnings-by-dates-data.php +++ b/wp-content/plugins/motopress-hotel-booking-lite/includes/reports/data/report-earnings-by-dates-data.php @@ -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(); } diff --git a/wp-content/plugins/mphb-ical-sync/includes/AdminUI.php b/wp-content/plugins/mphb-ical-sync/includes/AdminUI.php new file mode 100644 index 00000000..d3495d45 --- /dev/null +++ b/wp-content/plugins/mphb-ical-sync/includes/AdminUI.php @@ -0,0 +1,386 @@ + 'mphb_room', + 'post_status' => 'publish', + 'numberposts' => -1, + 'no_found_rows' => true, + 'orderby' => 'title', + 'order' => 'ASC', + ] ); + $syncRepo = MPHB()->getSyncUrlsRepository(); + ?> +
+

Synchronizacja kalendarzy iCal

+
+ + +

Brak opublikowanych pokoi.

+ + + + +

+ Eksport: skopiuj link i wklej w Booking.com → Ekstranet → Nieruchomość → Synchronizacja kalendarza → Eksportuj.
+ Import: URL iCal z Booking.com wklej na stronie edycji pokoju (metabox „iCal Synchronizacja"). +

+ + + + + + + + + + + + + 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 ); + ?> + + + + + + + + + + + + + +
PokójLink eksportu (→ Booking.com)URL importu (← Booking.com)Ostatnia synchronizacjaAkcje
+ + post_title ); ?> + +
ID: +
+
+ + +
+
+ + — brak — + dodaj w edycji pokoju + + + +
+ + +
+ + + + + +
+ + nowych, + − usuniętych + + + + nie uruchamiano + +
+ + + + + + +
+ +

+ ⏰ Automatyczna synchronizacja uruchamia się co 15 minut przez WP-Cron. +

+ + +
+ + + 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 ''; + + // Eksport + echo '
'; + echo '

📤 Link eksportu (dla Booking.com)

'; + echo '
'; + $inputId = 'mphb-ical-exp-meta-' . $roomId; + echo ''; + echo ''; + echo '
'; + echo '

Wklej w Booking.com: Ekstranet → Nieruchomość → Synchronizacja kalendarza → Eksportuj.

'; + echo '
'; + + // Import + echo '
'; + echo '

📥 URL importu z Booking.com

'; + echo ''; + echo '

URL iCal z Booking.com (jeden na linię). Znajdziesz go w Booking.com: Ekstranet → Synchronizacja kalendarza → Importuj.

'; + echo '
'; + + // Status + echo '
'; + if ( $lastSync ) { + $timeStr = wp_date( 'd.m.Y H:i', (int) $lastSync ); + $icon = $lastStatus === 'ok' ? '✓ OK' : '✗ Błąd'; + echo '

Ostatnia sync: ' . esc_html( $timeStr ) . ' — ' . esc_html( $icon ); + if ( is_array( $lastResult ) ) { + echo ' (+' . (int) ( $lastResult['created'] ?? 0 ) . ' / −' . (int) ( $lastResult['deleted'] ?? 0 ) . ')'; + } + echo '

'; + } else { + echo '

Synchronizacja jeszcze nie była uruchamiana.

'; + } + $nonce = wp_create_nonce( self::NONCE_AJAX ); + echo ''; + echo ' '; + echo '
'; + + ?> + + 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 ); + } +} diff --git a/wp-content/plugins/mphb-ical-sync/includes/Exporter.php b/wp-content/plugins/mphb-ical-sync/includes/Exporter.php new file mode 100644 index 00000000..fe2c8bca --- /dev/null +++ b/wp-content/plugins/mphb-ical-sync/includes/Exporter.php @@ -0,0 +1,103 @@ +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; + } +} diff --git a/wp-content/plugins/mphb-ical-sync/includes/FeedEndpoint.php b/wp-content/plugins/mphb-ical-sync/includes/FeedEndpoint.php new file mode 100644 index 00000000..0c1816b4 --- /dev/null +++ b/wp-content/plugins/mphb-ical-sync/includes/FeedEndpoint.php @@ -0,0 +1,85 @@ + $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; + } +} diff --git a/wp-content/plugins/mphb-ical-sync/includes/Importer.php b/wp-content/plugins/mphb-ical-sync/includes/Importer.php new file mode 100644 index 00000000..04ea015f --- /dev/null +++ b/wp-content/plugins/mphb-ical-sync/includes/Importer.php @@ -0,0 +1,154 @@ + 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 ); + } +} diff --git a/wp-content/plugins/mphb-ical-sync/includes/Parser.php b/wp-content/plugins/mphb-ical-sync/includes/Parser.php new file mode 100644 index 00000000..9857211e --- /dev/null +++ b/wp-content/plugins/mphb-ical-sync/includes/Parser.php @@ -0,0 +1,144 @@ +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; + } +} diff --git a/wp-content/plugins/mphb-ical-sync/includes/SyncCron.php b/wp-content/plugins/mphb-ical-sync/includes/SyncCron.php new file mode 100644 index 00000000..bc7f9256 --- /dev/null +++ b/wp-content/plugins/mphb-ical-sync/includes/SyncCron.php @@ -0,0 +1,153 @@ + 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; + } +} diff --git a/wp-content/plugins/mphb-ical-sync/mphb-ical-sync.php b/wp-content/plugins/mphb-ical-sync/mphb-ical-sync.php new file mode 100644 index 00000000..a9627165 --- /dev/null +++ b/wp-content/plugins/mphb-ical-sync/mphb-ical-sync.php @@ -0,0 +1,45 @@ +

MPHB iCal Sync: Plugin MotoPress Hotel Booking nie jest aktywny.

'; + } ); + 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(); +} ); diff --git a/wp-content/themes/hello-elementor/functions.php b/wp-content/themes/hello-elementor/functions.php index 4a3316ab..0310b6de 100644 --- a/wp-content/themes/hello-elementor/functions.php +++ b/wp-content/themes/hello-elementor/functions.php @@ -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; +} diff --git a/wp-content/themes/hello-elementor/includes/wrap-mphb-admin-booking-modal.php b/wp-content/themes/hello-elementor/includes/wrap-mphb-admin-booking-modal.php new file mode 100644 index 00000000..fe191bae --- /dev/null +++ b/wp-content/themes/hello-elementor/includes/wrap-mphb-admin-booking-modal.php @@ -0,0 +1,791 @@ +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 '' + + 'Wybrany pokoj: ' + data.roomName + ' (' + data.roomTypeName + ')
' + + 'Stawka: ' + data.rateName + '
' + + 'Termin: ' + data.checkIn + ' - ' + data.checkOut + '
' + + 'Goscie: ' + data.adults + ' doroslych, ' + data.children + ' dzieci
' + + 'Cena: ' + 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 = $(''); + var $radio = $('') + .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 = $('
'); + $text.append('' + item.room_name + ' (' + item.room_type_name + ')'); + $text.append('
Stawka: ' + item.rate_name + ' | Cena: ' + item.price + '
'); + $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 = $('Dodaj rezerwację'); + $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(); + } + ?> + + 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(); + } + } +);