This commit is contained in:
2026-05-08 00:12:37 +02:00
parent 811069a25c
commit 7278a422af
18 changed files with 1356 additions and 43 deletions

View File

@@ -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.
*

View File

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

View File

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

View File

@@ -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__ ) );