"Refactor global refresh functions and templates for better performance (feat)"

This commit is contained in:
Till Tomczak 2025-05-29 17:51:01 +02:00
parent e338c5835e
commit a473c06658
5 changed files with 958 additions and 3 deletions

View File

@ -5103,3 +5103,547 @@ def refresh_dashboard():
'success': False,
'error': 'Fehler beim Aktualisieren der Dashboard-Daten'
}), 500
# ===== ADMIN GASTAUFTRÄGE API-ENDPUNKTE =====
@app.route('/api/admin/guest-requests', methods=['GET'])
@admin_required
def get_admin_guest_requests():
"""Gibt alle Gastaufträge für Admin-Verwaltung zurück"""
try:
db_session = get_db_session()
# Parameter auslesen
status = request.args.get('status', 'all')
limit = int(request.args.get('limit', 50))
offset = int(request.args.get('offset', 0))
search = request.args.get('search', '')
# Basis-Query
query = db_session.query(GuestRequest)
# Status-Filter
if status != 'all':
query = query.filter(GuestRequest.status == status)
# Suchfilter
if search:
search_term = f"%{search}%"
query = query.filter(
(GuestRequest.name.ilike(search_term)) |
(GuestRequest.email.ilike(search_term)) |
(GuestRequest.file_name.ilike(search_term)) |
(GuestRequest.reason.ilike(search_term))
)
# Gesamtanzahl vor Pagination
total = query.count()
# Sortierung und Pagination
requests = query.order_by(GuestRequest.created_at.desc()).offset(offset).limit(limit).all()
# Statistiken berechnen
stats = {
'total': db_session.query(GuestRequest).count(),
'pending': db_session.query(GuestRequest).filter(GuestRequest.status == 'pending').count(),
'approved': db_session.query(GuestRequest).filter(GuestRequest.status == 'approved').count(),
'rejected': db_session.query(GuestRequest).filter(GuestRequest.status == 'rejected').count(),
}
# Requests zu Dictionary konvertieren
requests_data = []
for req in requests:
# Priorität berechnen
now = datetime.now()
hours_old = (now - req.created_at).total_seconds() / 3600 if req.created_at else 0
is_urgent = hours_old > 24 and req.status == 'pending'
request_data = {
'id': req.id,
'name': req.name,
'email': req.email,
'file_name': req.file_name,
'file_path': req.file_path,
'duration_minutes': req.duration_minutes,
'copies': req.copies,
'reason': req.reason,
'status': req.status,
'created_at': req.created_at.isoformat() if req.created_at else None,
'updated_at': req.updated_at.isoformat() if req.updated_at else None,
'approved_at': req.approved_at.isoformat() if req.approved_at else None,
'rejected_at': req.rejected_at.isoformat() if req.rejected_at else None,
'approval_notes': req.approval_notes,
'rejection_reason': req.rejection_reason,
'is_urgent': is_urgent,
'hours_old': round(hours_old, 1)
}
requests_data.append(request_data)
db_session.close()
app_logger.info(f"Admin-Gastaufträge geladen: {len(requests_data)} von {total} (Status: {status})")
return jsonify({
'success': True,
'requests': requests_data,
'stats': stats,
'total': total,
'offset': offset,
'limit': limit,
'has_more': offset + limit < total
})
except Exception as e:
app_logger.error(f"Fehler beim Laden der Admin-Gastaufträge: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Laden der Gastaufträge: {str(e)}'
}), 500
@app.route('/api/guest-requests/<int:request_id>/approve', methods=['POST'])
@admin_required
def approve_guest_request(request_id):
"""Genehmigt einen Gastauftrag"""
try:
db_session = get_db_session()
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
if not guest_request:
db_session.close()
return jsonify({
'success': False,
'message': 'Gastauftrag nicht gefunden'
}), 404
if guest_request.status != 'pending':
db_session.close()
return jsonify({
'success': False,
'message': f'Gastauftrag kann im Status "{guest_request.status}" nicht genehmigt werden'
}), 400
# Daten aus Request Body
data = request.get_json() or {}
notes = data.get('notes', '')
printer_id = data.get('printer_id')
# Status aktualisieren
guest_request.status = 'approved'
guest_request.approved_at = datetime.now()
guest_request.approved_by = current_user.id
guest_request.approval_notes = notes
guest_request.updated_at = datetime.now()
# Falls Drucker zugewiesen werden soll
if printer_id:
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
if printer:
guest_request.assigned_printer_id = printer_id
# OTP-Code generieren für den Gast
import secrets
otp_code = ''.join([str(secrets.randbelow(10)) for _ in range(6)])
guest_request.otp_code = otp_code
guest_request.otp_expires_at = datetime.now() + timedelta(hours=24)
db_session.commit()
# Benachrichtigung an den Gast senden (falls E-Mail verfügbar)
if guest_request.email:
try:
# Hier würde normalerweise eine E-Mail gesendet werden
app_logger.info(f"E-Mail-Benachrichtigung würde an {guest_request.email} gesendet (OTP: {otp_code})")
except Exception as e:
app_logger.warning(f"Fehler beim Senden der E-Mail-Benachrichtigung: {str(e)}")
db_session.close()
app_logger.info(f"Gastauftrag {request_id} von Admin {current_user.id} genehmigt (OTP: {otp_code})")
return jsonify({
'success': True,
'message': 'Gastauftrag erfolgreich genehmigt',
'otp_code': otp_code,
'expires_at': (datetime.now() + timedelta(hours=24)).isoformat()
})
except Exception as e:
app_logger.error(f"Fehler beim Genehmigen des Gastauftrags {request_id}: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Genehmigen: {str(e)}'
}), 500
@app.route('/api/guest-requests/<int:request_id>/reject', methods=['POST'])
@admin_required
def reject_guest_request(request_id):
"""Lehnt einen Gastauftrag ab"""
try:
db_session = get_db_session()
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
if not guest_request:
db_session.close()
return jsonify({
'success': False,
'message': 'Gastauftrag nicht gefunden'
}), 404
if guest_request.status != 'pending':
db_session.close()
return jsonify({
'success': False,
'message': f'Gastauftrag kann im Status "{guest_request.status}" nicht abgelehnt werden'
}), 400
# Daten aus Request Body
data = request.get_json() or {}
reason = data.get('reason', '').strip()
if not reason:
db_session.close()
return jsonify({
'success': False,
'message': 'Ablehnungsgrund ist erforderlich'
}), 400
# Status aktualisieren
guest_request.status = 'rejected'
guest_request.rejected_at = datetime.now()
guest_request.rejected_by = current_user.id
guest_request.rejection_reason = reason
guest_request.updated_at = datetime.now()
db_session.commit()
# Benachrichtigung an den Gast senden (falls E-Mail verfügbar)
if guest_request.email:
try:
# Hier würde normalerweise eine E-Mail gesendet werden
app_logger.info(f"Ablehnungs-E-Mail würde an {guest_request.email} gesendet (Grund: {reason})")
except Exception as e:
app_logger.warning(f"Fehler beim Senden der Ablehnungs-E-Mail: {str(e)}")
db_session.close()
app_logger.info(f"Gastauftrag {request_id} von Admin {current_user.id} abgelehnt (Grund: {reason})")
return jsonify({
'success': True,
'message': 'Gastauftrag erfolgreich abgelehnt'
})
except Exception as e:
app_logger.error(f"Fehler beim Ablehnen des Gastauftrags {request_id}: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Ablehnen: {str(e)}'
}), 500
@app.route('/api/guest-requests/<int:request_id>', methods=['DELETE'])
@admin_required
def delete_guest_request(request_id):
"""Löscht einen Gastauftrag"""
try:
db_session = get_db_session()
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
if not guest_request:
db_session.close()
return jsonify({
'success': False,
'message': 'Gastauftrag nicht gefunden'
}), 404
# Datei löschen falls vorhanden
if guest_request.file_path and os.path.exists(guest_request.file_path):
try:
os.remove(guest_request.file_path)
app_logger.info(f"Datei {guest_request.file_path} für Gastauftrag {request_id} gelöscht")
except Exception as e:
app_logger.warning(f"Fehler beim Löschen der Datei: {str(e)}")
# Gastauftrag aus Datenbank löschen
request_name = guest_request.name
db_session.delete(guest_request)
db_session.commit()
db_session.close()
app_logger.info(f"Gastauftrag {request_id} ({request_name}) von Admin {current_user.id} gelöscht")
return jsonify({
'success': True,
'message': 'Gastauftrag erfolgreich gelöscht'
})
except Exception as e:
app_logger.error(f"Fehler beim Löschen des Gastauftrags {request_id}: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Löschen: {str(e)}'
}), 500
@app.route('/api/guest-requests/<int:request_id>', methods=['GET'])
@admin_required
def get_guest_request_detail(request_id):
"""Gibt Details eines spezifischen Gastauftrags zurück"""
try:
db_session = get_db_session()
guest_request = db_session.query(GuestRequest).filter(GuestRequest.id == request_id).first()
if not guest_request:
db_session.close()
return jsonify({
'success': False,
'message': 'Gastauftrag nicht gefunden'
}), 404
# Detaildaten zusammenstellen
request_data = {
'id': guest_request.id,
'name': guest_request.name,
'email': guest_request.email,
'file_name': guest_request.file_name,
'file_path': guest_request.file_path,
'file_size': None,
'duration_minutes': guest_request.duration_minutes,
'copies': guest_request.copies,
'reason': guest_request.reason,
'status': guest_request.status,
'created_at': guest_request.created_at.isoformat() if guest_request.created_at else None,
'updated_at': guest_request.updated_at.isoformat() if guest_request.updated_at else None,
'approved_at': guest_request.approved_at.isoformat() if guest_request.approved_at else None,
'rejected_at': guest_request.rejected_at.isoformat() if guest_request.rejected_at else None,
'approval_notes': guest_request.approval_notes,
'rejection_reason': guest_request.rejection_reason,
'otp_code': guest_request.otp_code,
'otp_expires_at': guest_request.otp_expires_at.isoformat() if guest_request.otp_expires_at else None,
'author_ip': guest_request.author_ip
}
# Dateigröße ermitteln
if guest_request.file_path and os.path.exists(guest_request.file_path):
try:
file_size = os.path.getsize(guest_request.file_path)
request_data['file_size'] = file_size
request_data['file_size_mb'] = round(file_size / (1024 * 1024), 2)
except Exception as e:
app_logger.warning(f"Fehler beim Ermitteln der Dateigröße: {str(e)}")
# Bearbeiter-Informationen hinzufügen
if guest_request.approved_by:
approved_by_user = db_session.query(User).filter(User.id == guest_request.approved_by).first()
if approved_by_user:
request_data['approved_by_name'] = approved_by_user.name or approved_by_user.username
if guest_request.rejected_by:
rejected_by_user = db_session.query(User).filter(User.id == guest_request.rejected_by).first()
if rejected_by_user:
request_data['rejected_by_name'] = rejected_by_user.name or rejected_by_user.username
# Zugewiesener Drucker
if hasattr(guest_request, 'assigned_printer_id') and guest_request.assigned_printer_id:
assigned_printer = db_session.query(Printer).filter(Printer.id == guest_request.assigned_printer_id).first()
if assigned_printer:
request_data['assigned_printer'] = {
'id': assigned_printer.id,
'name': assigned_printer.name,
'location': assigned_printer.location,
'status': assigned_printer.status
}
db_session.close()
return jsonify({
'success': True,
'request': request_data
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Gastauftrag-Details {request_id}: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Abrufen der Details: {str(e)}'
}), 500
@app.route('/api/admin/guest-requests/stats', methods=['GET'])
@admin_required
def get_guest_requests_stats():
"""Gibt detaillierte Statistiken zu Gastaufträgen zurück"""
try:
db_session = get_db_session()
# Basis-Statistiken
total = db_session.query(GuestRequest).count()
pending = db_session.query(GuestRequest).filter(GuestRequest.status == 'pending').count()
approved = db_session.query(GuestRequest).filter(GuestRequest.status == 'approved').count()
rejected = db_session.query(GuestRequest).filter(GuestRequest.status == 'rejected').count()
# Zeitbasierte Statistiken
today = datetime.now().date()
week_ago = datetime.now() - timedelta(days=7)
month_ago = datetime.now() - timedelta(days=30)
today_requests = db_session.query(GuestRequest).filter(
func.date(GuestRequest.created_at) == today
).count()
week_requests = db_session.query(GuestRequest).filter(
GuestRequest.created_at >= week_ago
).count()
month_requests = db_session.query(GuestRequest).filter(
GuestRequest.created_at >= month_ago
).count()
# Dringende Requests (älter als 24h und pending)
urgent_cutoff = datetime.now() - timedelta(hours=24)
urgent_requests = db_session.query(GuestRequest).filter(
GuestRequest.status == 'pending',
GuestRequest.created_at < urgent_cutoff
).count()
# Durchschnittliche Bearbeitungszeit
avg_processing_time = None
try:
processed_requests = db_session.query(GuestRequest).filter(
GuestRequest.status.in_(['approved', 'rejected']),
GuestRequest.updated_at.isnot(None)
).all()
if processed_requests:
total_time = sum([
(req.updated_at - req.created_at).total_seconds()
for req in processed_requests
if req.updated_at and req.created_at
])
avg_processing_time = round(total_time / len(processed_requests) / 3600, 2) # Stunden
except Exception as e:
app_logger.warning(f"Fehler beim Berechnen der durchschnittlichen Bearbeitungszeit: {str(e)}")
# Erfolgsrate
success_rate = 0
if approved + rejected > 0:
success_rate = round((approved / (approved + rejected)) * 100, 1)
stats = {
'total': total,
'pending': pending,
'approved': approved,
'rejected': rejected,
'urgent': urgent_requests,
'today': today_requests,
'week': week_requests,
'month': month_requests,
'success_rate': success_rate,
'avg_processing_time_hours': avg_processing_time,
'completion_rate': round(((approved + rejected) / total * 100), 1) if total > 0 else 0
}
db_session.close()
return jsonify({
'success': True,
'stats': stats,
'generated_at': datetime.now().isoformat()
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der Gastauftrag-Statistiken: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Abrufen der Statistiken: {str(e)}'
}), 500
@app.route('/api/admin/guest-requests/export', methods=['GET'])
@admin_required
def export_guest_requests():
"""Exportiert Gastaufträge als CSV"""
try:
db_session = get_db_session()
# Filter-Parameter
status = request.args.get('status', 'all')
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
# Query aufbauen
query = db_session.query(GuestRequest)
if status != 'all':
query = query.filter(GuestRequest.status == status)
if start_date:
try:
start_dt = datetime.fromisoformat(start_date)
query = query.filter(GuestRequest.created_at >= start_dt)
except ValueError:
pass
if end_date:
try:
end_dt = datetime.fromisoformat(end_date)
query = query.filter(GuestRequest.created_at <= end_dt)
except ValueError:
pass
requests = query.order_by(GuestRequest.created_at.desc()).all()
# CSV-Daten erstellen
import csv
import io
output = io.StringIO()
writer = csv.writer(output)
# Header
writer.writerow([
'ID', 'Name', 'E-Mail', 'Datei', 'Status', 'Erstellt am',
'Dauer (Min)', 'Kopien', 'Begründung', 'Genehmigt am',
'Abgelehnt am', 'Bearbeitungsnotizen', 'Ablehnungsgrund'
])
# Daten
for req in requests:
writer.writerow([
req.id,
req.name or '',
req.email or '',
req.file_name or '',
req.status,
req.created_at.strftime('%Y-%m-%d %H:%M:%S') if req.created_at else '',
req.duration_minutes or '',
req.copies or '',
req.reason or '',
req.approved_at.strftime('%Y-%m-%d %H:%M:%S') if req.approved_at else '',
req.rejected_at.strftime('%Y-%m-%d %H:%M:%S') if req.rejected_at else '',
req.approval_notes or '',
req.rejection_reason or ''
])
db_session.close()
# Response erstellen
output_value = output.getvalue()
output.close()
response = make_response(output_value)
response.headers["Content-Disposition"] = f"attachment; filename=gastauftraege_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
response.headers["Content-Type"] = "text/csv; charset=utf-8"
app_logger.info(f"Gastaufträge-Export erstellt: {len(requests)} Einträge")
return response
except Exception as e:
app_logger.error(f"Fehler beim Exportieren der Gastaufträge: {str(e)}")
return jsonify({
'success': False,
'message': f'Fehler beim Export: {str(e)}'
}), 500
# ===== ENDE ADMIN GASTAUFTRÄGE API-ENDPUNKTE =====

View File

@ -0,0 +1,376 @@
/**
* MYP Platform - Globale Refresh-Funktionen
* Sammelt alle Refresh-Funktionen für verschiedene Seiten und Komponenten
*/
/**
* Dashboard-Refresh-Funktion
*/
window.refreshDashboard = async function() {
const refreshButton = document.getElementById('refreshDashboard');
if (refreshButton) {
// Button-State ändern
refreshButton.disabled = true;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.add('animate-spin');
}
}
try {
// Dashboard-Statistiken aktualisieren
const response = await fetch('/api/dashboard/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken()
}
});
const data = await response.json();
if (data.success) {
// Statistiken im DOM aktualisieren
updateDashboardStats(data.stats);
// Benachrichtigung anzeigen
showToast('✅ Dashboard erfolgreich aktualisiert', 'success');
// Seite neu laden für vollständige Aktualisierung
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
showToast('❌ Fehler beim Aktualisieren des Dashboards', 'error');
}
} catch (error) {
console.error('Dashboard-Refresh Fehler:', error);
showToast('❌ Netzwerkfehler beim Dashboard-Refresh', 'error');
} finally {
// Button-State zurücksetzen
if (refreshButton) {
refreshButton.disabled = false;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.remove('animate-spin');
}
}
}
};
/**
* Jobs-Refresh-Funktion
*/
window.refreshJobs = async function() {
const refreshButton = document.getElementById('refresh-button');
if (refreshButton) {
refreshButton.disabled = true;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.add('animate-spin');
}
}
try {
// Jobs-Daten neu laden
if (typeof jobManager !== 'undefined' && jobManager.loadJobs) {
await jobManager.loadJobs();
} else {
// Fallback: Seite neu laden
window.location.reload();
}
showToast('✅ Druckaufträge erfolgreich aktualisiert', 'success');
} catch (error) {
console.error('Jobs-Refresh Fehler:', error);
showToast('❌ Fehler beim Aktualisieren der Jobs', 'error');
} finally {
if (refreshButton) {
refreshButton.disabled = false;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.remove('animate-spin');
}
}
}
};
/**
* Calendar-Refresh-Funktion
*/
window.refreshCalendar = async function() {
const refreshButton = document.getElementById('refresh-button');
if (refreshButton) {
refreshButton.disabled = true;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.add('animate-spin');
}
}
try {
// FullCalendar neu laden falls verfügbar
if (typeof calendar !== 'undefined' && calendar.refetchEvents) {
calendar.refetchEvents();
showToast('✅ Kalender erfolgreich aktualisiert', 'success');
} else {
// Fallback: Seite neu laden
window.location.reload();
}
} catch (error) {
console.error('Calendar-Refresh Fehler:', error);
showToast('❌ Fehler beim Aktualisieren des Kalenders', 'error');
} finally {
if (refreshButton) {
refreshButton.disabled = false;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.remove('animate-spin');
}
}
}
};
/**
* Drucker-Refresh-Funktion
*/
window.refreshPrinters = async function() {
const refreshButton = document.getElementById('refresh-button');
if (refreshButton) {
refreshButton.disabled = true;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.add('animate-spin');
}
}
try {
// Drucker-Manager verwenden falls verfügbar
if (typeof printerManager !== 'undefined' && printerManager.loadPrinters) {
await printerManager.loadPrinters();
} else {
// Fallback: API-Aufruf für Drucker-Update
const response = await fetch('/api/printers/status/live', {
headers: {
'X-CSRFToken': getCSRFToken()
}
});
if (response.ok) {
// Seite neu laden für vollständige Aktualisierung
window.location.reload();
} else {
throw new Error('Drucker-Status konnte nicht abgerufen werden');
}
}
showToast('✅ Drucker erfolgreich aktualisiert', 'success');
} catch (error) {
console.error('Printer-Refresh Fehler:', error);
showToast('❌ Fehler beim Aktualisieren der Drucker', 'error');
} finally {
if (refreshButton) {
refreshButton.disabled = false;
const icon = refreshButton.querySelector('svg');
if (icon) {
icon.classList.remove('animate-spin');
}
}
}
};
/**
* Dashboard-Statistiken im DOM aktualisieren
*/
function updateDashboardStats(stats) {
// Aktive Jobs
const activeJobsEl = document.querySelector('[data-stat="active-jobs"]');
if (activeJobsEl) {
activeJobsEl.textContent = stats.active_jobs || 0;
}
// Verfügbare Drucker
const availablePrintersEl = document.querySelector('[data-stat="available-printers"]');
if (availablePrintersEl) {
availablePrintersEl.textContent = stats.available_printers || 0;
}
// Gesamte Jobs
const totalJobsEl = document.querySelector('[data-stat="total-jobs"]');
if (totalJobsEl) {
totalJobsEl.textContent = stats.total_jobs || 0;
}
// Erfolgsrate
const successRateEl = document.querySelector('[data-stat="success-rate"]');
if (successRateEl) {
successRateEl.textContent = (stats.success_rate || 0) + '%';
}
console.log('📊 Dashboard-Statistiken aktualisiert:', stats);
}
/**
* CSRF-Token abrufen
*/
function getCSRFToken() {
const token = document.querySelector('meta[name="csrf-token"]');
return token ? token.getAttribute('content') : '';
}
/**
* Toast-Benachrichtigung anzeigen
*/
function showToast(message, type = 'info') {
// Prüfen ob der OptimizationManager verfügbar ist und dessen Toast-Funktion verwenden
if (typeof optimizationManager !== 'undefined' && optimizationManager.showToast) {
optimizationManager.showToast(message, type);
return;
}
// Fallback: Einfache Console-Ausgabe
const emoji = {
success: '✅',
error: '❌',
warning: '⚠️',
info: ''
};
console.log(`${emoji[type] || ''} ${message}`);
// Versuche eine Alert-Benachrichtigung zu erstellen
try {
const toast = document.createElement('div');
toast.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg transition-all duration-300 transform translate-x-full`;
const colors = {
success: 'bg-green-500 text-white',
error: 'bg-red-500 text-white',
warning: 'bg-yellow-500 text-black',
info: 'bg-blue-500 text-white'
};
toast.className += ` ${colors[type]}`;
toast.textContent = message;
document.body.appendChild(toast);
// Animation einblenden
setTimeout(() => {
toast.classList.remove('translate-x-full');
}, 100);
// Nach 3 Sekunden automatisch entfernen
setTimeout(() => {
toast.classList.add('translate-x-full');
setTimeout(() => {
toast.remove();
}, 300);
}, 3000);
} catch (error) {
console.warn('Toast-Erstellung fehlgeschlagen:', error);
}
}
/**
* Universelle Refresh-Funktion basierend auf aktueller Seite
*/
window.universalRefresh = function() {
const currentPath = window.location.pathname;
if (currentPath.includes('/dashboard')) {
refreshDashboard();
} else if (currentPath.includes('/jobs')) {
refreshJobs();
} else if (currentPath.includes('/calendar') || currentPath.includes('/schichtplan')) {
refreshCalendar();
} else if (currentPath.includes('/printers') || currentPath.includes('/drucker')) {
refreshPrinters();
} else {
// Fallback: Seite neu laden
window.location.reload();
}
};
/**
* Auto-Refresh-Funktionalität
*/
class AutoRefreshManager {
constructor() {
this.isEnabled = false;
this.interval = null;
this.intervalTime = 30000; // 30 Sekunden
}
start() {
if (this.isEnabled) return;
this.isEnabled = true;
this.interval = setInterval(() => {
// Nur refresh wenn Seite sichtbar ist
if (!document.hidden) {
universalRefresh();
}
}, this.intervalTime);
console.log('🔄 Auto-Refresh aktiviert (alle 30 Sekunden)');
}
stop() {
if (!this.isEnabled) return;
this.isEnabled = false;
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
console.log('⏸️ Auto-Refresh deaktiviert');
}
toggle() {
if (this.isEnabled) {
this.stop();
} else {
this.start();
}
}
}
// Globaler Auto-Refresh-Manager
window.autoRefreshManager = new AutoRefreshManager();
/**
* Keyboard-Shortcuts
*/
document.addEventListener('keydown', function(e) {
// F5 oder Ctrl+R abfangen und eigene Refresh-Funktion verwenden
if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {
e.preventDefault();
universalRefresh();
}
// Ctrl+Shift+R für Auto-Refresh-Toggle
if (e.ctrlKey && e.shiftKey && e.key === 'R') {
e.preventDefault();
autoRefreshManager.toggle();
showToast(
autoRefreshManager.isEnabled ?
'🔄 Auto-Refresh aktiviert' :
'⏸️ Auto-Refresh deaktiviert',
'info'
);
}
});
/**
* Visibility API für Auto-Refresh bei Tab-Wechsel
*/
document.addEventListener('visibilitychange', function() {
if (!document.hidden && autoRefreshManager.isEnabled) {
// Verzögertes Refresh wenn Tab wieder sichtbar wird
setTimeout(universalRefresh, 1000);
}
});
console.log('🔄 Globale Refresh-Funktionen geladen');

View File

@ -33,6 +33,7 @@
<!-- Preload critical resources -->
<link rel="preload" href="{{ url_for('static', filename='js/ui-components.js') }}" as="script">
<link rel="preload" href="{{ url_for('static', filename='js/offline-app.js') }}" as="script">
<link rel="preload" href="{{ url_for('static', filename='js/optimization-features.js') }}" as="script">
<!-- Additional CSS -->
{% block extra_css %}{% endblock %}
@ -577,6 +578,8 @@
<!-- JavaScript -->
<script src="{{ url_for('static', filename='js/ui-components.js') }}"></script>
<script src="{{ url_for('static', filename='js/dark-mode-fix.js') }}"></script>
<script src="{{ url_for('static', filename='js/optimization-features.js') }}"></script>
<script src="{{ url_for('static', filename='js/global-refresh-functions.js') }}"></script>
{% if current_user.is_authenticated %}
<script src="{{ url_for('static', filename='js/notifications.js') }}"></script>
{% endif %}

View File

@ -325,11 +325,27 @@
<span>Exportieren</span>
</button>
<button onclick="toggleAutoOptimization()" id="auto-opt-toggle"
class="btn-secondary flex items-center gap-2">
class="btn-secondary flex items-center gap-2 relative group">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<span>Auto-Optimierung</span>
<!-- Info-Tooltip für Auto-Optimierung -->
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 w-80 bg-slate-900 dark:bg-slate-700 text-white text-xs rounded-lg p-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-50 shadow-lg">
<div class="font-semibold mb-2">🚀 Intelligente Auto-Optimierung</div>
<ul class="space-y-1 text-left">
<li><strong>Round Robin:</strong> Gleichmäßige Druckerverteilung</li>
<li><strong>Load Balancing:</strong> Auslastungsoptimierung</li>
<li><strong>Prioritätsbasiert:</strong> Wichtige Jobs zuerst</li>
<li><strong>Rüstzeiten:</strong> Minimierung der Umrüstzeiten</li>
<li><strong>Entfernungen:</strong> Berücksichtigung der Druckerstandorte</li>
</ul>
<div class="mt-2 text-xs text-slate-300">
Tastenkürzel: <kbd class="bg-slate-800 px-1 rounded">Ctrl+Alt+O</kbd>
</div>
<div class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900 dark:border-t-slate-700"></div>
</div>
</button>
</div>
</div>

View File

@ -397,11 +397,27 @@
<span>Aktualisieren</span>
</button>
<button onclick="toggleBatchMode()" id="batch-toggle"
class="btn-secondary flex items-center gap-2">
class="btn-secondary flex items-center gap-2 relative group">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Mehrfachauswahl</span>
<!-- Info-Tooltip für Batch-Planung -->
<div class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 w-80 bg-slate-900 dark:bg-slate-700 text-white text-xs rounded-lg p-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-50 shadow-lg">
<div class="font-semibold mb-2">📦 Intelligente Batch-Planung</div>
<ul class="space-y-1 text-left">
<li><strong>Mehrfachauswahl:</strong> Markieren Sie mehrere Jobs gleichzeitig</li>
<li><strong>Batch-Operationen:</strong> Starten, pausieren oder abbrechen</li>
<li><strong>Prioritäten setzen:</strong> Hoch/Normal für ausgewählte Jobs</li>
<li><strong>Gruppenlöschung:</strong> Mehrere abgeschlossene Jobs löschen</li>
<li><strong>Optimierte Planung:</strong> Intelligente Reihenfolge-Anpassung</li>
</ul>
<div class="mt-2 text-xs text-slate-300">
Tastenkürzel: <kbd class="bg-slate-800 px-1 rounded">Ctrl+Alt+B</kbd>
</div>
<div class="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-slate-900 dark:border-t-slate-700"></div>
</div>
</button>
<a href="{{ url_for('new_job_page') }}"
class="btn-primary flex items-center gap-2">