Files
hotel/modules/occupancyplan/views/js/occupancyplan.js
DanielS e4db9fae82 Add occupancyplan: Belegungsplan (rooms x days) with click-to-reserve
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>
2026-11-12 00:14:14 +01:00

112 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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();
}
})();