"""
feat: Update frontend and backend configurations for development environment - Downgrade PyP100 version in requirements.txt for compatibility. - Add new frontend routes for index, login, dashboard, printers, jobs, and profile pages. - Modify docker-compose files for development setup, including environment variables and service names. - Update Caddyfile for local development with Raspberry Pi backend. - Adjust health check route to use updated backend URL. - Enhance setup-backend-url.sh for development environment configuration. """
This commit is contained in:
410
backend/app/templates/profile.html
Normal file
410
backend/app/templates/profile.html
Normal file
@@ -0,0 +1,410 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Profil - MYP Platform{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-mercedes-black">Mein Profil</h1>
|
||||
<p class="mt-2 text-mercedes-gray">Verwalten Sie Ihre Kontoinformationen und Einstellungen</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- Profile Information -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="mercedes-card rounded-xl p-6 mb-6">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-bold text-mercedes-black">Persönliche Informationen</h2>
|
||||
<button onclick="toggleEditMode()" id="edit-button"
|
||||
class="bg-mercedes-blue hover:bg-blue-700 text-white px-4 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Bearbeiten
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="profile-form" onsubmit="handleProfileUpdate(event)">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="first-name" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Vorname
|
||||
</label>
|
||||
<input type="text" id="first-name" name="first_name" value="{{ current_user.first_name or '' }}" disabled
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="last-name" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Nachname
|
||||
</label>
|
||||
<input type="text" id="last-name" name="last_name" value="{{ current_user.last_name or '' }}" disabled
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
E-Mail-Adresse
|
||||
</label>
|
||||
<input type="email" id="email" name="email" value="{{ current_user.email }}" disabled
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="phone" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Telefonnummer
|
||||
</label>
|
||||
<input type="tel" id="phone" name="phone" value="{{ current_user.phone or '' }}" disabled
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="department" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Abteilung
|
||||
</label>
|
||||
<input type="text" id="department" name="department" value="{{ current_user.department or '' }}" disabled
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue disabled:bg-mercedes-light disabled:cursor-not-allowed">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="role" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Rolle
|
||||
</label>
|
||||
<input type="text" id="role" value="{{ 'Administrator' if current_user.is_admin else 'Benutzer' }}" disabled
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg bg-mercedes-light cursor-not-allowed">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="form-actions" class="hidden mt-6 flex space-x-4">
|
||||
<button type="submit"
|
||||
class="bg-mercedes-green hover:bg-green-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
Änderungen speichern
|
||||
</button>
|
||||
<button type="button" onclick="cancelEdit()"
|
||||
class="bg-mercedes-silver hover:bg-gray-400 text-mercedes-black px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Password Change -->
|
||||
<div class="mercedes-card rounded-xl p-6">
|
||||
<h2 class="text-xl font-bold text-mercedes-black mb-6">Passwort ändern</h2>
|
||||
|
||||
<form id="password-form" onsubmit="handlePasswordChange(event)" class="space-y-4">
|
||||
<div>
|
||||
<label for="current-password" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Aktuelles Passwort
|
||||
</label>
|
||||
<input type="password" id="current-password" name="current_password" required
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="new-password" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Neues Passwort
|
||||
</label>
|
||||
<input type="password" id="new-password" name="new_password" required minlength="8"
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
<p class="mt-1 text-xs text-mercedes-gray">Mindestens 8 Zeichen</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="confirm-password" class="block text-sm font-medium text-mercedes-black mb-2">
|
||||
Passwort bestätigen
|
||||
</label>
|
||||
<input type="password" id="confirm-password" name="confirm_password" required
|
||||
class="w-full px-3 py-2 border border-mercedes-silver rounded-lg focus:ring-2 focus:ring-mercedes-blue focus:border-mercedes-blue">
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="bg-mercedes-blue hover:bg-blue-700 text-white px-6 py-2 rounded-lg mercedes-button transition-all duration-200">
|
||||
Passwort ändern
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<div class="space-y-6">
|
||||
<!-- Profile Stats -->
|
||||
<div class="mercedes-card rounded-xl p-6">
|
||||
<h3 class="text-lg font-bold text-mercedes-black mb-4">Statistiken</h3>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-mercedes-gray">Gesamte Aufträge</span>
|
||||
<span id="total-jobs" class="text-lg font-bold text-mercedes-black">-</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-mercedes-gray">Abgeschlossene Aufträge</span>
|
||||
<span id="completed-jobs" class="text-lg font-bold text-mercedes-green">-</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-mercedes-gray">Aktive Aufträge</span>
|
||||
<span id="active-jobs" class="text-lg font-bold text-mercedes-blue">-</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-mercedes-gray">Fehlgeschlagene Aufträge</span>
|
||||
<span id="failed-jobs" class="text-lg font-bold text-mercedes-red">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Account Info -->
|
||||
<div class="mercedes-card rounded-xl p-6">
|
||||
<h3 class="text-lg font-bold text-mercedes-black mb-4">Kontoinformationen</h3>
|
||||
|
||||
<div class="space-y-3 text-sm">
|
||||
<div>
|
||||
<span class="text-mercedes-gray">Mitglied seit:</span>
|
||||
<div class="font-medium text-mercedes-black">{{ current_user.created_at.strftime('%d.%m.%Y') if current_user.created_at else 'Unbekannt' }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-mercedes-gray">Letzte Anmeldung:</span>
|
||||
<div class="font-medium text-mercedes-black">{{ current_user.last_login.strftime('%d.%m.%Y %H:%M') if current_user.last_login else 'Nie' }}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-mercedes-gray">Konto-Status:</span>
|
||||
<div class="font-medium">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-mercedes-green text-white">
|
||||
Aktiv
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="mercedes-card rounded-xl p-6">
|
||||
<h3 class="text-lg font-bold text-mercedes-black mb-4">Schnellaktionen</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<a href="/new-job"
|
||||
class="block w-full bg-mercedes-green hover:bg-green-700 text-white text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
|
||||
Neuer Auftrag
|
||||
</a>
|
||||
|
||||
<a href="/my/jobs"
|
||||
class="block w-full bg-mercedes-blue hover:bg-blue-700 text-white text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
|
||||
Meine Aufträge
|
||||
</a>
|
||||
|
||||
<button onclick="downloadUserData()"
|
||||
class="block w-full bg-mercedes-silver hover:bg-gray-400 text-mercedes-black text-center py-2 px-4 rounded-lg mercedes-button transition-all duration-200">
|
||||
Daten exportieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
let isEditMode = false;
|
||||
let originalFormData = {};
|
||||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadUserStats();
|
||||
storeOriginalFormData();
|
||||
});
|
||||
|
||||
// Store original form data
|
||||
function storeOriginalFormData() {
|
||||
const form = document.getElementById('profile-form');
|
||||
const formData = new FormData(form);
|
||||
originalFormData = {};
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
originalFormData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle edit mode
|
||||
function toggleEditMode() {
|
||||
isEditMode = !isEditMode;
|
||||
const inputs = document.querySelectorAll('#profile-form input:not(#role)');
|
||||
const editButton = document.getElementById('edit-button');
|
||||
const formActions = document.getElementById('form-actions');
|
||||
|
||||
inputs.forEach(input => {
|
||||
input.disabled = !isEditMode;
|
||||
});
|
||||
|
||||
if (isEditMode) {
|
||||
editButton.innerHTML = `
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Abbrechen
|
||||
`;
|
||||
editButton.onclick = cancelEdit;
|
||||
formActions.classList.remove('hidden');
|
||||
} else {
|
||||
editButton.innerHTML = `
|
||||
<svg class="h-5 w-5 inline mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
Bearbeiten
|
||||
`;
|
||||
editButton.onclick = toggleEditMode;
|
||||
formActions.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel edit
|
||||
function cancelEdit() {
|
||||
// Restore original values
|
||||
Object.keys(originalFormData).forEach(key => {
|
||||
const input = document.querySelector(`[name="${key}"]`);
|
||||
if (input) {
|
||||
input.value = originalFormData[key];
|
||||
}
|
||||
});
|
||||
|
||||
toggleEditMode();
|
||||
}
|
||||
|
||||
// Handle profile update
|
||||
async function handleProfileUpdate(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
const profileData = {};
|
||||
|
||||
for (let [key, value] of formData.entries()) {
|
||||
profileData[key] = value;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiCall('/api/user/profile', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(profileData)
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
showFlashMessage('Profil erfolgreich aktualisiert', 'success');
|
||||
storeOriginalFormData();
|
||||
toggleEditMode();
|
||||
} else {
|
||||
throw new Error(response.message || 'Unbekannter Fehler');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error);
|
||||
showFlashMessage('Fehler beim Aktualisieren des Profils: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle password change
|
||||
async function handlePasswordChange(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.target);
|
||||
const newPassword = formData.get('new_password');
|
||||
const confirmPassword = formData.get('confirm_password');
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showFlashMessage('Die Passwörter stimmen nicht überein', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordData = {
|
||||
current_password: formData.get('current_password'),
|
||||
new_password: newPassword
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await apiCall('/api/user/password', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(passwordData)
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
showFlashMessage('Passwort erfolgreich geändert', 'success');
|
||||
document.getElementById('password-form').reset();
|
||||
} else {
|
||||
throw new Error(response.message || 'Unbekannter Fehler');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error changing password:', error);
|
||||
showFlashMessage('Fehler beim Ändern des Passworts: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Load user statistics
|
||||
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;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading user stats:', error);
|
||||
// Don't show error message for stats, just keep the dashes
|
||||
}
|
||||
}
|
||||
|
||||
// Download user data
|
||||
async function downloadUserData() {
|
||||
try {
|
||||
const response = await fetch('/api/user/export', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = 'meine_daten.json';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
showFlashMessage('Daten erfolgreich exportiert', 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error downloading user data:', error);
|
||||
showFlashMessage('Fehler beim Exportieren der Daten', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 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');
|
||||
} else {
|
||||
this.setCustomValidity('');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user