- 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.
387 lines
14 KiB
PHP
387 lines
14 KiB
PHP
<?php
|
||
|
||
namespace MphbIcalSync;
|
||
|
||
/**
|
||
* Panel administracyjny iCal:
|
||
* – metabox na stronie edycji pokoju (mphb_room)
|
||
* – pełna strona pod admin.php?page=mphb_ical (przejmuje hook MPHB)
|
||
*/
|
||
class AdminUI {
|
||
|
||
const NONCE_FIELD = 'mphb_ical_sync_nonce';
|
||
const NONCE_ACTION = 'mphb_ical_sync_save';
|
||
const NONCE_AJAX = 'mphb_ical_sync_ajax';
|
||
|
||
public function __construct() {
|
||
// Metabox na stronie pokoju
|
||
add_action( 'add_meta_boxes', [ $this, 'addMetaBox' ] );
|
||
add_action( 'save_post_mphb_room', [ $this, 'saveMetaBox' ], 10, 1 );
|
||
|
||
// AJAX sync
|
||
add_action( 'wp_ajax_mphb_ical_sync_room', [ $this, 'ajaxSyncRoom' ] );
|
||
|
||
// Przejęcie strony mphb_ical — bardzo późny priorytet (po MPHB)
|
||
add_action( 'admin_menu', [ $this, 'overrideIcalPage' ], 9999 );
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Przejęcie strony admin.php?page=mphb_ical
|
||
// -------------------------------------------------------------------------
|
||
|
||
public function overrideIcalPage(): void {
|
||
// WordPress rejestruje callback strony jako akcję o nazwie zwróconej
|
||
// przez add_submenu_page(). Dla rodzica 'mphb_booking_menu' i sluga 'mphb_ical'
|
||
// ta nazwa to wynik get_plugin_page_hookname().
|
||
$hookname = get_plugin_page_hookname( 'mphb_ical', 'mphb_booking_menu' );
|
||
|
||
if ( ! $hookname ) {
|
||
return;
|
||
}
|
||
|
||
// Usuń MPHB-owy render (który jest pusty), dodaj nasz
|
||
remove_all_actions( $hookname );
|
||
add_action( $hookname, [ $this, 'renderIcalPage' ] );
|
||
}
|
||
|
||
public function renderIcalPage(): void {
|
||
$rooms = get_posts( [
|
||
'post_type' => 'mphb_room',
|
||
'post_status' => 'publish',
|
||
'numberposts' => -1,
|
||
'no_found_rows' => true,
|
||
'orderby' => 'title',
|
||
'order' => 'ASC',
|
||
] );
|
||
$syncRepo = MPHB()->getSyncUrlsRepository();
|
||
?>
|
||
<div class="wrap">
|
||
<h1 class="wp-heading-inline">Synchronizacja kalendarzy iCal</h1>
|
||
<hr class="wp-header-end" />
|
||
|
||
<?php if ( empty( $rooms ) ) : ?>
|
||
<p>Brak opublikowanych pokoi.</p>
|
||
<?php else : ?>
|
||
|
||
<style>
|
||
#mphb-ical-rooms-table { border-collapse: collapse; width: 100%; margin-top: 16px; background: #fff; }
|
||
#mphb-ical-rooms-table th,
|
||
#mphb-ical-rooms-table td { padding: 9px 13px; border: 1px solid #dcdcde; vertical-align: middle; font-size: 13px; }
|
||
#mphb-ical-rooms-table th { background: #f6f7f7; font-weight: 600; text-align: left; }
|
||
#mphb-ical-rooms-table tr:hover td { background: #f9f9f9; }
|
||
#mphb-ical-rooms-table .url-mono { font-family: monospace; font-size: 11px; word-break: break-all; }
|
||
#mphb-ical-rooms-table .no-val { color: #999; font-style: italic; }
|
||
.mphb-ical-status-ok { color: #1a7a1a; font-weight: 600; }
|
||
.mphb-ical-status-err { color: #c33; font-weight: 600; }
|
||
</style>
|
||
|
||
<p>
|
||
<strong>Eksport:</strong> skopiuj link i wklej w Booking.com → Ekstranet → Nieruchomość → Synchronizacja kalendarza → Eksportuj.<br>
|
||
<strong>Import:</strong> URL iCal z Booking.com wklej na stronie edycji pokoju (metabox „iCal Synchronizacja").
|
||
</p>
|
||
|
||
<table id="mphb-ical-rooms-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:200px;">Pokój</th>
|
||
<th>Link eksportu (→ Booking.com)</th>
|
||
<th>URL importu (← Booking.com)</th>
|
||
<th style="width:170px;">Ostatnia synchronizacja</th>
|
||
<th style="width:120px;">Akcje</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ( $rooms as $room ) :
|
||
$roomId = $room->ID;
|
||
$exportUrl = FeedEndpoint::getExportUrl( $roomId );
|
||
$importUrls = array_values( $syncRepo->getUrls( $roomId ) );
|
||
$editUrl = get_edit_post_link( $roomId );
|
||
$lastSync = get_post_meta( $roomId, '_mphb_ical_last_sync', true );
|
||
$lastStatus = get_post_meta( $roomId, '_mphb_ical_last_status', true );
|
||
$lastResult = get_post_meta( $roomId, '_mphb_ical_last_result', true );
|
||
$inputId = 'mphb-ical-exp-' . $roomId;
|
||
$nonce = wp_create_nonce( self::NONCE_AJAX );
|
||
?>
|
||
<tr>
|
||
<td>
|
||
<a href="<?php echo esc_url( $editUrl ); ?>" style="font-weight:600;">
|
||
<?php echo esc_html( $room->post_title ); ?>
|
||
</a>
|
||
<br><small style="color:#999;">ID: <?php echo $roomId; ?></small>
|
||
</td>
|
||
|
||
<td>
|
||
<div style="display:flex;gap:6px;align-items:flex-start;">
|
||
<input type="text"
|
||
id="<?php echo esc_attr( $inputId ); ?>"
|
||
value="<?php echo esc_attr( $exportUrl ); ?>"
|
||
readonly onclick="this.select()"
|
||
class="url-mono"
|
||
style="flex:1;padding:3px 6px;" />
|
||
<button type="button" class="button button-small"
|
||
onclick="(function(id){var el=document.getElementById(id);el.select();navigator.clipboard.writeText(el.value).then(function(){});})('<?php echo esc_js( $inputId ); ?>')">
|
||
Kopiuj
|
||
</button>
|
||
</div>
|
||
</td>
|
||
|
||
<td>
|
||
<?php if ( empty( $importUrls ) ) : ?>
|
||
<span class="no-val">— brak —
|
||
<a href="<?php echo esc_url( $editUrl ); ?>" style="font-size:11px;">dodaj w edycji pokoju</a>
|
||
</span>
|
||
<?php else : ?>
|
||
<?php foreach ( $importUrls as $url ) : ?>
|
||
<div class="url-mono"><?php echo esc_html( $url ); ?></div>
|
||
<?php endforeach; ?>
|
||
<?php endif; ?>
|
||
</td>
|
||
|
||
<td>
|
||
<?php if ( $lastSync ) :
|
||
$timeStr = wp_date( 'd.m.Y H:i', (int) $lastSync );
|
||
$cls = $lastStatus === 'ok' ? 'mphb-ical-status-ok' : 'mphb-ical-status-err';
|
||
$icon = $lastStatus === 'ok' ? '✓' : '✗';
|
||
?>
|
||
<span class="<?php echo esc_attr( $cls ); ?>">
|
||
<?php echo esc_html( $icon . ' ' . $timeStr ); ?>
|
||
</span>
|
||
<?php if ( is_array( $lastResult ) ) : ?>
|
||
<br><small style="color:#888;">
|
||
+<?php echo (int) ( $lastResult['created'] ?? 0 ); ?> nowych,
|
||
−<?php echo (int) ( $lastResult['deleted'] ?? 0 ); ?> usuniętych
|
||
</small>
|
||
<?php endif; ?>
|
||
<?php else : ?>
|
||
<span class="no-val">nie uruchamiano</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
|
||
<td>
|
||
<?php if ( ! empty( $importUrls ) ) : ?>
|
||
<button type="button" class="button mphb-ical-sync-btn"
|
||
data-room-id="<?php echo esc_attr( $roomId ); ?>"
|
||
data-nonce="<?php echo esc_attr( $nonce ); ?>">
|
||
🔄 Sync
|
||
</button>
|
||
<span class="mphb-ical-row-status" style="display:block;font-size:11px;margin-top:4px;"></span>
|
||
<?php else : ?>
|
||
<span class="no-val">—</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
|
||
<p style="margin-top:12px;color:#666;font-size:12px;">
|
||
⏰ Automatyczna synchronizacja uruchamia się co 15 minut przez WP-Cron.
|
||
</p>
|
||
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<script>
|
||
(function() {
|
||
document.querySelectorAll('.mphb-ical-sync-btn').forEach(function(btn) {
|
||
btn.addEventListener('click', function() {
|
||
var status = btn.nextElementSibling;
|
||
status.style.color = '#888';
|
||
status.textContent = 'Synchronizuję\u2026';
|
||
btn.disabled = true;
|
||
|
||
var fd = new FormData();
|
||
fd.append('action', 'mphb_ical_sync_room');
|
||
fd.append('room_id', btn.dataset.roomId);
|
||
fd.append('nonce', btn.dataset.nonce);
|
||
|
||
fetch(ajaxurl, { method: 'POST', body: fd })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(r) {
|
||
if (r.success) {
|
||
status.style.color = '#1a7a1a';
|
||
status.textContent = '\u2713 +' + r.data.created + ' / \u2212' + r.data.deleted;
|
||
} else {
|
||
status.style.color = '#c33';
|
||
status.textContent = '\u2717 ' + (r.data || 'b\u0142\u0105d');
|
||
}
|
||
btn.disabled = false;
|
||
})
|
||
.catch(function() {
|
||
status.style.color = '#c33';
|
||
status.textContent = '\u2717 b\u0142\u0105d po\u0142\u0105czenia';
|
||
btn.disabled = false;
|
||
});
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
<?php
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// Metabox na stronie edycji pokoju
|
||
// -------------------------------------------------------------------------
|
||
|
||
public function addMetaBox(): void {
|
||
add_meta_box(
|
||
'mphb_ical_sync',
|
||
'iCal Synchronizacja (Booking.com)',
|
||
[ $this, 'renderMetaBox' ],
|
||
'mphb_room',
|
||
'normal',
|
||
'default'
|
||
);
|
||
}
|
||
|
||
public function renderMetaBox( \WP_Post $post ): void {
|
||
$roomId = $post->ID;
|
||
$exportUrl = FeedEndpoint::getExportUrl( $roomId );
|
||
$syncRepo = MPHB()->getSyncUrlsRepository();
|
||
$urls = $syncRepo->getUrls( $roomId );
|
||
$importUrls = implode( "\n", array_values( $urls ) );
|
||
|
||
$lastSync = get_post_meta( $roomId, '_mphb_ical_last_sync', true );
|
||
$lastStatus = get_post_meta( $roomId, '_mphb_ical_last_status', true );
|
||
$lastResult = get_post_meta( $roomId, '_mphb_ical_last_result', true );
|
||
|
||
wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD );
|
||
|
||
echo '<style>
|
||
#mphb_ical_sync .mphb-section { margin-bottom: 14px; }
|
||
#mphb_ical_sync .mphb-section h4 { margin: 0 0 5px; font-size: 13px; font-weight: 600; }
|
||
#mphb_ical_sync .mphb-export-row { display: flex; gap: 8px; align-items: center; }
|
||
#mphb_ical_sync .mphb-export-row input { flex: 1; font-family: monospace; font-size: 11px; }
|
||
#mphb_ical_sync .description { color: #666; font-size: 12px; margin-top: 4px; }
|
||
</style>';
|
||
|
||
// Eksport
|
||
echo '<div class="mphb-section">';
|
||
echo '<h4>📤 Link eksportu (dla Booking.com)</h4>';
|
||
echo '<div class="mphb-export-row">';
|
||
$inputId = 'mphb-ical-exp-meta-' . $roomId;
|
||
echo '<input type="text" readonly id="' . esc_attr( $inputId ) . '" value="' . esc_attr( $exportUrl ) . '" onclick="this.select()" />';
|
||
echo '<button type="button" class="button" onclick="(function(id){var el=document.getElementById(id);el.select();navigator.clipboard.writeText(el.value).then(function(){});})(' . wp_json_encode( $inputId ) . ')">Kopiuj</button>';
|
||
echo '</div>';
|
||
echo '<p class="description">Wklej w Booking.com: Ekstranet → Nieruchomość → Synchronizacja kalendarza → Eksportuj.</p>';
|
||
echo '</div>';
|
||
|
||
// Import
|
||
echo '<div class="mphb-section">';
|
||
echo '<h4>📥 URL importu z Booking.com</h4>';
|
||
echo '<textarea name="mphb_ical_import_urls" rows="3" class="large-text" style="font-family:monospace;font-size:11px;">' . esc_textarea( $importUrls ) . '</textarea>';
|
||
echo '<p class="description">URL iCal z Booking.com (jeden na linię). Znajdziesz go w Booking.com: Ekstranet → Synchronizacja kalendarza → Importuj.</p>';
|
||
echo '</div>';
|
||
|
||
// Status
|
||
echo '<div class="mphb-section">';
|
||
if ( $lastSync ) {
|
||
$timeStr = wp_date( 'd.m.Y H:i', (int) $lastSync );
|
||
$icon = $lastStatus === 'ok' ? '✓ OK' : '✗ Błąd';
|
||
echo '<p style="margin:0 0 6px;">Ostatnia sync: <strong>' . esc_html( $timeStr ) . '</strong> — ' . esc_html( $icon );
|
||
if ( is_array( $lastResult ) ) {
|
||
echo ' (+' . (int) ( $lastResult['created'] ?? 0 ) . ' / −' . (int) ( $lastResult['deleted'] ?? 0 ) . ')';
|
||
}
|
||
echo '</p>';
|
||
} else {
|
||
echo '<p style="margin:0 0 6px;color:#666;">Synchronizacja jeszcze nie była uruchamiana.</p>';
|
||
}
|
||
$nonce = wp_create_nonce( self::NONCE_AJAX );
|
||
echo '<button type="button" class="button" id="mphb-ical-meta-sync" data-room-id="' . esc_attr( $roomId ) . '" data-nonce="' . esc_attr( $nonce ) . '">🔄 Synchronizuj teraz</button>';
|
||
echo ' <span id="mphb-ical-meta-status" style="font-size:12px;margin-left:8px;"></span>';
|
||
echo '</div>';
|
||
|
||
?>
|
||
<script>
|
||
(function() {
|
||
var btn = document.getElementById('mphb-ical-meta-sync');
|
||
if (!btn) return;
|
||
btn.addEventListener('click', function() {
|
||
var status = document.getElementById('mphb-ical-meta-status');
|
||
status.textContent = 'Synchronizuję\u2026';
|
||
btn.disabled = true;
|
||
var fd = new FormData();
|
||
fd.append('action', 'mphb_ical_sync_room');
|
||
fd.append('room_id', btn.dataset.roomId);
|
||
fd.append('nonce', btn.dataset.nonce);
|
||
fetch(ajaxurl, {method:'POST',body:fd})
|
||
.then(function(r){return r.json();})
|
||
.then(function(r){
|
||
if(r.success){
|
||
status.style.color='#1a7a1a';
|
||
status.textContent='\u2713 +'+r.data.created+' / \u2212'+r.data.deleted;
|
||
} else {
|
||
status.style.color='#c33';
|
||
status.textContent='\u2717 '+(r.data||'b\u0142\u0105d');
|
||
}
|
||
btn.disabled=false;
|
||
})
|
||
.catch(function(){
|
||
status.style.color='#c33';
|
||
status.textContent='\u2717 b\u0142\u0105d po\u0142\u0105czenia';
|
||
btn.disabled=false;
|
||
});
|
||
});
|
||
})();
|
||
</script>
|
||
<?php
|
||
}
|
||
|
||
public function saveMetaBox( int $postId ): void {
|
||
if ( ! isset( $_POST[ self::NONCE_FIELD ] ) ) {
|
||
return;
|
||
}
|
||
if ( ! wp_verify_nonce(
|
||
sanitize_text_field( wp_unslash( $_POST[ self::NONCE_FIELD ] ) ),
|
||
self::NONCE_ACTION
|
||
) ) {
|
||
return;
|
||
}
|
||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
|
||
return;
|
||
}
|
||
if ( ! current_user_can( 'edit_post', $postId ) ) {
|
||
return;
|
||
}
|
||
|
||
$raw = isset( $_POST['mphb_ical_import_urls'] )
|
||
? sanitize_textarea_field( wp_unslash( $_POST['mphb_ical_import_urls'] ) )
|
||
: '';
|
||
|
||
$validUrls = [];
|
||
foreach ( array_filter( array_map( 'trim', explode( "\n", $raw ) ) ) as $url ) {
|
||
$url = esc_url_raw( $url );
|
||
if ( filter_var( $url, FILTER_VALIDATE_URL ) ) {
|
||
$validUrls[] = $url;
|
||
}
|
||
}
|
||
|
||
MPHB()->getSyncUrlsRepository()->updateUrls( $postId, $validUrls );
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// AJAX
|
||
// -------------------------------------------------------------------------
|
||
|
||
public function ajaxSyncRoom(): void {
|
||
check_ajax_referer( self::NONCE_AJAX, 'nonce' );
|
||
|
||
if ( ! current_user_can( 'manage_options' ) ) {
|
||
wp_send_json_error( 'Brak uprawnień' );
|
||
}
|
||
|
||
$roomId = isset( $_POST['room_id'] ) ? absint( $_POST['room_id'] ) : 0;
|
||
if ( ! $roomId ) {
|
||
wp_send_json_error( 'Nieprawidłowy ID pokoju' );
|
||
}
|
||
|
||
$result = SyncCron::syncRoom( $roomId );
|
||
|
||
if ( isset( $result['error'] ) ) {
|
||
wp_send_json_error( $result['error'] );
|
||
}
|
||
|
||
wp_send_json_success( $result );
|
||
}
|
||
}
|