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>
This commit is contained in:
120
docker/test-frontdesk.php
Normal file
120
docker/test-frontdesk.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
/**
|
||||
* Checks modules/frontdesk with throw-away guests and writes an HTML preview.
|
||||
* docker compose ... exec -u www-data app php docker/test-frontdesk.php [preview-file]
|
||||
* casposbridge is switched off during the test so no POS bookings are made.
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
exit;
|
||||
}
|
||||
define('_PS_ADMIN_DIR_', __DIR__);
|
||||
require __DIR__.'/../config/config.inc.php';
|
||||
require_once _PS_MODULE_DIR_.'hotelreservationsystem/define.php';
|
||||
require_once _PS_MODULE_DIR_.'frontdesk/classes/FrontDeskPage.php';
|
||||
|
||||
$preview = isset($argv[1]) ? $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('frontdesk')) {
|
||||
check(Module::getInstanceByName('frontdesk')->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, '<!doctype html><meta charset="utf-8"><title>Rezeption Vorschau</title>'
|
||||
.'<body style="font-family:Open Sans,Arial,sans-serif;margin:16px;background:#eff1f2">'.$html.'</body>');
|
||||
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);
|
||||
198
modules/frontdesk/classes/FrontDeskPage.php
Normal file
198
modules/frontdesk/classes/FrontDeskPage.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
/**
|
||||
* HTML for the reception page: one screen, large type, one obvious button per guest.
|
||||
*/
|
||||
require_once dirname(__FILE__).'/FrontDeskService.php';
|
||||
|
||||
class FrontDeskPage
|
||||
{
|
||||
protected $service;
|
||||
protected $day;
|
||||
protected $today;
|
||||
/** @var string form action URL (with admin token) */
|
||||
protected $actionUrl;
|
||||
/** @var array planUrl, reserveUrl, orderUrl(callable) */
|
||||
protected $links;
|
||||
|
||||
public function __construct(FrontDeskService $service, $day, $actionUrl, array $links = array())
|
||||
{
|
||||
$this->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 = '<div class="fd">';
|
||||
$h .= '<div class="fd-top"><div><h1 class="fd-title">Rezeption</h1><p class="fd-date">'.self::e(self::longDate($this->day))
|
||||
.($isToday ? ' <span class="fd-today">Heute</span>' : '').'</p></div>'
|
||||
.'<nav class="fd-days" aria-label="Tag wählen">'
|
||||
.'<a class="fd-btn fd-btn-light" href="'.self::e($dayUrl($prev)).'">‹ Vortag</a>'
|
||||
.($isToday ? '' : '<a class="fd-btn fd-btn-light" href="'.self::e($dayUrl($this->today)).'">Heute</a>')
|
||||
.'<a class="fd-btn fd-btn-light" href="'.self::e($dayUrl($next)).'">Folgetag ›</a></nav></div>';
|
||||
|
||||
if ($flash) {
|
||||
$h .= '<div class="fd-flash '.($flash['ok'] ? 'fd-flash-ok' : 'fd-flash-err').'" role="status">'
|
||||
.'<span>'.($flash['ok'] ? '✓ ' : '✗ ').self::e($flash['msg']).'</span>';
|
||||
if (!empty($flash['undo'])) {
|
||||
$h .= $this->button($flash['undo'][0], $flash['undo'][1], 'Rückgängig', 'fd-btn-light');
|
||||
}
|
||||
$h .= '</div>';
|
||||
}
|
||||
|
||||
$h .= '<div class="fd-tiles">';
|
||||
if (!empty($this->links['planUrl'])) {
|
||||
$h .= '<a class="fd-tile" href="'.self::e($this->links['planUrl']).'"><span aria-hidden="true">▦</span>Belegungsplan</a>';
|
||||
}
|
||||
if (!empty($this->links['reserveUrl'])) {
|
||||
$h .= '<a class="fd-tile" href="'.self::e($this->links['reserveUrl']).'"><span aria-hidden="true">+</span>Neue Reservierung</a>';
|
||||
}
|
||||
$h .= '</div>';
|
||||
|
||||
if (!$isToday) {
|
||||
$h .= '<p class="fd-note">Sie sehen einen anderen Tag. Ein- und Auschecken geht nur heute.</p>';
|
||||
}
|
||||
|
||||
$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 .= '</div>'.self::css();
|
||||
return $h;
|
||||
}
|
||||
|
||||
protected function section($title, $sub, array $rows, $kind, $canAct, $extra = '')
|
||||
{
|
||||
$h = '<section class="fd-sec '.$extra.'"><h2>'.self::e($title).' <span class="fd-count">'.count($rows).'</span></h2><p class="fd-sub">'.self::e($sub).'</p>';
|
||||
if (!$rows) {
|
||||
return $h.'<p class="fd-empty">Niemand.</p></section>';
|
||||
}
|
||||
$h .= '<ul class="fd-list">';
|
||||
foreach ($rows as $r) {
|
||||
$h .= $this->card($r, $kind, $canAct);
|
||||
}
|
||||
return $h.'</ul></section>';
|
||||
}
|
||||
|
||||
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') : '<span class="fd-state">erwartet</span>';
|
||||
} else {
|
||||
$action = '<span class="fd-state fd-done">✓ Eingecheckt'.($r['check_in'] && $r['check_in'] !== '0000-00-00 00:00:00' ? ' '.date('H:i', strtotime($r['check_in'])) : '').'</span>';
|
||||
}
|
||||
} elseif ($kind === 'depart') {
|
||||
if ($status === FrontDeskService::CHECKED_IN) {
|
||||
$action = $canAct ? $this->button($r['id'], 'checkout', 'Auschecken', 'fd-btn-out') : '<span class="fd-state">reist ab</span>';
|
||||
} else {
|
||||
$action = '<span class="fd-state fd-done">✓ Ausgecheckt'.($r['check_out'] && $r['check_out'] !== '0000-00-00 00:00:00' ? ' '.date('H:i', strtotime($r['check_out'])) : '').'</span>';
|
||||
}
|
||||
} 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 = '<span class="fd-pos fd-pos-bad">Kasse: Fehler – bitte Kassenanbindung prüfen</span>';
|
||||
} elseif ((int)$r['pos_done']) {
|
||||
$pos = '<span class="fd-pos">Kasse: '.(int)$r['pos_done'].' '.((int)$r['pos_done'] === 1 ? 'Nacht' : 'Nächte').' auf Tisch '.self::e($r['room_num']).'</span>';
|
||||
}
|
||||
}
|
||||
$details = !empty($this->links['orderUrl']) && (int)$r['id_order']
|
||||
? '<a class="fd-details" href="'.self::e(call_user_func($this->links['orderUrl'], (int)$r['id_order'])).'">Details</a>' : '';
|
||||
|
||||
return '<li class="fd-card"><div class="fd-room" aria-label="Zimmer">'.self::e($r['room_num']).'</div>'
|
||||
.'<div class="fd-info"><div class="fd-name">'.self::e($name).'</div><div class="fd-facts">'.self::e($facts).'</div>'.$pos.$details.'</div>'
|
||||
.'<div class="fd-act">'.$action.'</div></li>';
|
||||
}
|
||||
|
||||
protected function button($idBooking, $action, $label, $class)
|
||||
{
|
||||
return '<form method="post" action="'.self::e($this->actionUrl.'&day='.$this->day).'" class="fd-form">'
|
||||
.'<input type="hidden" name="id_booking" value="'.(int)$idBooking.'">'
|
||||
.'<input type="hidden" name="fd_action" value="'.self::e($action).'">'
|
||||
.'<button type="submit" name="submitFrontDesk" value="1" class="fd-btn '.$class.'">'.self::e($label).'</button></form>';
|
||||
}
|
||||
|
||||
public static function css()
|
||||
{
|
||||
return '<style>
|
||||
.fd{font-size:18px;line-height:1.45;color:#1b2631;max-width:1100px}
|
||||
.fd-top{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-end;gap:12px;margin-bottom:14px}
|
||||
.fd-title{font-size:34px;font-weight:700;margin:0}
|
||||
.fd-date{font-size:22px;margin:2px 0 0}
|
||||
.fd-today{display:inline-block;background:#1b2631;color:#fff;border-radius:6px;padding:0 10px;font-size:16px;vertical-align:middle}
|
||||
.fd-days{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.fd-btn{display:inline-flex;align-items:center;justify-content:center;min-height:56px;min-width:140px;padding:10px 22px;border-radius:10px;border:2px solid transparent;font-size:20px;font-weight:700;cursor:pointer;text-decoration:none;line-height:1.2}
|
||||
.fd-btn:focus-visible,.fd-tile:focus-visible,.fd-details:focus-visible{outline:4px solid #f0b400;outline-offset:2px}
|
||||
.fd-btn-go{background:#1e7d32;color:#fff}.fd-btn-go:hover{background:#17652a;color:#fff}
|
||||
.fd-btn-out{background:#1557a0;color:#fff}.fd-btn-out:hover{background:#114a88;color:#fff}
|
||||
.fd-btn-light{background:#fff;color:#1b2631;border-color:#9aa7b4}.fd-btn-light:hover{background:#eef2f6;color:#1b2631;text-decoration:none}
|
||||
.fd-tiles{display:flex;gap:12px;flex-wrap:wrap;margin:6px 0 18px}
|
||||
.fd-tile{display:flex;align-items:center;gap:12px;flex:1 1 240px;min-height:72px;padding:14px 20px;border-radius:12px;background:#fff;border:2px solid #c9d2db;font-size:22px;font-weight:700;color:#1b2631;text-decoration:none}
|
||||
.fd-tile:hover{border-color:#1557a0;color:#1557a0;text-decoration:none}
|
||||
.fd-tile span{font-size:30px;line-height:1}
|
||||
.fd-flash{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:12px;padding:14px 18px;border-radius:12px;font-size:22px;font-weight:700;margin-bottom:16px}
|
||||
.fd-flash-ok{background:#dff3e2;border:2px solid #1e7d32;color:#0f4419}
|
||||
.fd-flash-err{background:#fde3e1;border:2px solid #b3261e;color:#6b1510}
|
||||
.fd-note{font-size:20px;background:#fff6d6;border:2px solid #e0b100;border-radius:10px;padding:10px 16px}
|
||||
.fd-sec{background:#fff;border:2px solid #dbe2e8;border-radius:14px;padding:16px 18px;margin-bottom:18px}
|
||||
.fd-sec h2{font-size:26px;font-weight:700;margin:0;display:flex;align-items:center;gap:10px}
|
||||
.fd-count{background:#1b2631;color:#fff;border-radius:999px;min-width:36px;text-align:center;font-size:18px;padding:2px 10px}
|
||||
.fd-sub{margin:2px 0 12px;color:#4a5866}
|
||||
.fd-warn{border-color:#e0b100;background:#fffbea}
|
||||
.fd-empty{font-size:20px;color:#4a5866;margin:0}
|
||||
.fd-list{list-style:none;margin:0;padding:0;display:grid;gap:10px}
|
||||
.fd-card{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:16px;padding:12px 14px;border:2px solid #e3e8ed;border-radius:12px}
|
||||
.fd-room{font-size:30px;font-weight:800;min-width:84px;text-align:center;background:#eef2f6;border-radius:10px;padding:8px 10px}
|
||||
.fd-name{font-size:24px;font-weight:700}
|
||||
.fd-facts{color:#3c4a57}
|
||||
.fd-pos{display:inline-block;margin-top:4px;font-size:16px;color:#1e7d32;font-weight:600}
|
||||
.fd-pos-bad{color:#b3261e}
|
||||
.fd-details{display:inline-block;margin:4px 0 0 12px;font-size:16px}
|
||||
.fd-state{font-size:20px;font-weight:700;color:#4a5866}
|
||||
.fd-done{color:#1e7d32}
|
||||
.fd-form{margin:0}
|
||||
@media (max-width:700px){.fd-card{grid-template-columns:auto 1fr}.fd-act{grid-column:1 / -1}.fd-act .fd-btn{width:100%}}
|
||||
</style>';
|
||||
}
|
||||
}
|
||||
121
modules/frontdesk/classes/FrontDeskService.php
Normal file
121
modules/frontdesk/classes/FrontDeskService.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?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.');
|
||||
}
|
||||
}
|
||||
32
modules/frontdesk/classes/index.php
Normal file
32
modules/frontdesk/classes/index.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* NOTICE OF LICENSE
|
||||
*
|
||||
* This source file is subject to the Open Software License version 3.0
|
||||
* that is bundled with this package in the file LICENSE.md
|
||||
* It is also available through the world-wide-web at this URL:
|
||||
* https://opensource.org/license/osl-3-0-php
|
||||
* If you did not receive a copy of the license and are unable to
|
||||
* obtain it through the world-wide-web, please send an email
|
||||
* to support@qloapps.com so we can send you a copy immediately.
|
||||
*
|
||||
* DISCLAIMER
|
||||
*
|
||||
* Do not edit or add to this file if you wish to upgrade this module to a newer
|
||||
* versions in the future. If you wish to customize this module for your needs
|
||||
* please refer to https://store.webkul.com/customisation-guidelines for more information.
|
||||
*
|
||||
* @author Webkul IN
|
||||
* @copyright Since 2010 Webkul
|
||||
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
|
||||
*/
|
||||
|
||||
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
|
||||
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
|
||||
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
|
||||
header('Location: ../../../');
|
||||
exit;
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
require_once _PS_MODULE_DIR_.'frontdesk/classes/FrontDeskPage.php';
|
||||
|
||||
class AdminFrontDeskController extends ModuleAdminController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
32
modules/frontdesk/controllers/admin/index.php
Normal file
32
modules/frontdesk/controllers/admin/index.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* NOTICE OF LICENSE
|
||||
*
|
||||
* This source file is subject to the Open Software License version 3.0
|
||||
* that is bundled with this package in the file LICENSE.md
|
||||
* It is also available through the world-wide-web at this URL:
|
||||
* https://opensource.org/license/osl-3-0-php
|
||||
* If you did not receive a copy of the license and are unable to
|
||||
* obtain it through the world-wide-web, please send an email
|
||||
* to support@qloapps.com so we can send you a copy immediately.
|
||||
*
|
||||
* DISCLAIMER
|
||||
*
|
||||
* Do not edit or add to this file if you wish to upgrade this module to a newer
|
||||
* versions in the future. If you wish to customize this module for your needs
|
||||
* please refer to https://store.webkul.com/customisation-guidelines for more information.
|
||||
*
|
||||
* @author Webkul IN
|
||||
* @copyright Since 2010 Webkul
|
||||
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
|
||||
*/
|
||||
|
||||
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
|
||||
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
|
||||
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
|
||||
header('Location: ../../../');
|
||||
exit;
|
||||
32
modules/frontdesk/controllers/index.php
Normal file
32
modules/frontdesk/controllers/index.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* NOTICE OF LICENSE
|
||||
*
|
||||
* This source file is subject to the Open Software License version 3.0
|
||||
* that is bundled with this package in the file LICENSE.md
|
||||
* It is also available through the world-wide-web at this URL:
|
||||
* https://opensource.org/license/osl-3-0-php
|
||||
* If you did not receive a copy of the license and are unable to
|
||||
* obtain it through the world-wide-web, please send an email
|
||||
* to support@qloapps.com so we can send you a copy immediately.
|
||||
*
|
||||
* DISCLAIMER
|
||||
*
|
||||
* Do not edit or add to this file if you wish to upgrade this module to a newer
|
||||
* versions in the future. If you wish to customize this module for your needs
|
||||
* please refer to https://store.webkul.com/customisation-guidelines for more information.
|
||||
*
|
||||
* @author Webkul IN
|
||||
* @copyright Since 2010 Webkul
|
||||
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
|
||||
*/
|
||||
|
||||
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
|
||||
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
|
||||
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
|
||||
header('Location: ../../../');
|
||||
exit;
|
||||
109
modules/frontdesk/frontdesk.php
Normal file
109
modules/frontdesk/frontdesk.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* Rezeption: one simple, large page for the front desk (arrivals, departures, in house,
|
||||
* check-in/out with undo) and an employee profile "Rezeption" that only sees what it needs.
|
||||
*/
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class Frontdesk extends Module
|
||||
{
|
||||
/** back-office pages the "Rezeption" profile may open (class name => 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'));
|
||||
}
|
||||
}
|
||||
32
modules/frontdesk/index.php
Normal file
32
modules/frontdesk/index.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/**
|
||||
* NOTICE OF LICENSE
|
||||
*
|
||||
* This source file is subject to the Open Software License version 3.0
|
||||
* that is bundled with this package in the file LICENSE.md
|
||||
* It is also available through the world-wide-web at this URL:
|
||||
* https://opensource.org/license/osl-3-0-php
|
||||
* If you did not receive a copy of the license and are unable to
|
||||
* obtain it through the world-wide-web, please send an email
|
||||
* to support@qloapps.com so we can send you a copy immediately.
|
||||
*
|
||||
* DISCLAIMER
|
||||
*
|
||||
* Do not edit or add to this file if you wish to upgrade this module to a newer
|
||||
* versions in the future. If you wish to customize this module for your needs
|
||||
* please refer to https://store.webkul.com/customisation-guidelines for more information.
|
||||
*
|
||||
* @author Webkul IN
|
||||
* @copyright Since 2010 Webkul
|
||||
* @license https://opensource.org/license/osl-3-0-php Open Software License version 3.0
|
||||
*/
|
||||
|
||||
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
|
||||
header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
|
||||
|
||||
header('Cache-Control: no-store, no-cache, must-revalidate');
|
||||
header('Cache-Control: post-check=0, pre-check=0', false);
|
||||
header('Pragma: no-cache');
|
||||
|
||||
header('Location: ../../../');
|
||||
exit;
|
||||
BIN
modules/frontdesk/logo.png
Normal file
BIN
modules/frontdesk/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
Reference in New Issue
Block a user