New back-office page "Hotel Reservation System > Belegungsplan": - rooms grouped by room type as rows, 14-62 days as columns, weekends and today highlighted, sticky room column, daily occupancy footer - bookings as bars (booked / checked in / checked out / no-show) with guest name, guests, tooltip with dates, order reference and price; click opens the order; stays outside the view are cut - booking source badge from orders.id_source (W website, T phone, B Booking.com, E Expedia ...; portals highlighted), so channel-manager bookings show up with their source - blocked periods (htl_room_disable_dates) hatched with reason - CASPOS badge: nights already posted by casposbridge, "!" on failures - reserve: click a free cell (first night) and a later one (last night) in the same row -> action bar -> "Book Now" opens with dates, hotel and room type prefilled; keyboard: Enter/Space, Escape - docker/test-occupancyplan.php: 15 checks with throw-away bookings, optional HTML preview Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
4.6 KiB
JavaScript
112 lines
4.6 KiB
JavaScript
/**
|
||
* Belegungsplan: select free nights in one room row and hand them to "Book Now".
|
||
* Click (or Enter/Space) a free cell = first night, click a later free cell in the same row = last night.
|
||
* Escape cancels. The bar's "Reservieren" link opens AdminHotelRoomsBooking with dates and room type.
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
function init() {
|
||
var root = document.querySelector('[data-reserve-url]');
|
||
if (!root) {
|
||
return;
|
||
}
|
||
var sel = null; // {row, start, end} cells
|
||
var bar = document.createElement('div');
|
||
bar.className = 'op-actionbar';
|
||
bar.setAttribute('role', 'status');
|
||
bar.hidden = true;
|
||
bar.innerHTML = '<span class="op-actiontext"></span>'
|
||
+ '<a class="btn btn-primary op-reserve" href="#"><i class="icon-plus"></i> Reservieren</a>'
|
||
+ '<button type="button" class="btn btn-default op-cancel">Abbrechen</button>';
|
||
root.appendChild(bar);
|
||
|
||
var weekday = ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'];
|
||
function fmt(iso) {
|
||
var p = iso.split('-');
|
||
var d = new Date(Date.UTC(+p[0], +p[1] - 1, +p[2]));
|
||
return weekday[d.getUTCDay()] + ' ' + p[2] + '.' + p[1] + '.';
|
||
}
|
||
function addDay(iso) {
|
||
var p = iso.split('-');
|
||
var d = new Date(Date.UTC(+p[0], +p[1] - 1, +p[2] + 1));
|
||
return d.toISOString().slice(0, 10);
|
||
}
|
||
function cells(row) {
|
||
return Array.prototype.slice.call(row.querySelectorAll('.op-cell'));
|
||
}
|
||
function clear() {
|
||
Array.prototype.forEach.call(root.querySelectorAll('.op-sel'), function (c) { c.classList.remove('op-sel'); });
|
||
sel = null;
|
||
bar.hidden = true;
|
||
}
|
||
function paint() {
|
||
Array.prototype.forEach.call(root.querySelectorAll('.op-sel'), function (c) { c.classList.remove('op-sel'); });
|
||
var list = cells(sel.row);
|
||
var a = list.indexOf(sel.start), b = list.indexOf(sel.end);
|
||
for (var i = a; i <= b; i++) {
|
||
list[i].classList.add('op-sel');
|
||
}
|
||
var from = sel.start.getAttribute('data-date');
|
||
var to = addDay(sel.end.getAttribute('data-date'));
|
||
var nights = b - a + 1;
|
||
bar.querySelector('.op-actiontext').textContent = 'Zimmer ' + sel.row.getAttribute('data-room-num') + ' · '
|
||
+ fmt(from) + ' – ' + fmt(to) + ' · ' + nights + (nights === 1 ? ' Nacht' : ' Nächte')
|
||
+ ' (in „Book Now“ dieses Zimmer auswählen)';
|
||
var url = root.getAttribute('data-reserve-url')
|
||
+ '&date_from=' + from + '&date_to=' + to
|
||
+ '&id_hotel=' + encodeURIComponent(sel.row.getAttribute('data-hotel'))
|
||
+ '&id_room_type=' + encodeURIComponent(sel.row.getAttribute('data-room-type'));
|
||
bar.querySelector('.op-reserve').setAttribute('href', url);
|
||
bar.hidden = false;
|
||
}
|
||
function pick(cell) {
|
||
var row = cell.closest('.op-row');
|
||
if (sel && sel.row === row) {
|
||
var list = cells(row);
|
||
var a = list.indexOf(sel.start), b = list.indexOf(cell);
|
||
if (b >= a) {
|
||
for (var i = a; i <= b; i++) {
|
||
if (!list[i].classList.contains('op-free')) {
|
||
sel = {row: row, start: cell, end: cell}; // range crosses a booking: start over
|
||
paint();
|
||
return;
|
||
}
|
||
}
|
||
sel.end = cell;
|
||
paint();
|
||
return;
|
||
}
|
||
}
|
||
sel = {row: row, start: cell, end: cell};
|
||
paint();
|
||
}
|
||
|
||
root.addEventListener('click', function (e) {
|
||
var cell = e.target.closest('.op-free');
|
||
if (cell && root.contains(cell)) {
|
||
pick(cell);
|
||
}
|
||
});
|
||
root.addEventListener('keydown', function (e) {
|
||
var cell = e.target.closest && e.target.closest('.op-free');
|
||
if (cell && (e.key === 'Enter' || e.key === ' ')) {
|
||
e.preventDefault();
|
||
pick(cell);
|
||
}
|
||
});
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape' && sel) {
|
||
clear();
|
||
}
|
||
});
|
||
bar.querySelector('.op-cancel').addEventListener('click', clear);
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|