This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/frontend/static/js/customer.js
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
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>
2026-02-09 09:51:32 +01:00

633 lines
19 KiB
JavaScript

/**
* BreakPilot Customer Portal - Slim JavaScript
*
* Features:
* - Login/Register
* - My Consents View
* - Data Export Request
* - Legal Documents
* - Theme Toggle
*/
// API Base URLs
const CONSENT_SERVICE_URL = 'http://localhost:8081';
const BACKEND_URL = '';
// State
let currentUser = null;
let authToken = localStorage.getItem('bp_token');
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', () => {
initTheme();
initEventListeners();
checkAuth();
});
// ============================================================
// Theme
// ============================================================
function initTheme() {
const saved = localStorage.getItem('bp_theme') || 'light';
document.body.setAttribute('data-theme', saved);
updateThemeIcon(saved);
}
function toggleTheme() {
const current = document.body.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.body.setAttribute('data-theme', next);
localStorage.setItem('bp_theme', next);
updateThemeIcon(next);
}
function updateThemeIcon(theme) {
const icon = document.getElementById('theme-icon');
if (icon) icon.textContent = theme === 'dark' ? '☀️' : '🌙';
}
// ============================================================
// Event Listeners
// ============================================================
function initEventListeners() {
// Theme toggle
const themeBtn = document.getElementById('btn-theme');
if (themeBtn) themeBtn.addEventListener('click', toggleTheme);
// Login button
const loginBtn = document.getElementById('btn-login');
if (loginBtn) loginBtn.addEventListener('click', showLoginModal);
// Legal button
const legalBtn = document.getElementById('btn-legal');
if (legalBtn) legalBtn.addEventListener('click', () => showLegalDocument('privacy'));
// User menu toggle
const userMenuBtn = document.getElementById('user-menu-btn');
if (userMenuBtn) {
userMenuBtn.addEventListener('click', () => {
document.getElementById('user-menu').classList.toggle('open');
});
}
// Close user menu on outside click
document.addEventListener('click', (e) => {
const userMenu = document.getElementById('user-menu');
if (userMenu && !userMenu.contains(e.target)) {
userMenu.classList.remove('open');
}
});
// Login form
const loginForm = document.getElementById('login-form');
if (loginForm) loginForm.addEventListener('submit', handleLogin);
// Register form
const registerForm = document.getElementById('register-form');
if (registerForm) registerForm.addEventListener('submit', handleRegister);
// Forgot password form
const forgotForm = document.getElementById('forgot-form');
if (forgotForm) forgotForm.addEventListener('submit', handleForgotPassword);
// Profile form
const profileForm = document.getElementById('profile-form');
if (profileForm) profileForm.addEventListener('submit', handleProfileUpdate);
// Password form
const passwordForm = document.getElementById('password-form');
if (passwordForm) passwordForm.addEventListener('submit', handlePasswordChange);
}
// ============================================================
// Authentication
// ============================================================
async function checkAuth() {
if (!authToken) {
showLoggedOutState();
return;
}
try {
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/me`, {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (res.ok) {
currentUser = await res.json();
showLoggedInState();
} else {
localStorage.removeItem('bp_token');
authToken = null;
showLoggedOutState();
}
} catch (err) {
console.error('Auth check failed:', err);
showLoggedOutState();
}
}
function showLoggedInState() {
document.getElementById('btn-login')?.classList.add('hidden');
document.getElementById('user-menu')?.classList.remove('hidden');
document.getElementById('welcome-section')?.classList.add('hidden');
document.getElementById('dashboard')?.classList.remove('hidden');
if (currentUser) {
const initials = getInitials(currentUser.name || currentUser.email);
document.querySelectorAll('.user-avatar').forEach(el => el.textContent = initials);
document.getElementById('user-name').textContent = currentUser.name || 'Benutzer';
document.getElementById('dropdown-name').textContent = currentUser.name || 'Benutzer';
document.getElementById('dropdown-email').textContent = currentUser.email;
}
loadConsentCount();
}
function showLoggedOutState() {
document.getElementById('btn-login')?.classList.remove('hidden');
document.getElementById('user-menu')?.classList.add('hidden');
document.getElementById('welcome-section')?.classList.remove('hidden');
document.getElementById('dashboard')?.classList.add('hidden');
}
function getInitials(name) {
if (!name) return 'BP';
const parts = name.split(' ').filter(p => p.length > 0);
if (parts.length >= 2) {
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
}
return name.substring(0, 2).toUpperCase();
}
async function handleLogin(e) {
e.preventDefault();
const email = document.getElementById('login-email').value;
const password = document.getElementById('login-password').value;
const messageEl = document.getElementById('login-message');
try {
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await res.json();
if (res.ok) {
authToken = data.token;
localStorage.setItem('bp_token', authToken);
currentUser = data.user;
showMessage(messageEl, 'Erfolgreich angemeldet!', 'success');
setTimeout(() => {
closeAllModals();
showLoggedInState();
}, 500);
} else {
showMessage(messageEl, data.error || 'Anmeldung fehlgeschlagen', 'error');
}
} catch (err) {
showMessage(messageEl, 'Verbindungsfehler', 'error');
}
}
async function handleRegister(e) {
e.preventDefault();
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;
const messageEl = document.getElementById('register-message');
if (password !== passwordConfirm) {
showMessage(messageEl, 'Passwörter stimmen nicht überein', 'error');
return;
}
try {
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email, password })
});
const data = await res.json();
if (res.ok) {
showMessage(messageEl, 'Registrierung erfolgreich! Sie können sich jetzt anmelden.', 'success');
setTimeout(() => switchAuthTab('login'), 1500);
} else {
showMessage(messageEl, data.error || 'Registrierung fehlgeschlagen', 'error');
}
} catch (err) {
showMessage(messageEl, 'Verbindungsfehler', 'error');
}
}
async function handleForgotPassword(e) {
e.preventDefault();
const email = document.getElementById('forgot-email').value;
const messageEl = document.getElementById('forgot-message');
try {
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/request-password-reset`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
showMessage(messageEl, 'Wenn ein Konto mit dieser E-Mail existiert, erhalten Sie einen Link zum Zurücksetzen.', 'success');
} catch (err) {
showMessage(messageEl, 'Verbindungsfehler', 'error');
}
}
function logout() {
localStorage.removeItem('bp_token');
authToken = null;
currentUser = null;
showLoggedOutState();
document.getElementById('user-menu')?.classList.remove('open');
}
// ============================================================
// Modals
// ============================================================
function showLoginModal() {
document.getElementById('login-modal')?.classList.add('active');
switchAuthTab('login');
}
function showMyConsents() {
document.getElementById('consents-modal')?.classList.add('active');
document.getElementById('user-menu')?.classList.remove('open');
loadMyConsents();
}
function showDataExport() {
document.getElementById('export-modal')?.classList.add('active');
document.getElementById('user-menu')?.classList.remove('open');
loadExportRequests();
}
function showAccountSettings() {
document.getElementById('settings-modal')?.classList.add('active');
document.getElementById('user-menu')?.classList.remove('open');
if (currentUser) {
document.getElementById('profile-name').value = currentUser.name || '';
document.getElementById('profile-email').value = currentUser.email || '';
}
}
function showLegalDocument(type) {
const modal = document.getElementById('legal-modal');
const titleEl = document.getElementById('legal-title');
const loadingEl = document.getElementById('legal-loading');
const contentEl = document.getElementById('legal-content');
const titles = {
privacy: 'Datenschutzerklärung',
terms: 'Allgemeine Geschäftsbedingungen',
imprint: 'Impressum',
cookies: 'Cookie-Richtlinie'
};
titleEl.textContent = titles[type] || 'Dokument';
loadingEl.style.display = 'block';
contentEl.innerHTML = '';
modal?.classList.add('active');
loadLegalDocument(type).then(html => {
loadingEl.style.display = 'none';
contentEl.innerHTML = html;
}).catch(() => {
loadingEl.style.display = 'none';
contentEl.innerHTML = '<p>Dokument konnte nicht geladen werden.</p>';
});
}
function showForgotPassword() {
document.getElementById('login-form')?.classList.add('hidden');
document.getElementById('register-form')?.classList.add('hidden');
document.getElementById('forgot-form')?.classList.remove('hidden');
document.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active'));
}
function closeAllModals() {
document.querySelectorAll('.modal.active').forEach(m => m.classList.remove('active'));
}
function switchAuthTab(tab) {
document.querySelectorAll('.auth-tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === tab);
});
document.getElementById('login-form')?.classList.toggle('hidden', tab !== 'login');
document.getElementById('register-form')?.classList.toggle('hidden', tab !== 'register');
document.getElementById('forgot-form')?.classList.add('hidden');
// Clear messages
document.querySelectorAll('.form-message').forEach(m => {
m.className = 'form-message';
m.textContent = '';
});
}
// ============================================================
// Consents
// ============================================================
async function loadConsentCount() {
if (!authToken) return;
try {
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/consents/my`, {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (res.ok) {
const data = await res.json();
const count = data.consents?.length || 0;
const badge = document.getElementById('consent-count');
if (badge) badge.textContent = `${count} aktiv`;
}
} catch (err) {
console.error('Failed to load consent count:', err);
}
}
async function loadMyConsents() {
const loadingEl = document.getElementById('consents-loading');
const listEl = document.getElementById('consents-list');
const emptyEl = document.getElementById('consents-empty');
loadingEl.style.display = 'block';
listEl.innerHTML = '';
emptyEl?.classList.add('hidden');
try {
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/consents/my`, {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (res.ok) {
const data = await res.json();
const consents = data.consents || [];
loadingEl.style.display = 'none';
if (consents.length === 0) {
emptyEl?.classList.remove('hidden');
return;
}
listEl.innerHTML = consents.map(c => `
<div class="consent-item">
<div class="consent-info">
<h3>${escapeHtml(c.document_name || c.document_type)}</h3>
<p>Version ${c.version || '1.0'}</p>
</div>
<div class="consent-date">
Zugestimmt am ${formatDate(c.created_at)}
</div>
</div>
`).join('');
} else {
loadingEl.style.display = 'none';
listEl.innerHTML = '<p>Fehler beim Laden der Zustimmungen.</p>';
}
} catch (err) {
loadingEl.style.display = 'none';
listEl.innerHTML = '<p>Verbindungsfehler.</p>';
}
}
// ============================================================
// Data Export (GDPR)
// ============================================================
async function requestDataExport() {
const messageEl = document.getElementById('export-message');
const btn = document.getElementById('btn-request-export');
btn.disabled = true;
try {
const res = await fetch(`${BACKEND_URL}/api/gdpr/request-export`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
if (res.ok) {
showMessage(messageEl, 'Ihre Anfrage wurde erfolgreich eingereicht. Sie erhalten eine E-Mail, sobald der Export bereit ist.', 'success');
loadExportRequests();
} else {
const data = await res.json();
showMessage(messageEl, data.error || 'Anfrage fehlgeschlagen', 'error');
}
} catch (err) {
showMessage(messageEl, 'Verbindungsfehler', 'error');
} finally {
btn.disabled = false;
}
}
async function loadExportRequests() {
const statusEl = document.getElementById('export-status');
const listEl = document.getElementById('export-requests-list');
if (!statusEl || !listEl) return;
try {
const res = await fetch(`${BACKEND_URL}/api/gdpr/my-requests`, {
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (res.ok) {
const data = await res.json();
const requests = data.requests || [];
if (requests.length > 0) {
statusEl.classList.remove('hidden');
listEl.innerHTML = requests.map(r => `
<div class="consent-item">
<div class="consent-info">
<h3>${r.type === 'export' ? 'Datenexport' : 'Datenlöschung'}</h3>
<p>Status: ${r.status}</p>
</div>
<div class="consent-date">
${formatDate(r.created_at)}
</div>
</div>
`).join('');
} else {
statusEl.classList.add('hidden');
}
}
} catch (err) {
console.error('Failed to load export requests:', err);
}
}
// ============================================================
// Legal Documents
// ============================================================
async function loadLegalDocument(type) {
try {
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/documents/type/${type}/published`);
if (res.ok) {
const data = await res.json();
return data.content || '<p>Dokument ist leer.</p>';
}
return '<p>Dokument nicht gefunden.</p>';
} catch (err) {
return '<p>Fehler beim Laden.</p>';
}
}
// ============================================================
// Profile & Settings
// ============================================================
async function handleProfileUpdate(e) {
e.preventDefault();
const name = document.getElementById('profile-name').value;
const messageEl = document.getElementById('profile-message');
try {
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/update-profile`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ name })
});
if (res.ok) {
currentUser.name = name;
showMessage(messageEl, 'Profil aktualisiert', 'success');
showLoggedInState();
} else {
showMessage(messageEl, 'Aktualisierung fehlgeschlagen', 'error');
}
} catch (err) {
showMessage(messageEl, 'Verbindungsfehler', 'error');
}
}
async function handlePasswordChange(e) {
e.preventDefault();
const currentPassword = document.getElementById('current-password').value;
const newPassword = document.getElementById('new-password').value;
const confirmPassword = document.getElementById('new-password-confirm').value;
const messageEl = document.getElementById('password-message');
if (newPassword !== confirmPassword) {
showMessage(messageEl, 'Passwörter stimmen nicht überein', 'error');
return;
}
try {
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/change-password`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword
})
});
if (res.ok) {
showMessage(messageEl, 'Passwort geändert', 'success');
document.getElementById('password-form').reset();
} else {
const data = await res.json();
showMessage(messageEl, data.error || 'Änderung fehlgeschlagen', 'error');
}
} catch (err) {
showMessage(messageEl, 'Verbindungsfehler', 'error');
}
}
function confirmDeleteAccount() {
if (confirm('Sind Sie sicher, dass Sie Ihr Konto löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.')) {
if (confirm('Letzte Warnung: Alle Ihre Daten werden unwiderruflich gelöscht. Fortfahren?')) {
deleteAccount();
}
}
}
async function deleteAccount() {
try {
const res = await fetch(`${CONSENT_SERVICE_URL}/api/v1/auth/delete-account`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (res.ok) {
alert('Ihr Konto wurde gelöscht.');
logout();
closeAllModals();
} else {
alert('Kontoloöschung fehlgeschlagen.');
}
} catch (err) {
alert('Verbindungsfehler');
}
}
// ============================================================
// Utilities
// ============================================================
function showMessage(el, text, type) {
if (!el) return;
el.textContent = text;
el.className = `form-message ${type}`;
}
function escapeHtml(str) {
if (!str) return '';
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
}
// Export for global access
window.showLoginModal = showLoginModal;
window.showMyConsents = showMyConsents;
window.showDataExport = showDataExport;
window.showAccountSettings = showAccountSettings;
window.showLegalDocument = showLegalDocument;
window.showForgotPassword = showForgotPassword;
window.closeAllModals = closeAllModals;
window.switchAuthTab = switchAuthTab;
window.logout = logout;
window.requestDataExport = requestDataExport;
window.confirmDeleteAccount = confirmDeleteAccount;