📚 Improved backend structure & documentation, added new features, and refactored scripts. 🚀🔧📝💻🖥️
This commit is contained in:
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort, make_response
|
||||
from flask_login import current_user, login_required
|
||||
from sqlalchemy import and_, or_, func
|
||||
|
||||
@ -158,9 +158,9 @@ def calendar_view():
|
||||
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')
|
||||
# Datumsbereich aus Anfrage - FullCalendar verwendet 'start' und 'end'
|
||||
start_str = request.args.get('start') or request.args.get('from')
|
||||
end_str = request.args.get('end') or request.args.get('to')
|
||||
|
||||
if not start_str or not end_str:
|
||||
# Standardmäßig eine Woche anzeigen
|
||||
@ -168,6 +168,12 @@ def api_get_calendar_events():
|
||||
end_date = start_date + timedelta(days=7)
|
||||
else:
|
||||
try:
|
||||
# FullCalendar sendet ISO-Format mit Zeitzone, das muss geparst werden
|
||||
if start_str and start_str.endswith('+02:00'):
|
||||
start_str = start_str[:-6] # Zeitzone entfernen
|
||||
if end_str and end_str.endswith('+02:00'):
|
||||
end_str = end_str[:-6] # Zeitzone entfernen
|
||||
|
||||
start_date = datetime.fromisoformat(start_str)
|
||||
end_date = datetime.fromisoformat(end_str)
|
||||
except ValueError:
|
||||
@ -233,12 +239,20 @@ def api_get_calendar_events():
|
||||
|
||||
events.append(event)
|
||||
|
||||
logger.info(f"📅 Kalender-Events abgerufen: {len(events)} Einträge für Zeitraum {start_date} bis {end_date}")
|
||||
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
|
||||
|
||||
# Zusätzliche Route für FullCalendar-Kompatibilität
|
||||
@calendar_blueprint.route('/api/calendar/events', methods=['GET'])
|
||||
@login_required
|
||||
def api_get_calendar_events_alt():
|
||||
"""Alternative Route für FullCalendar-Events - delegiert an api_get_calendar_events."""
|
||||
return api_get_calendar_events()
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/event', methods=['POST'])
|
||||
@login_required
|
||||
def api_create_calendar_event():
|
||||
@ -547,4 +561,281 @@ def api_get_smart_recommendation():
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der intelligenten Empfehlung: {str(e)}")
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
|
||||
|
||||
@calendar_blueprint.route('/api/calendar/export', methods=['GET'])
|
||||
@login_required
|
||||
def api_export_calendar():
|
||||
"""
|
||||
Exportiert Kalenderdaten in verschiedenen Formaten (CSV, JSON, Excel).
|
||||
|
||||
URL-Parameter:
|
||||
- format: csv, json, excel (Standard: csv)
|
||||
- start_date: ISO-Format Start-Datum (Optional)
|
||||
- end_date: ISO-Format End-Datum (Optional)
|
||||
- printer_id: Spezifischer Drucker-Filter (Optional)
|
||||
- status: Status-Filter (Optional)
|
||||
"""
|
||||
try:
|
||||
# Parameter aus Request extrahieren
|
||||
export_format = request.args.get('format', 'csv').lower()
|
||||
start_str = request.args.get('start_date')
|
||||
end_str = request.args.get('end_date')
|
||||
printer_id = request.args.get('printer_id')
|
||||
status_filter = request.args.get('status')
|
||||
|
||||
# Datumsbereich bestimmen (Standard: nächste 4 Wochen)
|
||||
if start_str and end_str:
|
||||
try:
|
||||
start_date = datetime.fromisoformat(start_str)
|
||||
end_date = datetime.fromisoformat(end_str)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültiges Datumsformat. Verwenden Sie ISO-Format."}), 400
|
||||
else:
|
||||
# Standard: Kommende 4 Wochen
|
||||
start_date = datetime.now()
|
||||
end_date = start_date + timedelta(days=28)
|
||||
|
||||
with get_cached_session() as db_session:
|
||||
# Basis-Query für Jobs im Zeitraum
|
||||
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)
|
||||
)
|
||||
)
|
||||
|
||||
# Filter anwenden
|
||||
if printer_id:
|
||||
try:
|
||||
printer_id_int = int(printer_id)
|
||||
query = query.filter(Job.printer_id == printer_id_int)
|
||||
except ValueError:
|
||||
return jsonify({"error": "Ungültige Drucker-ID"}), 400
|
||||
|
||||
if status_filter:
|
||||
query = query.filter(Job.status == status_filter)
|
||||
|
||||
# Jobs abrufen und sortieren
|
||||
jobs = query.order_by(Job.start_at.asc()).all()
|
||||
|
||||
if not jobs:
|
||||
return jsonify({"message": "Keine Daten im angegebenen Zeitraum gefunden"}), 404
|
||||
|
||||
# Zusätzliche Daten für Export sammeln
|
||||
export_data = []
|
||||
|
||||
for job in jobs:
|
||||
# Benutzerinformationen
|
||||
user = db_session.query(User).filter_by(id=job.user_id).first()
|
||||
user_name = user.name if user else "Unbekannt"
|
||||
user_email = user.email if user else ""
|
||||
|
||||
# Druckerinformationen
|
||||
printer = db_session.query(Printer).filter_by(id=job.printer_id).first()
|
||||
printer_name = printer.name if printer else "Unbekannt"
|
||||
printer_location = printer.location if printer else ""
|
||||
printer_model = printer.model if printer else ""
|
||||
|
||||
# Status-Übersetzung
|
||||
status_mapping = {
|
||||
"scheduled": "Geplant",
|
||||
"running": "Läuft",
|
||||
"finished": "Abgeschlossen",
|
||||
"cancelled": "Abgebrochen",
|
||||
"failed": "Fehlgeschlagen",
|
||||
"pending": "Wartend"
|
||||
}
|
||||
status_german = status_mapping.get(job.status, job.status)
|
||||
|
||||
# Priorität-Übersetzung
|
||||
priority_mapping = {
|
||||
"urgent": "Dringend",
|
||||
"high": "Hoch",
|
||||
"normal": "Standard",
|
||||
"low": "Niedrig"
|
||||
}
|
||||
priority_german = priority_mapping.get(job.priority if hasattr(job, 'priority') and job.priority else 'normal', 'Standard')
|
||||
|
||||
export_entry = {
|
||||
"Job_ID": job.id,
|
||||
"Auftragsname": job.name,
|
||||
"Beschreibung": job.description or "",
|
||||
"Status": status_german,
|
||||
"Priorität": priority_german,
|
||||
"Benutzer": user_name,
|
||||
"Benutzer_E-Mail": user_email,
|
||||
"Drucker": printer_name,
|
||||
"Drucker_Standort": printer_location,
|
||||
"Drucker_Modell": printer_model,
|
||||
"Startzeit": job.start_at.strftime('%d.%m.%Y %H:%M:%S') if job.start_at else "",
|
||||
"Endzeit": job.end_at.strftime('%d.%m.%Y %H:%M:%S') if job.end_at else "",
|
||||
"Dauer_Minuten": job.duration_minutes or 0,
|
||||
"Dauer_Stunden": round((job.duration_minutes or 0) / 60, 2),
|
||||
"Erstellt_am": job.created_at.strftime('%d.%m.%Y %H:%M:%S') if job.created_at else "",
|
||||
"Abgeschlossen_am": job.completed_at.strftime('%d.%m.%Y %H:%M:%S') if job.completed_at else "",
|
||||
"Dateiname": job.filename if hasattr(job, 'filename') and job.filename else "",
|
||||
"Materialverbrauch": getattr(job, 'material_used', "") or "",
|
||||
"Kosten_EUR": getattr(job, 'cost', "") or ""
|
||||
}
|
||||
|
||||
export_data.append(export_entry)
|
||||
|
||||
# Format-spezifischer Export
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
if export_format == 'csv':
|
||||
import csv
|
||||
import io
|
||||
|
||||
# CSV-Export implementierung
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_MINIMAL)
|
||||
|
||||
# Header-Zeile
|
||||
if export_data:
|
||||
headers = list(export_data[0].keys())
|
||||
writer.writerow(headers)
|
||||
|
||||
# Daten-Zeilen
|
||||
for row in export_data:
|
||||
formatted_row = []
|
||||
for value in row.values():
|
||||
if value is None:
|
||||
formatted_row.append('')
|
||||
else:
|
||||
formatted_row.append(str(value))
|
||||
writer.writerow(formatted_row)
|
||||
|
||||
# Response erstellen
|
||||
csv_content = output.getvalue()
|
||||
output.close()
|
||||
|
||||
# BOM für UTF-8 hinzufügen (Excel-Kompatibilität)
|
||||
csv_content = '\ufeff' + csv_content
|
||||
|
||||
response = make_response(csv_content)
|
||||
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="schichtplan_export_{timestamp}.csv"'
|
||||
|
||||
logger.info(f"📊 CSV-Export erstellt: {len(export_data)} Einträge für Benutzer {current_user.username}")
|
||||
return response
|
||||
|
||||
elif export_format == 'json':
|
||||
# JSON-Export implementierung
|
||||
export_metadata = {
|
||||
"export_info": {
|
||||
"erstellt_am": datetime.now().strftime('%d.%m.%Y %H:%M:%S'),
|
||||
"exportiert_von": current_user.username,
|
||||
"zeitraum_von": start_date.strftime('%d.%m.%Y'),
|
||||
"zeitraum_bis": end_date.strftime('%d.%m.%Y'),
|
||||
"anzahl_jobs": len(export_data),
|
||||
"filter_drucker_id": printer_id,
|
||||
"filter_status": status_filter
|
||||
},
|
||||
"produktionsplan": export_data
|
||||
}
|
||||
|
||||
json_content = json.dumps(export_metadata, indent=2, ensure_ascii=False)
|
||||
|
||||
response = make_response(json_content)
|
||||
response.headers['Content-Type'] = 'application/json; charset=utf-8'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="schichtplan_export_{timestamp}.json"'
|
||||
|
||||
logger.info(f"📊 JSON-Export erstellt: {len(export_data)} Einträge für Benutzer {current_user.username}")
|
||||
return response
|
||||
|
||||
elif export_format == 'excel':
|
||||
# Excel-Export implementierung
|
||||
try:
|
||||
import pandas as pd
|
||||
import io
|
||||
|
||||
# DataFrame erstellen
|
||||
df = pd.DataFrame(export_data)
|
||||
|
||||
# Excel-Datei in Memory erstellen
|
||||
output = io.BytesIO()
|
||||
|
||||
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||
# Hauptdaten-Sheet
|
||||
df.to_excel(writer, sheet_name='Produktionsplan', index=False)
|
||||
|
||||
# Zusammenfassung-Sheet
|
||||
summary_data = {
|
||||
'Metrik': [
|
||||
'Gesamte Jobs',
|
||||
'Geplante Jobs',
|
||||
'Laufende Jobs',
|
||||
'Abgeschlossene Jobs',
|
||||
'Abgebrochene Jobs',
|
||||
'Gesamte Produktionszeit (Stunden)',
|
||||
'Durchschnittliche Job-Dauer (Stunden)',
|
||||
'Anzahl verschiedener Drucker',
|
||||
'Anzahl verschiedener Benutzer',
|
||||
'Export erstellt am',
|
||||
'Zeitraum von',
|
||||
'Zeitraum bis'
|
||||
],
|
||||
'Wert': [
|
||||
len(export_data),
|
||||
len([j for j in export_data if j['Status'] == 'Geplant']),
|
||||
len([j for j in export_data if j['Status'] == 'Läuft']),
|
||||
len([j for j in export_data if j['Status'] == 'Abgeschlossen']),
|
||||
len([j for j in export_data if j['Status'] in ['Abgebrochen', 'Fehlgeschlagen']]),
|
||||
round(sum([j['Dauer_Stunden'] for j in export_data]), 2),
|
||||
round(sum([j['Dauer_Stunden'] for j in export_data]) / len(export_data), 2) if export_data else 0,
|
||||
len(set([j['Drucker'] for j in export_data])),
|
||||
len(set([j['Benutzer'] for j in export_data])),
|
||||
datetime.now().strftime('%d.%m.%Y %H:%M:%S'),
|
||||
start_date.strftime('%d.%m.%Y'),
|
||||
end_date.strftime('%d.%m.%Y')
|
||||
]
|
||||
}
|
||||
|
||||
summary_df = pd.DataFrame(summary_data)
|
||||
summary_df.to_excel(writer, sheet_name='Zusammenfassung', index=False)
|
||||
|
||||
# Auto-fit Spaltenbreite
|
||||
for sheet_name in writer.sheets:
|
||||
worksheet = writer.sheets[sheet_name]
|
||||
for column in worksheet.columns:
|
||||
max_length = 0
|
||||
column_letter = column[0].column_letter
|
||||
for cell in column:
|
||||
try:
|
||||
if len(str(cell.value)) > max_length:
|
||||
max_length = len(str(cell.value))
|
||||
except:
|
||||
pass
|
||||
adjusted_width = min(max_length + 2, 50)
|
||||
worksheet.column_dimensions[column_letter].width = adjusted_width
|
||||
|
||||
output.seek(0)
|
||||
|
||||
response = make_response(output.getvalue())
|
||||
response.headers['Content-Type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="schichtplan_export_{timestamp}.xlsx"'
|
||||
|
||||
logger.info(f"📊 Excel-Export erstellt: {len(export_data)} Einträge für Benutzer {current_user.username}")
|
||||
return response
|
||||
|
||||
except ImportError:
|
||||
return jsonify({
|
||||
"error": "Excel-Export nicht verfügbar. Pandas/openpyxl-Bibliotheken fehlen. "
|
||||
"Verwenden Sie CSV oder JSON Export."
|
||||
}), 501
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Excel-Export: {str(e)}")
|
||||
return jsonify({"error": f"Fehler beim Excel-Export: {str(e)}"}), 500
|
||||
|
||||
else:
|
||||
return jsonify({"error": f"Unbekanntes Export-Format: {export_format}. Unterstützt: csv, json, excel"}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Kalender-Export: {str(e)}")
|
||||
return jsonify({"error": f"Fehler beim Export: {str(e)}"}), 500
|
Reference in New Issue
Block a user