Files
hotel/modules/occupancyplan/controllers/admin/AdminOccupancyPlanController.php
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

80 lines
4.2 KiB
PHP

<?php
require_once _PS_MODULE_DIR_.'occupancyplan/classes/OccupancyPlanRenderer.php';
class AdminOccupancyPlanController extends ModuleAdminController
{
public function __construct()
{
$this->bootstrap = true;
$this->display = 'view';
parent::__construct();
$this->meta_title = $this->l('Belegungsplan');
}
public function initToolbar()
{
parent::initToolbar();
unset($this->toolbar_btn['new']);
}
public function setMedia()
{
parent::setMedia();
$this->addJS(_MODULE_DIR_.'occupancyplan/views/js/occupancyplan.js');
}
public function renderView()
{
$days = (int)Tools::getValue('days', 21);
$from = Tools::getValue('from');
if (!$from || !Validate::isDate($from)) {
$from = date('Y-m-d', strtotime('-2 day'));
}
$idHotel = (int)Tools::getValue('id_hotel');
$plan = new OccupancyPlanRenderer($from, $days, $idHotel, (int)$this->context->language->id);
$base = self::$currentIndex.'&token='.$this->token;
$nav = function ($shift) use ($from, $days, $idHotel, $base) {
return $base.'&from='.date('Y-m-d', strtotime($from.' '.$shift)).'&days='.(int)$days.($idHotel ? '&id_hotel='.$idHotel : '');
};
$link = $this->context->link;
$orderLink = function ($idOrder) use ($link) {
return $link->getAdminLink('AdminOrders').'&id_order='.(int)$idOrder.'&vieworder';
};
$hotels = Db::getInstance()->executeS('SELECT hbi.`id`, hbil.`hotel_name` FROM `'._DB_PREFIX_.'htl_branch_info` hbi
LEFT JOIN `'._DB_PREFIX_.'htl_branch_info_lang` hbil ON hbil.`id` = hbi.`id` AND hbil.`id_lang` = '.(int)$this->context->language->id.'
WHERE hbi.`active` = 1');
$e = function ($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); };
$form = '<form method="get" action="index.php" class="form-inline op-nav">'
.'<input type="hidden" name="controller" value="AdminOccupancyPlan"><input type="hidden" name="token" value="'.$e($this->token).'">'
.'<a class="btn btn-default" href="'.$e($nav('-7 day')).'"><i class="icon-chevron-left"></i> '.$this->l('Woche').'</a> '
.'<a class="btn btn-default" href="'.$e($base.'&days='.(int)$days.($idHotel ? '&id_hotel='.$idHotel : '')).'">'.$this->l('Heute').'</a> '
.'<a class="btn btn-default" href="'.$e($nav('+7 day')).'">'.$this->l('Woche').' <i class="icon-chevron-right"></i></a> '
.'<label for="op-from">'.$this->l('ab').'</label> <input id="op-from" type="date" class="form-control" name="from" value="'.$e($from).'"> '
.'<label for="op-days">'.$this->l('Tage').'</label> <select id="op-days" class="form-control" name="days">';
foreach (array(14, 21, 31, 45, 62) as $d) {
$form .= '<option value="'.$d.'"'.($d == $days ? ' selected' : '').'>'.$d.'</option>';
}
$form .= '</select> ';
if (count($hotels) > 1) {
$form .= '<select class="form-control" name="id_hotel" aria-label="'.$e($this->l('Hotel')).'"><option value="0">'.$this->l('Alle Hotels').'</option>';
foreach ($hotels as $h) {
$form .= '<option value="'.(int)$h['id'].'"'.($h['id'] == $idHotel ? ' selected' : '').'>'.$e($h['hotel_name']).'</option>';
}
$form .= '</select> ';
}
$form .= '<button class="btn btn-primary" type="submit">'.$this->l('Anzeigen').'</button></form>';
// "Book Now" with dates/hotel/room type taken over from the selection (see views/js/occupancyplan.js)
$reserveUrl = $link->getAdminLink('AdminHotelRoomsBooking');
$hint = '<p class="help-block">'.$this->l('Reservieren: freie Zelle anklicken (erste Nacht), dann die letzte Nacht in derselben Zeile anklicken.').'</p>';
return '<div class="panel" data-reserve-url="'.$e($reserveUrl).'"><div class="panel-heading"><i class="icon-calendar"></i> '.$this->l('Belegungsplan').'</div>'
.$form.$hint.'<style>.op-nav{margin-bottom:12px;display:flex;flex-wrap:wrap;gap:6px;align-items:center}</style>'
.$plan->render($orderLink).'</div>';
}
}