From f3b183840264a43566284dd16340f29a082fc754 Mon Sep 17 00:00:00 2001 From: DanielS Date: Thu, 12 Nov 2026 00:26:13 +0100 Subject: [PATCH] Add frontdesk: simple "Rezeption" page and staff profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docker/test-frontdesk.php | 120 +++++++++++ modules/frontdesk/classes/FrontDeskPage.php | 198 ++++++++++++++++++ .../frontdesk/classes/FrontDeskService.php | 121 +++++++++++ modules/frontdesk/classes/index.php | 32 +++ .../admin/AdminFrontDeskController.php | 77 +++++++ modules/frontdesk/controllers/admin/index.php | 32 +++ modules/frontdesk/controllers/index.php | 32 +++ modules/frontdesk/frontdesk.php | 109 ++++++++++ modules/frontdesk/index.php | 32 +++ modules/frontdesk/logo.png | Bin 0 -> 5839 bytes 10 files changed, 753 insertions(+) create mode 100644 docker/test-frontdesk.php create mode 100644 modules/frontdesk/classes/FrontDeskPage.php create mode 100644 modules/frontdesk/classes/FrontDeskService.php create mode 100644 modules/frontdesk/classes/index.php create mode 100644 modules/frontdesk/controllers/admin/AdminFrontDeskController.php create mode 100644 modules/frontdesk/controllers/admin/index.php create mode 100644 modules/frontdesk/controllers/index.php create mode 100644 modules/frontdesk/frontdesk.php create mode 100644 modules/frontdesk/index.php create mode 100644 modules/frontdesk/logo.png diff --git a/docker/test-frontdesk.php b/docker/test-frontdesk.php new file mode 100644 index 000000000..d9e77d745 --- /dev/null +++ b/docker/test-frontdesk.php @@ -0,0 +1,120 @@ +install(), 'module installed'); +} +check((bool)Tab::getIdFromClassName('AdminFrontDesk'), 'menu "Rezeption" exists'); +$idProfile = (int)Db::getInstance()->getValue('SELECT `id_profile` FROM `'._DB_PREFIX_.'profile_lang` WHERE `name` = \'Rezeption\''); +check($idProfile > 0, 'profile "Rezeption" exists'); +$visible = Db::getInstance()->executeS('SELECT t.`class_name` FROM `'._DB_PREFIX_.'access` a JOIN `'._DB_PREFIX_.'tab` t ON t.`id_tab` = a.`id_tab` WHERE a.`id_profile` = '.$idProfile.' AND a.`view` = 1'); +$visible = array_column($visible, 'class_name'); +check(in_array('AdminFrontDesk', $visible, true) && in_array('AdminOccupancyPlan', $visible, true), 'profile sees Rezeption and Belegungsplan'); +check(!in_array('AdminModules', $visible, true) && !in_array('AdminProducts', $visible, true) && !in_array('AdminEmployees', $visible, true), 'profile does not see modules, products, employees'); +check(count($visible) <= 15, 'profile sees only '.count($visible).' pages'); + +$cbEnabled = Configuration::get('CASPOSBRIDGE_ENABLED'); +Configuration::updateValue('CASPOSBRIDGE_ENABLED', 0); + +$db = Db::getInstance(); +$rooms = $db->executeS('SELECT `id`, `id_product`, `room_num` FROM `'._DB_PREFIX_.'htl_room_information` ORDER BY `id` DESC LIMIT 5'); +$d = function ($n) { return date('Y-m-d', strtotime(($n >= 0 ? '+' : '').$n.' day')); }; +$plan = array( + array($rooms[0], 0, 3, 1, 'Erika', 'Mustermann', 2, 0), // arrives today + array($rooms[1], 0, 1, 1, 'Hans', 'Schmidt', 1, 0), // arrives today + array($rooms[2], -3, 0, 2, 'Gisela', 'Meier', 2, 1), // leaves today + array($rooms[3], -1, 4, 2, 'Peter', 'Wagner', 2, 0), // in house + array($rooms[4], -1, 2, 1, 'Karl', 'Spät', 1, 0), // overdue +); +$ids = array(); +$customers = array(); +foreach ($plan as $p) { + list($room, $from, $to, $status, $fn, $ln, $ad, $ch) = $p; + $c = new Customer(); + $c->firstname = $fn; + $c->lastname = $ln; + $c->email = 'fd-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' => $status === 2 ? $d($from).' 15:10:00' : '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' => 'Doppelzimmer', 'room_num' => $room['room_num'], 'adults' => $ad, 'children' => $ch, + 'city' => 'Annweiler', 'country' => 'Deutschland', 'phone' => '0', 'email' => $c->email, + '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(); +} +$mine = function (array $rows) use ($ids) { + return array_values(array_filter($rows, function ($r) use ($ids) { return in_array((int)$r['id'], $ids, true); })); +}; + +try { + $s = new FrontDeskService(); + $today = $d(0); + check(count($mine($s->arrivals($today))) === 2, 'arrivals today: 2'); + check(count($mine($s->departures($today))) === 1, 'departures today: 1'); + check(count($mine($s->inHouse($today))) === 1, 'in house: 1'); + check(count($mine($s->overdue($today))) === 1, 'overdue: 1'); + + list($ok, $msg) = $s->act($ids[0], 'checkin'); + check($ok && strpos($msg, 'Erika Mustermann') !== false, 'check-in: '.$msg); + check((int)$db->getValue('SELECT id_status FROM `'._DB_PREFIX_.'htl_booking_detail` WHERE id = '.$ids[0]) === 2, 'status is checked in'); + list($ok) = $s->act($ids[0], 'checkin'); + check(!$ok, 'second check-in is refused'); + list($ok, $msg) = $s->act($ids[0], 'undo_checkin'); + check($ok && (int)$db->getValue('SELECT id_status FROM `'._DB_PREFIX_.'htl_booking_detail` WHERE id = '.$ids[0]) === 1, 'undo check-in: '.$msg); + list($ok, $msg) = $s->act($ids[2], 'checkout'); + check($ok, 'check-out without payment in QloApps (paid at POS): '.$msg); + list($ok, $msg) = $s->act($ids[2], 'undo_checkout'); + check($ok, 'undo check-out: '.$msg); + check($db->getValue('SELECT COUNT(*) FROM `'._DB_PREFIX_.'htl_booking_status_history` WHERE id_htl_booking = '.$ids[0]) >= 2, 'status history written'); + + $s->act($ids[1], 'checkin'); + $page = new FrontDeskPage($s, $today, 'index.php?controller=AdminFrontDesk&token=x', array('planUrl' => '#plan', 'reserveUrl' => '#reserve')); + $html = $page->render(array('ok' => true, 'msg' => 'Hans Schmidt (Zimmer '.$rooms[1]['room_num'].') ist eingecheckt.', 'undo' => array($ids[1], 'undo_checkin'))); + check(strpos($html, 'Einchecken') !== false && strpos($html, 'Auschecken') !== false, 'big check-in and check-out buttons'); + check(strpos($html, 'Rückgängig') !== false, 'undo button after an action'); + check(strpos($html, 'Noch nicht angekommen') !== false, 'overdue section shown'); + $future = (new FrontDeskPage($s, $d(1), 'x', array()))->render(); + check(strpos($future, 'name="fd_action"') === false, 'no action buttons on other days'); + if ($preview) { + file_put_contents($preview, 'Rezeption Vorschau' + .''.$html.''); + echo "Preview: $preview\n"; + } +} catch (Throwable $e) { + check(false, 'unexpected '.get_class($e).': '.$e->getMessage().' @ '.basename($e->getFile()).':'.$e->getLine()); +} finally { + $db->delete('htl_booking_status_history', '`id_htl_booking` IN ('.implode(',', $ids).')'); + $db->delete('htl_booking_detail', '`id` IN ('.implode(',', $ids).')'); + foreach ($customers as $idc) { + (new Customer($idc))->delete(); + } + Configuration::updateValue('CASPOSBRIDGE_ENABLED', $cbEnabled); +} +echo $fail ? "$fail check(s) FAILED\n" : "All checks passed\n"; +exit($fail ? 1 : 0); diff --git a/modules/frontdesk/classes/FrontDeskPage.php b/modules/frontdesk/classes/FrontDeskPage.php new file mode 100644 index 000000000..bf4cdb7da --- /dev/null +++ b/modules/frontdesk/classes/FrontDeskPage.php @@ -0,0 +1,198 @@ +service = $service; + $this->day = $day; + $this->today = date('Y-m-d'); + $this->actionUrl = $actionUrl; + $this->links = $links; + } + + protected static function e($s) + { + return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); + } + + public static function longDate($day) + { + $wd = array('Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'); + $mo = array(1 => 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'); + $t = strtotime($day); + return $wd[(int)date('w', $t)].', '.(int)date('j', $t).'. '.$mo[(int)date('n', $t)].' '.date('Y', $t); + } + + /** + * @param array|null $flash ['ok' => bool, 'msg' => string, 'undo' => [id, action]] + */ + public function render($flash = null) + { + $isToday = $this->day === $this->today; + $prev = date('Y-m-d', strtotime($this->day.' -1 day')); + $next = date('Y-m-d', strtotime($this->day.' +1 day')); + $dayUrl = function ($d) { + return $this->actionUrl.'&day='.$d; + }; + + $h = '
'; + $h .= '

Rezeption

'.self::e(self::longDate($this->day)) + .($isToday ? ' Heute' : '').'

' + .'
'; + + if ($flash) { + $h .= '
' + .''.($flash['ok'] ? '✓ ' : '✗ ').self::e($flash['msg']).''; + if (!empty($flash['undo'])) { + $h .= $this->button($flash['undo'][0], $flash['undo'][1], 'Rückgängig', 'fd-btn-light'); + } + $h .= '
'; + } + + $h .= '
'; + if (!empty($this->links['planUrl'])) { + $h .= 'Belegungsplan'; + } + if (!empty($this->links['reserveUrl'])) { + $h .= 'Neue Reservierung'; + } + $h .= '
'; + + if (!$isToday) { + $h .= '

Sie sehen einen anderen Tag. Ein- und Auschecken geht nur heute.

'; + } + + $overdue = $isToday ? $this->service->overdue($this->day) : array(); + if ($overdue) { + $h .= $this->section('Noch nicht angekommen', 'Diese Gäste hätten schon anreisen sollen.', $overdue, 'arrive', $isToday, 'fd-warn'); + } + $h .= $this->section('Anreise', 'Gäste, die heute ankommen.', $this->service->arrivals($this->day), 'arrive', $isToday); + $h .= $this->section('Abreise', 'Gäste, die heute abreisen. Bezahlt wird an der Kasse.', $this->service->departures($this->day), 'depart', $isToday); + $h .= $this->section('Im Haus', 'Gäste, die über Nacht bleiben.', $this->service->inHouse($this->day), 'stay', $isToday); + $h .= '
'.self::css(); + return $h; + } + + protected function section($title, $sub, array $rows, $kind, $canAct, $extra = '') + { + $h = '

'.self::e($title).' '.count($rows).'

'.self::e($sub).'

'; + if (!$rows) { + return $h.'

Niemand.

'; + } + $h .= ''; + } + + protected function card(array $r, $kind, $canAct) + { + $name = trim($r['firstname'].' '.$r['lastname']) ?: 'Buchung #'.(int)$r['id_order']; + $nights = (int)round((strtotime($r['date_to']) - strtotime($r['date_from'])) / 86400); + $people = (int)$r['adults'].' '.((int)$r['adults'] === 1 ? 'Erwachsener' : 'Erwachsene') + .((int)$r['children'] ? ', '.(int)$r['children'].' '.((int)$r['children'] === 1 ? 'Kind' : 'Kinder') : ''); + $facts = $people.' · '.$nights.' '.($nights === 1 ? 'Nacht' : 'Nächte').' · '.date('d.m.', strtotime($r['date_from'])).'–'.date('d.m.', strtotime($r['date_to'])) + .' · '.$r['room_type_name'].($r['source_name'] ? ' · '.$r['source_name'] : ''); + $status = (int)$r['id_status']; + + $action = ''; + if ($kind === 'arrive') { + if ($status === FrontDeskService::ASSIGNED) { + $action = $canAct ? $this->button($r['id'], 'checkin', 'Einchecken', 'fd-btn-go') : 'erwartet'; + } else { + $action = '✓ Eingecheckt'.($r['check_in'] && $r['check_in'] !== '0000-00-00 00:00:00' ? ' '.date('H:i', strtotime($r['check_in'])) : '').''; + } + } elseif ($kind === 'depart') { + if ($status === FrontDeskService::CHECKED_IN) { + $action = $canAct ? $this->button($r['id'], 'checkout', 'Auschecken', 'fd-btn-out') : 'reist ab'; + } else { + $action = '✓ Ausgecheckt'.($r['check_out'] && $r['check_out'] !== '0000-00-00 00:00:00' ? ' '.date('H:i', strtotime($r['check_out'])) : '').''; + } + } else { + $action = $canAct ? $this->button($r['id'], 'checkout', 'Früher abreisen', 'fd-btn-light') : ''; + } + + $pos = ''; + if (isset($r['pos_done'])) { + if ((int)$r['pos_failed']) { + $pos = 'Kasse: Fehler – bitte Kassenanbindung prüfen'; + } elseif ((int)$r['pos_done']) { + $pos = 'Kasse: '.(int)$r['pos_done'].' '.((int)$r['pos_done'] === 1 ? 'Nacht' : 'Nächte').' auf Tisch '.self::e($r['room_num']).''; + } + } + $details = !empty($this->links['orderUrl']) && (int)$r['id_order'] + ? 'Details' : ''; + + return '
  • '.self::e($r['room_num']).'
    ' + .'
    '.self::e($name).'
    '.self::e($facts).'
    '.$pos.$details.'
    ' + .'
    '.$action.'
  • '; + } + + protected function button($idBooking, $action, $label, $class) + { + return '
    ' + .'' + .'' + .'
    '; + } + + public static function css() + { + return ''; + } +} diff --git a/modules/frontdesk/classes/FrontDeskService.php b/modules/frontdesk/classes/FrontDeskService.php new file mode 100644 index 000000000..9dee920c3 --- /dev/null +++ b/modules/frontdesk/classes/FrontDeskService.php @@ -0,0 +1,121 @@ +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.'); + } +} diff --git a/modules/frontdesk/classes/index.php b/modules/frontdesk/classes/index.php new file mode 100644 index 000000000..409280877 --- /dev/null +++ b/modules/frontdesk/classes/index.php @@ -0,0 +1,32 @@ +bootstrap = true; + $this->display = 'view'; + parent::__construct(); + $this->meta_title = 'Rezeption'; + } + + public function initToolbar() + { + parent::initToolbar(); + $this->toolbar_btn = array(); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + $this->page_header_toolbar_btn = array(); + } + + protected function day() + { + $day = Tools::getValue('day'); + return $day && Validate::isDate($day) ? date('Y-m-d', strtotime($day)) : date('Y-m-d'); + } + + public function postProcess() + { + if (!Tools::isSubmit('submitFrontDesk')) { + return parent::postProcess(); + } + if (!$this->tabAccess['edit']) { + $this->errors[] = 'Keine Berechtigung.'; + return; + } + $id = (int)Tools::getValue('id_booking'); + $action = Tools::getValue('fd_action'); + $service = new FrontDeskService((int)$this->context->language->id); + list($ok, $msg) = $service->act($id, $action, (int)$this->context->employee->id); + $undo = array('checkin' => 'undo_checkin', 'checkout' => 'undo_checkout'); + $flash = array('ok' => (bool)$ok, 'msg' => $msg, 'undo' => $ok && isset($undo[$action]) ? array($id, $undo[$action]) : null); + $this->context->cookie->frontdesk_flash = json_encode($flash); + $this->context->cookie->write(); + // Post/Redirect/Get: reloading the page must not repeat the action + Tools::redirectAdmin(self::$currentIndex.'&token='.$this->token.'&day='.$this->day()); + } + + public function renderView() + { + $flash = null; + if (!empty($this->context->cookie->frontdesk_flash)) { + $flash = json_decode($this->context->cookie->frontdesk_flash, true); + unset($this->context->cookie->frontdesk_flash); + $this->context->cookie->write(); + } + $link = $this->context->link; + $page = new FrontDeskPage( + new FrontDeskService((int)$this->context->language->id), + $this->day(), + self::$currentIndex.'&token='.$this->token, + array( + 'planUrl' => Tab::getIdFromClassName('AdminOccupancyPlan') ? $link->getAdminLink('AdminOccupancyPlan') : null, + 'reserveUrl' => $link->getAdminLink('AdminHotelRoomsBooking'), + 'orderUrl' => function ($idOrder) use ($link) { + return $link->getAdminLink('AdminOrders').'&id_order='.(int)$idOrder.'&vieworder'; + }, + ) + ); + return $page->render($flash); + } +} diff --git a/modules/frontdesk/controllers/admin/index.php b/modules/frontdesk/controllers/admin/index.php new file mode 100644 index 000000000..409280877 --- /dev/null +++ b/modules/frontdesk/controllers/admin/index.php @@ -0,0 +1,32 @@ + edit rights) */ + const PROFILE_TABS = array( + 'AdminFrontDesk' => true, + 'AdminOccupancyPlan' => false, + 'AdminHotelReservationSystemManagement' => false, // parent menu of the two above + 'AdminHotelRoomsBooking' => true, // "Neue Reservierung" + 'AdminParentOrders' => false, + 'AdminOrders' => true, // order created from Book Now, details link + 'AdminGuestRegistration' => true, // Meldeschein / guest registration card + 'AdminCarts' => true, // Book Now builds the booking in a cart + 'AdminParentCustomer' => false, + 'AdminCustomers' => true, // create the guest during a booking + 'AdminAddresses' => true, + ); + + public function __construct() + { + $this->name = 'frontdesk'; + $this->tab = 'administration'; + $this->version = '1.0.0'; + $this->author = 'Zum alten Wasserrad'; + $this->need_instance = 0; + $this->bootstrap = true; + parent::__construct(); + + $this->displayName = 'Rezeption'; + $this->description = 'Einfache Tagesansicht für die Rezeption: Anreise, Abreise, Im Haus, Ein- und Auschecken mit großen Knöpfen.'; + $this->ps_versions_compliancy = array('min' => '1.6', 'max' => '1.6.99.99'); + } + + public function install() + { + return parent::install() && $this->installTab() && $this->installProfile(); + } + + public function uninstall() + { + $idTab = (int)Tab::getIdFromClassName('AdminFrontDesk'); + if ($idTab) { + (new Tab($idTab))->delete(); + } + return parent::uninstall(); + } + + protected function installTab() + { + if (Tab::getIdFromClassName('AdminFrontDesk')) { + return true; + } + $tab = new Tab(); + $tab->class_name = 'AdminFrontDesk'; + $tab->module = $this->name; + $tab->id_parent = (int)Tab::getIdFromClassName('AdminHotelReservationSystemManagement'); + foreach (Language::getLanguages(false) as $lang) { + $tab->name[$lang['id_lang']] = 'Rezeption'; + } + $tab->add(); // returns initAccess(), false on CLI; the tab exists anyway + if (!$tab->id) { + return false; + } + // first entry of its menu group + Db::getInstance()->execute('UPDATE `'._DB_PREFIX_.'tab` SET `position` = `position` + 1 WHERE `id_parent` = '.(int)$tab->id_parent.' AND `id_tab` <> '.(int)$tab->id); + Db::getInstance()->update('tab', array('position' => 0), '`id_tab` = '.(int)$tab->id); + return true; + } + + /** Profile "Rezeption" with view/edit only on the pages the front desk needs. */ + public function installProfile() + { + $idProfile = (int)Db::getInstance()->getValue('SELECT `id_profile` FROM `'._DB_PREFIX_.'profile_lang` WHERE `name` = \'Rezeption\''); + if (!$idProfile) { + $profile = new Profile(); + foreach (Language::getLanguages(false) as $lang) { + $profile->name[$lang['id_lang']] = 'Rezeption'; + } + if (!$profile->add()) { + return false; + } + $idProfile = (int)$profile->id; + } + // start from "no access" for every page, then open the listed ones + Db::getInstance()->delete('access', '`id_profile` = '.$idProfile); + $rows = array(); + foreach (Db::getInstance()->executeS('SELECT `id_tab`, `class_name` FROM `'._DB_PREFIX_.'tab`') as $t) { + $allowed = array_key_exists($t['class_name'], self::PROFILE_TABS); + $edit = $allowed && self::PROFILE_TABS[$t['class_name']]; + $rows[] = array('id_profile' => $idProfile, 'id_tab' => (int)$t['id_tab'], 'view' => (int)$allowed, + 'add' => (int)$edit, 'edit' => (int)$edit, 'delete' => 0, 'kpi' => 0); + } + return Db::getInstance()->insert('access', $rows); + } + + public function getContent() + { + Tools::redirectAdmin($this->context->link->getAdminLink('AdminFrontDesk')); + } +} diff --git a/modules/frontdesk/index.php b/modules/frontdesk/index.php new file mode 100644 index 000000000..409280877 --- /dev/null +++ b/modules/frontdesk/index.php @@ -0,0 +1,32 @@ +%MbFb#(c+?M5%lSWwefWRwO4M`=luK6tq!?1D!N-F&rZBr&RpkNr!o`_YB z!DgbC|Ep3i)K#E^$=|%ewg``L3f*aG!;{;$Wo5)I&{7M}~4KU>~@DUS?>9o-)N|_S5mQO^wT9bb94w^oY|@V_`^^ z8wz!HLwP+N8Ph$>xi!hgQI$y8Fnicjl|9I+yTZ;;s%{W+$!NJ#Z70q5Pf~rs>o9#% zHwg`%{!V@39OlM7fzElK?1gi9*R^%b8y>JPF&pNIhMenNHfNI9yNHQ*v_}(Cs4>-w z^M2jrwOl1p;bPZfb$&S#i0LlO0XV{heN=6r7u=>M{ z>2*le;Li#oUk=~!Zi%X`w!E=hqV+B+^*P!6G`^wiOTR>-of62>96mdZdPE8k`L*%_ zJ+%2i=*`MEPK~2c4lIqbc6?<|{?TQ@xCI0r4m2wD({|EWA%8sx+k=A(^yOwmRvO8@ zI=(NR<~3*_Qaq!E!+n_l?i}wc6g!%e8Jzi3XE`{J--ia)c|ileVI+&v=wVSQS^XKw zlx9?*6#Sg)@5ev})?yV!*l;PR$ZvsHBfbyq@s~{)zjvms6acaKqMiXv@Gw!f;$(>U zqs)>p!*drjsa-xM4za*+D(Ojl?Y`$brn$2>9jxXFyDt~a(0*LNrutInU3H{t+#E~9 z??)p=s^bfP{IQ>LS{{I*l6j0QDAqS#BR*O)z!$5@}=5 z0Z5T6U(qWa%$c2Cg=p5W@bC4OkF8sw(Kc%-g>&@>)g{A`AzTnYt&a-#pB!+-mS7)* zxqsiapY_QXg1o+eNV(I&0l7Oh>W--5HCI_7?UPQyQlcsCv$xF19;m6x4g5gu5)*CxHVE%G{%VW#B+K~OnszT!~|fiD3s_4m{H_*)cTf`Oi= z-!}YWYWr?OJMTqThlo#oxKouTn?igmdOh<|F0Bnu4gyb3j@DxrG*QH!J3$ndTFPWADhfn zuRzIQO`e6Pn#>@-J)(KGy~bDcu;=XU=Py@(7o-SL@K1w&kv(s*kN`)XDiPW}m)>u- zy&>RHsyj!r2T|3P9q(=qhHja?F$}ON> zXzjKEN5dAWSVp=YsR}3((BP_Jrf|5GfJo+r#p}mv@xXx>Q0QVkAk8yp~;+039 z^$dgHtuEfc{|R}{LExyey2U^eO$4H5E8J0gWLK^CtJ2iO%lsE4Ln=ab;LvEda_(MU zM;MZ-VG@d)o|T^nc^)k4JyXFOJC019eh>sX35(+fn0@C1l15ZC<__Z(&WOcpzdaM{` zW$N6}hnpP#$sm;dV)Qp}8%F6n01F$-&IjkYkoZQ$e6kVB#H@=`Zc&#ei+WiNzflXA z=sn*C22bsMD4!Y`Wjv>mhB^FwNHY6y#$7rBGNbNkd`CFr8^_}8XFq=-%+DQ{3=6fy zl}927s~`9r&IEd);u`P4i7~HQ;1FN&`xS~wHky2RFV@<|MliKYS+2h*eP_m5dC+|7yplH_$C(`Z51o zVC{=Ym#UO;Cr@yGfp46^63L<dO{>CbnO1kk9otML?rV7E!Xd_HC2NC4++(aL~qEli*2 znwKCQxTY;IT`}rxtZARz>OB)9Z*T@fzlg(}35PK2K~GSG9XFsTaVnGd#ZgVLndq9? zkmZ`~+0xkI!EZW5s?pcUp9=SeW(p=!KXSEha)qq13kq#0Pk!dck6&E+$>^$fm?OxY zU%%3XQbA94$3XqA*0f-e?J2`q#?joCfkDAh+7*%2Fmg+g7z>W^#peQG@iqLbr_tQ0 zg{}s2f7<@8zmkZgsro^SaJd&#rCNG1&$les*-aO9-*?fd-{U7v-q_NCUQ785g%fC0 zbVDl|ZJkz^6ZhBHEDdnCyMD|SSv+U2n?R*C)Pr}ojw_j0nQfbrTYQI8W+cj}t3hSsZI;%?HbZD?4j-xk`(NQlDZZ(%~d3k{DUnFMZgip90UC%tT(ApZ>R1z{JOC_DH!KyDHtZl9CXMJSFj_NwOZ(u1&VsFN;}I1L8JrGr!Lkq z?|<$b2HYd`t!pM$S}fV-8rxyazqgX;$qo;`6#Lc&M5{AgGBY9O3^ar;`}%IGh(XxW zZsC5F+%!6dTO>21yxI^>xjhds3MmDyn3;IUOS91oeKPoQg<%WM3({JAKU6VOuNL>L zo*Q7)uGH<}zpM>*m>1_(d$y4e7@E(1qy1VBw=;7?Rd=%hxxW;ix8qJ~RNV+Ea+4))p z34kdp^m3OxmOdl-B9CTv%DPJj3C8GP_Tk4rRTdh_Fa0i~^e{^fe?MuC`k5YRsermE_PpJ=|K!j#v(Nxg=)3czF)=go_=Qr!&OWAZt2pm-oOoESwJc$q}Ot?&__6 zDde4+w+_(bqvu$pIJh((MB65vm7)9##z~~q&H}LnxcinSg7RvDRc9}?WxYI;5=ON?IW`J&}v^7mrs@t zMv62A01;aBLiMr|uyZ_>{nf8Fj(IuBcQCCK-d^^lYjw`BbDIhb%Nvuzy-%|BbhG%b@SQE*`7ii0L|o^pu?TVl?Xu~264l-* z`%@XyW&_BcRNq#SRn;gnhrgGj!D;6`jDjfA9?#FuN8lGb`fJiirP+wVJ3Z(#&|m@e>g-bZewava zbV2}CXny7A?}0|3Ftm?TOj-oR|m%{&~gGdPNtdHXi74 zheUK9@{QL_zaVC$xhpH0u1(3b6IF>##el9(yti>|?CcbwdlJj3(`+-i7NyM7DevDZ zn{qM+9{J%X*z-6W-!3|1h~5M{RaatK_f_Z~+AddR^GSc>I{%DCr6uL|dY0@O1rBOcEnKIDQK$Mc!CEB6%wD?I#J-1t_O_jsr z53FAIgE8vsIj_JN-2OZ41Wxj{`(%AMmAv6-SYXb_+YoSYW0A1U9eyqhl7I8-)hG~S z5(+E4P^P^$a3qYxW8W%XY+yfd9E(bYe^Hm|`;(eWCOsT|V4i8)F5}uVtgnU~8FH28 zIw_tv#wlQrMNjKF8XW2>7k|jMd;&?d?L`ru;84VdpDZteOfgHOIX~`neVm52%Dst{ zWgAOj;{B0`JT)UmZB0VU>pn1`r;7T*cYO%PFskQNDy0|3R!(HKrbeu?cAjo6_o(xt zB)sFb8<%3CI5)@o{vopVt2Eh8;n<|DWuw{Worrcj?KL`K_L2R zEXQxJ6Fcdpcct_Kf<|7gb-~#A06$5fX$V@MTb@*dqsra!AAzUnqJNDK`7rnHCF!LB zT|I#fcKSZ#sZ-z5n;b)E>zv6EMsn1gNh-)Q2`BbJ=&mV-GxQQQ$;c*=tl^6NC4IJw z=)4~&CYz=|td4%<2rHF&&XGf@INalgE)wG4WKv8=Qo!JC#FJ7xxix_lW@9M2o?YEq zJA-ST7jrHN#l;}qM@ZmMPv*zpW<*^A!k2B#!;eXa%Eoe+k~#GzR)t!1l$#D+yFA

    FFLg{GkW{cn-I^x*plcZcTAM^^xAqy3-RSLnHGNxNmA{)V3xvb(@@_+LB ze?sPd0nBztt_GnRGJE+Pt)$ z^3EE3`CejwKJB35hYqN+2o7v8n6Uqw#GzL2i6$NDc}$W0EjMdMLxcT`wGP;?HNvdl z?R|tsNmqHeoa(jJi?MQx&7TWP50rtQmCr=jR4Q0U>QjLNM(wt#kzLUF(9eeMV%){ShWDGxv zI;qe0Kf79b94PQbYE$Ooyp#*iRB*|=(W4*zacMPzGPvVlv|Qe_pl!fD>Q}+}j_C^| zL|ne;ZH8H8r>76&LS$(-Zp{4IU?DcrsK4cU}%pIq# zkqaj&p_R6*hpg^*nu8#Zx~|XM))QYxwR|Zxu^h0DH2H`SW-`j4gcuA?gW?$kxcrKW7}*I#Q3r z?-m@!e_W@J=UKj3I=h%FhgV=SZ;7*g{5ttG375$s3t`5aAz}*RRSO8r=^(fnZ-br!2G*=+bd%(T8dyHyajw~^yXfnQLHPK3Io_$#Z?=zG6UYS<%O zdRflX8Z5Cieyeb~b(l7>&*VmuZNe}!Oz0z?@2*t~Vy#6gKfXPQV2X}`2{hZU3=y@+ z-763=Vi_~XLT?=o4^jIh{=AiK5$vT9)FEiIHLhTI1j#^XfRyB&9mX4*m^mai&^{~A zczE;sGfWz=lHLL^tnES)yQD#YbeQky!1#F>m(OX$pRy}{<93Fnx60c`G#mjr5hh^~ zG2TPisg}2qJl!MI;^Dr5@i}^!(IuO_6`d}Z1I#GQX{Gk&+u2y-Hy3_mB+fQYO1*Mx zT<~Cnz2*&_G`QUCp-18l)u0DpBvbgv(e}VBH;gX zC*%HeCt3c#JNaM2>3`dn|0l8XzZE7We>t~qH++$sAI3PeG@a)~6;YYj1rtdvxWv7! x-$9;n3bvLCjo0abX%RUo|HWUW?ENK<&oh3t`)Ty