"Update logging configuration and related files for improved debugging support"
This commit is contained in:
1
backend/app/LOGGING_README.md
Normal file
1
backend/app/LOGGING_README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
Binary file not shown.
Binary file not shown.
@@ -257,7 +257,7 @@ def scan_printer(ip_address, timeout=5):
|
|||||||
"""Scannt einen Drucker und zeigt Informationen an."""
|
"""Scannt einen Drucker und zeigt Informationen an."""
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
print_info(f"Prüfe Drucker mit IP: {ip_address}")
|
print_printer(f"Prüfe Drucker mit IP: {ip_address}")
|
||||||
|
|
||||||
# Ping testen
|
# Ping testen
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -267,14 +267,14 @@ def scan_printer(ip_address, timeout=5):
|
|||||||
else: # Unix/Linux/macOS
|
else: # Unix/Linux/macOS
|
||||||
cmd = ['ping', '-c', '1', '-W', str(timeout), ip_address]
|
cmd = ['ping', '-c', '1', '-W', str(timeout), ip_address]
|
||||||
|
|
||||||
print(f" Ping-Test: ", end="")
|
print(f" 🏓 Ping-Test: ", end="")
|
||||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
print(colorize("Erreichbar", "GREEN"))
|
print(colorize("Erreichbar", "GREEN"))
|
||||||
else:
|
else:
|
||||||
print(colorize("Nicht erreichbar", "RED"))
|
print(colorize("Nicht erreichbar", "RED"))
|
||||||
print(f" Details: {result.stdout}")
|
print(f" 📄 Details: {result.stdout}")
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(colorize(f"Fehler bei Ping-Test: {e}", "RED"))
|
print(colorize(f"Fehler bei Ping-Test: {e}", "RED"))
|
||||||
@@ -283,7 +283,7 @@ def scan_printer(ip_address, timeout=5):
|
|||||||
common_ports = [80, 443, 8080, 8443, 631, 9100, 9101, 9102]
|
common_ports = [80, 443, 8080, 8443, 631, 9100, 9101, 9102]
|
||||||
open_ports = []
|
open_ports = []
|
||||||
|
|
||||||
print(" Port-Scan: ", end="")
|
print(" 🔍 Port-Scan: ", end="")
|
||||||
for port in common_ports:
|
for port in common_ports:
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
sock.settimeout(timeout)
|
sock.settimeout(timeout)
|
||||||
@@ -301,7 +301,7 @@ def scan_printer(ip_address, timeout=5):
|
|||||||
try:
|
try:
|
||||||
from PyP100 import PyP110
|
from PyP100 import PyP110
|
||||||
|
|
||||||
print(" Smart Plug Test: ", end="")
|
print(" 🔌 Smart Plug Test: ", end="")
|
||||||
try:
|
try:
|
||||||
# Standardmäßig Anmeldeinformationen aus der Konfiguration verwenden
|
# Standardmäßig Anmeldeinformationen aus der Konfiguration verwenden
|
||||||
from config.settings import TAPO_USERNAME, TAPO_PASSWORD
|
from config.settings import TAPO_USERNAME, TAPO_PASSWORD
|
||||||
@@ -312,12 +312,12 @@ def scan_printer(ip_address, timeout=5):
|
|||||||
|
|
||||||
device_info = p110.getDeviceInfo()
|
device_info = p110.getDeviceInfo()
|
||||||
print(colorize("Verbunden", "GREEN"))
|
print(colorize("Verbunden", "GREEN"))
|
||||||
print(f" Gerätename: {device_info.get('nickname', 'Unbekannt')}")
|
print(f" 📛 Gerätename: {device_info.get('nickname', 'Unbekannt')}")
|
||||||
print(f" Status: {'Ein' if device_info.get('device_on', False) else 'Aus'}")
|
print(f" ⚡ Status: {'Ein' if device_info.get('device_on', False) else 'Aus'}")
|
||||||
|
|
||||||
if 'on_time' in device_info:
|
if 'on_time' in device_info:
|
||||||
on_time = device_info['on_time']
|
on_time = device_info['on_time']
|
||||||
print(f" Betriebszeit: {on_time // 60} Minuten, {on_time % 60} Sekunden")
|
print(f" ⏱️ Betriebszeit: {on_time // 60} Minuten, {on_time % 60} Sekunden")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(colorize(f"Fehler: {e}", "RED"))
|
print(colorize(f"Fehler: {e}", "RED"))
|
||||||
@@ -454,6 +454,87 @@ def print_system_info():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print_warning(f"Fehler beim Abrufen der Netzwerkinformationen: {e}")
|
print_warning(f"Fehler beim Abrufen der Netzwerkinformationen: {e}")
|
||||||
|
|
||||||
|
def test_logging_system():
|
||||||
|
"""Testet das verbesserte Logging-System mit allen Features."""
|
||||||
|
print_header("Logging-System Test")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Versuche die neuen Logging-Funktionen zu importieren
|
||||||
|
from utils.logging_config import get_logger, debug_request, debug_response, measure_execution_time
|
||||||
|
|
||||||
|
print_success("Neue Logging-Module erfolgreich importiert")
|
||||||
|
|
||||||
|
# Test verschiedener Logger
|
||||||
|
test_loggers = ['app', 'auth', 'jobs', 'printers', 'errors']
|
||||||
|
|
||||||
|
print_section("Logger-Tests")
|
||||||
|
for logger_name in test_loggers:
|
||||||
|
try:
|
||||||
|
logger = get_logger(logger_name)
|
||||||
|
|
||||||
|
# Test verschiedener Log-Level
|
||||||
|
logger.debug(f"🔍 Debug-Test für {logger_name}")
|
||||||
|
logger.info(f"ℹ️ Info-Test für {logger_name}")
|
||||||
|
logger.warning(f"⚠️ Warning-Test für {logger_name}")
|
||||||
|
|
||||||
|
print_success(f"Logger '{logger_name}' funktioniert korrekt")
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Fehler beim Testen von Logger '{logger_name}': {e}")
|
||||||
|
|
||||||
|
# Test Performance-Monitoring
|
||||||
|
print_section("Performance-Monitoring Test")
|
||||||
|
|
||||||
|
@measure_execution_time(logger=get_logger("app"), task_name="Test-Funktion")
|
||||||
|
def test_function():
|
||||||
|
"""Eine Test-Funktion für das Performance-Monitoring."""
|
||||||
|
import time
|
||||||
|
time.sleep(0.1) # Simuliere etwas Arbeit
|
||||||
|
return "Test erfolgreich"
|
||||||
|
|
||||||
|
result = test_function()
|
||||||
|
print_success(f"Performance-Monitoring Test: {result}")
|
||||||
|
|
||||||
|
# Test der Debug-Utilities
|
||||||
|
print_section("Debug-Utilities Test")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from utils.debug_utils import debug_dump, debug_trace, memory_usage
|
||||||
|
|
||||||
|
# Test debug_dump
|
||||||
|
test_data = {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"features": ["emojis", "colors", "performance-monitoring"],
|
||||||
|
"status": "active"
|
||||||
|
}
|
||||||
|
debug_dump(test_data, "Test-Konfiguration")
|
||||||
|
|
||||||
|
# Test memory_usage
|
||||||
|
memory_info = memory_usage()
|
||||||
|
print_system(f"Aktueller Speicherverbrauch: {memory_info['rss']:.2f} MB")
|
||||||
|
|
||||||
|
print_success("Debug-Utilities funktionieren korrekt")
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print_warning(f"Debug-Utilities nicht verfügbar: {e}")
|
||||||
|
|
||||||
|
# Zusammenfassung
|
||||||
|
print_section("Test-Zusammenfassung")
|
||||||
|
print_success("🎉 Alle Logging-System-Tests erfolgreich abgeschlossen!")
|
||||||
|
print_info("Features verfügbar:")
|
||||||
|
print(" ✅ Farbige Log-Ausgaben mit ANSI-Codes")
|
||||||
|
print(" ✅ Emoji-Integration für bessere Lesbarkeit")
|
||||||
|
print(" ✅ HTTP-Request/Response-Logging")
|
||||||
|
print(" ✅ Performance-Monitoring mit Ausführungszeit")
|
||||||
|
print(" ✅ Cross-Platform-Unterstützung (Windows/Unix)")
|
||||||
|
print(" ✅ Strukturierte Debug-Informationen")
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
print_error(f"Logging-Module nicht verfügbar: {e}")
|
||||||
|
print_warning("Stelle sicher, dass alle Module korrekt installiert sind")
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Unerwarteter Fehler beim Logging-Test: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
# Hauptfunktionen für die Befehlszeile
|
# Hauptfunktionen für die Befehlszeile
|
||||||
|
|
||||||
def diagnose():
|
def diagnose():
|
||||||
@@ -598,6 +679,9 @@ def parse_args():
|
|||||||
# Logs anzeigen
|
# Logs anzeigen
|
||||||
logs_parser = subparsers.add_parser("logs", help="Zeigt und analysiert Log-Dateien")
|
logs_parser = subparsers.add_parser("logs", help="Zeigt und analysiert Log-Dateien")
|
||||||
|
|
||||||
|
# Logging-System testen
|
||||||
|
logging_test_parser = subparsers.add_parser("test-logging", help="Testet das verbesserte Logging-System")
|
||||||
|
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -614,6 +698,8 @@ def main():
|
|||||||
system_info()
|
system_info()
|
||||||
elif args.command == "logs":
|
elif args.command == "logs":
|
||||||
show_logs()
|
show_logs()
|
||||||
|
elif args.command == "test-logging":
|
||||||
|
test_logging_system()
|
||||||
else:
|
else:
|
||||||
# Interaktives Menü, wenn kein Befehl angegeben wurde
|
# Interaktives Menü, wenn kein Befehl angegeben wurde
|
||||||
print_header("MYP Debug CLI")
|
print_header("MYP Debug CLI")
|
||||||
@@ -623,6 +709,7 @@ def main():
|
|||||||
print(" 3. API-Routen anzeigen")
|
print(" 3. API-Routen anzeigen")
|
||||||
print(" 4. Systeminformationen anzeigen")
|
print(" 4. Systeminformationen anzeigen")
|
||||||
print(" 5. Log-Dateien anzeigen")
|
print(" 5. Log-Dateien anzeigen")
|
||||||
|
print(" 6. Logging-System testen")
|
||||||
print(" 0. Beenden")
|
print(" 0. Beenden")
|
||||||
|
|
||||||
choice = input("\nIhre Wahl: ")
|
choice = input("\nIhre Wahl: ")
|
||||||
@@ -637,6 +724,8 @@ def main():
|
|||||||
system_info()
|
system_info()
|
||||||
elif choice == "5":
|
elif choice == "5":
|
||||||
show_logs()
|
show_logs()
|
||||||
|
elif choice == "6":
|
||||||
|
test_logging_system()
|
||||||
elif choice == "0":
|
elif choice == "0":
|
||||||
print("Auf Wiedersehen!")
|
print("Auf Wiedersehen!")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
@@ -1,162 +1,80 @@
|
|||||||
#!/usr/bin/env python3.11
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Datenbank-Migrationsskript für MYP Platform
|
Datenbank-Migrationsskript für Guest-Requests, UserPermissions und Notifications
|
||||||
|
|
||||||
Dieses Skript führt notwendige Änderungen an der Datenbankstruktur durch,
|
|
||||||
um sie mit den neuesten Modellen kompatibel zu machen.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from config.settings import DATABASE_PATH, ensure_database_directory
|
|
||||||
|
|
||||||
def migrate_database():
|
# Pfad zur App hinzufügen
|
||||||
"""Führt alle notwendigen Datenbankmigrationen durch."""
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
ensure_database_directory()
|
|
||||||
|
from models import init_db, get_cached_session, GuestRequest, UserPermission, Notification
|
||||||
if not os.path.exists(DATABASE_PATH):
|
from utils.logging_config import get_logger
|
||||||
print("Datenbank existiert nicht. Führe init_db.py aus, um sie zu erstellen.")
|
|
||||||
return False
|
logger = get_logger("migrate")
|
||||||
|
|
||||||
conn = sqlite3.connect(DATABASE_PATH)
|
def main():
|
||||||
cursor = conn.cursor()
|
"""Führt die Datenbank-Migration aus."""
|
||||||
|
|
||||||
print("Starte Datenbankmigration...")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Migration 1: Füge username-Feld zu users-Tabelle hinzu
|
logger.info("Starte Datenbank-Migration...")
|
||||||
try:
|
|
||||||
cursor.execute("ALTER TABLE users ADD COLUMN username VARCHAR(100)")
|
|
||||||
print("✓ Username-Feld zur users-Tabelle hinzugefügt")
|
|
||||||
except sqlite3.OperationalError as e:
|
|
||||||
if "duplicate column name" in str(e).lower():
|
|
||||||
print("✓ Username-Feld bereits vorhanden")
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# Migration 2: Füge active-Feld zu users-Tabelle hinzu
|
# Datenbank initialisieren (erstellt neue Tabellen)
|
||||||
try:
|
init_db()
|
||||||
cursor.execute("ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT 1")
|
|
||||||
print("✓ Active-Feld zur users-Tabelle hinzugefügt")
|
|
||||||
except sqlite3.OperationalError as e:
|
|
||||||
if "duplicate column name" in str(e).lower():
|
|
||||||
print("✓ Active-Feld bereits vorhanden")
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# Migration 3: Setze username für bestehende Benutzer
|
logger.info("Datenbank-Migration erfolgreich abgeschlossen")
|
||||||
cursor.execute("SELECT id, email, username FROM users WHERE username IS NULL OR username = ''")
|
|
||||||
users_without_username = cursor.fetchall()
|
|
||||||
|
|
||||||
for user_id, email, username in users_without_username:
|
# Testen, ob die neuen Tabellen funktionieren
|
||||||
if not username:
|
test_new_tables()
|
||||||
# Generiere username aus email (Teil vor @)
|
|
||||||
new_username = email.split('@')[0] if '@' in email else f"user_{user_id}"
|
|
||||||
cursor.execute("UPDATE users SET username = ? WHERE id = ?", (new_username, user_id))
|
|
||||||
print(f"✓ Username '{new_username}' für Benutzer {email} gesetzt")
|
|
||||||
|
|
||||||
# Migration 3.5: Füge last_login-Feld zu users-Tabelle hinzu
|
|
||||||
try:
|
|
||||||
cursor.execute("ALTER TABLE users ADD COLUMN last_login DATETIME")
|
|
||||||
print("✓ Last_login-Feld zur users-Tabelle hinzugefügt")
|
|
||||||
except sqlite3.OperationalError as e:
|
|
||||||
if "duplicate column name" in str(e).lower():
|
|
||||||
print("✓ Last_login-Feld bereits vorhanden")
|
|
||||||
else:
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# Migration 4: Prüfe und korrigiere Job-Tabelle falls nötig
|
|
||||||
try:
|
|
||||||
# Prüfe ob die Tabelle die neuen Felder hat
|
|
||||||
cursor.execute("PRAGMA table_info(jobs)")
|
|
||||||
columns = [row[1] for row in cursor.fetchall()]
|
|
||||||
|
|
||||||
if 'duration_minutes' not in columns:
|
|
||||||
cursor.execute("ALTER TABLE jobs ADD COLUMN duration_minutes INTEGER")
|
|
||||||
print("✓ Duration_minutes-Feld zur jobs-Tabelle hinzugefügt")
|
|
||||||
|
|
||||||
# Setze Standardwerte für bestehende Jobs (60 Minuten)
|
|
||||||
cursor.execute("UPDATE jobs SET duration_minutes = 60 WHERE duration_minutes IS NULL")
|
|
||||||
print("✓ Standardwerte für duration_minutes gesetzt")
|
|
||||||
|
|
||||||
# Prüfe ob title zu name umbenannt werden muss
|
|
||||||
if 'title' in columns and 'name' not in columns:
|
|
||||||
# SQLite unterstützt kein direktes Umbenennen von Spalten
|
|
||||||
# Wir müssen die Tabelle neu erstellen
|
|
||||||
print("⚠ Konvertierung von 'title' zu 'name' in jobs-Tabelle...")
|
|
||||||
|
|
||||||
# Backup der Daten
|
|
||||||
cursor.execute("""
|
|
||||||
CREATE TABLE jobs_backup AS
|
|
||||||
SELECT * FROM jobs
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Lösche alte Tabelle
|
|
||||||
cursor.execute("DROP TABLE jobs")
|
|
||||||
|
|
||||||
# Erstelle neue Tabelle mit korrekter Struktur
|
|
||||||
cursor.execute("""
|
|
||||||
CREATE TABLE jobs (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
name VARCHAR(200) NOT NULL,
|
|
||||||
description VARCHAR(500),
|
|
||||||
user_id INTEGER NOT NULL,
|
|
||||||
printer_id INTEGER NOT NULL,
|
|
||||||
start_at DATETIME,
|
|
||||||
end_at DATETIME,
|
|
||||||
actual_end_time DATETIME,
|
|
||||||
status VARCHAR(20) DEFAULT 'scheduled',
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
notes VARCHAR(500),
|
|
||||||
material_used FLOAT,
|
|
||||||
file_path VARCHAR(500),
|
|
||||||
owner_id INTEGER,
|
|
||||||
duration_minutes INTEGER NOT NULL DEFAULT 60,
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
||||||
FOREIGN KEY (printer_id) REFERENCES printers(id),
|
|
||||||
FOREIGN KEY (owner_id) REFERENCES users(id)
|
|
||||||
)
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Daten zurückkopieren (title -> name)
|
|
||||||
cursor.execute("""
|
|
||||||
INSERT INTO jobs (
|
|
||||||
id, name, description, user_id, printer_id, start_at, end_at,
|
|
||||||
actual_end_time, status, created_at, notes, material_used,
|
|
||||||
file_path, owner_id, duration_minutes
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
id, title, description, user_id, printer_id, start_at, end_at,
|
|
||||||
actual_end_time, status, created_at, notes, material_used,
|
|
||||||
file_path, owner_id, COALESCE(duration_minutes, 60)
|
|
||||||
FROM jobs_backup
|
|
||||||
""")
|
|
||||||
|
|
||||||
# Backup-Tabelle löschen
|
|
||||||
cursor.execute("DROP TABLE jobs_backup")
|
|
||||||
print("✓ Jobs-Tabelle erfolgreich konvertiert")
|
|
||||||
|
|
||||||
except sqlite3.OperationalError as e:
|
|
||||||
print(f"⚠ Fehler bei Job-Tabellen-Migration: {e}")
|
|
||||||
|
|
||||||
# Änderungen speichern
|
|
||||||
conn.commit()
|
|
||||||
print("\n✅ Datenbankmigration erfolgreich abgeschlossen!")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"\n❌ Fehler bei der Migration: {e}")
|
logger.error(f"Fehler bei der Datenbank-Migration: {str(e)}")
|
||||||
conn.rollback()
|
sys.exit(1)
|
||||||
return False
|
|
||||||
|
def test_new_tables():
|
||||||
finally:
|
"""Testet, ob die neuen Tabellen korrekt erstellt wurden."""
|
||||||
conn.close()
|
try:
|
||||||
|
with get_cached_session() as session:
|
||||||
|
# Test der GuestRequest-Tabelle
|
||||||
|
test_request = GuestRequest(
|
||||||
|
name="Test User",
|
||||||
|
email="test@example.com",
|
||||||
|
reason="Test migration",
|
||||||
|
duration_min=60
|
||||||
|
)
|
||||||
|
session.add(test_request)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
# Test der UserPermission-Tabelle (mit Admin-User falls vorhanden)
|
||||||
|
admin_user = session.query(User).filter_by(role="admin").first()
|
||||||
|
if admin_user:
|
||||||
|
permission = UserPermission(
|
||||||
|
user_id=admin_user.id,
|
||||||
|
can_start_jobs=True,
|
||||||
|
needs_approval=False,
|
||||||
|
can_approve_jobs=True
|
||||||
|
)
|
||||||
|
session.add(permission)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
# Test der Notification-Tabelle
|
||||||
|
notification = Notification(
|
||||||
|
user_id=admin_user.id,
|
||||||
|
type="test",
|
||||||
|
payload='{"message": "Test notification"}'
|
||||||
|
)
|
||||||
|
session.add(notification)
|
||||||
|
session.flush()
|
||||||
|
|
||||||
|
# Test-Daten wieder löschen
|
||||||
|
session.rollback()
|
||||||
|
|
||||||
|
logger.info("Alle neuen Tabellen wurden erfolgreich getestet")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Fehler beim Testen der neuen Tabellen: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
success = migrate_database()
|
main()
|
||||||
if success:
|
|
||||||
print("\nDie Datenbank wurde erfolgreich migriert.")
|
|
||||||
print("Sie können nun die Anwendung starten: python3.11 app.py")
|
|
||||||
else:
|
|
||||||
print("\nMigration fehlgeschlagen. Bitte überprüfen Sie die Fehlermeldungen.")
|
|
@@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Dark Mode Toggle Fix
|
* Dark Mode Toggle Fix - Premium Edition
|
||||||
* Diese Datei stellt sicher, dass der Dark Mode Toggle Button korrekt funktioniert
|
* Diese Datei stellt sicher, dass der neue Premium Dark Mode Toggle Button korrekt funktioniert
|
||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Dark Mode Toggle Button
|
// Dark Mode Toggle Button (Premium Design)
|
||||||
const darkModeToggle = document.getElementById('darkModeToggle');
|
const darkModeToggle = document.getElementById('darkModeToggle');
|
||||||
const html = document.documentElement;
|
const html = document.documentElement;
|
||||||
|
|
||||||
@@ -23,56 +23,57 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Icons im Toggle-Button aktualisieren
|
* Icons im Premium Toggle-Button aktualisieren
|
||||||
*/
|
*/
|
||||||
function updateIcons(isDark) {
|
function updateIcons(isDark) {
|
||||||
// Finde die Icons im Button - versuche sowohl direkte Kinder als auch verschachtelte
|
if (!darkModeToggle) return;
|
||||||
let sunIcon = darkModeToggle.querySelector('.sun-icon');
|
|
||||||
let moonIcon = darkModeToggle.querySelector('.moon-icon');
|
// Finde die Premium-Icons
|
||||||
|
const sunIcon = darkModeToggle.querySelector('.sun-icon');
|
||||||
|
const moonIcon = darkModeToggle.querySelector('.moon-icon');
|
||||||
|
|
||||||
// Wenn Icons nicht gefunden wurden, erstelle sie neu
|
|
||||||
if (!sunIcon || !moonIcon) {
|
if (!sunIcon || !moonIcon) {
|
||||||
console.warn('Icons nicht gefunden - erzeuge neue Icons');
|
console.warn('Premium Dark Mode Icons nicht gefunden');
|
||||||
|
return;
|
||||||
// Entferne vorhandene Inhalte im Button
|
|
||||||
darkModeToggle.innerHTML = '';
|
|
||||||
|
|
||||||
// Neue Icons erstellen
|
|
||||||
sunIcon = document.createElement('svg');
|
|
||||||
sunIcon.className = 'w-5 h-5 sm:w-5 sm:h-5 sun-icon';
|
|
||||||
sunIcon.setAttribute('fill', 'none');
|
|
||||||
sunIcon.setAttribute('stroke', 'currentColor');
|
|
||||||
sunIcon.setAttribute('viewBox', '0 0 24 24');
|
|
||||||
sunIcon.setAttribute('aria-hidden', 'true');
|
|
||||||
sunIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />';
|
|
||||||
|
|
||||||
moonIcon = document.createElement('svg');
|
|
||||||
moonIcon.className = 'w-5 h-5 sm:w-5 sm:h-5 moon-icon hidden';
|
|
||||||
moonIcon.setAttribute('fill', 'none');
|
|
||||||
moonIcon.setAttribute('stroke', 'currentColor');
|
|
||||||
moonIcon.setAttribute('viewBox', '0 0 24 24');
|
|
||||||
moonIcon.setAttribute('aria-hidden', 'true');
|
|
||||||
moonIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />';
|
|
||||||
|
|
||||||
darkModeToggle.appendChild(sunIcon);
|
|
||||||
darkModeToggle.appendChild(moonIcon);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Icons entsprechend dem Dark Mode Status anzeigen/verbergen
|
// Animation für Übergänge
|
||||||
if (isDark) {
|
if (isDark) {
|
||||||
sunIcon.classList.add('hidden');
|
// Dark Mode aktiviert - zeige Mond
|
||||||
moonIcon.classList.remove('hidden');
|
sunIcon.style.opacity = '0';
|
||||||
|
sunIcon.style.transform = 'scale(0.75) rotate(90deg)';
|
||||||
|
moonIcon.style.opacity = '1';
|
||||||
|
moonIcon.style.transform = 'scale(1) rotate(0deg)';
|
||||||
|
|
||||||
|
// CSS-Klassen für Dark Mode
|
||||||
|
sunIcon.classList.add('opacity-0', 'dark:opacity-0', 'scale-75', 'dark:scale-75', 'rotate-90', 'dark:rotate-90');
|
||||||
|
sunIcon.classList.remove('opacity-100', 'scale-100', 'rotate-0');
|
||||||
|
moonIcon.classList.add('opacity-100', 'dark:opacity-100', 'scale-100', 'dark:scale-100', 'rotate-0', 'dark:rotate-0');
|
||||||
|
moonIcon.classList.remove('opacity-0', 'scale-75', 'rotate-90');
|
||||||
} else {
|
} else {
|
||||||
sunIcon.classList.remove('hidden');
|
// Light Mode aktiviert - zeige Sonne
|
||||||
moonIcon.classList.add('hidden');
|
sunIcon.style.opacity = '1';
|
||||||
|
sunIcon.style.transform = 'scale(1) rotate(0deg)';
|
||||||
|
moonIcon.style.opacity = '0';
|
||||||
|
moonIcon.style.transform = 'scale(0.75) rotate(-90deg)';
|
||||||
|
|
||||||
|
// CSS-Klassen für Light Mode
|
||||||
|
sunIcon.classList.add('opacity-100', 'scale-100', 'rotate-0');
|
||||||
|
sunIcon.classList.remove('opacity-0', 'dark:opacity-0', 'scale-75', 'dark:scale-75', 'rotate-90', 'dark:rotate-90');
|
||||||
|
moonIcon.classList.add('opacity-0', 'dark:opacity-100', 'scale-75', 'dark:scale-100', 'rotate-90', 'dark:rotate-0');
|
||||||
|
moonIcon.classList.remove('opacity-100', 'scale-100', 'rotate-0');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Icon-Animationen hinzufügen
|
||||||
|
sunIcon.classList.toggle('icon-enter', !isDark);
|
||||||
|
moonIcon.classList.toggle('icon-enter', isDark);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dark Mode aktivieren/deaktivieren
|
* Premium Dark Mode aktivieren/deaktivieren
|
||||||
*/
|
*/
|
||||||
function setDarkMode(enable) {
|
function setDarkMode(enable) {
|
||||||
console.log(`Setze Dark Mode auf: ${enable ? 'Aktiviert' : 'Deaktiviert'}`);
|
console.log(`🎨 Setze Premium Dark Mode auf: ${enable ? 'Aktiviert' : 'Deaktiviert'}`);
|
||||||
|
|
||||||
if (enable) {
|
if (enable) {
|
||||||
html.classList.add('dark');
|
html.classList.add('dark');
|
||||||
@@ -82,7 +83,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (darkModeToggle) {
|
if (darkModeToggle) {
|
||||||
darkModeToggle.setAttribute('aria-pressed', 'true');
|
darkModeToggle.setAttribute('aria-pressed', 'true');
|
||||||
darkModeToggle.setAttribute('title', 'Light Mode aktivieren');
|
darkModeToggle.setAttribute('title', 'Light Mode aktivieren');
|
||||||
// Button-Icons aktualisieren
|
// Premium Button-Icons aktualisieren
|
||||||
updateIcons(true);
|
updateIcons(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -93,7 +94,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
if (darkModeToggle) {
|
if (darkModeToggle) {
|
||||||
darkModeToggle.setAttribute('aria-pressed', 'false');
|
darkModeToggle.setAttribute('aria-pressed', 'false');
|
||||||
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
|
darkModeToggle.setAttribute('title', 'Dark Mode aktivieren');
|
||||||
// Button-Icons aktualisieren
|
// Premium Button-Icons aktualisieren
|
||||||
updateIcons(false);
|
updateIcons(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,17 +112,33 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
window.dispatchEvent(new CustomEvent('darkModeChanged', {
|
window.dispatchEvent(new CustomEvent('darkModeChanged', {
|
||||||
detail: { isDark: enable }
|
detail: { isDark: enable }
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Premium-Feedback
|
||||||
|
console.log(`${enable ? '🌙' : '☀️'} Premium Design umgeschaltet auf: ${enable ? 'Dark Mode' : 'Light Mode'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle Dark Mode Funktion
|
// Toggle Dark Mode Funktion
|
||||||
function toggleDarkMode() {
|
function toggleDarkMode() {
|
||||||
const currentMode = isDarkMode();
|
const currentMode = isDarkMode();
|
||||||
setDarkMode(!currentMode);
|
setDarkMode(!currentMode);
|
||||||
|
|
||||||
|
// Premium-Animation beim Klick
|
||||||
|
if (darkModeToggle) {
|
||||||
|
const container = darkModeToggle.querySelector('div');
|
||||||
|
if (container) {
|
||||||
|
container.style.transform = 'scale(0.95)';
|
||||||
|
setTimeout(() => {
|
||||||
|
container.style.transform = '';
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event Listener für Toggle Button
|
// Event Listener für Premium Toggle Button
|
||||||
if (darkModeToggle) {
|
if (darkModeToggle) {
|
||||||
// Vorherige Event-Listener entfernen, um Duplikate zu vermeiden
|
console.log('🎨 Premium Dark Mode Toggle Button gefunden - initialisiere...');
|
||||||
|
|
||||||
|
// Entferne vorherige Event-Listener, um Duplikate zu vermeiden
|
||||||
const newDarkModeToggle = darkModeToggle.cloneNode(true);
|
const newDarkModeToggle = darkModeToggle.cloneNode(true);
|
||||||
darkModeToggle.parentNode.replaceChild(newDarkModeToggle, darkModeToggle);
|
darkModeToggle.parentNode.replaceChild(newDarkModeToggle, darkModeToggle);
|
||||||
|
|
||||||
@@ -133,15 +150,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Aktualisiere die Variable auf das neue Element
|
// Aktualisiere die Variable auf das neue Element
|
||||||
darkModeToggle = newDarkModeToggle;
|
const updatedToggle = document.getElementById('darkModeToggle');
|
||||||
|
|
||||||
// Initialen Status setzen
|
// Initialen Status setzen
|
||||||
const isDark = isDarkMode();
|
const isDark = isDarkMode();
|
||||||
setDarkMode(isDark);
|
setDarkMode(isDark);
|
||||||
|
|
||||||
console.log('Dark Mode Toggle Button erfolgreich initialisiert');
|
console.log('✨ Premium Dark Mode Toggle Button erfolgreich initialisiert');
|
||||||
} else {
|
} else {
|
||||||
console.error('Dark Mode Toggle Button konnte nicht gefunden werden!');
|
console.error('❌ Premium Dark Mode Toggle Button konnte nicht gefunden werden!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tastaturkürzel: Strg+Shift+D für Dark Mode Toggle
|
// Tastaturkürzel: Strg+Shift+D für Dark Mode Toggle
|
||||||
@@ -152,8 +169,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Alternative Tastaturkürzel: Alt+T für Theme Toggle
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.altKey && e.key === 't') {
|
||||||
|
toggleDarkMode();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Direkte Verfügbarkeit der Funktionen im globalen Bereich
|
// Direkte Verfügbarkeit der Funktionen im globalen Bereich
|
||||||
window.toggleDarkMode = toggleDarkMode;
|
window.toggleDarkMode = toggleDarkMode;
|
||||||
window.isDarkMode = isDarkMode;
|
window.isDarkMode = isDarkMode;
|
||||||
window.setDarkMode = setDarkMode;
|
window.setDarkMode = setDarkMode;
|
||||||
|
|
||||||
|
// Premium Features
|
||||||
|
window.premiumDarkMode = {
|
||||||
|
toggle: toggleDarkMode,
|
||||||
|
isDark: isDarkMode,
|
||||||
|
setMode: setDarkMode,
|
||||||
|
version: '3.0.0-premium'
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('🎨 Premium Dark Mode System geladen - Version 3.0.0');
|
||||||
});
|
});
|
2
backend/app/static/js/fullcalendar/main.min.css
vendored
Normal file
2
backend/app/static/js/fullcalendar/main.min.css
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/* FullCalendar v6 CSS is embedded in the JavaScript bundle */
|
||||||
|
/* This file is kept for template compatibility */
|
6
backend/app/static/js/fullcalendar/main.min.js
vendored
Normal file
6
backend/app/static/js/fullcalendar/main.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1,378 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Kalender - Mercedes-Benz MYP Platform{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||||
|
<!-- FullCalendar CSS -->
|
||||||
|
<link href="{{ url_for('static', filename='js/fullcalendar/main.min.css') }}" rel="stylesheet">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-slate-900">
|
||||||
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8 flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-slate-900 dark:text-white">Druckjob-Kalender</h1>
|
||||||
|
<p class="text-slate-600 dark:text-slate-400 mt-2">Übersicht aller geplanten und laufenden 3D-Druckjobs</p>
|
||||||
|
</div>
|
||||||
|
{% if can_edit %}
|
||||||
|
<div class="mt-4 sm:mt-0">
|
||||||
|
<button onclick="openCreateEventModal()"
|
||||||
|
class="px-4 py-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white rounded-xl hover:from-blue-600 hover:to-blue-700 transition-all duration-300 shadow-lg">
|
||||||
|
<svg class="w-5 h-5 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
Neuen Job erstellen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter -->
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-4 mb-6">
|
||||||
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="printerFilter" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Drucker filtern:
|
||||||
|
</label>
|
||||||
|
<select id="printerFilter"
|
||||||
|
class="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
|
||||||
|
<option value="">Alle Drucker</option>
|
||||||
|
{% for printer in printers %}
|
||||||
|
<option value="{{ printer.id }}">{{ printer.name }} {% if printer.location %}({{ printer.location }}){% endif %}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button onclick="refreshCalendar()"
|
||||||
|
class="px-3 py-2 bg-slate-200 dark:bg-slate-700 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-300 dark:hover:bg-slate-600 transition-all duration-300">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kalender -->
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-6">
|
||||||
|
<div id="calendar"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Legende -->
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-lg p-4 mt-6">
|
||||||
|
<h3 class="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">Legende</h3>
|
||||||
|
<div class="flex flex-wrap gap-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-4 h-4 bg-green-500 rounded mr-2"></div>
|
||||||
|
<span class="text-sm text-slate-600 dark:text-slate-400">Geplant / Abgeschlossen</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-4 h-4 bg-blue-500 rounded mr-2"></div>
|
||||||
|
<span class="text-sm text-slate-600 dark:text-slate-400">Läuft</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-4 h-4 bg-gray-500 rounded mr-2"></div>
|
||||||
|
<span class="text-sm text-slate-600 dark:text-slate-400">Wartend</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-4 h-4 bg-red-500 rounded mr-2"></div>
|
||||||
|
<span class="text-sm text-slate-600 dark:text-slate-400">Abgebrochen</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if can_edit %}
|
||||||
|
<!-- Event erstellen/bearbeiten Modal -->
|
||||||
|
<div id="eventModal" class="fixed inset-0 bg-black bg-opacity-50 hidden z-50">
|
||||||
|
<div class="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-xl max-w-md w-full p-6">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h3 id="modalTitle" class="text-lg font-medium text-slate-900 dark:text-white">Neuen Job erstellen</h3>
|
||||||
|
<button onclick="closeEventModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-300">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="eventForm" class="space-y-4">
|
||||||
|
<input type="hidden" id="eventId" name="eventId">
|
||||||
|
|
||||||
|
<!-- Titel -->
|
||||||
|
<div>
|
||||||
|
<label for="eventTitle" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Job-Titel
|
||||||
|
</label>
|
||||||
|
<input type="text" id="eventTitle" name="title" required
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Beschreibung -->
|
||||||
|
<div>
|
||||||
|
<label for="eventDescription" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Beschreibung (optional)
|
||||||
|
</label>
|
||||||
|
<textarea id="eventDescription" name="description" rows="2"
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white resize-none"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Drucker -->
|
||||||
|
<div>
|
||||||
|
<label for="eventPrinter" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Drucker
|
||||||
|
</label>
|
||||||
|
<select id="eventPrinter" name="printerId" required
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
|
||||||
|
<option value="">Drucker auswählen</option>
|
||||||
|
{% for printer in printers %}
|
||||||
|
<option value="{{ printer.id }}">{{ printer.name }} {% if printer.location %}({{ printer.location }}){% endif %}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Start-Zeit -->
|
||||||
|
<div>
|
||||||
|
<label for="eventStart" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
Start-Zeit
|
||||||
|
</label>
|
||||||
|
<input type="datetime-local" id="eventStart" name="start" required
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- End-Zeit -->
|
||||||
|
<div>
|
||||||
|
<label for="eventEnd" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
||||||
|
End-Zeit
|
||||||
|
</label>
|
||||||
|
<input type="datetime-local" id="eventEnd" name="end" required
|
||||||
|
class="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-slate-700 dark:text-white">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Buttons -->
|
||||||
|
<div class="flex items-center justify-end space-x-3 pt-4">
|
||||||
|
<button type="button" onclick="closeEventModal()"
|
||||||
|
class="px-4 py-2 border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-all duration-300">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-all duration-300">
|
||||||
|
Speichern
|
||||||
|
</button>
|
||||||
|
<button type="button" id="deleteEventBtn" onclick="deleteEvent()" style="display: none;"
|
||||||
|
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-all duration-300">
|
||||||
|
Löschen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- FullCalendar JS -->
|
||||||
|
<script src="{{ url_for('static', filename='js/fullcalendar/main.min.js') }}"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const calendarEl = document.getElementById('calendar');
|
||||||
|
const printerFilter = document.getElementById('printerFilter');
|
||||||
|
const canEdit = {% if can_edit %}true{% else %}false{% endif %};
|
||||||
|
|
||||||
|
let calendar = new FullCalendar.Calendar(calendarEl, {
|
||||||
|
initialView: 'timeGridWeek',
|
||||||
|
locale: 'de',
|
||||||
|
headerToolbar: {
|
||||||
|
left: 'prev,next today',
|
||||||
|
center: 'title',
|
||||||
|
right: 'dayGridMonth,timeGridWeek,timeGridDay'
|
||||||
|
},
|
||||||
|
height: 'auto',
|
||||||
|
slotMinTime: '06:00:00',
|
||||||
|
slotMaxTime: '22:00:00',
|
||||||
|
businessHours: {
|
||||||
|
daysOfWeek: [1, 2, 3, 4, 5], // Montag bis Freitag
|
||||||
|
startTime: '08:00',
|
||||||
|
endTime: '18:00'
|
||||||
|
},
|
||||||
|
events: function(info, successCallback, failureCallback) {
|
||||||
|
loadEvents(info.startStr, info.endStr, successCallback, failureCallback);
|
||||||
|
},
|
||||||
|
eventClick: function(info) {
|
||||||
|
if (canEdit) {
|
||||||
|
editEvent(info.event);
|
||||||
|
} else {
|
||||||
|
showEventDetails(info.event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectable: canEdit,
|
||||||
|
select: function(info) {
|
||||||
|
if (canEdit) {
|
||||||
|
openCreateEventModal(info.startStr, info.endStr);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
eventDidMount: function(info) {
|
||||||
|
// Tooltip hinzufügen
|
||||||
|
info.el.title = info.event.title + '\nDrucker: ' + info.event.extendedProps.printerName + '\nStatus: ' + info.event.extendedProps.status;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
calendar.render();
|
||||||
|
|
||||||
|
// Filter-Event
|
||||||
|
printerFilter.addEventListener('change', function() {
|
||||||
|
calendar.refetchEvents();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Globale Funktionen definieren
|
||||||
|
window.refreshCalendar = function() {
|
||||||
|
calendar.refetchEvents();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.openCreateEventModal = function(start, end) {
|
||||||
|
start = start || null;
|
||||||
|
end = end || null;
|
||||||
|
|
||||||
|
document.getElementById('modalTitle').textContent = 'Neuen Job erstellen';
|
||||||
|
document.getElementById('eventForm').reset();
|
||||||
|
document.getElementById('eventId').value = '';
|
||||||
|
document.getElementById('deleteEventBtn').style.display = 'none';
|
||||||
|
|
||||||
|
if (start) {
|
||||||
|
document.getElementById('eventStart').value = start.slice(0, 16);
|
||||||
|
}
|
||||||
|
if (end) {
|
||||||
|
document.getElementById('eventEnd').value = end.slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('eventModal').classList.remove('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeEventModal = function() {
|
||||||
|
document.getElementById('eventModal').classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.editEvent = function(event) {
|
||||||
|
document.getElementById('modalTitle').textContent = 'Job bearbeiten';
|
||||||
|
document.getElementById('eventId').value = event.id;
|
||||||
|
document.getElementById('eventTitle').value = event.title;
|
||||||
|
document.getElementById('eventDescription').value = event.extendedProps.description || '';
|
||||||
|
document.getElementById('eventPrinter').value = event.extendedProps.printerId;
|
||||||
|
document.getElementById('eventStart').value = event.startStr.slice(0, 16);
|
||||||
|
document.getElementById('eventEnd').value = event.endStr ? event.endStr.slice(0, 16) : '';
|
||||||
|
document.getElementById('deleteEventBtn').style.display = 'block';
|
||||||
|
|
||||||
|
document.getElementById('eventModal').classList.remove('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.showEventDetails = function(event) {
|
||||||
|
const status = event.extendedProps.status;
|
||||||
|
const statusText = status === 'scheduled' ? 'Geplant' :
|
||||||
|
status === 'running' ? 'Läuft' :
|
||||||
|
status === 'finished' ? 'Abgeschlossen' : status;
|
||||||
|
|
||||||
|
alert('Job: ' + event.title + '\nDrucker: ' + event.extendedProps.printerName + '\nStatus: ' + statusText + '\nBenutzer: ' + event.extendedProps.userName);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.deleteEvent = function() {
|
||||||
|
const eventId = document.getElementById('eventId').value;
|
||||||
|
if (!eventId) return;
|
||||||
|
|
||||||
|
if (!confirm('Möchten Sie diesen Job wirklich löschen?')) return;
|
||||||
|
|
||||||
|
fetch('/api/calendar/event/' + eventId, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
calendar.refetchEvents();
|
||||||
|
closeEventModal();
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Löschen: ' + (data.error || 'Unbekannter Fehler'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler:', error);
|
||||||
|
alert('Fehler beim Löschen des Jobs');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event-Form Submit
|
||||||
|
document.getElementById('eventForm').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(e.target);
|
||||||
|
const data = {
|
||||||
|
title: formData.get('title'),
|
||||||
|
description: formData.get('description'),
|
||||||
|
printerId: parseInt(formData.get('printerId')),
|
||||||
|
start: formData.get('start'),
|
||||||
|
end: formData.get('end')
|
||||||
|
};
|
||||||
|
|
||||||
|
const eventId = formData.get('eventId');
|
||||||
|
const isEdit = eventId && eventId !== '';
|
||||||
|
|
||||||
|
const url = isEdit ? ('/api/calendar/event/' + eventId) : '/api/calendar/event';
|
||||||
|
const method = isEdit ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
calendar.refetchEvents();
|
||||||
|
closeEventModal();
|
||||||
|
} else {
|
||||||
|
alert('Fehler beim Speichern: ' + (data.error || 'Unbekannter Fehler'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler:', error);
|
||||||
|
alert('Fehler beim Speichern des Jobs');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadEvents(start, end, successCallback, failureCallback) {
|
||||||
|
const printerId = printerFilter.value;
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
from: start,
|
||||||
|
to: end
|
||||||
|
});
|
||||||
|
|
||||||
|
if (printerId) {
|
||||||
|
params.append('printer_id', printerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('/api/calendar?' + params)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(events => {
|
||||||
|
successCallback(events);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Fehler beim Laden der Events:', error);
|
||||||
|
failureCallback(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-Refresh alle 30 Sekunden
|
||||||
|
setInterval(function() {
|
||||||
|
calendar.refetchEvents();
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@@ -50,6 +50,32 @@ LOG_EMOJIS = {
|
|||||||
'kiosk': '📺'
|
'kiosk': '📺'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ASCII-Fallback für Emojis bei Encoding-Problemen
|
||||||
|
EMOJI_FALLBACK = {
|
||||||
|
'🔍': '[DEBUG]',
|
||||||
|
'ℹ️': '[INFO]',
|
||||||
|
'⚠️': '[WARN]',
|
||||||
|
'❌': '[ERROR]',
|
||||||
|
'🔥': '[CRIT]',
|
||||||
|
'🖥️': '[APP]',
|
||||||
|
'⏱️': '[SCHED]',
|
||||||
|
'🔐': '[AUTH]',
|
||||||
|
'🖨️': '[JOBS]',
|
||||||
|
'🔧': '[PRINT]',
|
||||||
|
'💥': '[ERR]',
|
||||||
|
'👤': '[USER]',
|
||||||
|
'📺': '[KIOSK]'
|
||||||
|
}
|
||||||
|
|
||||||
|
def safe_emoji(emoji: str) -> str:
|
||||||
|
"""Gibt ein Emoji zurück oder einen ASCII-Fallback bei Encoding-Problemen."""
|
||||||
|
try:
|
||||||
|
# Teste, ob das Emoji dargestellt werden kann
|
||||||
|
emoji.encode(sys.stdout.encoding or 'utf-8')
|
||||||
|
return emoji
|
||||||
|
except (UnicodeEncodeError, LookupError):
|
||||||
|
return EMOJI_FALLBACK.get(emoji, '[?]')
|
||||||
|
|
||||||
# Prüfen, ob das Terminal ANSI-Farben unterstützt
|
# Prüfen, ob das Terminal ANSI-Farben unterstützt
|
||||||
def supports_color() -> bool:
|
def supports_color() -> bool:
|
||||||
"""Prüft, ob das Terminal ANSI-Farben unterstützt."""
|
"""Prüft, ob das Terminal ANSI-Farben unterstützt."""
|
||||||
@@ -59,6 +85,14 @@ def supports_color() -> bool:
|
|||||||
kernel32 = ctypes.windll.kernel32
|
kernel32 = ctypes.windll.kernel32
|
||||||
# Aktiviere VT100-Unterstützung unter Windows
|
# Aktiviere VT100-Unterstützung unter Windows
|
||||||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||||||
|
|
||||||
|
# Versuche UTF-8-Encoding für Emojis zu setzen
|
||||||
|
try:
|
||||||
|
import locale
|
||||||
|
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
Reference in New Issue
Block a user