A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
11704 lines
450 KiB
Plaintext
11704 lines
450 KiB
Plaintext
from fastapi import APIRouter
|
||
from fastapi.responses import HTMLResponse
|
||
|
||
router = APIRouter()
|
||
|
||
|
||
@router.get("/app", response_class=HTMLResponse)
|
||
def app_ui():
|
||
return """
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>BreakPilot – Arbeitsblatt Studio</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<style>
|
||
/* ==========================================
|
||
DARK MODE (Default) - Original Design
|
||
========================================== */
|
||
:root {
|
||
--bp-primary: #0f766e;
|
||
--bp-primary-soft: #ccfbf1;
|
||
--bp-bg: #020617;
|
||
--bp-surface: #020617;
|
||
--bp-surface-elevated: rgba(15,23,42,0.9);
|
||
--bp-border: #1f2937;
|
||
--bp-border-subtle: rgba(148,163,184,0.25);
|
||
--bp-accent: #22c55e;
|
||
--bp-accent-soft: rgba(34,197,94,0.2);
|
||
--bp-text: #e5e7eb;
|
||
--bp-text-muted: #9ca3af;
|
||
--bp-danger: #ef4444;
|
||
--bp-gradient-bg: radial-gradient(circle at top left, #1f2937 0, #020617 50%, #000 100%);
|
||
--bp-gradient-surface: radial-gradient(circle at top left, rgba(15,23,42,0.9) 0, #020617 50%, #000 100%);
|
||
--bp-gradient-sidebar: radial-gradient(circle at top, #020617 0, #020617 40%, #000 100%);
|
||
--bp-gradient-topbar: linear-gradient(to right, rgba(15,23,42,0.9), rgba(15,23,42,0.6));
|
||
--bp-btn-primary-bg: linear-gradient(to right, var(--bp-primary), #15803d);
|
||
--bp-btn-primary-hover: linear-gradient(to right, #0f766e, #166534);
|
||
--bp-card-bg: var(--bp-gradient-surface);
|
||
--bp-input-bg: rgba(255,255,255,0.05);
|
||
--bp-scrollbar-track: rgba(15,23,42,0.5);
|
||
--bp-scrollbar-thumb: rgba(148,163,184,0.5);
|
||
}
|
||
|
||
/* ==========================================
|
||
LIGHT MODE - Brandbook Design
|
||
Farben aus BPAI Brandbook:
|
||
- Graphitgrau #4A4A4A (Klarheit, Struktur)
|
||
- Bordeauxrot #6C1B1B (Wärme, Bildung)
|
||
- Goldgelb #F1C40F (Zukunft, Energie)
|
||
- Grün #5ABF60 (Natur, Nachhaltigkeit)
|
||
- Weißgrau #F8F8F8 (Ruhe, Raum)
|
||
========================================== */
|
||
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap');
|
||
|
||
[data-theme="light"] {
|
||
--bp-primary: #6C1B1B;
|
||
--bp-primary-soft: rgba(108, 27, 27, 0.1);
|
||
--bp-bg: #F8F8F8;
|
||
--bp-surface: #FFFFFF;
|
||
--bp-surface-elevated: #FFFFFF;
|
||
--bp-border: #E0E0E0;
|
||
--bp-border-subtle: rgba(108, 27, 27, 0.15);
|
||
--bp-accent: #5ABF60;
|
||
--bp-accent-soft: rgba(90, 191, 96, 0.15);
|
||
--bp-text: #4A4A4A;
|
||
--bp-text-muted: #6B6B6B;
|
||
--bp-danger: #6C1B1B;
|
||
--bp-gradient-bg: linear-gradient(145deg, #F8F8F8 0%, #EFEFEF 100%);
|
||
--bp-gradient-surface: linear-gradient(145deg, #FFFFFF 0%, #F8F8F8 100%);
|
||
--bp-gradient-sidebar: linear-gradient(180deg, #FFFFFF 0%, #F5F5F5 100%);
|
||
--bp-gradient-topbar: linear-gradient(to right, #FFFFFF, #FAFAFA);
|
||
--bp-btn-primary-bg: linear-gradient(135deg, #6C1B1B 0%, #8B2323 100%);
|
||
--bp-btn-primary-hover: linear-gradient(135deg, #8B2323 0%, #A52A2A 100%);
|
||
--bp-card-bg: linear-gradient(145deg, #FFFFFF 0%, #F8F8F8 100%);
|
||
--bp-input-bg: #FFFFFF;
|
||
--bp-scrollbar-track: rgba(0,0,0,0.05);
|
||
--bp-scrollbar-thumb: rgba(108, 27, 27, 0.3);
|
||
--bp-gold: #F1C40F;
|
||
}
|
||
|
||
[data-theme="light"] body {
|
||
font-family: 'Manrope', system-ui, -apple-system, sans-serif;
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
|
||
/* Custom Scrollbar */
|
||
::-webkit-scrollbar {
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: var(--bp-scrollbar-track);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: var(--bp-scrollbar-thumb);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb:hover {
|
||
background: var(--bp-scrollbar-thumb);
|
||
opacity: 0.8;
|
||
}
|
||
|
||
html, body {
|
||
height: 100%;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
|
||
background: var(--bp-gradient-bg);
|
||
color: var(--bp-text);
|
||
min-height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
transition: background 0.3s ease, color 0.3s ease;
|
||
}
|
||
|
||
.app-root {
|
||
display: grid;
|
||
grid-template-rows: 56px 1fr 32px;
|
||
flex: 1;
|
||
min-height: 0;
|
||
}
|
||
|
||
.topbar {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 20px;
|
||
border-bottom: 1px solid var(--bp-border-subtle);
|
||
backdrop-filter: blur(18px);
|
||
background: var(--bp-gradient-topbar);
|
||
transition: background 0.3s ease, border-color 0.3s ease;
|
||
}
|
||
|
||
.brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.brand-logo {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 999px;
|
||
border: 2px solid var(--bp-accent);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-weight: 700;
|
||
font-size: 14px;
|
||
color: var(--bp-accent);
|
||
}
|
||
|
||
.brand-text-main {
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.brand-text-sub {
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.top-nav {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 18px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.top-nav-item {
|
||
cursor: pointer;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
color: var(--bp-text-muted);
|
||
border: 1px solid transparent;
|
||
}
|
||
|
||
.top-nav-item.active {
|
||
color: var(--bp-primary);
|
||
border-color: var(--bp-accent-soft);
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
[data-theme="light"] .top-nav-item.active {
|
||
color: var(--bp-primary);
|
||
background: var(--bp-primary-soft);
|
||
border-color: var(--bp-primary);
|
||
}
|
||
|
||
.top-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.pill {
|
||
font-size: 11px;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
/* Theme Toggle Button */
|
||
.theme-toggle {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-text-muted);
|
||
cursor: pointer;
|
||
font-size: 11px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.theme-toggle:hover {
|
||
border-color: var(--bp-primary);
|
||
color: var(--bp-primary);
|
||
}
|
||
|
||
.theme-toggle-icon {
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* ==========================================
|
||
LEGAL MODAL STYLES
|
||
========================================== */
|
||
.legal-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
z-index: 10000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.legal-modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.legal-modal-content {
|
||
background: var(--bp-surface);
|
||
border-radius: 16px;
|
||
width: 90%;
|
||
max-width: 700px;
|
||
max-height: 80vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.legal-modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.legal-modal-header h2 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.legal-modal-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 28px;
|
||
cursor: pointer;
|
||
color: var(--bp-text-muted);
|
||
padding: 0;
|
||
line-height: 1;
|
||
}
|
||
|
||
.legal-modal-close:hover {
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.legal-tabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding: 12px 24px;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
.legal-tab {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--bp-text-muted);
|
||
cursor: pointer;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.legal-tab:hover {
|
||
background: var(--bp-border-subtle);
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.legal-tab.active {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
.legal-body {
|
||
padding: 24px;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
|
||
.legal-content {
|
||
display: none;
|
||
}
|
||
|
||
.legal-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.legal-content h3 {
|
||
margin-top: 0;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.legal-content p {
|
||
line-height: 1.6;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.legal-content ul {
|
||
color: var(--bp-text-muted);
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.cookie-categories {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.cookie-category {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 12px;
|
||
padding: 12px;
|
||
background: var(--bp-surface-elevated);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--bp-border);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.cookie-category input {
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.cookie-category span {
|
||
flex: 1;
|
||
font-size: 13px;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.gdpr-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.gdpr-action {
|
||
padding: 16px;
|
||
background: var(--bp-surface-elevated);
|
||
border-radius: 12px;
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.gdpr-action h4 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 14px;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.gdpr-action p {
|
||
margin: 0 0 12px 0;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: var(--bp-danger) !important;
|
||
border-color: var(--bp-danger) !important;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
filter: brightness(1.1);
|
||
}
|
||
|
||
[data-theme="light"] .legal-modal-content {
|
||
background: #FFFFFF;
|
||
border-color: #E0E0E0;
|
||
}
|
||
|
||
[data-theme="light"] .legal-tabs {
|
||
background: #F5F5F5;
|
||
}
|
||
|
||
[data-theme="light"] .legal-tab.active {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
[data-theme="light"] .cookie-category,
|
||
[data-theme="light"] .gdpr-action {
|
||
background: #F8F8F8;
|
||
border-color: #E0E0E0;
|
||
}
|
||
|
||
/* ==========================================
|
||
AUTH MODAL STYLES
|
||
========================================== */
|
||
.auth-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
z-index: 10000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.auth-modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.auth-modal-content {
|
||
background: var(--bp-surface);
|
||
border-radius: 16px;
|
||
width: 90%;
|
||
max-width: 420px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.auth-modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.auth-modal-header h2 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.auth-tabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding: 12px 24px;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
.auth-tab {
|
||
flex: 1;
|
||
padding: 10px 16px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--bp-text-muted);
|
||
cursor: pointer;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.auth-tab:hover {
|
||
background: var(--bp-border-subtle);
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.auth-tab.active {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
.auth-body {
|
||
padding: 24px;
|
||
}
|
||
|
||
.auth-content {
|
||
display: none;
|
||
}
|
||
|
||
.auth-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.auth-form-group {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.auth-form-label {
|
||
display: block;
|
||
margin-bottom: 6px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.auth-form-input {
|
||
width: 100%;
|
||
padding: 10px 14px;
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-text);
|
||
font-size: 14px;
|
||
transition: border-color 0.2s ease;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.auth-form-input:focus {
|
||
outline: none;
|
||
border-color: var(--bp-primary);
|
||
}
|
||
|
||
.auth-form-input::placeholder {
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.auth-error {
|
||
display: none;
|
||
padding: 10px 14px;
|
||
background: rgba(220, 53, 69, 0.15);
|
||
border: 1px solid var(--bp-danger);
|
||
border-radius: 8px;
|
||
color: var(--bp-danger);
|
||
font-size: 13px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.auth-error.active {
|
||
display: block;
|
||
}
|
||
|
||
.auth-success {
|
||
display: none;
|
||
padding: 10px 14px;
|
||
background: rgba(40, 167, 69, 0.15);
|
||
border: 1px solid var(--bp-success);
|
||
border-radius: 8px;
|
||
color: var(--bp-success);
|
||
font-size: 13px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.auth-success.active {
|
||
display: block;
|
||
}
|
||
|
||
.auth-btn {
|
||
width: 100%;
|
||
padding: 12px 20px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.auth-btn-primary {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
.auth-btn-primary:hover {
|
||
filter: brightness(1.1);
|
||
}
|
||
|
||
.auth-btn-primary:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.auth-link {
|
||
text-align: center;
|
||
margin-top: 16px;
|
||
font-size: 13px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.auth-link a {
|
||
color: var(--bp-primary);
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.auth-link a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.auth-divider {
|
||
display: flex;
|
||
align-items: center;
|
||
text-align: center;
|
||
margin: 20px 0;
|
||
color: var(--bp-text-muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.auth-divider::before,
|
||
.auth-divider::after {
|
||
content: '';
|
||
flex: 1;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.auth-divider::before {
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.auth-divider::after {
|
||
margin-left: 12px;
|
||
}
|
||
|
||
.auth-user-dropdown {
|
||
position: relative;
|
||
display: none;
|
||
}
|
||
|
||
.auth-user-dropdown.active {
|
||
display: flex;
|
||
}
|
||
|
||
.auth-user-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 12px;
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
color: var(--bp-text);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.auth-user-btn:hover {
|
||
border-color: var(--bp-primary);
|
||
}
|
||
|
||
.auth-user-avatar {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.auth-user-menu {
|
||
display: none;
|
||
position: absolute;
|
||
top: 100%;
|
||
right: 0;
|
||
margin-top: 8px;
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||
min-width: 200px;
|
||
z-index: 1000;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.auth-user-menu.active {
|
||
display: block;
|
||
}
|
||
|
||
.auth-user-menu-header {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.auth-user-menu-name {
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.auth-user-menu-email {
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.auth-user-menu-item {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 10px 16px;
|
||
border: none;
|
||
background: none;
|
||
text-align: left;
|
||
color: var(--bp-text);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
transition: background 0.2s ease;
|
||
}
|
||
|
||
.auth-user-menu-item:hover {
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
.auth-user-menu-item.danger {
|
||
color: var(--bp-danger);
|
||
}
|
||
|
||
[data-theme="light"] .auth-modal-content {
|
||
background: #FFFFFF;
|
||
border-color: #E0E0E0;
|
||
}
|
||
|
||
[data-theme="light"] .auth-tabs {
|
||
background: #F5F5F5;
|
||
}
|
||
|
||
[data-theme="light"] .auth-form-input {
|
||
background: #FFFFFF;
|
||
border-color: #E0E0E0;
|
||
}
|
||
|
||
[data-theme="light"] .auth-user-menu {
|
||
background: #FFFFFF;
|
||
border-color: #E0E0E0;
|
||
}
|
||
|
||
/* ==========================================
|
||
NOTIFICATION STYLES
|
||
========================================== */
|
||
.notification-bell {
|
||
position: relative;
|
||
display: none;
|
||
}
|
||
|
||
.notification-bell.active {
|
||
display: flex;
|
||
}
|
||
|
||
.notification-bell-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
color: var(--bp-text-muted);
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.notification-bell-btn:hover {
|
||
border-color: var(--bp-primary);
|
||
color: var(--bp-primary);
|
||
}
|
||
|
||
.notification-badge {
|
||
position: absolute;
|
||
top: 2px;
|
||
right: 2px;
|
||
min-width: 18px;
|
||
height: 18px;
|
||
background: var(--bp-danger);
|
||
color: white;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 5px;
|
||
}
|
||
|
||
.notification-badge.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.notification-panel {
|
||
display: none;
|
||
position: absolute;
|
||
top: 100%;
|
||
right: 0;
|
||
margin-top: 8px;
|
||
width: 360px;
|
||
max-height: 480px;
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 12px;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||
z-index: 1000;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.notification-panel.active {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.notification-panel-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.notification-panel-title {
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.notification-panel-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.notification-panel-action {
|
||
padding: 4px 8px;
|
||
background: none;
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 6px;
|
||
color: var(--bp-text-muted);
|
||
cursor: pointer;
|
||
font-size: 11px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.notification-panel-action:hover {
|
||
border-color: var(--bp-primary);
|
||
color: var(--bp-primary);
|
||
}
|
||
|
||
.notification-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
max-height: 380px;
|
||
}
|
||
|
||
.notification-item {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
cursor: pointer;
|
||
transition: background 0.2s ease;
|
||
}
|
||
|
||
.notification-item:hover {
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
.notification-item.unread {
|
||
background: rgba(15, 118, 110, 0.08);
|
||
}
|
||
|
||
.notification-item.unread::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 3px;
|
||
background: var(--bp-primary);
|
||
}
|
||
|
||
.notification-item {
|
||
position: relative;
|
||
}
|
||
|
||
.notification-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
background: var(--bp-primary-soft);
|
||
color: var(--bp-primary);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.notification-content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.notification-title {
|
||
font-weight: 500;
|
||
font-size: 13px;
|
||
color: var(--bp-text);
|
||
margin-bottom: 4px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.notification-body {
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
line-height: 1.4;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: -webkit-box;
|
||
-webkit-line-clamp: 2;
|
||
-webkit-box-orient: vertical;
|
||
}
|
||
|
||
.notification-time {
|
||
font-size: 11px;
|
||
color: var(--bp-text-muted);
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.notification-empty {
|
||
padding: 40px 20px;
|
||
text-align: center;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.notification-empty-icon {
|
||
font-size: 36px;
|
||
margin-bottom: 12px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.notification-footer {
|
||
padding: 12px 16px;
|
||
border-top: 1px solid var(--bp-border);
|
||
text-align: center;
|
||
}
|
||
|
||
.notification-footer-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--bp-primary);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.notification-footer-btn:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
/* Notification Preferences Modal */
|
||
.notification-prefs-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
z-index: 10001;
|
||
justify-content: center;
|
||
align-items: center;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.notification-prefs-modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.notification-prefs-content {
|
||
background: var(--bp-surface);
|
||
border-radius: 16px;
|
||
width: 90%;
|
||
max-width: 420px;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.notification-pref-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 14px 0;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.notification-pref-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.notification-pref-label {
|
||
font-size: 14px;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.notification-pref-desc {
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.toggle-switch {
|
||
position: relative;
|
||
width: 48px;
|
||
height: 26px;
|
||
background: var(--bp-border);
|
||
border-radius: 999px;
|
||
cursor: pointer;
|
||
transition: background 0.3s ease;
|
||
}
|
||
|
||
.toggle-switch.active {
|
||
background: var(--bp-primary);
|
||
}
|
||
|
||
.toggle-switch-handle {
|
||
position: absolute;
|
||
top: 3px;
|
||
left: 3px;
|
||
width: 20px;
|
||
height: 20px;
|
||
background: white;
|
||
border-radius: 50%;
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.toggle-switch.active .toggle-switch-handle {
|
||
transform: translateX(22px);
|
||
}
|
||
|
||
[data-theme="light"] .notification-panel {
|
||
background: #FFFFFF;
|
||
border-color: #E0E0E0;
|
||
}
|
||
|
||
[data-theme="light"] .notification-item.unread {
|
||
background: rgba(108, 27, 27, 0.05);
|
||
}
|
||
|
||
[data-theme="light"] .notification-item.unread::before {
|
||
background: var(--bp-primary);
|
||
}
|
||
|
||
/* ==========================================
|
||
SUSPENSION OVERLAY STYLES
|
||
========================================== */
|
||
.suspension-overlay {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.9);
|
||
z-index: 20000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
backdrop-filter: blur(8px);
|
||
}
|
||
|
||
.suspension-overlay.active {
|
||
display: flex;
|
||
}
|
||
|
||
.suspension-content {
|
||
background: var(--bp-surface);
|
||
border-radius: 20px;
|
||
width: 90%;
|
||
max-width: 500px;
|
||
padding: 40px;
|
||
text-align: center;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||
border: 1px solid var(--bp-danger);
|
||
}
|
||
|
||
.suspension-icon {
|
||
font-size: 64px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.suspension-title {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
color: var(--bp-danger);
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.suspension-message {
|
||
font-size: 15px;
|
||
color: var(--bp-text-muted);
|
||
margin-bottom: 24px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.suspension-docs {
|
||
background: var(--bp-surface-elevated);
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
margin-bottom: 24px;
|
||
text-align: left;
|
||
}
|
||
|
||
.suspension-docs-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--bp-text);
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.suspension-doc-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
background: var(--bp-bg);
|
||
border-radius: 8px;
|
||
margin-bottom: 8px;
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.suspension-doc-item:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.suspension-doc-name {
|
||
font-size: 14px;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.suspension-doc-deadline {
|
||
font-size: 12px;
|
||
color: var(--bp-danger);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.suspension-btn {
|
||
padding: 14px 28px;
|
||
background: var(--bp-btn-primary-bg);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 10px;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
width: 100%;
|
||
}
|
||
|
||
.suspension-btn:hover {
|
||
background: var(--bp-btn-primary-hover);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.suspension-countdown {
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
margin-top: 16px;
|
||
}
|
||
|
||
/* ==========================================
|
||
ADMIN PANEL STYLES
|
||
========================================== */
|
||
.admin-modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
z-index: 10000;
|
||
justify-content: center;
|
||
align-items: center;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.admin-modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.admin-modal-content {
|
||
background: var(--bp-surface);
|
||
border-radius: 16px;
|
||
width: 95%;
|
||
max-width: 1000px;
|
||
max-height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
box-shadow: 0 20px 60px rgba(0,0,0,0.4);
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.admin-modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.admin-modal-header h2 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.admin-tabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding: 12px 24px;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
.admin-tab {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--bp-text-muted);
|
||
cursor: pointer;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.admin-tab:hover {
|
||
background: var(--bp-border-subtle);
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.admin-tab.active {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
.admin-body {
|
||
padding: 24px;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
|
||
.admin-content {
|
||
display: none;
|
||
}
|
||
|
||
.admin-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.admin-toolbar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
gap: 12px;
|
||
}
|
||
|
||
.admin-toolbar-left {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
}
|
||
|
||
.admin-search {
|
||
padding: 8px 12px;
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-text);
|
||
width: 250px;
|
||
}
|
||
|
||
.admin-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.admin-table th,
|
||
.admin-table td {
|
||
padding: 12px;
|
||
text-align: left;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.admin-table th {
|
||
background: var(--bp-surface-elevated);
|
||
font-weight: 600;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.admin-table tr:hover {
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
.admin-table td {
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.admin-badge {
|
||
display: inline-block;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.admin-badge-published {
|
||
background: rgba(74, 222, 128, 0.2);
|
||
color: #4ADE80;
|
||
}
|
||
|
||
.admin-badge-draft {
|
||
background: rgba(251, 191, 36, 0.2);
|
||
color: #FBBF24;
|
||
}
|
||
|
||
.admin-badge-archived {
|
||
background: rgba(156, 163, 175, 0.2);
|
||
color: #9CA3AF;
|
||
}
|
||
|
||
.admin-badge-rejected {
|
||
background: rgba(239, 68, 68, 0.2);
|
||
color: #EF4444;
|
||
}
|
||
|
||
.admin-badge-review {
|
||
background: rgba(147, 51, 234, 0.2);
|
||
color: #A855F7;
|
||
}
|
||
|
||
.admin-badge-approved {
|
||
background: rgba(34, 197, 94, 0.2);
|
||
color: #22C55E;
|
||
}
|
||
|
||
.admin-badge-submitted {
|
||
background: rgba(59, 130, 246, 0.2);
|
||
color: #3B82F6;
|
||
}
|
||
|
||
.admin-badge-rejected {
|
||
background: rgba(239, 68, 68, 0.2);
|
||
color: #EF4444;
|
||
}
|
||
|
||
.admin-badge-mandatory {
|
||
background: rgba(239, 68, 68, 0.2);
|
||
color: #EF4444;
|
||
}
|
||
|
||
.admin-badge-optional {
|
||
background: rgba(96, 165, 250, 0.2);
|
||
color: #60A5FA;
|
||
}
|
||
|
||
.admin-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.admin-btn {
|
||
padding: 6px 10px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.admin-btn-edit {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
.admin-btn-edit:hover {
|
||
filter: brightness(1.1);
|
||
}
|
||
|
||
.admin-btn-delete {
|
||
background: rgba(239, 68, 68, 0.2);
|
||
color: #EF4444;
|
||
}
|
||
|
||
.admin-btn-delete:hover {
|
||
background: rgba(239, 68, 68, 0.3);
|
||
}
|
||
|
||
.admin-btn-publish {
|
||
background: rgba(74, 222, 128, 0.2);
|
||
color: #4ADE80;
|
||
}
|
||
|
||
.admin-btn-publish:hover {
|
||
background: rgba(74, 222, 128, 0.3);
|
||
}
|
||
|
||
.admin-form {
|
||
display: none;
|
||
background: var(--bp-surface-elevated);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
margin-bottom: 20px;
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.admin-form.active {
|
||
display: block;
|
||
}
|
||
|
||
/* Info Text in Toolbar */
|
||
.admin-info-text {
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
font-style: italic;
|
||
}
|
||
|
||
/* Dialog Overlay */
|
||
.admin-dialog {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.admin-dialog.active {
|
||
display: flex;
|
||
}
|
||
|
||
.admin-dialog-content {
|
||
background: var(--bp-surface);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
max-width: 500px;
|
||
width: 90%;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.admin-dialog-content h3 {
|
||
margin: 0 0 12px 0;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.admin-dialog-info {
|
||
font-size: 13px;
|
||
color: var(--bp-text-muted);
|
||
margin-bottom: 20px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.admin-dialog-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
/* Scheduled Badge */
|
||
.admin-badge-scheduled {
|
||
background: #f59e0b;
|
||
color: white;
|
||
}
|
||
|
||
/* Version Compare Overlay */
|
||
.version-compare-overlay {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: var(--bp-bg);
|
||
z-index: 2000;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.version-compare-overlay.active {
|
||
display: flex;
|
||
}
|
||
|
||
.version-compare-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 24px;
|
||
background: var(--bp-surface);
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.version-compare-header h2 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.version-compare-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.compare-vs {
|
||
color: var(--bp-text-muted);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.version-compare-container {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
gap: 0;
|
||
}
|
||
|
||
.version-compare-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
border-right: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.version-compare-panel:last-child {
|
||
border-right: none;
|
||
}
|
||
|
||
.version-compare-panel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 20px;
|
||
background: var(--bp-surface);
|
||
border-bottom: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.compare-label {
|
||
padding: 4px 12px;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.compare-label-published {
|
||
background: rgba(34, 197, 94, 0.15);
|
||
color: #22c55e;
|
||
}
|
||
|
||
.compare-label-draft {
|
||
background: rgba(59, 130, 246, 0.15);
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.version-compare-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 24px;
|
||
font-size: 14px;
|
||
line-height: 1.7;
|
||
}
|
||
|
||
.version-compare-content h1,
|
||
.version-compare-content h2,
|
||
.version-compare-content h3 {
|
||
margin-top: 24px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.version-compare-content p {
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.version-compare-content ul,
|
||
.version-compare-content ol {
|
||
margin-bottom: 12px;
|
||
padding-left: 24px;
|
||
}
|
||
|
||
.version-compare-content .no-content {
|
||
color: var(--bp-text-muted);
|
||
font-style: italic;
|
||
text-align: center;
|
||
padding: 40px;
|
||
}
|
||
|
||
.version-compare-footer {
|
||
padding: 12px 24px;
|
||
background: var(--bp-surface);
|
||
border-top: 1px solid var(--bp-border);
|
||
max-height: 150px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.compare-history-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.compare-history-item {
|
||
font-size: 12px;
|
||
padding: 4px 0;
|
||
border-bottom: 1px solid var(--bp-border-subtle);
|
||
}
|
||
|
||
.compare-history-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.compare-history-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px 8px;
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.admin-form-title {
|
||
margin: 0 0 16px 0;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.admin-form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.admin-form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.admin-form-group.full-width {
|
||
grid-column: 1 / -1;
|
||
}
|
||
|
||
.admin-form-label {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.admin-form-input,
|
||
.admin-form-select,
|
||
.admin-form-textarea {
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
background: var(--bp-surface);
|
||
color: var(--bp-text);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.admin-form-textarea {
|
||
min-height: 150px;
|
||
resize: vertical;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.admin-form-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
justify-content: flex-end;
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.admin-empty {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.admin-loading {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
[data-theme="light"] .admin-modal-content {
|
||
background: #FFFFFF;
|
||
border-color: #E0E0E0;
|
||
}
|
||
|
||
[data-theme="light"] .admin-tabs {
|
||
background: #F5F5F5;
|
||
}
|
||
|
||
[data-theme="light"] .admin-table th {
|
||
background: #F5F5F5;
|
||
}
|
||
|
||
[data-theme="light"] .admin-form {
|
||
background: #F8F8F8;
|
||
border-color: #E0E0E0;
|
||
}
|
||
|
||
/* DSMS Styles */
|
||
.dsms-subtab {
|
||
padding: 6px 14px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--bp-text-muted);
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
border-radius: 4px;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.dsms-subtab:hover {
|
||
background: var(--bp-border-subtle);
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.dsms-subtab.active {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
.dsms-content {
|
||
display: none;
|
||
}
|
||
|
||
.dsms-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.dsms-status-card {
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
}
|
||
|
||
.dsms-status-card h4 {
|
||
margin: 0 0 8px 0;
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
.dsms-status-card .value {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.dsms-status-card .value.online {
|
||
color: var(--bp-accent);
|
||
}
|
||
|
||
.dsms-status-card .value.offline {
|
||
color: var(--bp-danger);
|
||
}
|
||
|
||
.dsms-verify-success {
|
||
background: var(--bp-accent-soft);
|
||
border: 1px solid var(--bp-accent);
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
color: var(--bp-accent);
|
||
}
|
||
|
||
.dsms-verify-error {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
border: 1px solid var(--bp-danger);
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
color: var(--bp-danger);
|
||
}
|
||
|
||
/* DSMS WebUI Styles */
|
||
.dsms-webui-nav {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--bp-text-muted);
|
||
font-size: 14px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
width: 100%;
|
||
transition: all 0.2s;
|
||
}
|
||
.dsms-webui-nav:hover {
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-text);
|
||
}
|
||
.dsms-webui-nav.active {
|
||
background: var(--bp-primary-soft);
|
||
color: var(--bp-primary);
|
||
font-weight: 500;
|
||
}
|
||
.dsms-webui-section {
|
||
display: none;
|
||
}
|
||
.dsms-webui-section.active {
|
||
display: block;
|
||
}
|
||
.dsms-webui-stat-card {
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
}
|
||
.dsms-webui-stat-label {
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
margin-bottom: 4px;
|
||
}
|
||
.dsms-webui-stat-value {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: var(--bp-text);
|
||
}
|
||
.dsms-webui-stat-sub {
|
||
font-size: 11px;
|
||
color: var(--bp-text-muted);
|
||
margin-top: 4px;
|
||
}
|
||
.dsms-webui-upload-zone {
|
||
border: 2px dashed var(--bp-border);
|
||
border-radius: 12px;
|
||
padding: 48px 24px;
|
||
background: var(--bp-input-bg);
|
||
transition: all 0.2s;
|
||
}
|
||
.dsms-webui-upload-zone.dragover {
|
||
border-color: var(--bp-primary);
|
||
background: var(--bp-primary-soft);
|
||
}
|
||
.dsms-webui-file-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 16px;
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
.dsms-webui-file-item .cid {
|
||
font-family: monospace;
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
word-break: break-all;
|
||
}
|
||
|
||
.main-layout {
|
||
display: grid;
|
||
grid-template-columns: 240px minmax(0, 1fr);
|
||
height: 100%;
|
||
min-height: 0;
|
||
}
|
||
|
||
.sidebar {
|
||
border-right: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-gradient-sidebar);
|
||
padding: 14px 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 18px;
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
transition: background 0.3s ease, border-color 0.3s ease;
|
||
}
|
||
|
||
.sidebar-section-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.08em;
|
||
color: var(--bp-text);
|
||
padding: 8px 6px 6px 6px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.sidebar-menu {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.sidebar-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8px 10px;
|
||
border-radius: 9px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.sidebar-item.active {
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-accent);
|
||
border: 1px solid var(--bp-accent-soft);
|
||
}
|
||
|
||
[data-theme="light"] .sidebar-item.active {
|
||
background: var(--bp-primary-soft);
|
||
color: var(--bp-primary);
|
||
border: 1px solid var(--bp-primary);
|
||
}
|
||
|
||
.sidebar-item-label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
}
|
||
|
||
.sidebar-item-badge {
|
||
font-size: 10px;
|
||
border-radius: 999px;
|
||
padding: 2px 7px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
}
|
||
|
||
.sidebar-footer {
|
||
margin-top: auto;
|
||
font-size: 11px;
|
||
color: var(--bp-text-muted);
|
||
padding: 0 6px;
|
||
}
|
||
|
||
.content {
|
||
padding: 14px 16px 16px 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
height: 100%;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.panel {
|
||
background: var(--bp-gradient-surface);
|
||
border-radius: 16px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
padding: 12px 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 0;
|
||
height: 100%;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
transition: background 0.3s ease, border-color 0.3s ease;
|
||
}
|
||
|
||
[data-theme="light"] .panel {
|
||
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
|
||
}
|
||
|
||
.panel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
color: var(--bp-text);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.panel-subtitle {
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.panel-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.small-pill {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
color: var(--bp-text);
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
.upload-inline {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
padding: 10px;
|
||
background: var(--bp-surface-elevated);
|
||
border-radius: 8px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
}
|
||
|
||
[data-theme="light"] .upload-inline {
|
||
background: var(--bp-surface);
|
||
box-shadow: inset 0 1px 3px rgba(0,0,0,0.05);
|
||
}
|
||
|
||
.upload-inline input[type=file] {
|
||
font-size: 11px;
|
||
padding: 6px;
|
||
background: var(--bp-input-bg);
|
||
border: 1px solid var(--bp-border-subtle);
|
||
border-radius: 6px;
|
||
color: var(--bp-text);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.upload-inline input[type=file]::file-selector-button {
|
||
background: var(--bp-accent-soft);
|
||
border: 1px solid var(--bp-accent);
|
||
border-radius: 4px;
|
||
padding: 4px 10px;
|
||
color: var(--bp-accent);
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
[data-theme="light"] .upload-inline input[type=file]::file-selector-button {
|
||
background: var(--bp-primary-soft);
|
||
border-color: var(--bp-primary);
|
||
color: var(--bp-primary);
|
||
}
|
||
|
||
.upload-inline input[type=file]::file-selector-button:hover {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
.file-list {
|
||
list-style: none;
|
||
margin: 0;
|
||
padding: 0;
|
||
max-height: 150px;
|
||
overflow-y: auto;
|
||
border-radius: 10px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
[data-theme="light"] .file-list {
|
||
border-color: var(--bp-primary);
|
||
background: var(--bp-surface);
|
||
}
|
||
|
||
.file-item {
|
||
font-size: 12px;
|
||
padding: 8px 10px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
transition: background 0.15s ease;
|
||
}
|
||
|
||
.file-item:nth-child(odd) {
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
[data-theme="light"] .file-item:nth-child(odd) {
|
||
background: rgba(108, 27, 27, 0.03);
|
||
}
|
||
|
||
.file-item:hover {
|
||
background: var(--bp-accent-soft);
|
||
}
|
||
|
||
[data-theme="light"] .file-item:hover {
|
||
background: var(--bp-primary-soft);
|
||
}
|
||
|
||
.file-item.active {
|
||
background: var(--bp-accent-soft);
|
||
}
|
||
|
||
.file-item-name {
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.file-item-delete {
|
||
font-size: 14px;
|
||
color: #f97316;
|
||
cursor: pointer;
|
||
padding: 4px 6px;
|
||
border-radius: 4px;
|
||
transition: all 0.15s ease;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.file-item-delete:hover {
|
||
color: #fb923c;
|
||
background: rgba(249,115,22,0.15);
|
||
}
|
||
|
||
.file-empty {
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.inline-process {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
margin-top: 12px;
|
||
padding-top: 12px;
|
||
border-top: 1px solid rgba(148,163,184,0.2);
|
||
}
|
||
|
||
.preview-container {
|
||
flex: 1;
|
||
border-radius: 12px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-gradient-sidebar);
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: stretch;
|
||
justify-content: center;
|
||
position: relative;
|
||
min-height: 750px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
[data-theme="light"] .preview-container {
|
||
background: var(--bp-bg);
|
||
border: 2px solid var(--bp-primary);
|
||
box-shadow: 0 4px 20px rgba(108, 27, 27, 0.1);
|
||
}
|
||
|
||
.preview-placeholder {
|
||
font-size: 13px;
|
||
color: var(--bp-text-muted);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
text-align: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
.compare-wrapper {
|
||
display: grid;
|
||
grid-template-columns: minmax(0, 1fr) 110px minmax(0, 1fr);
|
||
gap: 8px;
|
||
width: 100%;
|
||
height: 100%;
|
||
position: relative;
|
||
}
|
||
|
||
.compare-section {
|
||
position: relative;
|
||
border-radius: 10px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-gradient-sidebar);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
min-height: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
[data-theme="light"] .compare-section {
|
||
border: 2px solid var(--bp-primary);
|
||
box-shadow: 0 4px 20px rgba(108, 27, 27, 0.1);
|
||
}
|
||
|
||
.compare-header {
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
border-bottom: 1px solid var(--bp-border-subtle);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
[data-theme="light"] .compare-header {
|
||
background: var(--bp-primary-soft);
|
||
border-bottom: 1px solid var(--bp-primary);
|
||
}
|
||
|
||
.compare-header span {
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
[data-theme="light"] .compare-header span {
|
||
color: var(--bp-primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.compare-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 6px;
|
||
}
|
||
|
||
.compare-body-inner {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.preview-img {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
object-fit: contain;
|
||
box-shadow: 0 18px 40px rgba(0,0,0,0.5);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
[data-theme="light"] .preview-img {
|
||
box-shadow: 0 8px 24px rgba(108, 27, 27, 0.15);
|
||
}
|
||
|
||
.clean-frame {
|
||
width: 100%;
|
||
height: 100%;
|
||
border: none;
|
||
border-radius: 10px;
|
||
background: white;
|
||
}
|
||
|
||
.preview-nav {
|
||
position: absolute;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
pointer-events: none;
|
||
padding: 0 4px;
|
||
}
|
||
|
||
.preview-nav button {
|
||
pointer-events: auto;
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-text);
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
[data-theme="light"] .preview-nav button {
|
||
border: 2px solid var(--bp-primary);
|
||
background: var(--bp-surface);
|
||
color: var(--bp-primary);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.preview-nav button:hover:not(:disabled) {
|
||
border-color: var(--bp-primary);
|
||
color: var(--bp-primary);
|
||
}
|
||
|
||
[data-theme="light"] .preview-nav button:hover:not(:disabled) {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
.preview-nav button:disabled {
|
||
opacity: 0.35;
|
||
cursor: default;
|
||
}
|
||
|
||
.preview-nav span {
|
||
position: absolute;
|
||
bottom: 6px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
font-size: 11px;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border-subtle);
|
||
}
|
||
|
||
[data-theme="light"] .preview-nav span {
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-primary);
|
||
color: var(--bp-primary);
|
||
}
|
||
|
||
.preview-thumbnails {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
padding: 8px 4px;
|
||
overflow-y: auto;
|
||
align-items: center;
|
||
}
|
||
|
||
.preview-thumb {
|
||
min-width: 90px;
|
||
width: 90px;
|
||
height: 70px;
|
||
border-radius: 8px;
|
||
border: 2px solid rgba(148,163,184,0.25);
|
||
background: rgba(15,23,42,0.5);
|
||
cursor: pointer;
|
||
overflow: hidden;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.preview-thumb:hover {
|
||
border-color: var(--bp-accent);
|
||
}
|
||
|
||
.preview-thumb.active {
|
||
border-color: var(--bp-accent);
|
||
border-width: 3px;
|
||
}
|
||
|
||
.preview-thumb img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.preview-thumb-label {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
font-size: 9px;
|
||
padding: 2px;
|
||
background: rgba(0,0,0,0.8);
|
||
color: white;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.pager {
|
||
grid-column: 1 / -1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
padding: 2px 0;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.pager button {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-text);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.pager button:hover {
|
||
border-color: var(--bp-primary);
|
||
color: var(--bp-primary);
|
||
}
|
||
|
||
[data-theme="light"] .pager button {
|
||
border: 2px solid var(--bp-primary);
|
||
background: var(--bp-surface);
|
||
color: var(--bp-primary);
|
||
font-weight: 700;
|
||
}
|
||
|
||
[data-theme="light"] .pager button:hover {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
.status-bar {
|
||
position: fixed;
|
||
right: 18px;
|
||
bottom: 18px;
|
||
padding: 8px 12px;
|
||
border-radius: 999px;
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border-subtle);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
min-width: 230px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
[data-theme="light"] .status-bar {
|
||
background: var(--bp-surface);
|
||
border: 2px solid var(--bp-primary);
|
||
box-shadow: 0 4px 16px rgba(108, 27, 27, 0.15);
|
||
}
|
||
|
||
.status-dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 999px;
|
||
background: var(--bp-text-muted);
|
||
}
|
||
|
||
.status-dot.busy {
|
||
background: var(--bp-accent);
|
||
}
|
||
|
||
.status-dot.error {
|
||
background: var(--bp-danger);
|
||
}
|
||
|
||
.status-text-main {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.status-text-sub {
|
||
font-size: 11px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 12px;
|
||
font-size: 11px;
|
||
color: var(--bp-text-muted);
|
||
border-top: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-surface-elevated);
|
||
transition: background 0.3s ease;
|
||
}
|
||
|
||
.footer a {
|
||
color: var(--bp-text-muted);
|
||
text-decoration: none;
|
||
}
|
||
|
||
.footer a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.btn {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
padding: 8px 16px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-text);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.btn:hover:not(:disabled) {
|
||
border-color: var(--bp-primary);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-primary {
|
||
border-color: var(--bp-accent);
|
||
background: var(--bp-btn-primary-bg);
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover:not(:disabled) {
|
||
background: var(--bp-btn-primary-hover);
|
||
box-shadow: 0 4px 12px rgba(34,197,94,0.3);
|
||
}
|
||
|
||
[data-theme="light"] .btn-primary:hover:not(:disabled) {
|
||
box-shadow: 0 4px 12px rgba(108, 27, 27, 0.3);
|
||
}
|
||
|
||
.btn-ghost {
|
||
background: transparent;
|
||
}
|
||
|
||
.btn-ghost:hover:not(:disabled) {
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
[data-theme="light"] .btn-ghost {
|
||
border-color: var(--bp-primary);
|
||
color: var(--bp-primary);
|
||
}
|
||
|
||
[data-theme="light"] .btn-ghost:hover:not(:disabled) {
|
||
background: var(--bp-primary-soft);
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.panel-tools-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.card-toggle-bar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.toggle-pill {
|
||
font-size: 11px;
|
||
padding: 4px 8px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-text-muted);
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.toggle-pill.active {
|
||
border-color: var(--bp-accent);
|
||
color: var(--bp-accent);
|
||
background: var(--bp-accent-soft);
|
||
}
|
||
|
||
[data-theme="light"] .toggle-pill.active {
|
||
border-color: var(--bp-primary);
|
||
color: var(--bp-primary);
|
||
background: var(--bp-primary-soft);
|
||
}
|
||
|
||
.cards-grid {
|
||
flex: 1;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
grid-auto-rows: 1fr;
|
||
gap: 10px;
|
||
min-height: 0;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.card {
|
||
border-radius: 14px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-card-bg);
|
||
padding: 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
cursor: pointer;
|
||
min-height: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
[data-theme="light"] .card {
|
||
border: 2px solid var(--bp-primary);
|
||
box-shadow: 0 4px 20px rgba(108, 27, 27, 0.1);
|
||
}
|
||
|
||
[data-theme="light"] .card:hover {
|
||
box-shadow: 0 6px 28px rgba(108, 27, 27, 0.2);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
[data-theme="light"] .card-header {
|
||
border-bottom: 1px solid var(--bp-border-subtle);
|
||
padding-bottom: 8px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.card-title {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
[data-theme="light"] .card-title {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: var(--bp-text);
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
.card-badge {
|
||
font-size: 10px;
|
||
border-radius: 999px;
|
||
padding: 2px 7px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
[data-theme="light"] .card-badge {
|
||
background: linear-gradient(135deg, var(--bp-accent) 0%, #4CAF50 100%);
|
||
color: white;
|
||
border: none;
|
||
font-weight: 600;
|
||
padding: 4px 10px;
|
||
}
|
||
|
||
.card-body {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
font-size: 11px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
[data-theme="light"] .card-body {
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.card-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.card-hidden {
|
||
display: none;
|
||
}
|
||
|
||
.card-full {
|
||
grid-column: 1 / -1;
|
||
min-height: 220px;
|
||
}
|
||
|
||
/* Vollbild-Overlay für Doppelklick */
|
||
.lightbox {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(15,23,42,0.96);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 40;
|
||
}
|
||
|
||
.lightbox.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.lightbox-inner {
|
||
position: relative;
|
||
max-width: 96vw;
|
||
max-height: 96vh;
|
||
}
|
||
|
||
.lightbox-img {
|
||
max-width: 96vw;
|
||
max-height: 96vh;
|
||
object-fit: contain;
|
||
border-radius: 12px;
|
||
box-shadow: 0 20px 45px rgba(0,0,0,0.7);
|
||
}
|
||
|
||
.lightbox-close {
|
||
position: absolute;
|
||
top: -32px;
|
||
right: 0;
|
||
font-size: 11px;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid rgba(148,163,184,0.6);
|
||
background: rgba(15,23,42,0.9);
|
||
color: var(--bp-text);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.lightbox-caption {
|
||
margin-top: 6px;
|
||
font-size: 11px;
|
||
color: var(--bp-text-muted);
|
||
text-align: center;
|
||
}
|
||
|
||
/* Lerneinheiten-Projektleiste im Sidebar */
|
||
.unit-form {
|
||
margin-top: 12px;
|
||
padding: 12px 10px;
|
||
border-radius: 10px;
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border-subtle);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
[data-theme="light"] .unit-form {
|
||
background: var(--bp-surface);
|
||
border: 2px solid var(--bp-primary);
|
||
box-shadow: 0 4px 16px rgba(108, 27, 27, 0.1);
|
||
}
|
||
|
||
.unit-form-row {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.unit-input {
|
||
flex: 1;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-bg);
|
||
color: var(--bp-text);
|
||
font-size: 12px;
|
||
padding: 8px 10px;
|
||
transition: border-color 0.2s ease;
|
||
}
|
||
|
||
[data-theme="light"] .unit-input {
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.unit-input:focus {
|
||
outline: none;
|
||
border-color: var(--bp-primary);
|
||
}
|
||
|
||
.unit-input::placeholder {
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
.unit-list {
|
||
margin: 8px 0 0 0;
|
||
padding: 0 2px;
|
||
max-height: 160px;
|
||
overflow-y: auto;
|
||
list-style: none;
|
||
}
|
||
|
||
.unit-item {
|
||
font-size: 12px;
|
||
padding: 8px 10px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
color: var(--bp-text);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
transition: all 0.15s ease;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.unit-item:hover {
|
||
background: var(--bp-surface-elevated);
|
||
}
|
||
|
||
[data-theme="light"] .unit-item:hover {
|
||
background: var(--bp-primary-soft);
|
||
}
|
||
|
||
.unit-item.active {
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-accent);
|
||
border: 1px solid var(--bp-accent);
|
||
font-weight: 600;
|
||
}
|
||
|
||
[data-theme="light"] .unit-item.active {
|
||
background: var(--bp-primary-soft);
|
||
color: var(--bp-primary);
|
||
border: 2px solid var(--bp-primary);
|
||
}
|
||
|
||
.unit-item-meta {
|
||
font-size: 10px;
|
||
color: var(--bp-text-muted);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.btn-unit-add {
|
||
align-self: flex-end;
|
||
}
|
||
|
||
/* MC Preview Styles */
|
||
.mc-preview {
|
||
margin-top: 8px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.mc-question {
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border-subtle);
|
||
border-radius: 8px;
|
||
padding: 10px;
|
||
margin-bottom: 8px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
[data-theme="light"] .mc-question {
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-primary);
|
||
box-shadow: 0 2px 8px rgba(108, 27, 27, 0.08);
|
||
}
|
||
|
||
.mc-question-text {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
margin-bottom: 8px;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.mc-options {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
}
|
||
|
||
.mc-option {
|
||
font-size: 11px;
|
||
padding: 6px 10px;
|
||
border-radius: 6px;
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border-subtle);
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
[data-theme="light"] .mc-option {
|
||
background: var(--bp-bg);
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.mc-option:hover {
|
||
background: var(--bp-accent-soft);
|
||
border-color: var(--bp-accent);
|
||
}
|
||
|
||
[data-theme="light"] .mc-option:hover {
|
||
background: var(--bp-primary-soft);
|
||
border-color: var(--bp-primary);
|
||
}
|
||
|
||
.mc-option.selected {
|
||
background: var(--bp-accent-soft);
|
||
border-color: var(--bp-accent);
|
||
}
|
||
|
||
[data-theme="light"] .mc-option.selected {
|
||
background: var(--bp-primary-soft);
|
||
border-color: var(--bp-primary);
|
||
}
|
||
|
||
.mc-option.correct {
|
||
background: rgba(90, 191, 96, 0.2);
|
||
border-color: var(--bp-accent);
|
||
}
|
||
|
||
.mc-option.incorrect {
|
||
background: rgba(108, 27, 27, 0.2);
|
||
border-color: rgba(239,68,68,0.6);
|
||
}
|
||
|
||
.mc-option-label {
|
||
font-weight: 600;
|
||
margin-right: 6px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.mc-feedback {
|
||
margin-top: 8px;
|
||
font-size: 11px;
|
||
padding: 6px 8px;
|
||
border-radius: 6px;
|
||
background: rgba(34,197,94,0.1);
|
||
border: 1px solid rgba(34,197,94,0.3);
|
||
color: var(--bp-accent);
|
||
}
|
||
|
||
.mc-stats {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-top: 8px;
|
||
padding: 8px;
|
||
background: var(--bp-surface-elevated);
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
[data-theme="light"] .mc-stats {
|
||
background: var(--bp-bg);
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.mc-stats-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
}
|
||
|
||
.mc-modal {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,0.85);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 50;
|
||
}
|
||
|
||
.mc-modal.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.mc-modal-content {
|
||
background: var(--bp-bg);
|
||
border: 1px solid var(--bp-border-subtle);
|
||
border-radius: 16px;
|
||
padding: 20px;
|
||
max-width: 600px;
|
||
width: 90%;
|
||
max-height: 80vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
[data-theme="light"] .mc-modal-content {
|
||
background: var(--bp-surface);
|
||
border: 2px solid var(--bp-primary);
|
||
box-shadow: 0 8px 32px rgba(108, 27, 27, 0.2);
|
||
}
|
||
|
||
.mc-modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.mc-modal-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.mc-modal-close {
|
||
font-size: 11px;
|
||
padding: 4px 10px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-text);
|
||
cursor: pointer;
|
||
}
|
||
|
||
[data-theme="light"] .mc-modal-close {
|
||
border: 1px solid var(--bp-primary);
|
||
background: var(--bp-surface);
|
||
color: var(--bp-primary);
|
||
}
|
||
|
||
[data-theme="light"] .mc-modal-close:hover {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
/* Cloze / Lückentext Styles */
|
||
.cloze-preview {
|
||
margin-top: 8px;
|
||
max-height: 250px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.cloze-item {
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border-subtle);
|
||
border-radius: 8px;
|
||
padding: 10px;
|
||
margin-bottom: 8px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
[data-theme="light"] .cloze-item {
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-primary);
|
||
box-shadow: 0 2px 8px rgba(108, 27, 27, 0.08);
|
||
}
|
||
|
||
.cloze-sentence {
|
||
font-size: 13px;
|
||
line-height: 1.8;
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.cloze-gap {
|
||
display: inline-block;
|
||
min-width: 60px;
|
||
border-bottom: 2px solid var(--bp-accent);
|
||
margin: 0 2px;
|
||
padding: 2px 4px;
|
||
text-align: center;
|
||
background: var(--bp-accent-soft);
|
||
border-radius: 4px 4px 0 0;
|
||
}
|
||
|
||
[data-theme="light"] .cloze-gap {
|
||
border-bottom-color: var(--bp-primary);
|
||
background: var(--bp-primary-soft);
|
||
}
|
||
|
||
.cloze-gap-input {
|
||
width: 80px;
|
||
padding: 4px 8px;
|
||
font-size: 12px;
|
||
border: 1px solid var(--bp-border-subtle);
|
||
border-radius: 4px;
|
||
background: var(--bp-surface-elevated);
|
||
color: var(--bp-text);
|
||
text-align: center;
|
||
}
|
||
|
||
[data-theme="light"] .cloze-gap-input {
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.cloze-gap-input:focus {
|
||
outline: none;
|
||
border-color: var(--bp-primary);
|
||
}
|
||
|
||
.cloze-gap-input.correct {
|
||
border-color: var(--bp-accent);
|
||
background: rgba(90, 191, 96, 0.2);
|
||
}
|
||
|
||
.cloze-gap-input.incorrect {
|
||
border-color: var(--bp-danger);
|
||
background: rgba(108, 27, 27, 0.2);
|
||
}
|
||
|
||
.cloze-translation {
|
||
margin-top: 8px;
|
||
padding: 8px;
|
||
background: var(--bp-accent-soft);
|
||
border: 1px solid var(--bp-accent);
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
color: var(--bp-text-muted);
|
||
}
|
||
|
||
[data-theme="light"] .cloze-translation {
|
||
background: var(--bp-primary-soft);
|
||
border: 1px solid var(--bp-primary);
|
||
}
|
||
|
||
.cloze-translation-label {
|
||
font-size: 10px;
|
||
color: var(--bp-text-muted);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.cloze-hint {
|
||
font-size: 10px;
|
||
color: var(--bp-text-muted);
|
||
font-style: italic;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.cloze-stats {
|
||
display: flex;
|
||
gap: 12px;
|
||
padding: 8px;
|
||
background: var(--bp-surface-elevated);
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
[data-theme="light"] .cloze-stats {
|
||
background: var(--bp-bg);
|
||
border: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.cloze-feedback {
|
||
margin-top: 6px;
|
||
font-size: 11px;
|
||
padding: 4px 8px;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.cloze-feedback.correct {
|
||
background: rgba(34,197,94,0.15);
|
||
color: var(--bp-accent);
|
||
}
|
||
|
||
.cloze-feedback.incorrect {
|
||
background: rgba(239,68,68,0.15);
|
||
color: #ef4444;
|
||
}
|
||
|
||
/* Language Selector */
|
||
.language-selector {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.lang-select {
|
||
background: var(--bp-surface-elevated);
|
||
border: 1px solid var(--bp-border-subtle);
|
||
border-radius: 6px;
|
||
color: var(--bp-text);
|
||
padding: 6px 10px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
[data-theme="light"] .lang-select {
|
||
background: var(--bp-surface);
|
||
border: 1px solid var(--bp-primary);
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
.lang-select:hover {
|
||
border-color: var(--bp-primary);
|
||
}
|
||
|
||
.lang-select:focus {
|
||
outline: none;
|
||
border-color: var(--bp-primary);
|
||
}
|
||
|
||
[data-theme="light"] .lang-select option {
|
||
background: var(--bp-surface);
|
||
color: var(--bp-text);
|
||
}
|
||
|
||
/* RTL Support for Arabic */
|
||
[dir="rtl"] .sidebar {
|
||
border-right: none;
|
||
border-left: 1px solid rgba(148,163,184,0.2);
|
||
}
|
||
|
||
[dir="rtl"] .main-layout {
|
||
direction: rtl;
|
||
}
|
||
|
||
[dir="rtl"] .card-actions {
|
||
flex-direction: row-reverse;
|
||
}
|
||
|
||
/* ==========================================
|
||
RICH TEXT EDITOR STYLES
|
||
========================================== */
|
||
.editor-container {
|
||
border: 1px solid var(--bp-border);
|
||
border-radius: 8px;
|
||
background: var(--bp-surface-elevated);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.editor-toolbar {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
padding: 8px 12px;
|
||
border-bottom: 1px solid var(--bp-border);
|
||
background: var(--bp-surface);
|
||
}
|
||
|
||
.editor-toolbar-group {
|
||
display: flex;
|
||
gap: 2px;
|
||
padding-right: 8px;
|
||
margin-right: 8px;
|
||
border-right: 1px solid var(--bp-border);
|
||
}
|
||
|
||
.editor-toolbar-group:last-child {
|
||
border-right: none;
|
||
margin-right: 0;
|
||
padding-right: 0;
|
||
}
|
||
|
||
.editor-btn {
|
||
padding: 6px 10px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--bp-text);
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
transition: all 0.2s ease;
|
||
min-width: 32px;
|
||
}
|
||
|
||
.editor-btn:hover {
|
||
background: var(--bp-border-subtle);
|
||
}
|
||
|
||
.editor-btn.active {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
}
|
||
|
||
.editor-btn-upload {
|
||
background: var(--bp-primary);
|
||
color: white;
|
||
padding: 6px 12px;
|
||
}
|
||
|
||
.editor-btn-upload:hover {
|
||
filter: brightness(1.1);
|
||
}
|
||
|
||
.editor-content {
|
||
min-height: 300px;
|
||
max-height: 500px;
|
||
overflow-y: auto;
|
||
padding: 16px;
|
||
color: var(--bp-text);
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.editor-content:focus {
|
||
outline: none;
|
||
}
|
||
|
||
.editor-content[contenteditable="true"] {
|
||
cursor: text;
|
||
}
|
||
|
||
.editor-content h1 {
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
margin: 0 0 16px 0;
|
||
}
|
||
|
||
.editor-content h2 {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
margin: 24px 0 12px 0;
|
||
}
|
||
|
||
.editor-content h3 {
|
||
font-size: 17px;
|
||
font-weight: 600;
|
||
margin: 20px 0 10px 0;
|
||
}
|
||
|
||
.editor-content p {
|
||
margin: 0 0 12px 0;
|
||
}
|
||
|
||
.editor-content ul, .editor-content ol {
|
||
margin: 0 0 12px 0;
|
||
padding-left: 24px;
|
||
}
|
||
|
||
.editor-content li {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.editor-content strong {
|
||
font-weight: 600;
|
||
}
|
||
|
||
.editor-content em {
|
||
font-style: italic;
|
||
}
|
||
|
||
.editor-content a {
|
||
color: var(--bp-primary);
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.editor-content blockquote {
|
||
border-left: 3px solid var(--bp-primary);
|
||
margin: 16px 0;
|
||
padding: 8px 16px;
|
||
background: var(--bp-surface);
|
||
font-style: italic;
|
||
}
|
||
|
||
.editor-content hr {
|
||
border: none;
|
||
border-top: 1px solid var(--bp-border);
|
||
margin: 20px 0;
|
||
}
|
||
|
||
[data-theme="light"] .editor-container {
|
||
border-color: #E0E0E0;
|
||
}
|
||
|
||
[data-theme="light"] .editor-toolbar {
|
||
background: #F5F5F5;
|
||
border-color: #E0E0E0;
|
||
}
|
||
|
||
[data-theme="light"] .editor-content {
|
||
background: #FFFFFF;
|
||
}
|
||
|
||
.word-upload-input {
|
||
display: none;
|
||
}
|
||
|
||
.editor-status {
|
||
padding: 8px 12px;
|
||
font-size: 12px;
|
||
color: var(--bp-text-muted);
|
||
border-top: 1px solid var(--bp-border);
|
||
background: var(--bp-surface);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-root">
|
||
<header class="topbar">
|
||
<div class="brand">
|
||
<div class="brand-logo">BP</div>
|
||
<div>
|
||
<div class="brand-text-main">BreakPilot</div>
|
||
<div class="brand-text-sub">Studio</div>
|
||
</div>
|
||
</div>
|
||
<nav class="top-nav" style="display:none;">
|
||
<div class="top-nav-item active" data-screen="1"></div>
|
||
<div class="top-nav-item" data-screen="2"></div>
|
||
</nav>
|
||
<div class="top-actions">
|
||
<div class="language-selector">
|
||
<select id="language-select" class="lang-select">
|
||
<option value="de">🇩🇪 Deutsch</option>
|
||
<option value="tr">🇹🇷 Türkçe</option>
|
||
<option value="ar">🇸🇦 العربية</option>
|
||
<option value="ru">🇷🇺 Русский</option>
|
||
<option value="uk">🇺🇦 Українська</option>
|
||
<option value="pl">🇵🇱 Polski</option>
|
||
<option value="en">🇬🇧 English</option>
|
||
</select>
|
||
</div>
|
||
<button class="theme-toggle" id="theme-toggle" title="Dark/Light Mode">
|
||
<span class="theme-toggle-icon" id="theme-icon">🌙</span>
|
||
<span id="theme-label">Dark</span>
|
||
</button>
|
||
<button class="btn btn-sm btn-ghost" id="btn-legal" title="Rechtliches">⚖️</button>
|
||
<button class="btn btn-sm btn-ghost" id="btn-admin" title="Admin Panel">⚙️</button>
|
||
<button class="btn btn-sm btn-ghost" id="btn-login" data-i18n="login">Login / Anmeldung</button>
|
||
<!-- Notification Bell (shown when logged in) -->
|
||
<div class="notification-bell" id="notification-bell">
|
||
<button class="notification-bell-btn" id="notification-bell-btn" title="Benachrichtigungen">
|
||
🔔
|
||
</button>
|
||
<span class="notification-badge hidden" id="notification-badge">0</span>
|
||
<div class="notification-panel" id="notification-panel">
|
||
<div class="notification-panel-header">
|
||
<span class="notification-panel-title">Benachrichtigungen</span>
|
||
<div class="notification-panel-actions">
|
||
<button class="notification-panel-action" onclick="markAllNotificationsRead()" title="Alle als gelesen markieren">✓ Alle</button>
|
||
<button class="notification-panel-action" onclick="showNotificationPreferences()" title="Einstellungen">⚙️</button>
|
||
</div>
|
||
</div>
|
||
<div class="notification-list" id="notification-list">
|
||
<div class="notification-empty">
|
||
<div class="notification-empty-icon">🔔</div>
|
||
<div>Keine Benachrichtigungen</div>
|
||
</div>
|
||
</div>
|
||
<div class="notification-footer">
|
||
<button class="notification-footer-btn" onclick="loadMoreNotifications()">Mehr laden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- User Dropdown (shown when logged in) -->
|
||
<div class="auth-user-dropdown">
|
||
<button class="auth-user-btn" id="auth-user-btn">
|
||
<div class="auth-user-avatar">BP</div>
|
||
<span>▼</span>
|
||
</button>
|
||
<div class="auth-user-menu" id="auth-user-menu">
|
||
<div class="auth-user-menu-header">
|
||
<div class="auth-user-menu-name">Benutzer</div>
|
||
<div class="auth-user-menu-email">user@example.com</div>
|
||
</div>
|
||
<button class="auth-user-menu-item" onclick="showProfileModal()">👤 Profil</button>
|
||
<button class="auth-user-menu-item" onclick="showSessionsModal()">🔐 Aktive Sitzungen</button>
|
||
<button class="auth-user-menu-item danger" onclick="logout()">🚪 Abmelden</button>
|
||
</div>
|
||
</div>
|
||
<div class="pill" data-i18n="mvp_local">MVP · Lokal auf deinem Mac</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="main-layout">
|
||
<aside class="sidebar">
|
||
<div>
|
||
<div class="sidebar-section-title">Bereiche</div>
|
||
<div class="sidebar-menu">
|
||
<div class="sidebar-item active">
|
||
<div class="sidebar-item-label">
|
||
<span>Arbeitsblatt Studio</span>
|
||
</div>
|
||
<div class="sidebar-item-badge">aktiv</div>
|
||
</div>
|
||
<div class="sidebar-item">
|
||
<div class="sidebar-item-label">
|
||
<span>Eltern-Kanal</span>
|
||
</div>
|
||
<div class="sidebar-item-badge">demnächst</div>
|
||
</div>
|
||
<div class="sidebar-item">
|
||
<div class="sidebar-item-label">
|
||
<span>Korrektur / Noten</span>
|
||
</div>
|
||
<div class="sidebar-item-badge">demnächst</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<div class="sidebar-section-title">Lerneinheiten (lokal)</div>
|
||
<div class="unit-form">
|
||
<input class="unit-input" id="unit-student" placeholder="Schüler/in">
|
||
<input class="unit-input" id="unit-subject" placeholder="Fach">
|
||
<input class="unit-input" id="unit-grade" placeholder="Klasse (z.B. 7a)">
|
||
<input class="unit-input" id="unit-title" placeholder="Lerneinheit / Thema">
|
||
<button type="button" class="btn btn-sm btn-primary btn-unit-add" id="btn-add-unit">Anlegen</button>
|
||
<ul class="unit-list" id="unit-list"></ul>
|
||
<button type="button" class="btn btn-sm btn-ghost" id="btn-attach-current-to-lu">Aktuelles Arbeitsblatt hinzufügen</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<div class="sidebar-section-title">Dateien hochladen</div>
|
||
<div class="upload-inline">
|
||
<input type="file" id="file-input" multiple>
|
||
<button type="button" class="btn btn-sm btn-primary" id="btn-upload-inline">Hochladen</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<div class="sidebar-section-title">Speicherort</div>
|
||
<div class="sidebar-footer">
|
||
CI / Brandbook: Farben kommen bereits aus Variablen und sind später leicht anpassbar.
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<main class="content">
|
||
<!-- Screen 1: Eingangsdateien & Alt/Neu-Vergleich -->
|
||
<section class="panel" id="panel-compare" style="display:flex;">
|
||
<div class="panel-header">
|
||
<div>
|
||
<div class="panel-title">Arbeitsblätter & Vergleich</div>
|
||
<div class="panel-subtitle">Links Scan · Rechts neu aufgebautes Arbeitsblatt</div>
|
||
<div class="panel-subtitle" id="unit-heading-screen1">Keine Lerneinheit ausgewählt</div>
|
||
</div>
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<button type="button" class="btn btn-sm btn-primary" id="btn-toggle-filter" style="font-size: 10px;">
|
||
Nur Lerneinheit
|
||
</button>
|
||
<span class="small-pill" id="eingang-count">0 Dateien</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel-body">
|
||
<ul class="file-list" id="eingang-list"></ul>
|
||
|
||
<div class="inline-process">
|
||
<button type="button" class="btn btn-sm btn-primary" id="btn-full-process">
|
||
Arbeitsblätter neu aufbauen
|
||
</button>
|
||
</div>
|
||
|
||
<div class="preview-container" id="preview-container">
|
||
<div class="preview-placeholder">
|
||
Lade Arbeitsblätter hoch und klicke auf „Arbeitsblätter neu aufbauen".<br>
|
||
Dann kannst du mit den Pfeilen durch die Scans und die bereinigten Versionen blättern.<br>
|
||
Mit Doppelklick öffnest du das Dokument im Vollbild.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Screen 2: Kacheln -->
|
||
<section class="panel" id="panel-tiles" style="display:none;">
|
||
<div class="panel-tools-header">
|
||
<div>
|
||
<div class="panel-title"><span id="unit-heading-screen2">Keine Lerneinheit ausgewählt</span> – Bitte wähle Deine Lernkacheln aus, die Du verwenden möchtest.</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel-body">
|
||
<div class="card-toggle-bar">
|
||
<button class="toggle-pill active" data-tile="mindmap">Mindmap</button>
|
||
<button class="toggle-pill active" data-tile="qa">Frage–Antwort</button>
|
||
<button class="toggle-pill active" data-tile="mc">Multiple Choice</button>
|
||
<button class="toggle-pill active" data-tile="cloze">Lückentext</button>
|
||
</div>
|
||
|
||
<div class="cards-grid">
|
||
<!-- Mindmap Lernposter -->
|
||
<div class="card card-half" data-tile="mindmap">
|
||
<div class="card-header">
|
||
<div class="card-title">Mindmap Lernposter</div>
|
||
<div class="card-badge" id="mindmap-badge">Bereit</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div>Erstellt eine kindgerechte Mindmap mit dem Hauptthema in der Mitte und allen Fachbegriffen in farbigen Kategorien. Perfekt zum Ausdrucken als A3-Poster!</div>
|
||
<div id="mindmap-preview" class="mindmap-preview"></div>
|
||
<div class="card-actions" style="flex-wrap:wrap;gap:6px;">
|
||
<button class="btn btn-sm btn-primary" id="btn-mindmap-generate">Mindmap erstellen</button>
|
||
<button class="btn btn-sm btn-ghost" id="btn-mindmap-show" style="display:none;">Ansehen</button>
|
||
<button class="btn btn-sm btn-ghost" id="btn-mindmap-print" style="display:none;">A3 Drucken</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Frage–Antwort mit Leitner-System -->
|
||
<div class="card card-half" data-tile="qa">
|
||
<div class="card-header">
|
||
<div class="card-title">Frage–Antwort-Blatt</div>
|
||
<div class="card-badge" id="qa-badge">Bereit</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div>Frage-Antwort-Paare mit Leitner-Box System. Wiederholung nach Schwierigkeitsgrad für nachhaltiges Lernen.</div>
|
||
<div id="qa-preview" class="mc-preview"></div>
|
||
<div class="card-actions" style="flex-wrap:wrap;gap:6px;">
|
||
<button class="btn btn-sm btn-primary" id="btn-qa-generate">Q&A generieren</button>
|
||
<button class="btn btn-sm btn-ghost" id="btn-qa-learn" style="display:none;">Lernen starten</button>
|
||
<button class="btn btn-sm btn-ghost" id="btn-qa-print" style="display:none;">Drucken</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Multiple Choice -->
|
||
<div class="card card-half" data-tile="mc">
|
||
<div class="card-header">
|
||
<div class="card-title">Multiple Choice Test</div>
|
||
<div class="card-badge" id="mc-badge">Bereit</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div>Erzeugt passende MC-Aufgaben zur ursprünglichen Schwierigkeit (z. B. Klasse 7), ohne das Niveau zu verändern.</div>
|
||
<div id="mc-preview" class="mc-preview"></div>
|
||
<div class="card-actions" style="flex-wrap:wrap;gap:6px;">
|
||
<button class="btn btn-sm btn-primary" id="btn-mc-generate">MC generieren</button>
|
||
<button class="btn btn-sm btn-ghost" id="btn-mc-show" style="display:none;">Quiz starten</button>
|
||
<button class="btn btn-sm btn-ghost" id="btn-mc-print" style="display:none;">Drucken</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Lückentext -->
|
||
<div class="card card-half" data-tile="cloze">
|
||
<div class="card-header">
|
||
<div class="card-title">Lückentext</div>
|
||
<div class="card-badge" id="cloze-badge">Bereit</div>
|
||
</div>
|
||
<div class="card-body">
|
||
<div>Erzeugt Lückentexte mit mehreren sinnvollen Lücken pro Satz. Inkl. Übersetzung für Eltern.</div>
|
||
<div class="cloze-language-select" style="margin-top:8px;">
|
||
<label style="font-size:11px;color:var(--bp-text-muted);">Übersetzung:</label>
|
||
<select id="cloze-language" class="unit-input" style="padding:4px 8px;font-size:11px;width:auto;">
|
||
<option value="tr">Türkisch</option>
|
||
<option value="ar">Arabisch</option>
|
||
<option value="ru">Russisch</option>
|
||
<option value="uk">Ukrainisch</option>
|
||
<option value="pl">Polnisch</option>
|
||
<option value="en">Englisch</option>
|
||
<option value="fr">Französisch</option>
|
||
<option value="es">Spanisch</option>
|
||
</select>
|
||
</div>
|
||
<div id="cloze-preview" class="cloze-preview"></div>
|
||
<div class="card-actions" style="flex-wrap:wrap;gap:6px;">
|
||
<button class="btn btn-sm btn-primary" id="btn-cloze-generate">Lückentext generieren</button>
|
||
<button class="btn btn-sm btn-ghost" id="btn-cloze-show" style="display:none;">Übung starten</button>
|
||
<button class="btn btn-sm btn-ghost" id="btn-cloze-print" style="display:none;">Drucken</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<div class="pager">
|
||
<button id="pager-prev"><</button>
|
||
<span id="pager-label">1 von 2</span>
|
||
<button id="pager-next">></button>
|
||
</div>
|
||
|
||
<div class="status-bar" id="status-bar">
|
||
<div class="status-dot" id="status-dot"></div>
|
||
<div>
|
||
<div class="status-text-main" id="status-main"></div>
|
||
<div class="status-text-sub" id="status-sub"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Vollbild-Overlay -->
|
||
<div class="lightbox hidden" id="lightbox">
|
||
<div class="lightbox-inner">
|
||
<button type="button" class="lightbox-close" id="lightbox-close">Schließen ✕</button>
|
||
<img id="lightbox-img" class="lightbox-img" src="" alt="">
|
||
<div id="lightbox-caption" class="lightbox-caption"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MC Quiz Modal -->
|
||
<div class="mc-modal hidden" id="mc-modal">
|
||
<div class="mc-modal-content">
|
||
<div class="mc-modal-header">
|
||
<div class="mc-modal-title">Multiple Choice Quiz</div>
|
||
<button type="button" class="mc-modal-close" id="mc-modal-close">Schließen ✕</button>
|
||
</div>
|
||
<div id="mc-modal-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cloze / Lückentext Modal -->
|
||
<div class="mc-modal hidden" id="cloze-modal">
|
||
<div class="mc-modal-content" style="max-width:700px;">
|
||
<div class="mc-modal-header">
|
||
<div class="mc-modal-title">Lückentext-Übung</div>
|
||
<button type="button" class="mc-modal-close" id="cloze-modal-close">Schließen ✕</button>
|
||
</div>
|
||
<div id="cloze-modal-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Q&A / Frage-Antwort Modal mit Leitner-System -->
|
||
<div class="mc-modal hidden" id="qa-modal">
|
||
<div class="mc-modal-content" style="max-width:700px;">
|
||
<div class="mc-modal-header">
|
||
<div class="mc-modal-title">Frage-Antwort Lernmodus</div>
|
||
<button type="button" class="mc-modal-close" id="qa-modal-close">Schließen ✕</button>
|
||
</div>
|
||
<div id="qa-modal-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="footer">
|
||
<span>BreakPilot · MVP lokal</span>
|
||
<span>·</span>
|
||
<a href="#" onclick="openImprintModal(); return false;">Impressum</a>
|
||
<a href="#" onclick="openLegalModal(); return false;">Rechtliches</a>
|
||
</div>
|
||
|
||
<script>
|
||
// ==========================================
|
||
// THEME TOGGLE (Dark/Light Mode)
|
||
// ==========================================
|
||
(function() {
|
||
const savedTheme = localStorage.getItem('bp-theme') || 'dark';
|
||
if (savedTheme === 'light') {
|
||
document.documentElement.setAttribute('data-theme', 'light');
|
||
}
|
||
})();
|
||
|
||
function initThemeToggle() {
|
||
const toggle = document.getElementById('theme-toggle');
|
||
const icon = document.getElementById('theme-icon');
|
||
const label = document.getElementById('theme-label');
|
||
|
||
function updateToggleUI(theme) {
|
||
if (theme === 'light') {
|
||
icon.textContent = '☀️';
|
||
label.textContent = 'Light';
|
||
} else {
|
||
icon.textContent = '🌙';
|
||
label.textContent = 'Dark';
|
||
}
|
||
}
|
||
|
||
// Initialize UI based on current theme
|
||
const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
|
||
updateToggleUI(currentTheme);
|
||
|
||
toggle.addEventListener('click', function() {
|
||
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
||
const newTheme = current === 'dark' ? 'light' : 'dark';
|
||
|
||
if (newTheme === 'light') {
|
||
document.documentElement.setAttribute('data-theme', 'light');
|
||
} else {
|
||
document.documentElement.removeAttribute('data-theme');
|
||
}
|
||
|
||
localStorage.setItem('bp-theme', newTheme);
|
||
updateToggleUI(newTheme);
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// INTERNATIONALISIERUNG (i18n)
|
||
// ==========================================
|
||
const translations = {
|
||
de: {
|
||
// Navigation & Header
|
||
brand_sub: "Studio",
|
||
nav_compare: "Arbeitsblätter",
|
||
nav_tiles: "Lern-Kacheln",
|
||
login: "Login / Anmeldung",
|
||
mvp_local: "MVP · Lokal auf deinem Mac",
|
||
|
||
// Sidebar
|
||
sidebar_areas: "Bereiche",
|
||
sidebar_studio: "Arbeitsblatt Studio",
|
||
sidebar_active: "aktiv",
|
||
sidebar_parents: "Eltern-Kanal",
|
||
sidebar_soon: "demnächst",
|
||
sidebar_correction: "Korrektur / Noten",
|
||
sidebar_units: "Lerneinheiten (lokal)",
|
||
input_student: "Schüler/in",
|
||
input_subject: "Fach",
|
||
input_grade: "Klasse (z.B. 7a)",
|
||
input_unit_title: "Lerneinheit / Thema",
|
||
btn_create: "Anlegen",
|
||
btn_add_current: "Aktuelles Arbeitsblatt hinzufügen",
|
||
btn_filter_unit: "Nur Lerneinheit",
|
||
btn_filter_all: "Alle Dateien",
|
||
|
||
// Screen 1 - Compare
|
||
uploaded_worksheets: "Hochgeladene Arbeitsblätter",
|
||
files: "Dateien",
|
||
btn_upload: "Hochladen",
|
||
btn_delete: "Löschen",
|
||
original_scan: "Original-Scan",
|
||
cleaned_version: "Bereinigt (Handschrift entfernt)",
|
||
no_cleaned: "Noch keine bereinigte Version vorhanden.",
|
||
process_hint: "Klicke auf 'Verarbeiten', um das Arbeitsblatt zu analysieren und zu bereinigen.",
|
||
worksheet_print: "Drucken",
|
||
worksheet_no_data: "Keine Arbeitsblatt-Daten vorhanden.",
|
||
btn_full_process: "Verarbeiten (Analyse + Bereinigung + HTML)",
|
||
btn_original_generate: "Nur Original-HTML generieren",
|
||
|
||
// Screen 2 - Tiles
|
||
learning_unit: "Lerneinheit",
|
||
no_unit_selected: "Keine Lerneinheit ausgewählt",
|
||
|
||
// MC Tile
|
||
mc_title: "Multiple Choice Test",
|
||
mc_ready: "Bereit",
|
||
mc_generating: "Generiert...",
|
||
mc_done: "Fertig",
|
||
mc_error: "Fehler",
|
||
mc_desc: "Erzeugt passende MC-Aufgaben zur ursprünglichen Schwierigkeit (z. B. Klasse 7), ohne das Niveau zu verändern.",
|
||
mc_generate: "MC generieren",
|
||
mc_show: "Fragen anzeigen",
|
||
mc_quiz_title: "Multiple Choice Quiz",
|
||
mc_evaluate: "Auswerten",
|
||
mc_correct: "Richtig!",
|
||
mc_incorrect: "Leider falsch.",
|
||
mc_not_answered: "Nicht beantwortet. Richtig wäre:",
|
||
mc_result: "von",
|
||
mc_result_correct: "richtig",
|
||
mc_percent: "korrekt",
|
||
mc_no_questions: "Noch keine MC-Fragen für dieses Arbeitsblatt generiert.",
|
||
mc_print: "Drucken",
|
||
mc_print_with_answers: "Mit Lösungen drucken?",
|
||
|
||
// Cloze Tile
|
||
cloze_title: "Lückentext",
|
||
cloze_desc: "Erzeugt Lückentexte mit mehreren sinnvollen Lücken pro Satz. Inkl. Übersetzung für Eltern.",
|
||
cloze_translation: "Übersetzung:",
|
||
cloze_generate: "Lückentext generieren",
|
||
cloze_start: "Übung starten",
|
||
cloze_exercise_title: "Lückentext-Übung",
|
||
cloze_instruction: "Fülle die Lücken aus und klicke auf 'Prüfen'.",
|
||
cloze_check: "Prüfen",
|
||
cloze_show_answers: "Lösungen zeigen",
|
||
cloze_no_texts: "Noch keine Lückentexte für dieses Arbeitsblatt generiert.",
|
||
cloze_sentences: "Sätze",
|
||
cloze_gaps: "Lücken",
|
||
cloze_gaps_total: "Lücken gesamt",
|
||
cloze_with_gaps: "(mit Lücken)",
|
||
cloze_print: "Drucken",
|
||
cloze_print_with_answers: "Mit Lösungen drucken?",
|
||
|
||
// QA Tile
|
||
qa_title: "Frage-Antwort-Blatt",
|
||
qa_desc: "Frage-Antwort-Paare mit Leitner-Box System. Wiederholung nach Schwierigkeitsgrad.",
|
||
qa_generate: "Q&A generieren",
|
||
qa_learn: "Lernen starten",
|
||
qa_print: "Drucken",
|
||
qa_no_questions: "Noch keine Q&A für dieses Arbeitsblatt generiert.",
|
||
qa_box_new: "Neu",
|
||
qa_box_learning: "Gelernt",
|
||
qa_box_mastered: "Gefestigt",
|
||
qa_show_answer: "Antwort zeigen",
|
||
qa_your_answer: "Deine Antwort",
|
||
qa_type_answer: "Schreibe deine Antwort hier...",
|
||
qa_check_answer: "Antwort prüfen",
|
||
qa_correct_answer: "Richtige Antwort",
|
||
qa_self_evaluate: "War deine Antwort richtig?",
|
||
qa_no_answer: "(keine Antwort eingegeben)",
|
||
qa_correct: "Richtig",
|
||
qa_incorrect: "Falsch",
|
||
qa_key_terms: "Schlüsselbegriffe",
|
||
qa_session_correct: "Richtig",
|
||
qa_session_incorrect: "Falsch",
|
||
qa_session_complete: "Lernrunde abgeschlossen!",
|
||
qa_result_correct: "richtig",
|
||
qa_restart: "Nochmal lernen",
|
||
qa_print_with_answers: "Mit Lösungen drucken?",
|
||
question: "Frage",
|
||
answer: "Antwort",
|
||
status_generating_qa: "Generiere Q&A …",
|
||
status_qa_generated: "Q&A generiert",
|
||
|
||
// Common
|
||
close: "Schließen",
|
||
subject: "Fach",
|
||
grade: "Stufe",
|
||
questions: "Fragen",
|
||
worksheet: "Arbeitsblatt",
|
||
loading: "Lädt...",
|
||
error: "Fehler",
|
||
success: "Erfolgreich",
|
||
|
||
// Footer
|
||
imprint: "Impressum",
|
||
privacy: "Datenschutz",
|
||
contact: "Kontakt",
|
||
|
||
// Status messages
|
||
status_ready: "Bereit",
|
||
status_processing: "Verarbeitet...",
|
||
status_generating_mc: "Generiere MC-Fragen …",
|
||
status_generating_cloze: "Generiere Lückentexte …",
|
||
status_please_wait: "Bitte warten, KI arbeitet.",
|
||
status_mc_generated: "MC-Fragen generiert",
|
||
status_cloze_generated: "Lückentexte generiert",
|
||
status_files_created: "Dateien erstellt",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Mindmap Lernposter",
|
||
mindmap_desc: "Erstellt eine kindgerechte Mindmap mit dem Hauptthema in der Mitte und allen Fachbegriffen in farbigen Kategorien.",
|
||
mindmap_generate: "Mindmap erstellen",
|
||
mindmap_show: "Ansehen",
|
||
mindmap_print_a3: "A3 Drucken",
|
||
generating_mindmap: "Erstelle Mindmap...",
|
||
mindmap_generated: "Mindmap erstellt!",
|
||
no_analysis: "Keine Analyse",
|
||
analyze_first: "Bitte zuerst analysieren (Verarbeiten starten)",
|
||
categories: "Kategorien",
|
||
terms: "Begriffe",
|
||
},
|
||
|
||
tr: {
|
||
brand_sub: "Stüdyo",
|
||
nav_compare: "Çalışma Sayfaları",
|
||
nav_tiles: "Öğrenme Kartları",
|
||
login: "Giriş / Kayıt",
|
||
mvp_local: "MVP · Mac'inizde Yerel",
|
||
|
||
sidebar_areas: "Alanlar",
|
||
sidebar_studio: "Çalışma Sayfası Stüdyosu",
|
||
sidebar_active: "aktif",
|
||
sidebar_parents: "Ebeveyn Kanalı",
|
||
sidebar_soon: "yakında",
|
||
sidebar_correction: "Düzeltme / Notlar",
|
||
sidebar_units: "Öğrenme Birimleri (yerel)",
|
||
input_student: "Öğrenci",
|
||
input_subject: "Ders",
|
||
input_grade: "Sınıf (örn. 7a)",
|
||
input_unit_title: "Öğrenme Birimi / Konu",
|
||
btn_create: "Oluştur",
|
||
btn_add_current: "Mevcut çalışma sayfasını ekle",
|
||
btn_filter_unit: "Sadece Birim",
|
||
btn_filter_all: "Tüm Dosyalar",
|
||
|
||
uploaded_worksheets: "Yüklenen Çalışma Sayfaları",
|
||
files: "Dosya",
|
||
btn_upload: "Yükle",
|
||
btn_delete: "Sil",
|
||
original_scan: "Orijinal Tarama",
|
||
cleaned_version: "Temizlenmiş (El yazısı kaldırıldı)",
|
||
no_cleaned: "Henüz temizlenmiş sürüm yok.",
|
||
process_hint: "Çalışma sayfasını analiz etmek ve temizlemek için 'İşle'ye tıklayın.",
|
||
worksheet_print: "Yazdır",
|
||
worksheet_no_data: "Çalışma sayfası verisi yok.",
|
||
btn_full_process: "İşle (Analiz + Temizleme + HTML)",
|
||
btn_original_generate: "Sadece Orijinal HTML Oluştur",
|
||
|
||
learning_unit: "Öğrenme Birimi",
|
||
no_unit_selected: "Öğrenme birimi seçilmedi",
|
||
|
||
mc_title: "Çoktan Seçmeli Test",
|
||
mc_ready: "Hazır",
|
||
mc_generating: "Oluşturuluyor...",
|
||
mc_done: "Tamamlandı",
|
||
mc_error: "Hata",
|
||
mc_desc: "Orijinal zorluğa uygun (örn. 7. sınıf) çoktan seçmeli sorular oluşturur.",
|
||
mc_generate: "ÇS Oluştur",
|
||
mc_show: "Soruları Göster",
|
||
mc_quiz_title: "Çoktan Seçmeli Quiz",
|
||
mc_evaluate: "Değerlendir",
|
||
mc_correct: "Doğru!",
|
||
mc_incorrect: "Maalesef yanlış.",
|
||
mc_not_answered: "Cevaplanmadı. Doğru cevap:",
|
||
mc_result: "/",
|
||
mc_result_correct: "doğru",
|
||
mc_percent: "doğru",
|
||
mc_no_questions: "Bu çalışma sayfası için henüz ÇS sorusu oluşturulmadı.",
|
||
mc_print: "Yazdır",
|
||
mc_print_with_answers: "Cevaplarla yazdır?",
|
||
|
||
cloze_title: "Boşluk Doldurma",
|
||
cloze_desc: "Her cümlede birden fazla anlamlı boşluk içeren metinler oluşturur. Ebeveynler için çeviri dahil.",
|
||
cloze_translation: "Çeviri:",
|
||
cloze_generate: "Boşluk Metni Oluştur",
|
||
cloze_start: "Alıştırmayı Başlat",
|
||
cloze_exercise_title: "Boşluk Doldurma Alıştırması",
|
||
cloze_instruction: "Boşlukları doldurun ve 'Kontrol Et'e tıklayın.",
|
||
cloze_check: "Kontrol Et",
|
||
cloze_show_answers: "Cevapları Göster",
|
||
cloze_no_texts: "Bu çalışma sayfası için henüz boşluk metni oluşturulmadı.",
|
||
cloze_sentences: "Cümle",
|
||
cloze_gaps: "Boşluk",
|
||
cloze_gaps_total: "Toplam boşluk",
|
||
cloze_with_gaps: "(boşluklu)",
|
||
cloze_print: "Yazdır",
|
||
cloze_print_with_answers: "Cevaplarla yazdır?",
|
||
|
||
qa_title: "Soru-Cevap Sayfası",
|
||
qa_desc: "Leitner kutu sistemiyle soru-cevap çiftleri. Zorluk derecesine göre tekrar.",
|
||
qa_generate: "S&C Oluştur",
|
||
qa_learn: "Öğrenmeye Başla",
|
||
qa_print: "Yazdır",
|
||
qa_no_questions: "Bu çalışma sayfası için henüz S&C oluşturulmadı.",
|
||
qa_box_new: "Yeni",
|
||
qa_box_learning: "Öğreniliyor",
|
||
qa_box_mastered: "Pekiştirildi",
|
||
qa_show_answer: "Cevabı Göster",
|
||
qa_your_answer: "Senin Cevabın",
|
||
qa_type_answer: "Cevabını buraya yaz...",
|
||
qa_check_answer: "Cevabı Kontrol Et",
|
||
qa_correct_answer: "Doğru Cevap",
|
||
qa_self_evaluate: "Cevabın doğru muydu?",
|
||
qa_no_answer: "(cevap girilmedi)",
|
||
qa_correct: "Doğru",
|
||
qa_incorrect: "Yanlış",
|
||
qa_key_terms: "Anahtar Kavramlar",
|
||
qa_session_correct: "Doğru",
|
||
qa_session_incorrect: "Yanlış",
|
||
qa_session_complete: "Öğrenme turu tamamlandı!",
|
||
qa_result_correct: "doğru",
|
||
qa_restart: "Tekrar Öğren",
|
||
qa_print_with_answers: "Cevaplarla yazdır?",
|
||
question: "Soru",
|
||
answer: "Cevap",
|
||
status_generating_qa: "S&C oluşturuluyor…",
|
||
status_qa_generated: "S&C oluşturuldu",
|
||
|
||
close: "Kapat",
|
||
subject: "Ders",
|
||
grade: "Seviye",
|
||
questions: "Soru",
|
||
worksheet: "Çalışma Sayfası",
|
||
loading: "Yükleniyor...",
|
||
error: "Hata",
|
||
success: "Başarılı",
|
||
|
||
imprint: "Künye",
|
||
privacy: "Gizlilik",
|
||
contact: "İletişim",
|
||
|
||
status_ready: "Hazır",
|
||
status_processing: "İşleniyor...",
|
||
status_generating_mc: "ÇS soruları oluşturuluyor…",
|
||
status_generating_cloze: "Boşluk metinleri oluşturuluyor…",
|
||
status_please_wait: "Lütfen bekleyin, yapay zeka çalışıyor.",
|
||
status_mc_generated: "ÇS soruları oluşturuldu",
|
||
status_cloze_generated: "Boşluk metinleri oluşturuldu",
|
||
status_files_created: "dosya oluşturuldu",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Zihin Haritası Poster",
|
||
mindmap_desc: "Ana konuyu ortada ve tüm terimleri renkli kategorilerde gösteren çocuk dostu bir zihin haritası oluşturur.",
|
||
mindmap_generate: "Zihin Haritası Oluştur",
|
||
mindmap_show: "Görüntüle",
|
||
mindmap_print_a3: "A3 Yazdır",
|
||
generating_mindmap: "Zihin haritası oluşturuluyor...",
|
||
mindmap_generated: "Zihin haritası oluşturuldu!",
|
||
no_analysis: "Analiz yok",
|
||
analyze_first: "Lütfen önce analiz edin (İşle'ye tıklayın)",
|
||
categories: "Kategoriler",
|
||
terms: "Terimler",
|
||
},
|
||
|
||
ar: {
|
||
brand_sub: "ستوديو",
|
||
nav_compare: "أوراق العمل",
|
||
nav_tiles: "بطاقات التعلم",
|
||
login: "تسجيل الدخول / التسجيل",
|
||
mvp_local: "MVP · محلي على جهازك",
|
||
|
||
sidebar_areas: "الأقسام",
|
||
sidebar_studio: "استوديو أوراق العمل",
|
||
sidebar_active: "نشط",
|
||
sidebar_parents: "قناة الوالدين",
|
||
sidebar_soon: "قريباً",
|
||
sidebar_correction: "التصحيح / الدرجات",
|
||
sidebar_units: "وحدات التعلم (محلية)",
|
||
input_student: "الطالب/ة",
|
||
input_subject: "المادة",
|
||
input_grade: "الصف (مثل 7أ)",
|
||
input_unit_title: "وحدة التعلم / الموضوع",
|
||
btn_create: "إنشاء",
|
||
btn_add_current: "إضافة ورقة العمل الحالية",
|
||
btn_filter_unit: "الوحدة فقط",
|
||
btn_filter_all: "جميع الملفات",
|
||
|
||
uploaded_worksheets: "أوراق العمل المحملة",
|
||
files: "ملفات",
|
||
btn_upload: "تحميل",
|
||
btn_delete: "حذف",
|
||
original_scan: "المسح الأصلي",
|
||
cleaned_version: "منظف (تم إزالة الكتابة اليدوية)",
|
||
no_cleaned: "لا توجد نسخة منظفة بعد.",
|
||
process_hint: "انقر على 'معالجة' لتحليل وتنظيف ورقة العمل.",
|
||
worksheet_print: "طباعة",
|
||
worksheet_no_data: "لا توجد بيانات ورقة العمل.",
|
||
btn_full_process: "معالجة (تحليل + تنظيف + HTML)",
|
||
btn_original_generate: "إنشاء HTML الأصلي فقط",
|
||
|
||
learning_unit: "وحدة التعلم",
|
||
no_unit_selected: "لم يتم اختيار وحدة تعلم",
|
||
|
||
mc_title: "اختبار متعدد الخيارات",
|
||
mc_ready: "جاهز",
|
||
mc_generating: "جاري الإنشاء...",
|
||
mc_done: "تم",
|
||
mc_error: "خطأ",
|
||
mc_desc: "ينشئ أسئلة اختيار من متعدد تناسب مستوى الصعوبة الأصلي (مثل الصف 7).",
|
||
mc_generate: "إنشاء أسئلة",
|
||
mc_show: "عرض الأسئلة",
|
||
mc_quiz_title: "اختبار متعدد الخيارات",
|
||
mc_evaluate: "تقييم",
|
||
mc_correct: "صحيح!",
|
||
mc_incorrect: "للأسف خطأ.",
|
||
mc_not_answered: "لم تتم الإجابة. الإجابة الصحيحة:",
|
||
mc_result: "من",
|
||
mc_result_correct: "صحيح",
|
||
mc_percent: "صحيح",
|
||
mc_no_questions: "لم يتم إنشاء أسئلة بعد لورقة العمل هذه.",
|
||
mc_print: "طباعة",
|
||
mc_print_with_answers: "طباعة مع الإجابات؟",
|
||
|
||
cloze_title: "ملء الفراغات",
|
||
cloze_desc: "ينشئ نصوصاً بفراغات متعددة في كل جملة. يشمل الترجمة للوالدين.",
|
||
cloze_translation: "الترجمة:",
|
||
cloze_generate: "إنشاء نص الفراغات",
|
||
cloze_start: "بدء التمرين",
|
||
cloze_exercise_title: "تمرين ملء الفراغات",
|
||
cloze_instruction: "املأ الفراغات وانقر على 'تحقق'.",
|
||
cloze_check: "تحقق",
|
||
cloze_show_answers: "عرض الإجابات",
|
||
cloze_no_texts: "لم يتم إنشاء نصوص فراغات بعد لورقة العمل هذه.",
|
||
cloze_sentences: "جمل",
|
||
cloze_gaps: "فراغات",
|
||
cloze_gaps_total: "إجمالي الفراغات",
|
||
cloze_with_gaps: "(مع فراغات)",
|
||
cloze_print: "طباعة",
|
||
cloze_print_with_answers: "طباعة مع الإجابات؟",
|
||
|
||
qa_title: "ورقة الأسئلة والأجوبة",
|
||
qa_desc: "أزواج أسئلة وأجوبة مع نظام صندوق لايتنر. التكرار حسب الصعوبة.",
|
||
qa_generate: "إنشاء س&ج",
|
||
qa_learn: "بدء التعلم",
|
||
qa_print: "طباعة",
|
||
qa_no_questions: "لم يتم إنشاء س&ج بعد لورقة العمل هذه.",
|
||
qa_box_new: "جديد",
|
||
qa_box_learning: "قيد التعلم",
|
||
qa_box_mastered: "متقن",
|
||
qa_show_answer: "عرض الإجابة",
|
||
qa_your_answer: "إجابتك",
|
||
qa_type_answer: "اكتب إجابتك هنا...",
|
||
qa_check_answer: "تحقق من الإجابة",
|
||
qa_correct_answer: "الإجابة الصحيحة",
|
||
qa_self_evaluate: "هل كانت إجابتك صحيحة؟",
|
||
qa_no_answer: "(لم يتم إدخال إجابة)",
|
||
qa_correct: "صحيح",
|
||
qa_incorrect: "خطأ",
|
||
qa_key_terms: "المصطلحات الرئيسية",
|
||
qa_session_correct: "صحيح",
|
||
qa_session_incorrect: "خطأ",
|
||
qa_session_complete: "اكتملت جولة التعلم!",
|
||
qa_result_correct: "صحيح",
|
||
qa_restart: "تعلم مرة أخرى",
|
||
qa_print_with_answers: "طباعة مع الإجابات؟",
|
||
question: "سؤال",
|
||
answer: "إجابة",
|
||
status_generating_qa: "جاري إنشاء س&ج…",
|
||
status_qa_generated: "تم إنشاء س&ج",
|
||
|
||
close: "إغلاق",
|
||
subject: "المادة",
|
||
grade: "المستوى",
|
||
questions: "أسئلة",
|
||
worksheet: "ورقة العمل",
|
||
loading: "جاري التحميل...",
|
||
error: "خطأ",
|
||
success: "نجاح",
|
||
|
||
imprint: "البصمة",
|
||
privacy: "الخصوصية",
|
||
contact: "اتصل بنا",
|
||
|
||
status_ready: "جاهز",
|
||
status_processing: "جاري المعالجة...",
|
||
status_generating_mc: "جاري إنشاء الأسئلة…",
|
||
status_generating_cloze: "جاري إنشاء نصوص الفراغات…",
|
||
status_please_wait: "يرجى الانتظار، الذكاء الاصطناعي يعمل.",
|
||
status_mc_generated: "تم إنشاء الأسئلة",
|
||
status_cloze_generated: "تم إنشاء نصوص الفراغات",
|
||
status_files_created: "ملفات تم إنشاؤها",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "ملصق خريطة ذهنية",
|
||
mindmap_desc: "ينشئ خريطة ذهنية مناسبة للأطفال مع الموضوع الرئيسي في المنتصف وجميع المصطلحات في فئات ملونة.",
|
||
mindmap_generate: "إنشاء خريطة ذهنية",
|
||
mindmap_show: "عرض",
|
||
mindmap_print_a3: "طباعة A3",
|
||
generating_mindmap: "جاري إنشاء الخريطة الذهنية...",
|
||
mindmap_generated: "تم إنشاء الخريطة الذهنية!",
|
||
no_analysis: "لا يوجد تحليل",
|
||
analyze_first: "يرجى التحليل أولاً (انقر على معالجة)",
|
||
categories: "الفئات",
|
||
terms: "المصطلحات",
|
||
},
|
||
|
||
ru: {
|
||
brand_sub: "Студия",
|
||
nav_compare: "Рабочие листы",
|
||
nav_tiles: "Учебные карточки",
|
||
login: "Вход / Регистрация",
|
||
mvp_local: "MVP · Локально на вашем Mac",
|
||
|
||
sidebar_areas: "Разделы",
|
||
sidebar_studio: "Студия рабочих листов",
|
||
sidebar_active: "активно",
|
||
sidebar_parents: "Канал для родителей",
|
||
sidebar_soon: "скоро",
|
||
sidebar_correction: "Проверка / Оценки",
|
||
sidebar_units: "Учебные блоки (локально)",
|
||
input_student: "Ученик",
|
||
input_subject: "Предмет",
|
||
input_grade: "Класс (напр. 7а)",
|
||
input_unit_title: "Учебный блок / Тема",
|
||
btn_create: "Создать",
|
||
btn_add_current: "Добавить текущий лист",
|
||
btn_filter_unit: "Только блок",
|
||
btn_filter_all: "Все файлы",
|
||
|
||
uploaded_worksheets: "Загруженные рабочие листы",
|
||
files: "файлов",
|
||
btn_upload: "Загрузить",
|
||
btn_delete: "Удалить",
|
||
original_scan: "Оригинальный скан",
|
||
cleaned_version: "Очищено (рукопись удалена)",
|
||
no_cleaned: "Очищенная версия пока недоступна.",
|
||
process_hint: "Нажмите 'Обработать' для анализа и очистки листа.",
|
||
worksheet_print: "Печать",
|
||
worksheet_no_data: "Нет данных рабочего листа.",
|
||
btn_full_process: "Обработать (Анализ + Очистка + HTML)",
|
||
btn_original_generate: "Только оригинальный HTML",
|
||
|
||
learning_unit: "Учебный блок",
|
||
no_unit_selected: "Блок не выбран",
|
||
|
||
mc_title: "Тест с выбором ответа",
|
||
mc_ready: "Готово",
|
||
mc_generating: "Создается...",
|
||
mc_done: "Готово",
|
||
mc_error: "Ошибка",
|
||
mc_desc: "Создает вопросы с выбором ответа соответствующей сложности (напр. 7 класс).",
|
||
mc_generate: "Создать тест",
|
||
mc_show: "Показать вопросы",
|
||
mc_quiz_title: "Тест с выбором ответа",
|
||
mc_evaluate: "Оценить",
|
||
mc_correct: "Правильно!",
|
||
mc_incorrect: "К сожалению, неверно.",
|
||
mc_not_answered: "Нет ответа. Правильный ответ:",
|
||
mc_result: "из",
|
||
mc_result_correct: "правильно",
|
||
mc_percent: "верно",
|
||
mc_no_questions: "Вопросы для этого листа еще не созданы.",
|
||
mc_print: "Печать",
|
||
mc_print_with_answers: "Печатать с ответами?",
|
||
|
||
cloze_title: "Текст с пропусками",
|
||
cloze_desc: "Создает тексты с несколькими пропусками в каждом предложении. Включая перевод для родителей.",
|
||
cloze_translation: "Перевод:",
|
||
cloze_generate: "Создать текст",
|
||
cloze_start: "Начать упражнение",
|
||
cloze_exercise_title: "Упражнение с пропусками",
|
||
cloze_instruction: "Заполните пропуски и нажмите 'Проверить'.",
|
||
cloze_check: "Проверить",
|
||
cloze_show_answers: "Показать ответы",
|
||
cloze_no_texts: "Тексты для этого листа еще не созданы.",
|
||
cloze_sentences: "предложений",
|
||
cloze_gaps: "пропусков",
|
||
cloze_gaps_total: "Всего пропусков",
|
||
cloze_with_gaps: "(с пропусками)",
|
||
cloze_print: "Печать",
|
||
cloze_print_with_answers: "Печатать с ответами?",
|
||
|
||
qa_title: "Лист вопросов и ответов",
|
||
qa_desc: "Пары вопрос-ответ с системой Лейтнера. Повторение по уровню сложности.",
|
||
qa_generate: "Создать В&О",
|
||
qa_learn: "Начать обучение",
|
||
qa_print: "Печать",
|
||
qa_no_questions: "В&О для этого листа еще не созданы.",
|
||
qa_box_new: "Новый",
|
||
qa_box_learning: "Изучается",
|
||
qa_box_mastered: "Освоено",
|
||
qa_show_answer: "Показать ответ",
|
||
qa_your_answer: "Твой ответ",
|
||
qa_type_answer: "Напиши свой ответ здесь...",
|
||
qa_check_answer: "Проверить ответ",
|
||
qa_correct_answer: "Правильный ответ",
|
||
qa_self_evaluate: "Твой ответ был правильным?",
|
||
qa_no_answer: "(ответ не введён)",
|
||
qa_correct: "Правильно",
|
||
qa_incorrect: "Неправильно",
|
||
qa_key_terms: "Ключевые термины",
|
||
qa_session_correct: "Правильно",
|
||
qa_session_incorrect: "Неправильно",
|
||
qa_session_complete: "Раунд обучения завершен!",
|
||
qa_result_correct: "правильно",
|
||
qa_restart: "Учить снова",
|
||
qa_print_with_answers: "Печатать с ответами?",
|
||
question: "Вопрос",
|
||
answer: "Ответ",
|
||
status_generating_qa: "Создание В&О…",
|
||
status_qa_generated: "В&О созданы",
|
||
|
||
close: "Закрыть",
|
||
subject: "Предмет",
|
||
grade: "Уровень",
|
||
questions: "вопросов",
|
||
worksheet: "Рабочий лист",
|
||
loading: "Загрузка...",
|
||
error: "Ошибка",
|
||
success: "Успешно",
|
||
|
||
imprint: "Импрессум",
|
||
privacy: "Конфиденциальность",
|
||
contact: "Контакт",
|
||
|
||
status_ready: "Готово",
|
||
status_processing: "Обработка...",
|
||
status_generating_mc: "Создание вопросов…",
|
||
status_generating_cloze: "Создание текстов…",
|
||
status_please_wait: "Пожалуйста, подождите, ИИ работает.",
|
||
status_mc_generated: "Вопросы созданы",
|
||
status_cloze_generated: "Тексты созданы",
|
||
status_files_created: "файлов создано",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Плакат Майнд-карта",
|
||
mindmap_desc: "Создает детскую ментальную карту с главной темой в центре и всеми терминами в цветных категориях.",
|
||
mindmap_generate: "Создать карту",
|
||
mindmap_show: "Просмотр",
|
||
mindmap_print_a3: "Печать A3",
|
||
generating_mindmap: "Создание карты...",
|
||
mindmap_generated: "Карта создана!",
|
||
no_analysis: "Нет анализа",
|
||
analyze_first: "Сначала выполните анализ (нажмите Обработать)",
|
||
categories: "Категории",
|
||
terms: "Термины",
|
||
},
|
||
|
||
uk: {
|
||
brand_sub: "Студія",
|
||
nav_compare: "Робочі аркуші",
|
||
nav_tiles: "Навчальні картки",
|
||
login: "Вхід / Реєстрація",
|
||
mvp_local: "MVP · Локально на вашому Mac",
|
||
|
||
sidebar_areas: "Розділи",
|
||
sidebar_studio: "Студія робочих аркушів",
|
||
sidebar_active: "активно",
|
||
sidebar_parents: "Канал для батьків",
|
||
sidebar_soon: "незабаром",
|
||
sidebar_correction: "Перевірка / Оцінки",
|
||
sidebar_units: "Навчальні блоки (локально)",
|
||
input_student: "Учень",
|
||
input_subject: "Предмет",
|
||
input_grade: "Клас (напр. 7а)",
|
||
input_unit_title: "Навчальний блок / Тема",
|
||
btn_create: "Створити",
|
||
btn_add_current: "Додати поточний аркуш",
|
||
btn_filter_unit: "Лише блок",
|
||
btn_filter_all: "Усі файли",
|
||
|
||
uploaded_worksheets: "Завантажені робочі аркуші",
|
||
files: "файлів",
|
||
btn_upload: "Завантажити",
|
||
btn_delete: "Видалити",
|
||
original_scan: "Оригінальний скан",
|
||
cleaned_version: "Очищено (рукопис видалено)",
|
||
no_cleaned: "Очищена версія ще недоступна.",
|
||
process_hint: "Натисніть 'Обробити' для аналізу та очищення аркуша.",
|
||
worksheet_print: "Друк",
|
||
worksheet_no_data: "Немає даних робочого аркуша.",
|
||
btn_full_process: "Обробити (Аналіз + Очищення + HTML)",
|
||
btn_original_generate: "Лише оригінальний HTML",
|
||
|
||
learning_unit: "Навчальний блок",
|
||
no_unit_selected: "Блок не вибрано",
|
||
|
||
mc_title: "Тест з вибором відповіді",
|
||
mc_ready: "Готово",
|
||
mc_generating: "Створюється...",
|
||
mc_done: "Готово",
|
||
mc_error: "Помилка",
|
||
mc_desc: "Створює питання з вибором відповіді відповідної складності (напр. 7 клас).",
|
||
mc_generate: "Створити тест",
|
||
mc_show: "Показати питання",
|
||
mc_quiz_title: "Тест з вибором відповіді",
|
||
mc_evaluate: "Оцінити",
|
||
mc_correct: "Правильно!",
|
||
mc_incorrect: "На жаль, неправильно.",
|
||
mc_not_answered: "Немає відповіді. Правильна відповідь:",
|
||
mc_result: "з",
|
||
mc_result_correct: "правильно",
|
||
mc_percent: "вірно",
|
||
mc_no_questions: "Питання для цього аркуша ще не створені.",
|
||
mc_print: "Друк",
|
||
mc_print_with_answers: "Друкувати з відповідями?",
|
||
|
||
cloze_title: "Текст з пропусками",
|
||
cloze_desc: "Створює тексти з кількома пропусками в кожному реченні. Включаючи переклад для батьків.",
|
||
cloze_translation: "Переклад:",
|
||
cloze_generate: "Створити текст",
|
||
cloze_start: "Почати вправу",
|
||
cloze_exercise_title: "Вправа з пропусками",
|
||
cloze_instruction: "Заповніть пропуски та натисніть 'Перевірити'.",
|
||
cloze_check: "Перевірити",
|
||
cloze_show_answers: "Показати відповіді",
|
||
cloze_no_texts: "Тексти для цього аркуша ще не створені.",
|
||
cloze_sentences: "речень",
|
||
cloze_gaps: "пропусків",
|
||
cloze_gaps_total: "Всього пропусків",
|
||
cloze_with_gaps: "(з пропусками)",
|
||
cloze_print: "Друк",
|
||
cloze_print_with_answers: "Друкувати з відповідями?",
|
||
|
||
qa_title: "Аркуш питань і відповідей",
|
||
qa_desc: "Пари питання-відповідь з системою Лейтнера. Повторення за рівнем складності.",
|
||
qa_generate: "Створити П&В",
|
||
qa_learn: "Почати навчання",
|
||
qa_print: "Друк",
|
||
qa_no_questions: "П&В для цього аркуша ще не створені.",
|
||
qa_box_new: "Новий",
|
||
qa_box_learning: "Вивчається",
|
||
qa_box_mastered: "Засвоєно",
|
||
qa_show_answer: "Показати відповідь",
|
||
qa_your_answer: "Твоя відповідь",
|
||
qa_type_answer: "Напиши свою відповідь тут...",
|
||
qa_check_answer: "Перевірити відповідь",
|
||
qa_correct_answer: "Правильна відповідь",
|
||
qa_self_evaluate: "Твоя відповідь була правильною?",
|
||
qa_no_answer: "(відповідь не введена)",
|
||
qa_correct: "Правильно",
|
||
qa_incorrect: "Неправильно",
|
||
qa_key_terms: "Ключові терміни",
|
||
qa_session_correct: "Правильно",
|
||
qa_session_incorrect: "Неправильно",
|
||
qa_session_complete: "Раунд навчання завершено!",
|
||
qa_result_correct: "правильно",
|
||
qa_restart: "Вчити знову",
|
||
qa_print_with_answers: "Друкувати з відповідями?",
|
||
question: "Питання",
|
||
answer: "Відповідь",
|
||
status_generating_qa: "Створення П&В…",
|
||
status_qa_generated: "П&В створені",
|
||
|
||
close: "Закрити",
|
||
subject: "Предмет",
|
||
grade: "Рівень",
|
||
questions: "питань",
|
||
worksheet: "Робочий аркуш",
|
||
loading: "Завантаження...",
|
||
error: "Помилка",
|
||
success: "Успішно",
|
||
|
||
imprint: "Імпресум",
|
||
privacy: "Конфіденційність",
|
||
contact: "Контакт",
|
||
|
||
status_ready: "Готово",
|
||
status_processing: "Обробка...",
|
||
status_generating_mc: "Створення питань…",
|
||
status_generating_cloze: "Створення текстів…",
|
||
status_please_wait: "Будь ласка, зачекайте, ШІ працює.",
|
||
status_mc_generated: "Питання створені",
|
||
status_cloze_generated: "Тексти створені",
|
||
status_files_created: "файлів створено",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Плакат Інтелект-карта",
|
||
mindmap_desc: "Створює дитячу інтелект-карту з головною темою в центрі та всіма термінами в кольорових категоріях.",
|
||
mindmap_generate: "Створити карту",
|
||
mindmap_show: "Переглянути",
|
||
mindmap_print_a3: "Друк A3",
|
||
generating_mindmap: "Створення карти...",
|
||
mindmap_generated: "Карту створено!",
|
||
no_analysis: "Немає аналізу",
|
||
analyze_first: "Спочатку виконайте аналіз (натисніть Обробити)",
|
||
categories: "Категорії",
|
||
terms: "Терміни",
|
||
},
|
||
|
||
pl: {
|
||
brand_sub: "Studio",
|
||
nav_compare: "Karty pracy",
|
||
nav_tiles: "Karty nauki",
|
||
login: "Logowanie / Rejestracja",
|
||
mvp_local: "MVP · Lokalnie na Twoim Mac",
|
||
|
||
sidebar_areas: "Sekcje",
|
||
sidebar_studio: "Studio kart pracy",
|
||
sidebar_active: "aktywne",
|
||
sidebar_parents: "Kanał dla rodziców",
|
||
sidebar_soon: "wkrótce",
|
||
sidebar_correction: "Korekta / Oceny",
|
||
sidebar_units: "Jednostki nauki (lokalnie)",
|
||
input_student: "Uczeń",
|
||
input_subject: "Przedmiot",
|
||
input_grade: "Klasa (np. 7a)",
|
||
input_unit_title: "Jednostka nauki / Temat",
|
||
btn_create: "Utwórz",
|
||
btn_add_current: "Dodaj bieżącą kartę",
|
||
btn_filter_unit: "Tylko jednostka",
|
||
btn_filter_all: "Wszystkie pliki",
|
||
|
||
uploaded_worksheets: "Przesłane karty pracy",
|
||
files: "plików",
|
||
btn_upload: "Prześlij",
|
||
btn_delete: "Usuń",
|
||
original_scan: "Oryginalny skan",
|
||
cleaned_version: "Oczyszczone (pismo ręczne usunięte)",
|
||
no_cleaned: "Oczyszczona wersja jeszcze niedostępna.",
|
||
process_hint: "Kliknij 'Przetwórz', aby przeanalizować i oczyścić kartę.",
|
||
worksheet_print: "Drukuj",
|
||
worksheet_no_data: "Brak danych arkusza.",
|
||
btn_full_process: "Przetwórz (Analiza + Czyszczenie + HTML)",
|
||
btn_original_generate: "Tylko oryginalny HTML",
|
||
|
||
learning_unit: "Jednostka nauki",
|
||
no_unit_selected: "Nie wybrano jednostki",
|
||
|
||
mc_title: "Test wielokrotnego wyboru",
|
||
mc_ready: "Gotowe",
|
||
mc_generating: "Tworzenie...",
|
||
mc_done: "Gotowe",
|
||
mc_error: "Błąd",
|
||
mc_desc: "Tworzy pytania wielokrotnego wyboru o odpowiednim poziomie trudności (np. klasa 7).",
|
||
mc_generate: "Utwórz test",
|
||
mc_show: "Pokaż pytania",
|
||
mc_quiz_title: "Test wielokrotnego wyboru",
|
||
mc_evaluate: "Oceń",
|
||
mc_correct: "Dobrze!",
|
||
mc_incorrect: "Niestety źle.",
|
||
mc_not_answered: "Brak odpowiedzi. Poprawna odpowiedź:",
|
||
mc_result: "z",
|
||
mc_result_correct: "poprawnie",
|
||
mc_percent: "poprawnie",
|
||
mc_no_questions: "Pytania dla tej karty jeszcze nie zostały utworzone.",
|
||
mc_print: "Drukuj",
|
||
mc_print_with_answers: "Drukować z odpowiedziami?",
|
||
|
||
cloze_title: "Tekst z lukami",
|
||
cloze_desc: "Tworzy teksty z wieloma lukami w każdym zdaniu. W tym tłumaczenie dla rodziców.",
|
||
cloze_translation: "Tłumaczenie:",
|
||
cloze_generate: "Utwórz tekst",
|
||
cloze_start: "Rozpocznij ćwiczenie",
|
||
cloze_exercise_title: "Ćwiczenie z lukami",
|
||
cloze_instruction: "Wypełnij luki i kliknij 'Sprawdź'.",
|
||
cloze_check: "Sprawdź",
|
||
cloze_show_answers: "Pokaż odpowiedzi",
|
||
cloze_no_texts: "Teksty dla tej karty jeszcze nie zostały utworzone.",
|
||
cloze_sentences: "zdań",
|
||
cloze_gaps: "luk",
|
||
cloze_gaps_total: "Łącznie luk",
|
||
cloze_with_gaps: "(z lukami)",
|
||
cloze_print: "Drukuj",
|
||
cloze_print_with_answers: "Drukować z odpowiedziami?",
|
||
|
||
qa_title: "Arkusz pytań i odpowiedzi",
|
||
qa_desc: "Pary pytanie-odpowiedź z systemem Leitnera. Powtórki według poziomu trudności.",
|
||
qa_generate: "Utwórz P&O",
|
||
qa_learn: "Rozpocznij naukę",
|
||
qa_print: "Drukuj",
|
||
qa_no_questions: "P&O dla tej karty jeszcze nie zostały utworzone.",
|
||
qa_box_new: "Nowy",
|
||
qa_box_learning: "W nauce",
|
||
qa_box_mastered: "Opanowane",
|
||
qa_show_answer: "Pokaż odpowiedź",
|
||
qa_your_answer: "Twoja odpowiedź",
|
||
qa_type_answer: "Napisz swoją odpowiedź tutaj...",
|
||
qa_check_answer: "Sprawdź odpowiedź",
|
||
qa_correct_answer: "Prawidłowa odpowiedź",
|
||
qa_self_evaluate: "Czy twoja odpowiedź była poprawna?",
|
||
qa_no_answer: "(nie wprowadzono odpowiedzi)",
|
||
qa_correct: "Dobrze",
|
||
qa_incorrect: "Źle",
|
||
qa_key_terms: "Kluczowe pojęcia",
|
||
qa_session_correct: "Dobrze",
|
||
qa_session_incorrect: "Źle",
|
||
qa_session_complete: "Runda nauki zakończona!",
|
||
qa_result_correct: "poprawnie",
|
||
qa_restart: "Ucz się ponownie",
|
||
qa_print_with_answers: "Drukować z odpowiedziami?",
|
||
question: "Pytanie",
|
||
answer: "Odpowiedź",
|
||
status_generating_qa: "Tworzenie P&O…",
|
||
status_qa_generated: "P&O utworzone",
|
||
|
||
close: "Zamknij",
|
||
subject: "Przedmiot",
|
||
grade: "Poziom",
|
||
questions: "pytań",
|
||
worksheet: "Karta pracy",
|
||
loading: "Ładowanie...",
|
||
error: "Błąd",
|
||
success: "Sukces",
|
||
|
||
imprint: "Impressum",
|
||
privacy: "Prywatność",
|
||
contact: "Kontakt",
|
||
|
||
status_ready: "Gotowe",
|
||
status_processing: "Przetwarzanie...",
|
||
status_generating_mc: "Tworzenie pytań…",
|
||
status_generating_cloze: "Tworzenie tekstów…",
|
||
status_please_wait: "Proszę czekać, AI pracuje.",
|
||
status_mc_generated: "Pytania utworzone",
|
||
status_cloze_generated: "Teksty utworzone",
|
||
status_files_created: "plików utworzono",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Plakat Mapa myśli",
|
||
mindmap_desc: "Tworzy przyjazną dla dzieci mapę myśli z głównym tematem w centrum i wszystkimi terminami w kolorowych kategoriach.",
|
||
mindmap_generate: "Utwórz mapę",
|
||
mindmap_show: "Podgląd",
|
||
mindmap_print_a3: "Drukuj A3",
|
||
generating_mindmap: "Tworzenie mapy...",
|
||
mindmap_generated: "Mapa utworzona!",
|
||
no_analysis: "Brak analizy",
|
||
analyze_first: "Najpierw wykonaj analizę (kliknij Przetwórz)",
|
||
categories: "Kategorie",
|
||
terms: "Terminy",
|
||
},
|
||
|
||
en: {
|
||
brand_sub: "Studio",
|
||
nav_compare: "Worksheets",
|
||
nav_tiles: "Learning Tiles",
|
||
login: "Login / Sign Up",
|
||
mvp_local: "MVP · Local on your Mac",
|
||
|
||
sidebar_areas: "Areas",
|
||
sidebar_studio: "Worksheet Studio",
|
||
sidebar_active: "active",
|
||
sidebar_parents: "Parents Channel",
|
||
sidebar_soon: "coming soon",
|
||
sidebar_correction: "Correction / Grades",
|
||
sidebar_units: "Learning Units (local)",
|
||
input_student: "Student",
|
||
input_subject: "Subject",
|
||
input_grade: "Grade (e.g. 7a)",
|
||
input_unit_title: "Learning Unit / Topic",
|
||
btn_create: "Create",
|
||
btn_add_current: "Add current worksheet",
|
||
btn_filter_unit: "Unit only",
|
||
btn_filter_all: "All files",
|
||
|
||
uploaded_worksheets: "Uploaded Worksheets",
|
||
files: "files",
|
||
btn_upload: "Upload",
|
||
btn_delete: "Delete",
|
||
original_scan: "Original Scan",
|
||
cleaned_version: "Cleaned (handwriting removed)",
|
||
no_cleaned: "No cleaned version available yet.",
|
||
process_hint: "Click 'Process' to analyze and clean the worksheet.",
|
||
worksheet_print: "Print",
|
||
worksheet_no_data: "No worksheet data available.",
|
||
btn_full_process: "Process (Analysis + Cleaning + HTML)",
|
||
btn_original_generate: "Generate Original HTML Only",
|
||
|
||
learning_unit: "Learning Unit",
|
||
no_unit_selected: "No unit selected",
|
||
|
||
mc_title: "Multiple Choice Test",
|
||
mc_ready: "Ready",
|
||
mc_generating: "Generating...",
|
||
mc_done: "Done",
|
||
mc_error: "Error",
|
||
mc_desc: "Creates multiple choice questions matching the original difficulty level (e.g. Grade 7).",
|
||
mc_generate: "Generate MC",
|
||
mc_show: "Show Questions",
|
||
mc_quiz_title: "Multiple Choice Quiz",
|
||
mc_evaluate: "Evaluate",
|
||
mc_correct: "Correct!",
|
||
mc_incorrect: "Unfortunately wrong.",
|
||
mc_not_answered: "Not answered. Correct answer:",
|
||
mc_result: "of",
|
||
mc_result_correct: "correct",
|
||
mc_percent: "correct",
|
||
mc_no_questions: "No MC questions generated yet for this worksheet.",
|
||
mc_print: "Print",
|
||
mc_print_with_answers: "Print with answers?",
|
||
|
||
cloze_title: "Fill in the Blanks",
|
||
cloze_desc: "Creates texts with multiple meaningful gaps per sentence. Including translation for parents.",
|
||
cloze_translation: "Translation:",
|
||
cloze_generate: "Generate Cloze Text",
|
||
cloze_start: "Start Exercise",
|
||
cloze_exercise_title: "Fill in the Blanks Exercise",
|
||
cloze_instruction: "Fill in the blanks and click 'Check'.",
|
||
cloze_check: "Check",
|
||
cloze_show_answers: "Show Answers",
|
||
cloze_no_texts: "No cloze texts generated yet for this worksheet.",
|
||
cloze_sentences: "sentences",
|
||
cloze_gaps: "gaps",
|
||
cloze_gaps_total: "Total gaps",
|
||
cloze_with_gaps: "(with gaps)",
|
||
cloze_print: "Print",
|
||
cloze_print_with_answers: "Print with answers?",
|
||
|
||
qa_title: "Question & Answer Sheet",
|
||
qa_desc: "Q&A pairs with Leitner box system. Spaced repetition by difficulty level.",
|
||
qa_generate: "Generate Q&A",
|
||
qa_learn: "Start Learning",
|
||
qa_print: "Print",
|
||
qa_no_questions: "No Q&A generated yet for this worksheet.",
|
||
qa_box_new: "New",
|
||
qa_box_learning: "Learning",
|
||
qa_box_mastered: "Mastered",
|
||
qa_show_answer: "Show Answer",
|
||
qa_your_answer: "Your Answer",
|
||
qa_type_answer: "Write your answer here...",
|
||
qa_check_answer: "Check Answer",
|
||
qa_correct_answer: "Correct Answer",
|
||
qa_self_evaluate: "Was your answer correct?",
|
||
qa_no_answer: "(no answer entered)",
|
||
qa_correct: "Correct",
|
||
qa_incorrect: "Incorrect",
|
||
qa_key_terms: "Key Terms",
|
||
qa_session_correct: "Correct",
|
||
qa_session_incorrect: "Incorrect",
|
||
qa_session_complete: "Learning session complete!",
|
||
qa_result_correct: "correct",
|
||
qa_restart: "Learn Again",
|
||
qa_print_with_answers: "Print with answers?",
|
||
question: "Question",
|
||
answer: "Answer",
|
||
status_generating_qa: "Generating Q&A…",
|
||
status_qa_generated: "Q&A generated",
|
||
|
||
close: "Close",
|
||
subject: "Subject",
|
||
grade: "Level",
|
||
questions: "questions",
|
||
worksheet: "Worksheet",
|
||
loading: "Loading...",
|
||
error: "Error",
|
||
success: "Success",
|
||
|
||
imprint: "Imprint",
|
||
privacy: "Privacy",
|
||
contact: "Contact",
|
||
|
||
status_ready: "Ready",
|
||
status_processing: "Processing...",
|
||
status_generating_mc: "Generating MC questions…",
|
||
status_generating_cloze: "Generating cloze texts…",
|
||
status_please_wait: "Please wait, AI is working.",
|
||
status_mc_generated: "MC questions generated",
|
||
status_cloze_generated: "Cloze texts generated",
|
||
status_files_created: "files created",
|
||
|
||
// Mindmap Tile
|
||
mindmap_title: "Mindmap Learning Poster",
|
||
mindmap_desc: "Creates a child-friendly mindmap with the main topic in the center and all terms in colorful categories.",
|
||
mindmap_generate: "Create Mindmap",
|
||
mindmap_show: "View",
|
||
mindmap_print_a3: "Print A3",
|
||
generating_mindmap: "Creating mindmap...",
|
||
mindmap_generated: "Mindmap created!",
|
||
no_analysis: "No analysis",
|
||
analyze_first: "Please analyze first (click Process)",
|
||
categories: "Categories",
|
||
terms: "Terms",
|
||
}
|
||
};
|
||
|
||
// Aktuelle Sprache (aus localStorage oder Browser-Sprache)
|
||
let currentLang = localStorage.getItem('bp_language') || 'de';
|
||
|
||
// RTL-Sprachen
|
||
const rtlLanguages = ['ar'];
|
||
|
||
// Übersetzungsfunktion
|
||
function t(key) {
|
||
const lang = translations[currentLang] || translations['de'];
|
||
return lang[key] || translations['de'][key] || key;
|
||
}
|
||
|
||
// Sprache anwenden
|
||
function applyLanguage(lang) {
|
||
currentLang = lang;
|
||
localStorage.setItem('bp_language', lang);
|
||
|
||
// RTL für Arabisch
|
||
if (rtlLanguages.includes(lang)) {
|
||
document.documentElement.setAttribute('dir', 'rtl');
|
||
} else {
|
||
document.documentElement.setAttribute('dir', 'ltr');
|
||
}
|
||
|
||
// Alle Elemente mit data-i18n aktualisieren
|
||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||
const key = el.getAttribute('data-i18n');
|
||
if (el.tagName === 'INPUT' && el.hasAttribute('placeholder')) {
|
||
el.placeholder = t(key);
|
||
} else {
|
||
el.textContent = t(key);
|
||
}
|
||
});
|
||
|
||
// Spezielle Elemente manuell aktualisieren
|
||
updateUITexts();
|
||
}
|
||
|
||
// UI-Texte aktualisieren
|
||
function updateUITexts() {
|
||
// Header
|
||
const brandSub = document.querySelector('.brand-text-sub');
|
||
if (brandSub) brandSub.textContent = t('brand_sub');
|
||
|
||
// Navigation (Text entfernt - wird nicht mehr angezeigt)
|
||
// const navItems = document.querySelectorAll('.top-nav-item');
|
||
// if (navItems[0]) navItems[0].textContent = t('nav_compare');
|
||
// if (navItems[1]) navItems[1].textContent = t('nav_tiles');
|
||
|
||
// Sidebar Bereiche
|
||
const sidebarTitles = document.querySelectorAll('.sidebar-section-title');
|
||
if (sidebarTitles[0]) sidebarTitles[0].textContent = t('sidebar_areas');
|
||
if (sidebarTitles[1]) sidebarTitles[1].textContent = t('sidebar_units');
|
||
|
||
const sidebarLabels = document.querySelectorAll('.sidebar-item-label span');
|
||
if (sidebarLabels[0]) sidebarLabels[0].textContent = t('sidebar_studio');
|
||
if (sidebarLabels[1]) sidebarLabels[1].textContent = t('sidebar_parents');
|
||
if (sidebarLabels[2]) sidebarLabels[2].textContent = t('sidebar_correction');
|
||
|
||
const sidebarBadges = document.querySelectorAll('.sidebar-item-badge');
|
||
if (sidebarBadges[0]) sidebarBadges[0].textContent = t('sidebar_active');
|
||
if (sidebarBadges[1]) sidebarBadges[1].textContent = t('sidebar_soon');
|
||
if (sidebarBadges[2]) sidebarBadges[2].textContent = t('sidebar_soon');
|
||
|
||
// Input Placeholders
|
||
if (unitStudentInput) unitStudentInput.placeholder = t('input_student');
|
||
if (unitSubjectInput) unitSubjectInput.placeholder = t('input_subject');
|
||
if (unitGradeInput) unitGradeInput.placeholder = t('input_grade');
|
||
if (unitTitleInput) unitTitleInput.placeholder = t('input_unit_title');
|
||
|
||
// Buttons
|
||
if (btnAddUnit) btnAddUnit.textContent = t('btn_create');
|
||
if (btnAttachCurrentToLu) btnAttachCurrentToLu.textContent = t('btn_add_current');
|
||
if (btnToggleFilter) {
|
||
btnToggleFilter.textContent = showOnlyUnitFiles ? t('btn_filter_unit') : t('btn_filter_all');
|
||
}
|
||
if (btnFullProcess) btnFullProcess.textContent = t('btn_full_process');
|
||
if (btnOriginalGenerate) btnOriginalGenerate.textContent = t('btn_original_generate');
|
||
|
||
// Titles
|
||
const uploadedTitle = document.querySelector('.panel-left > div:first-child');
|
||
if (uploadedTitle) {
|
||
uploadedTitle.innerHTML = '<span data-i18n="uploaded_worksheets">' + t('uploaded_worksheets') + '</span> <span class="pill" id="eingang-count">0 ' + t('files') + '</span>';
|
||
}
|
||
|
||
// MC Tile
|
||
const mcTitle = document.querySelector('[data-tile="mc"] .card-title');
|
||
if (mcTitle) mcTitle.textContent = t('mc_title');
|
||
const mcDesc = document.querySelector('[data-tile="mc"] .card-body > div:first-child');
|
||
if (mcDesc) mcDesc.textContent = t('mc_desc');
|
||
if (btnMcGenerate) btnMcGenerate.textContent = t('mc_generate');
|
||
if (btnMcShow) btnMcShow.textContent = t('mc_show');
|
||
|
||
// Cloze Tile
|
||
const clozeTitle = document.querySelector('[data-tile="cloze"] .card-title');
|
||
if (clozeTitle) clozeTitle.textContent = t('cloze_title');
|
||
const clozeDesc = document.querySelector('[data-tile="cloze"] .card-body > div:first-child');
|
||
if (clozeDesc) clozeDesc.textContent = t('cloze_desc');
|
||
const clozeLabel = document.querySelector('.cloze-language-select label');
|
||
if (clozeLabel) clozeLabel.textContent = t('cloze_translation');
|
||
if (btnClozeGenerate) btnClozeGenerate.textContent = t('cloze_generate');
|
||
if (btnClozeShow) btnClozeShow.textContent = t('cloze_start');
|
||
|
||
// QA Tile
|
||
const qaTitle = document.querySelector('[data-tile="qa"] .card-title');
|
||
if (qaTitle) qaTitle.textContent = t('qa_title');
|
||
const qaDesc = document.querySelector('[data-tile="qa"] .card-body > div:first-child');
|
||
if (qaDesc) qaDesc.textContent = t('qa_desc');
|
||
const qaBadge = document.querySelector('[data-tile="qa"] .card-badge');
|
||
if (qaBadge) qaBadge.textContent = t('qa_soon');
|
||
|
||
// Modal Titles
|
||
const mcModalTitle = document.querySelector('#mc-modal .mc-modal-title');
|
||
if (mcModalTitle) mcModalTitle.textContent = t('mc_quiz_title');
|
||
const clozeModalTitle = document.querySelector('#cloze-modal .mc-modal-title');
|
||
if (clozeModalTitle) clozeModalTitle.textContent = t('cloze_exercise_title');
|
||
|
||
// Close Buttons
|
||
document.querySelectorAll('.mc-modal-close').forEach(btn => {
|
||
btn.textContent = t('close') + ' ✕';
|
||
});
|
||
if (lightboxClose) lightboxClose.textContent = t('close') + ' ✕';
|
||
|
||
// Footer
|
||
const footerLinks = document.querySelectorAll('.footer a');
|
||
if (footerLinks[0]) footerLinks[0].textContent = t('imprint');
|
||
if (footerLinks[1]) footerLinks[1].textContent = t('privacy');
|
||
if (footerLinks[2]) footerLinks[2].textContent = t('contact');
|
||
}
|
||
|
||
const eingangListEl = document.getElementById('eingang-list');
|
||
const eingangCountEl = document.getElementById('eingang-count');
|
||
const previewContainer = document.getElementById('preview-container');
|
||
const fileInput = document.getElementById('file-input');
|
||
const btnUploadInline = document.getElementById('btn-upload-inline');
|
||
const btnFullProcess = document.getElementById('btn-full-process');
|
||
const btnOriginalGenerate = document.getElementById('btn-original-generate');
|
||
const statusBar = document.getElementById('status-bar');
|
||
const statusDot = document.getElementById('status-dot');
|
||
const statusMain = document.getElementById('status-main');
|
||
const statusSub = document.getElementById('status-sub');
|
||
|
||
const panelCompare = document.getElementById('panel-compare');
|
||
const panelTiles = document.getElementById('panel-tiles');
|
||
const pagerPrev = document.getElementById('pager-prev');
|
||
const pagerNext = document.getElementById('pager-next');
|
||
const pagerLabel = document.getElementById('pager-label');
|
||
|
||
const topNavItems = document.querySelectorAll('.top-nav-item');
|
||
|
||
const lightboxEl = document.getElementById('lightbox');
|
||
const lightboxImg = document.getElementById('lightbox-img');
|
||
const lightboxCaption = document.getElementById('lightbox-caption');
|
||
const lightboxClose = document.getElementById('lightbox-close');
|
||
|
||
const unitStudentInput = document.getElementById('unit-student');
|
||
const unitSubjectInput = document.getElementById('unit-subject');
|
||
const unitGradeInput = document.getElementById('unit-grade');
|
||
const unitTitleInput = document.getElementById('unit-title');
|
||
const unitListEl = document.getElementById('unit-list');
|
||
const btnAddUnit = document.getElementById('btn-add-unit');
|
||
const btnAttachCurrentToLu = document.getElementById('btn-attach-current-to-lu');
|
||
const unitHeading1 = document.getElementById('unit-heading-screen1');
|
||
const unitHeading2 = document.getElementById('unit-heading-screen2');
|
||
const btnToggleFilter = document.getElementById('btn-toggle-filter');
|
||
|
||
let currentSelectedFile = null;
|
||
let allEingangFiles = []; // Master-Liste aller Dateien
|
||
let eingangFiles = []; // aktuell gefilterte Ansicht
|
||
let currentIndex = 0;
|
||
let showOnlyUnitFiles = true; // Filter-Modus: true = nur Lerneinheit (Standard), false = alle
|
||
|
||
let allWorksheetPairs = {}; // Master-Mapping original -> { clean_html, clean_image }
|
||
let worksheetPairs = {}; // aktuell gefiltertes Mapping
|
||
|
||
let tileState = {
|
||
mindmap: true,
|
||
qa: true,
|
||
mc: true,
|
||
cloze: true,
|
||
};
|
||
let currentScreen = 1;
|
||
|
||
// Lerneinheiten aus dem Backend
|
||
let units = [];
|
||
let currentUnitId = null;
|
||
|
||
// --- Lightbox / Vollbild ---
|
||
function openLightbox(src, caption) {
|
||
if (!src) return;
|
||
lightboxImg.src = src;
|
||
lightboxCaption.textContent = caption || '';
|
||
lightboxEl.classList.remove('hidden');
|
||
}
|
||
|
||
function closeLightbox() {
|
||
lightboxEl.classList.add('hidden');
|
||
lightboxImg.src = '';
|
||
lightboxCaption.textContent = '';
|
||
}
|
||
|
||
lightboxClose.addEventListener('click', closeLightbox);
|
||
lightboxEl.addEventListener('click', (ev) => {
|
||
if (ev.target === lightboxEl) {
|
||
closeLightbox();
|
||
}
|
||
});
|
||
document.addEventListener('keydown', (ev) => {
|
||
if (ev.key === 'Escape') {
|
||
closeLightbox();
|
||
// Close compare view if open
|
||
const compareView = document.getElementById('version-compare-view');
|
||
if (compareView && compareView.classList.contains('active')) {
|
||
hideCompareView();
|
||
}
|
||
}
|
||
});
|
||
|
||
// --- Status-Balken ---
|
||
function setStatus(main, sub = '', state = 'idle') {
|
||
statusMain.textContent = main;
|
||
statusSub.textContent = sub;
|
||
statusDot.classList.remove('busy', 'error');
|
||
if (state === 'busy') {
|
||
statusDot.classList.add('busy');
|
||
} else if (state === 'error') {
|
||
statusDot.classList.add('error');
|
||
}
|
||
}
|
||
|
||
setStatus('Bereit', 'Lade Arbeitsblätter hoch und starte den Neuaufbau.');
|
||
|
||
// --- API-Helfer ---
|
||
async function apiFetch(url, options = {}) {
|
||
const resp = await fetch(url, options);
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
return resp.json();
|
||
}
|
||
|
||
// --- Dateien laden & rendern ---
|
||
async function loadEingangFiles() {
|
||
try {
|
||
const data = await apiFetch('/api/eingang-dateien');
|
||
allEingangFiles = data.eingang || [];
|
||
eingangFiles = allEingangFiles.slice();
|
||
currentIndex = 0;
|
||
renderEingangList();
|
||
} catch (e) {
|
||
console.error(e);
|
||
setStatus('Fehler beim Laden der Dateien', String(e), 'error');
|
||
}
|
||
}
|
||
|
||
function renderEingangList() {
|
||
eingangListEl.innerHTML = '';
|
||
|
||
if (!eingangFiles.length) {
|
||
const li = document.createElement('li');
|
||
li.className = 'file-empty';
|
||
li.textContent = 'Noch keine Dateien vorhanden.';
|
||
eingangListEl.appendChild(li);
|
||
eingangCountEl.textContent = '0 Dateien';
|
||
return;
|
||
}
|
||
|
||
eingangFiles.forEach((filename, idx) => {
|
||
const li = document.createElement('li');
|
||
li.className = 'file-item';
|
||
if (idx === currentIndex) {
|
||
li.classList.add('active');
|
||
}
|
||
|
||
const nameSpan = document.createElement('span');
|
||
nameSpan.className = 'file-item-name';
|
||
nameSpan.textContent = filename;
|
||
|
||
const actionsSpan = document.createElement('span');
|
||
actionsSpan.style.display = 'flex';
|
||
actionsSpan.style.gap = '6px';
|
||
|
||
// Button: Aus Lerneinheit entfernen
|
||
const removeFromUnitBtn = document.createElement('span');
|
||
removeFromUnitBtn.className = 'file-item-delete';
|
||
removeFromUnitBtn.textContent = '✕';
|
||
removeFromUnitBtn.title = 'Aus Lerneinheit entfernen';
|
||
removeFromUnitBtn.addEventListener('click', (ev) => {
|
||
ev.stopPropagation();
|
||
|
||
if (!currentUnitId) {
|
||
alert('Zum Entfernen bitte zuerst eine Lerneinheit auswählen.');
|
||
return;
|
||
}
|
||
|
||
const ok = confirm('Dieses Arbeitsblatt aus der aktuellen Lerneinheit entfernen? Die Datei selbst bleibt erhalten.');
|
||
if (!ok) return;
|
||
|
||
removeWorksheetFromCurrentUnit(eingangFiles[idx]);
|
||
});
|
||
|
||
// Button: Datei komplett löschen
|
||
const deleteFileBtn = document.createElement('span');
|
||
deleteFileBtn.className = 'file-item-delete';
|
||
deleteFileBtn.textContent = '🗑️';
|
||
deleteFileBtn.title = 'Datei komplett löschen';
|
||
deleteFileBtn.style.color = '#ef4444';
|
||
deleteFileBtn.addEventListener('click', async (ev) => {
|
||
ev.stopPropagation();
|
||
|
||
const ok = confirm(`Datei "${eingangFiles[idx]}" wirklich komplett löschen? Diese Aktion kann nicht rückgängig gemacht werden.`);
|
||
if (!ok) return;
|
||
|
||
await deleteFileCompletely(eingangFiles[idx]);
|
||
});
|
||
|
||
actionsSpan.appendChild(removeFromUnitBtn);
|
||
actionsSpan.appendChild(deleteFileBtn);
|
||
|
||
li.appendChild(nameSpan);
|
||
li.appendChild(actionsSpan);
|
||
|
||
li.addEventListener('click', () => {
|
||
currentIndex = idx;
|
||
currentSelectedFile = filename;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
});
|
||
|
||
eingangListEl.appendChild(li);
|
||
});
|
||
|
||
eingangCountEl.textContent = eingangFiles.length + (eingangFiles.length === 1 ? ' Datei' : ' Dateien');
|
||
}
|
||
|
||
async function loadWorksheetPairs() {
|
||
try {
|
||
const data = await apiFetch('/api/worksheet-pairs');
|
||
allWorksheetPairs = {};
|
||
(data.pairs || []).forEach((p) => {
|
||
allWorksheetPairs[p.original] = { clean_html: p.clean_html, clean_image: p.clean_image };
|
||
});
|
||
worksheetPairs = { ...allWorksheetPairs };
|
||
renderPreviewForCurrent();
|
||
} catch (e) {
|
||
console.error(e);
|
||
setStatus('Fehler beim Laden der Neuaufbau-Daten', String(e), 'error');
|
||
}
|
||
}
|
||
|
||
function renderPreviewForCurrent() {
|
||
if (!eingangFiles.length) {
|
||
const message = showOnlyUnitFiles && currentUnitId
|
||
? 'Dieser Lerneinheit sind noch keine Arbeitsblätter zugeordnet.'
|
||
: 'Keine Dateien vorhanden.';
|
||
previewContainer.innerHTML = `<div class="preview-placeholder">${message}</div>`;
|
||
return;
|
||
}
|
||
if (currentIndex < 0) currentIndex = 0;
|
||
if (currentIndex >= eingangFiles.length) currentIndex = eingangFiles.length - 1;
|
||
|
||
const filename = eingangFiles[currentIndex];
|
||
const entry = worksheetPairs[filename] || { clean_html: null, clean_image: null };
|
||
|
||
renderPreview(entry, currentIndex);
|
||
}
|
||
|
||
function renderThumbnailsInColumn(container) {
|
||
container.innerHTML = '';
|
||
|
||
if (eingangFiles.length <= 1) {
|
||
return; // Keine Thumbnails nötig wenn nur 1 oder 0 Dateien
|
||
}
|
||
|
||
// Zeige bis zu 5 Thumbnails (die nächsten Dateien nach dem aktuellen)
|
||
const maxThumbs = 5;
|
||
let thumbCount = 0;
|
||
|
||
for (let i = 0; i < eingangFiles.length && thumbCount < maxThumbs; i++) {
|
||
if (i === currentIndex) continue; // Aktuelles Dokument überspringen
|
||
|
||
const filename = eingangFiles[i];
|
||
const thumb = document.createElement('div');
|
||
thumb.className = 'preview-thumb';
|
||
|
||
const img = document.createElement('img');
|
||
img.src = '/preview-file/' + encodeURIComponent(filename);
|
||
img.alt = filename;
|
||
|
||
const label = document.createElement('div');
|
||
label.className = 'preview-thumb-label';
|
||
label.textContent = `${i + 1}`;
|
||
|
||
thumb.appendChild(img);
|
||
thumb.appendChild(label);
|
||
|
||
thumb.addEventListener('click', () => {
|
||
currentIndex = i;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
});
|
||
|
||
container.appendChild(thumb);
|
||
thumbCount++;
|
||
}
|
||
}
|
||
|
||
function renderPreview(entry, index) {
|
||
previewContainer.innerHTML = '';
|
||
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'compare-wrapper';
|
||
|
||
// Original
|
||
const originalSection = document.createElement('div');
|
||
originalSection.className = 'compare-section';
|
||
const origHeader = document.createElement('div');
|
||
origHeader.className = 'compare-header';
|
||
origHeader.innerHTML = '<span>Original-Scan</span><span>Alt (links)</span>';
|
||
const origBody = document.createElement('div');
|
||
origBody.className = 'compare-body';
|
||
const origInner = document.createElement('div');
|
||
origInner.className = 'compare-body-inner';
|
||
|
||
const img = document.createElement('img');
|
||
img.className = 'preview-img';
|
||
const imgSrc = '/preview-file/' + encodeURIComponent(eingangFiles[index]);
|
||
img.src = imgSrc;
|
||
img.alt = 'Original ' + eingangFiles[index];
|
||
img.addEventListener('dblclick', () => openLightbox(imgSrc, eingangFiles[index]));
|
||
|
||
origInner.appendChild(img);
|
||
origBody.appendChild(origInner);
|
||
originalSection.appendChild(origHeader);
|
||
originalSection.appendChild(origBody);
|
||
|
||
// Neu aufgebaut
|
||
const cleanSection = document.createElement('div');
|
||
cleanSection.className = 'compare-section';
|
||
const cleanHeader = document.createElement('div');
|
||
cleanHeader.className = 'compare-header';
|
||
cleanHeader.innerHTML = '<span>Neu aufgebautes Arbeitsblatt</span><span style="display:flex;align-items:center;gap:8px;"><button type="button" class="btn btn-sm btn-ghost no-print" id="btn-print-worksheet" style="padding:4px 10px;font-size:11px;">🖨️ Drucken</button><span>Neu (rechts)</span></span>';
|
||
const cleanBody = document.createElement('div');
|
||
cleanBody.className = 'compare-body';
|
||
const cleanInner = document.createElement('div');
|
||
cleanInner.className = 'compare-body-inner';
|
||
|
||
// Bevorzuge bereinigtes Bild über HTML (für pixel-genaue Darstellung)
|
||
if (entry.clean_image) {
|
||
const imgClean = document.createElement('img');
|
||
imgClean.className = 'preview-img';
|
||
const cleanSrc = '/preview-clean-file/' + encodeURIComponent(entry.clean_image);
|
||
imgClean.src = cleanSrc;
|
||
imgClean.alt = 'Neu aufgebaut ' + eingangFiles[index];
|
||
imgClean.addEventListener('dblclick', () => openLightbox(cleanSrc, eingangFiles[index] + ' (neu)'));
|
||
cleanInner.appendChild(imgClean);
|
||
} else if (entry.clean_html) {
|
||
const frame = document.createElement('iframe');
|
||
frame.className = 'clean-frame';
|
||
frame.src = '/api/clean-html/' + encodeURIComponent(entry.clean_html);
|
||
frame.title = 'Neu aufgebautes Arbeitsblatt';
|
||
frame.addEventListener('dblclick', () => {
|
||
window.open('/api/clean-html/' + encodeURIComponent(entry.clean_html), '_blank');
|
||
});
|
||
cleanInner.appendChild(frame);
|
||
} else {
|
||
cleanInner.innerHTML = '<div class="preview-placeholder">Noch keine Neuaufbau-Daten vorhanden.</div>';
|
||
}
|
||
|
||
cleanBody.appendChild(cleanInner);
|
||
cleanSection.appendChild(cleanHeader);
|
||
cleanSection.appendChild(cleanBody);
|
||
|
||
// Print-Button Event-Listener
|
||
const printWorksheetBtn = cleanHeader.querySelector('#btn-print-worksheet');
|
||
if (printWorksheetBtn) {
|
||
printWorksheetBtn.addEventListener('click', () => {
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) {
|
||
alert(t('worksheet_no_data') || 'Keine Arbeitsblatt-Daten vorhanden.');
|
||
return;
|
||
}
|
||
window.open('/api/print-worksheet/' + encodeURIComponent(currentFile), '_blank');
|
||
});
|
||
}
|
||
|
||
// Thumbnails in der Mitte
|
||
const thumbsColumn = document.createElement('div');
|
||
thumbsColumn.className = 'preview-thumbnails';
|
||
thumbsColumn.id = 'preview-thumbnails-middle';
|
||
renderThumbnailsInColumn(thumbsColumn);
|
||
|
||
wrapper.appendChild(originalSection);
|
||
wrapper.appendChild(thumbsColumn);
|
||
wrapper.appendChild(cleanSection);
|
||
|
||
// Navigation-Buttons hinzufügen
|
||
const navDiv = document.createElement('div');
|
||
navDiv.className = 'preview-nav';
|
||
|
||
const prevBtn = document.createElement('button');
|
||
prevBtn.type = 'button';
|
||
prevBtn.textContent = '‹';
|
||
prevBtn.disabled = currentIndex === 0;
|
||
prevBtn.addEventListener('click', () => {
|
||
if (currentIndex > 0) {
|
||
currentIndex--;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
}
|
||
});
|
||
|
||
const nextBtn = document.createElement('button');
|
||
nextBtn.type = 'button';
|
||
nextBtn.textContent = '›';
|
||
nextBtn.disabled = currentIndex >= eingangFiles.length - 1;
|
||
nextBtn.addEventListener('click', () => {
|
||
if (currentIndex < eingangFiles.length - 1) {
|
||
currentIndex++;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
}
|
||
});
|
||
|
||
const positionSpan = document.createElement('span');
|
||
positionSpan.textContent = `${currentIndex + 1} von ${eingangFiles.length}`;
|
||
|
||
navDiv.appendChild(prevBtn);
|
||
navDiv.appendChild(positionSpan);
|
||
navDiv.appendChild(nextBtn);
|
||
|
||
wrapper.appendChild(navDiv);
|
||
|
||
previewContainer.appendChild(wrapper);
|
||
}
|
||
|
||
// --- Upload ---
|
||
btnUploadInline.addEventListener('click', async (ev) => {
|
||
ev.preventDefault();
|
||
ev.stopPropagation();
|
||
|
||
const files = fileInput.files;
|
||
if (!files || !files.length) {
|
||
alert('Bitte erst Dateien auswählen.');
|
||
return;
|
||
}
|
||
|
||
const formData = new FormData();
|
||
for (const file of files) {
|
||
formData.append('files', file);
|
||
}
|
||
|
||
try {
|
||
setStatus('Upload läuft …', 'Dateien werden in den Ordner „Eingang“ geschrieben.', 'busy');
|
||
|
||
const resp = await fetch('/api/upload-multi', {
|
||
method: 'POST',
|
||
body: formData,
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
console.error('Upload-Fehler: HTTP', resp.status);
|
||
setStatus('Fehler beim Upload', 'Serverantwort: HTTP ' + resp.status, 'error');
|
||
return;
|
||
}
|
||
|
||
setStatus('Upload abgeschlossen', 'Dateien wurden gespeichert.');
|
||
fileInput.value = '';
|
||
|
||
// Liste neu laden
|
||
await loadEingangFiles();
|
||
await loadWorksheetPairs();
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Upload', e);
|
||
setStatus('Netzwerkfehler beim Upload', String(e), 'error');
|
||
}
|
||
});
|
||
|
||
// --- Vollpipeline ---
|
||
async function runFullPipeline() {
|
||
try {
|
||
setStatus('Entferne Handschrift …', 'Bilder werden aufbereitet.', 'busy');
|
||
await apiFetch('/api/remove-handwriting-all', { method: 'POST' });
|
||
|
||
setStatus('Analysiere Arbeitsblätter …', 'Struktur wird erkannt.', 'busy');
|
||
await apiFetch('/api/analyze-all', { method: 'POST' });
|
||
|
||
setStatus('Erzeuge HTML-Arbeitsblätter …', 'Neuaufbau läuft.', 'busy');
|
||
await apiFetch('/api/generate-clean', { method: 'POST' });
|
||
|
||
setStatus('Fertig', 'Alt & Neu können jetzt verglichen werden.');
|
||
await loadWorksheetPairs();
|
||
renderPreviewForCurrent();
|
||
} catch (e) {
|
||
console.error(e);
|
||
setStatus('Fehler in der Verarbeitung', String(e), 'error');
|
||
}
|
||
}
|
||
|
||
if (btnFullProcess) btnFullProcess.addEventListener('click', runFullPipeline);
|
||
if (btnOriginalGenerate) btnOriginalGenerate.addEventListener('click', runFullPipeline);
|
||
|
||
// --- Screen-Navigation (oben + Pager unten) ---
|
||
function updateScreen() {
|
||
if (currentScreen === 1) {
|
||
panelCompare.style.display = 'flex';
|
||
panelTiles.style.display = 'none';
|
||
pagerLabel.textContent = '1 von 2';
|
||
} else {
|
||
panelCompare.style.display = 'none';
|
||
panelTiles.style.display = 'flex';
|
||
pagerLabel.textContent = '2 von 2';
|
||
}
|
||
|
||
topNavItems.forEach((item) => {
|
||
const screen = Number(item.getAttribute('data-screen'));
|
||
if (screen === currentScreen) {
|
||
item.classList.add('active');
|
||
} else {
|
||
item.classList.remove('active');
|
||
}
|
||
});
|
||
}
|
||
|
||
topNavItems.forEach((item) => {
|
||
item.addEventListener('click', () => {
|
||
const screen = Number(item.getAttribute('data-screen'));
|
||
currentScreen = screen;
|
||
updateScreen();
|
||
});
|
||
});
|
||
|
||
pagerPrev.addEventListener('click', () => {
|
||
if (currentScreen > 1) {
|
||
currentScreen -= 1;
|
||
updateScreen();
|
||
}
|
||
});
|
||
|
||
pagerNext.addEventListener('click', () => {
|
||
if (currentScreen < 2) {
|
||
currentScreen += 1;
|
||
updateScreen();
|
||
}
|
||
});
|
||
|
||
// --- Toggle-Kacheln ---
|
||
const tileToggles = document.querySelectorAll('.toggle-pill');
|
||
const cards = document.querySelectorAll('.card');
|
||
|
||
function updateTiles() {
|
||
let activeTiles = Object.keys(tileState).filter((k) => tileState[k]);
|
||
cards.forEach((card) => {
|
||
const key = card.getAttribute('data-tile');
|
||
if (!tileState[key]) {
|
||
card.classList.add('card-hidden');
|
||
} else {
|
||
card.classList.remove('card-hidden');
|
||
}
|
||
card.classList.remove('card-full');
|
||
});
|
||
|
||
if (activeTiles.length === 1) {
|
||
const only = activeTiles[0];
|
||
cards.forEach((card) => {
|
||
if (card.getAttribute('data-tile') === only) {
|
||
card.classList.add('card-full');
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
tileToggles.forEach((btn) => {
|
||
btn.addEventListener('click', () => {
|
||
const key = btn.getAttribute('data-tile');
|
||
tileState[key] = !tileState[key];
|
||
btn.classList.toggle('active', tileState[key]);
|
||
updateTiles();
|
||
});
|
||
});
|
||
|
||
// --- Lerneinheiten-Logik (Backend) ---
|
||
function updateUnitHeading(unit = null) {
|
||
if (!unit && currentUnitId && units && units.length) {
|
||
unit = units.find((u) => u.id === currentUnitId) || null;
|
||
}
|
||
|
||
let text = 'Keine Lerneinheit ausgewählt';
|
||
if (unit) {
|
||
const name = unit.label || unit.title || 'Lerneinheit';
|
||
text = 'Lerneinheit: ' + name;
|
||
}
|
||
|
||
if (unitHeading1) unitHeading1.textContent = text;
|
||
if (unitHeading2) unitHeading2.textContent = text;
|
||
}
|
||
|
||
function applyUnitFilter() {
|
||
let unit = null;
|
||
if (currentUnitId && units && units.length) {
|
||
unit = units.find((u) => u.id === currentUnitId) || null;
|
||
}
|
||
|
||
// Wenn Filter deaktiviert ODER keine Lerneinheit ausgewählt -> alle Dateien anzeigen
|
||
if (!showOnlyUnitFiles || !unit || !Array.isArray(unit.worksheet_files) || unit.worksheet_files.length === 0) {
|
||
eingangFiles = allEingangFiles.slice();
|
||
worksheetPairs = { ...allWorksheetPairs };
|
||
currentIndex = 0;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
updateUnitHeading(unit);
|
||
return;
|
||
}
|
||
|
||
// Filter aktiv: nur Dateien der aktuellen Lerneinheit anzeigen
|
||
const allowed = new Set(unit.worksheet_files || []);
|
||
eingangFiles = allEingangFiles.filter((f) => allowed.has(f));
|
||
|
||
const filteredPairs = {};
|
||
Object.keys(allWorksheetPairs).forEach((key) => {
|
||
if (allowed.has(key)) {
|
||
filteredPairs[key] = allWorksheetPairs[key];
|
||
}
|
||
});
|
||
worksheetPairs = filteredPairs;
|
||
|
||
currentIndex = 0;
|
||
renderEingangList();
|
||
renderPreviewForCurrent();
|
||
updateUnitHeading(unit);
|
||
}
|
||
|
||
async function loadLearningUnits() {
|
||
try {
|
||
const resp = await fetch('/api/learning-units/');
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Laden der Lerneinheiten', resp.status);
|
||
return;
|
||
}
|
||
units = await resp.json();
|
||
if (units.length && !currentUnitId) {
|
||
currentUnitId = units[0].id;
|
||
}
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Laden der Lerneinheiten', e);
|
||
}
|
||
}
|
||
|
||
function renderUnits() {
|
||
unitListEl.innerHTML = '';
|
||
|
||
if (!units.length) {
|
||
const li = document.createElement('li');
|
||
li.className = 'unit-item';
|
||
li.textContent = 'Noch keine Lerneinheiten angelegt.';
|
||
unitListEl.appendChild(li);
|
||
updateUnitHeading(null);
|
||
return;
|
||
}
|
||
|
||
units.forEach((u) => {
|
||
const li = document.createElement('li');
|
||
li.className = 'unit-item';
|
||
if (u.id === currentUnitId) {
|
||
li.classList.add('active');
|
||
}
|
||
|
||
const contentDiv = document.createElement('div');
|
||
contentDiv.style.flex = '1';
|
||
contentDiv.style.minWidth = '0';
|
||
|
||
const titleEl = document.createElement('div');
|
||
titleEl.textContent = u.label || u.title || 'Lerneinheit';
|
||
|
||
const metaEl = document.createElement('div');
|
||
metaEl.className = 'unit-item-meta';
|
||
|
||
const metaParts = [];
|
||
if (u.meta) {
|
||
metaParts.push(u.meta);
|
||
}
|
||
if (Array.isArray(u.worksheet_files)) {
|
||
metaParts.push('Blätter: ' + u.worksheet_files.length);
|
||
}
|
||
|
||
metaEl.textContent = metaParts.join(' · ');
|
||
|
||
contentDiv.appendChild(titleEl);
|
||
contentDiv.appendChild(metaEl);
|
||
|
||
// Delete-Button
|
||
const deleteBtn = document.createElement('span');
|
||
deleteBtn.textContent = '🗑️';
|
||
deleteBtn.style.cursor = 'pointer';
|
||
deleteBtn.style.fontSize = '12px';
|
||
deleteBtn.style.color = '#ef4444';
|
||
deleteBtn.title = 'Lerneinheit löschen';
|
||
deleteBtn.addEventListener('click', async (ev) => {
|
||
ev.stopPropagation();
|
||
const ok = confirm(`Lerneinheit "${u.label || u.title}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`);
|
||
if (!ok) return;
|
||
await deleteLearningUnit(u.id);
|
||
});
|
||
|
||
li.appendChild(contentDiv);
|
||
li.appendChild(deleteBtn);
|
||
|
||
li.addEventListener('click', () => {
|
||
currentUnitId = u.id;
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
});
|
||
|
||
unitListEl.appendChild(li);
|
||
});
|
||
}
|
||
|
||
async function addUnitFromForm() {
|
||
const student = (unitStudentInput.value || '').trim();
|
||
const subject = (unitSubjectInput.value || '').trim();
|
||
const grade = (unitGradeInput && unitGradeInput.value || '').trim();
|
||
const title = (unitTitleInput.value || '').trim();
|
||
|
||
if (!student && !subject && !title) {
|
||
alert('Bitte mindestens einen Wert (Schüler/in, Fach oder Thema) eintragen.');
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
student,
|
||
subject,
|
||
title,
|
||
grade,
|
||
};
|
||
|
||
try {
|
||
const resp = await fetch('/api/learning-units/', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Anlegen der Lerneinheit', resp.status);
|
||
alert('Lerneinheit konnte nicht angelegt werden.');
|
||
return;
|
||
}
|
||
|
||
const created = await resp.json();
|
||
units.push(created);
|
||
currentUnitId = created.id;
|
||
|
||
unitStudentInput.value = '';
|
||
unitSubjectInput.value = '';
|
||
unitTitleInput.value = '';
|
||
if (unitGradeInput) unitGradeInput.value = '';
|
||
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Anlegen der Lerneinheit', e);
|
||
alert('Netzwerkfehler beim Anlegen der Lerneinheit.');
|
||
}
|
||
}
|
||
|
||
function getCurrentWorksheetBasename() {
|
||
if (!eingangFiles.length) return null;
|
||
if (currentIndex < 0 || currentIndex >= eingangFiles.length) return null;
|
||
return eingangFiles[currentIndex];
|
||
}
|
||
|
||
async function attachCurrentWorksheetToUnit() {
|
||
if (!currentUnitId) {
|
||
alert('Bitte zuerst eine Lerneinheit auswählen oder anlegen.');
|
||
return;
|
||
}
|
||
const basename = getCurrentWorksheetBasename();
|
||
if (!basename) {
|
||
alert('Bitte zuerst ein Arbeitsblatt im linken Bereich auswählen.');
|
||
return;
|
||
}
|
||
|
||
const payload = { worksheet_files: [basename] };
|
||
|
||
try {
|
||
const resp = await fetch(`/api/learning-units/${currentUnitId}/attach-worksheets`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Zuordnen des Arbeitsblatts', resp.status);
|
||
alert('Arbeitsblatt konnte nicht zugeordnet werden.');
|
||
return;
|
||
}
|
||
|
||
const updated = await resp.json();
|
||
const idx = units.findIndex((u) => u.id === updated.id);
|
||
if (idx !== -1) {
|
||
units[idx] = updated;
|
||
}
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Zuordnen des Arbeitsblatts', e);
|
||
alert('Netzwerkfehler beim Zuordnen des Arbeitsblatts.');
|
||
}
|
||
}
|
||
async function removeWorksheetFromCurrentUnit(filename) {
|
||
if (!currentUnitId) {
|
||
alert('Bitte zuerst eine Lerneinheit auswählen.');
|
||
return;
|
||
}
|
||
if (!filename) {
|
||
alert('Fehler: kein Dateiname übergeben.');
|
||
return;
|
||
}
|
||
|
||
const payload = { worksheet_file: filename };
|
||
|
||
try {
|
||
const resp = await fetch(`/api/learning-units/${currentUnitId}/remove-worksheet`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Entfernen des Arbeitsblatts', resp.status);
|
||
alert('Arbeitsblatt konnte nicht aus der Lerneinheit entfernt werden.');
|
||
return;
|
||
}
|
||
|
||
const updated = await resp.json();
|
||
const idx = units.findIndex((u) => u.id === updated.id);
|
||
if (idx !== -1) {
|
||
units[idx] = updated;
|
||
}
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Entfernen des Arbeitsblatts', e);
|
||
alert('Netzwerkfehler beim Entfernen des Arbeitsblatts.');
|
||
}
|
||
}
|
||
|
||
async function deleteFileCompletely(filename) {
|
||
if (!filename) {
|
||
alert('Fehler: kein Dateiname übergeben.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setStatus('Lösche Datei …', filename, 'busy');
|
||
|
||
const resp = await fetch(`/api/eingang-dateien/${encodeURIComponent(filename)}`, {
|
||
method: 'DELETE',
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Löschen der Datei', resp.status);
|
||
setStatus('Fehler beim Löschen', filename, 'error');
|
||
alert('Datei konnte nicht gelöscht werden.');
|
||
return;
|
||
}
|
||
|
||
const result = await resp.json();
|
||
if (result.status === 'OK') {
|
||
setStatus('Datei gelöscht', filename);
|
||
// Dateien neu laden
|
||
await loadEingangFiles();
|
||
await loadWorksheetPairs();
|
||
await loadLearningUnits();
|
||
} else {
|
||
setStatus('Fehler', result.message, 'error');
|
||
alert(result.message);
|
||
}
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Löschen der Datei', e);
|
||
setStatus('Netzwerkfehler', String(e), 'error');
|
||
alert('Netzwerkfehler beim Löschen der Datei.');
|
||
}
|
||
}
|
||
|
||
async function deleteLearningUnit(unitId) {
|
||
if (!unitId) {
|
||
alert('Fehler: keine Lerneinheit-ID übergeben.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setStatus('Lösche Lerneinheit …', '', 'busy');
|
||
|
||
const resp = await fetch(`/api/learning-units/${unitId}`, {
|
||
method: 'DELETE',
|
||
});
|
||
|
||
if (!resp.ok) {
|
||
console.error('Fehler beim Löschen der Lerneinheit', resp.status);
|
||
setStatus('Fehler beim Löschen', '', 'error');
|
||
alert('Lerneinheit konnte nicht gelöscht werden.');
|
||
return;
|
||
}
|
||
|
||
const result = await resp.json();
|
||
if (result.status === 'deleted') {
|
||
setStatus('Lerneinheit gelöscht', '');
|
||
|
||
// Lerneinheit aus der lokalen Liste entfernen
|
||
units = units.filter((u) => u.id !== unitId);
|
||
|
||
// Wenn die gelöschte Einheit ausgewählt war, Auswahl zurücksetzen
|
||
if (currentUnitId === unitId) {
|
||
currentUnitId = units.length > 0 ? units[0].id : null;
|
||
}
|
||
|
||
renderUnits();
|
||
applyUnitFilter();
|
||
} else {
|
||
setStatus('Fehler', 'Unbekannter Fehler', 'error');
|
||
alert('Fehler beim Löschen der Lerneinheit.');
|
||
}
|
||
} catch (e) {
|
||
console.error('Netzwerkfehler beim Löschen der Lerneinheit', e);
|
||
setStatus('Netzwerkfehler', String(e), 'error');
|
||
alert('Netzwerkfehler beim Löschen der Lerneinheit.');
|
||
}
|
||
}
|
||
|
||
if (btnAddUnit) {
|
||
btnAddUnit.addEventListener('click', (ev) => {
|
||
ev.preventDefault();
|
||
addUnitFromForm();
|
||
});
|
||
}
|
||
|
||
if (btnAttachCurrentToLu) {
|
||
btnAttachCurrentToLu.addEventListener('click', (ev) => {
|
||
ev.preventDefault();
|
||
attachCurrentWorksheetToUnit();
|
||
});
|
||
}
|
||
|
||
// --- Filter-Toggle ---
|
||
if (btnToggleFilter) {
|
||
btnToggleFilter.addEventListener('click', () => {
|
||
showOnlyUnitFiles = !showOnlyUnitFiles;
|
||
if (showOnlyUnitFiles) {
|
||
btnToggleFilter.textContent = 'Nur Lerneinheit';
|
||
btnToggleFilter.classList.add('btn-primary');
|
||
} else {
|
||
btnToggleFilter.textContent = 'Alle Dateien';
|
||
btnToggleFilter.classList.remove('btn-primary');
|
||
}
|
||
applyUnitFilter();
|
||
});
|
||
}
|
||
|
||
// --- Multiple Choice Logik ---
|
||
const btnMcGenerate = document.getElementById('btn-mc-generate');
|
||
const btnMcShow = document.getElementById('btn-mc-show');
|
||
const btnMcPrint = document.getElementById('btn-mc-print');
|
||
const mcPreview = document.getElementById('mc-preview');
|
||
const mcBadge = document.getElementById('mc-badge');
|
||
const mcModal = document.getElementById('mc-modal');
|
||
const mcModalBody = document.getElementById('mc-modal-body');
|
||
const mcModalClose = document.getElementById('mc-modal-close');
|
||
|
||
let currentMcData = null;
|
||
let mcAnswers = {}; // Speichert Nutzerantworten
|
||
|
||
async function generateMcQuestions() {
|
||
try {
|
||
setStatus('Generiere MC-Fragen …', 'Bitte warten, KI arbeitet.', 'busy');
|
||
if (mcBadge) mcBadge.textContent = 'Generiert...';
|
||
|
||
const resp = await fetch('/api/generate-mc', { method: 'POST' });
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
|
||
const result = await resp.json();
|
||
if (result.status === 'OK' && result.generated.length > 0) {
|
||
setStatus('MC-Fragen generiert', result.generated.length + ' Dateien erstellt.');
|
||
if (mcBadge) mcBadge.textContent = 'Fertig';
|
||
if (btnMcShow) btnMcShow.style.display = 'inline-block';
|
||
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
|
||
|
||
// Lade die erste MC-Datei für Vorschau
|
||
await loadMcPreviewForCurrent();
|
||
} else if (result.errors && result.errors.length > 0) {
|
||
setStatus('Fehler bei MC-Generierung', result.errors[0].error, 'error');
|
||
if (mcBadge) mcBadge.textContent = 'Fehler';
|
||
} else {
|
||
setStatus('Keine MC-Fragen generiert', 'Möglicherweise fehlen Analyse-Daten.', 'error');
|
||
if (mcBadge) mcBadge.textContent = 'Bereit';
|
||
}
|
||
} catch (e) {
|
||
console.error('MC-Generierung fehlgeschlagen:', e);
|
||
setStatus('Fehler bei MC-Generierung', String(e), 'error');
|
||
if (mcBadge) mcBadge.textContent = 'Fehler';
|
||
}
|
||
}
|
||
|
||
async function loadMcPreviewForCurrent() {
|
||
if (!eingangFiles.length) {
|
||
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Arbeitsblätter vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
|
||
try {
|
||
const resp = await fetch('/api/mc-data/' + encodeURIComponent(currentFile));
|
||
const result = await resp.json();
|
||
|
||
if (result.status === 'OK' && result.data) {
|
||
currentMcData = result.data;
|
||
renderMcPreview(result.data);
|
||
if (btnMcShow) btnMcShow.style.display = 'inline-block';
|
||
if (btnMcPrint) btnMcPrint.style.display = 'inline-block';
|
||
} else {
|
||
if (mcPreview) mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Noch keine MC-Fragen für dieses Arbeitsblatt generiert.</div>';
|
||
currentMcData = null;
|
||
if (btnMcPrint) btnMcPrint.style.display = 'none';
|
||
}
|
||
} catch (e) {
|
||
console.error('Fehler beim Laden der MC-Daten:', e);
|
||
if (mcPreview) mcPreview.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
function renderMcPreview(mcData) {
|
||
if (!mcPreview) return;
|
||
if (!mcData || !mcData.questions || mcData.questions.length === 0) {
|
||
mcPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Fragen vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const questions = mcData.questions;
|
||
const metadata = mcData.metadata || {};
|
||
|
||
let html = '';
|
||
|
||
// Zeige Metadaten
|
||
if (metadata.grade_level || metadata.subject) {
|
||
html += '<div class="mc-stats">';
|
||
if (metadata.subject) {
|
||
html += '<div class="mc-stats-item"><strong>Fach:</strong> ' + metadata.subject + '</div>';
|
||
}
|
||
if (metadata.grade_level) {
|
||
html += '<div class="mc-stats-item"><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
|
||
}
|
||
html += '<div class="mc-stats-item"><strong>Fragen:</strong> ' + questions.length + '</div>';
|
||
html += '</div>';
|
||
}
|
||
|
||
// Zeige erste 2 Fragen als Vorschau
|
||
const previewQuestions = questions.slice(0, 2);
|
||
previewQuestions.forEach((q, idx) => {
|
||
html += '<div class="mc-question">';
|
||
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + q.question + '</div>';
|
||
html += '<div class="mc-options">';
|
||
q.options.forEach(opt => {
|
||
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
|
||
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + opt.text;
|
||
html += '</div>';
|
||
});
|
||
html += '</div>';
|
||
html += '</div>';
|
||
});
|
||
|
||
if (questions.length > 2) {
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (questions.length - 2) + ' weitere Fragen</div>';
|
||
}
|
||
|
||
mcPreview.innerHTML = html;
|
||
|
||
// Event-Listener für Antwort-Auswahl
|
||
mcPreview.querySelectorAll('.mc-option').forEach(optEl => {
|
||
optEl.addEventListener('click', () => handleMcOptionClick(optEl));
|
||
});
|
||
}
|
||
|
||
function handleMcOptionClick(optEl) {
|
||
const qid = optEl.getAttribute('data-qid');
|
||
const optId = optEl.getAttribute('data-opt');
|
||
|
||
if (!currentMcData) return;
|
||
|
||
// Finde die Frage
|
||
const question = currentMcData.questions.find(q => q.id === qid);
|
||
if (!question) return;
|
||
|
||
// Markiere alle Optionen dieser Frage
|
||
const questionEl = optEl.closest('.mc-question');
|
||
const allOptions = questionEl.querySelectorAll('.mc-option');
|
||
|
||
allOptions.forEach(opt => {
|
||
opt.classList.remove('selected', 'correct', 'incorrect');
|
||
const thisOptId = opt.getAttribute('data-opt');
|
||
if (thisOptId === question.correct_answer) {
|
||
opt.classList.add('correct');
|
||
} else if (thisOptId === optId) {
|
||
opt.classList.add('incorrect');
|
||
}
|
||
});
|
||
|
||
// Speichere Antwort
|
||
mcAnswers[qid] = optId;
|
||
|
||
// Zeige Feedback wenn gewünscht
|
||
const isCorrect = optId === question.correct_answer;
|
||
let feedbackEl = questionEl.querySelector('.mc-feedback');
|
||
if (!feedbackEl) {
|
||
feedbackEl = document.createElement('div');
|
||
feedbackEl.className = 'mc-feedback';
|
||
questionEl.appendChild(feedbackEl);
|
||
}
|
||
|
||
if (isCorrect) {
|
||
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
|
||
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
|
||
feedbackEl.style.color = 'var(--bp-accent)';
|
||
feedbackEl.textContent = 'Richtig! ' + (question.explanation || '');
|
||
} else {
|
||
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
|
||
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
|
||
feedbackEl.style.color = '#ef4444';
|
||
feedbackEl.textContent = 'Leider falsch. ' + (question.explanation || '');
|
||
}
|
||
}
|
||
|
||
function openMcModal() {
|
||
if (!currentMcData || !currentMcData.questions) {
|
||
alert('Keine MC-Fragen vorhanden. Bitte zuerst generieren.');
|
||
return;
|
||
}
|
||
|
||
mcAnswers = {}; // Reset Antworten
|
||
renderMcModal(currentMcData);
|
||
mcModal.classList.remove('hidden');
|
||
}
|
||
|
||
function closeMcModal() {
|
||
mcModal.classList.add('hidden');
|
||
}
|
||
|
||
function renderMcModal(mcData) {
|
||
const questions = mcData.questions;
|
||
const metadata = mcData.metadata || {};
|
||
|
||
let html = '';
|
||
|
||
// Header mit Metadaten
|
||
html += '<div class="mc-stats" style="margin-bottom:16px;">';
|
||
if (metadata.source_title) {
|
||
html += '<div class="mc-stats-item"><strong>Arbeitsblatt:</strong> ' + metadata.source_title + '</div>';
|
||
}
|
||
if (metadata.subject) {
|
||
html += '<div class="mc-stats-item"><strong>Fach:</strong> ' + metadata.subject + '</div>';
|
||
}
|
||
if (metadata.grade_level) {
|
||
html += '<div class="mc-stats-item"><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// Alle Fragen
|
||
questions.forEach((q, idx) => {
|
||
html += '<div class="mc-question" data-qid="' + q.id + '">';
|
||
html += '<div class="mc-question-text">' + (idx + 1) + '. ' + q.question + '</div>';
|
||
html += '<div class="mc-options">';
|
||
q.options.forEach(opt => {
|
||
html += '<div class="mc-option" data-qid="' + q.id + '" data-opt="' + opt.id + '">';
|
||
html += '<span class="mc-option-label">' + opt.id + ')</span> ' + opt.text;
|
||
html += '</div>';
|
||
});
|
||
html += '</div>';
|
||
html += '</div>';
|
||
});
|
||
|
||
// Auswertungs-Button
|
||
html += '<div style="margin-top:16px;text-align:center;">';
|
||
html += '<button class="btn btn-primary" id="btn-mc-evaluate">Auswerten</button>';
|
||
html += '</div>';
|
||
|
||
mcModalBody.innerHTML = html;
|
||
|
||
// Event-Listener
|
||
mcModalBody.querySelectorAll('.mc-option').forEach(optEl => {
|
||
optEl.addEventListener('click', () => {
|
||
const qid = optEl.getAttribute('data-qid');
|
||
const optId = optEl.getAttribute('data-opt');
|
||
|
||
// Deselektiere andere Optionen der gleichen Frage
|
||
const questionEl = optEl.closest('.mc-question');
|
||
questionEl.querySelectorAll('.mc-option').forEach(o => o.classList.remove('selected'));
|
||
optEl.classList.add('selected');
|
||
|
||
mcAnswers[qid] = optId;
|
||
});
|
||
});
|
||
|
||
const btnEvaluate = document.getElementById('btn-mc-evaluate');
|
||
if (btnEvaluate) {
|
||
btnEvaluate.addEventListener('click', evaluateMcQuiz);
|
||
}
|
||
}
|
||
|
||
function evaluateMcQuiz() {
|
||
if (!currentMcData) return;
|
||
|
||
let correct = 0;
|
||
let total = currentMcData.questions.length;
|
||
|
||
currentMcData.questions.forEach(q => {
|
||
const questionEl = mcModalBody.querySelector('.mc-question[data-qid="' + q.id + '"]');
|
||
if (!questionEl) return;
|
||
|
||
const userAnswer = mcAnswers[q.id];
|
||
const allOptions = questionEl.querySelectorAll('.mc-option');
|
||
|
||
allOptions.forEach(opt => {
|
||
opt.classList.remove('correct', 'incorrect');
|
||
const optId = opt.getAttribute('data-opt');
|
||
if (optId === q.correct_answer) {
|
||
opt.classList.add('correct');
|
||
} else if (optId === userAnswer && userAnswer !== q.correct_answer) {
|
||
opt.classList.add('incorrect');
|
||
}
|
||
});
|
||
|
||
// Zeige Erklärung
|
||
let feedbackEl = questionEl.querySelector('.mc-feedback');
|
||
if (!feedbackEl) {
|
||
feedbackEl = document.createElement('div');
|
||
feedbackEl.className = 'mc-feedback';
|
||
questionEl.appendChild(feedbackEl);
|
||
}
|
||
|
||
if (userAnswer === q.correct_answer) {
|
||
correct++;
|
||
feedbackEl.style.background = 'rgba(34,197,94,0.1)';
|
||
feedbackEl.style.borderColor = 'rgba(34,197,94,0.3)';
|
||
feedbackEl.style.color = 'var(--bp-accent)';
|
||
feedbackEl.textContent = 'Richtig! ' + (q.explanation || '');
|
||
} else if (userAnswer) {
|
||
feedbackEl.style.background = 'rgba(239,68,68,0.1)';
|
||
feedbackEl.style.borderColor = 'rgba(239,68,68,0.3)';
|
||
feedbackEl.style.color = '#ef4444';
|
||
feedbackEl.textContent = 'Falsch. ' + (q.explanation || '');
|
||
} else {
|
||
feedbackEl.style.background = 'rgba(148,163,184,0.1)';
|
||
feedbackEl.style.borderColor = 'rgba(148,163,184,0.3)';
|
||
feedbackEl.style.color = 'var(--bp-text-muted)';
|
||
feedbackEl.textContent = 'Nicht beantwortet. Richtig wäre: ' + q.correct_answer.toUpperCase();
|
||
}
|
||
});
|
||
|
||
// Zeige Gesamtergebnis
|
||
const resultHtml = '<div style="margin-top:16px;padding:12px;background:rgba(15,23,42,0.6);border-radius:8px;text-align:center;">' +
|
||
'<div style="font-size:18px;font-weight:600;">' + correct + ' von ' + total + ' richtig</div>' +
|
||
'<div style="font-size:12px;color:var(--bp-text-muted);margin-top:4px;">' + Math.round(correct / total * 100) + '% korrekt</div>' +
|
||
'</div>';
|
||
|
||
const existingResult = mcModalBody.querySelector('.mc-result');
|
||
if (existingResult) {
|
||
existingResult.remove();
|
||
}
|
||
|
||
const resultDiv = document.createElement('div');
|
||
resultDiv.className = 'mc-result';
|
||
resultDiv.innerHTML = resultHtml;
|
||
mcModalBody.appendChild(resultDiv);
|
||
}
|
||
|
||
function openMcPrintDialog() {
|
||
if (!currentMcData) {
|
||
alert(t('mc_no_questions') || 'Keine MC-Fragen vorhanden.');
|
||
return;
|
||
}
|
||
const currentFile = eingangFiles[currentIndex];
|
||
const choice = confirm((t('mc_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Lösungsblatt mit markierten Antworten\\nAbbrechen = Übungsblatt ohne Lösungen');
|
||
const url = '/api/print-mc/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
// Event Listener für MC-Buttons
|
||
if (btnMcGenerate) {
|
||
btnMcGenerate.addEventListener('click', generateMcQuestions);
|
||
}
|
||
|
||
if (btnMcShow) {
|
||
btnMcShow.addEventListener('click', openMcModal);
|
||
}
|
||
|
||
if (btnMcPrint) {
|
||
btnMcPrint.addEventListener('click', openMcPrintDialog);
|
||
}
|
||
|
||
if (mcModalClose) {
|
||
mcModalClose.addEventListener('click', closeMcModal);
|
||
}
|
||
|
||
if (mcModal) {
|
||
mcModal.addEventListener('click', (ev) => {
|
||
if (ev.target === mcModal) {
|
||
closeMcModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- Lückentext (Cloze) Logik ---
|
||
const btnClozeGenerate = document.getElementById('btn-cloze-generate');
|
||
const btnClozeShow = document.getElementById('btn-cloze-show');
|
||
const btnClozePrint = document.getElementById('btn-cloze-print');
|
||
const clozePreview = document.getElementById('cloze-preview');
|
||
const clozeBadge = document.getElementById('cloze-badge');
|
||
const clozeLanguageSelect = document.getElementById('cloze-language');
|
||
const clozeModal = document.getElementById('cloze-modal');
|
||
const clozeModalBody = document.getElementById('cloze-modal-body');
|
||
const clozeModalClose = document.getElementById('cloze-modal-close');
|
||
|
||
let currentClozeData = null;
|
||
let clozeAnswers = {}; // Speichert Nutzerantworten
|
||
|
||
async function generateClozeTexts() {
|
||
const targetLang = clozeLanguageSelect ? clozeLanguageSelect.value : 'tr';
|
||
|
||
try {
|
||
setStatus('Generiere Lückentexte …', 'Bitte warten, KI arbeitet.', 'busy');
|
||
if (clozeBadge) clozeBadge.textContent = 'Generiert...';
|
||
|
||
const resp = await fetch('/api/generate-cloze?target_language=' + targetLang, { method: 'POST' });
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
|
||
const result = await resp.json();
|
||
if (result.status === 'OK' && result.generated.length > 0) {
|
||
setStatus('Lückentexte generiert', result.generated.length + ' Dateien erstellt.');
|
||
if (clozeBadge) clozeBadge.textContent = 'Fertig';
|
||
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
|
||
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
|
||
|
||
// Lade Vorschau für aktuelle Datei
|
||
await loadClozePreviewForCurrent();
|
||
} else if (result.errors && result.errors.length > 0) {
|
||
setStatus('Fehler bei Lückentext-Generierung', result.errors[0].error, 'error');
|
||
if (clozeBadge) clozeBadge.textContent = 'Fehler';
|
||
} else {
|
||
setStatus('Keine Lückentexte generiert', 'Möglicherweise fehlen Analyse-Daten.', 'error');
|
||
if (clozeBadge) clozeBadge.textContent = 'Bereit';
|
||
}
|
||
} catch (e) {
|
||
console.error('Lückentext-Generierung fehlgeschlagen:', e);
|
||
setStatus('Fehler bei Lückentext-Generierung', String(e), 'error');
|
||
if (clozeBadge) clozeBadge.textContent = 'Fehler';
|
||
}
|
||
}
|
||
|
||
async function loadClozePreviewForCurrent() {
|
||
if (!eingangFiles.length) {
|
||
if (clozePreview) clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Arbeitsblätter vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
|
||
try {
|
||
const resp = await fetch('/api/cloze-data/' + encodeURIComponent(currentFile));
|
||
const result = await resp.json();
|
||
|
||
if (result.status === 'OK' && result.data) {
|
||
currentClozeData = result.data;
|
||
renderClozePreview(result.data);
|
||
if (btnClozeShow) btnClozeShow.style.display = 'inline-block';
|
||
if (btnClozePrint) btnClozePrint.style.display = 'inline-block';
|
||
} else {
|
||
if (clozePreview) clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Noch keine Lückentexte für dieses Arbeitsblatt generiert.</div>';
|
||
currentClozeData = null;
|
||
if (btnClozeShow) btnClozeShow.style.display = 'none';
|
||
if (btnClozePrint) btnClozePrint.style.display = 'none';
|
||
}
|
||
} catch (e) {
|
||
console.error('Fehler beim Laden der Lückentext-Daten:', e);
|
||
if (clozePreview) clozePreview.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
function renderClozePreview(clozeData) {
|
||
if (!clozePreview) return;
|
||
if (!clozeData || !clozeData.cloze_items || clozeData.cloze_items.length === 0) {
|
||
clozePreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">Keine Lückentexte vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const items = clozeData.cloze_items;
|
||
const metadata = clozeData.metadata || {};
|
||
|
||
let html = '';
|
||
|
||
// Statistiken
|
||
html += '<div class="cloze-stats">';
|
||
if (metadata.subject) {
|
||
html += '<div><strong>Fach:</strong> ' + metadata.subject + '</div>';
|
||
}
|
||
if (metadata.grade_level) {
|
||
html += '<div><strong>Stufe:</strong> ' + metadata.grade_level + '</div>';
|
||
}
|
||
html += '<div><strong>Sätze:</strong> ' + items.length + '</div>';
|
||
if (metadata.total_gaps) {
|
||
html += '<div><strong>Lücken:</strong> ' + metadata.total_gaps + '</div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// Zeige erste 2 Sätze als Vorschau
|
||
const previewItems = items.slice(0, 2);
|
||
previewItems.forEach((item, idx) => {
|
||
html += '<div class="cloze-item">';
|
||
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + item.sentence_with_gaps.replace(/___/g, '<span class="cloze-gap">___</span>') + '</div>';
|
||
|
||
// Übersetzung anzeigen
|
||
if (item.translation && item.translation.full_sentence) {
|
||
html += '<div class="cloze-translation">';
|
||
html += '<div class="cloze-translation-label">' + (item.translation.language_name || 'Übersetzung') + ':</div>';
|
||
html += item.translation.full_sentence;
|
||
html += '</div>';
|
||
}
|
||
html += '</div>';
|
||
});
|
||
|
||
if (items.length > 2) {
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:8px;">+ ' + (items.length - 2) + ' weitere Sätze</div>';
|
||
}
|
||
|
||
clozePreview.innerHTML = html;
|
||
}
|
||
|
||
function openClozeModal() {
|
||
if (!currentClozeData || !currentClozeData.cloze_items) {
|
||
alert('Keine Lückentexte vorhanden. Bitte zuerst generieren.');
|
||
return;
|
||
}
|
||
|
||
clozeAnswers = {}; // Reset Antworten
|
||
renderClozeModal(currentClozeData);
|
||
clozeModal.classList.remove('hidden');
|
||
}
|
||
|
||
function closeClozeModal() {
|
||
clozeModal.classList.add('hidden');
|
||
}
|
||
|
||
function renderClozeModal(clozeData) {
|
||
const items = clozeData.cloze_items;
|
||
const metadata = clozeData.metadata || {};
|
||
|
||
let html = '';
|
||
|
||
// Header
|
||
html += '<div class="cloze-stats" style="margin-bottom:16px;">';
|
||
if (metadata.source_title) {
|
||
html += '<div><strong>Arbeitsblatt:</strong> ' + metadata.source_title + '</div>';
|
||
}
|
||
if (metadata.total_gaps) {
|
||
html += '<div><strong>Lücken gesamt:</strong> ' + metadata.total_gaps + '</div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
html += '<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">Fülle die Lücken aus und klicke auf "Prüfen".</div>';
|
||
|
||
// Alle Sätze mit Eingabefeldern
|
||
items.forEach((item, idx) => {
|
||
html += '<div class="cloze-item" data-cid="' + item.id + '">';
|
||
|
||
// Satz mit Eingabefeldern statt ___
|
||
let sentenceHtml = item.sentence_with_gaps;
|
||
const gaps = item.gaps || [];
|
||
|
||
// Ersetze ___ durch Eingabefelder
|
||
let gapIndex = 0;
|
||
sentenceHtml = sentenceHtml.replace(/___/g, () => {
|
||
const gap = gaps[gapIndex] || { id: 'g' + gapIndex, word: '' };
|
||
const inputId = item.id + '_' + gap.id;
|
||
gapIndex++;
|
||
return '<input type="text" class="cloze-gap-input" data-cid="' + item.id + '" data-gid="' + gap.id + '" data-answer="' + gap.word + '" id="input_' + inputId + '" autocomplete="off">';
|
||
});
|
||
|
||
html += '<div class="cloze-sentence">' + (idx + 1) + '. ' + sentenceHtml + '</div>';
|
||
|
||
// Übersetzung als Hilfe
|
||
if (item.translation && item.translation.sentence_with_gaps) {
|
||
html += '<div class="cloze-translation">';
|
||
html += '<div class="cloze-translation-label">' + (item.translation.language_name || 'Übersetzung') + ' (mit Lücken):</div>';
|
||
html += item.translation.sentence_with_gaps;
|
||
html += '</div>';
|
||
}
|
||
|
||
html += '</div>';
|
||
});
|
||
|
||
// Buttons
|
||
html += '<div style="margin-top:16px;text-align:center;display:flex;gap:8px;justify-content:center;">';
|
||
html += '<button class="btn btn-primary" id="btn-cloze-check">Prüfen</button>';
|
||
html += '<button class="btn btn-ghost" id="btn-cloze-show-answers">Lösungen zeigen</button>';
|
||
html += '</div>';
|
||
|
||
clozeModalBody.innerHTML = html;
|
||
|
||
// Event-Listener für Prüfen-Button
|
||
const btnCheck = document.getElementById('btn-cloze-check');
|
||
if (btnCheck) {
|
||
btnCheck.addEventListener('click', checkClozeAnswers);
|
||
}
|
||
|
||
// Event-Listener für Lösungen zeigen
|
||
const btnShowAnswers = document.getElementById('btn-cloze-show-answers');
|
||
if (btnShowAnswers) {
|
||
btnShowAnswers.addEventListener('click', showClozeAnswers);
|
||
}
|
||
|
||
// Enter-Taste zum Prüfen
|
||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||
input.addEventListener('keypress', (e) => {
|
||
if (e.key === 'Enter') {
|
||
checkClozeAnswers();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function checkClozeAnswers() {
|
||
let correct = 0;
|
||
let total = 0;
|
||
|
||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||
const userAnswer = input.value.trim().toLowerCase();
|
||
const correctAnswer = input.getAttribute('data-answer').toLowerCase();
|
||
total++;
|
||
|
||
// Entferne vorherige Klassen
|
||
input.classList.remove('correct', 'incorrect');
|
||
|
||
if (userAnswer === correctAnswer) {
|
||
input.classList.add('correct');
|
||
correct++;
|
||
} else if (userAnswer !== '') {
|
||
input.classList.add('incorrect');
|
||
}
|
||
});
|
||
|
||
// Zeige Ergebnis
|
||
let existingResult = clozeModalBody.querySelector('.cloze-result');
|
||
if (existingResult) existingResult.remove();
|
||
|
||
const resultHtml = '<div class="cloze-result" style="margin-top:16px;padding:12px;background:rgba(15,23,42,0.6);border-radius:8px;text-align:center;">' +
|
||
'<div style="font-size:18px;font-weight:600;">' + correct + ' von ' + total + ' richtig</div>' +
|
||
'<div style="font-size:12px;color:var(--bp-text-muted);margin-top:4px;">' + Math.round(correct / total * 100) + '% korrekt</div>' +
|
||
'</div>';
|
||
|
||
const resultDiv = document.createElement('div');
|
||
resultDiv.innerHTML = resultHtml;
|
||
clozeModalBody.appendChild(resultDiv.firstChild);
|
||
}
|
||
|
||
function showClozeAnswers() {
|
||
clozeModalBody.querySelectorAll('.cloze-gap-input').forEach(input => {
|
||
const correctAnswer = input.getAttribute('data-answer');
|
||
input.value = correctAnswer;
|
||
input.classList.remove('incorrect');
|
||
input.classList.add('correct');
|
||
});
|
||
}
|
||
|
||
function openClozePrintDialog() {
|
||
if (!currentClozeData) {
|
||
alert(t('cloze_no_texts') || 'Keine Lückentexte vorhanden.');
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
|
||
// Öffne Druck-Optionen
|
||
const choice = confirm((t('cloze_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Mit ausgefüllten Lücken\\nAbbrechen = Übungsblatt mit Wortbank');
|
||
|
||
const url = '/api/print-cloze/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
// Event Listener für Cloze-Buttons
|
||
if (btnClozeGenerate) {
|
||
btnClozeGenerate.addEventListener('click', generateClozeTexts);
|
||
}
|
||
|
||
if (btnClozeShow) {
|
||
btnClozeShow.addEventListener('click', openClozeModal);
|
||
}
|
||
|
||
if (btnClozePrint) {
|
||
btnClozePrint.addEventListener('click', openClozePrintDialog);
|
||
}
|
||
|
||
if (clozeModalClose) {
|
||
clozeModalClose.addEventListener('click', closeClozeModal);
|
||
}
|
||
|
||
if (clozeModal) {
|
||
clozeModal.addEventListener('click', (ev) => {
|
||
if (ev.target === clozeModal) {
|
||
closeClozeModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- Mindmap Lernposter Logik ---
|
||
const btnMindmapGenerate = document.getElementById('btn-mindmap-generate');
|
||
const btnMindmapShow = document.getElementById('btn-mindmap-show');
|
||
const btnMindmapPrint = document.getElementById('btn-mindmap-print');
|
||
const mindmapPreview = document.getElementById('mindmap-preview');
|
||
const mindmapBadge = document.getElementById('mindmap-badge');
|
||
|
||
let currentMindmapData = null;
|
||
|
||
async function generateMindmap() {
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) {
|
||
setStatus('error', t('select_file_first') || 'Bitte zuerst eine Datei auswählen');
|
||
return;
|
||
}
|
||
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('generating') || 'Generiere...';
|
||
mindmapBadge.className = 'card-badge badge-working';
|
||
}
|
||
setStatus('working', t('generating_mindmap') || 'Erstelle Mindmap...');
|
||
|
||
try {
|
||
const resp = await fetch('/api/generate-mindmap/' + encodeURIComponent(currentFile), {
|
||
method: 'POST'
|
||
});
|
||
const data = await resp.json();
|
||
|
||
if (data.status === 'OK') {
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('ready') || 'Fertig';
|
||
mindmapBadge.className = 'card-badge badge-success';
|
||
}
|
||
setStatus('ok', t('mindmap_generated') || 'Mindmap erstellt!');
|
||
|
||
// Lade Mindmap-Daten
|
||
await loadMindmapData();
|
||
} else if (data.status === 'NOT_FOUND') {
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('no_analysis') || 'Keine Analyse';
|
||
mindmapBadge.className = 'card-badge badge-error';
|
||
}
|
||
setStatus('error', t('analyze_first') || 'Bitte zuerst analysieren (Neuaufbau starten)');
|
||
} else {
|
||
throw new Error(data.message || 'Fehler bei der Mindmap-Generierung');
|
||
}
|
||
} catch (err) {
|
||
console.error('Mindmap error:', err);
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('error') || 'Fehler';
|
||
mindmapBadge.className = 'card-badge badge-error';
|
||
}
|
||
setStatus('error', err.message);
|
||
}
|
||
}
|
||
|
||
async function loadMindmapData() {
|
||
if (!eingangFiles.length) {
|
||
if (mindmapPreview) mindmapPreview.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
|
||
try {
|
||
const resp = await fetch('/api/mindmap-data/' + encodeURIComponent(currentFile));
|
||
const data = await resp.json();
|
||
|
||
if (data.status === 'OK' && data.data) {
|
||
currentMindmapData = data.data;
|
||
renderMindmapPreview();
|
||
if (btnMindmapShow) btnMindmapShow.style.display = 'inline-block';
|
||
if (btnMindmapPrint) btnMindmapPrint.style.display = 'inline-block';
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('ready') || 'Fertig';
|
||
mindmapBadge.className = 'card-badge badge-success';
|
||
}
|
||
} else {
|
||
currentMindmapData = null;
|
||
if (mindmapPreview) mindmapPreview.innerHTML = '';
|
||
if (btnMindmapShow) btnMindmapShow.style.display = 'none';
|
||
if (btnMindmapPrint) btnMindmapPrint.style.display = 'none';
|
||
if (mindmapBadge) {
|
||
mindmapBadge.textContent = t('ready') || 'Bereit';
|
||
mindmapBadge.className = 'card-badge';
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('Error loading mindmap:', err);
|
||
}
|
||
}
|
||
|
||
function renderMindmapPreview() {
|
||
if (!mindmapPreview) return;
|
||
|
||
if (!currentMindmapData) {
|
||
mindmapPreview.innerHTML = '';
|
||
return;
|
||
}
|
||
|
||
const topic = currentMindmapData.topic || 'Thema';
|
||
const categories = currentMindmapData.categories || [];
|
||
const categoryCount = categories.length;
|
||
const termCount = categories.reduce((sum, cat) => sum + (cat.terms ? cat.terms.length : 0), 0);
|
||
|
||
mindmapPreview.innerHTML = '<div style="margin-top:10px;padding:12px;background:linear-gradient(135deg,#f0f9ff,#e0f2fe);border-radius:10px;text-align:center;">' +
|
||
'<div style="font-size:18px;font-weight:bold;color:#0369a1;margin-bottom:8px;">' + topic + '</div>' +
|
||
'<div style="font-size:12px;color:#64748b;">' + categoryCount + ' ' + (t('categories') || 'Kategorien') + ' | ' + termCount + ' ' + (t('terms') || 'Begriffe') + '</div>' +
|
||
'</div>';
|
||
}
|
||
|
||
function openMindmapView() {
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a4', '_blank');
|
||
}
|
||
|
||
function openMindmapPrint() {
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
window.open('/api/mindmap-html/' + encodeURIComponent(currentFile) + '?format=a3', '_blank');
|
||
}
|
||
|
||
if (btnMindmapGenerate) {
|
||
btnMindmapGenerate.addEventListener('click', generateMindmap);
|
||
}
|
||
|
||
if (btnMindmapShow) {
|
||
btnMindmapShow.addEventListener('click', openMindmapView);
|
||
}
|
||
|
||
if (btnMindmapPrint) {
|
||
btnMindmapPrint.addEventListener('click', openMindmapPrint);
|
||
}
|
||
|
||
// --- Frage-Antwort (Q&A) mit Leitner-System ---
|
||
const btnQaGenerate = document.getElementById('btn-qa-generate');
|
||
const btnQaLearn = document.getElementById('btn-qa-learn');
|
||
const btnQaPrint = document.getElementById('btn-qa-print');
|
||
const qaPreview = document.getElementById('qa-preview');
|
||
const qaBadge = document.getElementById('qa-badge');
|
||
const qaModal = document.getElementById('qa-modal');
|
||
const qaModalBody = document.getElementById('qa-modal-body');
|
||
const qaModalClose = document.getElementById('qa-modal-close');
|
||
|
||
let currentQaData = null;
|
||
let currentQaIndex = 0;
|
||
let qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
||
|
||
async function generateQaQuestions() {
|
||
try {
|
||
setStatus(t('status_generating_qa') || 'Generiere Q&A …', t('status_please_wait'), 'busy');
|
||
if (qaBadge) qaBadge.textContent = t('mc_generating');
|
||
|
||
const resp = await fetch('/api/generate-qa', { method: 'POST' });
|
||
if (!resp.ok) {
|
||
throw new Error('HTTP ' + resp.status);
|
||
}
|
||
|
||
const result = await resp.json();
|
||
if (result.status === 'OK' && result.generated.length > 0) {
|
||
setStatus(t('status_qa_generated') || 'Q&A generiert', result.generated.length + ' ' + t('status_files_created'));
|
||
if (qaBadge) qaBadge.textContent = t('mc_done');
|
||
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
|
||
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
|
||
|
||
await loadQaPreviewForCurrent();
|
||
} else if (result.errors && result.errors.length > 0) {
|
||
setStatus(t('error'), result.errors[0].error, 'error');
|
||
if (qaBadge) qaBadge.textContent = t('mc_error');
|
||
} else {
|
||
setStatus(t('error'), 'Keine Q&A generiert.', 'error');
|
||
if (qaBadge) qaBadge.textContent = t('mc_ready');
|
||
}
|
||
} catch (e) {
|
||
console.error('Q&A-Generierung fehlgeschlagen:', e);
|
||
setStatus(t('error'), String(e), 'error');
|
||
if (qaBadge) qaBadge.textContent = t('mc_error');
|
||
}
|
||
}
|
||
|
||
async function loadQaPreviewForCurrent() {
|
||
if (!eingangFiles.length) {
|
||
if (qaPreview) qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + t('qa_no_questions') + '</div>';
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
if (!currentFile) return;
|
||
|
||
try {
|
||
const resp = await fetch('/api/qa-data/' + encodeURIComponent(currentFile));
|
||
const result = await resp.json();
|
||
|
||
if (result.status === 'OK' && result.data) {
|
||
currentQaData = result.data;
|
||
renderQaPreview(result.data);
|
||
if (btnQaLearn) btnQaLearn.style.display = 'inline-block';
|
||
if (btnQaPrint) btnQaPrint.style.display = 'inline-block';
|
||
} else {
|
||
if (qaPreview) qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_no_questions') || 'Noch keine Q&A für dieses Arbeitsblatt generiert.') + '</div>';
|
||
currentQaData = null;
|
||
if (btnQaLearn) btnQaLearn.style.display = 'none';
|
||
if (btnQaPrint) btnQaPrint.style.display = 'none';
|
||
}
|
||
} catch (e) {
|
||
console.error('Fehler beim Laden der Q&A-Daten:', e);
|
||
if (qaPreview) qaPreview.innerHTML = '';
|
||
}
|
||
}
|
||
|
||
function renderQaPreview(qaData) {
|
||
if (!qaPreview) return;
|
||
if (!qaData || !qaData.qa_items || qaData.qa_items.length === 0) {
|
||
qaPreview.innerHTML = '<div style="font-size:11px;color:var(--bp-text-muted);">' + t('qa_no_questions') + '</div>';
|
||
return;
|
||
}
|
||
|
||
const items = qaData.qa_items;
|
||
const metadata = qaData.metadata || {};
|
||
|
||
let html = '';
|
||
|
||
// Leitner-Box Statistiken
|
||
html += '<div class="mc-stats" style="margin-bottom:8px;">';
|
||
|
||
// Zähle Fragen nach Box
|
||
let box0 = 0, box1 = 0, box2 = 0;
|
||
items.forEach(item => {
|
||
const box = item.leitner ? item.leitner.box : 0;
|
||
if (box === 0) box0++;
|
||
else if (box === 1) box1++;
|
||
else box2++;
|
||
});
|
||
|
||
html += '<div style="display:flex;gap:12px;font-size:11px;">';
|
||
html += '<div style="color:#ef4444;">' + (t('qa_box_new') || 'Neu') + ': ' + box0 + '</div>';
|
||
html += '<div style="color:#f59e0b;">' + (t('qa_box_learning') || 'Lernt') + ': ' + box1 + '</div>';
|
||
html += '<div style="color:#22c55e;">' + (t('qa_box_mastered') || 'Gefestigt') + ': ' + box2 + '</div>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
|
||
// Zeige erste 2 Fragen als Vorschau
|
||
const previewItems = items.slice(0, 2);
|
||
previewItems.forEach((item, idx) => {
|
||
html += '<div class="mc-question" style="padding:8px;margin-bottom:6px;background:rgba(255,255,255,0.03);border-radius:6px;">';
|
||
html += '<div style="font-size:12px;font-weight:500;margin-bottom:4px;">' + (idx + 1) + '. ' + item.question + '</div>';
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);">→ ' + item.answer.substring(0, 60) + (item.answer.length > 60 ? '...' : '') + '</div>';
|
||
html += '</div>';
|
||
});
|
||
|
||
if (items.length > 2) {
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);text-align:center;margin-top:4px;">+ ' + (items.length - 2) + ' ' + (t('questions') || 'weitere Fragen') + '</div>';
|
||
}
|
||
|
||
qaPreview.innerHTML = html;
|
||
}
|
||
|
||
function openQaModal() {
|
||
if (!currentQaData || !currentQaData.qa_items) {
|
||
alert(t('qa_no_questions') || 'Keine Q&A vorhanden. Bitte zuerst generieren.');
|
||
return;
|
||
}
|
||
|
||
currentQaIndex = 0;
|
||
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
||
renderQaLearningCard();
|
||
qaModal.classList.remove('hidden');
|
||
}
|
||
|
||
function closeQaModal() {
|
||
qaModal.classList.add('hidden');
|
||
}
|
||
|
||
function renderQaLearningCard() {
|
||
const items = currentQaData.qa_items;
|
||
|
||
if (currentQaIndex >= items.length) {
|
||
// Alle Fragen durch - Zeige Zusammenfassung
|
||
renderQaSessionSummary();
|
||
return;
|
||
}
|
||
|
||
const item = items[currentQaIndex];
|
||
const leitner = item.leitner || { box: 0 };
|
||
const boxNames = [t('qa_box_new') || 'Neu', t('qa_box_learning') || 'Gelernt', t('qa_box_mastered') || 'Gefestigt'];
|
||
const boxColors = ['#ef4444', '#f59e0b', '#22c55e'];
|
||
|
||
let html = '';
|
||
|
||
// Fortschrittsanzeige
|
||
html += '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">';
|
||
html += '<div style="font-size:12px;color:var(--bp-text-muted);">' + (t('question') || 'Frage') + ' ' + (currentQaIndex + 1) + ' / ' + items.length + '</div>';
|
||
html += '<div style="display:flex;align-items:center;gap:6px;">';
|
||
html += '<span style="font-size:10px;color:' + boxColors[leitner.box] + ';background:' + boxColors[leitner.box] + '22;padding:2px 8px;border-radius:10px;">' + boxNames[leitner.box] + '</span>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
|
||
// Frage
|
||
html += '<div style="background:rgba(255,255,255,0.05);padding:20px;border-radius:12px;margin-bottom:16px;">';
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">' + (t('question') || 'Frage') + ':</div>';
|
||
html += '<div style="font-size:16px;font-weight:500;line-height:1.5;">' + item.question + '</div>';
|
||
html += '</div>';
|
||
|
||
// Eingabefeld für eigene Antwort
|
||
html += '<div id="qa-input-container" style="margin-bottom:16px;">';
|
||
html += '<div style="font-size:11px;color:var(--bp-text-muted);margin-bottom:8px;">' + (t('qa_your_answer') || 'Deine Antwort') + ':</div>';
|
||
html += '<textarea id="qa-user-answer" style="width:100%;min-height:80px;padding:12px;border:1px solid rgba(255,255,255,0.2);border-radius:8px;background:rgba(255,255,255,0.05);color:var(--bp-text);font-size:14px;resize:vertical;font-family:inherit;" placeholder="' + (t('qa_type_answer') || 'Schreibe deine Antwort hier...') + '"></textarea>';
|
||
html += '</div>';
|
||
|
||
// Prüfen-Button
|
||
html += '<div id="qa-check-btn-container" style="text-align:center;margin-bottom:16px;">';
|
||
html += '<button class="btn btn-primary" id="btn-qa-check-answer" style="padding:12px 32px;">' + (t('qa_check_answer') || 'Antwort prüfen') + '</button>';
|
||
html += '</div>';
|
||
|
||
// Vergleichs-Container (versteckt)
|
||
html += '<div id="qa-comparison-container" style="display:none;">';
|
||
|
||
// Eigene Antwort (wird befüllt)
|
||
html += '<div id="qa-user-answer-display" style="background:rgba(59,130,246,0.1);padding:16px;border-radius:12px;margin-bottom:12px;border-left:3px solid #3b82f6;">';
|
||
html += '<div style="font-size:11px;color:#3b82f6;margin-bottom:8px;">' + (t('qa_your_answer') || 'Deine Antwort') + ':</div>';
|
||
html += '<div id="qa-user-answer-text" style="font-size:14px;line-height:1.5;"></div>';
|
||
html += '</div>';
|
||
|
||
// Richtige Antwort
|
||
html += '<div style="background:rgba(34,197,94,0.1);padding:16px;border-radius:12px;margin-bottom:16px;border-left:3px solid #22c55e;">';
|
||
html += '<div style="font-size:11px;color:#22c55e;margin-bottom:8px;">' + (t('qa_correct_answer') || 'Richtige Antwort') + ':</div>';
|
||
html += '<div style="font-size:14px;line-height:1.5;">' + item.answer + '</div>';
|
||
if (item.key_terms && item.key_terms.length > 0) {
|
||
html += '<div style="margin-top:12px;font-size:11px;color:var(--bp-text-muted);">' + (t('qa_key_terms') || 'Schlüsselbegriffe') + ': <span style="color:#22c55e;">' + item.key_terms.join(', ') + '</span></div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// Selbstbewertung
|
||
html += '<div style="text-align:center;margin-bottom:8px;">';
|
||
html += '<div style="font-size:12px;color:var(--bp-text-muted);margin-bottom:12px;">' + (t('qa_self_evaluate') || 'War deine Antwort richtig?') + '</div>';
|
||
html += '<div style="display:flex;gap:12px;justify-content:center;">';
|
||
html += '<button class="btn" id="btn-qa-incorrect" style="background:#ef4444;padding:12px 24px;">' + (t('qa_incorrect') || 'Falsch') + '</button>';
|
||
html += '<button class="btn" id="btn-qa-correct" style="background:#22c55e;padding:12px 24px;">' + (t('qa_correct') || 'Richtig') + '</button>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
|
||
html += '</div>'; // Ende qa-comparison-container
|
||
|
||
// Session-Statistik
|
||
html += '<div style="margin-top:16px;padding-top:12px;border-top:1px solid rgba(255,255,255,0.1);display:flex;justify-content:center;gap:20px;font-size:11px;">';
|
||
html += '<div style="color:#22c55e;">' + (t('qa_session_correct') || 'Richtig') + ': ' + qaSessionStats.correct + '</div>';
|
||
html += '<div style="color:#ef4444;">' + (t('qa_session_incorrect') || 'Falsch') + ': ' + qaSessionStats.incorrect + '</div>';
|
||
html += '</div>';
|
||
|
||
qaModalBody.innerHTML = html;
|
||
|
||
// Event Listener für Prüfen-Button
|
||
document.getElementById('btn-qa-check-answer').addEventListener('click', () => {
|
||
const userAnswer = document.getElementById('qa-user-answer').value.trim();
|
||
|
||
// Zeige die eigene Antwort im Vergleich
|
||
document.getElementById('qa-user-answer-text').textContent = userAnswer || (t('qa_no_answer') || '(keine Antwort eingegeben)');
|
||
|
||
// Verstecke Eingabe, zeige Vergleich
|
||
document.getElementById('qa-input-container').style.display = 'none';
|
||
document.getElementById('qa-check-btn-container').style.display = 'none';
|
||
document.getElementById('qa-comparison-container').style.display = 'block';
|
||
});
|
||
|
||
// Enter-Taste im Textfeld löst Prüfen aus
|
||
document.getElementById('qa-user-answer').addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
document.getElementById('btn-qa-check-answer').click();
|
||
}
|
||
});
|
||
|
||
// Fokus auf Textfeld setzen
|
||
setTimeout(() => {
|
||
document.getElementById('qa-user-answer').focus();
|
||
}, 100);
|
||
|
||
document.getElementById('btn-qa-correct').addEventListener('click', () => handleQaAnswer(true));
|
||
document.getElementById('btn-qa-incorrect').addEventListener('click', () => handleQaAnswer(false));
|
||
}
|
||
|
||
async function handleQaAnswer(correct) {
|
||
const item = currentQaData.qa_items[currentQaIndex];
|
||
|
||
// Update Session-Statistik
|
||
qaSessionStats.total++;
|
||
if (correct) qaSessionStats.correct++;
|
||
else qaSessionStats.incorrect++;
|
||
|
||
// Update Leitner-Fortschritt auf dem Server
|
||
try {
|
||
const currentFile = eingangFiles[currentIndex];
|
||
await fetch('/api/qa-progress?filename=' + encodeURIComponent(currentFile) + '&item_id=' + encodeURIComponent(item.id) + '&correct=' + correct, {
|
||
method: 'POST'
|
||
});
|
||
} catch (e) {
|
||
console.error('Fehler beim Speichern des Fortschritts:', e);
|
||
}
|
||
|
||
// Nächste Frage
|
||
currentQaIndex++;
|
||
renderQaLearningCard();
|
||
}
|
||
|
||
function renderQaSessionSummary() {
|
||
const percent = qaSessionStats.total > 0 ? Math.round(qaSessionStats.correct / qaSessionStats.total * 100) : 0;
|
||
|
||
let html = '';
|
||
html += '<div style="text-align:center;padding:20px;">';
|
||
html += '<div style="font-size:48px;margin-bottom:16px;">' + (percent >= 80 ? '🎉' : percent >= 50 ? '👍' : '💪') + '</div>';
|
||
html += '<div style="font-size:24px;font-weight:600;margin-bottom:8px;">' + (t('qa_session_complete') || 'Lernrunde abgeschlossen!') + '</div>';
|
||
html += '<div style="font-size:18px;margin-bottom:24px;">' + qaSessionStats.correct + ' / ' + qaSessionStats.total + ' ' + (t('qa_result_correct') || 'richtig') + ' (' + percent + '%)</div>';
|
||
|
||
// Statistik
|
||
html += '<div style="display:flex;justify-content:center;gap:24px;margin-bottom:24px;">';
|
||
html += '<div style="text-align:center;"><div style="font-size:32px;color:#22c55e;">' + qaSessionStats.correct + '</div><div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_correct') || 'Richtig') + '</div></div>';
|
||
html += '<div style="text-align:center;"><div style="font-size:32px;color:#ef4444;">' + qaSessionStats.incorrect + '</div><div style="font-size:11px;color:var(--bp-text-muted);">' + (t('qa_incorrect') || 'Falsch') + '</div></div>';
|
||
html += '</div>';
|
||
|
||
html += '<div style="display:flex;gap:12px;justify-content:center;">';
|
||
html += '<button class="btn btn-primary" id="btn-qa-restart">' + (t('qa_restart') || 'Nochmal lernen') + '</button>';
|
||
html += '<button class="btn btn-ghost" id="btn-qa-close-summary">' + (t('close') || 'Schließen') + '</button>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
|
||
qaModalBody.innerHTML = html;
|
||
|
||
document.getElementById('btn-qa-restart').addEventListener('click', () => {
|
||
currentQaIndex = 0;
|
||
qaSessionStats = { correct: 0, incorrect: 0, total: 0 };
|
||
renderQaLearningCard();
|
||
});
|
||
|
||
document.getElementById('btn-qa-close-summary').addEventListener('click', closeQaModal);
|
||
|
||
// Aktualisiere Preview nach Session
|
||
loadQaPreviewForCurrent();
|
||
}
|
||
|
||
function openQaPrintDialog() {
|
||
if (!currentQaData) {
|
||
alert(t('qa_no_questions') || 'Keine Q&A vorhanden.');
|
||
return;
|
||
}
|
||
|
||
const currentFile = eingangFiles[currentIndex];
|
||
const baseName = currentFile.split('.')[0];
|
||
|
||
// Öffne Druck-Optionen
|
||
const choice = confirm((t('qa_print_with_answers') || 'Mit Lösungen drucken?') + '\\n\\nOK = Mit Lösungen\\nAbbrechen = Nur Fragen');
|
||
|
||
const url = '/api/print-qa/' + encodeURIComponent(currentFile) + '?show_answers=' + choice;
|
||
window.open(url, '_blank');
|
||
}
|
||
|
||
// Event Listener für Q&A-Buttons
|
||
if (btnQaGenerate) {
|
||
btnQaGenerate.addEventListener('click', generateQaQuestions);
|
||
}
|
||
|
||
if (btnQaLearn) {
|
||
btnQaLearn.addEventListener('click', openQaModal);
|
||
}
|
||
|
||
if (btnQaPrint) {
|
||
btnQaPrint.addEventListener('click', openQaPrintDialog);
|
||
}
|
||
|
||
if (qaModalClose) {
|
||
qaModalClose.addEventListener('click', closeQaModal);
|
||
}
|
||
|
||
if (qaModal) {
|
||
qaModal.addEventListener('click', (ev) => {
|
||
if (ev.target === qaModal) {
|
||
closeQaModal();
|
||
}
|
||
});
|
||
}
|
||
|
||
// --- Sprachauswahl Event Listener ---
|
||
const languageSelect = document.getElementById('language-select');
|
||
if (languageSelect) {
|
||
// Setze initiale Auswahl basierend auf gespeicherter Sprache
|
||
languageSelect.value = currentLang;
|
||
|
||
languageSelect.addEventListener('change', (e) => {
|
||
applyLanguage(e.target.value);
|
||
});
|
||
}
|
||
|
||
// --- Initial ---
|
||
async function init() {
|
||
// Theme Toggle initialisieren
|
||
initThemeToggle();
|
||
|
||
// Sprache anwenden (aus localStorage oder default)
|
||
applyLanguage(currentLang);
|
||
|
||
updateScreen();
|
||
updateTiles();
|
||
await loadEingangFiles();
|
||
await loadWorksheetPairs();
|
||
await loadLearningUnits();
|
||
// Lade MC-Vorschau für aktuelle Datei
|
||
await loadMcPreviewForCurrent();
|
||
// Lade Lückentext-Vorschau
|
||
await loadClozePreviewForCurrent();
|
||
// Lade Q&A-Vorschau
|
||
await loadQaPreviewForCurrent();
|
||
// Lade Mindmap-Vorschau
|
||
await loadMindmapData();
|
||
}
|
||
|
||
init();
|
||
</script>
|
||
|
||
<!-- Admin Panel Modal -->
|
||
<div id="admin-modal" class="admin-modal">
|
||
<div class="admin-modal-content">
|
||
<div class="admin-modal-header">
|
||
<h2><span>⚙️</span> Consent Admin Panel</h2>
|
||
<button id="admin-modal-close" class="legal-modal-close">×</button>
|
||
</div>
|
||
<div class="admin-tabs">
|
||
<button class="admin-tab active" data-tab="documents">Dokumente</button>
|
||
<button class="admin-tab" data-tab="versions">Versionen</button>
|
||
<button class="admin-tab" data-tab="cookies">Cookie-Kategorien</button>
|
||
<button class="admin-tab" data-tab="stats">Statistiken</button>
|
||
<button class="admin-tab" data-tab="emails">E-Mail Vorlagen</button>
|
||
<button class="admin-tab" data-tab="dsms">DSMS</button>
|
||
</div>
|
||
<div class="admin-body">
|
||
<!-- Documents Tab -->
|
||
<div id="admin-documents" class="admin-content active">
|
||
<div class="admin-toolbar">
|
||
<div class="admin-toolbar-left">
|
||
<input type="text" class="admin-search" placeholder="Dokumente suchen..." id="admin-doc-search">
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" onclick="showDocumentForm()">+ Neues Dokument</button>
|
||
</div>
|
||
|
||
<!-- Document Creation Form -->
|
||
<div id="admin-document-form" class="admin-form" style="display: none;">
|
||
<h3 class="admin-form-title" id="admin-document-form-title">Neues Dokument erstellen</h3>
|
||
<input type="hidden" id="admin-document-id">
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Dokumenttyp *</label>
|
||
<select class="admin-form-select" id="admin-document-type">
|
||
<option value="">-- Typ auswählen --</option>
|
||
<option value="terms">AGB (Allgemeine Geschäftsbedingungen)</option>
|
||
<option value="privacy">Datenschutzerklärung</option>
|
||
<option value="cookies">Cookie-Richtlinie</option>
|
||
<option value="community">Community Guidelines</option>
|
||
<option value="imprint">Impressum</option>
|
||
</select>
|
||
</div>
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Name *</label>
|
||
<input type="text" class="admin-form-input" id="admin-document-name" placeholder="z.B. Allgemeine Geschäftsbedingungen">
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group full-width">
|
||
<label class="admin-form-label">Beschreibung</label>
|
||
<input type="text" class="admin-form-input" id="admin-document-description" placeholder="Kurze Beschreibung des Dokuments">
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">
|
||
<input type="checkbox" id="admin-document-mandatory" style="margin-right: 8px;">
|
||
Pflichtdokument (Nutzer müssen zustimmen)
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-actions">
|
||
<button class="btn btn-ghost btn-sm" onclick="hideDocumentForm()">Abbrechen</button>
|
||
<button class="btn btn-primary btn-sm" onclick="saveDocument()">Dokument erstellen</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="admin-doc-table-container">
|
||
<div class="admin-loading">Lade Dokumente...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Versions Tab -->
|
||
<div id="admin-versions" class="admin-content">
|
||
<div class="admin-toolbar">
|
||
<div class="admin-toolbar-left">
|
||
<select class="admin-form-select" id="admin-version-doc-select" onchange="loadVersionsForDocument()">
|
||
<option value="">-- Dokument auswählen --</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" onclick="showVersionForm()" id="btn-new-version" disabled>+ Neue Version</button>
|
||
</div>
|
||
|
||
<div id="admin-version-form" class="admin-form">
|
||
<h3 class="admin-form-title" id="admin-version-form-title">Neue Version erstellen</h3>
|
||
<input type="hidden" id="admin-version-id">
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Version *</label>
|
||
<input type="text" class="admin-form-input" id="admin-version-number" placeholder="z.B. 1.0.0">
|
||
</div>
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Sprache *</label>
|
||
<select class="admin-form-select" id="admin-version-lang">
|
||
<option value="de">Deutsch</option>
|
||
<option value="en">English</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group full-width">
|
||
<label class="admin-form-label">Titel *</label>
|
||
<input type="text" class="admin-form-input" id="admin-version-title" placeholder="Titel der Version">
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group full-width">
|
||
<label class="admin-form-label">Zusammenfassung</label>
|
||
<input type="text" class="admin-form-input" id="admin-version-summary" placeholder="Kurze Zusammenfassung der Änderungen">
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group full-width">
|
||
<label class="admin-form-label">Inhalt *</label>
|
||
<div class="editor-container">
|
||
<div class="editor-toolbar">
|
||
<div class="editor-toolbar-group">
|
||
<button type="button" class="editor-btn" onclick="formatDoc('bold')" title="Fett (Strg+B)"><b>B</b></button>
|
||
<button type="button" class="editor-btn" onclick="formatDoc('italic')" title="Kursiv (Strg+I)"><i>I</i></button>
|
||
<button type="button" class="editor-btn" onclick="formatDoc('underline')" title="Unterstrichen (Strg+U)"><u>U</u></button>
|
||
</div>
|
||
<div class="editor-toolbar-group">
|
||
<button type="button" class="editor-btn" onclick="formatBlock('h1')" title="Überschrift 1">H1</button>
|
||
<button type="button" class="editor-btn" onclick="formatBlock('h2')" title="Überschrift 2">H2</button>
|
||
<button type="button" class="editor-btn" onclick="formatBlock('h3')" title="Überschrift 3">H3</button>
|
||
<button type="button" class="editor-btn" onclick="formatBlock('p')" title="Absatz">P</button>
|
||
</div>
|
||
<div class="editor-toolbar-group">
|
||
<button type="button" class="editor-btn" onclick="formatDoc('insertUnorderedList')" title="Aufzählung">• Liste</button>
|
||
<button type="button" class="editor-btn" onclick="formatDoc('insertOrderedList')" title="Nummerierung">1. Liste</button>
|
||
</div>
|
||
<div class="editor-toolbar-group">
|
||
<button type="button" class="editor-btn" onclick="insertLink()" title="Link einfügen">🔗</button>
|
||
<button type="button" class="editor-btn" onclick="formatDoc('formatBlock', 'blockquote')" title="Zitat">❝</button>
|
||
<button type="button" class="editor-btn" onclick="formatDoc('insertHorizontalRule')" title="Trennlinie">—</button>
|
||
</div>
|
||
<div class="editor-toolbar-group">
|
||
<button type="button" class="editor-btn editor-btn-upload" onclick="document.getElementById('word-upload').click()" title="Word-Dokument importieren">📄 Word Import</button>
|
||
<input type="file" id="word-upload" class="word-upload-input" accept=".docx,.doc" onchange="handleWordUpload(event)">
|
||
</div>
|
||
</div>
|
||
<div id="admin-version-editor" class="editor-content" contenteditable="true" placeholder="Schreiben Sie hier den Inhalt..."></div>
|
||
<div class="editor-status">
|
||
<span id="editor-char-count">0 Zeichen</span> |
|
||
<span style="color: var(--bp-text-muted);">Tipp: Sie können direkt aus Word kopieren und einfügen!</span>
|
||
</div>
|
||
</div>
|
||
<input type="hidden" id="admin-version-content">
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-actions">
|
||
<button class="btn btn-ghost btn-sm" onclick="hideVersionForm()">Abbrechen</button>
|
||
<button class="btn btn-primary btn-sm" onclick="saveVersion()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="admin-version-table-container">
|
||
<div class="admin-empty">Wählen Sie ein Dokument aus, um dessen Versionen anzuzeigen.</div>
|
||
</div>
|
||
|
||
<!-- Approval Dialog -->
|
||
<div id="approval-dialog" class="admin-dialog">
|
||
<div class="admin-dialog-content">
|
||
<h3>Version genehmigen</h3>
|
||
<p class="admin-dialog-info">
|
||
Legen Sie einen Veröffentlichungszeitpunkt fest. Die Version wird automatisch zum gewählten Zeitpunkt veröffentlicht.
|
||
</p>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Veröffentlichungsdatum *</label>
|
||
<input type="date" class="admin-form-input" id="approval-date" required>
|
||
</div>
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Uhrzeit</label>
|
||
<input type="time" class="admin-form-input" id="approval-time" value="00:00">
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group full-width">
|
||
<label class="admin-form-label">Kommentar (optional)</label>
|
||
<input type="text" class="admin-form-input" id="approval-comment" placeholder="z.B. Genehmigt nach rechtlicher Prüfung">
|
||
</div>
|
||
</div>
|
||
<div class="admin-dialog-actions">
|
||
<button class="btn btn-ghost btn-sm" onclick="hideApprovalDialog()">Abbrechen</button>
|
||
<button class="btn btn-primary btn-sm" onclick="submitApproval()">Genehmigen & Planen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Version Compare View (Full Screen Overlay) -->
|
||
<div id="version-compare-view" class="version-compare-overlay">
|
||
<div class="version-compare-header">
|
||
<h2>Versionsvergleich</h2>
|
||
<div class="version-compare-info">
|
||
<span id="compare-published-info"></span>
|
||
<span class="compare-vs">vs</span>
|
||
<span id="compare-draft-info"></span>
|
||
</div>
|
||
<button class="btn btn-ghost" onclick="hideCompareView()">Schließen</button>
|
||
</div>
|
||
<div class="version-compare-container">
|
||
<div class="version-compare-panel">
|
||
<div class="version-compare-panel-header">
|
||
<span class="compare-label compare-label-published">Veröffentlichte Version</span>
|
||
<span id="compare-published-version"></span>
|
||
</div>
|
||
<div class="version-compare-content" id="compare-content-left"></div>
|
||
</div>
|
||
<div class="version-compare-panel">
|
||
<div class="version-compare-panel-header">
|
||
<span class="compare-label compare-label-draft">Neue Version</span>
|
||
<span id="compare-draft-version"></span>
|
||
</div>
|
||
<div class="version-compare-content" id="compare-content-right"></div>
|
||
</div>
|
||
</div>
|
||
<div class="version-compare-footer">
|
||
<div id="compare-history-container"></div>
|
||
<div id="compare-actions-container" style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 12px;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cookie Categories Tab -->
|
||
<div id="admin-cookies" class="admin-content">
|
||
<div class="admin-toolbar">
|
||
<div class="admin-toolbar-left">
|
||
<span style="color: var(--bp-text-muted);">Cookie-Kategorien verwalten</span>
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" onclick="showCookieForm()">+ Neue Kategorie</button>
|
||
</div>
|
||
|
||
<div id="admin-cookie-form" class="admin-form">
|
||
<h3 class="admin-form-title">Neue Cookie-Kategorie</h3>
|
||
<input type="hidden" id="admin-cookie-id">
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Technischer Name *</label>
|
||
<input type="text" class="admin-form-input" id="admin-cookie-name" placeholder="z.B. analytics">
|
||
</div>
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Anzeigename (DE) *</label>
|
||
<input type="text" class="admin-form-input" id="admin-cookie-display-de" placeholder="z.B. Analyse-Cookies">
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Anzeigename (EN)</label>
|
||
<input type="text" class="admin-form-input" id="admin-cookie-display-en" placeholder="z.B. Analytics Cookies">
|
||
</div>
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">
|
||
<input type="checkbox" id="admin-cookie-mandatory"> Notwendig (kann nicht deaktiviert werden)
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group full-width">
|
||
<label class="admin-form-label">Beschreibung (DE)</label>
|
||
<input type="text" class="admin-form-input" id="admin-cookie-desc-de" placeholder="Beschreibung auf Deutsch">
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-actions">
|
||
<button class="btn btn-ghost btn-sm" onclick="hideCookieForm()">Abbrechen</button>
|
||
<button class="btn btn-primary btn-sm" onclick="saveCookieCategory()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="admin-cookie-table-container">
|
||
<div class="admin-loading">Lade Cookie-Kategorien...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Statistics Tab -->
|
||
<div id="admin-stats" class="admin-content">
|
||
<div id="admin-stats-container">
|
||
<div class="admin-loading">Lade Statistiken...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- E-Mail Templates Tab -->
|
||
<div id="admin-emails" class="admin-content">
|
||
<div class="admin-toolbar">
|
||
<div class="admin-toolbar-left">
|
||
<select class="admin-form-select" id="email-template-select" onchange="loadEmailTemplateVersions()">
|
||
<option value="">-- E-Mail-Vorlage auswählen --</option>
|
||
</select>
|
||
</div>
|
||
<button class="btn btn-ghost btn-sm" onclick="initializeEmailTemplates()">Templates initialisieren</button>
|
||
<button class="btn btn-primary btn-sm" onclick="showEmailVersionForm()" id="btn-new-email-version" disabled>+ Neue Version</button>
|
||
</div>
|
||
|
||
<!-- E-Mail Template Info Card -->
|
||
<div id="email-template-info" style="display: none; margin-bottom: 16px;">
|
||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px; border: 1px solid var(--bp-border);">
|
||
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||
<div>
|
||
<h3 style="margin: 0 0 8px 0; font-size: 16px;" id="email-template-name">-</h3>
|
||
<p style="margin: 0; color: var(--bp-text-muted); font-size: 13px;" id="email-template-description">-</p>
|
||
</div>
|
||
<div style="text-align: right;">
|
||
<div class="admin-badge" id="email-template-type-badge">-</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--bp-border);">
|
||
<span style="font-size: 12px; color: var(--bp-text-muted);">Variablen: </span>
|
||
<span id="email-template-variables" style="font-size: 12px; font-family: monospace; color: var(--bp-primary);"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- E-Mail Version Form -->
|
||
<div id="email-version-form" class="admin-form" style="display: none;">
|
||
<h3 class="admin-form-title" id="email-version-form-title">Neue E-Mail-Version erstellen</h3>
|
||
<input type="hidden" id="email-version-id">
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Version *</label>
|
||
<input type="text" class="admin-form-input" id="email-version-number" placeholder="z.B. 1.0.0">
|
||
</div>
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Sprache *</label>
|
||
<select class="admin-form-select" id="email-version-lang">
|
||
<option value="de">Deutsch</option>
|
||
<option value="en">English</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group full-width">
|
||
<label class="admin-form-label">Betreff *</label>
|
||
<input type="text" class="admin-form-input" id="email-version-subject" placeholder="E-Mail Betreff (kann Variablen enthalten)">
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group full-width">
|
||
<label class="admin-form-label">HTML-Inhalt *</label>
|
||
<div class="editor-container">
|
||
<div class="editor-toolbar">
|
||
<div class="editor-toolbar-group">
|
||
<button type="button" class="editor-btn" onclick="formatEmailDoc('bold')" title="Fett"><b>B</b></button>
|
||
<button type="button" class="editor-btn" onclick="formatEmailDoc('italic')" title="Kursiv"><i>I</i></button>
|
||
<button type="button" class="editor-btn" onclick="formatEmailDoc('underline')" title="Unterstrichen"><u>U</u></button>
|
||
</div>
|
||
<div class="editor-toolbar-group">
|
||
<button type="button" class="editor-btn" onclick="formatEmailBlock('h1')" title="Überschrift 1">H1</button>
|
||
<button type="button" class="editor-btn" onclick="formatEmailBlock('h2')" title="Überschrift 2">H2</button>
|
||
<button type="button" class="editor-btn" onclick="formatEmailBlock('p')" title="Absatz">P</button>
|
||
</div>
|
||
<div class="editor-toolbar-group">
|
||
<button type="button" class="editor-btn" onclick="insertEmailVariable()" title="Variable einfügen">{{var}}</button>
|
||
<button type="button" class="editor-btn" onclick="insertEmailLink()" title="Link einfügen">🔗</button>
|
||
<button type="button" class="editor-btn" onclick="insertEmailButton()" title="Button einfügen">🔘</button>
|
||
</div>
|
||
</div>
|
||
<div id="email-version-editor" class="editor-content" contenteditable="true" placeholder="HTML-Inhalt der E-Mail..." style="min-height: 200px;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group full-width">
|
||
<label class="admin-form-label">Text-Version (Plain Text)</label>
|
||
<textarea class="admin-form-input" id="email-version-text" rows="5" placeholder="Plain-Text-Version der E-Mail (optional, wird aus HTML generiert falls leer)"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-actions">
|
||
<button class="btn btn-ghost btn-sm" onclick="hideEmailVersionForm()">Abbrechen</button>
|
||
<button class="btn btn-ghost btn-sm" onclick="previewEmailVersion()">Vorschau</button>
|
||
<button class="btn btn-primary btn-sm" onclick="saveEmailVersion()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- E-Mail Versions Table -->
|
||
<div id="email-version-table-container">
|
||
<div class="admin-empty">Wählen Sie eine E-Mail-Vorlage aus, um deren Versionen anzuzeigen.</div>
|
||
</div>
|
||
|
||
<!-- E-Mail Preview Dialog -->
|
||
<div id="email-preview-dialog" class="admin-dialog" style="display: none;">
|
||
<div class="admin-dialog-content" style="max-width: 700px; max-height: 80vh; overflow-y: auto;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||
<h3 style="margin: 0;">E-Mail Vorschau</h3>
|
||
<button class="btn btn-ghost btn-sm" onclick="hideEmailPreview()">Schließen</button>
|
||
</div>
|
||
<div style="margin-bottom: 12px;">
|
||
<strong>Betreff:</strong> <span id="email-preview-subject"></span>
|
||
</div>
|
||
<div style="border: 1px solid var(--bp-border); border-radius: 8px; padding: 16px; background: white; color: #333;">
|
||
<div id="email-preview-content"></div>
|
||
</div>
|
||
<div style="margin-top: 16px; display: flex; gap: 8px;">
|
||
<input type="email" class="admin-form-input" id="email-test-address" placeholder="Test-E-Mail-Adresse" style="flex: 1;">
|
||
<button class="btn btn-primary btn-sm" onclick="sendTestEmail()">Test-E-Mail senden</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- E-Mail Approval Dialog -->
|
||
<div id="email-approval-dialog" class="admin-dialog" style="display: none;">
|
||
<div class="admin-dialog-content">
|
||
<h3>E-Mail-Version genehmigen</h3>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group full-width">
|
||
<label class="admin-form-label">Kommentar (optional)</label>
|
||
<input type="text" class="admin-form-input" id="email-approval-comment" placeholder="z.B. Genehmigt nach Marketing-Prüfung">
|
||
</div>
|
||
</div>
|
||
<div class="admin-dialog-actions">
|
||
<button class="btn btn-ghost btn-sm" onclick="hideEmailApprovalDialog()">Abbrechen</button>
|
||
<button class="btn btn-primary btn-sm" onclick="submitEmailApproval()">Genehmigen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DSMS Tab -->
|
||
<div id="admin-dsms" class="admin-content">
|
||
<div class="admin-toolbar">
|
||
<div class="admin-toolbar-left">
|
||
<span style="font-weight: 600; color: var(--bp-primary);">Dezentrales Speichersystem (IPFS)</span>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<button class="btn btn-ghost btn-sm" onclick="openDsmsWebUI()">DSMS WebUI</button>
|
||
<button class="btn btn-primary btn-sm" onclick="loadDsmsData()">Aktualisieren</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DSMS Status Cards -->
|
||
<div id="dsms-status-cards" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px;">
|
||
<div class="admin-loading">Lade DSMS Status...</div>
|
||
</div>
|
||
|
||
<!-- DSMS Tabs -->
|
||
<div style="display: flex; gap: 8px; margin-bottom: 16px; border-bottom: 1px solid var(--bp-border); padding-bottom: 8px;">
|
||
<button class="dsms-subtab active" data-dsms-tab="archives" onclick="switchDsmsTab('archives')">Archivierte Dokumente</button>
|
||
<button class="dsms-subtab" data-dsms-tab="verify" onclick="switchDsmsTab('verify')">Verifizierung</button>
|
||
<button class="dsms-subtab" data-dsms-tab="settings" onclick="switchDsmsTab('settings')">Einstellungen</button>
|
||
</div>
|
||
|
||
<!-- Archives Sub-Tab -->
|
||
<div id="dsms-archives" class="dsms-content active">
|
||
<div class="admin-toolbar" style="margin-bottom: 16px;">
|
||
<div class="admin-toolbar-left">
|
||
<input type="text" class="admin-search" placeholder="CID suchen..." id="dsms-cid-search" style="width: 300px;">
|
||
</div>
|
||
<button class="btn btn-primary btn-sm" onclick="showArchiveForm()">+ Dokument archivieren</button>
|
||
</div>
|
||
|
||
<!-- Archive Form -->
|
||
<div id="dsms-archive-form" class="admin-form" style="display: none; margin-bottom: 16px;">
|
||
<h3 class="admin-form-title">Dokument im DSMS archivieren</h3>
|
||
<div class="admin-form-row">
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Dokument auswählen *</label>
|
||
<select class="admin-form-select" id="dsms-archive-doc-select">
|
||
<option value="">-- Dokument wählen --</option>
|
||
</select>
|
||
</div>
|
||
<div class="admin-form-group">
|
||
<label class="admin-form-label">Version *</label>
|
||
<select class="admin-form-select" id="dsms-archive-version-select" disabled>
|
||
<option value="">-- Erst Dokument wählen --</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="admin-form-actions">
|
||
<button class="btn btn-ghost btn-sm" onclick="hideArchiveForm()">Abbrechen</button>
|
||
<button class="btn btn-primary btn-sm" onclick="archiveDocumentToDsms()">Archivieren</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="dsms-archives-table">
|
||
<div class="admin-loading">Lade archivierte Dokumente...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Verify Sub-Tab -->
|
||
<div id="dsms-verify" class="dsms-content" style="display: none;">
|
||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
|
||
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Dokumentenintegrität prüfen</h3>
|
||
<p style="color: var(--bp-text-muted); margin-bottom: 16px; font-size: 14px;">
|
||
Geben Sie einen CID (Content Identifier) ein, um die Integrität eines archivierten Dokuments zu verifizieren.
|
||
</p>
|
||
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
|
||
<input type="text" class="admin-form-input" id="dsms-verify-cid" placeholder="Qm... oder bafy..." style="flex: 1;">
|
||
<button class="btn btn-primary btn-sm" onclick="verifyDsmsDocument()">Verifizieren</button>
|
||
</div>
|
||
<div id="dsms-verify-result" style="display: none;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Settings Sub-Tab -->
|
||
<div id="dsms-settings" class="dsms-content" style="display: none;">
|
||
<div style="display: grid; gap: 16px;">
|
||
<!-- Node Info -->
|
||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
|
||
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Node-Informationen</h3>
|
||
<div id="dsms-node-info">
|
||
<div class="admin-loading">Lade Node-Info...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quick Links -->
|
||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
|
||
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Schnellzugriff</h3>
|
||
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
|
||
<button class="btn btn-primary btn-sm" onclick="openDsmsWebUI()">DSMS WebUI</button>
|
||
<a href="http://localhost:8082/docs" target="_blank" class="btn btn-ghost btn-sm">DSMS API Docs</a>
|
||
<a href="http://localhost:8085" target="_blank" class="btn btn-ghost btn-sm">IPFS Gateway</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Lizenzhinweise -->
|
||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; border: 1px solid var(--bp-border);">
|
||
<h3 style="margin: 0 0 16px 0; font-size: 16px;">Open Source Lizenzen</h3>
|
||
<p style="color: var(--bp-text-muted); font-size: 13px; margin-bottom: 12px;">
|
||
DSMS verwendet folgende Open-Source-Komponenten:
|
||
</p>
|
||
<ul style="color: var(--bp-text-muted); font-size: 13px; margin: 0; padding-left: 20px;">
|
||
<li><strong>IPFS Kubo</strong> - MIT + Apache 2.0 (Dual License) - Protocol Labs, Inc.</li>
|
||
<li><strong>IPFS WebUI</strong> - MIT License - Protocol Labs, Inc.</li>
|
||
<li><strong>FastAPI</strong> - MIT License</li>
|
||
</ul>
|
||
<p style="color: var(--bp-text-muted); font-size: 12px; margin-top: 12px; font-style: italic;">
|
||
Alle Komponenten erlauben kommerzielle Nutzung.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DSMS WebUI Modal -->
|
||
<div id="dsms-webui-modal" class="legal-modal" style="display: none;">
|
||
<div class="legal-modal-content" style="max-width: 1200px; width: 95%; height: 90vh;">
|
||
<div class="legal-modal-header" style="border-bottom: 1px solid var(--bp-border);">
|
||
<h2 style="display: flex; align-items: center; gap: 10px;">
|
||
<span style="font-size: 24px;">🌐</span> DSMS WebUI
|
||
</h2>
|
||
<button id="dsms-webui-modal-close" class="legal-modal-close" onclick="closeDsmsWebUI()">×</button>
|
||
</div>
|
||
<div style="display: flex; height: calc(100% - 60px);">
|
||
<!-- Sidebar -->
|
||
<div style="width: 200px; background: var(--bp-surface); border-right: 1px solid var(--bp-border); padding: 16px;">
|
||
<nav style="display: flex; flex-direction: column; gap: 4px;">
|
||
<button class="dsms-webui-nav active" data-section="overview" onclick="switchDsmsWebUISection('overview')">
|
||
<span>📈</span> Übersicht
|
||
</button>
|
||
<button class="dsms-webui-nav" data-section="files" onclick="switchDsmsWebUISection('files')">
|
||
<span>📁</span> Dateien
|
||
</button>
|
||
<button class="dsms-webui-nav" data-section="explore" onclick="switchDsmsWebUISection('explore')">
|
||
<span>🔍</span> Erkunden
|
||
</button>
|
||
<button class="dsms-webui-nav" data-section="peers" onclick="switchDsmsWebUISection('peers')">
|
||
<span>🌐</span> Peers
|
||
</button>
|
||
<button class="dsms-webui-nav" data-section="config" onclick="switchDsmsWebUISection('config')">
|
||
<span>⚙</span> Konfiguration
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
<!-- Main Content -->
|
||
<div style="flex: 1; overflow-y: auto; padding: 24px;" id="dsms-webui-content">
|
||
<!-- Overview Section (default) -->
|
||
<div id="dsms-webui-overview" class="dsms-webui-section active">
|
||
<h3 style="margin: 0 0 24px 0;">Node Übersicht</h3>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px;">
|
||
<div class="dsms-webui-stat-card">
|
||
<div class="dsms-webui-stat-label">Status</div>
|
||
<div class="dsms-webui-stat-value" id="webui-status">--</div>
|
||
</div>
|
||
<div class="dsms-webui-stat-card">
|
||
<div class="dsms-webui-stat-label">Node ID</div>
|
||
<div class="dsms-webui-stat-value" id="webui-node-id" style="font-size: 11px; word-break: break-all;">--</div>
|
||
</div>
|
||
<div class="dsms-webui-stat-card">
|
||
<div class="dsms-webui-stat-label">Protokoll</div>
|
||
<div class="dsms-webui-stat-value" id="webui-protocol">--</div>
|
||
</div>
|
||
<div class="dsms-webui-stat-card">
|
||
<div class="dsms-webui-stat-label">Agent</div>
|
||
<div class="dsms-webui-stat-value" id="webui-agent">--</div>
|
||
</div>
|
||
</div>
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px;">
|
||
<div class="dsms-webui-stat-card">
|
||
<div class="dsms-webui-stat-label">Repo Größe</div>
|
||
<div class="dsms-webui-stat-value" id="webui-repo-size">--</div>
|
||
<div class="dsms-webui-stat-sub" id="webui-storage-info">--</div>
|
||
</div>
|
||
<div class="dsms-webui-stat-card">
|
||
<div class="dsms-webui-stat-label">Objekte</div>
|
||
<div class="dsms-webui-stat-value" id="webui-num-objects">--</div>
|
||
</div>
|
||
<div class="dsms-webui-stat-card">
|
||
<div class="dsms-webui-stat-label">Gepinnte Dokumente</div>
|
||
<div class="dsms-webui-stat-value" id="webui-pinned-count">--</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 24px;">
|
||
<h4 style="margin: 0 0 12px 0;">Adressen</h4>
|
||
<div id="webui-addresses" style="background: var(--bp-input-bg); border-radius: 8px; padding: 12px; font-family: monospace; font-size: 12px; max-height: 150px; overflow-y: auto;">
|
||
Lade...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Files Section -->
|
||
<div id="dsms-webui-files" class="dsms-webui-section" style="display: none;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
|
||
<h3 style="margin: 0;">Dateien hochladen</h3>
|
||
</div>
|
||
<div class="dsms-webui-upload-zone" id="dsms-upload-zone" ondrop="handleDsmsFileDrop(event)" ondragover="handleDsmsDragOver(event)" ondragleave="handleDsmsDragLeave(event)">
|
||
<div style="text-align: center;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">📥</div>
|
||
<p style="color: var(--bp-text); margin-bottom: 8px;">Dateien hierher ziehen</p>
|
||
<p style="color: var(--bp-text-muted); font-size: 13px;">oder</p>
|
||
<input type="file" id="dsms-file-input" style="display: none;" onchange="handleDsmsFileSelect(event)" multiple>
|
||
<button class="btn btn-primary btn-sm" onclick="document.getElementById('dsms-file-input').click()">Dateien auswählen</button>
|
||
</div>
|
||
</div>
|
||
<div id="dsms-upload-progress" style="display: none; margin-top: 16px;">
|
||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
|
||
<div id="dsms-upload-status">Hochladen...</div>
|
||
<div style="background: var(--bp-border); border-radius: 4px; height: 8px; margin-top: 8px; overflow: hidden;">
|
||
<div id="dsms-upload-bar" style="background: var(--bp-primary); height: 100%; width: 0%; transition: width 0.3s;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="dsms-upload-results" style="margin-top: 24px;"></div>
|
||
</div>
|
||
|
||
<!-- Explore Section -->
|
||
<div id="dsms-webui-explore" class="dsms-webui-section" style="display: none;">
|
||
<h3 style="margin: 0 0 24px 0;">IPFS Explorer</h3>
|
||
<div style="display: flex; gap: 12px; margin-bottom: 24px;">
|
||
<input type="text" class="admin-search" placeholder="CID eingeben (z.B. QmXyz...)" id="webui-explore-cid" style="flex: 1;">
|
||
<button class="btn btn-primary btn-sm" onclick="exploreDsmsCid()">Erkunden</button>
|
||
</div>
|
||
<div id="dsms-explore-result" style="display: none;">
|
||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
|
||
<div id="dsms-explore-content"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Peers Section -->
|
||
<div id="dsms-webui-peers" class="dsms-webui-section" style="display: none;">
|
||
<h3 style="margin: 0 0 24px 0;">Verbundene Peers</h3>
|
||
<p style="color: var(--bp-text-muted); margin-bottom: 16px;">
|
||
Hinweis: In einem privaten DSMS-Netzwerk sind normalerweise keine externen Peers verbunden.
|
||
</p>
|
||
<div id="webui-peers-list">
|
||
<div class="admin-loading">Lade Peers...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Config Section -->
|
||
<div id="dsms-webui-config" class="dsms-webui-section" style="display: none;">
|
||
<h3 style="margin: 0 0 24px 0;">Konfiguration</h3>
|
||
<div style="display: grid; gap: 16px;">
|
||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
|
||
<h4 style="margin: 0 0 12px 0;">API Endpoints</h4>
|
||
<table style="width: 100%; font-size: 13px;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted);">IPFS API</td>
|
||
<td style="padding: 8px 0; font-family: monospace;">http://localhost:5001</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted);">DSMS Gateway</td>
|
||
<td style="padding: 8px 0; font-family: monospace;">http://localhost:8082</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted);">IPFS Gateway</td>
|
||
<td style="padding: 8px 0; font-family: monospace;">http://localhost:8085</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted);">Swarm P2P</td>
|
||
<td style="padding: 8px 0; font-family: monospace;">:4001</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 16px;">
|
||
<h4 style="margin: 0 0 12px 0;">Aktionen</h4>
|
||
<div style="display: flex; flex-wrap: wrap; gap: 12px;">
|
||
<button class="btn btn-ghost btn-sm" onclick="runDsmsGarbageCollection()">🗑 Garbage Collection</button>
|
||
<button class="btn btn-ghost btn-sm" onclick="loadDsmsWebUIData()">↻ Daten aktualisieren</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Legal Modal -->
|
||
<div id="legal-modal" class="legal-modal">
|
||
<div class="legal-modal-content">
|
||
<div class="legal-modal-header">
|
||
<h2>Rechtliches</h2>
|
||
<button id="legal-modal-close" class="legal-modal-close">×</button>
|
||
</div>
|
||
<div class="legal-tabs">
|
||
<button class="legal-tab active" data-tab="terms">AGB</button>
|
||
<button class="legal-tab" data-tab="privacy">Datenschutz</button>
|
||
<button class="legal-tab" data-tab="community">Community Guidelines</button>
|
||
<button class="legal-tab" data-tab="cookies">Cookie-Einstellungen</button>
|
||
<button class="legal-tab" data-tab="gdpr">DSGVO-Rechte</button>
|
||
</div>
|
||
<div class="legal-body">
|
||
<div id="legal-terms" class="legal-content active">
|
||
<div id="legal-terms-content">
|
||
<div class="legal-loading">Lade AGB...</div>
|
||
</div>
|
||
</div>
|
||
<div id="legal-privacy" class="legal-content">
|
||
<div id="legal-privacy-content">
|
||
<div class="legal-loading">Lade Datenschutzerklärung...</div>
|
||
</div>
|
||
</div>
|
||
<div id="legal-community" class="legal-content">
|
||
<div id="legal-community-content">
|
||
<div class="legal-loading">Lade Community Guidelines...</div>
|
||
</div>
|
||
</div>
|
||
<div id="legal-cookies" class="legal-content">
|
||
<h3>Cookie-Einstellungen</h3>
|
||
<p>Wir verwenden Cookies, um Ihnen die bestmögliche Erfahrung zu bieten. Hier können Sie Ihre Präferenzen jederzeit anpassen.</p>
|
||
<div id="cookie-categories-container" class="cookie-categories">
|
||
<div class="legal-loading">Lade Cookie-Kategorien...</div>
|
||
</div>
|
||
<button class="btn btn-primary" style="margin-top:16px" onclick="saveCookiePreferences()">Einstellungen speichern</button>
|
||
</div>
|
||
<div id="legal-gdpr" class="legal-content">
|
||
<h3>Ihre DSGVO-Rechte</h3>
|
||
<p>Nach der Datenschutz-Grundverordnung haben Sie folgende Rechte:</p>
|
||
<div class="gdpr-actions">
|
||
<div class="gdpr-action">
|
||
<h4>📋 Datenauskunft (Art. 15)</h4>
|
||
<p>Erfahren Sie, welche Daten wir über Sie gespeichert haben.</p>
|
||
<button class="btn btn-sm" onclick="requestDataExport()">Meine Daten anfordern</button>
|
||
</div>
|
||
<div class="gdpr-action">
|
||
<h4>📤 Datenübertragbarkeit (Art. 20)</h4>
|
||
<p>Exportieren Sie Ihre Daten in einem maschinenlesbaren Format.</p>
|
||
<button class="btn btn-sm" onclick="requestDataDownload()">Daten exportieren</button>
|
||
</div>
|
||
<div class="gdpr-action">
|
||
<h4>🗑️ Recht auf Löschung (Art. 17)</h4>
|
||
<p>Beantragen Sie die vollständige Löschung Ihrer Daten.</p>
|
||
<button class="btn btn-sm btn-danger" onclick="requestDataDeletion()">Löschung beantragen</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Imprint Modal (Impressum - muss direkt erreichbar sein) -->
|
||
<div id="imprint-modal" class="legal-modal">
|
||
<div class="legal-modal-content">
|
||
<div class="legal-modal-header">
|
||
<h2>Impressum</h2>
|
||
<button id="imprint-modal-close" class="legal-modal-close">×</button>
|
||
</div>
|
||
<div class="legal-body" style="padding: 24px;">
|
||
<div id="imprint-content">
|
||
<div class="legal-loading">Lade Impressum...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Auth Modal -->
|
||
<div id="auth-modal" class="auth-modal">
|
||
<div class="auth-modal-content">
|
||
<div class="auth-modal-header">
|
||
<h2><span>🔐</span> Anmeldung</h2>
|
||
<button id="auth-modal-close" class="legal-modal-close">×</button>
|
||
</div>
|
||
<div class="auth-tabs">
|
||
<button class="auth-tab active" data-tab="login">Anmelden</button>
|
||
<button class="auth-tab" data-tab="register">Registrieren</button>
|
||
</div>
|
||
<div class="auth-body">
|
||
<!-- Login Tab -->
|
||
<div id="auth-login" class="auth-content active">
|
||
<div id="auth-login-error" class="auth-error"></div>
|
||
<div id="auth-login-success" class="auth-success"></div>
|
||
<form id="auth-login-form">
|
||
<div class="auth-form-group">
|
||
<label class="auth-form-label">E-Mail</label>
|
||
<input type="email" class="auth-form-input" id="login-email" placeholder="ihre@email.de" required>
|
||
</div>
|
||
<div class="auth-form-group">
|
||
<label class="auth-form-label">Passwort</label>
|
||
<input type="password" class="auth-form-input" id="login-password" placeholder="Ihr Passwort" required>
|
||
</div>
|
||
<button type="submit" class="auth-btn auth-btn-primary" id="login-btn">Anmelden</button>
|
||
</form>
|
||
<div class="auth-link">
|
||
<a href="#" id="auth-forgot-password">Passwort vergessen?</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Register Tab -->
|
||
<div id="auth-register" class="auth-content">
|
||
<div id="auth-register-error" class="auth-error"></div>
|
||
<div id="auth-register-success" class="auth-success"></div>
|
||
<form id="auth-register-form">
|
||
<div class="auth-form-group">
|
||
<label class="auth-form-label">Name (optional)</label>
|
||
<input type="text" class="auth-form-input" id="register-name" placeholder="Ihr Name">
|
||
</div>
|
||
<div class="auth-form-group">
|
||
<label class="auth-form-label">E-Mail</label>
|
||
<input type="email" class="auth-form-input" id="register-email" placeholder="ihre@email.de" required>
|
||
</div>
|
||
<div class="auth-form-group">
|
||
<label class="auth-form-label">Passwort</label>
|
||
<input type="password" class="auth-form-input" id="register-password" placeholder="Mind. 8 Zeichen" required minlength="8">
|
||
</div>
|
||
<div class="auth-form-group">
|
||
<label class="auth-form-label">Passwort bestätigen</label>
|
||
<input type="password" class="auth-form-input" id="register-password-confirm" placeholder="Passwort wiederholen" required>
|
||
</div>
|
||
<button type="submit" class="auth-btn auth-btn-primary" id="register-btn">Registrieren</button>
|
||
</form>
|
||
<div class="auth-link">
|
||
Bereits registriert? <a href="#" id="auth-goto-login">Hier anmelden</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Forgot Password Tab (hidden by default) -->
|
||
<div id="auth-forgot" class="auth-content">
|
||
<div id="auth-forgot-error" class="auth-error"></div>
|
||
<div id="auth-forgot-success" class="auth-success"></div>
|
||
<p style="color: var(--bp-text-muted); font-size: 13px; margin-bottom: 16px;">
|
||
Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen einen Link zum Zurücksetzen Ihres Passworts.
|
||
</p>
|
||
<form id="auth-forgot-form">
|
||
<div class="auth-form-group">
|
||
<label class="auth-form-label">E-Mail</label>
|
||
<input type="email" class="auth-form-input" id="forgot-email" placeholder="ihre@email.de" required>
|
||
</div>
|
||
<button type="submit" class="auth-btn auth-btn-primary" id="forgot-btn">Link senden</button>
|
||
</form>
|
||
<div class="auth-link">
|
||
<a href="#" id="auth-back-to-login">Zurück zur Anmeldung</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Reset Password Tab (hidden by default, shown via URL token) -->
|
||
<div id="auth-reset" class="auth-content">
|
||
<div id="auth-reset-error" class="auth-error"></div>
|
||
<div id="auth-reset-success" class="auth-success"></div>
|
||
<form id="auth-reset-form">
|
||
<div class="auth-form-group">
|
||
<label class="auth-form-label">Neues Passwort</label>
|
||
<input type="password" class="auth-form-input" id="reset-password" placeholder="Mind. 8 Zeichen" required minlength="8">
|
||
</div>
|
||
<div class="auth-form-group">
|
||
<label class="auth-form-label">Passwort bestätigen</label>
|
||
<input type="password" class="auth-form-input" id="reset-password-confirm" placeholder="Passwort wiederholen" required>
|
||
</div>
|
||
<input type="hidden" id="reset-token">
|
||
<button type="submit" class="auth-btn auth-btn-primary" id="reset-btn">Passwort ändern</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Email Verification Tab (hidden by default, shown via URL token) -->
|
||
<div id="auth-verify" class="auth-content">
|
||
<div id="auth-verify-error" class="auth-error"></div>
|
||
<div id="auth-verify-success" class="auth-success"></div>
|
||
<div style="text-align: center; padding: 20px 0;">
|
||
<div id="auth-verify-loading" style="color: var(--bp-text-muted);">
|
||
E-Mail wird verifiziert...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Notification Preferences Modal -->
|
||
<div id="notification-prefs-modal" class="notification-prefs-modal">
|
||
<div class="notification-prefs-content">
|
||
<div class="auth-modal-header">
|
||
<h2>Benachrichtigungseinstellungen</h2>
|
||
<button class="legal-modal-close" onclick="closeNotificationPreferences()">×</button>
|
||
</div>
|
||
<div class="auth-body">
|
||
<div class="notification-pref-item">
|
||
<div>
|
||
<div class="notification-pref-label">E-Mail-Benachrichtigungen</div>
|
||
<div class="notification-pref-desc">Wichtige Updates per E-Mail erhalten</div>
|
||
</div>
|
||
<div class="toggle-switch active" id="pref-email-toggle" onclick="toggleNotificationPref('email')">
|
||
<div class="toggle-switch-handle"></div>
|
||
</div>
|
||
</div>
|
||
<div class="notification-pref-item">
|
||
<div>
|
||
<div class="notification-pref-label">In-App-Benachrichtigungen</div>
|
||
<div class="notification-pref-desc">Benachrichtigungen in der App anzeigen</div>
|
||
</div>
|
||
<div class="toggle-switch active" id="pref-inapp-toggle" onclick="toggleNotificationPref('inapp')">
|
||
<div class="toggle-switch-handle"></div>
|
||
</div>
|
||
</div>
|
||
<div class="notification-pref-item">
|
||
<div>
|
||
<div class="notification-pref-label">Push-Benachrichtigungen</div>
|
||
<div class="notification-pref-desc">Browser-Push-Benachrichtigungen aktivieren</div>
|
||
</div>
|
||
<div class="toggle-switch" id="pref-push-toggle" onclick="toggleNotificationPref('push')">
|
||
<div class="toggle-switch-handle"></div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 20px;">
|
||
<button class="auth-btn auth-btn-primary" onclick="saveNotificationPreferences()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Suspension Overlay -->
|
||
<div id="suspension-overlay" class="suspension-overlay">
|
||
<div class="suspension-content">
|
||
<div class="suspension-icon">🚫</div>
|
||
<div class="suspension-title">Account vorübergehend gesperrt</div>
|
||
<div class="suspension-message">
|
||
Ihr Account wurde gesperrt, da ausstehende Zustimmungen nicht innerhalb der Frist erteilt wurden.
|
||
Bitte bestätigen Sie die folgenden Dokumente, um Ihren Account wiederherzustellen.
|
||
</div>
|
||
<div class="suspension-docs">
|
||
<div class="suspension-docs-title">Ausstehende Dokumente:</div>
|
||
<div id="suspension-doc-list">
|
||
<!-- Will be populated dynamically -->
|
||
</div>
|
||
</div>
|
||
<button class="suspension-btn" onclick="showConsentModal()">
|
||
Jetzt bestätigen
|
||
</button>
|
||
<div class="suspension-countdown" id="suspension-countdown"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// GDPR Actions
|
||
async function saveCookiePreferences() {
|
||
const functional = document.getElementById('cookie-functional')?.checked || false;
|
||
const analytics = document.getElementById('cookie-analytics')?.checked || false;
|
||
|
||
// Save to localStorage for now
|
||
localStorage.setItem('bp_cookies', JSON.stringify({functional, analytics}));
|
||
alert('Cookie-Einstellungen gespeichert!');
|
||
}
|
||
|
||
async function requestDataExport() {
|
||
alert('Ihre Datenanfrage wurde erstellt. Sie erhalten eine E-Mail, sobald Ihre Daten bereit sind.');
|
||
}
|
||
|
||
async function requestDataDownload() {
|
||
alert('Ihr Datenexport wurde gestartet. Sie erhalten eine E-Mail mit dem Download-Link.');
|
||
}
|
||
|
||
async function requestDataDeletion() {
|
||
if (confirm('Sind Sie sicher, dass Sie alle Ihre Daten löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.')) {
|
||
alert('Ihre Löschanfrage wurde erstellt. Wir werden sie innerhalb von 30 Tagen bearbeiten.');
|
||
}
|
||
}
|
||
|
||
// Load saved cookie preferences
|
||
const savedCookies = localStorage.getItem('bp_cookies');
|
||
if (savedCookies) {
|
||
const prefs = JSON.parse(savedCookies);
|
||
if (document.getElementById('cookie-functional')) {
|
||
document.getElementById('cookie-functional').checked = prefs.functional;
|
||
}
|
||
if (document.getElementById('cookie-analytics')) {
|
||
document.getElementById('cookie-analytics').checked = prefs.analytics;
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// LEGAL MODAL (now after modal HTML exists)
|
||
// ==========================================
|
||
const legalModal = document.getElementById('legal-modal');
|
||
const legalModalClose = document.getElementById('legal-modal-close');
|
||
const legalTabs = document.querySelectorAll('.legal-tab');
|
||
const legalContents = document.querySelectorAll('.legal-content');
|
||
const btnLegal = document.getElementById('btn-legal');
|
||
|
||
// Imprint Modal
|
||
const imprintModal = document.getElementById('imprint-modal');
|
||
const imprintModalClose = document.getElementById('imprint-modal-close');
|
||
|
||
// Open legal modal from footer
|
||
function openLegalModal(tab = 'terms') {
|
||
legalModal.classList.add('active');
|
||
// Switch to specified tab
|
||
if (tab) {
|
||
legalTabs.forEach(t => t.classList.remove('active'));
|
||
legalContents.forEach(c => c.classList.remove('active'));
|
||
const targetTab = document.querySelector(`.legal-tab[data-tab="${tab}"]`);
|
||
if (targetTab) targetTab.classList.add('active');
|
||
document.getElementById(`legal-${tab}`)?.classList.add('active');
|
||
}
|
||
loadLegalDocuments();
|
||
}
|
||
|
||
// Open imprint modal from footer
|
||
function openImprintModal() {
|
||
imprintModal.classList.add('active');
|
||
loadImprintContent();
|
||
}
|
||
|
||
// Open legal modal
|
||
btnLegal?.addEventListener('click', async () => {
|
||
openLegalModal();
|
||
});
|
||
|
||
// Close legal modal
|
||
legalModalClose?.addEventListener('click', () => {
|
||
legalModal.classList.remove('active');
|
||
});
|
||
|
||
// Close imprint modal
|
||
imprintModalClose?.addEventListener('click', () => {
|
||
imprintModal.classList.remove('active');
|
||
});
|
||
|
||
// Close on background click
|
||
legalModal?.addEventListener('click', (e) => {
|
||
if (e.target === legalModal) {
|
||
legalModal.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
imprintModal?.addEventListener('click', (e) => {
|
||
if (e.target === imprintModal) {
|
||
imprintModal.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
// Tab switching
|
||
legalTabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
const tabId = tab.dataset.tab;
|
||
legalTabs.forEach(t => t.classList.remove('active'));
|
||
legalContents.forEach(c => c.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
document.getElementById(`legal-${tabId}`)?.classList.add('active');
|
||
|
||
// Load cookie categories when switching to cookies tab
|
||
if (tabId === 'cookies') {
|
||
loadCookieCategories();
|
||
}
|
||
});
|
||
});
|
||
|
||
// Load legal documents from consent service
|
||
async function loadLegalDocuments() {
|
||
const lang = document.getElementById('language-select')?.value || 'de';
|
||
|
||
// Load all documents in parallel
|
||
await Promise.all([
|
||
loadDocumentContent('terms', 'legal-terms-content', getDefaultTerms, lang),
|
||
loadDocumentContent('privacy', 'legal-privacy-content', getDefaultPrivacy, lang),
|
||
loadDocumentContent('community_guidelines', 'legal-community-content', getDefaultCommunityGuidelines, lang)
|
||
]);
|
||
}
|
||
|
||
// Load imprint content
|
||
async function loadImprintContent() {
|
||
const lang = document.getElementById('language-select')?.value || 'de';
|
||
await loadDocumentContent('imprint', 'imprint-content', getDefaultImprint, lang);
|
||
}
|
||
|
||
// Generic function to load document content
|
||
async function loadDocumentContent(docType, containerId, defaultFn, lang) {
|
||
const container = document.getElementById(containerId);
|
||
if (!container) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/documents/${docType}/latest?language=${lang}`);
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
if (data.content) {
|
||
container.innerHTML = data.content;
|
||
return;
|
||
}
|
||
}
|
||
} catch(e) {
|
||
console.log(`Could not load ${docType}:`, e);
|
||
}
|
||
|
||
// Fallback to default
|
||
container.innerHTML = defaultFn(lang);
|
||
}
|
||
|
||
// Load cookie categories for the cookie settings tab
|
||
async function loadCookieCategories() {
|
||
const container = document.getElementById('cookie-categories-container');
|
||
if (!container) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/cookies/categories');
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
const categories = data.categories || [];
|
||
|
||
if (categories.length === 0) {
|
||
container.innerHTML = getDefaultCookieCategories();
|
||
return;
|
||
}
|
||
|
||
// Get current preferences from localStorage
|
||
const savedPrefs = JSON.parse(localStorage.getItem('bp_cookie_consent') || '{}');
|
||
|
||
container.innerHTML = categories.map(cat => `
|
||
<label class="cookie-category">
|
||
<input type="checkbox" id="cookie-${cat.name}"
|
||
${cat.is_mandatory ? 'checked disabled' : (savedPrefs[cat.name] ? 'checked' : '')}>
|
||
<span>
|
||
<strong>${cat.display_name_de || cat.name}</strong>
|
||
${cat.is_mandatory ? ' (erforderlich)' : ''}
|
||
${cat.description_de ? ` - ${cat.description_de}` : ''}
|
||
</span>
|
||
</label>
|
||
`).join('');
|
||
} else {
|
||
container.innerHTML = getDefaultCookieCategories();
|
||
}
|
||
} catch(e) {
|
||
container.innerHTML = getDefaultCookieCategories();
|
||
}
|
||
}
|
||
|
||
function getDefaultCookieCategories() {
|
||
return `
|
||
<label class="cookie-category">
|
||
<input type="checkbox" checked disabled>
|
||
<span><strong>Notwendig</strong> (erforderlich) - Erforderlich für die Grundfunktionen</span>
|
||
</label>
|
||
<label class="cookie-category">
|
||
<input type="checkbox" id="cookie-functional">
|
||
<span><strong>Funktional</strong> - Erweiterte Funktionen und Personalisierung</span>
|
||
</label>
|
||
<label class="cookie-category">
|
||
<input type="checkbox" id="cookie-analytics">
|
||
<span><strong>Analyse</strong> - Hilft uns, die Nutzung zu verstehen</span>
|
||
</label>
|
||
`;
|
||
}
|
||
|
||
function getDefaultTerms(lang) {
|
||
const terms = {
|
||
de: '<h3>Allgemeine Geschäftsbedingungen</h3><p>Die BreakPilot-Plattform wird von der BreakPilot UG bereitgestellt.</p><p><strong>Nutzung:</strong> Die Plattform dient zur Erstellung und Verwaltung von Lernmaterialien für Bildungszwecke.</p><p><strong>Haftung:</strong> Die Nutzung erfolgt auf eigene Verantwortung.</p><p><strong>Änderungen:</strong> Wir behalten uns vor, diese AGB jederzeit zu ändern.</p>',
|
||
en: '<h3>Terms of Service</h3><p>The BreakPilot platform is provided by BreakPilot UG.</p><p><strong>Usage:</strong> The platform is designed for creating and managing learning materials for educational purposes.</p><p><strong>Liability:</strong> Use at your own risk.</p><p><strong>Changes:</strong> We reserve the right to modify these terms at any time.</p>'
|
||
};
|
||
return terms[lang] || terms.de;
|
||
}
|
||
|
||
function getDefaultPrivacy(lang) {
|
||
const privacy = {
|
||
de: '<h3>Datenschutzerklärung</h3><p><strong>Verantwortlicher:</strong> BreakPilot UG</p><p><strong>Erhobene Daten:</strong> Bei der Nutzung werden technische Daten (IP-Adresse, Browser-Typ) sowie von Ihnen eingegebene Inhalte verarbeitet.</p><p><strong>Zweck:</strong> Die Daten werden zur Bereitstellung der Plattform und zur Verbesserung unserer Dienste genutzt.</p><p><strong>Ihre Rechte (DSGVO):</strong></p><ul><li>Auskunftsrecht (Art. 15)</li><li>Recht auf Berichtigung (Art. 16)</li><li>Recht auf Löschung (Art. 17)</li><li>Recht auf Datenübertragbarkeit (Art. 20)</li></ul><p><strong>Kontakt:</strong> datenschutz@breakpilot.app</p>',
|
||
en: '<h3>Privacy Policy</h3><p><strong>Controller:</strong> BreakPilot UG</p><p><strong>Data Collected:</strong> Technical data (IP address, browser type) and content you provide are processed.</p><p><strong>Purpose:</strong> Data is used to provide the platform and improve our services.</p><p><strong>Your Rights (GDPR):</strong></p><ul><li>Right of access (Art. 15)</li><li>Right to rectification (Art. 16)</li><li>Right to erasure (Art. 17)</li><li>Right to data portability (Art. 20)</li></ul><p><strong>Contact:</strong> privacy@breakpilot.app</p>'
|
||
};
|
||
return privacy[lang] || privacy.de;
|
||
}
|
||
|
||
function getDefaultCommunityGuidelines(lang) {
|
||
const guidelines = {
|
||
de: '<h3>Community Guidelines</h3><p>Willkommen bei BreakPilot! Um eine positive und respektvolle Umgebung zu gewährleisten, bitten wir alle Nutzer, diese Richtlinien zu befolgen.</p><p><strong>Respektvoller Umgang:</strong> Behandeln Sie andere Nutzer mit Respekt und Höflichkeit.</p><p><strong>Keine illegalen Inhalte:</strong> Das Erstellen oder Teilen von illegalen Inhalten ist streng untersagt.</p><p><strong>Urheberrecht:</strong> Respektieren Sie das geistige Eigentum anderer. Verwenden Sie nur Inhalte, für die Sie die Rechte besitzen.</p><p><strong>Datenschutz:</strong> Teilen Sie keine persönlichen Daten anderer ohne deren ausdrückliche Zustimmung.</p><p><strong>Qualität:</strong> Bemühen Sie sich um qualitativ hochwertige Lerninhalte.</p><p>Verstöße gegen diese Richtlinien können zur Sperrung des Accounts führen.</p>',
|
||
en: '<h3>Community Guidelines</h3><p>Welcome to BreakPilot! To ensure a positive and respectful environment, we ask all users to follow these guidelines.</p><p><strong>Respectful Behavior:</strong> Treat other users with respect and courtesy.</p><p><strong>No Illegal Content:</strong> Creating or sharing illegal content is strictly prohibited.</p><p><strong>Copyright:</strong> Respect the intellectual property of others. Only use content you have rights to.</p><p><strong>Privacy:</strong> Do not share personal data of others without their explicit consent.</p><p><strong>Quality:</strong> Strive for high-quality learning content.</p><p>Violations of these guidelines may result in account suspension.</p>'
|
||
};
|
||
return guidelines[lang] || guidelines.de;
|
||
}
|
||
|
||
function getDefaultImprint(lang) {
|
||
const imprint = {
|
||
de: '<h3>Impressum</h3><p><strong>Angaben gemäß § 5 TMG:</strong></p><p>BreakPilot UG (haftungsbeschränkt)<br>Musterstraße 1<br>12345 Musterstadt<br>Deutschland</p><p><strong>Vertreten durch:</strong><br>Geschäftsführer: Max Mustermann</p><p><strong>Kontakt:</strong><br>Telefon: +49 (0) 123 456789<br>E-Mail: info@breakpilot.app</p><p><strong>Registereintrag:</strong><br>Eintragung im Handelsregister<br>Registergericht: Amtsgericht Musterstadt<br>Registernummer: HRB 12345</p><p><strong>Umsatzsteuer-ID:</strong><br>Umsatzsteuer-Identifikationsnummer gemäß § 27 a UStG: DE123456789</p><p><strong>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV:</strong><br>Max Mustermann<br>Musterstraße 1<br>12345 Musterstadt</p>',
|
||
en: '<h3>Legal Notice</h3><p><strong>Information according to § 5 TMG:</strong></p><p>BreakPilot UG (limited liability)<br>Musterstraße 1<br>12345 Musterstadt<br>Germany</p><p><strong>Represented by:</strong><br>Managing Director: Max Mustermann</p><p><strong>Contact:</strong><br>Phone: +49 (0) 123 456789<br>Email: info@breakpilot.app</p><p><strong>Register entry:</strong><br>Entry in the commercial register<br>Register court: Amtsgericht Musterstadt<br>Register number: HRB 12345</p><p><strong>VAT ID:</strong><br>VAT identification number according to § 27 a UStG: DE123456789</p><p><strong>Responsible for content according to § 55 Abs. 2 RStV:</strong><br>Max Mustermann<br>Musterstraße 1<br>12345 Musterstadt</p>'
|
||
};
|
||
return imprint[lang] || imprint.de;
|
||
}
|
||
|
||
// Save cookie preferences
|
||
function saveCookiePreferences() {
|
||
const prefs = {};
|
||
const checkboxes = document.querySelectorAll('#cookie-categories-container input[type="checkbox"]');
|
||
checkboxes.forEach(cb => {
|
||
const name = cb.id.replace('cookie-', '');
|
||
if (name && !cb.disabled) {
|
||
prefs[name] = cb.checked;
|
||
}
|
||
});
|
||
localStorage.setItem('bp_cookie_consent', JSON.stringify(prefs));
|
||
localStorage.setItem('bp_cookie_consent_date', new Date().toISOString());
|
||
|
||
// TODO: Send to consent service if user is logged in
|
||
alert('Cookie-Einstellungen gespeichert!');
|
||
}
|
||
|
||
// ==========================================
|
||
// AUTH MODAL
|
||
// ==========================================
|
||
const authModal = document.getElementById('auth-modal');
|
||
const authModalClose = document.getElementById('auth-modal-close');
|
||
const authTabs = document.querySelectorAll('.auth-tab');
|
||
const authContents = document.querySelectorAll('.auth-content');
|
||
const btnLogin = document.getElementById('btn-login');
|
||
|
||
// Auth state
|
||
let currentUser = null;
|
||
let accessToken = localStorage.getItem('bp_access_token');
|
||
let refreshToken = localStorage.getItem('bp_refresh_token');
|
||
|
||
// Update UI based on auth state
|
||
function updateAuthUI() {
|
||
const loginBtn = document.getElementById('btn-login');
|
||
const userDropdown = document.querySelector('.auth-user-dropdown');
|
||
const notificationBell = document.getElementById('notification-bell');
|
||
|
||
if (currentUser && accessToken) {
|
||
// User is logged in - hide login button
|
||
if (loginBtn) loginBtn.style.display = 'none';
|
||
|
||
// Show notification bell
|
||
if (notificationBell) {
|
||
notificationBell.classList.add('active');
|
||
loadNotifications(); // Load notifications on login
|
||
startNotificationPolling(); // Start polling for new notifications
|
||
checkSuspensionStatus(); // Check if account is suspended
|
||
}
|
||
|
||
// Show user dropdown if it exists
|
||
if (userDropdown) {
|
||
userDropdown.classList.add('active');
|
||
const avatar = userDropdown.querySelector('.auth-user-avatar');
|
||
const menuName = userDropdown.querySelector('.auth-user-menu-name');
|
||
const menuEmail = userDropdown.querySelector('.auth-user-menu-email');
|
||
|
||
if (avatar) {
|
||
const initials = currentUser.name
|
||
? currentUser.name.substring(0, 2).toUpperCase()
|
||
: currentUser.email.substring(0, 2).toUpperCase();
|
||
avatar.textContent = initials;
|
||
}
|
||
if (menuName) menuName.textContent = currentUser.name || 'Benutzer';
|
||
if (menuEmail) menuEmail.textContent = currentUser.email;
|
||
}
|
||
} else {
|
||
// User is logged out - show login button
|
||
if (loginBtn) loginBtn.style.display = 'block';
|
||
if (userDropdown) userDropdown.classList.remove('active');
|
||
if (notificationBell) notificationBell.classList.remove('active');
|
||
stopNotificationPolling();
|
||
}
|
||
}
|
||
|
||
// Check if user is already logged in
|
||
async function checkAuthStatus() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/profile', {
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
if (response.ok) {
|
||
currentUser = await response.json();
|
||
updateAuthUI();
|
||
} else if (response.status === 401 && refreshToken) {
|
||
// Try to refresh the token
|
||
await refreshAccessToken();
|
||
} else {
|
||
// Clear invalid tokens
|
||
logout(false);
|
||
}
|
||
} catch (e) {
|
||
console.error('Auth check failed:', e);
|
||
}
|
||
}
|
||
|
||
// Refresh access token
|
||
async function refreshAccessToken() {
|
||
if (!refreshToken) return false;
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/refresh', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ refresh_token: refreshToken })
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
accessToken = data.access_token;
|
||
refreshToken = data.refresh_token;
|
||
currentUser = data.user;
|
||
|
||
localStorage.setItem('bp_access_token', accessToken);
|
||
localStorage.setItem('bp_refresh_token', refreshToken);
|
||
updateAuthUI();
|
||
return true;
|
||
} else {
|
||
logout(false);
|
||
return false;
|
||
}
|
||
} catch (e) {
|
||
console.error('Token refresh failed:', e);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Logout
|
||
function logout(showMessage = true) {
|
||
if (refreshToken) {
|
||
fetch('/api/auth/logout', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ refresh_token: refreshToken })
|
||
}).catch(() => {});
|
||
}
|
||
|
||
currentUser = null;
|
||
accessToken = null;
|
||
refreshToken = null;
|
||
localStorage.removeItem('bp_access_token');
|
||
localStorage.removeItem('bp_refresh_token');
|
||
updateAuthUI();
|
||
|
||
if (showMessage) {
|
||
alert('Sie wurden erfolgreich abgemeldet.');
|
||
}
|
||
}
|
||
|
||
// Open auth modal
|
||
btnLogin?.addEventListener('click', () => {
|
||
authModal.classList.add('active');
|
||
showAuthTab('login');
|
||
clearAuthErrors();
|
||
});
|
||
|
||
// Close auth modal
|
||
authModalClose?.addEventListener('click', () => {
|
||
authModal.classList.remove('active');
|
||
clearAuthErrors();
|
||
});
|
||
|
||
// Close on background click
|
||
authModal?.addEventListener('click', (e) => {
|
||
if (e.target === authModal) {
|
||
authModal.classList.remove('active');
|
||
clearAuthErrors();
|
||
}
|
||
});
|
||
|
||
// Tab switching
|
||
authTabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
const tabId = tab.dataset.tab;
|
||
showAuthTab(tabId);
|
||
});
|
||
});
|
||
|
||
function showAuthTab(tabId) {
|
||
authTabs.forEach(t => t.classList.remove('active'));
|
||
authContents.forEach(c => c.classList.remove('active'));
|
||
|
||
const activeTab = document.querySelector(`.auth-tab[data-tab="${tabId}"]`);
|
||
if (activeTab) activeTab.classList.add('active');
|
||
|
||
document.getElementById(`auth-${tabId}`)?.classList.add('active');
|
||
clearAuthErrors();
|
||
}
|
||
|
||
function clearAuthErrors() {
|
||
document.querySelectorAll('.auth-error, .auth-success').forEach(el => {
|
||
el.classList.remove('active');
|
||
el.textContent = '';
|
||
});
|
||
}
|
||
|
||
function showAuthError(elementId, message) {
|
||
const el = document.getElementById(elementId);
|
||
if (el) {
|
||
el.textContent = message;
|
||
el.classList.add('active');
|
||
}
|
||
}
|
||
|
||
function showAuthSuccess(elementId, message) {
|
||
const el = document.getElementById(elementId);
|
||
if (el) {
|
||
el.textContent = message;
|
||
el.classList.add('active');
|
||
}
|
||
}
|
||
|
||
// Login form
|
||
document.getElementById('auth-login-form')?.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
clearAuthErrors();
|
||
|
||
const email = document.getElementById('login-email').value;
|
||
const password = document.getElementById('login-password').value;
|
||
const btn = document.getElementById('login-btn');
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = 'Anmelden...';
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email, password })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
accessToken = data.access_token;
|
||
refreshToken = data.refresh_token;
|
||
currentUser = data.user;
|
||
|
||
localStorage.setItem('bp_access_token', accessToken);
|
||
localStorage.setItem('bp_refresh_token', refreshToken);
|
||
|
||
updateAuthUI();
|
||
authModal.classList.remove('active');
|
||
|
||
// Clear form
|
||
document.getElementById('login-email').value = '';
|
||
document.getElementById('login-password').value = '';
|
||
} else {
|
||
showAuthError('auth-login-error', data.detail || data.error || 'Anmeldung fehlgeschlagen');
|
||
}
|
||
} catch (e) {
|
||
showAuthError('auth-login-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = 'Anmelden';
|
||
});
|
||
|
||
// Register form
|
||
document.getElementById('auth-register-form')?.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
clearAuthErrors();
|
||
|
||
const name = document.getElementById('register-name').value;
|
||
const email = document.getElementById('register-email').value;
|
||
const password = document.getElementById('register-password').value;
|
||
const passwordConfirm = document.getElementById('register-password-confirm').value;
|
||
|
||
if (password !== passwordConfirm) {
|
||
showAuthError('auth-register-error', 'Passwörter stimmen nicht überein');
|
||
return;
|
||
}
|
||
|
||
if (password.length < 8) {
|
||
showAuthError('auth-register-error', 'Passwort muss mindestens 8 Zeichen lang sein');
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('register-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Registrieren...';
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/register', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email, password, name: name || undefined })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
showAuthSuccess('auth-register-success',
|
||
'Registrierung erfolgreich! Bitte überprüfen Sie Ihre E-Mails zur Bestätigung.');
|
||
|
||
// Clear form
|
||
document.getElementById('register-name').value = '';
|
||
document.getElementById('register-email').value = '';
|
||
document.getElementById('register-password').value = '';
|
||
document.getElementById('register-password-confirm').value = '';
|
||
} else {
|
||
showAuthError('auth-register-error', data.detail || data.error || 'Registrierung fehlgeschlagen');
|
||
}
|
||
} catch (e) {
|
||
showAuthError('auth-register-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = 'Registrieren';
|
||
});
|
||
|
||
// Forgot password form
|
||
document.getElementById('auth-forgot-form')?.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
clearAuthErrors();
|
||
|
||
const email = document.getElementById('forgot-email').value;
|
||
const btn = document.getElementById('forgot-btn');
|
||
|
||
btn.disabled = true;
|
||
btn.textContent = 'Senden...';
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/forgot-password', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email })
|
||
});
|
||
|
||
// Always show success to prevent email enumeration
|
||
showAuthSuccess('auth-forgot-success',
|
||
'Falls ein Konto mit dieser E-Mail existiert, wurde ein Link zum Zurücksetzen gesendet.');
|
||
|
||
document.getElementById('forgot-email').value = '';
|
||
} catch (e) {
|
||
showAuthError('auth-forgot-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = 'Link senden';
|
||
});
|
||
|
||
// Reset password form
|
||
document.getElementById('auth-reset-form')?.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
clearAuthErrors();
|
||
|
||
const password = document.getElementById('reset-password').value;
|
||
const passwordConfirm = document.getElementById('reset-password-confirm').value;
|
||
const token = document.getElementById('reset-token').value;
|
||
|
||
if (password !== passwordConfirm) {
|
||
showAuthError('auth-reset-error', 'Passwörter stimmen nicht überein');
|
||
return;
|
||
}
|
||
|
||
if (password.length < 8) {
|
||
showAuthError('auth-reset-error', 'Passwort muss mindestens 8 Zeichen lang sein');
|
||
return;
|
||
}
|
||
|
||
const btn = document.getElementById('reset-btn');
|
||
btn.disabled = true;
|
||
btn.textContent = 'Ändern...';
|
||
|
||
try {
|
||
const response = await fetch('/api/auth/reset-password', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ token, new_password: password })
|
||
});
|
||
|
||
const data = await response.json();
|
||
|
||
if (response.ok) {
|
||
showAuthSuccess('auth-reset-success',
|
||
'Passwort erfolgreich geändert! Sie können sich jetzt anmelden.');
|
||
|
||
// Clear URL params
|
||
window.history.replaceState({}, document.title, window.location.pathname);
|
||
|
||
// Switch to login after 2 seconds
|
||
setTimeout(() => showAuthTab('login'), 2000);
|
||
} else {
|
||
showAuthError('auth-reset-error', data.detail || data.error || 'Passwort zurücksetzen fehlgeschlagen');
|
||
}
|
||
} catch (e) {
|
||
showAuthError('auth-reset-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||
}
|
||
|
||
btn.disabled = false;
|
||
btn.textContent = 'Passwort ändern';
|
||
});
|
||
|
||
// Navigation links
|
||
document.getElementById('auth-forgot-password')?.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
showAuthTab('forgot');
|
||
// Hide tabs for forgot password
|
||
document.querySelector('.auth-tabs').style.display = 'none';
|
||
});
|
||
|
||
document.getElementById('auth-back-to-login')?.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
showAuthTab('login');
|
||
document.querySelector('.auth-tabs').style.display = 'flex';
|
||
});
|
||
|
||
document.getElementById('auth-goto-login')?.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
showAuthTab('login');
|
||
});
|
||
|
||
// Check for URL parameters (email verification, password reset)
|
||
function checkAuthUrlParams() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const verifyToken = urlParams.get('verify');
|
||
const resetToken = urlParams.get('reset');
|
||
|
||
if (verifyToken) {
|
||
authModal.classList.add('active');
|
||
document.querySelector('.auth-tabs').style.display = 'none';
|
||
showAuthTab('verify');
|
||
verifyEmail(verifyToken);
|
||
} else if (resetToken) {
|
||
authModal.classList.add('active');
|
||
document.querySelector('.auth-tabs').style.display = 'none';
|
||
showAuthTab('reset');
|
||
document.getElementById('reset-token').value = resetToken;
|
||
}
|
||
}
|
||
|
||
async function verifyEmail(token) {
|
||
try {
|
||
const response = await fetch('/api/auth/verify-email', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ token })
|
||
});
|
||
|
||
const data = await response.json();
|
||
const loadingEl = document.getElementById('auth-verify-loading');
|
||
|
||
if (response.ok) {
|
||
if (loadingEl) loadingEl.style.display = 'none';
|
||
showAuthSuccess('auth-verify-success', 'E-Mail erfolgreich verifiziert! Sie können sich jetzt anmelden.');
|
||
|
||
// Clear URL params
|
||
window.history.replaceState({}, document.title, window.location.pathname);
|
||
|
||
// Switch to login after 2 seconds
|
||
setTimeout(() => {
|
||
showAuthTab('login');
|
||
document.querySelector('.auth-tabs').style.display = 'flex';
|
||
}, 2000);
|
||
} else {
|
||
if (loadingEl) loadingEl.style.display = 'none';
|
||
showAuthError('auth-verify-error', data.detail || data.error || 'Verifizierung fehlgeschlagen. Der Link ist möglicherweise abgelaufen.');
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('auth-verify-loading').style.display = 'none';
|
||
showAuthError('auth-verify-error', 'Verbindungsfehler. Bitte versuchen Sie es erneut.');
|
||
}
|
||
}
|
||
|
||
// User dropdown toggle
|
||
const authUserBtn = document.getElementById('auth-user-btn');
|
||
const authUserMenu = document.getElementById('auth-user-menu');
|
||
|
||
authUserBtn?.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
authUserMenu.classList.toggle('active');
|
||
});
|
||
|
||
// Close dropdown when clicking outside
|
||
document.addEventListener('click', () => {
|
||
authUserMenu?.classList.remove('active');
|
||
});
|
||
|
||
// Placeholder functions for profile/sessions
|
||
function showProfileModal() {
|
||
alert('Profil-Einstellungen kommen bald!');
|
||
}
|
||
|
||
function showSessionsModal() {
|
||
alert('Sitzungsverwaltung kommt bald!');
|
||
}
|
||
|
||
// ==========================================
|
||
// NOTIFICATION FUNCTIONS
|
||
// ==========================================
|
||
let notificationPollingInterval = null;
|
||
let notificationOffset = 0;
|
||
let notificationPrefs = {
|
||
email_enabled: true,
|
||
push_enabled: false,
|
||
in_app_enabled: true
|
||
};
|
||
|
||
// Toggle notification panel
|
||
document.getElementById('notification-bell-btn')?.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const panel = document.getElementById('notification-panel');
|
||
panel.classList.toggle('active');
|
||
|
||
// Close user menu if open
|
||
const userMenu = document.getElementById('auth-user-menu');
|
||
userMenu?.classList.remove('active');
|
||
});
|
||
|
||
// Close notification panel when clicking outside
|
||
document.addEventListener('click', (e) => {
|
||
const bell = document.getElementById('notification-bell');
|
||
const panel = document.getElementById('notification-panel');
|
||
if (bell && panel && !bell.contains(e.target)) {
|
||
panel.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
// Load notifications from API
|
||
async function loadNotifications(append = false) {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const limit = 10;
|
||
const offset = append ? notificationOffset : 0;
|
||
|
||
const response = await fetch(`/api/v1/notifications?limit=${limit}&offset=${offset}`, {
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
if (!response.ok) return;
|
||
|
||
const data = await response.json();
|
||
|
||
if (!append) {
|
||
notificationOffset = 0;
|
||
}
|
||
notificationOffset += data.notifications?.length || 0;
|
||
|
||
renderNotifications(data.notifications || [], data.total || 0, append);
|
||
updateNotificationBadge();
|
||
} catch (e) {
|
||
console.error('Failed to load notifications:', e);
|
||
}
|
||
}
|
||
|
||
// Render notifications in the panel
|
||
function renderNotifications(notifications, total, append = false) {
|
||
const list = document.getElementById('notification-list');
|
||
if (!list) return;
|
||
|
||
if (!append) {
|
||
list.innerHTML = '';
|
||
}
|
||
|
||
if (notifications.length === 0 && !append) {
|
||
list.innerHTML = `
|
||
<div class="notification-empty">
|
||
<div class="notification-empty-icon">🔔</div>
|
||
<div>Keine Benachrichtigungen</div>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
notifications.forEach(n => {
|
||
const item = document.createElement('div');
|
||
item.className = `notification-item ${!n.read_at ? 'unread' : ''}`;
|
||
item.onclick = () => markNotificationRead(n.id);
|
||
|
||
const icon = getNotificationIcon(n.type);
|
||
const timeAgo = formatTimeAgo(new Date(n.created_at));
|
||
|
||
item.innerHTML = `
|
||
<div class="notification-icon">${icon}</div>
|
||
<div class="notification-content">
|
||
<div class="notification-title">${escapeHtml(n.title)}</div>
|
||
<div class="notification-body">${escapeHtml(n.body)}</div>
|
||
<div class="notification-time">${timeAgo}</div>
|
||
</div>
|
||
`;
|
||
list.appendChild(item);
|
||
});
|
||
|
||
// Show/hide load more button
|
||
const footer = document.querySelector('.notification-footer');
|
||
if (footer) {
|
||
footer.style.display = notificationOffset < total ? 'block' : 'none';
|
||
}
|
||
}
|
||
|
||
// Get icon for notification type
|
||
function getNotificationIcon(type) {
|
||
const icons = {
|
||
'consent_required': '📋',
|
||
'consent_reminder': '⏰',
|
||
'version_published': '📢',
|
||
'version_approved': '✅',
|
||
'version_rejected': '❌',
|
||
'account_suspended': '🚫',
|
||
'account_restored': '🔓',
|
||
'general': '🔔'
|
||
};
|
||
return icons[type] || '🔔';
|
||
}
|
||
|
||
// Format time ago
|
||
function formatTimeAgo(date) {
|
||
const now = new Date();
|
||
const diff = Math.floor((now - date) / 1000);
|
||
|
||
if (diff < 60) return 'Gerade eben';
|
||
if (diff < 3600) return `vor ${Math.floor(diff / 60)} Min.`;
|
||
if (diff < 86400) return `vor ${Math.floor(diff / 3600)} Std.`;
|
||
if (diff < 604800) return `vor ${Math.floor(diff / 86400)} Tagen`;
|
||
return date.toLocaleDateString('de-DE');
|
||
}
|
||
|
||
// Escape HTML to prevent XSS
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
// Update notification badge
|
||
async function updateNotificationBadge() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/notifications/unread-count', {
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
if (!response.ok) return;
|
||
|
||
const data = await response.json();
|
||
const badge = document.getElementById('notification-badge');
|
||
|
||
if (badge) {
|
||
const count = data.unread_count || 0;
|
||
badge.textContent = count > 99 ? '99+' : count;
|
||
badge.classList.toggle('hidden', count === 0);
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to update badge:', e);
|
||
}
|
||
}
|
||
|
||
// Mark notification as read
|
||
async function markNotificationRead(id) {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
await fetch(`/api/v1/notifications/${id}/read`, {
|
||
method: 'PUT',
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
// Update UI
|
||
const item = document.querySelector(`.notification-item[onclick*="${id}"]`);
|
||
if (item) item.classList.remove('unread');
|
||
updateNotificationBadge();
|
||
} catch (e) {
|
||
console.error('Failed to mark notification as read:', e);
|
||
}
|
||
}
|
||
|
||
// Mark all notifications as read
|
||
async function markAllNotificationsRead() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
await fetch('/api/v1/notifications/read-all', {
|
||
method: 'PUT',
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
// Update UI
|
||
document.querySelectorAll('.notification-item.unread').forEach(item => {
|
||
item.classList.remove('unread');
|
||
});
|
||
updateNotificationBadge();
|
||
} catch (e) {
|
||
console.error('Failed to mark all as read:', e);
|
||
}
|
||
}
|
||
|
||
// Load more notifications
|
||
function loadMoreNotifications() {
|
||
loadNotifications(true);
|
||
}
|
||
|
||
// Start polling for new notifications
|
||
function startNotificationPolling() {
|
||
stopNotificationPolling();
|
||
notificationPollingInterval = setInterval(() => {
|
||
updateNotificationBadge();
|
||
}, 30000); // Poll every 30 seconds
|
||
}
|
||
|
||
// Stop polling
|
||
function stopNotificationPolling() {
|
||
if (notificationPollingInterval) {
|
||
clearInterval(notificationPollingInterval);
|
||
notificationPollingInterval = null;
|
||
}
|
||
}
|
||
|
||
// Show notification preferences modal
|
||
function showNotificationPreferences() {
|
||
document.getElementById('notification-panel')?.classList.remove('active');
|
||
document.getElementById('notification-prefs-modal')?.classList.add('active');
|
||
loadNotificationPreferences();
|
||
}
|
||
|
||
// Close notification preferences modal
|
||
function closeNotificationPreferences() {
|
||
document.getElementById('notification-prefs-modal')?.classList.remove('active');
|
||
}
|
||
|
||
// Load notification preferences
|
||
async function loadNotificationPreferences() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/notifications/preferences', {
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
if (!response.ok) return;
|
||
|
||
const prefs = await response.json();
|
||
notificationPrefs = prefs;
|
||
|
||
// Update UI toggles
|
||
updateToggle('pref-email-toggle', prefs.email_enabled);
|
||
updateToggle('pref-inapp-toggle', prefs.in_app_enabled);
|
||
updateToggle('pref-push-toggle', prefs.push_enabled);
|
||
} catch (e) {
|
||
console.error('Failed to load preferences:', e);
|
||
}
|
||
}
|
||
|
||
// Update toggle UI
|
||
function updateToggle(id, active) {
|
||
const toggle = document.getElementById(id);
|
||
if (toggle) {
|
||
toggle.classList.toggle('active', active);
|
||
}
|
||
}
|
||
|
||
// Toggle notification preference
|
||
function toggleNotificationPref(type) {
|
||
const toggleMap = {
|
||
'email': 'pref-email-toggle',
|
||
'inapp': 'pref-inapp-toggle',
|
||
'push': 'pref-push-toggle'
|
||
};
|
||
const prefMap = {
|
||
'email': 'email_enabled',
|
||
'inapp': 'in_app_enabled',
|
||
'push': 'push_enabled'
|
||
};
|
||
|
||
const toggleId = toggleMap[type];
|
||
const prefKey = prefMap[type];
|
||
const toggle = document.getElementById(toggleId);
|
||
|
||
if (toggle) {
|
||
const isActive = toggle.classList.toggle('active');
|
||
notificationPrefs[prefKey] = isActive;
|
||
}
|
||
}
|
||
|
||
// Save notification preferences
|
||
async function saveNotificationPreferences() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/notifications/preferences', {
|
||
method: 'PUT',
|
||
headers: {
|
||
'Authorization': `Bearer ${accessToken}`,
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify(notificationPrefs)
|
||
});
|
||
|
||
if (response.ok) {
|
||
closeNotificationPreferences();
|
||
alert('Einstellungen gespeichert!');
|
||
} else {
|
||
alert('Fehler beim Speichern der Einstellungen');
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to save preferences:', e);
|
||
alert('Fehler beim Speichern der Einstellungen');
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// SUSPENSION CHECK FUNCTIONS
|
||
// ==========================================
|
||
let isSuspended = false;
|
||
|
||
// Check suspension status after login
|
||
async function checkSuspensionStatus() {
|
||
if (!accessToken) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/account/suspension-status', {
|
||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||
});
|
||
|
||
if (!response.ok) return;
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.suspended) {
|
||
isSuspended = true;
|
||
showSuspensionOverlay(data);
|
||
} else {
|
||
isSuspended = false;
|
||
hideSuspensionOverlay();
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to check suspension status:', e);
|
||
}
|
||
}
|
||
|
||
// Show suspension overlay
|
||
function showSuspensionOverlay(data) {
|
||
const overlay = document.getElementById('suspension-overlay');
|
||
const docList = document.getElementById('suspension-doc-list');
|
||
|
||
if (!overlay || !docList) return;
|
||
|
||
// Populate document list
|
||
if (data.pending_deadlines && data.pending_deadlines.length > 0) {
|
||
docList.innerHTML = data.pending_deadlines.map(d => {
|
||
const deadline = new Date(d.deadline_at);
|
||
const isOverdue = deadline < new Date();
|
||
return `
|
||
<div class="suspension-doc-item">
|
||
<span class="suspension-doc-name">${escapeHtml(d.document_name)}</span>
|
||
<span class="suspension-doc-deadline">${isOverdue ? 'Überfällig' : deadline.toLocaleDateString('de-DE')}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
} else if (data.details && data.details.documents) {
|
||
docList.innerHTML = data.details.documents.map(doc => `
|
||
<div class="suspension-doc-item">
|
||
<span class="suspension-doc-name">${escapeHtml(doc)}</span>
|
||
<span class="suspension-doc-deadline">Bestätigung erforderlich</span>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
overlay.classList.add('active');
|
||
}
|
||
|
||
// Hide suspension overlay
|
||
function hideSuspensionOverlay() {
|
||
const overlay = document.getElementById('suspension-overlay');
|
||
if (overlay) {
|
||
overlay.classList.remove('active');
|
||
}
|
||
}
|
||
|
||
// Show consent modal from suspension overlay
|
||
function showConsentModal() {
|
||
hideSuspensionOverlay();
|
||
// Open legal modal to consent tab
|
||
document.getElementById('legal-modal')?.classList.add('active');
|
||
// Switch to appropriate tab
|
||
}
|
||
|
||
// Initialize auth on page load
|
||
checkAuthStatus();
|
||
checkAuthUrlParams();
|
||
|
||
// ==========================================
|
||
// RICH TEXT EDITOR FUNCTIONS
|
||
// ==========================================
|
||
const versionEditor = document.getElementById('admin-version-editor');
|
||
const versionContentHidden = document.getElementById('admin-version-content');
|
||
const editorCharCount = document.getElementById('editor-char-count');
|
||
|
||
// Update hidden field and char count when editor content changes
|
||
versionEditor?.addEventListener('input', () => {
|
||
versionContentHidden.value = versionEditor.innerHTML;
|
||
const textLength = versionEditor.textContent.length;
|
||
editorCharCount.textContent = `${textLength} Zeichen`;
|
||
});
|
||
|
||
// Format document with execCommand
|
||
function formatDoc(cmd, value = null) {
|
||
versionEditor.focus();
|
||
document.execCommand(cmd, false, value);
|
||
}
|
||
|
||
// Format block element
|
||
function formatBlock(tag) {
|
||
versionEditor.focus();
|
||
document.execCommand('formatBlock', false, `<${tag}>`);
|
||
}
|
||
|
||
// Insert link
|
||
function insertLink() {
|
||
const url = prompt('Link-URL eingeben:', 'https://');
|
||
if (url) {
|
||
versionEditor.focus();
|
||
document.execCommand('createLink', false, url);
|
||
}
|
||
}
|
||
|
||
// Handle Word document upload
|
||
async function handleWordUpload(event) {
|
||
const file = event.target.files[0];
|
||
if (!file) return;
|
||
|
||
// Show loading indicator
|
||
const editor = document.getElementById('admin-version-editor');
|
||
const originalContent = editor.innerHTML;
|
||
editor.innerHTML = '<p style="color: var(--bp-text-muted); text-align: center; padding: 40px;">Word-Dokument wird verarbeitet...</p>';
|
||
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
|
||
try {
|
||
const response = await fetch('/api/consent/admin/versions/upload-word', {
|
||
method: 'POST',
|
||
body: formData
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
editor.innerHTML = data.html || '<p>Konvertierung fehlgeschlagen</p>';
|
||
versionContentHidden.value = editor.innerHTML;
|
||
|
||
// Update char count
|
||
const textLength = editor.textContent.length;
|
||
editorCharCount.textContent = `${textLength} Zeichen`;
|
||
} else {
|
||
const error = await response.json();
|
||
editor.innerHTML = originalContent;
|
||
alert('Fehler beim Importieren: ' + (error.detail || 'Unbekannter Fehler'));
|
||
}
|
||
} catch (e) {
|
||
editor.innerHTML = originalContent;
|
||
alert('Fehler beim Hochladen: ' + e.message);
|
||
}
|
||
|
||
// Reset file input
|
||
event.target.value = '';
|
||
}
|
||
|
||
// Handle paste from Word - clean up the HTML
|
||
versionEditor?.addEventListener('paste', (e) => {
|
||
// Get pasted data via clipboard API
|
||
const clipboardData = e.clipboardData || window.clipboardData;
|
||
const pastedData = clipboardData.getData('text/html') || clipboardData.getData('text/plain');
|
||
|
||
if (pastedData && clipboardData.getData('text/html')) {
|
||
e.preventDefault();
|
||
|
||
// Clean the HTML
|
||
const cleanHtml = cleanWordHtml(pastedData);
|
||
document.execCommand('insertHTML', false, cleanHtml);
|
||
|
||
// Update hidden field
|
||
versionContentHidden.value = versionEditor.innerHTML;
|
||
}
|
||
});
|
||
|
||
// Clean Word-specific HTML
|
||
function cleanWordHtml(html) {
|
||
// Create a temporary container
|
||
const temp = document.createElement('div');
|
||
temp.innerHTML = html;
|
||
|
||
// Remove Word-specific elements and attributes
|
||
const elementsToRemove = temp.querySelectorAll('style, script, meta, link, xml');
|
||
elementsToRemove.forEach(el => el.remove());
|
||
|
||
// Get text content from Word spans with specific styling
|
||
let cleanedHtml = temp.innerHTML;
|
||
|
||
// Remove mso-* styles and other Office-specific CSS
|
||
cleanedHtml = cleanedHtml.replace(/\s*mso-[^:]+:[^;]+;?/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/\s*style="[^"]*"/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/\s*class="[^"]*"/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/<o:p><\/o:p>/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/<\/?o:[^>]*>/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/<\/?w:[^>]*>/gi, '');
|
||
cleanedHtml = cleanedHtml.replace(/<\/?m:[^>]*>/gi, '');
|
||
|
||
// Clean up empty spans
|
||
cleanedHtml = cleanedHtml.replace(/<span[^>]*>\s*<\/span>/gi, '');
|
||
|
||
// Convert Word list markers to proper lists
|
||
cleanedHtml = cleanedHtml.replace(/<p[^>]*>\s*[•·]\s*/gi, '<li>');
|
||
|
||
return cleanedHtml;
|
||
}
|
||
|
||
// ==========================================
|
||
// ADMIN PANEL
|
||
// ==========================================
|
||
const adminModal = document.getElementById('admin-modal');
|
||
const adminModalClose = document.getElementById('admin-modal-close');
|
||
const adminTabs = document.querySelectorAll('.admin-tab');
|
||
const adminContents = document.querySelectorAll('.admin-content');
|
||
const btnAdmin = document.getElementById('btn-admin');
|
||
|
||
// Admin data cache
|
||
let adminDocuments = [];
|
||
let adminCookieCategories = [];
|
||
|
||
// Open admin modal
|
||
btnAdmin?.addEventListener('click', async () => {
|
||
adminModal.classList.add('active');
|
||
await loadAdminDocuments();
|
||
await loadAdminCookieCategories();
|
||
populateDocumentSelect();
|
||
});
|
||
|
||
// Close admin modal
|
||
adminModalClose?.addEventListener('click', () => {
|
||
adminModal.classList.remove('active');
|
||
});
|
||
|
||
// Close on background click
|
||
adminModal?.addEventListener('click', (e) => {
|
||
if (e.target === adminModal) {
|
||
adminModal.classList.remove('active');
|
||
}
|
||
});
|
||
|
||
// Admin tab switching
|
||
adminTabs.forEach(tab => {
|
||
tab.addEventListener('click', () => {
|
||
const tabId = tab.dataset.tab;
|
||
adminTabs.forEach(t => t.classList.remove('active'));
|
||
adminContents.forEach(c => c.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
document.getElementById(`admin-${tabId}`)?.classList.add('active');
|
||
|
||
// Load stats when stats tab is clicked
|
||
if (tabId === 'stats') {
|
||
loadAdminStats();
|
||
}
|
||
});
|
||
});
|
||
|
||
// ==========================================
|
||
// DOCUMENTS MANAGEMENT
|
||
// ==========================================
|
||
async function loadAdminDocuments() {
|
||
const container = document.getElementById('admin-doc-table-container');
|
||
container.innerHTML = '<div class="admin-loading">Lade Dokumente...</div>';
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/admin/documents');
|
||
if (!res.ok) throw new Error('Failed to load');
|
||
const data = await res.json();
|
||
adminDocuments = data.documents || [];
|
||
renderDocumentsTable();
|
||
} catch(e) {
|
||
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Dokumente.</div>';
|
||
}
|
||
}
|
||
|
||
function renderDocumentsTable() {
|
||
const container = document.getElementById('admin-doc-table-container');
|
||
|
||
// Alle Dokumente anzeigen
|
||
const allDocs = adminDocuments;
|
||
|
||
if (allDocs.length === 0) {
|
||
container.innerHTML = '<div class="admin-empty">Keine Dokumente vorhanden. Klicken Sie auf "+ Neues Dokument" um ein Dokument zu erstellen.</div>';
|
||
return;
|
||
}
|
||
|
||
const typeLabels = {
|
||
'terms': 'AGB',
|
||
'privacy': 'Datenschutz',
|
||
'cookies': 'Cookies',
|
||
'community': 'Community',
|
||
'imprint': 'Impressum'
|
||
};
|
||
|
||
const html = `
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Typ</th>
|
||
<th>Name</th>
|
||
<th>Beschreibung</th>
|
||
<th>Status</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${allDocs.map(doc => `
|
||
<tr>
|
||
<td><span class="admin-badge admin-badge-${doc.is_mandatory ? 'mandatory' : 'optional'}">${typeLabels[doc.type] || doc.type}</span></td>
|
||
<td>${doc.name}</td>
|
||
<td style="color: var(--bp-text-muted); font-size: 12px;">${doc.description || '-'}</td>
|
||
<td>
|
||
${doc.is_active ? '<span class="admin-badge admin-badge-published">Aktiv</span>' : '<span class="admin-badge admin-badge-draft">Inaktiv</span>'}
|
||
${doc.is_mandatory ? '<span class="admin-badge admin-badge-mandatory" style="margin-left: 4px;">Pflicht</span>' : ''}
|
||
</td>
|
||
<td class="admin-actions">
|
||
<button class="admin-btn admin-btn-secondary" onclick="editDocument('${doc.id}')" title="Bearbeiten">✏️</button>
|
||
<button class="admin-btn admin-btn-primary" onclick="goToVersions('${doc.id}')">Versionen</button>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function goToVersions(docId) {
|
||
// Wechsle zum Versionen-Tab und wähle das Dokument aus
|
||
const versionsTab = document.querySelector('.admin-tab[data-tab="versions"]');
|
||
if (versionsTab) {
|
||
versionsTab.click();
|
||
setTimeout(() => {
|
||
const select = document.getElementById('admin-version-doc-select');
|
||
if (select) {
|
||
select.value = docId;
|
||
loadVersionsForDocument();
|
||
}
|
||
}, 100);
|
||
}
|
||
}
|
||
|
||
function showDocumentForm(doc = null) {
|
||
const form = document.getElementById('admin-document-form');
|
||
const title = document.getElementById('admin-document-form-title');
|
||
|
||
if (doc) {
|
||
title.textContent = 'Dokument bearbeiten';
|
||
document.getElementById('admin-document-id').value = doc.id;
|
||
document.getElementById('admin-document-type').value = doc.type;
|
||
document.getElementById('admin-document-name').value = doc.name;
|
||
document.getElementById('admin-document-description').value = doc.description || '';
|
||
document.getElementById('admin-document-mandatory').checked = doc.is_mandatory;
|
||
} else {
|
||
title.textContent = 'Neues Dokument erstellen';
|
||
document.getElementById('admin-document-id').value = '';
|
||
document.getElementById('admin-document-type').value = '';
|
||
document.getElementById('admin-document-name').value = '';
|
||
document.getElementById('admin-document-description').value = '';
|
||
document.getElementById('admin-document-mandatory').checked = true;
|
||
}
|
||
|
||
form.style.display = 'block';
|
||
}
|
||
|
||
function hideDocumentForm() {
|
||
document.getElementById('admin-document-form').style.display = 'none';
|
||
}
|
||
|
||
function editDocument(docId) {
|
||
const doc = adminDocuments.find(d => d.id === docId);
|
||
if (doc) showDocumentForm(doc);
|
||
}
|
||
|
||
async function saveDocument() {
|
||
const docId = document.getElementById('admin-document-id').value;
|
||
const docType = document.getElementById('admin-document-type').value;
|
||
const docName = document.getElementById('admin-document-name').value;
|
||
|
||
if (!docType || !docName) {
|
||
alert('Bitte füllen Sie alle Pflichtfelder aus (Typ und Name).');
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
type: docType,
|
||
name: docName,
|
||
description: document.getElementById('admin-document-description').value || null,
|
||
is_mandatory: document.getElementById('admin-document-mandatory').checked
|
||
};
|
||
|
||
try {
|
||
const url = docId ? `/api/consent/admin/documents/${docId}` : '/api/consent/admin/documents';
|
||
const method = docId ? 'PUT' : 'POST';
|
||
|
||
const res = await fetch(url, {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (!res.ok) throw new Error('Failed to save');
|
||
|
||
hideDocumentForm();
|
||
await loadAdminDocuments();
|
||
populateDocumentSelect();
|
||
alert('Dokument gespeichert!');
|
||
} catch(e) {
|
||
alert('Fehler beim Speichern: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteDocument(docId) {
|
||
if (!confirm('Dokument wirklich deaktivieren?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/documents/${docId}`, { method: 'DELETE' });
|
||
if (!res.ok) throw new Error('Failed to delete');
|
||
|
||
await loadAdminDocuments();
|
||
populateDocumentSelect();
|
||
alert('Dokument deaktiviert!');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// VERSIONS MANAGEMENT
|
||
// ==========================================
|
||
function populateDocumentSelect() {
|
||
const select = document.getElementById('admin-version-doc-select');
|
||
const uniqueDocs = [...new Map(adminDocuments.map(d => [d.type, d])).values()];
|
||
|
||
select.innerHTML = '<option value="">-- Dokument auswählen --</option>' +
|
||
adminDocuments.filter(d => d.is_active).map(doc =>
|
||
`<option value="${doc.id}">${doc.name} (${doc.type})</option>`
|
||
).join('');
|
||
}
|
||
|
||
async function loadVersionsForDocument() {
|
||
const docId = document.getElementById('admin-version-doc-select').value;
|
||
const container = document.getElementById('admin-version-table-container');
|
||
const btnNew = document.getElementById('btn-new-version');
|
||
|
||
if (!docId) {
|
||
container.innerHTML = '<div class="admin-empty">Wählen Sie ein Dokument aus.</div>';
|
||
btnNew.disabled = true;
|
||
return;
|
||
}
|
||
|
||
btnNew.disabled = false;
|
||
container.innerHTML = '<div class="admin-loading">Lade Versionen...</div>';
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
|
||
if (!res.ok) throw new Error('Failed to load');
|
||
const data = await res.json();
|
||
renderVersionsTable(data.versions || []);
|
||
} catch(e) {
|
||
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Versionen.</div>';
|
||
}
|
||
}
|
||
|
||
function renderVersionsTable(versions) {
|
||
const container = document.getElementById('admin-version-table-container');
|
||
if (versions.length === 0) {
|
||
container.innerHTML = '<div class="admin-empty">Keine Versionen vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const getStatusBadge = (status) => {
|
||
const statusLabels = {
|
||
'draft': 'Entwurf',
|
||
'review': 'In Prüfung',
|
||
'approved': 'Genehmigt',
|
||
'rejected': 'Abgelehnt',
|
||
'scheduled': 'Geplant',
|
||
'published': 'Veröffentlicht',
|
||
'archived': 'Archiviert'
|
||
};
|
||
return statusLabels[status] || status;
|
||
};
|
||
|
||
const formatScheduledDate = (isoDate) => {
|
||
if (!isoDate) return '';
|
||
const date = new Date(isoDate);
|
||
return date.toLocaleString('de-DE', {
|
||
day: '2-digit', month: '2-digit', year: 'numeric',
|
||
hour: '2-digit', minute: '2-digit'
|
||
});
|
||
};
|
||
|
||
const html = `
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Version</th>
|
||
<th>Sprache</th>
|
||
<th>Titel</th>
|
||
<th>Status</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${versions.map(v => `
|
||
<tr>
|
||
<td>${v.version}</td>
|
||
<td>${v.language.toUpperCase()}</td>
|
||
<td>${v.title}</td>
|
||
<td>
|
||
<span class="admin-badge admin-badge-${v.status}">${getStatusBadge(v.status)}</span>
|
||
${v.scheduled_publish_at ? `<br><small>Geplant: ${formatScheduledDate(v.scheduled_publish_at)}</small>` : ''}
|
||
</td>
|
||
<td class="admin-actions">
|
||
${v.status === 'draft' ? `
|
||
<button class="admin-btn admin-btn-edit" onclick="editVersion('${v.id}')">Bearbeiten</button>
|
||
<button class="admin-btn admin-btn-primary" onclick="submitForReview('${v.id}')">Zur Prüfung</button>
|
||
<button class="admin-btn admin-btn-delete" onclick="deleteVersion('${v.id}')" title="Version dauerhaft löschen">🗑️</button>
|
||
` : ''}
|
||
${v.status === 'review' ? `
|
||
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
|
||
<button class="admin-btn admin-btn-primary" onclick="showApprovalDialog('${v.id}')">Genehmigen</button>
|
||
<button class="admin-btn admin-btn-delete" onclick="rejectVersion('${v.id}')">Ablehnen</button>
|
||
` : ''}
|
||
${v.status === 'rejected' ? `
|
||
<button class="admin-btn admin-btn-edit" onclick="editVersion('${v.id}')">Bearbeiten</button>
|
||
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
|
||
<button class="admin-btn admin-btn-delete" onclick="deleteVersion('${v.id}')" title="Version dauerhaft löschen">🗑️</button>
|
||
` : ''}
|
||
${v.status === 'scheduled' ? `
|
||
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
|
||
<span class="admin-info-text">Wartet auf Veröffentlichung</span>
|
||
` : ''}
|
||
${v.status === 'approved' ? `
|
||
<button class="admin-btn admin-btn-edit" onclick="showCompareView('${v.id}')">Vergleichen</button>
|
||
<button class="admin-btn admin-btn-publish" onclick="publishVersion('${v.id}')">Sofort veröffentlichen</button>
|
||
<button class="admin-btn admin-btn-delete" onclick="rejectVersion('${v.id}')">Zurücksetzen</button>
|
||
` : ''}
|
||
${v.status === 'published' ? `
|
||
<button class="admin-btn admin-btn-delete" onclick="archiveVersion('${v.id}')">Archivieren</button>
|
||
` : ''}
|
||
<button class="admin-btn" onclick="showApprovalHistory('${v.id}')">Historie</button>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function showVersionForm() {
|
||
const form = document.getElementById('admin-version-form');
|
||
document.getElementById('admin-version-id').value = '';
|
||
document.getElementById('admin-version-number').value = '';
|
||
document.getElementById('admin-version-lang').value = 'de';
|
||
document.getElementById('admin-version-title').value = '';
|
||
document.getElementById('admin-version-summary').value = '';
|
||
document.getElementById('admin-version-content').value = '';
|
||
// Clear rich text editor
|
||
const editor = document.getElementById('admin-version-editor');
|
||
if (editor) {
|
||
editor.innerHTML = '';
|
||
document.getElementById('editor-char-count').textContent = '0 Zeichen';
|
||
}
|
||
form.classList.add('active');
|
||
}
|
||
|
||
function hideVersionForm() {
|
||
document.getElementById('admin-version-form').classList.remove('active');
|
||
}
|
||
|
||
async function editVersion(versionId) {
|
||
// Lade die Version und fülle das Formular
|
||
const docId = document.getElementById('admin-version-doc-select').value;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
|
||
if (!res.ok) throw new Error('Failed to load versions');
|
||
const data = await res.json();
|
||
|
||
const version = (data.versions || []).find(v => v.id === versionId);
|
||
if (!version) {
|
||
alert('Version nicht gefunden');
|
||
return;
|
||
}
|
||
|
||
// Formular öffnen und Daten einfügen
|
||
const form = document.getElementById('admin-version-form');
|
||
document.getElementById('admin-version-id').value = version.id;
|
||
document.getElementById('admin-version-number').value = version.version;
|
||
document.getElementById('admin-version-lang').value = version.language;
|
||
document.getElementById('admin-version-title').value = version.title;
|
||
document.getElementById('admin-version-summary').value = version.summary || '';
|
||
|
||
// Rich-Text-Editor mit Inhalt füllen
|
||
const editor = document.getElementById('admin-version-editor');
|
||
if (editor) {
|
||
editor.innerHTML = version.content || '';
|
||
const charCount = editor.textContent.length;
|
||
document.getElementById('editor-char-count').textContent = charCount + ' Zeichen';
|
||
}
|
||
document.getElementById('admin-version-content').value = version.content || '';
|
||
|
||
form.classList.add('active');
|
||
} catch(e) {
|
||
alert('Fehler beim Laden der Version: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function saveVersion() {
|
||
const docId = document.getElementById('admin-version-doc-select').value;
|
||
const versionId = document.getElementById('admin-version-id').value;
|
||
|
||
// Get content from rich text editor
|
||
const editor = document.getElementById('admin-version-editor');
|
||
const content = editor ? editor.innerHTML : document.getElementById('admin-version-content').value;
|
||
|
||
const data = {
|
||
document_id: docId,
|
||
version: document.getElementById('admin-version-number').value,
|
||
language: document.getElementById('admin-version-lang').value,
|
||
title: document.getElementById('admin-version-title').value,
|
||
summary: document.getElementById('admin-version-summary').value,
|
||
content: content
|
||
};
|
||
|
||
try {
|
||
const url = versionId ? `/api/consent/admin/versions/${versionId}` : '/api/consent/admin/versions';
|
||
const method = versionId ? 'PUT' : 'POST';
|
||
|
||
const res = await fetch(url, {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (!res.ok) throw new Error('Failed to save');
|
||
|
||
hideVersionForm();
|
||
await loadVersionsForDocument();
|
||
alert('Version gespeichert!');
|
||
} catch(e) {
|
||
alert('Fehler beim Speichern: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function publishVersion(versionId) {
|
||
if (!confirm('Version wirklich veröffentlichen?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/publish`, { method: 'POST' });
|
||
if (!res.ok) throw new Error('Failed to publish');
|
||
|
||
await loadVersionsForDocument();
|
||
alert('Version veröffentlicht!');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function archiveVersion(versionId) {
|
||
if (!confirm('Version wirklich archivieren?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/archive`, { method: 'POST' });
|
||
if (!res.ok) throw new Error('Failed to archive');
|
||
|
||
await loadVersionsForDocument();
|
||
alert('Version archiviert!');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteVersion(versionId) {
|
||
if (!confirm('Version wirklich dauerhaft löschen?\\n\\nDie Versionsnummer wird wieder frei und kann erneut verwendet werden.\\n\\nDiese Aktion kann nicht rückgängig gemacht werden!')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}`, { method: 'DELETE' });
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.message || err.error || 'Löschen fehlgeschlagen');
|
||
}
|
||
|
||
await loadVersionsForDocument();
|
||
alert('Version wurde dauerhaft gelöscht.');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// DSB APPROVAL WORKFLOW
|
||
// ==========================================
|
||
|
||
async function submitForReview(versionId) {
|
||
if (!confirm('Version zur DSB-Prüfung einreichen?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/submit-review`, { method: 'POST' });
|
||
if (!res.ok) {
|
||
const data = await res.json();
|
||
throw new Error(data.detail?.error || 'Einreichung fehlgeschlagen');
|
||
}
|
||
|
||
await loadVersionsForDocument();
|
||
alert('Version wurde zur Prüfung eingereicht!');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Dialog für Genehmigung mit Veröffentlichungszeitpunkt
|
||
let approvalVersionId = null;
|
||
|
||
function showApprovalDialog(versionId) {
|
||
approvalVersionId = versionId;
|
||
const dialog = document.getElementById('approval-dialog');
|
||
|
||
// Setze Minimum-Datum auf morgen
|
||
const tomorrow = new Date();
|
||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||
tomorrow.setHours(0, 0, 0, 0);
|
||
document.getElementById('approval-date').min = tomorrow.toISOString().split('T')[0];
|
||
document.getElementById('approval-date').value = '';
|
||
document.getElementById('approval-time').value = '00:00';
|
||
document.getElementById('approval-comment').value = '';
|
||
|
||
dialog.classList.add('active');
|
||
}
|
||
|
||
function hideApprovalDialog() {
|
||
document.getElementById('approval-dialog').classList.remove('active');
|
||
approvalVersionId = null;
|
||
}
|
||
|
||
async function submitApproval() {
|
||
if (!approvalVersionId) return;
|
||
|
||
const dateInput = document.getElementById('approval-date').value;
|
||
const timeInput = document.getElementById('approval-time').value;
|
||
const comment = document.getElementById('approval-comment').value;
|
||
|
||
let scheduledPublishAt = null;
|
||
if (dateInput) {
|
||
// Kombiniere Datum und Zeit zu ISO 8601
|
||
const datetime = new Date(dateInput + 'T' + (timeInput || '00:00') + ':00');
|
||
scheduledPublishAt = datetime.toISOString();
|
||
}
|
||
|
||
try {
|
||
const body = { comment: comment || '' };
|
||
if (scheduledPublishAt) {
|
||
body.scheduled_publish_at = scheduledPublishAt;
|
||
}
|
||
|
||
const res = await fetch(`/api/consent/admin/versions/${approvalVersionId}/approve`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(body)
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const data = await res.json();
|
||
throw new Error(data.detail?.error || data.detail || 'Genehmigung fehlgeschlagen');
|
||
}
|
||
|
||
hideApprovalDialog();
|
||
await loadVersionsForDocument();
|
||
|
||
if (scheduledPublishAt) {
|
||
const date = new Date(scheduledPublishAt);
|
||
alert('Version genehmigt! Geplante Veröffentlichung: ' + date.toLocaleString('de-DE'));
|
||
} else {
|
||
alert('Version genehmigt! Sie kann jetzt manuell veröffentlicht werden.');
|
||
}
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Alte Funktion für Rückwärtskompatibilität
|
||
async function approveVersion(versionId) {
|
||
showApprovalDialog(versionId);
|
||
}
|
||
|
||
async function rejectVersion(versionId) {
|
||
const comment = prompt('Begründung für Ablehnung (erforderlich):');
|
||
if (!comment) {
|
||
alert('Eine Begründung ist erforderlich.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/reject`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ comment: comment })
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const data = await res.json();
|
||
throw new Error(data.detail?.error || data.detail || 'Ablehnung fehlgeschlagen');
|
||
}
|
||
|
||
await loadVersionsForDocument();
|
||
alert('Version abgelehnt und zurück in Entwurf-Status versetzt.');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Store current compare version for actions
|
||
let currentCompareVersionId = null;
|
||
let currentCompareVersionStatus = null;
|
||
let currentCompareDocId = null;
|
||
|
||
async function showCompareView(versionId) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/compare`);
|
||
if (!res.ok) throw new Error('Vergleich konnte nicht geladen werden');
|
||
const data = await res.json();
|
||
|
||
const currentVersion = data.current_version;
|
||
const publishedVersion = data.published_version;
|
||
const history = data.approval_history || [];
|
||
|
||
// Store version info for actions
|
||
currentCompareVersionId = versionId;
|
||
currentCompareVersionStatus = currentVersion.status;
|
||
currentCompareDocId = currentVersion.document_id;
|
||
|
||
// Update header info
|
||
document.getElementById('compare-published-info').textContent =
|
||
publishedVersion ? `${publishedVersion.title} (v${publishedVersion.version})` : 'Keine Version';
|
||
document.getElementById('compare-draft-info').textContent =
|
||
`${currentVersion.title} (v${currentVersion.version})`;
|
||
document.getElementById('compare-published-version').textContent =
|
||
publishedVersion ? `v${publishedVersion.version}` : '';
|
||
document.getElementById('compare-draft-version').textContent =
|
||
`v${currentVersion.version} - ${currentVersion.status}`;
|
||
|
||
// Populate content panels
|
||
const leftPanel = document.getElementById('compare-content-left');
|
||
const rightPanel = document.getElementById('compare-content-right');
|
||
|
||
leftPanel.innerHTML = publishedVersion
|
||
? publishedVersion.content
|
||
: '<div class="no-content">Keine veröffentlichte Version vorhanden</div>';
|
||
rightPanel.innerHTML = currentVersion.content || '<div class="no-content">Kein Inhalt</div>';
|
||
|
||
// Populate history
|
||
const historyContainer = document.getElementById('compare-history-container');
|
||
if (history.length > 0) {
|
||
historyContainer.innerHTML = `
|
||
<div class="compare-history-title">Genehmigungsverlauf</div>
|
||
<div class="compare-history-list">
|
||
${history.map(h => `
|
||
<span class="compare-history-item">
|
||
<strong>${h.action}</strong> von ${h.approver || 'System'}
|
||
(${new Date(h.created_at).toLocaleString('de-DE')})
|
||
${h.comment ? ': ' + h.comment : ''}
|
||
</span>
|
||
`).join(' | ')}
|
||
</div>
|
||
`;
|
||
} else {
|
||
historyContainer.innerHTML = '';
|
||
}
|
||
|
||
// Render action buttons based on status
|
||
renderCompareActions(currentVersion.status, versionId);
|
||
|
||
// Setup synchronized scrolling
|
||
setupSyncScroll(leftPanel, rightPanel);
|
||
|
||
// Show the overlay
|
||
document.getElementById('version-compare-view').classList.add('active');
|
||
document.body.style.overflow = 'hidden';
|
||
} catch(e) {
|
||
alert('Fehler beim Laden des Vergleichs: ' + e.message);
|
||
}
|
||
}
|
||
|
||
function renderCompareActions(status, versionId) {
|
||
const actionsContainer = document.getElementById('compare-actions-container');
|
||
|
||
let buttons = '';
|
||
|
||
// Edit button - available for draft, review, and rejected
|
||
if (status === 'draft' || status === 'review' || status === 'rejected') {
|
||
buttons += `<button class="btn btn-primary" onclick="editVersionFromCompare('${versionId}')">
|
||
✏️ Version bearbeiten
|
||
</button>`;
|
||
}
|
||
|
||
// Status-specific actions
|
||
if (status === 'draft') {
|
||
buttons += `<button class="btn" onclick="submitForReviewFromCompare('${versionId}')">
|
||
📤 Zur Prüfung einreichen
|
||
</button>`;
|
||
}
|
||
|
||
if (status === 'review') {
|
||
buttons += `<button class="btn btn-success" onclick="approveVersionFromCompare('${versionId}')">
|
||
✅ Genehmigen
|
||
</button>`;
|
||
buttons += `<button class="btn btn-danger" onclick="rejectVersionFromCompare('${versionId}')">
|
||
❌ Ablehnen
|
||
</button>`;
|
||
}
|
||
|
||
if (status === 'approved') {
|
||
buttons += `<button class="btn btn-primary" onclick="publishVersionFromCompare('${versionId}')">
|
||
🚀 Veröffentlichen
|
||
</button>`;
|
||
}
|
||
|
||
// Delete button for draft/rejected
|
||
if (status === 'draft' || status === 'rejected') {
|
||
buttons += `<button class="btn btn-danger" onclick="deleteVersionFromCompare('${versionId}')" style="margin-left: auto;">
|
||
🗑️ Löschen
|
||
</button>`;
|
||
}
|
||
|
||
actionsContainer.innerHTML = buttons;
|
||
}
|
||
|
||
async function editVersionFromCompare(versionId) {
|
||
// Store the doc ID before closing compare view
|
||
const docId = currentCompareDocId;
|
||
|
||
// Close compare view
|
||
hideCompareView();
|
||
|
||
// Switch to versions tab
|
||
const versionsTab = document.querySelector('.admin-tab[data-tab="versions"]');
|
||
if (versionsTab) {
|
||
versionsTab.click();
|
||
}
|
||
|
||
// Wait a moment for the tab to become active
|
||
await new Promise(resolve => setTimeout(resolve, 150));
|
||
|
||
// Ensure document select is populated
|
||
populateDocumentSelect();
|
||
|
||
// Set the document select if we have the doc ID
|
||
if (docId) {
|
||
const select = document.getElementById('admin-version-doc-select');
|
||
if (select) {
|
||
select.value = docId;
|
||
// Load versions for this document
|
||
await loadVersionsForDocument();
|
||
}
|
||
}
|
||
|
||
// Now load the version data directly and open the form
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
|
||
if (!res.ok) throw new Error('Failed to load versions');
|
||
const data = await res.json();
|
||
|
||
const version = (data.versions || []).find(v => v.id === versionId);
|
||
if (!version) {
|
||
alert('Version nicht gefunden');
|
||
return;
|
||
}
|
||
|
||
// Open the form and fill with version data
|
||
const form = document.getElementById('admin-version-form');
|
||
document.getElementById('admin-version-id').value = version.id;
|
||
document.getElementById('admin-version-number').value = version.version;
|
||
document.getElementById('admin-version-lang').value = version.language;
|
||
document.getElementById('admin-version-title').value = version.title;
|
||
document.getElementById('admin-version-summary').value = version.summary || '';
|
||
|
||
// Fill rich text editor with content
|
||
const editor = document.getElementById('admin-version-editor');
|
||
if (editor) {
|
||
editor.innerHTML = version.content || '';
|
||
const charCount = editor.textContent.length;
|
||
document.getElementById('editor-char-count').textContent = charCount + ' Zeichen';
|
||
}
|
||
document.getElementById('admin-version-content').value = version.content || '';
|
||
|
||
form.classList.add('active');
|
||
} catch(e) {
|
||
alert('Fehler beim Laden der Version: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function submitForReviewFromCompare(versionId) {
|
||
await submitForReview(versionId);
|
||
hideCompareView();
|
||
await loadVersionsForDocument();
|
||
}
|
||
|
||
async function approveVersionFromCompare(versionId) {
|
||
const comment = prompt('Kommentar zur Genehmigung (optional):');
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/approve`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ comment: comment || '' })
|
||
});
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.error || err.error || 'Genehmigung fehlgeschlagen');
|
||
}
|
||
alert('Version genehmigt!');
|
||
hideCompareView();
|
||
await loadVersionsForDocument();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function rejectVersionFromCompare(versionId) {
|
||
const comment = prompt('Begründung für die Ablehnung (erforderlich):');
|
||
if (!comment) {
|
||
alert('Eine Begründung ist erforderlich.');
|
||
return;
|
||
}
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/reject`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ comment: comment })
|
||
});
|
||
if (!res.ok) throw new Error('Ablehnung fehlgeschlagen');
|
||
alert('Version abgelehnt. Der Autor kann sie überarbeiten.');
|
||
hideCompareView();
|
||
await loadVersionsForDocument();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function publishVersionFromCompare(versionId) {
|
||
if (!confirm('Version wirklich veröffentlichen?')) return;
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/publish`, { method: 'POST' });
|
||
if (!res.ok) throw new Error('Veröffentlichung fehlgeschlagen');
|
||
alert('Version veröffentlicht!');
|
||
hideCompareView();
|
||
await loadVersionsForDocument();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteVersionFromCompare(versionId) {
|
||
if (!confirm('Version wirklich dauerhaft löschen?\\n\\nDie Versionsnummer wird wieder frei.')) return;
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}`, { method: 'DELETE' });
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail?.message || err.error || 'Löschen fehlgeschlagen');
|
||
}
|
||
alert('Version gelöscht!');
|
||
hideCompareView();
|
||
await loadVersionsForDocument();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
function hideCompareView() {
|
||
document.getElementById('version-compare-view').classList.remove('active');
|
||
document.body.style.overflow = '';
|
||
// Remove scroll listeners
|
||
const leftPanel = document.getElementById('compare-content-left');
|
||
const rightPanel = document.getElementById('compare-content-right');
|
||
if (leftPanel) leftPanel.onscroll = null;
|
||
if (rightPanel) rightPanel.onscroll = null;
|
||
}
|
||
|
||
// Synchronized scrolling between two panels
|
||
let syncScrollActive = false;
|
||
function setupSyncScroll(leftPanel, rightPanel) {
|
||
// Remove any existing listeners first
|
||
leftPanel.onscroll = null;
|
||
rightPanel.onscroll = null;
|
||
|
||
// Flag to prevent infinite scroll loops
|
||
let isScrolling = false;
|
||
|
||
rightPanel.onscroll = function() {
|
||
if (isScrolling) return;
|
||
isScrolling = true;
|
||
|
||
// Calculate scroll percentage
|
||
const rightScrollPercent = rightPanel.scrollTop / (rightPanel.scrollHeight - rightPanel.clientHeight);
|
||
|
||
// Apply same percentage to left panel
|
||
const leftMaxScroll = leftPanel.scrollHeight - leftPanel.clientHeight;
|
||
leftPanel.scrollTop = rightScrollPercent * leftMaxScroll;
|
||
|
||
setTimeout(() => { isScrolling = false; }, 10);
|
||
};
|
||
|
||
leftPanel.onscroll = function() {
|
||
if (isScrolling) return;
|
||
isScrolling = true;
|
||
|
||
// Calculate scroll percentage
|
||
const leftScrollPercent = leftPanel.scrollTop / (leftPanel.scrollHeight - leftPanel.clientHeight);
|
||
|
||
// Apply same percentage to right panel
|
||
const rightMaxScroll = rightPanel.scrollHeight - rightPanel.clientHeight;
|
||
rightPanel.scrollTop = leftScrollPercent * rightMaxScroll;
|
||
|
||
setTimeout(() => { isScrolling = false; }, 10);
|
||
};
|
||
}
|
||
|
||
async function showApprovalHistory(versionId) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/versions/${versionId}/approval-history`);
|
||
if (!res.ok) throw new Error('Historie konnte nicht geladen werden');
|
||
const data = await res.json();
|
||
const history = data.approval_history || [];
|
||
|
||
const content = history.length === 0
|
||
? '<p>Keine Genehmigungshistorie vorhanden.</p>'
|
||
: `
|
||
<table class="admin-table" style="font-size: 14px;">
|
||
<thead>
|
||
<tr>
|
||
<th>Aktion</th>
|
||
<th>Benutzer</th>
|
||
<th>Kommentar</th>
|
||
<th>Datum</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${history.map(h => `
|
||
<tr>
|
||
<td><span class="admin-badge admin-badge-${h.action}">${h.action}</span></td>
|
||
<td>${h.approver || h.name || '-'}</td>
|
||
<td>${h.comment || '-'}</td>
|
||
<td>${new Date(h.created_at).toLocaleString('de-DE')}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
|
||
showCustomModal('Genehmigungsverlauf', content, [
|
||
{ text: 'Schließen', onClick: () => hideCustomModal() }
|
||
]);
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Custom Modal Functions
|
||
function showCustomModal(title, content, buttons = []) {
|
||
let modal = document.getElementById('custom-modal');
|
||
if (!modal) {
|
||
modal = document.createElement('div');
|
||
modal.id = 'custom-modal';
|
||
modal.className = 'modal-overlay';
|
||
document.body.appendChild(modal);
|
||
}
|
||
|
||
modal.innerHTML = `
|
||
<div class="modal-content" style="max-width: 900px; width: 95%;">
|
||
<div class="modal-header">
|
||
<h2>${title}</h2>
|
||
<button class="modal-close" onclick="hideCustomModal()">×</button>
|
||
</div>
|
||
<div class="modal-body" style="max-height: 80vh; overflow-y: auto;">
|
||
${content}
|
||
</div>
|
||
${buttons.length > 0 ? `
|
||
<div class="modal-footer" style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px;">
|
||
${buttons.map(b => `
|
||
<button class="admin-btn ${b.primary ? 'admin-btn-primary' : ''}" onclick="(${b.onClick.toString()})()">${b.text}</button>
|
||
`).join('')}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
modal.classList.add('active');
|
||
}
|
||
|
||
function hideCustomModal() {
|
||
const modal = document.getElementById('custom-modal');
|
||
if (modal) modal.classList.remove('active');
|
||
}
|
||
|
||
// ==========================================
|
||
// COOKIE CATEGORIES MANAGEMENT
|
||
// ==========================================
|
||
async function loadAdminCookieCategories() {
|
||
const container = document.getElementById('admin-cookie-table-container');
|
||
container.innerHTML = '<div class="admin-loading">Lade Cookie-Kategorien...</div>';
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/admin/cookies/categories');
|
||
if (!res.ok) throw new Error('Failed to load');
|
||
const data = await res.json();
|
||
adminCookieCategories = data.categories || [];
|
||
renderCookieCategoriesTable();
|
||
} catch(e) {
|
||
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Kategorien.</div>';
|
||
}
|
||
}
|
||
|
||
function renderCookieCategoriesTable() {
|
||
const container = document.getElementById('admin-cookie-table-container');
|
||
if (adminCookieCategories.length === 0) {
|
||
container.innerHTML = '<div class="admin-empty">Keine Cookie-Kategorien vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
const html = `
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Anzeigename (DE)</th>
|
||
<th>Typ</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${adminCookieCategories.map(cat => `
|
||
<tr>
|
||
<td><code>${cat.name}</code></td>
|
||
<td>${cat.display_name_de}</td>
|
||
<td>
|
||
${cat.is_mandatory ? '<span class="admin-badge admin-badge-mandatory">Notwendig</span>' : '<span class="admin-badge admin-badge-optional">Optional</span>'}
|
||
</td>
|
||
<td class="admin-actions">
|
||
<button class="admin-btn admin-btn-edit" onclick="editCookieCategory('${cat.id}')">Bearbeiten</button>
|
||
${!cat.is_mandatory ? `<button class="admin-btn admin-btn-delete" onclick="deleteCookieCategory('${cat.id}')">Löschen</button>` : ''}
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
function showCookieForm(cat = null) {
|
||
const form = document.getElementById('admin-cookie-form');
|
||
|
||
if (cat) {
|
||
document.getElementById('admin-cookie-id').value = cat.id;
|
||
document.getElementById('admin-cookie-name').value = cat.name;
|
||
document.getElementById('admin-cookie-display-de').value = cat.display_name_de;
|
||
document.getElementById('admin-cookie-display-en').value = cat.display_name_en || '';
|
||
document.getElementById('admin-cookie-desc-de').value = cat.description_de || '';
|
||
document.getElementById('admin-cookie-mandatory').checked = cat.is_mandatory;
|
||
} else {
|
||
document.getElementById('admin-cookie-id').value = '';
|
||
document.getElementById('admin-cookie-name').value = '';
|
||
document.getElementById('admin-cookie-display-de').value = '';
|
||
document.getElementById('admin-cookie-display-en').value = '';
|
||
document.getElementById('admin-cookie-desc-de').value = '';
|
||
document.getElementById('admin-cookie-mandatory').checked = false;
|
||
}
|
||
|
||
form.classList.add('active');
|
||
}
|
||
|
||
function hideCookieForm() {
|
||
document.getElementById('admin-cookie-form').classList.remove('active');
|
||
}
|
||
|
||
function editCookieCategory(catId) {
|
||
const cat = adminCookieCategories.find(c => c.id === catId);
|
||
if (cat) showCookieForm(cat);
|
||
}
|
||
|
||
async function saveCookieCategory() {
|
||
const catId = document.getElementById('admin-cookie-id').value;
|
||
const data = {
|
||
name: document.getElementById('admin-cookie-name').value,
|
||
display_name_de: document.getElementById('admin-cookie-display-de').value,
|
||
display_name_en: document.getElementById('admin-cookie-display-en').value,
|
||
description_de: document.getElementById('admin-cookie-desc-de').value,
|
||
is_mandatory: document.getElementById('admin-cookie-mandatory').checked
|
||
};
|
||
|
||
try {
|
||
const url = catId ? `/api/consent/admin/cookies/categories/${catId}` : '/api/consent/admin/cookies/categories';
|
||
const method = catId ? 'PUT' : 'POST';
|
||
|
||
const res = await fetch(url, {
|
||
method,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
|
||
if (!res.ok) throw new Error('Failed to save');
|
||
|
||
hideCookieForm();
|
||
await loadAdminCookieCategories();
|
||
alert('Kategorie gespeichert!');
|
||
} catch(e) {
|
||
alert('Fehler beim Speichern: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function deleteCookieCategory(catId) {
|
||
if (!confirm('Kategorie wirklich löschen?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/cookies/categories/${catId}`, { method: 'DELETE' });
|
||
if (!res.ok) throw new Error('Failed to delete');
|
||
|
||
await loadAdminCookieCategories();
|
||
alert('Kategorie gelöscht!');
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// STATISTICS & GDPR EXPORT
|
||
// ==========================================
|
||
let dataCategories = [];
|
||
|
||
async function loadAdminStats() {
|
||
const container = document.getElementById('admin-stats-container');
|
||
container.innerHTML = '<div class="admin-loading">Lade Statistiken & DSGVO-Informationen...</div>';
|
||
|
||
try {
|
||
// Lade Datenkategorien
|
||
const catRes = await fetch('/api/consent/privacy/data-categories');
|
||
if (catRes.ok) {
|
||
const catData = await catRes.json();
|
||
dataCategories = catData.categories || [];
|
||
}
|
||
|
||
renderStatsPanel();
|
||
} catch(e) {
|
||
container.innerHTML = '<div class="admin-empty">Fehler beim Laden: ' + e.message + '</div>';
|
||
}
|
||
}
|
||
|
||
function renderStatsPanel() {
|
||
const container = document.getElementById('admin-stats-container');
|
||
|
||
// Kategorisiere Daten
|
||
const essential = dataCategories.filter(c => c.is_essential);
|
||
const optional = dataCategories.filter(c => !c.is_essential);
|
||
|
||
const html = `
|
||
<div style="display: grid; gap: 24px;">
|
||
<!-- GDPR Export Section -->
|
||
<div class="admin-form" style="padding: 20px;">
|
||
<h3 style="margin: 0 0 16px 0; font-size: 16px; color: var(--bp-text);">
|
||
<span style="margin-right: 8px;">📋</span> DSGVO-Datenauskunft (Art. 15)
|
||
</h3>
|
||
<p style="color: var(--bp-text-muted); font-size: 13px; margin-bottom: 16px;">
|
||
Exportieren Sie alle personenbezogenen Daten eines Nutzers als PDF-Dokument.
|
||
Dies erfüllt die Anforderungen der DSGVO Art. 15 (Auskunftsrecht).
|
||
</p>
|
||
|
||
<div style="display: flex; gap: 12px; align-items: center; flex-wrap: wrap;">
|
||
<input type="text" id="gdpr-export-user-id" class="admin-form-input"
|
||
placeholder="Benutzer-ID (optional für eigene Daten)"
|
||
style="flex: 1; min-width: 200px;">
|
||
<button class="admin-btn admin-btn-primary" onclick="exportUserDataPdf()">
|
||
PDF exportieren
|
||
</button>
|
||
<button class="admin-btn" onclick="previewUserDataHtml()">
|
||
HTML-Vorschau
|
||
</button>
|
||
</div>
|
||
|
||
<div id="gdpr-export-status" style="margin-top: 12px; font-size: 13px;"></div>
|
||
</div>
|
||
|
||
<!-- Data Retention Overview -->
|
||
<div class="admin-form" style="padding: 20px;">
|
||
<h3 style="margin: 0 0 16px 0; font-size: 16px; color: var(--bp-text);">
|
||
<span style="margin-right: 8px;">🗄️</span> Datenkategorien & Löschfristen
|
||
</h3>
|
||
|
||
<div style="margin-bottom: 20px;">
|
||
<h4 style="font-size: 14px; color: var(--bp-primary); margin: 0 0 12px 0;">
|
||
Essentielle Daten (Pflicht für Betrieb)
|
||
</h4>
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Kategorie</th>
|
||
<th>Beschreibung</th>
|
||
<th>Löschfrist</th>
|
||
<th>Rechtsgrundlage</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${essential.map(cat => `
|
||
<tr>
|
||
<td><strong>${cat.name_de}</strong></td>
|
||
<td>${cat.description_de}</td>
|
||
<td><span class="admin-badge admin-badge-published">${cat.retention_period}</span></td>
|
||
<td style="font-size: 11px; color: var(--bp-text-muted);">${cat.legal_basis}</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div>
|
||
<h4 style="font-size: 14px; color: var(--bp-warning); margin: 0 0 12px 0;">
|
||
Optionale Daten (nur bei Einwilligung)
|
||
</h4>
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Kategorie</th>
|
||
<th>Beschreibung</th>
|
||
<th>Cookie-Kategorie</th>
|
||
<th>Löschfrist</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${optional.map(cat => `
|
||
<tr>
|
||
<td><strong>${cat.name_de}</strong></td>
|
||
<td>${cat.description_de}</td>
|
||
<td><span class="admin-badge">${cat.cookie_category || '-'}</span></td>
|
||
<td><span class="admin-badge admin-badge-optional">${cat.retention_period}</span></td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quick Stats -->
|
||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
|
||
<div class="admin-form" style="padding: 16px; text-align: center;">
|
||
<div style="font-size: 28px; font-weight: bold; color: var(--bp-primary);">${dataCategories.length}</div>
|
||
<div style="color: var(--bp-text-muted); font-size: 13px;">Datenkategorien</div>
|
||
</div>
|
||
<div class="admin-form" style="padding: 16px; text-align: center;">
|
||
<div style="font-size: 28px; font-weight: bold; color: var(--bp-success);">${essential.length}</div>
|
||
<div style="color: var(--bp-text-muted); font-size: 13px;">Essentiell</div>
|
||
</div>
|
||
<div class="admin-form" style="padding: 16px; text-align: center;">
|
||
<div style="font-size: 28px; font-weight: bold; color: var(--bp-warning);">${optional.length}</div>
|
||
<div style="color: var(--bp-text-muted); font-size: 13px;">Optional (Opt-in)</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.innerHTML = html;
|
||
}
|
||
|
||
async function exportUserDataPdf() {
|
||
const userIdInput = document.getElementById('gdpr-export-user-id');
|
||
const statusDiv = document.getElementById('gdpr-export-status');
|
||
const userId = userIdInput?.value?.trim();
|
||
|
||
statusDiv.innerHTML = '<span style="color: var(--bp-primary);">Generiere PDF...</span>';
|
||
|
||
try {
|
||
let url = '/api/consent/privacy/export-pdf';
|
||
|
||
// Wenn eine User-ID angegeben wurde, verwende den Admin-Endpoint
|
||
if (userId) {
|
||
url = `/api/consent/admin/privacy/export-pdf/${userId}`;
|
||
}
|
||
|
||
const res = await fetch(url, { method: 'POST' });
|
||
|
||
if (!res.ok) {
|
||
const error = await res.json();
|
||
throw new Error(error.detail?.message || error.detail || 'Export fehlgeschlagen');
|
||
}
|
||
|
||
// PDF herunterladen
|
||
const blob = await res.blob();
|
||
const downloadUrl = window.URL.createObjectURL(blob);
|
||
const a = document.createElement('a');
|
||
a.href = downloadUrl;
|
||
a.download = userId ? `datenauskunft_${userId.slice(0,8)}.pdf` : 'breakpilot_datenauskunft.pdf';
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
window.URL.revokeObjectURL(downloadUrl);
|
||
|
||
statusDiv.innerHTML = '<span style="color: var(--bp-success);">✓ PDF erfolgreich generiert!</span>';
|
||
} catch(e) {
|
||
statusDiv.innerHTML = `<span style="color: var(--bp-danger);">Fehler: ${e.message}</span>`;
|
||
}
|
||
}
|
||
|
||
async function previewUserDataHtml() {
|
||
const statusDiv = document.getElementById('gdpr-export-status');
|
||
statusDiv.innerHTML = '<span style="color: var(--bp-primary);">Lade Vorschau...</span>';
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/privacy/export-html');
|
||
|
||
if (!res.ok) {
|
||
throw new Error('Vorschau konnte nicht geladen werden');
|
||
}
|
||
|
||
const html = await res.text();
|
||
|
||
// In neuem Tab öffnen
|
||
const win = window.open('', '_blank');
|
||
win.document.write(html);
|
||
win.document.close();
|
||
|
||
statusDiv.innerHTML = '<span style="color: var(--bp-success);">✓ Vorschau in neuem Tab geöffnet</span>';
|
||
} catch(e) {
|
||
statusDiv.innerHTML = `<span style="color: var(--bp-danger);">Fehler: ${e.message}</span>`;
|
||
}
|
||
}
|
||
|
||
// ==========================================
|
||
// DSMS FUNCTIONS
|
||
// ==========================================
|
||
const DSMS_GATEWAY_URL = 'http://localhost:8082';
|
||
let dsmsArchives = [];
|
||
|
||
function switchDsmsTab(tabName) {
|
||
document.querySelectorAll('.dsms-subtab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.dsms-content').forEach(c => c.classList.remove('active'));
|
||
|
||
document.querySelector(`.dsms-subtab[data-dsms-tab="${tabName}"]`)?.classList.add('active');
|
||
document.getElementById(`dsms-${tabName}`)?.classList.add('active');
|
||
|
||
// Load data for specific tabs
|
||
if (tabName === 'settings') {
|
||
loadDsmsNodeInfo();
|
||
}
|
||
}
|
||
|
||
async function loadDsmsData() {
|
||
await Promise.all([
|
||
loadDsmsStatus(),
|
||
loadDsmsArchives(),
|
||
loadDsmsDocumentSelect()
|
||
]);
|
||
}
|
||
|
||
async function loadDsmsStatus() {
|
||
const container = document.getElementById('dsms-status-cards');
|
||
container.innerHTML = '<div class="admin-loading">Lade DSMS Status...</div>';
|
||
|
||
try {
|
||
const [healthRes, nodeRes] = await Promise.all([
|
||
fetch(`${DSMS_GATEWAY_URL}/health`).catch(() => null),
|
||
fetch(`${DSMS_GATEWAY_URL}/api/v1/node/info`).catch(() => null)
|
||
]);
|
||
|
||
const health = healthRes?.ok ? await healthRes.json() : null;
|
||
const nodeInfo = nodeRes?.ok ? await nodeRes.json() : null;
|
||
|
||
const isOnline = health?.ipfs_connected === true;
|
||
const repoSize = nodeInfo?.repo_size ? formatBytes(nodeInfo.repo_size) : '-';
|
||
const storageMax = nodeInfo?.storage_max ? formatBytes(nodeInfo.storage_max) : '-';
|
||
const numObjects = nodeInfo?.num_objects ?? '-';
|
||
|
||
container.innerHTML = `
|
||
<div class="dsms-status-card">
|
||
<h4>Status</h4>
|
||
<div class="value ${isOnline ? 'online' : 'offline'}">${isOnline ? 'Online' : 'Offline'}</div>
|
||
</div>
|
||
<div class="dsms-status-card">
|
||
<h4>Speicher verwendet</h4>
|
||
<div class="value">${repoSize}</div>
|
||
</div>
|
||
<div class="dsms-status-card">
|
||
<h4>Max. Speicher</h4>
|
||
<div class="value">${storageMax}</div>
|
||
</div>
|
||
<div class="dsms-status-card">
|
||
<h4>Objekte</h4>
|
||
<div class="value">${numObjects}</div>
|
||
</div>
|
||
`;
|
||
} catch(e) {
|
||
container.innerHTML = `
|
||
<div class="dsms-status-card" style="grid-column: 1/-1;">
|
||
<h4>Status</h4>
|
||
<div class="value offline">Nicht erreichbar</div>
|
||
<p style="font-size: 12px; color: var(--bp-text-muted); margin-top: 8px;">
|
||
DSMS Gateway ist nicht verfügbar. Stellen Sie sicher, dass die Container laufen.
|
||
</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
async function loadDsmsArchives() {
|
||
const container = document.getElementById('dsms-archives-table');
|
||
container.innerHTML = '<div class="admin-loading">Lade archivierte Dokumente...</div>';
|
||
|
||
try {
|
||
const token = localStorage.getItem('bp_token') || '';
|
||
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/documents`, {
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!res.ok) {
|
||
throw new Error('Fehler beim Laden');
|
||
}
|
||
|
||
const data = await res.json();
|
||
dsmsArchives = data.documents || [];
|
||
|
||
if (dsmsArchives.length === 0) {
|
||
container.innerHTML = `
|
||
<div class="admin-empty">
|
||
<p>Keine archivierten Dokumente vorhanden.</p>
|
||
<p style="font-size: 12px; color: var(--bp-text-muted);">
|
||
Klicken Sie auf "+ Dokument archivieren" um ein Legal Document im DSMS zu speichern.
|
||
</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>CID</th>
|
||
<th>Dokument</th>
|
||
<th>Version</th>
|
||
<th>Archiviert am</th>
|
||
<th>Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${dsmsArchives.map(doc => `
|
||
<tr>
|
||
<td>
|
||
<code style="font-size: 11px; background: var(--bp-surface-elevated); padding: 2px 6px; border-radius: 4px;">
|
||
${doc.cid.substring(0, 12)}...
|
||
</code>
|
||
</td>
|
||
<td>${doc.metadata?.document_id || doc.filename || '-'}</td>
|
||
<td>${doc.metadata?.version || '-'}</td>
|
||
<td>${doc.metadata?.created_at ? new Date(doc.metadata.created_at).toLocaleString('de-DE') : '-'}</td>
|
||
<td>
|
||
<button class="btn btn-ghost btn-sm" onclick="verifyDsmsDocumentByCid('${doc.cid}')" title="Verifizieren">
|
||
✓
|
||
</button>
|
||
<button class="btn btn-ghost btn-sm" onclick="copyToClipboard('${doc.cid}')" title="CID kopieren">
|
||
📋
|
||
</button>
|
||
<a href="${DSMS_GATEWAY_URL.replace('8082', '8085')}/ipfs/${doc.cid}" target="_blank" class="btn btn-ghost btn-sm" title="Im Gateway öffnen">
|
||
↗
|
||
</a>
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
} catch(e) {
|
||
container.innerHTML = `<div class="admin-empty">Fehler: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
async function loadDsmsDocumentSelect() {
|
||
const select = document.getElementById('dsms-archive-doc-select');
|
||
if (!select) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/admin/documents');
|
||
if (!res.ok) return;
|
||
|
||
const data = await res.json();
|
||
const docs = data.documents || [];
|
||
|
||
select.innerHTML = '<option value="">-- Dokument wählen --</option>' +
|
||
docs.map(d => `<option value="${d.id}">${d.name} (${d.type})</option>`).join('');
|
||
} catch(e) {
|
||
console.error('Error loading documents:', e);
|
||
}
|
||
}
|
||
|
||
async function loadDsmsVersionSelect() {
|
||
const docSelect = document.getElementById('dsms-archive-doc-select');
|
||
const versionSelect = document.getElementById('dsms-archive-version-select');
|
||
const docId = docSelect?.value;
|
||
|
||
if (!docId) {
|
||
versionSelect.innerHTML = '<option value="">-- Erst Dokument wählen --</option>';
|
||
versionSelect.disabled = true;
|
||
return;
|
||
}
|
||
|
||
versionSelect.disabled = false;
|
||
versionSelect.innerHTML = '<option value="">Lade Versionen...</option>';
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/documents/${docId}/versions`);
|
||
if (!res.ok) throw new Error('Fehler');
|
||
|
||
const data = await res.json();
|
||
const versions = data.versions || [];
|
||
|
||
if (versions.length === 0) {
|
||
versionSelect.innerHTML = '<option value="">Keine Versionen vorhanden</option>';
|
||
return;
|
||
}
|
||
|
||
versionSelect.innerHTML = '<option value="">-- Version wählen --</option>' +
|
||
versions.map(v => `<option value="${v.id}" data-content="${encodeURIComponent(v.content || '')}" data-version="${v.version}">
|
||
${v.version} (${v.language}) - ${v.status}
|
||
</option>`).join('');
|
||
} catch(e) {
|
||
versionSelect.innerHTML = '<option value="">Fehler beim Laden</option>';
|
||
}
|
||
}
|
||
|
||
// Attach event listener for doc select change
|
||
document.getElementById('dsms-archive-doc-select')?.addEventListener('change', loadDsmsVersionSelect);
|
||
|
||
function showArchiveForm() {
|
||
document.getElementById('dsms-archive-form').style.display = 'block';
|
||
loadDsmsDocumentSelect();
|
||
}
|
||
|
||
function hideArchiveForm() {
|
||
document.getElementById('dsms-archive-form').style.display = 'none';
|
||
}
|
||
|
||
async function archiveDocumentToDsms() {
|
||
const docSelect = document.getElementById('dsms-archive-doc-select');
|
||
const versionSelect = document.getElementById('dsms-archive-version-select');
|
||
const selectedOption = versionSelect.options[versionSelect.selectedIndex];
|
||
|
||
if (!docSelect.value || !versionSelect.value) {
|
||
alert('Bitte Dokument und Version auswählen');
|
||
return;
|
||
}
|
||
|
||
const content = decodeURIComponent(selectedOption.dataset.content || '');
|
||
const version = selectedOption.dataset.version;
|
||
const docId = docSelect.value;
|
||
|
||
if (!content) {
|
||
alert('Die ausgewählte Version hat keinen Inhalt');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const token = localStorage.getItem('bp_token') || '';
|
||
const params = new URLSearchParams({
|
||
document_id: docId,
|
||
version: version,
|
||
content: content,
|
||
language: 'de'
|
||
});
|
||
|
||
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/legal-documents/archive?${params}`, {
|
||
method: 'POST',
|
||
headers: { 'Authorization': `Bearer ${token}` }
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail || 'Archivierung fehlgeschlagen');
|
||
}
|
||
|
||
const result = await res.json();
|
||
alert(`Dokument erfolgreich archiviert!\\n\\nCID: ${result.cid}\\nChecksum: ${result.checksum}`);
|
||
hideArchiveForm();
|
||
loadDsmsArchives();
|
||
} catch(e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
async function verifyDsmsDocument() {
|
||
const cidInput = document.getElementById('dsms-verify-cid');
|
||
const resultDiv = document.getElementById('dsms-verify-result');
|
||
const cid = cidInput?.value?.trim();
|
||
|
||
if (!cid) {
|
||
alert('Bitte CID eingeben');
|
||
return;
|
||
}
|
||
|
||
await verifyDsmsDocumentByCid(cid);
|
||
}
|
||
|
||
async function verifyDsmsDocumentByCid(cid) {
|
||
const resultDiv = document.getElementById('dsms-verify-result');
|
||
resultDiv.style.display = 'block';
|
||
resultDiv.innerHTML = '<div class="admin-loading">Verifiziere...</div>';
|
||
|
||
// Switch to verify tab
|
||
switchDsmsTab('verify');
|
||
document.getElementById('dsms-verify-cid').value = cid;
|
||
|
||
try {
|
||
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/verify/${cid}`);
|
||
const data = await res.json();
|
||
|
||
if (data.exists && data.integrity_valid) {
|
||
resultDiv.innerHTML = `
|
||
<div class="dsms-verify-success">
|
||
<h4 style="margin: 0 0 12px 0;">✓ Dokument verifiziert</h4>
|
||
<div style="display: grid; gap: 8px; font-size: 13px;">
|
||
<div><strong>CID:</strong> <code>${cid}</code></div>
|
||
<div><strong>Integrität:</strong> Gültig</div>
|
||
<div><strong>Typ:</strong> ${data.metadata?.document_type || '-'}</div>
|
||
<div><strong>Dokument-ID:</strong> ${data.metadata?.document_id || '-'}</div>
|
||
<div><strong>Version:</strong> ${data.metadata?.version || '-'}</div>
|
||
<div><strong>Erstellt:</strong> ${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '-'}</div>
|
||
<div><strong>Checksum:</strong> <code style="font-size: 10px;">${data.stored_checksum || '-'}</code></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} else if (data.exists && !data.integrity_valid) {
|
||
resultDiv.innerHTML = `
|
||
<div class="dsms-verify-error">
|
||
<h4 style="margin: 0 0 12px 0;">⚠ Integritätsfehler</h4>
|
||
<p>Das Dokument existiert, aber die Prüfsumme stimmt nicht überein.</p>
|
||
<p style="font-size: 12px;">Gespeichert: ${data.stored_checksum}</p>
|
||
<p style="font-size: 12px;">Berechnet: ${data.calculated_checksum}</p>
|
||
</div>
|
||
`;
|
||
} else {
|
||
resultDiv.innerHTML = `
|
||
<div class="dsms-verify-error">
|
||
<h4 style="margin: 0 0 12px 0;">✗ Nicht gefunden</h4>
|
||
<p>Kein Dokument mit diesem CID gefunden.</p>
|
||
${data.error ? `<p style="font-size: 12px;">${data.error}</p>` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
} catch(e) {
|
||
resultDiv.innerHTML = `
|
||
<div class="dsms-verify-error">
|
||
<h4 style="margin: 0 0 12px 0;">Fehler</h4>
|
||
<p>${e.message}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
async function loadDsmsNodeInfo() {
|
||
const container = document.getElementById('dsms-node-info');
|
||
container.innerHTML = '<div class="admin-loading">Lade Node-Info...</div>';
|
||
|
||
try {
|
||
const res = await fetch(`${DSMS_GATEWAY_URL}/api/v1/node/info`);
|
||
if (!res.ok) throw new Error('Nicht erreichbar');
|
||
|
||
const info = await res.json();
|
||
|
||
container.innerHTML = `
|
||
<div style="display: grid; gap: 12px; font-size: 13px;">
|
||
<div><strong>Node ID:</strong> <code style="font-size: 11px;">${info.node_id || '-'}</code></div>
|
||
<div><strong>Agent:</strong> ${info.agent_version || '-'}</div>
|
||
<div><strong>Repo-Größe:</strong> ${info.repo_size ? formatBytes(info.repo_size) : '-'}</div>
|
||
<div><strong>Max. Speicher:</strong> ${info.storage_max ? formatBytes(info.storage_max) : '-'}</div>
|
||
<div><strong>Objekte:</strong> ${info.num_objects ?? '-'}</div>
|
||
<div>
|
||
<strong>Adressen:</strong>
|
||
<ul style="margin: 4px 0 0 0; padding-left: 20px; font-size: 11px; color: var(--bp-text-muted);">
|
||
${(info.addresses || []).map(a => `<li><code>${a}</code></li>`).join('')}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
`;
|
||
} catch(e) {
|
||
container.innerHTML = `<div class="admin-empty">DSMS nicht erreichbar: ${e.message}</div>`;
|
||
}
|
||
}
|
||
|
||
function formatBytes(bytes) {
|
||
if (bytes === 0) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
}
|
||
|
||
function copyToClipboard(text) {
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
// Optional: Show toast
|
||
}).catch(err => {
|
||
console.error('Copy failed:', err);
|
||
});
|
||
}
|
||
|
||
// ==========================================
|
||
// DSMS WEBUI FUNCTIONS
|
||
// ==========================================
|
||
function openDsmsWebUI() {
|
||
document.getElementById('dsms-webui-modal').style.display = 'flex';
|
||
loadDsmsWebUIData();
|
||
}
|
||
|
||
function closeDsmsWebUI() {
|
||
document.getElementById('dsms-webui-modal').style.display = 'none';
|
||
}
|
||
|
||
function switchDsmsWebUISection(section) {
|
||
// Update nav buttons
|
||
document.querySelectorAll('.dsms-webui-nav').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.section === section);
|
||
});
|
||
// Update sections
|
||
document.querySelectorAll('.dsms-webui-section').forEach(sec => {
|
||
sec.classList.remove('active');
|
||
sec.style.display = 'none';
|
||
});
|
||
const activeSection = document.getElementById('dsms-webui-' + section);
|
||
if (activeSection) {
|
||
activeSection.classList.add('active');
|
||
activeSection.style.display = 'block';
|
||
}
|
||
// Load section-specific data
|
||
if (section === 'peers') loadDsmsPeers();
|
||
}
|
||
|
||
async function loadDsmsWebUIData() {
|
||
try {
|
||
// Load node info
|
||
const infoRes = await fetch(DSMS_GATEWAY_URL + '/api/v1/node/info');
|
||
const info = await infoRes.json();
|
||
|
||
document.getElementById('webui-status').innerHTML = '<span style="color: var(--bp-accent);">Online</span>';
|
||
document.getElementById('webui-node-id').textContent = info.node_id || '--';
|
||
document.getElementById('webui-protocol').textContent = info.protocol_version || '--';
|
||
document.getElementById('webui-agent').textContent = info.agent_version || '--';
|
||
document.getElementById('webui-repo-size').textContent = formatBytes(info.repo_size || 0);
|
||
document.getElementById('webui-storage-info').textContent = 'Max: ' + formatBytes(info.storage_max || 0);
|
||
document.getElementById('webui-num-objects').textContent = (info.num_objects || 0).toLocaleString();
|
||
|
||
// Addresses
|
||
const addresses = info.addresses || [];
|
||
document.getElementById('webui-addresses').innerHTML = addresses.length > 0
|
||
? addresses.map(a => '<div style="margin-bottom: 4px;">' + a + '</div>').join('')
|
||
: '<span style="color: var(--bp-text-muted);">Keine Adressen verfügbar</span>';
|
||
|
||
// Load pinned count
|
||
const token = localStorage.getItem('bp_token') || '';
|
||
const docsRes = await fetch(DSMS_GATEWAY_URL + '/api/v1/documents', {
|
||
headers: { 'Authorization': 'Bearer ' + token }
|
||
});
|
||
if (docsRes.ok) {
|
||
const docs = await docsRes.json();
|
||
document.getElementById('webui-pinned-count').textContent = docs.total || 0;
|
||
}
|
||
} catch (e) {
|
||
console.error('Failed to load WebUI data:', e);
|
||
document.getElementById('webui-status').innerHTML = '<span style="color: var(--bp-danger);">Offline</span>';
|
||
}
|
||
}
|
||
|
||
async function loadDsmsPeers() {
|
||
const container = document.getElementById('webui-peers-list');
|
||
try {
|
||
// IPFS peers endpoint via proxy would need direct IPFS API access
|
||
// For now, show info that private network has no peers
|
||
container.innerHTML = `
|
||
<div style="background: var(--bp-surface-elevated); border-radius: 8px; padding: 24px; text-align: center;">
|
||
<div style="font-size: 48px; margin-bottom: 16px;">🔒</div>
|
||
<h4 style="margin: 0 0 8px 0;">Privates Netzwerk</h4>
|
||
<p style="color: var(--bp-text-muted); margin: 0;">
|
||
DSMS läuft als isolierter Node. Keine externen Peers verbunden.
|
||
</p>
|
||
</div>
|
||
`;
|
||
} catch (e) {
|
||
container.innerHTML = '<div class="dsms-verify-error">Fehler beim Laden der Peers</div>';
|
||
}
|
||
}
|
||
|
||
// File upload handlers
|
||
function handleDsmsDragOver(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
document.getElementById('dsms-upload-zone').classList.add('dragover');
|
||
}
|
||
|
||
function handleDsmsDragLeave(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
document.getElementById('dsms-upload-zone').classList.remove('dragover');
|
||
}
|
||
|
||
async function handleDsmsFileDrop(e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
document.getElementById('dsms-upload-zone').classList.remove('dragover');
|
||
const files = e.dataTransfer.files;
|
||
if (files.length > 0) {
|
||
await uploadDsmsFiles(files);
|
||
}
|
||
}
|
||
|
||
async function handleDsmsFileSelect(e) {
|
||
const files = e.target.files;
|
||
if (files.length > 0) {
|
||
await uploadDsmsFiles(files);
|
||
}
|
||
}
|
||
|
||
async function uploadDsmsFiles(files) {
|
||
const token = localStorage.getItem('bp_token') || '';
|
||
const progressDiv = document.getElementById('dsms-upload-progress');
|
||
const statusDiv = document.getElementById('dsms-upload-status');
|
||
const barDiv = document.getElementById('dsms-upload-bar');
|
||
const resultsDiv = document.getElementById('dsms-upload-results');
|
||
|
||
progressDiv.style.display = 'block';
|
||
resultsDiv.innerHTML = '';
|
||
|
||
const results = [];
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
statusDiv.textContent = 'Lade hoch: ' + file.name + ' (' + (i+1) + '/' + files.length + ')';
|
||
barDiv.style.width = ((i / files.length) * 100) + '%';
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('document_type', 'legal_document');
|
||
|
||
const res = await fetch(DSMS_GATEWAY_URL + '/api/v1/documents', {
|
||
method: 'POST',
|
||
headers: { 'Authorization': 'Bearer ' + token },
|
||
body: formData
|
||
});
|
||
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
results.push({ file: file.name, cid: data.cid, success: true });
|
||
} else {
|
||
results.push({ file: file.name, error: 'Upload fehlgeschlagen', success: false });
|
||
}
|
||
} catch (e) {
|
||
results.push({ file: file.name, error: e.message, success: false });
|
||
}
|
||
}
|
||
|
||
barDiv.style.width = '100%';
|
||
statusDiv.textContent = 'Upload abgeschlossen!';
|
||
|
||
// Show results
|
||
resultsDiv.innerHTML = '<h4 style="margin: 0 0 12px 0;">Ergebnisse</h4>' +
|
||
results.map(r => `
|
||
<div class="dsms-webui-file-item">
|
||
<div>
|
||
<div style="font-weight: 500;">${r.file}</div>
|
||
${r.success
|
||
? '<div class="cid" style="color: var(--bp-accent);">CID: ' + r.cid + '</div>'
|
||
: '<div style="color: var(--bp-danger);">' + r.error + '</div>'
|
||
}
|
||
</div>
|
||
${r.success ? '<button class="btn btn-ghost btn-sm" onclick="copyToClipboard(\\''+r.cid+'\\')">Kopieren</button>' : ''}
|
||
</div>
|
||
`).join('');
|
||
|
||
setTimeout(() => {
|
||
progressDiv.style.display = 'none';
|
||
barDiv.style.width = '0%';
|
||
}, 2000);
|
||
}
|
||
|
||
async function exploreDsmsCid() {
|
||
const cid = document.getElementById('webui-explore-cid').value.trim();
|
||
if (!cid) return;
|
||
|
||
const resultDiv = document.getElementById('dsms-explore-result');
|
||
const contentDiv = document.getElementById('dsms-explore-content');
|
||
|
||
resultDiv.style.display = 'block';
|
||
contentDiv.innerHTML = '<div class="admin-loading">Lade...</div>';
|
||
|
||
try {
|
||
const res = await fetch(DSMS_GATEWAY_URL + '/api/v1/verify/' + cid);
|
||
const data = await res.json();
|
||
|
||
if (data.exists) {
|
||
contentDiv.innerHTML = `
|
||
<div style="margin-bottom: 16px;">
|
||
<span style="font-size: 24px; ${data.integrity_valid ? 'color: var(--bp-accent);' : 'color: var(--bp-danger);'}">
|
||
${data.integrity_valid ? '✓' : '✗'}
|
||
</span>
|
||
<span style="font-weight: 600; margin-left: 8px;">
|
||
${data.integrity_valid ? 'Dokument verifiziert' : 'Integritätsfehler'}
|
||
</span>
|
||
</div>
|
||
<table style="width: 100%; font-size: 13px;">
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted); width: 150px;">CID</td>
|
||
<td style="padding: 8px 0; font-family: monospace; word-break: break-all;">${cid}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted);">Typ</td>
|
||
<td style="padding: 8px 0;">${data.metadata?.document_type || '--'}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted);">Erstellt</td>
|
||
<td style="padding: 8px 0;">${data.metadata?.created_at ? new Date(data.metadata.created_at).toLocaleString('de-DE') : '--'}</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px 0; color: var(--bp-text-muted);">Checksum</td>
|
||
<td style="padding: 8px 0; font-family: monospace; font-size: 11px; word-break: break-all;">${data.stored_checksum || '--'}</td>
|
||
</tr>
|
||
</table>
|
||
<div style="margin-top: 16px;">
|
||
<a href="http://localhost:8085/ipfs/${cid}" target="_blank" class="btn btn-ghost btn-sm">Im Gateway öffnen</a>
|
||
</div>
|
||
`;
|
||
} else {
|
||
contentDiv.innerHTML = `
|
||
<div class="dsms-verify-error">
|
||
<strong>Nicht gefunden</strong><br>
|
||
CID existiert nicht im DSMS: ${cid}
|
||
</div>
|
||
`;
|
||
}
|
||
} catch (e) {
|
||
contentDiv.innerHTML = `
|
||
<div class="dsms-verify-error">
|
||
<strong>Fehler</strong><br>
|
||
${e.message}
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
async function runDsmsGarbageCollection() {
|
||
if (!confirm('Garbage Collection ausführen? Dies entfernt nicht gepinnte Objekte.')) return;
|
||
|
||
try {
|
||
// Note: Direct GC requires IPFS API access - show info for now
|
||
alert('Garbage Collection wird im Hintergrund ausgeführt. Dies kann einige Minuten dauern.');
|
||
} catch (e) {
|
||
alert('Fehler: ' + e.message);
|
||
}
|
||
}
|
||
|
||
// Close modal on escape key
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Escape') {
|
||
closeDsmsWebUI();
|
||
}
|
||
});
|
||
|
||
// Close modal on backdrop click
|
||
document.getElementById('dsms-webui-modal')?.addEventListener('click', (e) => {
|
||
if (e.target.id === 'dsms-webui-modal') {
|
||
closeDsmsWebUI();
|
||
}
|
||
});
|
||
|
||
// Load DSMS data when tab is clicked
|
||
document.querySelector('.admin-tab[data-tab="dsms"]')?.addEventListener('click', loadDsmsData);
|
||
|
||
// ==========================================
|
||
// E-MAIL TEMPLATE MANAGEMENT
|
||
// ==========================================
|
||
|
||
let emailTemplates = [];
|
||
let emailTemplateVersions = [];
|
||
let currentEmailTemplateId = null;
|
||
let currentEmailVersionId = null;
|
||
|
||
// E-Mail-Template-Typen mit deutschen Namen
|
||
const emailTypeNames = {
|
||
'welcome': 'Willkommens-E-Mail',
|
||
'email_verification': 'E-Mail-Verifizierung',
|
||
'password_reset': 'Passwort zurücksetzen',
|
||
'password_changed': 'Passwort geändert',
|
||
'2fa_enabled': '2FA aktiviert',
|
||
'2fa_disabled': '2FA deaktiviert',
|
||
'new_device_login': 'Neues Gerät Login',
|
||
'suspicious_activity': 'Verdächtige Aktivität',
|
||
'account_locked': 'Account gesperrt',
|
||
'account_unlocked': 'Account entsperrt',
|
||
'deletion_requested': 'Löschung angefordert',
|
||
'deletion_confirmed': 'Löschung bestätigt',
|
||
'data_export_ready': 'Datenexport bereit',
|
||
'email_changed': 'E-Mail geändert',
|
||
'new_version_published': 'Neue Version veröffentlicht',
|
||
'consent_reminder': 'Consent Erinnerung',
|
||
'consent_deadline_warning': 'Consent Frist Warnung',
|
||
'account_suspended': 'Account suspendiert'
|
||
};
|
||
|
||
// Load E-Mail Templates when tab is clicked
|
||
document.querySelector('.admin-tab[data-tab="emails"]')?.addEventListener('click', loadEmailTemplates);
|
||
|
||
async function loadEmailTemplates() {
|
||
try {
|
||
const res = await fetch('/api/consent/admin/email-templates');
|
||
if (!res.ok) throw new Error('Fehler beim Laden der Templates');
|
||
const data = await res.json();
|
||
emailTemplates = data.templates || [];
|
||
populateEmailTemplateSelect();
|
||
} catch (e) {
|
||
console.error('Error loading email templates:', e);
|
||
showToast('Fehler beim Laden der E-Mail-Templates', 'error');
|
||
}
|
||
}
|
||
|
||
function populateEmailTemplateSelect() {
|
||
const select = document.getElementById('email-template-select');
|
||
select.innerHTML = '<option value="">-- E-Mail-Vorlage auswählen --</option>';
|
||
|
||
emailTemplates.forEach(template => {
|
||
const opt = document.createElement('option');
|
||
opt.value = template.id;
|
||
opt.textContent = emailTypeNames[template.type] || template.name;
|
||
select.appendChild(opt);
|
||
});
|
||
}
|
||
|
||
async function loadEmailTemplateVersions() {
|
||
const select = document.getElementById('email-template-select');
|
||
const templateId = select.value;
|
||
const newVersionBtn = document.getElementById('btn-new-email-version');
|
||
const infoCard = document.getElementById('email-template-info');
|
||
const container = document.getElementById('email-version-table-container');
|
||
|
||
if (!templateId) {
|
||
newVersionBtn.disabled = true;
|
||
infoCard.style.display = 'none';
|
||
container.innerHTML = '<div class="admin-empty">Wählen Sie eine E-Mail-Vorlage aus, um deren Versionen anzuzeigen.</div>';
|
||
currentEmailTemplateId = null;
|
||
return;
|
||
}
|
||
|
||
currentEmailTemplateId = templateId;
|
||
newVersionBtn.disabled = false;
|
||
|
||
// Finde das Template
|
||
const template = emailTemplates.find(t => t.id === templateId);
|
||
if (template) {
|
||
infoCard.style.display = 'block';
|
||
document.getElementById('email-template-name').textContent = emailTypeNames[template.type] || template.name;
|
||
document.getElementById('email-template-description').textContent = template.description || 'Keine Beschreibung';
|
||
document.getElementById('email-template-type-badge').textContent = template.type;
|
||
|
||
// Variablen anzeigen (wird aus dem Default-Inhalt ermittelt)
|
||
try {
|
||
const defaultRes = await fetch(`/api/consent/admin/email-templates/default/${template.type}`);
|
||
if (defaultRes.ok) {
|
||
const defaultData = await defaultRes.json();
|
||
const variables = extractVariables(defaultData.body_html || '');
|
||
document.getElementById('email-template-variables').textContent = variables.join(', ') || 'Keine';
|
||
}
|
||
} catch (e) {
|
||
document.getElementById('email-template-variables').textContent = '-';
|
||
}
|
||
}
|
||
|
||
// Lade Versionen
|
||
container.innerHTML = '<div class="admin-loading">Lade Versionen...</div>';
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-templates/${templateId}/versions`);
|
||
if (!res.ok) throw new Error('Fehler beim Laden');
|
||
const data = await res.json();
|
||
emailTemplateVersions = data.versions || [];
|
||
renderEmailVersionsTable();
|
||
} catch (e) {
|
||
container.innerHTML = '<div class="admin-empty">Fehler beim Laden der Versionen.</div>';
|
||
}
|
||
}
|
||
|
||
function extractVariables(content) {
|
||
const matches = content.match(/\\{\\{([^}]+)\\}\\}/g) || [];
|
||
return [...new Set(matches.map(m => m.replace(/[{}]/g, '')))];
|
||
}
|
||
|
||
function renderEmailVersionsTable() {
|
||
const container = document.getElementById('email-version-table-container');
|
||
|
||
if (emailTemplateVersions.length === 0) {
|
||
container.innerHTML = '<div class="admin-empty">Keine Versionen vorhanden. Erstellen Sie eine neue Version.</div>';
|
||
return;
|
||
}
|
||
|
||
const statusColors = {
|
||
'draft': 'draft',
|
||
'review': 'review',
|
||
'approved': 'approved',
|
||
'published': 'published',
|
||
'archived': 'archived'
|
||
};
|
||
|
||
const statusNames = {
|
||
'draft': 'Entwurf',
|
||
'review': 'In Prüfung',
|
||
'approved': 'Genehmigt',
|
||
'published': 'Veröffentlicht',
|
||
'archived': 'Archiviert'
|
||
};
|
||
|
||
container.innerHTML = `
|
||
<table class="admin-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Version</th>
|
||
<th>Sprache</th>
|
||
<th>Betreff</th>
|
||
<th>Status</th>
|
||
<th>Aktualisiert</th>
|
||
<th style="text-align: right;">Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${emailTemplateVersions.map(v => `
|
||
<tr>
|
||
<td><strong>${v.version}</strong></td>
|
||
<td>${v.language === 'de' ? '🇩🇪 DE' : '🇬🇧 EN'}</td>
|
||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${v.subject}</td>
|
||
<td><span class="admin-badge badge-${statusColors[v.status]}">${statusNames[v.status] || v.status}</span></td>
|
||
<td>${new Date(v.updated_at).toLocaleDateString('de-DE')}</td>
|
||
<td style="text-align: right;">
|
||
<button class="btn btn-ghost btn-xs" onclick="previewEmailVersionById('${v.id}')" title="Vorschau">👁️</button>
|
||
${v.status === 'draft' ? `
|
||
<button class="btn btn-ghost btn-xs" onclick="editEmailVersion('${v.id}')" title="Bearbeiten">✏️</button>
|
||
<button class="btn btn-ghost btn-xs" onclick="submitEmailForReview('${v.id}')" title="Zur Prüfung">📤</button>
|
||
<button class="btn btn-ghost btn-xs" onclick="deleteEmailVersion('${v.id}')" title="Löschen">🗑️</button>
|
||
` : ''}
|
||
${v.status === 'review' ? `
|
||
<button class="btn btn-ghost btn-xs" onclick="showEmailApprovalDialogFor('${v.id}')" title="Genehmigen">✅</button>
|
||
<button class="btn btn-ghost btn-xs" onclick="rejectEmailVersion('${v.id}')" title="Ablehnen">❌</button>
|
||
` : ''}
|
||
${v.status === 'approved' ? `
|
||
<button class="btn btn-primary btn-xs" onclick="publishEmailVersion('${v.id}')" title="Veröffentlichen">🚀</button>
|
||
` : ''}
|
||
</td>
|
||
</tr>
|
||
`).join('')}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
function showEmailVersionForm() {
|
||
document.getElementById('email-version-form').style.display = 'block';
|
||
document.getElementById('email-version-form-title').textContent = 'Neue E-Mail-Version erstellen';
|
||
document.getElementById('email-version-id').value = '';
|
||
document.getElementById('email-version-number').value = '';
|
||
document.getElementById('email-version-subject').value = '';
|
||
document.getElementById('email-version-editor').innerHTML = '';
|
||
document.getElementById('email-version-text').value = '';
|
||
|
||
// Lade Default-Inhalt
|
||
const template = emailTemplates.find(t => t.id === currentEmailTemplateId);
|
||
if (template) {
|
||
loadDefaultEmailContent(template.type);
|
||
}
|
||
}
|
||
|
||
async function loadDefaultEmailContent(templateType) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-templates/default/${templateType}`);
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
document.getElementById('email-version-subject').value = data.subject || '';
|
||
document.getElementById('email-version-editor').innerHTML = data.body_html || '';
|
||
document.getElementById('email-version-text').value = data.body_text || '';
|
||
}
|
||
} catch (e) {
|
||
console.error('Error loading default content:', e);
|
||
}
|
||
}
|
||
|
||
function hideEmailVersionForm() {
|
||
document.getElementById('email-version-form').style.display = 'none';
|
||
}
|
||
|
||
async function saveEmailVersion() {
|
||
const versionId = document.getElementById('email-version-id').value;
|
||
const templateId = currentEmailTemplateId;
|
||
const version = document.getElementById('email-version-number').value.trim();
|
||
const language = document.getElementById('email-version-lang').value;
|
||
const subject = document.getElementById('email-version-subject').value.trim();
|
||
const bodyHtml = document.getElementById('email-version-editor').innerHTML;
|
||
const bodyText = document.getElementById('email-version-text').value.trim();
|
||
|
||
if (!version || !subject || !bodyHtml) {
|
||
showToast('Bitte füllen Sie alle Pflichtfelder aus', 'error');
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
template_id: templateId,
|
||
version: version,
|
||
language: language,
|
||
subject: subject,
|
||
body_html: bodyHtml,
|
||
body_text: bodyText || stripHtml(bodyHtml)
|
||
};
|
||
|
||
try {
|
||
let res;
|
||
if (versionId) {
|
||
// Update existing version
|
||
res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
} else {
|
||
// Create new version
|
||
res = await fetch('/api/consent/admin/email-template-versions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
}
|
||
|
||
if (!res.ok) {
|
||
const error = await res.json();
|
||
throw new Error(error.detail || 'Fehler beim Speichern');
|
||
}
|
||
|
||
showToast('E-Mail-Version gespeichert!', 'success');
|
||
hideEmailVersionForm();
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler: ' + e.message, 'error');
|
||
}
|
||
}
|
||
|
||
function stripHtml(html) {
|
||
const div = document.createElement('div');
|
||
div.innerHTML = html;
|
||
return div.textContent || div.innerText || '';
|
||
}
|
||
|
||
async function editEmailVersion(versionId) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`);
|
||
if (!res.ok) throw new Error('Version nicht gefunden');
|
||
const version = await res.json();
|
||
|
||
document.getElementById('email-version-form').style.display = 'block';
|
||
document.getElementById('email-version-form-title').textContent = 'E-Mail-Version bearbeiten';
|
||
document.getElementById('email-version-id').value = versionId;
|
||
document.getElementById('email-version-number').value = version.version;
|
||
document.getElementById('email-version-lang').value = version.language;
|
||
document.getElementById('email-version-subject').value = version.subject;
|
||
document.getElementById('email-version-editor').innerHTML = version.body_html;
|
||
document.getElementById('email-version-text').value = version.body_text || '';
|
||
} catch (e) {
|
||
showToast('Fehler beim Laden der Version', 'error');
|
||
}
|
||
}
|
||
|
||
async function deleteEmailVersion(versionId) {
|
||
if (!confirm('Möchten Sie diese Version wirklich löschen?')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
if (!res.ok) throw new Error('Fehler beim Löschen');
|
||
showToast('Version gelöscht', 'success');
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler beim Löschen', 'error');
|
||
}
|
||
}
|
||
|
||
async function submitEmailForReview(versionId) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/submit`, {
|
||
method: 'POST'
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Zur Prüfung eingereicht', 'success');
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler beim Einreichen', 'error');
|
||
}
|
||
}
|
||
|
||
function showEmailApprovalDialogFor(versionId) {
|
||
currentEmailVersionId = versionId;
|
||
document.getElementById('email-approval-dialog').style.display = 'flex';
|
||
document.getElementById('email-approval-comment').value = '';
|
||
}
|
||
|
||
function hideEmailApprovalDialog() {
|
||
document.getElementById('email-approval-dialog').style.display = 'none';
|
||
currentEmailVersionId = null;
|
||
}
|
||
|
||
async function submitEmailApproval() {
|
||
if (!currentEmailVersionId) return;
|
||
|
||
const comment = document.getElementById('email-approval-comment').value.trim();
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${currentEmailVersionId}/approve`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ comment: comment })
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Version genehmigt', 'success');
|
||
hideEmailApprovalDialog();
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler bei der Genehmigung', 'error');
|
||
}
|
||
}
|
||
|
||
async function rejectEmailVersion(versionId) {
|
||
const reason = prompt('Ablehnungsgrund:');
|
||
if (!reason) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/reject`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ reason: reason })
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Version abgelehnt', 'success');
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler bei der Ablehnung', 'error');
|
||
}
|
||
}
|
||
|
||
async function publishEmailVersion(versionId) {
|
||
if (!confirm('Möchten Sie diese Version veröffentlichen? Die vorherige Version wird archiviert.')) return;
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/publish`, {
|
||
method: 'POST'
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Version veröffentlicht!', 'success');
|
||
loadEmailTemplateVersions();
|
||
} catch (e) {
|
||
showToast('Fehler beim Veröffentlichen', 'error');
|
||
}
|
||
}
|
||
|
||
async function previewEmailVersionById(versionId) {
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${versionId}/preview`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({})
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
const data = await res.json();
|
||
|
||
document.getElementById('email-preview-subject').textContent = data.subject;
|
||
document.getElementById('email-preview-content').innerHTML = data.body_html;
|
||
document.getElementById('email-preview-dialog').style.display = 'flex';
|
||
currentEmailVersionId = versionId;
|
||
} catch (e) {
|
||
showToast('Fehler bei der Vorschau', 'error');
|
||
}
|
||
}
|
||
|
||
function previewEmailVersion() {
|
||
const subject = document.getElementById('email-version-subject').value;
|
||
const bodyHtml = document.getElementById('email-version-editor').innerHTML;
|
||
|
||
document.getElementById('email-preview-subject').textContent = subject;
|
||
document.getElementById('email-preview-content').innerHTML = bodyHtml;
|
||
document.getElementById('email-preview-dialog').style.display = 'flex';
|
||
}
|
||
|
||
function hideEmailPreview() {
|
||
document.getElementById('email-preview-dialog').style.display = 'none';
|
||
}
|
||
|
||
async function sendTestEmail() {
|
||
const email = document.getElementById('email-test-address').value.trim();
|
||
if (!email) {
|
||
showToast('Bitte geben Sie eine E-Mail-Adresse ein', 'error');
|
||
return;
|
||
}
|
||
|
||
if (!currentEmailVersionId) {
|
||
showToast('Keine Version ausgewählt', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const res = await fetch(`/api/consent/admin/email-template-versions/${currentEmailVersionId}/send-test`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email: email })
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Test-E-Mail gesendet!', 'success');
|
||
} catch (e) {
|
||
showToast('Fehler beim Senden der Test-E-Mail', 'error');
|
||
}
|
||
}
|
||
|
||
async function initializeEmailTemplates() {
|
||
if (!confirm('Möchten Sie alle Standard-E-Mail-Templates initialisieren?')) return;
|
||
|
||
try {
|
||
const res = await fetch('/api/consent/admin/email-templates/initialize', {
|
||
method: 'POST'
|
||
});
|
||
if (!res.ok) throw new Error('Fehler');
|
||
showToast('Templates initialisiert!', 'success');
|
||
loadEmailTemplates();
|
||
} catch (e) {
|
||
showToast('Fehler bei der Initialisierung', 'error');
|
||
}
|
||
}
|
||
|
||
// E-Mail Editor Helpers
|
||
function formatEmailDoc(command) {
|
||
document.execCommand(command, false, null);
|
||
document.getElementById('email-version-editor').focus();
|
||
}
|
||
|
||
function formatEmailBlock(tag) {
|
||
document.execCommand('formatBlock', false, '<' + tag + '>');
|
||
document.getElementById('email-version-editor').focus();
|
||
}
|
||
|
||
function insertEmailVariable() {
|
||
const variable = prompt('Variablenname eingeben (z.B. user_name, reset_link):');
|
||
if (variable) {
|
||
document.execCommand('insertText', false, '{{' + variable + '}}');
|
||
}
|
||
}
|
||
|
||
function insertEmailLink() {
|
||
const url = prompt('Link-URL:');
|
||
if (url) {
|
||
const text = prompt('Link-Text:', url);
|
||
document.execCommand('insertHTML', false, `<a href="${url}" style="color: #5B21B6;">${text}</a>`);
|
||
}
|
||
}
|
||
|
||
function insertEmailButton() {
|
||
const url = prompt('Button-Link:');
|
||
if (url) {
|
||
const text = prompt('Button-Text:', 'Klicken');
|
||
const buttonHtml = `<table cellpadding="0" cellspacing="0" style="margin: 16px 0;"><tr><td style="background: #5B21B6; border-radius: 6px; padding: 12px 24px;"><a href="${url}" style="color: white; text-decoration: none; font-weight: 600;">${text}</a></td></tr></table>`;
|
||
document.execCommand('insertHTML', false, buttonHtml);
|
||
}
|
||
}
|
||
</script>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|