From a473c0665890acbfbd2d1cd8ecf0c3bf5447b04d Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Thu, 29 May 2025 17:51:01 +0200 Subject: [PATCH] "Refactor global refresh functions and templates for better performance (feat)" --- backend/app/app.py | 546 +++++++++++++++++- .../app/static/js/global-refresh-functions.js | 376 ++++++++++++ backend/app/templates/base.html | 3 + backend/app/templates/calendar.html | 18 +- backend/app/templates/jobs.html | 18 +- 5 files changed, 958 insertions(+), 3 deletions(-) create mode 100644 backend/app/static/js/global-refresh-functions.js diff --git a/backend/app/app.py b/backend/app/app.py index 5c5cc77a..1a26ac7b 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -5102,4 +5102,548 @@ def refresh_dashboard(): return jsonify({ 'success': False, 'error': 'Fehler beim Aktualisieren der Dashboard-Daten' - }), 500 \ No newline at end of file + }), 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//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//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/', 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/', 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 ===== \ No newline at end of file diff --git a/backend/app/static/js/global-refresh-functions.js b/backend/app/static/js/global-refresh-functions.js new file mode 100644 index 00000000..a2aec288 --- /dev/null +++ b/backend/app/static/js/global-refresh-functions.js @@ -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'); \ No newline at end of file diff --git a/backend/app/templates/base.html b/backend/app/templates/base.html index a405ac20..d02ffc5c 100644 --- a/backend/app/templates/base.html +++ b/backend/app/templates/base.html @@ -33,6 +33,7 @@ + {% block extra_css %}{% endblock %} @@ -577,6 +578,8 @@ + + {% if current_user.is_authenticated %} {% endif %} diff --git a/backend/app/templates/calendar.html b/backend/app/templates/calendar.html index 77a5e304..9efcc8aa 100644 --- a/backend/app/templates/calendar.html +++ b/backend/app/templates/calendar.html @@ -325,11 +325,27 @@ Exportieren diff --git a/backend/app/templates/jobs.html b/backend/app/templates/jobs.html index 741e46d8..3d792396 100644 --- a/backend/app/templates/jobs.html +++ b/backend/app/templates/jobs.html @@ -397,11 +397,27 @@ Aktualisieren