403Webshell
Server IP : 202.61.199.114  /  Your IP : 216.73.217.139
Web Server : nginx/1.22.1
System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64
User : web20 ( 1018)
PHP Version : 8.4.23
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : ON
Directory :  /var/www/seaside.pacim.de/web/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /var/www/seaside.pacim.de/web/terminal.php
<?php
/**
 * terminal.php – Terminal-Portal (fester Hash). PIN-gesichert; zeigt die zugeordneten Mobile-Ansichten
 * als Übersicht. Auswahl führt zu day-list.php?t=<token>; die Einzel-PIN wird dabei NICHT abgefragt
 * (Terminal-Sitzung = Cookie term_<hash>; siehe pacim_terminal_grants() in day-list.php/checkin.php).
 */
require_once( __DIR__ . '/includes/config.inc.php' );
require_once( __DIR__ . '/includes/mobileaccess.php' );
require_once( __DIR__ . '/includes/day-arrivals.php' );   // Dashboard-Aggregation + native Karten-Renderer
require_once( __DIR__ . '/includes/terminals.php' );
@ini_set( 'display_errors', '0' );
error_reporting( 0 );
while ( ob_get_level() > 0 ) ob_end_clean();

$hash   = trim( (string) ( $_GET['h'] ?? ( $_POST['h'] ?? '' ) ) );
$action = (string) ( $_POST['action'] ?? ( $_GET['action'] ?? '' ) );   // unlock=POST, more=GET (AJAX)
$db = new MysqliDb( MYSQL_HOST, MYSQL_USER, MYSQL_PASS, MYSQL_DB );

function t_e( $s ) { return htmlspecialchars( (string) $s, ENT_QUOTES, 'UTF-8' ); }
// Suchstring normalisieren (muss zur JS-Normalisierung der Bestandssuche passen).
function t_norm( $s ) { return preg_replace( '/[^0-9a-zäöüß]/u', '', mb_strtolower( (string) $s, 'UTF-8' ) ); }
function t_css() {
    return '@font-face{font-family:"EuroPlate";src:url("' . SITE_URL . '/assets/fonts/EuroPlate.ttf") format("truetype");font-display:swap;}
    :root{--navy:#0d2c44;--navy2:#11436b;--blue:#1b6ca8;--ink:#16384f;--muted:#5b7488;--line:#e6eef4;}
    *{box-sizing:border-box;margin:0;padding:0;}
    .d-none{display:none!important;}
    html{-webkit-text-size-adjust:100%;text-size-adjust:100%;}
    body{font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:#eef3f7;color:var(--ink);min-height:100vh;
         padding:0 env(safe-area-inset-right,0px) calc(40px + env(safe-area-inset-bottom,0px)) env(safe-area-inset-left,0px);touch-action:manipulation;}
    /* Kopfbereich (Titel + Suche) bleibt beim Scrollen oben fixiert. */
    .tm-sticky{position:sticky;top:0;z-index:50;background:#eef3f7;box-shadow:0 6px 14px rgba(13,44,68,.06);}
    .hd{background:linear-gradient(135deg,var(--navy) 0%,var(--navy2) 65%,var(--blue) 130%);color:#fff;padding:calc(22px + env(safe-area-inset-top,0px)) 18px 18px;}
    .tm-hdwrap{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;}
    .tm-hdact{flex:0 0 auto;display:flex;gap:8px;}
    .tm-set{flex:0 0 auto;width:40px;height:40px;border:0;border-radius:50%;background:rgba(255,255,255,.16);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;text-decoration:none;-webkit-tap-highlight-color:transparent;}
    .tm-set:active{background:rgba(255,255,255,.3);} .tm-set svg{width:21px;height:21px;}
    /* „Alle Ansichten": gleiche Außen-Abstände (Seitengutter) wie der Dashboard-Slider */
    .tm-allwrap{padding:0 14px;}
    .tmset-lbl{display:block;font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);margin:12px 0 5px;}
    .tmset-in{width:100%;border:1px solid var(--line);border-radius:10px;padding:11px 12px;font-size:15px;font-family:inherit;}
    .tmset-pinbtn{width:100%;border:1px dashed #cdd9e4;background:#f6f9fc;border-radius:10px;padding:12px;font-size:15px;font-weight:700;color:var(--navy);cursor:pointer;font-family:inherit;}
    .tmset-hint{font-size:12px;color:var(--muted);margin-top:6px;}
    .tmset-msg{font-size:13px;font-weight:600;margin:10px 0;min-height:16px;} .tmset-msg.ok{color:#1d7a33;} .tmset-msg.err{color:#a23030;}
    .tmset-save{width:100%;background:var(--blue);color:#fff;border:0;border-radius:11px;padding:13px;font-size:16px;font-weight:800;cursor:pointer;font-family:inherit;}
    .hd .k{font-size:11px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;opacity:.8;}
    .hd .t{font-size:22px;font-weight:800;margin-top:3px;}
    .wrap{max-width:560px;margin:0 auto;}
    .grid{display:grid;grid-template-columns:1fr;gap:12px;padding:16px;}
    @media(min-width:480px){.grid{grid-template-columns:1fr 1fr;}}
    .tile{display:block;text-decoration:none;color:inherit;background:#fff;border:1px solid var(--line);border-radius:14px;padding:16px;box-shadow:0 2px 8px rgba(13,44,68,.06);transition:transform .08s ease,box-shadow .12s ease;}
    .tile:active{transform:scale(.98);}
    .tile .tl{font-size:17px;font-weight:800;color:var(--navy);}
    .tile .td{font-size:13px;color:var(--muted);margin-top:2px;}
    .tile .chips{margin-top:8px;display:flex;flex-wrap:wrap;gap:5px;}
    .chip{background:#eef3f7;border-radius:999px;padding:2px 9px;font-size:11.5px;font-weight:600;color:var(--navy);}
    .tile .go{margin-top:10px;font-size:13px;font-weight:700;color:var(--blue);}
    .empty{margin:30px 18px;padding:24px;background:#fff;border-radius:14px;text-align:center;color:var(--muted);border:1px dashed var(--line);}
    .foot{color:var(--muted);font-size:12px;text-align:center;margin-top:8px;}
    /* Dashboard-Slider (oben): bis zu 3 Anreisetage als native Widget-Karten (horizontal swipebar). */
    .dswipe-wrap{padding:14px 0 4px;}
    .dswipe{display:flex;overflow-x:auto;scroll-snap-type:x mandatory;-webkit-overflow-scrolling:touch;gap:0;scrollbar-width:none;}
    .dswipe::-webkit-scrollbar{display:none;}
    .dslide{flex:0 0 100%;scroll-snap-align:center;padding:0 14px;box-sizing:border-box;}
    .dwidget{background:#fff;border-radius:16px;box-shadow:0 4px 18px rgba(13,44,68,.12);overflow:hidden;display:flex;flex-direction:column;max-height:66vh;}
    .dwidget-h{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:14px 16px 4px;}
    .dwidget-k{font-size:10.5px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:#8aa0b3;}
    .dwidget-t{font-size:18px;font-weight:800;color:#16384f;margin-top:2px;}
    .dwidget-open{flex:0 0 auto;background:#eef3f7;color:#1b6ca8;text-decoration:none;font-size:13px;font-weight:700;border-radius:9px;padding:7px 11px;}
    .dwidget-open:active{background:#dde7f0;}
    .dwidget-sub{padding:0 16px 10px;font-size:13px;color:#5b7488;font-weight:600;border-bottom:1px solid #eef3f7;}
    .dwidget .dwt{flex:1 1 auto;min-height:0;}
    .dwidget .dwt-body .dw-card:last-child{border-bottom:0;}
    .dwidget-body{overflow-y:auto;-webkit-overflow-scrolling:touch;}
    .dwidget-body .dw-card:last-child{border-bottom:0;}
    .dwidget-empty{padding:24px 16px;text-align:center;color:#8aa0b3;}
    .dwt-sub{padding:10px 16px;font-size:13px;color:#5b7488;font-weight:600;border-bottom:1px solid #eef3f7;}
    .dwt-load{display:flex;align-items:center;justify-content:center;gap:10px;padding:46px 16px;color:#8aa0b3;font-size:14px;}
    .ddots{display:flex;justify-content:center;gap:7px;margin-top:10px;}
    .ddot{width:8px;height:8px;border-radius:50%;background:#c4d2de;transition:background .15s,transform .15s;}
    .ddot.on{background:var(--navy);transform:scale(1.25);}
    .sec-h{max-width:560px;margin:18px auto 0;padding:0 16px;font-size:12px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);}
    /* Login-Auswahl (mehrere Login-Zugänge) */
    .login-pick{padding:16px;}
    .login-pick-h{font-size:13px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin:4px 2px 12px;}
    .login-item{display:flex;align-items:center;gap:14px;background:#fff;border:1px solid var(--line);border-radius:14px;padding:16px;margin-bottom:10px;text-decoration:none;color:inherit;box-shadow:0 2px 8px rgba(13,44,68,.06);}
    .login-item:active{transform:scale(.99);}
    .login-ic{flex:0 0 auto;width:42px;height:42px;border-radius:50%;background:var(--navy);color:#fff;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:800;}
    .login-nm{flex:1;min-width:0;font-size:17px;font-weight:700;color:var(--navy);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
    .login-item svg{width:20px;height:20px;color:var(--muted);flex:0 0 auto;}
    /* Alle Ansichten: nach Datum gruppiert (Akkordeon) + Info-Modal */
    .dgroup{background:#fff;border:1px solid var(--line);border-radius:14px;margin-bottom:10px;overflow:hidden;box-shadow:0 2px 8px rgba(13,44,68,.05);}
    .dgroup-h{width:100%;display:flex;align-items:center;gap:10px;background:none;border:0;cursor:pointer;padding:14px 16px;font:inherit;text-align:left;-webkit-tap-highlight-color:transparent;}
    .dgroup-l{flex:1;min-width:0;display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;}
    .dgroup-d{font-size:16px;font-weight:800;color:var(--navy);}
    .dgroup-rel{font-size:12px;font-weight:700;color:#fff;background:var(--blue);border-radius:999px;padding:2px 9px;}
    .dgroup-n{flex:0 0 auto;background:#eef3f7;color:var(--navy);font-size:13px;font-weight:800;border-radius:999px;min-width:24px;height:24px;display:flex;align-items:center;justify-content:center;padding:0 7px;}
    .dgroup-c{width:20px;height:20px;color:var(--muted);transition:transform .18s ease;flex:0 0 auto;}
    .dgroup.open .dgroup-c{transform:rotate(180deg);}
    .dgroup-body{display:none;border-top:1px solid var(--line);}
    .dgroup.open .dgroup-body{display:block;}
    .vrow{display:flex;align-items:stretch;border-bottom:1px solid var(--line);}
    .vrow:last-child{border-bottom:0;}
    .vrow-main{flex:1;min-width:0;display:flex;align-items:center;gap:10px;text-decoration:none;color:inherit;padding:13px 14px;-webkit-tap-highlight-color:transparent;}
    .vrow-main:active{background:#f4f8fb;}
    .vmode{flex:0 0 auto;font-size:11px;font-weight:800;border-radius:7px;padding:3px 8px;text-transform:uppercase;letter-spacing:.02em;}
    .vmode.m-anreise{background:#e7f1fb;color:#1b6ca8;} .vmode.m-abreise{background:#fdeee7;color:#b5651d;} .vmode.m-dashboard{background:#e9f7ee;background:#e6f4ea;color:#1d7a33;}
    .vtext{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;}
    .vlabel{font-size:15px;font-weight:700;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
    .vstatus{font-size:12px;color:var(--muted);font-weight:600;} .vstatus b{color:var(--navy);}
    .vgo{width:18px;height:18px;color:var(--muted);flex:0 0 auto;}
    .vinfo{flex:0 0 auto;width:50px;border:0;border-left:1px solid var(--line);background:none;color:var(--blue);cursor:pointer;display:flex;align-items:center;justify-content:center;-webkit-tap-highlight-color:transparent;}
    .vinfo svg{width:22px;height:22px;} .vinfo:active{background:#eef3f7;}
    .vmodal{position:fixed;inset:0;z-index:70;background:rgba(13,44,68,.45);display:flex;align-items:flex-end;}
    .vsheet{background:#fff;width:100%;max-width:560px;margin:0 auto;border-radius:18px 18px 0 0;max-height:80vh;display:flex;flex-direction:column;padding-bottom:env(safe-area-inset-bottom,0px);}
    .vsheet-h{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:16px 18px;font-size:17px;font-weight:800;color:var(--navy);border-bottom:1px solid var(--line);}
    .vsheet-x{background:none;border:0;font-size:28px;line-height:1;color:var(--muted);cursor:pointer;}
    .vsheet-b{overflow-y:auto;padding:16px 18px 22px;}
    .vsheet-sub{font-size:12.5px;color:var(--muted);margin-bottom:12px;}
    .vbadges{display:flex;flex-wrap:wrap;gap:8px;}
    .vbadge{border-radius:999px;padding:7px 13px;font-size:13.5px;font-weight:700;border:1.5px solid transparent;}
    .vbadge.b-mode{background:var(--navy);color:#fff;}
    .vbadge.b-info{background:#eef3f7;color:var(--navy);border-color:#dde7f0;}
    .vbadge.b-ok{background:#e6f4ea;color:#1d7a33;}
    .vbadge.b-warn{background:#fdeee7;color:#b5651d;}
    .vbadge.b-lock{background:#fdeaea;color:#a23030;}
    /* „Alle anzeigen"-Button (weitere Termin-Tage nachladen) */
    .term-more{width:100%;max-width:560px;margin:4px auto 0;display:flex;align-items:center;justify-content:center;gap:10px;background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px;font:inherit;font-size:15px;font-weight:700;color:var(--blue);cursor:pointer;box-shadow:0 2px 8px rgba(13,44,68,.05);-webkit-tap-highlight-color:transparent;}
    .term-more:active{background:#f4f8fb;}
    .tm-spin{width:18px;height:18px;border:2.5px solid #cdd9e4;border-top-color:var(--blue);border-radius:50%;animation:tmspin .7s linear infinite;}
    @keyframes tmspin{to{transform:rotate(360deg);}}'
        . pacim_dash_css() . '
    /* PIN-Screen: gemeinsame Komponente (pacim_pin_css), hier nicht nötig. */
    .err{max-width:440px;background:#fff;border-radius:18px;padding:34px 26px;text-align:center;box-shadow:0 14px 40px rgba(13,44,68,.16);margin:60px auto;}
    .err h1{font-size:40px;color:var(--navy);} .err p{color:var(--muted);margin-top:8px;}
    /* ---- Bestands-Live-Suche (über dem Dashboard) ---- */
    .ts-searchwrap{padding:12px 16px 12px;}
    .ts-search{position:relative;}
    .ts-search input{width:100%;padding:13px 44px 13px 16px;border:1px solid var(--line);border-radius:12px;font-size:15px;background:#fff;color:var(--ink);font-family:inherit;box-shadow:0 2px 8px rgba(13,44,68,.05);}
    .ts-search input:focus{outline:none;border-color:var(--blue);box-shadow:0 0 0 3px rgba(27,108,168,.12);}
    .ts-search input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none;}
    .ts-clear{position:absolute;right:7px;top:50%;transform:translateY(-50%);width:30px;height:30px;border:0;border-radius:50%;background:#eef3f7;color:var(--muted);font-size:20px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;-webkit-tap-highlight-color:transparent;}
    .ts-clear:active{filter:brightness(.94);}
    .suggest{background:#fff8e6;border:1px solid #f3e3b0;border-radius:12px;padding:9px 11px;margin:9px 0 0;display:flex;flex-wrap:wrap;align-items:center;gap:8px;}
    .suggest .sg-l{font-size:12.5px;font-weight:700;color:#8a6400;}
    .suggest .sg-chip{background:#fff;border:1.5px solid #e8c766;border-radius:8px;padding:6px 12px;font-size:14px;font-weight:800;color:var(--navy);cursor:pointer;font-family:inherit;}
    .suggest .sg-chip:active{filter:brightness(.95);}
    .ts-results{margin-top:8px;}
    .ts-rescount{padding:4px 4px 8px;font-size:12.5px;color:var(--muted);font-weight:600;}
    .tblwrap{overflow-x:auto;-webkit-overflow-scrolling:touch;background:#fff;border-radius:14px;box-shadow:0 2px 8px rgba(13,44,68,.05);}
    .tbl{width:100%;border-collapse:collapse;background:#fff;}
    .tbl th{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);text-align:left;padding:10px 12px;border-bottom:2px solid var(--line);background:#f6f9fc;}
    .tbl th:first-child{border-top-left-radius:14px;} .tbl th:last-child{border-top-right-radius:14px;}
    .tbl td{padding:12px 12px;border-bottom:1px solid var(--line);font-size:14px;vertical-align:middle;}
    .tbl tbody tr{cursor:pointer;transition:background .2s ease;-webkit-tap-highlight-color:transparent;}
    .tbl tbody tr:last-child td{border-bottom:0;}
    .tbl tbody tr.st-in{background:rgba(47,179,68,.18);box-shadow:inset 3px 0 0 #2fb344;}
    .tbl tbody tr.st-open{background:rgba(245,159,0,.18);box-shadow:inset 3px 0 0 #f59f00;}
    .tbl tbody tr.st-paid{background:rgba(27,108,168,.12);box-shadow:inset 3px 0 0 #1b6ca8;}
    .tbl tbody tr.st-storno{background:rgba(214,57,57,.10);box-shadow:inset 3px 0 0 #d63939;}
    .tbl tbody tr.st-storno .plate{text-decoration:line-through;color:#a23030;}
    .tbl tbody tr:active{filter:brightness(.96);}
    .tbl td.plate{font-family:"EuroPlate",-apple-system,Segoe UI,Roboto,sans-serif;font-weight:800;font-size:20.4px;letter-spacing:.5px;color:var(--navy);white-space:nowrap;-webkit-text-stroke:.45px currentColor;}
    .tbl .nm{color:var(--ink);} .tbl .mut{color:var(--muted);}
    .tbl td.typ-cell{white-space:nowrap;}
    .tbl td.typ-cell .typ-name{display:block;font-weight:700;color:var(--navy);font-size:13.5px;line-height:1.2;}
    .tbl td.typ-cell .typ-days{display:block;font-size:11.5px;color:var(--muted);margin-top:1px;line-height:1.2;}
    .tbl .nm .nm-name{display:block;font-size:15.5px;font-weight:600;color:var(--navy);line-height:1.2;}
    .tbl .nm .nm-ship{display:block;font-size:11.5px;font-weight:500;color:var(--muted);margin-top:1px;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:170px;}
    .tbl mark{background:#ffe28a;color:inherit;border-radius:3px;padding:0 1px;}
    .ts-empty{padding:34px 20px;text-align:center;color:var(--muted);}
    /* Check-in-Overlay (iframe) – wie in der Tagesliste */
    .ov{position:fixed;inset:0;z-index:80;background:#fff;display:flex;flex-direction:column;padding-bottom:env(safe-area-inset-bottom,0px);}
    .ov-top{flex:0 0 auto;height:env(safe-area-inset-top,0px);background:var(--navy);}
    .ov-frame{flex:1 1 auto;width:100%;border:0;background:#fff;min-height:0;}';
}
function t_html( $title, $body, $code = 200 ) {
    http_response_code( $code );
    header( 'Content-Type: text/html; charset=utf-8' );
    header( 'Cache-Control: no-store' );   // Status/Zahlen sind tagesaktuell – kein CDN-Cache
    echo '<!DOCTYPE html><html lang="de"><head><meta charset="utf-8">'
       . '<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover">'
       . '<meta name="robots" content="noindex,nofollow">'
       // Web-App-Modus: chrome-loses Vollbild beim Start vom Home-Bildschirm (iOS + Android)
       . '<meta name="apple-mobile-web-app-capable" content="yes"><meta name="mobile-web-app-capable" content="yes">'
       . '<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"><meta name="theme-color" content="#0d2c44">'
       // Web-App (PWA): Manifest + iOS-Home-Icon für das Terminal (eigene „PaCIM Terminal"-App), sobald ein gültiger Hash vorliegt.
       . ( empty( $GLOBALS['tm_hash'] ) ? '' :
             '<link rel="manifest" href="' . SITE_URL . '/manifest.php?term=' . rawurlencode( $GLOBALS['tm_hash'] ) . '">'
           . '<link rel="apple-touch-icon" href="' . SITE_URL . '/assets/webapp/icon-180.png">'
           . '<meta name="apple-mobile-web-app-title" content="PaCIM Terminal">' )
       . '<title>' . t_e( $title ) . '</title><style>' . t_css() . '</style></head><body>' . $body
       // Vollbild beim ersten Tippen anfordern (Android-Chrome unterstützt das; iOS-Safari ignoriert es still).
       . '<script>(function(){function go(){var e=document.documentElement,r=e.requestFullscreen||e.webkitRequestFullscreen;if(r){try{r.call(e);}catch(_){}}}'
       . 'document.addEventListener("pointerdown",function f(e){if(e.target&&e.target.closest&&e.target.closest("a,button,input,textarea,select,label"))return;document.removeEventListener("pointerdown",f);go();},{passive:true});})();</script>'
       // „Zum Startbildschirm hinzufügen"-Anleitung, falls im Browser (nicht als installierte App) geöffnet.
       . ( empty( $GLOBALS['tm_hash'] ) ? '' : pacim_webapp_install_html( 'PaCIM Terminal', SITE_URL . '/assets/webapp/icon-192.png' ) )
       . pacim_webapp_loader_html()
       . ( function_exists( 'pacim_webapp_offline_html' ) ? pacim_webapp_offline_html() : '' )
       // Chat nur im angemeldeten Zustand – NICHT auf PIN-/Login-Sperrbildschirmen (würde dort über der PIN liegen).
       . ( ( function_exists( 'pacim_chat_widget_html' ) && empty( $GLOBALS['tm_locked'] ) ) ? pacim_chat_widget_html( '', (string) ( $GLOBALS['tm_hash'] ?? '' ), 0 ) : '' )
       . '</body></html>';
    exit();
}

/** Eine Datums-Gruppe der „Alle Ansichten"-Liste rendern (Termine eines Tages).
 *  Status (angereist/abgereist) nur für HEUTE – für künftige Tage gibt es noch keinen Anreisestatus. */
function t_daygroup( $db, $d, $list, $hash, $relLbl, $wdT, $modeLbl, $open, $today ) {
    $ts = strtotime( $d ); $rel = $relLbl( $d ); $isToday = ( $d === $today );
    $dlabel = $wdT[ (int) date( 'N', $ts ) ] . ', ' . date( 'd.m.Y', $ts );
    ob_start(); ?>
  <div class="dgroup<?= $open ? ' open' : '' ?>">
    <button type="button" class="dgroup-h" aria-expanded="<?= $open ? 'true' : 'false' ?>">
      <span class="dgroup-l"><span class="dgroup-d"><?= t_e( $dlabel ) ?></span><?php if ( $rel ) { ?> <span class="dgroup-rel"><?= t_e( $rel ) ?></span><?php } ?></span>
      <span class="dgroup-n"><?= count( $list ) ?></span>
      <svg class="dgroup-c" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
    </button>
    <div class="dgroup-body">
      <?php foreach ( $list as $m ) {
            $url    = SITE_URL . '/day-list.php?t=' . $m['ma_token'] . '&term=' . $hash . '&d=' . $d;
            $mode   = pacim_ma_mode( $m );
            $badges = function_exists( 'pacim_ma_perm_badges' ) ? pacim_ma_perm_badges( $m ) : array();
            $vlabel = trim( (string) $m['ma_label'] ) !== '' ? $m['ma_label'] : ( $modeLbl[ $mode ] . 'liste' );
            // Status nur für heute. Zuerst aus der Hilfstabelle (Cron); bei Fehlen live berechnen + cachen.
            $vstat = '';
            if ( $isToday ) {
                $cache = function_exists( 'pacim_vs_get' ) ? pacim_vs_get( $db, $m['ma_token'], $d ) : null;
                if ( $cache ) { $vtot = (int) $cache['vs_total']; $varr = (int) $cache['vs_arrived']; }
                else {
                    $vda  = pacim_day_arrivals( $db, $d, pacim_ma_filter_opts( $m ) );
                    $vtot = count( $vda['items'] ); $varr = 0; foreach ( $vda['items'] as $vi ) if ( ! empty( $vi['arrived'] ) ) $varr++;
                    if ( function_exists( 'pacim_vs_set' ) ) pacim_vs_set( $db, $m['ma_token'], $d, $vtot, $varr );
                }
                $vstat = '<b>' . (int) $varr . '</b> / ' . (int) $vtot . ' ' . ( $mode === 'abreise' ? 'abgereist' : 'angereist' );
            } ?>
        <div class="vrow">
          <a class="vrow-main" href="<?= t_e( $url ) ?>">
            <span class="vmode m-<?= $mode ?>"><?= t_e( $modeLbl[ $mode ] ) ?></span>
            <span class="vtext">
              <span class="vlabel"><?= t_e( $vlabel ) ?></span>
              <?php if ( $vstat !== '' ) { ?><span class="vstatus"><?= $vstat ?></span><?php } ?>
            </span>
            <svg class="vgo" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>
          </a>
          <button type="button" class="vinfo" data-title="<?= t_e( $vlabel . ' · ' . $dlabel ) ?>" data-badges="<?= t_e( json_encode( $badges, JSON_UNESCAPED_UNICODE ) ) ?>" aria-label="Berechtigungen ansehen">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5M12 7.5v.5"/></svg>
          </button>
        </div>
      <?php } ?>
    </div>
  </div>
    <?php return ob_get_clean();
}

$t = pacim_terminal_load( $db, $hash );
if ( $t ) $GLOBALS['tm_hash'] = $hash;  // aktiviert Manifest/Home-Icon der „PaCIM Terminal"-App in t_html()
if ( ! $t ) {
    if ( $action !== '' ) { header( 'Content-Type: application/json' ); echo '{"ok":false,"error":"Terminal ungültig oder deaktiviert."}'; exit(); }
    t_html( 'Ungültig', '<div class="err"><h1>403</h1><p>Dieses Terminal ist ungültig oder deaktiviert.</p></div>', 403 );
}

// Abmelden: Terminal-Sitzung beenden (Cookie löschen) → zurück zur PIN-/Login-Abfrage.
if ( $action === 'logout' ) {
    // Abmeldung protokollieren (Login-ID der noch aktiven Sitzung ermitteln).
    if ( function_exists( 'pacim_ts_end' ) ) {
        $__ls = pacim_terminal_session( $db, $t );
        $__lid = ( $__ls && $__ls['type'] === 'login' ) ? (int) $__ls['login']['tl_id'] : 0;
        pacim_ts_end( $db, $hash, $__lid );
    }
    setcookie( 'term_' . $hash, '', array( 'expires' => time() - 3600, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax' ) );
    header( 'Location: ' . SITE_URL . '/terminal.php?h=' . $hash ); exit();
}

$pinHash     = (string) $t['terminal_pin'];
$cookieNm    = 'term_' . $hash;
$logins      = pacim_terminal_logins( $db, (int) $t['terminal_id'], true );   // nutzbare Login-Zugänge
$session     = pacim_terminal_session( $db, $t );
$activeLogin = ( $session && $session['type'] === 'login' ) ? $session['login'] : null;
$needAuth    = ( $logins || $pinHash !== '' );
$unlocked    = (bool) $session || ! $needAuth;

if ( $action === 'unlock' ) {
    header( 'Content-Type: application/json; charset=utf-8' ); header( 'Cache-Control: no-store' );
    $pin = preg_replace( '/\D/', '', (string) ( $_POST['pin'] ?? '' ) );
    $ckOpt = array( 'expires' => time() + 12 * 3600, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax' );
    if ( $logins ) {
        $lid = (int) ( $_POST['login'] ?? 0 ); $sel = null;
        foreach ( $logins as $lg ) if ( (int) $lg['tl_id'] === $lid ) { $sel = $lg; break; }
        if ( $sel && strlen( $pin ) >= 4 && strlen( $pin ) <= 10 && hash_equals( (string) $sel['tl_pin'], pacim_terminal_login_pin_hash( $hash, $lid, $pin ) ) ) {
            setcookie( $cookieNm, pacim_terminal_login_cookie( $hash, $lid ), $ckOpt );
            if ( function_exists( 'pacim_ts_login' ) ) pacim_ts_login( $db, $t, $lid, ( $t['terminal_label'] ?: 'Terminal' ) . ' · ' . ( trim( (string) $sel['tl_name'] ) ?: 'Login' ) );
            echo '{"ok":true}'; exit();
        }
    } elseif ( $pinHash !== '' ) {
        if ( strlen( $pin ) >= 4 && strlen( $pin ) <= 10 && hash_equals( $pinHash, pacim_terminal_pin_hash( $hash, $pin ) ) ) {
            setcookie( $cookieNm, pacim_terminal_unlock( $hash ), $ckOpt );
            if ( function_exists( 'pacim_ts_login' ) ) pacim_ts_login( $db, $t, 0, ( $t['terminal_label'] ?: 'Terminal' ) . ' · Master-PIN' );
            echo '{"ok":true}'; exit();
        }
    } else { echo '{"ok":true}'; exit(); }
    usleep( 300000 );
    // Fehlgeschlagenen Login-Versuch protokollieren (versuchte Identität, IP, User-Agent).
    if ( function_exists( 'pacim_ts_fail' ) ) {
        $failLid = (int) ( $_POST['login'] ?? 0 ); $failNm = ( $t['terminal_label'] ?: 'Terminal' );
        if ( $logins && $failLid > 0 ) { foreach ( $logins as $lg ) if ( (int) $lg['tl_id'] === $failLid ) { $failNm .= ' · ' . ( trim( (string) $lg['tl_name'] ) ?: 'Login' ); break; } }
        else { $failNm .= ' · Master-PIN'; }
        pacim_ts_fail( $db, $t, $failLid, $failNm );
    }
    echo json_encode( array( 'ok' => false, 'error' => 'Falsche PIN.' ) ); exit();
}

if ( ! $unlocked ) {
    $GLOBALS['tm_locked'] = true;   // Sperrbildschirm: Chat-Widget & Co. nicht einblenden
    if ( $logins ) {
        $lid = (int) ( $_GET['login'] ?? 0 ); $sel = null;
        foreach ( $logins as $lg ) if ( (int) $lg['tl_id'] === $lid ) { $sel = $lg; break; }
        if ( ! $sel && count( $logins ) === 1 ) $sel = $logins[0];
        if ( ! $sel ) {
            // Mehrere Logins → erst Login wählen.
            ob_start(); ?>
<div class="hd"><div class="wrap"><div class="k">Terminal</div><div class="t"><?= t_e( $t['terminal_label'] ?: 'Terminal' ) ?></div></div></div>
<div class="wrap"><div class="login-pick">
  <div class="login-pick-h">Bitte Login wählen</div>
  <?php foreach ( $logins as $lg ) { $nm = trim( (string) $lg['tl_name'] ) ?: 'Login'; ?>
    <a class="login-item" href="?h=<?= t_e( $hash ) ?>&login=<?= (int) $lg['tl_id'] ?>">
      <span class="login-ic"><?= t_e( mb_strtoupper( mb_substr( $nm, 0, 1 ) ) ) ?></span>
      <span class="login-nm"><?= t_e( $nm ) ?></span>
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>
    </a>
  <?php } ?>
</div></div>
<?php       t_html( $t['terminal_label'] ?: 'Terminal', ob_get_clean() );
        }
        $snm  = trim( (string) $sel['tl_name'] ) ?: 'Login';
        // Bei mehreren Logins: rotes X (statt grünem Haken, solange keine Ziffer) führt zurück zur Login-Auswahl.
        $backUrl = ( count( $logins ) > 1 ) ? ( '?h=' . rawurlencode( $hash ) ) : '';
        $body = '<style>' . pacim_pin_css() . '</style>'
              . pacim_pin_markup( $snm, 'Bitte PIN eingeben.' )
              . pacim_pin_js( 'location.pathname', array( 'action' => 'unlock', 'h' => $hash, 'login' => (int) $sel['tl_id'] ), $backUrl );
        t_html( $snm, $body );
    } elseif ( $pinHash !== '' ) {
        $body = '<style>' . pacim_pin_css() . '</style>'
              . pacim_pin_markup( $t['terminal_label'] ?: 'Terminal', 'Bitte Terminal-PIN eingeben.' )
              . pacim_pin_js( 'location.pathname', array( 'action' => 'unlock', 'h' => $hash ) );
        t_html( $t['terminal_label'] ?: 'Terminal', $body );
    }
}

// Eigene Einstellungen (E-Mail / PIN) der aktuellen Sitzung speichern.
if ( $action === 'settings_save' ) {
    header( 'Content-Type: application/json; charset=utf-8' ); header( 'Cache-Control: no-store' );
    if ( ! $session ) { echo '{"ok":false,"error":"Nicht angemeldet."}'; exit(); }
    $email = trim( mb_substr( (string) ( $_POST['email'] ?? '' ), 0, 190 ) );
    if ( $email !== '' && ! filter_var( $email, FILTER_VALIDATE_EMAIL ) ) { echo '{"ok":false,"error":"Ungültige E-Mail."}'; exit(); }
    $pin = preg_replace( '/\D/', '', (string) ( $_POST['pin'] ?? '' ) );
    if ( $pin !== '' && ( strlen( $pin ) < 4 || strlen( $pin ) > 10 ) ) { echo '{"ok":false,"error":"PIN muss 4–10 Ziffern haben."}'; exit(); }
    $pinChanged = ( $pin !== '' );
    $saved = false;
    if ( $activeLogin ) {
        $upd = array( 'tl_email' => $email );
        if ( $pinChanged ) $upd['tl_pin'] = pacim_terminal_login_pin_hash( $hash, (int) $activeLogin['tl_id'], $pin );
        $db->where( 'tl_id', (int) $activeLogin['tl_id'] ); $saved = $db->update( 'pacim_terminal_logins', $upd );
    } else {
        $upd = array( 'terminal_email' => $email );
        if ( $pinChanged ) $upd['terminal_pin'] = pacim_terminal_pin_hash( $hash, $pin );
        $db->where( 'terminal_id', (int) $t['terminal_id'] ); $saved = $db->update( 'pacim_terminals', $upd );
    }
    if ( ! $saved ) { echo '{"ok":false,"error":"Konnte nicht gespeichert werden."}'; exit(); }
    // Bei PIN-Änderung in einer Login-Sitzung das Cookie erneuern (unlock-Token ist login-id-basiert, bleibt gültig).
    echo json_encode( array( 'ok' => true, 'pinChanged' => $pinChanged ) ); exit();
}

/* ---- Übersicht ---- */
// Zugriff auf die Terminal-Übersicht als Sitzung erfassen (auch wenn bereits angemeldet): aktive Sitzung
// für IP+Gerät fortschreiben oder – bei neuem Gerät/IP bzw. nach Leerlauf – eine neue Sitzung anlegen.
if ( $unlocked && $needAuth && function_exists( 'pacim_ts_touch' ) ) {
    pacim_ts_touch( $db, $t, ( $activeLogin ? (int) $activeLogin['tl_id'] : 0 ) );
}
$accs = pacim_terminal_accesses( $db, (int) $t['terminal_id'] );
$curEmail = $activeLogin ? (string) $activeLogin['tl_email'] : (string) $t['terminal_email'];
$curName  = $activeLogin ? trim( (string) $activeLogin['tl_name'] ) : '';

$today    = date( 'Y-m-d' );
$wdT      = array( 1 => 'Mo', 2 => 'Di', 3 => 'Mi', 4 => 'Do', 5 => 'Fr', 6 => 'Sa', 7 => 'So' );
$modeLbl  = array( 'anreise' => 'Anreisen', 'abreise' => 'Abreisen', 'dashboard' => 'Dashboard' );
$relLbl   = function ( $d ) use ( $today ) {
    if ( $d === $today ) return 'heute';
    if ( $d === date( 'Y-m-d', strtotime( $today . ' +1 day' ) ) ) return 'morgen';
    return '';
};
$mRank = function ( $m ) { $md = pacim_ma_mode( $m ); return $md === 'abreise' ? 0 : ( $md === 'anreise' ? 1 : 2 ); };

// Dashboard-Termine (je gültigem Anreisetag mit Anreisen) → Slider oben, max. 3 nächste.
$dashDays = array();
foreach ( $accs as $m ) {
    if ( pacim_ma_mode( $m ) !== 'dashboard' ) continue;
    foreach ( pacim_ma_active_dates( $db, $m, $today ) as $d ) $dashDays[] = array( 'm' => $m, 'd' => $d );
}
usort( $dashDays, function ( $a, $b ) { return strcmp( $a['d'], $b['d'] ); } );
$dashDays = array_slice( $dashDays, 0, 5 );   // heute + nächste Termine (max. 5 Slides)

// Bestands-Live-Suche: nur wenn diesem Terminal eine Dashboard-Ansicht zugeordnet ist.
// Durchsucht ALLE Buchungen, deren Reisezeitraum HEUTE enthält (inkl. Stornos). Treffer öffnen
// die Buchungseinzelansicht (signierter checkin.php-Link – Terminal-PIN ist die Absicherung).
$hasDash = false;
foreach ( $accs as $m ) { if ( pacim_ma_mode( $m ) === 'dashboard' ) { $hasDash = true; break; } }
$invRows = '';
if ( $hasDash && $action === '' ) {   // nur beim Vollseiten-Render (nicht bei AJAX-Aktionen wie „more")
    $inv = pacim_day_inventory( $db, $today );
    foreach ( $inv['items'] as $it ) {
        $state = ! empty( $it['storno'] ) ? 'st-storno' : ( $it['arrived'] ? 'st-in' : ( $it['paid'] ? 'st-paid' : 'st-open' ) );
        $href  = SITE_URL . '/checkin.php?b=' . (int) $it['id'] . '&h=' . pacim_sign( 'checkin:' . (int) $it['id'] );
        $srch  = t_norm( $it['plate'] . ' ' . $it['name'] . ' ' . $it['booking_no'] . ' ' . $it['invoice_no'] );
        $tm    = array( 'H' => 'Halle', 'A' => 'Außen', 'VH' => 'V-Halle', 'VA' => 'V-Außen' );
        $typLbl = isset( $tm[ $it['letter'] ] ) ? $tm[ $it['letter'] ] : (string) $it['letter'];
        $plateD = preg_replace( '/\s*-\s*/', '-', (string) $it['plate'] );   // „HEI - JC 7" -> „HEI-JC 7"
        $invRows .= '<tr class="' . $state . '" data-noloader="1" data-href="' . t_e( $href ) . '" data-id="' . (int) $it['id']
            . '" data-s="' . t_e( $srch ) . '" data-plate="' . t_e( $plateD ) . '">'
            . '<td class="plate">' . t_e( $plateD ) . '</td>'
            . '<td class="nm"><span class="nm-name">' . t_e( $it['name'] ) . '</span>'
                . ( trim( (string) ( $it['ship'] ?? '' ) ) !== '' && trim( (string) $it['ship'] ) !== '—' ? '<span class="nm-ship">' . t_e( $it['ship'] ) . '</span>' : '' ) . '</td>'
            . '<td class="typ-cell"><span class="typ-name">' . t_e( $typLbl ) . '</span><span class="typ-days">' . ( $it['dur'] > 0 ? (int) $it['dur'] . ' Tage' : '—' ) . '</span></td>'
            . '</tr>';
    }
}

// „Alle Ansichten": Termine je Tag. Mehrtägige Zugänge erscheinen an JEDEM gültigen Tag mit An-/Abreisen.
$byDate = array();
foreach ( $accs as $m ) {
    if ( pacim_ma_mode( $m ) === 'dashboard' ) continue;
    foreach ( pacim_ma_active_dates( $db, $m, $today ) as $d ) $byDate[ $d ][] = $m;
}
foreach ( $byDate as $d => $lst ) {   // je Tag: Abreiselisten zuerst, dann Anreisen
    usort( $lst, function ( $a, $b ) use ( $mRank ) { return $mRank( $a ) - $mRank( $b ); } );
    $byDate[ $d ] = $lst;
}
ksort( $byDate );                     // aufsteigend (nur heute/kommende Tage)
$dateOrder = array_keys( $byDate );
$INIT_DAYS = 3;   // anfangs nur die nächsten 3 Termin-Tage; Rest per „Alle anzeigen"

// AJAX: weitere Termin-Tage nachladen.
if ( $action === 'more' ) {
    while ( ob_get_level() > 0 ) ob_end_clean();
    header( 'Content-Type: text/html; charset=utf-8' ); header( 'Cache-Control: no-store' );
    $off  = max( 0, (int) ( $_GET['offset'] ?? 0 ) );
    $rest = array_slice( $dateOrder, $off );
    $h = '';
    foreach ( $rest as $d ) $h .= t_daygroup( $db, $d, $byDate[ $d ], $hash, $relLbl, $wdT, $modeLbl, false, $today );
    echo $h; exit();
}

ob_start(); ?>
<div class="tm-sticky">
<div class="hd"><div class="wrap tm-hdwrap"><div><div class="k">Terminal<?php if ( $curName !== '' ) echo ' · ' . t_e( $curName ); ?></div><div class="t"><?= t_e( $t['terminal_label'] ?: 'Terminal' ) ?></div></div>
<?php if ( $session ) { ?><div class="tm-hdact">
  <a class="tm-set" href="?h=<?= t_e( $hash ) ?>&action=logout" title="Abmelden" aria-label="Abmelden"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/></svg></a>
  <button type="button" class="tm-set" id="tmSetBtn" title="Einstellungen" aria-label="Einstellungen"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></button>
</div><?php } ?>
</div></div>
<?php if ( $hasDash ) { ?>
<div class="wrap ts-searchwrap">
  <div class="ts-search">
    <input type="search" id="tsSearch" placeholder="Bestand durchsuchen: Kennzeichen, Name, Buchungs-/Rechnungsnr. …" autocomplete="off" autocapitalize="characters">
    <button type="button" class="ts-clear d-none" id="tsClear" aria-label="Suche löschen">&times;</button>
  </div>
  <div class="suggest d-none" id="tsSuggest"></div>
</div>
<?php } ?>
</div><!-- /.tm-sticky -->
<?php if ( $hasDash ) { ?>
<div class="ts-results d-none" id="tsResults">
  <div class="wrap">
    <div class="ts-rescount" id="tsResCount"></div>
    <div class="tblwrap"><table class="tbl"><thead><tr><th>Kennz.</th><th>Name</th><th>Typ</th></tr></thead><tbody id="tsBody"><?= $invRows ?></tbody></table></div>
    <div class="ts-empty d-none" id="tsNone">Kein Treffer im Bestand.</div>
  </div>
</div>
<?php } ?>
<div id="tsNormal">
<?php if ( $dashDays ) { ?>
<div class="wrap dswipe-wrap">
  <div class="dswipe" id="dswipe">
    <?php foreach ( $dashDays as $dde ) {
          $m = $dde['m']; $dd = $dde['d']; $tsW = strtotime( $dd );
          $wlbl = $wdT[ (int) date( 'N', $tsW ) ] . ', ' . date( 'd.m.Y', $tsW );
          $openUrl = SITE_URL . '/day-list.php?t=' . $m['ma_token'] . '&term=' . $hash . '&d=' . $dd;
          // Kennzahlen direkt serverseitig aus dem Cache rendern (schnell) -> sofort sichtbar, kein AJAX-Warten.
          // data-src bleibt: das Widget aktualisiert sich danach im Hintergrund (refresh() unten).
          $dOpts   = pacim_ma_filter_opts( $m );
          $dDash   = pacim_day_dashboard_cached( $db, $dd, $dOpts );
          $dPk     = $dDash['parking'];
          $dWidget = pacim_dash_widget_html( $dDash, in_array( 'halle', $dPk, true ), in_array( 'aussen', $dPk, true ), in_array( 'valet', $dPk, true ) ); ?>
      <div class="dslide">
        <div class="dwidget">
          <div class="dwidget-h">
            <div><div class="dwidget-k">Dashboard · Anreisetag</div><div class="dwidget-t"><?= t_e( $wlbl ) ?></div></div>
            <a class="dwidget-open" href="<?= t_e( $openUrl ) ?>">Details &#8594;</a>
          </div>
          <div class="dwt" data-src="<?= t_e( $openUrl . '&action=dashwidget' ) ?>"><?= $dWidget ?></div>
        </div>
      </div>
    <?php } ?>
  </div>
  <?php if ( count( $dashDays ) > 1 ) { ?><div class="ddots" id="ddots"><?php foreach ( $dashDays as $i => $dde ) { ?><span class="ddot<?= $i === 0 ? ' on' : '' ?>"></span><?php } ?></div><?php } ?>
</div>
<?php } ?>
<?php if ( $dateOrder ) { ?><div class="sec-h">Alle Ansichten</div><?php } ?>
<div class="wrap tm-allwrap">
<?php if ( ! $accs ) { ?>
  <div class="empty">Diesem Terminal ist derzeit keine Ansicht zugeordnet.<br>Es ist nichts auswählbar.</div>
<?php } elseif ( ! $dateOrder ) { ?>
  <div class="empty">Aktuell keine an-/abreisenden Fahrzeuge in den zugeordneten Ansichten.</div>
<?php } else {
    $shown = array_slice( $dateOrder, 0, $INIT_DAYS );
    foreach ( $shown as $di => $d ) echo t_daygroup( $db, $d, $byDate[ $d ], $hash, $relLbl, $wdT, $modeLbl, $di === 0, $today ); ?>
  <div id="termMore"></div>
  <?php $moreN = count( $dateOrder ) - count( $shown ); if ( $moreN > 0 ) { ?>
    <button type="button" class="term-more" id="termMoreBtn" data-offset="<?= count( $shown ) ?>">
      <span class="tm-label">Alle anzeigen (<?= (int) $moreN ?> weitere <?= $moreN === 1 ? 'Tag' : 'Tage' ?>)</span>
      <span class="tm-spin d-none" id="termMoreSpin"></span>
    </button>
  <?php } ?>
<?php } ?>
  <div class="foot">PaCIM Terminal</div>
</div>
</div><!-- /#tsNormal -->

<?php if ( $hasDash ) { ?>
<!-- Buchungseinzelansicht (checkin.php) im Overlay – wie in der An-/Abreiseansicht -->
<div class="ov d-none" id="tsOv">
  <div class="ov-top"></div>
  <iframe class="ov-frame" id="tsOvFrame" title="Buchung" src="about:blank" allowfullscreen></iframe>
</div>
<?php } ?>

<!-- Info-Modal: Berechtigungen/Filter einer Ansicht -->
<div class="vmodal d-none" id="vModal">
  <div class="vsheet">
    <div class="vsheet-h"><span id="vmTitle">Ansicht</span><button type="button" class="vsheet-x" id="vmClose">&times;</button></div>
    <div class="vsheet-b"><div class="vsheet-sub">Berechtigungen &amp; Filter dieser Ansicht</div><div class="vbadges" id="vmBadges"></div></div>
  </div>
</div>

<?php if ( $session ) { ?>
<!-- Einstellungen (eigene PIN / E-Mail) -->
<div class="vmodal d-none" id="tmSet">
  <div class="vsheet">
    <div class="vsheet-h"><span>Einstellungen<?php if ( $curName !== '' ) echo ' · ' . t_e( $curName ); ?></span><button type="button" class="vsheet-x" id="tmSetX">&times;</button></div>
    <div class="vsheet-b">
      <label class="tmset-lbl">E-Mail</label>
      <input type="email" class="tmset-in" id="tmSetEmail" value="<?= t_e( $curEmail ) ?>" placeholder="optional">
      <label class="tmset-lbl">PIN</label>
      <button type="button" class="tmset-pinbtn" id="tmSetPinBtn">PIN ändern</button>
      <input type="hidden" id="tmSetPin" value="">
      <div class="tmset-hint">PIN leer lassen = unverändert.</div>
      <label class="tmset-lbl">Team-Chat</label>
      <label style="display:flex;align-items:center;gap:9px;font-size:15px;font-weight:600;color:var(--navy);cursor:pointer;"><input type="checkbox" id="tmSetChat" style="width:20px;height:20px;flex:0 0 auto;"> Team-Chat auf diesem Gerät anzeigen</label>
      <div class="tmset-hint">Gilt nur für dieses Gerät. Standardmäßig deaktiviert.</div>
      <div class="tmset-msg" id="tmSetMsg"></div>
      <button type="button" class="tmset-save" id="tmSetSave">Speichern</button>
    </div>
  </div>
</div>
<script src="<?= SITE_URL ?>/assets/js/pin-pad-modal.js?v=<?= @filemtime( PATH_SYSTEM . '/assets/js/pin-pad-modal.js' ) ?>"></script>
<script>(function(){
  var btn=document.getElementById('tmSetBtn'), m=document.getElementById('tmSet'); if(!btn||!m) return;
  var emailEl=document.getElementById('tmSetEmail'), pinHid=document.getElementById('tmSetPin'),
      pinBtn=document.getElementById('tmSetPinBtn'), msg=document.getElementById('tmSetMsg'), save=document.getElementById('tmSetSave');
  var chatCb=document.getElementById('tmSetChat');
  function chatState(){ try{ return localStorage.getItem('pacim_chat_enabled')==='1'; }catch(e){ return false; } }
  if(chatCb){ chatCb.checked=chatState();
    chatCb.addEventListener('change', function(){
      try{ localStorage.setItem('pacim_chat_enabled', chatCb.checked?'1':'0'); }catch(e){}
      if(window.pacimChat&&window.pacimChat.enable) window.pacimChat.enable(chatCb.checked);
    });
  }
  btn.addEventListener('click', function(){ pinHid.value=''; pinBtn.textContent='PIN ändern'; msg.textContent=''; if(chatCb) chatCb.checked=chatState(); m.classList.remove('d-none'); });
  document.getElementById('tmSetX').addEventListener('click', function(){ m.classList.add('d-none'); });
  m.addEventListener('click', function(e){ if(e.target===m) m.classList.add('d-none'); });
  if(window.PinSetter) PinSetter.attach(pinBtn, pinHid, { title:'Neue PIN festlegen', optional:true, setLabel:'PIN ändern' });
  save.addEventListener('click', function(){ save.disabled=true; msg.textContent='Speichern …'; msg.className='tmset-msg';
    var b=new URLSearchParams(); b.set('action','settings_save'); b.set('h','<?= t_e( $hash ) ?>'); b.set('email', emailEl.value||''); b.set('pin', pinHid.value||'');
    fetch(location.pathname,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:b.toString()})
      .then(function(r){return r.json();}).then(function(res){ save.disabled=false;
        if(res&&res.ok){
          msg.textContent = res.pinChanged ? '✓ Neue PIN gespeichert' : '✓ Gespeichert.';
          msg.className='tmset-msg ok';
          pinHid.value=''; pinBtn.textContent='PIN ändern';   // Eingabe verbraucht – Button zurücksetzen
          setTimeout(function(){ m.classList.add('d-none'); }, res.pinChanged ? 1600 : 900);
        }
        else { msg.textContent=(res&&res.error)||'Fehler.'; msg.className='tmset-msg err'; } })
      .catch(function(){ save.disabled=false; msg.textContent='Netzwerkfehler.'; msg.className='tmset-msg err'; });
  });
})();</script>
<?php } ?>
<?php if ( $accs ) { ?>
<script>(function(){
  var modal=document.getElementById('vModal'), vmB=document.getElementById('vmBadges'), vmT=document.getElementById('vmTitle');
  var KMAP={mode:'b-mode',info:'b-info',ok:'b-ok',warn:'b-warn',lock:'b-lock'};
  function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
  // Akkordeon (Datum auf-/zuklappen) + Info-Button (Berechtigungs-Modal), delegiert.
  document.addEventListener('click', function(e){
    var h=e.target.closest('.dgroup-h'); if(h){ var g=h.closest('.dgroup'); var op=g.classList.toggle('open'); h.setAttribute('aria-expanded', op?'true':'false'); return; }
    var inf=e.target.closest('.vinfo'); if(inf && modal){ vmT.textContent=inf.getAttribute('data-title')||'Ansicht'; var arr=[]; try{ arr=JSON.parse(inf.getAttribute('data-badges')||'[]'); }catch(_){}
      vmB.innerHTML=arr.map(function(b){ return '<span class="vbadge '+(KMAP[b.k]||'b-info')+'">'+esc(b.t)+'</span>'; }).join(''); modal.classList.remove('d-none'); return; }
  });
  if(modal){ document.getElementById('vmClose').addEventListener('click',function(){ modal.classList.add('d-none'); });
    modal.addEventListener('click',function(e){ if(e.target===modal) modal.classList.add('d-none'); }); }

  // „Alle anzeigen": weitere Termin-Tage per AJAX nachladen (Ladesymbol bis fertig).
  var moreBtn=document.getElementById('termMoreBtn'), moreBox=document.getElementById('termMore'), moreSpin=document.getElementById('termMoreSpin');
  if(moreBtn && moreBox){ moreBtn.addEventListener('click', function(){
    var off=moreBtn.getAttribute('data-offset')||'0'; moreBtn.disabled=true;
    var lab=moreBtn.querySelector('.tm-label'); if(lab) lab.classList.add('d-none'); if(moreSpin) moreSpin.classList.remove('d-none');
    var u=location.pathname+location.search; u+=(u.indexOf('?')>=0?'&':'?')+'action=more&offset='+encodeURIComponent(off);
    fetch(u,{headers:{'X-Requested-With':'XMLHttpRequest'},cache:'no-store'}).then(function(r){return r.ok?r.text():'';}).then(function(html){
      moreBox.insertAdjacentHTML('beforeend', html||''); moreBtn.parentNode.removeChild(moreBtn);
    }).catch(function(){ moreBtn.disabled=false; if(lab) lab.classList.remove('d-none'); if(moreSpin) moreSpin.classList.add('d-none'); });
  }); }
})();</script>
<?php } ?>
<?php if ( $dashDays ) { ?>
<script>(function(){
  // Dots-Indikator für den Tages-Slider
  var sw=document.getElementById('dswipe'), dots=document.getElementById('ddots');
  if(sw&&dots){ var d=dots.querySelectorAll('.ddot');
    sw.addEventListener('scroll',function(){ var i=Math.round(sw.scrollLeft/sw.clientWidth); for(var k=0;k<d.length;k++) d[k].classList.toggle('on',k===i); },{passive:true}); }

  // Tab-Umschaltung in den Widgets (delegiert – funktioniert für alle Tages-Widgets).
  document.addEventListener('click',function(e){ var b=e.target.closest('.dwt-tab'); if(!b) return;
    var dwt=b.closest('.dwt'); if(!dwt) return; var p=b.getAttribute('data-p');
    var ts=dwt.querySelectorAll('.dwt-tab'); for(var i=0;i<ts.length;i++) ts[i].classList.toggle('on', ts[i]===b);
    var ps=dwt.querySelectorAll('.dwt-pane'); for(var j=0;j<ps.length;j++) ps[j].classList.toggle('on', ps[j].getAttribute('data-p')===p);
  });

  // Zahlen live nachladen (AJAX, jede Minute) – aktiver Tab + Scrollposition bleiben erhalten.
  function refresh(){
    var ws=document.querySelectorAll('.dwt[data-src]');
    for(var i=0;i<ws.length;i++){ (function(dwt){
      fetch(dwt.getAttribute('data-src'),{headers:{'X-Requested-With':'XMLHttpRequest'},cache:'no-store'})
        .then(function(r){return r.ok?r.text():null;}).then(function(html){ if(!html) return;
          var at=dwt.querySelector('.dwt-tab.on'); var ap=at?at.getAttribute('data-p'):'ges';
          var body=dwt.querySelector('.dwt-body'); var sc=body?body.scrollTop:0;
          dwt.innerHTML=html;
          var tb=dwt.querySelector('.dwt-tab[data-p="'+ap+'"]');
          if(tb){ var ts=dwt.querySelectorAll('.dwt-tab'); for(var k=0;k<ts.length;k++) ts[k].classList.toggle('on',ts[k]===tb);
            var ps=dwt.querySelectorAll('.dwt-pane'); for(var m=0;m<ps.length;m++) ps[m].classList.toggle('on',ps[m].getAttribute('data-p')===ap); }
          var nb=dwt.querySelector('.dwt-body'); if(nb) nb.scrollTop=sc;
        }).catch(function(){});
    })(ws[i]); }
  }
  refresh();                 // sofort laden (Kennzahlen erscheinen, sobald fertig)
  setInterval(refresh, 60000);
})();</script>
<?php } ?>
<script>(function(){
  // Ansichten im Hintergrund VORLADEN, damit sie beim Antippen sofort aus dem Cache öffnen (kein Doppeltippen).
  // Jede Ansicht ist eine eigene, noch ungecachte day-list-URL -> der erste echte Tipp müsste sonst aufs Netz warten.
  if(!("serviceWorker" in navigator)) return;
  var done={};
  function warm(scope){
    var links=(scope||document).querySelectorAll('a.vrow-main[href]'), i=0;
    links.forEach(function(a){
      var u=a.getAttribute('href'); if(!u||done[u]) return; done[u]=1;
      setTimeout(function(){ fetch(u,{credentials:"same-origin"}).catch(function(){}); }, (i++)*200);
    });
  }
  window.addEventListener("load",function(){ setTimeout(function(){ warm(document); }, 800); });
  // Wird ein eingeklappter Tag aufgeklappt, dessen Ansichten ebenfalls vorladen.
  document.addEventListener("click",function(e){ var h=e.target.closest&&e.target.closest(".dgroup-h"); if(h){ var g=h.closest(".dgroup"); if(g) setTimeout(function(){ warm(g); },60); } }, true);
})();</script>
<?php if ( $hasDash ) { ?>
<script>(function(){
  // Bestands-Live-Suche (über dem Dashboard). Filtert clientseitig über alle heute aktiven
  // Buchungen; leer => normale Terminalstartseite. Treffer öffnen die Buchung im Overlay.
  var inp=document.getElementById('tsSearch'); if(!inp) return;
  var body=document.getElementById('tsBody'), results=document.getElementById('tsResults'),
      normal=document.getElementById('tsNormal'), none=document.getElementById('tsNone'),
      sgBox=document.getElementById('tsSuggest'), clearBtn=document.getElementById('tsClear'),
      cnt=document.getElementById('tsResCount');
  if(!body) return;
  function esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }
  function norm(s){ return (s||'').toLowerCase().replace(/[^0-9a-zäöüß]/g,''); }
  function subseq(q,s){ var i=0; for(var j=0;j<s.length && i<q.length;j++) if(s[j]===q[i]) i++; return i===q.length; }
  function dlev(a,b){ var al=a.length, bl=b.length; if(!al) return bl; if(!bl) return al;
    var d=[]; for(var i=0;i<=al;i++){ d[i]=[i]; } for(var j=0;j<=bl;j++){ d[0][j]=j; }
    for(i=1;i<=al;i++) for(j=1;j<=bl;j++){ var c=a[i-1]===b[j-1]?0:1;
      d[i][j]=Math.min(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+c);
      if(i>1&&j>1&&a[i-1]===b[j-2]&&a[i-2]===b[j-1]) d[i][j]=Math.min(d[i][j], d[i-2][j-2]+1); }
    return d[al][bl]; }
  function plateCore(plate){ var p=(plate||'').split(/[^0-9a-zA-ZäöüÄÖÜß]+/).filter(Boolean); return norm(p.length>1?p.slice(1).join(''):plate); }
  function freq(s){ var m={}; for(var i=0;i<s.length;i++){ m[s[i]]=(m[s[i]]||0)+1; } return m; }
  function multiSub(q,s){ var fs=freq(s); for(var k in q){ if((fs[k]||0)<q[k]) return false; } return true; }
  // Trefferstärke (kleiner = besser, -1 = kein Treffer). Die besten Treffer entsprechen der Eingabe am ehesten:
  //  0 = Kennzeichen BEGINNT mit der Eingabe (z. B. „TR" -> „TR-KW…")
  //  1 = ein Segment beginnt mit der Eingabe (z. B. „KW" -> „TR-KW…")
  //  2 = Eingabe als zusammenhängende Teilkette irgendwo   3 = Zeichen in Reihenfolge (verteilt)
  //  4 = nur der Kern (ohne Ortskürzel) in Reihenfolge      5 = alle Zeichen vorhanden, Reihenfolge vertauscht
  function plateRank(q, plate){
    var np=norm(plate);
    if(np.indexOf(q)===0) return 0;
    var segs=(plate||'').split(/[^0-9a-zA-ZäöüÄÖÜß]+/).filter(Boolean);
    for(var i=0;i<segs.length;i++) if(norm(segs[i]).indexOf(q)===0) return 1;
    if(np.indexOf(q)>=0) return 2;
    if(subseq(q,np)) return 3;
    var core=plateCore(plate); if(core && subseq(q,core)) return 4;
    if(q.length>=3 && multiSub(freq(q), np)) return 5;
    return -1;
  }
  function fuzzyDist(q, s){ if(!s) return 99; var best=99, ql=q.length, lo=Math.max(1,ql-3), hi=Math.min(s.length, ql+3);
    for(var L=lo; L<=hi; L++){ for(var st=0; st+L<=s.length; st++){ var d=dlev(q, s.substr(st,L)); if(d<best){ best=d; if(best===0) return 0; } } } return best; }
  function hl(plate,q){ if(!q) return esc(plate);
    var qi=0,out=''; for(var i=0;i<plate.length;i++){ var ch=plate[i], n=norm(ch);
      if(qi<q.length && n && n===q[qi]){ out+='<mark>'+esc(ch)+'</mark>'; qi++; } else out+=esc(ch); }
    if(qi===q.length) return out;
    var need=freq(q), out2='', rem=q.length;
    for(var j=0;j<plate.length;j++){ var c=plate[j], m=norm(c);
      if(m && need[m]>0){ out2+='<mark>'+esc(c)+'</mark>'; need[m]--; rem--; } else out2+=esc(c); }
    return rem===0 ? out2 : esc(plate); }
  var orig=[].slice.call(body.querySelectorAll('tr'));

  function suggest(q){
    if(q.length<2) return [];
    var maxd = q.length<=3?1:(q.length<=6?2:3), res=[], seen={};
    for(var i=0;i<orig.length;i++){ var r=orig[i], plate=r.getAttribute('data-plate')||'';
      if(plate===''||plate==='—'||seen[plate]) continue;
      if(plateRank(q, plate)>=0) continue;
      var dist=Math.min(fuzzyDist(q, norm(plate)), fuzzyDist(q, plateCore(plate)));
      if(dist<=maxd){ seen[plate]=1; res.push({plate:plate, d:dist}); } }
    res.sort(function(a,b){return a.d-b.d;});
    return res.slice(0,4);
  }
  function renderSuggest(q, hits){
    if(!sgBox) return;
    if(!q || q.length<2 || hits>0){ sgBox.classList.add('d-none'); sgBox.innerHTML=''; return; }
    var s=suggest(q);
    if(!s.length){ sgBox.classList.add('d-none'); sgBox.innerHTML=''; return; }
    var html='<span class="sg-l">Meinten Sie?</span>';
    for(var i=0;i<s.length;i++) html+='<button type="button" class="sg-chip" data-plate="'+esc(s[i].plate)+'">'+esc(s[i].plate)+'</button>';
    sgBox.innerHTML=html; sgBox.classList.remove('d-none');
  }
  if(sgBox) sgBox.addEventListener('click', function(e){ var b=e.target.closest('.sg-chip'); if(!b) return; inp.value=b.getAttribute('data-plate'); runSearch(); inp.focus(); });

  // Check-in/Buchung im Overlay (kein Seitenwechsel) – schließt per postMessage aus checkin.php.
  var ov=document.getElementById('tsOv'), ovFrame=document.getElementById('tsOvFrame');
  function openCk(href){ if(!ov||!href) return; ovFrame.src=href+(href.indexOf('?')>=0?'&':'?')+'embed=1'; ov.classList.remove('d-none'); document.documentElement.style.overflow='hidden'; }
  function closeCk(){ if(!ov) return; ov.classList.add('d-none'); ovFrame.src='about:blank'; document.documentElement.style.overflow=''; }
  body.addEventListener('click', function(e){ var tr=e.target.closest('tr'); if(tr && tr.dataset.href) openCk(tr.dataset.href); });
  window.addEventListener('message', function(ev){ var d=ev.data; if(d && d.pacimCi && d.close) closeCk(); });

  function runSearch(){
    var q=norm(inp.value);
    if(!q){ results.classList.add('d-none'); if(normal) normal.classList.remove('d-none');
      if(sgBox){ sgBox.classList.add('d-none'); sgBox.innerHTML=''; } if(clearBtn) clearBtn.classList.add('d-none'); return; }
    if(clearBtn) clearBtn.classList.remove('d-none');
    results.classList.remove('d-none'); if(normal) normal.classList.add('d-none');
    var shown=0, hitsP=[], hitsO=[];
    for(var i=0;i<orig.length;i++){ var r=orig[i];
      var ps=plateRank(q, r.getAttribute('data-plate')||''); var pl=ps>=0;
      var full = pl || subseq(q, r.getAttribute('data-s')||'');
      r.style.display = full?'table-row':'none';
      var cell=r.querySelector('td.plate'); if(cell){ var p=r.getAttribute('data-plate')||''; cell.innerHTML = full ? hl(p,q) : esc(p); }
      if(full){ shown++; if(pl){ r._ps=ps; hitsP.push(r); } else hitsO.push(r); } }
    hitsP.sort(function(a,b){ return a._ps-b._ps; });
    var order=hitsP.concat(hitsO);
    for(var k=0;k<order.length;k++) body.appendChild(order[k]);
    if(none) none.classList.toggle('d-none', shown!==0);
    if(cnt) cnt.textContent = shown>0 ? (shown+' Treffer im Bestand') : '';
    renderSuggest(q, hitsP.length);
  }
  inp.addEventListener('input', runSearch);
  if(clearBtn) clearBtn.addEventListener('click', function(){ inp.value=''; runSearch(); inp.focus(); });
})();</script>
<?php } ?>
<?php
t_html( $t['terminal_label'] ?: 'Terminal', ob_get_clean() );

Youez - 2016 - github.com/yon3zu
LinuXploit