🎉 Improved backend configuration and documentation 🖥️📚
This commit is contained in:
@@ -6,6 +6,7 @@ from sqlalchemy import and_, or_, func
|
||||
|
||||
from models import Job, Printer, User, UserPermission, get_cached_session
|
||||
from utils.logging_config import get_logger
|
||||
from utils.conflict_manager import conflict_manager, ConflictType, ConflictSeverity
|
||||
|
||||
calendar_blueprint = Blueprint('calendar', __name__)
|
||||
logger = get_logger("calendar")
|
||||
@@ -838,4 +839,480 @@ def api_export_calendar():
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Kalender-Export: {str(e)}")
|
||||
return jsonify({"error": f"Fehler beim Export: {str(e)}"}), 500
|
||||
return jsonify({"error": f"Fehler beim Export: {str(e)}"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/check-conflicts', methods=['POST'])
|
||||
@login_required
|
||||
def api_check_conflicts():
|
||||
"""
|
||||
Prüft explizit auf Konflikte für eine geplante Reservierung
|
||||
und gibt detaillierte Konfliktinformationen zurück.
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
# Pflichtfelder prüfen
|
||||
required_fields = ['start_time', 'end_time']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return jsonify({"error": f"Feld '{field}' fehlt"}), 400
|
||||
|
||||
# Daten extrahieren
|
||||
try:
|
||||
start_time = datetime.fromisoformat(data['start_time'])
|
||||
end_time = datetime.fromisoformat(data['end_time'])
|
||||
printer_id = data.get('printer_id')
|
||||
priority = data.get('priority', 'normal')
|
||||
duration_minutes = int((end_time - start_time).total_seconds() / 60)
|
||||
except (ValueError, TypeError) as e:
|
||||
return jsonify({"error": f"Ungültige Datenformate: {str(e)}"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Job-Daten für Konfliktanalyse vorbereiten
|
||||
job_data = {
|
||||
'printer_id': printer_id,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
'priority': priority,
|
||||
'duration_minutes': duration_minutes
|
||||
}
|
||||
|
||||
# Konfliktanalyse durchführen
|
||||
conflicts = conflict_manager.detect_conflicts(job_data, db_session)
|
||||
|
||||
# Konfliktinformationen für Response aufbereiten
|
||||
conflict_info = []
|
||||
total_severity_score = 0
|
||||
|
||||
for conflict in conflicts:
|
||||
severity_scores = {
|
||||
ConflictSeverity.CRITICAL: 4,
|
||||
ConflictSeverity.HIGH: 3,
|
||||
ConflictSeverity.MEDIUM: 2,
|
||||
ConflictSeverity.LOW: 1,
|
||||
ConflictSeverity.INFO: 0
|
||||
}
|
||||
|
||||
total_severity_score += severity_scores.get(conflict.severity, 0)
|
||||
|
||||
# Konflikthafte Jobs laden für Details
|
||||
conflicting_job_details = []
|
||||
for job_id in conflict.conflicting_job_ids:
|
||||
job = db_session.query(Job).filter_by(id=job_id).first()
|
||||
if job:
|
||||
conflicting_job_details.append({
|
||||
'id': job.id,
|
||||
'name': job.name,
|
||||
'start_time': job.start_at.isoformat() if job.start_at else None,
|
||||
'end_time': job.end_at.isoformat() if job.end_at else None,
|
||||
'user_name': job.user.name if job.user else "Unbekannt"
|
||||
})
|
||||
|
||||
# Drucker-Details laden
|
||||
printer_info = None
|
||||
if conflict.affected_printer_id:
|
||||
printer = db_session.query(Printer).filter_by(id=conflict.affected_printer_id).first()
|
||||
if printer:
|
||||
printer_info = {
|
||||
'id': printer.id,
|
||||
'name': printer.name,
|
||||
'location': printer.location,
|
||||
'status': printer.status,
|
||||
'active': printer.active
|
||||
}
|
||||
|
||||
conflict_detail = {
|
||||
'type': conflict.conflict_type.value,
|
||||
'severity': conflict.severity.value,
|
||||
'description': conflict.description,
|
||||
'estimated_impact': conflict.estimated_impact,
|
||||
'auto_resolvable': conflict.auto_resolvable,
|
||||
'conflict_start': conflict.conflict_start.isoformat() if conflict.conflict_start else None,
|
||||
'conflict_end': conflict.conflict_end.isoformat() if conflict.conflict_end else None,
|
||||
'affected_printer': printer_info,
|
||||
'conflicting_jobs': conflicting_job_details,
|
||||
'suggested_solutions': conflict.suggested_solutions
|
||||
}
|
||||
|
||||
conflict_info.append(conflict_detail)
|
||||
|
||||
# Gesamtbewertung
|
||||
has_conflicts = len(conflicts) > 0
|
||||
can_proceed = all(c.auto_resolvable for c in conflicts) or len(conflicts) == 0
|
||||
|
||||
# Empfehlungen basierend auf Konflikten
|
||||
recommendations = []
|
||||
if has_conflicts:
|
||||
if can_proceed:
|
||||
recommendations.append({
|
||||
'type': 'auto_resolve',
|
||||
'message': 'Konflikte können automatisch gelöst werden',
|
||||
'action': 'Automatische Lösung anwenden'
|
||||
})
|
||||
else:
|
||||
recommendations.append({
|
||||
'type': 'manual_intervention',
|
||||
'message': 'Manuelle Anpassung erforderlich',
|
||||
'action': 'Zeitraum oder Drucker ändern'
|
||||
})
|
||||
|
||||
# Alternative Zeitfenster vorschlagen
|
||||
if printer_id:
|
||||
alternative_slots = conflict_manager._find_alternative_time_slots(job_data, db_session)
|
||||
if alternative_slots:
|
||||
slot_suggestions = []
|
||||
for start, end, confidence in alternative_slots[:3]:
|
||||
slot_suggestions.append({
|
||||
'start_time': start.isoformat(),
|
||||
'end_time': end.isoformat(),
|
||||
'confidence': confidence,
|
||||
'description': f"{start.strftime('%H:%M')} - {end.strftime('%H:%M')}"
|
||||
})
|
||||
|
||||
recommendations.append({
|
||||
'type': 'alternative_times',
|
||||
'message': 'Alternative Zeitfenster verfügbar',
|
||||
'suggestions': slot_suggestions
|
||||
})
|
||||
|
||||
# Alternative Drucker vorschlagen
|
||||
alternative_printers = conflict_manager._find_alternative_printers(job_data, db_session)
|
||||
if alternative_printers:
|
||||
printer_suggestions = []
|
||||
for printer_id_alt, confidence in alternative_printers[:3]:
|
||||
printer = db_session.query(Printer).filter_by(id=printer_id_alt).first()
|
||||
if printer:
|
||||
printer_suggestions.append({
|
||||
'printer_id': printer.id,
|
||||
'printer_name': printer.name,
|
||||
'location': printer.location,
|
||||
'confidence': confidence
|
||||
})
|
||||
|
||||
recommendations.append({
|
||||
'type': 'alternative_printers',
|
||||
'message': 'Alternative Drucker verfügbar',
|
||||
'suggestions': printer_suggestions
|
||||
})
|
||||
|
||||
logger.info(f"🔍 Konfliktprüfung abgeschlossen: {len(conflicts)} Konflikte, "
|
||||
f"Schweregrad: {total_severity_score}, Automatisch lösbar: {can_proceed}")
|
||||
|
||||
return jsonify({
|
||||
'has_conflicts': has_conflicts,
|
||||
'can_proceed': can_proceed,
|
||||
'severity_score': total_severity_score,
|
||||
'conflict_count': len(conflicts),
|
||||
'conflicts': conflict_info,
|
||||
'recommendations': recommendations,
|
||||
'summary': {
|
||||
'critical_conflicts': len([c for c in conflicts if c.severity == ConflictSeverity.CRITICAL]),
|
||||
'high_conflicts': len([c for c in conflicts if c.severity == ConflictSeverity.HIGH]),
|
||||
'medium_conflicts': len([c for c in conflicts if c.severity == ConflictSeverity.MEDIUM]),
|
||||
'low_conflicts': len([c for c in conflicts if c.severity == ConflictSeverity.LOW])
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Fehler bei Konfliktprüfung: {str(e)}", exc_info=True)
|
||||
return jsonify({"error": "Fehler bei der Konfliktanalyse"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/resolve-conflicts', methods=['POST'])
|
||||
@login_required
|
||||
def api_resolve_conflicts():
|
||||
"""
|
||||
Löst erkannte Konflikte automatisch und erstellt den Job
|
||||
mit den optimalen Parametern.
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({"error": "Keine Daten erhalten"}), 400
|
||||
|
||||
# Pflichtfelder prüfen
|
||||
required_fields = ['start_time', 'end_time', 'title']
|
||||
for field in required_fields:
|
||||
if field not in data:
|
||||
return jsonify({"error": f"Feld '{field}' fehlt"}), 400
|
||||
|
||||
# Daten extrahieren
|
||||
try:
|
||||
start_time = datetime.fromisoformat(data['start_time'])
|
||||
end_time = datetime.fromisoformat(data['end_time'])
|
||||
title = data['title']
|
||||
description = data.get('description', '')
|
||||
printer_id = data.get('printer_id')
|
||||
priority = data.get('priority', 'normal')
|
||||
duration_minutes = int((end_time - start_time).total_seconds() / 60)
|
||||
auto_resolve = data.get('auto_resolve', True)
|
||||
except (ValueError, TypeError) as e:
|
||||
return jsonify({"error": f"Ungültige Datenformate: {str(e)}"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Job-Daten für Konfliktanalyse vorbereiten
|
||||
job_data = {
|
||||
'printer_id': printer_id,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
'priority': priority,
|
||||
'duration_minutes': duration_minutes,
|
||||
'title': title,
|
||||
'description': description
|
||||
}
|
||||
|
||||
# Konfliktanalyse durchführen
|
||||
conflicts = conflict_manager.detect_conflicts(job_data, db_session)
|
||||
|
||||
final_job_data = job_data.copy()
|
||||
resolution_messages = []
|
||||
|
||||
if conflicts and auto_resolve:
|
||||
# Konflikte automatisch lösen
|
||||
resolutions = conflict_manager.resolve_conflicts(conflicts, job_data, db_session)
|
||||
|
||||
# Erste erfolgreiche Lösung anwenden
|
||||
successful_resolution = next((r for r in resolutions if r.success), None)
|
||||
|
||||
if successful_resolution:
|
||||
# Job-Parameter basierend auf Lösung anpassen
|
||||
if successful_resolution.new_printer_id:
|
||||
final_job_data['printer_id'] = successful_resolution.new_printer_id
|
||||
if successful_resolution.new_start_time:
|
||||
final_job_data['start_time'] = successful_resolution.new_start_time
|
||||
if successful_resolution.new_end_time:
|
||||
final_job_data['end_time'] = successful_resolution.new_end_time
|
||||
|
||||
resolution_messages.append(successful_resolution.message)
|
||||
logger.info(f"🔧 Konflikt automatisch gelöst: {successful_resolution.strategy_used.value}")
|
||||
else:
|
||||
return jsonify({
|
||||
"error": "Konflikte können nicht automatisch gelöst werden",
|
||||
"conflicts": [c.description for c in conflicts],
|
||||
"requires_manual_intervention": True
|
||||
}), 409
|
||||
elif conflicts and not auto_resolve:
|
||||
return jsonify({
|
||||
"error": "Konflikte erkannt - automatische Lösung deaktiviert",
|
||||
"conflicts": [c.description for c in conflicts],
|
||||
"suggestions": [s for c in conflicts for s in c.suggested_solutions]
|
||||
}), 409
|
||||
|
||||
# Finalen Drucker ermitteln
|
||||
if not final_job_data.get('printer_id'):
|
||||
# Intelligente Druckerzuweisung
|
||||
printer_id = get_smart_printer_assignment(
|
||||
start_date=final_job_data['start_time'],
|
||||
end_date=final_job_data['end_time'],
|
||||
priority=priority,
|
||||
db_session=db_session
|
||||
)
|
||||
|
||||
if not printer_id:
|
||||
return jsonify({
|
||||
"error": "Keine verfügbaren Drucker für den gewünschten Zeitraum gefunden"
|
||||
}), 409
|
||||
|
||||
final_job_data['printer_id'] = printer_id
|
||||
resolution_messages.append("Drucker automatisch zugewiesen")
|
||||
|
||||
# Drucker validieren
|
||||
printer = db_session.query(Printer).filter_by(id=final_job_data['printer_id']).first()
|
||||
if not printer:
|
||||
return jsonify({"error": "Zugewiesener Drucker nicht gefunden"}), 404
|
||||
|
||||
if not printer.active:
|
||||
return jsonify({"error": f"Drucker '{printer.name}' ist nicht aktiv"}), 400
|
||||
|
||||
# Job erstellen
|
||||
job = Job(
|
||||
name=title,
|
||||
description=description,
|
||||
user_id=current_user.id,
|
||||
printer_id=final_job_data['printer_id'],
|
||||
start_at=final_job_data['start_time'],
|
||||
end_at=final_job_data['end_time'],
|
||||
status="scheduled",
|
||||
duration_minutes=duration_minutes,
|
||||
owner_id=current_user.id
|
||||
)
|
||||
|
||||
db_session.add(job)
|
||||
db_session.commit()
|
||||
|
||||
assignment_type = "automatisch mit Konfliktlösung" if conflicts else "automatisch"
|
||||
logger.info(f"✅ Job mit Konfliktlösung erstellt: ID {job.id}, "
|
||||
f"Drucker: {printer.name} ({assignment_type})")
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"job": {
|
||||
"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
|
||||
},
|
||||
"conflict_resolution": {
|
||||
"conflicts_detected": len(conflicts),
|
||||
"conflicts_resolved": len([r for r in (resolutions if conflicts else []) if r.success]),
|
||||
"messages": resolution_messages,
|
||||
"assignment_type": assignment_type
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Fehler bei Konfliktlösung und Job-Erstellung: {str(e)}", exc_info=True)
|
||||
return jsonify({"error": "Fehler bei der Verarbeitung"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/printer-availability', methods=['GET'])
|
||||
@login_required
|
||||
def api_printer_availability():
|
||||
"""
|
||||
Zeigt detaillierte Verfügbarkeit aller Drucker für einen Zeitraum an.
|
||||
"""
|
||||
try:
|
||||
# Parameter extrahieren
|
||||
start_str = request.args.get('start')
|
||||
end_str = request.args.get('end')
|
||||
|
||||
if not start_str or not end_str:
|
||||
return jsonify({"error": "Start- und Endzeit erforderlich"}), 400
|
||||
|
||||
try:
|
||||
start_time = datetime.fromisoformat(start_str)
|
||||
end_time = datetime.fromisoformat(end_str)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat"}), 400
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Alle aktiven Drucker laden
|
||||
printers = db_session.query(Printer).filter_by(active=True).all()
|
||||
|
||||
availability_info = []
|
||||
|
||||
for printer in printers:
|
||||
# Jobs im Zeitraum finden
|
||||
jobs_in_period = db_session.query(Job).filter(
|
||||
Job.printer_id == printer.id,
|
||||
Job.status.in_(["scheduled", "running"]),
|
||||
or_(
|
||||
and_(Job.start_at >= start_time, Job.start_at < end_time),
|
||||
and_(Job.end_at > start_time, Job.end_at <= end_time),
|
||||
and_(Job.start_at <= start_time, Job.end_at >= end_time)
|
||||
)
|
||||
).all()
|
||||
|
||||
# Auslastung der letzten 24 Stunden
|
||||
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()
|
||||
|
||||
# Verfügbarkeits-Status bestimmen
|
||||
is_available = len(jobs_in_period) == 0
|
||||
|
||||
if recent_jobs == 0:
|
||||
availability_status = "optimal"
|
||||
availability_icon = "🟢"
|
||||
elif recent_jobs <= 2:
|
||||
availability_status = "gut"
|
||||
availability_icon = "🟡"
|
||||
elif recent_jobs <= 5:
|
||||
availability_status = "mäßig"
|
||||
availability_icon = "🟠"
|
||||
else:
|
||||
availability_status = "hoch_belegt"
|
||||
availability_icon = "🔴"
|
||||
|
||||
if not printer.active:
|
||||
availability_status = "offline"
|
||||
availability_icon = "⚫"
|
||||
|
||||
# Nächste freie Zeitfenster finden
|
||||
next_free_slots = []
|
||||
if not is_available:
|
||||
# Einfache Implementierung: Zeitfenster nach bestehenden Jobs
|
||||
last_job_end = max([job.end_at for job in jobs_in_period])
|
||||
next_free_slots.append({
|
||||
'start': last_job_end.isoformat(),
|
||||
'description': f"Frei ab {last_job_end.strftime('%H:%M')}"
|
||||
})
|
||||
|
||||
# Belegte Zeitfenster
|
||||
occupied_slots = []
|
||||
for job in jobs_in_period:
|
||||
occupied_slots.append({
|
||||
'job_id': job.id,
|
||||
'job_name': job.name,
|
||||
'start': job.start_at.isoformat() if job.start_at else None,
|
||||
'end': job.end_at.isoformat() if job.end_at else None,
|
||||
'user_name': job.user.name if job.user else "Unbekannt",
|
||||
'status': job.status
|
||||
})
|
||||
|
||||
printer_info = {
|
||||
'printer_id': printer.id,
|
||||
'printer_name': printer.name,
|
||||
'location': printer.location,
|
||||
'model': printer.model,
|
||||
'is_available': is_available,
|
||||
'availability_status': availability_status,
|
||||
'availability_icon': availability_icon,
|
||||
'recent_jobs_24h': recent_jobs,
|
||||
'jobs_in_period': len(jobs_in_period),
|
||||
'occupied_slots': occupied_slots,
|
||||
'next_free_slots': next_free_slots,
|
||||
'status_description': {
|
||||
'optimal': 'Keine kürzlichen Jobs, sofort verfügbar',
|
||||
'gut': 'Wenige kürzliche Jobs, gute Verfügbarkeit',
|
||||
'mäßig': 'Moderate Auslastung',
|
||||
'hoch_belegt': 'Hohe Auslastung, möglicherweise Wartezeit',
|
||||
'offline': 'Drucker offline oder nicht aktiv'
|
||||
}.get(availability_status, availability_status)
|
||||
}
|
||||
|
||||
availability_info.append(printer_info)
|
||||
|
||||
# Nach Verfügbarkeit sortieren (beste zuerst)
|
||||
availability_info.sort(key=lambda x: (
|
||||
not x['is_available'], # Verfügbare zuerst
|
||||
x['recent_jobs_24h'], # Dann nach geringster Auslastung
|
||||
x['printer_name'] # Dann alphabetisch
|
||||
))
|
||||
|
||||
# Zusammenfassung erstellen
|
||||
total_printers = len(availability_info)
|
||||
available_printers = len([p for p in availability_info if p['is_available']])
|
||||
optimal_printers = len([p for p in availability_info if p['availability_status'] == 'optimal'])
|
||||
|
||||
summary = {
|
||||
'total_printers': total_printers,
|
||||
'available_printers': available_printers,
|
||||
'optimal_printers': optimal_printers,
|
||||
'availability_rate': round((available_printers / total_printers * 100) if total_printers > 0 else 0, 1),
|
||||
'period': {
|
||||
'start': start_time.isoformat(),
|
||||
'end': end_time.isoformat(),
|
||||
'duration_hours': round((end_time - start_time).total_seconds() / 3600, 1)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"📊 Verfügbarkeitsabfrage: {available_printers}/{total_printers} Drucker verfügbar")
|
||||
|
||||
return jsonify({
|
||||
'summary': summary,
|
||||
'printers': availability_info
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Fehler bei Verfügbarkeitsabfrage: {str(e)}", exc_info=True)
|
||||
return jsonify({"error": "Fehler bei der Verfügbarkeitsanalyse"}), 500
|
Reference in New Issue
Block a user