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 = '';
+ }
+ $h .= '';
+ foreach ($rows as $r) {
+ $h .= $this->card($r, $kind, $canAct);
+ }
+ return $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 @@
+