Files
2026-05-21 13:57:27 +02:00

1465 lines
87 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>COM|hub</title>
<link href="{{ url_for('static', filename='bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ url_for('static', filename='bootstrap-icons.css') }}" rel="stylesheet">
<style>
:root {
--primary-color: #0077b6;
--primary-light: #00b4d8;
--primary-subtle: rgba(0, 119, 182, 0.08);
--accent-color: #0096c7;
--bg-body: #f0f4f8;
--bg-sidebar: #ffffff;
--bg-card: #ffffff;
--text-main: #1a2332;
--text-secondary: #475569;
--text-muted: #94a3b8;
--border-color: rgba(0,0,0,0.06);
--card-shadow: 0 1px 3px rgba(0,0,0,0.04), 0 6px 16px rgba(0,0,0,0.04);
--card-shadow-hover: 0 4px 12px rgba(0,0,0,0.08), 0 12px 28px rgba(0,0,0,0.06);
--hover-bg: #f8fafc;
--input-bg: #ffffff;
--input-border: #e2e8f0;
--sidebar-width: 260px;
--navbar-height: 56px;
--success-color: #059669;
--danger-color: #dc2626;
--warning-color: #d97706;
}
[data-theme="dark"] {
--primary-color: #38bdf8;
--primary-light: #7dd3fc;
--primary-subtle: rgba(56, 189, 248, 0.1);
--accent-color: #48cae4;
--bg-body: #0f172a;
--bg-sidebar: #1e293b;
--bg-card: #1e293b;
--text-main: #e2e8f0;
--text-secondary: #cbd5e1;
--text-muted: #64748b;
--border-color: rgba(255,255,255,0.08);
--card-shadow: 0 1px 3px rgba(0,0,0,0.2), 0 6px 16px rgba(0,0,0,0.15);
--card-shadow-hover: 0 4px 12px rgba(0,0,0,0.25), 0 12px 28px rgba(0,0,0,0.2);
--hover-bg: #334155;
--input-bg: #1e293b;
--input-border: #334155;
--success-color: #34d399;
--danger-color: #f87171;
--warning-color: #fbbf24;
}
*, *::before, *::after { transition: background-color 0.2s ease, border-color 0.2s ease, color 0.15s ease, box-shadow 0.2s ease; }
body {
font-family: 'Inter', system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: .875rem;
background-color: var(--bg-body);
color: var(--text-main);
overflow-x: hidden;
line-height: 1.6;
}
/* --- Navbar --- */
.navbar {
background: var(--bg-sidebar) !important;
border-bottom: 1px solid var(--border-color);
height: var(--navbar-height);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
z-index: 1030;
}
.navbar-brand {
color: var(--primary-color) !important;
font-weight: 800;
font-size: 1.15rem;
letter-spacing: -0.5px;
}
.navbar-brand span { font-weight: 300; opacity: 0.5; }
/* --- Sidebar --- */
.sidebar {
position: fixed;
top: var(--navbar-height);
bottom: 0;
left: 0;
z-index: 100;
padding: 20px 12px;
background-color: var(--bg-sidebar);
border-right: 1px solid var(--border-color);
width: var(--sidebar-width);
overflow-y: auto;
overflow-x: hidden;
}
.sidebar::-webkit-scrollbar { width: 4px; }
.sidebar::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; }
.sidebar-heading {
font-size: 0.65rem;
font-weight: 700;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--text-muted);
padding: 0 12px;
margin-top: 1.5rem !important;
margin-bottom: 0.5rem !important;
}
.nav-link {
font-weight: 500;
font-size: 0.85rem;
color: var(--text-secondary);
padding: 0.6rem 0.85rem;
border-radius: 10px;
margin-bottom: 2px;
display: flex;
align-items: center;
position: relative;
}
.nav-link i { font-size: 1.05rem; width: 22px; text-align: center; opacity: 0.7; }
.nav-link:hover { color: var(--primary-color); background-color: var(--primary-subtle); }
.nav-link:hover i { opacity: 1; }
.nav-link.active {
color: var(--primary-color);
background-color: var(--primary-subtle);
font-weight: 600;
}
.nav-link.active::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 60%;
background: var(--primary-color);
border-radius: 0 3px 3px 0;
}
.nav-link.active i { opacity: 1; }
.nav-link .badge { font-size: 0.7rem; font-weight: 600; padding: 0.25em 0.55em; }
/* --- Main Content --- */
main {
margin-top: var(--navbar-height);
margin-left: var(--sidebar-width);
padding: 28px 32px;
min-height: calc(100vh - var(--navbar-height));
}
/* --- Cards --- */
.card {
background-color: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 14px;
box-shadow: var(--card-shadow);
}
.card:hover { box-shadow: var(--card-shadow-hover); }
/* --- Tables --- */
.table { color: var(--text-main); --bs-table-bg: transparent; margin-bottom: 0; }
.table thead th {
background-color: var(--bg-card);
color: var(--text-muted);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.5px;
text-transform: uppercase;
border-bottom: 1px solid var(--border-color);
padding: 0.85rem 1rem;
}
.table td {
border-bottom: 1px solid var(--border-color);
padding: 0.85rem 1rem;
vertical-align: middle;
}
.table tbody tr:last-child td { border-bottom: none; }
.table-hover tbody tr:hover td { background-color: var(--hover-bg); }
/* --- Forms --- */
.form-control, .form-select {
background-color: var(--input-bg);
border: 1.5px solid var(--input-border);
color: var(--text-main);
border-radius: 10px;
padding: 0.5rem 0.85rem;
font-size: 0.85rem;
}
.form-control:focus, .form-select:focus {
background-color: var(--input-bg);
color: var(--text-main);
border-color: var(--primary-color);
box-shadow: 0 0 0 3px var(--primary-subtle);
}
.form-label { font-weight: 600; font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 0.35rem; }
.form-text { font-size: 0.75rem; color: var(--text-muted); }
/* --- Buttons --- */
.btn { border-radius: 8px; font-weight: 600; font-size: 0.8rem; padding: 0.4rem 0.85rem; letter-spacing: 0.01em; }
.btn-sm { padding: 0.3rem 0.65rem; font-size: 0.75rem; border-radius: 7px; }
.btn-primary { background-color: var(--primary-color); border-color: var(--primary-color); }
.btn-primary:hover { background-color: var(--primary-light); border-color: var(--primary-light); filter: brightness(0.95); }
.btn-outline-primary { color: var(--primary-color); border-color: var(--primary-color); }
.btn-outline-primary:hover { background-color: var(--primary-color); border-color: var(--primary-color); }
.btn-success { background-color: var(--success-color); border-color: var(--success-color); }
.btn-danger { background-color: var(--danger-color); border-color: var(--danger-color); }
/* --- Modals --- */
.modal-content {
background-color: var(--bg-card);
color: var(--text-main);
border: 1px solid var(--border-color);
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
}
.modal-header { border-color: var(--border-color); padding: 1.15rem 1.5rem; }
.modal-body { padding: 1.25rem 1.5rem; }
.modal-footer { border-color: var(--border-color); padding: 1rem 1.5rem; }
[data-theme="light"] .btn-close { filter: none; }
[data-theme="dark"] .btn-close { filter: invert(1) grayscale(100%) brightness(200%); }
/* --- Nav Tabs (Settings) --- */
.nav-tabs { border-color: var(--border-color); }
.nav-tabs .nav-link {
border-radius: 8px 8px 0 0;
padding: 0.5rem 1rem;
font-size: 0.8rem;
color: var(--text-muted);
}
.nav-tabs .nav-link.active {
background-color: var(--bg-card);
color: var(--primary-color);
border-color: var(--border-color) var(--border-color) var(--bg-card);
}
.nav-tabs .nav-link::before { display: none; }
/* --- Utility --- */
.text-muted { color: var(--text-muted) !important; }
.text-dark { color: var(--text-main) !important; }
.bg-light { background-color: var(--hover-bg) !important; }
.pdf-frame { width: 100%; height: 650px; border: none; background-color: var(--bg-body); border-radius: 12px; }
.transcription-large {
font-size: 1.05rem;
line-height: 1.75;
background-color: var(--hover-bg);
color: var(--text-main);
border-radius: 12px;
border: 1px solid var(--border-color);
}
.transcription-text { font-size: 0.85rem; line-height: 1.6; white-space: pre-wrap; color: var(--text-secondary); }
/* --- Status Indicator --- */
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 6px; }
.status-dot.online { background: var(--success-color); box-shadow: 0 0 6px rgba(5, 150, 105, 0.4); }
.status-dot.offline { background: var(--danger-color); box-shadow: 0 0 6px rgba(220, 38, 38, 0.4); }
/* --- Animations --- */
@keyframes pulse {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.08); }
100% { opacity: 1; transform: scale(1); }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.pulse-icon { animation: pulse 1.5s infinite; color: var(--danger-color); }
.save-pulse { border-color: var(--success-color) !important; box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.15) !important; }
.card { animation: fadeIn 0.3s ease-out; }
/* --- Empty State --- */
.empty-state { padding: 3.5rem 1rem; }
.empty-state i { font-size: 3rem; color: var(--text-muted); opacity: 0.4; margin-bottom: 1rem; }
.empty-state p { font-size: 0.95rem; color: var(--text-muted); }
/* --- Live Queue Card --- */
.queue-card {
border: none;
border-radius: 12px;
background: linear-gradient(135deg, rgba(220, 38, 38, 0.05), rgba(220, 38, 38, 0.02));
border-left: 4px solid var(--danger-color);
}
/* --- Mobile Sidebar --- */
.sidebar-toggle { display: none; background: none; border: none; font-size: 1.25rem; color: var(--text-main); padding: 0.25rem 0.5rem; }
@media (max-width: 991.98px) {
.sidebar-toggle { display: inline-flex; align-items: center; }
.sidebar {
transform: translateX(-100%);
transition: transform 0.25s ease;
box-shadow: none;
}
.sidebar.show {
transform: translateX(0);
box-shadow: 4px 0 16px rgba(0,0,0,0.1);
}
main { margin-left: 0; padding: 20px 16px; }
.sidebar-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.3);
z-index: 99;
backdrop-filter: blur(2px);
}
.sidebar-overlay.show { display: block; }
}
/* --- Page Header --- */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color);
}
.page-header h1 { font-size: 1.35rem; font-weight: 700; margin: 0; }
.page-header .subtitle { font-weight: 400; color: var(--text-muted); font-size: 1rem; }
/* --- Form Switch --- */
.form-check-input:checked { background-color: var(--primary-color); border-color: var(--primary-color); }
/* --- Item Row (Card-List) --- */
.item-row {
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
}
.item-row:last-child { border-bottom: none; }
.item-row:hover { background-color: var(--hover-bg); }
.item-row-highlight { background-color: var(--primary-subtle); }
.item-row-highlight:hover { background-color: var(--primary-subtle); filter: brightness(0.98); }
.item-row-warning { background-color: rgba(217, 119, 6, 0.04); }
.item-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.05rem;
flex-shrink: 0;
background: var(--primary-subtle);
color: var(--primary-color);
}
.item-icon-new { background: rgba(5, 150, 105, 0.1); color: var(--success-color); }
.item-icon-done { background: var(--hover-bg); color: var(--text-muted); }
.item-date { font-size: 0.75rem; color: var(--text-muted); }
.item-content { font-size: 0.85rem; color: var(--text-secondary); line-height: 1.5; }
.min-width-0 { min-width: 0; }
/* --- Status Badges --- */
.status-badge {
display: inline-flex;
align-items: center;
padding: 0.25em 0.6em;
border-radius: 6px;
font-size: 0.7rem;
font-weight: 600;
white-space: nowrap;
}
.status-success { background: rgba(5, 150, 105, 0.1); color: var(--success-color); }
.status-danger { background: rgba(220, 38, 38, 0.1); color: var(--danger-color); }
.status-warning { background: rgba(217, 119, 6, 0.1); color: var(--warning-color); }
/* --- Settings Panel --- */
.settings-nav-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 12px;
border: none;
border-radius: 8px;
background: transparent;
color: var(--text-secondary);
font-size: 0.8rem;
font-weight: 500;
text-align: left;
cursor: pointer;
margin-bottom: 2px;
}
.settings-nav-item:hover { background: var(--primary-subtle); color: var(--primary-color); }
.settings-nav-item.active { background: var(--primary-subtle); color: var(--primary-color); font-weight: 600; }
.settings-nav-item i { font-size: 0.95rem; width: 18px; text-align: center; }
.settings-nav-danger { color: var(--danger-color) !important; }
.settings-nav-danger.active, .settings-nav-danger:hover { background: rgba(220, 38, 38, 0.08) !important; color: var(--danger-color) !important; }
.settings-pane { display: none; }
.settings-pane.active { display: block; animation: fadeIn 0.2s ease; }
.settings-card {
padding: 16px;
border: 1px solid var(--border-color);
border-radius: 12px;
background: var(--bg-card);
}
.settings-icon {
width: 36px;
height: 36px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
flex-shrink: 0;
}
</style>
</head>
<body data-theme="light">
<header class="navbar sticky-top flex-md-nowrap p-0 px-3">
<div class="d-flex align-items-center">
<button class="sidebar-toggle me-2" onclick="toggleSidebar()" aria-label="Menu">
<i class="bi bi-list"></i>
</button>
<a class="navbar-brand me-0" href="#">COM<span>|</span>hub</a>
</div>
<div class="d-flex align-items-center gap-2">
<button class="btn btn-link nav-link px-2" onclick="toggleTheme()" title="Design wechseln" style="font-size: 1rem;">
<i class="bi bi-moon-stars-fill" id="themeIcon"></i>
</button>
<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('trigger_sync') }}" title="Daten synchronisieren"><i class="bi bi-arrow-repeat"></i></a>
<button class="btn btn-sm btn-primary" onclick="handleSettingsClick()"><i class="bi bi-gear-fill me-1"></i> Einstellungen</button>
</div>
</header>
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block sidebar collapse">
<div class="position-sticky sidebar-sticky">
{% if config.get('module_voicemail', 'true') == 'true' %}
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-2 mt-3 mb-2 text-uppercase text-muted"><span>Voicemail</span></h6>
<ul class="nav flex-column">
<li class="nav-item"><a class="nav-link {% if active_tab == 'vm_inbox' %}active{% endif %}" href="{{ url_for('voicemail_inbox') }}"><i class="bi bi-mic me-2"></i> Sprachbox <span id="badge-vm" class="badge bg-danger rounded-pill float-end {% if cnt_vm == 0 %}d-none{% endif %}">{{ cnt_vm }}</span></a></li>
<li class="nav-item"><a class="nav-link {% if active_tab == 'vm_archive' %}active{% endif %}" href="{{ url_for('voicemail_archive') }}"><i class="bi bi-archive me-2"></i> Archiv</a></li>
</ul>
{% endif %}
{% if config.get('module_fax', 'true') == 'true' or config.get('module_docs', 'true') == 'true' %}
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-2 mt-4 mb-2 text-uppercase text-muted"><span>Dokumenten Management</span></h6>
<ul class="nav flex-column mb-2">
{% if config.get('module_fax', 'true') == 'true' %}
<li class="nav-item"><a class="nav-link {% if active_tab == 'fax_inbox' %}active{% endif %}" href="{{ url_for('fax_inbox') }}"><i class="bi bi-printer me-2"></i> Faxeingang <span id="badge-fax" class="badge bg-danger rounded-pill float-end {% if cnt_fax == 0 %}d-none{% endif %}">{{ cnt_fax }}</span></a></li>
{% endif %}
{% if config.get('module_docs', 'true') == 'true' %}
<li class="nav-item"><a class="nav-link {% if active_tab == 'doc_inbox' %}active{% endif %}" href="{{ url_for('doc_inbox') }}"><i class="bi bi-folder2-open me-2"></i> Dokumente <span id="badge-doc" class="badge bg-info text-dark rounded-pill float-end {% if cnt_doc == 0 %}d-none{% endif %}">{{ cnt_doc }}</span></a></li>
{% endif %}
{% if config.get('module_fax', 'true') == 'true' %}
<li class="nav-item"><a class="nav-link {% if active_tab == 'fax_outbox' %}active{% endif %}" href="{{ url_for('fax_outbox') }}"><i class="bi bi-send me-2"></i> Faxausgang <span id="badge-out" class="badge bg-primary rounded-pill float-end {% if cnt_out == 0 %}d-none{% endif %}">{{ cnt_out }}</span></a></li>
{% endif %}
{% if config.get('module_docs', 'true') == 'true' %}
<li class="nav-item"><a class="nav-link {% if active_tab == 'fax_review' %}active{% endif %}" href="{{ url_for('fax_review') }}"><i class="bi bi-eye me-2"></i> Prüfliste <span id="badge-rev" class="badge bg-warning text-dark rounded-pill float-end {% if cnt_rev == 0 %}d-none{% endif %}">{{ cnt_rev }}</span></a></li>
{% endif %}
</ul>
{% endif %}
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-2 mt-4 mb-2 text-uppercase text-muted"><span>Kontakte</span></h6>
<ul class="nav flex-column mb-2">
<li class="nav-item"><a class="nav-link {% if active_tab == 'contacts' %}active{% endif %}" href="{{ url_for('contact_page') }}"><i class="bi bi-book me-2"></i> Adressbuch</a></li>
</ul>
<div class="mt-auto px-3 py-4 small text-muted">
<span class="status-dot {% if config.placetel_api_token %}online{% else %}offline{% endif %}"></span> {% if config.placetel_api_token %}Verbunden{% else %}Getrennt{% endif %}
</div>
</div>
</nav>
<main class="col-md-9 ms-sm-auto col-lg-10">
<div class="mt-2 mb-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show shadow-sm border-0">{{ msg }} <button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
{% if config.get('module_voicemail', 'true') == 'true' %}
<div id="liveQueueContainer" class="mb-4 d-none">
<h5 class="fw-bold mb-3"><i class="bi bi-telephone-inbound-fill me-2 pulse-icon"></i> Aktuelle Warteschlange</h5>
<div class="row g-3" id="liveQueueCards"></div>
</div>
{% endif %}
<div class="page-header">
<h1>
{% if active_tab == 'vm_inbox' %}Sprachbox <span class="subtitle">Voicemail</span>{% endif %}
{% if active_tab == 'vm_archive' %}Archiv <span class="subtitle">Voicemail</span>{% endif %}
{% if active_tab == 'fax_inbox' %}Faxeingang <span class="subtitle">Dokumenten Management</span>{% endif %}
{% if active_tab == 'doc_inbox' %}Dokumente <span class="subtitle">Ordner-Import</span>{% endif %}
{% if active_tab == 'fax_outbox' %}Faxausgang <span class="subtitle">Dokumenten Management</span>{% endif %}
{% if active_tab == 'fax_review' %}Prüfliste <span class="subtitle">Dokumenten Management</span>{% endif %}
{% if active_tab == 'contacts' %}Adressbuch{% endif %}
</h1>
<div>
{% if active_tab == 'contacts' %}
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#contactModal"><i class="bi bi-person-plus-fill me-1"></i> Neuer Kontakt</button>
{% endif %}
{% if active_tab == 'fax_review' and config.get('review_pin_enabled', 'false') == 'true' and not is_locked %}
<a href="{{ url_for('lock_review') }}" class="btn btn-outline-warning"><i class="bi bi-lock-fill me-1"></i> Sperren</a>
{% endif %}
</div>
</div>
<div class="card">
{% if 'vm_' in active_tab %}
<div class="card-body p-0">
{% for vm in voicemails %}
<div class="item-row {% if vm.status == 'Neu' %}item-row-highlight{% endif %}">
<div class="d-flex align-items-start gap-3">
<div class="item-icon {% if vm.status == 'Neu' %}item-icon-new{% else %}item-icon-done{% endif %}">
<i class="bi bi-{% if vm.status == 'Neu' %}mic-fill{% else %}check-lg{% endif %}"></i>
</div>
<div class="flex-grow-1 min-width-0">
<div class="d-flex justify-content-between align-items-start mb-1">
<div class="item-date">{{ vm.received_formatted }}</div>
<div class="d-flex gap-1 flex-shrink-0">
<button class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#vmModal{{ vm.id }}" title="Details"><i class="bi bi-eye"></i></button>
{% if active_tab == 'vm_inbox' %}<a href="/archive_toggle/{{ vm.id }}" class="btn btn-sm btn-outline-secondary" title="Archivieren"><i class="bi bi-archive"></i></a>{% endif %}
</div>
</div>
<div class="item-content mb-2">
{% if vm.transcription == '[Transkription wird vorbereitet...]' or vm.transcription == '[Transkription läuft...]' %}
<span class="text-primary fw-bold" style="animation: pulse 2s infinite;"><i class="bi bi-mic-fill me-1"></i> {{ vm.transcription }}</span>
{% else %}
<div class="transcription-text" id="text_{{ vm.id }}">{{ vm.transcription }}</div>
{% endif %}
</div>
<div>
{% if vm.status == 'Neu' %}
<a href="/status/{{ vm.id }}/Erledigt" class="btn btn-success btn-sm"><i class="bi bi-check-lg me-1"></i>Erledigt</a>
{% else %}
<a href="/status/{{ vm.id }}/Neu" class="btn btn-outline-secondary btn-sm"><i class="bi bi-arrow-counterclockwise me-1"></i>Zurück</a>
{% endif %}
</div>
</div>
</div>
<!-- VM Detail Modal -->
<div class="modal fade" id="vmModal{{ vm.id }}" tabindex="-1">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0 pb-0">
<div>
<h6 class="modal-title fw-bold mb-0">Voicemail Details</h6>
<small class="text-muted">{{ vm.received_formatted }}</small>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body pt-3 px-4 pb-4">
<label class="form-label">Transkription</label>
<div class="settings-card mb-3">
<div style="font-size: 0.95rem; line-height: 1.7;">{{ vm.transcription }}</div>
</div>
<label class="form-label">Audio</label>
{% if vm.audio_path %}
<audio controls class="w-100" style="border-radius: 10px;"><source src="{{ url_for('serve_file', filename=vm.audio_path) }}" type="audio/mpeg"></audio>
{% else %}
<div class="settings-card text-center text-muted py-3"><i class="bi bi-exclamation-circle me-1"></i> Audio nicht verfügbar</div>
{% endif %}
</div>
<div class="modal-footer d-flex justify-content-between">
{% if config.get('allow_retranscription', 'false') == 'true' and config.get('module_transcription', 'true') == 'true' %}
<a href="{{ url_for('retranscribe_voicemail', v_id=vm.id) }}" class="btn btn-outline-primary btn-sm"><i class="bi bi-arrow-repeat me-1"></i> Neu transkribieren</a>
{% else %}
<div></div>
{% endif %}
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Schließen</button>
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="empty-state text-center"><i class="bi bi-mic-mute d-block"></i><p class="mb-0">Keine Voicemails vorhanden.</p></div>
{% endfor %}
</div>
{% endif %}
{% if active_tab in ['fax_inbox', 'doc_inbox', 'fax_review'] %}
{% if active_tab == 'fax_review' and is_locked %}
<div class="card-body p-0">
<div class="empty-state text-center">
<i class="bi bi-lock-fill d-block" style="opacity: 0.6;"></i>
<p class="fw-bold mb-1 text-dark" style="font-size: 1rem;">Prüfliste gesperrt</p>
<p class="text-muted small mb-3">PIN eingeben, um vertrauliche Dokumente anzuzeigen.</p>
<form action="{{ url_for('unlock_review') }}" method="POST" class="d-inline-block" style="max-width: 220px;">
<div class="input-group">
<input type="password" name="pin" class="form-control text-center font-monospace" placeholder="----" required autofocus style="letter-spacing: 6px;">
<button class="btn btn-primary" type="submit"><i class="bi bi-unlock-fill"></i></button>
</div>
</form>
</div>
</div>
{% else %}
<div class="card-body p-0">
{% for fax in faxes %}
<div class="item-row {% if fax.status == 'Prüfung' %}item-row-warning{% endif %}">
<div class="d-flex align-items-start gap-3">
<div class="item-icon" style="background: {% if active_tab == 'fax_inbox' %}rgba(0, 119, 182, 0.1); color: var(--primary-color){% elif active_tab == 'doc_inbox' %}rgba(217, 119, 6, 0.1); color: var(--warning-color){% else %}rgba(99, 102, 241, 0.1); color: #6366f1{% endif %};">
<i class="bi bi-{% if active_tab == 'fax_inbox' %}printer{% elif active_tab == 'doc_inbox' %}file-earmark-text{% else %}eye{% endif %}"></i>
</div>
<div class="flex-grow-1 min-width-0">
<div class="d-flex justify-content-between align-items-start mb-1">
<div>
<div class="fw-bold" style="font-size: 0.85rem; color: var(--text-main);">{{ fax.title }}</div>
<div class="small text-muted">
{% if fax.resolved_name != fax.sender %}
<span title="{{ fax.sender }}" style="cursor: help; border-bottom: 1px dotted var(--text-muted);">{{ fax.resolved_name }}</span>
{% else %}
<span>{{ fax.sender }}</span>
{% endif %}
<span class="mx-1">-</span>{{ fax.received_formatted }}
</div>
</div>
<button type="button" class="btn btn-outline-danger btn-sm flex-shrink-0" onclick="confirmDelete('{{ fax.id }}', null)" title="Löschen"><i class="bi bi-trash"></i></button>
</div>
<div class="d-flex flex-wrap gap-1 mt-2">
<button class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#pdfModal{{ fax.id }}"><i class="bi bi-search me-1"></i>Vorschau</button>
{% if active_tab in ['fax_inbox', 'doc_inbox'] and config.get('module_docs', 'true') == 'true' %}
<a href="{{ url_for('fax_action', f_id=fax.id, action='to_review') }}" class="btn btn-outline-warning btn-sm"><i class="bi bi-eye me-1"></i>Prüfen</a>
{% endif %}
<button type="button" class="btn btn-success btn-sm" onclick="openStandardExportModal('{{ fax.id }}')"><i class="bi bi-download me-1"></i>Export</button>
{% if config.get('module_gdt', 'false') == 'true' %}
<button type="button" class="btn btn-info btn-sm text-white" onclick="openGdtExportModal('{{ fax.id }}')"><i class="bi bi-diagram-3 me-1"></i>PVS</button>
{% endif %}
</div>
</div>
</div>
<!-- PDF Preview Modal -->
<div class="modal fade" id="pdfModal{{ fax.id }}" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered" style="height: 90vh;"><div class="modal-content h-100"><div class="modal-header"><h5 class="modal-title fw-bold" style="font-size: 0.9rem;">{{ fax.title }} - {{ fax.sender }}</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe src="{{ url_for('serve_file', filename=fax.file_path) }}" class="pdf-frame" style="height: 100%;"></iframe></div></div></div></div>
</div>
{% else %}
<div class="empty-state text-center"><i class="bi bi-file-earmark-x d-block"></i><p class="mb-0">Keine Dokumente vorhanden.</p></div>
{% endfor %}
</div>
{% endif %}
{% endif %}
{% if active_tab == 'fax_outbox' %}
<div class="card-body p-0">
{% for out in faxes_out %}
<div class="item-row {% if out.status == 'Entwurf' %}item-row-highlight{% endif %}">
<div class="d-flex align-items-start gap-3">
<div class="item-icon" style="background: {% if out.status == 'Erfolgreich' %}rgba(5, 150, 105, 0.1); color: var(--success-color){% elif out.status == 'Fehlgeschlagen' %}rgba(220, 38, 38, 0.1); color: var(--danger-color){% elif out.status == 'In Arbeit' %}rgba(217, 119, 6, 0.1); color: var(--warning-color){% else %}rgba(0, 119, 182, 0.1); color: var(--primary-color){% endif %};">
<i class="bi bi-{% if out.status == 'Erfolgreich' %}check-circle{% elif out.status == 'Fehlgeschlagen' %}exclamation-triangle{% elif out.status == 'In Arbeit' %}hourglass-split{% else %}send{% endif %}"></i>
</div>
<div class="flex-grow-1 min-width-0">
<div class="d-flex justify-content-between align-items-start mb-1">
<div>
{% if out.status != 'Entwurf' %}
<div class="fw-bold" style="font-size: 0.85rem;">
{% if out.resolved_name and out.resolved_name != out.recipient %}
<span title="{{ out.recipient }}" style="cursor: help;">{{ out.resolved_name }}</span>
{% else %}
<span class="font-monospace">{{ out.recipient }}</span>
{% endif %}
</div>
{% endif %}
<div class="item-date">{{ out.created_formatted }}{% if out.note and out.status != 'Entwurf' %} - <span class="text-muted">{{ out.note }}</span>{% endif %}</div>
</div>
<div class="d-flex gap-1 flex-shrink-0 align-items-center">
{% if out.status == 'Erfolgreich' %}
<span class="status-badge status-success"><i class="bi bi-check-circle me-1"></i>Erfolgreich</span>
{% elif out.status == 'Fehlgeschlagen' %}
<span class="status-badge status-danger"><i class="bi bi-exclamation-triangle me-1"></i>Fehlgeschlagen</span>
{% elif out.status == 'In Arbeit' %}
<span class="status-badge status-warning"><i class="bi bi-hourglass-split me-1 pulse-icon"></i>In Arbeit</span>
{% endif %}
</div>
</div>
{% if out.status == 'Entwurf' %}
<form action="{{ url_for('fax_send') }}" method="POST" id="form_send_{{ out.id }}" class="m-0">
<input type="hidden" name="out_id" value="{{ out.id }}">
<div class="row g-2 mb-2">
<div class="col-sm-5">
<div class="input-group input-group-sm">
<input type="text" id="recip_{{ out.id }}" name="recipient" class="form-control font-monospace" placeholder="Faxnummer..." required>
<button class="btn btn-outline-secondary" type="button" data-bs-toggle="modal" data-bs-target="#selectContactModal" onclick="setTargetInput('recip_{{ out.id }}')" title="Aus Adressbuch"><i class="bi bi-book"></i></button>
</div>
</div>
<div class="col-sm-4">
<input type="text" name="note" value="{{ out.note if out.note else '' }}" class="form-control form-control-sm" placeholder="Notiz (optional)" onchange="saveFaxNote('{{ out.id }}', this)">
</div>
<div class="col-sm-3">
<button type="submit" class="btn btn-primary btn-sm w-100"><i class="bi bi-send-fill me-1"></i>Senden</button>
</div>
</div>
</form>
{% endif %}
<div class="d-flex gap-1">
<button class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#pdfModalOut{{ out.id }}"><i class="bi bi-search me-1"></i>Vorschau</button>
<button type="button" class="btn btn-outline-danger btn-sm" onclick="confirmDelete(null, '{{ out.id }}')"><i class="bi bi-trash"></i></button>
</div>
</div>
</div>
<div class="modal fade" id="pdfModalOut{{ out.id }}" tabindex="-1"><div class="modal-dialog modal-xl modal-dialog-centered" style="height: 90vh;"><div class="modal-content h-100"><div class="modal-header"><h5 class="modal-title fw-bold" style="font-size: 0.9rem;">Ausgehendes Dokument</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body p-0"><iframe src="{{ url_for('serve_file', filename=out.file_path) }}" class="pdf-frame" style="height: 100%;"></iframe></div></div></div></div>
</div>
{% else %}
<div class="empty-state text-center"><i class="bi bi-send-x d-block"></i><p class="mb-0">Keine ausgehenden Faxe.<br><span class="small">Drucke ein Dokument über deinen PDF-Drucker.</span></p></div>
{% endfor %}
</div>
{% endif %}
{% if active_tab == 'contacts' %}
<div class="card-body p-0">
{% for c in contacts %}
<div class="item-row">
<div class="d-flex align-items-center gap-3">
<div class="item-icon" style="background: rgba(99, 102, 241, 0.1); color: #6366f1;">
<i class="bi bi-person"></i>
</div>
<div class="flex-grow-1 min-width-0">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="fw-bold" style="font-size: 0.85rem;">
{{ c.first_name }} {{ c.last_name }}
{% if c.patient_id %}<span class="text-muted fw-normal" style="font-size: 0.75rem;"> #{{ c.patient_id }}</span>{% endif %}
</div>
<div class="d-flex flex-wrap gap-3 mt-1">
{% if c.company %}<span class="small text-muted"><i class="bi bi-building me-1"></i>{{ c.company }}</span>{% endif %}
{% if c.number %}<span class="small font-monospace" style="color: var(--primary-color);"><i class="bi bi-telephone me-1"></i>{{ c.number }}</span>{% endif %}
{% if c.fax_number %}<span class="small font-monospace text-info"><i class="bi bi-printer me-1"></i>{{ c.fax_number }}</span>{% endif %}
</div>
</div>
<div class="d-flex gap-1 flex-shrink-0">
<button class="btn btn-sm btn-outline-secondary" onclick="openEditContact('{{c.id}}', '{{c.first_name}}', '{{c.last_name}}', '{{c.company}}', '{{c.number}}', '{{c.fax_number}}', '{{c.patient_id}}')" title="Bearbeiten"><i class="bi bi-pencil"></i></button>
<button class="btn btn-sm btn-outline-danger" onclick="confirmDeleteContact('{{c.id}}')" title="Löschen"><i class="bi bi-trash"></i></button>
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="empty-state text-center"><i class="bi bi-person-x d-block"></i><p class="mb-0">Keine Kontakte vorhanden.</p></div>
{% endfor %}
</div>
{% endif %}
</div>
</main>
</div>
</div>
<div class="modal fade" id="selectContactModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content border-0 shadow-lg">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title fw-bold"><i class="bi bi-book me-2"></i> Kontakt wählen</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0">
<div class="list-group list-group-flush">
{% for c in contacts %}
{% if c.fax_number %}
<button type="button" class="list-group-item list-group-item-action py-3 border-bottom" onclick="selectContact('{{ c.fax_number }}')">
<div class="d-flex justify-content-between align-items-center">
<div class="fw-bold">{% if c.patient_id %}[{{c.patient_id}}] {% endif %}{{ c.first_name }} {{ c.last_name }}</div>
<span class="badge bg-light text-primary font-monospace fs-6"><i class="bi bi-printer me-1"></i>{{ c.fax_number }}</span>
</div>
{% if c.company %}<div class="small text-muted mt-1"><i class="bi bi-building me-1"></i>{{ c.company }}</div>{% endif %}
</button>
{% endif %}
{% else %}
<div class="p-5 text-center text-muted">
<i class="bi bi-journal-x fs-1 d-block mb-3"></i>
Keine Kontakte vorhanden.<br>Bitte lege zuerst im Adressbuch welche an.
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="standardExportModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form action="{{ url_for('fax_approve_export') }}" method="POST">
<div class="modal-header bg-success text-white">
<h5 class="modal-title fw-bold"><i class="bi bi-download me-2"></i> PDF Exportieren</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body py-4">
<input type="hidden" name="fax_id" id="standardExportFaxId">
<label class="form-label fw-bold">Dateiname (optional):</label>
<input type="text" name="filename" class="form-control form-control-lg" placeholder="z.B. Befund_Max_Mustermann">
<div class="form-text">Wird das Feld leergelassen, wird automatisch ein Name generiert. (.pdf wird automatisch ergänzt)</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-success fw-bold">Exportieren</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="gdtExportModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form action="{{ url_for('fax_export_gdt') }}" method="POST">
<div class="modal-header bg-info text-white">
<h5 class="modal-title fw-bold"><i class="bi bi-diagram-3-fill me-2"></i> PVS Export (GDT)</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body py-4">
<input type="hidden" name="fax_id" id="gdtExportFaxId">
<div class="mb-3">
<label class="form-label fw-bold">Dateiname (optional):</label>
<input type="text" name="custom_filename" class="form-control" placeholder="z.B. Befund_Dr_Müller">
<div class="form-text">Ohne Endung (.pdf wird automatisch ergänzt). Bleibt das Feld leer, wird automatisch ein Name generiert.</div>
</div>
<label class="form-label fw-bold">Patient auswählen:</label>
<input type="text" id="gdtSearchInput" class="form-control mb-3" placeholder="Suchen nach Name oder ID..." onkeyup="filterGdtPatients()">
<select name="patient_id" id="gdtPatientSelect" class="form-select form-select-lg" size="5" required>
<option value="" disabled selected hidden>Bitte wählen...</option>
{% for c in contacts %}
{% if c.patient_id %}
<option value="{{ c.patient_id }}">{{ c.patient_id }} - {{ c.last_name }}, {{ c.first_name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-info text-white fw-bold">Exportieren</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="loginModal" tabindex="-1" data-bs-backdrop="static">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<form action="{{ url_for('login') }}" method="POST">
<div class="modal-body p-4 text-center">
<div class="settings-icon mx-auto mb-3" style="background: var(--primary-subtle); color: var(--primary-color); width: 48px; height: 48px; font-size: 1.3rem;">
<i class="bi bi-shield-lock"></i>
</div>
<h6 class="fw-bold mb-3">COM|hub Login</h6>
<input type="hidden" name="username" value="admin">
<div class="mb-3">
<input type="password" name="password" class="form-control text-center" placeholder="Passwort eingeben" required autofocus>
</div>
<button type="submit" class="btn btn-primary w-100">Anmelden</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="deletePinModal" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<form action="{{ url_for('fax_delete') }}" method="POST">
<div class="modal-body p-4 text-center">
<div class="settings-icon mx-auto mb-3" style="background: rgba(220, 38, 38, 0.1); color: var(--danger-color); width: 48px; height: 48px; font-size: 1.3rem;">
<i class="bi bi-trash"></i>
</div>
<h6 class="fw-bold mb-1">Löschen bestätigen</h6>
<p class="text-muted small mb-3">Admin PIN eingeben</p>
<input type="hidden" name="fax_id" id="deleteFaxId">
<input type="hidden" name="out_id" id="deleteOutId">
<div class="mb-3">
<input type="password" name="pin" class="form-control text-center font-monospace" required maxlength="4" placeholder="----" style="letter-spacing: 6px;">
</div>
<button type="submit" class="btn btn-danger w-100">Löschen</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="deleteContactModal" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<form action="{{ url_for('contact_delete') }}" method="POST">
<div class="modal-body p-4 text-center">
<div class="settings-icon mx-auto mb-3" style="background: rgba(220, 38, 38, 0.1); color: var(--danger-color); width: 48px; height: 48px; font-size: 1.3rem;">
<i class="bi bi-person-x"></i>
</div>
<h6 class="fw-bold mb-1">Kontakt löschen</h6>
<p class="text-muted small mb-3">Admin PIN eingeben</p>
<input type="hidden" name="id" id="deleteContactId">
<div class="mb-3">
<input type="password" name="pin" class="form-control text-center font-monospace" required maxlength="4" placeholder="----" style="letter-spacing: 6px;">
</div>
<button type="submit" class="btn btn-danger w-100">Löschen</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="contactModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form action="{{ url_for('contact_add') }}" method="POST">
<div class="modal-header border-0 pb-0">
<h6 class="modal-title fw-bold"><i class="bi bi-person-plus me-2" style="color: var(--primary-color);"></i> Neuer Kontakt</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body pt-3">
<div class="row g-3">
<div class="col-12"><label class="form-label">Patienten-ID (optional)</label><input type="text" name="patient_id" class="form-control" placeholder="z.B. 12345"></div>
<div class="col-6"><label class="form-label">Vorname</label><input type="text" name="first_name" class="form-control"></div>
<div class="col-6"><label class="form-label">Nachname</label><input type="text" name="last_name" class="form-control" required></div>
<div class="col-12"><label class="form-label">Firma</label><input type="text" name="company" class="form-control"></div>
<div class="col-6"><label class="form-label">Telefon</label><input type="text" name="number" class="form-control font-monospace"></div>
<div class="col-6"><label class="form-label">Fax</label><input type="text" name="fax_number" class="form-control font-monospace"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-sm btn-primary"><i class="bi bi-check-lg me-1"></i> Anlegen</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="editContactModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<form action="{{ url_for('contact_edit') }}" method="POST">
<div class="modal-header border-0 pb-0">
<h6 class="modal-title fw-bold"><i class="bi bi-pencil me-2" style="color: var(--warning-color);"></i> Kontakt bearbeiten</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body pt-3">
<input type="hidden" name="id" id="editContactId">
<div class="row g-3">
<div class="col-12"><label class="form-label">Patienten-ID</label><input type="text" name="patient_id" id="editPatientId" class="form-control"></div>
<div class="col-6"><label class="form-label">Vorname</label><input type="text" name="first_name" id="editFirstName" class="form-control"></div>
<div class="col-6"><label class="form-label">Nachname</label><input type="text" name="last_name" id="editLastName" class="form-control" required></div>
<div class="col-12"><label class="form-label">Firma</label><input type="text" name="company" id="editCompany" class="form-control"></div>
<div class="col-6"><label class="form-label">Telefon</label><input type="text" name="number" id="editNumber" class="form-control font-monospace"></div>
<div class="col-6"><label class="form-label">Fax</label><input type="text" name="fax_number" id="editFaxNumber" class="form-control font-monospace"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-sm btn-primary"><i class="bi bi-check-lg me-1"></i> Speichern</button>
</div>
</form>
</div>
</div>
</div>
<div class="modal fade" id="settingsModal" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable" style="max-width: 900px;">
<div class="modal-content" style="min-height: 560px;">
<div class="modal-header border-0 pb-0 px-4 pt-3">
<div>
<h5 class="modal-title fw-bold mb-0"><i class="bi bi-sliders me-2" style="color: var(--primary-color);"></i> Einstellungen</h5>
<small class="text-muted">Angemeldet als <strong>Admin</strong></small>
</div>
<div class="d-flex align-items-center gap-2">
<a href="{{ url_for('logout') }}" class="btn btn-sm btn-outline-danger"><i class="bi bi-box-arrow-right me-1"></i>Logout</a>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
</div>
<div class="modal-body p-0">
<div class="d-flex" style="min-height: 480px;">
<!-- Settings Sidebar -->
<div class="settings-nav border-end" style="width: 200px; min-width: 200px; padding: 16px 10px; background: var(--hover-bg);">
<button class="settings-nav-item active" onclick="switchSettingsTab(this, 'module')" type="button">
<i class="bi bi-grid-3x3-gap"></i> Module
</button>
<button class="settings-nav-item" onclick="switchSettingsTab(this, 'general')" type="button">
<i class="bi bi-sliders2"></i> Allgemein
</button>
<button class="settings-nav-item" onclick="switchSettingsTab(this, 'paths')" type="button">
<i class="bi bi-folder2"></i> Pfade
</button>
<button class="settings-nav-item" onclick="switchSettingsTab(this, 'security')" type="button">
<i class="bi bi-shield-lock"></i> Sicherheit
</button>
<div style="border-top: 1px solid var(--border-color); margin: 12px 8px;"></div>
<button class="settings-nav-item settings-nav-danger" onclick="switchSettingsTab(this, 'system')" type="button">
<i class="bi bi-cpu"></i> System
</button>
</div>
<!-- Settings Content -->
<div class="flex-grow-1 p-4" style="overflow-y: auto;">
<form action="{{ url_for('settings') }}" method="POST" id="mainSettingsForm">
<!-- MODULE -->
<div class="settings-pane active" id="pane-module">
<h6 class="fw-bold mb-1" style="color: var(--primary-color);">Module</h6>
<p class="text-muted small mb-3">Aktiviere oder deaktiviere einzelne Funktionsbereiche.</p>
<div class="settings-card mb-2">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-3">
<div class="settings-icon" style="background: rgba(5, 150, 105, 0.1); color: var(--success-color);"><i class="bi bi-mic"></i></div>
<div>
<div class="fw-bold" style="font-size: 0.85rem;">Voicemail Empfang</div>
<div class="text-muted" style="font-size: 0.75rem;">Sprachnachrichten automatisch abrufen</div>
</div>
</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="moduleVoicemail" name="module_voicemail" {% if config.get('module_voicemail', 'true') == 'true' %}checked{% endif %} style="transform: scale(1.2);">
</div>
</div>
</div>
<div class="settings-card mb-2">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-3">
<div class="settings-icon" style="background: rgba(99, 102, 241, 0.1); color: #6366f1;"><i class="bi bi-chat-dots"></i></div>
<div>
<div class="fw-bold" style="font-size: 0.85rem;">Voicemail Transkription</div>
<div class="text-muted" style="font-size: 0.75rem;">Sprache-zu-Text via Whisper AI</div>
</div>
</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="moduleTranscription" name="module_transcription" {% if config.get('module_transcription', 'true') == 'true' %}checked{% endif %} style="transform: scale(1.2);">
</div>
</div>
</div>
<div class="settings-card mb-2">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-3">
<div class="settings-icon" style="background: rgba(0, 119, 182, 0.1); color: var(--primary-color);"><i class="bi bi-printer"></i></div>
<div>
<div class="fw-bold" style="font-size: 0.85rem;">Fax Eingang & Versand</div>
<div class="text-muted" style="font-size: 0.75rem;">Faxe empfangen und versenden via Placetel</div>
</div>
</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="moduleFax" name="module_fax" {% if config.get('module_fax', 'true') == 'true' %}checked{% endif %} style="transform: scale(1.2);">
</div>
</div>
</div>
<div class="settings-card mb-2">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-3">
<div class="settings-icon" style="background: rgba(217, 119, 6, 0.1); color: var(--warning-color);"><i class="bi bi-folder-check"></i></div>
<div>
<div class="fw-bold" style="font-size: 0.85rem;">Dokumentenverwaltung & Prüfliste</div>
<div class="text-muted" style="font-size: 0.75rem;">Dokumente importieren, prüfen und exportieren</div>
</div>
</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="moduleDocs" name="module_docs" {% if config.get('module_docs', 'true') == 'true' %}checked{% endif %} style="transform: scale(1.2);">
</div>
</div>
</div>
<div class="settings-card mb-2">
<div class="d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-3">
<div class="settings-icon" style="background: rgba(6, 182, 212, 0.1); color: #06b6d4;"><i class="bi bi-diagram-3"></i></div>
<div>
<div class="fw-bold" style="font-size: 0.85rem;">GDT Import & Export (PVS)</div>
<div class="text-muted" style="font-size: 0.75rem;">Datenaustausch mit dem Praxisverwaltungssystem</div>
</div>
</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="moduleGdt" name="module_gdt" {% if config.get('module_gdt', 'false') == 'true' %}checked{% endif %} style="transform: scale(1.2);">
</div>
</div>
</div>
</div>
<!-- GENERAL -->
<div class="settings-pane" id="pane-general">
<h6 class="fw-bold mb-1" style="color: var(--primary-color);">Allgemein</h6>
<p class="text-muted small mb-3">API-Verbindung und Synchronisierung konfigurieren.</p>
<div class="settings-card mb-3">
<div class="fw-bold small mb-2"><i class="bi bi-key me-1"></i> Placetel API</div>
<div class="mb-3">
<label class="form-label">API Token</label>
<input type="text" class="form-control font-monospace" name="api_token" value="{{ config.get('placetel_api_token', '') }}" style="font-size: 0.75rem;">
</div>
<div class="row g-3">
<div class="col-6">
<label class="form-label">Sync-Intervall (Minuten)</label>
<input type="number" class="form-control" name="interval" value="{{ config.get('check_interval_minutes', 10) }}" min="1">
</div>
<div class="col-6">
<label class="form-label">Auto-Cleanup (Tage)</label>
<input type="number" class="form-control" name="days" value="{{ config.get('delete_recordings_after_days', 7) }}" min="1" max="7">
<div class="form-text">Archiv & Faxausgang</div>
</div>
</div>
</div>
<div class="settings-card mb-3">
<div class="fw-bold small mb-2"><i class="bi bi-send me-1"></i> Fax-Versanddaten</div>
<div class="row g-3">
<div class="col-6">
<label class="form-label">Absender-Nummer</label>
<input type="text" class="form-control font-monospace" name="fax_from_number" value="{{ config.get('fax_from_number', '') }}" placeholder="z.B. 022199980633">
</div>
<div class="col-6">
<label class="form-label">Absender E-Mail</label>
<input type="email" class="form-control font-monospace" name="fax_email" value="{{ config.get('fax_email', '') }}">
</div>
</div>
</div>
<div class="settings-card">
<div class="fw-bold small mb-2"><i class="bi bi-mic me-1"></i> Transkription (Whisper)</div>
<div class="d-flex align-items-center justify-content-between">
<div>
<div style="font-size: 0.85rem;">Manuelle Neu-Transkription erlauben</div>
<div class="form-text mt-0">Button in den Voicemail-Details zum erneuten Transkribieren</div>
</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="allowRetranscription" name="allow_retranscription" {% if config.get('allow_retranscription', 'false') == 'true' %}checked{% endif %} style="transform: scale(1.2);">
</div>
</div>
</div>
</div>
<!-- PATHS -->
<div class="settings-pane" id="pane-paths">
<h6 class="fw-bold mb-1" style="color: var(--primary-color);">Pfade</h6>
<p class="text-muted small mb-3">Import- und Export-Verzeichnisse konfigurieren.</p>
<div class="settings-card mb-3">
<div class="fw-bold small mb-2"><i class="bi bi-box-arrow-in-down me-1 text-success"></i> Eingänge (Input)</div>
<div class="mb-3">
<label class="form-label">Fax-Drucker Ordner</label>
<input type="text" class="form-control font-monospace" name="fax_outbox_path" value="{{ config.get('fax_outbox_path', '/opt/comhub/share/FaxOut') }}">
<div class="form-text">PDFs landen automatisch als Entwurf im Faxausgang.</div>
</div>
<div>
<label class="form-label">Dokumenten-Import Ordner</label>
<input type="text" class="form-control font-monospace" name="monitored_folder_path" value="{{ config.get('monitored_folder_path', '/opt/comhub/share/Dokumente/Import') }}">
<div class="form-text">PDFs landen im Posteingang "Dokumente".</div>
</div>
</div>
<div class="settings-card mb-3">
<div class="fw-bold small mb-2"><i class="bi bi-box-arrow-up me-1" style="color: var(--primary-color);"></i> Ausgänge (Output)</div>
<div>
<label class="form-label">Standard Export-Ordner</label>
<input type="text" class="form-control font-monospace" name="standard_export_path" value="{{ config.get('standard_export_path', '/opt/comhub/share/Export') }}">
<div class="form-text">Zielordner für manuellen PDF-Export (ohne GDT).</div>
</div>
</div>
<div class="settings-card">
<div class="fw-bold small mb-2"><i class="bi bi-diagram-3 me-1 text-info"></i> GDT-Verzeichnisse</div>
<div class="row g-3 mb-3">
<div class="col-6">
<label class="form-label">GDT-Import (vom PVS)</label>
<input type="text" class="form-control font-monospace" name="gdt_import_path" value="{{ config.get('gdt_import_path', '/opt/comhub/share/GDT/Import') }}">
</div>
<div class="col-6">
<label class="form-label">GDT-Export (zum PVS)</label>
<input type="text" class="form-control font-monospace" name="gdt_export_path" value="{{ config.get('gdt_export_path', '/opt/comhub/share/GDT/Export') }}">
</div>
</div>
<div class="row g-3">
<div class="col-6">
<label class="form-label">Sender ID (Feld 8316)</label>
<input type="text" class="form-control font-monospace" name="gdt_sender_id" value="{{ config.get('gdt_sender_id', 'COMHUB') }}">
</div>
<div class="col-6">
<label class="form-label">Empfänger ID (Feld 8315)</label>
<input type="text" class="form-control font-monospace" name="gdt_receiver_id" value="{{ config.get('gdt_receiver_id', 'PVS') }}">
</div>
</div>
</div>
</div>
<!-- SECURITY -->
<div class="settings-pane" id="pane-security">
<h6 class="fw-bold mb-1" style="color: var(--primary-color);">Sicherheit</h6>
<p class="text-muted small mb-3">PINs und Passwörter verwalten.</p>
<div class="settings-card mb-3">
<div class="fw-bold small mb-2"><i class="bi bi-eye-slash me-1" style="color: var(--warning-color);"></i> Prüflisten-Schutz</div>
<div class="d-flex align-items-center justify-content-between mb-3">
<div style="font-size: 0.85rem;">PIN-Schutz für Prüfliste aktivieren</div>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="reviewPinEnabled" name="review_pin_enabled" {% if config.get('review_pin_enabled', 'false') == 'true' %}checked{% endif %} style="transform: scale(1.2);">
</div>
</div>
<div>
<label class="form-label">Prüflisten-PIN</label>
<input type="password" class="form-control font-monospace text-center" name="review_pin" value="{{ config.get('review_pin', '0000') }}" maxlength="4" style="max-width: 150px; letter-spacing: 4px;">
</div>
</div>
<div class="settings-card" style="border-color: rgba(220, 38, 38, 0.15);">
<div class="fw-bold small mb-2"><i class="bi bi-shield-exclamation me-1" style="color: var(--danger-color);"></i> Administrator</div>
<div class="row g-3">
<div class="col-6">
<label class="form-label">Lösch-PIN</label>
<input type="password" class="form-control font-monospace text-center" name="admin_pin" value="{{ config.get('admin_pin', '1234') }}" style="letter-spacing: 4px;">
</div>
{% if session.user == 'admin' %}
<div class="col-6">
<label class="form-label">Neues Passwort</label>
<input type="password" class="form-control" name="admin_password" placeholder="Leer = beibehalten">
</div>
{% endif %}
</div>
</div>
</div>
</form>
<!-- SYSTEM -->
<div class="settings-pane" id="pane-system">
<h6 class="fw-bold mb-1" style="color: var(--danger-color);">System-Update</h6>
<p class="text-muted small mb-3">Backend oder Frontend manuell aktualisieren.</p>
<div class="settings-card mb-3" style="border-color: rgba(220, 38, 38, 0.2); background: rgba(220, 38, 38, 0.03);">
<div class="d-flex align-items-start gap-2 mb-3">
<i class="bi bi-exclamation-triangle-fill" style="color: var(--warning-color); font-size: 1.1rem; margin-top: 2px;"></i>
<div class="small" style="color: var(--text-secondary);">
<strong>Achtung:</strong> Fehlerhafte Dateien führen zu einem Systemausfall. Ein Syntax-Check wird automatisch durchgeführt.
</div>
</div>
<form action="{{ url_for('system_update') }}" method="POST" enctype="multipart/form-data" id="systemUpdateForm">
<div class="mb-3">
<label class="form-label"><i class="bi bi-filetype-py me-1"></i> Backend (app.py)</label>
<input type="file" class="form-control form-control-sm" name="app_file" accept=".py">
</div>
<div class="mb-3">
<label class="form-label"><i class="bi bi-filetype-html me-1"></i> Frontend (index.html)</label>
<input type="file" class="form-control form-control-sm" name="index_file" accept=".html">
</div>
<button type="submit" class="btn btn-danger btn-sm w-100"><i class="bi bi-cloud-upload me-1"></i> Importieren & Neustarten</button>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer" style="background: var(--hover-bg);">
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" form="mainSettingsForm" class="btn btn-sm btn-primary px-4"><i class="bi bi-check-lg me-1"></i> Speichern</button>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='bootstrap.bundle.min.js') }}"></script>
<script>
const isAuthenticated = {{ 'true' if is_authenticated else 'false' }};
const shouldOpenSettings = {{ 'true' if open_settings else 'false' }};
// Sidebar Toggle (Mobile)
function toggleSidebar() {
document.getElementById('sidebarMenu').classList.toggle('show');
document.getElementById('sidebarOverlay').classList.toggle('show');
}
// Settings Tab Switch
function switchSettingsTab(btn, tabId) {
document.querySelectorAll('.settings-nav-item').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.settings-pane').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById('pane-' + tabId).classList.add('active');
}
// GDT Patient Search Filter
function filterGdtPatients() {
const input = document.getElementById('gdtSearchInput').value.toLowerCase();
const select = document.getElementById('gdtPatientSelect');
const options = select.getElementsByTagName('option');
for (let i = 0; i < options.length; i++) {
if (options[i].value === "") continue; // Skip default option
const text = options[i].text.toLowerCase();
if (text.includes(input)) {
options[i].hidden = false;
} else {
options[i].hidden = true;
}
}
}
// Background-Save Funktion für Notizen
function saveFaxNote(outId, el) {
const formData = new FormData();
formData.append('out_id', outId);
formData.append('note', el.value);
fetch('/fax/save_note', {
method: 'POST',
body: formData
}).then(res => {
if(res.ok) {
el.classList.add('save-pulse');
setTimeout(() => el.classList.remove('save-pulse'), 1000);
}
}).catch(err => console.error('Notiz konnte nicht gespeichert werden:', err));
}
// Adressbuch Integration Funktion
let currentTargetInput = null;
function setTargetInput(id) {
currentTargetInput = id;
}
function selectContact(number) {
if (currentTargetInput) {
const el = document.getElementById(currentTargetInput);
if(el) el.value = number;
}
const modalEl = document.getElementById('selectContactModal');
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
}
function toggleTheme() {
const body = document.body;
const icon = document.getElementById('themeIcon');
const currentTheme = body.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
body.setAttribute('data-theme', newTheme);
icon.className = newTheme === 'light' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
localStorage.setItem('theme', newTheme);
}
const savedTheme = localStorage.getItem('theme') || 'light';
document.body.setAttribute('data-theme', savedTheme);
document.getElementById('themeIcon').className = savedTheme === 'light' ? 'bi bi-moon-stars-fill' : 'bi bi-sun-fill';
function handleSettingsClick() {
if (isAuthenticated) {
bootstrap.Modal.getOrCreateInstance(document.getElementById('settingsModal')).show();
} else {
bootstrap.Modal.getOrCreateInstance(document.getElementById('loginModal')).show();
}
}
function confirmDelete(faxId, outId) {
document.getElementById('deleteFaxId').value = faxId || '';
document.getElementById('deleteOutId').value = outId || '';
bootstrap.Modal.getOrCreateInstance(document.getElementById('deletePinModal')).show();
}
function confirmDeleteContact(cId) {
document.getElementById('deleteContactId').value = cId;
bootstrap.Modal.getOrCreateInstance(document.getElementById('deleteContactModal')).show();
}
function openStandardExportModal(faxId) {
document.getElementById('standardExportFaxId').value = faxId;
document.querySelector('#standardExportModal input[name="filename"]').value = '';
bootstrap.Modal.getOrCreateInstance(document.getElementById('standardExportModal')).show();
}
function openGdtExportModal(faxId) {
document.getElementById('gdtExportFaxId').value = faxId;
document.getElementById('gdtSearchInput').value = '';
document.querySelector('#gdtExportModal input[name="custom_filename"]').value = '';
filterGdtPatients();
bootstrap.Modal.getOrCreateInstance(document.getElementById('gdtExportModal')).show();
}
function openEditContact(id, first, last, comp, num, fax_num, pat_id) {
document.getElementById('editContactId').value = id;
document.getElementById('editFirstName').value = first || '';
document.getElementById('editLastName').value = last || '';
document.getElementById('editCompany').value = comp || '';
document.getElementById('editNumber').value = num || '';
document.getElementById('editFaxNumber').value = fax_num || '';
document.getElementById('editPatientId').value = pat_id || '';
bootstrap.Modal.getOrCreateInstance(document.getElementById('editContactModal')).show();
}
function fetchLiveQueue() {
fetch('/api/live_queue')
.then(res => res.json())
.then(data => {
const container = document.getElementById('liveQueueContainer');
const cards = document.getElementById('liveQueueCards');
if (!data.calls || data.calls.length === 0) {
if(container) container.classList.add('d-none');
return;
}
if(container) container.classList.remove('d-none');
if(cards) cards.innerHTML = '';
data.calls.forEach(call => {
const min = Math.floor(call.wait_time / 60); const sec = call.wait_time % 60;
const timeStr = `${min}:${sec < 10 ? '0' : ''}${sec}`;
const card = `
<div class="col-md-4 col-sm-6">
<div class="card queue-card h-100">
<div class="card-body p-3">
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="fw-bold fs-6 text-truncate pe-2" title="${call.name}">${call.name}</div>
<span class="badge bg-danger rounded-pill">${timeStr}</span>
</div>
<div class="text-muted small font-monospace"><i class="bi bi-telephone me-1"></i> ${call.caller}</div>
</div>
</div>
</div>`;
if(cards) cards.insertAdjacentHTML('beforeend', card);
});
}).catch(err => console.error(err));
}
function fetchCounts() {
fetch('/api/counts')
.then(res => res.json())
.then(data => {
const updateBadge = (id, count) => {
const el = document.getElementById(id);
if (el) {
el.textContent = count;
if (count > 0) el.classList.remove('d-none');
else el.classList.add('d-none');
}
};
updateBadge('badge-vm', data.cnt_vm);
updateBadge('badge-fax', data.cnt_fax);
updateBadge('badge-doc', data.cnt_doc);
updateBadge('badge-out', data.cnt_out);
updateBadge('badge-rev', data.cnt_rev);
})
.catch(err => console.error("Fehler beim Abrufen der Zähler:", err));
}
document.addEventListener('DOMContentLoaded', function() {
// Automatisch Settings öffnen, falls gewünscht
if (shouldOpenSettings && isAuthenticated) {
bootstrap.Modal.getOrCreateInstance(document.getElementById('settingsModal')).show();
}
// Polling für Queue und Badges, unabhängig vom Login-Status
fetchLiveQueue();
setInterval(fetchLiveQueue, 3000);
setInterval(fetchCounts, 5000);
// Automatischer Logout, wenn das Settings-Fenster geschlossen wird
const settingsModalEl = document.getElementById('settingsModal');
if (settingsModalEl) {
settingsModalEl.addEventListener('hidden.bs.modal', function () {
if (isAuthenticated) {
window.location.href = "{{ url_for('logout') }}";
}
});
}
});
</script>
</body>
</html>