Files
hotel/modules/frontdesk/classes/FrontDeskService.php
DanielS f3b1838402 Add frontdesk: simple "Rezeption" page and staff profile
One large-type page for the front desk (Hotel Reservation System >
Rezeption, first menu entry):
- today's arrivals, departures, guests in house and overdue arrivals,
  each guest as a card with big room number, name, people, nights,
  room type and booking source
- one obvious button per guest: Einchecken / Auschecken (and "Früher
  abreisen"); after each action a big confirmation with "Rückgängig"
  instead of confirm dialogs; Post/Redirect/Get so reload never repeats
- uses HotelBookingDetail::changeStatus(), so status history and hooks
  (casposbridge posts the nights) run as in the order page; check-out
  does not require payment in QloApps because guests pay at the POS
- shows the CASPOS state per guest; other days are read-only
- tiles to Belegungsplan and Neue Reservierung (Book Now)
- employee profile "Rezeption" that only sees Rezeption, Belegungsplan,
  Book Now, orders, customers, carts and guest registration cards
- docker/test-frontdesk.php: 21 checks with throw-away guests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 00:26:13 +01:00

122 lines
6.2 KiB
PHP

<?php
/**
* Data and actions for the reception page: who arrives, who leaves, who is in house,
* plus check-in / check-out / undo through HotelBookingDetail::changeStatus(), so the
* same hooks run as in the order page (e.g. casposbridge posts the nights to the POS).
*/
class FrontDeskService
{
const ASSIGNED = 1;
const CHECKED_IN = 2;
const CHECKED_OUT = 3;
const NO_SHOW = 4;
const CANCELLED = 5;
protected $idLang;
public function __construct($idLang = null)
{
$this->idLang = $idLang ?: (int)Configuration::get('PS_LANG_DEFAULT');
}
protected function select($where, $order)
{
$hasCaspos = (bool)Db::getInstance()->getValue('SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = \''._DB_PREFIX_.'casposbridge_job\'');
$sql = 'SELECT bd.`id`, bd.`id_order`, bd.`id_room`, bd.`room_num`, bd.`room_type_name`, bd.`id_status`,
bd.`adults`, bd.`children`, DATE(bd.`date_from`) AS date_from, DATE(bd.`date_to`) AS date_to,
bd.`check_in`, bd.`check_out`, c.`firstname`, c.`lastname`, c.`email`, o.`reference`,
srcl.`name` AS source_name, src.`code` AS source_code'
.($hasCaspos ? ', (SELECT COUNT(*) FROM `'._DB_PREFIX_.'casposbridge_job` j WHERE j.`id_htl_booking` = bd.`id` AND j.`status` = \'done\') AS pos_done,
(SELECT COUNT(*) FROM `'._DB_PREFIX_.'casposbridge_job` j WHERE j.`id_htl_booking` = bd.`id` AND j.`status` = \'failed\') AS pos_failed' : '').'
FROM `'._DB_PREFIX_.'htl_booking_detail` bd
LEFT JOIN `'._DB_PREFIX_.'customer` c ON c.`id_customer` = bd.`id_customer`
LEFT JOIN `'._DB_PREFIX_.'orders` o ON o.`id_order` = bd.`id_order`
LEFT JOIN `'._DB_PREFIX_.'source` src ON src.`id_source` = o.`id_source`
LEFT JOIN `'._DB_PREFIX_.'source_lang` srcl ON srcl.`id_source` = src.`id_source` AND srcl.`id_lang` = '.(int)$this->idLang.'
WHERE bd.`is_back_order` = 0 AND '.$where
.(method_exists('OrderReturn', 'getRefundedBookingIdsSubquery') ? ' AND bd.`id` NOT IN ('.OrderReturn::getRefundedBookingIdsSubquery().')' : '')
.' ORDER BY '.$order;
return (array)Db::getInstance()->executeS($sql);
}
/** Guests arriving on $day: still to check in, and those already checked in that day. */
public function arrivals($day)
{
return $this->select('DATE(bd.`date_from`) = \''.pSQL($day).'\' AND bd.`id_status` IN ('.self::ASSIGNED.', '.self::CHECKED_IN.')',
'bd.`id_status`, bd.`room_num`');
}
/** Guests due to leave on $day (still in house) and those already checked out that day. */
public function departures($day)
{
return $this->select('DATE(bd.`date_to`) = \''.pSQL($day).'\' AND bd.`id_status` IN ('.self::CHECKED_IN.', '.self::CHECKED_OUT.')',
'FIELD(bd.`id_status`, '.self::CHECKED_IN.', '.self::CHECKED_OUT.'), bd.`room_num`');
}
/** Guests in house who neither arrive nor leave on $day. */
public function inHouse($day)
{
return $this->select('bd.`id_status` = '.self::CHECKED_IN.' AND DATE(bd.`date_from`) < \''.pSQL($day).'\' AND DATE(bd.`date_to`) > \''.pSQL($day).'\'',
'bd.`room_num`');
}
/** Overdue: should have arrived before $day but are still only booked. */
public function overdue($day)
{
return $this->select('bd.`id_status` = '.self::ASSIGNED.' AND DATE(bd.`date_from`) < \''.pSQL($day).'\' AND DATE(bd.`date_to`) > \''.pSQL($day).'\'',
'bd.`date_from`, bd.`room_num`');
}
/**
* @param string $action checkin | checkout | undo_checkin | undo_checkout
* @return array [bool ok, string message]
*/
public function act($idBooking, $action, $idEmployee = 0)
{
$b = new HotelBookingDetail((int)$idBooking);
if (!Validate::isLoadedObject($b)) {
return array(false, 'Buchung nicht gefunden.');
}
$guest = trim((string)Db::getInstance()->getValue('SELECT CONCAT(`firstname`, " ", `lastname`) FROM `'._DB_PREFIX_.'customer` WHERE `id_customer` = '.(int)$b->id_customer));
$who = ($guest ?: 'Gast').' (Zimmer '.$b->room_num.')';
$now = date('Y-m-d H:i:s');
$params = array('id_employee' => (int)$idEmployee, 'remark' => 'Rezeption');
switch ($action) {
case 'checkin':
if ((int)$b->id_status !== self::ASSIGNED) {
return array(false, $who.' ist nicht im Status „gebucht“.');
}
$b->check_in = $now;
$b->check_out = '';
$ok = $b->changeStatus(self::CHECKED_IN, $params);
return array($ok, $ok ? $who.' ist eingecheckt.' : 'Einchecken hat nicht geklappt.');
case 'checkout':
if ((int)$b->id_status !== self::CHECKED_IN) {
return array(false, $who.' ist nicht eingecheckt.');
}
$b->check_out = $now;
// billing happens at the POS (CASPOS); the QloApps payment state is not checked here
$ok = $b->changeStatus(self::CHECKED_OUT, $params);
return array($ok, $ok ? $who.' ist ausgecheckt.' : 'Auschecken hat nicht geklappt.');
case 'undo_checkin':
if ((int)$b->id_status !== self::CHECKED_IN) {
return array(false, $who.' ist nicht eingecheckt.');
}
$b->check_in = '';
$b->check_out = '';
$ok = $b->changeStatus(self::ASSIGNED, $params);
return array($ok, $ok ? 'Einchecken von '.$who.' zurückgenommen.' : 'Zurücknehmen hat nicht geklappt.');
case 'undo_checkout':
if ((int)$b->id_status !== self::CHECKED_OUT) {
return array(false, $who.' ist nicht ausgecheckt.');
}
$b->check_out = '';
$ok = $b->changeStatus(self::CHECKED_IN, $params);
return array($ok, $ok ? 'Auschecken von '.$who.' zurückgenommen.' : 'Zurücknehmen hat nicht geklappt.');
}
return array(false, 'Unbekannte Aktion.');
}
}