From 0b5a1f874daec34719d98199e5034f2a8c9267c9 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Thu, 29 May 2025 19:11:56 +0200 Subject: [PATCH] "Refactor calendar blueprint and test printer creation" --- backend/app/blueprints/calendar.py | 284 +++++++++++++++++++++++++++- backend/app/create_test_printers.py | 49 +++-- backend/app/templates/calendar.html | 145 ++++++++------ 3 files changed, 394 insertions(+), 84 deletions(-) diff --git a/backend/app/blueprints/calendar.py b/backend/app/blueprints/calendar.py index e82b2559..4d195e88 100644 --- a/backend/app/blueprints/calendar.py +++ b/backend/app/blueprints/calendar.py @@ -2,7 +2,7 @@ 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_ +from sqlalchemy import and_, or_, func from models import Job, Printer, User, UserPermission, get_cached_session from utils.logging_config import get_logger @@ -21,6 +21,125 @@ def can_edit_events(user): 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(): @@ -137,10 +256,11 @@ def api_create_calendar_event(): title = data.get('title') start = data.get('start') end = data.get('end') - printer_id = data.get('printerId') + printer_id = data.get('printerId') # Jetzt optional + priority = data.get('priority', 'normal') - if not all([title, start, end, printer_id]): - return jsonify({"error": "Titel, Start, Ende und Drucker sind erforderlich"}), 400 + if not all([title, start, end]): + return jsonify({"error": "Titel, Start und Ende sind erforderlich"}), 400 # Datumsfelder konvertieren try: @@ -153,11 +273,48 @@ def api_create_calendar_event(): duration_minutes = int((end_date - start_date).total_seconds() / 60) with get_cached_session() as db_session: - # Drucker prüfen + # 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, @@ -174,7 +331,9 @@ def api_create_calendar_event(): db_session.add(job) db_session.commit() - logger.info(f"Neuer Kalendereintrag erstellt: ID {job.id}, Name: {title}") + 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, @@ -182,7 +341,14 @@ def api_create_calendar_event(): "title": job.name, "start": job.start_at.isoformat(), "end": job.end_at.isoformat(), - "status": job.status + "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: @@ -277,4 +443,108 @@ def api_delete_calendar_event(event_id): 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 \ No newline at end of file diff --git a/backend/app/create_test_printers.py b/backend/app/create_test_printers.py index e212e6f7..75964560 100644 --- a/backend/app/create_test_printers.py +++ b/backend/app/create_test_printers.py @@ -19,47 +19,54 @@ def create_test_printers(): # Test-Drucker Daten test_printers = [ { - 'name': 'Ultimaker S3 #01', - 'model': 'Ultimaker S3', - 'location': 'Produktionshalle A', - 'plug_ip': '192.168.1.100', + 'name': 'Mercedes-Benz FDM Pro #01', + 'model': 'Ultimaker S5 Pro', + 'location': 'Werkhalle Sindelfingen', + 'plug_ip': '192.168.10.101', 'status': 'available', 'active': True }, { - 'name': 'Prusa MK3S+ #02', + 'name': 'Mercedes-Benz FDM #02', 'model': 'Prusa MK3S+', - 'location': 'Produktionshalle B', - 'plug_ip': '192.168.1.101', - 'status': 'offline', + 'location': 'Entwicklungszentrum Stuttgart', + 'plug_ip': '192.168.10.102', + 'status': 'printing', 'active': True }, { - 'name': 'Bambu Lab X1 #03', - 'model': 'Bambu Lab X1 Carbon', - 'location': 'Labor R&D', - 'plug_ip': '192.168.1.102', + 'name': 'Mercedes-Benz SLA #01', + 'model': 'Formlabs Form 3+', + 'location': 'Prototypenlabor', + 'plug_ip': '192.168.10.103', 'status': 'available', 'active': True }, { - 'name': 'Formlabs Form 3 #04', - 'model': 'Formlabs Form 3', - 'location': 'Prototyping Lab', - 'plug_ip': '192.168.1.103', + 'name': 'Mercedes-Benz Industrial #01', + 'model': 'Stratasys F370', + 'location': 'Industriehalle Bremen', + 'plug_ip': '192.168.10.104', 'status': 'maintenance', 'active': False }, { - 'name': 'Ender 3 V2 #05', - 'model': 'Creality Ender 3 V2', - 'location': 'Testbereich', - 'plug_ip': '192.168.1.104', + 'name': 'Mercedes-Benz Rapid #01', + 'model': 'Bambu Lab X1 Carbon', + 'location': 'Designabteilung', + 'plug_ip': '192.168.10.105', 'status': 'offline', 'active': True + }, + { + 'name': 'Mercedes-Benz SLS #01', + 'model': 'HP Jet Fusion 5200', + 'location': 'Produktionszentrum Berlin', + 'plug_ip': '192.168.10.106', + 'status': 'available', + 'active': True } ] - try: created_count = 0 for printer_data in test_printers: diff --git a/backend/app/templates/calendar.html b/backend/app/templates/calendar.html index 8a61aa9a..adbf61ad 100644 --- a/backend/app/templates/calendar.html +++ b/backend/app/templates/calendar.html @@ -1065,14 +1065,42 @@ document.addEventListener('DOMContentLoaded', function() { const priority = document.getElementById('eventPriority').value; if (printerSelect.value === '' && startTime && endTime) { - // Dynamische Empfehlung anzeigen - showSmartRecommendation(startTime, endTime, priority); + // Echte API-Empfehlung abrufen + fetchSmartRecommendation(startTime, endTime, priority); } else { hideSmartRecommendation(); } } - function showSmartRecommendation(start, end, priority) { + async function fetchSmartRecommendation(start, end, priority) { + try { + const response = await fetch('/api/calendar/smart-recommendation', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content') + }, + body: JSON.stringify({ + start: start, + end: end, + priority: priority + }) + }); + + const data = await response.json(); + + if (data.success && data.recommendation) { + showSmartRecommendation(data.recommendation); + } else { + showNoRecommendationMessage(data.message || 'Keine Empfehlung verfügbar'); + } + } catch (error) { + console.error('Fehler beim Abrufen der Empfehlung:', error); + showNoRecommendationMessage('Fehler beim Abrufen der Empfehlung'); + } + } + + function showSmartRecommendation(recommendation) { let existingRecommendation = document.getElementById('smart-recommendation'); if (existingRecommendation) { existingRecommendation.remove(); @@ -1083,32 +1111,46 @@ document.addEventListener('DOMContentLoaded', function() { recommendationDiv.id = 'smart-recommendation'; recommendationDiv.className = 'mt-3 p-4 bg-gradient-to-r from-green-50 to-blue-50 dark:from-green-900/20 dark:to-blue-900/20 rounded-lg border border-green-200 dark:border-green-800 transition-all duration-300'; - // Simuliere intelligente Empfehlung basierend auf Zeit und Priorität - const recommendations = getSmartRecommendation(start, end, priority); + // Optimierungsindikator + const optimizedBadge = recommendation.priority_optimized + ? '✨ Priorität optimiert' + : ''; recommendationDiv.innerHTML = `
-
+
-

🎯 Intelligente Empfehlung

+

+ 🎯 Intelligente Empfehlung${optimizedBadge} +

Empfohlener Drucker: - - 🖨️ ${recommendations.printer} + + 🖨️ ${recommendation.printer_name} + ${recommendation.location ? `(📍 ${recommendation.location})` : ''}
- 💡 ${recommendations.reason} + 💡 ${recommendation.reason}
- ⚡ Verfügbarkeit: ${recommendations.availability} - 📊 Auslastung: ${recommendations.utilization} - 🎯 Eignung: ${recommendations.suitability} + + + Verfügbarkeit: ${recommendation.availability} + + + + Auslastung: ${recommendation.utilization} + + + + Eignung: ${recommendation.suitability} +
@@ -1120,10 +1162,43 @@ document.addEventListener('DOMContentLoaded', function() { // Animation setTimeout(() => { recommendationDiv.classList.add('animate-pulse'); - setTimeout(() => recommendationDiv.classList.remove('animate-pulse'), 1000); + setTimeout(() => recommendationDiv.classList.remove('animate-pulse'), 1500); }, 100); } + function showNoRecommendationMessage(message) { + let existingRecommendation = document.getElementById('smart-recommendation'); + if (existingRecommendation) { + existingRecommendation.remove(); + } + + const printerContainer = document.getElementById('eventPrinter').parentElement; + const recommendationDiv = document.createElement('div'); + recommendationDiv.id = 'smart-recommendation'; + recommendationDiv.className = 'mt-3 p-4 bg-gradient-to-r from-yellow-50 to-orange-50 dark:from-yellow-900/20 dark:to-orange-900/20 rounded-lg border border-yellow-200 dark:border-yellow-800 transition-all duration-300'; + + recommendationDiv.innerHTML = ` +
+
+ + + +
+
+

⚠️ Keine automatische Zuweisung möglich

+
+ ${message} +
+ Bitte wählen Sie einen spezifischen Drucker aus der Liste oder ändern Sie den Zeitraum. +
+
+
+
+ `; + + printerContainer.appendChild(recommendationDiv); + } + function hideSmartRecommendation() { const existingRecommendation = document.getElementById('smart-recommendation'); if (existingRecommendation) { @@ -1133,48 +1208,6 @@ document.addEventListener('DOMContentLoaded', function() { } } - function getSmartRecommendation(start, end, priority) { - // Simuliere intelligente Logik basierend auf Zeit und Priorität - const startHour = new Date(start).getHours(); - const duration = (new Date(end) - new Date(start)) / (1000 * 60 * 60); // Stunden - - let recommendations = { - printer: "MYP-Drucker-01 (Halle A)", - reason: "Optimale Verfügbarkeit und geringe Auslastung im gewählten Zeitraum", - availability: "98%", - utilization: "24%", - suitability: "Ausgezeichnet" - }; - - if (priority === 'urgent') { - recommendations = { - printer: "MYP-Express-Drucker (Halle B)", - reason: "Schnellster verfügbarer Drucker für dringende Aufträge", - availability: "100%", - utilization: "15%", - suitability: "Perfekt" - }; - } else if (startHour >= 18 || startHour <= 6) { - recommendations = { - printer: "MYP-Nacht-Drucker (Halle C)", - reason: "Speziell für Nachtschichten optimiert", - availability: "95%", - utilization: "12%", - suitability: "Optimal" - }; - } else if (duration > 8) { - recommendations = { - printer: "MYP-Langzeit-Drucker (Halle A)", - reason: "Zuverlässig für lange Druckaufträge", - availability: "90%", - utilization: "35%", - suitability: "Sehr gut" - }; - } - - return recommendations; - } - // Event Listeners für dynamische Empfehlung document.getElementById('eventStart').addEventListener('change', updatePrinterRecommendation); document.getElementById('eventEnd').addEventListener('change', updatePrinterRecommendation);