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/day-list.php
<?php
/**
 * day-list.php – Mobile Tages-Anreiseliste (aus der Pushover-Nachricht verlinkt).
 *
 * Zeigt dieselbe gefilterte Liste wie die Pushover-Nachricht als echte, ausgerichtete
 * Tabelle. Jedes Kennzeichen ist fett und verlinkt auf die hash-gesicherte Check-in-/
 * Zahlungsseite (checkin.php). Zugriff ohne Login, abgesichert über einen Hash, der
 * individuell für diesen Tag erzeugt (pacim_sign('daylist:'.date)) und hier verglichen wird.
 */
require_once( __DIR__ . '/includes/config.inc.php' );
require_once( __DIR__ . '/includes/day-arrivals.php' );
require_once( __DIR__ . '/includes/mobileaccess.php' );
require_once( __DIR__ . '/includes/booking-images.php' );
@ini_set( 'display_errors', '0' );
error_reporting( 0 );
while ( ob_get_level() > 0 ) ob_end_clean();

$date = substr( (string) ( $_GET['date'] ?? '' ), 0, 10 );
$h    = trim( (string) ( $_GET['h'] ?? '' ) );
$tok  = trim( (string) ( $_GET['t'] ?? '' ) );   // Mobile-Zugang-Token (alternativ zu date+h)
$action = (string) ( $_GET['action'] ?? '' );    // ''=Seite, 'poll'=Status-JSON, 'rows'=Tabellen-HTML
// Terminal-Kontext (kam über ein Terminal): erlaubt „Zurück zur Terminal-Übersicht".
$termHash = preg_replace( '/[^a-f0-9]/', '', strtolower( (string) ( $_GET['term'] ?? '' ) ) );
$termBack = '';      // gültige Terminal-Sitzung? → „Terminal"-Button (wird nach DB-Init gesetzt)
$directLogout = '';  // Direktaufruf mit PIN (keine Terminal-Sitzung) → „Logout"-Button

function dl_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;--green:#2fb344;--red:#d63939;--gold:#f3b53f;--ink:#16384f;--muted:#5b7488;--line:#e6eef4;}
    *{box-sizing:border-box;margin:0;padding:0;}
    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);padding:0 0 40px;touch-action:manipulation;}
    .wrap{max-width:560px;margin:0 auto;}
    .topbar{position:sticky;top:0;z-index:6;}
    /* iOS: Inhalt nicht unter Dynamic Island / Statusleiste schieben (viewport-fit=cover + Safe-Area). */
    .hd{position:relative;background:linear-gradient(135deg,var(--navy) 0%,var(--navy2) 65%,var(--blue) 130%);color:#fff;
        padding:calc(20px + env(safe-area-inset-top,0px)) calc(18px + env(safe-area-inset-right,0px)) 18px calc(18px + env(safe-area-inset-left,0px));}
    .term-back{position:absolute;top:calc(14px + env(safe-area-inset-top,0px));right:calc(14px + env(safe-area-inset-right,0px));
        display:inline-flex;align-items:center;gap:5px;background:rgba(255,255,255,.16);color:#fff;text-decoration:none;
        border-radius:9px;padding:7px 12px;font-size:13px;font-weight:700;-webkit-tap-highlight-color:transparent;}
    .term-back:active{background:rgba(255,255,255,.3);}
    .term-back svg{width:16px;height:16px;}
    .hd .k{font-size:11px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;opacity:.8;}
    .hd .t{font-size:21px;font-weight:800;margin-top:2px;}
    .hd .sub{font-size:12.5px;opacity:.85;margin-top:3px;}
    .stats{display:flex;gap:8px;margin-top:9px;}
    .stat{background:rgba(255,255,255,.14);border-radius:10px;padding:6px 11px;font-size:12.5px;font-weight:600;}
    .stat b{font-size:15px;} .stat.open b{color:#ffd0cf;}
    .stat.filt{cursor:pointer;user-select:none;-webkit-tap-highlight-color:transparent;transition:background .15s ease,box-shadow .15s ease;}
    .stat.filt.active{background:rgba(255,255,255,.34);box-shadow:inset 0 0 0 1.5px rgba(255,255,255,.6);}
    .stat.live{display:inline-flex;align-items:center;gap:6px;cursor:pointer;-webkit-tap-highlight-color:transparent;}
    .stat.live:active{filter:brightness(.9);}
    /* „Zuletzt geöffnet"-Panel (Bottom-Sheet) */
    .rec{position:fixed;inset:0;z-index:55;background:rgba(13,44,68,.45);display:flex;align-items:flex-end;}
    .rec-sheet{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);}
    .rec-h{display:flex;align-items:center;justify-content:space-between;padding:15px 18px;font-size:17px;font-weight:800;color:var(--navy);border-bottom:1px solid #e6eef4;}
    .rec-h-act{display:flex;align-items:center;gap:6px;}
    .rec-reset{background:#eef3f7;border:0;border-radius:8px;padding:7px 11px;font:inherit;font-size:13px;font-weight:700;color:#a23030;cursor:pointer;}
    .rec-reset:active{background:#e2e9f0;}
    .rec-x{background:none;border:0;font-size:28px;line-height:1;color:#5b7488;cursor:pointer;}
    .rec-list{overflow-y:auto;-webkit-overflow-scrolling:touch;}
    .rec-item{display:flex;align-items:center;gap:12px;width:100%;border:0;border-bottom:1px solid #eef3f7;background:none;text-align:left;padding:13px 18px;font:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent;}
    .rec-item:active{background:#f4f8fb;}
    .rec-plate{font-weight:800;font-size:15px;color:var(--navy);font-family:"EuroPlate",monospace;flex:0 0 auto;}
    .rec-name{flex:1;min-width:0;color:#5b7488;font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
    .rec-item svg{width:18px;height:18px;color:#9fb3c4;flex:0 0 auto;}
    .rec-empty{padding:30px 18px;text-align:center;color:#5b7488;}
    .live-dot{width:8px;height:8px;border-radius:50%;background:#34d058;box-shadow:0 0 0 0 rgba(52,208,88,.6);animation:dlpulse 2s infinite;}
    .stat.live.stale .live-dot{background:#ffb84d;animation:none;}
    @keyframes dlpulse{0%{box-shadow:0 0 0 0 rgba(52,208,88,.6);}70%{box-shadow:0 0 0 7px rgba(52,208,88,0);}100%{box-shadow:0 0 0 0 rgba(52,208,88,0);}}
    @media (prefers-reduced-motion: reduce){ .live-dot{animation:none;} }
    /* Check-in-Overlay (Iframe, kein Seitenreload) */
    /* Bottom-Abstand für die eingebettete Einzelansicht (iframe liest env() selbst nicht):
       iPhone-Home-Indikator (inset>0) freihalten; Android (inset≈0) übernimmt die Tableiste selbst. */
    .ov{position:fixed;inset:0;z-index:50;background:#fff;display:flex;flex-direction:column;padding-bottom:env(safe-area-inset-bottom,0px);}
    /* Safe-Area kann das eingebettete iframe (checkin.php) selbst NICHT lesen (env()=0 im iframe),
       darum hier oben ein navyfarbener Streifen (= Header-Farbe) unter der Dynamic Island/Statusleiste. */
    .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;}
    .row-flash{animation:dlflash 1.1s ease-out;}
    @keyframes dlflash{0%{background:#fff6cc;}100%{}}
    /* Sitzungsende-Bildschirm */
    .ended{position:fixed;inset:0;z-index:60;background:linear-gradient(160deg,var(--navy) 0%,var(--navy2) 70%,var(--blue) 140%);display:flex;align-items:center;justify-content:center;padding:24px;}
    .ended-box{text-align:center;color:#fff;max-width:340px;}
    .ended-ic{width:64px;height:64px;color:#cfe2f2;margin:0 auto 16px;}
    .ended-t{font-size:24px;font-weight:800;}
    .ended-x{font-size:14.5px;opacity:.85;margin-top:10px;line-height:1.5;}
    .flt{display:flex;flex-wrap:wrap;gap:6px;margin-top:9px;}
    .chip{background:rgba(255,255,255,.16);border-radius:999px;padding:3px 9px;font-size:11.5px;font-weight:600;}
    .search{background:#fff;padding:10px 14px;border-bottom:1px solid var(--line);}
    .search-box{position:relative;}
    .search input{width:100%;padding:11px 40px 11px 14px;border:1px solid var(--line);border-radius:10px;font-size:15px;background:#f6f9fc;}
    .search input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none;appearance:none;}
    .search-clear{position:absolute;right:6px;top:50%;transform:translateY(-50%);width:30px;height:30px;border:0;border-radius:50%;
        background:#e6eef4;color:var(--navy);font-size:20px;line-height:1;cursor:pointer;display:flex;align-items:center;justify-content:center;}
    .search-clear:active{filter:brightness(.94);}
    .d-none{display:none!important;}
    /* Meinten Sie? – Vorschläge bei ähnlichem Kennzeichen */
    .suggest{background:#fff8e6;border-bottom:1px solid #f3e3b0;padding:10px 14px;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);}
    .tblwrap{overflow-x:auto;-webkit-overflow-scrolling:touch;background:#fff;}
    .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:9px 10px;border-bottom:2px solid var(--line);background:#f6f9fc;}
    .tbl td{padding:12px 10px;border-bottom:1px solid var(--line);font-size:14px;vertical-align:middle;}
    .tbl tbody tr{cursor:pointer;}
    /* Zeilen-Status: eingecheckt grün, Zahlung offen orange, bezahlt aber nicht eingecheckt leicht blau */
    .tbl tbody tr.st-in{background:rgba(47,179,68,.20);box-shadow:inset 3px 0 0 #2fb344;}
    .tbl tbody tr.st-open{background:rgba(245,159,0,.20);box-shadow:inset 3px 0 0 #f59f00;}
    .tbl tbody tr.st-paid{background:rgba(27,108,168,.13);box-shadow:inset 3px 0 0 #1b6ca8;}
    .tbl tbody tr.st-storno{background:rgba(214,57,57,.12);box-shadow:inset 3px 0 0 #d63939;}
    .tbl tbody tr.st-storno .plate{text-decoration:line-through;color:#a23030;}
    /* außerhalb des Filters – nur per Suche sichtbar, dezent + Markierung */
    .tbl tbody tr.row-out{display:none;background:#f4f6f9;box-shadow:inset 3px 0 0 #b6c2cf;opacity:.92;}
    .outflag{display:inline-block;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#6b7a89;background:#e4e9ef;border-radius:5px;padding:1px 6px;margin-left:6px;vertical-align:middle;}
    .pcam{display:inline-flex;align-items:center;gap:2px;font-size:11px;font-weight:700;color:var(--blue);background:rgba(27,108,168,.10);border-radius:5px;padding:1px 5px;margin-left:6px;vertical-align:middle;white-space:nowrap;}
    .pcam svg{width:12px;height:12px;}
    .tbl tbody tr:active{filter:brightness(.96);}
    /* Kennzeichen in der FE-Schrift (EuroPlate). Nur ein Schnitt -> „fett" per Kontur. */
    .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 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;}
    .nm{font-weight:600;}
    .nm .nm-name{display:block;font-size:15.5px;font-weight:600;color:var(--navy);line-height:1.2;}
    .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;}
    .mut{color:var(--muted);}
    mark{background:#ffd54a;color:#3a2c00;border-radius:2px;padding:0 1px;}
    .empty{padding:40px 20px;text-align:center;color:var(--muted);}
    .foot{font-size:12px;color:var(--muted);text-align:center;padding:16px;}
    .err{max-width:440px;margin:60px auto;background:#fff;border-radius:18px;padding:34px 26px;text-align:center;box-shadow:0 14px 40px rgba(13,44,68,.16);}
    .err h1{font-size:40px;color:var(--navy);} .err p{color:var(--muted);margin-top:8px;}
    /* PIN-Sperrbildschirm: gemeinsame Komponente (pacim_pin_css), siehe dl_pin_screen(). */
    /* ---- ressourcenarme Animationen: nur opacity/transform (Compositor), keine Layout-Props ---- */
    @keyframes dlFadeUp{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}
    @keyframes dlFade{from{opacity:0;}to{opacity:1;}}
    @keyframes dlSheet{from{opacity:.3;transform:translateY(16px);}to{opacity:1;transform:none;}}
    @keyframes dlPop{from{opacity:0;transform:scale(.92);}to{opacity:1;transform:none;}}
    .tblwrap{animation:dlFadeUp .28s ease both;}
    .suggest:not(.d-none){animation:dlFadeUp .18s ease;}
    .ov:not(.d-none){animation:dlFade .18s ease;}
    .ov-frame{animation:dlSheet .24s cubic-bezier(.22,.61,.36,1);}
    .ended:not(.d-none){animation:dlFade .25s ease;}
    .ended-box{animation:dlPop .32s cubic-bezier(.22,.61,.36,1) both;}
    .tbl tbody tr{transition:background .25s ease;}
    @media (prefers-reduced-motion: reduce){
      .tblwrap,.suggest,.ov,.ov-frame,.ended,.ended-box,.row-flash,.pin-card.shake{animation:none!important;}
      .tbl tbody tr{transition:none;} .pin-key.lit,.pin-key:active{transform:none;}
    }
    ';
}
function dl_html( $title, $body, $code = 200 ) {
    http_response_code( $code );
    header( 'Content-Type: text/html; charset=utf-8' );
    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 NUR in der mobilen Ansicht, sobald ein gültiger Token vorliegt.
       . ( empty( $GLOBALS['dl_mtoken'] ) ? '' :
             '<link rel="manifest" href="' . SITE_URL . '/manifest.php?t=' . rawurlencode( $GLOBALS['dl_mtoken'] ) . '">'
           . '<link rel="apple-touch-icon" href="' . SITE_URL . '/assets/webapp/icon-180.png">'
           . '<meta name="apple-mobile-web-app-title" content="Seaside Parking">' )
       . '<title>' . htmlspecialchars( $title, ENT_QUOTES, 'UTF-8' ) . '</title>'
       . '<style>' . dl_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['dl_mtoken'] ) ? '' : pacim_webapp_install_html( 'Seaside Parking', SITE_URL . '/assets/webapp/icon-192.png' ) )
       . pacim_webapp_loader_html()
       . ( function_exists( 'pacim_webapp_offline_html' ) ? pacim_webapp_offline_html() : '' )
       . ( function_exists( 'pacim_chat_widget_html' ) ? pacim_chat_widget_html( (string) ( $GLOBALS['dl_mtoken'] ?? '' ), '', 0 ) : '' )
       . '</body></html>';
    exit();
}
/** PIN-Sperrbildschirm mit eigener Tastatur (Tippen ODER Ziehen über die Ziffern). */
function dl_pin_screen( $tok ) {
    // Gemeinsame PIN-Komponente (Tippen/Wischen mit Muster-Linien); Token steckt in der URL.
    $body = '<style>' . pacim_pin_css() . '</style>'
          . pacim_pin_markup( 'PIN eingeben', 'Dieser Zugang ist mit einer PIN geschützt.' )
          . pacim_pin_js( "location.pathname+location.search+'&action=unlock'" );
    dl_html( 'PIN', $body );
}

$db = new MysqliDb( MYSQL_HOST, MYSQL_USER, MYSQL_PASS, MYSQL_DB );

// Terminal-Kontext: gültige Terminal-Sitzung (Master ODER Login)? → „Terminal"-Button (zurück zur Übersicht) + Terminal-Scope.
$termLoginSuffix = '';
if ( $termHash !== '' && function_exists( 'pacim_terminal_load' ) ) {
    $tRow = pacim_terminal_load( $db, $termHash );
    if ( $tRow && pacim_terminal_session( $db, $tRow ) ) {
        $termBack = SITE_URL . '/terminal.php?h=' . $termHash;
        $cv = (string) ( $_COOKIE[ 'term_' . $termHash ] ?? '' );
        if ( isset( $cv[0] ) && $cv[0] === 'L' ) { $p = strpos( $cv, ':' ); if ( $p ) $termLoginSuffix = ':' . substr( $cv, 1, $p - 1 ); }
    }
}

$access = null; $canAct = true; $searchAll = true; $outItems = array(); $chips = array();

if ( $tok !== '' ) {
    /* ---- Mobile-Zugang (Token): Filter + Rechte aus der DB */
    $access = pacim_ma_load( $db, $tok );
    if ( ! $access ) {
        // Autorefresh (poll/rows): klares JSON-Signal statt HTML, damit der Client die Sitzung beendet.
        if ( $action === 'poll' || $action === 'rows' ) {
            while ( ob_get_level() > 0 ) ob_end_clean();
            header( 'Content-Type: application/json; charset=utf-8' ); header( 'Cache-Control: no-store' );
            echo '{"gone":1}'; exit();
        }
        dl_html( 'Ungültiger Link', '<div class="err"><h1>403</h1><p>Dieser Zugang ist ungültig, widerrufen oder abgelaufen.</p></div>', 403 );
    }

    // Web-App-Manifest nur bei gültigem Token einbinden (Token wandert in start_url -> installierte App öffnet diese Liste).
    $GLOBALS['dl_mtoken'] = $tok;

    /* ---- PIN-Gate (optional). Token bleibt das eigentliche Geheimnis; PIN = Zusatzschutz. */
    $pinHash  = (string) $access['ma_pin'];
    $needPin  = ( $pinHash !== '' );
    $cookieNm = 'mau_' . $tok;
    $unlocked = ! $needPin
        || ( isset( $_COOKIE[ $cookieNm ] ) && hash_equals( pacim_ma_unlock_token( $tok ), (string) $_COOKIE[ $cookieNm ] ) )
        || ( function_exists( 'pacim_terminal_grants' ) && pacim_terminal_grants( $db, $tok ) );   // Zugriff über Terminal-Sitzung

    if ( $action === 'unlock' ) {
        while ( ob_get_level() > 0 ) ob_end_clean();
        header( 'Content-Type: application/json; charset=utf-8' ); header( 'Cache-Control: no-store' );
        if ( ! $needPin ) { echo '{"ok":true}'; exit(); }
        $pin = preg_replace( '/\D/', '', (string) ( $_POST['pin'] ?? '' ) );
        if ( strlen( $pin ) >= 4 && strlen( $pin ) <= 10 && hash_equals( $pinHash, pacim_ma_pin_hash( $tok, $pin ) ) ) {
            $exp = ! empty( $access['ma_expires'] ) ? strtotime( $access['ma_expires'] ) : ( time() + 12 * 3600 );
            setcookie( $cookieNm, pacim_ma_unlock_token( $tok ), array( 'expires' => $exp, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax' ) );
            echo '{"ok":true}'; exit();
        }
        usleep( 300000 );   // Brute-Force leicht bremsen
        echo json_encode( array( 'ok' => false, 'error' => 'Falsche PIN.' ) ); exit();
    }

    // Abmelden (nur Direktaufruf mit eigener PIN): Freischalt-Cookie löschen → zurück zum PIN-Screen.
    if ( $action === 'logout' ) {
        setcookie( $cookieNm, '', array( 'expires' => time() - 3600, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax' ) );
        header( 'Location: ' . SITE_URL . '/day-list.php?t=' . $tok ); exit();
    }

    if ( $needPin && ! $unlocked ) {
        if ( $action === 'poll' || $action === 'rows' ) {
            while ( ob_get_level() > 0 ) ob_end_clean();
            header( 'Content-Type: application/json; charset=utf-8' ); header( 'Cache-Control: no-store' );
            echo '{"locked":1}'; exit();
        }
        dl_pin_screen( $tok );   // Sperrbildschirm rendern (kein Listendaten-Leak) und beenden
    }

    // Zugriff über ein Terminal als Sitzung erfassen – auch OHNE neuen Login (bestehendes term_-Cookie).
    // Bei neuem Gerät/IP oder nach Leerlauf entsteht eine neue Sitzung, sonst wird die aktive fortgeschrieben.
    // (Nicht beim Status-Poll, um die Heartbeat-Frequenz nicht zu tracken.)
    if ( $action !== 'poll' && function_exists( 'pacim_ts_touch_tok' ) ) pacim_ts_touch_tok( $db, $tok );

    /* ===== Tag bestimmen: Standard = ma_date; optional &d=<Datum> innerhalb des Gültigkeitszeitraums (mehrtägig). ===== */
    $maDate0 = substr( (string) $access['ma_date'], 0, 10 );
    $maExp0  = ! empty( $access['ma_expires'] ) ? substr( (string) $access['ma_expires'], 0, 10 ) : '';
    $date    = $maDate0;
    $reqD    = (string) ( $_GET['d'] ?? '' );
    if ( preg_match( '/^\d{4}-\d{2}-\d{2}$/', $reqD ) && $reqD >= $maDate0 && ( $maExp0 === '' || $reqD <= $maExp0 ) ) $date = $reqD;

    // „Logout"-Button NUR beim Direktaufruf mit eigener PIN (keine Terminal-Sitzung); über Terminal gilt der „Terminal"-Button.
    if ( $termBack === '' && $needPin && isset( $_COOKIE[ $cookieNm ] ) && hash_equals( pacim_ma_unlock_token( $tok ), (string) $_COOKIE[ $cookieNm ] ) )
        $directLogout = SITE_URL . '/day-list.php?t=' . $tok . '&action=logout';

    /* ===== „Zuletzt geöffnet" (Recent Views) ===== */
    // Scope: über Terminal-Sitzung -> Terminal (geteilt); sonst aktuelle (Browser-)Session des Direktaufrufs.
    $rvScope = ( $termHash !== '' && $termBack !== '' ) ? ( 'term:' . $termHash . $termLoginSuffix ) : ( 'sess:' . session_id() );
    if ( $action === 'recent_add' ) {
        while ( ob_get_level() > 0 ) ob_end_clean();
        header( 'Content-Type: application/json; charset=utf-8' ); header( 'Cache-Control: no-store' );
        $rb = (int) ( $_POST['b'] ?? ( $_GET['b'] ?? 0 ) );
        if ( $rb > 0 && function_exists( 'pacim_rv_add' ) ) pacim_rv_add( $db, $rvScope, $tok, $rb );
        echo '{"ok":true}'; exit();
    }
    if ( $action === 'recent_clear' ) {
        while ( ob_get_level() > 0 ) ob_end_clean();
        header( 'Content-Type: application/json; charset=utf-8' ); header( 'Cache-Control: no-store' );
        if ( function_exists( 'pacim_rv_clear' ) ) pacim_rv_clear( $db, $rvScope, $tok );
        echo '{"ok":true}'; exit();
    }
    if ( $action === 'recent_list' ) {
        while ( ob_get_level() > 0 ) ob_end_clean();
        header( 'Content-Type: application/json; charset=utf-8' ); header( 'Cache-Control: no-store' );
        $ids = function_exists( 'pacim_rv_list' ) ? pacim_rv_list( $db, $rvScope, $tok, 20 ) : array();
        $out = array();
        if ( $ids ) {
            $place = implode( ',', array_fill( 0, count( $ids ), '?' ) );
            $rows  = (array) $db->rawQuery(
                "SELECT b.booking_id AS id, license_plate(b.booking_serial) AS plate,
                        c.customer_lastname AS lastname, c.customer_name AS company, c.customer_firstname AS firstname
                   FROM pacim_booking b LEFT JOIN pacim_customer c ON c.customer_id = b.booking_customer
                  WHERE b.booking_id IN ($place)", $ids );
            $map = array();
            foreach ( $rows as $r ) $map[ (int) $r['id'] ] = $r;
            foreach ( $ids as $id ) {   // Reihenfolge = Recency
                if ( ! isset( $map[ $id ] ) ) continue;
                $r = $map[ $id ];
                $nm = trim( (string) $r['lastname'] ); if ( $nm === '' ) $nm = trim( (string) $r['company'] ); if ( $nm === '' ) $nm = trim( (string) $r['firstname'] );
                $pl = trim( (string) $r['plate'] );
                $out[] = array( 'id' => (int) $id, 'plate' => $pl !== '' ? $pl : '—', 'name' => $nm !== '' ? $nm : '—',
                    'href' => SITE_URL . '/checkin.php?t=' . $tok . '&b=' . (int) $id );
            }
        }
        echo json_encode( array( 'ok' => true, 'items' => $out ), JSON_UNESCAPED_UNICODE ); exit();
    }

    /* ===== Dashboard-Modus: Tageskennzahlen statt Anreiseliste ===== */
    if ( pacim_ma_mode( $access ) === 'dashboard' ) {
        // $date kommt von oben (ma_date bzw. &d innerhalb der Gültigkeit).
        $eD   = function ( $s ) { return htmlspecialchars( (string) $s, ENT_QUOTES, 'UTF-8' ); };
        $wdD  = array( 1 => 'Mo', 2 => 'Di', 3 => 'Mi', 4 => 'Do', 5 => 'Fr', 6 => 'Sa', 7 => 'So' );
        $tsD  = strtotime( $date );
        $dLabelD = $wdD[ (int) date( 'N', $tsD ) ] . ', ' . date( 'd.m.Y', $tsD );
        $dash = pacim_day_dashboard_cached( $db, $date, pacim_ma_filter_opts( $access ) );   // aus Cache (Cron alle 5 Min), sonst live + cachen
        $chipsD = pacim_ma_summary( $access );
        if ( trim( (string) $access['ma_label'] ) !== '' ) array_unshift( $chipsD, $access['ma_label'] );
        $pk = $dash['parking'];
        $showH = in_array( 'halle', $pk, true ); $showA = in_array( 'aussen', $pk, true ); $showV = in_array( 'valet', $pk, true );

        // AJAX: nur das getabbte Widget (Terminal-Startseite lädt damit die Zahlen live nach).
        if ( $action === 'dashwidget' ) {
            while ( ob_get_level() > 0 ) ob_end_clean();
            header( 'Content-Type: text/html; charset=utf-8' ); header( 'Cache-Control: no-store' );
            echo pacim_dash_widget_html( $dash, $showH, $showA, $showV );
            exit();
        }

        // Schiffe nach Reiselänge gruppieren – je Reiselänge eine Footer-Tab.
        $byTrip = array();
        foreach ( $dash['ships'] as $s ) { $byTrip[ (int) $s['trip'] ][] = $s; }
        ksort( $byTrip );
        $tripKeys = array_keys( $byTrip );

        // Daten für das Diagramme-/Filter-Modul (clientseitig): je Schiff Gesamt- + Offen-Zahlen.
        $diagShips = array();
        foreach ( $dash['ships'] as $s ) $diagShips[] = array(
            'n' => $s['ship'], 't' => (int) $s['trip'],
            'h' => (int) $s['total']['halle'], 'a' => (int) $s['total']['aussen'], 'v' => (int) $s['total']['valet'],
            'oh' => (int) $s['open']['halle'], 'oa' => (int) $s['open']['aussen'], 'ov' => (int) $s['open']['valet'],
        );

        ob_start(); ?>
        <div class="wrap">
          <div class="hd">
            <?php if ( $termBack ) { ?><a class="term-back" href="<?= $eD( $termBack ) ?>" title="Zurück zur Terminal-Übersicht"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l9-8 9 8"/><path d="M5 10v9a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-9"/></svg> Terminal</a>
            <?php } elseif ( $directLogout ) { ?><a class="term-back" href="<?= $eD( $directLogout ) ?>" title="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> Logout</a><?php } ?>
            <div class="k">Dashboard · Anreisetag</div>
            <div class="t"><?= $eD( $dLabelD ) ?></div>
            <div class="stats"><span class="stat"><b><?= number_format( (int) $dash['guests'], 0, ',', '.' ) ?></b> Gäste<?php if ( $dash['shipCount'] > 1 ) echo ' · ' . (int) $dash['shipCount'] . ' Schiffe'; ?></span></div>
            <?php if ( $chipsD ) { ?><div class="flt"><?php foreach ( $chipsD as $c ) { ?><span class="chip"><?= $eD( $c ) ?></span><?php } ?></div><?php } ?>
          </div>

          <?php if ( ! $dash['ships'] ) { ?>
            <div class="empty">Keine Anreisen für diese Auswahl.</div>
          <?php } else { ?>
          <div class="dash-shell">
            <?php foreach ( $tripKeys as $ti => $trip ) {
                  $ships  = $byTrip[ $trip ];
                  $tlabel = $trip > 0 ? ( $trip . ' Tage' ) : 'Ohne Reiselänge'; ?>
              <section class="dash-pane<?= $ti === 0 ? ' active' : '' ?>" data-pane="t<?= (int) $trip ?>">
                <?php if ( count( $ships ) > 1 ) echo pacim_dash_sum_html( $ships, $tlabel, $showH, $showA, $showV );
                foreach ( $ships as $s ) echo pacim_dash_card_html( $s, $showH, $showA, $showV );
                ?>
              </section>
            <?php } ?>

            <section class="dash-pane" data-pane="charts">
              <div class="diag-bar">
                <button type="button" class="diag-fbtn" id="diagFilter">
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
                  Filter<span class="diag-fcount d-none" id="diagFcount">0</span>
                </button>
              </div>
              <div id="diagOut"></div>
            </section>
          </div>

          <nav class="dash-tabs" id="dashTabs">
            <?php foreach ( $tripKeys as $ti => $trip ) { ?>
              <button type="button" class="dash-tab<?= $ti === 0 ? ' active' : '' ?>" data-tab="t<?= (int) $trip ?>"><span class="dt-n"><?= $trip > 0 ? (int) $trip : '—' ?></span><span class="dt-l"><?= $trip > 0 ? 'Tage' : 'Reise' ?></span></button>
            <?php } ?>
            <button type="button" class="dash-tab" data-tab="charts"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:20px;height:20px;"><path d="M3 3v18h18"/><rect x="7" y="10" width="3" height="7"/><rect x="12" y="6" width="3" height="11"/><rect x="17" y="13" width="3" height="4"/></svg><span class="dt-l">Diagramme</span></button>
          </nav>

          <!-- Filter-Modal (Badges) -->
          <div class="diag-modal d-none" id="diagModal">
            <div class="diag-sheet">
              <div class="diag-sheet-h"><span>Filter</span><button type="button" class="diag-x" id="diagClose">&times;</button></div>
              <div class="diag-body">
                <div class="diag-grp"><div class="diag-grp-t">Stellplatz</div><div class="diag-badges" id="diagPark"></div></div>
                <div class="diag-grp"><div class="diag-grp-t">Reiselänge</div><div class="diag-badges" id="diagTrip"></div></div>
                <div class="diag-grp"><div class="diag-grp-t">Schiff</div><div class="diag-badges" id="diagShip"></div></div>
              </div>
              <div class="diag-sheet-f"><button type="button" class="diag-reset" id="diagReset">Zurücksetzen</button><button type="button" class="diag-apply" id="diagApply">Anwenden</button></div>
            </div>
          </div>
          <?php } ?>
        </div>
        <style>
          <?= pacim_dash_css() ?>
          .dash-shell{padding:0 0 calc(72px + env(safe-area-inset-bottom,0px));}
          .dash-pane{display:none;} .dash-pane.active{display:block;}
          /* Footer-Tabs */
          .dash-tabs{position:fixed;left:0;right:0;bottom:0;max-width:560px;margin:0 auto;display:flex;background:#fff;border-top:1px solid #e6eef4;box-shadow:0 -2px 14px rgba(13,44,68,.1);padding-bottom:env(safe-area-inset-bottom,0px);z-index:20;}
          .dash-tab{flex:1;border:0;background:none;cursor:pointer;-webkit-tap-highlight-color:transparent;font:inherit;display:flex;flex-direction:column;align-items:center;gap:2px;padding:9px 4px;position:relative;color:#5b7488;}
          .dash-tab .dt-n{font-size:17px;font-weight:800;line-height:1;}
          .dash-tab .dt-l{font-size:10.5px;font-weight:700;}
          .dash-tab.active{color:#1b6ca8;}
          .dash-tab.active::before{content:"";position:absolute;top:0;left:24%;right:24%;height:3px;border-radius:0 0 3px 3px;background:#1b6ca8;}
          .dash-tab:active{background:rgba(27,108,168,.06);}
          /* Diagramme: Filter + Statistik */
          .diag-bar{padding:14px 16px;}
          .diag-fbtn{display:inline-flex;align-items:center;gap:8px;background:#0d2c44;color:#fff;border:0;border-radius:11px;padding:11px 16px;font-size:15px;font-weight:700;cursor:pointer;-webkit-tap-highlight-color:transparent;}
          .diag-fbtn svg{width:18px;height:18px;}
          .diag-fcount{background:#fff;color:#0d2c44;border-radius:999px;min-width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;padding:0 6px;}
          .diag-sec{padding:14px 16px 4px;font-size:12px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:#5b7488;}
          .diag-hint{padding:0 16px 4px;font-size:12px;color:#5b7488;}
          .dw-grid .dv-sub{display:block;font-size:10px;font-weight:600;color:#8aa0b3;margin-top:1px;}
          .diag-modal{position:fixed;inset:0;z-index:60;background:rgba(13,44,68,.45);display:flex;align-items:flex-end;}
          .diag-sheet{background:#fff;width:100%;max-width:560px;margin:0 auto;border-radius:18px 18px 0 0;max-height:85vh;display:flex;flex-direction:column;padding-bottom:env(safe-area-inset-bottom,0px);}
          .diag-sheet-h{display:flex;align-items:center;justify-content:space-between;padding:16px 18px;font-size:18px;font-weight:800;color:#16384f;border-bottom:1px solid #eef3f7;}
          .diag-x{background:none;border:0;font-size:28px;line-height:1;color:#5b7488;cursor:pointer;}
          .diag-body{overflow-y:auto;padding:8px 18px 4px;}
          .diag-grp{margin-bottom:14px;}
          .diag-grp-t{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;color:#5b7488;margin-bottom:8px;}
          .diag-badges{display:flex;flex-wrap:wrap;gap:8px;}
          .diag-badge{border:1.5px solid #cdd9e4;background:#fff;color:#16384f;border-radius:999px;padding:8px 14px;font-size:14px;font-weight:700;cursor:pointer;-webkit-tap-highlight-color:transparent;}
          .diag-badge.on{background:#1b6ca8;border-color:#1b6ca8;color:#fff;}
          .diag-sheet-f{display:flex;gap:10px;padding:14px 18px;border-top:1px solid #eef3f7;}
          .diag-reset{flex:0 0 auto;background:#eef3f7;color:#16384f;border:0;border-radius:11px;padding:13px 18px;font-size:15px;font-weight:700;cursor:pointer;}
          .diag-apply{flex:1;background:#1b6ca8;color:#fff;border:0;border-radius:11px;padding:13px;font-size:16px;font-weight:800;cursor:pointer;}
        </style>
        <script>
        window.DASH = { ships: <?= json_encode( $diagShips, JSON_UNESCAPED_UNICODE ) ?>, parks: <?= json_encode( array_values( $pk ), JSON_UNESCAPED_UNICODE ) ?> };
        (function(){
          var tabs=document.getElementById('dashTabs'); if(!tabs) return;
          var panes=document.querySelectorAll('.dash-pane');
          tabs.addEventListener('click',function(e){ var b=e.target.closest('.dash-tab'); if(!b) return;
            var t=b.getAttribute('data-tab');
            for(var i=0;i<panes.length;i++) panes[i].classList.toggle('active', panes[i].getAttribute('data-pane')===t);
            var bs=tabs.querySelectorAll('.dash-tab'); for(var j=0;j<bs.length;j++) bs[j].classList.toggle('active', bs[j]===b);
            window.scrollTo(0,0);
          });

          /* ---- Diagramme: Filter-Badges + Einzel-/Gesamtstatistik ---- */
          var D=window.DASH, out=document.getElementById('diagOut'); if(!out) return;
          function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];});}
          var PARK_LBL={halle:'Halle',aussen:'Außen',valet:'Valet'};
          // verfügbare Reiselängen + Schiffe
          var trips=[], ships=[];
          D.ships.forEach(function(s){ if(trips.indexOf(s.t)<0) trips.push(s.t); if(ships.indexOf(s.n)<0) ships.push(s.n); });
          trips.sort(function(a,b){return a-b;}); ships.sort();
          var selPark=D.parks.slice(), selTrip=[], selShip=[];

          function badge(txt,on){ return '<button type="button" class="diag-badge'+(on?' on':'')+'" data-v="'+esc(txt)+'">'+esc(txt)+'</button>'; }
          function buildModal(){
            document.getElementById('diagPark').innerHTML = D.parks.map(function(p){return badge(PARK_LBL[p]||p, selPark.indexOf(p)>=0);}).join('');
            document.getElementById('diagTrip').innerHTML = trips.length? trips.map(function(t){return '<button type="button" class="diag-badge'+(selTrip.indexOf(t)>=0?' on':'')+'" data-t="'+t+'">'+t+' Tage</button>';}).join('') : '<span class="dw-muted">–</span>';
            document.getElementById('diagShip').innerHTML = ships.length? ships.map(function(n){return '<button type="button" class="diag-badge'+(selShip.indexOf(n)>=0?' on':'')+'" data-s="'+esc(n)+'">'+esc(n)+'</button>';}).join('') : '<span class="dw-muted">–</span>';
          }
          // Karte „noch erwartet": große Zahl = offen (nicht eingecheckt), klein darunter „von <gesamt>".
          function cell(lbl,cls,o,t){ return '<div'+(cls==='g'?' class="g"':'')+'><span class="dl">'+lbl+'</span><span class="dv '+(cls==='g'?'':cls)+'">'+(o||'–')+'</span><span class="dv-sub">von '+t+'</span></div>'; }
          function statCard(title, oh,oa,ov, th,ta,tv){
            var sh=selPark.indexOf('halle')>=0, sa=selPark.indexOf('aussen')>=0, sv=selPark.indexOf('valet')>=0;
            var og=(sh?oh:0)+(sa?oa:0), tg=(sh?th:0)+(sa?ta:0);
            var g='<div class="dw-card"><div class="dw-card-h">'+esc(title)+'</div><div class="dw-stat"><div class="dw-grid">';
            if(sh)g+=cell('Halle','h',oh,th);
            if(sa)g+=cell('Außen','a',oa,ta);
            if(sv)g+=cell('Valet','v',ov,tv);
            g+=cell('Gesamt','g',og,tg)+'</div></div></div>';
            return g;
          }
          function render(){
            var fc=selTrip.length+selShip.length, fcEl=document.getElementById('diagFcount');
            if(fcEl){ fcEl.textContent=fc; fcEl.classList.toggle('d-none', fc===0); }
            var match=D.ships.filter(function(s){ return (!selShip.length||selShip.indexOf(s.n)>=0) && (!selTrip.length||selTrip.indexOf(s.t)>=0); });
            var h='<div class="diag-hint">Große Zahl = <b>noch erwartet</b> (nicht eingecheckt) · „von" = gesamt</div>';
            if(!match.length){ out.innerHTML=h+'<div class="dw-card"><div class="dw-muted" style="padding:6px 0;">Keine Auswahl trifft zu.</div></div>'; return; }
            h+='<div class="diag-sec">Einzeln</div>';
            match.forEach(function(s){ h+=statCard(s.n+(s.t>0?' · '+s.t+'T':''), s.oh,s.oa,s.ov, s.h,s.a,s.v); });
            var oh=0,oa=0,ov=0,th=0,ta=0,tv=0; match.forEach(function(s){oh+=s.oh;oa+=s.oa;ov+=s.ov;th+=s.h;ta+=s.a;tv+=s.v;});
            h+='<div class="diag-sec">Gesamt der Auswahl</div>';
            h+=statCard('Gesamt · '+match.length+' Schiff'+(match.length===1?'':'e'), oh,oa,ov, th,ta,tv);
            out.innerHTML=h;
          }
          var modal=document.getElementById('diagModal');
          document.getElementById('diagFilter').addEventListener('click', function(){ buildModal(); modal.classList.remove('d-none'); });
          document.getElementById('diagClose').addEventListener('click', function(){ modal.classList.add('d-none'); });
          modal.addEventListener('click', function(e){ if(e.target===modal) modal.classList.add('d-none'); });
          document.getElementById('diagPark').addEventListener('click', function(e){ var b=e.target.closest('.diag-badge'); if(!b)return; var p=Object.keys(PARK_LBL).filter(function(k){return PARK_LBL[k]===b.getAttribute('data-v');})[0]; if(!p)return; var i=selPark.indexOf(p); if(i>=0)selPark.splice(i,1); else selPark.push(p); b.classList.toggle('on'); });
          document.getElementById('diagTrip').addEventListener('click', function(e){ var b=e.target.closest('.diag-badge'); if(!b)return; var t=parseInt(b.getAttribute('data-t'),10); var i=selTrip.indexOf(t); if(i>=0)selTrip.splice(i,1); else selTrip.push(t); b.classList.toggle('on'); });
          document.getElementById('diagShip').addEventListener('click', function(e){ var b=e.target.closest('.diag-badge'); if(!b)return; var n=b.getAttribute('data-s'); var i=selShip.indexOf(n); if(i>=0)selShip.splice(i,1); else selShip.push(n); b.classList.toggle('on'); });
          document.getElementById('diagReset').addEventListener('click', function(){ selPark=D.parks.slice(); selTrip=[]; selShip=[]; buildModal(); });
          document.getElementById('diagApply').addEventListener('click', function(){ modal.classList.add('d-none'); render(); });
          render();
        })();</script>
        <?php
        dl_html( 'Dashboard ' . $dLabelD, ob_get_clean() );
    }

    // $date kommt von oben (ma_date bzw. &d innerhalb der Gültigkeit).
    $canAct    = ( (int) $access['ma_perm_action'] === 1 );
    $searchAll = ( (int) $access['ma_perm_search'] === 1 );
    $cols      = array_merge( array( 'name' ), pacim_ma_clean_cols( isset( $access['ma_cols'] ) ? $access['ma_cols'] : 'type,time,dur' ) );
    $da        = pacim_day_arrivals( $db, $date, pacim_ma_filter_opts( $access ) );
    $items     = $da['items'];
    if ( $searchAll && $action !== 'poll' ) {
        // Suche über den Filter hinaus: restliche Fahrzeuge des Tages laden und als „außerhalb" markieren.
        $allDa = pacim_day_arrivals( $db, $date, array( 'mode' => pacim_ma_mode( $access ), 'parking' => array( 'halle', 'aussen', 'valet' ), 'statuses' => array( 'open', 'arrived', 'storno' ) ) );
        $inIds = array(); foreach ( $items as $it ) $inIds[ (int) $it['id'] ] = true;
        foreach ( $allDa['items'] as $it ) if ( empty( $inIds[ (int) $it['id'] ] ) ) $outItems[] = $it;
    }
    $chips = pacim_ma_summary( $access );
    if ( ! $canAct ) $chips[] = 'nur Ansicht';
    if ( trim( (string) $access['ma_label'] ) !== '' ) array_unshift( $chips, $access['ma_label'] );
} else {
    /* ---- Legacy: date + Hash (Pushover-Tagesliste an den eigenen Account) */
    if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) || ! function_exists( 'pacim_sign_verify' ) || ! pacim_sign_verify( 'daylist:' . $date, $h ) ) {
        dl_html( 'Ungültiger Link', '<div class="err"><h1>403</h1><p>Dieser Link ist ungültig oder abgelaufen.</p></div>', 403 );
    }
    $cols = array_values( array_intersect( array( 'name', 'type', 'time', 'dur' ),
        array_map( 'trim', explode( ',', (string) ( $_GET['columns'] ?? 'name,type' ) ) ) ) );
    $da = pacim_day_arrivals( $db, $date, array(
        'parking' => explode( ',', (string) ( $_GET['parking'] ?? 'halle,aussen' ) ),
        'status'  => (string) ( $_GET['status'] ?? 'all' ),
        'from'    => (string) ( $_GET['from'] ?? '' ), 'to' => (string) ( $_GET['to'] ?? '' ),
        'ship'    => (string) ( $_GET['ship'] ?? '' ), 'trip' => (int) ( $_GET['trip'] ?? 0 ),
    ) );
    $items = $da['items'];
    if ( $da['status'] === 'open' )    $chips[] = 'nur offen';
    if ( $da['status'] === 'arrived' ) $chips[] = 'nur angereist';
    if ( $da['from'] !== '' || $da['to'] !== '' ) $chips[] = trim( ( $da['from'] !== '' ? 'ab ' . $da['from'] : '' ) . ( $da['to'] !== '' ? ' bis ' . $da['to'] : '' ) );
    if ( $da['ship'] !== '' ) $chips[] = $da['ship'];
    if ( $da['trip'] > 0 )    $chips[] = $da['trip'] . ' Tage';
    $plabels = array( 'halle' => 'Halle', 'aussen' => 'Außen', 'valet' => 'Valet' );
    if ( count( $da['parking'] ) < 3 ) $chips[] = implode( '+', array_map( function ( $p ) use ( $plabels ) { return $plabels[ $p ]; }, $da['parking'] ) );
}

// Modus dieser Liste (Anreisen/Abreisen) – steuert Beschriftungen (Check-in vs. Check-out).
$isDep = ( isset( $da['mode'] ) && $da['mode'] === 'abreise' );

// Kompakter Status-Code je Buchung + Fingerprint über das In-Filter-Set (für den Autorefresh).
$stateOf = function ( $it ) { return ! empty( $it['storno'] ) ? 'x' : ( $it['arrived'] ? 'i' : ( $it['paid'] ? 'p' : 'o' ) ); };
$fpParts = array();
foreach ( $items as $it ) $fpParts[] = $it['id'] . $stateOf( $it ) . $it['time'];   // Zeit mit einbeziehen (z. B. geplant -> tatsächlich)
$fp = substr( md5( implode( ',', $fpParts ) ), 0, 12 );

/* ---- Autorefresh-Endpunkt: nur Status (winzige Antwort, kein HTML). Ressourcenschonend:
 *      Bei unverändertem Fingerprint kommt nur {"v":…} zurück (kein Datentransfer/Render). */
if ( $action === 'poll' ) {
    while ( ob_get_level() > 0 ) ob_end_clean();
    header( 'Content-Type: application/json; charset=utf-8' );
    header( 'Cache-Control: no-store' );
    if ( (string) ( $_GET['v'] ?? '' ) === $fp ) { echo '{"v":"' . $fp . '"}'; exit(); }
    $out = array();
    foreach ( $items as $it ) $out[] = array( (int) $it['id'], $stateOf( $it ), (string) $it['time'] );
    echo json_encode( array( 'v' => $fp, 'items' => $out ) );
    exit();
}

$e = function ( $s ) { return htmlspecialchars( (string) $s, ENT_QUOTES, 'UTF-8' ); };
// Check-in-Link je Buchung: im Token-Modus mit t=, sonst Legacy-Hash.
$ckUrl = function ( $id ) use ( $tok ) {
    $id = (int) $id;
    return $tok !== '' ? SITE_URL . '/checkin.php?t=' . $tok . '&b=' . $id
                       : SITE_URL . '/checkin.php?b=' . $id . '&h=' . pacim_sign( 'checkin:' . $id );
};
$wdays = array( 1 => 'Mo', 2 => 'Di', 3 => 'Mi', 4 => 'Do', 5 => 'Fr', 6 => 'Sa', 7 => 'So' );
$dLabel = $wdays[ (int) date( 'N', strtotime( $date ) ) ] . ', ' . date( 'd.m.Y', strtotime( $date ) );

// „Typ"-Spalte (H/A/VH/VA) nur sinnvoll, wenn mehr als ein Stellplatz oder Valet (=VH/VA) im Filter ist.
// Redundant ist sie nur bei genau „nur Halle" (alles H) bzw. „nur Außen" (alles A).
if ( count( $da['parking'] ) <= 1 && ! in_array( 'valet', $da['parking'], true ) )
    $cols = array_values( array_diff( $cols, array( 'type' ) ) );

// Tabellenkopf. Typ + Tage in EINER Spalte (Typ fett, „x Tage" klein darunter) – keine eigene Tage-Spalte mehr.
$showTyp = ( in_array( 'type', $cols, true ) || in_array( 'dur', $cols, true ) );
$thead = '<th>Kennz.</th>';
if ( in_array( 'name', $cols, true ) ) $thead .= '<th>Name</th>';
if ( $showTyp )                        $thead .= '<th>Typ</th>';
if ( in_array( 'time', $cols, true ) ) $thead .= '<th>Zeit</th>';

// Storno zählt NICHT bei „angereist"/„offen" (und erscheint nicht in diesen Filtern).
$arrivedCount = 0; $openCount = 0;
foreach ( $items as $it ) { if ( ! empty( $it['storno'] ) ) continue; if ( $it['arrived'] ) $arrivedCount++; else $openCount++; }
$activeCount = $arrivedCount + $openCount;   // Gesamt ohne Storno

// Suchstring normalisieren (Leer-/Sonderzeichen raus) – muss zur JS-Normalisierung passen.
function dl_norm( $s ) { return preg_replace( '/[^0-9a-zäöüß]/u', '', mb_strtolower( (string) $s, 'UTF-8' ) ); }

// Bildanzahl je Buchung (nur beim Voll-Render/rows, NICHT im Poll – hält den Poll schlank).
$allBids = array();
foreach ( $items as $it ) $allBids[] = $it['id'];
foreach ( $outItems as $it ) $allBids[] = $it['id'];
$imgCounts = function_exists( 'pacim_bi_count_map' ) ? pacim_bi_count_map( $db, $allBids ) : array();

// Eine Zeile rendern. $out = außerhalb des Zugangsfilters (nur per Suche auffindbar, markiert).
$mkRow = function ( $it, $out ) use ( $e, $cols, $ckUrl, $imgCounts, $showTyp ) {
    $state = ! empty( $it['storno'] ) ? 'st-storno' : ( $it['arrived'] ? 'st-in' : ( $it['paid'] ? 'st-paid' : 'st-open' ) );
    if ( $out ) $state .= ' row-out';
    // Durchsucht: Kennzeichen, Name, Buchungs- und Rechnungsnummer (Letztere nicht angezeigt).
    $srch = dl_norm( $it['plate'] . ' ' . $it['name'] . ' ' . $it['booking_no'] . ' ' . $it['invoice_no'] );
    // Sortierschlüssel der tatsächlichen Check-in-Zeit (HHMM) – nur für Angereiste; sonst leer.
    $cin = ( ! empty( $it['arrived'] ) && $it['time'] !== '' ) ? preg_replace( '/\D/', '', $it['time'] ) : '';
    $plateD = preg_replace( '/\s*-\s*/', '-', (string) $it['plate'] );   // „HEI - JC 7" -> „HEI-JC 7"
    $r  = '<tr class="' . $state . '" data-noloader="1" data-id="' . (int) $it['id'] . '" data-cin="' . $e( $cin ) . '" data-href="' . $e( $ckUrl( $it['id'] ) ) . '" data-s="' . $e( $srch ) . '" data-plate="' . $e( $plateD ) . '" data-out="' . ( $out ? '1' : '0' ) . '">';
    $r .= '<td class="plate">' . $e( $plateD ) . '</td>';
    $ic = isset( $imgCounts[ (int) $it['id'] ] ) ? (int) $imgCounts[ (int) $it['id'] ] : 0;
    $cam = $ic > 0 ? ' <span class="pcam" title="' . $ic . ' Foto(s)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>' . $ic . '</span>' : '';
    if ( in_array( 'name', $cols, true ) ) {
        $shipTxt = trim( (string) ( $it['ship'] ?? '' ) );
        $r .= '<td class="nm"><span class="nm-name">' . $e( $it['name'] ) . ( $out ? ' <span class="outflag">außerhalb</span>' : '' ) . $cam . '</span>'
            . ( $shipTxt !== '' && $shipTxt !== '—' ? '<span class="nm-ship">' . $e( $shipTxt ) . '</span>' : '' ) . '</td>';
    }
    if ( $showTyp ) {
        $tm = array( 'H' => 'Halle', 'A' => 'Außen', 'VH' => 'V-Halle', 'VA' => 'V-Außen' );
        $c = '<td class="typ-cell">';
        if ( in_array( 'type', $cols, true ) ) $c .= '<span class="typ-name">' . $e( isset( $tm[ $it['letter'] ] ) ? $tm[ $it['letter'] ] : $it['letter'] ) . '</span>';
        if ( in_array( 'dur', $cols, true ) )  $c .= '<span class="typ-days">' . ( $it['dur'] > 0 ? (int) $it['dur'] . ' Tage' : '—' ) . '</span>';
        $r .= $c . '</td>';
    }
    if ( in_array( 'time', $cols, true ) ) $r .= '<td class="mut tcell">' . ( $it['time'] !== '' ? $e( $it['time'] ) : '—' ) . '</td>';
    $r .= '</tr>';
    return $r;
};
// Stabile Reihenfolge nach Kennzeichen – Angereiste rutschen NICHT nach unten (bleiben an ihrem Platz).
$sorter = function ( $a, $b ) { return strnatcasecmp( $a['plate'], $b['plate'] ); };
usort( $items, $sorter );
usort( $outItems, $sorter );

$rowsHtml = '';
foreach ( $items as $it )    $rowsHtml .= $mkRow( $it, false );
foreach ( $outItems as $it ) $rowsHtml .= $mkRow( $it, true );

/* ---- Autorefresh: neue Tabellen-Zeilen (nur bei geänderter Zusammensetzung angefordert). */
if ( $action === 'rows' ) {
    while ( ob_get_level() > 0 ) ob_end_clean();
    header( 'Content-Type: application/json; charset=utf-8' );
    header( 'Cache-Control: no-store' );
    echo json_encode( array( 'v' => $fp, 'arr' => $arrivedCount, 'open' => $openCount, 'total' => $activeCount, 'html' => $rowsHtml ) );
    exit();
}

ob_start(); ?>
<div class="wrap">
  <div class="topbar">
  <div class="hd">
    <?php if ( $termBack ) { ?><a class="term-back" href="<?= $e( $termBack ) ?>" title="Zurück zur Terminal-Übersicht"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M3 11l9-8 9 8"/><path d="M5 10v9a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-9"/></svg> Terminal</a>
    <?php } elseif ( $directLogout ) { ?><a class="term-back" href="<?= $e( $directLogout ) ?>" title="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> Logout</a><?php } ?>
    <div class="k"><?= $isDep ? 'Abreisen · Check-out' : 'Anreisen · Check-in' ?></div>
    <div class="t"><?= $e( $dLabel ) ?></div>
    <div class="stats">
      <span class="stat filt" id="filt-arr" role="button" tabindex="0"><b id="st-arr"><?= $arrivedCount ?></b> / <span id="st-tot"><?= $activeCount ?></span> <?= $isDep ? 'abgereist' : 'angereist' ?></span>
      <span class="stat open filt" id="filt-open" role="button" tabindex="0"><b id="st-open"><?= $openCount ?></b> offen</span>
      <span class="stat live" id="dl-live" role="button" tabindex="0" title="Zuletzt geöffnete Fahrzeuge"><span class="live-dot"></span> live</span>
    </div>
    <?php if ( $chips ) { ?><div class="flt"><?php foreach ( $chips as $c ) { ?><span class="chip"><?= $e( $c ) ?></span><?php } ?></div><?php } ?>
  </div>
  <div class="search">
    <div class="search-box">
      <input type="search" id="dl-search" placeholder="Suche: Kennzeichen, Name, Buchungs-/Rechnungsnr. …" autocomplete="off" autocapitalize="characters">
      <button type="button" class="search-clear d-none" id="dl-clear" aria-label="Suche löschen">&times;</button>
    </div>
  </div>
  </div><!-- /topbar -->
  <div class="suggest d-none" id="dl-suggest"></div>
  <?php if ( $items || $outItems ) { ?>
    <div class="tblwrap"><table class="tbl"><thead><tr><?= $thead ?></tr></thead><tbody id="dl-body"><?= $rowsHtml ?></tbody></table></div>
    <div class="empty d-none" id="dl-none">Kein Treffer.</div>
  <?php } else { ?>
    <div class="empty"><?= $isDep ? 'Keine Abreisen für diese Auswahl.' : 'Keine Anreisen für diese Auswahl.' ?></div>
  <?php } ?>
</div>

<!-- Sitzungsende: Zugang nicht mehr verfügbar (vom Autorefresh erkannt) -->
<div class="ended d-none" id="dl-ended">
  <div class="ended-box">
    <svg class="ended-ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6"><rect x="5" y="11" width="14" height="9" rx="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3"/></svg>
    <div class="ended-t">Zugang beendet</div>
    <div class="ended-x">Dieser mobile Zugang ist nicht mehr verfügbar (widerrufen oder abgelaufen).<br>Bitte wende dich an das Büro.</div>
  </div>
</div>

<!-- Check-in/Zahlung im Overlay (kein Seitenwechsel) -->
<div class="ov d-none" id="dl-ov">
  <!-- Kein eigener Zurück-Balken mehr: die Buchungseinzelansicht (checkin.php) hat ihren eigenen Zurück-Button oben rechts im Header (postMessage {close:1}).
       Nur ein navyfarbener Safe-Area-Streifen, damit der eingebettete Header nicht unter der iPhone-Dynamic-Island liegt. -->
  <div class="ov-top"></div>
  <iframe class="ov-frame" id="dl-ov-frame" title="Check-in" src="about:blank" allowfullscreen></iframe>
</div>

<!-- „Zuletzt geöffnet" (Klick auf „live") -->
<div class="rec d-none" id="dl-recent">
  <div class="rec-sheet">
    <div class="rec-h"><span>Zuletzt geöffnet</span><span class="rec-h-act"><button type="button" class="rec-reset" id="dl-recent-reset">Zurücksetzen</button><button type="button" class="rec-x" id="dl-recent-x" aria-label="Schließen">&times;</button></span></div>
    <div class="rec-list" id="dl-recent-list"></div>
  </div>
</div>
<script>
(function(){
  // Browser-Zoom unterbinden (iOS Safari ignoriert user-scalable=no)
  ['gesturestart','gesturechange','gestureend'].forEach(function(ev){ document.addEventListener(ev, function(e){ e.preventDefault(); }, {passive:false}); });
  document.addEventListener('touchmove', function(e){ if(e.touches && e.touches.length>1) e.preventDefault(); }, {passive:false});

  var inp=document.getElementById('dl-search'), body=document.getElementById('dl-body'), none=document.getElementById('dl-none');
  if(!body) return;
  var DL_FP = <?= json_encode( $fp ) ?>;
  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; }
  // Damerau-Levenshtein (OSA) – erlaubt Buchstaben-/Zahlendreher als 1 Edit.
  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];
  }
  // Kennzeichen ohne Ortskürzel (erstes Segment), normalisiert – für „anderer Ort, Rest passt".
  function plateCore(plate){ var p=(plate||'').split(/[^0-9a-zA-ZäöüÄÖÜß]+/).filter(Boolean); return norm(p.length>1?p.slice(1).join(''):plate); }
  // Zeichen-Häufigkeit (Multiset) eines normalisierten Strings.
  function freq(s){ var m={}; for(var i=0;i<s.length;i++){ m[s[i]]=(m[s[i]]||0)+1; } return m; }
  // Enthält s alle Zeichen von q (mit Vielfachheit), egal in welcher Reihenfolge? (Gruppen-/Zeichendreher tolerant)
  function multiSub(q,s){ var fs=freq(s); for(var k in q){ if((fs[k]||0)<q[k]) return false; } return true; }
  /* Kennzeichen-Trefferstärke (kleiner = besser, -1 = kein Treffer). Beste Treffer = 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 plateScore(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;
  }
  // Kleinste Editierdistanz von q zu IRGENDEINEM Teilstück von s (längen-tolerant) – fürs „Meinten Sie?".
  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;
  }
  // Kennzeichen mit Treffer-Hervorhebung: erst exakte Subsequenz, sonst Multiset (Reihenfolge egal).
  function hl(plate,q){
    if(!q) return esc(plate);
    var qi=0,out='';   // 1) Subsequenz in Reihenfolge
    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;   // 2) Multiset (für vertauschte Reihenfolge)
    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'));      // Ausgangsreihenfolge merken
  var sgBox=document.getElementById('dl-suggest');

  // „Meinten Sie?": ähnliche Kennzeichen (Tippfehler, Dreher, falsches Ortskürzel), die NICHT bereits Treffer sind.
  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(plateScore(q, plate)>=0) continue;   // bereits (auch unscharfer) Kennzeichen-Treffer → nicht vorschlagen
      var dist=Math.min(fuzzyDist(q, norm(plate)), fuzzyDist(q, plateCore(plate)));
      if(dist<=maxd){ seen[plate]=1; res.push({plate:plate, href:r.dataset.href, d:dist}); }
    }
    res.sort(function(a,b){return a.d-b.d;});
    return res.slice(0,4);
  }
  function renderSuggest(q, plateHits){
    if(!sgBox) return;
    if(!q || q.length<2 || plateHits>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/Zahlung im Overlay öffnen (kein Seitenwechsel)
  var ov=document.getElementById('dl-ov'), ovFrame=document.getElementById('dl-ov-frame');
  function openCheckin(href){ if(!ov||!href) return; ovFrame.src = href + (href.indexOf('?')>=0?'&':'?') + 'embed=1'; ov.classList.remove('d-none'); document.documentElement.style.overflow='hidden'; }
  function closeCheckin(){ if(!ov) return; ov.classList.add('d-none'); ovFrame.src='about:blank'; document.documentElement.style.overflow='';
    if(inp) inp.value='';   // nach Öffnen+Zurück: Suche leeren
    runSearch();            // Status-Filter neu anwenden -> Fahrzeug erscheint/verschwindet entsprechend
    poll(); }
  // ganze Zeile klickbar
  body.addEventListener('click', function(e){ var tr=e.target.closest('tr'); if(tr && tr.dataset.href){ if(tr.dataset.id) recordRecent(tr.dataset.id); openCheckin(tr.dataset.href); } });

  /* ---- „Zuletzt geöffnet": Öffnen vermerken + Panel über „live" ---- */
  function recentUrl(extra){ var b=location.pathname+location.search; return b+(b.indexOf('?')>=0?'&':'?')+extra; }
  function recordRecent(id){ if(!id) return; try{ fetch(recentUrl('action=recent_add'),{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'b='+encodeURIComponent(id),keepalive:true}); }catch(e){} }
  var recPanel=document.getElementById('dl-recent'), recList=document.getElementById('dl-recent-list'), recX=document.getElementById('dl-recent-x');
  function openRecent(){ if(!recPanel) return; recList.innerHTML='<div class="rec-empty">Wird geladen …</div>'; recPanel.classList.remove('d-none'); document.documentElement.style.overflow='hidden';
    fetch(recentUrl('action=recent_list'),{headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){return r.json();}).then(function(res){
      var items=(res&&res.items)||[];
      if(!items.length){ recList.innerHTML='<div class="rec-empty">Noch nichts geöffnet.</div>'; return; }
      var h=''; for(var i=0;i<items.length;i++){ var it=items[i];
        h+='<button type="button" class="rec-item" data-noloader="1" data-href="'+esc(it.href)+'" data-id="'+it.id+'"><span class="rec-plate">'+esc(it.plate)+'</span><span class="rec-name">'+esc(it.name)+'</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></button>'; }
      recList.innerHTML=h;
    }).catch(function(){ recList.innerHTML='<div class="rec-empty">Konnte nicht geladen werden.</div>'; });
  }
  function closeRecent(){ if(recPanel) recPanel.classList.add('d-none'); document.documentElement.style.overflow=''; }
  var liveBtn=document.getElementById('dl-live');
  if(liveBtn) liveBtn.addEventListener('click', openRecent);
  if(recX) recX.addEventListener('click', closeRecent);
  var recReset=document.getElementById('dl-recent-reset');
  if(recReset) recReset.addEventListener('click', function(){
    if(!confirm('Liste „Zuletzt geöffnet" wirklich zurücksetzen?')) return;
    recReset.disabled=true;
    fetch(recentUrl('action=recent_clear'),{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'}})
      .then(function(r){return r.json();}).then(function(){ recReset.disabled=false; recList.innerHTML='<div class="rec-empty">Noch nichts geöffnet.</div>'; })
      .catch(function(){ recReset.disabled=false; });
  });
  if(recPanel) recPanel.addEventListener('click', function(e){ if(e.target===recPanel){ closeRecent(); return; }
    var it=e.target.closest('.rec-item'); if(it){ closeRecent(); if(it.dataset.id) recordRecent(it.dataset.id); openCheckin(it.dataset.href); } });

  // Live-Suche: filtern, Kennzeichen-Treffer nach oben, Hervorhebung, Vorschläge.
  function runSearch(){
    var q=norm(inp?inp.value:''), shown=0, plateHits=0, hitsP=[], hitsO=[];
    for(var i=0;i<orig.length;i++){ var r=orig[i];
      var isOut = r.getAttribute('data-out')==='1';
      var ps   = q ? plateScore(q, r.getAttribute('data-plate')||'') : -1;  // -1 = kein Kennzeichen-Treffer
      var pl   = ps>=0;
      // ohne Suche: nur Fahrzeuge im Filter; mit Suche: Kennzeichen-Treffer (auch vertauscht) ODER Treffer in Name/Nr.
      var full = q ? (pl || subseq(q, r.getAttribute('data-s')||'')) : (!isOut && passStatus(r));
      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){ plateHits++; r._ps=ps; hitsP.push(r); } else hitsO.push(r); }
    }
    // Reihenfolge: bei Suche Kennzeichen-Treffer zuerst (exakte Reihenfolge vor vertauschten); im „angereist"-Filter
    // nach tatsächlicher Check-in-Zeit absteigend (neueste zuerst); sonst Ausgangsreihenfolge (nach Kennzeichen).
    var order;
    if(q){ hitsP.sort(function(a,b){ return (a._ps-b._ps); }); order = hitsP.concat(hitsO); }
    else if(statusFilter==='arrived'){
      var vis=[], hid=[];
      for(var z=0;z<orig.length;z++){ (orig[z].style.display!=='none'?vis:hid).push(orig[z]); }
      vis.sort(function(a,b){ return (b.getAttribute('data-cin')||'').localeCompare(a.getAttribute('data-cin')||''); });
      order = vis.concat(hid);
    }
    else { order = orig; }
    for(var k=0;k<order.length;k++) body.appendChild(order[k]);
    if(none) none.classList.toggle('d-none', shown!==0);
    if(clearBtn) clearBtn.classList.toggle('d-none', !(inp && inp.value));
    renderSuggest(q, plateHits);
    if(q){ var fh=hitsP[0]||hitsO[0]; if(fh) scrollToFirst(fh); }   // bei Suche zum ersten Treffer springen
  }
  function scrollToFirst(el){
    var tb=document.querySelector('.topbar'); var off=(tb?tb.offsetHeight:0)+6;
    var y=el.getBoundingClientRect().top + (window.pageYOffset||document.documentElement.scrollTop||0) - off;
    window.scrollTo({top:(y<0?0:y), behavior:'smooth'});
  }
  var clearBtn=document.getElementById('dl-clear');
  function clearSearch(){ if(inp) inp.value=''; runSearch(); }
  if(clearBtn) clearBtn.addEventListener('click', function(){ clearSearch(); if(inp) inp.focus(); });
  if(inp) inp.addEventListener('input', runSearch);

  // Status-Schnellfilter über die Kopf-Statistik (angereist / offen). Beide schließen sich aus.
  var statusFilter='';   // '' | 'arrived' | 'open'
  function passStatus(r){ if(!statusFilter) return true; if(r.classList.contains('st-storno')) return false; var arr=r.classList.contains('st-in'); return statusFilter==='arrived'?arr:!arr; }
  function updateFilt(){ var fa=document.getElementById('filt-arr'), fo=document.getElementById('filt-open'); if(fa) fa.classList.toggle('active',statusFilter==='arrived'); if(fo) fo.classList.toggle('active',statusFilter==='open'); }
  (function(){
    var fa=document.getElementById('filt-arr'), fo=document.getElementById('filt-open');
    function tog(which){ statusFilter=(statusFilter===which)?'':which; updateFilt(); if(inp) inp.value=''; runSearch(); }
    if(fa){ fa.addEventListener('click',function(){tog('arrived');}); fa.addEventListener('keydown',function(e){if(e.key==='Enter'||e.key===' '){e.preventDefault();tog('arrived');}}); }
    if(fo){ fo.addEventListener('click',function(){tog('open');}); fo.addEventListener('keydown',function(e){if(e.key==='Enter'||e.key===' '){e.preventDefault();tog('open');}}); }
  })();

  // ================= Autorefresh (ressourcenschonend) =================
  // Poll schickt den letzten Fingerprint; ist nichts passiert, kommt nur {"v":…} zurück
  // (kein Datentransfer, kein Render). Pausiert bei verstecktem Tab und offenem Overlay.
  var fp = DL_FP, INT = 20000, live=document.getElementById('dl-live'), ended=false, pollTimer=null;
  // Zugang nicht mehr verfügbar -> App „beenden": Sitzungsende-Bildschirm, Polling stoppen.
  function endSession(){
    if(ended) return; ended=true;
    if(pollTimer){ clearInterval(pollTimer); pollTimer=null; }
    if(ov) ov.classList.add('d-none');
    var el=document.getElementById('dl-ended'); if(el) el.classList.remove('d-none');
    document.documentElement.style.overflow='hidden';
  }
  function base(){ return location.pathname + location.search; }
  function pollUrl(extra){ var b=base(); return b + (b.indexOf('?')>=0?'&':'?') + extra; }
  function setText(id,v){ var el=document.getElementById(id); if(el) el.textContent=v; }
  function stOf(tr){ return tr.classList.contains('st-in')?'i':tr.classList.contains('st-paid')?'p':tr.classList.contains('st-storno')?'x':'o'; }
  function applyState(tr, st){ tr.classList.remove('st-in','st-open','st-paid','st-storno'); tr.classList.add(st==='i'?'st-in':st==='p'?'st-paid':st==='x'?'st-storno':'st-open'); }
  function flash(tr){ tr.classList.remove('row-flash'); void tr.offsetWidth; tr.classList.add('row-flash'); }
  function sameSet(a,b){ if(a.length!==b.length) return false; var m={},i; for(i=0;i<a.length;i++) m[a[i]]=1; for(i=0;i<b.length;i++) if(!m[b[i]]) return false; return true; }

  function applyPoll(items){
    var map={}, tmap={}, ids=[], i;
    for(i=0;i<items.length;i++){ map[items[i][0]]=items[i][1]; tmap[items[i][0]]=items[i][2]; ids.push(String(items[i][0])); }
    var domRows=body.querySelectorAll('tr[data-out="0"]'), domIds=[];
    for(i=0;i<domRows.length;i++) domIds.push(domRows[i].getAttribute('data-id'));
    if(!sameSet(ids, domIds)){ fetchRows(); return; }   // Zusammensetzung geändert -> einmalig Zeilen neu laden
    var dirty=false;
    for(i=0;i<domRows.length;i++){ var tr=domRows[i], id=tr.getAttribute('data-id'), st=map[id], tm=tmap[id], chg=false;
      if(st && st!==stOf(tr)){ applyState(tr, st); chg=true; }
      var tc=tr.querySelector('.tcell'); if(tc && tm!==undefined){ var nv=tm||'—'; if(tc.textContent!==nv){ tc.textContent=nv; chg=true; } }
      if(st!==undefined) tr.setAttribute('data-cin', (st==='i' && tm) ? tm.replace(/\D/g,'') : '');   // Sortierzeit aktuell halten
      if(chg){ flash(tr); dirty=true; }
    }
    var arr=0, act=0; for(i=0;i<items.length;i++){ if(items[i][1]==='x') continue; act++; if(items[i][1]==='i') arr++; }
    setText('st-arr',arr); setText('st-tot',act); setText('st-open',act-arr);
    if(dirty && (statusFilter || (inp&&inp.value))) runSearch();   // nur bei aktivem Filter/Suche neu anwenden (sonst Flash erhalten)
  }
  function fetchRows(){
    fetch(pollUrl('action=rows'), {headers:{'X-Requested-With':'XMLHttpRequest'}}).then(function(r){return r.json();}).then(function(res){
      if(res && res.gone){ endSession(); return; }
      if(res && res.locked){ location.reload(); return; }
      if(!res||typeof res.html!=='string') return;
      fp=res.v; body.innerHTML=res.html; orig=[].slice.call(body.querySelectorAll('tr'));
      setText('st-arr',res.arr); setText('st-open',res.open); setText('st-tot',res.total); runSearch();
    }).catch(function(){});
  }
  function poll(){
    if(ended || document.hidden) return;
    if(ov && !ov.classList.contains('d-none')) return;     // während Overlay offen nicht pollen
    fetch(pollUrl('action=poll&v='+encodeURIComponent(fp)), {headers:{'X-Requested-With':'XMLHttpRequest'}})
      .then(function(r){return r.json();}).then(function(res){
        if(res && res.gone){ endSession(); return; }       // Zugang widerrufen/abgelaufen
        if(res && res.locked){ location.reload(); return; } // PIN-Sperre erneut aktiv (Cookie abgelaufen)
        if(live) live.classList.remove('stale');
        if(res && res.v && res.v!==fp){ fp=res.v; if(res.items) applyPoll(res.items); }
      }).catch(function(){ if(live) live.classList.add('stale'); });
  }
  pollTimer=setInterval(poll, INT);
  document.addEventListener('visibilitychange', function(){ if(!document.hidden) poll(); });

  // Sofort-Updates aus dem Check-in-Overlay (ohne auf den Poll zu warten)
  window.addEventListener('message', function(ev){
    var d=ev.data; if(!d || !d.pacimCi) return;
    if(d.rows){ fetchRows(); }
    else if(d.b && d.state){ var tr=body.querySelector('tr[data-id="'+d.b+'"]'); if(tr){ applyState(tr,d.state); flash(tr); if(statusFilter||(inp&&inp.value)) runSearch(); } }
    if(d.close){ closeCheckin(); }
  });
})();
</script>
<?php
dl_html( ( $isDep ? 'Abreisen ' : 'Anreisen ' ) . $dLabel, ob_get_clean() );

Youez - 2016 - github.com/yon3zu
LinuXploit