🎉 Improved backend functionality & documentation, added OTP documentation & JavaScript file. 🎨
This commit is contained in:
parent
b2bdc2d123
commit
91548dfb0e
492
backend/app.py
492
backend/app.py
@ -41,6 +41,29 @@ from utils.queue_manager import start_queue_manager, stop_queue_manager, get_que
|
||||
from config.settings import SECRET_KEY, UPLOAD_FOLDER, ALLOWED_EXTENSIONS, ENVIRONMENT, SESSION_LIFETIME, SCHEDULER_ENABLED, SCHEDULER_INTERVAL, TAPO_USERNAME, TAPO_PASSWORD
|
||||
from utils.file_manager import file_manager, save_job_file, save_guest_file, save_avatar_file, save_asset_file, save_log_file, save_backup_file, save_temp_file, delete_file as delete_file_safe
|
||||
|
||||
# ===== OFFLINE-MODUS KONFIGURATION =====
|
||||
# System läuft im Offline-Modus ohne Internetverbindung
|
||||
OFFLINE_MODE = True # Produktionseinstellung für Offline-Betrieb
|
||||
|
||||
# ===== BEDINGTE IMPORTS FÜR OFFLINE-MODUS =====
|
||||
if not OFFLINE_MODE:
|
||||
# Nur laden wenn Online-Modus
|
||||
import requests
|
||||
else:
|
||||
# Offline-Mock für requests
|
||||
class OfflineRequestsMock:
|
||||
"""Mock-Klasse für requests im Offline-Modus"""
|
||||
|
||||
@staticmethod
|
||||
def get(*args, **kwargs):
|
||||
raise ConnectionError("System läuft im Offline-Modus - keine Internet-Verbindung verfügbar")
|
||||
|
||||
@staticmethod
|
||||
def post(*args, **kwargs):
|
||||
raise ConnectionError("System läuft im Offline-Modus - keine Internet-Verbindung verfügbar")
|
||||
|
||||
requests = OfflineRequestsMock()
|
||||
|
||||
# Datenbank-Engine für Kompatibilität mit init_simple_db.py
|
||||
from models import engine as db_engine
|
||||
|
||||
@ -311,7 +334,7 @@ def format_datetime_filter(value, format='%d.%m.%Y %H:%M'):
|
||||
setup_logging()
|
||||
log_startup_info()
|
||||
|
||||
# Logger für verschiedene Komponenten
|
||||
# app_logger für verschiedene Komponenten
|
||||
app_logger = get_logger("app")
|
||||
auth_logger = get_logger("auth")
|
||||
jobs_logger = get_logger("jobs")
|
||||
@ -1695,7 +1718,7 @@ def api_admin_system_health():
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}")
|
||||
app_logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
@ -1731,7 +1754,7 @@ def api_admin_system_health():
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}")
|
||||
app_logger.error(f"Fehler beim System-Gesundheitscheck: {str(e)}")
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
@ -3210,6 +3233,118 @@ def test_admin_guest_requests():
|
||||
'user_is_admin': current_user.is_admin if current_user.is_authenticated else False
|
||||
})
|
||||
|
||||
@app.route('/api/guest-status', methods=['POST'])
|
||||
def get_guest_request_status():
|
||||
"""
|
||||
Öffentliche Route für Gäste um ihren Auftragsstatus mit OTP-Code zu prüfen.
|
||||
Keine Authentifizierung erforderlich.
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
if not data:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Keine Daten empfangen'
|
||||
}), 400
|
||||
|
||||
otp_code = data.get('otp_code', '').strip()
|
||||
email = data.get('email', '').strip() # Optional für zusätzliche Verifikation
|
||||
|
||||
if not otp_code:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'OTP-Code ist erforderlich'
|
||||
}), 400
|
||||
|
||||
db_session = get_db_session()
|
||||
|
||||
# Alle Gastaufträge finden, die den OTP-Code haben könnten
|
||||
# Da OTP gehashed ist, müssen wir durch alle iterieren
|
||||
guest_requests = db_session.query(GuestRequest).filter(
|
||||
GuestRequest.otp_code.isnot(None)
|
||||
).all()
|
||||
|
||||
found_request = None
|
||||
for request_obj in guest_requests:
|
||||
if request_obj.verify_otp(otp_code):
|
||||
# Zusätzliche E-Mail-Verifikation falls angegeben
|
||||
if email and request_obj.email.lower() != email.lower():
|
||||
continue
|
||||
found_request = request_obj
|
||||
break
|
||||
|
||||
if not found_request:
|
||||
db_session.close()
|
||||
app_logger.warning(f"Ungültiger OTP-Code für Gast-Status-Abfrage: {otp_code[:4]}****")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Ungültiger Code oder E-Mail-Adresse'
|
||||
}), 404
|
||||
|
||||
# Status-Informationen für den Gast zusammenstellen
|
||||
status_info = {
|
||||
'id': found_request.id,
|
||||
'name': found_request.name,
|
||||
'file_name': found_request.file_name,
|
||||
'status': found_request.status,
|
||||
'created_at': found_request.created_at.isoformat() if found_request.created_at else None,
|
||||
'updated_at': found_request.updated_at.isoformat() if found_request.updated_at else None,
|
||||
'duration_minutes': found_request.duration_minutes,
|
||||
'copies': found_request.copies,
|
||||
'reason': found_request.reason
|
||||
}
|
||||
|
||||
# Status-spezifische Informationen hinzufügen
|
||||
if found_request.status == 'approved':
|
||||
status_info.update({
|
||||
'approved_at': found_request.approved_at.isoformat() if found_request.approved_at else None,
|
||||
'approval_notes': found_request.approval_notes,
|
||||
'message': 'Ihr Auftrag wurde genehmigt! Sie können mit dem Drucken beginnen.'
|
||||
})
|
||||
|
||||
elif found_request.status == 'rejected':
|
||||
status_info.update({
|
||||
'rejected_at': found_request.rejected_at.isoformat() if found_request.rejected_at else None,
|
||||
'rejection_reason': found_request.rejection_reason,
|
||||
'message': 'Ihr Auftrag wurde leider abgelehnt.'
|
||||
})
|
||||
|
||||
elif found_request.status == 'pending':
|
||||
# Berechne wie lange der Auftrag schon wartet
|
||||
if found_request.created_at:
|
||||
waiting_time = datetime.now() - found_request.created_at
|
||||
hours_waiting = int(waiting_time.total_seconds() / 3600)
|
||||
status_info.update({
|
||||
'hours_waiting': hours_waiting,
|
||||
'message': f'Ihr Auftrag wird bearbeitet. Wartezeit: {hours_waiting} Stunden.'
|
||||
})
|
||||
else:
|
||||
status_info['message'] = 'Ihr Auftrag wird bearbeitet.'
|
||||
|
||||
db_session.commit() # OTP als verwendet markieren
|
||||
db_session.close()
|
||||
|
||||
app_logger.info(f"Gast-Status-Abfrage erfolgreich für Request {found_request.id}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'request': status_info
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler bei Gast-Status-Abfrage: {str(e)}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'message': 'Fehler beim Abrufen des Status'
|
||||
}), 500
|
||||
|
||||
@app.route('/guest-status')
|
||||
def guest_status_page():
|
||||
"""
|
||||
Öffentliche Seite für Gäste um ihren Auftragsstatus zu prüfen.
|
||||
"""
|
||||
return render_template('guest_status.html')
|
||||
|
||||
@app.route('/api/admin/guest-requests', methods=['GET'])
|
||||
@admin_required
|
||||
def get_admin_guest_requests():
|
||||
@ -3374,32 +3509,37 @@ def approve_guest_request(request_id):
|
||||
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)
|
||||
# OTP-Code generieren falls noch nicht vorhanden (nutze die Methode aus models.py)
|
||||
otp_code = None
|
||||
if not guest_request.otp_code:
|
||||
otp_code = guest_request.generate_otp()
|
||||
guest_request.otp_expires_at = datetime.now() + timedelta(hours=48) # 48h gültig
|
||||
|
||||
db_session.commit()
|
||||
|
||||
# Benachrichtigung an den Gast senden (falls E-Mail verfügbar)
|
||||
if guest_request.email:
|
||||
if guest_request.email and otp_code:
|
||||
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})")
|
||||
app_logger.info(f"Genehmigungs-E-Mail würde an {guest_request.email} gesendet (OTP für Status-Abfrage verfügbar)")
|
||||
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})")
|
||||
app_logger.info(f"Gastauftrag {request_id} von Admin {current_user.id} genehmigt")
|
||||
|
||||
return jsonify({
|
||||
response_data = {
|
||||
'success': True,
|
||||
'message': 'Gastauftrag erfolgreich genehmigt',
|
||||
'otp_code': otp_code,
|
||||
'expires_at': (datetime.now() + timedelta(hours=24)).isoformat()
|
||||
})
|
||||
'message': 'Gastauftrag erfolgreich genehmigt'
|
||||
}
|
||||
|
||||
# OTP-Code nur zurückgeben wenn er neu generiert wurde (für Admin-Info)
|
||||
if otp_code:
|
||||
response_data['otp_code_generated'] = True
|
||||
response_data['status_check_url'] = url_for('guest_status_page', _external=True)
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"Fehler beim Genehmigen des Gastauftrags {request_id}: {str(e)}")
|
||||
@ -4065,7 +4205,7 @@ def get_validation_js():
|
||||
response.headers['Cache-Control'] = 'public, max-age=3600' # 1 Stunde Cache
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden des Validierungs-JS: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Laden des Validierungs-JS: {str(e)}")
|
||||
return "console.error('Validierungs-JavaScript konnte nicht geladen werden');", 500
|
||||
|
||||
@app.route('/api/validation/validate-form', methods=['POST'])
|
||||
@ -4099,7 +4239,7 @@ def validate_form_api():
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Formular-Validierung: {str(e)}")
|
||||
app_logger.error(f"Fehler bei Formular-Validierung: {str(e)}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
# ===== REPORT GENERATOR API =====
|
||||
@ -4171,7 +4311,7 @@ def generate_report():
|
||||
return jsonify({'error': 'Report-Generierung fehlgeschlagen'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Report-Generierung: {str(e)}")
|
||||
app_logger.error(f"Fehler bei Report-Generierung: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# ===== REALTIME DASHBOARD API =====
|
||||
@ -4183,7 +4323,7 @@ def get_dashboard_config():
|
||||
config = dashboard_manager.get_dashboard_config(current_user.id)
|
||||
return jsonify(config)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der Dashboard-Konfiguration: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Laden der Dashboard-Konfiguration: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/dashboard/widgets/<widget_id>/data', methods=['GET'])
|
||||
@ -4198,7 +4338,7 @@ def get_widget_data(widget_id):
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden der Widget-Daten für {widget_id}: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Laden der Widget-Daten für {widget_id}: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/dashboard/emit-event', methods=['POST'])
|
||||
@ -4223,7 +4363,7 @@ def emit_dashboard_event():
|
||||
return jsonify({'success': True})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Senden des Dashboard-Ereignisses: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Senden des Dashboard-Ereignisses: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/dashboard/client-js', methods=['GET'])
|
||||
@ -4236,7 +4376,7 @@ def get_dashboard_js():
|
||||
response.headers['Cache-Control'] = 'public, max-age=1800' # 30 Minuten Cache
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden des Dashboard-JS: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Laden des Dashboard-JS: {str(e)}")
|
||||
return "console.error('Dashboard-JavaScript konnte nicht geladen werden');", 500
|
||||
|
||||
# ===== DRAG & DROP API =====
|
||||
@ -4270,7 +4410,7 @@ def update_job_order():
|
||||
return jsonify({'error': 'Fehler beim Aktualisieren der Job-Reihenfolge'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktualisieren der Job-Reihenfolge: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Aktualisieren der Job-Reihenfolge: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/dragdrop/get-job-order/<int:printer_id>', methods=['GET'])
|
||||
@ -4300,7 +4440,7 @@ def get_job_order_api(printer_id):
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Job-Reihenfolge: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Abrufen der Job-Reihenfolge: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/dragdrop/upload-session', methods=['POST'])
|
||||
@ -4318,7 +4458,7 @@ def create_upload_session():
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen der Upload-Session: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Erstellen der Upload-Session: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/dragdrop/upload-progress/<session_id>', methods=['GET'])
|
||||
@ -4329,7 +4469,7 @@ def get_upload_progress(session_id):
|
||||
progress = drag_drop_manager.get_session_progress(session_id)
|
||||
return jsonify(progress)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen des Upload-Progress: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Abrufen des Upload-Progress: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/dragdrop/client-js', methods=['GET'])
|
||||
@ -4342,7 +4482,7 @@ def get_dragdrop_js():
|
||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden des Drag & Drop JS: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Laden des Drag & Drop JS: {str(e)}")
|
||||
return "console.error('Drag & Drop JavaScript konnte nicht geladen werden');", 500
|
||||
|
||||
@app.route('/api/dragdrop/client-css', methods=['GET'])
|
||||
@ -4355,7 +4495,7 @@ def get_dragdrop_css():
|
||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden des Drag & Drop CSS: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Laden des Drag & Drop CSS: {str(e)}")
|
||||
return "/* Drag & Drop CSS konnte nicht geladen werden */", 500
|
||||
|
||||
# ===== ADVANCED TABLES API =====
|
||||
@ -4422,7 +4562,7 @@ def query_advanced_table():
|
||||
return jsonify(result)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei erweiterte Tabellen-Abfrage: {str(e)}")
|
||||
app_logger.error(f"Fehler bei erweiterte Tabellen-Abfrage: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/tables/export', methods=['POST'])
|
||||
@ -4435,29 +4575,135 @@ def export_table_data():
|
||||
export_format = data.get('format', 'csv')
|
||||
query_params = data.get('query', {})
|
||||
|
||||
# Hier würde die Export-Logik implementiert
|
||||
# Für jetzt einfache CSV-Export-Simulation
|
||||
# Vollständige Export-Logik implementierung
|
||||
app_logger.info(f"📊 Starte Tabellen-Export: {table_type} als {export_format}")
|
||||
|
||||
# Tabellen-Konfiguration basierend auf Typ erstellen
|
||||
if table_type == 'jobs':
|
||||
config = create_table_config(
|
||||
'jobs',
|
||||
['id', 'filename', 'status', 'printer_name', 'user_name', 'created_at', 'completed_at'],
|
||||
base_query='Job'
|
||||
)
|
||||
elif table_type == 'printers':
|
||||
config = create_table_config(
|
||||
'printers',
|
||||
['id', 'name', 'ip_address', 'status', 'location', 'model'],
|
||||
base_query='Printer'
|
||||
)
|
||||
elif table_type == 'users':
|
||||
config = create_table_config(
|
||||
'users',
|
||||
['id', 'name', 'email', 'role', 'active', 'last_login'],
|
||||
base_query='User'
|
||||
)
|
||||
else:
|
||||
return jsonify({'error': 'Unbekannter Tabellen-Typ für Export'}), 400
|
||||
|
||||
# Erweiterte Abfrage für Export-Daten erstellen
|
||||
query_builder = AdvancedTableQuery(config)
|
||||
|
||||
# Filter aus Query-Parametern anwenden
|
||||
if 'filters' in query_params:
|
||||
for filter_data in query_params['filters']:
|
||||
query_builder.add_filter(
|
||||
filter_data['column'],
|
||||
filter_data['operator'],
|
||||
filter_data['value']
|
||||
)
|
||||
|
||||
# Sortierung anwenden
|
||||
if 'sort' in query_params:
|
||||
query_builder.set_sorting(
|
||||
query_params['sort']['column'],
|
||||
query_params['sort']['direction']
|
||||
)
|
||||
|
||||
# Für Export: Alle Daten ohne Paginierung
|
||||
query_builder.set_pagination(1, 10000) # Maximale Anzahl für Export
|
||||
|
||||
# Daten abrufen
|
||||
result = query_builder.execute()
|
||||
export_data = result.get('data', [])
|
||||
|
||||
if export_format == 'csv':
|
||||
import csv
|
||||
import io
|
||||
|
||||
# CSV-Export implementierung
|
||||
output = io.StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer = csv.writer(output, delimiter=';', quoting=csv.QUOTE_MINIMAL)
|
||||
|
||||
# Beispiel-Daten (würde durch echte Abfrage ersetzt)
|
||||
writer.writerow(['ID', 'Name', 'Status', 'Erstellt'])
|
||||
writer.writerow([1, 'Beispiel Job', 'Aktiv', '2025-01-07'])
|
||||
# Header-Zeile schreiben
|
||||
if export_data:
|
||||
headers = list(export_data[0].keys())
|
||||
writer.writerow(headers)
|
||||
|
||||
response = make_response(output.getvalue())
|
||||
response.headers['Content-Type'] = 'text/csv'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="{table_type}_export.csv"'
|
||||
# Daten-Zeilen schreiben
|
||||
for row in export_data:
|
||||
# Werte für CSV formatieren
|
||||
formatted_row = []
|
||||
for value in row.values():
|
||||
if value is None:
|
||||
formatted_row.append('')
|
||||
elif isinstance(value, datetime):
|
||||
formatted_row.append(value.strftime('%d.%m.%Y %H:%M:%S'))
|
||||
else:
|
||||
formatted_row.append(str(value))
|
||||
writer.writerow(formatted_row)
|
||||
|
||||
# Response erstellen
|
||||
csv_content = output.getvalue()
|
||||
output.close()
|
||||
|
||||
response = make_response(csv_content)
|
||||
response.headers['Content-Type'] = 'text/csv; charset=utf-8'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="{table_type}_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv"'
|
||||
|
||||
app_logger.info(f"✅ CSV-Export erfolgreich: {len(export_data)} Datensätze")
|
||||
return response
|
||||
|
||||
return jsonify({'error': 'Export-Format nicht unterstützt'}), 400
|
||||
elif export_format == 'json':
|
||||
# JSON-Export implementierung
|
||||
json_content = json.dumps(export_data, indent=2, default=str, ensure_ascii=False)
|
||||
|
||||
response = make_response(json_content)
|
||||
response.headers['Content-Type'] = 'application/json; charset=utf-8'
|
||||
response.headers['Content-Disposition'] = f'attachment; filename="{table_type}_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json"'
|
||||
|
||||
app_logger.info(f"✅ JSON-Export erfolgreich: {len(export_data)} Datensätze")
|
||||
return response
|
||||
|
||||
elif export_format == 'excel':
|
||||
# Excel-Export implementierung (falls openpyxl verfügbar)
|
||||
try:
|
||||
import openpyxl
|
||||
from openpyxl.utils.dataframe import dataframe_to_rows
|
||||
import pandas as pd
|
||||
|
||||
# DataFrame erstellen
|
||||
df = pd.DataFrame(export_data)
|
||||
|
||||
# Excel-Datei in Memory erstellen
|
||||
output = io.BytesIO()
|
||||
with pd.ExcelWriter(output, engine='openpyxl') as writer:
|
||||
df.to_excel(writer, sheet_name=table_type.capitalize(), index=False)
|
||||
|
||||
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="{table_type}_export_{datetime.now().strftime("%Y%m%d_%H%M%S")}.xlsx"'
|
||||
|
||||
app_logger.info(f"✅ Excel-Export erfolgreich: {len(export_data)} Datensätze")
|
||||
return response
|
||||
|
||||
except ImportError:
|
||||
app_logger.warning("⚠️ Excel-Export nicht verfügbar - openpyxl/pandas fehlt")
|
||||
return jsonify({'error': 'Excel-Export nicht verfügbar - erforderliche Bibliotheken fehlen'}), 400
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Tabellen-Export: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Tabellen-Export: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/tables/client-js', methods=['GET'])
|
||||
@ -4470,7 +4716,7 @@ def get_tables_js():
|
||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden des Tables-JS: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Laden des Tables-JS: {str(e)}")
|
||||
return "console.error('Advanced Tables JavaScript konnte nicht geladen werden');", 500
|
||||
|
||||
@app.route('/api/tables/client-css', methods=['GET'])
|
||||
@ -4483,7 +4729,7 @@ def get_tables_css():
|
||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden des Tables-CSS: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Laden des Tables-CSS: {str(e)}")
|
||||
return "/* Advanced Tables CSS konnte nicht geladen werden */", 500
|
||||
|
||||
# ===== MAINTENANCE SYSTEM API =====
|
||||
@ -4508,7 +4754,7 @@ def maintenance_tasks():
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Wartungsaufgaben: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Abrufen der Wartungsaufgaben: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
elif request.method == 'POST':
|
||||
@ -4542,7 +4788,7 @@ def maintenance_tasks():
|
||||
return jsonify({'error': 'Fehler beim Erstellen der Wartungsaufgabe'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen der Wartungsaufgabe: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Erstellen der Wartungsaufgabe: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/maintenance/tasks/<int:task_id>/status', methods=['PUT'])
|
||||
@ -4570,7 +4816,7 @@ def update_maintenance_task_status(task_id):
|
||||
return jsonify({'error': 'Fehler beim Aktualisieren des Status'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Aktualisieren des Wartungsaufgaben-Status: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Aktualisieren des Wartungsaufgaben-Status: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/maintenance/overview', methods=['GET'])
|
||||
@ -4581,7 +4827,7 @@ def get_maintenance_overview():
|
||||
overview = get_maintenance_overview()
|
||||
return jsonify(overview)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Wartungs-Übersicht: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Abrufen der Wartungs-Übersicht: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/maintenance/schedule', methods=['POST'])
|
||||
@ -4609,7 +4855,7 @@ def schedule_maintenance_api():
|
||||
return jsonify({'error': 'Fehler beim Erstellen des Wartungsplans'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Planen der Wartung: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Planen der Wartung: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# ===== MULTI-LOCATION SYSTEM API =====
|
||||
@ -4631,7 +4877,7 @@ def locations():
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Standorte: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Abrufen der Standorte: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
elif request.method == 'POST':
|
||||
@ -4657,7 +4903,7 @@ def locations():
|
||||
return jsonify({'error': 'Fehler beim Erstellen des Standorts'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Erstellen des Standorts: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Erstellen des Standorts: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/locations/<int:location_id>/users', methods=['GET', 'POST'])
|
||||
@ -4675,7 +4921,7 @@ def location_users(location_id):
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Standort-Benutzer: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Abrufen der Standort-Benutzer: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
elif request.method == 'POST':
|
||||
@ -4698,7 +4944,7 @@ def location_users(location_id):
|
||||
return jsonify({'error': 'Fehler bei der Benutzer-Zuweisung'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Benutzer-Zuweisung: {str(e)}")
|
||||
app_logger.error(f"Fehler bei der Benutzer-Zuweisung: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/locations/user/<int:user_id>', methods=['GET'])
|
||||
@ -4718,7 +4964,7 @@ def get_user_locations_api(user_id):
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Abrufen der Benutzer-Standorte: {str(e)}")
|
||||
app_logger.error(f"Fehler beim Abrufen der Benutzer-Standorte: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/locations/distance', methods=['POST'])
|
||||
@ -4741,7 +4987,7 @@ def calculate_distance_api():
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei Entfernungsberechnung: {str(e)}")
|
||||
app_logger.error(f"Fehler bei Entfernungsberechnung: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route('/api/locations/nearest', methods=['POST'])
|
||||
@ -4776,10 +5022,47 @@ def find_nearest_location_api():
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler bei der Suche nach nächstem Standort: {str(e)}")
|
||||
app_logger.error(f"Fehler bei der Suche nach nächstem Standort: {str(e)}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# ===== GASTANTRÄGE API-ROUTEN =====
|
||||
|
||||
def setup_database_with_migrations():
|
||||
"""
|
||||
Datenbank initialisieren und alle erforderlichen Tabellen erstellen.
|
||||
Führt Migrationen für neue Tabellen wie JobOrder durch.
|
||||
"""
|
||||
try:
|
||||
app_logger.info("🔄 Starte Datenbank-Setup und Migrationen...")
|
||||
|
||||
# Standard-Datenbank-Initialisierung
|
||||
init_database()
|
||||
|
||||
# Explizite Migration für JobOrder-Tabelle
|
||||
engine = get_engine()
|
||||
|
||||
# Erstelle alle Tabellen (nur neue werden tatsächlich erstellt)
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
# Prüfe ob JobOrder-Tabelle existiert
|
||||
from sqlalchemy import inspect
|
||||
inspector = inspect(engine)
|
||||
existing_tables = inspector.get_table_names()
|
||||
|
||||
if 'job_orders' in existing_tables:
|
||||
app_logger.info("✅ JobOrder-Tabelle bereits vorhanden")
|
||||
else:
|
||||
# Tabelle manuell erstellen
|
||||
JobOrder.__table__.create(engine, checkfirst=True)
|
||||
app_logger.info("✅ JobOrder-Tabelle erfolgreich erstellt")
|
||||
|
||||
# Initial-Admin erstellen falls nicht vorhanden
|
||||
create_initial_admin()
|
||||
|
||||
app_logger.info("✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen")
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler bei Datenbank-Setup: {str(e)}")
|
||||
raise e
|
||||
|
||||
# ===== STARTUP UND MAIN =====
|
||||
if __name__ == "__main__":
|
||||
@ -5008,100 +5291,3 @@ if __name__ == "__main__":
|
||||
except:
|
||||
pass
|
||||
sys.exit(1)
|
||||
|
||||
def setup_database_with_migrations():
|
||||
"""
|
||||
Datenbank initialisieren und alle erforderlichen Tabellen erstellen.
|
||||
Führt Migrationen für neue Tabellen wie JobOrder durch.
|
||||
"""
|
||||
try:
|
||||
app_logger.info("🔄 Starte Datenbank-Setup und Migrationen...")
|
||||
|
||||
# Standard-Datenbank-Initialisierung
|
||||
init_database()
|
||||
|
||||
# Explizite Migration für JobOrder-Tabelle
|
||||
engine = get_engine()
|
||||
|
||||
# Erstelle alle Tabellen (nur neue werden tatsächlich erstellt)
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
# Prüfe ob JobOrder-Tabelle existiert
|
||||
from sqlalchemy import inspect
|
||||
inspector = inspect(engine)
|
||||
existing_tables = inspector.get_table_names()
|
||||
|
||||
if 'job_orders' in existing_tables:
|
||||
app_logger.info("✅ JobOrder-Tabelle bereits vorhanden")
|
||||
else:
|
||||
# Tabelle manuell erstellen
|
||||
JobOrder.__table__.create(engine, checkfirst=True)
|
||||
app_logger.info("✅ JobOrder-Tabelle erfolgreich erstellt")
|
||||
|
||||
# Initial-Admin erstellen falls nicht vorhanden
|
||||
create_initial_admin()
|
||||
|
||||
app_logger.info("✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen")
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler bei Datenbank-Setup: {str(e)}")
|
||||
raise e
|
||||
|
||||
@app.route("/admin/printers/<int:printer_id>/settings")
|
||||
@login_required
|
||||
def admin_printer_settings_page(printer_id):
|
||||
"""Zeigt die Drucker-Einstellungsseite an."""
|
||||
if not current_user.is_admin:
|
||||
flash("Sie haben keine Berechtigung für den Admin-Bereich.", "error")
|
||||
return redirect(url_for("index"))
|
||||
|
||||
db_session = get_db_session()
|
||||
try:
|
||||
printer = db_session.get(Printer, printer_id)
|
||||
if not printer:
|
||||
flash("Drucker nicht gefunden.", "error")
|
||||
return redirect(url_for("admin_page"))
|
||||
|
||||
printer_data = {
|
||||
"id": printer.id,
|
||||
"name": printer.name,
|
||||
"model": printer.model or 'Unbekanntes Modell',
|
||||
"location": printer.location or 'Unbekannter Standort',
|
||||
"mac_address": printer.mac_address,
|
||||
"plug_ip": printer.plug_ip,
|
||||
"status": printer.status or "offline",
|
||||
"active": printer.active if hasattr(printer, 'active') else True,
|
||||
"created_at": printer.created_at.isoformat() if printer.created_at else datetime.now().isoformat()
|
||||
}
|
||||
|
||||
db_session.close()
|
||||
return render_template("admin_printer_settings.html", printer=printer_data)
|
||||
|
||||
except Exception as e:
|
||||
db_session.close()
|
||||
app_logger.error(f"Fehler beim Laden der Drucker-Einstellungen: {str(e)}")
|
||||
flash("Fehler beim Laden der Drucker-Daten.", "error")
|
||||
return redirect(url_for("admin_page"))
|
||||
# Erstelle alle Tabellen (nur neue werden tatsächlich erstellt)
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
# Prüfe ob JobOrder-Tabelle existiert
|
||||
from sqlalchemy import inspect
|
||||
inspector = inspect(engine)
|
||||
existing_tables = inspector.get_table_names()
|
||||
|
||||
if 'job_orders' in existing_tables:
|
||||
app_logger.info("✅ JobOrder-Tabelle bereits vorhanden")
|
||||
else:
|
||||
# Tabelle manuell erstellen
|
||||
JobOrder.__table__.create(engine, checkfirst=True)
|
||||
app_logger.info("✅ JobOrder-Tabelle erfolgreich erstellt")
|
||||
|
||||
# Initial-Admin erstellen falls nicht vorhanden
|
||||
create_initial_admin()
|
||||
|
||||
app_logger.info("✅ Datenbank-Setup und Migrationen erfolgreich abgeschlossen")
|
||||
|
||||
except Exception as e:
|
||||
app_logger.error(f"❌ Fehler bei Datenbank-Setup: {str(e)}")
|
||||
raise e
|
1
backend/docs/GASTAUFTRAG_OTP_DOKUMENTATION.md
Normal file
1
backend/docs/GASTAUFTRAG_OTP_DOKUMENTATION.md
Normal file
@ -0,0 +1 @@
|
||||
|
1
backend/static/js/charts/chart.min.js
vendored
Normal file
1
backend/static/js/charts/chart.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
412
backend/templates/guest_status_check.html
Normal file
412
backend/templates/guest_status_check.html
Normal file
@ -0,0 +1,412 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gastauftrag Status-Abfrage - Mercedes-Benz TBA Marienfelde</title>
|
||||
<link href="{{ url_for('static', filename='css/output.css') }}" rel="stylesheet">
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
<style>
|
||||
.status-card {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .status-card {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||
border-color: #334155;
|
||||
}
|
||||
|
||||
.otp-input {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
letter-spacing: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.status-pending { background-color: #fef3c7; color: #92400e; }
|
||||
.status-approved { background-color: #d1fae5; color: #065f46; }
|
||||
.status-rejected { background-color: #fee2e2; color: #991b1b; }
|
||||
|
||||
.loading-spinner {
|
||||
border: 2px solid #f3f4f6;
|
||||
border-top: 2px solid #0073ce;
|
||||
border-radius: 50%;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-50 font-mercedes">
|
||||
<div class="min-h-screen flex items-center justify-center p-4">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<div class="mx-auto h-12 w-12 bg-mercedes-blue rounded-full flex items-center justify-center">
|
||||
<svg class="h-6 w-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="mt-6 text-3xl font-bold text-gray-900">
|
||||
Auftragsstatus prüfen
|
||||
</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
Geben Sie Ihren Statuscode ein, um Informationen über Ihren Gastauftrag zu erhalten
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Status-Abfrage-Formular -->
|
||||
<div class="status-card p-6" id="query-form">
|
||||
<form id="status-form" class="space-y-6">
|
||||
<div>
|
||||
<label for="otp_code" class="block text-sm font-medium text-gray-700">
|
||||
Statuscode (16 Zeichen)
|
||||
</label>
|
||||
<input type="text"
|
||||
id="otp_code"
|
||||
name="otp_code"
|
||||
maxlength="16"
|
||||
class="otp-input mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-mercedes-blue focus:border-mercedes-blue focus:z-10 sm:text-sm"
|
||||
placeholder="XXXXXXXXXXXXXXXX"
|
||||
required>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Der Code wurde Ihnen bei der Antragsstellung mitgeteilt
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700">
|
||||
E-Mail-Adresse (optional)
|
||||
</label>
|
||||
<input type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
class="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-mercedes-blue focus:border-mercedes-blue focus:z-10 sm:text-sm"
|
||||
placeholder="ihre.email@example.com">
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Zusätzliche Sicherheit (empfohlen)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit"
|
||||
class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-mercedes-blue hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<span id="submit-text">Status prüfen</span>
|
||||
<span id="loading-spinner" class="loading-spinner ml-2 hidden"></span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Status-Ergebnis -->
|
||||
<div id="status-result" class="hidden">
|
||||
<div class="status-card p-6">
|
||||
<div id="status-content">
|
||||
<!-- Wird per JavaScript gefüllt -->
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex gap-3">
|
||||
<button onclick="resetForm()"
|
||||
class="flex-1 py-2 px-4 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Neue Abfrage
|
||||
</button>
|
||||
<button onclick="refreshStatus()"
|
||||
class="flex-1 py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-mercedes-blue hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fehleranzeige -->
|
||||
<div id="error-message" class="hidden">
|
||||
<div class="status-card p-6 border-l-4 border-red-500">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">Fehler</h3>
|
||||
<div class="mt-2 text-sm text-red-700" id="error-text">
|
||||
<!-- Wird per JavaScript gefüllt -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button onclick="resetForm()"
|
||||
class="py-2 px-4 border border-transparent rounded-md text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="text-center">
|
||||
<p class="text-xs text-gray-500">
|
||||
Mercedes-Benz Technische Berufsausbildung Marienfelde<br>
|
||||
<a href="/guest/request" class="text-mercedes-blue hover:underline">Neuen Antrag stellen</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentRequestData = null;
|
||||
|
||||
// Formular-Submit-Handler
|
||||
document.getElementById('status-form').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const otpCode = document.getElementById('otp_code').value.trim();
|
||||
const email = document.getElementById('email').value.trim();
|
||||
|
||||
if (!otpCode) {
|
||||
showError('Bitte geben Sie Ihren Statuscode ein.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (otpCode.length !== 16) {
|
||||
showError('Der Statuscode muss genau 16 Zeichen lang sein.');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
hideAll();
|
||||
|
||||
try {
|
||||
const response = await fetch('/guest/api/guest/status', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
otp_code: otpCode,
|
||||
email: email || undefined
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
currentRequestData = data.request;
|
||||
showStatus(data.request);
|
||||
} else {
|
||||
showError(data.message || 'Ungültiger Code oder E-Mail-Adresse');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Fehler bei Status-Abfrage:', error);
|
||||
showError('Verbindungsfehler. Bitte versuchen Sie es später erneut.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Status anzeigen
|
||||
function showStatus(request) {
|
||||
const statusContent = document.getElementById('status-content');
|
||||
|
||||
// Status-Badge
|
||||
let statusBadge = '';
|
||||
let statusIcon = '';
|
||||
|
||||
switch (request.status) {
|
||||
case 'pending':
|
||||
statusBadge = '<span class="status-badge status-pending">🕒 In Bearbeitung</span>';
|
||||
statusIcon = '🕒';
|
||||
break;
|
||||
case 'approved':
|
||||
statusBadge = '<span class="status-badge status-approved">✅ Genehmigt</span>';
|
||||
statusIcon = '✅';
|
||||
break;
|
||||
case 'rejected':
|
||||
statusBadge = '<span class="status-badge status-rejected">❌ Abgelehnt</span>';
|
||||
statusIcon = '❌';
|
||||
break;
|
||||
default:
|
||||
statusBadge = '<span class="status-badge">❓ Unbekannt</span>';
|
||||
statusIcon = '❓';
|
||||
}
|
||||
|
||||
let html = `
|
||||
<div class="text-center mb-6">
|
||||
<div class="text-4xl mb-2">${statusIcon}</div>
|
||||
<h3 class="text-xl font-semibold text-gray-900 mb-2">
|
||||
Antrag von ${request.name}
|
||||
</h3>
|
||||
${statusBadge}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="font-medium text-gray-900 mb-2">Antragsdetails</h4>
|
||||
<dl class="space-y-1 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-600">Erstellt am:</dt>
|
||||
<dd class="text-gray-900">${formatDate(request.created_at)}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-600">Dauer:</dt>
|
||||
<dd class="text-gray-900">${request.duration_min} Minuten</dd>
|
||||
</div>
|
||||
${request.file_name ? `
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-gray-600">Datei:</dt>
|
||||
<dd class="text-gray-900">${request.file_name}</dd>
|
||||
</div>
|
||||
` : ''}
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<p class="text-sm text-blue-800">
|
||||
${request.message}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
${request.status === 'approved' && request.can_start_job ? `
|
||||
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<h4 class="font-medium text-green-800 mb-2">🎯 Bereit zum Drucken!</h4>
|
||||
<p class="text-sm text-green-700 mb-3">
|
||||
Ihr Auftrag wurde genehmigt. Sie können mit dem 3D-Druck beginnen.
|
||||
</p>
|
||||
<a href="/guest/start-job"
|
||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700">
|
||||
🚀 Jetzt drucken
|
||||
</a>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${request.status === 'rejected' && request.rejection_reason ? `
|
||||
<div class="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<h4 class="font-medium text-red-800 mb-2">Ablehnungsgrund:</h4>
|
||||
<p class="text-sm text-red-700">${request.rejection_reason}</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
${request.job ? `
|
||||
<div class="bg-indigo-50 border border-indigo-200 rounded-lg p-4">
|
||||
<h4 class="font-medium text-indigo-800 mb-2">📋 Job-Informationen</h4>
|
||||
<dl class="space-y-1 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-indigo-600">Job-Name:</dt>
|
||||
<dd class="text-indigo-900">${request.job.name}</dd>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-indigo-600">Status:</dt>
|
||||
<dd class="text-indigo-900">${request.job.status}</dd>
|
||||
</div>
|
||||
${request.job.printer_name ? `
|
||||
<div class="flex justify-between">
|
||||
<dt class="text-indigo-600">Drucker:</dt>
|
||||
<dd class="text-indigo-900">${request.job.printer_name}</dd>
|
||||
</div>
|
||||
` : ''}
|
||||
</dl>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
statusContent.innerHTML = html;
|
||||
document.getElementById('status-result').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Fehler anzeigen
|
||||
function showError(message) {
|
||||
document.getElementById('error-text').textContent = message;
|
||||
document.getElementById('error-message').classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Loading-Zustand setzen
|
||||
function setLoading(loading) {
|
||||
const submitText = document.getElementById('submit-text');
|
||||
const loadingSpinner = document.getElementById('loading-spinner');
|
||||
const submitButton = document.querySelector('button[type="submit"]');
|
||||
|
||||
if (loading) {
|
||||
submitText.textContent = 'Prüfe...';
|
||||
loadingSpinner.classList.remove('hidden');
|
||||
submitButton.disabled = true;
|
||||
} else {
|
||||
submitText.textContent = 'Status prüfen';
|
||||
loadingSpinner.classList.add('hidden');
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Alle Anzeigen ausblenden
|
||||
function hideAll() {
|
||||
document.getElementById('status-result').classList.add('hidden');
|
||||
document.getElementById('error-message').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Formular zurücksetzen
|
||||
function resetForm() {
|
||||
document.getElementById('status-form').reset();
|
||||
hideAll();
|
||||
document.getElementById('query-form').classList.remove('hidden');
|
||||
currentRequestData = null;
|
||||
}
|
||||
|
||||
// Status aktualisieren
|
||||
function refreshStatus() {
|
||||
if (currentRequestData) {
|
||||
const otpCode = document.getElementById('otp_code').value;
|
||||
const email = document.getElementById('email').value;
|
||||
|
||||
// Formular erneut abschicken
|
||||
document.getElementById('status-form').dispatchEvent(new Event('submit'));
|
||||
}
|
||||
}
|
||||
|
||||
// Datum formatieren
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return 'Unbekannt';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
// OTP-Input Formatierung
|
||||
document.getElementById('otp_code').addEventListener('input', function(e) {
|
||||
// Nur alphanumerische Zeichen erlauben
|
||||
e.target.value = e.target.value.replace(/[^A-Fa-f0-9]/g, '').toUpperCase();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
229
backend/utils/offline_config.py
Normal file
229
backend/utils/offline_config.py
Normal file
@ -0,0 +1,229 @@
|
||||
"""
|
||||
Offline-Konfiguration für MYP-System
|
||||
===================================
|
||||
|
||||
Konfiguriert das System für den Offline-Betrieb ohne Internetverbindung.
|
||||
Stellt Fallback-Lösungen für internetabhängige Funktionen bereit.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from utils.logging_config import get_logger
|
||||
|
||||
logger = get_logger("offline_config")
|
||||
|
||||
# ===== OFFLINE-MODUS KONFIGURATION =====
|
||||
OFFLINE_MODE = True # Produktionseinstellung - System läuft offline
|
||||
|
||||
# ===== OFFLINE-KOMPATIBILITÄT PRÜFUNGEN =====
|
||||
|
||||
def check_internet_connectivity() -> bool:
|
||||
"""
|
||||
Prüft ob eine Internetverbindung verfügbar ist.
|
||||
Im Offline-Modus gibt immer False zurück.
|
||||
|
||||
Returns:
|
||||
bool: True wenn Internet verfügbar, False im Offline-Modus
|
||||
"""
|
||||
if OFFLINE_MODE:
|
||||
return False
|
||||
|
||||
# In einem echten Online-Modus könnte hier eine echte Prüfung stehen
|
||||
try:
|
||||
import socket
|
||||
socket.create_connection(("8.8.8.8", 53), timeout=3)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def is_oauth_available() -> bool:
|
||||
"""
|
||||
Prüft ob OAuth-Funktionalität verfügbar ist.
|
||||
|
||||
Returns:
|
||||
bool: False im Offline-Modus
|
||||
"""
|
||||
return not OFFLINE_MODE and check_internet_connectivity()
|
||||
|
||||
def is_email_sending_available() -> bool:
|
||||
"""
|
||||
Prüft ob E-Mail-Versand verfügbar ist.
|
||||
|
||||
Returns:
|
||||
bool: False im Offline-Modus (nur Logging)
|
||||
"""
|
||||
return not OFFLINE_MODE and check_internet_connectivity()
|
||||
|
||||
def is_cdn_available() -> bool:
|
||||
"""
|
||||
Prüft ob CDN-Links verfügbar sind.
|
||||
|
||||
Returns:
|
||||
bool: False im Offline-Modus (lokale Fallbacks verwenden)
|
||||
"""
|
||||
return not OFFLINE_MODE and check_internet_connectivity()
|
||||
|
||||
# ===== CDN FALLBACK-KONFIGURATION =====
|
||||
|
||||
CDN_FALLBACKS = {
|
||||
# Chart.js CDN -> Lokale Datei
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.min.js": "/static/js/charts/chart.min.js",
|
||||
"https://cdn.jsdelivr.net/npm/chart.js": "/static/js/charts/chart.min.js",
|
||||
|
||||
# FontAwesome (bereits lokal verfügbar)
|
||||
"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css": "/static/fontawesome/css/all.min.css",
|
||||
|
||||
# Weitere CDN-Fallbacks können hier hinzugefügt werden
|
||||
}
|
||||
|
||||
def get_local_asset_path(cdn_url: str) -> Optional[str]:
|
||||
"""
|
||||
Gibt den lokalen Pfad für eine CDN-URL zurück.
|
||||
|
||||
Args:
|
||||
cdn_url: URL des CDN-Assets
|
||||
|
||||
Returns:
|
||||
str: Lokaler Pfad oder None wenn kein Fallback verfügbar
|
||||
"""
|
||||
return CDN_FALLBACKS.get(cdn_url)
|
||||
|
||||
def replace_cdn_links(html_content: str) -> str:
|
||||
"""
|
||||
Ersetzt CDN-Links durch lokale Fallbacks im HTML-Inhalt.
|
||||
|
||||
Args:
|
||||
html_content: HTML-Inhalt mit CDN-Links
|
||||
|
||||
Returns:
|
||||
str: HTML-Inhalt mit lokalen Links
|
||||
"""
|
||||
if not OFFLINE_MODE:
|
||||
return html_content
|
||||
|
||||
modified_content = html_content
|
||||
|
||||
for cdn_url, local_path in CDN_FALLBACKS.items():
|
||||
if cdn_url in modified_content:
|
||||
modified_content = modified_content.replace(cdn_url, local_path)
|
||||
logger.info(f"🔄 CDN-Link ersetzt: {cdn_url} -> {local_path}")
|
||||
|
||||
return modified_content
|
||||
|
||||
# ===== SECURITY POLICY ANPASSUNGEN =====
|
||||
|
||||
def get_offline_csp_policy() -> Dict[str, List[str]]:
|
||||
"""
|
||||
Gibt CSP-Policy für Offline-Modus zurück.
|
||||
Entfernt externe CDN-Domains aus der Policy.
|
||||
|
||||
Returns:
|
||||
Dict: CSP-Policy ohne externe Domains
|
||||
"""
|
||||
if not OFFLINE_MODE:
|
||||
# Online-Modus: Originale Policy mit CDNs
|
||||
return {
|
||||
"script-src": [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
"'unsafe-eval'",
|
||||
"https://cdn.jsdelivr.net",
|
||||
"https://unpkg.com",
|
||||
"https://cdnjs.cloudflare.com"
|
||||
],
|
||||
"style-src": [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
"https://fonts.googleapis.com",
|
||||
"https://cdn.jsdelivr.net"
|
||||
],
|
||||
"font-src": [
|
||||
"'self'",
|
||||
"https://fonts.gstatic.com"
|
||||
]
|
||||
}
|
||||
else:
|
||||
# Offline-Modus: Nur lokale Ressourcen
|
||||
return {
|
||||
"script-src": [
|
||||
"'self'",
|
||||
"'unsafe-inline'",
|
||||
"'unsafe-eval'"
|
||||
],
|
||||
"style-src": [
|
||||
"'self'",
|
||||
"'unsafe-inline'"
|
||||
],
|
||||
"font-src": [
|
||||
"'self'"
|
||||
],
|
||||
"img-src": [
|
||||
"'self'",
|
||||
"data:"
|
||||
]
|
||||
}
|
||||
|
||||
# ===== OFFLINE-MODUS HILFSFUNKTIONEN =====
|
||||
|
||||
def log_offline_mode_status():
|
||||
"""Loggt den aktuellen Offline-Modus Status."""
|
||||
if OFFLINE_MODE:
|
||||
logger.info("🌐 System läuft im OFFLINE-MODUS")
|
||||
logger.info(" ❌ OAuth deaktiviert")
|
||||
logger.info(" ❌ E-Mail-Versand deaktiviert (nur Logging)")
|
||||
logger.info(" ❌ CDN-Links werden durch lokale Dateien ersetzt")
|
||||
logger.info(" ✅ Alle Kernfunktionen verfügbar")
|
||||
else:
|
||||
logger.info("🌐 System läuft im ONLINE-MODUS")
|
||||
logger.info(" ✅ OAuth verfügbar")
|
||||
logger.info(" ✅ E-Mail-Versand verfügbar")
|
||||
logger.info(" ✅ CDN-Links verfügbar")
|
||||
|
||||
def get_feature_availability() -> Dict[str, bool]:
|
||||
"""
|
||||
Gibt die Verfügbarkeit verschiedener Features zurück.
|
||||
|
||||
Returns:
|
||||
Dict: Feature-Verfügbarkeit
|
||||
"""
|
||||
return {
|
||||
"oauth": is_oauth_available(),
|
||||
"email_sending": is_email_sending_available(),
|
||||
"cdn_resources": is_cdn_available(),
|
||||
"offline_mode": OFFLINE_MODE,
|
||||
"core_functionality": True, # Kernfunktionen immer verfügbar
|
||||
"printer_control": True, # Drucker-Steuerung immer verfügbar
|
||||
"job_management": True, # Job-Verwaltung immer verfügbar
|
||||
"user_management": True # Benutzer-Verwaltung immer verfügbar
|
||||
}
|
||||
|
||||
# ===== STARTUP-FUNKTIONEN =====
|
||||
|
||||
def initialize_offline_mode():
|
||||
"""Initialisiert den Offline-Modus beim System-Start."""
|
||||
log_offline_mode_status()
|
||||
|
||||
if OFFLINE_MODE:
|
||||
logger.info("🔧 Initialisiere Offline-Modus-Anpassungen...")
|
||||
|
||||
# Prüfe ob lokale Chart.js verfügbar ist
|
||||
chart_js_path = "static/js/charts/chart.min.js"
|
||||
if not os.path.exists(chart_js_path):
|
||||
logger.warning(f"⚠️ Lokale Chart.js nicht gefunden: {chart_js_path}")
|
||||
logger.warning(" Diagramme könnten nicht funktionieren")
|
||||
else:
|
||||
logger.info(f"✅ Lokale Chart.js gefunden: {chart_js_path}")
|
||||
|
||||
# Prüfe weitere lokale Assets
|
||||
fontawesome_path = "static/fontawesome/css/all.min.css"
|
||||
if not os.path.exists(fontawesome_path):
|
||||
logger.warning(f"⚠️ Lokale FontAwesome nicht gefunden: {fontawesome_path}")
|
||||
else:
|
||||
logger.info(f"✅ Lokale FontAwesome gefunden: {fontawesome_path}")
|
||||
|
||||
logger.info("✅ Offline-Modus erfolgreich initialisiert")
|
||||
|
||||
# Beim Import automatisch initialisieren
|
||||
initialize_offline_mode()
|
Loading…
x
Reference in New Issue
Block a user