update
This commit is contained in:
@@ -89,6 +89,13 @@ class Admin {
|
||||
check_admin_referer( 'yacht_booking_save_global_ical' );
|
||||
$import_url = isset( $_POST['global_ical_import_url'] ) ? esc_url_raw( wp_unslash( $_POST['global_ical_import_url'] ) ) : '';
|
||||
update_option( 'yacht_booking_global_ical_import_url', $import_url );
|
||||
|
||||
$sync_mode = isset( $_POST['ical_sync_mode'] ) ? sanitize_text_field( wp_unslash( $_POST['ical_sync_mode'] ) ) : 'per_yacht';
|
||||
if ( ! in_array( $sync_mode, array( 'per_yacht', 'global' ), true ) ) {
|
||||
$sync_mode = 'per_yacht';
|
||||
}
|
||||
update_option( 'yacht_booking_ical_sync_mode', $sync_mode );
|
||||
|
||||
wp_safe_redirect( admin_url( 'admin.php?page=yacht-bookings-settings&tab=google-calendar&global_ical_saved=1' ) );
|
||||
exit;
|
||||
}
|
||||
@@ -1242,6 +1249,7 @@ class Admin {
|
||||
$global_import_url = (string) get_option( 'yacht_booking_global_ical_import_url', '' );
|
||||
$global_export_url = \YachtBooking\Integrations\ICal\ICal_Feed::get_global_feed_url();
|
||||
$last_global_run = (string) get_option( 'yacht_booking_global_ical_last_import', '' );
|
||||
$ical_sync_mode = Settings::get_ical_sync_mode();
|
||||
?>
|
||||
<div class="card" style="margin-top: 30px;">
|
||||
<h3><?php esc_html_e( 'Globalna synchronizacja iCal (jeden wspólny kalendarz)', 'yacht-booking' ); ?></h3>
|
||||
@@ -1284,6 +1292,27 @@ class Admin {
|
||||
<form method="post" action="">
|
||||
<?php wp_nonce_field( 'yacht_booking_save_global_ical' ); ?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="ical_sync_mode"><?php esc_html_e( 'Tryb synchronizacji iCal', 'yacht-booking' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select name="ical_sync_mode" id="ical_sync_mode">
|
||||
<option value="per_yacht" <?php selected( $ical_sync_mode, 'per_yacht' ); ?>>
|
||||
<?php esc_html_e( 'Per jacht — dopasowanie po prefiksie SUMMARY', 'yacht-booking' ); ?>
|
||||
</option>
|
||||
<option value="global" <?php selected( $ical_sync_mode, 'global' ); ?>>
|
||||
<?php esc_html_e( 'Wspólny kalendarz — wszystkie eventy bez filtrowania', 'yacht-booking' ); ?>
|
||||
</option>
|
||||
</select>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Per jacht: importowane są tylko eventy z prefiksem nazwy jachtu w tytule (format: "Nazwa jachtu - opis"). Eventy bez dopasowania są ignorowane. Tworzą blokady dostępności.', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Wspólny kalendarz: importowane są wszystkie eventy bez wyjątku, jako wspólne wydarzenia kalendarza. NIE blokują dostępności poszczególnych jachtów. Pokazywane na widgecie zbiorczym "wszystkie jachty".', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="global_ical_import_url"><?php esc_html_e( 'iCal Import URL', 'yacht-booking' ); ?></label>
|
||||
|
||||
@@ -24,6 +24,30 @@ class Rest_Controller extends \WP_REST_Controller {
|
||||
*/
|
||||
const NAMESPACE = 'yacht-booking/v1';
|
||||
|
||||
/**
|
||||
* Deterministyczna paleta kolorów dla widgetu zbiorczego (wszystkie jachty).
|
||||
*
|
||||
* Mapowanie: yacht_id (sortowane rosnąco) → palette[index % count].
|
||||
* Wspólna dla REST i frontendu (używana przez calendar-all.js do legendy).
|
||||
*/
|
||||
const YACHT_COLOR_PALETTE = array(
|
||||
'#3498db',
|
||||
'#e74c3c',
|
||||
'#2ecc71',
|
||||
'#f39c12',
|
||||
'#9b59b6',
|
||||
'#1abc9c',
|
||||
'#34495e',
|
||||
'#d35400',
|
||||
);
|
||||
|
||||
/**
|
||||
* Kolor dla wspólnych wydarzeń kalendarza (yacht_id=0, source=ical_global_calendar).
|
||||
* Jasnoniebieski — dobry kontrast na ciemnogranatowym tle kalendarza, łagodniejszy
|
||||
* dla oka niż akcent czerwony.
|
||||
*/
|
||||
const GLOBAL_EVENT_COLOR = '#7fb3d5';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
@@ -97,6 +121,27 @@ class Rest_Controller extends \WP_REST_Controller {
|
||||
)
|
||||
);
|
||||
|
||||
// GET /yacht-booking/v1/availability/all
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/availability/all',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_all_availability' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'start' => array(
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
'end' => array(
|
||||
'required' => false,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// POST /yacht-booking/v1/bookings
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
@@ -297,6 +342,136 @@ class Rest_Controller extends \WP_REST_Controller {
|
||||
return rest_ensure_response( $events );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated availability for all yachts + globalne wydarzenia kalendarza.
|
||||
*
|
||||
* Zwraca tablicę FullCalendar events (timed 12:00 → 12:00 dla efektu half-day).
|
||||
* Eventy z yacht_id > 0: kolor z palety per-jacht.
|
||||
* Eventy yacht_id = 0 (sync_mode=global): kolor `GLOBAL_EVENT_COLOR`.
|
||||
*
|
||||
* @param \WP_REST_Request $request Request object.
|
||||
* @return \WP_REST_Response
|
||||
*/
|
||||
public function get_all_availability( $request ) {
|
||||
$start = $request->get_param( 'start' );
|
||||
$end = $request->get_param( 'end' );
|
||||
|
||||
if ( ! $start || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $start ) ) {
|
||||
$start = gmdate( 'Y-m-01' );
|
||||
}
|
||||
if ( ! $end || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $end ) ) {
|
||||
$end = gmdate( 'Y-m-d', strtotime( $start . ' +12 months' ) );
|
||||
}
|
||||
|
||||
$is_global_mode = ( 'global' === Settings::get_ical_sync_mode() );
|
||||
|
||||
// Build yacht_id → color map (deterministic by ascending yacht_id).
|
||||
$yacht_posts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'ID',
|
||||
'order' => 'ASC',
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
$color_map = self::get_yacht_color_palette( $yacht_posts );
|
||||
|
||||
// Query bookings overlapping [start, end] with status confirmed or pending.
|
||||
$bookings = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_booking_status',
|
||||
'value' => array( 'confirmed', 'pending' ),
|
||||
'compare' => 'IN',
|
||||
),
|
||||
array(
|
||||
'key' => '_booking_end_date',
|
||||
'value' => $start,
|
||||
'compare' => '>=',
|
||||
'type' => 'DATE',
|
||||
),
|
||||
array(
|
||||
'key' => '_booking_start_date',
|
||||
'value' => $end,
|
||||
'compare' => '<=',
|
||||
'type' => 'DATE',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$events = array();
|
||||
foreach ( $bookings as $booking ) {
|
||||
$booking_id = $booking->ID;
|
||||
$start_date = Booking::get_start_date( $booking_id );
|
||||
$end_date = Booking::get_end_date( $booking_id );
|
||||
|
||||
if ( ! $start_date || ! $end_date ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$yacht_id = Booking::get_yacht_id( $booking_id );
|
||||
$source = (string) get_post_meta( $booking_id, '_booking_source', true );
|
||||
$is_global_event = ( 0 === $yacht_id || \YachtBooking\Integrations\ICal\ICal_Import::GLOBAL_CALENDAR_SOURCE === $source );
|
||||
|
||||
// W trybie global wszystko traktujemy jak wspólne wydarzenia: szary kolor,
|
||||
// brak yacht_id, generyczny tytuł "Rezerwacja" — bez wycieku danych klientów.
|
||||
if ( $is_global_mode || $is_global_event ) {
|
||||
$color = self::GLOBAL_EVENT_COLOR;
|
||||
$title = __( 'Rezerwacja', 'yacht-booking' );
|
||||
$y_id = 0;
|
||||
} else {
|
||||
$color = isset( $color_map[ $yacht_id ] ) ? $color_map[ $yacht_id ] : self::GLOBAL_EVENT_COLOR;
|
||||
$yacht = get_post( $yacht_id );
|
||||
// Tryb per_yacht: pokazujemy tylko nazwę jachtu (bez nazwiska klienta — privacy).
|
||||
$title = $yacht ? $yacht->post_title : __( 'Rezerwacja', 'yacht-booking' );
|
||||
$y_id = $yacht_id;
|
||||
}
|
||||
|
||||
$events[] = array(
|
||||
'id' => $booking_id,
|
||||
'title' => $title,
|
||||
'start' => $start_date . 'T12:00:00',
|
||||
'end' => $end_date . 'T12:00:00',
|
||||
'color' => $color,
|
||||
'yacht_id' => $y_id,
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( $events );
|
||||
}
|
||||
|
||||
/**
|
||||
* Buduje deterministyczną mapę yacht_id → kolor z palety.
|
||||
*
|
||||
* Sortuje yacht_ids rosnąco i indeksuje paletę modulo długość. Zapewnia stabilny
|
||||
* kolor dla danego jachtu między requestami i wspólny przypisanie z frontendem.
|
||||
*
|
||||
* @param array $yacht_ids Lista ID jachtów.
|
||||
* @return array<int,string> yacht_id => hex color.
|
||||
*/
|
||||
public static function get_yacht_color_palette( $yacht_ids ) {
|
||||
$ids = array_map( 'intval', (array) $yacht_ids );
|
||||
sort( $ids, SORT_NUMERIC );
|
||||
|
||||
$palette = self::YACHT_COLOR_PALETTE;
|
||||
$count = count( $palette );
|
||||
$map = array();
|
||||
|
||||
foreach ( $ids as $i => $yacht_id ) {
|
||||
$map[ $yacht_id ] = $palette[ $i % $count ];
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new booking
|
||||
*
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/* Yacht Calendar (All) — wspólny widok wszystkich jachtów.
|
||||
Layout, instrukcja, legenda i formularz dziedziczą z calendar.css
|
||||
(klasy .yacht-inquiry-layout, .yacht-calendar-instructions, .yacht-calendar-legend,
|
||||
.yacht-inquiry-form-container, .yacht-inquiry-form). Tutaj tylko nadpisania
|
||||
specyficzne dla widgetu zbiorczego. */
|
||||
|
||||
.yacht-calendar-all-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto 40px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.yacht-calendar-all {
|
||||
width: 100%;
|
||||
/* Ciemne granatowe tło — emuluje styl /rezerwacja-maja/ gdzie komórki bg-event
|
||||
są semi-transparent (#f5f9ff @ 0.66) nad ciemnym tłem parent containera. */
|
||||
background: #0e2036;
|
||||
}
|
||||
|
||||
/* Komórki przyszłe — semi-transparent biały, daje ciemnoszary efekt nad #0e2036
|
||||
(efekt identyczny jak yacht-day-available bg-event w single-yacht widget). */
|
||||
.yacht-calendar-all .fc-daygrid-day {
|
||||
background: rgba(245, 249, 255, 0.4);
|
||||
}
|
||||
|
||||
/* Przeszłe — białe (jak yacht-day-available .fc-event-past w single-yacht). */
|
||||
.yacht-calendar-all .fc-daygrid-day.fc-day-past {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* Dziś — lekko jaśniejszy ciemny. */
|
||||
.yacht-calendar-all .fc-daygrid-day.fc-day-today {
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
/* Sąsiedni miesiąc — taki sam jak przyszłe (spójny ciemny). */
|
||||
.yacht-calendar-all .fc-daygrid-day.fc-day-other {
|
||||
background: rgba(245, 249, 255, 0.32);
|
||||
}
|
||||
|
||||
|
||||
/* Select w formularzu zapytania — calendar.css nie ma reguł dla <select>,
|
||||
wymuszamy ten sam styl co inputy (ciemnoprzezroczysty, biały tekst, custom strzałka). */
|
||||
.yacht-inquiry-form select {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid hsla(0, 0%, 100%, 0.2);
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
background-color: hsla(0, 0%, 100%, 0.1);
|
||||
color: #fff;
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1l5 5 5-5' stroke='white' stroke-width='2' fill='none'/></svg>");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
padding-right: 32px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.yacht-inquiry-form select:focus {
|
||||
outline: none;
|
||||
border-color: #bc1834;
|
||||
box-shadow: 0 0 0 3px rgba(188, 24, 52, 0.3);
|
||||
background-color: hsla(0, 0%, 100%, 0.15);
|
||||
}
|
||||
|
||||
.yacht-inquiry-form select option {
|
||||
background: #021526;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Event styling — pełne wypełnienie kafelka kolorem jachtu, bez kropek/czasu */
|
||||
.yacht-calendar-all .fc-event {
|
||||
border: none !important;
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.yacht-calendar-all .fc-event-title,
|
||||
.yacht-calendar-all .fc-daygrid-event-dot,
|
||||
.yacht-calendar-all .fc-event-time {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Pasek eventu zawsze wyższy żeby był czytelny bez tekstu */
|
||||
.yacht-calendar-all .fc-daygrid-event {
|
||||
min-height: 18px;
|
||||
}
|
||||
|
||||
/* Half-day visual: gradient wpisywany przez calendar-all.js (eventDidMount), który
|
||||
liczy szerokość komórki dnia i ustawia background-image dla danego segmentu. */
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 600px) {
|
||||
.yacht-calendar-all .fc-event {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.yacht-calendar-all .fc-toolbar.fc-header-toolbar {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Yacht Calendar (All) — wspólny widok wszystkich jachtów.
|
||||
*
|
||||
* Inicjalizuje FullCalendar dayGridMonth dla każdego elementu .yacht-calendar-all-wrapper
|
||||
* na stronie. Pobiera eventy z REST `/availability/all` (timed 12:00→12:00), renderuje
|
||||
* legendę kolorów wyciągniętą z eventów i dodaje klasy half-day na pierwszym/ostatnim
|
||||
* dniu każdej rezerwacji.
|
||||
*/
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
if (typeof window.FullCalendar === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
function pad(n) {
|
||||
return n < 10 ? '0' + n : '' + n;
|
||||
}
|
||||
|
||||
function ymd(date) {
|
||||
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate());
|
||||
}
|
||||
|
||||
function initCalendar(wrapper) {
|
||||
var $wrapper = $(wrapper);
|
||||
var $cal = $wrapper.find('.yacht-calendar-all');
|
||||
if (!$cal.length) return;
|
||||
|
||||
var restUrl = $wrapper.data('rest');
|
||||
if (!restUrl) return;
|
||||
|
||||
var heightPx = parseInt($wrapper.data('height'), 10);
|
||||
if (!heightPx || heightPx < 200) heightPx = 650;
|
||||
|
||||
var calendar = new window.FullCalendar.Calendar($cal.get(0), {
|
||||
initialView: 'dayGridMonth',
|
||||
locale: 'pl',
|
||||
firstDay: 1,
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: ''
|
||||
},
|
||||
height: heightPx,
|
||||
displayEventTime: false,
|
||||
eventDisplay: 'block',
|
||||
events: function (fetchInfo, successCallback, failureCallback) {
|
||||
var url = restUrl
|
||||
+ (restUrl.indexOf('?') === -1 ? '?' : '&')
|
||||
+ 'start=' + ymd(fetchInfo.start)
|
||||
+ '&end=' + ymd(fetchInfo.end);
|
||||
|
||||
$.getJSON(url)
|
||||
.done(function (data) {
|
||||
successCallback(data || []);
|
||||
})
|
||||
.fail(function () {
|
||||
failureCallback(new Error('Failed to fetch /availability/all'));
|
||||
});
|
||||
},
|
||||
eventDidMount: function (info) {
|
||||
// Per-event color via CSS variable.
|
||||
var color = info.event.backgroundColor || '#3498db';
|
||||
info.el.style.setProperty('--yc-event-color', color);
|
||||
|
||||
// Half-day visual: gradient z half-cell transparent na pierwszym/ostatnim dniu.
|
||||
// Liczone po renderze (wymaga znajomości szerokości komórki dnia w siatce).
|
||||
// Przekładamy na requestAnimationFrame żeby DOM był ułożony.
|
||||
window.requestAnimationFrame(function () {
|
||||
applyHalfDayGradient(info);
|
||||
});
|
||||
},
|
||||
eventContent: function () {
|
||||
// Pasek koloru bez treści — privacy: nie pokazujemy nazwisk klientów ani nazw jachtów.
|
||||
return { html: '' };
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
|
||||
// Inquiry form submission (yacht select dropdown).
|
||||
var $form = $wrapper.find('.yacht-calendar-all-inquiry-form');
|
||||
if ($form.length) {
|
||||
$form.on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
var $submitBtn = $form.find('.yacht-booking-submit');
|
||||
var $response = $form.find('.yacht-calendar-all-inquiry-response');
|
||||
var originalBtnText = $submitBtn.text();
|
||||
var i18n = (window.yachtBookingData && window.yachtBookingData.i18n) || {};
|
||||
|
||||
var yachtId = parseInt($form.find('[name="yacht_id"]').val(), 10) || 0;
|
||||
if (!yachtId) {
|
||||
$response.html('<div class="booking-error" style="padding: 12px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px;"><strong>' +
|
||||
(i18n.errorTitle || 'Błąd!') + '</strong> Wybierz jacht z listy.</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
$submitBtn.prop('disabled', true).text(i18n.submitting || 'Wysyłanie...');
|
||||
$response.html('');
|
||||
|
||||
var formData = {
|
||||
yacht_id: yachtId,
|
||||
customer_name: $form.find('[name="customer_name"]').val(),
|
||||
customer_email: $form.find('[name="customer_email"]').val(),
|
||||
customer_phone: $form.find('[name="customer_phone"]').val(),
|
||||
preferred_dates: $form.find('[name="preferred_dates"]').val(),
|
||||
message: $form.find('[name="message"]').val()
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: window.yachtBookingData.apiUrl + '/inquiries',
|
||||
method: 'POST',
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader('X-WP-Nonce', window.yachtBookingData.nonce);
|
||||
},
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(formData),
|
||||
success: function (resp) {
|
||||
$response.html('<div class="booking-success" style="padding: 12px; background: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 4px;"><strong>' +
|
||||
(i18n.successTitle || 'Sukces!') + '</strong> ' +
|
||||
((resp && resp.message) || i18n.inquirySuccess || 'Twoje zapytanie zostało wysłane.') +
|
||||
'</div>');
|
||||
$form[0].reset();
|
||||
},
|
||||
error: function (xhr) {
|
||||
var msg = i18n.errorMessage || 'Wystąpił błąd. Spróbuj ponownie.';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
msg = xhr.responseJSON.message;
|
||||
}
|
||||
$response.html('<div class="booking-error" style="padding: 12px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px;"><strong>' +
|
||||
(i18n.errorTitle || 'Błąd!') + '</strong> ' + msg + '</div>');
|
||||
},
|
||||
complete: function () {
|
||||
$submitBtn.prop('disabled', false).text(originalBtnText);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply half-day gradient to event bar. Pierwsze dni rezerwacji: lewa połowa pierwszej
|
||||
* komórki transparentna (yacht wraca w południe). Ostatnie: prawa połowa ostatniej
|
||||
* komórki transparentna (yacht wypływa w południe). Dla segmentów środkowych — pełny kolor.
|
||||
*/
|
||||
function applyHalfDayGradient(info) {
|
||||
var el = info.el;
|
||||
var rect = el.getBoundingClientRect();
|
||||
if (!rect.width) return;
|
||||
|
||||
// Find a sibling day cell to read its width.
|
||||
var dayCell = el.closest('.fc-daygrid-day') || el.closest('td');
|
||||
// Fallback: scan parent for any .fc-daygrid-day sibling.
|
||||
if (!dayCell) {
|
||||
var grid = el.closest('.fc-daygrid');
|
||||
if (grid) {
|
||||
dayCell = grid.querySelector('.fc-daygrid-day');
|
||||
}
|
||||
}
|
||||
if (!dayCell) return;
|
||||
|
||||
var cellRect = dayCell.getBoundingClientRect();
|
||||
if (!cellRect.width) return;
|
||||
|
||||
var halfPct = (cellRect.width / 2) / rect.width * 100;
|
||||
// Clamp to safe bounds.
|
||||
if (halfPct < 1) halfPct = 1;
|
||||
if (halfPct > 49) halfPct = 49;
|
||||
|
||||
var color = el.style.getPropertyValue('--yc-event-color') || '#3498db';
|
||||
var startTrans = info.isStart;
|
||||
var endTrans = info.isEnd;
|
||||
|
||||
// Build gradient.
|
||||
var stops;
|
||||
if (startTrans && endTrans) {
|
||||
// Single segment containing both start and end.
|
||||
stops = [
|
||||
'transparent 0%',
|
||||
'transparent ' + halfPct.toFixed(2) + '%',
|
||||
color + ' ' + halfPct.toFixed(2) + '%',
|
||||
color + ' ' + (100 - halfPct).toFixed(2) + '%',
|
||||
'transparent ' + (100 - halfPct).toFixed(2) + '%',
|
||||
'transparent 100%'
|
||||
];
|
||||
} else if (startTrans) {
|
||||
stops = [
|
||||
'transparent 0%',
|
||||
'transparent ' + halfPct.toFixed(2) + '%',
|
||||
color + ' ' + halfPct.toFixed(2) + '%',
|
||||
color + ' 100%'
|
||||
];
|
||||
} else if (endTrans) {
|
||||
stops = [
|
||||
color + ' 0%',
|
||||
color + ' ' + (100 - halfPct).toFixed(2) + '%',
|
||||
'transparent ' + (100 - halfPct).toFixed(2) + '%',
|
||||
'transparent 100%'
|
||||
];
|
||||
} else {
|
||||
// Middle segment — pełny kolor.
|
||||
el.style.backgroundColor = color;
|
||||
el.style.backgroundImage = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.backgroundImage = 'linear-gradient(to right, ' + stops.join(', ') + ')';
|
||||
el.style.backgroundColor = 'transparent';
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str).replace(/[&<>"']/g, function (m) {
|
||||
return ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
})[m];
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
$('.yacht-calendar-all-wrapper').each(function () {
|
||||
initCalendar(this);
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
/**
|
||||
* Yacht Calendar (All) Widget for Elementor
|
||||
*
|
||||
* Wspólny kalendarz pokazujący rezerwacje WSZYSTKICH publikowanych jachtów + globalne
|
||||
* wydarzenia (sync_mode=global). Read-only, kolory per-jacht z auto palety, half-day
|
||||
* przez timed events 12:00 → 12:00.
|
||||
*
|
||||
* @package YachtBooking
|
||||
*/
|
||||
|
||||
namespace YachtBooking;
|
||||
|
||||
// Exit if accessed directly
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use Elementor\Controls_Manager;
|
||||
use Elementor\Widget_Base;
|
||||
|
||||
/**
|
||||
* Yacht Calendar (All) Widget Class
|
||||
*/
|
||||
class Calendar_Widget_All extends Widget_Base {
|
||||
|
||||
/**
|
||||
* Widget name (Elementor identifier).
|
||||
*/
|
||||
public function get_name() {
|
||||
return 'yacht-calendar-all';
|
||||
}
|
||||
|
||||
/**
|
||||
* Widget title.
|
||||
*/
|
||||
public function get_title() {
|
||||
return esc_html__( 'Kalendarz Jachtów (wszystkie)', 'yacht-booking' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Widget icon.
|
||||
*/
|
||||
public function get_icon() {
|
||||
return 'eicon-calendar';
|
||||
}
|
||||
|
||||
/**
|
||||
* Widget categories.
|
||||
*/
|
||||
public function get_categories() {
|
||||
return array( 'basic' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Widget keywords.
|
||||
*/
|
||||
public function get_keywords() {
|
||||
return array( 'yacht', 'calendar', 'wszystkie', 'flota', 'kalendarz', 'rezerwacje' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register widget controls.
|
||||
*/
|
||||
protected function register_controls() {
|
||||
$this->start_controls_section(
|
||||
'content_section',
|
||||
array(
|
||||
'label' => esc_html__( 'Ustawienia Kalendarza', 'yacht-booking' ),
|
||||
'tab' => Controls_Manager::TAB_CONTENT,
|
||||
)
|
||||
);
|
||||
|
||||
$this->add_control(
|
||||
'calendar_height',
|
||||
array(
|
||||
'label' => esc_html__( 'Wysokość kalendarza', 'yacht-booking' ),
|
||||
'type' => Controls_Manager::SLIDER,
|
||||
'range' => array(
|
||||
'px' => array(
|
||||
'min' => 400,
|
||||
'max' => 1000,
|
||||
),
|
||||
),
|
||||
'default' => array(
|
||||
'size' => 650,
|
||||
'unit' => 'px',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$this->add_control(
|
||||
'show_legend',
|
||||
array(
|
||||
'label' => esc_html__( 'Pokaż legendę kolorów', 'yacht-booking' ),
|
||||
'type' => Controls_Manager::SWITCHER,
|
||||
'label_on' => esc_html__( 'Tak', 'yacht-booking' ),
|
||||
'label_off' => esc_html__( 'Nie', 'yacht-booking' ),
|
||||
'return_value' => 'yes',
|
||||
'default' => 'yes',
|
||||
)
|
||||
);
|
||||
|
||||
$this->end_controls_section();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render widget output.
|
||||
*/
|
||||
protected function render() {
|
||||
$settings = $this->get_settings_for_display();
|
||||
$height = ! empty( $settings['calendar_height']['size'] ) ? (int) $settings['calendar_height']['size'] : 650;
|
||||
$show_legend = ! isset( $settings['show_legend'] ) || 'yes' === $settings['show_legend'];
|
||||
$dom_id = 'yacht-calendar-all-' . $this->get_id();
|
||||
|
||||
echo Calendar_All_View::render( $dom_id, $height, $show_legend ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pomocnicza klasa renderująca markup widgetu i shortcode.
|
||||
*
|
||||
* Wspólna dla widgetu Elementor i shortcode `[yacht_calendar_all]` żeby uniknąć duplikacji.
|
||||
*/
|
||||
class Calendar_All_View {
|
||||
|
||||
/**
|
||||
* Render markup wspólnego kalendarza.
|
||||
*
|
||||
* @param string $dom_id Unikalny ID kontenera FullCalendar.
|
||||
* @param int $height Wysokość kalendarza w px.
|
||||
* @param bool $show_legend Czy renderować legendę.
|
||||
* @return string HTML.
|
||||
*/
|
||||
public static function render( $dom_id, $height = 650, $show_legend = true ) {
|
||||
$rest_url = esc_url_raw( rest_url( 'yacht-booking/v1/availability/all' ) );
|
||||
|
||||
// Yachts for legend + form select.
|
||||
$yacht_posts = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'orderby' => 'ID',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
$yacht_ids = wp_list_pluck( $yacht_posts, 'ID' );
|
||||
$color_map = \YachtBooking\Rest_Controller::get_yacht_color_palette( $yacht_ids );
|
||||
$global_color = \YachtBooking\Rest_Controller::GLOBAL_EVENT_COLOR;
|
||||
$sync_mode = \YachtBooking\Settings::get_ical_sync_mode();
|
||||
$terms_url = \YachtBooking\Settings::get_terms_page_url();
|
||||
$form_uid = preg_replace( '/[^a-z0-9_-]/i', '', $dom_id );
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="yacht-calendar-all-wrapper"
|
||||
data-rest="<?php echo esc_attr( $rest_url ); ?>"
|
||||
data-show-legend="<?php echo $show_legend ? '1' : '0'; ?>"
|
||||
data-height="<?php echo esc_attr( (int) $height ); ?>">
|
||||
|
||||
<div class="yacht-calendar-instructions">
|
||||
<p>
|
||||
<?php esc_html_e( 'Aby zarezerwować termin, wypełnij formularz po prawej stronie albo skontaktuj się z nami telefonicznie lub mailowo.', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if ( $show_legend ) : ?>
|
||||
<div class="yacht-calendar-legend yacht-calendar-all-legend" aria-label="<?php esc_attr_e( 'Legenda kalendarza', 'yacht-booking' ); ?>">
|
||||
<?php if ( 'global' === $sync_mode ) : ?>
|
||||
<span class="yacht-legend-item">
|
||||
<span class="yacht-legend-swatch" style="background-color: <?php echo esc_attr( $global_color ); ?>;"></span>
|
||||
<?php esc_html_e( 'Rezerwacja', 'yacht-booking' ); ?>
|
||||
</span>
|
||||
<?php else : ?>
|
||||
<?php foreach ( $yacht_posts as $yacht ) : ?>
|
||||
<?php $color = isset( $color_map[ $yacht->ID ] ) ? $color_map[ $yacht->ID ] : $global_color; ?>
|
||||
<span class="yacht-legend-item">
|
||||
<span class="yacht-legend-swatch" style="background-color: <?php echo esc_attr( $color ); ?>;"></span>
|
||||
<?php echo esc_html( $yacht->post_title ); ?>
|
||||
</span>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="yacht-inquiry-layout yacht-calendar-all-layout">
|
||||
<div class="yacht-inquiry-calendar-col">
|
||||
<div id="<?php echo esc_attr( $dom_id ); ?>"
|
||||
class="yacht-calendar yacht-calendar-all"
|
||||
style="height: <?php echo esc_attr( $height ); ?>px;"></div>
|
||||
</div>
|
||||
|
||||
<div class="yacht-inquiry-form-col">
|
||||
<div class="yacht-inquiry-form-container">
|
||||
<h4><?php esc_html_e( 'Zapytaj o rezerwację', 'yacht-booking' ); ?></h4>
|
||||
<p class="yacht-inquiry-desc">
|
||||
<?php esc_html_e( 'Wybierz jacht i wypełnij formularz — odezwiemy się w sprawie dostępności i cen.', 'yacht-booking' ); ?>
|
||||
</p>
|
||||
<form class="yacht-inquiry-form yacht-calendar-all-inquiry-form">
|
||||
<?php wp_nonce_field( 'yacht_inquiry_submit', 'yacht_inquiry_nonce' ); ?>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="all_inquiry_yacht_<?php echo esc_attr( $form_uid ); ?>">
|
||||
<?php esc_html_e( 'Jacht', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<select id="all_inquiry_yacht_<?php echo esc_attr( $form_uid ); ?>" name="yacht_id" required>
|
||||
<option value=""><?php esc_html_e( '— wybierz jacht —', 'yacht-booking' ); ?></option>
|
||||
<?php foreach ( $yacht_posts as $yacht ) : ?>
|
||||
<option value="<?php echo esc_attr( $yacht->ID ); ?>"><?php echo esc_html( $yacht->post_title ); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="all_inquiry_name_<?php echo esc_attr( $form_uid ); ?>">
|
||||
<?php esc_html_e( 'Imię i nazwisko', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="text" id="all_inquiry_name_<?php echo esc_attr( $form_uid ); ?>" name="customer_name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="all_inquiry_email_<?php echo esc_attr( $form_uid ); ?>">
|
||||
<?php esc_html_e( 'Email', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="email" id="all_inquiry_email_<?php echo esc_attr( $form_uid ); ?>" name="customer_email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="all_inquiry_phone_<?php echo esc_attr( $form_uid ); ?>">
|
||||
<?php esc_html_e( 'Telefon', 'yacht-booking' ); ?> <span class="required">*</span>
|
||||
</label>
|
||||
<input type="tel" id="all_inquiry_phone_<?php echo esc_attr( $form_uid ); ?>" name="customer_phone" required>
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="all_inquiry_dates_<?php echo esc_attr( $form_uid ); ?>">
|
||||
<?php esc_html_e( 'Preferowane terminy', 'yacht-booking' ); ?>
|
||||
</label>
|
||||
<input type="text"
|
||||
id="all_inquiry_dates_<?php echo esc_attr( $form_uid ); ?>"
|
||||
name="preferred_dates"
|
||||
placeholder="<?php esc_attr_e( 'np. 15-22 lipca', 'yacht-booking' ); ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="all_inquiry_message_<?php echo esc_attr( $form_uid ); ?>">
|
||||
<?php esc_html_e( 'Wiadomość', 'yacht-booking' ); ?>
|
||||
</label>
|
||||
<textarea id="all_inquiry_message_<?php echo esc_attr( $form_uid ); ?>"
|
||||
name="message"
|
||||
rows="3"
|
||||
placeholder="<?php esc_attr_e( 'Dodatkowe pytania lub uwagi...', 'yacht-booking' ); ?>"></textarea>
|
||||
</div>
|
||||
|
||||
<?php if ( $terms_url ) : ?>
|
||||
<p class="booking-terms">
|
||||
<?php
|
||||
printf(
|
||||
wp_kses_post( __( 'Wysyłając formularz akceptujesz %s.', 'yacht-booking' ) ),
|
||||
'<a href="' . esc_url( $terms_url ) . '" target="_blank" rel="noopener">' . esc_html__( 'regulamin', 'yacht-booking' ) . '</a>'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="yacht-booking-submit">
|
||||
<?php esc_html_e( 'Wyślij zapytanie', 'yacht-booking' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="yacht-inquiry-response yacht-calendar-all-inquiry-response"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,39 @@ class Shortcode {
|
||||
*/
|
||||
private function __construct() {
|
||||
add_shortcode( 'yacht_calendar', array( $this, 'render_calendar' ) );
|
||||
add_shortcode( 'yacht_calendar_all', array( $this, 'render_calendar_all' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render shortcode `[yacht_calendar_all]` — wspólny kalendarz wszystkich jachtów.
|
||||
*
|
||||
* Atrybuty:
|
||||
* - height: wysokość w px (default 650)
|
||||
* - show_legend: yes|no (default yes)
|
||||
*
|
||||
* @param array $atts Shortcode attributes.
|
||||
* @return string HTML.
|
||||
*/
|
||||
public function render_calendar_all( $atts ) {
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'height' => 650,
|
||||
'show_legend' => 'yes',
|
||||
),
|
||||
$atts,
|
||||
'yacht_calendar_all'
|
||||
);
|
||||
|
||||
// Lazy-load widget class for the View helper.
|
||||
if ( ! class_exists( '\YachtBooking\Calendar_All_View' ) ) {
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'frontend/class-calendar-widget-all.php';
|
||||
}
|
||||
|
||||
$dom_id = 'yacht-calendar-all-' . wp_rand( 1000, 9999 );
|
||||
$height = (int) $atts['height'];
|
||||
$show_legend = 'yes' === strtolower( (string) $atts['show_legend'] );
|
||||
|
||||
return Calendar_All_View::render( $dom_id, $height, $show_legend );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -167,6 +167,7 @@ class Installer {
|
||||
'yacht_booking_global_ical_import_url' => '',
|
||||
'yacht_booking_global_ical_token' => '',
|
||||
'yacht_booking_global_ical_last_import' => '',
|
||||
'yacht_booking_ical_sync_mode' => 'per_yacht',
|
||||
);
|
||||
|
||||
foreach ( $options as $key => $value ) {
|
||||
|
||||
@@ -113,6 +113,20 @@ class Settings {
|
||||
return trim( number_format_i18n( (float) $amount, 2 ) . ' ' . self::get_currency_symbol() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get iCal sync mode.
|
||||
*
|
||||
* Tryb synchronizacji iCal:
|
||||
* - per_yacht: import po prefiksie SUMMARY ("Nazwa - opis"), wpisy do availability
|
||||
* - global: wszystkie eventy zapisane jako wspólne wydarzenia kalendarza, bez wpływu na availability
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_ical_sync_mode() {
|
||||
$mode = get_option( 'yacht_booking_ical_sync_mode', 'per_yacht' );
|
||||
return in_array( $mode, array( 'per_yacht', 'global' ), true ) ? $mode : 'per_yacht';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get terms page ID.
|
||||
*
|
||||
|
||||
@@ -77,6 +77,9 @@ class Yacht_Booking {
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'includes/class-booking.php';
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'includes/class-availability.php';
|
||||
|
||||
// REST controller — eagerly loaded because View helpers use its color palette + stałe.
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'api/class-rest-controller.php';
|
||||
|
||||
// Load admin classes
|
||||
if ( is_admin() ) {
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'admin/class-admin.php';
|
||||
@@ -157,6 +160,24 @@ class Yacht_Booking {
|
||||
true
|
||||
);
|
||||
|
||||
// Calendar (all yachts) — wspólny widget. Załaduj tylko gdy strona go potrzebuje.
|
||||
if ( $this->should_load_calendar_all_assets() ) {
|
||||
wp_enqueue_style(
|
||||
'yacht-booking-calendar-all',
|
||||
YACHT_BOOKING_PLUGIN_URL . 'frontend/assets/css/calendar-all.css',
|
||||
array( 'fullcalendar' ),
|
||||
filemtime( YACHT_BOOKING_PLUGIN_DIR . 'frontend/assets/css/calendar-all.css' )
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'yacht-booking-calendar-all',
|
||||
YACHT_BOOKING_PLUGIN_URL . 'frontend/assets/js/calendar-all.js',
|
||||
array( 'jquery', 'fullcalendar', 'fullcalendar-pl' ),
|
||||
filemtime( YACHT_BOOKING_PLUGIN_DIR . 'frontend/assets/js/calendar-all.js' ),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Localize script
|
||||
wp_localize_script(
|
||||
'yacht-booking-calendar',
|
||||
@@ -228,8 +249,53 @@ class Yacht_Booking {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if post contains yacht calendar shortcode or widget
|
||||
if ( $post && ( has_shortcode( $post->post_content, 'yacht_calendar' ) || $this->has_yacht_calendar_widget( $post->ID ) ) ) {
|
||||
// Check if post contains yacht calendar shortcode or widget (per-jacht lub wszystkie)
|
||||
if ( $post ) {
|
||||
if ( has_shortcode( $post->post_content, 'yacht_calendar' )
|
||||
|| has_shortcode( $post->post_content, 'yacht_calendar_all' )
|
||||
|| $this->has_yacht_calendar_widget( $post->ID )
|
||||
|| $this->has_yacht_calendar_all_widget( $post->ID ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if post contains the wspólny widget kalendarza floty.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @return bool
|
||||
*/
|
||||
private function has_yacht_calendar_all_widget( $post_id ) {
|
||||
if ( ! class_exists( '\Elementor\Plugin' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$document = \Elementor\Plugin::$instance->documents->get( $post_id );
|
||||
if ( ! $document ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = $document->get_elements_data();
|
||||
return $this->find_widget_recursive( $data, 'yacht-calendar-all' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if calendar-all (wspólny) assets should be loaded — used to enqueue
|
||||
* dodatkowe pliki tylko na stronach które ich potrzebują.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_load_calendar_all_assets() {
|
||||
global $post;
|
||||
|
||||
if ( class_exists( '\Elementor\Plugin' ) && \Elementor\Plugin::$instance->preview->is_preview_mode() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( $post && ( has_shortcode( $post->post_content, 'yacht_calendar_all' ) || $this->has_yacht_calendar_all_widget( $post->ID ) ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -296,11 +362,13 @@ class Yacht_Booking {
|
||||
* @param object $widgets_manager Elementor widgets manager.
|
||||
*/
|
||||
public function register_elementor_widgets( $widgets_manager ) {
|
||||
// Load widget class
|
||||
// Load widget classes
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'frontend/class-calendar-widget.php';
|
||||
require_once YACHT_BOOKING_PLUGIN_DIR . 'frontend/class-calendar-widget-all.php';
|
||||
|
||||
// Register widget
|
||||
// Register widgets
|
||||
$widgets_manager->register( new Calendar_Widget() );
|
||||
$widgets_manager->register( new Calendar_Widget_All() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
namespace YachtBooking\Integrations\ICal;
|
||||
|
||||
use YachtBooking\Availability;
|
||||
use YachtBooking\Settings;
|
||||
use YachtBooking\Yacht;
|
||||
|
||||
// Exit if accessed directly.
|
||||
@@ -29,6 +30,14 @@ class ICal_Import {
|
||||
*/
|
||||
const GLOBAL_IMPORT_SOURCE = 'ical_import_global';
|
||||
|
||||
/**
|
||||
* Booking source identifier dla trybu "wspólny kalendarz" (sync_mode=global).
|
||||
*
|
||||
* Eventy z tym source nie blokują dostępności jachtów (yacht_id=0) i są pokazywane
|
||||
* tylko na widgecie zbiorczym "wszystkie jachty".
|
||||
*/
|
||||
const GLOBAL_CALENDAR_SOURCE = 'ical_global_calendar';
|
||||
|
||||
/**
|
||||
* Separator między prefiksem nazwy jachtu a resztą tytułu eventu.
|
||||
*
|
||||
@@ -95,13 +104,30 @@ class ICal_Import {
|
||||
return false;
|
||||
}
|
||||
|
||||
$events = self::parse_ics( $body );
|
||||
$events = self::parse_ics( $body );
|
||||
$mode = Settings::get_ical_sync_mode();
|
||||
|
||||
if ( 'global' === $mode ) {
|
||||
self::run_global_calendar_mode( $events );
|
||||
} else {
|
||||
self::run_per_yacht_mode( $events );
|
||||
}
|
||||
|
||||
update_option( 'yacht_booking_global_ical_last_import', current_time( 'mysql' ) );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tryb per-jacht: dopasowanie po prefiksie SUMMARY, wpisy do availability.
|
||||
*
|
||||
* @param array $events Sparsowane eventy iCal.
|
||||
*/
|
||||
protected static function run_per_yacht_mode( $events ) {
|
||||
$yacht_map = self::build_yacht_lookup_map();
|
||||
|
||||
if ( empty( $yacht_map ) ) {
|
||||
self::log( 'Global iCal: no yachts in DB — nothing to match', 'error' );
|
||||
update_option( 'yacht_booking_global_ical_last_import', current_time( 'mysql' ) );
|
||||
return true;
|
||||
self::log( 'Per-yacht iCal: no yachts in DB — nothing to match', 'error' );
|
||||
return;
|
||||
}
|
||||
|
||||
$existing_map = self::get_existing_global_import_map();
|
||||
@@ -117,11 +143,11 @@ class ICal_Import {
|
||||
continue;
|
||||
}
|
||||
|
||||
$summary = isset( $event['summary'] ) ? (string) $event['summary'] : '';
|
||||
$summary = isset( $event['summary'] ) ? (string) $event['summary'] : '';
|
||||
$yacht_id = self::match_yacht_by_prefix( $summary, $yacht_map );
|
||||
|
||||
if ( ! $yacht_id ) {
|
||||
self::log( sprintf( 'Global iCal: skip event "%s" — no yacht match for prefix', $summary ) );
|
||||
self::log( sprintf( 'Per-yacht iCal: skip event "%s" — no yacht match for prefix', $summary ) );
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -131,20 +157,76 @@ class ICal_Import {
|
||||
$booking_id = self::upsert_global_booking( $yacht_id, $event, $existing_id );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
self::log( sprintf( 'Global iCal: failed to upsert event %s', $event['uid'] ), 'error' );
|
||||
self::log( sprintf( 'Per-yacht iCal: failed to upsert event %s', $event['uid'] ), 'error' );
|
||||
}
|
||||
}
|
||||
|
||||
// Stale cleanup — usuń bookingi których UID nie ma już w feedzie.
|
||||
// Ograniczone do GLOBAL_IMPORT_SOURCE (per-yacht) — nie tyka GLOBAL_CALENDAR_SOURCE.
|
||||
foreach ( $existing_map as $uid => $booking_id ) {
|
||||
if ( ! in_array( $uid, $seen_uids, true ) ) {
|
||||
\YachtBooking\Availability::clear_booking_availability( $booking_id );
|
||||
wp_delete_post( $booking_id, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
update_option( 'yacht_booking_global_ical_last_import', current_time( 'mysql' ) );
|
||||
return true;
|
||||
/**
|
||||
* Tryb global: wszystkie eventy (bez filtrowania) zapisane jako wspólne wydarzenia
|
||||
* kalendarza. yacht_id=0, brak wpisów do wp_yacht_availability.
|
||||
*
|
||||
* @param array $events Sparsowane eventy iCal.
|
||||
*/
|
||||
protected static function run_global_calendar_mode( $events ) {
|
||||
$existing_map = self::get_existing_global_calendar_map();
|
||||
$seen_uids = array();
|
||||
$imported = 0;
|
||||
$updated = 0;
|
||||
|
||||
foreach ( $events as $event ) {
|
||||
if ( empty( $event['uid'] ) || empty( $event['start'] ) || empty( $event['end'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip past events.
|
||||
if ( strtotime( $event['end'] ) < time() ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$seen_uids[] = $event['uid'];
|
||||
$existing_id = isset( $existing_map[ $event['uid'] ] ) ? (int) $existing_map[ $event['uid'] ] : 0;
|
||||
|
||||
$booking_id = self::upsert_global_calendar_event( $event, $existing_id );
|
||||
|
||||
if ( ! $booking_id ) {
|
||||
self::log( sprintf( 'Global calendar iCal: failed to upsert event %s', $event['uid'] ), 'error' );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $existing_id > 0 ) {
|
||||
$updated++;
|
||||
} else {
|
||||
$imported++;
|
||||
}
|
||||
}
|
||||
|
||||
// Stale cleanup — usuń tylko eventy z GLOBAL_CALENDAR_SOURCE których UID brakuje.
|
||||
$deleted = 0;
|
||||
foreach ( $existing_map as $uid => $booking_id ) {
|
||||
if ( ! in_array( $uid, $seen_uids, true ) ) {
|
||||
wp_delete_post( $booking_id, true );
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
self::log(
|
||||
sprintf(
|
||||
'Global calendar iCal: imported=%d, updated=%d, deleted=%d',
|
||||
$imported,
|
||||
$updated,
|
||||
$deleted
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,6 +379,95 @@ class ICal_Import {
|
||||
return (int) $booking_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapa istniejących eventów GLOBAL_CALENDAR_SOURCE (tryb global): uid => booking_id.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function get_existing_global_calendar_map() {
|
||||
$bookings = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => -1,
|
||||
'fields' => 'ids',
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => '_booking_source',
|
||||
'value' => self::GLOBAL_CALENDAR_SOURCE,
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$map = array();
|
||||
foreach ( $bookings as $booking_id ) {
|
||||
$uid = get_post_meta( $booking_id, '_ical_event_uid', true );
|
||||
if ( $uid ) {
|
||||
$map[ $uid ] = (int) $booking_id;
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tworzy lub aktualizuje wspólne wydarzenie kalendarza (sync_mode=global).
|
||||
*
|
||||
* yacht_id=0, brak wpisów do availability — event tylko informacyjny na widgecie zbiorczym.
|
||||
*
|
||||
* @param array $event Parsed event data.
|
||||
* @param int $existing_id Existing booking ID (0 dla nowego).
|
||||
* @return int|false
|
||||
*/
|
||||
protected static function upsert_global_calendar_event( $event, $existing_id = 0 ) {
|
||||
$summary = ! empty( $event['summary'] )
|
||||
? sanitize_text_field( $event['summary'] )
|
||||
: __( 'Wydarzenie kalendarza', 'yacht-booking' );
|
||||
|
||||
$post_data = array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'post_status' => 'publish',
|
||||
'post_title' => sprintf(
|
||||
/* translators: %s: event summary */
|
||||
__( 'GCal (wspólny): %s', 'yacht-booking' ),
|
||||
$summary
|
||||
),
|
||||
);
|
||||
|
||||
if ( $existing_id > 0 ) {
|
||||
$post_data['ID'] = $existing_id;
|
||||
$booking_id = wp_update_post( $post_data, true );
|
||||
} else {
|
||||
$booking_id = wp_insert_post( $post_data, true );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $booking_id ) || ! $booking_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
update_post_meta( $booking_id, '_booking_yacht_id', 0 );
|
||||
update_post_meta( $booking_id, '_booking_start_date', $event['start'] );
|
||||
update_post_meta( $booking_id, '_booking_end_date', $event['end'] );
|
||||
update_post_meta( $booking_id, '_booking_status', 'confirmed' );
|
||||
update_post_meta( $booking_id, '_booking_customer_name', __( 'Google Calendar (kalendarz wspólny)', 'yacht-booking' ) );
|
||||
update_post_meta( $booking_id, '_booking_customer_email', '' );
|
||||
update_post_meta( $booking_id, '_booking_customer_phone', '' );
|
||||
update_post_meta( $booking_id, '_booking_total_price', 0 );
|
||||
update_post_meta( $booking_id, '_booking_source', self::GLOBAL_CALENDAR_SOURCE );
|
||||
update_post_meta( $booking_id, '_ical_event_uid', $event['uid'] );
|
||||
update_post_meta( $booking_id, '_booking_notes', $summary );
|
||||
|
||||
// CELOWO BEZ Availability::mark_as_booked() — wspólne eventy nie blokują dostępności jachtów.
|
||||
// Defensywnie: jeśli kiedyś existing_id miał wpisy availability (np. po przełączeniu trybu),
|
||||
// usuń je, by nie zostawiały śmieci.
|
||||
if ( $existing_id > 0 ) {
|
||||
\YachtBooking\Availability::clear_booking_availability( $existing_id );
|
||||
}
|
||||
|
||||
return (int) $booking_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse .ics content into array of events.
|
||||
*
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: Yacht Booking System
|
||||
* Plugin URI: https://jachty.pagedev.pl
|
||||
* Description: System rezerwacji jachtów z kalendarzem i integracją z Google Calendar
|
||||
* Version: 1.0.0
|
||||
* Version: 1.1.0
|
||||
* Author: PageDev
|
||||
* Author URI: https://pagedev.pl
|
||||
* Text Domain: yacht-booking
|
||||
@@ -20,7 +20,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
}
|
||||
|
||||
// Define plugin constants
|
||||
define( 'YACHT_BOOKING_VERSION', '1.0.0' );
|
||||
define( 'YACHT_BOOKING_VERSION', '1.1.0' );
|
||||
define( 'YACHT_BOOKING_PLUGIN_FILE', __FILE__ );
|
||||
define( 'YACHT_BOOKING_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'YACHT_BOOKING_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
Reference in New Issue
Block a user