import json from datetime import datetime, timedelta from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort from flask_login import current_user, login_required from sqlalchemy import and_, or_, func from models import Job, Printer, User, UserPermission, get_cached_session from utils.logging_config import get_logger calendar_blueprint = Blueprint('calendar', __name__) logger = get_logger("calendar") def can_edit_events(user): """Prüft, ob ein Benutzer Kalendereinträge bearbeiten darf.""" if user.is_admin: return True with get_cached_session() as db_session: permission = db_session.query(UserPermission).filter_by(user_id=user.id).first() if not permission: return False return permission.can_approve_jobs def get_smart_printer_assignment(start_date, end_date, priority="normal", db_session=None): """ Intelligente Druckerzuweisung basierend auf verschiedenen Faktoren. Args: start_date: Startzeit des Jobs end_date: Endzeit des Jobs priority: Prioritätsstufe ('urgent', 'high', 'normal', 'low') db_session: Datenbankverbindung Returns: printer_id: ID des empfohlenen Druckers oder None """ if not db_session: return None try: # Verfügbare Drucker ermitteln available_printers = db_session.query(Printer).filter( Printer.active == True ).all() if not available_printers: logger.warning("Keine aktiven Drucker für automatische Zuweisung gefunden") return None printer_scores = [] for printer in available_printers: score = 0 # 1. Verfügbarkeit prüfen - Jobs im gleichen Zeitraum conflicting_jobs = db_session.query(Job).filter( Job.printer_id == printer.id, Job.status.in_(["scheduled", "running"]), or_( and_(Job.start_at >= start_date, Job.start_at < end_date), and_(Job.end_at > start_date, Job.end_at <= end_date), and_(Job.start_at <= start_date, Job.end_at >= end_date) ) ).count() if conflicting_jobs > 0: continue # Drucker ist nicht verfügbar score += 100 # Grundpunkte für Verfügbarkeit # 2. Auslastung in den letzten 24 Stunden bewerten last_24h = datetime.now() - timedelta(hours=24) recent_jobs = db_session.query(Job).filter( Job.printer_id == printer.id, Job.start_at >= last_24h, Job.status.in_(["scheduled", "running", "finished"]) ).count() # Weniger Auslastung = höhere Punktzahl score += max(0, 50 - (recent_jobs * 10)) # 3. Prioritätsbasierte Zuweisung if priority == "urgent": # Für dringende Jobs: Express-Drucker bevorzugen if "express" in printer.name.lower() or "schnell" in printer.name.lower(): score += 30 elif priority == "high": # Für hohe Priorität: Weniger belastete Drucker if recent_jobs <= 2: score += 20 # 4. Zeitfenster-basierte Zuweisung start_hour = start_date.hour if start_hour >= 18 or start_hour <= 6: # Nachtschicht if "nacht" in printer.name.lower() or printer.location and "c" in printer.location.lower(): score += 25 elif start_hour >= 8 and start_hour <= 17: # Tagschicht if "tag" in printer.name.lower() or printer.location and "a" in printer.location.lower(): score += 15 # 5. Standort-basierte Bewertung (Round-Robin ähnlich) if printer.location: location_penalty = hash(printer.location) % 10 # Verteilung basierend auf Standort score += location_penalty # 6. Druckerdauer-Eignung job_duration_hours = (end_date - start_date).total_seconds() / 3600 if job_duration_hours > 8: # Lange Jobs if "langzeit" in printer.name.lower() or "marathon" in printer.name.lower(): score += 20 elif job_duration_hours <= 2: # Kurze Jobs if "express" in printer.name.lower() or "schnell" in printer.name.lower(): score += 15 # 7. Wartungszyklen berücksichtigen # Neuere Drucker (falls last_maintenance_date verfügbar) bevorzugen # TODO: Implementierung abhängig von Printer-Model-Erweiterungen printer_scores.append({ 'printer': printer, 'score': score, 'conflicts': conflicting_jobs, 'recent_load': recent_jobs }) # Nach Punktzahl sortieren und besten Drucker auswählen if not printer_scores: logger.warning("Keine verfügbaren Drucker für den gewünschten Zeitraum gefunden") return None printer_scores.sort(key=lambda x: x['score'], reverse=True) best_printer = printer_scores[0] logger.info(f"Automatische Druckerzuweisung: {best_printer['printer'].name} " f"(Score: {best_printer['score']}, Load: {best_printer['recent_load']})") return best_printer['printer'].id except Exception as e: logger.error(f"Fehler bei automatischer Druckerzuweisung: {str(e)}") return None @calendar_blueprint.route('/calendar', methods=['GET']) @login_required def calendar_view(): """Kalender-Ansicht anzeigen.""" can_edit = can_edit_events(current_user) with get_cached_session() as db_session: printers = db_session.query(Printer).filter_by(active=True).all() return render_template('calendar.html', printers=printers, can_edit=can_edit) @calendar_blueprint.route('/api/calendar', methods=['GET']) @login_required def api_get_calendar_events(): """Kalendereinträge als JSON für FullCalendar zurückgeben.""" try: # Datumsbereich aus Anfrage start_str = request.args.get('from') end_str = request.args.get('to') if not start_str or not end_str: # Standardmäßig eine Woche anzeigen start_date = datetime.now() end_date = start_date + timedelta(days=7) else: try: start_date = datetime.fromisoformat(start_str) end_date = datetime.fromisoformat(end_str) except ValueError: return jsonify({"error": "Ungültiges Datumsformat"}), 400 # Optional: Filter nach Druckern printer_id = request.args.get('printer_id') with get_cached_session() as db_session: # Jobs im angegebenen Zeitraum abfragen query = db_session.query(Job).filter( or_( # Jobs, die im Zeitraum beginnen and_(Job.start_at >= start_date, Job.start_at <= end_date), # Jobs, die im Zeitraum enden and_(Job.end_at >= start_date, Job.end_at <= end_date), # Jobs, die den Zeitraum komplett umfassen and_(Job.start_at <= start_date, Job.end_at >= end_date) ) ) if printer_id: query = query.filter(Job.printer_id == printer_id) jobs = query.all() # Jobs in FullCalendar-Event-Format umwandeln events = [] for job in jobs: # Farbe basierend auf Status bestimmen color = "#6B7280" # Grau für pending if job.status == "running": color = "#3B82F6" # Blau für running elif job.status == "finished": color = "#10B981" # Grün für finished elif job.status == "scheduled": color = "#10B981" # Grün für approved elif job.status == "cancelled" or job.status == "failed": color = "#EF4444" # Rot für abgebrochen/fehlgeschlagen # Benutzerinformationen laden user = db_session.query(User).filter_by(id=job.user_id).first() user_name = user.name if user else "Unbekannt" # Druckerinformationen laden printer = db_session.query(Printer).filter_by(id=job.printer_id).first() printer_name = printer.name if printer else "Unbekannt" event = { "id": job.id, "title": job.name, "start": job.start_at.isoformat(), "end": job.end_at.isoformat(), "color": color, "extendedProps": { "status": job.status, "description": job.description, "userName": user_name, "printerId": job.printer_id, "printerName": printer_name } } events.append(event) return jsonify(events) except Exception as e: logger.error(f"Fehler beim Abrufen der Kalendereinträge: {str(e)}") return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500 @calendar_blueprint.route('/api/calendar/event', methods=['POST']) @login_required def api_create_calendar_event(): """Neuen Kalendereintrag (Job) erstellen.""" # Nur Admins und Benutzer mit can_approve_jobs dürfen Einträge erstellen if not can_edit_events(current_user): return jsonify({"error": "Keine Berechtigung zum Erstellen von Kalendereinträgen"}), 403 try: data = request.get_json() if not data: return jsonify({"error": "Keine Daten erhalten"}), 400 # Pflichtfelder prüfen title = data.get('title') start = data.get('start') end = data.get('end') printer_id = data.get('printerId') # Jetzt optional priority = data.get('priority', 'normal') if not all([title, start, end]): return jsonify({"error": "Titel, Start und Ende sind erforderlich"}), 400 # Datumsfelder konvertieren try: start_date = datetime.fromisoformat(start) end_date = datetime.fromisoformat(end) except ValueError: return jsonify({"error": "Ungültiges Datumsformat"}), 400 # Dauer in Minuten berechnen duration_minutes = int((end_date - start_date).total_seconds() / 60) with get_cached_session() as db_session: # Intelligente Druckerzuweisung falls kein Drucker angegeben if not printer_id: logger.info(f"Automatische Druckerzuweisung wird verwendet für Job '{title}'") printer_id = get_smart_printer_assignment( start_date=start_date, end_date=end_date, priority=priority, db_session=db_session ) if not printer_id: return jsonify({ "error": "Keine verfügbaren Drucker für den gewünschten Zeitraum gefunden. " "Bitte wählen Sie einen spezifischen Drucker oder einen anderen Zeitraum." }), 409 # Drucker prüfen/validieren printer = db_session.query(Printer).filter_by(id=printer_id).first() if not printer: return jsonify({"error": "Drucker nicht gefunden"}), 404 if not printer.active: return jsonify({"error": f"Drucker '{printer.name}' ist nicht aktiv"}), 400 # Nochmals Verfügbarkeit prüfen bei automatischer Zuweisung if not data.get('printerId'): # War automatische Zuweisung conflicting_jobs = db_session.query(Job).filter( Job.printer_id == printer_id, Job.status.in_(["scheduled", "running"]), or_( and_(Job.start_at >= start_date, Job.start_at < end_date), and_(Job.end_at > start_date, Job.end_at <= end_date), and_(Job.start_at <= start_date, Job.end_at >= end_date) ) ).first() if conflicting_jobs: return jsonify({ "error": f"Automatisch zugewiesener Drucker '{printer.name}' ist nicht mehr verfügbar. " "Bitte wählen Sie einen anderen Zeitraum oder spezifischen Drucker." }), 409 # Neuen Job erstellen job = Job( name=title, description=data.get('description', ''), user_id=current_user.id, printer_id=printer_id, start_at=start_date, end_at=end_date, status="scheduled", duration_minutes=duration_minutes, owner_id=current_user.id ) db_session.add(job) db_session.commit() assignment_type = "automatisch" if not data.get('printerId') else "manuell" logger.info(f"Neuer Kalendereintrag erstellt: ID {job.id}, Name: {title}, " f"Drucker: {printer.name} ({assignment_type} zugewiesen)") return jsonify({ "success": True, "id": job.id, "title": job.name, "start": job.start_at.isoformat(), "end": job.end_at.isoformat(), "status": job.status, "printer": { "id": printer.id, "name": printer.name, "location": printer.location, "assignment_type": assignment_type }, "message": f"Auftrag erfolgreich erstellt und {assignment_type} dem Drucker '{printer.name}' zugewiesen." }) except Exception as e: logger.error(f"Fehler beim Erstellen des Kalendereintrags: {str(e)}") return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500 @calendar_blueprint.route('/api/calendar/event/', methods=['PUT']) @login_required def api_update_calendar_event(event_id): """Kalendereintrag (Job) aktualisieren.""" # Nur Admins und Benutzer mit can_approve_jobs dürfen Einträge bearbeiten if not can_edit_events(current_user): return jsonify({"error": "Keine Berechtigung zum Bearbeiten von Kalendereinträgen"}), 403 try: data = request.get_json() if not data: return jsonify({"error": "Keine Daten erhalten"}), 400 with get_cached_session() as db_session: job = db_session.query(Job).filter_by(id=event_id).first() if not job: return jsonify({"error": "Kalendereintrag nicht gefunden"}), 404 # Felder aktualisieren, die im Request enthalten sind if 'title' in data: job.name = data['title'] if 'description' in data: job.description = data['description'] if 'start' in data and 'end' in data: try: start_date = datetime.fromisoformat(data['start']) end_date = datetime.fromisoformat(data['end']) job.start_at = start_date job.end_at = end_date job.duration_minutes = int((end_date - start_date).total_seconds() / 60) except ValueError: return jsonify({"error": "Ungültiges Datumsformat"}), 400 if 'printerId' in data: printer = db_session.query(Printer).filter_by(id=data['printerId']).first() if not printer: return jsonify({"error": "Drucker nicht gefunden"}), 404 job.printer_id = data['printerId'] if 'status' in data: # Status nur ändern, wenn er gültig ist valid_statuses = ["scheduled", "running", "finished", "cancelled"] if data['status'] in valid_statuses: job.status = data['status'] db_session.commit() logger.info(f"Kalendereintrag aktualisiert: ID {job.id}") return jsonify({ "success": True, "id": job.id, "title": job.name, "start": job.start_at.isoformat(), "end": job.end_at.isoformat(), "status": job.status }) except Exception as e: logger.error(f"Fehler beim Aktualisieren des Kalendereintrags: {str(e)}") return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500 @calendar_blueprint.route('/api/calendar/event/', methods=['DELETE']) @login_required def api_delete_calendar_event(event_id): """Kalendereintrag (Job) löschen.""" # Nur Admins und Benutzer mit can_approve_jobs dürfen Einträge löschen if not can_edit_events(current_user): return jsonify({"error": "Keine Berechtigung zum Löschen von Kalendereinträgen"}), 403 try: with get_cached_session() as db_session: job = db_session.query(Job).filter_by(id=event_id).first() if not job: return jsonify({"error": "Kalendereintrag nicht gefunden"}), 404 db_session.delete(job) db_session.commit() logger.info(f"Kalendereintrag gelöscht: ID {event_id}") return jsonify({"success": True}) except Exception as e: logger.error(f"Fehler beim Löschen des Kalendereintrags: {str(e)}") return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500 @calendar_blueprint.route('/api/calendar/smart-recommendation', methods=['POST']) @login_required def api_get_smart_recommendation(): """Intelligente Druckerempfehlung für gegebenes Zeitfenster abrufen.""" try: data = request.get_json() if not data: return jsonify({"error": "Keine Daten erhalten"}), 400 start = data.get('start') end = data.get('end') priority = data.get('priority', 'normal') if not all([start, end]): return jsonify({"error": "Start und Ende sind erforderlich"}), 400 # Datumsfelder konvertieren try: start_date = datetime.fromisoformat(start) end_date = datetime.fromisoformat(end) except ValueError: return jsonify({"error": "Ungültiges Datumsformat"}), 400 with get_cached_session() as db_session: # Empfohlenen Drucker ermitteln recommended_printer_id = get_smart_printer_assignment( start_date=start_date, end_date=end_date, priority=priority, db_session=db_session ) if not recommended_printer_id: return jsonify({ "success": False, "message": "Keine verfügbaren Drucker für den gewünschten Zeitraum gefunden." }) # Drucker-Details abrufen printer = db_session.query(Printer).filter_by(id=recommended_printer_id).first() if not printer: return jsonify({ "success": False, "message": "Empfohlener Drucker nicht mehr verfügbar." }) # Zusätzliche Statistiken für die Empfehlung berechnen last_24h = datetime.now() - timedelta(hours=24) recent_jobs_count = db_session.query(Job).filter( Job.printer_id == printer.id, Job.start_at >= last_24h, Job.status.in_(["scheduled", "running", "finished"]) ).count() # Verfügbarkeit als Prozentsatz berechnen total_time_slots = 24 # Stunden pro Tag availability_percent = max(0, 100 - (recent_jobs_count * 4)) # Grobe Schätzung # Auslastung berechnen utilization_percent = min(100, recent_jobs_count * 8) # Grobe Schätzung # Eignung basierend auf Priorität und Zeitfenster bestimmen suitability = "Gut" if priority == "urgent" and ("express" in printer.name.lower() or "schnell" in printer.name.lower()): suitability = "Perfekt" elif priority == "high" and recent_jobs_count <= 2: suitability = "Ausgezeichnet" elif recent_jobs_count == 0: suitability = "Optimal" # Begründung generieren reason = f"Optimale Verfügbarkeit und geringe Auslastung im gewählten Zeitraum" job_duration_hours = (end_date - start_date).total_seconds() / 3600 start_hour = start_date.hour if priority == "urgent": reason = "Schnellster verfügbarer Drucker für dringende Aufträge" elif start_hour >= 18 or start_hour <= 6: reason = "Speziell für Nachtschichten optimiert" elif job_duration_hours > 8: reason = "Zuverlässig für lange Druckaufträge" elif job_duration_hours <= 2: reason = "Optimal für schnelle Druckaufträge" return jsonify({ "success": True, "recommendation": { "printer_id": printer.id, "printer_name": f"{printer.name}", "location": printer.location or "Haupthalle", "reason": reason, "availability": f"{availability_percent}%", "utilization": f"{utilization_percent}%", "suitability": suitability, "recent_jobs": recent_jobs_count, "priority_optimized": priority in ["urgent", "high"] and suitability in ["Perfekt", "Ausgezeichnet"] } }) except Exception as e: logger.error(f"Fehler beim Abrufen der intelligenten Empfehlung: {str(e)}") return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500