"Refactor global refresh functions and templates for better performance (feat)"
This commit is contained in:
parent
e338c5835e
commit
a473c06658
@ -5102,4 +5102,548 @@ def refresh_dashboard():
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': 'Fehler beim Aktualisieren der Dashboard-Daten'
|
||||
}), 500
|
||||
}), 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 =====
|
376
backend/app/static/js/global-refresh-functions.js
Normal file
376
backend/app/static/js/global-refresh-functions.js
Normal 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');
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user