diff --git a/dashboard-dash-backup/Dockerfile b/dashboard-dash-backup/Dockerfile deleted file mode 100644 index 4f60efc..0000000 --- a/dashboard-dash-backup/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# dashboard/Dockerfile -# Produktions-Dockerfile für die Dash-Applikation - -# --- Basis-Image --- -FROM python:3.13-slim - -# --- Arbeitsverzeichnis im Container --- -WORKDIR /app - -# --- Systemabhängigkeiten installieren (falls benötigt) --- -RUN apt-get update \ - && apt-get install -y --no-install-recommends \ - build-essential git \ - && rm -rf /var/lib/apt/lists/* - -# --- Python-Abhängigkeiten kopieren und installieren --- -COPY dash_using_fullcalendar-0.1.0.tar.gz ./ -COPY requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt - -# --- Applikationscode kopieren --- -COPY dashboard/ /app - -# --- Non-Root-User anlegen und Rechte setzen --- -ARG USER_ID=1000 -ARG GROUP_ID=1000 -RUN groupadd -g ${GROUP_ID} infoscreen_taa \ - && useradd -u ${USER_ID} -g ${GROUP_ID} \ - --shell /bin/bash --create-home infoscreen_taa \ - && chown -R infoscreen_taa:infoscreen_taa /app -USER infoscreen_taa - -# --- Port für Dash exposed --- -EXPOSE 8050 - -# --- Startbefehl: Gunicorn mit Dash-Server --- -# "app.py" enthält: app = dash.Dash(...); server = app.server -CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:8050", "app:server"] diff --git a/dashboard-dash-backup/Dockerfile.dev b/dashboard-dash-backup/Dockerfile.dev deleted file mode 100644 index e6884a4..0000000 --- a/dashboard-dash-backup/Dockerfile.dev +++ /dev/null @@ -1,51 +0,0 @@ -# dashboard/Dockerfile.dev -# Entwicklungs-Dockerfile für das Dash-Dashboard - -FROM python:3.13-slim - -# Build args für UID/GID -ARG USER_ID=1000 -ARG GROUP_ID=1000 - -# Systemabhängigkeiten (falls nötig) -RUN apt-get update \ - && apt-get install -y --no-install-recommends locales curl \ - && sed -i 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen \ - && locale-gen \ - && rm -rf /var/lib/apt/lists/* - -# Locale setzen -ENV LANG=de_DE.UTF-8 \ - LANGUAGE=de_DE:de \ - LC_ALL=de_DE.UTF-8 - -# Non-root User anlegen -RUN groupadd -g ${GROUP_ID} infoscreen_taa \ - && useradd -u ${USER_ID} -g ${GROUP_ID} --shell /bin/bash --create-home infoscreen_taa - -# Arbeitsverzeichnis -WORKDIR /app - -# Kopiere nur Requirements für schnellen Rebuild -COPY dash_using_fullcalendar-0.1.0.tar.gz ./ -COPY requirements.txt ./ -COPY requirements-dev.txt ./ - -# Installiere Abhängigkeiten -RUN pip install --upgrade pip \ - && pip install --no-cache-dir -r requirements.txt \ - && pip install --no-cache-dir -r requirements-dev.txt - -# Setze Entwicklungs-Modus -ENV DASH_DEBUG_MODE=True -ENV API_URL=http://server:8000/api - -# Ports für Dash und optional Live-Reload -EXPOSE 8050 -EXPOSE 5678 - -# Wechsle zum non-root User -USER infoscreen_taa - -# Dev-Start: Dash mit Hot-Reload -CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5637", "app.py"] diff --git a/dashboard-dash-backup/__init__.py b/dashboard-dash-backup/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dashboard-dash-backup/app.py b/dashboard-dash-backup/app.py deleted file mode 100644 index 70b5a3f..0000000 --- a/dashboard-dash-backup/app.py +++ /dev/null @@ -1,79 +0,0 @@ -# dashboard/app.py -import sys -sys.path.append('/workspace') - -from dash import Dash, html, dcc, page_container -from flask import Flask -import dash_bootstrap_components as dbc -import dash_mantine_components as dmc -from components.header import Header -import callbacks.ui_callbacks -import dashboard.callbacks.overview_callbacks -import dashboard.callbacks.appointments_callbacks -import dashboard.callbacks.appointment_modal_callbacks -from config import SECRET_KEY, ENV -import os -import threading -import logging - -# Logging konfigurieren -logging.basicConfig( - level=logging.DEBUG if ENV == "development" else logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -server = Flask(__name__) -server.secret_key = SECRET_KEY - -# Flask's eigene Logs aktivieren -if ENV == "development": - logging.getLogger('werkzeug').setLevel(logging.INFO) - -app = Dash( - __name__, - server=server, - use_pages=True, - external_stylesheets=[dbc.themes.BOOTSTRAP], - suppress_callback_exceptions=True, - serve_locally=True, - meta_tags=[ - {"name": "viewport", "content": "width=device-width, initial-scale=1"}, - {"charset": "utf-8"} - ] -) - -app.layout = dmc.MantineProvider([ - Header(), - html.Div([ - html.Div(id="sidebar"), - html.Div(page_container, className="page-content"), - dcc.Store(id="sidebar-state", data={"collapsed": False}), - ], style={"display": "flex"}), -]) - -# def open_browser(): -# """Öffnet die HTTPS-URL im Standardbrowser.""" -# os.system('$BROWSER https://localhost:8050') # Entferne das "&", um sicherzustellen, dass der Browser korrekt geöffnet wird - -print("Testausgabe: Debug-Print funktioniert!") # Testausgabe - -if __name__ == "__main__": - debug_mode = ENV == "development" - - logger.info(f"Starting application in {'DEBUG' if debug_mode else 'PRODUCTION'} mode") - logger.info(f"Environment: {ENV}") - logger.info("🔧 Debug features: print(), logging, hot reload all active") - logger.info("🚀 Dashboard starting up...") - - # Browser nur einmal öffnen, nicht bei Reload-Prozessen - # if debug_mode and os.environ.get("WERKZEUG_RUN_MAIN") != "true": - # threading.Timer(1.0, open_browser).start() - - app.run( - host="0.0.0.0", - port=8050, - debug=debug_mode, - ssl_context=("/workspace/certs/dev.crt", "/workspace/certs/dev.key"), - use_reloader=False # Verhindert doppeltes Öffnen durch Dash - ) \ No newline at end of file diff --git a/dashboard-dash-backup/assets/TAA_Logo.png b/dashboard-dash-backup/assets/TAA_Logo.png deleted file mode 100644 index 698eb92..0000000 Binary files a/dashboard-dash-backup/assets/TAA_Logo.png and /dev/null differ diff --git a/dashboard-dash-backup/assets/custom.css b/dashboard-dash-backup/assets/custom.css deleted file mode 100644 index c2bda9b..0000000 --- a/dashboard-dash-backup/assets/custom.css +++ /dev/null @@ -1,249 +0,0 @@ -/* ========================== - Allgemeines Layout - ========================== */ -:root { - --mb-z-index: 2000 !important; - --mantine-z-index-popover: 2100 !important; - --mantine-z-index-overlay: 2999 !important; - --mantine-z-index-dropdown: 2100 !important; - --mantine-z-index-max: 9999 !important; - --mantine-z-index-modal: 3000 !important; -} -body { - margin: 0; - font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; - background-color: #f8f9fa; - padding-top: 60px; /* Platz für den fixen Header schaffen */ -} - -/* page-content (rechts neben der Sidebar) */ -.page-content { - flex: 1 1 0%; - padding: 20px; - min-width: 0; /* verhindert Überlauf bei zu breitem Inhalt */ - transition: margin-left 0.3s ease; - min-height: calc(100vh - 60px); /* Mindesthöhe minus Header-Höhe */ - margin-left: 220px; /* <--- Ergänzen */ -} - -/* Wenn Sidebar collapsed ist, reduziere margin-left */ -.sidebar.collapsed ~ .page-content { - margin-left: 70px; -} - -/* ========================== - Header - ========================== */ -.app-header { - position: fixed; - top: 0; - left: 0; - height: 60px; - width: 100%; - background-color: #e4d5c1; - color: #7c5617; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 20px; - z-index: 1100; - box-shadow: 0 2px 4px rgba(0,0,0,0.2); -} - -.logo { - height: 40px; - margin-right: 10px; -} - -.app-title { - font-size: 1.5rem; - font-weight: bold; -} - -.org-name { - font-size: 1rem; - color: #7c5617; -} - -.header-right { - display: flex; - align-items: center; -} - -/* ========================== - Sidebar - ========================== */ -.sidebar { - width: 220px; - transition: width 0.3s ease; - background-color: #e4d5c1; - color: black; - height: calc(100vh - 60px); /* Höhe minus Header */ - top: 60px; /* Den gleichen Wert wie Header-Höhe verwenden */ - left: 0; - z-index: 1000; - position: fixed; /* <--- Ändere das von relative zu fixed */ - overflow-x: hidden; - overflow-y: auto; -} - -.sidebar.collapsed { - width: 70px; -} - -/* Sidebar Toggle Button (Burger-Icon) */ -.sidebar-toggle { - text-align: right; - padding: 5px 10px; -} - -.toggle-button { - background: none; - border: none; - color: #7c5617; - cursor: pointer; - font-size: 1.2rem; - transition: transform 0.1s ease, background-color 0.2s ease; - padding: 8px; - border-radius: 4px; -} - -.toggle-button:hover { - background-color: #7c5617; - color: #e4d5c1; - transform: scale(1.05); -} - -.toggle-button:active { - transform: scale(0.95); -} - -/* Navigation in der Sidebar */ -.sidebar-nav .nav-link { - color: #7c5617; - padding: 10px 15px; - display: flex; - align-items: center; - border-radius: 4px; - margin: 2px 8px; - transition: background-color 0.2s ease, color 0.2s ease; -} - -.sidebar-nav .nav-link:hover { - background-color: #7c5617; - color: #e4d5c1; -} - -.sidebar-nav .nav-link.active { - background-color: #7c5617; - color: #e4d5c1; -} - -/* Text neben Icons */ -.sidebar-label { - display: inline-block; - margin-left: 10px; - white-space: nowrap; - transition: opacity 0.3s ease, width 0.3s ease; -} - -/* Wenn Sidebar collapsed ist, blendet das Label aus */ -.sidebar.collapsed .sidebar-label { - opacity: 0; - width: 0; - overflow: hidden; -} - -/* Tooltips (Bootstrap-Tooltips) */ -.tooltip { - z-index: 2000; - background-color: #7c5617; - color: #e4d5c1; -} - -/* Optional: Tooltips nur anzeigen, wenn Sidebar collapsed ist */ -/* Da dash-bootstrap-components Tooltips in einen anderen DOM-Layer rendert, - kann man bei Bedarf per Callback steuern, ob sie geöffnet sind oder nicht. - Dieser Block ist nur ein Zusatz – das Haupt-Show/Hiding erfolgt per - is_open-Callback. */ -.sidebar:not(.collapsed) ~ .tooltip { - display: none !important; -} - -/* ========================== - Responsive (bei Bedarf) - ========================== */ -/* @media (max-width: 768px) { - body { - padding-top: 60px; /* Header-Platz auch auf mobilen Geräten */ - /* } - - .sidebar { - position: fixed; - height: calc(100vh - 60px); - z-index: 1050; - } - - .page-content { - margin-left: 0; - } - - .sidebar.collapsed { - width: 0; - } - - .sidebar.collapsed ~ .page-content { - margin-left: 0; - } -} */ - -.mantine-Modal-modal { - z-index: var(--mantine-z-index-modal, 3000) !important; -} - -/* Modalbox */ -.mantine-Modal-inner, -.mantine-Modal-content { - z-index: 4000 !important; -} - -/* Popups (Dropdowns, Datepicker, Autocomplete, Menüs) innerhalb der Modalbox */ -.mantine-Popover-dropdown, -.mantine-Select-dropdown, -.mantine-DatePicker-dropdown, -.mantine-Autocomplete-dropdown, -.mantine-Menu-dropdown { - z-index: 4100 !important; -} - -/* Optional: Overlay für Popups noch höher, falls benötigt */ -.mantine-Popover-root, -.mantine-Select-root, -.mantine-DatePicker-root, -.mantine-Autocomplete-root, -.mantine-Menu-root { - z-index: 4101 !important; -} - -/* Sidebar collapsed: Icon-Farbe normal */ -.sidebar.collapsed .sidebar-item-collapsed svg { - color: #7c5617; /* Icon-Linie/Text */ - fill: #e4d5c1; /* Icon-Fläche */ - width: 24px; - height: 24px; - margin: 0 auto; - display: block; - transition: color 0.2s, fill 0.2s; -} - -/* Sidebar collapsed: Hintergrund und Icon invertieren bei Hover/Active */ -.sidebar.collapsed .nav-link:hover, -.sidebar.collapsed .nav-link.active { - background-color: #7c5617 !important; -} - -.sidebar.collapsed .nav-link:hover svg, -.sidebar.collapsed .nav-link.active svg { - color: #e4d5c1; /* Icon-Linie/Text invertiert */ - fill: #7c5617; /* Icon-Fläche invertiert */ -} diff --git a/dashboard-dash-backup/assets/logo.png b/dashboard-dash-backup/assets/logo.png deleted file mode 100644 index 0ffb524..0000000 Binary files a/dashboard-dash-backup/assets/logo.png and /dev/null differ diff --git a/dashboard-dash-backup/callbacks/__init__.py b/dashboard-dash-backup/callbacks/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dashboard-dash-backup/callbacks/appointment_modal_callbacks.py b/dashboard-dash-backup/callbacks/appointment_modal_callbacks.py deleted file mode 100644 index 60398d4..0000000 --- a/dashboard-dash-backup/callbacks/appointment_modal_callbacks.py +++ /dev/null @@ -1,370 +0,0 @@ -from dash import Input, Output, State, callback, html, dcc, no_update, ctx -import dash_mantine_components as dmc -from dash_iconify import DashIconify -import dash_quill -from datetime import datetime, date, timedelta -import re - -# --- Typ-spezifische Felder anzeigen --- -@callback( - Output('type-specific-fields', 'children'), - Input('type-input', 'value'), - prevent_initial_call=True -) -def show_type_specific_fields(event_type): - if not event_type: - return html.Div() - - if event_type == "presentation": - return dmc.Stack([ - dmc.Divider(label="Präsentations-Details", labelPosition="center"), - dmc.Group([ - dcc.Upload( - id='presentation-upload', - children=dmc.Button( - "Datei hochladen", - leftSection=DashIconify(icon="mdi:upload"), - variant="outline" - ), - style={'width': 'auto'} - ), - dmc.TextInput( - label="Präsentationslink", - placeholder="https://...", - leftSection=DashIconify(icon="mdi:link"), - id="presentation-link", - style={"minWidth": 0, "flex": 1, "marginBottom": 0} - ) - ], grow=True, align="flex-end", style={"marginBottom": 10}), - html.Div(id="presentation-upload-status") - ], gap="sm") - - elif event_type == "video": - return dmc.Stack([ - dmc.Divider(label="Video-Details", labelPosition="center"), - dmc.Group([ - dcc.Upload( - id='video-upload', - children=dmc.Button( - "Video hochladen", - leftSection=DashIconify(icon="mdi:video-plus"), - variant="outline" - ), - style={'width': 'auto'} - ), - dmc.TextInput( - label="Videolink", - placeholder="https://youtube.com/...", - leftSection=DashIconify(icon="mdi:youtube"), - id="video-link", - style={"minWidth": 0, "flex": 1, "marginBottom": 0} - ) - ], grow=True, align="flex-end", style={"marginBottom": 10}), - dmc.Group([ - dmc.Checkbox( - label="Endlos wiederholen", - id="video-endless", - checked=True, - style={"marginRight": 20} - ), - dmc.NumberInput( - label="Wiederholungen", - id="video-repeats", - value=1, - min=1, - max=99, - step=1, - disabled=True, - style={"width": 150} - ), - dmc.Slider( - label="Lautstärke", - id="video-volume", - value=70, - min=0, - max=100, - step=5, - marks=[ - {"value": 0, "label": "0%"}, - {"value": 50, "label": "50%"}, - {"value": 100, "label": "100%"} - ], - style={"flex": 1, "marginLeft": 20} - ) - ], grow=True, align="flex-end"), - html.Div(id="video-upload-status") - ], gap="sm") - - elif event_type == "website": - return dmc.Stack([ - dmc.Divider(label="Website-Details", labelPosition="center"), - dmc.TextInput( - label="Website-URL", - placeholder="https://example.com", - leftSection=DashIconify(icon="mdi:web"), - id="website-url", - required=True, - style={"flex": 1} - ) - ], gap="sm") - - elif event_type == "message": - return dmc.Stack([ - dmc.Divider(label="Nachrichten-Details", labelPosition="center"), - dash_quill.Quill( - id="message-content", - value="", - ), - dmc.Group([ - dmc.Select( - label="Schriftgröße", - data=[ - {"value": "small", "label": "Klein"}, - {"value": "medium", "label": "Normal"}, - {"value": "large", "label": "Groß"}, - {"value": "xlarge", "label": "Sehr groß"} - ], - id="message-font-size", - value="medium", - style={"flex": 1} - ), - dmc.ColorPicker( - id="message-color", - value="#000000", - format="hex", - swatches=[ - "#000000", "#ffffff", "#ff0000", "#00ff00", - "#0000ff", "#ffff00", "#ff00ff", "#00ffff" - ] - ) - ], grow=True, align="flex-end") - ], gap="sm") - - return html.Div() - -# --- Callback zum Aktivieren/Deaktivieren des Wiederholungsfelds bei Video --- -@callback( - Output("video-repeats", "disabled"), - Input("video-endless", "checked"), - prevent_initial_call=True -) -def toggle_video_repeats(endless_checked): - return endless_checked - -# --- Upload-Status für Präsentation --- -@callback( - Output('presentation-upload-status', 'children'), - Input('presentation-upload', 'contents'), - State('presentation-upload', 'filename'), - prevent_initial_call=True -) -def update_presentation_upload_status(contents, filename): - if contents is not None and filename is not None: - return dmc.Alert( - f"✓ Datei '{filename}' erfolgreich hochgeladen", - color="green", - className="mt-2" - ) - return html.Div() - -# --- Upload-Status für Video --- -@callback( - Output('video-upload-status', 'children'), - Input('video-upload', 'contents'), - State('video-upload', 'filename'), - prevent_initial_call=True -) -def update_video_upload_status(contents, filename): - if contents is not None and filename is not None: - return dmc.Alert( - f"✓ Video '{filename}' erfolgreich hochgeladen", - color="green", - className="mt-2" - ) - return html.Div() - -# --- Wiederholungsoptionen aktivieren/deaktivieren --- -@callback( - [ - Output('weekdays-select', 'disabled'), - Output('repeat-until-date', 'disabled'), - Output('skip-holidays-checkbox', 'disabled'), - Output('weekdays-select', 'value'), - Output('repeat-until-date', 'value'), - Output('skip-holidays-checkbox', 'checked') - ], - Input('repeat-checkbox', 'checked'), - prevent_initial_call=True -) -def toggle_repeat_options(is_repeat): - if is_repeat: - next_month = datetime.now().date() + timedelta(weeks=4) - return ( - False, # weekdays-select enabled - False, # repeat-until-date enabled - False, # skip-holidays-checkbox enabled - None, # weekdays value - next_month, # repeat-until-date value - False # skip-holidays-checkbox checked - ) - else: - return ( - True, # weekdays-select disabled - True, # repeat-until-date disabled - True, # skip-holidays-checkbox disabled - None, # weekdays value - None, # repeat-until-date value - False # skip-holidays-checkbox checked - ) - -# --- Dynamische Zeitoptionen für Startzeit --- -def validate_and_format_time(time_str): - if not time_str: - return None, "Keine Zeit angegeben" - if re.match(r'^\d{2}:\d{2}$', time_str): - try: - h, m = map(int, time_str.split(':')) - if 0 <= h <= 23 and 0 <= m <= 59: - return time_str, "Gültige Zeit" - except: - pass - patterns = [ - (r'^(\d{1,2}):(\d{2})$', lambda m: (int(m.group(1)), int(m.group(2)))), - (r'^(\d{1,2})\.(\d{2})$', lambda m: (int(m.group(1)), int(m.group(2)))), - (r'^(\d{1,2})(\d{2})$', lambda m: (int(m.group(1)), int(m.group(2)))), - (r'^(\d{1,2})$', lambda m: (int(m.group(1)), 0)), - ] - for pattern, extractor in patterns: - match = re.match(pattern, time_str.strip()) - if match: - try: - hours, minutes = extractor(match) - if 0 <= hours <= 23 and 0 <= minutes <= 59: - return f"{hours:02d}:{minutes:02d}", "Automatisch formatiert" - except: - continue - return None, "Ungültiges Zeitformat" - -@callback( - [ - Output('time-start', 'data'), - Output('start-time-feedback', 'children') - ], - Input('time-start', 'searchValue'), - prevent_initial_call=True -) -def update_start_time_options(search_value): - time_options = [ - {"value": f"{h:02d}:{m:02d}", "label": f"{h:02d}:{m:02d}"} - for h in range(6, 24) for m in [0, 30] - ] - base_options = time_options.copy() - feedback = None - if search_value: - validated_time, status = validate_and_format_time(search_value) - if validated_time: - if not any(opt["value"] == validated_time for opt in base_options): - base_options.insert(0, { - "value": validated_time, - "label": f"{validated_time} (Ihre Eingabe)" - }) - feedback = dmc.Text(f"✓ {status}: {validated_time}", size="xs", c="green") - else: - feedback = dmc.Text(f"✗ {status}", size="xs", c="red") - return base_options, feedback - -# --- Dynamische Zeitoptionen für Endzeit --- -@callback( - [ - Output('time-end', 'data'), - Output('end-time-feedback', 'children') - ], - Input('time-end', 'searchValue'), - prevent_initial_call=True -) -def update_end_time_options(search_value): - time_options = [ - {"value": f"{h:02d}:{m:02d}", "label": f"{h:02d}:{m:02d}"} - for h in range(6, 24) for m in [0, 30] - ] - base_options = time_options.copy() - feedback = None - if search_value: - validated_time, status = validate_and_format_time(search_value) - if validated_time: - if not any(opt["value"] == validated_time for opt in base_options): - base_options.insert(0, { - "value": validated_time, - "label": f"{validated_time} (Ihre Eingabe)" - }) - feedback = dmc.Text(f"✓ {status}: {validated_time}", size="xs", c="green") - else: - feedback = dmc.Text(f"✗ {status}", size="xs", c="red") - return base_options, feedback - -# --- Automatische Endzeit-Berechnung mit Validation --- -@callback( - Output('time-end', 'value'), - [ - Input('time-start', 'value'), - Input('btn-reset', 'n_clicks') - ], - State('time-end', 'value'), - prevent_initial_call=True -) -def handle_end_time(start_time, reset_clicks, current_end_time): - if not ctx.triggered: - return no_update - trigger_id = ctx.triggered[0]['prop_id'].split('.')[0] - if trigger_id == 'btn-reset' and reset_clicks: - return None - if trigger_id == 'time-start' and start_time: - if current_end_time: - return no_update - try: - validated_start, _ = validate_and_format_time(start_time) - if validated_start: - start_dt = datetime.strptime(validated_start, "%H:%M") - end_dt = start_dt + timedelta(hours=1, minutes=30) - if end_dt.hour >= 24: - end_dt = end_dt.replace(hour=23, minute=59) - return end_dt.strftime("%H:%M") - except: - pass - return no_update - -# --- Reset-Funktion erweitert --- -@callback( - [ - Output('title-input', 'value'), - Output('start-date-input', 'value', allow_duplicate=True), - Output('time-start', 'value'), - Output('type-input', 'value'), - Output('description-input', 'value'), - Output('repeat-checkbox', 'checked'), - Output('weekdays-select', 'value', allow_duplicate=True), - Output('repeat-until-date', 'value', allow_duplicate=True), - Output('skip-holidays-checkbox', 'checked', allow_duplicate=True) - ], - Input('btn-reset', 'n_clicks'), - prevent_initial_call=True -) -def reset_form(n_clicks): - if n_clicks: - return "", datetime.now().date(), "09:00", None, "", False, None, None, False - return no_update - -# --- Speichern-Funktion (Demo) --- -@callback( - Output('save-feedback', 'children'), - Input('btn-save', 'n_clicks'), - prevent_initial_call=True -) -def save_appointments_demo(n_clicks): - if not n_clicks: - return no_update - return dmc.Alert( - "Demo: Termine würden hier gespeichert werden", - color="blue", - title="Speichern (Demo)" - ) \ No newline at end of file diff --git a/dashboard-dash-backup/callbacks/appointments_callbacks.py b/dashboard-dash-backup/callbacks/appointments_callbacks.py deleted file mode 100644 index 3bd8a12..0000000 --- a/dashboard-dash-backup/callbacks/appointments_callbacks.py +++ /dev/null @@ -1,168 +0,0 @@ -import logging -from math import fabs - -logger = logging.getLogger(__name__) -# dashboard/callbacks/appointments_callbacks.py -import requests -import json -from flask import session -from dash import Input, Output, State, callback, ctx, dash, no_update -import os -import sys -from datetime import datetime, timedelta - -# This message will now appear in the terminal during startup -logger.debug("Registering appointments page...") - - -# --- Modalbox öffnen: jetzt auch auf Kalenderklick reagieren --- - -sys.path.append('/workspace') - -print("appointments_callbacks.py geladen") - -API_BASE_URL = os.getenv("API_BASE_URL", "http://192.168.43.100") -ENV = os.getenv("ENV", "development") - - -@callback( - dash.Output('output', 'children'), - dash.Input('calendar', 'lastDateClick') -) -def display_date(date_str): - if date_str: - return f"Letzter Klick auf: {date_str}" - return "Klicke auf ein Datum im Kalender!" - - -@callback( - dash.Output('event-output', 'children'), - dash.Input('calendar', 'lastEventClick') -) -def display_event(event_id): - if event_id: - return f"Letztes Event geklickt: {event_id}" - return "Klicke auf ein Event im Kalender!" - - -@callback( - dash.Output('select-output', 'children'), - dash.Input('calendar', 'lastSelect') -) -def display_select(select_info): - if select_info: - return f"Markiert: {select_info['start']} bis {select_info['end']} (ganztägig: {select_info['allDay']})" - return "Markiere einen Bereich im Kalender!" - - -@callback( - dash.Output('calendar', 'events'), - dash.Input('calendar', 'lastNavClick'), -) -def load_events(view_dates): - logger.info(f"Lade Events für Zeitraum: {view_dates}") - if not view_dates or "start" not in view_dates or "end" not in view_dates: - return [] - start = view_dates["start"] - end = view_dates["end"] - try: - verify_ssl = True if ENV == "production" else False - resp = requests.get( - f"{API_BASE_URL}/api/events", - params={"start": start, "end": end}, - verify=verify_ssl - ) - resp.raise_for_status() - events = resp.json() - return events - except Exception as e: - logger.info(f"Fehler beim Laden der Events: {e}") - return [] - -# --- Modalbox öffnen --- - - -@callback( - [ - Output("appointment-modal", "opened"), - Output("start-date-input", "value", allow_duplicate=True), - Output("time-start", "value", allow_duplicate=True), - Output("time-end", "value", allow_duplicate=True), - ], - [ - Input("calendar", "lastDateClick"), - Input("calendar", "lastSelect"), - Input("open-appointment-modal-btn", "n_clicks"), - Input("close-appointment-modal-btn", "n_clicks"), - ], - State("appointment-modal", "opened"), - prevent_initial_call=True -) -def open_modal(date_click, select, open_click, close_click, is_open): - trigger = ctx.triggered_id - - # Bereichsauswahl (lastSelect) - if trigger == "calendar" and select: - try: - start_dt = datetime.fromisoformat(select["start"]) - end_dt = datetime.fromisoformat(select["end"]) - - return ( - True, - start_dt.date().isoformat(), - start_dt.strftime("%H:%M"), - end_dt.strftime("%H:%M"), - ) - except Exception as e: - print("Fehler beim Parsen von select:", e) - return no_update, no_update, no_update, no_update - - # Einzelklick (lastDateClick) - if trigger == "calendar" and date_click: - try: - dt = datetime.fromisoformat(date_click) - # Versuche, die Slotlänge aus dem Kalender zu übernehmen (optional) - # Hier als Beispiel 30 Minuten aufaddieren, falls keine Endzeit vorhanden - end_dt = dt + timedelta(minutes=30) - return ( - True, - dt.date().isoformat(), - dt.strftime("%H:%M"), - end_dt.strftime("%H:%M"), - ) - except Exception as e: - print("Fehler beim Parsen von date_click:", e) - return no_update, no_update, no_update, no_update - - # Modal öffnen per Button - if trigger == "open-appointment-modal-btn" and open_click: - now = datetime.now() - end_dt = now + timedelta(minutes=30) - return True, now.date().isoformat(), now.strftime("%H:%M"), end_dt.strftime("%H:%M") - - # Modal schließen - if trigger == "close-appointment-modal-btn" and close_click: - return False, no_update, no_update, no_update - - return is_open, no_update, no_update, no_update - - -# @callback( -# Output("time-end", "value", allow_duplicate=True), -# Input("time-start", "value"), -# prevent_initial_call=True -# ) -# def handle_end_time(start_time, duration="00:30"): -# trigger = ctx.triggered_id -# if trigger == "time-start" and start_time and duration: -# try: -# # Beispiel für start_time: "09:00" -# start_dt = datetime.strptime(start_time, "%H:%M") -# # Dauer in Stunden und Minuten, z.B. "01:30" -# hours, minutes = map(int, duration.split(":")) -# # Endzeit berechnen: Dauer addieren! -# end_dt = start_dt + timedelta(hours=hours, minutes=minutes) -# return end_dt.strftime("%H:%M") -# except Exception as e: -# print("Fehler bei der Berechnung der Endzeit:", e) -# return no_update diff --git a/dashboard-dash-backup/callbacks/auth_callbacks.py b/dashboard-dash-backup/callbacks/auth_callbacks.py deleted file mode 100644 index ee0a85c..0000000 --- a/dashboard-dash-backup/callbacks/auth_callbacks.py +++ /dev/null @@ -1,31 +0,0 @@ -# dashboard/callbacks/auth_callbacks.py -import dash -from dash import Input, Output, State, dcc -from flask import session -from utils.auth import check_password, get_user_role -from config import ENV -from utils.db import execute_query - -@dash.callback( - Output("login-feedback", "children"), - Output("header-right", "children"), - Input("btn-login", "n_clicks"), - State("input-user", "value"), - State("input-pass", "value"), - prevent_initial_call=True -) -def login_user(n_clicks, username, password): - if ENV == "development": - # Dev‐Bypass: setze immer Admin‐Session und leite weiter - session["username"] = "dev_admin" - session["role"] = "admin" - return dcc.Location(href="/overview", id="redirect-dev"), None - - # Produktions‐Login: User in DB suchen - user = execute_query("SELECT username, pwd_hash, role FROM users WHERE username=%s", (username,), fetch_one=True) - if user and check_password(password, user["pwd_hash"]): - session["username"] = user["username"] - session["role"] = user["role"] - return dcc.Location(href="/overview", id="redirect-ok"), None - else: - return "Ungültige Zugangsdaten.", None diff --git a/dashboard-dash-backup/callbacks/overview_callbacks.py b/dashboard-dash-backup/callbacks/overview_callbacks.py deleted file mode 100644 index c0b4b75..0000000 --- a/dashboard-dash-backup/callbacks/overview_callbacks.py +++ /dev/null @@ -1,139 +0,0 @@ -# dashboard/callbacks/overview_callbacks.py -import sys -sys.path.append('/workspace') -import threading -import dash -import requests -from dash import Input, Output, State, MATCH, html, dcc -from flask import session -from utils.db import get_session # Diese Funktion muss eine SQLAlchemy-Session liefern! -from utils.mqtt_client import publish, start_loop -from config import ENV -import dash_bootstrap_components as dbc -import os -import time -import pytz -from datetime import datetime - -print("overview_callbacks.py geladen") - -API_BASE_URL = os.getenv("API_BASE_URL", "https://192.168.43.100") - -mqtt_thread_started = False -SCREENSHOT_DIR = "received-screenshots" - -def ensure_mqtt_running(): - global mqtt_thread_started - if not mqtt_thread_started: - thread = threading.Thread(target=start_loop, daemon=True) - thread.start() - mqtt_thread_started = True - -def get_latest_screenshot(client_uuid): - cache_buster = int(time.time()) # aktuelle Unix-Zeit in Sekunden - # TODO: Hier genau im Produkitv-Modus die IPs testen! - # Wenn API_BASE_URL auf "http" beginnt, absolute URL verwenden (z.B. im lokalen Dev) - if API_BASE_URL.startswith("http"): - return f"{API_BASE_URL}/screenshots/{client_uuid}?t={cache_buster}" - # Sonst relative URL (nginx-Proxy übernimmt das Routing) - return f"/screenshots/{client_uuid}?t={cache_buster}" - -def fetch_clients(): - try: - verify_ssl = True if ENV == "production" else False - resp = requests.get( - f"{API_BASE_URL}/api/clients", - verify=verify_ssl - ) - resp.raise_for_status() - return resp.json() - except Exception as e: - print("Fehler beim Abrufen der Clients:", e) - return [] - -@dash.callback( - Output("clients-cards-container", "children"), - Input("interval-update", "n_intervals") -) -def update_clients(n): - # ... Session-Handling wie gehabt ... - ensure_mqtt_running() - clients = fetch_clients() - cards = [] - for client in clients: - uuid = client["uuid"] - screenshot = get_latest_screenshot(uuid) - last_alive_utc = client.get("last_alive") - if last_alive_utc: - try: - # Unterstützt sowohl "2024-06-08T12:34:56Z" als auch "2024-06-08T12:34:56" - if last_alive_utc.endswith("Z"): - dt_utc = datetime.strptime(last_alive_utc, "%Y-%m-%dT%H:%M:%SZ") - else: - dt_utc = datetime.strptime(last_alive_utc, "%Y-%m-%dT%H:%M:%S") - dt_utc = dt_utc.replace(tzinfo=pytz.UTC) - # Lokale Zeitzone fest angeben, z.B. Europe/Berlin - local_tz = pytz.timezone("Europe/Berlin") - dt_local = dt_utc.astimezone(local_tz) - last_alive_str = dt_local.strftime("%d.%m.%Y %H:%M:%S") - except Exception: - last_alive_str = last_alive_utc - else: - last_alive_str = "-" - - card = dbc.Card( - [ - dbc.CardHeader(client["location"]), - dbc.CardBody([ - html.Img( - src=screenshot, - style={ - "width": "240px", - "height": "135px", - "object-fit": "cover", - "display": "block", - "margin-left": "auto", - "margin-right": "auto" - }, - ), - html.P(f"IP: {client['ip_address'] or '-'}", className="card-text"), - html.P(f"Letzte Aktivität: {last_alive_str}", className="card-text"), - dbc.ButtonGroup([ - dbc.Button("Reload Page", color="primary", id={"type": "btn-reload", "index": uuid}, n_clicks=0), - dbc.Button("Restart Client", color="danger", id={"type": "btn-restart", "index": uuid}, n_clicks=0), - ], className="mt-2"), - html.Div(id={"type": "restart-feedback", "index": uuid}), - html.Div(id={"type": "reload-feedback", "index": uuid}), - ]), - ], - className="mb-4", - style={"width": "18rem"}, - ) - cards.append(dbc.Col(card, width=4)) - return dbc.Row(cards) - -@dash.callback( - Output({"type": "restart-feedback", "index": MATCH}, "children"), - Input({"type": "btn-restart", "index": MATCH}, "n_clicks"), - State({"type": "btn-restart", "index": MATCH}, "id") -) -def on_restart(n_clicks, btn_id): - if n_clicks and n_clicks > 0: - cid = btn_id["index"] - payload = '{"command": "restart"}' - ok = publish(f"clients/{cid}/control", payload) - return "Befehl gesendet." if ok else "Fehler beim Senden." - return "" - -@dash.callback( - Output({"type": "reload-feedback", "index": MATCH}, "children"), - Input({"type": "btn-reload", "index": MATCH}, "n_clicks"), - State({"type": "btn-reload", "index": MATCH}, "id") -) -def on_reload(n_clicks, btn_id): - if n_clicks and n_clicks > 0: - cid = btn_id["index"] - payload = '{"command": "reload"}' - ok = publish(f"clients/{cid}/control", payload) - return "Befehl gesendet." if ok else "Fehler beim Senden." - return "" diff --git a/dashboard-dash-backup/callbacks/settings_callbacks.py b/dashboard-dash-backup/callbacks/settings_callbacks.py deleted file mode 100644 index f26901b..0000000 --- a/dashboard-dash-backup/callbacks/settings_callbacks.py +++ /dev/null @@ -1,20 +0,0 @@ -# dashboard/callbacks/settings_callbacks.py -import dash -from dash import Input, Output, State, dcc -from flask import session -from utils.db import execute_query, execute_non_query - -@dash.callback( - Output("settings-feedback", "children"), - Input("btn-save-settings", "n_clicks"), - State("input-default-volume", "value"), - prevent_initial_call=True -) -def save_settings(n_clicks, volume): - if "role" not in session: - return dcc.Location(href="/login") - if n_clicks and n_clicks > 0: - sql = "UPDATE global_settings SET value=%s WHERE key='default_volume'" - rc = execute_non_query(sql, (volume,)) - return "Einstellungen gespeichert." if rc else "Speichern fehlgeschlagen." - return "" diff --git a/dashboard-dash-backup/callbacks/ui_callbacks.py b/dashboard-dash-backup/callbacks/ui_callbacks.py deleted file mode 100644 index 9b18d97..0000000 --- a/dashboard-dash-backup/callbacks/ui_callbacks.py +++ /dev/null @@ -1,26 +0,0 @@ -# dashboard/callbacks/ui_callbacks.py - -from dash import Input, Output, State, callback -from components.sidebar import Sidebar - -@callback( - Output("sidebar", "children"), - Output("sidebar", "className"), - Input("sidebar-state", "data"), -) -def render_sidebar(data): - collapsed = data.get("collapsed", False) - return Sidebar(collapsed=collapsed), f"sidebar{' collapsed' if collapsed else ''}" - -@callback( - Output("sidebar-state", "data"), - Input("btn-toggle-sidebar", "n_clicks"), - State("sidebar-state", "data"), - prevent_initial_call=True, -) -def toggle_sidebar(n, data): - if n is None: - # Kein Klick, nichts ändern! - return data - collapsed = not data.get("collapsed", False) - return {"collapsed": collapsed} \ No newline at end of file diff --git a/dashboard-dash-backup/callbacks/users_callbacks.py b/dashboard-dash-backup/callbacks/users_callbacks.py deleted file mode 100644 index 03e8261..0000000 --- a/dashboard-dash-backup/callbacks/users_callbacks.py +++ /dev/null @@ -1,24 +0,0 @@ -# dashboard/callbacks/users_callbacks.py -import dash -from dash import Input, Output, State, dcc -from flask import session -from utils.db import execute_query, execute_non_query -from utils.auth import hash_password - -@dash.callback( - Output("users-feedback", "children"), - Input("btn-new-user", "n_clicks"), - State("input-new-username", "value"), - State("input-new-password", "value"), - State("input-new-role", "value"), - prevent_initial_call=True -) -def create_user(n_clicks, uname, pwd, role): - if session.get("role") != "admin": - return "Keine Berechtigung." - if n_clicks and n_clicks > 0: - pwd_hash = hash_password(pwd) - sql = "INSERT INTO users (username, pwd_hash, role) VALUES (%s, %s, %s)" - rc = execute_non_query(sql, (uname, pwd_hash, role)) - return "Benutzer erstellt." if rc else "Fehler beim Erstellen." - return "" diff --git a/dashboard-dash-backup/components/appointment_modal.py b/dashboard-dash-backup/components/appointment_modal.py deleted file mode 100644 index 3afd7d7..0000000 --- a/dashboard-dash-backup/components/appointment_modal.py +++ /dev/null @@ -1,258 +0,0 @@ -from dash import html, dcc -import dash_mantine_components as dmc -from dash_iconify import DashIconify -import dash_quill - -def create_input_with_tooltip_full(component, tooltip_text): - return dmc.Stack([ - dmc.Group([ - component, - dmc.Tooltip( - children=[ - dmc.ActionIcon( - DashIconify(icon="mdi:help-circle-outline", width=16), - variant="subtle", - color="gray", - size="sm" - ) - ], - label=tooltip_text, - position="top", - multiline=True, - w=300 - ) - ], gap="xs", align="flex-end") - ], gap=0) - -def create_input_with_tooltip_time(component, tooltip_text): - return dmc.Group([ - component, - dmc.Tooltip( - children=[ - dmc.ActionIcon( - DashIconify(icon="mdi:help-circle-outline", width=16), - variant="subtle", - color="gray", - size="sm" - ) - ], - label=tooltip_text, - position="top", - multiline=True, - w=300 - ) - ], gap="xs", align="flex-end") - -def get_appointment_modal(): - weekday_options = [ - {"value": "0", "label": "Montag"}, - {"value": "1", "label": "Dienstag"}, - {"value": "2", "label": "Mittwoch"}, - {"value": "3", "label": "Donnerstag"}, - {"value": "4", "label": "Freitag"}, - {"value": "5", "label": "Samstag"}, - {"value": "6", "label": "Sonntag"} - ] - time_options = [ - {"value": f"{h:02d}:{m:02d}", "label": f"{h:02d}:{m:02d}"} - for h in range(6, 24) for m in [0, 30] - ] - return dmc.Modal( - id="appointment-modal", - title="Neuen Termin anlegen", - centered=True, - size="auto", # oder "80vw" - children=[ - dmc.Container([ - dmc.Grid([ - dmc.GridCol([ - dmc.Paper([ - dmc.Title("Termindetails", order=3, className="mb-3"), - dmc.Stack([ - create_input_with_tooltip_full( - dmc.TextInput( - label="Titel", - placeholder="Terminbezeichnung eingeben", - leftSection=DashIconify(icon="mdi:calendar-text"), - id="title-input", - required=True, - style={"flex": 1} - ), - "Geben Sie eine aussagekräftige Bezeichnung für den Termin ein" - ), - create_input_with_tooltip_full( - dmc.DatePickerInput( - label="Startdatum", - id="start-date-input", - firstDayOfWeek=1, - weekendDays=[0, 6], - valueFormat="DD.MM.YYYY", - placeholder="Datum auswählen", - leftSection=DashIconify(icon="mdi:calendar"), - clearable=False, - style={"flex": 1} - ), - "Wählen Sie das Datum für den Termin aus dem Kalender" - ), - dmc.Grid([ - dmc.GridCol([ - dmc.Stack([ - create_input_with_tooltip_time( - dmc.Select( - label="Startzeit", - placeholder="Zeit auswählen", - data=time_options, - searchable=True, - clearable=True, - id="time-start", - value="09:00", - required=True, - style={"flex": 1} - ), - "Eingabe: 10:25, 10.25, 1025 oder 10 (wird automatisch formatiert)" - ), - html.Div(id="start-time-feedback") - ], gap="xs") - ], span=6), - dmc.GridCol([ - dmc.Stack([ - create_input_with_tooltip_time( - dmc.Select( - label="Endzeit", - placeholder="Zeit auswählen", - data=time_options, - searchable=True, - clearable=True, - id="time-end", - required=True, - style={"flex": 1} - ), - "Endzeit muss nach der Startzeit am selben Tag liegen. Termine können nicht über Mitternacht hinausgehen." - ), - html.Div(id="end-time-feedback") - ], gap="xs") - ], span=6) - ]), - create_input_with_tooltip_full( - dmc.Select( - label="Termintyp", - placeholder="Typ auswählen", - data=[ - {"value": "presentation", "label": "Präsentation"}, - {"value": "website", "label": "Website"}, - {"value": "video", "label": "Video"}, - {"value": "message", "label": "Nachricht"}, - {"value": "webuntis", "label": "WebUntis"}, - {"value": "other", "label": "Sonstiges"} - ], - id="type-input", - required=True, - style={"flex": 1} - ), - "Wählen Sie die Art der Präsentation aus." - ), - html.Div(id="type-specific-fields"), - create_input_with_tooltip_full( - dmc.Textarea( - label="Beschreibung", - placeholder="Zusätzliche Informationen...", - minRows=3, - autosize=True, - id="description-input", - style={"flex": 1} - ), - "Optionale Beschreibung mit weiteren Details zum Termin" - ) - ], gap="md") - ], p="md", shadow="sm") - ], span=6), - dmc.GridCol([ - dmc.Paper([ - dmc.Title("Wiederholung", order=3, className="mb-3"), - dmc.Stack([ - create_input_with_tooltip_full( - dmc.Checkbox( - label="Wiederholender Termin", - id="repeat-checkbox", - description="Aktivieren für wöchentliche Wiederholung" - ), - "Aktivieren Sie diese Option, um den Termin an mehreren Wochentagen zu wiederholen" - ), - html.Div(id="repeat-options", children=[ - create_input_with_tooltip_full( - dmc.MultiSelect( - label="Wochentage", - placeholder="Wochentage auswählen", - data=weekday_options, - id="weekdays-select", - description="An welchen Wochentagen soll der Termin stattfinden?", - disabled=True, - style={"flex": 1} - ), - "Wählen Sie mindestens einen Wochentag für die Wiederholung aus" - ), - dmc.Space(h="md"), - create_input_with_tooltip_full( - dmc.DatePickerInput( - label="Wiederholung bis", - id="repeat-until-date", - firstDayOfWeek=1, - weekendDays=[0, 6], - valueFormat="DD.MM.YYYY", - placeholder="Enddatum auswählen", - leftSection=DashIconify(icon="mdi:calendar-end"), - description="Letzter Tag der Wiederholung", - disabled=True, - clearable=True, - style={"flex": 1} - ), - "Datum bis wann die Wiederholung stattfinden soll. Muss nach dem Startdatum liegen." - ), - dmc.Space(h="lg"), - create_input_with_tooltip_full( - dmc.Checkbox( - label="Ferientage berücksichtigen", - id="skip-holidays-checkbox", - description="Termine an Feiertagen und in Schulferien auslassen", - disabled=True - ), - "Aktivieren Sie diese Option, um Termine an deutschen Feiertagen und in Schulferien automatisch zu überspringen" - ) - ]) - ], gap="md") - ], p="md", shadow="sm"), - dmc.Paper([ - dmc.Title("Aktionen", order=3, className="mb-3"), - dmc.Stack([ - dmc.Button( - "Termin(e) speichern", - color="green", - leftSection=DashIconify(icon="mdi:content-save"), - id="btn-save", - size="lg", - fullWidth=True - ), - dmc.Button( - "Zurücksetzen", - color="gray", - variant="outline", - leftSection=DashIconify(icon="mdi:refresh"), - id="btn-reset", - fullWidth=True - ), - dmc.Button( - "Schließen", - id="close-appointment-modal-btn", - color="red", # oder "danger" - leftSection=DashIconify(icon="mdi:close"), - variant="filled", - style={"marginBottom": 10} - ), - html.Div(id="save-feedback", className="mt-3") - ], gap="md") - ], p="md", shadow="sm", className="mt-3") - ], span=6) - ]) - ], size="lg") - ] - ) \ No newline at end of file diff --git a/dashboard-dash-backup/components/header.py b/dashboard-dash-backup/components/header.py deleted file mode 100644 index ca92250..0000000 --- a/dashboard-dash-backup/components/header.py +++ /dev/null @@ -1,13 +0,0 @@ -# dashboard/components/header.py -from dash import html - -def Header(): - return html.Div( - className="app-header", - children=[ - html.Img(src="/assets/logo.png", className="logo"), - html.Span("Infoscreen-Manager", className="app-title"), - html.Span(" – Organisationsname", className="org-name"), - html.Div(id="header-right", className="header-right") # Platzhalter für Login/Profil‐Button - ] - ) diff --git a/dashboard-dash-backup/components/sidebar.py b/dashboard-dash-backup/components/sidebar.py deleted file mode 100644 index 6b073a0..0000000 --- a/dashboard-dash-backup/components/sidebar.py +++ /dev/null @@ -1,72 +0,0 @@ -# dashboard/components/sidebar.py - -from dash import html -import dash_bootstrap_components as dbc -from dash_iconify import DashIconify - -def Sidebar(collapsed: bool = False): - """ - Gibt nur den Inhalt der Sidebar zurück (ohne das äußere div mit id="sidebar"). - Das äußere div wird bereits in app.py definiert. - """ - - nav_items = [ - {"label": "Übersicht", "href": "/overview", "icon": "mdi:view-dashboard"}, - {"label": "Termine", "href": "/appointments","icon": "mdi:calendar"}, - {"label": "Bildschirme", "href": "/clients", "icon": "mdi:monitor"}, - {"label": "Einstellungen","href": "/settings", "icon": "mdi:cog"}, - {"label": "Benutzer", "href": "/users", "icon": "mdi:account"}, - ] - - if collapsed: - nav_links = [ - dbc.NavLink( - DashIconify(icon=item["icon"], width=24), - href=item["href"], - active="exact", - className="sidebar-item-collapsed", - id={"type": "nav-item", "index": item["label"]}, - ) - for item in nav_items - ] - else: - nav_links = [ - dbc.NavLink( - [ - DashIconify(icon=item["icon"], width=24), - html.Span(item["label"], className="ms-2 sidebar-label"), - ], - href=item["href"], - active="exact", - className="sidebar-item", - id={"type": "nav-item", "index": item["label"]}, - ) - for item in nav_items - ] - - return [ - html.Div( - className="sidebar-toggle", - children=html.Button( - DashIconify(icon="mdi:menu", width=28), - id="btn-toggle-sidebar", - className="toggle-button", - ) - ), - dbc.Collapse( - dbc.Nav( - nav_links, - vertical=True, - pills=True, - className="sidebar-nav", - ), - is_open=not collapsed, - className="sidebar-nav", - ) if not collapsed else - dbc.Nav( - nav_links, - vertical=True, - pills=True, - className="sidebar-nav-collapsed", - ), - ] \ No newline at end of file diff --git a/dashboard-dash-backup/config.py b/dashboard-dash-backup/config.py deleted file mode 100644 index ed723a9..0000000 --- a/dashboard-dash-backup/config.py +++ /dev/null @@ -1,28 +0,0 @@ -# dashboard/config.py -import os -from dotenv import load_dotenv - -# .env aus Root‐Verzeichnis laden -base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) -load_dotenv(os.path.join(base_dir, ".env")) - -# DB‐Einstellungen -DB_HOST = os.getenv("DB_HOST") -DB_PORT = int(os.getenv("DB_PORT", "3306")) -DB_USER = os.getenv("DB_USER") -DB_PASSWORD = os.getenv("DB_PASSWORD") -DB_NAME = os.getenv("DB_NAME") -DB_POOL_NAME = os.getenv("DB_POOL_NAME", "my_pool") -DB_POOL_SIZE = int(os.getenv("DB_POOL_SIZE", "5")) - -# MQTT‐Einstellungen -MQTT_BROKER_HOST = os.getenv("MQTT_BROKER_HOST") -MQTT_BROKER_PORT = int(os.getenv("MQTT_BROKER_PORT", "1883")) -MQTT_USERNAME = os.getenv("MQTT_USERNAME") -MQTT_PASSWORD = os.getenv("MQTT_PASSWORD") -MQTT_KEEPALIVE = int(os.getenv("MQTT_KEEPALIVE", "60")) -MQTT_CLIENT_ID = os.getenv("MQTT_CLIENT_ID") - -# Sonstige Einstellungen -SECRET_KEY = os.getenv("SECRET_KEY", "changeme") -ENV = os.getenv("ENV", "development") diff --git a/dashboard-dash-backup/dash_using_fullcalendar-0.1.0.tar.gz b/dashboard-dash-backup/dash_using_fullcalendar-0.1.0.tar.gz deleted file mode 100644 index ae7dd89..0000000 Binary files a/dashboard-dash-backup/dash_using_fullcalendar-0.1.0.tar.gz and /dev/null differ diff --git a/dashboard-dash-backup/fullcalendar_test.py b/dashboard-dash-backup/fullcalendar_test.py deleted file mode 100644 index 2dfe1b2..0000000 --- a/dashboard-dash-backup/fullcalendar_test.py +++ /dev/null @@ -1,402 +0,0 @@ -import dash -import full_calendar_component as fcc -from dash import * -import dash_mantine_components as dmc -from dash.exceptions import PreventUpdate -from datetime import datetime, date, timedelta -import dash_quill - -# dash._dash_renderer._set_react_version('18.2.0') - -app = Dash(__name__, prevent_initial_callbacks=True) - -quill_mods = [ - [{"header": "1"}, {"header": "2"}, {"font": []}], - [{"size": []}], - ["bold", "italic", "underline", "strike", "blockquote"], - [{"list": "ordered"}, {"list": "bullet"}, {"indent": "-1"}, {"indent": "+1"}], - ["link", "image"], -] - -# Get today's date -today = datetime.now() - -# Format the date -formatted_date = today.strftime("%Y-%m-%d") - -app.layout = html.Div( - [ - fcc.FullCalendarComponent( - id="calendar", # Unique ID for the component - initialView="listWeek", # dayGridMonth, timeGridWeek, timeGridDay, listWeek, - # dayGridWeek, dayGridYear, multiMonthYear, resourceTimeline, resourceTimeGridDay, resourceTimeLineWeek - headerToolbar={ - "left": "prev,next today", - "center": "", - "right": "listWeek,timeGridDay,timeGridWeek,dayGridMonth", - }, # Calendar header - initialDate=f"{formatted_date}", # Start date for calendar - editable=True, # Allow events to be edited - selectable=True, # Allow dates to be selected - events=[], - nowIndicator=True, # Show current time indicator - navLinks=True, # Allow navigation to other dates - ), - dmc.MantineProvider( - theme={"colorScheme": "dark"}, - children=[ - dmc.Modal( - id="modal", - size="xl", - title="Event Details", - zIndex=10000, - children=[ - html.Div(id="modal_event_display_context"), - dmc.Space(h=20), - dmc.Group( - [ - dmc.Button( - "Close", - color="red", - variant="outline", - id="modal-close-button", - ), - ], - pos="right", - ), - ], - ) - ], - ), - dmc.MantineProvider( - theme={"colorScheme": "dark"}, - children=[ - dmc.Modal( - id="add_modal", - title="New Event", - size="xl", - children=[ - dmc.Grid( - children=[ - dmc.GridCol( - html.Div( - dmc.DatePickerInput( - id="start_date", - label="Start Date", - value=datetime.now().date(), - styles={"width": "100%"}, - disabled=True, - ), - style={"width": "100%"}, - ), - span=6, - ), - dmc.GridCol( - html.Div( - dmc.TimeInput( - label="Start Time", - withSeconds=True, - value=datetime.now(), - # format="12", - id="start_time", - ), - style={"width": "100%"}, - ), - span=6, - ), - ], - gutter="xl", - ), - dmc.Grid( - children=[ - dmc.GridCol( - html.Div( - dmc.DatePickerInput( - id="end_date", - label="End Date", - value=datetime.now().date(), - styles={"width": "100%"}, - ), - style={"width": "100%"}, - ), - span=6, - ), - dmc.GridCol( - html.Div( - dmc.TimeInput( - label="End Time", - withSeconds=True, - value=datetime.now(), - # format="12", - id="end_time", - ), - style={"width": "100%"}, - ), - span=6, - ), - ], - gutter="xl", - ), - dmc.Grid( - children=[ - dmc.GridCol( - span=6, - children=[ - dmc.TextInput( - label="Event Title:", - style={"width": "100%"}, - id="event_name_input", - required=True, - ) - ], - ), - dmc.GridCol( - span=6, - children=[ - dmc.Select( - label="Select event color", - placeholder="Select one", - id="event_color_select", - value="ng", - data=[ - { - "value": "bg-gradient-primary", - "label": "bg-gradient-primary", - }, - { - "value": "bg-gradient-secondary", - "label": "bg-gradient-secondary", - }, - { - "value": "bg-gradient-success", - "label": "bg-gradient-success", - }, - { - "value": "bg-gradient-info", - "label": "bg-gradient-info", - }, - { - "value": "bg-gradient-warning", - "label": "bg-gradient-warning", - }, - { - "value": "bg-gradient-danger", - "label": "bg-gradient-danger", - }, - { - "value": "bg-gradient-light", - "label": "bg-gradient-light", - }, - { - "value": "bg-gradient-dark", - "label": "bg-gradient-dark", - }, - { - "value": "bg-gradient-white", - "label": "bg-gradient-white", - }, - ], - style={"width": "100%", "marginBottom": 10}, - required=True, - ) - ], - ), - ] - ), - dash_quill.Quill( - id="rich_text_input", - modules={ - "toolbar": quill_mods, - "clipboard": { - "matchVisual": False, - }, - }, - ), - dmc.Accordion( - children=[ - dmc.AccordionItem( - [ - dmc.AccordionControl("Raw HTML"), - dmc.AccordionPanel( - html.Div( - id="rich_text_output", - style={ - "height": "300px", - "overflowY": "scroll", - }, - ) - ), - ], - value="raw_html", - ), - ], - ), - dmc.Space(h=20), - dmc.Group( - [ - dmc.Button( - "Submit", - id="modal_submit_new_event_button", - color="green", - ), - dmc.Button( - "Close", - color="red", - variant="outline", - id="modal_close_new_event_button", - ), - ], - pos="right", - ), - ], - ), - ], - ), - ] -) - - -@app.callback( - Output("modal", "opened"), - Output("modal", "title"), - Output("modal_event_display_context", "children"), - Input("modal-close-button", "n_clicks"), - Input("calendar", "clickedEvent"), - State("modal", "opened"), -) -def open_event_modal(n, clickedEvent, opened): - ctx = callback_context - - if not ctx.triggered: - raise PreventUpdate - else: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - - if button_id == "calendar" and clickedEvent is not None: - event_title = clickedEvent["title"] - event_context = clickedEvent["extendedProps"]["context"] - return ( - True, - event_title, - html.Div( - dash_quill.Quill( - id="input3", - value=f"{event_context}", - modules={ - "toolbar": False, - "clipboard": { - "matchVisual": False, - }, - }, - ), - style={"width": "100%", "overflowY": "auto"}, - ), - ) - elif button_id == "modal-close-button" and n is not None: - return False, dash.no_update, dash.no_update - - return opened, dash.no_update - - -@app.callback( - Output("add_modal", "opened"), - Output("start_date", "value"), - Output("end_date", "value"), - Output("start_time", "value"), - Output("end_time", "value"), - Input("calendar", "dateClicked"), - Input("modal_close_new_event_button", "n_clicks"), - State("add_modal", "opened"), -) -def open_add_modal(dateClicked, close_clicks, opened): - - ctx = callback_context - - if not ctx.triggered: - raise PreventUpdate - else: - button_id = ctx.triggered[0]["prop_id"].split(".")[0] - - if button_id == "calendar" and dateClicked is not None: - try: - start_time = datetime.strptime(dateClicked, "%Y-%m-%dT%H:%M:%S%z").time() - start_date_obj = datetime.strptime(dateClicked, "%Y-%m-%dT%H:%M:%S%z") - start_date = start_date_obj.strftime("%Y-%m-%d") - end_date = start_date_obj.strftime("%Y-%m-%d") - except ValueError: - start_time = datetime.now().time() - start_date_obj = datetime.strptime(dateClicked, "%Y-%m-%d") - start_date = start_date_obj.strftime("%Y-%m-%d") - end_date = start_date_obj.strftime("%Y-%m-%d") - end_time = datetime.combine(date.today(), start_time) + timedelta(hours=1) - start_time_str = start_time.strftime("%Y-%m-%d %H:%M:%S") - end_time_str = end_time.strftime("%Y-%m-%d %H:%M:%S") - return True, start_date, end_date, start_time_str, end_time_str - - elif button_id == "modal_close_new_event_button" and close_clicks is not None: - return False, dash.no_update, dash.no_update, dash.no_update, dash.no_update - - return opened, dash.no_update, dash.no_update, dash.no_update, dash.no_update - - -@app.callback( - Output("calendar", "events"), - Output("add_modal", "opened", allow_duplicate=True), - Output("event_name_input", "value"), - Output("event_color_select", "value"), - Output("rich_text_input", "value"), - Input("modal_submit_new_event_button", "n_clicks"), - State("start_date", "value"), - State("start_time", "value"), - State("end_date", "value"), - State("end_time", "value"), - State("event_name_input", "value"), - State("event_color_select", "value"), - State("rich_text_output", "children"), - State("calendar", "events"), -) -def add_new_event( - n, - start_date, - start_time, - end_date, - end_time, - event_name, - event_color, - event_context, - current_events, -): - if n is None: - raise PreventUpdate - - start_time_obj = datetime.strptime(start_time, "%Y-%m-%d %H:%M:%S") - end_time_obj = datetime.strptime(end_time, "%Y-%m-%d %H:%M:%S") - - start_time_str = start_time_obj.strftime("%H:%M:%S") - end_time_str = end_time_obj.strftime("%H:%M:%S") - - start_date = f"{start_date}T{start_time_str}" - end_date = f"{end_date}T{end_time_str}" - - new_event = { - "title": event_name, - "start": start_date, - "end": end_date, - "className": event_color, - "context": event_context, - } - - return current_events + [new_event], False, "", "bg-gradient-primary", "" - - -@app.callback( - Output("rich_text_output", "children"), - [Input("rich_text_input", "value")], - [State("rich_text_input", "charCount")], -) -def display_output(value, charCount): - return value - - -if __name__ == "__main__": - app.run(host='0.0.0.0', debug=True, port=8050) \ No newline at end of file diff --git a/dashboard-dash-backup/infoscreen_server.py b/dashboard-dash-backup/infoscreen_server.py deleted file mode 100644 index dbfc21e..0000000 --- a/dashboard-dash-backup/infoscreen_server.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Collapsible navbar on both desktop and mobile -""" - - -import dash_mantine_components as dmc -from dash import Dash, Input, Output, State, callback -from dash_iconify import DashIconify - -app = Dash(external_stylesheets=dmc.styles.ALL) - -logo = "https://github.com/user-attachments/assets/c1ff143b-4365-4fd1-880f-3e97aab5c302" - -def get_icon(icon): - return DashIconify(icon=icon, height=16) - -layout = dmc.AppShell( - [ - dmc.AppShellHeader( - dmc.Group( - [ - dmc.Burger( - id="mobile-burger", - size="sm", - hiddenFrom="sm", - opened=False, - ), - dmc.Burger( - id="desktop-burger", - size="sm", - visibleFrom="sm", - opened=True, - ), - dmc.Image(src=logo, h=40), - dmc.Title("Demo App", c="blue"), - ], - h="100%", - px="md", - ) - ), - dmc.AppShellNavbar( - id="navbar", - children=[ - "Navbar", - dmc.NavLink( - label="With icon", - leftSection=get_icon(icon="bi:house-door-fill"), - ), - ], - p="md", - ), - dmc.AppShellMain("Main"), - ], - header={"height": 60}, - navbar={ - "width": 300, - "breakpoint": "sm", - "collapsed": {"mobile": True, "desktop": False}, - }, - padding="md", - id="appshell", -) - -app.layout = dmc.MantineProvider(layout) - - -@callback( - Output("appshell", "navbar"), - Input("mobile-burger", "opened"), - Input("desktop-burger", "opened"), - State("appshell", "navbar"), -) -def toggle_navbar(mobile_opened, desktop_opened, navbar): - navbar["collapsed"] = { - "mobile": not mobile_opened, - "desktop": not desktop_opened, - } - return navbar - - - -if __name__ == "__main__": - app.run(host='0.0.0.0', debug=True, port=8050) diff --git a/dashboard-dash-backup/manitine_test.py b/dashboard-dash-backup/manitine_test.py deleted file mode 100644 index 9106510..0000000 --- a/dashboard-dash-backup/manitine_test.py +++ /dev/null @@ -1,892 +0,0 @@ -import dash -from dash import html, Input, Output, State, callback, dcc -import dash_mantine_components as dmc -from dash_iconify import DashIconify -from datetime import datetime, date, timedelta -import re -import base64 -import dash_quill - -app = dash.Dash(__name__, suppress_callback_exceptions=True) # Wichtig für dynamische IDs - -# Deutsche Lokalisierung für Mantine -german_dates_provider_props = { - "settings": { - "locale": "de", - "firstDayOfWeek": 1, - "weekendDays": [0, 6], - "labels": { - "ok": "OK", - "cancel": "Abbrechen", - "clear": "Löschen", - "monthPickerControl": "Monat auswählen", - "yearPickerControl": "Jahr auswählen", - "nextMonth": "Nächster Monat", - "previousMonth": "Vorheriger Monat", - "nextYear": "Nächstes Jahr", - "previousYear": "Vorheriges Jahr", - "nextDecade": "Nächstes Jahrzehnt", - "previousDecade": "Vorheriges Jahrzehnt" - } - } -} - -# Wochentage für Wiederholung -weekday_options = [ - {"value": "0", "label": "Montag"}, - {"value": "1", "label": "Dienstag"}, - {"value": "2", "label": "Mittwoch"}, - {"value": "3", "label": "Donnerstag"}, - {"value": "4", "label": "Freitag"}, - {"value": "5", "label": "Samstag"}, - {"value": "6", "label": "Sonntag"} -] - -# Deutsche Feiertage (vereinfacht, ohne Berechnung von Ostern etc.) -GERMAN_HOLIDAYS_2025 = [ - date(2025, 1, 1), # Neujahr - date(2025, 1, 6), # Heilige Drei Könige - date(2025, 4, 18), # Karfreitag (Beispiel - muss berechnet werden) - date(2025, 4, 21), # Ostermontag (Beispiel - muss berechnet werden) - date(2025, 5, 1), # Tag der Arbeit - date(2025, 5, 29), # Christi Himmelfahrt (Beispiel - muss berechnet werden) - date(2025, 6, 9), # Pfingstmontag (Beispiel - muss berechnet werden) - date(2025, 10, 3), # Tag der Deutschen Einheit - date(2025, 12, 25), # 1. Weihnachtstag - date(2025, 12, 26), # 2. Weihnachtstag -] - -# Schulferien (Beispiel für NRW 2025) -SCHOOL_HOLIDAYS_2025 = [ - # Weihnachtsferien - (date(2024, 12, 23), date(2025, 1, 6)), - # Osterferien - (date(2025, 4, 14), date(2025, 4, 26)), - # Sommerferien - (date(2025, 7, 14), date(2025, 8, 26)), - # Herbstferien - (date(2025, 10, 14), date(2025, 10, 25)), -] - -def is_holiday_or_vacation(check_date): - """Prüft, ob ein Datum ein Feiertag oder in den Ferien liegt""" - # Feiertage prüfen - if check_date in GERMAN_HOLIDAYS_2025: - return True - - # Schulferien prüfen - for start_vacation, end_vacation in SCHOOL_HOLIDAYS_2025: - if start_vacation <= check_date <= end_vacation: - return True - - return False - -# Zeitraster für 30-Minuten-Intervalle generieren -def generate_time_options(): - options = [] - - # Basis: 30-Minuten-Raster - for h in range(6, 24): # Bis 23:30 - for m in [0, 30]: - options.append({"value": f"{h:02d}:{m:02d}", "label": f"{h:02d}:{m:02d}"}) - - return options - -time_options = generate_time_options() - -# Hilfsfunktion für Input-Felder mit Tooltip - volle Breite -def create_input_with_tooltip_full(component, tooltip_text): - """Erstellt ein Input-Feld mit Tooltip-Icon über die volle Breite""" - return dmc.Stack([ - dmc.Group([ - component, - dmc.Tooltip( - children=[ - dmc.ActionIcon( - DashIconify(icon="mdi:help-circle-outline", width=16), - variant="subtle", - color="gray", - size="sm" - ) - ], - label=tooltip_text, - position="top", - multiline=True, - w=300 - ) - ], gap="xs", align="flex-end") - ], gap=0) - -# Hilfsfunktion für Input-Felder mit Tooltip - für Zeit-Grid -def create_input_with_tooltip_time(component, tooltip_text): - """Erstellt ein Input-Feld mit Tooltip-Icon für Zeit-Eingaben""" - return dmc.Group([ - component, - dmc.Tooltip( - children=[ - dmc.ActionIcon( - DashIconify(icon="mdi:help-circle-outline", width=16), - variant="subtle", - color="gray", - size="sm" - ) - ], - label=tooltip_text, - position="top", - multiline=True, - w=300 - ) - ], gap="xs", align="flex-end") - -app.layout = dmc.MantineProvider([ - dmc.DatesProvider(**german_dates_provider_props, children=[ - dmc.Container([ - dmc.Title("Erweiterte Terminverwaltung", order=1, className="mb-4"), - - dmc.Grid([ - dmc.GridCol([ - dmc.Paper([ - dmc.Title("Termindetails", order=3, className="mb-3"), - - dmc.Stack([ - create_input_with_tooltip_full( - dmc.TextInput( - label="Titel", - placeholder="Terminbezeichnung eingeben", - leftSection=DashIconify(icon="mdi:calendar-text"), - id="title-input", - required=True, - style={"flex": 1} - ), - "Geben Sie eine aussagekräftige Bezeichnung für den Termin ein" - ), - - create_input_with_tooltip_full( - dmc.DatePickerInput( - label="Startdatum", - value=datetime.now().date(), - id="start-date-input", - firstDayOfWeek=1, - weekendDays=[0, 6], - valueFormat="DD.MM.YYYY", - placeholder="Datum auswählen", - leftSection=DashIconify(icon="mdi:calendar"), - clearable=False, - style={"flex": 1} - ), - "Wählen Sie das Datum für den Termin aus dem Kalender" - ), - - # Zeitbereich - nebeneinander - dmc.Grid([ - dmc.GridCol([ - dmc.Stack([ - create_input_with_tooltip_time( - dmc.Select( - label="Startzeit", - placeholder="Zeit auswählen", - data=time_options, - searchable=True, - clearable=True, - id="time-start", - value="09:00", - required=True, - style={"flex": 1} - ), - "Eingabe: 10:25, 10.25, 1025 oder 10 (wird automatisch formatiert)" - ), - html.Div(id="start-time-feedback") - ], gap="xs") - ], span=6), - dmc.GridCol([ - dmc.Stack([ - create_input_with_tooltip_time( - dmc.Select( - label="Endzeit", - placeholder="Zeit auswählen", - data=time_options, - searchable=True, - clearable=True, - id="time-end", - required=True, - style={"flex": 1} - ), - "Endzeit muss nach der Startzeit am selben Tag liegen. Termine können nicht über Mitternacht hinausgehen." - ), - html.Div(id="end-time-feedback") - ], gap="xs") - ], span=6) - ]), - - create_input_with_tooltip_full( - dmc.Select( - label="Termintyp", - placeholder="Typ auswählen", - data=[ - {"value": "presentation", "label": "Präsentation"}, - {"value": "website", "label": "Website"}, - {"value": "video", "label": "Video"}, - {"value": "message", "label": "Nachricht"}, - {"value": "other", "label": "Sonstiges"} - ], - id="type-input", - required=True, - style={"flex": 1} - ), - "Wählen Sie den Typ des Termins für bessere Kategorisierung" - ), - - # Dynamische typ-spezifische Felder - html.Div(id="type-specific-fields"), - - create_input_with_tooltip_full( - dmc.Textarea( - label="Beschreibung", - placeholder="Zusätzliche Informationen...", - minRows=3, - autosize=True, - id="description-input", - style={"flex": 1} - ), - "Optionale Beschreibung mit weiteren Details zum Termin" - ) - ], gap="md") - ], p="md", shadow="sm") - ], span=6), - - dmc.GridCol([ - dmc.Paper([ - dmc.Title("Wiederholung", order=3, className="mb-3"), - - dmc.Stack([ - create_input_with_tooltip_full( - dmc.Checkbox( - label="Wiederholender Termin", - id="repeat-checkbox", - description="Aktivieren für wöchentliche Wiederholung" - ), - "Aktivieren Sie diese Option, um den Termin an mehreren Wochentagen zu wiederholen" - ), - - html.Div(id="repeat-options", children=[ - create_input_with_tooltip_full( - dmc.MultiSelect( - label="Wochentage", - placeholder="Wochentage auswählen", - data=weekday_options, - id="weekdays-select", - description="An welchen Wochentagen soll der Termin stattfinden?", - disabled=True, - style={"flex": 1} - ), - "Wählen Sie mindestens einen Wochentag für die Wiederholung aus" - ), - - dmc.Space(h="md"), # Abstand zwischen DatePicker und Ferientage - - create_input_with_tooltip_full( - dmc.DatePickerInput( - label="Wiederholung bis", - id="repeat-until-date", - firstDayOfWeek=1, - weekendDays=[0, 6], - valueFormat="DD.MM.YYYY", - placeholder="Enddatum auswählen", - leftSection=DashIconify(icon="mdi:calendar-end"), - description="Letzter Tag der Wiederholung", - disabled=True, - clearable=True, - style={"flex": 1} - ), - "Datum bis wann die Wiederholung stattfinden soll. Muss nach dem Startdatum liegen." - ), - - dmc.Space(h="lg"), # Größerer Abstand vor Ferientage - - create_input_with_tooltip_full( - dmc.Checkbox( - label="Ferientage berücksichtigen", - id="skip-holidays-checkbox", - description="Termine an Feiertagen und in Schulferien auslassen", - disabled=True - ), - "Aktivieren Sie diese Option, um Termine an deutschen Feiertagen und in Schulferien automatisch zu überspringen" - ) - ]) - ], gap="md") - ], p="md", shadow="sm"), - - dmc.Paper([ - dmc.Title("Aktionen", order=3, className="mb-3"), - - dmc.Stack([ - dmc.Button( - "Termin(e) speichern", - color="green", - leftSection=DashIconify(icon="mdi:content-save"), - id="btn-save", - size="lg", - fullWidth=True - ), - - dmc.Button( - "Zurücksetzen", - color="gray", - variant="outline", - leftSection=DashIconify(icon="mdi:refresh"), - id="btn-reset", - fullWidth=True - ), - - html.Div(id="save-feedback", className="mt-3") - ], gap="md") - ], p="md", shadow="sm", className="mt-3") - ], span=6) - ]), - - # Vorschau-Bereich - dmc.Paper([ - dmc.Title("Vorschau", order=3, className="mb-3"), - html.Div(id="preview-area") - ], p="md", shadow="sm", className="mt-4") - ], size="lg") - ]) -]) - -# Zeit-Validierungsfunktion -def validate_and_format_time(time_str): - """Validiert und formatiert Zeitangaben""" - if not time_str: - return None, "Keine Zeit angegeben" - - # Bereits korrektes Format - if re.match(r'^\d{2}:\d{2}$', time_str): - try: - h, m = map(int, time_str.split(':')) - if 0 <= h <= 23 and 0 <= m <= 59: - return time_str, "Gültige Zeit" - except: - pass - - # Verschiedene Eingabeformate versuchen - patterns = [ - (r'^(\d{1,2}):(\d{2})$', lambda m: (int(m.group(1)), int(m.group(2)))), - (r'^(\d{1,2})\.(\d{2})$', lambda m: (int(m.group(1)), int(m.group(2)))), - (r'^(\d{1,2})(\d{2})$', lambda m: (int(m.group(1)), int(m.group(2)))), - (r'^(\d{1,2})$', lambda m: (int(m.group(1)), 0)), - ] - - for pattern, extractor in patterns: - match = re.match(pattern, time_str.strip()) - if match: - try: - hours, minutes = extractor(match) - if 0 <= hours <= 23 and 0 <= minutes <= 59: - return f"{hours:02d}:{minutes:02d}", "Automatisch formatiert" - except: - continue - - return None, "Ungültiges Zeitformat" - -# Typ-spezifische Felder anzeigen -@callback( - Output('type-specific-fields', 'children'), - Input('type-input', 'value'), - prevent_initial_call=True -) -def show_type_specific_fields(event_type): - if not event_type: - return html.Div() - - if event_type == "presentation": - return dmc.Stack([ - dmc.Divider(label="Präsentations-Details", labelPosition="center"), - dmc.Group([ - dcc.Upload( - id='presentation-upload', - children=dmc.Button( - "Datei hochladen", - leftSection=DashIconify(icon="mdi:upload"), - variant="outline" - ), - style={'width': 'auto'} - ), - dmc.TextInput( - label="Präsentationslink", - placeholder="https://...", - leftSection=DashIconify(icon="mdi:link"), - id="presentation-link", - style={"minWidth": 0, "flex": 1, "marginBottom": 0} - ) - ], grow=True, align="flex-end", style={"marginBottom": 10}), - html.Div(id="presentation-upload-status") - ], gap="sm") - - elif event_type == "video": - return dmc.Stack([ - dmc.Divider(label="Video-Details", labelPosition="center"), - dmc.Group([ - dcc.Upload( - id='video-upload', - children=dmc.Button( - "Video hochladen", - leftSection=DashIconify(icon="mdi:video-plus"), - variant="outline" - ), - style={'width': 'auto'} - ), - dmc.TextInput( - label="Videolink", - placeholder="https://youtube.com/...", - leftSection=DashIconify(icon="mdi:youtube"), - id="video-link", - style={"minWidth": 0, "flex": 1, "marginBottom": 0} - ) - ], grow=True, align="flex-end", style={"marginBottom": 10}), - dmc.Group([ - dmc.Checkbox( - label="Endlos wiederholen", - id="video-endless", - checked=True, - style={"marginRight": 20} - ), - dmc.NumberInput( - label="Wiederholungen", - id="video-repeats", - value=1, - min=1, - max=99, - step=1, - disabled=True, - style={"width": 150} - ), - dmc.Slider( - label="Lautstärke", - id="video-volume", - value=70, - min=0, - max=100, - step=5, - marks=[ - {"value": 0, "label": "0%"}, - {"value": 50, "label": "50%"}, - {"value": 100, "label": "100%"} - ], - style={"flex": 1, "marginLeft": 20} - ) - ], grow=True, align="flex-end"), - html.Div(id="video-upload-status") - ], gap="sm") - - elif event_type == "website": - return dmc.Stack([ - dmc.Divider(label="Website-Details", labelPosition="center"), - dmc.TextInput( - label="Website-URL", - placeholder="https://example.com", - leftSection=DashIconify(icon="mdi:web"), - id="website-url", - required=True, - style={"flex": 1} - ) - # Anzeigedauer entfernt! - ], gap="sm") - - elif event_type == "message": - return dmc.Stack([ - dmc.Divider(label="Nachrichten-Details", labelPosition="center"), - dash_quill.Quill( - id="message-content", - value="", - # theme="snow", - # style={"height": "150px", "marginBottom": 10} - ), - dmc.Group([ - dmc.Select( - label="Schriftgröße", - data=[ - {"value": "small", "label": "Klein"}, - {"value": "medium", "label": "Normal"}, - {"value": "large", "label": "Groß"}, - {"value": "xlarge", "label": "Sehr groß"} - ], - id="message-font-size", - value="medium", - style={"flex": 1} - ), - dmc.ColorPicker( - id="message-color", - value="#000000", - format="hex", - swatches=[ - "#000000", "#ffffff", "#ff0000", "#00ff00", - "#0000ff", "#ffff00", "#ff00ff", "#00ffff" - ] - ) - ], grow=True, align="flex-end") - ], gap="sm") - - return html.Div() - -# Callback zum Aktivieren/Deaktivieren des Wiederholungsfelds bei Video -@callback( - Output("video-repeats", "disabled"), - Input("video-endless", "checked"), - prevent_initial_call=True -) -def toggle_video_repeats(endless_checked): - return endless_checked - -# Upload-Status für Präsentation -@callback( - Output('presentation-upload-status', 'children'), - Input('presentation-upload', 'contents'), - State('presentation-upload', 'filename'), - prevent_initial_call=True -) -def update_presentation_upload_status(contents, filename): - """Zeigt Status des Präsentations-Uploads""" - if contents is not None and filename is not None: - return dmc.Alert( - f"✓ Datei '{filename}' erfolgreich hochgeladen", - color="green", - className="mt-2" - ) - return html.Div() - -# Upload-Status für Video -@callback( - Output('video-upload-status', 'children'), - Input('video-upload', 'contents'), - State('video-upload', 'filename'), - prevent_initial_call=True -) -def update_video_upload_status(contents, filename): - """Zeigt Status des Video-Uploads""" - if contents is not None and filename is not None: - return dmc.Alert( - f"✓ Video '{filename}' erfolgreich hochgeladen", - color="green", - className="mt-2" - ) - return html.Div() - -# Wiederholungsoptionen aktivieren/deaktivieren -@callback( - [ - Output('weekdays-select', 'disabled'), - Output('repeat-until-date', 'disabled'), - Output('skip-holidays-checkbox', 'disabled'), - Output('weekdays-select', 'value'), - Output('repeat-until-date', 'value'), - Output('skip-holidays-checkbox', 'checked') - ], - Input('repeat-checkbox', 'checked'), - prevent_initial_call=True -) -def toggle_repeat_options(is_repeat): - """Aktiviert/deaktiviert Wiederholungsoptionen""" - if is_repeat: - # Aktiviert und setzt Standardwerte - next_month = datetime.now().date() + timedelta(weeks=4) # 4 Wochen später - return ( - False, # weekdays-select enabled - False, # repeat-until-date enabled - False, # skip-holidays-checkbox enabled - None, # weekdays value - next_month, # repeat-until-date value - False # skip-holidays-checkbox checked - ) - else: - # Deaktiviert und löscht Werte - return ( - True, # weekdays-select disabled - True, # repeat-until-date disabled - True, # skip-holidays-checkbox disabled - None, # weekdays value - None, # repeat-until-date value - False # skip-holidays-checkbox checked - ) - -# Dynamische Zeitoptionen für Startzeit -@callback( - [ - Output('time-start', 'data'), - Output('start-time-feedback', 'children') - ], - Input('time-start', 'searchValue'), - prevent_initial_call=True -) -def update_start_time_options(search_value): - """Erweitert Zeitoptionen basierend auf Sucheingabe""" - base_options = time_options.copy() - feedback = None - - if search_value: - validated_time, status = validate_and_format_time(search_value) - - if validated_time: - if not any(opt["value"] == validated_time for opt in base_options): - base_options.insert(0, { - "value": validated_time, - "label": f"{validated_time} (Ihre Eingabe)" - }) - - feedback = dmc.Text(f"✓ {status}: {validated_time}", size="xs", c="green") - else: - feedback = dmc.Text(f"✗ {status}", size="xs", c="red") - - return base_options, feedback - -# Dynamische Zeitoptionen für Endzeit -@callback( - [ - Output('time-end', 'data'), - Output('end-time-feedback', 'children') - ], - Input('time-end', 'searchValue'), - prevent_initial_call=True -) -def update_end_time_options(search_value): - """Erweitert Zeitoptionen basierend auf Sucheingabe""" - base_options = time_options.copy() - feedback = None - - if search_value: - validated_time, status = validate_and_format_time(search_value) - - if validated_time: - if not any(opt["value"] == validated_time for opt in base_options): - base_options.insert(0, { - "value": validated_time, - "label": f"{validated_time} (Ihre Eingabe)" - }) - - feedback = dmc.Text(f"✓ {status}: {validated_time}", size="xs", c="green") - else: - feedback = dmc.Text(f"✗ {status}", size="xs", c="red") - - return base_options, feedback - -# Automatische Endzeit-Berechnung mit Validation -@callback( - Output('time-end', 'value'), - [ - Input('time-start', 'value'), - Input('btn-reset', 'n_clicks') - ], - State('time-end', 'value'), - prevent_initial_call=True -) -def handle_end_time(start_time, reset_clicks, current_end_time): - """Behandelt automatische Endzeit-Berechnung und Reset""" - ctx = dash.callback_context - if not ctx.triggered: - return dash.no_update - - trigger_id = ctx.triggered[0]['prop_id'].split('.')[0] - - if trigger_id == 'btn-reset' and reset_clicks: - return None - - if trigger_id == 'time-start' and start_time: - if current_end_time: - return dash.no_update - - try: - validated_start, _ = validate_and_format_time(start_time) - if validated_start: - start_dt = datetime.strptime(validated_start, "%H:%M") - # 1.5 Stunden später, aber maximal 23:59 - end_dt = start_dt + timedelta(hours=1, minutes=30) - if end_dt.hour >= 24: - end_dt = end_dt.replace(hour=23, minute=59) - return end_dt.strftime("%H:%M") - except: - pass - - return dash.no_update - -# Hilfsfunktion für sichere Werte-Abfrage -def get_safe_value(ctx, prop_id): - """Gibt den Wert einer Property zurück oder None, wenn sie nicht existiert""" - try: - return ctx.states.get(prop_id, {}).get('value') - except: - return None - -# Vorschau-Bereich mit Ferientags-Berücksichtigung und typ-spezifischen Daten -@callback( - Output('preview-area', 'children'), - [ - Input('title-input', 'value'), - Input('start-date-input', 'value'), - Input('time-start', 'value'), - Input('time-end', 'value'), - Input('type-input', 'value'), - Input('description-input', 'value'), - Input('repeat-checkbox', 'checked'), - Input('weekdays-select', 'value'), - Input('repeat-until-date', 'value'), - Input('skip-holidays-checkbox', 'checked') - ], - prevent_initial_call=True -) -def update_preview(title, start_date, start_time, end_time, event_type, description, - is_repeat, weekdays, repeat_until, skip_holidays): - """Zeigt Live-Vorschau der Termine mit typ-spezifischen Daten""" - - validated_start, start_status = validate_and_format_time(start_time) - validated_end, end_status = validate_and_format_time(end_time) - - # Zeitvalidierung - time_valid = True - time_error = "" - - if validated_start and validated_end: - start_dt = datetime.strptime(validated_start, "%H:%M") - end_dt = datetime.strptime(validated_end, "%H:%M") - - if end_dt <= start_dt: - time_valid = False - time_error = "Endzeit muss nach Startzeit liegen" - elif end_dt.hour < start_dt.hour: # Über Mitternacht - time_valid = False - time_error = "Termine dürfen nicht über Mitternacht hinausgehen" - - # Typ-spezifische Details mit sicherer Abfrage - type_details = [] - if event_type == "presentation": - # Hier würden wir normalerweise die Werte abfragen, aber da sie dynamisch sind, - # zeigen wir nur den Typ an - type_details.append(dmc.Text("🎯 Präsentationsdetails werden nach Auswahl angezeigt", size="sm")) - elif event_type == "video": - type_details.append(dmc.Text("📹 Videodetails werden nach Auswahl angezeigt", size="sm")) - elif event_type == "website": - type_details.append(dmc.Text("🌐 Website-Details werden nach Auswahl angezeigt", size="sm")) - elif event_type == "message": - type_details.append(dmc.Text("💬 Nachrichten-Details werden nach Auswahl angezeigt", size="sm")) - - # Wiederholungslogik mit Ferientags-Berücksichtigung - if is_repeat and weekdays and start_date and repeat_until and time_valid: - weekday_names = ["Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"] - selected_days = [weekday_names[int(day)] for day in weekdays] - - # Termine berechnen - termine_count = 0 - skipped_holidays = 0 - - # Sicherstellen, dass start_date ein date-Objekt ist - if isinstance(start_date, str): - try: - current_date = datetime.strptime(start_date, "%Y-%m-%d").date() - except: - current_date = datetime.now().date() - else: - current_date = start_date - - # Sicherstellen, dass repeat_until ein date-Objekt ist - if isinstance(repeat_until, str): - try: - end_date = datetime.strptime(repeat_until, "%Y-%m-%d").date() - except: - end_date = current_date + timedelta(weeks=4) - else: - end_date = repeat_until - - # Kopie für Iteration erstellen - iter_date = current_date - while iter_date <= end_date: - if str(iter_date.weekday()) in weekdays: - if skip_holidays and is_holiday_or_vacation(iter_date): - skipped_holidays += 1 - else: - termine_count += 1 - iter_date += timedelta(days=1) - - holiday_info = [] - if skip_holidays: - holiday_info = [ - dmc.Text(f"🚫 Übersprungene Ferientage: {skipped_holidays}", size="sm", c="orange"), - dmc.Text(f"📅 Tatsächliche Termine: {termine_count}", size="sm", fw=500) - ] - - repeat_info = dmc.Stack([ - dmc.Text(f"📅 Wiederholung: {', '.join(selected_days)}", size="sm"), - dmc.Text(f"📆 Zeitraum: {current_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}", size="sm"), - dmc.Text(f"🔢 Geplante Termine: {termine_count + skipped_holidays if skip_holidays else termine_count}", size="sm"), - *holiday_info - ]) - else: - repeat_info = dmc.Text("📅 Einzeltermin", size="sm") - - # Datum formatieren - date_str = start_date.strftime('%d.%m.%Y') if isinstance(start_date, date) else (start_date or "Nicht gesetzt") - - return dmc.Stack([ - dmc.Title(title or "Unbenannter Termin", order=4), - dmc.Text(f"📅 Datum: {date_str}", size="sm"), - dmc.Text(f"🕐 Zeit: {validated_start or 'Nicht gesetzt'} - {validated_end or 'Nicht gesetzt'}", size="sm"), - dmc.Text(f"📋 Typ: {event_type or 'Nicht gesetzt'}", size="sm"), - - # Typ-spezifische Details - *type_details, - - dmc.Text(f"📝 Beschreibung: {description[:100] + '...' if description and len(description) > 100 else description or 'Keine'}", size="sm"), - - dmc.Divider(className="my-2"), - - repeat_info, - - dmc.Divider(className="my-2"), - - dmc.Stack([ - dmc.Text("Validierung:", fw=500, size="xs"), - dmc.Text(f"Start: {start_status}", size="xs", c="green" if validated_start else "red"), - dmc.Text(f"Ende: {end_status}", size="xs", c="green" if validated_end else "red"), - dmc.Text(f"Zeitbereich: {'✓ Gültig' if time_valid else f'✗ {time_error}'}", - size="xs", c="green" if time_valid else "red") - ], gap="xs") - ]) - -# Reset-Funktion erweitert -@callback( - [ - Output('title-input', 'value'), - Output('start-date-input', 'value'), - Output('time-start', 'value'), - Output('type-input', 'value'), - Output('description-input', 'value'), - Output('repeat-checkbox', 'checked'), - Output('weekdays-select', 'value', allow_duplicate=True), - Output('repeat-until-date', 'value', allow_duplicate=True), - Output('skip-holidays-checkbox', 'checked', allow_duplicate=True) - ], - Input('btn-reset', 'n_clicks'), - prevent_initial_call=True -) -def reset_form(n_clicks): - """Setzt das komplette Formular zurück""" - if n_clicks: - return "", datetime.now().date(), "09:00", None, "", False, None, None, False - return dash.no_update - -# Speichern-Funktion (vereinfacht für Demo) -@callback( - Output('save-feedback', 'children'), - Input('btn-save', 'n_clicks'), - prevent_initial_call=True -) -def save_appointments_demo(n_clicks): - """Demo-Speicherfunktion""" - if not n_clicks: - return dash.no_update - - return dmc.Alert( - "Demo: Termine würden hier gespeichert werden", - color="blue", - title="Speichern (Demo-Modus)" - ) - -if __name__ == "__main__": - app.run(debug=True, host="0.0.0.0", port=8051) \ No newline at end of file diff --git a/dashboard-dash-backup/pages/appointments.py b/dashboard-dash-backup/pages/appointments.py deleted file mode 100644 index 61555fa..0000000 --- a/dashboard-dash-backup/pages/appointments.py +++ /dev/null @@ -1,63 +0,0 @@ -# dashboard/pages/appointments.py -from dash import html, dcc -import dash -from dash_using_fullcalendar import DashUsingFullcalendar -import dash_bootstrap_components as dbc -from dashboard.components.appointment_modal import get_appointment_modal - -dash.register_page(__name__, path="/appointments", name="Termine") - -layout = dbc.Container([ - dbc.Row([ - dbc.Col(html.H2("Dash FullCalendar")) - ]), - # Button zum Öffnen der Modalbox - dbc.Row([ - dbc.Col( - dbc.Button( - "Neuen Termin anlegen", - id="open-appointment-modal-btn", - color="primary", - className="mb-3" - ) - ) - ]), - dbc.Row([ - dbc.Col( - DashUsingFullcalendar( - id='calendar', - events=[], - initialView="timeGridWeek", - headerToolbar={ - "left": "prev,next today", - "center": "title", - # "right": "dayGridMonth,timeGridWeek,timeGridDay" - }, - height=600, - locale="de", - slotDuration="00:30:00", - slotMinTime="00:00:00", - slotMaxTime="24:00:00", - scrollTime="07:00:00", - weekends=True, - allDaySlot=False, - firstDay=1, - # themeSystem kann auf "bootstrap5" gesetzt werden, wenn das Plugin eingebunden ist - # themeSystem="bootstrap5" - ) - ) - ]), - dbc.Row([ - dbc.Col(html.Div(id='output')) - ]), - dbc.Row([ - dbc.Col(html.Div(id='event-output')) - ]), - dbc.Row([ - dbc.Col(html.Div(id='select-output')) - ]), - dbc.Row([ - dbc.Col(html.Div(id='modal-output', children=get_appointment_modal())) - ]) -], fluid=True) - diff --git a/dashboard-dash-backup/pages/clients.py b/dashboard-dash-backup/pages/clients.py deleted file mode 100644 index b8dcc65..0000000 --- a/dashboard-dash-backup/pages/clients.py +++ /dev/null @@ -1,12 +0,0 @@ -# dashboard/pages/clients.py -from dash import html, dcc -import dash - -dash.register_page(__name__, path="/clients", name="Bildschirme") - -layout = html.Div( - className="clients-page", - children=[ - html.H3("Bildschirme"), - ] -) \ No newline at end of file diff --git a/dashboard-dash-backup/pages/login.py b/dashboard-dash-backup/pages/login.py deleted file mode 100644 index 8e97912..0000000 --- a/dashboard-dash-backup/pages/login.py +++ /dev/null @@ -1,16 +0,0 @@ -# dashboard/pages/login.py -from dash import html, dcc -import dash - -dash.register_page(__name__, path="/login", name="Login") - -layout = html.Div( - className="login-page", - children=[ - html.H2("Bitte einloggen"), - dcc.Input(id="input-user", type="text", placeholder="Benutzername"), - dcc.Input(id="input-pass", type="password", placeholder="Passwort"), - html.Button("Einloggen", id="btn-login"), - html.Div(id="login-feedback", className="text-danger") - ] -) diff --git a/dashboard-dash-backup/pages/overview.py b/dashboard-dash-backup/pages/overview.py deleted file mode 100644 index 7e2933d..0000000 --- a/dashboard-dash-backup/pages/overview.py +++ /dev/null @@ -1,13 +0,0 @@ -# dashboard/pages/overview.py -from dash import html, dcc -import dash - -dash.register_page(__name__, path="/overview", name="Übersicht") - -layout = html.Div( - className="overview-page", - children=[ - dcc.Interval(id="interval-update", interval=10_000, n_intervals=0), - html.Div(id="clients-cards-container") - ] -) diff --git a/dashboard-dash-backup/pages/settings.py b/dashboard-dash-backup/pages/settings.py deleted file mode 100644 index 045823c..0000000 --- a/dashboard-dash-backup/pages/settings.py +++ /dev/null @@ -1,13 +0,0 @@ -# dashboard/pages/settings.py -from dash import html -import dash - -dash.register_page(__name__, path="/settings", name="Einstellungen") - -layout = html.Div( - className="settings-page", - children=[ - html.H3("Allgemeine Einstellungen"), - # Formularfelder / Tabs für globale Optionen - ] -) diff --git a/dashboard-dash-backup/pages/test.py b/dashboard-dash-backup/pages/test.py deleted file mode 100644 index e943674..0000000 --- a/dashboard-dash-backup/pages/test.py +++ /dev/null @@ -1,5 +0,0 @@ -import dash -from dash import html - -dash.register_page(__name__, path="/test", name="Testseite") -layout = html.Div("Testseite funktioniert!") \ No newline at end of file diff --git a/dashboard-dash-backup/pages/users.py b/dashboard-dash-backup/pages/users.py deleted file mode 100644 index a1718b2..0000000 --- a/dashboard-dash-backup/pages/users.py +++ /dev/null @@ -1,15 +0,0 @@ -# dashboard/pages/users.py -from dash import html, dash_table, dcc -import dash - -dash.register_page(__name__, path="/users", name="Benutzer") - -layout = html.Div( - className="users-page", - children=[ - html.H3("Benutzerverwaltung"), - html.Button("Neuen Benutzer anlegen", id="btn-new-user"), - html.Div(id="users-table-container"), - html.Div(id="users-feedback") - ] -) diff --git a/dashboard-dash-backup/requirements-dev.txt b/dashboard-dash-backup/requirements-dev.txt deleted file mode 100644 index 8ccada8..0000000 --- a/dashboard-dash-backup/requirements-dev.txt +++ /dev/null @@ -1 +0,0 @@ -debugpy \ No newline at end of file diff --git a/dashboard-dash-backup/requirements.txt b/dashboard-dash-backup/requirements.txt deleted file mode 100644 index 80d4642..0000000 --- a/dashboard-dash-backup/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -bcrypt>=4.3.0 -dash>=3.0.4 -dash-bootstrap-components>=2.0.3 -dash_iconify>=0.1.2 -dash_mantine_components>=1.2.0 -dash-quill>=0.0.4 -full-calendar-component>=0.0.4 -pandas>=2.2.3 -paho-mqtt>=2.1.0 -python-dotenv>=1.1.0 -PyMySQL>=1.1.1 -SQLAlchemy>=2.0.41 -./dash_using_fullcalendar-0.1.0.tar.gz diff --git a/dashboard-dash-backup/sidebar_test.py b/dashboard-dash-backup/sidebar_test.py deleted file mode 100644 index 360e777..0000000 --- a/dashboard-dash-backup/sidebar_test.py +++ /dev/null @@ -1,193 +0,0 @@ -""" -This app creates a collapsible, responsive sidebar layout with -dash-bootstrap-components and some custom css with media queries. - -When the screen is small, the sidebar moved to the top of the page, and the -links get hidden in a collapse element. We use a callback to toggle the -collapse when on a small screen, and the custom CSS to hide the toggle, and -force the collapse to stay open when the screen is large. - -dcc.Location is used to track the current location, a callback uses the current -location to render the appropriate page content. The active prop of each -NavLink is set automatically according to the current pathname. To use this -feature you must install dash-bootstrap-components >= 0.11.0. - -For more details on building multi-page Dash applications, check out the Dash -documentation: https://dash.plotly.com/urls -""" -import sys -sys.path.append('/workspace') -import dash -import dash_bootstrap_components as dbc -from dash import Input, Output, State, dcc, html, page_container -from dash_iconify import DashIconify -# import callbacks.ui_callbacks -import dashboard.callbacks.appointments_callbacks -import dashboard.callbacks.appointment_modal_callbacks -import dash_mantine_components as dmc - -app = dash.Dash( - external_stylesheets=[dbc.themes.BOOTSTRAP], - # these meta_tags ensure content is scaled correctly on different devices - # see: https://www.w3schools.com/css/css_rwd_viewport.asp for more - meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}], - use_pages=True, - suppress_callback_exceptions=True, -) - -nav_items = [ - {"label": "Übersicht", "href": "/overview", "icon": "mdi:view-dashboard"}, - {"label": "Termine", "href": "/appointments","icon": "mdi:calendar"}, - {"label": "Bildschirme", "href": "/clients", "icon": "mdi:monitor"}, - {"label": "Einstellungen","href": "/settings", "icon": "mdi:cog"}, - {"label": "Benutzer", "href": "/users", "icon": "mdi:account"}, - ] - -nav_links = [] - -for item in nav_items: - # Create a NavLink for each item - link_id = {"type": "nav-item", "index": item["label"]} - nav_link = dbc.NavLink( - [ - DashIconify(icon=item["icon"], width=24), - html.Span(item["label"], className="ms-2 sidebar-label"), - ], - href=item["href"], - active="exact", - className="sidebar-item", - id=link_id, - ) - nav_links.append( - html.Div( - children=nav_link, - className="nav-item-container" - ) - ) -# we use the Row and Col components to construct the sidebar header -# it consists of a title, and a toggle, the latter is hidden on large screens -sidebar_header = dbc.Row( - [ - dbc.Col(html.H2("Sidebar", className="display-4")), - dbc.Col( - [ - html.Button( - # use the Bootstrap navbar-toggler classes to style - html.Span(className="navbar-toggler-icon"), - className="navbar-toggler", - # the navbar-toggler classes don't set color - style={ - "color": "rgba(0,0,0,.5)", - "border-color": "rgba(0,0,0,.1)", - }, - id="navbar-toggle", - ), - html.Button( - # use the Bootstrap navbar-toggler classes to style - html.Span(className="navbar-toggler-icon"), - className="navbar-toggler", - # the navbar-toggler classes don't set color - style={ - "color": "rgba(0,0,0,.5)", - "border-color": "rgba(0,0,0,.1)", - }, - id="sidebar-toggle", - ), - ], - # the column containing the toggle will be only as wide as the - # toggle, resulting in the toggle being right aligned - width="auto", - # vertically align the toggle in the center - align="center", - ), - ] -) - -sidebar = html.Div( - [ - sidebar_header, - # we wrap the horizontal rule and short blurb in a div that can be - # hidden on a small screen - html.Div( - [ - html.Hr(), - html.P( - "A responsive sidebar layout with collapsible navigation " "links.", - className="lead", - ), - ], - id="blurb", - ), - # use the Collapse component to animate hiding / revealing links - dbc.Collapse( - dbc.Nav( - nav_links, # <-- Korrigiert: keine zusätzliche Liste - vertical=True, - pills=True, - ), - id="collapse", - ), - ], - id="sidebar", -) - -content = dmc.MantineProvider([ - html.Div( - html.Div(page_container, className="page-content"),style={"flex": "1", "padding": "20px"} - ) -]) - - -app.layout = html.Div([dcc.Location(id="url"), sidebar, content]) - - -# @app.callback(Output("page-content", "children"), [Input("url", "pathname")]) -# def render_page_content(pathname): -# if pathname == "/": -# return html.P("This is the content of the home page!") -# elif pathname == "/page-1": -# return html.P("This is the content of page 1. Yay!") -# elif pathname == "/page-2": -# return html.P("Oh cool, this is page 2!") -# # If the user tries to reach a different page, return a 404 message -# return html.Div( -# [ -# html.H1("404: Not found", className="text-danger"), -# html.Hr(), -# html.P(f"The pathname {pathname} was not recognised..."), -# ], -# className="p-3 bg-light rounded-3", -# ) - - -@app.callback( - [Output("sidebar", "className"), Output("collapse", "is_open")], - [ - Input("sidebar-toggle", "n_clicks"), - Input("navbar-toggle", "n_clicks"), - ], - [ - State("sidebar", "className"), - State("collapse", "is_open"), - ], -) -def toggle_sidebar_and_collapse(sidebar_n, navbar_n, classname, is_open): - ctx = dash.callback_context - if not ctx.triggered: - return classname, is_open - trigger_id = ctx.triggered[0]["prop_id"].split(".")[0] - if trigger_id == "sidebar-toggle": - # Toggle sidebar collapse - if sidebar_n and classname == "": - return "collapsed", is_open - return "", is_open - elif trigger_id == "navbar-toggle": - # Toggle collapse - if navbar_n: - return classname, not is_open - return classname, is_open - return classname, is_open - - -if __name__ == "__main__": - app.run(port=8888, debug=True) diff --git a/dashboard-dash-backup/utils/__init__.py b/dashboard-dash-backup/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/dashboard-dash-backup/utils/auth.py b/dashboard-dash-backup/utils/auth.py deleted file mode 100644 index 0583a06..0000000 --- a/dashboard-dash-backup/utils/auth.py +++ /dev/null @@ -1,12 +0,0 @@ -# dashboard/utils/auth.py -import bcrypt - -def hash_password(plain_text: str) -> str: - return bcrypt.hashpw(plain_text.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") - -def check_password(plain_text: str, hashed: str) -> bool: - return bcrypt.checkpw(plain_text.encode("utf-8"), hashed.encode("utf-8")) - -def get_user_role(username: str) -> str: - # Beispiel: aus der Datenbank auslesen (oder Hardcode während Dev-Phase) - pass diff --git a/dashboard-dash-backup/utils/db.py b/dashboard-dash-backup/utils/db.py deleted file mode 100644 index 834422b..0000000 --- a/dashboard-dash-backup/utils/db.py +++ /dev/null @@ -1,46 +0,0 @@ -import os -from dotenv import load_dotenv -from sqlalchemy import create_engine, text -from sqlalchemy.orm import sessionmaker - -# .env laden -load_dotenv() - -# Datenbank-Zugangsdaten aus .env -DB_USER = os.getenv("DB_USER") -DB_PASSWORD = os.getenv("DB_PASSWORD") -DB_HOST = os.getenv("DB_HOST", "localhost") -DB_PORT = os.getenv("DB_PORT", "3306") -DB_NAME = os.getenv("DB_NAME") - -# Pooling Parameter aus .env (optional mit Default-Werten) -POOL_SIZE = int(os.getenv("POOL_SIZE", 10)) -MAX_OVERFLOW = int(os.getenv("MAX_OVERFLOW", 20)) -POOL_TIMEOUT = int(os.getenv("POOL_TIMEOUT", 30)) -POOL_RECYCLE = int(os.getenv("POOL_RECYCLE", 1800)) - -# Connection-String zusammenbauen -DATABASE_URL = ( - f"mysql+pymysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" -) - -# Engine mit Pooling konfigurieren -engine = create_engine( - DATABASE_URL, - pool_size=POOL_SIZE, - max_overflow=MAX_OVERFLOW, - pool_timeout=POOL_TIMEOUT, - pool_recycle=POOL_RECYCLE, - echo=True, # für Debug, später False -) - -# Session Factory -SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False) - -def get_session(): - return SessionLocal() - -def execute_query(query): - with engine.connect() as connection: - result = connection.execute(text(query)) - return [dict(row) for row in result] diff --git a/dashboard-dash-backup/utils/mqtt_client.py b/dashboard-dash-backup/utils/mqtt_client.py deleted file mode 100644 index 1ab151a..0000000 --- a/dashboard-dash-backup/utils/mqtt_client.py +++ /dev/null @@ -1,124 +0,0 @@ -# dashboard/utils/mqtt_client.py - -import os -import threading -import time -from dotenv import load_dotenv -import paho.mqtt.client as mqtt -import random - -# 1. Laden der Umgebungsvariablen aus .env -load_dotenv(dotenv_path=os.path.join(os.path.dirname(__file__), "..", ".env")) - -# 2. Lese MQTT‐Einstellungen -MQTT_BROKER_HOST = os.getenv("MQTT_BROKER_HOST", "localhost") -MQTT_BROKER_PORT = int(os.getenv("MQTT_BROKER_PORT", "1883")) -MQTT_USERNAME = os.getenv("MQTT_USERNAME", None) -MQTT_PASSWORD = os.getenv("MQTT_PASSWORD", None) -MQTT_KEEPALIVE = int(os.getenv("MQTT_KEEPALIVE", "60")) -base_id = os.getenv("MQTT_CLIENT_ID", "dash") -unique_part = f"{os.getpid()}_{random.randint(1000,9999)}" -MQTT_CLIENT_ID = f"{base_id}-{unique_part}" - -# 3. Erstelle eine globale Client‐Instanz -client = mqtt.Client(client_id=MQTT_CLIENT_ID) - -# Falls Nutzer/Passwort gesetzt sind, authentifizieren -if MQTT_USERNAME and MQTT_PASSWORD: - client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) - - -# 4. Callback‐Stubs (kannst du bei Bedarf anpassen) -def _on_connect(client, userdata, flags, rc): - if rc == 0: - print(f"[mqtt_client.py] Erfolgreich mit MQTT‐Broker verbunden (Code {rc})") - else: - print(f"[mqtt_client.py] Verbindungsfehler, rc={rc}") - - -def _on_disconnect(client, userdata, rc): - print(f"[mqtt_client.py] Verbindung getrennt (rc={rc}). Versuche, neu zu verbinden …") - - -def _on_message(client, userdata, msg): - """ - Diese Callback‐Funktion wird aufgerufen, sobald eine Nachricht auf einem - Topic ankommt, auf das wir subscribed haben. Du kannst hier eine Queue - füllen oder direkt eine Datenbank‐Funktion aufrufen. - """ - topic = msg.topic - payload = msg.payload.decode("utf-8", errors="ignore") - print(f"[mqtt_client.py] Nachricht eingegangen – Topic: {topic}, Payload: {payload}") - # Beispiel: Wenn du Live‐Statusdaten in die Datenbank schreibst, - # könntest du hier utils/db.execute_non_query(...) aufrufen. - - -# 5. Setze die Callbacks -client.on_connect = _on_connect -client.on_disconnect = _on_disconnect -client.on_message = _on_message - - -def start_loop(): - """ - Startet die Endlos‐Schleife, in der der Client auf eingehende - MQTT‐Nachrichten hört und automatisch reconnectet. - Muss idealerweise in einem eigenen Thread laufen, damit Dash‐Callbacks - nicht blockieren. - """ - try: - client.connect(MQTT_BROKER_HOST, MQTT_BROKER_PORT, keepalive=MQTT_KEEPALIVE) - client.loop_start() - except Exception as e: - print(f"[mqtt_client.py] Konnte keine Verbindung zum MQTT‐Broker herstellen: {e}") - - -def stop_loop(): - """ - Stoppt die MQTT‐Loop und trennt die Verbindung. - """ - try: - client.loop_stop() - client.disconnect() - except Exception as e: - print(f"[mqtt_client.py] Fehler beim Stoppen der MQTT‐Schleife: {e}") - - -def publish(topic: str, payload: str, qos: int = 0, retain: bool = False) -> bool: - """ - Verschickt eine MQTT‐Nachricht: - - topic: z. B. "clients/{client_id}/control" - - payload: z. B. '{"command":"restart"}' - - qos: 0, 1 oder 2 - - retain: True/False - Rückgabe: True, falls Veröffentlichung bestätigt wurde; sonst False. - """ - try: - result = client.publish(topic, payload, qos=qos, retain=retain) - status = result.rc # 0=Erfolg, sonst Fehler - if status == mqtt.MQTT_ERR_SUCCESS: - return True - else: - print(f"[mqtt_client.py] Publish-Fehler für Topic {topic}, rc={status}") - return False - except Exception as e: - print(f"[mqtt_client.py] Exception beim Publish: {e}") - return False - - -def subscribe(topic: str, qos: int = 0) -> bool: - """ - Abonniert ein MQTT‐Topic, sodass _on_message gerufen wird, sobald Nachrichten - ankommen. - Rückgabe: True bei Erfolg, ansonsten False. - """ - try: - result, mid = client.subscribe(topic, qos=qos) - if result == mqtt.MQTT_ERR_SUCCESS: - return True - else: - print(f"[mqtt_client.py] Subscribe‐Fehler für Topic {topic}, rc={result}") - return False - except Exception as e: - print(f"[mqtt_client.py] Exception beim Subscribe: {e}") - return False