Files
hotel/docker/test-occupancyplan.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

110 lines
6.0 KiB
PHP

<?php
/**
* Checks modules/occupancyplan with throw-away bookings and writes an HTML preview.
* docker compose ... exec -u www-data app php docker/test-occupancyplan.php [preview-file]
* Bookings, the blocked period and the preview are removed again unless --keep is given.
*/
if (PHP_SAPI !== 'cli') {
exit;
}
define('_PS_ADMIN_DIR_', __DIR__);
require __DIR__.'/../config/config.inc.php';
require_once _PS_MODULE_DIR_.'occupancyplan/classes/OccupancyPlanRenderer.php';
$keep = in_array('--keep', $argv, true);
$preview = isset($argv[1]) && $argv[1] !== '--keep' ? $argv[1] : null;
$fail = 0;
function check($c, $m)
{
global $fail;
echo ($c ? ' [ok] ' : ' [FAIL] ').$m.PHP_EOL;
$fail += $c ? 0 : 1;
}
if (!Module::isInstalled('occupancyplan')) {
check(Module::getInstanceByName('occupancyplan')->install(), 'module installed');
}
check((bool)Tab::getIdFromClassName('AdminOccupancyPlan'), 'admin tab "Belegungsplan" exists');
$db = Db::getInstance();
$rooms = $db->executeS('SELECT `id`, `id_product`, `room_num` FROM `'._DB_PREFIX_.'htl_room_information` ORDER BY `id` LIMIT 6');
$d = function ($n) { return date('Y-m-d', strtotime(($n >= 0 ? '+' : '').$n.' day')); };
$guests = array(
array($rooms[0], -4, 2, 3, 'Anna', 'Becker', 2, 0), // checked out, ends today+? no: -4..2? see dates below
array($rooms[1], -1, 3, 2, 'Luca', 'Rossi', 2, 1), // checked in
array($rooms[2], 2, 6, 1, 'Mia', 'Schneider', 1, 0), // booked
array($rooms[3], -6, -1, 3, 'Jonas', 'Weber', 2, 0), // checked out
array($rooms[4], 5, 30, 1, 'Familie', 'Mirabile-Test', 2, 2), // long stay beyond the view
array($rooms[1], 3, 5, 1, 'Ole', 'Hansen', 1, 0), // next guest in the same room
array($rooms[5], 0, 1, 4, 'Kein', 'Erscheinen', 2, 0), // no-show
);
$ids = array();
$customers = array();
foreach ($guests as $g) {
list($room, $from, $to, $status, $fn, $ln, $ad, $ch) = $g;
$c = new Customer();
$c->firstname = $fn;
$c->lastname = $ln;
$c->email = 'op-test-'.uniqid().'@hotel.test';
$c->passwd = Tools::passwdGen(16);
$c->add();
$customers[] = (int)$c->id;
$db->insert('htl_booking_detail', array(
'id_product' => (int)$room['id_product'], 'id_order' => 0, 'id_order_detail' => 1, 'id_cart' => 1, 'id_room' => (int)$room['id'],
'id_hotel' => 1, 'id_customer' => (int)$c->id, 'booking_type' => 1, 'id_status' => $status,
'check_in' => '0000-00-00 00:00:00', 'check_out' => '0000-00-00 00:00:00', 'planned_check_out' => $d($to).' 11:00:00',
'date_from' => $d($from).' 00:00:00', 'date_to' => $d($to).' 00:00:00',
'total_price_tax_excl' => 100, 'total_price_tax_incl' => 107, 'total_paid_amount' => 0, 'is_back_order' => 0,
'hotel_name' => 'Test', 'room_type_name' => 'Test', 'room_num' => $room['room_num'], 'adults' => $ad, 'children' => $ch,
'city' => 'Annweiler', 'country' => 'Deutschland', 'phone' => '0', 'email' => 'test@hotel.test',
'check_in_time' => '15:00', 'check_out_time' => '11:00', 'date_add' => date('Y-m-d H:i:s'), 'date_upd' => date('Y-m-d H:i:s'),
));
$ids[] = (int)$db->Insert_ID();
}
$db->insert('htl_room_disable_dates', array('id_room_type' => (int)$rooms[5]['id_product'], 'id_room' => (int)$rooms[5]['id'],
'date_from' => $d(4), 'date_to' => $d(8), 'reason' => 'Malerarbeiten', 'date_add' => date('Y-m-d H:i:s'), 'date_upd' => date('Y-m-d H:i:s')));
$idBlock = (int)$db->Insert_ID();
try {
$plan = new OccupancyPlanRenderer($d(-2), 21);
$html = $plan->render(function ($id) { return '#order-'.$id; });
check(substr_count($html, 'class="op-bar op-in') === 2, 'one checked-in bar (+ legend)');
check(substr_count($html, 'class="op-bar op-assigned') >= 3, 'booked bars incl. legend');
check(substr_count($html, 'op-cut-l') >= 1, 'stay starting before the view is cut on the left');
check(substr_count($html, 'op-cut-r') >= 1, 'long stay is cut on the right');
check(strpos($html, 'Malerarbeiten') !== false, 'blocked period with reason');
check(substr_count($html, 'op-today') >= 2, 'today highlighted');
check(strpos($html, '<script') === false, 'no script in output');
check(substr_count($html, 'op-free') > 50 && substr_count($html, 'op-busy') > 10, 'free and busy cells marked');
check(strpos($html, 'data-room-type=') !== false && strpos($html, 'data-date="'.$d(0).'"') !== false, 'cells carry room type and date for reserving');
$s = OccupancyPlanRenderer::source(array('source_code' => 'BOOKING_COM', 'source_name' => 'Booking.com', 'id_business_source' => 2));
check($s[0] === 'B' && $s[2] === 'op-src-ota', 'Booking.com badge "B" styled as portal');
$s = OccupancyPlanRenderer::source(array('source_code' => 'PHONE', 'source_name' => 'Telefon', 'id_business_source' => 1));
check($s[0] === 'T' && $s[2] === '', 'phone badge "T"');
$found = 0;
foreach ($plan->getBookings() as $list) {
foreach ($list as $bk) {
$found += in_array((int)$bk['id'], $ids, true) ? 1 : 0;
}
}
check($found === 7, 'all 7 test bookings found (got '.$found.')');
if ($preview) {
file_put_contents($preview, '<!doctype html><meta charset="utf-8"><title>Belegungsplan Vorschau</title>'
.'<body style="font-family:Open Sans,Arial,sans-serif;margin:16px;background:#eff1f2">'
.'<div data-reserve-url="#book-now?controller=AdminHotelRoomsBooking" style="background:#fff;padding:12px;border-radius:4px">'
.'<h3 style="margin-top:0">Belegungsplan (Vorschau mit Testbuchungen)</h3>'.$html.'</div>'
.'<script>'.file_get_contents(_PS_MODULE_DIR_.'occupancyplan/views/js/occupancyplan.js').'</script></body>');
echo "Preview: $preview\n";
}
} finally {
if (!$keep) {
$db->delete('htl_booking_detail', '`id` IN ('.implode(',', $ids).')');
$db->delete('htl_room_disable_dates', '`id` = '.$idBlock);
foreach ($customers as $idc) {
(new Customer($idc))->delete();
}
}
}
echo $fail ? "$fail check(s) FAILED\n" : "All checks passed\n";
exit($fail ? 1 : 0);