update
This commit is contained in:
@@ -142,6 +142,17 @@ class Rest_Controller extends \WP_REST_Controller {
|
||||
)
|
||||
);
|
||||
|
||||
// GET /yacht-booking/v1/availability/bounds
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
'/availability/bounds',
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_availability_bounds' ),
|
||||
'permission_callback' => '__return_true',
|
||||
)
|
||||
);
|
||||
|
||||
// POST /yacht-booking/v1/bookings
|
||||
register_rest_route(
|
||||
self::NAMESPACE,
|
||||
@@ -407,6 +418,11 @@ class Rest_Controller extends \WP_REST_Controller {
|
||||
)
|
||||
);
|
||||
|
||||
$ical_sources = array(
|
||||
\YachtBooking\Integrations\ICal\ICal_Import::GLOBAL_CALENDAR_SOURCE,
|
||||
\YachtBooking\Integrations\ICal\ICal_Import::GLOBAL_IMPORT_SOURCE,
|
||||
);
|
||||
|
||||
$events = array();
|
||||
foreach ( $bookings as $booking ) {
|
||||
$booking_id = $booking->ID;
|
||||
@@ -421,33 +437,113 @@ class Rest_Controller extends \WP_REST_Controller {
|
||||
$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.
|
||||
// Color: zachowane z poprzedniej logiki (per-yacht paleta lub kolor global).
|
||||
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,
|
||||
);
|
||||
// Title: raw SUMMARY z _booking_notes (iCal) lub customer_name (frontend).
|
||||
// Klient świadomie cofa privacy z 09-04 — tytuły rezerwacji widoczne publicznie.
|
||||
if ( in_array( $source, $ical_sources, true ) ) {
|
||||
$notes = (string) get_post_meta( $booking_id, '_booking_notes', true );
|
||||
$title = '' !== trim( $notes ) ? $notes : __( 'Rezerwacja', 'yacht-booking' );
|
||||
} else {
|
||||
$customer = (string) Booking::get_customer_name( $booking_id );
|
||||
$title = '' !== trim( $customer ) ? $customer : __( 'Rezerwacja', 'yacht-booking' );
|
||||
}
|
||||
$title = sanitize_text_field( $title );
|
||||
|
||||
// Split na N eventów per dzień (allDay = każdy event mieści się w jednej komórce).
|
||||
// Iteracja od start_date do end_date INCLUSIVE — pierwszy i ostatni dzień
|
||||
// mają half-day visual (yacht odbierany / zwracany w południe).
|
||||
try {
|
||||
$cursor = new \DateTimeImmutable( $start_date );
|
||||
$end_dt = new \DateTimeImmutable( $end_date );
|
||||
} catch ( \Exception $e ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while ( $cursor <= $end_dt ) {
|
||||
$day = $cursor->format( 'Y-m-d' );
|
||||
|
||||
$is_first = ( $day === $start_date );
|
||||
$is_last = ( $day === $end_date );
|
||||
|
||||
$events[] = array(
|
||||
'id' => $booking_id . '-' . $day,
|
||||
'title' => $title,
|
||||
'start' => $day,
|
||||
'allDay' => true,
|
||||
'color' => $color,
|
||||
'yacht_id' => $y_id,
|
||||
'extendedProps' => array(
|
||||
'is_first' => $is_first,
|
||||
'is_last_night' => $is_last,
|
||||
'booking_id' => (int) $booking_id,
|
||||
),
|
||||
);
|
||||
|
||||
$cursor = $cursor->modify( '+1 day' );
|
||||
}
|
||||
}
|
||||
|
||||
return rest_ensure_response( $events );
|
||||
}
|
||||
|
||||
/**
|
||||
* Zwraca granice nawigacji kalendarza zbiorczego: datę ostatniej rezerwacji
|
||||
* (confirmed/pending) z `_booking_end_date >= dziś`.
|
||||
*
|
||||
* Frontend używa tej wartości do ustawienia `validRange.end` w FullCalendar,
|
||||
* blokując nawigację w przód poza miesiąc ostatniej rezerwacji.
|
||||
*
|
||||
* @return \WP_REST_Response { max_booking_date: 'YYYY-MM-DD' | null }
|
||||
*/
|
||||
public function get_availability_bounds() {
|
||||
$today = gmdate( 'Y-m-d' );
|
||||
|
||||
$bookings = get_posts(
|
||||
array(
|
||||
'post_type' => 'yacht_booking',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
'meta_key' => '_booking_end_date',
|
||||
'orderby' => 'meta_value',
|
||||
'meta_type' => 'DATE',
|
||||
'order' => 'DESC',
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => '_booking_status',
|
||||
'value' => array( 'confirmed', 'pending' ),
|
||||
'compare' => 'IN',
|
||||
),
|
||||
array(
|
||||
'key' => '_booking_end_date',
|
||||
'value' => $today,
|
||||
'compare' => '>=',
|
||||
'type' => 'DATE',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$max_date = null;
|
||||
if ( ! empty( $bookings ) ) {
|
||||
$end = (string) get_post_meta( (int) $bookings[0], '_booking_end_date', true );
|
||||
if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $end ) ) {
|
||||
$max_date = $end;
|
||||
}
|
||||
}
|
||||
|
||||
return rest_ensure_response( array( 'max_booking_date' => $max_date ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Buduje deterministyczną mapę yacht_id → kolor z palety.
|
||||
*
|
||||
|
||||
@@ -86,15 +86,30 @@
|
||||
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 */
|
||||
/* Custom kontener tytułu (renderowany przez eventContent w JS). */
|
||||
.yacht-calendar-all .yc-event-title {
|
||||
padding: 1px 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.35);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* Pasek eventu wyższy + gap między dziennymi segmentami (rezerwacja wielonocna
|
||||
= N osobnych pasków zamiast jednej belki — patrz REST split per-day). */
|
||||
.yacht-calendar-all .fc-daygrid-event {
|
||||
min-height: 18px;
|
||||
margin: 1px 2px !important;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Half-day visual: gradient wpisywany przez calendar-all.js (eventDidMount), który
|
||||
@@ -106,6 +121,11 @@
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.yacht-calendar-all .yc-event-title {
|
||||
font-size: 10px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.yacht-calendar-all .fc-toolbar.fc-header-toolbar {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate());
|
||||
}
|
||||
|
||||
function firstOfMonth(d) {
|
||||
return new Date(d.getFullYear(), d.getMonth(), 1);
|
||||
}
|
||||
|
||||
function nextMonthFirst(d) {
|
||||
return new Date(d.getFullYear(), d.getMonth() + 1, 1);
|
||||
}
|
||||
|
||||
function initCalendar(wrapper) {
|
||||
var $wrapper = $(wrapper);
|
||||
var $cal = $wrapper.find('.yacht-calendar-all');
|
||||
@@ -32,6 +40,31 @@
|
||||
var heightPx = parseInt($wrapper.data('height'), 10);
|
||||
if (!heightPx || heightPx < 200) heightPx = 650;
|
||||
|
||||
// Bounds: pobierz max_booking_date żeby ograniczyć validRange w FullCalendar.
|
||||
// Endpoint `/availability/bounds` jest siostrzany do `/availability/all`.
|
||||
var boundsUrl = restUrl.replace(/\/availability\/all.*$/, '/availability/bounds');
|
||||
|
||||
$.getJSON(boundsUrl)
|
||||
.done(function (data) {
|
||||
buildCalendar($wrapper, $cal, restUrl, heightPx, data && data.max_booking_date ? data.max_booking_date : null);
|
||||
})
|
||||
.fail(function () {
|
||||
// Graceful degradation — kalendarz bez validRange.
|
||||
buildCalendar($wrapper, $cal, restUrl, heightPx, null);
|
||||
});
|
||||
}
|
||||
|
||||
function buildCalendar($wrapper, $cal, restUrl, heightPx, maxBookingDate) {
|
||||
var today = new Date();
|
||||
var rangeStart = firstOfMonth(today);
|
||||
var rangeEnd;
|
||||
if (maxBookingDate) {
|
||||
var maxDate = new Date(maxBookingDate + 'T00:00:00');
|
||||
rangeEnd = maxDate >= today ? nextMonthFirst(maxDate) : nextMonthFirst(today);
|
||||
} else {
|
||||
rangeEnd = nextMonthFirst(today);
|
||||
}
|
||||
|
||||
var calendar = new window.FullCalendar.Calendar($cal.get(0), {
|
||||
initialView: 'dayGridMonth',
|
||||
locale: 'pl',
|
||||
@@ -41,6 +74,7 @@
|
||||
center: 'title',
|
||||
right: ''
|
||||
},
|
||||
validRange: { start: rangeStart, end: rangeEnd },
|
||||
height: heightPx,
|
||||
displayEventTime: false,
|
||||
eventDisplay: 'block',
|
||||
@@ -63,6 +97,10 @@
|
||||
var color = info.event.backgroundColor || '#3498db';
|
||||
info.el.style.setProperty('--yc-event-color', color);
|
||||
|
||||
// Native tooltip na hover — tytuł + data dnia.
|
||||
var dayStr = info.event.startStr ? info.event.startStr.slice(0, 10) : '';
|
||||
info.el.setAttribute('title', info.event.title + (dayStr ? ' (' + dayStr + ')' : ''));
|
||||
|
||||
// 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.
|
||||
@@ -70,9 +108,11 @@
|
||||
applyHalfDayGradient(info);
|
||||
});
|
||||
},
|
||||
eventContent: function () {
|
||||
// Pasek koloru bez treści — privacy: nie pokazujemy nazwisk klientów ani nazw jachtów.
|
||||
return { html: '' };
|
||||
eventContent: function (arg) {
|
||||
// Tytuł renderowany przez .text() → automatyczny escaping (XSS safe).
|
||||
var title = arg.event.title || '';
|
||||
var $el = $('<div class="yc-event-title"></div>').text(title);
|
||||
return { domNodes: [$el.get(0)] };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -169,8 +209,11 @@
|
||||
if (halfPct > 49) halfPct = 49;
|
||||
|
||||
var color = el.style.getPropertyValue('--yc-event-color') || '#3498db';
|
||||
var startTrans = info.isStart;
|
||||
var endTrans = info.isEnd;
|
||||
// Server emituje 1 event per doba — info.isStart/isEnd byłyby zawsze true.
|
||||
// Flagi half-day pochodzą z extendedProps (server wie czy to pierwszy/ostatni dzień rezerwacji).
|
||||
var props = info.event.extendedProps || {};
|
||||
var startTrans = !!props.is_first;
|
||||
var endTrans = !!props.is_last_night;
|
||||
|
||||
// Build gradient.
|
||||
var stops;
|
||||
|
||||
@@ -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.1.0
|
||||
* Version: 1.2.1
|
||||
* 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.1.0' );
|
||||
define( 'YACHT_BOOKING_VERSION', '1.2.1' );
|
||||
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