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:
2026-03-02 18:14:50 +01:00
parent 2bca262769
commit c49d3b39d4
17 changed files with 2185 additions and 24 deletions

View File

@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"mcp__serena__activate_project",
"mcp__serena__check_onboarding_performed"
]
}
}

1
.serena/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/cache

126
.serena/project.yml Normal file
View 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
View File

@@ -14,6 +14,7 @@
".git",
"/.vscode",
"/.serena",
"/.claude"
"/.claude",
"/docs"
]
}

View File

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

View File

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

View 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 ); ?>">
&#x1F504; 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;">
&#x23F0; 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>&#x1F4E4; 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>&#x1F4E5; 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 ) . '">&#x1F504; Synchronizuj teraz</button>';
echo '&nbsp;<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 );
}
}

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

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

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

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

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

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

View File

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

View File

@@ -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">&times;</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();
}
}
);