"Refactor dashboard
This commit is contained in:
parent
fb148832c6
commit
bb0dd812a1
@ -200,26 +200,49 @@
|
||||
|
||||
/* Light/Dark Mode compatible cards */
|
||||
.glass-card-light {
|
||||
@apply bg-white/70 dark:bg-black/80 backdrop-blur-lg border border-gray-200/80 dark:border-slate-700/30 rounded-xl shadow-xl transition-all duration-300;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(229, 231, 235, 0.8);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .glass-card-light {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-color: rgba(100, 116, 139, 0.3);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@apply text-slate-800 dark:text-white text-xl font-semibold mb-4;
|
||||
color: #1e293b;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dark .section-title {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
@apply text-slate-900 dark:text-white font-bold text-2xl;
|
||||
color: #0f172a;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.dark .stat-value {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
@apply text-slate-500 dark:text-slate-400 text-sm;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.dark .stat-label {
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
@ -26,6 +26,37 @@
|
||||
</div>
|
||||
|
||||
<form id="profile-form" onsubmit="handleProfileUpdate(event)">
|
||||
<!-- Avatar Section -->
|
||||
<div class="flex items-center space-x-6 mb-8 pb-6 border-b border-gray-200 dark:border-slate-600">
|
||||
<div class="relative">
|
||||
<div class="w-24 h-24 rounded-full bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center text-white text-2xl font-bold shadow-lg">
|
||||
<span id="avatar-text">{{ (current_user.first_name[0] if current_user.first_name else current_user.email[0]) | upper }}</span>
|
||||
<img id="avatar-image" src="{{ current_user.avatar_url if current_user.avatar_url else '' }}"
|
||||
alt="Profilbild" class="w-24 h-24 rounded-full object-cover {{ 'hidden' if not current_user.avatar_url else '' }}">
|
||||
</div>
|
||||
<button type="button" onclick="triggerAvatarUpload()"
|
||||
class="absolute bottom-0 right-0 bg-blue-600 hover:bg-blue-700 text-white rounded-full p-2 shadow-lg transition-all duration-200">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white mb-1">
|
||||
{{ current_user.first_name + ' ' + current_user.last_name if current_user.first_name and current_user.last_name else current_user.email.split('@')[0] }}
|
||||
</h3>
|
||||
<p class="text-slate-500 dark:text-slate-400">{{ current_user.email }}</p>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-400 mt-1">
|
||||
{{ 'Administrator' if current_user.is_admin else 'Benutzer' }} •
|
||||
{{ current_user.department or 'Keine Abteilung' }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<input type="file" id="avatar-upload" name="avatar" accept="image/*" class="hidden" onchange="handleAvatarUpload(event)">
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="first-name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">
|
||||
@ -212,11 +243,14 @@
|
||||
<script>
|
||||
let isEditMode = false;
|
||||
let originalFormData = {};
|
||||
let avatarFile = null;
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadUserStats();
|
||||
storeOriginalFormData();
|
||||
setupPasswordStrengthMeter();
|
||||
setupFormValidation();
|
||||
});
|
||||
|
||||
// Store original form data
|
||||
@ -226,11 +260,234 @@
|
||||
originalFormData = {};
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
originalFormData[key] = value;
|
||||
if (key !== 'avatar') { // Don't store file data
|
||||
originalFormData[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle edit mode
|
||||
// Avatar Upload Functions
|
||||
function triggerAvatarUpload() {
|
||||
document.getElementById('avatar-upload').click();
|
||||
}
|
||||
|
||||
async function handleAvatarUpload(event) {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// Validate file
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showFlashMessage('Bitte wählen Sie eine gültige Bilddatei aus', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) { // 5MB limit
|
||||
showFlashMessage('Die Datei ist zu groß. Maximale Größe: 5MB', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Show loading state
|
||||
const uploadButton = event.target.parentElement.querySelector('button');
|
||||
const originalButtonContent = uploadButton.innerHTML;
|
||||
uploadButton.innerHTML = `
|
||||
<svg class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
`;
|
||||
uploadButton.disabled = true;
|
||||
|
||||
// Preview image
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
const avatarImage = document.getElementById('avatar-image');
|
||||
const avatarText = document.getElementById('avatar-text');
|
||||
|
||||
avatarImage.src = e.target.result;
|
||||
avatarImage.classList.remove('hidden');
|
||||
avatarText.style.display = 'none';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// Upload avatar
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', file);
|
||||
|
||||
const response = await fetch('/api/user/avatar', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showFlashMessage('Profilbild erfolgreich aktualisiert', 'success');
|
||||
avatarFile = file;
|
||||
} else {
|
||||
throw new Error(result.message || 'Upload fehlgeschlagen');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Avatar upload error:', error);
|
||||
showFlashMessage('Fehler beim Hochladen des Profilbilds: ' + error.message, 'error');
|
||||
|
||||
// Reset preview on error
|
||||
const avatarImage = document.getElementById('avatar-image');
|
||||
const avatarText = document.getElementById('avatar-text');
|
||||
avatarImage.classList.add('hidden');
|
||||
avatarText.style.display = 'flex';
|
||||
|
||||
} finally {
|
||||
// Restore button
|
||||
const uploadButton = event.target.parentElement.querySelector('button');
|
||||
uploadButton.innerHTML = originalButtonContent;
|
||||
uploadButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Password Strength Meter
|
||||
function setupPasswordStrengthMeter() {
|
||||
const newPasswordInput = document.getElementById('new-password');
|
||||
|
||||
// Create password strength indicator
|
||||
const strengthMeter = document.createElement('div');
|
||||
strengthMeter.id = 'password-strength-meter';
|
||||
strengthMeter.className = 'mt-2';
|
||||
|
||||
const strengthBar = document.createElement('div');
|
||||
strengthBar.className = 'w-full bg-gray-200 dark:bg-slate-600 rounded-full h-2';
|
||||
|
||||
const strengthIndicator = document.createElement('div');
|
||||
strengthIndicator.id = 'strength-indicator';
|
||||
strengthIndicator.className = 'h-2 rounded-full transition-all duration-300';
|
||||
|
||||
const strengthText = document.createElement('div');
|
||||
strengthText.id = 'strength-text';
|
||||
strengthText.className = 'text-xs mt-1';
|
||||
|
||||
strengthBar.appendChild(strengthIndicator);
|
||||
strengthMeter.appendChild(strengthBar);
|
||||
strengthMeter.appendChild(strengthText);
|
||||
|
||||
newPasswordInput.parentElement.appendChild(strengthMeter);
|
||||
|
||||
newPasswordInput.addEventListener('input', function() {
|
||||
const password = this.value;
|
||||
const strength = calculatePasswordStrength(password);
|
||||
updatePasswordStrengthMeter(strength);
|
||||
});
|
||||
}
|
||||
|
||||
function calculatePasswordStrength(password) {
|
||||
let score = 0;
|
||||
const checks = {
|
||||
length: password.length >= 8,
|
||||
lowercase: /[a-z]/.test(password),
|
||||
uppercase: /[A-Z]/.test(password),
|
||||
numbers: /\d/.test(password),
|
||||
special: /[!@#$%^&*(),.?":{}|<>]/.test(password)
|
||||
};
|
||||
|
||||
score = Object.values(checks).filter(Boolean).length;
|
||||
|
||||
let strength = 'weak';
|
||||
if (score >= 4) strength = 'strong';
|
||||
else if (score >= 3) strength = 'medium';
|
||||
|
||||
return { score, strength, checks };
|
||||
}
|
||||
|
||||
function updatePasswordStrengthMeter(strengthData) {
|
||||
const indicator = document.getElementById('strength-indicator');
|
||||
const text = document.getElementById('strength-text');
|
||||
|
||||
const { strength, score } = strengthData;
|
||||
const percentage = (score / 5) * 100;
|
||||
|
||||
// Update bar
|
||||
indicator.style.width = `${percentage}%`;
|
||||
|
||||
// Update colors and text
|
||||
switch (strength) {
|
||||
case 'weak':
|
||||
indicator.className = 'h-2 rounded-full transition-all duration-300 bg-red-500';
|
||||
text.textContent = 'Schwach';
|
||||
text.className = 'text-xs mt-1 text-red-500';
|
||||
break;
|
||||
case 'medium':
|
||||
indicator.className = 'h-2 rounded-full transition-all duration-300 bg-yellow-500';
|
||||
text.textContent = 'Mittel';
|
||||
text.className = 'text-xs mt-1 text-yellow-500';
|
||||
break;
|
||||
case 'strong':
|
||||
indicator.className = 'h-2 rounded-full transition-all duration-300 bg-green-500';
|
||||
text.textContent = 'Stark';
|
||||
text.className = 'text-xs mt-1 text-green-500';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced Form Validation
|
||||
function setupFormValidation() {
|
||||
// Real-time email validation
|
||||
const emailInput = document.getElementById('email');
|
||||
emailInput.addEventListener('blur', validateEmail);
|
||||
|
||||
// Phone number formatting
|
||||
const phoneInput = document.getElementById('phone');
|
||||
phoneInput.addEventListener('input', formatPhoneNumber);
|
||||
|
||||
// Name validation
|
||||
['first-name', 'last-name'].forEach(id => {
|
||||
const input = document.getElementById(id);
|
||||
input.addEventListener('input', function() {
|
||||
validateName(this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function validateEmail(event) {
|
||||
const email = event.target.value;
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
|
||||
if (email && !emailRegex.test(email)) {
|
||||
event.target.setCustomValidity('Bitte geben Sie eine gültige E-Mail-Adresse ein');
|
||||
event.target.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
} else {
|
||||
event.target.setCustomValidity('');
|
||||
event.target.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
}
|
||||
}
|
||||
|
||||
function formatPhoneNumber(event) {
|
||||
let value = event.target.value.replace(/\D/g, '');
|
||||
|
||||
if (value.length >= 10) {
|
||||
value = value.replace(/(\d{2})(\d{3})(\d{3})(\d{4})/, '+49 $2 $3 $4');
|
||||
}
|
||||
|
||||
event.target.value = value;
|
||||
}
|
||||
|
||||
function validateName(input) {
|
||||
const nameRegex = /^[a-zA-ZäöüÄÖÜß\s-]+$/;
|
||||
const value = input.value.trim();
|
||||
|
||||
if (value && !nameRegex.test(value)) {
|
||||
input.setCustomValidity('Namen dürfen nur Buchstaben, Leerzeichen und Bindestriche enthalten');
|
||||
input.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
} else {
|
||||
input.setCustomValidity('');
|
||||
input.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle edit mode with enhanced UX
|
||||
function toggleEditMode() {
|
||||
isEditMode = !isEditMode;
|
||||
const inputs = document.querySelectorAll('#profile-form input:not(#role)');
|
||||
@ -239,6 +496,11 @@
|
||||
|
||||
inputs.forEach(input => {
|
||||
input.disabled = !isEditMode;
|
||||
if (isEditMode) {
|
||||
input.classList.add('focus:ring-2', 'focus:ring-blue-600', 'focus:border-blue-600');
|
||||
} else {
|
||||
input.classList.remove('focus:ring-2', 'focus:ring-blue-600', 'focus:border-blue-600');
|
||||
}
|
||||
});
|
||||
|
||||
if (isEditMode) {
|
||||
@ -248,6 +510,7 @@
|
||||
</svg>
|
||||
Abbrechen
|
||||
`;
|
||||
editButton.className = 'bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-500 text-white px-4 py-2 rounded-lg transition-all duration-200';
|
||||
editButton.onclick = cancelEdit;
|
||||
formActions.classList.remove('hidden');
|
||||
} else {
|
||||
@ -257,13 +520,23 @@
|
||||
</svg>
|
||||
Bearbeiten
|
||||
`;
|
||||
editButton.className = 'bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500 text-white px-4 py-2 rounded-lg transition-all duration-200';
|
||||
editButton.onclick = toggleEditMode;
|
||||
formActions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel edit
|
||||
// Cancel edit with confirmation
|
||||
function cancelEdit() {
|
||||
// Check if there are unsaved changes
|
||||
const hasChanges = checkForUnsavedChanges();
|
||||
|
||||
if (hasChanges) {
|
||||
if (!confirm('Sie haben ungespeicherte Änderungen. Möchten Sie wirklich abbrechen?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original values
|
||||
Object.keys(originalFormData).forEach(key => {
|
||||
const input = document.querySelector(`[name="${key}"]`);
|
||||
@ -272,18 +545,49 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Reset avatar if changed
|
||||
if (avatarFile) {
|
||||
const avatarImage = document.getElementById('avatar-image');
|
||||
const avatarText = document.getElementById('avatar-text');
|
||||
avatarImage.classList.add('hidden');
|
||||
avatarText.style.display = 'flex';
|
||||
avatarFile = null;
|
||||
}
|
||||
|
||||
toggleEditMode();
|
||||
}
|
||||
|
||||
// Handle profile update
|
||||
function checkForUnsavedChanges() {
|
||||
return Object.keys(originalFormData).some(key => {
|
||||
const input = document.querySelector(`[name="${key}"]`);
|
||||
return input && input.value !== originalFormData[key];
|
||||
}) || avatarFile !== null;
|
||||
}
|
||||
|
||||
// Enhanced profile update with loading state
|
||||
async function handleProfileUpdate(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const submitButton = event.target.querySelector('button[type="submit"]');
|
||||
const originalButtonText = submitButton.innerHTML;
|
||||
|
||||
// Show loading state
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = `
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Speichern...
|
||||
`;
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
const profileData = {};
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
profileData[key] = value;
|
||||
if (key !== 'avatar') {
|
||||
profileData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@ -296,6 +600,11 @@
|
||||
showFlashMessage('Profil erfolgreich aktualisiert', 'success');
|
||||
storeOriginalFormData();
|
||||
toggleEditMode();
|
||||
|
||||
// Update avatar text if name changed
|
||||
const newInitial = (profileData.first_name?.[0] || profileData.email?.[0] || 'U').toUpperCase();
|
||||
document.getElementById('avatar-text').textContent = newInitial;
|
||||
|
||||
} else {
|
||||
throw new Error(response.message || 'Unbekannter Fehler');
|
||||
}
|
||||
@ -303,22 +612,46 @@
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error);
|
||||
showFlashMessage('Fehler beim Aktualisieren des Profils: ' + error.message, 'error');
|
||||
} finally {
|
||||
// Restore button
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle password change
|
||||
// Enhanced password change with better validation
|
||||
async function handlePasswordChange(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const submitButton = event.target.querySelector('button[type="submit"]');
|
||||
const originalButtonText = submitButton.innerHTML;
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
const newPassword = formData.get('new_password');
|
||||
const confirmPassword = formData.get('confirm_password');
|
||||
|
||||
// Enhanced password validation
|
||||
const strength = calculatePasswordStrength(newPassword);
|
||||
if (strength.score < 3) {
|
||||
showFlashMessage('Das Passwort ist zu schwach. Verwenden Sie eine Kombination aus Groß- und Kleinbuchstaben, Zahlen und Sonderzeichen.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showFlashMessage('Die Passwörter stimmen nicht überein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
submitButton.disabled = true;
|
||||
submitButton.innerHTML = `
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Ändern...
|
||||
`;
|
||||
|
||||
const passwordData = {
|
||||
current_password: formData.get('current_password'),
|
||||
new_password: newPassword
|
||||
@ -333,6 +666,15 @@
|
||||
if (response.success) {
|
||||
showFlashMessage('Passwort erfolgreich geändert', 'success');
|
||||
document.getElementById('password-form').reset();
|
||||
|
||||
// Reset password strength meter
|
||||
const strengthIndicator = document.getElementById('strength-indicator');
|
||||
const strengthText = document.getElementById('strength-text');
|
||||
if (strengthIndicator) {
|
||||
strengthIndicator.style.width = '0%';
|
||||
strengthText.textContent = '';
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(response.message || 'Unbekannter Fehler');
|
||||
}
|
||||
@ -340,35 +682,72 @@
|
||||
} catch (error) {
|
||||
console.error('Error changing password:', error);
|
||||
showFlashMessage('Fehler beim Ändern des Passworts: ' + error.message, 'error');
|
||||
} finally {
|
||||
// Restore button
|
||||
submitButton.disabled = false;
|
||||
submitButton.innerHTML = originalButtonText;
|
||||
}
|
||||
}
|
||||
|
||||
// Load user statistics
|
||||
// Load user statistics with enhanced error handling
|
||||
async function loadUserStats() {
|
||||
try {
|
||||
const response = await apiCall('/api/user/stats');
|
||||
|
||||
if (response.success) {
|
||||
const stats = response.stats;
|
||||
document.getElementById('total-jobs').textContent = stats.total_jobs || 0;
|
||||
document.getElementById('completed-jobs').textContent = stats.completed_jobs || 0;
|
||||
document.getElementById('active-jobs').textContent = stats.active_jobs || 0;
|
||||
document.getElementById('failed-jobs').textContent = stats.failed_jobs || 0;
|
||||
|
||||
// Animate counter updates
|
||||
animateCounter('total-jobs', stats.total_jobs || 0);
|
||||
animateCounter('completed-jobs', stats.completed_jobs || 0);
|
||||
animateCounter('active-jobs', stats.active_jobs || 0);
|
||||
animateCounter('failed-jobs', stats.failed_jobs || 0);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading user stats:', error);
|
||||
// Don't show error message for stats, just keep the dashes
|
||||
// Keep default values (-) if loading fails
|
||||
}
|
||||
}
|
||||
|
||||
// Download user data
|
||||
function animateCounter(elementId, targetValue) {
|
||||
const element = document.getElementById(elementId);
|
||||
const startValue = parseInt(element.textContent) || 0;
|
||||
const duration = 1500;
|
||||
const increment = (targetValue - startValue) / (duration / 16);
|
||||
let currentValue = startValue;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
currentValue += increment;
|
||||
if (increment > 0 ? currentValue >= targetValue : currentValue <= targetValue) {
|
||||
element.textContent = targetValue;
|
||||
clearInterval(timer);
|
||||
} else {
|
||||
element.textContent = Math.floor(currentValue);
|
||||
}
|
||||
}, 16);
|
||||
}
|
||||
|
||||
// Enhanced data export with progress tracking
|
||||
async function downloadUserData() {
|
||||
try {
|
||||
// Show loading state
|
||||
const button = event.target;
|
||||
const originalButtonText = button.innerHTML;
|
||||
button.disabled = true;
|
||||
button.innerHTML = `
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 inline" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Exportiere...
|
||||
`;
|
||||
|
||||
const response = await fetch('/api/user/export', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || ''
|
||||
}
|
||||
});
|
||||
|
||||
@ -381,7 +760,7 @@
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = 'meine_daten.json';
|
||||
a.download = `meine_daten_${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
@ -391,19 +770,33 @@
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error downloading user data:', error);
|
||||
showFlashMessage('Fehler beim Exportieren der Daten', 'error');
|
||||
showFlashMessage('Fehler beim Exportieren der Daten: ' + error.message, 'error');
|
||||
} finally {
|
||||
// Restore button
|
||||
button.disabled = false;
|
||||
button.innerHTML = originalButtonText;
|
||||
}
|
||||
}
|
||||
|
||||
// Password confirmation validation
|
||||
// Enhanced password confirmation validation
|
||||
document.getElementById('confirm-password').addEventListener('input', function() {
|
||||
const newPassword = document.getElementById('new-password').value;
|
||||
const confirmPassword = this.value;
|
||||
|
||||
if (confirmPassword && newPassword !== confirmPassword) {
|
||||
this.setCustomValidity('Die Passwörter stimmen nicht überein');
|
||||
this.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
this.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-500');
|
||||
}
|
||||
});
|
||||
|
||||
// Warn user about unsaved changes before leaving
|
||||
window.addEventListener('beforeunload', function(e) {
|
||||
if (isEditMode && checkForUnsavedChanges()) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user