430 lines
27 KiB
PHP
430 lines
27 KiB
PHP
<?php
|
||
/** @var array $staff (current_staff(), wird von der aufrufenden Seite gesetzt) */
|
||
$staff = current_staff();
|
||
$activePage = $activePage ?? '';
|
||
header('Content-Type: text/html; charset=UTF-8');
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title><?= e($pageTitle ?? 'DeskHub – Mitarbeiterbereich') ?></title>
|
||
<meta name="csrf" content="<?= csrf_token() ?>">
|
||
<?php
|
||
// VAPID Public Key – aus Config oder automatisch aus DB
|
||
if (file_exists(__DIR__ . '/../includes/push.php')) {
|
||
require_once __DIR__ . '/../includes/push.php';
|
||
$__vapidKeys = get_or_create_vapid_keys();
|
||
$pushKey = $__vapidKeys['public_key'] ?? '';
|
||
} else {
|
||
$pushKey = config()['push']['public_key'] ?? '';
|
||
}
|
||
?>
|
||
<?php if ($pushKey): ?>
|
||
<meta name="push-key" content="<?= e($pushKey) ?>">
|
||
<?php endif; ?>
|
||
<link rel="stylesheet" href="/assets/css/style.css?v=<?= @filemtime(__DIR__ . "/../assets/css/style.css") ?: time() ?>">
|
||
<?= site_icon_links() ?>
|
||
<link rel="manifest" href="/manifest-staff.php">
|
||
<meta name="theme-color" content="#0f6f6a">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<script>
|
||
if ('serviceWorker' in navigator) {
|
||
navigator.serviceWorker.register('/staff/sw-staff.js', {scope: '/staff/'}).catch(function(){});
|
||
}
|
||
</script>
|
||
</head>
|
||
|
||
<body>
|
||
<script>try { if (localStorage.getItem('deskhub_sidebar') === 'closed') document.documentElement.classList.add('sidebar-closed'); } catch (e) {}</script>
|
||
<button type="button" id="sidebar-open" class="sidebar-toggle sidebar-toggle-open" aria-label="Seitenleiste einblenden" title="Seitenleiste einblenden" onclick="document.getElementById('sidebar-toggle').click()">☰</button>
|
||
<div class="app-shell">
|
||
<aside class="sidebar" id="sidebar">
|
||
<button type="button" id="sidebar-toggle" class="sidebar-toggle sidebar-toggle-close" aria-label="Seitenleiste einklappen" title="Seitenleiste einklappen">«</button>
|
||
<div class="brand">
|
||
<?php $logo = get_setting('logo_path'); ?>
|
||
<?php if ($logo): ?>
|
||
<img src="<?= e($logo) ?>" alt="<?= e(get_setting('display_name', 'DeskHub')) ?>" style="max-height:34px; max-width:180px; display:block">
|
||
<?php else: ?>
|
||
Desk<span>Hub</span>
|
||
<?php endif; ?>
|
||
</div>
|
||
<nav>
|
||
<?php // Übersicht + ehemals separates "Berichte" wurden zu einer Seite zusammengelegt
|
||
// (siehe staff/reports.php) – der Login landete ohnehin schon immer dort. ?>
|
||
<a href="/staff/reports.php" class="<?= $activePage==='reports'?'active':'' ?>">Übersicht</a>
|
||
<a href="/staff/report_builder.php" class="<?= $activePage==='report_builder'?'active':'' ?>">Bericht-Baukasten</a>
|
||
<a href="/staff/tickets.php" class="<?= $activePage==='tickets'?'active':'' ?>">Tickets</a>
|
||
<a href="/staff/tasks.php" class="<?= $activePage==='tasks'?'active':'' ?>">Aufgaben</a>
|
||
<a href="/staff/banlist.php" class="<?= $activePage==='banlist'?'active':'' ?>">Sperrliste</a>
|
||
<a href="/staff/agent_directory.php" class="<?= $activePage==='agent_directory'?'active':'' ?>">Verzeichnis</a>
|
||
<a href="/staff/apps.php" class="<?= $activePage==='apps'?'active':'' ?>">Apps</a>
|
||
<?php foreach (plugin_menu_items($staff ?? null) as $__pm): ?>
|
||
<a href="<?= e($__pm['url']) ?>" class="<?= ($activePage ?? '') === 'plugin_' . $__pm['plugin'] ? 'active' : '' ?>"><?= e($__pm['label']) ?></a>
|
||
<?php endforeach; ?>
|
||
<?php if (get_setting('live_chat_enabled', '0') === '1' && $staff): ?>
|
||
<?php
|
||
// Badge = Anzahl Live-Chats, deren letzte Nachricht vom Kunden kam (also unbeantwortet),
|
||
// auf die eigenen Abteilungen begrenzt (wie die Ticketliste selbst).
|
||
[$__lcDeptSql, $__lcDeptParams] = dept_filter_sql(staff_department_ids($staff), 't.department_id');
|
||
$__lcSql = "SELECT COUNT(*) FROM tickets t WHERE t.source = 'chat' AND t.status != 'closed'
|
||
AND (SELECT tt.author_type FROM ticket_threads tt WHERE tt.ticket_id = t.id ORDER BY tt.created_at DESC, tt.id DESC LIMIT 1) = 'customer'"
|
||
. $__lcDeptSql; // enthält bereits das führende " AND " (dept_filter_sql)
|
||
$__lcStmt = get_db()->prepare($__lcSql);
|
||
$__lcStmt->execute($__lcDeptParams);
|
||
$__lcPending = (int) $__lcStmt->fetchColumn();
|
||
?>
|
||
<a href="/staff/live_chat.php" class="<?= $activePage==='live_chat'?'active':'' ?>">
|
||
Live-Chat<?php if ($__lcPending): ?> <span class="nav-badge" style="background:var(--danger, #dc2626); color:#fff; border-radius:10px; min-width:18px; height:18px; font-size:.68rem; font-weight:700; display:inline-flex; align-items:center; justify-content:center; padding:0 5px; margin-left:4px; vertical-align:middle"><?= $__lcPending > 9 ? '9+' : $__lcPending ?></span><?php endif; ?>
|
||
</a>
|
||
<?php endif; ?>
|
||
|
||
<?php
|
||
// Adminportal auch für "Co-Admins" zeigen: Agenten, denen per benutzerdefinierter Rolle
|
||
// gezielt einzelne Adminportal-Rechte zugewiesen wurden (admin_section_permissions()).
|
||
// has_permission() liefert für echte Admins ohnehin immer true, daher genügt die reine
|
||
// Rechteprüfung je Link – kein zusätzliches role==='admin' nötig.
|
||
$hasAnyAdminAccess = false;
|
||
if ($staff) {
|
||
foreach (array_keys(admin_section_permissions()) as $__perm) {
|
||
if (has_permission($staff, $__perm)) { $hasAnyAdminAccess = true; break; }
|
||
}
|
||
}
|
||
?>
|
||
<?php if ($hasAnyAdminAccess): ?>
|
||
<div class="nav-section-label" id="admin-toggle" role="button" tabindex="0">
|
||
Adminportal <span class="nav-toggle-chevron">▾</span>
|
||
</div>
|
||
<div class="nav-admin-block" id="admin-block">
|
||
<span class="nav-group-label">Verwalten</span>
|
||
<?php if (has_permission($staff, 'manage_departments')): ?><a href="/staff/departments.php" class="<?= $activePage==='departments'?'active':'' ?>">Abteilungen</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'manage_priorities')): ?><a href="/staff/priorities.php" class="<?= $activePage==='priorities'?'active':'' ?>">Prioritäten / SLA</a><a href="/staff/sla_plans.php" class="<?= $activePage==='sla_plans'?'active':'' ?>">SLA-Pläne</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'manage_help_topics')): ?><a href="/staff/help_topics.php" class="<?= $activePage==='help_topics'?'active':'' ?>">Hilfethemen</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/custom_fields.php" class="<?= $activePage==='custom_fields'?'active':'' ?>">Ticket-Felder</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/api_keys.php" class="<?= $activePage==='api_keys'?'active':'' ?>">API-Schlüssel</a><a href="/staff/webhooks.php" class="<?= $activePage==='webhooks'?'active':'' ?>">Webhooks</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/custom_lists.php" class="<?= $activePage==='custom_lists'?'active':'' ?>">Benutzerdefinierte Listen</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/canned_responses.php" class="<?= $activePage==='canned'?'active':'' ?>">Vorlagen</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/site_pages.php" class="<?= $activePage==='site_pages'?'active':'' ?>">Seiten</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/languages.php" class="<?= $activePage==='languages'?'active':'' ?>">Sprachen</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/ticket_templates.php" class="<?= $activePage==='ticket_templates'?'active':'' ?>">Ticket-Vorlagen</a><a href="/staff/status_page.php" class="<?= $activePage==='status_page'?'active':'' ?>">Statusseite</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'manage_email_addresses')): ?><a href="/staff/email_addresses.php" class="<?= $activePage==='email_addresses'?'active':'' ?>">E-Mail-Adressen</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'manage_email_templates')): ?><a href="/staff/email_templates.php" class="<?= $activePage==='email_templates'?'active':'' ?>">E-Mail-Vorlagen</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'manage_kb')): ?><a href="/staff/kb_manage.php" class="<?= $activePage==='kb'?'active':'' ?>">Wissensdatenbank</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'manage_assistant')): ?><a href="/staff/assistant.php" class="<?= $activePage==='assistant'?'active':'' ?>">Digitaler Assistent</a><?php endif; ?>
|
||
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/plugins.php" class="<?= $activePage==='plugins'?'active':'' ?>">Plugins</a><?php endif; ?>
|
||
<?php foreach (plugin_admin_menu_items($staff) as $__pm): ?>
|
||
<a href="<?= e($__pm['url']) ?>" class="<?= ($activePage ?? '') === 'plugin_' . $__pm['plugin'] ? 'active' : '' ?>"><?= e($__pm['label']) ?></a>
|
||
<?php endforeach; ?>
|
||
<span class="nav-group-label">Kunden</span>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/customers.php" class="<?= $activePage==='customers'?'active':'' ?>">Kundenverzeichnis</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'manage_organizations')): ?><a href="/staff/organizations.php" class="<?= $activePage==='organizations'?'active':'' ?>">Organisationen</a><?php endif; ?>
|
||
|
||
<span class="nav-group-label">Agenten</span>
|
||
<?php if (has_permission($staff, 'manage_staff')): ?><a href="/staff/staff_manage.php" class="<?= $activePage==='staff'?'active':'' ?>">Mitarbeiter</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'manage_teams')): ?><a href="/staff/teams.php" class="<?= $activePage==='teams'?'active':'' ?>">Teams</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'manage_roles')): ?><a href="/staff/roles.php" class="<?= $activePage==='roles'?'active':'' ?>">Rollen</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/availability.php" class="<?= $activePage==='availability'?'active':'' ?>">Status & Schichten</a><?php endif; ?>
|
||
|
||
<span class="nav-group-label">System</span>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/security_check.php" class="<?= $activePage==='security_check'?'active':'' ?>">Sicherheitsprüfung</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'view_audit_log')): ?><a href="/staff/audit_log.php" class="<?= $activePage==='audit_log'?'active':'' ?>">Admin-Protokoll</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/ticket_rules.php" class="<?= $activePage==='ticket_rules'?'active':'' ?>">Ticket-Regeln</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'manage_escalations')): ?><a href="/staff/escalation_rules.php" class="<?= $activePage==='escalation_rules'?'active':'' ?>">Eskalationsregeln</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/recurring_tickets.php" class="<?= $activePage==='recurring'?'active':'' ?>">Wiederkehrende Tickets</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'view_time_entries')): ?><a href="/staff/time_entries.php" class="<?= $activePage==='time_entries'?'active':'' ?>">Zeiterfassung</a><?php endif; ?>
|
||
<?php if (has_permission($staff, 'manage_business_hours')): ?><a href="/staff/business_hours.php" class="<?= $activePage==='business_hours'?'active':'' ?>">Geschäftszeiten</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/settings.php" class="<?= $activePage==='settings'?'active':'' ?>">Einstellungen</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/backup.php" class="<?= $activePage==='backup'?'active':'' ?>">Backup</a><?php endif; ?>
|
||
<?php if ($staff['role'] === 'admin'): ?><a href="/staff/update.php" class="<?= $activePage==='update'?'active':'' ?>">System-Update</a><?php endif; ?>
|
||
</div>
|
||
<script>
|
||
(function () {
|
||
var toggle = document.getElementById('admin-toggle');
|
||
var block = document.getElementById('admin-block');
|
||
var KEY = 'deskhub_admin_nav_collapsed';
|
||
// In diesen Adminportal-Unterseiten immer ausgeklappt zeigen, damit die aktive Seite sichtbar bleibt
|
||
var forceOpen = block.querySelector('a.active') !== null;
|
||
function apply(collapsed) {
|
||
block.style.display = collapsed ? 'none' : '';
|
||
toggle.querySelector('.nav-toggle-chevron').textContent = collapsed ? '▸' : '▾';
|
||
}
|
||
var stored = localStorage.getItem(KEY);
|
||
apply(!forceOpen && stored === '1');
|
||
toggle.addEventListener('click', function () {
|
||
var collapsed = block.style.display !== 'none';
|
||
apply(collapsed);
|
||
localStorage.setItem(KEY, collapsed ? '1' : '0');
|
||
});
|
||
toggle.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle.click(); }
|
||
});
|
||
})();
|
||
</script>
|
||
<?php endif; ?>
|
||
</nav>
|
||
<div class="sidebar-footer">
|
||
<?php if ($staff): ?>
|
||
<div style="display:flex; align-items:center; gap:10px; margin-bottom:8px">
|
||
<?php if (!empty($staff['avatar_path'])): ?>
|
||
<img src="<?= e($staff['avatar_path']) ?>" alt="" style="width:32px; height:32px; border-radius:50%; object-fit:cover; flex-shrink:0; border:1px solid #2a3744">
|
||
<?php else: ?>
|
||
<div style="width:32px; height:32px; border-radius:50%; background:rgba(15,111,106,.3); display:flex; align-items:center; justify-content:center; font-size:.85rem; font-weight:700; color:#8bc4c2; flex-shrink:0">
|
||
<?= mb_strtoupper(mb_substr($staff['name'], 0, 1)) ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<div style="min-width:0">
|
||
<div style="font-size:.82rem; font-weight:600; color:#fff; overflow:hidden; text-overflow:ellipsis; white-space:nowrap"><?= e($staff['name']) ?></div>
|
||
<div style="font-size:.72rem; color:#4e6278"><?= $staff['role']==='admin'?'Administrator':'Mitarbeiter' ?></div>
|
||
<?php if (!empty($staff['is_on_vacation'])): ?>
|
||
<div style="font-size:.7rem; color:var(--accent)">🌴 Im Urlaub</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<div class="lang-row" style="margin-bottom:10px"><?= i18n_switcher_html('dark') ?></div>
|
||
<div style="display:flex; gap:14px; align-items:center">
|
||
|
||
<?php
|
||
$__unreadMentions = 0;
|
||
$__unreadChat = 0;
|
||
if ($staff) {
|
||
$__ms = get_db()->prepare('SELECT COUNT(*) FROM staff_mentions WHERE mentioned_staff_id=? AND is_read=0');
|
||
$__ms->execute([$staff['id']]);
|
||
$__unreadMentions = (int)$__ms->fetchColumn();
|
||
$__cs = get_db()->prepare('SELECT COUNT(*) FROM staff_chat_messages WHERE to_staff_id=? AND is_read=0');
|
||
$__cs->execute([$staff['id']]);
|
||
$__unreadChat = (int)$__cs->fetchColumn();
|
||
}
|
||
?>
|
||
<a href="/staff/chat.php" title="Interner Chat" style="position:relative; color:#8b97a3; font-size:.78rem; text-decoration:none">
|
||
💬<?php if ($__unreadChat > 0): ?><span style="position:absolute;top:-6px;right:-8px;background:#dc2626;color:#fff;border-radius:50%;font-size:.6rem;font-weight:700;width:15px;height:15px;display:flex;align-items:center;justify-content:center;line-height:1"><?= $__unreadChat > 9 ? '9+' : $__unreadChat ?></span><?php endif; ?>
|
||
</a>
|
||
<a href="/staff/mentions.php" title="Erwähnungen" style="position:relative; color:#8b97a3; font-size:.78rem; text-decoration:none">
|
||
🔔<?php if ($__unreadMentions > 0): ?><span style="position:absolute;top:-6px;right:-8px;background:#dc2626;color:#fff;border-radius:50%;font-size:.6rem;font-weight:700;width:15px;height:15px;display:flex;align-items:center;justify-content:center;line-height:1"><?= $__unreadMentions > 9 ? '9+' : $__unreadMentions ?></span><?php endif; ?>
|
||
</a>
|
||
<a href="/staff/profile.php" style="color:#8b97a3; font-size:.78rem">Profil</a>
|
||
<a href="/staff/logout.php" style="color:#8b97a3; font-size:.78rem">Abmelden</a>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</aside>
|
||
|
||
<?php
|
||
// Timeout in JS übergeben
|
||
$_jsTimeout = (int)(get_setting('staff_session_timeout', '30') ?: 30);
|
||
$_jsLastAct = $_SESSION['staff_last_activity'] ?? time();
|
||
if (!isset($_SESSION['staff_last_activity'])) {
|
||
$_SESSION['staff_last_activity'] = time();
|
||
$_jsLastAct = time();
|
||
}
|
||
$_jsRemaining = max(10, ($_jsLastAct + $_jsTimeout * 60) - time());
|
||
?>
|
||
<script>
|
||
window.sessionTimerRemaining = <?= (int)$_jsRemaining ?>;
|
||
window.sessionTimerTimeout = <?= (int)$_jsTimeout ?>;
|
||
</script>
|
||
<script>
|
||
(function() {
|
||
var remaining = window.sessionTimerRemaining || 0;
|
||
var timeoutMin = window.sessionTimerTimeout || 30;
|
||
|
||
function init() {
|
||
var el = document.getElementById('session-timer');
|
||
var bar = document.getElementById('session-timer-bar');
|
||
if (!el || !bar || remaining <= 0) return;
|
||
|
||
function fmt(s) {
|
||
var m = Math.floor(s / 60);
|
||
var sec = s % 60;
|
||
return m + ':' + (sec < 10 ? '0' : '') + sec;
|
||
}
|
||
|
||
function tick() {
|
||
if (remaining <= 0) {
|
||
window.location.href = '/staff/login.php';
|
||
return;
|
||
}
|
||
|
||
el.textContent = fmt(remaining);
|
||
|
||
// Immer sichtbar
|
||
bar.style.opacity = '1';
|
||
bar.style.pointerEvents = 'auto';
|
||
|
||
// Farbe nach Dringlichkeit
|
||
if (remaining <= 60) {
|
||
bar.style.background = 'rgba(239,68,68,.9)';
|
||
bar.style.borderColor = 'rgba(239,68,68,.5)';
|
||
el.style.color = '#fff';
|
||
} else if (remaining <= 300) {
|
||
bar.style.background = 'rgba(245,158,11,.9)';
|
||
bar.style.borderColor = 'rgba(245,158,11,.5)';
|
||
el.style.color = '#fff';
|
||
} else {
|
||
bar.style.background = 'rgba(15,23,42,.72)';
|
||
bar.style.borderColor = 'rgba(255,255,255,.1)';
|
||
el.style.color = '#cbd5e1';
|
||
}
|
||
|
||
remaining--;
|
||
setTimeout(tick, 1000);
|
||
}
|
||
|
||
tick();
|
||
|
||
// Bei jeder Nutzeraktion (Klick, Tastatur) Timer via fetch zurücksetzen
|
||
var resetDebounce;
|
||
function resetTimer() {
|
||
clearTimeout(resetDebounce);
|
||
resetDebounce = setTimeout(function() {
|
||
fetch('/staff/session_ping.php', { method:'POST',
|
||
headers:{'X-CSRF-Token': document.querySelector('meta[name=csrf]')?.content || ''},
|
||
credentials:'same-origin' })
|
||
.then(function(r){ return r.json(); })
|
||
.then(function(d){ if (d.remaining) remaining = d.remaining; })
|
||
.catch(function(){});
|
||
}, 2000); // max. alle 2 Sek. ein Ping
|
||
}
|
||
|
||
['click','keydown','mousemove','scroll','touchstart'].forEach(function(ev) {
|
||
document.addEventListener(ev, resetTimer, { passive: true });
|
||
});
|
||
|
||
// Restzeit regelmäßig beim Server erfragen (verlängert nichts): Aktivität in einem anderen Tab verlängert die Zeit hier mit,
|
||
// und ist die Sitzung wirklich abgelaufen, geht es sofort zur Anmeldung
|
||
function syncTimer() {
|
||
fetch('/staff/session_ping.php?check=1', { credentials:'same-origin', cache:'no-store' })
|
||
.then(function(r){ if (r.status === 401) { window.location.href = '/staff/login.php'; return null; } return r.json(); })
|
||
.then(function(d){ if (d && typeof d.remaining === 'number') remaining = d.remaining; })
|
||
.catch(function(){});
|
||
}
|
||
setInterval(syncTimer, 30000);
|
||
} // end init
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
// ── Browser Push Notifications ───────────────────────────────────────────
|
||
(function() {
|
||
var pushKey = document.querySelector('meta[name=push-key]')?.content || document.getElementById('push-vapid-key')?.value;
|
||
if (!pushKey || !('serviceWorker' in navigator) || !('PushManager' in window)) return;
|
||
var btn = document.getElementById('push-toggle-btn');
|
||
|
||
function urlB64ToUint8(b64) {
|
||
var pad = '='.repeat((4 - b64.length % 4) % 4);
|
||
var raw = atob(b64.replace(/-/g,'+').replace(/_/g,'/') + pad);
|
||
return Uint8Array.from([...raw].map(function(ch){ return ch.charCodeAt(0); }));
|
||
}
|
||
|
||
function setBtn(active) {
|
||
if (!btn) return;
|
||
btn.textContent = active ? '🔔' : '🔕';
|
||
btn.style.color = active ? '#22c55e' : '#8b97a3';
|
||
btn.title = active ? 'Push aktiv – klicken zum Deaktivieren' : 'Push-Benachrichtigungen aktivieren';
|
||
}
|
||
|
||
navigator.serviceWorker.ready.then(function(reg) {
|
||
reg.pushManager.getSubscription().then(function(s) {
|
||
setBtn(!!(s && Notification.permission === 'granted'));
|
||
});
|
||
});
|
||
|
||
window.togglePushNotifications = function() {
|
||
navigator.serviceWorker.ready.then(function(reg) {
|
||
reg.pushManager.getSubscription().then(function(sub) {
|
||
if (sub) {
|
||
sub.unsubscribe().then(function() {
|
||
fetch('/staff/push_subscribe.php', { method:'POST', credentials:'same-origin',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({action:'unsubscribe', endpoint: sub.endpoint}) });
|
||
setBtn(false);
|
||
});
|
||
} else {
|
||
Notification.requestPermission().then(function(perm) {
|
||
if (perm !== 'granted') { alert('Bitte Benachrichtigungen im Browser erlauben.'); return; }
|
||
reg.pushManager.subscribe({ userVisibleOnly:true, applicationServerKey: urlB64ToUint8(pushKey) })
|
||
.then(function(ns) {
|
||
var j = ns.toJSON();
|
||
fetch('/staff/push_subscribe.php', { method:'POST', credentials:'same-origin',
|
||
headers:{'Content-Type':'application/json'},
|
||
body: JSON.stringify({action:'subscribe', endpoint:j.endpoint, keys:j.keys}) })
|
||
.then(function() { setBtn(true); });
|
||
}).catch(function(err) { alert('Push fehlgeschlagen: ' + err.message); });
|
||
});
|
||
}
|
||
});
|
||
});
|
||
};
|
||
})();
|
||
</script>
|
||
<script src="/assets/js/sidebar.js?v=<?= @filemtime(__DIR__ . "/../assets/js/sidebar.js") ?: time() ?>"></script>
|
||
<main class="main">
|
||
<!-- Session-Timer: kompakte Pille oben rechts -->
|
||
<div id="session-timer-bar" style="position:fixed;top:12px;right:16px;z-index:9999;display:flex;align-items:center;gap:6px;background:rgba(15,23,42,.75);backdrop-filter:blur(6px);border:1px solid rgba(255,255,255,.1);border-radius:20px;padding:4px 12px 4px 8px;pointer-events:none;opacity:0;transition:opacity .3s">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#94a3b8" stroke-width="2.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||
<span id="session-timer" style="font-size:.72rem;font-weight:700;color:#cbd5e1;font-variant-numeric:tabular-nums;letter-spacing:.02em"></span>
|
||
</div>
|
||
<?= plugin_filter('staff.page.top', '', ['staff' => $staff ?? null, 'activePage' => $activePage ?? '']) /* Hinweise von Plugins oben auf jeder Seite (fertiges HTML aus vertrauenswürdigem Plugin-Code) */ ?>
|
||
<?php foreach (get_flashes() as $f): ?>
|
||
<div class="flash <?= e($f['type']) ?>"><?= e($f['message']) ?></div>
|
||
<?php endforeach; ?>
|
||
<?php
|
||
if (isset($staff) && has_permission($staff, 'manage_email_addresses')) {
|
||
foreach (system_health(get_db()) as $__issue): ?>
|
||
<div class="flash <?= $__issue['level'] === 'error' ? 'error' : 'warning' ?>" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||
<strong><?= $__issue['level'] === 'error' ? 'Störung:' : 'Hinweis:' ?></strong> <?= e($__issue['message']) ?>
|
||
<a href="/staff/email_addresses.php" style="margin-left:auto">E-Mail-Adressen öffnen</a>
|
||
</div>
|
||
<?php endforeach; } ?>
|
||
<?php
|
||
if (isset($staff) && ($staff['role'] ?? '') === 'admin' && ($activePage ?? '') !== 'update') {
|
||
require_once __DIR__ . '/../database/migrate.php';
|
||
$__pending = migrations_pending(get_db());
|
||
if ($__pending): ?>
|
||
<div class="flash warning" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||
<strong>Datenbank veraltet:</strong> <?= count($__pending) ?> ausstehende Änderung(en) – der Code ist neuer als die Datenbank.
|
||
<a href="/staff/update.php" style="margin-left:auto">Jetzt aktualisieren</a>
|
||
</div>
|
||
<?php endif; } ?>
|
||
<?php
|
||
if (isset($staff) && ($staff['role'] ?? '') === 'admin' && ($activePage ?? '') !== 'plugins' && plugin_official_configured()) {
|
||
$__upd = plugin_updates_available(get_db());
|
||
if ($__upd): ?>
|
||
<div class="flash warning" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||
<strong>Plugin-Updates verfügbar:</strong> <?= e(implode(', ', array_map(fn($u) => $u['name'] . ' ' . $u['available'], $__upd))) ?>
|
||
<a href="/staff/plugins.php" style="margin-left:auto">Zu den Plugins</a>
|
||
</div>
|
||
<?php endif; } ?>
|
||
<?php $lockedOverride = get_locked_ticket_override(); ?>
|
||
<?php if ($lockedOverride && isset($staff) && $staff['role'] === 'admin'): ?>
|
||
<div class="flash warning" style="display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||
🔓 Als Admin können Sie die Sperre von „<?= e($lockedOverride['name']) ?>“ auf Ticket #<?= e($lockedOverride['number']) ?> sofort aufheben, statt auf den automatischen Timeout zu warten.
|
||
<form method="post" action="/staff/ticket_view.php?n=<?= urlencode($lockedOverride['number']) ?>" style="margin:0">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="admin_force_unlock">
|
||
<input type="hidden" name="locked_by_name" value="<?= e($lockedOverride['name']) ?>">
|
||
<button type="submit" class="btn btn-sm" style="padding:4px 10px;font-size:.78rem">Sperre jetzt aufheben</button>
|
||
</form>
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php if (isset($staff) && $staff['role'] === 'admin' && (config()['db']['driver'] ?? 'sqlite') === 'sqlite'): ?>
|
||
<div class="flash warning" style="display:flex;align-items:center;gap:10px">
|
||
⚠️ <strong>SQLite-Modus aktiv:</strong> Nicht für den Produktivbetrieb geeignet.
|
||
Bitte in <code>config/config.php</code> auf <code>driver = 'mysql'</code> umstellen und MySQL-Zugangsdaten eintragen.
|
||
</div>
|
||
<?php endif; ?>
|
||
<?php if (isset($staff) && $staff['role'] === 'admin' && empty(config()['app_key']) && !get_setting('app_key')): ?>
|
||
<div class="flash warning" style="display:flex;align-items:center;gap:10px">
|
||
🔑 <strong>Kein App-Key gesetzt:</strong> Passwörter werden nur schwach verschlüsselt.
|
||
Bitte in <code>config/config.php</code> den Wert <code>app_key</code> setzen.
|
||
Generieren: <code>php -r "echo bin2hex(random_bytes(32));"</code>
|
||
</div>
|
||
<?php endif; ?>
|