OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

main.php

161 KB
<!DOCTYPE html>
<html lang="<?= $this->e($this->lang()) ?>">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="robots" content="noindex, nofollow">
    <meta name="csrf-token" content="<?= $this->csrf() ?>">
    <meta name="money-format" content="<?= $this->e(json_encode($this->moneyJs(), JSON_UNESCAPED_UNICODE)) ?>">
    <title><?= $this->te($page_title ?? ($app_name ?? 'OpenNIT')) ?> - <?= $this->e($app_name ?? 'OpenNIT') ?></title>

    <link rel="icon" href="<?= !empty($app_favicon) ? $this->e($app_favicon) : '/assets/img/favicon-default.png' ?>">

    <!-- Kritische Fonts vorladen (400 + 600 – häufigste Gewichte) -->
    <link rel="preload" href="/assets/fonts/source-sans-pro/source-sans-pro-latin-400-normal.woff2" as="font" type="font/woff2" crossorigin>
    <link rel="preload" href="/assets/fonts/source-sans-pro/source-sans-pro-latin-600-normal.woff2" as="font" type="font/woff2" crossorigin>

    <link rel="stylesheet" href="/assets/css/fonts.css?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/css/fonts.css') ?>">
    <link rel="stylesheet" href="/assets/css/tokens.css?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/css/tokens.css') ?>">
    <link rel="stylesheet" href="/assets/css/app.css?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/css/app.css') ?>">
    <?php $_material = ($design_style ?? 'standard') === 'material'; ?>
    <?php if ($_material): ?>
    <link rel="stylesheet" href="/assets/css/theme-material.css?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/css/theme-material.css') ?>">
    <?php endif; ?>
    <link rel="stylesheet" href="/assets/css/responsive.css?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/css/responsive.css') ?>">
    <?php // ai-chat.css nur laden, wenn das KI-Chat-Widget/-Seiten überhaupt sichtbar sind (spart ~21 KB sonst). ?>
    <?php if (($modules['ai_chat'] ?? false) && $this->hasCapability('use_ai_chat')): ?>
    <link rel="stylesheet" href="/assets/css/ai-chat.css?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/css/ai-chat.css') ?>">
    <?php endif; ?>
    <link rel="stylesheet" href="/assets/css/attachments.css?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/css/attachments.css') ?>">
    <link rel="stylesheet" href="/assets/fontawesome/css/fontawesome.min.css?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/fontawesome/css/fontawesome.min.css') ?>">
    <link rel="stylesheet" href="/assets/fontawesome/css/solid.min.css?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/fontawesome/css/solid.min.css') ?>">

    <!-- app.js defer: Download startet sofort, blockiert aber kein HTML-Parsing -->
    <script src="/assets/js/app.js?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/js/app.js') ?>" defer></script>
    <!-- Dialoge mit Tastatur: Fokusfalle, Escape und Rücksprung – modulübergreifend. -->
    <script src="/assets/js/dialog.js?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/js/dialog.js') ?>" defer></script>
    <?php if ($this->strictScripts()): ?>
    <!-- Seiten ohne 'unsafe-inline' hängen ihr Verhalten an data-Attribute; der Verteiler verdrahtet sie. -->
    <script src="/assets/js/vault-actions.js?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/js/vault-actions.js') ?>" defer></script>
    <?php endif; ?>

    <?php if (isset($extra_css)): ?>
        <?php foreach ($extra_css as $css): ?>
            <link rel="stylesheet" href="<?= $this->asset($css) ?>">
        <?php endforeach; ?>
    <?php endif; ?>

    <!-- PWA -->
    <link rel="manifest" href="/manifest.webmanifest">
    <meta name="theme-color" content="<?= $_material ? '#1976d2' : '#2c3e50' ?>">
    <meta name="mobile-web-app-capable" content="yes">
    <?php // Ältere iOS-Fassungen kennen nur die eigene Schreibweise. ?>
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <meta name="apple-mobile-web-app-title" content="<?= $this->e($app_name ?? 'OpenNIT') ?>">
    <link rel="apple-touch-icon" href="<?= !empty($app_favicon) ? $this->e($app_favicon) : '/icons/icon-192.svg' ?>">
    <?php
    // AI-Editor-Flag früh setzen, damit wysiwyg.js es beim Initialisieren lesen kann
    ?>
    <script<?= $this->scriptNonce() ?>>window.AI_EDITOR_ENABLED = <?= !empty($_global_ai_editor_enabled) ? 'true' : 'false' ?>;window.AI_SUGGEST_ENABLED = <?= !empty($_global_ai_suggest_enabled) ? 'true' : 'false' ?>;</script>
    <?php
    /* Texte, die Skripte selbst zusammensetzen. Der Umweg über ein JSON-Objekt
       hält Übersetzungen mit Anführungszeichen aus den JS-Zeichenketten heraus. */
    ?>
    <script<?= $this->scriptNonce() ?>>
    window.ONIT_I18N = <?= $this->jsTranslations([
        ' [Priorität: {prio}]',
        ' bei „{firma}"',
        ' von {hersteller}',
        ' – {anzahl} Seats',
        ' → Priorität: „{prio}"',
        ' → Status: „{status}"',
        '<strong>Asset:</strong> {asset}<br><strong>Notiz:</strong> {notiz}',
        '<strong>Empfänger:</strong> {name}',
        '<strong>Lieferart:</strong> {wert}',
        '<strong>Notiz:</strong> {wert}',
        '<strong>Titel:</strong> {titel}',
        'Abbrechen',
        'Abgeschlossen',
        'Abhaken',
        'Abholbereit',
        'Absatz',
        'Abschnitt hinzufügen',
        'Aktion: {typ}',
        'Aktiv',
        'Aktualisieren',
        'Alle {anzahl} Einträge sind integer',
        'Anschlüsse',
        'Asset zuweisen',
        'Asset „{asset}" wird ausgegeben an: {person}',
        'Asset „{asset}" → {person}',
        'Asset „{asset}" → „{status}"',
        'Asset-Notiz ergänzen',
        'Asset-Status ändern',
        'Asset-Übergabe starten',
        'Aufgabe #{nummer}',
        'Aufgabe anlegen',
        'Aufgabe wirklich löschen?',
        'Aufzählung',
        'Aus dieser Störung ein Ticket erstellen?',
        'Ausführen',
        'Ausführlicher',
        'Ausgeben & Unterschrift anfordern',
        'Ausgemustert',
        'ausgewählt',
        'Ausgänge',
        'Bestand ändern',
        'Bestellposition abhaken',
        'Bestellstatus ändern',
        'Bestellung #{nummer} auf „{status}" setzen.',
        'Bestellung anlegen',
        'Bild einfügen',
        'Bitte auf dem Unterschriften-Tablet unterschreiben.',
        'Bitte ein Themengebiet auswählen',
        'Bitte Popup-Fenster für diese Seite erlauben.',
        'Chat-Verlauf löschen?',
        'Code',
        'Codeblock',
        'Datei zu groß (max. 10MB)',
        'Datum: {datum}',
        'Dialoge/Menüs schließen',
        'Diese Aktion kann nicht rückgängig gemacht werden.',
        'Diese Hilfe anzeigen',
        'Diese Seite',
        'Diese Sitzung löschen?',
        'Dokumentationsseite aktualisieren',
        'Dokumentationsseite anlegen',
        'Dringend',
        'Durchgestrichen',
        'Eingetroffen',
        'Eintrag löschen?',
        'Erfolgreich ausgeführt.',
        'Erledigen',
        'ersetzen',
        'erweitern',
        'Etikett konnte nicht erzeugt werden. Bitte später erneut versuchen.',
        'Etikett über DYMO Connect drucken',
        'Etikett über Windows-Druckdialog drucken',
        'Fehler beim Löschen',
        'Fehler korrigieren',
        'Fett (Ctrl+B)',
        'Firma anlegen',
        'Firma „{name}" im Adressbuch anlegen.',
        '„{name}" als Firma anlegen',
        'Format',
        'Formatierung entfernen',
        'Freundlicher',
        'Fällig: {datum}',
        'Gelöst',
        'gerade eben',
        'Gerät ausgeben',
        'Gerät-ID: {id}',
        'Gerät: {geraet}',
        'Geschlossen',
        'Global',
        'Groß',
        'Größer',
        'Gültig bis: {datum}',
        'Hallo! Wie kann ich helfen?',
        'Heute',
        'Hinzufügen',
        'Hoch',
        'Ich kenne alle Inhalte dieses IT-Tools. Frag mich zu Assets, Tickets, Vertr&auml;gen, VMs und mehr.',
        'Info-Box',
        'Inhalt ersetzen',
        'Integritätsfehler bei {anzahl} Einträgen gefunden!',
        'IP eintragen',
        'IP-Adresse eintragen',
        'IP-Eintrag aktualisieren',
        'Jetzt anlegen',
        'Kanban-Aufgabe anlegen',
        'Kanban-Aufgabe ändern',
        'Kein DYMO LabelWriter-Drucker gefunden. Bitte Drucker anschließen und DYMO Connect neu starten.',
        'Keine Benachrichtigungen',
        'Keine früheren Sitzungen',
        'Keine Zusatzdaten.',
        'KI-Assistent: Inhalt erstellen, bearbeiten oder ergänzen',
        'Klein',
        'Klicken zum Schließen',
        'Kontakt anlegen',
        'Kursiv (Ctrl+I)',
        'Kürzer',
        'Lager',
        'Lagerbestand ändern',
        'Leihgerät reservieren',
        'Leihgerät zurückgeben',
        'Leihvorgang #{nummer} als zurückgegeben markieren.',
        'Leihvorgang öffnen →',
        'Link einfügen',
        'Lizenz anlegen',
        'Löschen',
        'Min.',
        'Morgen',
        'Neu',
        'Neue Sitzung',
        'Niedrig',
        'Normal',
        'Notiz speichern',
        'Notiz: {notiz}',
        'Nummerierung',
        'Offen',
        'Person: {person}',
        'Ports',
        'Position [ID {id}] wird als erhalten abgehakt.',
        'Position „{name}" der Bestellung #{nummer} wird als erhalten abgehakt.',
        'Professioneller',
        'Prüfe alle…',
        'Prüfe…',
        'Reservieren',
        'Reservierung #{nummer} aktivieren und Unterschrift einholen.',
        'Review',
        'Review abschließen',
        'Riesig',
        'Rolle gelöscht',
        'Rolle „{name}“ wirklich löschen?',
        'Schließen',
        'Schriftgröße',
        'Sehr groß',
        'Sehr klein',
        'Seite #{nummer} – Inhalt {modus}',
        'Seite aktualisiert',
        'Seite angelegt',
        'Seite anlegen',
        'Seite öffnen →',
        'Sonstiges',
        'Status ändern',
        'Std.',
        'Suche Daten',
        'Suche öffnen',
        'Tabelle einfügen',
        'Tagen',
        'Tastaturkürzel',
        'Termin anlegen',
        'Ticket #{nummer}',
        'Ticket #{nummer} → {person}',
        'Ticket aktualisieren',
        'Ticket anlegen',
        'Ticket konnte nicht erstellt werden.',
        'Ticket zuweisen',
        'To-Do anlegen',
        'To-Do erledigen',
        'Unterschrift erhalten',
        'Unterstrichen (Ctrl+U)',
        'Verlängerung',
        'Vertrag anlegen',
        'VM #{nummer} → Status: „{status}"',
        'Voucher-Etikett drucken',
        'Warnung',
        'Warte auf Unterschrift…',
        'Wartend',
        'Wartung',
        'Wartungstermin anlegen',
        'Widget verschieben – Pfeiltasten zum Umordnen, Pfeil links/rechts für Spalte',
        'z. B. Büro 1.02',
        'z. B. Viele Grüße / Vorname Nachname',
        'Zeitraum: {von} – {bis}',
        'Zitat',
        'Zurückgeben',
        'Zuweisen',
        'Zweck: {zweck}',
        'Ändern',
        'Öffnen →',
        'Überfällig',
        'Übergabe bestätigen',
        'Überschrift 2',
        'Überschrift 3',
        'Überschrift 4',
        '— auswählen —',
        '„{titel}" zu deiner To-Do-Liste hinzufügen.',
        '⏹ Stoppen',
        '▶ Starten',
        '⚠️ <strong>Achtung:</strong> Der gesamte bisherige Seiteninhalt wird unwiderruflich überschrieben!',
        '✓ Kopiert',
    ]) ?>;
    /* Übersetzt einen Text aus der Karte; `werte` füllt die {name}-Stellen darin. */
    window.onitText = function (text, werte) {
        var ergebnis = (window.ONIT_I18N || {})[text] || text;
        if (werte) {
            Object.keys(werte).forEach(function (name) {
                ergebnis = ergebnis.split('{' + name + '}').join(werte[name]);
            });
        }
        return ergebnis;
    };
    </script>
</head>
<body class="layout-fixed sidebar-mini-md<?= $_material ? ' design-material' : '' ?>">
<a class="skip-link" href="#main-content"><?= $this->te('Zum Inhalt springen') ?></a>
<div class="wrapper">
<?php
$_useFA   = ($icon_style ?? 'fontawesome') === 'fontawesome';
$_iconMap = \OpenNIT\Core\IconMap::MAP;
$icon = function (string $fa, string $emoji = '') use ($_useFA, $_iconMap): string {
    if ($_useFA) {
        return '<i class="fa-solid ' . $fa . '"></i>';
    }
    if ($emoji !== '') {
        return $emoji;
    }
    // Nicht gemappt → Font-Awesome-Icon beibehalten (kein „•"-Platzhalter).
    return $_iconMap[$fa] ?? ('<i class="fa-solid ' . $fa . '"></i>');
};
?>

    <!-- ===== NAVBAR ===== -->
    <nav class="main-header navbar" aria-label="<?= $this->te('Kopfleiste') ?>">
        <ul class="navbar-nav">
            <!-- Sidebar Toggle (alle Screens) -->
            <li class="nav-item">
                <a class="nav-link sidebar-toggle-btn" id="sidebarToggle" href="#" role="button" aria-label="<?= $this->te('Menü öffnen') ?>">
                    <span class="burger-icon"><span></span><span></span><span></span></span>
                </a>
            </li>
            <!-- Brand / Logo -->
            <li class="nav-item navbar-brand-item">
                <a href="/" class="navbar-brand-link">
                    <?php $navbarLogo = !empty($app_logo_on_dark) ? $app_logo_on_dark : ($app_logo ?? ''); ?>
                    <?php if (!empty($navbarLogo)): ?>
                        <img src="<?= $this->e($navbarLogo) ?>" alt="<?= $this->e($app_name ?? 'OpenNIT') ?>" class="navbar-brand-img">
                    <?php else: ?>
                        <img src="/assets/img/logo-default.png" alt="OpenNIT" class="navbar-brand-img">
                    <?php endif; ?>
                    <span class="navbar-brand-text"><?= $this->e($app_name ?? 'OpenNIT') ?></span>
                </a>
            </li>
        </ul>

        <ul class="navbar-nav ml-auto">
            <!-- Favoriten-Schnellzugriff (konfigurierbare Anzahl gepinnter Services, nur Desktop) -->
            <?php if (!empty($navbar_pinned_services)): ?>
            <li class="nav-item navbar-fav-item">
                <div class="navbar-fav-wrap">
                    <?php foreach ($navbar_pinned_services as $__fav): ?>
                    <a href="<?= $this->e($__fav['url']) ?>" target="_blank" rel="noopener"
                       class="navbar-fav-link" title="<?= $this->e($__fav['name']) ?>" aria-label="<?= $this->e($__fav['name']) ?>">
                        <?php if (!empty($__fav['icon'])): ?>
                            <img src="/uploads/service-icons/<?= $this->e($__fav['icon']) ?>" alt="" class="navbar-fav-icon">
                        <?php else: ?>
                            <span class="navbar-fav-letter"<?php if (!empty($__fav['category_color'])): ?> style="background:<?= $this->e($__fav['category_color']) ?>"<?php endif; ?>><?= strtoupper(mb_substr($__fav['name'], 0, 1)) ?></span>
                        <?php endif; ?>
                    </a>
                    <?php endforeach; ?>
                </div>
            </li>
            <?php endif; ?>

            <!-- Kontakt-Schnellsuche (nur Desktop, nur wenn mind. ein Modul aktiv) -->
            <?php
            $navbarHasPhonebook = ($modules['phonebook'] ?? true) && $this->hasCapability('view_phonebook');
            $navbarHasContacts  = ($modules['contacts']  ?? true) && $this->hasCapability('view_contacts');
            if ($navbarHasPhonebook || $navbarHasContacts):
            ?>
            <li class="nav-item navbar-phonebook-item">
                <div class="navbar-phonebook-wrap">
                    <span class="navbar-phonebook-icon"><?= $icon('fa-phone', '📞') ?></span>
                    <input type="text" id="navbarPbInput" class="navbar-phonebook-input"
                           placeholder="<?= $this->te('Kontakt suchen …') ?>" autocomplete="off" aria-label="<?= $this->te('Kontakt-Schnellsuche') ?>">
                </div>
            </li>
            <?php endif; ?>

            <!-- Suche -->
            <li class="nav-item">
                <form action="/search" method="GET" class="navbar-search-form">
                    <div class="navbar-search-wrap">
                        <span class="navbar-search-icon"><?= $icon('fa-search', '🔍') ?></span>
                        <input type="search" name="q" id="globalSearchInput" placeholder="<?= $this->te('Suchen …') ?>"
                               value="<?= $this->e($_GET['q'] ?? '') ?>" autocomplete="off" class="navbar-search-input">
                    </div>
                </form>
            </li>

            <!-- Benachrichtigungen -->
            <li class="nav-item notif-bell" id="notifBell">
                <button class="nav-link notif-bell-btn" id="notifBellBtn" aria-label="<?= $this->te('Benachrichtigungen') ?>" aria-expanded="false">
                    <span class="notif-bell-icon"><?= $icon('fa-bell', '🔔') ?></span>
                    <span class="notif-badge" id="notifBadge" style="display:none;">0</span>
                </button>
                <div class="notif-dropdown" id="notifDropdown">
                    <div class="notif-dropdown-header">
                        <strong><?= $this->te('Benachrichtigungen') ?></strong>
                        <div class="notif-header-actions">
                            <button type="button" class="notif-push-toggle" id="notifPushToggle" title="<?= $this->te('Push-Benachrichtigungen') ?>" style="display:none;"></button>
                            <button type="button" class="notif-mark-all" id="notifMarkAll" title="<?= $this->te('Alle als gelesen markieren') ?>"><?= $icon('fa-check', '✓') ?> <?= $this->te('Gelesen') ?></button>
                            <button type="button" class="notif-delete-read" id="notifDeleteRead" title="<?= $this->te('Gelesene löschen') ?>"><?= $icon('fa-trash', '🗑') ?> <?= $this->te('Löschen') ?></button>
                        </div>
                    </div>
                    <div class="notif-dropdown-body" id="notifList">
                        <div class="notif-empty"><?= $this->te('Keine Benachrichtigungen') ?></div>
                    </div>
                </div>
            </li>

            <!-- Globale Stoppuhr-Pill (sichtbar wenn Timer läuft) -->
            <?php if (!empty($_global_running_timer)): ?>
            <li class="nav-item" style="display:flex;align-items:center;padding:0 0.5rem;">
                <?php include __DIR__ . '/../_timer_pill.php'; ?>
            </li>
            <?php endif; ?>

            <!-- User Menu -->
            <li class="nav-item user-menu">
                <button class="nav-link user-menu-trigger" aria-expanded="false">
                    <?php if (!empty($current_user->avatar_image_path)): ?>
                        <img src="<?= $this->e($current_user->avatar_image_path) ?>"
                             alt="<?= $this->e($current_user->display_name ?? '') ?>"
                             class="user-avatar user-avatar-photo">
                    <?php else: ?>
                    <span class="user-avatar">
                        <?= strtoupper(substr($current_user->display_name ?? 'U', 0, 1)) ?>
                    </span>
                    <?php endif; ?>
                    <span class="user-name d-none d-md-inline"><?= $this->e($current_user->display_name ?? $this->t('Benutzer', [], 'anzeigename')) ?></span>
                    <span class="user-caret"><?= $icon('fa-chevron-down', '▾') ?></span>
                </button>
                <div class="user-menu-dropdown">
                    <div class="user-menu-header">
                        <strong><?= $this->e($current_user->display_name ?? '') ?></strong>
                        <small><?= $this->e($current_user->email ?? '') ?></small>
                    </div>
                    <hr>
                    <a href="/my-responsibilities" class="dropdown-item"><?= $icon('fa-pen', '✏️') ?> <?= $this->te('Meine Verantwortlichkeiten') ?></a>
                    <?php if ($modules['tickets'] ?? true): ?>
                    <a href="/profile/signature" class="dropdown-item" id="sigPrefsTrigger"><?= $icon('fa-file-signature', '✍️') ?> <?= $this->te('Ticket-Signatur') ?></a>
                    <a href="/profile/notifications" class="dropdown-item" id="notifyPrefsTrigger"><?= $icon('fa-bell', '🔔') ?> <?= $this->te('Meine Benachrichtigungen') ?></a>
                    <?php endif; ?>
                    <?php if (!empty($current_user->is_local_account)): ?>
                    <a href="#" class="dropdown-item" id="pwChangeTrigger"><?= $icon('fa-key', '🔑') ?> <?= $this->te('Eigenes Passwort ändern') ?></a>
                    <a href="/account/mfa" class="dropdown-item"><?= $icon('fa-lock', '🔒') ?> <?= $this->te('Zwei-Faktor-Authentifizierung') ?></a>
                    <?php endif; ?>
                    <a href="#" class="dropdown-item" id="navSortTrigger"><?= $icon('fa-bars', '☰') ?> <?= $this->te('Navigation anpassen') ?> &hellip;</a>
                    <a href="#" class="dropdown-item" id="langTrigger"><?= $icon('fa-language', '🌐') ?> <?= $this->te('Sprache') ?> &hellip;</a>
                    <a href="/support/feedback" class="dropdown-item" id="vendorFeedbackTrigger"><?= $icon('fa-comment-dots', '💬') ?> <?= $this->te('Rückmeldung an den Hersteller') ?></a>
                    <?php if (!empty($portal_available)): ?>
                    <a href="/portal" class="dropdown-item"><?= $icon('fa-concierge-bell', '🛎') ?> <?= $this->te('Self-Service-Portal') ?></a>
                    <?php endif; ?>
                    <?php if ($this->canAccessAdminArea()): ?>
                        <hr>
                        <a href="/admin" class="dropdown-item"><?= $icon('fa-cog', '⚙️') ?> <?= $this->te('Administration') ?></a>
                        <?php if ($this->hasCapability('manage_users')): ?>
                        <a href="/admin/users" class="dropdown-item"><?= $icon('fa-user', '👤') ?> <?= $this->te('Benutzerverwaltung') ?></a>
                        <?php endif; ?>
                        <?php if ($this->hasCapability('view_audit_log')): ?>
                        <a href="/admin/audit" class="dropdown-item"><?= $icon('fa-file-contract', '📄') ?> <?= $this->te('Audit-Log') ?></a>
                        <?php endif; ?>
                    <?php endif; ?>
                    <hr>
                    <form action="/auth/logout" method="POST" class="logout-form">
                        <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                        <button type="submit" class="dropdown-item logout-btn"><?= $icon('fa-arrow-right-from-bracket', '🚪') ?> <?= $this->te('Abmelden') ?></button>
                    </form>
                </div>
            </li>
        </ul>
    </nav>

    <!-- ===== SIDEBAR ===== -->
    <?php
    // Gruppen-Definition
    $navGroupDefs = [
        'business' => ['label' => 'Betrieb',            'icon' => 'fa-building'],
        'infra'    => ['label' => 'Infrastruktur',       'icon' => 'fa-server'],
        'tools'    => ['label' => 'Produktivit&auml;t',  'icon' => 'fa-wrench'],
    ];
    $allNavItems = [
        'topics'    => ['icon' => 'fa-book', 'label' => 'Doku',             'href' => '/topics',    'route' => 'topics',     'topic_gate' => true],
        'orders'    => ['icon' => 'fa-shopping-cart', 'label' => 'Bestellungen',     'href' => '/orders',    'route' => 'orders',     'module' => 'orders',     'cap' => 'view_orders',       'group' => 'business'],
        'services'  => ['icon' => 'fa-cog',   'label' => 'IT-Services',      'href' => '/services',  'route' => 'services',   'module' => 'services',   'cap' => 'view_services',     'group' => 'business'],
        'contacts'  => ['icon' => 'fa-address-book', 'label' => 'Adressbuch',       'href' => '/contacts',  'route' => 'contacts',   'module' => 'contacts',   'cap' => 'view_contacts',     'group' => 'business'],
        'contracts' => ['icon' => 'fa-file-contract', 'label' => 'Vertr&auml;ge',    'href' => '/contracts', 'route' => 'contracts',  'module' => 'contracts',  'cap' => 'view_contracts',    'group' => 'business'],
        'costs'     => ['icon' => 'fa-coins', 'label' => 'Kosten&uuml;bersicht', 'href' => '/costs', 'route' => 'costs', 'module' => 'contracts', 'cap' => 'view_contracts', 'group' => 'business'],
        'loans'     => ['icon' => 'fa-laptop', 'label' => 'Leihger&auml;te',  'href' => '/loans',     'route' => 'loans',      'module' => 'loans',      'cap' => 'view_loan_devices', 'group' => 'business'],
        'tickets'   => ['icon' => 'fa-ticket-alt', 'label' => 'Tickets',          'href' => '/tickets',   'route' => 'tickets',    'module' => 'tickets',    'cap' => 'view_tickets',      'group' => 'business'],
        'ipam'      => ['icon' => 'fa-globe', 'label' => 'IP-Verwaltung',    'href' => '/ipam',      'route' => 'ipam',       'module' => 'ipam',       'cap' => 'view_ipam',         'group' => 'infra'],
        'dns'       => ['icon' => 'fa-sitemap', 'label' => 'DNS-Zonen',      'href' => '/dns',       'route' => 'dns',        'module' => 'dns',        'cap' => 'view_dns',          'group' => 'infra'],
        'vms'       => ['icon' => 'fa-server', 'label' => 'Virtualisierung',  'href' => '/vms',       'route' => 'vms',        'module' => 'vms',        'cap' => 'view_vms',          'group' => 'infra'],
        'assets'    => ['icon' => 'fa-boxes-stacked', 'label' => 'Inventar',         'href' => '/inventory', 'route' => 'assets',     'module' => 'assets',     'cap' => 'view_assets',       'group' => 'infra'],
        'stock'     => ['icon' => 'fa-warehouse', 'label' => 'Lager',            'href' => '/stock',     'route' => 'stock',      'module' => 'stock',      'cap' => 'view_stock',        'group' => 'infra'],
        'monitoring'=> ['icon' => 'fa-chart-line', 'label' => 'Monitoring',       'href' => '/monitoring','route' => 'monitoring',  'module' => 'monitoring', 'cap' => 'view_monitoring',   'group' => 'infra'],
        'cve'       => ['icon' => 'fa-shield-alt', 'label' => 'CVE-Meldungen',    'href' => '/cve',       'route' => 'cve',         'module' => 'cve',        'cap' => 'view_cve',          'group' => 'infra'],
        'xdr'       => ['icon' => 'fa-shield-virus', 'label' => 'XDR-Warnungen', 'href' => '/xdr',     'route' => 'xdr',         'module' => 'xdr',        'cap' => 'view_xdr',          'group' => 'infra'],
        'isms'      => ['icon' => 'fa-user-shield', 'label' => 'Informationssicherheit', 'href' => '/isms', 'route' => 'isms', 'module' => 'isms', 'cap' => 'view_isms', 'group' => 'infra'],
        'statuspage'=> ['icon' => 'fa-traffic-light', 'label' => 'Statusseite', 'href' => '/status/manage', 'route' => 'statuspage', 'module' => 'statuspage', 'cap' => 'manage_statuspage', 'group' => 'infra'],
        'unifi'     => ['icon' => 'fa-network-wired', 'label' => 'UniFi-Integration', 'href' => '/unifi',     'route' => 'unifi',      'module' => 'unifi',      'cap' => 'view_unifi',        'group' => 'infra'],
        'backup'    => ['icon' => 'fa-hard-drive', 'label' => 'Backup',           'href' => '/backup',    'route' => 'backup',     'module' => 'backup',     'cap' => 'view_backup',       'group' => 'infra'],
        'ssl'       => ['icon' => 'fa-lock', 'label' => 'SSL-Checker',      'href' => '/ssl',       'route' => 'ssl',        'module' => 'ssl',        'cap' => 'view_ssl',          'group' => 'infra'],
        'vault'     => ['icon' => 'fa-vault', 'label' => 'Passw&ouml;rter',  'href' => '/vault',     'route' => 'vault',      'module' => 'vault',      'cap' => 'view_vault',        'group' => 'tools'],
        'files'     => ['icon' => 'fa-folder', 'label' => 'Datei-Explorer',   'href' => '/files',     'route' => 'files',      'module' => 'files',      'cap' => 'view_files',        'group' => 'tools'],
        'kanban'    => ['icon' => 'fa-rectangle-list', 'label' => 'Kanban',           'href' => '/kanban',    'route' => 'kanban',     'module' => 'kanban',     'cap' => 'view_kanban',       'group' => 'tools'],
        'todos'        => ['icon' => 'fa-check',   'label' => 'To-Do-Liste',      'href' => '/todos',       'route' => 'todos',       'module' => 'todos',       'cap' => 'view_todos',      'group' => 'tools'],
        'teamboard'    => ['icon' => 'fa-sticky-note', 'label' => 'Notizbrett',   'href' => '/teamboard',   'route' => 'teamboard',   'module' => 'teamboard',   'cap' => 'view_teamboard',  'group' => 'tools'],
        'workflows'    => ['icon' => 'fa-clipboard-check', 'label' => 'Antr&auml;ge', 'href' => '/antraege', 'route' => 'workflows', 'module' => 'workflows', 'cap' => 'view_workflows', 'group' => 'business'],
        // API-Auto ist ein reines Backend-Werkzeug und erscheint nur in der Admin-Navigation (/admin/api-auto).
        'maintenance'  => ['icon' => 'fa-calendar', 'label' => 'Wartungskalender', 'href' => '/maintenance', 'route' => 'maintenance', 'module' => 'maintenance', 'cap' => 'view_maintenance', 'group' => 'tools'],
        'deadlines'    => ['icon' => 'fa-hourglass-half', 'label' => 'Fristen-Cockpit', 'href' => '/deadlines', 'route' => 'deadlines', 'module' => 'deadlines', 'cap' => 'view_deadlines', 'group' => 'tools'],
        'netdiagram'   => ['icon' => 'fa-project-diagram', 'label' => 'Netzwerkpl&auml;ne', 'href' => '/netdiagram', 'route' => 'netdiagram', 'module' => 'netdiagram', 'cap' => 'view_netdiagram', 'group' => 'infra'],
        'infrastructure' => ['icon' => 'fa-server', 'label' => 'Infrastruktur', 'href' => '/infrastructure', 'route' => 'infrastructure', 'module' => 'infrastructure', 'cap' => 'view_infrastructure', 'group' => 'infra'],
        'configlog'    => ['icon' => 'fa-history', 'label' => '&Auml;nderungsprotokoll', 'href' => '/configlog', 'route' => 'configlog', 'module' => 'configlog', 'cap' => 'view_config_log', 'group' => 'infra'],
        'licenses'     => ['icon' => 'fa-certificate', 'label' => 'Lizenzen',              'href' => '/licenses',  'route' => 'licenses',  'module' => 'licenses',  'cap' => 'view_licenses',   'group' => 'business'],
        'phonebook'    => ['icon' => 'fa-phone', 'label' => 'Telefonbuch',           'href' => '/phonebook', 'route' => 'phonebook', 'module' => 'phonebook', 'cap' => 'view_phonebook',  'group' => 'tools'],
        'budget'       => ['icon' => 'fa-chart-bar', 'label' => 'Budget',             'href' => '/budget',    'route' => 'budget',    'module' => 'budget',    'cap' => 'view_budgets',    'group' => 'business'],
        'catalog'      => ['icon' => 'fa-tags', 'label' => 'Ausstattung &amp; Preise', 'href' => '/catalog',   'route' => 'catalog',   'module' => 'catalog',   'cap' => 'view_catalog',    'group' => 'business'],
        'software'     => ['icon' => 'fa-box-open', 'label' => 'Software-Inventar', 'href' => '/software', 'route' => 'software', 'module' => 'software', 'cap' => 'view_software', 'group' => 'infra'],
    ];

    /* Menü-Beschriftungen an einer Stelle übersetzen. Die Beschriftungen stehen
       als HTML in der Liste; der Schlüssel im Katalog ist der Klartext. Alle
       Darstellungsvarianten (frei gruppiert, thematisch gruppiert, flach) und der
       Dialog „Navigation anpassen" greifen auf dieselbe Liste zu. */
    $navUebersetzen = function (string $html): string {
        $klartext = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        return htmlspecialchars($this->t($klartext), ENT_QUOTES, 'UTF-8');
    };
    if (!$this->isSourceLanguage()) {
        foreach ($allNavItems as $navTk => $navTi) {
            $allNavItems[$navTk]['label'] = $navUebersetzen($navTi['label']);
        }
        foreach ($navGroupDefs as $navTk => $navTg) {
            $navGroupDefs[$navTk]['label'] = $navUebersetzen($navTg['label']);
        }
    }

    // Nur zugängliche Items
    $availableNavItems = [];
    foreach ($allNavItems as $key => $item) {
        if (isset($item['module']) && !($modules[$item['module']] ?? true)) continue;
        if (isset($item['cap']) && !$this->hasCapability($item['cap'])) continue;
        if (!empty($item['topic_gate']) && !$this->canAccessAnyTopic()) continue;
        $availableNavItems[$key] = $item;
    }
    // Benutzerdefinierte Reihenfolge
    $savedNavOrder = json_decode($nav_order ?? '[]', true) ?: [];
    if (!empty($savedNavOrder)) {
        $ordered = [];
        foreach ($savedNavOrder as $k) {
            if (isset($availableNavItems[$k])) $ordered[$k] = $availableNavItems[$k];
        }
        foreach ($availableNavItems as $k => $item) {
            if (!isset($ordered[$k])) $ordered[$k] = $item;
        }
        $availableNavItems = $ordered;
    }
    // Benutzerdefinierte Menü-Kategorien: eigene Gruppen ersetzen die
    // Standard-Gruppierung vollständig; das Dashboard bleibt fest oben.
    // Punkte, die keiner Gruppe zugeordnet sind (z. B. neu freigeschaltete
    // Module), erscheinen automatisch am Ende unter „Weitere".
    $navCustomGroups = null;
    $navLayoutData = json_decode($nav_layout ?? '', true);
    if (is_array($navLayoutData) && !empty($navLayoutData['groups']) && is_array($navLayoutData['groups'])) {
        $navCustomGroups = [];
        $navPlaced = [];
        foreach ($navLayoutData['groups'] as $navGrp) {
            if (!is_array($navGrp)) continue;
            $grpItems = [];
            foreach ((array)($navGrp['items'] ?? []) as $navKey) {
                if (isset($availableNavItems[$navKey]) && !isset($navPlaced[$navKey])) {
                    $grpItems[] = $navKey;
                    $navPlaced[$navKey] = true;
                }
            }
            $navCustomGroups[] = ['label' => trim((string)($navGrp['label'] ?? '')), 'items' => $grpItems];
        }
        $navRest = array_diff_key($availableNavItems, $navPlaced);
        if (!empty($navRest)) {
            $navCustomGroups[] = ['label' => 'Weitere', 'items' => array_keys($navRest)];
        }
    }

    $currentRoute = $current_route ?? '';

    // Label inkl. optionalem Badge (z. B. offene Workflow-Genehmigungen).
    // MUSS vor if/else stehen, da beide Navigations-Varianten (gruppiert UND flach)
    // $navLabel verwenden – sonst „null is not callable" im flachen Zweig.
    $wfBadge = (int)($workflow_inbox_count ?? 0);
    $navLabel = function ($key, $item) use ($wfBadge) {
        $html = $item['label'];
        if ($key === 'workflows' && $wfBadge > 0) {
            $html .= ' <span style="display:inline-block;min-width:1.1rem;padding:0 .35rem;border-radius:.6rem;background:var(--red-600);color:var(--color-on-accent);font-size:.7rem;line-height:1.1rem;text-align:center;margin-left:.35rem;">' . $wfBadge . '</span>';
        }
        return $html;
    };
    ?>

    <aside class="main-sidebar sidebar-dark-primary">
        <div class="sidebar">
            <nav class="mt-2" aria-label="<?= $this->te('Hauptmenü') ?>">
                <ul class="nav-sidebar nav-pills flex-column" role="menu">

                    <!-- Dashboard -->
                    <li class="nav-item">
                        <a href="/" class="nav-link <?= $currentRoute === 'dashboard' ? 'active' : '' ?>">
                            <span class="nav-icon"><?= $icon('fa-home', '🏠') ?></span>
                            <p><?= $this->te('Dashboard') ?></p>
                        </a>
                    </li>

                    <?php if ($navCustomGroups !== null): ?>
                        <?php foreach ($navCustomGroups as $navGrp): ?>
                            <?php if (empty($navGrp['items'])) continue; ?>
                            <?php if ($navGrp['label'] !== ''): ?>
                                <li class="nav-header"><?= $this->e($navGrp['label']) ?></li>
                            <?php endif; ?>
                            <?php foreach ($navGrp['items'] as $key): $item = $availableNavItems[$key]; ?>
                                <li class="nav-item">
                                    <a href="<?= $item['href'] ?>" class="nav-link <?= $currentRoute === $item['route'] ? 'active' : '' ?>">
                                        <span class="nav-icon"><?= $icon($item['icon']) ?></span>
                                        <p><?= $navLabel($key, $item) ?></p>
                                    </a>
                                </li>
                            <?php endforeach; ?>
                        <?php endforeach; ?>

                    <?php elseif (!empty($nav_grouped)): ?>
                        <?php
                        // Flache Items ohne Gruppe (z.B. Doku)
                        foreach ($availableNavItems as $key => $item) {
                            if (!empty($item['group'])) continue;
                        ?>
                            <li class="nav-item">
                                <a href="<?= $item['href'] ?>" class="nav-link <?= $currentRoute === $item['route'] ? 'active' : '' ?>">
                                    <span class="nav-icon"><?= $icon($item['icon']) ?></span>
                                    <p><?= $navLabel($key, $item) ?></p>
                                </a>
                            </li>
                        <?php } ?>

                        <?php foreach ($navGroupDefs as $groupKey => $groupDef):
                            $groupItems = array_filter($availableNavItems, fn($i) => ($i['group'] ?? '') === $groupKey);
                            if (empty($groupItems)) continue;
                            $groupActive = array_filter($groupItems, fn($i) => $currentRoute === $i['route']);
                        ?>
                            <li class="nav-header"><?= $groupDef['label'] ?></li>
                            <?php foreach ($groupItems as $key => $item): ?>
                                <li class="nav-item">
                                    <a href="<?= $item['href'] ?>" class="nav-link <?= $currentRoute === $item['route'] ? 'active' : '' ?>">
                                        <span class="nav-icon"><?= $icon($item['icon']) ?></span>
                                        <p><?= $navLabel($key, $item) ?></p>
                                    </a>
                                </li>
                            <?php endforeach; ?>
                        <?php endforeach; ?>

                    <?php else: ?>
                        <?php foreach ($availableNavItems as $key => $item): ?>
                            <li class="nav-item">
                                <a href="<?= $item['href'] ?>" class="nav-link <?= $currentRoute === $item['route'] ? 'active' : '' ?>">
                                    <span class="nav-icon"><?= $icon($item['icon']) ?></span>
                                    <p><?= $navLabel($key, $item) ?></p>
                                </a>
                            </li>
                        <?php endforeach; ?>
                    <?php endif; ?>

                </ul>
            </nav>
        </div>

        <!-- Sidebar Collapse Button (nur Desktop) -->
        <div class="sidebar-collapse-btn d-none d-lg-flex" id="sidebarCollapseBtn" title="<?= $this->te('Seitenleiste ein-/ausklappen') ?>">
            <span class="sidebar-collapse-icon">&#171;</span>
            <span class="sidebar-collapse-text"><?= $this->te('Einklappen') ?></span>
        </div>
    </aside>

    <!-- ===== CONTENT WRAPPER ===== -->
    <div class="content-wrapper">

        <?php if ($flash = $this->flash()): ?>
            <div class="toast-container" id="toastContainer">
                <?php foreach ($flash as $msg): ?>
                    <div class="toast toast-<?= $this->e($msg['type'] ?? 'info') ?>" role="alert">
                        <div class="toast-icon">
                            <?php
                            $_ti = match($msg['type'] ?? 'info') {
                                'success' => $icon('fa-check', '✓'),
                                'error'   => $icon('fa-times', '✕'),
                                'warning' => $icon('fa-exclamation-triangle', '⚠️'),
                                default   => $icon('fa-info-circle', 'ℹ️')
                            };
                            echo $_ti;
                            ?>
                        </div>
                        <div class="toast-content">
                            <div class="toast-message"><?= $this->e($msg['message'] ?? '') ?></div>
                        </div>
                        <button type="button" class="toast-close" aria-label="<?= $this->te('Schließen') ?>">&times;</button>
                    </div>
                <?php endforeach; ?>
            </div>
        <?php endif; ?>

        <main id="main-content" tabindex="-1" class="main-content <?= !empty($full_width) ? 'main-content-fullwidth' : '' ?>">
            <?php if (isset($breadcrumbs) && !empty($breadcrumbs)): ?>
                <nav class="breadcrumb" aria-label="<?= $this->te('Sie sind hier') ?>">
                    <ol>
                        <li><a href="/"><?= $this->te('Start') ?></a></li>
                        <?php foreach ($breadcrumbs as $crumb): ?>
                            <?php if (isset($crumb['url'])): ?>
                                <li><a href="<?= $this->e($crumb['url']) ?>"><?= $this->e($crumb['label']) ?></a></li>
                            <?php else: ?>
                                <li aria-current="page"><?= $this->e($crumb['label']) ?></li>
                            <?php endif; ?>
                        <?php endforeach; ?>
                    </ol>
                </nav>
            <?php endif; ?>

            <?= $content ?>
        </main>

        <footer class="main-footer">
            <div class="footer-content">
                <span class="footer-copyright">
                    <?php if (!empty($app_footer_text)): ?>
                        <?= $this->e($app_footer_text) ?>
                    <?php else: ?>
                        &copy; <?= date('Y') ?> OpenNIT
                    <?php endif; ?>
                </span>
                <span class="footer-links">
                    <span class="footer-version"><?= $this->te('Version') ?> <?= $this->version() ?></span>
                    <span class="footer-sep" aria-hidden="true">|</span>
                    <button type="button" class="footer-link"
                            data-dialog-show="aboutModal"
                            aria-haspopup="dialog">
                        <?= $this->te('Über') ?>
                    </button>
                    <span class="footer-sep" aria-hidden="true">|</span>
                    <button type="button" class="footer-link"
                            data-act-oss
                            aria-haspopup="dialog">
                        <?= $this->te('Open-Source-Lizenzen') ?>
                    </button>
                </span>
            </div>
        </footer>
    </div>

    <!-- Sidebar Overlay (mobile) -->
    <div class="sidebar-overlay" id="sidebarOverlay"></div>

</div><!-- /.wrapper -->

<!-- ===== MODALS & SCRIPTS ===== -->

<?php
/* Sprachwahl: persönliche Einstellung. Der leere Wert folgt der Vorgabe der
   Instanz – so wirkt eine spätere Änderung der Vorgabe auch für diesen Benutzer. */
$_langList     = $languages ?? \OpenNIT\Core\Translator::SPRACHEN;
$_langInstance = $instance_language ?? \OpenNIT\Core\Translator::QUELLSPRACHE;
$_langChosen   = $user_language ?? '';
?>
<div class="modal-overlay" id="langModal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="langTitle">
    <div class="modal-card" style="max-width:420px;">
        <div class="modal-header">
            <h2 id="langTitle"><?= $icon('fa-language', '🌐') ?> <?= $this->te('Sprache') ?></h2>
            <button type="button" class="modal-close" data-modal-close aria-label="<?= $this->te('Schließen') ?>">&times;</button>
        </div>
        <div class="modal-body">
            <div class="form-group">
                <label for="langSelect"><?= $this->te('Sprache der Oberfläche') ?></label>
                <select id="langSelect" class="form-control">
                    <option value=""<?= $_langChosen === '' ? ' selected' : '' ?>>
                        <?= $this->te('Vorgabe der Instanz ({sprache})', ['sprache' => $_langList[$_langInstance] ?? $_langInstance]) ?>
                    </option>
                    <?php foreach ($_langList as $_lc => $_ln): ?>
                    <option value="<?= $this->e($_lc) ?>"<?= $_langChosen === $_lc ? ' selected' : '' ?>><?= $this->e($_ln) ?></option>
                    <?php endforeach; ?>
                </select>
                <small class="form-text text-muted"><?= $this->te('Gilt nur für Ihr Konto. Die Seite wird nach dem Speichern neu geladen.') ?></small>
            </div>
            <p class="text-muted" style="font-size:0.8rem;margin-bottom:0;">
                <?= $this->te('Noch nicht übersetzte Texte erscheinen auf Deutsch.') ?>
            </p>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-default" data-modal-close><?= $this->te('Abbrechen') ?></button>
            <button type="button" class="btn btn-primary" id="langSave"><?= $this->te('Speichern') ?></button>
        </div>
    </div>
</div>

<script<?= $this->scriptNonce() ?>>
/**
 * Sprachwahl aus dem Benutzermenue: speichert die Einstellung und laedt die
 * Seite neu, damit die Oberflaeche sofort in der gewaehlten Sprache steht.
 */
(function () {
    var ausloeser = document.getElementById('langTrigger');
    var dialog    = document.getElementById('langModal');
    var auswahl   = document.getElementById('langSelect');
    var speichern = document.getElementById('langSave');
    if (!ausloeser || !dialog || !auswahl || !speichern) { return; }

    function schliessen() { dialog.style.display = 'none'; }

    ausloeser.addEventListener('click', function (e) {
        e.preventDefault();
        dialog.style.display = 'flex';
        auswahl.focus();
    });
    dialog.querySelectorAll('[data-modal-close]').forEach(function (b) {
        b.addEventListener('click', schliessen);
    });
    dialog.addEventListener('click', function (e) { if (e.target === dialog) { schliessen(); } });
    document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape' && dialog.style.display !== 'none') { schliessen(); }
    });

    speichern.addEventListener('click', function () {
        var meta = document.querySelector('meta[name="csrf-token"]');
        var daten = new FormData();
        daten.append('key', 'language');
        daten.append('value', auswahl.value);
        daten.append('_csrf', meta ? meta.content : '');
        speichern.disabled = true;
        fetch('/api/user-settings', { method: 'POST', body: daten })
            .then(function (r) { return r.json(); })
            .then(function (a) {
                if (a && a.ok) { window.location.reload(); return; }
                speichern.disabled = false;
            })
            .catch(function () { speichern.disabled = false; });
    });
})();
</script>

<?php if ($modules['tickets'] ?? true): ?>
<!-- Eigene Benachrichtigungen: Inhalt wird beim Öffnen geholt. -->
<div class="modal-overlay" id="notifyPrefsModal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="notifyPrefsTitle">
    <div class="modal-card" style="max-width:560px;">
        <div class="modal-header">
            <h2 id="notifyPrefsTitle"><?= $icon('fa-bell', '🔔') ?> <?= $this->te('Meine Benachrichtigungen') ?></h2>
            <button type="button" class="modal-close" data-modal-close aria-label="<?= $this->te('Schließen') ?>">&times;</button>
        </div>
        <div class="modal-body" id="notifyPrefsBody">
            <p class="text-muted"><?= $this->te('Wird geladen …') ?></p>
        </div>
    </div>
</div>
<?php endif; ?>

<?php if ($modules['tickets'] ?? true): ?>
<!-- Ticket-Signatur: Inhalt wird beim Öffnen geholt. -->
<div class="modal-overlay" id="sigPrefsModal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="sigPrefsTitle">
    <div class="modal-card" style="max-width:720px;">
        <div class="modal-header">
            <h2 id="sigPrefsTitle"><?= $icon('fa-file-signature', '✍️') ?> <?= $this->te('Ticket-Signatur') ?></h2>
            <button type="button" class="modal-close" data-modal-close aria-label="<?= $this->te('Schließen') ?>">&times;</button>
        </div>
        <div class="modal-body" id="sigPrefsBody">
            <p class="text-muted"><?= $this->te('Wird geladen …') ?></p>
        </div>
    </div>
</div>
<?php endif; ?>

<?php if ($modules['tickets'] ?? true): ?>
<script<?= $this->scriptNonce() ?>>
/**
 * Oeffnet einen Dialog aus dem Benutzermenue und holt sein Formular erst dabei -
 * so kostet er auf jeder anderen Seite nichts.
 *
 * `nachher` laeuft, sobald das Markup steht; der Signatur-Dialog setzt dort
 * seinen Editor auf.
 */
(function () {
    function dialogAusMenue(ausloeserId, dialogId, koerperId, quelle, nachher) {
        var ausloeser = document.getElementById(ausloeserId);
        var dialog    = document.getElementById(dialogId);
        var koerper   = document.getElementById(koerperId);
        if (!ausloeser || !dialog || !koerper) { return; }

        function schliessen() { dialog.style.display = 'none'; }

        ausloeser.addEventListener('click', function (e) {
            e.preventDefault();
            dialog.style.display = 'flex';
            koerper.innerHTML = '<p class="text-muted">Wird geladen …</p>';

            var ziel = quelle + (quelle.indexOf('?') === -1 ? '?' : '&')
                     + 'fragment=1&return=' + encodeURIComponent(location.pathname + location.search);

            fetch(ziel, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
                .then(function (r) { return r.ok ? r.text() : Promise.reject(r.status); })
                .then(function (html) {
                    koerper.innerHTML = html;
                    koerper.querySelectorAll('[data-modal-close]').forEach(function (b) {
                        b.addEventListener('click', schliessen);
                    });
                    if (typeof nachher === 'function') { nachher(koerper); }
                })
                .catch(function () {
                    koerper.innerHTML = '<p class="text-danger">Das liess sich nicht laden. '
                        + '<a href="' + quelle + '">Als Seite oeffnen</a></p>';
                });
        });

        dialog.querySelectorAll('[data-modal-close]').forEach(function (b) {
            b.addEventListener('click', schliessen);
        });
        dialog.addEventListener('click', function (e) { if (e.target === dialog) { schliessen(); } });
        document.addEventListener('keydown', function (e) {
            if (e.key === 'Escape' && dialog.style.display !== 'none') { schliessen(); }
        });
    }

    dialogAusMenue('notifyPrefsTrigger', 'notifyPrefsModal', 'notifyPrefsBody', '/profile/notifications');

    // Der Signatur-Editor braucht Skript und Stil; beide kommen erst beim
    // ersten Oeffnen dazu, statt auf jeder Seite mitzulaufen.
    dialogAusMenue('sigPrefsTrigger', 'sigPrefsModal', 'sigPrefsBody', '/profile/signature', function (koerper) {
        nachladen('/assets/css/wysiwyg.css', '/assets/js/wysiwyg.js', '/assets/js/profile-signature.js', function () {
            if (window.OpenNIT && window.OpenNIT.initSignatureEditor) {
                window.OpenNIT.initSignatureEditor(koerper);
            }
        });
    });

    /** Laedt Stil und Skripte einmalig nach und ruft danach `fertig`. */
    function nachladen(css, skriptA, skriptB, fertig) {
        if (!document.querySelector('link[href^="' + css + '"]')) {
            var l = document.createElement('link');
            l.rel = 'stylesheet'; l.href = css;
            document.head.appendChild(l);
        }
        reihum([skriptA, skriptB], fertig);
    }

    /** Laedt Skripte nacheinander - die Reihenfolge zaehlt. */
    function reihum(pfade, fertig) {
        if (pfade.length === 0) { fertig(); return; }
        var pfad = pfade[0], rest = pfade.slice(1);
        if (document.querySelector('script[src^="' + pfad + '"]')) { reihum(rest, fertig); return; }
        var s = document.createElement('script');
        s.src = pfad;
        s.onload = function () { reihum(rest, fertig); };
        s.onerror = function () { fertig(); };
        document.body.appendChild(s);
    }
})();
</script>
<?php endif; ?>

<?php if (!empty($current_user->is_local_account)): ?>
<!-- Passwort-ändern-Modal (nur lokale Konten) -->
<div class="modal-overlay" id="pwChangeModal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="pwChangeTitle">
    <div class="modal-card" style="max-width:480px;">
        <div class="modal-header">
            <h2 id="pwChangeTitle"><?= $icon('fa-key', '🔑') ?> <?= $this->te('Eigenes Passwort ändern') ?></h2>
            <button type="button" class="modal-close" id="pwChangeClose" aria-label="<?= $this->te('Schließen') ?>">&times;</button>
        </div>
        <form method="post" action="/account/password" autocomplete="off">
            <div class="modal-body">
                <p class="text-muted" style="margin-top:0;">
                    <?= $this->te('Zur Sicherheit ist die Eingabe Ihres aktuellen Passworts erforderlich.') ?>
                </p>
                <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                <input type="hidden" name="return" id="pwChangeReturn" value="<?= $this->e($_SERVER['REQUEST_URI'] ?? '/dashboard') ?>">
                <div class="form-group">
                    <label for="pwCurrent"><?= $this->te('Aktuelles Passwort') ?> <span class="text-danger">*</span></label>
                    <input type="password" name="current_password" id="pwCurrent"
                           class="form-control form-control-sm" required autocomplete="current-password">
                </div>
                <div class="form-group">
                    <label for="pwNew"><?= $this->te('Neues Passwort') ?> <span class="text-danger">*</span></label>
                    <input type="password" name="new_password" id="pwNew"
                           class="form-control form-control-sm" required minlength="10" autocomplete="new-password">
                    <small class="text-muted"><?= $this->te('Mindestens 10 Zeichen; ggf. gelten weitere Vorgaben Ihrer Passwort-Richtlinie.') ?></small>
                </div>
                <div class="form-group">
                    <label for="pwConfirm"><?= $this->te('Neues Passwort bestätigen') ?> <span class="text-danger">*</span></label>
                    <input type="password" name="new_password_confirm" id="pwConfirm"
                           class="form-control form-control-sm" required minlength="10" autocomplete="new-password">
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" id="pwChangeCancel"><?= $this->te('Abbrechen') ?></button>
                <button type="submit" class="btn btn-primary"><?= $icon('fa-check', '✓') ?> <?= $this->te('Passwort ändern') ?></button>
            </div>
        </form>
    </div>
</div>
<script<?= $this->scriptNonce() ?>>
(function () {
    var modal = document.getElementById('pwChangeModal');
    if (!modal) return;
    var trigger = document.getElementById('pwChangeTrigger');
    function open(e) {
        if (e) e.preventDefault();
        var ret = document.getElementById('pwChangeReturn');
        if (ret) ret.value = window.location.pathname + window.location.search;
        modal.style.display = 'flex';
        var f = document.getElementById('pwCurrent');
        if (f) f.focus();
    }
    function close() {
        modal.style.display = 'none';
        modal.querySelectorAll('input[type=password]').forEach(function (i) { i.value = ''; });
    }
    if (trigger) trigger.addEventListener('click', open);
    document.getElementById('pwChangeClose').addEventListener('click', close);
    document.getElementById('pwChangeCancel').addEventListener('click', close);
    modal.addEventListener('click', function (e) { if (e.target === modal) close(); });
    document.addEventListener('keydown', function (e) {
        if (e.key === 'Escape' && modal.style.display === 'flex') close();
    });
    // Nach einem zurückgesetzten Passwort direkt zum Ändern auffordern
    try {
        if (new URLSearchParams(window.location.search).get('pw') === 'change') open();
    } catch (e) { /* ältere Browser ohne URLSearchParams */ }
})();
</script>
<?php endif; ?>

<!-- Rückmeldung an den Hersteller (Modal) -->
<?php include __DIR__ . '/../partials/_vendor_feedback_modal.php'; ?>

<!-- Nav-Sort Modal -->
<div id="navSortModal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="navSortTitle">
    <div id="navSortBackdrop" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:1050;backdrop-filter:blur(2px)"></div>
    <div style="position:fixed;inset:0;z-index:1051;display:flex;align-items:flex-start;justify-content:center;padding:1rem;overflow-y:auto">
        <div style="background:var(--color-bg);border-radius:0.5rem;box-shadow:0 20px 60px rgba(0,0,0,0.3);width:100%;max-width:420px;overflow:hidden;margin:auto">
            <div style="display:flex;align-items:center;justify-content:space-between;padding:1rem 1.25rem;border-bottom:1px solid var(--color-gray-300);background:var(--color-gray-800)">
                <h3 id="navSortTitle" style="margin:0;font-size:1rem;font-weight:600;color:var(--color-on-accent)"><?= $this->te('Navigation anpassen') ?></h3>
                <button id="navSortClose" type="button" class="modal-close" aria-label="<?= $this->te('Schließen') ?>">&times;</button>
            </div>
            <div style="padding:1rem 1.25rem">
                <label style="display:flex;align-items:center;gap:0.75rem;padding:0.625rem 0.875rem;background:var(--color-gray-50);border:1px solid var(--color-gray-300);border-radius:0.375rem;cursor:pointer;margin-bottom:0.5rem">
                    <input type="checkbox" id="navGroupToggle" style="width:1rem;height:1rem;accent-color:var(--color-primary);flex-shrink:0" <?= !empty($nav_grouped) ? 'checked' : '' ?>>
                    <span>
                        <strong style="font-size:0.875rem;color:var(--color-gray-900);display:block"><?= $this->te('Gruppierte Navigation') ?></strong>
                        <span style="font-size:0.75rem;color:var(--color-secondary)"><?= $this->te('Betrieb, Infrastruktur und Produktivit&auml;t als Bereiche zusammenfassen') ?></span>
                    </span>
                </label>
                <label style="display:flex;align-items:center;gap:0.75rem;padding:0.625rem 0.875rem;background:var(--color-gray-50);border:1px solid var(--color-gray-300);border-radius:0.375rem;cursor:pointer;margin-bottom:1rem">
                    <input type="checkbox" id="navCustomToggle" style="width:1rem;height:1rem;accent-color:var(--color-primary);flex-shrink:0" <?= $navCustomGroups !== null ? 'checked' : '' ?>>
                    <span>
                        <strong style="font-size:0.875rem;color:var(--color-gray-900);display:block"><?= $this->te('Eigene Kategorien') ?></strong>
                        <span style="font-size:0.75rem;color:var(--color-secondary)"><?= $this->te('Kategorien selbst benennen und alle Men&uuml;punkte frei einsortieren &ndash; nur das Dashboard bleibt oben') ?></span>
                    </span>
                </label>
                <p id="navSortHint" style="margin:0 0 1rem;font-size:0.8125rem;color:var(--color-secondary)">
                    <?= $this->te('Verschiebe die Eintr&auml;ge per Drag &amp; Drop in deine gew&uuml;nschte Reihenfolge.') ?>
                </p>
                <!-- Editor für eigene Kategorien (wird per JS befüllt) -->
                <div id="navCatEditor" style="display:none">
                    <div id="navCatList" style="display:flex;flex-direction:column;gap:0.625rem;max-height:50vh;overflow-y:auto;padding-right:0.25rem"></div>
                    <button type="button" id="navCatAdd" style="margin-top:0.625rem;width:100%;padding:0.45rem;border:1px dashed var(--color-gray-400);border-radius:0.375rem;background:none;color:var(--color-secondary);font-size:0.8125rem;cursor:pointer"><?= $this->te('+ Kategorie hinzuf&uuml;gen') ?></button>
                </div>
                <div id="navFlatWrap" style="background:var(--color-gray-50);border:1px solid var(--color-gray-300);border-radius:0.375rem;padding:0.5rem;max-height:50vh;overflow-y:auto">
                    <div style="display:flex;align-items:center;gap:0.625rem;padding:0.5rem 0.625rem;border-radius:0.25rem;background:var(--color-gray-200);color:var(--color-secondary);margin-bottom:0.25rem">
                        <span style="font-size:1rem;opacity:0.4"><?= $icon('fa-grip-lines', '☰') ?></span>
                        <span style="font-size:0.875rem;font-weight:500"><?= $this->te('Dashboard') ?></span>
                        <span style="margin-left:auto;font-size:0.75rem;background:var(--color-gray-300);color:var(--color-secondary);padding:0.125rem 0.5rem;border-radius:999px"><?= $this->te('Fixiert') ?></span>
                    </div>
                    <ul id="navSortList" style="list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:0.25rem">
                        <?php foreach ($availableNavItems as $key => $item): ?>
                        <li data-key="<?= $this->e($key) ?>"
                            draggable="true"
                            style="display:flex;align-items:center;gap:0.625rem;padding:0.5rem 0.625rem;border-radius:0.25rem;background:var(--color-bg);border:1px solid var(--color-gray-300);cursor:grab;user-select:none;transition:box-shadow 0.15s,opacity 0.15s">
                            <span style="font-size:1rem;color:var(--color-gray-500);flex-shrink:0"><?= $icon('fa-grip-lines', '☰') ?></span>
                            <span style="font-size:0.875rem;font-weight:500;color:var(--color-gray-900)"><?= $item['label'] ?></span>
                        </li>
                        <?php endforeach; ?>
                    </ul>
                </div>
            </div>
            <div style="display:flex;justify-content:flex-end;gap:0.5rem;padding:1rem 1.25rem;border-top:1px solid var(--color-gray-300);background:var(--color-gray-50)">
                <button id="navSortCancel" class="btn btn-sm btn-default"><?= $this->te('Abbrechen') ?></button>
                <button id="navSortSave" class="btn btn-sm btn-primary"><?= $this->te('Speichern') ?></button>
            </div>
        </div>
    </div>
</div>

<?php if (isset($extra_js)): ?>
    <?php foreach ($extra_js as $js): ?>
        <script src="<?= $this->asset($js) ?>" defer></script>
    <?php endforeach; ?>
<?php endif; ?>

<script<?= $this->scriptNonce() ?>>
var _useFAIcons = <?= $_useFA ? 'true' : 'false' ?>;

// ===== Sidebar Toggle =====
(function() {
    var mobileBtn   = document.getElementById('sidebarToggle');   // burger – mobile only
    var collapseBtn = document.getElementById('sidebarCollapseBtn'); // desktop sidebar-bottom
    var overlay     = document.getElementById('sidebarOverlay');
    var body        = document.body;

    function isDesktop() { return window.innerWidth >= 993; }

    function toggleDesktop() {
        body.classList.toggle('sidebar-collapsed');
        try { localStorage.setItem('sidebarCollapsed', body.classList.contains('sidebar-collapsed') ? '1' : '0'); } catch(e) {}
    }
    function openMobile()  { body.classList.add('sidebar-open');    overlay.classList.add('active'); }
    function closeMobile() { body.classList.remove('sidebar-open'); overlay.classList.remove('active'); }

    // Restore desktop collapsed state
    try { if (isDesktop() && localStorage.getItem('sidebarCollapsed') === '1') body.classList.add('sidebar-collapsed'); } catch(e) {}

    if (mobileBtn)   mobileBtn.addEventListener('click', function(e) { e.preventDefault(); if (isDesktop()) toggleDesktop(); else openMobile(); });
    if (collapseBtn) collapseBtn.addEventListener('click', function() { if (isDesktop()) toggleDesktop(); });
    if (overlay)     overlay.addEventListener('click', closeMobile);
    document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeMobile(); });

    window.addEventListener('resize', function() {
        if (isDesktop()) { closeMobile(); }
        else             { body.classList.remove('sidebar-collapsed'); }
    });
})();


// ===== Toast Notifications =====
document.addEventListener('DOMContentLoaded', function() {
    const toasts = document.querySelectorAll('.toast');
    toasts.forEach(function(toast, index) {
        setTimeout(function() { dismissToast(toast); }, 5000 + (index * 500));
        var closeBtn = toast.querySelector('.toast-close');
        if (closeBtn) closeBtn.addEventListener('click', function() { dismissToast(toast); });
    });

    function dismissToast(toast) {
        if (!toast || toast.classList.contains('toast-hiding')) return;
        toast.classList.add('toast-hiding');
        setTimeout(function() {
            toast.remove();
            var container = document.getElementById('toastContainer');
            if (container && container.children.length === 0) container.remove();
        }, 300);
    }
});

// ===== Notification Bell =====
(function() {
    var bell = document.getElementById('notifBell');
    var btn = document.getElementById('notifBellBtn');
    var dropdown = document.getElementById('notifDropdown');
    var badge = document.getElementById('notifBadge');
    var list = document.getElementById('notifList');
    var markAllBtn = document.getElementById('notifMarkAll');
    var deleteReadBtn = document.getElementById('notifDeleteRead');

    if (!bell || !btn) return;

    var csrfToken = document.querySelector('meta[name="csrf-token"]');
    var csrf = csrfToken ? csrfToken.content : '';
    var isOpen = false;

    function getIcon(type) {
        if (_useFAIcons) {
            if (type === 'page_changed') return '<i class="fa-solid fa-file-alt"></i>';
            if (type === 'review_due') return '<i class="fa-solid fa-exclamation-triangle"></i>';
            return '<i class="fa-solid fa-info-circle"></i>';
        }
        if (type === 'page_changed') return '📄';
        if (type === 'review_due') return '⚠️';
        return 'ℹ️';
    }

    function timeAgo(dateStr) {
        var now = new Date();
        var date = new Date(dateStr);
        var diff = Math.floor((now - date) / 1000);
        if (diff < 60) return window.onitText('gerade eben');
        if (diff < 3600) return Math.floor(diff / 60) + ' ' + window.onitText('Min.');
        if (diff < 86400) return Math.floor(diff / 3600) + ' ' + window.onitText('Std.');
        if (diff < 604800) return Math.floor(diff / 86400) + ' ' + window.onitText('Tagen');
        return date.toLocaleDateString(document.documentElement.lang || 'de');
    }

    function escHtml(str) {
        var div = document.createElement('div');
        div.textContent = str || '';
        return div.innerHTML;
    }

    function renderNotifications(data) {
        var notifications = data.notifications || [];
        var unread = data.unread_count || 0;
        if (unread > 0) {
            badge.textContent = unread > 99 ? '99+' : unread;
            badge.style.display = 'flex';
        } else {
            badge.style.display = 'none';
        }
        if (notifications.length === 0) {
            list.innerHTML = '<div class="notif-empty">' + escHtml(window.onitText('Keine Benachrichtigungen')) + '</div>';
            return;
        }
        var html = '';
        notifications.forEach(function(n) {
            var cls = n.is_read == 0 ? ' unread' : '';
            html += '<div class="notif-item' + cls + '" data-id="' + n.id + '"'
                 + (n.link ? ' data-link="' + escHtml(n.link) + '"' : '') + '>';
            html += '<div class="notif-item-icon type-' + escHtml(n.type) + '">' + getIcon(n.type) + '</div>';
            html += '<div class="notif-item-content">';
            html += '<div class="notif-item-title">' + escHtml(n.title) + '</div>';
            html += '<div class="notif-item-message">' + escHtml(n.message) + '</div>';
            html += '<div class="notif-item-time">' + timeAgo(n.created_at) + '</div>';
            html += '</div>';
            html += '<button type="button" class="notif-delete-btn" data-id="' + n.id + '" title="' + escHtml(window.onitText('Löschen')) + '">&times;</button>';
            html += '</div>';
        });
        list.innerHTML = html;

        list.querySelectorAll('.notif-item-content').forEach(function(content) {
            content.addEventListener('click', function() {
                var item = this.closest('.notif-item');
                var id = item.dataset.id;
                var link = item.dataset.link;
                fetch('/api/notifications/' + id + '/read', {
                    method: 'POST',
                    headers: { 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' }
                });
                if (link) { window.location.href = link; }
                else { item.classList.remove('unread'); updateBadge(-1); }
            });
        });

        list.querySelectorAll('.notif-delete-btn').forEach(function(btn) {
            btn.addEventListener('click', function(e) {
                e.stopPropagation();
                var id = this.dataset.id;
                var item = this.closest('.notif-item');
                var wasUnread = item.classList.contains('unread');
                fetch('/api/notifications/' + id, {
                    method: 'DELETE',
                    headers: { 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' }
                }).then(function(r) { return r.json(); })
                .then(function(data) {
                    if (data.success) {
                        item.style.transition = 'opacity 0.2s, max-height 0.2s';
                        item.style.opacity = '0';
                        item.style.maxHeight = '0';
                        item.style.overflow = 'hidden';
                        setTimeout(function() {
                            item.remove();
                            if (list.querySelectorAll('.notif-item').length === 0) {
                                list.innerHTML = '<div class="notif-empty">' + escHtml(window.onitText('Keine Benachrichtigungen')) + '</div>';
                            }
                        }, 200);
                        if (wasUnread) updateBadge(-1);
                    }
                });
            });
        });
    }

    function updateBadge(delta) {
        var current = parseInt(badge.textContent) || 0;
        var newCount = Math.max(0, current + delta);
        if (newCount > 0) {
            badge.textContent = newCount > 99 ? '99+' : newCount;
            badge.style.display = 'flex';
        } else {
            badge.style.display = 'none';
        }
    }

    function loadNotifications() {
        fetch('/api/notifications', { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } })
        .then(function(r) { return r.json(); })
        .then(renderNotifications)
        .catch(function() {});
    }

    var notificationPollInterval = null;

    function pollUnreadCount() {
        fetch('/api/notifications/unread-count', { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } })
        .then(function(r) {
            if (r.status === 401) {
                if (notificationPollInterval) { clearInterval(notificationPollInterval); notificationPollInterval = null; }
                badge.style.display = 'none';
                return null;
            }
            return r.json();
        })
        .then(function(data) {
            if (!data) return;
            var unread = data.unread_count || 0;
            if (unread > 0) {
                badge.textContent = unread > 99 ? '99+' : unread;
                badge.style.display = 'flex';
            } else {
                badge.style.display = 'none';
            }
        })
        .catch(function() {});
    }

    btn.addEventListener('click', function(e) {
        e.stopPropagation();
        isOpen = !isOpen;
        dropdown.classList.toggle('open', isOpen);
        btn.setAttribute('aria-expanded', isOpen);
        if (isOpen) loadNotifications();
    });

    if (markAllBtn) {
        markAllBtn.addEventListener('click', function(e) {
            e.stopPropagation();
            fetch('/api/notifications/read-all', {
                method: 'POST',
                headers: { 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' }
            }).then(function(r) { return r.json(); })
            .then(function() {
                badge.style.display = 'none';
                list.querySelectorAll('.notif-item.unread').forEach(function(item) { item.classList.remove('unread'); });
            });
        });
    }

    if (deleteReadBtn) {
        deleteReadBtn.addEventListener('click', function(e) {
            e.stopPropagation();
            fetch('/api/notifications/delete-read', {
                method: 'POST',
                headers: { 'X-CSRF-Token': csrf, 'X-Requested-With': 'XMLHttpRequest' }
            }).then(function(r) { return r.json(); })
            .then(function(data) {
                if (data.success) {
                    list.querySelectorAll('.notif-item:not(.unread)').forEach(function(item) { item.remove(); });
                    if (list.querySelectorAll('.notif-item').length === 0) {
                        list.innerHTML = '<div class="notif-empty">' + escHtml(window.onitText('Keine Benachrichtigungen')) + '</div>';
                    }
                }
            });
        });
    }

    document.addEventListener('click', function(e) {
        if (isOpen && !bell.contains(e.target)) {
            isOpen = false;
            dropdown.classList.remove('open');
            btn.setAttribute('aria-expanded', 'false');
        }
    });

    document.addEventListener('keydown', function(e) {
        if (e.key === 'Escape' && isOpen) {
            isOpen = false;
            dropdown.classList.remove('open');
            btn.setAttribute('aria-expanded', 'false');
        }
    });

    pollUnreadCount();
    notificationPollInterval = setInterval(pollUnreadCount, 60000);
})();

// ===== PWA: Service Worker & Push-Benachrichtigungen =====
(function() {
    'use strict';

    if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
        return; // Browser unterstuetzt PWA Push nicht
    }

    var csrf = (document.querySelector('meta[name="csrf-token"]') || {}).content || '';
    var pushBtn = document.getElementById('notifPushToggle');
    var swReg = null;
    var vapidPublicKey = null;

    // VAPID Public Key laden und Service Worker registrieren
    function init() {
        fetch('/api/push/vapid-public-key', {
            headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
        })
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (!data.enabled || !data.public_key) return; // Push nicht konfiguriert
            vapidPublicKey = data.public_key;

            // Service Worker registrieren
            return navigator.serviceWorker.register('/sw.js', { scope: '/' });
        })
        .then(function(registration) {
            if (!registration) return;
            swReg = registration;
            updatePushButton();
            if (pushBtn) pushBtn.style.display = '';
        })
        .catch(function() {});
    }

    function urlBase64ToUint8Array(base64String) {
        var padding = '='.repeat((4 - base64String.length % 4) % 4);
        var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
        var rawData = window.atob(base64);
        var outputArray = new Uint8Array(rawData.length);
        for (var i = 0; i < rawData.length; ++i) {
            outputArray[i] = rawData.charCodeAt(i);
        }
        return outputArray;
    }

    function updatePushButton() {
        if (!pushBtn || !swReg) return;
        swReg.pushManager.getSubscription().then(function(sub) {
            if (sub) {
                pushBtn.title = 'Push-Benachrichtigungen deaktivieren';
                pushBtn.innerHTML = (_useFAIcons ? '<i class="fa-solid fa-bell"></i>' : '🔔') + ' An';
                pushBtn.classList.add('push-active');
            } else {
                pushBtn.title = 'Push-Benachrichtigungen aktivieren';
                pushBtn.innerHTML = (_useFAIcons ? '<i class="fa-solid fa-bell"></i>' : '🔔') + ' Push';
                pushBtn.classList.remove('push-active');
            }
        });
    }

    function subscribeUser() {
        if (!swReg || !vapidPublicKey) return;
        var applicationServerKey = urlBase64ToUint8Array(vapidPublicKey);
        swReg.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey: applicationServerKey,
        })
        .then(function(subscription) {
            var json = subscription.toJSON();
            return fetch('/api/push/subscribe', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': csrf,
                    'X-Requested-With': 'XMLHttpRequest',
                },
                body: JSON.stringify({
                    endpoint: json.endpoint,
                    p256dh:   json.keys.p256dh,
                    auth:     json.keys.auth,
                }),
            });
        })
        .then(function() { updatePushButton(); })
        .catch(function() {});
    }

    function unsubscribeUser() {
        if (!swReg) return;
        swReg.pushManager.getSubscription().then(function(subscription) {
            if (!subscription) return;
            var endpoint = subscription.endpoint;
            subscription.unsubscribe().then(function() {
                fetch('/api/push/unsubscribe', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-Token': csrf,
                        'X-Requested-With': 'XMLHttpRequest',
                    },
                    body: JSON.stringify({ endpoint: endpoint }),
                });
                updatePushButton();
            });
        });
    }

    if (pushBtn) {
        pushBtn.addEventListener('click', function(e) {
            e.stopPropagation();
            if (!swReg) return;
            swReg.pushManager.getSubscription().then(function(sub) {
                if (sub) {
                    unsubscribeUser();
                } else {
                    // Erlaubnis anfragen und abonnieren
                    Notification.requestPermission().then(function(permission) {
                        if (permission === 'granted') {
                            subscribeUser();
                        }
                    });
                }
            });
        });
    }

    init();
})();

// ===== Nav Sort Modal =====
<?php
// Daten für den Kategorien-Editor: Klartext-Labels der sichtbaren Menüpunkte,
// das aktuell gespeicherte Layout sowie die Standard-Gruppierung als Startpunkt.
$navEditorItems = [];
foreach ($availableNavItems as $navEdKey => $navEdItem) {
    $navEditorItems[$navEdKey] = html_entity_decode(strip_tags($navEdItem['label']), ENT_QUOTES | ENT_HTML5);
}
$navEditorDefaults = [];
$navEdNoGroup = [];
foreach ($availableNavItems as $navEdKey => $navEdItem) {
    if (empty($navEdItem['group'])) $navEdNoGroup[] = $navEdKey;
}
if (!empty($navEdNoGroup)) {
    $navEditorDefaults[] = ['label' => '', 'items' => $navEdNoGroup];
}
foreach ($navGroupDefs as $navEdGk => $navEdGd) {
    $navEdKeys = [];
    foreach ($availableNavItems as $navEdKey => $navEdItem) {
        if (($navEdItem['group'] ?? '') === $navEdGk) $navEdKeys[] = $navEdKey;
    }
    if (!empty($navEdKeys)) {
        $navEditorDefaults[] = ['label' => html_entity_decode($navEdGd['label'], ENT_QUOTES | ENT_HTML5), 'items' => $navEdKeys];
    }
}
?>
window.__navCfg = {
    items: <?= json_encode($navEditorItems, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS) ?>,
    layout: <?= $navCustomGroups !== null ? json_encode($navCustomGroups, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS) : 'null' ?>,
    defaults: <?= json_encode($navEditorDefaults, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS) ?>
};
(function() {
    var trigger  = document.getElementById('navSortTrigger');
    var modal    = document.getElementById('navSortModal');
    var backdrop = document.getElementById('navSortBackdrop');
    var closeBtn = document.getElementById('navSortClose');
    var cancelBtn= document.getElementById('navSortCancel');
    var saveBtn  = document.getElementById('navSortSave');
    var list     = document.getElementById('navSortList');
    if (!trigger || !modal || !list) return;

    // ── Editor für eigene Kategorien ─────────────────────────
    var cfg          = window.__navCfg || { items: {}, layout: null, defaults: [] };
    var customToggle = document.getElementById('navCustomToggle');
    var groupToggle  = document.getElementById('navGroupToggle');
    var catEditor    = document.getElementById('navCatEditor');
    var catList      = document.getElementById('navCatList');
    var catAdd       = document.getElementById('navCatAdd');
    var flatWrap     = document.getElementById('navFlatWrap');
    var hint         = document.getElementById('navSortHint');

    function mkCatBtn(txt, title, fn) {
        var b = document.createElement('button');
        b.type = 'button'; b.textContent = txt; b.title = title;
        b.style.cssText = 'flex-shrink:0;width:1.75rem;height:1.75rem;border:1px solid var(--color-gray-300);border-radius:0.25rem;background:var(--color-bg);color:var(--color-secondary);font-size:0.75rem;cursor:pointer;line-height:1';
        b.addEventListener('click', fn);
        return b;
    }
    function makeCatItem(key) {
        var li = document.createElement('li');
        li.dataset.key = key;
        li.draggable = true;
        li.style.cssText = 'display:flex;align-items:center;gap:0.625rem;padding:0.4rem 0.625rem;border-radius:0.25rem;background:var(--color-bg);border:1px solid var(--color-gray-300);cursor:grab;user-select:none;font-size:0.8438rem;font-weight:500;color:var(--color-gray-900)';
        var grip = document.createElement('span');
        grip.textContent = '☰';
        grip.style.cssText = 'font-size:0.9rem;color:var(--color-gray-500);flex-shrink:0';
        li.appendChild(grip);
        li.appendChild(document.createTextNode(cfg.items[key] || key));
        return li;
    }
    function makeCatBox(label, items) {
        var box = document.createElement('div');
        box.className = 'navcat-box';
        box.style.cssText = 'background:var(--color-gray-50);border:1px solid var(--color-gray-300);border-radius:0.375rem;padding:0.5rem';
        var head = document.createElement('div');
        head.style.cssText = 'display:flex;align-items:center;gap:0.375rem;margin-bottom:0.4rem';
        var inp = document.createElement('input');
        inp.type = 'text'; inp.value = label || ''; inp.maxLength = 40;
        inp.placeholder = 'Ohne Überschrift';
        inp.className = 'navcat-label';
        inp.style.cssText = 'flex:1;min-width:0;padding:0.3rem 0.5rem;border:1px solid var(--color-gray-300);border-radius:0.25rem;font-size:0.8125rem;font-weight:600;background:var(--color-bg);color:var(--color-gray-900)';
        var ul = document.createElement('ul');
        ul.className = 'navcat-items';
        ul.style.cssText = 'list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:0.25rem;min-height:1.6rem;border-radius:0.25rem';
        head.appendChild(inp);
        head.appendChild(mkCatBtn('▲', 'Nach oben', function() {
            var p = box.previousElementSibling; if (p) catList.insertBefore(box, p);
        }));
        head.appendChild(mkCatBtn('▼', 'Nach unten', function() {
            var n = box.nextElementSibling; if (n) catList.insertBefore(n, box);
        }));
        head.appendChild(mkCatBtn('✕', 'Kategorie entfernen', function() {
            var target = box.previousElementSibling || box.nextElementSibling;
            if (!target && ul.children.length > 0) return;
            if (target) {
                var tul = target.querySelector('.navcat-items');
                while (ul.firstChild) tul.appendChild(ul.firstChild);
            }
            box.remove();
        }));
        (items || []).forEach(function(k) { if (cfg.items[k]) ul.appendChild(makeCatItem(k)); });
        box.appendChild(head);
        box.appendChild(ul);
        return box;
    }
    function buildCatEditor(groups) {
        catList.innerHTML = '';
        (groups || []).forEach(function(g) { catList.appendChild(makeCatBox(g.label, g.items)); });
        if (!catList.children.length) catList.appendChild(makeCatBox('', []));
    }
    if (catAdd) catAdd.addEventListener('click', function() {
        var box = makeCatBox('', []);
        catList.appendChild(box);
        box.querySelector('.navcat-label').focus();
    });

    // Drag & Drop zwischen allen Kategorien-Listen (delegiert)
    var catDragSrc = null;
    if (catList) {
        catList.addEventListener('dragstart', function(e) {
            catDragSrc = e.target.closest('li');
            if (!catDragSrc) return;
            catDragSrc.style.opacity = '0.4';
            e.dataTransfer.effectAllowed = 'move';
        });
        catList.addEventListener('dragend', function(e) {
            var li = e.target.closest('li');
            if (li) { li.style.opacity = ''; }
            catDragSrc = null;
        });
        catList.addEventListener('dragover', function(e) {
            if (!catDragSrc) return;
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
            var li = e.target.closest('li');
            if (li && li !== catDragSrc) {
                var rect = li.getBoundingClientRect();
                if (e.clientY < rect.top + rect.height / 2) li.parentNode.insertBefore(catDragSrc, li);
                else li.parentNode.insertBefore(catDragSrc, li.nextSibling);
                return;
            }
            var ul = e.target.closest('.navcat-items');
            if (ul && !ul.contains(catDragSrc)) ul.appendChild(catDragSrc);
            else if (!ul) {
                var box = e.target.closest('.navcat-box');
                if (box) {
                    var boxUl = box.querySelector('.navcat-items');
                    if (boxUl && !boxUl.contains(catDragSrc)) boxUl.appendChild(catDragSrc);
                }
            }
        });
        catList.addEventListener('drop', function(e) { e.preventDefault(); });
    }

    function refreshNavMode() {
        var custom = customToggle && customToggle.checked;
        if (catEditor) catEditor.style.display = custom ? '' : 'none';
        if (flatWrap) flatWrap.style.display = custom ? 'none' : '';
        if (groupToggle) groupToggle.disabled = !!custom;
        if (hint) hint.textContent = custom
            ? 'Kategorien benennen, mit ▲/▼ sortieren und Menüpunkte per Drag & Drop frei einsortieren. Eine Kategorie ohne Überschrift zeigt ihre Punkte ohne Zwischentitel.'
            : 'Verschiebe die Einträge per Drag & Drop in deine gewünschte Reihenfolge.';
        if (custom && catList && !catList.children.length) {
            buildCatEditor(cfg.layout || cfg.defaults);
        }
    }
    if (customToggle) { customToggle.addEventListener('change', refreshNavMode); refreshNavMode(); }

    function openModal() { modal.style.display = 'block'; document.body.style.overflow = 'hidden'; }
    function closeModal() { modal.style.display = 'none'; document.body.style.overflow = ''; }

    trigger.addEventListener('click', function(e) { e.preventDefault(); closeModal(); openModal(); });
    if (backdrop) backdrop.addEventListener('click', closeModal);
    if (closeBtn) closeBtn.addEventListener('click', closeModal);
    if (cancelBtn) cancelBtn.addEventListener('click', closeModal);
    document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeModal(); });

    var dragSrc = null;
    list.addEventListener('dragstart', function(e) {
        dragSrc = e.target.closest('li');
        if (!dragSrc) return;
        dragSrc.style.opacity = '0.4';
        e.dataTransfer.effectAllowed = 'move';
    });
    list.addEventListener('dragend', function(e) {
        var li = e.target.closest('li');
        if (!li) return;
        li.style.opacity = '';
        li.style.boxShadow = '';
        dragSrc = null;
    });
    list.addEventListener('dragover', function(e) {
        e.preventDefault();
        e.dataTransfer.dropEffect = 'move';
        var target = e.target.closest('li');
        if (!target || target === dragSrc) return;
        var rect = target.getBoundingClientRect();
        if (e.clientY < rect.top + rect.height / 2) {
            list.insertBefore(dragSrc, target);
        } else {
            list.insertBefore(dragSrc, target.nextSibling);
        }
    });
    list.addEventListener('dragenter', function(e) {
        var target = e.target.closest('li');
        if (target && target !== dragSrc) target.style.boxShadow = '0 0 0 2px #3c8dbc';
    });
    list.addEventListener('dragleave', function(e) {
        var target = e.target.closest('li');
        if (target) target.style.boxShadow = '';
    });
    list.addEventListener('drop', function(e) {
        e.preventDefault();
        list.querySelectorAll('li').forEach(function(li) { li.style.boxShadow = ''; });
    });

    if (saveBtn) {
        saveBtn.addEventListener('click', function() {
            saveBtn.textContent = '...';
            saveBtn.disabled = true;
            var csrfMeta = document.querySelector('meta[name="csrf-token"]');
            var csrf = csrfMeta ? csrfMeta.content : '';
            function postSetting(key, value) {
                var f = new FormData();
                f.append('key', key); f.append('value', value); f.append('_csrf', csrf);
                return fetch('/api/user-settings', { method: 'POST', body: f }).then(function(r) { return r.json(); });
            }
            var posts = [];
            if (customToggle && customToggle.checked) {
                var groups = [];
                catList.querySelectorAll('.navcat-box').forEach(function(box) {
                    var items = [];
                    box.querySelectorAll('li[data-key]').forEach(function(li) { items.push(li.dataset.key); });
                    groups.push({ label: box.querySelector('.navcat-label').value.trim(), items: items });
                });
                posts.push(postSetting('nav_layout', JSON.stringify({ groups: groups })));
            } else {
                var order = [];
                list.querySelectorAll('li[data-key]').forEach(function(li) { order.push(li.dataset.key); });
                var grouped = groupToggle && groupToggle.checked ? '1' : '0';
                posts.push(postSetting('nav_layout', ''));
                posts.push(postSetting('nav_order', JSON.stringify(order)));
                posts.push(postSetting('nav_grouped', grouped));
            }
            Promise.all(posts)
            .then(function(results) {
                if (results.every(function(d) { return d.ok; })) {
                    closeModal();
                    window.location.reload();
                } else {
                    saveBtn.textContent = 'Speichern';
                    saveBtn.disabled = false;
                }
            }).catch(function() {
                saveBtn.textContent = 'Speichern';
                saveBtn.disabled = false;
            });
        });
    }
})();

// ===== CSRF-Token Auto-Refresh & Session-Keepalive =====
// Verhindert Token-Ablauf und ungewolltes Abmelden bei langen Bearbeitungssessions
(function() {
    // CSRF-Token alle 4 Stunden erneuern (Token-Lifetime: 8h, Refresh bei 50%)
    var CSRF_REFRESH_INTERVAL = 4 * 60 * 60 * 1000;
    // Session-Keepalive alle 10 Minuten (verhindert Session-Ablauf bei inaktiven Formularen)
    var KEEPALIVE_INTERVAL = 10 * 60 * 1000;

    function refreshCsrfToken() {
        fetch('/api/csrf-token', {
            headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
        })
        .then(function(r) {
            if (!r.ok) return null;
            return r.json();
        })
        .then(function(data) {
            if (!data || !data.token) return;
            // Meta-Tag aktualisieren (für AJAX-Requests)
            var meta = document.querySelector('meta[name="csrf-token"]');
            if (meta) meta.content = data.token;
            // Alle CSRF-Hidden-Felder auf der Seite aktualisieren
            document.querySelectorAll('input[name="_csrf"]').forEach(function(input) {
                input.value = data.token;
            });
        })
        .catch(function() {});
    }

    function sessionKeepalive() {
        // Leichter Ping um Session am Leben zu halten
        fetch('/api/notifications/unread-count', {
            headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
        }).catch(function() {});
    }

    setInterval(refreshCsrfToken, CSRF_REFRESH_INTERVAL);
    setInterval(sessionKeepalive, KEEPALIVE_INTERVAL);
})();
</script>

<?php
// KI-Chat anzeigen wenn aktiviert
$aiChatEnabled = false;
$aiChatConfigFile = dirname(__DIR__, 2) . '/config/runtime.php';
if (file_exists($aiChatConfigFile)) {
    $aiChatRuntimeConf = include $aiChatConfigFile;
    $aiChatEnabled = !empty($aiChatRuntimeConf['modules']['ai_chat'])
                     && !empty($aiChatRuntimeConf['ai']['chat_enabled'])
                     && !empty($aiChatRuntimeConf['ai']['provider'])
                     && (
                         !empty($aiChatRuntimeConf['ai']['openai_api_key'])
                         || !empty($aiChatRuntimeConf['ai']['anthropic_api_key'])
                         || !empty($aiChatRuntimeConf['ai']['deepseek_api_key'])
                         || ($aiChatRuntimeConf['ai']['provider'] ?? '') === 'openai_compatible'
                     );
}
// SICHERHEIT: KI-Widget nur für Nutzer mit der Capability `use_ai_chat` anzeigen
// (die API-Endpunkte sind ebenfalls so gegated).
// $hide_ai_bubble wird von der Vollbild-Seite /assistant gesetzt, damit dort
// nicht zusätzlich die Bubble (mit identischen Element-IDs) gerendert wird.
$aiPrivacyDesc = $aiChatEnabled
    ? \OpenNIT\AI\PrivacyFilter::describe($aiChatRuntimeConf['ai'] ?? [])
    : '';
if ($aiChatEnabled && empty($hide_ai_bubble) && $this->hasCapability('use_ai_chat')):
?>
<!-- ===== KI-CHAT BUBBLE ===== -->
<div class="ai-chat-bubble">
    <!-- Chat-Panel -->
    <div class="ai-chat-panel hidden" id="aiChatPanel" role="dialog" aria-modal="false" aria-label="<?= $this->te('KI-Assistent') ?>">

        <!-- Header -->
        <div class="ai-chat-header">
            <span class="ai-chat-header-icon"><?= $icon('fa-robot', '🤖') ?></span>
            <div class="ai-chat-header-info">
                <div class="ai-chat-header-title"><?= $this->te('KI-Assistent') ?></div>
                <div class="ai-chat-header-subtitle"><?= $this->te($aiPrivacyDesc) ?></div>
            </div>
            <div class="ai-chat-header-actions">
                <a class="ai-chat-header-btn" href="/assistant" title="<?= $this->te('Im Vollbild &ouml;ffnen') ?>">
                    <?= $icon('fa-expand', '⛶') ?>
                </a>
                <button type="button" class="ai-chat-header-btn" id="aiChatNewSessionBtn" title="<?= $this->te('Neues Gespr&auml;ch starten') ?>">
                    <?= $icon('fa-plus', '➕') ?> <?= $this->te('Neu') ?>
                </button>
                <button type="button" class="ai-chat-header-btn" id="aiChatSessionToggleBtn" title="<?= $this->te('Gespr&auml;chsverlauf') ?>">
                    <?= $icon('fa-history', '🕐') ?>
                </button>
                <button type="button" class="ai-chat-header-btn" id="aiChatClearBtn" title="<?= $this->te('Aktuellen Verlauf l&ouml;schen') ?>">
                    <?= $icon('fa-trash', '🗑') ?>
                </button>
            </div>
        </div>

        <!-- Session-Drawer -->
        <div class="ai-session-drawer hidden" id="aiChatSessionDrawer">
            <div class="ai-session-drawer-header">
                <span><?= $this->te('Gespr&auml;chsverl&auml;ufe') ?></span>
            </div>
            <div class="ai-session-list" id="aiChatSessionList"></div>
        </div>

        <!-- Fehler-Bar -->
        <div class="ai-error-bar" id="aiChatErrorBar" style="display:none">
            <span id="aiChatErrorText"></span>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="ai-error-close" id="aiChatErrorClose">&times;</button>
        </div>

        <!-- Nachrichten -->
        <div class="ai-chat-messages" id="aiChatMessages">
            <!-- Willkommens-Nachricht -->
            <div class="ai-chat-welcome" id="aiChatWelcome">
                <span class="ai-chat-welcome-icon"><?= $icon('fa-robot', '🤖') ?></span>
                <h3><?= $this->te('Hallo! Wie kann ich helfen?') ?></h3>
                <p><?= $this->te('Ich kenne alle Inhalte dieses IT-Tools. Frag mich zu Assets, Tickets, Vertr&auml;gen, VMs und mehr – oder lass mich direkt etwas anlegen.') ?></p>
                <p class="ai-chat-welcome-dev-hint"><?= $icon('fa-exclamation-triangle', '⚠️') ?> <?= $this->te('Dieses Modul befindet sich in Entwicklung und kann Fehler machen.') ?></p>
                <div class="ai-chat-welcome-hints">
                    <button type="button" class="ai-chat-hint-btn" data-hint="Wie viel freier RAM ist auf den Hypervisor-Hosts?">
                        <?= $icon('fa-chart-bar', '📊') ?> <?= $this->te('Freier RAM auf den Hosts?') ?>
                    </button>
                    <button type="button" class="ai-chat-hint-btn" data-hint="Wie viele Assets haben wir im Inventar? Bitte nach Modell aufschl&uuml;sseln.">
                        <?= $icon('fa-boxes-stacked', '📦') ?> <?= $this->te('Inventar-&Uuml;bersicht nach Modell') ?>
                    </button>
                    <button type="button" class="ai-chat-hint-btn" data-hint="Welche Tickets sind aktuell offen?">
                        <?= $icon('fa-ticket-alt', '🎫') ?> <?= $this->te('Offene Tickets anzeigen') ?>
                    </button>
                    <button type="button" class="ai-chat-hint-btn" data-hint="Leg eine neue Bestellung an: 2x Dell Laptop E5540, 1x Dockingstation">
                        <?= $icon('fa-shopping-cart', '🛒') ?> <?= $this->te('Neue Bestellung anlegen') ?>
                    </button>
                </div>
            </div>
        </div>

        <!-- Eingabe -->
        <div class="ai-chat-input-area">
            <div class="ai-chat-disclaimer">
                <?= $icon('fa-lock', '🔒') ?> <?= $this->te($aiPrivacyDesc) ?> &bull; <?= $this->te('Keine Passw&ouml;rter, keine Vault-Daten') ?><br>
                <?= $icon('fa-exclamation-triangle', '⚠️') ?> <?= $this->te('Dieses Modul befindet sich in Entwicklung und kann Fehler machen.') ?>
            </div>
            <div class="ai-chat-input-row">
                <textarea
                    class="ai-chat-textarea"
                    id="aiChatTextarea"
                    placeholder="<?= $this->te('Nachricht eingeben... (Enter zum Senden)') ?>"
                    rows="1"
                    maxlength="2000"
                    aria-label="<?= $this->te('Nachricht') ?>"
                ></textarea>
                <button type="button" class="ai-chat-send-btn" id="aiChatSendBtn" title="<?= $this->te('Senden') ?>" aria-label="<?= $this->te('Senden') ?>">
                    <?= $icon('fa-paper-plane', '➤') ?>
                </button>
            </div>
        </div>
    </div>

    <!-- Bubble-Button -->
    <button type="button"
            class="ai-chat-bubble-btn"
            id="aiChatBubbleBtn"
            aria-label="<?= $this->te('KI-Assistent &ouml;ffnen') ?>"
            aria-expanded="false"
            aria-controls="aiChatPanel"
            title="<?= $this->te('KI-Assistent') ?>">
        <span class="ai-bubble-icon"><?= $icon('fa-robot', '🤖') ?></span>
        <span class="pulse-ring"></span>
    </button>
</div>

<script src="<?= $this->asset('/assets/js/ai-chat.js') ?>"></script>
<?php endif; ?>

<?php if ($navbarHasPhonebook || $navbarHasContacts): ?>
<script<?= $this->scriptNonce() ?>>
(function(){
    var inp = document.getElementById('navbarPbInput');
    if (!inp) return;

    var hasPhonebook = <?= json_encode($navbarHasPhonebook) ?>;
    var hasContacts  = <?= json_encode($navbarHasContacts) ?>;

    var res = document.createElement('div');
    res.id = 'navbar-pb-results';
    res.style.cssText = 'display:none;position:fixed;z-index:99999;background:#fff;border:1px solid #d1d5db;border-radius:6px;box-shadow:0 4px 16px rgba(0,0,0,.18);max-height:360px;overflow-y:auto;min-width:280px;';
    document.body.appendChild(res);

    function position() {
        var r = inp.getBoundingClientRect();
        res.style.top  = (r.bottom + 4) + 'px';
        res.style.left = r.left + 'px';
        res.style.width = Math.max(r.width, 300) + 'px';
    }

    function renderResult(p) {
        var av = p.image_path
            ? '<img src="'+p.image_path+'" style="width:36px;height:36px;border-radius:50%;object-fit:cover;flex-shrink:0;" loading="lazy">'
            : '<div style="width:36px;height:36px;border-radius:50%;background:'+p.avatar_color+';display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:.8rem;flex-shrink:0;">'+p.initials+'</div>';
        var sub = [p.job_title, p.department].filter(Boolean).join(' · ');
        var badge = p.source === 'contact'
            ? '<span style="font-size:.65rem;background:#e0f2fe;color:#0369a1;border-radius:4px;padding:1px 5px;white-space:nowrap;flex-shrink:0;">Adressbuch</span>'
            : '<span style="font-size:.65rem;background:#f0fdf4;color:#166534;border-radius:4px;padding:1px 5px;white-space:nowrap;flex-shrink:0;">Telefonbuch</span>';
        var phones = '';
        var _pbPhone  = _useFAIcons ? '<i class="fa-solid fa-phone"></i>'      : '📞';
        var _pbMobile = _useFAIcons ? '<i class="fa-solid fa-mobile-alt"></i>' : '📱';
        var _pbEmail  = _useFAIcons ? '<i class="fa-solid fa-envelope"></i>'   : '✉';
        if (p.phone)  phones += '<a href="tel:'+p.phone.replace(/[^\d+]/g,'')+'" style="color:#1d4ed8;text-decoration:none;font-size:.78rem;white-space:nowrap;" data-dialog-keep>'+_pbPhone+' '+p.phone+'</a> ';
        if (p.mobile) phones += '<a href="tel:'+p.mobile.replace(/[^\d+]/g,'')+'" style="color:#059669;text-decoration:none;font-size:.78rem;white-space:nowrap;" data-dialog-keep>'+_pbMobile+' '+p.mobile+'</a> ';
        if (p.email)  phones += '<a href="mailto:'+p.email+'" style="color:#6b7280;text-decoration:none;font-size:.78rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px;display:inline-block;" data-dialog-keep>'+_pbEmail+' '+p.email+'</a>';
        return '<div style="display:flex;align-items:center;gap:.6rem;padding:.5rem .75rem;border-bottom:1px solid #f3f4f6;cursor:default;">'
            + av
            + '<div style="min-width:0;flex:1;">'
            + '<div style="display:flex;align-items:center;gap:.4rem;">'
            + '<span style="font-weight:600;font-size:.875rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">'+p.display_name+'</span>'
            + (hasPhonebook && hasContacts ? badge : '')
            + '</div>'
            + (sub ? '<div style="font-size:.72rem;color:#6b7280;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">'+sub+'</div>' : '')
            + (phones ? '<div style="margin-top:.2rem;display:flex;flex-wrap:wrap;gap:.35rem;">'+phones+'</div>' : '')
            + '</div></div>';
    }

    var timer;
    inp.addEventListener('input', function(){
        clearTimeout(timer);
        var q = inp.value.trim();
        if (q.length < 2) { res.style.display='none'; res.innerHTML=''; return; }
        timer = setTimeout(function(){
            var qs = encodeURIComponent(q);
            var fetches = [];
            if (hasPhonebook) fetches.push(fetch('/api/phonebook/search?q='+qs).then(function(r){ return r.ok ? r.json() : []; }).catch(function(){ return []; }));
            if (hasContacts)  fetches.push(fetch('/api/contacts/search?q='+qs).then(function(r){ return r.ok ? r.json() : []; }).catch(function(){ return []; }));
            Promise.all(fetches).then(function(results){
                var data = [].concat.apply([], results);
                data.sort(function(a,b){ return a.display_name.localeCompare(b.display_name, 'de'); });
                if (!data.length) {
                    res.innerHTML='<div style="padding:.6rem 1rem;color:#9ca3af;font-size:.85rem;">Keine Ergebnisse</div>';
                } else {
                    res.innerHTML = data.map(renderResult).join('');
                }
                position(); res.style.display='block';
            });
        }, 250);
    });

    inp.addEventListener('focus', function(){
        if (res.innerHTML && inp.value.trim().length >= 2) { position(); res.style.display='block'; }
    });

    document.addEventListener('click', function(e){
        if (!inp.contains(e.target) && !res.contains(e.target)) {
            res.style.display='none';
        }
    });

    window.addEventListener('scroll', function(){ if (res.style.display!=='none') position(); }, { passive: true, capture: true });
    window.addEventListener('resize', function(){ if (res.style.display!=='none') position(); });
})();
</script>
<?php endif; ?>

<!-- ===== Globale Header-Suche (gruppiertes Live-Dropdown) ===== -->
<style>
#global-search-results { position:fixed; z-index:99999; background:var(--color-bg); border:1px solid var(--gray-200); border-radius:8px; box-shadow:0 8px 28px rgba(0,0,0,.16); max-height:72vh; overflow-y:auto; display:none; }
#global-search-results .gs-group + .gs-group { border-top:1px solid var(--slate-100); }
#global-search-results .gs-group-head { display:flex; align-items:center; gap:.45rem; padding:.45rem .8rem; font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing:.04em; color:var(--slate-400); background:var(--slate-50); }
#global-search-results .gs-count { margin-left:auto; font-weight:600; color:var(--slate-300); }
#global-search-results .gs-item { display:block; padding:.45rem .8rem; text-decoration:none; color:inherit; border-top:1px solid var(--slate-50); }
#global-search-results .gs-item:hover, #global-search-results .gs-item.gs-active { background:var(--blue-50); }
#global-search-results .gs-more.gs-active { background:var(--color-primary-pale); }
#global-search-results .gs-item-title { font-size:.85rem; font-weight:600; color:var(--slate-800); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
#global-search-results .gs-item-sub { font-size:.72rem; color:var(--slate-400); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
#global-search-results .gs-more { display:block; padding:.4rem .8rem; font-size:.76rem; font-weight:600; color:var(--color-primary); text-decoration:none; }
#global-search-results .gs-more:hover { background:var(--color-primary-pale); }
#global-search-results .gs-empty, #global-search-results .gs-loading { padding:.7rem 1rem; color:var(--gray-400); font-size:.85rem; }
</style>
<script<?= $this->scriptNonce() ?>>
(function(){
    var inp = document.getElementById('globalSearchInput');
    if (!inp) return;

    var box = document.createElement('div');
    box.id = 'global-search-results';
    document.body.appendChild(box);

    function esc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
    function position(){
        var r = inp.getBoundingClientRect();
        box.style.top = (r.bottom + 5) + 'px';
        box.style.width = Math.max(r.width, 340) + 'px';
        var left = Math.min(r.left, window.innerWidth - box.offsetWidth - 8);
        box.style.left = Math.max(8, left) + 'px';
    }
    function hide(){ box.style.display='none'; }
    function show(){ position(); box.style.display='block'; }

    function render(groups, q){
        if (!groups.length){ box.innerHTML = '<div class="gs-empty">Keine Ergebnisse für „'+esc(q)+'"</div>'; show(); return; }
        var html = '';
        groups.forEach(function(g){
            html += '<div class="gs-group"><div class="gs-group-head"><span>'+esc(g.icon)+'</span>'+esc(g.label)+'<span class="gs-count">'+g.total+'</span></div>';
            g.items.forEach(function(it){
                html += '<a class="gs-item" href="'+esc(it.url)+'"><div class="gs-item-title">'+esc(it.title)+'</div>'
                     + (it.subtitle ? '<div class="gs-item-sub">'+esc(it.subtitle)+'</div>' : '') + '</a>';
            });
            if (g.has_more){ html += '<a class="gs-more" href="'+esc(g.more_url)+'">Alle '+g.total+' anzeigen →</a>'; }
            html += '</div>';
        });
        box.innerHTML = html;
        sel = -1;
        show();
    }

    // Tastatur-Navigation
    var sel = -1;
    function anchors(){ return box.querySelectorAll('a.gs-item, a.gs-more'); }
    function highlight(){
        var a = anchors();
        for (var i = 0; i < a.length; i++) a[i].classList.toggle('gs-active', i === sel);
        if (sel >= 0 && a[sel]) a[sel].scrollIntoView({ block: 'nearest' });
    }

    var timer, lastQ='';
    inp.addEventListener('input', function(){
        clearTimeout(timer);
        var q = inp.value.trim();
        if (q.length < 2){ hide(); box.innerHTML=''; return; }
        timer = setTimeout(function(){
            lastQ = q;
            box.innerHTML = '<div class="gs-loading">Suche…</div>'; show();
            fetch('/api/global-search?q='+encodeURIComponent(q), { headers:{'X-Requested-With':'XMLHttpRequest'} })
                .then(function(r){ return r.ok ? r.json() : {groups:[]}; })
                .then(function(d){ if (inp.value.trim() !== lastQ) return; render(d.groups||[], q); })
                .catch(function(){ hide(); });
        }, 220);
    });

    inp.addEventListener('focus', function(){ if (box.innerHTML && inp.value.trim().length>=2) show(); });
    inp.addEventListener('keydown', function(e){
        if (e.key === 'Escape') { hide(); return; }
        if (box.style.display === 'none') return;
        var a = anchors();
        if (e.key === 'ArrowDown') { e.preventDefault(); if (a.length){ sel = (sel + 1) % a.length; highlight(); } }
        else if (e.key === 'ArrowUp') { e.preventDefault(); if (a.length){ sel = (sel - 1 + a.length) % a.length; highlight(); } }
        else if (e.key === 'Enter') { if (sel >= 0 && a[sel]) { e.preventDefault(); window.location.href = a[sel].getAttribute('href'); } }
    });
    document.addEventListener('click', function(e){ if (!inp.contains(e.target) && !box.contains(e.target)) hide(); });
    window.addEventListener('scroll', function(){ if (box.style.display!=='none') position(); }, { passive:true, capture:true });
    window.addEventListener('resize', function(){ if (box.style.display!=='none') position(); });
})();
</script>

<!-- ===== Über OpenNIT Modal ===== -->
<div id="aboutModal" class="modal" hidden role="dialog" aria-modal="true" aria-labelledby="aboutModalTitle">
    <div class="modal-backdrop" data-dialog-hide="aboutModal"></div>
    <div class="modal-dialog" style="max-width:480px">
        <button type="button" class="modal-close"
                data-dialog-hide="aboutModal"
                aria-label="<?= $this->te('Schließen') ?>" style="position:absolute;top:0.75rem;right:0.875rem;z-index:1">&times;</button>

        <div class="about-header">
            <div class="about-app-icon">
            <img src="/assets/img/logo-default.png" alt="OpenNIT" style="max-height:64px;">
        </div>
            <div class="about-app-name" id="aboutModalTitle">OpenNIT</div>
            <div class="about-app-version"><?= $this->t('Version') ?> <?= $this->version() ?></div>
            <div class="about-app-tagline"><?= $this->te('Open Source IT-Dokumentations- und Asset-Management-System') ?></div>
        </div>

        <div class="about-body">

            <!-- Entwickler -->
            <div>
                <div class="about-section-title"><?= $this->te('Entwickler') ?></div>
                <div class="about-row">
                    <span class="about-row-icon"><?= $icon('fa-user', '👤') ?></span>
                    Friederich Loheide
                </div>
                <div class="about-row">
                    <span class="about-row-icon"><?= $icon('fa-globe', '🌐') ?></span>
                    <a href="https://loheide.eu" target="_blank" rel="noopener">loheide.eu</a>
                </div>
                <div class="about-row">
                    <span class="about-row-icon"><?= $icon('fa-envelope', '✉') ?></span>
                    <a href="mailto:info@opennit.de">info@opennit.de</a>
                </div>
            </div>

            <hr class="about-divider">

            <!-- Quellcode -->
            <div>
                <div class="about-section-title"><?= $this->te('Quellcode &amp; Download') ?></div>
                <div class="about-row">
                    <span class="about-row-icon"><?= $icon('fa-save', '💾') ?></span>
                    <a href="https://opennit.de" target="_blank" rel="noopener">opennit.de</a>
                </div>
            </div>

            <hr class="about-divider">

            <!-- Lizenz -->
            <div>
                <div class="about-section-title"><?= $this->te('Lizenz') ?></div>
                <div class="about-row">
                    <span class="about-row-icon"><?= $icon('fa-scroll', '📜') ?></span>
                    <a class="about-license-badge" href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">
                        <?= $icon('fa-balance-scale', '⚖️') ?> AGPL-3.0
                    </a>
                </div>
                <p class="about-license-text">
                    <?= $this->t('Dieses Programm ist freie Software. Du kannst es unter den Bedingungen der GNU Affero General Public License, Version&nbsp;3, weitergeben und/oder verändern. Den zugehörigen Quelltext erhältst du über <a href="https://opennit.de" target="_blank" rel="noopener">opennit.de</a> oder auf Anfrage unter <a href="mailto:info@opennit.de">info@opennit.de</a>.') ?>
                </p>
                <p class="about-license-text">
                    <?= $this->t('Die Veröffentlichung erfolgt in der Hoffnung, dass sie nützlich ist, jedoch <strong>ohne jede Gewährleistung</strong> &ndash; sogar ohne die implizite Gewährleistung der Marktreife oder der Eignung für einen bestimmten Zweck. Einzelheiten regeln die Abschnitte&nbsp;15 und&nbsp;16 der Lizenz.') ?>
                </p>
                <p class="about-license-text">
                    <?= $this->t('Abweichend davon steht das mitgelieferte <strong>WordPress-Plugin</strong> (<code>wordpress-plugin/opennit-connector/</code>) als eigenst&auml;ndiges Werk unter <a href="https://www.gnu.org/licenses/gpl-2.0.html" target="_blank" rel="noopener">GPL&#8209;2.0&#8209;or&#8209;later</a>. Es spricht ausschlie&szlig;lich &uuml;ber HTTP mit OpenNIT und bindet keinen Servercode ein.') ?>
                </p>
            </div>

            <hr class="about-divider">

            <!-- Marken -->
            <div>
                <div class="about-section-title"><?= $this->te('Marken') ?></div>
                <p class="about-license-text">
                    <?= $this->te('Genannte Produkt- und Firmennamen &ndash; etwa UniFi, Dell, HP, HPE, VMware, Hyper&#8209;V, Veeam, Microsoft&nbsp;365, Intune, GLPI oder Trend Vision&nbsp;One &ndash; sind Marken ihrer jeweiligen Inhaber. Sie werden ausschlie&szlig;lich beschreibend verwendet, um anzugeben, mit welchen Systemen OpenNIT zusammenarbeitet.') ?>
                </p>
                <p class="about-license-text">
                    <?= $this->t('Daraus folgt <strong>keine</strong> gesch&auml;ftliche Verbindung, Partnerschaft, Empfehlung, Billigung oder Zertifizierung durch die Markeninhaber.') ?>
                </p>
            </div>

            <hr class="about-divider">

            <div class="about-footer-line">
                <?= $this->te('Copyright &copy; 2025 Friederich Loheide &mdash; Erstellt mit &#9829; in Deutschland') ?>
            </div>

            <div class="about-actions">
                <button type="button" class="btn-report-bug"
                        data-dialog-hide="aboutModal" data-dialog-show="bugReportModal">
                    <?= $icon('fa-bug', '🐛') ?> <?= $this->te('Bug melden') ?>
                </button>
            </div>

        </div>
    </div>
</div>

<!-- ===== Bug-Melden Modal ===== -->
<div id="bugReportModal" class="modal" hidden role="dialog" aria-modal="true" aria-labelledby="bugReportModalTitle">
    <div class="modal-backdrop" data-dialog-hide="bugReportModal"></div>
    <div class="modal-dialog" style="max-width:480px">
        <div class="modal-header">
            <h3 id="bugReportModalTitle"><?= $icon('fa-bug', '🐛') ?> <?= $this->te('Bug melden') ?></h3>
            <button type="button" class="modal-close"
                    data-dialog-hide="bugReportModal" data-dialog-show="aboutModal"
                    aria-label="<?= $this->te('Zurück') ?>">&times;</button>
        </div>
        <div class="modal-body">
            <form class="bug-report-form" id="bugReportForm" data-bug-report-form>
                <div>
                    <label>
                        <?= $this->te('Dein Name') ?> <span><?= $this->te('(optional)') ?></span>
                        <input type="text" id="bugName" placeholder="<?= $this->te('Max Mustermann') ?>" autocomplete="name">
                    </label>
                </div>
                <div>
                    <label>
                        <?= $this->te('Deine E-Mail-Adresse') ?> <span><?= $this->te('(optional, für Rückmeldung)') ?></span>
                        <input type="email" id="bugEmail" placeholder="<?= $this->te('max@example.com') ?>" autocomplete="email">
                    </label>
                </div>
                <div>
                    <label>
                        <?= $this->te('Betreff') ?>
                        <input type="text" id="bugSubject" value="Bug-Meldung – OpenNIT v<?= $this->version() ?>">
                    </label>
                </div>
                <div>
                    <label>
                        <?= $this->te('Beschreibung') ?> <span><?= $this->te('(Was ist passiert? Wie lässt sich der Fehler reproduzieren?)') ?></span>
                        <textarea id="bugDescription" placeholder="<?= $this->te('Ich habe folgendes festgestellt...') ?>"></textarea>
                    </label>
                </div>
                <p class="bug-report-hint">
                    <?= $icon('fa-info-circle', 'ℹ️') ?> <?= $this->t('Durch Klick auf &ldquo;E-Mail öffnen&rdquo; wird dein Standard-E-Mail-Programm geöffnet, um eine vorausgefüllte Nachricht an <strong>info@opennit.de</strong> zu senden.') ?>
                </p>
                <div class="bug-report-actions">
                    <button type="button" class="btn btn-secondary"
                            data-dialog-hide="bugReportModal" data-dialog-show="aboutModal">
                        <?= $this->te('Abbrechen') ?>
                    </button>
                    <button type="submit" class="btn btn-primary"><?= $this->te('E-Mail öffnen &#10148;') ?></button>
                </div>
            </form>
        </div>
    </div>
</div>

<script<?= $this->scriptNonce() ?>>
function openBugReportMail(e) {
    e.preventDefault();
    var name    = document.getElementById('bugName').value.trim();
    var email   = document.getElementById('bugEmail').value.trim();
    var subject = document.getElementById('bugSubject').value.trim() || 'Bug-Meldung – OpenNIT';
    var desc    = document.getElementById('bugDescription').value.trim();

    var body = '';
    if (name)  body += 'Name: ' + name + '\n';
    if (email) body += 'E-Mail: ' + email + '\n';
    body += 'Version: <?= $this->version() ?>' + '\n';
    body += 'URL: ' + window.location.href + '\n';
    body += 'User-Agent: ' + navigator.userAgent + '\n';
    body += '\n--- Fehlerbeschreibung ---\n';
    body += desc || '(keine Beschreibung angegeben)';

    var mailto = 'mailto:info@opennit.de'
        + '?subject=' + encodeURIComponent(subject)
        + '&body='    + encodeURIComponent(body);

    window.location.href = mailto;
    return false;
}
</script>

<!-- ===== Open-Source-Lizenzen Modal ===== -->
<div id="licensesModal" class="modal" hidden role="dialog" aria-modal="true" aria-labelledby="licensesModalTitle">
    <div class="modal-backdrop" data-dialog-hide="licensesModal"></div>
    <div class="modal-dialog modal-dialog-lg">
        <div class="modal-header">
            <h3 id="licensesModalTitle"><?= $this->te('Open-Source-Lizenzen') ?></h3>
            <button type="button" class="modal-close"
                    data-dialog-hide="licensesModal"
                    aria-label="<?= $this->te('Schließen') ?>">&times;</button>
        </div>
        <?php // Inhalt wird erst beim ersten Öffnen aus dem <template> geklont (spart ~540 DOM-Knoten je Seite). ?>
        <div class="modal-body" id="licensesModalBody"></div>
        <template id="licensesModalTpl">

            <!-- Hero / Intro -->
            <div class="license-hero">
                <div class="license-hero-title"><?= $icon('fa-heart', '♥') ?> <?= $this->te('Mit Open Source gebaut') ?></div>
                <p class="license-lead">
                    <?= $this->te('OpenNIT steht auf den Schultern hervorragender freier Software. Die folgenden Bibliotheken, Frameworks und Werkzeuge machen dieses Tool möglich &ndash; allen Autorinnen, Autoren und Contributors gebührt unser ausdrücklicher Dank.') ?>
                </p>
                <p class="license-lead" style="font-size:.86rem;opacity:.85;">
                    <?= $this->t('Aufgeführt sind die direkt eingebundenen Komponenten. Hinzu kommen deren transitive Abhängigkeiten (überwiegend MIT/Apache&#8209;2.0) &ndash; die vollständige Auflistung findet sich in <code>docs/OPEN_SOURCE_LICENSES.md</code> und im Verzeichnis <code>licenses/</code>; die gemeinsamen Lizenz-Volltexte liegen unter <code>licenses/texts/</code>, die Texte der Composer-Pakete zusätzlich im jeweiligen Paket unter <code>vendor/&lt;paket&gt;/LICENSE</code>.') ?>
                </p>
                <div class="license-legend">
                    <?= $this->t('<span class="license-legend-label">Lizenztypen:</span> <span class="license-badge badge-mit">MIT</span> <span class="license-badge badge-apache">Apache-2.0</span> <span class="license-badge badge-bsd">BSD-3-Clause</span> <span class="license-badge badge-lgpl">LGPL</span> <span class="license-badge badge-gpl">GPL</span> <span class="license-badge badge-mpl">MPL-2.0</span> <span class="license-badge badge-ofl">SIL OFL 1.1</span> <span class="license-badge badge-ccby">CC BY 4.0</span>') ?>
                </div>
            </div>

            <!-- Design & Frontend -->
            <div class="license-section">
                <h4 class="license-section-title"><?= $icon('fa-palette', '🎨') ?> <?= $this->te('Design &amp; Frontend') ?> <span class="license-count">8</span></h4>
                <div class="license-grid">

                <article class="license-card license-card--mit">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://adminlte.io" target="_blank" rel="noopener">AdminLTE <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-mit">MIT</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-lightbulb', '💡') ?> <?= $this->te('Design-Inspiration') ?></span>
                    </div>
                    <p class="license-card-desc">
                        <?= $this->t('Das UI-Design dieses Tools ist von AdminLTE inspiriert. Es wurden keine AdminLTE-Dateien eingebunden, jedoch wurden Farbpalette und CSS-Klassennamen (wie <code>.wrapper</code>, <code>.main-sidebar</code>) übernommen. Copyright &copy; 2014–2021 ColorlibHQ.') ?>
                    </p>
                    <details class="license-text">
                        <summary><?= $this->te('MIT-Lizenztext anzeigen') ?></summary>
                        <pre>MIT License

Copyright (c) 2014-2021 ColorlibHQ

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.</pre>
                    </details>
                </article>

                <article class="license-card license-card--multi">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://fontawesome.com" target="_blank" rel="noopener">Font Awesome Free <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-mit">MIT</span>
                        <span class="license-badge badge-ofl">SIL OFL 1.1</span>
                        <span class="license-badge badge-ccby">CC BY 4.0</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-tag', '#') ?> v5.15.4</span>
                        <span class="license-tag"><?= $icon('fa-box', '📦') ?> <?= $this->te('lokal gebündelt') ?></span>
                    </div>
                    <p class="license-card-desc">
                        <?= $this->t('Icon-Font für UI-Symbole (Solid-Stil). Lokal eingebunden als <code>fontawesome.min.css</code>, <code>solid.min.css</code> und <code>fa-solid-900.woff2</code>. Copyright &copy; Fonticons, Inc.<br> <em>Icons:</em> CC BY 4.0 &middot; <em>Schriftdateien:</em> SIL OFL 1.1 &middot; <em>Code/CSS:</em> MIT') ?>
                    </p>
                    <details class="license-text">
                        <summary><?= $this->te('Lizenztexte anzeigen') ?></summary>
                        <pre>--- Code (CSS): MIT License ---

MIT License

Copyright (c) Fonticons, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

--- Schriftdateien (.woff2): SIL Open Font License 1.1 ---

Full text: https://scripts.sil.org/OFL

--- Icons (SVG/JS): Creative Commons Attribution 4.0 International ---

Full text: https://creativecommons.org/licenses/by/4.0/</pre>
                    </details>
                </article>

                <article class="license-card license-card--gpl">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://www.tiny.cloud" target="_blank" rel="noopener">TinyMCE <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-gpl">GPL-2.0-or-later</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-tag', '#') ?> <?= $this->te('jeweils aktuelle Version') ?></span>
                        <span class="license-tag"><?= $icon('fa-puzzle-piece', '🧩') ?> <?= $this->te('optional (via Backend nachladen)') ?></span>
                    </div>
                    <p class="license-card-desc">
                        <?= $this->t('WYSIWYG-Rich-Text-Editor für E-Mail- und Seiten-Templates. Kann im Admin-Bereich optional nachgeladen werden; ohne Installation wird der integrierte wysiwyg.js-Editor verwendet. Nicht im Repository enthalten &ndash; die Installation lädt die jeweils aktuelle Fassung direkt bei npm. <strong>Seit Version&nbsp;7 steht TinyMCE unter GPL-2.0-or-later</strong> (Versionen 5 und 6 waren MIT). Die Kombination mit der AGPL-3.0 dieses Projekts ist zulässig. Copyright &copy; Ephox Corporation DBA Tiny Technologies, Inc.') ?>
                    </p>
                    <details class="license-text">
                        <summary><?= $this->te('Lizenzhinweis anzeigen') ?></summary>
                        <pre><?= $this->te('TinyMCE ab Version 7: GNU General Public License, Version 2 oder später.
TinyMCE Version 5 und 6: MIT License.

Copyright (c) Tiny Technologies, Inc.

Welche Lizenz gilt, richtet sich nach der tatsächlich installierten Fassung.
Die installierte Version steht im Admin-Bereich unter „Editor / TinyMCE".
Der vollständige Lizenztext liegt der heruntergeladenen Installation bei
(public/assets/tinymce/) bzw. unter https://www.gnu.org/licenses/old-licenses/gpl-2.0.html') ?></pre>
                    </details>
                </article>

                <article class="license-card license-card--apache">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://visjs.github.io/vis-network/" target="_blank" rel="noopener"><?= $this->t('vis-network') ?> <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-apache">Apache-2.0</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-tag', '#') ?> v9.1.9</span>
                        <span class="license-tag"><?= $icon('fa-box', '📦') ?> <?= $this->te('lokal gebündelt') ?></span>
                    </div>
                    <p class="license-card-desc"><?= $this->t('Netzwerktopologie-Visualisierung für die UniFi-Übersicht. Lokal als <code>vis-network.min.js</code> eingebunden. Copyright &copy; 2010–2022 Almende B.V. and visjs contributors.') ?></p>
                    <details class="license-text">
                        <summary><?= $this->te('Apache-2.0-Lizenztext anzeigen') ?></summary>
                        <pre>Apache License, Version 2.0

Copyright (c) 2010-2022 Almende B.V. and Contributors
Copyright (c) 2017-2022 visjs contributors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.</pre>
                    </details>
                </article>

                <article class="license-card license-card--apache">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/zxing-js/browser" target="_blank" rel="noopener"><?= $this->t('ZXing Browser') ?> <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-apache">Apache-2.0</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-box', '📦') ?> <?= $this->te('lokal gebündelt') ?></span>
                    </div>
                    <p class="license-card-desc"><?= $this->t('Barcode- und QR-Code-Scanner per Browser-Kamera. Lokal als <code>zxing-browser.min.js</code> eingebunden. Das Bündel vereint <code>@zxing/library</code> (Apache-2.0) und <code>@zxing/browser</code> (MIT). Copyright &copy; 2007&ndash;2014 ZXing Authors sowie ZXing-for-JS-Autoren.') ?></p>
                    <details class="license-text">
                        <summary><?= $this->te('Apache-2.0 + NOTICE anzeigen') ?></summary>
                        <pre>Apache License, Version 2.0

Copyright 2018 ZXing Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

---
NOTICE (required by Apache-2.0 Section 4d):

ZXing JavaScript Library
Copyright 2018 ZXing Authors

This product includes software developed at
ZXing JS (https://github.com/zxing-js/library).

The original Java implementation "ZXing" ("Zebra Crossing") was
developed by: Copyright 2007-2014 ZXing authors
(https://github.com/zxing/zxing).</pre>
                    </details>
                </article>

                <article class="license-card license-card--mit">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/soldair/node-qrcode" target="_blank" rel="noopener"><?= $this->t('node-qrcode') ?> <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-mit">MIT</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-box', '📦') ?> <?= $this->te('lokal gebündelt') ?></span>
                    </div>
                    <p class="license-card-desc">
                        <?= $this->t('Clientseitige QR-Code-Generierung (Inventar-Etiketten, Vault, MFA-Einrichtung). Lokal als <code>qrcode.min.js</code> eingebunden. Das Bündel enthält neben node-qrcode (&copy; 2011 Ryan Day) auch den Encoder <em>qrcode-generator</em> (&copy; 2009 Kazuhiko Arase), <em>dijkstrajs</em> (&copy; 2008 Wyatt Baldwin) und <em>encode-utf8</em> &ndash; alle unter MIT.') ?>
                    </p>
                    <details class="license-text">
                        <summary><?= $this->te('MIT-Lizenztext anzeigen') ?></summary>
                        <pre>MIT License

Copyright (c) 2011 Ryan Day (node-qrcode)
Copyright (c) 2009 Kazuhiko Arase (qrcode-generator)
Copyright (C) 2008 Wyatt Baldwin (dijkstrajs)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.</pre>
                    </details>
                </article>

                <article class="license-card license-card--apache">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/swagger-api/swagger-ui" target="_blank" rel="noopener"><?= $this->t('Swagger UI') ?> <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-apache">Apache-2.0</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-tag', '#') ?> v5.32.6</span>
                        <span class="license-tag"><?= $icon('fa-box', '📦') ?> <?= $this->te('lokal gebündelt') ?></span>
                    </div>
                    <p class="license-card-desc"><?= $this->t('Interaktive REST-API-Dokumentation unter <em>Admin &rarr; API</em> sowie für das API-Auto-Modul. Lokal aus <code>swagger-ui-dist</code> eingebunden und nur authentifiziert ausgeliefert. Copyright &copy; SmartBear Software Inc. Das ausgelieferte Bündel enthält weitere Komponenten &ndash; darunter <strong>DOMPurify</strong> (&copy; Cure53), das <em>wahlweise</em> unter MPL-2.0 <em>oder</em> Apache-2.0 steht; OpenNIT nutzt es unter Apache-2.0. Hinzu kommen ieee754 (BSD-3-Clause), React/react-dom/scheduler (&copy; Meta, MIT) sowie js-yaml, classnames, buffer, safe-buffer, deep-extend, JSON-Patch und repeat-string (MIT). Die vollständigen Hinweise liefert <code>swagger-ui-bundle.js.LICENSE.txt</code> im selben Verzeichnis.') ?></p>
                    <details class="license-text">
                        <summary><?= $this->te('Apache-2.0-Lizenztext anzeigen') ?></summary>
                        <pre>Apache License, Version 2.0

Copyright 2020 SmartBear Software Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.</pre>
                    </details>
                </article>

                <article class="license-card license-card--ofl">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://fonts.google.com/specimen/Source+Sans+Pro" target="_blank" rel="noopener"><?= $this->t('Source Sans Pro') ?> <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-ofl">SIL OFL 1.1</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-box', '📦') ?> <?= $this->te('lokal gebündelt') ?></span>
                        <span class="license-tag"><?= $icon('fa-shield-alt', '🛡') ?> <?= $this->te('DSGVO-konform') ?></span>
                    </div>
                    <p class="license-card-desc">
                        <?= $this->t('Schriftart von Adobe / Paul D. Hunt. Lokal eingebunden via <code>@fontsource/source-sans-pro v5.2.5</code> (Subsets: latin, latin-ext; Gewichte: 300, 400, 600, 700). Kein Laden über Google-Server – DSGVO-konform. Die SIL Open Font License erlaubt freie Nutzung, Einbettung und Weitergabe.') ?>
                    </p>
                </article>

                </div>
            </div>

            <!-- PHP-Backend -->
            <div class="license-section">
                <h4 class="license-section-title"><?= $icon('fa-code', '🐘') ?> <?= $this->te('PHP-Backend (Composer)') ?> <span class="license-count">12</span></h4>
                <div class="license-grid">

                <article class="license-card license-card--lgpl">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/dompdf/dompdf" target="_blank" rel="noopener">dompdf/dompdf <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-lgpl">LGPL-2.1</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-tag', '#') ?> ^3.1.6</span>
                    </div>
                    <p class="license-card-desc"><?= $this->t('HTML-zu-PDF-Konvertierung für Übergabeprotokolle und Exporte. Copyright &copy; Fabien Ménager, Brian Sweeney und Contributors.<br> <em>LGPL-Hinweis: Nutzer haben das Recht, diese Bibliothek durch eine modifizierte Version zu ersetzen – möglich durch Anpassung der <code>composer.json</code> und erneuten Aufruf von <code>composer install</code>.</em>') ?></p>
                    <details class="license-text">
                        <summary><?= $this->te('LGPL-2.1-Lizenztext anzeigen') ?></summary>
                        <pre>GNU Lesser General Public License v2.1 or later (LGPL-2.1-or-later)

Copyright (C) Fabien Ménager, Brian Sweeney and Contributors

This library is free software; you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.

This library is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE.

Full license text: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html</pre>
                    </details>
                </article>

                <article class="license-card license-card--bsd">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/firebase/php-jwt" target="_blank" rel="noopener">firebase/php-jwt <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-bsd">BSD-3-Clause</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-tag', '#') ?> ^7.0</span>
                    </div>
                    <p class="license-card-desc"><?= $this->te('JWT-Erstellung und -Validierung für Authentifizierung und API-Token. Copyright &copy; 2011 Neuman Vong.') ?></p>
                    <details class="license-text">
                        <summary><?= $this->te('BSD-3-Clause-Lizenztext anzeigen') ?></summary>
                        <pre>BSD 3-Clause License

Copyright (c) 2011, Neuman Vong
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
   this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
   may be used to endorse or promote products derived from this software
   without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED.</pre>
                    </details>
                </article>

                <article class="license-card license-card--mit">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/lbuchs/WebAuthn" target="_blank" rel="noopener">lbuchs/webauthn <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-mit">MIT</span>
                    </div>
                    <div class="license-card-meta"><span class="license-tag"><?= $icon('fa-tag', '#') ?> ^2.2</span></div>
                    <p class="license-card-desc"><?= $this->te('WebAuthn/FIDO2-Authentifizierung (Hardware-Sicherheitsschlüssel, Touch ID, Windows Hello). Copyright &copy; 2022 Lukas Buchs.') ?></p>
                </article>

                <article class="license-card license-card--mit">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/thephpleague/oauth2-client" target="_blank" rel="noopener">league/oauth2-client <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-mit">MIT</span>
                    </div>
                    <div class="license-card-meta"><span class="license-tag"><?= $icon('fa-tag', '#') ?> ^2.7</span></div>
                    <p class="license-card-desc"><?= $this->te('OAuth2-Client-Protokoll (Basis für Azure-AD-Anmeldung). Copyright &copy; 2013–2023 Alex Bilbie, The PHP League.') ?></p>
                </article>

                <article class="license-card license-card--mit">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/web-push-libs/web-push-php" target="_blank" rel="noopener">minishlink/web-push <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-mit">MIT</span>
                    </div>
                    <div class="license-card-meta"><span class="license-tag"><?= $icon('fa-tag', '#') ?> ^10.0</span></div>
                    <p class="license-card-desc"><?= $this->te('Browser-Push-Benachrichtigungen (PWA). Copyright &copy; 2016 Louis Lagrange.') ?></p>
                </article>

                <article class="license-card license-card--mit">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/TheNetworg/oauth2-azure" target="_blank" rel="noopener">thenetworg/oauth2-azure <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-mit">MIT</span>
                    </div>
                    <div class="license-card-meta"><span class="license-tag"><?= $icon('fa-tag', '#') ?> ^2.2</span></div>
                    <p class="license-card-desc"><?= $this->te('Microsoft Azure AD / Entra ID Single Sign-On. Copyright &copy; 2016 The Networg.') ?></p>
                </article>

                <article class="license-card license-card--bsd">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/vlucas/phpdotenv" target="_blank" rel="noopener">vlucas/phpdotenv <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-bsd">BSD-3-Clause</span>
                    </div>
                    <div class="license-card-meta"><span class="license-tag"><?= $icon('fa-tag', '#') ?> ^5.5</span></div>
                    <p class="license-card-desc"><?= $this->t('Laden von Konfiguration aus <code>.env</code>-Dateien. Copyright &copy; 2013 Vance Lucas.') ?></p>
                </article>

                <article class="license-card license-card--mit">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/guzzle/guzzle" target="_blank" rel="noopener">guzzlehttp/guzzle <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-mit">MIT</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-tag', '#') ?> 7.15.2</span>
                        <span class="license-tag"><?= $icon('fa-link', '🔗') ?> <?= $this->te('transitiv') ?></span>
                    </div>
                    <p class="license-card-desc"><?= $this->te('PHP HTTP-Client (transitiv via oauth2-client, web-push). Copyright &copy; 2011 Michael Dowling und Contributors.') ?></p>
                </article>

                <article class="license-card license-card--lgpl">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/dompdf/php-svg-lib" target="_blank" rel="noopener">dompdf/php-svg-lib <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-lgpl">LGPL-3.0</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-tag', '#') ?> 1.0.2</span>
                        <span class="license-tag"><?= $icon('fa-link', '🔗') ?> <?= $this->te('transitiv') ?></span>
                    </div>
                    <p class="license-card-desc"><?= $this->t('SVG-Verarbeitung in PDFs (transitiv via dompdf). Copyright &copy; Fabien Ménager und Contributors.<br> <em>LGPL-Hinweis: Ersetzbar durch Anpassung der <code>composer.json</code>.</em>') ?></p>
                </article>

                <article class="license-card license-card--lgpl">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/dompdf/php-font-lib" target="_blank" rel="noopener">dompdf/php-font-lib <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-lgpl">LGPL-2.1</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-tag', '#') ?> 1.0.2</span>
                        <span class="license-tag"><?= $icon('fa-link', '🔗') ?> <?= $this->te('transitiv') ?></span>
                    </div>
                    <p class="license-card-desc"><?= $this->t('Schriftarten-Verarbeitung in PDFs (transitiv via dompdf). Copyright &copy; Fabien Ménager und Contributors.<br> <em>LGPL-Hinweis: Ersetzbar durch Anpassung der <code>composer.json</code>.</em>') ?></p>
                </article>

                <article class="license-card license-card--mit">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/Masterminds/html5-php" target="_blank" rel="noopener">masterminds/html5 <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-mit">MIT</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-tag', '#') ?> 2.10.1</span>
                        <span class="license-tag"><?= $icon('fa-link', '🔗') ?> <?= $this->te('transitiv') ?></span>
                    </div>
                    <p class="license-card-desc"><?= $this->te('HTML5-Parser für dompdf (transitiv). Copyright &copy; 2013 Matt Butcher und Contributors.') ?></p>
                </article>

                <article class="license-card license-card--mit">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/thecodingmachine/safe" target="_blank" rel="noopener">thecodingmachine/safe <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-mit">MIT</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-tag', '#') ?> 3.4.0</span>
                        <span class="license-tag"><?= $icon('fa-link', '🔗') ?> <?= $this->te('transitiv') ?></span>
                    </div>
                    <p class="license-card-desc"><?= $this->te('Fehlerbehandelnde Wrapper für PHP-Standardfunktionen (transitiv via dompdf). Copyright &copy; TheCodingMachine und Contributors.') ?></p>
                </article>

                </div>
            </div>

            <!-- Tools & externe Dienste -->
            <div class="license-section">
                <h4 class="license-section-title"><?= $icon('fa-toolbox', '🧰') ?> <?= $this->te('Werkzeuge &amp; externe Dienste') ?> <span class="license-count">2</span></h4>
                <div class="license-grid">

                <article class="license-card license-card--apache">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://github.com/jgraph/drawio" target="_blank" rel="noopener"><?= $this->t('draw.io / diagrams.net') ?> <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-apache">Apache-2.0</span>
                    </div>
                    <div class="license-card-meta"><span class="license-tag"><?= $icon('fa-server', '🖥') ?> <?= $this->te('self-hosted') ?></span></div>
                    <p class="license-card-desc"><?= $this->t('Netzwerk- und Infrastrukturdiagramm-Editor. Wird separat unter <code>public/drawio/</code> installiert (nicht im Repository enthalten). Copyright &copy; JGraph Ltd.') ?></p>
                    <details class="license-text">
                        <summary><?= $this->te('Apache-2.0-Lizenztext anzeigen') ?></summary>
                        <pre>Apache License, Version 2.0

Copyright 2021 JGraph Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.</pre>
                    </details>
                </article>

                <article class="license-card license-card--gpl">
                    <div class="license-card-top">
                        <span class="license-card-name"><a href="https://www.clamav.net" target="_blank" rel="noopener">ClamAV <?= $icon('fa-external-link-alt', '↗') ?></a></span>
                        <span class="license-badge badge-gpl">GPL-2.0</span>
                    </div>
                    <div class="license-card-meta">
                        <span class="license-tag"><?= $icon('fa-server', '🖥') ?> <?= $this->te('extern, optional') ?></span>
                        <span class="license-tag"><?= $icon('fa-shield-virus', '🦠') ?> <?= $this->te('Virenscan') ?></span>
                    </div>
                    <p class="license-card-desc">
                        <?= $this->t('Optionaler Virenscan beim Datei-Upload im Datei-Explorer. ClamAV läuft als eigenständiger Dienst (<code>clamd</code>) und wird ausschließlich über das Netzwerk-/Socket-Protokoll angesprochen &ndash; es werden <strong>keine ClamAV-Bestandteile gebündelt oder gelinkt</strong>. Aktivierung unter <em>Admin &rarr; Dateien</em>. Copyright &copy; Cisco Systems, Inc.') ?>
                    </p>
                </article>

                </div>
            </div>

            <p class="license-footer-note">
                <?= $icon('fa-cloud', '☁') ?>
                <?= $this->t('<strong>Externe Dienste:</strong> Einzelne, optionale Funktionen rufen externe APIs auf, statt Software zu bündeln &ndash; u.&nbsp;a. <strong>KI-Anbieter</strong> (Anthropic, OpenAI, DeepSeek oder OpenAI-kompatibel) für den KI-Chat (API-Auto nutzt Anthropic, inkl. optionaler Web-Suche) und die <strong>Microsoft Graph API</strong> (Microsoft 365 / Intune / Lizenzsync). Diese Dienste sind nur aktiv, wenn sie konfiguriert wurden. Vollständige Attributionen &amp; Lizenztexte liegen jeder Installation bei: <code>NOTICE.md</code> und <code>licenses/</code> im Programmverzeichnis &ndash; die gemeinsamen Lizenz-Volltexte (Apache-2.0, LGPL, GPL, MPL-2.0, SIL&nbsp;OFL&nbsp;1.1) unter <code>licenses/texts/</code>. Den Quelltext erhältst du über <a href="https://opennit.de" target="_blank" rel="noopener">opennit.de</a>.') ?>
            </p>

        </template><!-- /#licensesModalTpl -->
    </div><!-- /.modal-dialog -->
</div><!-- /#licensesModal -->

<script<?= $this->scriptNonce() ?>>
// Lizenz-Inhalt erst beim ersten Öffnen in den Modal-Body klonen.
// Aufruf über data-Attribut statt Ereignisattribut – die Tresor-Seiten laufen
// ohne 'unsafe-inline'.
document.addEventListener('click', function (e) {
    if (e.target.closest('[data-act-oss]')) { openOssLicenses(); }
});
document.addEventListener('submit', function (e) {
    var f = e.target;
    if (f && f.hasAttribute && f.hasAttribute('data-bug-report-form') && openBugReportMail(e) === false) {
        e.preventDefault();
    }
});

function openOssLicenses() {
    var modal = document.getElementById('licensesModal');
    var body  = document.getElementById('licensesModalBody');
    var tpl   = document.getElementById('licensesModalTpl');
    if (body && tpl && !body.firstChild) {
        body.appendChild(tpl.content.cloneNode(true));
    }
    if (modal) { modal.removeAttribute('hidden'); }
}
</script>

<?php include __DIR__ . '/../partials/_attachment_modal.php'; ?>

<?php
// ============================================================
// Schnellere Navigation: interne Links beim Hovern vorausladen
// (Speculation Rules API, Chrome/Edge). Holt das Ziel-HTML "just in time"
// kurz vor dem Klick -> fühlt sich sofort an, OHNE dauerhaftes Cachen.
// Browser ohne Unterstützung ignorieren den Block (kein Nachteil).
//
// Echtzeit bleibt erhalten: Module mit Live-Daten (Tickets, Monitoring, XDR,
// Dashboard) sind ausgenommen und werden immer frisch beim Klick geladen.
// Einzelne Links lassen sich zusätzlich per data-no-prefetch ausschließen.
// ============================================================
?>
<script<?= $this->scriptNonce() ?> type="speculationrules">
{
  "prefetch": [
    {
      "source": "document",
      "eagerness": "moderate",
      "where": {
        "and": [
          { "href_matches": "/*" },
          { "not": { "href_matches": "/logout" } },
          { "not": { "href_matches": "/login" } },
          { "not": { "href_matches": "/api/*" } },
          { "not": { "href_matches": "/tickets*" } },
          { "not": { "href_matches": "/monitoring*" } },
          { "not": { "href_matches": "/xdr*" } },
          { "not": { "href_matches": "/dashboard*" } },
          { "not": { "selector_matches": "[data-no-prefetch], [target=\"_blank\"], [download], [href^=\"mailto:\"], [href*=\"/export\"], [href*=\"/download\"], [href*=\"/pdf\"]" } }
        ]
      }
    }
  ]
}
</script>

<?php if (!$_useFA): ?>
<script<?= $this->scriptNonce() ?>>
/* Globaler Icon-Umschalter: ersetzt im Emoji-Modus alle Font-Awesome-Icons
   (<i class="fa-…">) durch das gemappte Emoji – modulübergreifend und auch für
   nachgeladene Inhalte (MutationObserver). Nicht gemappte Icons bleiben als
   Font-Awesome-Icon erhalten. Quelle der Map: src/Core/IconMap.php. */
(function () {
    var MAP = <?= json_encode($_iconMap, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
    var SKIP = { 'fa-solid':1,'fas':1,'fa-regular':1,'far':1,'fa-brands':1,'fab':1,'fa':1 };

    function emojiFor(el) {
        var cls = (el.getAttribute('class') || '').split(/\s+/);
        for (var i = 0; i < cls.length; i++) {
            var c = cls[i];
            if (c.indexOf('fa-') !== 0 || SKIP[c]) continue;
            if (/^fa-(fw|lg|sm|xs|spin|pulse|stack|inverse|border|pull-left|pull-right|rotate-\d+|flip-\w+|\d+x)$/.test(c)) continue;
            if (MAP[c]) return MAP[c];
        }
        return null;
    }

    function convert(el) {
        // Animierte Icons (Spinner) als Font-Awesome belassen.
        var className = el.getAttribute('class') || '';
        if (/\bfa-(spin|pulse)\b/.test(className)) return;
        var em = emojiFor(el);
        if (!em) return;
        var span = document.createElement('span');
        span.className = 'emoji-icon';
        span.setAttribute('aria-hidden', 'true');
        span.textContent = em;
        if (el.title) span.title = el.title;
        if (el.style && el.style.cssText) span.style.cssText = el.style.cssText;
        el.parentNode.replaceChild(span, el);
    }

    function scan(root) {
        if (!root || !root.querySelectorAll) return;
        var nodes = root.querySelectorAll('i.fa-solid, i.fas, i.fa-regular, i.far, i.fa-brands, i.fab, i.fa');
        for (var i = 0; i < nodes.length; i++) convert(nodes[i]);
    }

    function run() {
        scan(document.body);
        try {
            new MutationObserver(function (muts) {
                for (var m = 0; m < muts.length; m++) {
                    var added = muts[m].addedNodes;
                    for (var n = 0; n < added.length; n++) {
                        var node = added[n];
                        if (node.nodeType !== 1) continue;
                        if (node.matches && node.matches('i.fa-solid, i.fas, i.fa-regular, i.far, i.fa-brands, i.fab, i.fa')) {
                            convert(node);
                        } else {
                            scan(node);
                        }
                    }
                }
            }).observe(document.body, { childList: true, subtree: true });
        } catch (e) { /* MutationObserver optional */ }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', run);
    } else {
        run();
    }
})();
</script>
<?php endif; ?>

</body>
</html>