🎯 Fix: Vollständige Behebung der JavaScript exportStats-Funktion und Admin-System-Optimierungen
✅ **Stats Export API implementiert**: - Neuer /api/stats/export Endpunkt für CSV-Download - Umfassende Systemstatistiken mit Drucker-Details - Zeitbasierte Metriken und Erfolgsraten-Berechnung - Sichere Authentifizierung und Fehlerbehandlung ✅ **API-Datenkompatibilität verbessert**: - Frontend-Aliases hinzugefügt: online_printers, active_jobs, success_rate - Einheitliche Datenstruktur für Stats-Anzeige - Korrekte Erfolgsraten-Berechnung mit Null-Division-Schutz ✅ **Admin-System erweitert**: - Erweiterte CRUD-Funktionalität für Benutzerverwaltung - Verbesserte Template-Integration und Formular-Validierung - Optimierte Datenbankabfragen und Session-Management 🔧 **Technische Details**: - CSV-Export mit strukturierten Headers und Zeitstempel - Defensive Programmierung mit umfassender Fehlerbehandlung - Performance-optimierte Datenbankabfragen - Vollständige API-Kompatibilität zu bestehender Frontend-Logik Das MYP-System ist jetzt vollständig funktionsfähig mit korrekter Statistik-Export-Funktionalität. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1697,11 +1697,324 @@ class SystemControlManager {
|
||||
// Globaler System-Control-Manager
|
||||
let systemControlManager = null;
|
||||
|
||||
// ===== BENUTZER-CRUD-FUNKTIONALITÄT =====
|
||||
|
||||
class UserManagement {
|
||||
constructor() {
|
||||
this.initializeEventListeners();
|
||||
console.log('👥 User-Management initialisiert');
|
||||
}
|
||||
|
||||
initializeEventListeners() {
|
||||
// Benutzer hinzufügen Button
|
||||
const addUserBtn = document.getElementById('add-user-btn');
|
||||
if (addUserBtn) {
|
||||
addUserBtn.addEventListener('click', () => {
|
||||
window.location.href = '/admin/users/add';
|
||||
});
|
||||
}
|
||||
|
||||
// Benutzer bearbeiten Buttons
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.edit-user-btn')) {
|
||||
const userId = e.target.closest('.edit-user-btn').dataset.userId;
|
||||
this.editUser(userId);
|
||||
}
|
||||
|
||||
// Benutzer löschen Buttons
|
||||
if (e.target.closest('.delete-user-btn')) {
|
||||
const userId = e.target.closest('.delete-user-btn').dataset.userId;
|
||||
const userName = e.target.closest('.delete-user-btn').dataset.userName;
|
||||
this.deleteUser(userId, userName);
|
||||
}
|
||||
|
||||
// Passwort zurücksetzen Buttons
|
||||
if (e.target.closest('.reset-password-btn')) {
|
||||
const userId = e.target.closest('.reset-password-btn').dataset.userId;
|
||||
const userName = e.target.closest('.reset-password-btn').dataset.userName;
|
||||
this.resetPassword(userId, userName);
|
||||
}
|
||||
});
|
||||
|
||||
// Rolle/Status Updates durch Inline-Editing
|
||||
document.addEventListener('change', (e) => {
|
||||
if (e.target.classList.contains('role-select')) {
|
||||
const userId = e.target.dataset.userId;
|
||||
const newRole = e.target.value;
|
||||
this.updateUserRole(userId, newRole);
|
||||
}
|
||||
|
||||
if (e.target.classList.contains('status-toggle')) {
|
||||
const userId = e.target.dataset.userId;
|
||||
const isActive = e.target.checked;
|
||||
this.updateUserStatus(userId, isActive);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
editUser(userId) {
|
||||
window.location.href = `/admin/users/${userId}/edit`;
|
||||
}
|
||||
|
||||
async deleteUser(userId, userName) {
|
||||
const confirmed = await this.showConfirmDialog(
|
||||
'Benutzer löschen',
|
||||
`Möchten Sie den Benutzer "${userName}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`,
|
||||
'Löschen',
|
||||
'danger'
|
||||
);
|
||||
|
||||
if (!confirmed) return;
|
||||
|
||||
try {
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = `/admin/users/${userId}/delete`;
|
||||
|
||||
const csrfToken = document.createElement('input');
|
||||
csrfToken.type = 'hidden';
|
||||
csrfToken.name = 'csrf_token';
|
||||
csrfToken.value = getCsrfToken();
|
||||
form.appendChild(csrfToken);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Benutzers:', error);
|
||||
showNotification('Fehler beim Löschen des Benutzers', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async resetPassword(userId, userName) {
|
||||
const newPassword = await this.showPasswordDialog(
|
||||
'Passwort zurücksetzen',
|
||||
`Neues Passwort für Benutzer "${userName}"`
|
||||
);
|
||||
|
||||
if (!newPassword) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/users/${userId}/reset-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
new_password: newPassword
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification(`Passwort für "${userName}" erfolgreich zurückgesetzt`, 'success');
|
||||
} else {
|
||||
showNotification(data.error || 'Fehler beim Zurücksetzen des Passworts', 'error');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Zurücksetzen des Passworts:', error);
|
||||
showNotification('Fehler beim Zurücksetzen des Passworts', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async updateUserRole(userId, newRole) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/users/${userId}/role`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
role: newRole
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification('Benutzerrolle erfolgreich aktualisiert', 'success');
|
||||
// Seite nach kurzer Verzögerung neu laden für UI-Update
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} else {
|
||||
showNotification(data.error || 'Fehler beim Aktualisieren der Rolle', 'error');
|
||||
// Revert auf alte Auswahl
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren der Rolle:', error);
|
||||
showNotification('Fehler beim Aktualisieren der Rolle', 'error');
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async updateUserStatus(userId, isActive) {
|
||||
try {
|
||||
const response = await fetch(`/api/admin/users/${userId}/status`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': getCsrfToken()
|
||||
},
|
||||
body: JSON.stringify({
|
||||
active: isActive
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showNotification(
|
||||
`Benutzer ${isActive ? 'aktiviert' : 'deaktiviert'}`,
|
||||
'success'
|
||||
);
|
||||
// UI-Update für Status-Text
|
||||
const statusText = document.querySelector(`[data-user-id="${userId}"] .status-toggle`).nextElementSibling;
|
||||
if (statusText) {
|
||||
statusText.textContent = isActive ? 'Aktiv' : 'Inaktiv';
|
||||
statusText.className = isActive ?
|
||||
'ml-2 text-xs text-green-700 dark:text-green-400' :
|
||||
'ml-2 text-xs text-red-700 dark:text-red-400';
|
||||
}
|
||||
} else {
|
||||
showNotification(data.error || 'Fehler beim Aktualisieren des Status', 'error');
|
||||
// Revert Checkbox
|
||||
document.querySelector(`[data-user-id="${userId}"] .status-toggle`).checked = !isActive;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Status:', error);
|
||||
showNotification('Fehler beim Aktualisieren des Status', 'error');
|
||||
// Revert Checkbox
|
||||
document.querySelector(`[data-user-id="${userId}"] .status-toggle`).checked = !isActive;
|
||||
}
|
||||
}
|
||||
|
||||
showConfirmDialog(title, message, confirmText = 'Bestätigen', type = 'warning') {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 max-w-md mx-4 shadow-2xl">
|
||||
<div class="flex items-center mb-4">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
type === 'danger' ? 'bg-red-100 dark:bg-red-900' : 'bg-yellow-100 dark:bg-yellow-900'
|
||||
}">
|
||||
<svg class="w-6 h-6 ${type === 'danger' ? 'text-red-600' : 'text-yellow-600'}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.268 15.5c-.77.833.192 2.5 1.732 2.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<h3 class="text-lg font-medium text-slate-900 dark:text-white">${title}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-6">${message}</p>
|
||||
<div class="flex space-x-3">
|
||||
<button id="confirm-btn" class="flex-1 px-4 py-2 ${
|
||||
type === 'danger' ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700'
|
||||
} text-white rounded-lg transition-colors">${confirmText}</button>
|
||||
<button id="cancel-btn" class="flex-1 px-4 py-2 bg-slate-300 dark:bg-slate-600 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-400 dark:hover:bg-slate-500 transition-colors">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
modal.querySelector('#confirm-btn').addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
modal.querySelector('#cancel-btn').addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
// ESC zum Abbrechen
|
||||
const handleEsc = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.body.removeChild(modal);
|
||||
document.removeEventListener('keydown', handleEsc);
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEsc);
|
||||
});
|
||||
}
|
||||
|
||||
showPasswordDialog(title, description) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="bg-white dark:bg-slate-800 rounded-xl p-6 max-w-md mx-4 shadow-2xl">
|
||||
<h3 class="text-lg font-medium text-slate-900 dark:text-white mb-2">${title}</h3>
|
||||
<p class="text-slate-600 dark:text-slate-400 mb-4">${description}</p>
|
||||
<input type="password" id="password-input" placeholder="Neues Passwort"
|
||||
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-slate-700 dark:text-white mb-4">
|
||||
<div class="flex space-x-3">
|
||||
<button id="set-password-btn" class="flex-1 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">Setzen</button>
|
||||
<button id="cancel-password-btn" class="flex-1 px-4 py-2 bg-slate-300 dark:bg-slate-600 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-400 dark:hover:bg-slate-500 transition-colors">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const passwordInput = modal.querySelector('#password-input');
|
||||
passwordInput.focus();
|
||||
|
||||
modal.querySelector('#set-password-btn').addEventListener('click', () => {
|
||||
const password = passwordInput.value.trim();
|
||||
document.body.removeChild(modal);
|
||||
resolve(password || null);
|
||||
});
|
||||
|
||||
modal.querySelector('#cancel-password-btn').addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
resolve(null);
|
||||
});
|
||||
|
||||
// Enter zum Bestätigen
|
||||
passwordInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
const password = passwordInput.value.trim();
|
||||
document.body.removeChild(modal);
|
||||
resolve(password || null);
|
||||
}
|
||||
});
|
||||
|
||||
// ESC zum Abbrechen
|
||||
const handleEsc = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.body.removeChild(modal);
|
||||
document.removeEventListener('keydown', handleEsc);
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleEsc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Globaler User-Manager
|
||||
let userManagement = null;
|
||||
|
||||
// Initialisierung beim DOM-Laden
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!systemControlManager) {
|
||||
systemControlManager = new SystemControlManager();
|
||||
}
|
||||
|
||||
if (!userManagement) {
|
||||
userManagement = new UserManagement();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@ -184,7 +184,7 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% block scripts %}
|
||||
<!-- JavaScript für Form-Validierung und AJAX-Submit -->
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
@ -165,7 +165,7 @@
|
||||
|
||||
<!-- Hauptformular -->
|
||||
<div class="admin-form-container rounded-2xl p-8 transition-all duration-300">
|
||||
<form id="userForm" action="{{ url_for('admin.add_user_page') }}" method="POST" class="space-y-8">
|
||||
<form id="userForm" action="{{ url_for('admin.create_user') }}" method="POST" class="space-y-8">
|
||||
<!-- CSRF Token -->
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
|
||||
@ -305,6 +305,144 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Erweiterte Profil-Informationen (Optional) -->
|
||||
<div class="form-field-premium mt-8">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6 flex items-center">
|
||||
<svg class="w-5 h-5 mr-3 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
Erweiterte Profil-Informationen <span class="text-sm font-normal text-slate-500">(Optional)</span>
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Abteilung -->
|
||||
<div class="form-field-premium">
|
||||
<label for="department" class="block text-sm font-medium text-slate-900 dark:text-white mb-3">
|
||||
<svg class="w-4 h-4 inline mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-4m-5 0H9m0 0H5m4 0V9a2 2 0 012-2h2a2 2 0 012 2v12"/>
|
||||
</svg>
|
||||
Abteilung
|
||||
</label>
|
||||
<input type="text"
|
||||
id="department"
|
||||
name="department"
|
||||
class="input-premium w-full px-4 py-3 rounded-xl focus:outline-none transition-all duration-300 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400"
|
||||
placeholder="z.B. TBA Marienfelde, Produktion, IT">
|
||||
</div>
|
||||
|
||||
<!-- Position -->
|
||||
<div class="form-field-premium">
|
||||
<label for="position" class="block text-sm font-medium text-slate-900 dark:text-white mb-3">
|
||||
<svg class="w-4 h-4 inline mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m8 0H8m8 0v6a2 2 0 01-2 2H10a2 2 0 01-2-2V6m8 0V4a2 2 0 00-2-2H10a2 2 0 00-2 2v2"/>
|
||||
</svg>
|
||||
Position/Rolle
|
||||
</label>
|
||||
<input type="text"
|
||||
id="position"
|
||||
name="position"
|
||||
class="input-premium w-full px-4 py-3 rounded-xl focus:outline-none transition-all duration-300 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400"
|
||||
placeholder="z.B. Ausbilder, Auszubildender, Techniker">
|
||||
</div>
|
||||
|
||||
<!-- Telefonnummer -->
|
||||
<div class="form-field-premium">
|
||||
<label for="phone" class="block text-sm font-medium text-slate-900 dark:text-white mb-3">
|
||||
<svg class="w-4 h-4 inline mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"/>
|
||||
</svg>
|
||||
Telefonnummer
|
||||
</label>
|
||||
<input type="tel"
|
||||
id="phone"
|
||||
name="phone"
|
||||
class="input-premium w-full px-4 py-3 rounded-xl focus:outline-none transition-all duration-300 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400"
|
||||
placeholder="+49 30 7566-8000">
|
||||
</div>
|
||||
|
||||
<!-- Bio/Beschreibung -->
|
||||
<div class="form-field-premium">
|
||||
<label for="bio" class="block text-sm font-medium text-slate-900 dark:text-white mb-3">
|
||||
<svg class="w-4 h-4 inline mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Kurze Beschreibung
|
||||
</label>
|
||||
<textarea id="bio"
|
||||
name="bio"
|
||||
rows="3"
|
||||
class="input-premium w-full px-4 py-3 rounded-xl focus:outline-none transition-all duration-300 text-slate-900 dark:text-white placeholder-slate-500 dark:placeholder-slate-400 resize-none"
|
||||
placeholder="Kurze Beschreibung des Benutzers, Verantwortlichkeiten, etc."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Benutzereinstellungen (Optional) -->
|
||||
<div class="form-field-premium mt-8">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-6 flex items-center">
|
||||
<svg class="w-5 h-5 mr-3 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
Benutzereinstellungen <span class="text-sm font-normal text-slate-500">(Optional)</span>
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Theme Preference -->
|
||||
<div class="form-field-premium">
|
||||
<label for="theme_preference" class="block text-sm font-medium text-slate-900 dark:text-white mb-3">
|
||||
Theme-Präferenz
|
||||
</label>
|
||||
<select id="theme_preference"
|
||||
name="theme_preference"
|
||||
class="input-premium w-full px-4 py-3 rounded-xl focus:outline-none transition-all duration-300 text-slate-900 dark:text-white">
|
||||
<option value="auto">🔄 Automatisch (System)</option>
|
||||
<option value="light">☀️ Hell</option>
|
||||
<option value="dark">🌙 Dunkel</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Language Preference -->
|
||||
<div class="form-field-premium">
|
||||
<label for="language_preference" class="block text-sm font-medium text-slate-900 dark:text-white mb-3">
|
||||
Sprache
|
||||
</label>
|
||||
<select id="language_preference"
|
||||
name="language_preference"
|
||||
class="input-premium w-full px-4 py-3 rounded-xl focus:outline-none transition-all duration-300 text-slate-900 dark:text-white">
|
||||
<option value="de">🇩🇪 Deutsch</option>
|
||||
<option value="en">🇺🇸 English</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Email Notifications -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="checkbox"
|
||||
id="email_notifications"
|
||||
name="email_notifications"
|
||||
checked
|
||||
class="w-5 h-5 text-blue-600 bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-blue-500 dark:focus:ring-blue-600 focus:ring-2">
|
||||
<label for="email_notifications" class="flex-1 cursor-pointer">
|
||||
<div class="font-medium text-slate-900 dark:text-white">E-Mail-Benachrichtigungen</div>
|
||||
<div class="text-sm text-slate-600 dark:text-slate-400">Benutzer erhält E-Mail-Benachrichtigungen</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Browser Notifications -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="checkbox"
|
||||
id="browser_notifications"
|
||||
name="browser_notifications"
|
||||
checked
|
||||
class="w-5 h-5 text-blue-600 bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-blue-500 dark:focus:ring-blue-600 focus:ring-2">
|
||||
<label for="browser_notifications" class="flex-1 cursor-pointer">
|
||||
<div class="font-medium text-slate-900 dark:text-white">Browser-Benachrichtigungen</div>
|
||||
<div class="text-sm text-slate-600 dark:text-slate-400">Push-Benachrichtigungen im Browser</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Granulare Berechtigungen -->
|
||||
<div class="form-field-premium mt-8" id="permissionsSection">
|
||||
<label class="block text-sm font-semibold text-slate-900 dark:text-white mb-3 transition-colors duration-300">
|
||||
@ -350,6 +488,66 @@
|
||||
<div class="text-sm text-slate-600 dark:text-slate-400">Benutzer kann Gastanfragen bearbeiten und genehmigen/ablehnen</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Can Manage Printers -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="checkbox"
|
||||
id="can_manage_printers"
|
||||
name="can_manage_printers"
|
||||
class="w-5 h-5 text-blue-600 bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-blue-500 dark:focus:ring-blue-600 focus:ring-2">
|
||||
<label for="can_manage_printers" class="flex-1 cursor-pointer">
|
||||
<div class="font-medium text-slate-900 dark:text-white">Kann Drucker verwalten</div>
|
||||
<div class="text-sm text-slate-600 dark:text-slate-400">Drucker hinzufügen, bearbeiten, Smart Plugs steuern</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Can View All Jobs -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="checkbox"
|
||||
id="can_view_all_jobs"
|
||||
name="can_view_all_jobs"
|
||||
class="w-5 h-5 text-blue-600 bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-blue-500 dark:focus:ring-blue-600 focus:ring-2">
|
||||
<label for="can_view_all_jobs" class="flex-1 cursor-pointer">
|
||||
<div class="font-medium text-slate-900 dark:text-white">Kann alle Aufträge einsehen</div>
|
||||
<div class="text-sm text-slate-600 dark:text-slate-400">Zugriff auf alle Druckaufträge, nicht nur eigene</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Can Access Admin Panel -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="checkbox"
|
||||
id="can_access_admin_panel"
|
||||
name="can_access_admin_panel"
|
||||
class="w-5 h-5 text-blue-600 bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-blue-500 dark:focus:ring-blue-600 focus:ring-2">
|
||||
<label for="can_access_admin_panel" class="flex-1 cursor-pointer">
|
||||
<div class="font-medium text-slate-900 dark:text-white">Kann Admin Panel einsehen</div>
|
||||
<div class="text-sm text-slate-600 dark:text-slate-400">Zugriff auf System-Statistiken und Monitoring</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Can Manage Users -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="checkbox"
|
||||
id="can_manage_users"
|
||||
name="can_manage_users"
|
||||
class="w-5 h-5 text-blue-600 bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-blue-500 dark:focus:ring-blue-600 focus:ring-2">
|
||||
<label for="can_manage_users" class="flex-1 cursor-pointer">
|
||||
<div class="font-medium text-slate-900 dark:text-white">Kann Benutzer verwalten</div>
|
||||
<div class="text-sm text-slate-600 dark:text-slate-400">Benutzer erstellen, bearbeiten und löschen</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Can Access Energy Monitoring -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="checkbox"
|
||||
id="can_access_energy_monitoring"
|
||||
name="can_access_energy_monitoring"
|
||||
class="w-5 h-5 text-blue-600 bg-white dark:bg-slate-700 border-slate-300 dark:border-slate-600 rounded focus:ring-blue-500 dark:focus:ring-blue-600 focus:ring-2">
|
||||
<label for="can_access_energy_monitoring" class="flex-1 cursor-pointer">
|
||||
<div class="font-medium text-slate-900 dark:text-white">Kann Energiemonitoring einsehen</div>
|
||||
<div class="text-sm text-slate-600 dark:text-slate-400">Zugriff auf Stromverbrauch und Smart Plug Daten</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -231,7 +231,7 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% block scripts %}
|
||||
<!-- JavaScript für Form-Handling und AJAX -->
|
||||
<script>
|
||||
let printerData = null;
|
||||
|
@ -234,7 +234,7 @@ input:checked + .toggle-slider:before {
|
||||
|
||||
<!-- Modern Form with Glass Effect -->
|
||||
<div class="glass-card rounded-3xl p-10 shadow-2xl">
|
||||
<form method="POST" action="{{ url_for('admin_api.update_user_api', user_id=user.id) }}" class="space-y-8" id="userEditForm">
|
||||
<form method="POST" action="{{ url_for('admin.update_user', user_id=user.id) }}" class="space-y-8" id="userEditForm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||
<input type="hidden" name="_method" value="PUT"/>
|
||||
|
||||
|
@ -875,7 +875,7 @@ function createRequestRow(request) {
|
||||
<i class="fas fa-clock mr-1"></i>${request.duration_minutes || 0} Min,
|
||||
<i class="fas fa-copy ml-2 mr-1"></i>${request.copies || 1} Kopien
|
||||
</div>
|
||||
${request.reason ? `<div class="text-xs text-gray-400 mt-1">${escapeHtml(request.reason.substring(0, 100))}${request.reason.length > 100 ? '...' : ''}</div>` : ''}
|
||||
${request.reason ? `<div class="text-xs text-gray-400 mt-1 whitespace-pre-wrap break-words max-w-md">${escapeHtml(request.reason)}</div>` : ''}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="status-badge status-${request.status}">
|
||||
|
@ -444,7 +444,7 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% block scripts %}
|
||||
<!-- Auto-Refresh über Jinja-Macro -->
|
||||
{% if config.get('AUTO_REFRESH_DASHBOARD', False) %}
|
||||
{{ auto_refresh(60) }}
|
||||
|
@ -204,7 +204,7 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let currentRequestData = null;
|
||||
|
||||
|
@ -436,7 +436,7 @@
|
||||
const autoLogout = document.getElementById('auto-logout').value;
|
||||
|
||||
// Validate settings
|
||||
if (!validateSettings({ theme, contrast, autoLogout })) {
|
||||
if (!validateSettings({ theme, contrast, auto_logout: autoLogout })) {
|
||||
throw new Error('Ungültige Einstellungen erkannt');
|
||||
}
|
||||
|
||||
@ -515,7 +515,7 @@
|
||||
|
||||
return validThemes.includes(settings.theme) &&
|
||||
validContrast.includes(settings.contrast) &&
|
||||
validLogoutValues.includes(settings.autoLogout);
|
||||
validLogoutValues.includes(settings.auto_logout || settings.autoLogout);
|
||||
}
|
||||
|
||||
// Enhanced settings loading with caching
|
||||
@ -770,8 +770,32 @@
|
||||
|
||||
// Scroll to section with offset for header
|
||||
const targetId = this.getAttribute('href').substring(1);
|
||||
const targetElement = document.querySelector(`[id="${targetId}"]`) ||
|
||||
document.querySelector(`h2:contains("${targetId}")`);
|
||||
let targetElement = document.querySelector(`[id="${targetId}"]`);
|
||||
|
||||
// Fallback: Search for h2 elements containing the target text
|
||||
if (!targetElement) {
|
||||
const h2Elements = document.querySelectorAll('h2');
|
||||
for (const h2 of h2Elements) {
|
||||
if (h2.textContent.toLowerCase().includes(targetId.toLowerCase())) {
|
||||
targetElement = h2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback: scroll to the containing card
|
||||
if (!targetElement) {
|
||||
const cardContainers = document.querySelectorAll('.glass-card');
|
||||
const targetMap = {
|
||||
'appearance': 0,
|
||||
'notifications': 1,
|
||||
'privacy': 2
|
||||
};
|
||||
const cardIndex = targetMap[targetId];
|
||||
if (cardIndex !== undefined && cardContainers[cardIndex]) {
|
||||
targetElement = cardContainers[cardIndex];
|
||||
}
|
||||
}
|
||||
|
||||
if (targetElement) {
|
||||
const offsetTop = targetElement.offsetTop - 100; // Account for header
|
||||
|
@ -148,7 +148,7 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% block scripts %}
|
||||
<!-- Chart.js - Lokale Version -->
|
||||
<script src="{{ url_for('static', filename='js/charts/chart.min.js') }}"></script>
|
||||
|
||||
|
Reference in New Issue
Block a user