OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

95 KB
<?php
$_dashCss = ['/assets/css/dashboard.css?v=' . filemtime(dirname(__DIR__, 2) . '/public/assets/css/dashboard.css')];
// Das Notizbrett-Widget nutzt die Stile seines Moduls; ohne sie steht es
// unformatiert auf der Seite.
if (($modules['teamboard'] ?? false) && in_array('teamboard', $all_widgets ?? [], true)) {
    $_dashCss[] = '/assets/css/teamboard.css';
}

$this->layout('layouts/main', [
    'page_title' => 'Dashboard',
    'full_width' => true,
    'extra_css'  => $_dashCss,
]);

$_useFA   = ($icon_style ?? 'fontawesome') === 'fontawesome';
// Zentrale Icon-Map (eine Quelle für PHP-Helper und globalen JS-Umschalter).
$_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>');
};

$pinnedServices = $pinned_services ?? [];
$hasViewServices = $has_view_services ?? false;
$openOrders        = $open_orders        ?? [];
$deliveredOrders   = $delivered_orders   ?? [];
$abholbereitOrders = $abholbereit_orders ?? [];
$abholbereitEnabled = $abholbereit_enabled ?? false;
$orderCounts       = $order_counts       ?? ['open' => 0, 'delivered' => 0, 'abholbereit' => 0];
$hasManageOrders   = $has_manage_orders  ?? false;
$allOrders = array_merge($openOrders, $deliveredOrders, $abholbereitOrders);
$loanDevices  = $loan_devices   ?? [];
$loanSummary  = $loan_summary   ?? [];
$hasViewLoans = $has_view_loans ?? false;
$widgetsByColumn = $widgets_by_column ?? [1 => [], 2 => [], 3 => []];
$hiddenWidgets   = $hidden_widgets   ?? [];
$allWidgets      = $all_widgets      ?? ['pinned_services','open_orders','pending_reviews','quick_actions','my_todos','loan_devices','my_tickets','my_kanban_tasks','monitoring','maintenance_upcoming','phonebook_search','cve_security','contracts_expiring','workflow_approvals','teamboard'];
// Rückwärtskompatibilität: Spalten-Map für das Konfig-Panel aufbauen
$widgetColumnMap = [];
foreach ([1,2,3] as $_col) {
    foreach ($widgetsByColumn[$_col] as $_w) { $widgetColumnMap[$_w] = $_col; }
}
$teamboardNotes   = $teamboard_notes    ?? [];
$myTodos          = $my_todos           ?? [];
$myTodosDoneCount = $my_todos_done_count ?? 0;
$myKanbanTasks    = $my_kanban_tasks    ?? [];
$hasViewKanban    = $has_view_kanban    ?? false;
$monitoringEvents = $monitoring_events  ?? [];
$monitoringCounts = $monitoring_counts  ?? ['active' => 0, 'critical' => 0];
$hasViewMonitoring= $has_view_monitoring?? false;
$backupCounts     = $backup_counts      ?? ['total' => 0, 'failed' => 0, 'warning' => 0];
$backupJobs       = $backup_jobs        ?? [];
$hasViewBackup    = $has_view_backup    ?? false;
$contractsExpiring = $contracts_expiring ?? [];
$maintenanceUpcoming    = $maintenance_upcoming     ?? [];
$maintenanceUrgentCount = $maintenance_urgent_count ?? 0;
$hasViewMaintenance     = $has_view_maintenance     ?? false;
$maintenanceSources     = $maintenance_sources      ?? ['manual', 'ssl', 'contract'];
$dashboardColumns    = max(1, min(3, (int)($dashboard_columns ?? 2)));
$iconBasePath        = '/uploads/service-icons/';
$availableQuickLinks = $available_quick_links  ?? [];
$activeQuickLinks    = $active_quick_links     ?? [];
$activeQuickLinkKeys = $active_quick_link_keys ?? array_keys($availableQuickLinks);
$quickAccessCols     = max(2, min(3, (int)($quick_access_cols ?? 2)));
$unifiVoucherSites      = $unifi_voucher_sites       ?? [];
$hasManageUnifiVouchers = $has_manage_unifi_vouchers ?? false;
$dashModules            = $modules                   ?? [];
$unifiModuleEnabled     = (bool)($dashModules['unifi'] ?? false);

// Widget-Labels für Konfig-Panel
$widgetLabels = [
    'pinned_services'      => [$icon('fa-thumbtack', '📌') . ' ' . $this->t('Meine Pins'),              $this->t('Angepinnte Services')],
    'open_orders'          => [$icon('fa-shopping-cart', '🛒') . ' ' . $this->t('Bestellungen'),        $this->t('Offene Bestellungen')],
    'pending_reviews'      => [$icon('fa-exclamation-triangle', '⚠️') . ' ' . $this->t('Anstehende Reviews'), $this->t('Fällige Seitenreviews')],
    'quick_actions'        => [$icon('fa-bolt', '🚀') . ' ' . $this->t('Schnellzugriff'),               $this->t('Schnellzugriff-Grid')],
    'loan_devices'         => [$icon('fa-laptop', '💻') . ' ' . $this->t('Leihger&auml;te'),            $this->t('Ger&auml;testatus-&Uuml;bersicht')],
    'my_tickets'           => [$icon('fa-ticket-alt', '🎫') . ' ' . $this->t('Meine Tickets'),              $this->t('Neue &amp; mir zugewiesene Tickets')],
    'my_kanban_tasks'      => [$icon('fa-clipboard-list', '📋') . ' ' . $this->t('Meine Aufgaben'),     $this->t('Mir zugewiesene Kanban-Aufgaben')],
    'workflow_approvals'   => [$icon('fa-clipboard-check', '✅') . ' ' . $this->t('Genehmigungen'),      $this->t('Offene Anträge, die auf mich warten')],
    'my_todos'             => [$icon('fa-check-square', '✅') . ' ' . $this->t('To-Do-Liste'),           $this->t('Pers&ouml;nliche Aufgabenliste')],
    'teamboard'            => [$icon('fa-sticky-note', '🗒️') . ' ' . $this->t('Notizbrett'),          $this->t('Die letzten Zurufe im Team')],
    'monitoring'           => [$icon('fa-chart-bar', '📊') . ' ' . $this->t('Monitoring'),              $this->t('Zabbix-Probleme &amp; Alarme')],
    'backup'               => [$icon('fa-database', '💾') . ' ' . $this->t('Backup'),                    $this->t('Veeam &amp; Proxmox Status')],
    'maintenance_upcoming' => [$icon('fa-calendar', '📅') . ' ' . $this->t('Wartungstermine'),          $this->t('N&auml;chste F&auml;lligkeiten (60 Tage)')],
    'phonebook_search'     => [$icon('fa-phone', '📞') . ' ' . $this->t('Telefonbuch'),                 $this->t('Personen schnell suchen')],
    'cve_security'         => [$icon('fa-shield-alt', '🛡') . ' ' . $this->t('CVE-Meldungen'),              $this->t('Aktuelle Sicherheitsl&uuml;cken')],
    'unifi_vouchers'       => [$icon('fa-wifi', '📶') . ' ' . $this->t('UniFi Voucher'),                $this->t('Gast-Voucher direkt erstellen')],
    'contracts_expiring'   => [$icon('fa-file-contract', '📄') . ' ' . $this->t('Kündigungsfristen'),   $this->t('Vertr&auml;ge mit Frist in 60 Tagen')],
];
?>

<div class="dashboard-fullwidth">
    <!-- Content Header -->
    <?php
    $displayName = $current_user->display_name ?? 'Benutzer';
    $firstName = $displayName;
    if (str_contains($displayName, ',')) {
        $parts = explode(',', $displayName, 2);
        $firstName = trim($parts[1] ?? $displayName);
    } elseif (str_contains($displayName, ' ')) {
        $parts = explode(' ', $displayName, 2);
        $firstName = trim($parts[0]);
    }
    ?>
    <div class="content-header">
        <div>
            <h1><?= $this->te('Willkommen') ?>, <?= $this->e($firstName) ?></h1>
            <ol class="breadcrumb">
                <li class="breadcrumb-item active"><?= $this->te('Dashboard') ?></li>
            </ol>
        </div>
        <div class="content-header-actions">
            <a href="/topics" class="btn btn-default btn-sm"><?= $this->te('Themengebiete') ?></a>
            <button class="btn btn-default btn-sm" onclick="openWidgetConfig()" title="<?= $this->te('Dashboard anpassen') ?>">
                <?= $icon('fa-cog', '⚙️') ?> <?= $this->te('Anpassen') ?>
            </button>
        </div>
    </div>

    <div class="dashboard-body">
        <!-- Widget-Grid (3 explizite Spalten, per Drag & Drop verschiebbar) -->
        <div class="dashboard-grid" id="widgetGrid" style="grid-template-columns:repeat(<?= $dashboardColumns ?>,1fr)">

        <?php foreach ([1, 2, 3] as $_colNum): ?>
        <div class="dashboard-col" id="dashCol<?= $_colNum ?>" data-col="<?= $_colNum ?>">
        <?php foreach ($widgetsByColumn[$_colNum] as $widgetId): ?>
        <?php switch($widgetId):

            case 'pinned_services': ?>
            <?php if ($hasViewServices && !empty($pinnedServices)): ?>
            <section class="dashboard-card" data-widget="pinned_services">
                <div class="card-header">
                    <div style="display:flex;align-items:center;gap:0.5rem;">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <h2><?= $icon('fa-thumbtack', '📌') ?> <?= $this->te('Meine Pins') ?></h2>
                    </div>
                    <a href="/services" class="card-link"><?= $this->te('Alle Services &rarr;') ?></a>
                </div>
                <div class="card-body">
                    <div class="pin-grid" id="pinGrid">
                        <?php foreach ($pinnedServices as $s): ?>
                        <a href="<?= $this->e($s['url']) ?>" target="_blank" rel="noopener"
                           class="pin-tile" draggable="true" data-service-id="<?= $s['id'] ?>">
                            <div class="pin-drag-handle" title="<?= $this->te('Ziehen zum Sortieren') ?>">&#8942;&#8942;</div>
                            <div class="pin-icon-wrap">
                                <?php if (!empty($s['icon'])): ?>
                                    <img src="<?= $iconBasePath . $this->e($s['icon']) ?>" alt="" class="pin-icon-img">
                                <?php else: ?>
                                    <span class="pin-icon-letter" <?php if (!empty($s['category_color'])): ?>style="background:<?= $this->e($s['category_color']) ?>"<?php endif; ?>>
                                        <?= strtoupper(mb_substr($s['name'], 0, 1)) ?>
                                    </span>
                                <?php endif; ?>
                            </div>
                            <div class="pin-name"><?= $this->e($s['name']) ?></div>
                            <?php if ($s['reachability']): ?>
                                <span class="pin-reach"><?= $s['reachability'] === 'internal' ? $icon('fa-building', '🏢') : $icon('fa-globe', '🌐') ?></span>
                            <?php endif; ?>
                        </a>
                        <?php endforeach; ?>
                    </div>
                </div>
            </section>
            <?php endif; ?>
            <?php break; ?>

            <?php case 'open_orders': ?>
            <?php if ($hasManageOrders && !empty($allOrders)): ?>
            <section class="dashboard-card" data-widget="open_orders">
                <div class="card-header">
                    <div style="display:flex;align-items:center;gap:0.5rem;">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <h2><?= $icon('fa-shopping-cart', '🛒') ?> <?= $this->te('Bestellungen') ?></h2>
                    </div>
                    <a href="/orders" class="card-link"><?= $this->te('Bestell-Board &rarr;') ?></a>
                </div>
                <div class="card-body">
                    <?php if (!empty($openOrders)): ?>
                    <div class="order-group-label"><?= $this->te('Offen') ?> (<?= count($openOrders) ?>)</div>
                    <div class="dash-order-list">
                        <?php foreach (array_slice($openOrders, 0, 5) as $o):
                            $total = (int)($o['comp_total'] ?? 0);
                            $done = (int)($o['comp_done'] ?? 0);
                            $pct = ($total > 0) ? (int)floor($done * 100 / $total) : 0;
                        ?>
                        <a href="/orders?tab=open#order-<?= $o['id'] ?>" class="dash-order-item">
                            <span class="dash-order-dot open"></span>
                            <span class="dash-order-title"><?= $this->e($o['title']) ?></span>
                            <span class="dash-order-progress">
                                <span class="dash-progress-bar"><span class="dash-progress-fill" style="width:<?= $pct ?>%"></span></span>
                                <span class="dash-progress-text"><?= $done ?>/<?= $total ?></span>
                            </span>
                        </a>
                        <?php endforeach; ?>
                        <?php if (count($openOrders) > 5): ?>
                            <div class="dash-order-more"><?= $this->te('+ {anzahl} weitere', ['anzahl' => count($openOrders) - 5]) ?></div>
                        <?php endif; ?>
                    </div>
                    <?php endif; ?>

                    <?php if (!empty($deliveredOrders)): ?>
                    <div class="order-group-label" <?= !empty($openOrders) ? 'style="margin-top:1rem;"' : '' ?>><?= $this->te('Eingetroffen') ?> (<?= count($deliveredOrders) ?>)</div>
                    <div class="dash-order-list">
                        <?php foreach (array_slice($deliveredOrders, 0, 5) as $o): ?>
                        <a href="/orders?tab=delivered#order-<?= $o['id'] ?>" class="dash-order-item delivered">
                            <span class="dash-order-dot delivered"></span>
                            <span class="dash-order-title"><?= $this->e($o['title']) ?></span>
                            <span class="dash-order-badge"><?= $icon('fa-check', '✓') ?> <?= $this->te('Bereit') ?></span>
                        </a>
                        <?php endforeach; ?>
                        <?php if (count($deliveredOrders) > 5): ?>
                            <div class="dash-order-more"><?= $this->te('+ {anzahl} weitere', ['anzahl' => count($deliveredOrders) - 5]) ?></div>
                        <?php endif; ?>
                    </div>
                    <?php endif; ?>

                    <?php if ($abholbereitEnabled && !empty($abholbereitOrders)): ?>
                    <div class="order-group-label" style="margin-top:1rem;"><?= $this->te('Abholbereit') ?> (<?= count($abholbereitOrders) ?>)</div>
                    <div class="dash-order-list">
                        <?php foreach (array_slice($abholbereitOrders, 0, 5) as $o): ?>
                        <a href="/orders?tab=abholbereit#order-<?= $o['id'] ?>" class="dash-order-item abholbereit">
                            <span class="dash-order-dot abholbereit"></span>
                            <span class="dash-order-title"><?= $this->e($o['title']) ?></span>
                            <span class="dash-order-badge" style="color:var(--violet-500);"><?= $icon('fa-box-open', '📦') ?> <?= $this->te('Abholbereit') ?></span>
                        </a>
                        <?php endforeach; ?>
                        <?php if (count($abholbereitOrders) > 5): ?>
                            <div class="dash-order-more"><?= $this->te('+ {anzahl} weitere', ['anzahl' => count($abholbereitOrders) - 5]) ?></div>
                        <?php endif; ?>
                    </div>
                    <?php endif; ?>
                </div>
            </section>
            <?php endif; ?>
            <?php break; ?>

            <?php case 'pending_reviews': ?>
            <?php if (!empty($pending_reviews)): ?>
            <section class="dashboard-card attention" data-widget="pending_reviews">
                <div class="card-header">
                    <div style="display:flex;align-items:center;gap:0.5rem;">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <h2><?= $icon('fa-exclamation-triangle', '⚠️') ?> <?= $this->te('Anstehende Reviews') ?></h2>
                    </div>
                    <a href="/my-responsibilities" class="card-link"><?= $this->te('Alle &rarr;') ?></a>
                </div>
                <div class="card-body">
                    <div class="review-list">
                        <?php foreach ($pending_reviews as $review): ?>
                        <?php $isOverdue = strtotime($review['next_review_date']) < time(); ?>
                        <a href="/pages/<?= $review['id'] ?>/responsibilities" class="review-item <?= $isOverdue ? 'overdue' : '' ?>">
                            <div class="review-content">
                                <div class="review-title"><?= $this->e($review['title']) ?></div>
                                <div class="review-date">
                                    <?php if ($isOverdue): ?>
                                    <span class="text-danger"><?= $this->te('&Uuml;berf&auml;llig seit') ?> <?= date('d.m.Y', strtotime($review['next_review_date'])) ?></span>
                                    <?php else: ?>
                                    <?= $this->te('F&auml;llig am') ?> <?= date('d.m.Y', strtotime($review['next_review_date'])) ?>
                                    <?php endif; ?>
                                </div>
                            </div>
                            <span class="review-badge <?= $isOverdue ? 'badge-danger' : 'badge-warning' ?>">
                                <?= $isOverdue ? '!' : '?' ?>
                            </span>
                        </a>
                        <?php endforeach; ?>
                    </div>
                </div>
            </section>
            <?php endif; ?>
            <?php break; ?>

            <?php case 'quick_actions': ?>
            <section class="dashboard-card" data-widget="quick_actions">
                <div class="card-header">
                    <div style="display:flex;align-items:center;gap:0.5rem;">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <h2><?= $icon('fa-bolt', '🚀') ?> <?= $this->te('Schnellzugriff') ?></h2>
                    </div>
                    <button onclick="openQAConfig()" class="card-link" style="background:none;border:none;cursor:pointer;" title="<?= $this->te('Schnellzugriff anpassen') ?>"><?= $icon('fa-cog', '⚙️') ?> <?= $this->te('Anpassen') ?></button>
                </div>
                <div class="card-body">
                    <div class="quick-actions-grid" id="qaGrid" style="grid-template-columns:repeat(<?= $quickAccessCols ?>,1fr)">
                        <?php foreach ($activeQuickLinks as $ql): ?>
                        <a href="<?= $this->e($ql['url']) ?>" class="quick-action-card" data-qakey="<?= $this->e($ql['key']) ?>">
                            <span class="qa-icon"><?= $icon($ql['icon']) ?></span>
                            <span class="qa-label"><?= $this->te($ql['label']) ?></span>
                            <span class="qa-desc"><?= $this->te($ql['desc']) ?></span>
                        </a>
                        <?php endforeach; ?>
                        <?php if (empty($activeQuickLinks)): ?>
                        <div style="grid-column:1/-1;text-align:center;padding:1.5rem;color:var(--gray-400);font-size:0.82rem;">
                            <?= $this->te('Keine Einträge ausgewählt.') ?><br>
                            <button onclick="openQAConfig()" style="margin-top:0.5rem;background:none;border:none;color:var(--color-primary);cursor:pointer;font-size:0.82rem;text-decoration:underline;"><?= $this->te('Jetzt einrichten') ?></button>
                        </div>
                        <?php endif; ?>
                    </div>
                </div>
            </section>
            <?php break; ?>

            <?php case 'loan_devices': ?>
            <?php if ($hasViewLoans): ?>
            <section class="dashboard-card" data-widget="loan_devices">
                <div class="card-header">
                    <div style="display:flex;align-items:center;gap:0.5rem;">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <h2><?= $icon('fa-laptop', '💻') ?> <?= $this->te('Leihger&auml;te') ?></h2>
                    </div>
                    <a href="/loans" class="card-link"><?= $this->te('Kalender &rarr;') ?></a>
                </div>
                <div class="card-body">
                    <?php if (!empty($loanSummary)): ?>
                    <div class="loan-kpi-row">
                        <span class="loan-kpi" style="--kc:var(--gray-500);">
                            <span class="loan-kpi-val"><?= (int)($loanSummary['total'] ?? 0) ?></span>
                            <span class="loan-kpi-lbl"><?= $this->te('Gesamt') ?></span>
                        </span>
                        <span class="loan-kpi" style="--kc:var(--green-600);">
                            <span class="loan-kpi-val"><?= (int)($loanSummary['available'] ?? 0) ?></span>
                            <span class="loan-kpi-lbl"><?= $this->te('Verf&uuml;gbar') ?></span>
                        </span>
                        <span class="loan-kpi" style="--kc:var(--red-600);">
                            <span class="loan-kpi-val"><?= (int)($loanSummary['lent'] ?? 0) ?></span>
                            <span class="loan-kpi-lbl"><?= $this->te('Ausgeliehen') ?></span>
                        </span>
                        <span class="loan-kpi" style="--kc:var(--amber-600);">
                            <span class="loan-kpi-val"><?= (int)($loanSummary['reserved'] ?? 0) ?></span>
                            <span class="loan-kpi-lbl"><?= $this->te('Reserviert') ?></span>
                        </span>
                        <?php if (($loanSummary['overdue'] ?? 0) > 0): ?>
                        <span class="loan-kpi" style="--kc:var(--red-600);">
                            <span class="loan-kpi-val"><?= $icon('fa-exclamation-triangle', '⚠️') ?> <?= (int)$loanSummary['overdue'] ?></span>
                            <span class="loan-kpi-lbl"><?= $this->te('Überfällig') ?></span>
                        </span>
                        <?php endif; ?>
                    </div>
                    <?php endif; ?>

                    <?php if (empty($loanDevices)): ?>
                        <p style="font-size:0.85rem;color:var(--gray-400);text-align:center;padding:1rem 0;"><?= $this->te('Keine Leihgeräte vorhanden.') ?></p>
                    <?php else: ?>
                    <div class="loan-device-list">
                        <?php foreach ($loanDevices as $dev):
                            $status = $dev['status'];
                            $statusCfg = [
                                'available'   => ['Verfügbar',    '#16a34a', '#dcfce7'],
                                'lent'        => ['Ausgeliehen',  '#dc2626', '#fee2e2'],
                                'reserved'    => ['Reserviert',   '#d97706', '#fef3c7'],
                                'maintenance' => ['Wartung',      '#7c3aed', '#ede9fe'],
                            ];
                            [$slabel, $scol, $sbg] = $statusCfg[$status] ?? ['Unbekannt','#6b7280','#f3f4f6'];
                            $activeLend   = $dev['active_lending'] ?? null;
                            $nextRes      = $dev['next_reservation'] ?? null;
                            $isOverdue    = $activeLend && $activeLend['planned_end'] < date('Y-m-d');
                            if ($isOverdue) { $slabel = 'Überfällig'; $scol = '#fff'; $sbg = '#dc2626'; }
                        ?>
                        <a href="/loans/devices/<?= $dev['id'] ?>" class="loan-device-row">
                            <div class="loan-device-icon">
                                <?php if (!empty($dev['image_path'])): ?>
                                    <img src="<?= $this->e($dev['image_path']) ?>" alt="">
                                <?php else: ?>
                                    <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#9ca3af" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
                                <?php endif; ?>
                            </div>
                            <div class="loan-device-info">
                                <div class="loan-device-name"><?= $this->e($dev['title']) ?></div>
                                <?php if ($activeLend): ?>
                                    <div class="loan-device-sub">
                                        <span style="color:<?= $isOverdue ? 'var(--red-600)' : 'var(--red-600)' ?>;"><?= $icon('fa-user', '👤') ?> <?= $this->e($activeLend['borrower_name']) ?></span>
                                        &mdash; <?= $isOverdue ? '<strong style="color:var(--red-600);">' . $icon('fa-exclamation-triangle', '⚠️') . ' seit ' . date('d.m.Y', strtotime($activeLend['planned_end'])) . ' überfällig</strong>' : 'bis ' . date('d.m.Y', strtotime($activeLend['planned_end'])) ?>
                                    </div>
                                <?php elseif ($nextRes && $status === 'available'): ?>
                                    <div class="loan-device-sub" style="color:var(--amber-600);">
                                        <?= $icon('fa-calendar', '📅') ?> <?= $this->te('Reserviert ab') ?> <?= date('d.m.Y', strtotime($nextRes['planned_start'])) ?>
                                        (<?= $this->e($nextRes['borrower_name']) ?>)
                                    </div>
                                <?php elseif ($status === 'reserved' && $nextRes): ?>
                                    <div class="loan-device-sub" style="color:var(--amber-600);">
                                        <?= $icon('fa-calendar', '📅') ?> <?= $this->e($nextRes['borrower_name']) ?>
                                        <?= $this->te('ab') ?> <?= date('d.m.Y', strtotime($nextRes['planned_start'])) ?>
                                    </div>
                                <?php else: ?>
                                    <div class="loan-device-sub" style="color:var(--gray-500);"><?= $this->e($dev['type'] ?? '') ?></div>
                                <?php endif; ?>
                                <?php if (!empty($dev['pending_count'])): ?>
                                    <div class="loan-device-sub" style="color:var(--blue-700);">
                                        <?= $icon('fa-hourglass-half', '⏳') ?> <?= (int)$dev['pending_count'] ?> <?= $this->te((int)$dev['pending_count'] === 1 ? 'Anfrage – Freigabe offen' : 'Anfragen – Freigabe offen') ?>
                                    </div>
                                <?php endif; ?>
                            </div>
                            <span class="loan-device-badge" style="background:<?= $sbg ?>;color:<?= $scol ?>;"><?= $slabel ?></span>
                        </a>
                        <?php endforeach; ?>
                    </div>
                    <?php if ($this->hasCapability('issue_loan_devices')): ?>
                    <div style="margin-top:0.75rem;text-align:right;">
                        <a href="/loans/issue" style="font-size:0.82rem;color:var(--amber-600);font-weight:600;"><?= $this->te('+ Ger&auml;t ausgeben') ?></a>
                    </div>
                    <?php endif; ?>
                    <?php endif; ?>
                </div>
            </section>
            <?php endif; ?>
            <?php break; ?>

            <?php case 'my_tickets': ?>
            <?php
            $myTickets      = $my_tickets      ?? [];
            $myTicketCounts = $my_ticket_counts ?? ['new' => 0, 'active' => 0];
            $hasViewTickets = $has_view_tickets ?? false;
            ?>
            <?php if ($hasViewTickets): ?>
            <?= $this->render('dashboard/_widget_tickets', [
                'my_tickets'       => $myTickets,
                'my_ticket_counts' => $myTicketCounts,
                'has_view_tickets' => $hasViewTickets,
                'icon_style'       => $icon_style ?? 'fontawesome',
            ]) ?>
            <script>
            // Auto-Refresh für "Meine Tickets" – fragt den schlanken Endpoint
            // /dashboard/tickets-widget ab (rendert nur diese Section, nicht das
            // komplette Dashboard) und tauscht die Section aus. Läuft NUR, wenn
            // das Widget gerendert wurde und der Tab sichtbar ist.
            (function () {
                var section = document.getElementById('wMyTickets');
                if (!section || window._wMyTicketsPollStarted) return;
                window._wMyTicketsPollStarted = true;

                var INTERVAL = 30000;

                function refresh() {
                    if (document.visibilityState === 'hidden') return;
                    var current = document.getElementById('wMyTickets');
                    if (!current) return; // Widget wurde inzwischen ausgeblendet
                    fetch('/dashboard/tickets-widget', {
                        headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'text/html' },
                        credentials: 'same-origin'
                    })
                    .then(function (r) { return r.ok ? r.text() : null; })
                    .then(function (html) {
                        if (!html) return;
                        var doc  = new DOMParser().parseFromString(html, 'text/html');
                        var next = doc.getElementById('wMyTickets');
                        var cur  = document.getElementById('wMyTickets');
                        if (next && cur) cur.replaceWith(next);
                    })
                    .catch(function () { /* nächster Tick versucht es erneut */ });
                }

                setInterval(refresh, INTERVAL);
                // Beim Wiedereinblenden des Tabs: sofort einmal aktualisieren.
                document.addEventListener('visibilitychange', function () {
                    if (document.visibilityState === 'visible') refresh();
                });
            })();
            </script>
            <?php endif; ?>
            <?php break; ?>

            <?php case 'teamboard': ?>
            <section class="dashboard-card" data-widget="teamboard" id="wTeamboard">
                <div class="card-header">
                    <div style="display:flex;align-items:center;gap:0.5rem;">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <h2><?= $icon('fa-sticky-note', '🗒️') ?> <?= $this->te('Notizbrett') ?></h2>
                    </div>
                    <a href="/teamboard" class="card-link"><?= $this->te('Alle &rarr;') ?></a>
                </div>
                <div style="padding:0.75rem 1rem 0.9rem;">
                    <?php if (empty($teamboardNotes)): ?>
                    <p class="text-muted" style="margin:0;"><?= $this->te('Noch keine Notizen.') ?></p>
                    <?php else: ?>
                    <div class="tb-mini">
                        <?php foreach ($teamboardNotes as $_n): ?>
                        <?php
                            $_uid  = (int)($_n['created_by'] ?? 0);
                            $_wer  = (string)($_n['display_name'] ?? '') !== '' ? (string)$_n['display_name'] : $this->t('Gelöschtes Konto');
                            $_ts   = strtotime((string)($_n['created_at'] ?? '')) ?: null;
                            $_stil = '--tb-color:' . \OpenNIT\Core\AuthorColor::css($_uid)
                                   . ';--tb-soft:' . \OpenNIT\Core\AuthorColor::softCss($_uid) . ';';
                        ?>
                        <div class="tb-mini-item" style="<?= $this->e($_stil) ?>">
                            <div class="tb-mini-head">
                                <span class="tb-mini-author"><?= $this->e($_wer) ?></span>
                                <span class="tb-mini-time"><?= $_ts ? $this->e(date('d.m. H:i', $_ts)) : '' ?></span>
                            </div>
                            <div class="tb-mini-body"><?= $this->e($_n['body']) ?></div>
                        </div>
                        <?php endforeach; ?>
                    </div>
                    <?php endif; ?>
                </div>
            </section>
            <?php break; ?>

            <?php case 'my_todos': ?>
            <section class="dashboard-card" data-widget="my_todos" id="wTodos">
                <div class="card-header">
                    <div style="display:flex;align-items:center;gap:0.5rem;">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <h2><?= $icon('fa-check-square', '✅') ?> <?= $this->te('To-Do-Liste') ?></h2>
                    </div>
                    <a href="/todos" class="card-link"><?= $this->te('Alle &rarr;') ?></a>
                </div>
                <div style="padding:0.75rem 1rem 0.6rem;">
                    <!-- Schnell-Eingabe -->
                    <form onsubmit="wTodoAdd(event)" autocomplete="off" class="wtd-add-row">
                        <input id="wTodoInput" type="text" placeholder="<?= $this->te('Neue Aufgabe&hellip;') ?>" maxlength="500" class="wtd-add-input">
                        <input id="wTodoDueIn" type="date" class="wtd-add-due" title="<?= $this->te('Fälligkeitsdatum') ?>">
                        <button aria-label="<?= $this->te('Hinzufügen') ?>" type="submit" class="wtd-add-btn">+</button>
                    </form>

                    <!-- Liste -->
                    <ul id="wTodoList" class="wtd-list">
                        <?php if (empty($myTodos)): ?>
                        <li id="wTodoEmpty" class="wtd-empty"><?= $this->te('Keine offenen Aufgaben') ?> <?= $icon('fa-check-circle', '✔️') ?></li>
                        <?php else: ?>
                        <?php
                        $_wtoday = date('Y-m-d');
                        foreach ($myTodos as $t):
                            $_isOver  = !empty($t['due_date']) && $t['due_date'] < $_wtoday;
                            $_isToday = !empty($t['due_date']) && $t['due_date'] === $_wtoday;
                            $_isSoon  = !empty($t['due_date']) && $t['due_date'] === date('Y-m-d', strtotime('+1 day'));
                            $_dueCls  = $_isOver ? 'wtd-due--overdue' : ($_isToday ? 'wtd-due--today' : ($_isSoon ? 'wtd-due--soon' : ''));
                            $_dueFmt  = !empty($t['due_date']) ? date('d.m.Y', strtotime($t['due_date'])) : '';
                            $_duePfx  = $_isOver ? 'Überfällig · ' : ($_isToday ? 'Heute · ' : ($_isSoon ? 'Morgen · ' : ''));
                        ?>
                        <li class="wtd-item<?= $_isOver ? ' wtd-item--overdue' : '' ?>"
                            data-id="<?= (int)$t['id'] ?>" data-due="<?= $this->e($t['due_date'] ?? '') ?>" draggable="true">
                            <span class="wtd-drag" title="<?= $this->te('Ziehen') ?>">&#8942;&#8942;</span>
                            <label class="wtd-check">
                                <input type="checkbox" class="wtd-cb" onchange="wTodoDone(this)">
                                <span class="wtd-cm"></span>
                            </label>
                            <div class="wtd-content">
                                <span class="wtd-title" ondblclick="wTodoStartEdit(this)"><?= $this->e($t['title']) ?></span>
                                <input type="text" class="wtd-edit-inp" style="display:none"
                                       onblur="wTodoSaveEdit(this)" onkeydown="wTodoEditKey(event,this)" maxlength="500">
                                <?php if (!empty($t['due_date'])): ?>
                                <span class="wtd-due-badge <?= $_dueCls ?>"><?= $_duePfx ?><?= $_dueFmt ?></span>
                                <?php endif; ?>
                                <input type="date" class="wtd-due-inp" style="display:none"
                                       onblur="wTodoSaveDue(this)" onchange="wTodoSaveDue(this)"
                                       value="<?= $this->e($t['due_date'] ?? '') ?>">
                                <?php if (!empty($t['notes'])): ?>
                                <span class="wtd-notes"><?= $this->e($t['notes']) ?></span>
                                <?php endif; ?>
                                <textarea class="wtd-notes-inp" style="display:none"
                                          onblur="wTodoSaveNotes(this)" placeholder="<?= $this->te('Notiz&hellip;') ?>" maxlength="2000"><?= $this->e($t['notes'] ?? '') ?></textarea>
                            </div>
                            <div class="wtd-actions">
                                <button class="wtd-act-btn<?= !empty($t['due_date']) ? ' active' : '' ?>" onclick="wTodoToggleDue(this)" title="<?= $this->te('Fälligkeitsdatum') ?>">
                                    <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="12" height="11" rx="1.5"/><line x1="5" y1="1" x2="5" y2="5"/><line x1="11" y1="1" x2="11" y2="5"/><line x1="2" y1="7" x2="14" y2="7"/></svg>
                                </button>
                                <button class="wtd-act-btn<?= !empty($t['notes']) ? ' active' : '' ?>" onclick="wTodoToggleNotes(this)" title="<?= $this->te('Notiz') ?>">
                                    <svg width="12" height="12" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M11.5 2.5a1.414 1.414 0 0 1 2 2L5 13l-3 1 1-3 8.5-8.5z"/></svg>
                                </button>
                                <button class="wtd-act-btn wtd-del" onclick="wTodoDelete(this)" title="<?= $this->te('Löschen') ?>">
                                    <svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="2" y1="2" x2="14" y2="14"/><line x1="14" y1="2" x2="2" y2="14"/></svg>
                                </button>
                            </div>
                        </li>
                        <?php endforeach; ?>
                        <?php if (count($myTodos) >= 10): ?>
                        <li style="font-size:0.72rem;color:var(--gray-400);text-align:center;padding:0.3rem 0;">
                            <a href="/todos" style="color:var(--indigo-500);"><?= $this->te('Weitere ansehen &rarr;') ?></a>
                        </li>
                        <?php endif; ?>
                        <?php endif; ?>
                    </ul>

                    <!-- Footer -->
                    <?php if ($myTodosDoneCount > 0): ?>
                    <div style="margin-top:0.5rem;padding-top:0.5rem;border-top:1px solid var(--gray-100);font-size:0.72rem;color:var(--gray-400);text-align:center;">
                        <?= $icon('fa-check', '✓') ?> <?= $this->te('{anzahl} erledigt', ['anzahl' => $myTodosDoneCount]) ?> &middot;
                        <a href="/todos" style="color:var(--indigo-500);"><?= $this->te('Alle anzeigen') ?></a>
                    </div>
                    <?php endif; ?>
                </div>
            </section>
            <?php break; ?>

            <?php case 'my_kanban_tasks': ?>
            <?php if ($hasViewKanban): ?>
            <?php
                $nowMonth  = date('Y-m');
                $nextMonth = date('Y-m', strtotime('+1 month'));

                $overdue  = array_filter($myKanbanTasks, fn($t) => $t['month'] < $nowMonth && !$t['done']);
                $current  = array_filter($myKanbanTasks, fn($t) => $t['month'] === $nowMonth);
                $upcoming = array_filter($myKanbanTasks, fn($t) => $t['month'] === $nextMonth);

                $monthLabel = fn(string $m): string => (new \DateTime($m . '-01'))->format('F Y');

                $priorityColor = [
                    'urgent' => ['#fee2e2','#991b1b'],
                    'high'   => ['#fef3c7','#92400e'],
                    'normal' => ['#f3f4f6','#374151'],
                    'low'    => ['#f0fdf4','#166534'],
                ];
                $priorityLabel = ['urgent'=>'Dringend','high'=>'Hoch','normal'=>'Normal','low'=>'Niedrig'];

                $renderTask = function(array $t) use ($priorityColor, $priorityLabel): string {
                    [$bg, $fg] = $priorityColor[$t['priority']] ?? ['#f3f4f6','#374151'];
                    $done = $t['done'] ? 'text-decoration:line-through;opacity:0.55;' : '';
                    $categoryDot = '';
                    if (!empty($t['category_color'])) {
                        $c = htmlspecialchars($t['category_color']);
                        $n = htmlspecialchars($t['category_name'] ?? '');
                        $categoryDot = "<span title=\"$n\" style=\"display:inline-block;width:8px;height:8px;border-radius:50%;background:$c;flex-shrink:0;\"></span>";
                    }
                    $title = htmlspecialchars($t['title']);
                    $pLabel = $priorityLabel[$t['priority']] ?? $t['priority'];
                    $dueHtml = '';
                    if (!empty($t['due_date'])) {
                        $due = new \DateTime($t['due_date']);
                        $isOverdue = !$t['done'] && $due < new \DateTime('today');
                        $dueColor  = $isOverdue ? '#dc2626' : '#9ca3af';
                        $dueHtml   = '<span style="font-size:0.68rem;color:'.$dueColor.';white-space:nowrap;">'.$due->format('d.m.').'</span>';
                    }
                    return '<a href="/kanban" style="display:flex;align-items:center;gap:0.5rem;padding:0.4rem 0.6rem;border-radius:0.4rem;background:var(--gray-50);text-decoration:none;border:1px solid var(--gray-100);">'
                        . $categoryDot
                        . '<span style="flex:1;font-size:0.82rem;font-weight:600;color:var(--gray-900);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'.$done.'">'.$title.'</span>'
                        . $dueHtml
                        . '<span style="font-size:0.65rem;font-weight:700;padding:0.12rem 0.45rem;border-radius:9999px;background:'.$bg.';color:'.$fg.';white-space:nowrap;">'.$pLabel.'</span>'
                        . '</a>';
                };

                $totalCount = count($overdue) + count($current) + count($upcoming);
            ?>
            <section class="dashboard-card" data-widget="my_kanban_tasks">
                <div class="card-header">
                    <div style="display:flex;align-items:center;gap:0.5rem;">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <h2><?= $icon('fa-clipboard-list', '📋') ?> <?= $this->te('Meine Aufgaben') ?></h2>
                    </div>
                    <a href="/kanban" class="card-link"><?= $this->te('Kanban &rarr;') ?></a>
                </div>
                <div class="card-body">
                    <?php if ($totalCount === 0): ?>
                        <p style="font-size:0.85rem;color:var(--gray-400);text-align:center;padding:0.75rem 0;"><?= $this->te('Keine Aufgaben zugewiesen.') ?></p>
                    <?php else: ?>

                    <?php if (!empty($overdue)): ?>
                    <div style="margin-bottom:0.9rem;">
                        <div style="display:flex;align-items:center;gap:0.4rem;margin-bottom:0.4rem;">
                            <span style="font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--red-600);"><?= $icon('fa-exclamation-triangle', '⚠️') ?> <?= $this->te('Überfällig') ?></span>
                            <span style="font-size:0.7rem;color:var(--red-600);background:var(--red-100);border-radius:9999px;padding:0.05rem 0.45rem;font-weight:700;"><?= count($overdue) ?></span>
                        </div>
                        <div style="display:flex;flex-direction:column;gap:0.3rem;">
                            <?php foreach ($overdue as $t): echo $renderTask($t); endforeach; ?>
                        </div>
                    </div>
                    <?php endif; ?>

                    <?php if (!empty($current)): ?>
                    <div style="margin-bottom:0.9rem;">
                        <div style="display:flex;align-items:center;gap:0.4rem;margin-bottom:0.4rem;">
                            <span style="font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--color-primary);"><?= $this->te('Dieser Monat') ?></span>
                            <span style="font-size:0.7rem;color:var(--color-primary-strong);background:var(--color-primary-soft);border-radius:9999px;padding:0.05rem 0.45rem;font-weight:700;"><?= count($current) ?></span>
                        </div>
                        <div style="display:flex;flex-direction:column;gap:0.3rem;">
                            <?php foreach ($current as $t): echo $renderTask($t); endforeach; ?>
                        </div>
                    </div>
                    <?php endif; ?>

                    <?php if (!empty($upcoming)): ?>
                    <div>
                        <div style="display:flex;align-items:center;gap:0.4rem;margin-bottom:0.4rem;">
                            <span style="font-size:0.72rem;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:var(--emerald-600);"><?= $this->te('N&auml;chster Monat') ?></span>
                            <span style="font-size:0.7rem;color:var(--emerald-800);background:var(--emerald-100);border-radius:9999px;padding:0.05rem 0.45rem;font-weight:700;"><?= count($upcoming) ?></span>
                        </div>
                        <div style="display:flex;flex-direction:column;gap:0.3rem;">
                            <?php foreach ($upcoming as $t): echo $renderTask($t); endforeach; ?>
                        </div>
                    </div>
                    <?php endif; ?>

                    <?php endif; ?>
                </div>
            </section>
            <?php endif; ?>
            <?php break; ?>

        <?php case 'monitoring': ?>
            <?php if ($hasViewMonitoring): ?>
            <section class="dashboard-card <?= (int)($monitoringCounts['critical'] ?? 0) > 0 ? 'attention' : '' ?>" data-widget="monitoring">
                <div class="card-header">
                    <h2 class="card-title">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <?= $icon('fa-chart-bar', '📊') ?> Monitoring
                        <?php if ((int)($monitoringCounts['critical'] ?? 0) > 0): ?>
                        <span style="background:var(--red-600);color:var(--color-on-accent);font-size:.7rem;padding:.15rem .5rem;border-radius:9999px;margin-left:.4rem">
                            <?= $this->te('{anzahl} kritisch', ['anzahl' => (int)$monitoringCounts['critical']]) ?>
                        </span>
                        <?php elseif ((int)($monitoringCounts['active'] ?? 0) > 0): ?>
                        <span style="background:var(--amber-500);color:var(--color-on-accent);font-size:.7rem;padding:.15rem .5rem;border-radius:9999px;margin-left:.4rem">
                            <?= $this->te('{anzahl} aktiv', ['anzahl' => (int)$monitoringCounts['active']]) ?>
                        </span>
                        <?php endif; ?>
                    </h2>
                    <a href="/monitoring" class="card-action"><?= $this->te('Alle &rarr;') ?></a>
                </div>
                <?php if (empty($monitoringEvents)): ?>
                <div style="padding:1.25rem 1rem;text-align:center;color:var(--gray-500);font-size:.85rem">
                    <span style="font-size:1.5rem"><?= $icon('fa-check', '✓') ?></span><br><?= $this->te('Keine aktiven Probleme') ?>
                </div>
                <?php else: ?>
                <div style="overflow:hidden">
                    <?php
                    $sevColors = [
                        'disaster'       => ['#dc2626', '#fef2f2'],
                        'high'           => ['#ea580c', '#fff7ed'],
                        'average'        => ['#d97706', '#fffbeb'],
                        'warning'        => ['#ca8a04', '#fefce8'],
                        'information'    => ['#3c8dbc', '#e8f4fd'],
                        'not_classified' => ['#6b7280', '#f3f4f6'],
                    ];
                    $sevLabels = ['disaster'=>'DISASTER','high'=>'HIGH','average'=>'AVG','warning'=>'WARN','information'=>'INFO','not_classified'=>'N/C'];
                    foreach ($monitoringEvents as $me):
                        [$fg, $bg] = $sevColors[$me['severity']] ?? ['#6b7280', '#f3f4f6'];
                        $sinceMin = (int)((time() - strtotime($me['created_at'])) / 60);
                        $since = $sinceMin < 60 ? $sinceMin.'m' : (round($sinceMin/60,1).'h');
                    ?>
                    <div style="display:flex;align-items:center;gap:.5rem;padding:.45rem 1rem;border-bottom:1px solid var(--gray-100)">
                        <span style="flex-shrink:0;font-size:.7rem;font-weight:700;color:<?= $fg ?>;background:<?= $bg ?>;padding:.15rem .45rem;border-radius:.25rem;min-width:42px;text-align:center">
                            <?= $sevLabels[$me['severity']] ?? '?' ?>
                        </span>
                        <span style="flex:1;min-width:0;font-size:.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
                            <strong><?= $this->e($me['host']) ?></strong>
                            <span style="color:var(--gray-500)"> – <?= $this->e(mb_strimwidth($me['trigger_name'], 0, 50, '…')) ?></span>
                        </span>
                        <span style="flex-shrink:0;font-size:.72rem;color:var(--gray-400)"><?= $since ?></span>
                    </div>
                    <?php endforeach; ?>
                </div>
                <?php endif; ?>
            </section>
            <?php endif; ?>
            <?php break; ?>

        <?php case 'backup': ?>
            <?php if ($hasViewBackup): ?>
            <?php $bkFailed = (int)($backupCounts['failed'] ?? 0); $bkWarn = (int)($backupCounts['warning'] ?? 0); ?>
            <section class="dashboard-card <?= $bkFailed > 0 ? 'attention' : '' ?>" data-widget="backup">
                <div class="card-header">
                    <h2 class="card-title">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <?= $icon('fa-database', '💾') ?> Backup
                        <?php if ($bkFailed > 0): ?>
                        <span style="background:var(--red-600);color:var(--color-on-accent);font-size:.7rem;padding:.15rem .5rem;border-radius:9999px;margin-left:.4rem">
                            <?= $this->te('{anzahl} fehlgeschlagen', ['anzahl' => $bkFailed]) ?>
                        </span>
                        <?php elseif ($bkWarn > 0): ?>
                        <span style="background:var(--amber-500);color:var(--color-on-accent);font-size:.7rem;padding:.15rem .5rem;border-radius:9999px;margin-left:.4rem">
                            <?= $bkWarn ?> <?= $this->te('Warnung') ?>
                        </span>
                        <?php endif; ?>
                    </h2>
                    <a href="/backup" class="card-action"><?= $this->te('Alle &rarr;') ?></a>
                </div>
                <?php if (empty($backupJobs)): ?>
                <div style="padding:1.25rem 1rem;text-align:center;color:var(--gray-500);font-size:.85rem">
                    <span style="font-size:1.5rem"><?= $icon('fa-check', '✓') ?></span><br>
                    <?= $this->te((int)($backupCounts['total'] ?? 0) > 0 ? 'Alle Backups in Ordnung' : 'Noch keine Backup-Daten') ?>
                </div>
                <?php else: ?>
                <div style="overflow:hidden">
                    <?php foreach ($backupJobs as $bj):
                        $isFail = $bj['last_status'] === 'failed';
                        [$fg, $bg] = $isFail ? ['#dc2626', '#fef2f2'] : ['#ca8a04', '#fefce8'];
                        $when = $bj['last_run'] ? date('d.m. H:i', strtotime($bj['last_run'])) : '—';
                    ?>
                    <div style="display:flex;align-items:center;gap:.5rem;padding:.45rem 1rem;border-bottom:1px solid var(--gray-100)">
                        <span style="flex-shrink:0;font-size:.7rem;font-weight:700;color:<?= $fg ?>;background:<?= $bg ?>;padding:.15rem .45rem;border-radius:.25rem;min-width:42px;text-align:center">
                            <?= $isFail ? 'FAIL' : 'WARN' ?>
                        </span>
                        <span style="flex:1;min-width:0;font-size:.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
                            <strong><?= $this->e($bj['name']) ?></strong>
                            <?php if (!empty($bj['server_name'])): ?><span style="color:var(--gray-400)"> · <?= $this->e($bj['server_name']) ?></span><?php endif; ?>
                        </span>
                        <span style="flex-shrink:0;font-size:.72rem;color:var(--gray-400)"><?= $when ?></span>
                    </div>
                    <?php endforeach; ?>
                </div>
                <?php endif; ?>
            </section>
            <?php endif; ?>
            <?php break; ?>

        <?php case 'maintenance_upcoming': ?>
            <?php if ($hasViewMaintenance): ?>
            <?php
            $_mSrcLabels = ['manual' => 'Manuell', 'ssl' => 'SSL', 'asset' => 'Garantien', 'contract' => 'Verträge', 'license' => 'Lizenzen', 'configlog' => 'Änderungsprotokoll'];
            $_mSrcColors = ['manual' => '#1d4ed8', 'ssl' => '#dc2626', 'asset' => '#d97706', 'contract' => '#7c3aed', 'license' => '#059669', 'configlog' => '#6366f1'];
            ?>
            <section class="dashboard-card <?= $maintenanceUrgentCount > 0 ? 'attention' : '' ?>" data-widget="maintenance_upcoming">
                <div class="card-header" style="position:relative;">
                    <h2 class="card-title">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <?= $icon('fa-calendar', '📅') ?> <?= $this->te('Wartungstermine') ?>
                        <!-- Badge wird vom Lazy-Loader gefüllt (Anzahl erst nach Body-Fetch bekannt) -->
                        <span id="mtUrgentBadge" style="display:none;background:var(--red-600);color:var(--color-on-accent);font-size:.7rem;padding:.15rem .5rem;border-radius:9999px;margin-left:.4rem"></span>
                    </h2>
                    <div style="display:flex;align-items:center;gap:0.5rem;">
                        <button class="mf-toggle-btn" onclick="mfTogglePanel(this)" title="<?= $this->te('Quellen filtern') ?>"
                                style="background:none;border:none;cursor:pointer;color:var(--gray-400);padding:0.2rem 0.3rem;border-radius:0.3rem;line-height:1;font-size:0.8rem;display:flex;align-items:center;gap:0.25rem;transition:color 0.1s,background 0.1s;">
                            <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="1" y1="4" x2="15" y2="4"/><line x1="3" y1="8" x2="13" y2="8"/><line x1="5" y1="12" x2="11" y2="12"/></svg>
                            <?= $this->te('Filter') ?>
                        </button>
                        <a href="/maintenance" class="card-action"><?= $this->te('Kalender &rarr;') ?></a>
                    </div>
                    <!-- Filter-Popover (wird per JS an body gehängt, um overflow:hidden der Card zu umgehen) -->
                    <div class="mf-panel" id="mfPanel" style="display:none;position:absolute;">
                        <p class="mf-panel-title"><?= $this->te('Angezeigte Quellen') ?></p>
                        <?php
                        // Alle Quellen, die im Kalender auftauchen können – nur jene
                        // zeigen, deren Modul aktiv ist (sonst entstehen ohnehin keine
                        // Termine). Muss mit der Whitelist in
                        // DashboardController::saveMaintenanceFilter() übereinstimmen.
                        $_mfPanelSources = [
                            'manual'    => ['Manuelle Termine',     true],
                            'ssl'       => ['SSL-Zertifikate',      true],
                            'asset'     => ['Asset-Garantien',      $dashModules['assets']    ?? true],
                            'contract'  => ['Verträge &amp; Fristen', $dashModules['contracts'] ?? true],
                            'license'   => ['Lizenzen',             $dashModules['licenses']  ?? true],
                            'configlog' => ['Änderungsprotokoll',   $dashModules['configlog'] ?? true],
                        ];
                        foreach ($_mfPanelSources as $_src => [$_lbl, $_enabled]):
                            if (!$_enabled) continue;
                        ?>
                        <label class="mf-option">
                            <input type="checkbox" class="mf-cb" value="<?= $_src ?>"
                                   <?= in_array($_src, $maintenanceSources, true) ? 'checked' : '' ?>
                                   onchange="mfSave()">
                            <span class="mf-dot" style="background:<?= $_mSrcColors[$_src] ?>"></span>
                            <?= $_lbl ?>
                        </label>
                        <?php endforeach; ?>
                    </div>
                </div>
                <!-- Body wird per Lazy-Load nachgeladen (/dashboard/widget/maintenance),
                     damit die teure UNION-Abfrage nicht den ersten Seitenaufbau blockiert. -->
                <div id="mtBody" data-lazy="maintenance">
                    <div style="padding:1.25rem 1rem;text-align:center;color:var(--gray-400);font-size:.82rem">
                        <span class="dash-spinner" aria-hidden="true"></span> <?= $this->te('Termine werden geladen&hellip;') ?>
                    </div>
                </div>
            </section>
            <?php endif; ?>
            <?php break; ?>

        <?php case 'phonebook_search': ?>
            <?php if ($has_view_phonebook || ($has_view_contacts ?? false)): ?>
            <section class="dashboard-card" data-widget="phonebook_search" style="overflow:visible;">
                <div class="card-header">
                    <div style="display:flex;align-items:center;gap:0.5rem;">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <h2><?= $icon('fa-phone', '📞') ?> <?= $this->te('Kontakte') ?></h2>
                    </div>
                    <?php if ($has_view_phonebook): ?>
                    <a href="/phonebook" class="card-link"><?= $this->te('Telefonbuch &rarr;') ?></a>
                    <?php elseif ($has_view_contacts ?? false): ?>
                    <a href="/contacts" class="card-link"><?= $this->te('Adressbuch &rarr;') ?></a>
                    <?php endif; ?>
                </div>
                <div class="card-body" style="padding:.75rem;overflow:visible;">
                    <div style="position:relative;">
                        <input type="text" id="pb-widget-input" class="form-control form-control-sm"
                               placeholder="<?= $this->te('Name, Firma, Telefon …') ?>" autocomplete="off">
                    </div>
                </div>
            </section>
            <script>
            (function(){
                var inp = document.getElementById('pb-widget-input');
                if (!inp) return;

                var hasPhonebook = <?= json_encode((bool)$has_view_phonebook) ?>;
                var hasContacts  = <?= json_encode((bool)($has_view_contacts ?? false)) ?>;

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

                function positionDropdown() {
                    var rect = inp.getBoundingClientRect();
                    res.style.top  = (rect.bottom + window.scrollY + 2) + 'px';
                    res.style.left = (rect.left  + window.scrollX) + 'px';
                    res.style.width = rect.width + 'px';
                }

                function renderResult(p) {
                    var av = p.image_path
                        ? '<img src="'+p.image_path+'" style="width:38px;height:38px;border-radius:50%;object-fit:cover;flex-shrink:0;" loading="lazy">'
                        : '<div style="width:38px;height:38px;border-radius:50%;background:'+p.avatar_color+';display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:.85rem;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 _pbPhone  = <?= $_useFA ? '"<i class=\"fa-solid fa-phone\"></i>"'      : '"📞"' ?>;
                    var _pbMobile = <?= $_useFA ? '"<i class=\"fa-solid fa-mobile-alt\"></i>"' : '"📱"' ?>;
                    var _pbEmail  = <?= $_useFA ? '"<i class=\"fa-solid fa-envelope\"></i>"'   : '"✉"' ?>;
                    var phones = '';
                    if (p.phone)  phones += '<a href="tel:'+p.phone.replace(/[^\d+]/g,'')+'" style="color:#1d4ed8;text-decoration:none;font-size:.78rem;white-space:nowrap;" onclick="event.stopPropagation()">'+_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;" onclick="event.stopPropagation()">'+_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:160px;display:inline-block;" onclick="event.stopPropagation()">'+_pbEmail+' '+p.email+'</a>';
                    var row = '<div style="display:flex;align-items:center;gap:.6rem;padding:.5rem .75rem;border-bottom:1px solid #f3f4f6;"'
                        + (p.link ? ' style="cursor:pointer;" onclick="window.location=\''+p.link+'\'"' : '') + '>'
                        + 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>';
                    return row;
                }

                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('');
                            }
                            positionDropdown(); res.style.display='block';
                        });
                    }, 250);
                });

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

                window.addEventListener('scroll', function(){ if (res.style.display!=='none') positionDropdown(); }, true);
                window.addEventListener('resize', function(){ if (res.style.display!=='none') positionDropdown(); });

                document.addEventListener('click', function(e){
                    if (!inp.contains(e.target) && !res.contains(e.target)) { res.style.display='none'; }
                });
            })();
            </script>
            <?php endif; ?>
            <?php break; ?>

        <?php case 'cve_security': ?>
        <?php if ($has_view_cve ?? false): ?>
        <?php
        $_cveFilter     = $cve_widget_filter ?? 'all';
        $_cveIsFiltered = $_cveFilter === 'filtered';
        ?>
        <section class="dashboard-card" data-widget="cve_security">
            <div class="card-header">
                <div style="display:flex;align-items:center;gap:0.5rem;">
                    <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                    <h2><?= $icon('fa-shield-alt', '🛡') ?> <?= $this->te('CVE-Meldungen') ?></h2>
                </div>
                <div style="display:flex;align-items:center;gap:0.5rem;">
                    <!-- Filter-Toggle (Zustand in data-filter, ohne Reload) -->
                    <button id="cveWidgetToggle"
                            onclick="toggleCveWidgetFilter()"
                            data-filter="<?= $this->e($_cveFilter) ?>"
                            aria-pressed="<?= $_cveIsFiltered ? 'true' : 'false' ?>"
                            title="<?= $_cveIsFiltered ? 'Zeige alle CVEs' : 'Zeige nur Keywords' ?>"
                            style="background:<?= $_cveIsFiltered ? '#eff6ff' : '#f3f4f6' ?>;
                                   color:<?= $_cveIsFiltered ? '#1d4ed8' : '#6b7280' ?>;
                                   border:1px solid <?= $_cveIsFiltered ? '#bfdbfe' : '#e5e7eb' ?>;
                                   border-radius:9999px;padding:0.15rem 0.6rem;
                                   font-size:0.72rem;font-weight:600;cursor:pointer;white-space:nowrap;">
                        <?= $_cveIsFiltered ? $icon('fa-search', '🔍') . ' Keywords' : 'Alle' ?>
                    </button>
                    <a href="/cve" class="card-link"><?= $this->te('Anzeigen &rarr;') ?></a>
                </div>
            </div>
            <!-- Body wird per Lazy-Load nachgeladen (/dashboard/widget/cve) -->
            <div id="cveBody" data-lazy="cve" class="card-body" style="padding-bottom:0.5rem;">
                <div style="padding:1rem;text-align:center;color:var(--gray-400);font-size:.82rem">
                    <span class="dash-spinner" aria-hidden="true"></span> <?= $this->te('CVE-Daten werden geladen&hellip;') ?>
                </div>
            </div>
        </section>
        <script>
        // Filter-Toggle ohne Voll-Reload: Setting speichern, Button-Zustand
        // aktualisieren, CVE-Body neu laden.
        var CVE_FILTER_ICON = <?= $_useFA ? '"<i class=\"fa-solid fa-search\"></i> Keywords"' : '"🔍 Keywords"' ?>;
        function toggleCveWidgetFilter() {
            var btn  = document.getElementById('cveWidgetToggle');
            var next = btn.dataset.filter === 'filtered' ? 'all' : 'filtered';
            fetch('/api/user-settings', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content,
                    'X-Requested-With': 'XMLHttpRequest'
                },
                body: 'key=cve_widget_filter&value=' + encodeURIComponent(next)
            }).then(function(r) {
                if (!r.ok) return;
                var filtered = next === 'filtered';
                btn.dataset.filter = next;
                btn.setAttribute('aria-pressed', filtered ? 'true' : 'false');
                btn.title = filtered ? 'Zeige alle CVEs' : 'Zeige nur Keywords';
                btn.style.background  = filtered ? '#eff6ff' : '#f3f4f6';
                btn.style.color       = filtered ? '#1d4ed8' : '#6b7280';
                btn.style.borderColor = filtered ? '#bfdbfe' : '#e5e7eb';
                btn.innerHTML = filtered ? CVE_FILTER_ICON : 'Alle';
                if (window.loadWidgetBody) window.loadWidgetBody('cve');
                else window.location.reload();
            });
        }
        </script>
        <?php endif; ?>
        <?php break; ?>

        <?php case 'unifi_vouchers': ?>
        <?php if ($unifiModuleEnabled && $hasManageUnifiVouchers): ?>
        <?php
        $_uvwSubmitLabel = $icon('fa-plus', '➕') . ' Voucher erstellen';
        $_uvwLabelCfg = $voucher_label ?? [];
        ?>
        <script src="<?= $this->asset('/assets/js/voucher-label.js') ?>"></script>
        <script>window.VoucherLabelConfig = <?= json_encode([
            'printMethod' => $_uvwLabelCfg['print_method'] ?? 'both',
            'logoUrl'     => $_uvwLabelCfg['logo_url'] ?? '',
            'geo'         => $_uvwLabelCfg['geo'] ?? [],
            'apiBase'     => '/unifi/vouchers/dymo-label',
        ], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) ?>;</script>
        <section class="dashboard-card" data-widget="unifi_vouchers">
            <div class="card-header">
                <div style="display:flex;align-items:center;gap:0.5rem;">
                    <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                    <h2><?= $icon('fa-wifi', '📶') ?> <?= $this->te('UniFi Voucher') ?></h2>
                </div>
                <a href="/unifi/vouchers" class="card-link"><?= $this->te('Übersicht &rarr;') ?></a>
            </div>
            <div class="card-body">
                <?php if (empty($unifiVoucherSites)): ?>
                <p style="font-size:0.82rem;color:var(--gray-400);text-align:center;padding:0.5rem 0;">
                    <?= $this->t('Noch keine Sites bekannt. Bitte zuerst auf der <a href="/unifi">UniFi-Übersicht</a> aktualisieren.') ?>
                </p>
                <?php else: ?>
                <div class="form-group" style="margin-bottom:.6rem;">
                    <label class="uvw-label" for="uvw-site"><?= $this->te('Site') ?></label>
                    <select id="uvw-site" class="form-control form-control-sm">
                        <?php foreach ($unifiVoucherSites as $s): ?>
                        <option value="<?= $this->e($s['site_id']) ?>"><?= $this->e($s['site_name'] ?: $s['site_id']) ?></option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <div style="display:grid;grid-template-columns:1fr 1fr;gap:.6rem;margin-bottom:.6rem;">
                    <div class="form-group" style="margin:0;">
                        <label class="uvw-label" for="uvw-hours"><?= $this->te('Stunden') ?></label>
                        <input type="number" id="uvw-hours" class="form-control form-control-sm" value="24" min="1" max="720">
                    </div>
                    <div class="form-group" style="margin:0;">
                        <label class="uvw-label" for="uvw-count"><?= $this->te('Geräte') ?></label>
                        <input type="number" id="uvw-count" class="form-control form-control-sm" value="1" min="1" max="100">
                    </div>
                </div>
                <div class="form-group" style="margin-bottom:.7rem;">
                    <label class="uvw-label" for="uvw-note"><?= $this->te('Notiz') ?> <span style="font-weight:400;color:var(--gray-400);"><?= $this->te('(optional)') ?></span></label>
                    <input type="text" id="uvw-note" class="form-control form-control-sm" maxlength="64" placeholder="<?= $this->te('z. B. Gast-Konferenz') ?>">
                </div>
                <button type="button" class="btn btn-primary btn-block btn-sm uvw-submit" onclick="uvwCreate()">
                    <?= $_uvwSubmitLabel ?>
                </button>
                <div id="uvw-inline-error" class="uvw-result uvw-err" style="display:none;margin-top:.6rem;"></div>
                <?php endif; ?>
            </div>
        </section>

        <!-- Modal: zeigt den erstellten Voucher-Code -->
        <div id="uvwModalBackdrop" class="uvw-modal-backdrop" onclick="uvwCloseModal(event)" style="display:none;">
            <div class="uvw-modal" onclick="event.stopPropagation()">
                <div class="uvw-modal-header">
                    <h3 id="uvwModalTitle"><?= $icon('fa-check-circle', '✔️') ?> <?= $this->te('Voucher erstellt') ?></h3>
                    <button type="button" class="uvw-modal-close" onclick="uvwCloseModal()" aria-label="<?= $this->te('Schließen') ?>">&times;</button>
                </div>
                <div class="uvw-modal-body">
                    <p id="uvwModalMeta" class="uvw-modal-meta"></p>
                    <div id="uvwModalCode" class="uvw-modal-bigcode"></div>
                </div>
                <div class="uvw-modal-footer">
                    <button type="button" id="uvwCopyBtn" class="btn btn-primary btn-sm" onclick="uvwCopySingle()">
                        <?= $icon('fa-clipboard-list', '📋') ?> <?= $this->te('Code kopieren') ?>
                    </button>
                    <span id="uvwPrintBtns" style="display:inline-flex;gap:.4rem;"></span>
                    <button type="button" class="btn btn-default btn-sm" onclick="uvwCloseModal()"><?= $this->te('Schließen') ?></button>
                </div>
            </div>
        </div>

        <script>
        (function(){
            if (window.uvwCreate) return;

            var SUBMIT_HTML = <?= json_encode($_uvwSubmitLabel) ?>;

            function fmtCode(c) {
                var s = String(c).replace(/[-\s]/g, '');
                return s.length >= 10 ? s.slice(0, 5) + '-' + s.slice(5) : c;
            }

            function showInlineError(msg) {
                var el = document.getElementById('uvw-inline-error');
                if (!el) return;
                el.style.display = 'block';
                el.textContent = msg;
            }
            function hideInlineError() {
                var el = document.getElementById('uvw-inline-error');
                if (el) el.style.display = 'none';
            }

            window.uvwOpenModal = function(siteName, hours, devices, codes) {
                var bd   = document.getElementById('uvwModalBackdrop');
                var meta = document.getElementById('uvwModalMeta');
                var box  = document.getElementById('uvwModalCode');
                var btn  = document.getElementById('uvwCopyBtn');
                if (!bd || !meta || !box) return;

                var code = fmtCode(codes[0] || '');
                var devLabel = devices === 1 ? '1 Gerät' : devices + ' Geräte';
                meta.textContent = 'Voucher für „' + siteName + '" · gültig ' + hours + ' h · ' + devLabel;
                box.textContent = code;
                box.dataset.code = code;
                var pb = document.getElementById('uvwPrintBtns');
                if (pb) pb.innerHTML = window.VoucherLabel ? VoucherLabel.buttons(code, { btnClass: 'btn btn-default btn-sm' }) : '';
                if (btn) {
                    btn.disabled = false;
                    btn.classList.remove('uvw-copied');
                    btn.innerHTML = '<?= $icon('fa-clipboard-list', '📋') ?> <?= $this->te('Code kopieren') ?>';
                }
                bd.style.display = 'flex';
                document.body.style.overflow = 'hidden';
            };
            window.uvwCloseModal = function(e) {
                if (e && e.target && e.target.id !== 'uvwModalBackdrop') return;
                var bd = document.getElementById('uvwModalBackdrop');
                if (bd) bd.style.display = 'none';
                document.body.style.overflow = '';
            };
            window.uvwCopySingle = function() {
                var box = document.getElementById('uvwModalCode');
                var btn = document.getElementById('uvwCopyBtn');
                if (!box || !navigator.clipboard) return;
                var code = box.dataset.code || box.textContent || '';
                navigator.clipboard.writeText(code).then(function(){
                    if (!btn) return;
                    btn.classList.add('uvw-copied');
                    btn.innerHTML = '✓ Kopiert!';
                    setTimeout(function(){
                        btn.classList.remove('uvw-copied');
                        btn.innerHTML = '<?= $icon('fa-clipboard-list', '📋') ?> <?= $this->te('Code kopieren') ?>';
                    }, 1800);
                }).catch(function(){
                    if (btn) btn.innerHTML = '✗ Fehler beim Kopieren';
                });
            };
            // Code-Anzeige selbst klickbar machen → kopiert auch
            document.addEventListener('click', function(e){
                if (e.target && e.target.id === 'uvwModalCode') uvwCopySingle();
            });
            document.addEventListener('keydown', function(e){
                if (e.key === 'Escape') uvwCloseModal();
            });

            window.uvwCreate = function() {
                hideInlineError();
                var siteEl  = document.getElementById('uvw-site');
                var hoursEl = document.getElementById('uvw-hours');
                var countEl = document.getElementById('uvw-count');
                var noteEl  = document.getElementById('uvw-note');
                if (!siteEl || !hoursEl || !countEl) return;

                var siteId = siteEl.value;
                var siteName = siteEl.options[siteEl.selectedIndex]?.text || siteId;
                var hours  = parseInt(hoursEl.value, 10);
                var devices = parseInt(countEl.value, 10);
                var note   = (noteEl?.value || '').trim();
                if (!siteId || isNaN(hours) || hours < 1 || isNaN(devices) || devices < 1 || devices > 200) {
                    showInlineError('Bitte Site, gültige Stunden und 1–200 Geräte angeben.');
                    return;
                }

                var btn = document.querySelector('#widgetGrid [data-widget="unifi_vouchers"] .uvw-submit');
                if (btn) { btn.disabled = true; btn.innerHTML = '⏳ Wird erstellt…'; }

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

                // "Geräte" = Anzahl der zulässigen Nutzungen pro Voucher (quota),
                // wir erzeugen immer genau einen Voucher (n=1).
                var body = new URLSearchParams({
                    _csrf: csrf,
                    site_id: siteId,
                    expire: String(hours * 60),
                    n: '1',
                    quota: String(devices),
                    down: '0',
                    up: '0',
                    note: note
                });

                fetch('/unifi/vouchers', { method: 'POST', body: body, headers: { 'X-Requested-With': 'XMLHttpRequest' } })
                    .then(function(r){ return r.json(); })
                    .then(function(d){
                        if (d.ok) {
                            var codes = (d.vouchers || []).filter(function(c){ return c && c.trim() !== ''; });
                            if (codes.length === 0) {
                                showInlineError('Voucher erstellt – Code konnte nicht ermittelt werden.');
                            } else {
                                uvwOpenModal(siteName, hours, devices, codes);
                                if (noteEl) noteEl.value = '';
                            }
                        } else {
                            showInlineError('Fehler: ' + (d.error || 'Unbekannt'));
                        }
                    })
                    .catch(function(e){ showInlineError('Netzwerkfehler: ' + e.message); })
                    .finally(function(){
                        if (btn) { btn.disabled = false; btn.innerHTML = SUBMIT_HTML; }
                    });
            };
        })();
        </script>
        <?php endif; ?>
        <?php break; ?>

        <?php case 'contracts_expiring': ?>
            <?php if ($hasViewContracts ?? false): ?>
            <?php
                $today_ts = strtotime('today');
                $ceUrgent = 0;
                foreach ($contractsExpiring as $ce) {
                    $dl = (int)floor((strtotime($ce['cancellation_notice_date']) - $today_ts) / 86400);
                    if ($dl <= 14) $ceUrgent++;
                }
            ?>
            <section class="dashboard-card <?= $ceUrgent > 0 ? 'attention' : '' ?>" data-widget="contracts_expiring">
                <div class="card-header">
                    <h2 class="card-title">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <?= $icon('fa-file-contract', '📄') ?> <?= $this->te('Kündigungsfristen') ?>
                        <?php if ($ceUrgent > 0): ?>
                        <span style="background:var(--red-600);color:var(--color-on-accent);font-size:.7rem;padding:.15rem .5rem;border-radius:9999px;margin-left:.4rem">
                            <?= $this->te('{anzahl} dringend', ['anzahl' => $ceUrgent]) ?>
                        </span>
                        <?php endif; ?>
                    </h2>
                    <a href="/contracts?view=cancel_soon" class="card-action"><?= $this->te('Alle &rarr;') ?></a>
                </div>
                <?php if (empty($contractsExpiring)): ?>
                <div style="padding:1.25rem 1rem;text-align:center;color:var(--gray-500);font-size:.85rem">
                    <span style="font-size:1.5rem"><?= $icon('fa-check', '✓') ?></span><br>
                    <?= $this->te('Keine Kündigungsfristen in den nächsten 60 Tagen') ?>
                </div>
                <?php else: ?>
                <div style="overflow:hidden">
                    <?php foreach ($contractsExpiring as $ce):
                        $daysLeft = (int)floor((strtotime($ce['cancellation_notice_date']) - $today_ts) / 86400);
                        [$fg, $bg] = $daysLeft <= 7 ? ['#dc2626', '#fef2f2'] : ($daysLeft <= 30 ? ['#d97706', '#fffbeb'] : ['#2471a3', '#eff6ff']);
                    ?>
                    <a href="/contracts/<?= (int)$ce['id'] ?>" style="display:flex;align-items:center;gap:.5rem;padding:.45rem 1rem;border-bottom:1px solid var(--gray-100);text-decoration:none;color:inherit">
                        <span style="flex-shrink:0;font-size:.7rem;font-weight:700;color:<?= $fg ?>;background:<?= $bg ?>;padding:.15rem .45rem;border-radius:.25rem;min-width:54px;text-align:center">
                            <?= $daysLeft <= 0 ? 'heute' : $daysLeft . ' Tg' ?>
                        </span>
                        <span style="flex:1;min-width:0;font-size:.8rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
                            <strong><?= $this->e($ce['title']) ?></strong>
                            <span style="color:var(--gray-400)"> · <?= $this->e($ce['company_name']) ?></span>
                        </span>
                        <span style="flex-shrink:0;font-size:.72rem;color:var(--gray-400)"><?= date('d.m.Y', strtotime($ce['cancellation_notice_date'])) ?></span>
                    </a>
                    <?php endforeach; ?>
                </div>
                <?php endif; ?>
            </section>
            <?php endif; ?>
            <?php break; ?>

        <?php case 'workflow_approvals': ?>
            <section class="dashboard-card" data-widget="workflow_approvals">
                <div class="card-header">
                    <div style="display:flex;align-items:center;gap:0.5rem;">
                        <span class="widget-drag-handle" title="<?= $this->te('Verschieben') ?>">&#8942;&#8942;</span>
                        <h2><?= $icon('fa-clipboard-check', '✅') ?> <?= $this->te('Genehmigungen') ?></h2>
                    </div>
                    <a href="/antraege" class="card-link"><?= $this->te('Alle &rarr;') ?></a>
                </div>
                <div class="card-body">
                    <?php $wfA = $workflow_approvals ?? []; ?>
                    <?php if (empty($wfA)): ?>
                        <p style="font-size:0.85rem;color:var(--gray-400);text-align:center;padding:0.75rem 0;"><?= $this->te('Nichts zu genehmigen.') ?></p>
                    <?php else: ?>
                        <div style="display:flex;flex-direction:column;gap:0.3rem;">
                        <?php foreach ($wfA as $row): ?>
                            <a href="/antraege/<?= (int)$row['request_id'] ?>" style="display:flex;align-items:center;justify-content:space-between;gap:0.5rem;padding:0.4rem 0.6rem;border-radius:0.4rem;background:var(--gray-50);text-decoration:none;border:1px solid var(--gray-100);">
                                <span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--gray-700);">
                                    <?= htmlspecialchars($row['title']) ?>
                                    <span style="color:var(--gray-400);font-size:0.72rem;">&middot; <?= htmlspecialchars($row['step_name']) ?></span>
                                    <?php if (!empty($row['via_deputy'])): ?><span style="color:var(--violet-600);font-size:0.68rem;"><?= $this->te('(Vertretung)') ?></span><?php endif; ?>
                                </span>
                                <?php if (!empty($row['deadline_at'])): ?>
                                    <?php $od = strtotime($row['deadline_at']) < time(); ?>
                                    <span style="font-size:0.68rem;white-space:nowrap;color:<?= $od ? 'var(--red-600)' : 'var(--gray-400)' ?>;"><?= (new \DateTime($row['deadline_at']))->format('d.m.') ?></span>
                                <?php endif; ?>
                            </a>
                        <?php endforeach; ?>
                        </div>
                    <?php endif; ?>
                </div>
            </section>
        <?php break; ?>

        <?php endswitch; ?>
        <?php endforeach; ?>
        </div><!-- /dashCol -->
        <?php endforeach; ?>

        </div><!-- /widgetGrid -->

        <!-- Statistiken -->
        <footer class="dashboard-stats-footer">
            <span class="dsf-item">
                <span class="dsf-value"><?= number_format($stats['total_pages'] ?? 0) ?></span>
                <span class="dsf-label"><?= $this->te('Seiten') ?></span>
            </span>
            <span class="dsf-sep">&middot;</span>
            <span class="dsf-item">
                <span class="dsf-value"><?= number_format($stats['total_topics'] ?? 0) ?></span>
                <span class="dsf-label"><?= $this->te('Themengebiete') ?></span>
            </span>
            <span class="dsf-sep">&middot;</span>
            <span class="dsf-item">
                <span class="dsf-value"><?= number_format($stats['my_pages'] ?? 0) ?></span>
                <span class="dsf-label"><?= $this->te('Meine Seiten') ?></span>
            </span>
            <?php if (($stats['pending_reviews'] ?? 0) > 0): ?>
            <span class="dsf-sep">&middot;</span>
            <span class="dsf-item dsf-warn">
                <span class="dsf-value"><?= number_format($stats['pending_reviews'] ?? 0) ?></span>
                <span class="dsf-label"><?= $this->te('Offene Reviews') ?></span>
            </span>
            <?php endif; ?>
        </footer>
    </div>
</div>

<!-- Widget-Konfig-Panel (Slide-over) -->
<div id="widgetConfigBackdrop" class="wcfg-backdrop" onclick="closeWidgetConfig()"></div>
<div id="widgetConfigPanel" class="wcfg-panel" role="dialog" aria-modal="true" aria-labelledby="widgetConfigTitle" aria-hidden="true">
    <div class="wcfg-header">
        <h3 id="widgetConfigTitle"><?= $this->te('Dashboard anpassen') ?></h3>
        <button aria-label="<?= $this->te('Schließen') ?>" onclick="closeWidgetConfig()" class="wcfg-close">&times;</button>
    </div>
    <div id="wcfgList" class="wcfg-list">
    <!-- Hinweis -->
    <p style="font-size:0.8rem;color:var(--gray-500);margin:0 0 0.75rem;"><?= $this->te('Widgets ein-/ausblenden. Spalte und Reihenfolge direkt per Drag &amp; Drop auf dem Dashboard ändern.') ?></p>

    <!-- Spaltenanzahl -->
    <div style="margin-bottom:1.25rem;padding-bottom:1.25rem;border-bottom:1px solid var(--gray-200);">
        <p style="font-size:0.8rem;font-weight:600;color:var(--gray-700);margin:0 0 0.5rem;"><?= $this->te('Spaltenanzahl') ?></p>
        <div style="display:flex;gap:0.5rem;" id="wcfgColBtns">
            <?php foreach ([1 => '1 Spalte', 2 => '2 Spalten', 3 => '3 Spalten'] as $n => $lbl): ?>
            <button
                type="button"
                class="wcfg-col-btn <?= $dashboardColumns === $n ? 'active' : '' ?>"
                data-cols="<?= $n ?>"
                onclick="setDashboardColumns(<?= $n ?>)"
                style="flex:1;padding:0.4rem 0.5rem;border-radius:0.4rem;border:1px solid var(--gray-300);background:<?= $dashboardColumns === $n ? 'var(--color-primary)' : 'var(--gray-50)' ?>;color:<?= $dashboardColumns === $n ? 'var(--color-bg)' : 'var(--gray-700)' ?>;font-size:0.8rem;cursor:pointer;transition:all 0.15s;">
                <?= $lbl ?>
            </button>
            <?php endforeach; ?>
        </div>
    </div>
        <?php foreach ($allWidgets as $wId):
            $isHidden = in_array($wId, $hiddenWidgets, true);
            $wCol     = $widgetColumnMap[$wId] ?? 1;
            $label    = $widgetLabels[$wId][0] ?? $wId;
            $desc     = $widgetLabels[$wId][1] ?? '';
        ?>
        <div class="wcfg-item <?= $isHidden ? 'hidden' : 'visible' ?>" draggable="true"
             data-widget="<?= $wId ?>" data-col="<?= $wCol ?>">
            <span class="wcfg-handle">&#8942;&#8942;</span>
            <div class="wcfg-item-text">
                <span class="wcfg-item-label"><?= $label ?></span>
                <span class="wcfg-item-desc"><?= $this->e($desc) ?></span>
            </div>
            <button class="wcfg-toggle" onclick="toggleWidget(this)" title="<?= $this->te('Ein-/ausblenden') ?>"
                    aria-pressed="<?= $isHidden ? 'false' : 'true' ?>"
                    aria-label="<?= $this->e(strip_tags($label)) ?> <?= $this->te('ein- oder ausblenden') ?>">
                <span class="wcfg-eye"><?= $isHidden ? $icon('fa-eye-slash', '🚫') : $icon('fa-eye', '👁') ?></span>
            </button>
        </div>
        <?php endforeach; ?>
    </div>
    <div class="wcfg-footer">
        <button onclick="saveWidgetConfig()" class="wcfg-save-btn"><?= $this->te('Speichern') ?></button>
    </div>
</div>

<!-- Schnellzugriff-Konfig-Panel -->
<div id="qaConfigBackdrop" class="wcfg-backdrop" onclick="closeQAConfig()"></div>
<div id="qaConfigPanel" class="wcfg-panel" role="dialog" aria-modal="true" aria-labelledby="qaConfigTitle" aria-hidden="true">
    <div class="wcfg-header">
        <h3 id="qaConfigTitle"><?= $icon('fa-bolt', '🚀') ?> <?= $this->te('Schnellzugriff anpassen') ?></h3>
        <button aria-label="<?= $this->te('Schließen') ?>" onclick="closeQAConfig()" class="wcfg-close">&times;</button>
    </div>
    <div id="qaList" class="wcfg-list">
        <p style="font-size:0.8rem;color:var(--gray-500);margin:0 0 0.75rem;"><?= $this->te('Wähle welche Kacheln angezeigt werden sollen. Reihenfolge per Drag &amp; Drop.') ?></p>

        <!-- Spaltenanzahl -->
        <div style="margin-bottom:1.25rem;padding-bottom:1.25rem;border-bottom:1px solid var(--gray-200);">
            <p style="font-size:0.8rem;font-weight:600;color:var(--gray-700);margin:0 0 0.5rem;"><?= $this->te('Spaltenanzahl') ?></p>
            <div style="display:flex;gap:0.5rem;" id="qaColBtns">
                <?php foreach ([2 => '2 Spalten', 3 => '3 Spalten'] as $n => $lbl): ?>
                <button type="button"
                    data-cols="<?= $n ?>"
                    onclick="setQACols(<?= $n ?>)"
                    style="flex:1;padding:0.4rem 0.5rem;border-radius:0.4rem;border:1px solid var(--gray-300);background:<?= $quickAccessCols === $n ? 'var(--color-primary)' : 'var(--gray-50)' ?>;color:<?= $quickAccessCols === $n ? 'var(--color-bg)' : 'var(--gray-700)' ?>;font-size:0.8rem;cursor:pointer;transition:all 0.15s;">
                    <?= $lbl ?>
                </button>
                <?php endforeach; ?>
            </div>
        </div>

        <?php
        // Aktive zuerst (gespeicherte Reihenfolge), danach inaktive
        $qaInactive = array_values(array_diff(array_keys($availableQuickLinks), $activeQuickLinkKeys));
        $qaOrdered  = array_merge($activeQuickLinkKeys, $qaInactive);
        foreach ($qaOrdered as $key):
            if (!isset($availableQuickLinks[$key])) continue;
            $ql = $availableQuickLinks[$key];
            $isActive = in_array($key, $activeQuickLinkKeys, true);
        ?>
        <div class="wcfg-item <?= $isActive ? 'visible' : 'hidden' ?>" draggable="true" data-qakey="<?= $this->e($key) ?>">
            <span class="wcfg-handle">&#8942;&#8942;</span>
            <div class="wcfg-item-text">
                <span class="wcfg-item-label"><?= $icon($ql['icon']) ?>&nbsp;<?= $this->te($ql['label']) ?></span>
                <span class="wcfg-item-desc"><?= $this->te($ql['desc']) ?></span>
            </div>
            <button class="wcfg-toggle" onclick="toggleQAItem(this)" title="<?= $this->te('Ein-/ausblenden') ?>"
                    aria-pressed="<?= $isActive ? 'true' : 'false' ?>"
                    aria-label="<?= $this->e($ql['label']) ?> <?= $this->te('ein- oder ausblenden') ?>">
                <span class="wcfg-eye"><?= $isActive ? $icon('fa-eye', '👁') : $icon('fa-eye-slash', '🚫') ?></span>
            </button>
        </div>
        <?php endforeach; ?>
    </div>
    <div class="wcfg-footer">
        <button onclick="saveQAConfig()" class="wcfg-save-btn"><?= $this->te('Speichern') ?></button>
    </div>
</div>

<!-- Dashboard-CSS ausgelagert nach public/assets/css/dashboard.css (per extra_css eingebunden, browser-cachebar) -->

<!-- Dashboard-Kern-JS ausgelagert nach public/assets/js/dashboard.js (browser-cachebar) -->
<script>window.__DASHBOARD__ = {
    hiddenWidgets: <?= json_encode($hiddenWidgets) ?>,
    dashCols: <?= (int)$dashboardColumns ?>,
    qaCols: <?= (int)$quickAccessCols ?>
};</script>
<script src="/assets/js/dashboard.js?v=<?= filemtime(dirname(__DIR__, 2) . '/public/assets/js/dashboard.js') ?>" defer></script>