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>
122 lines
6.2 KiB
PHP
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.');
|
|
}
|
|
}
|