From f1d9d05519c2967196159c238414218a04d5c6f5 Mon Sep 17 00:00:00 2001 From: olaf Date: Mon, 16 Jun 2025 20:31:49 +0000 Subject: [PATCH] modalbox for appoiments --- .gitignore | 7 +- dashboard/app.py | 18 +- dashboard/assets/custom.css | 37 +- .../callbacks/appointment_modal_callbacks.py | 371 ++++++++++++++++++ dashboard/callbacks/appointments_callbacks.py | 35 +- dashboard/components/appointment_modal.py | 262 +++++++++++++ dashboard/pages/appointments.py | 15 + 7 files changed, 727 insertions(+), 18 deletions(-) create mode 100644 dashboard/callbacks/appointment_modal_callbacks.py create mode 100644 dashboard/components/appointment_modal.py diff --git a/.gitignore b/.gitignore index 69ffbd5..ab2a88a 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,9 @@ desktop.ini received_screenshots/ mosquitto/ alte/ -screenshots/ \ No newline at end of file +screenshots/ +dashboard/manitine_test.py +dashboard/pages/test.py +.gitignore +dashboard/sidebar_test.py +dashboard/assets/responsive-sidebar.css diff --git a/dashboard/app.py b/dashboard/app.py index 6d17f61..5f4212f 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -5,11 +5,13 @@ 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 # from components.sidebar import Sidebar import callbacks.ui_callbacks # wichtig! import dashboard.callbacks.overview_callbacks # <-- Das registriert die Callbacks import dashboard.callbacks.appointments_callbacks +import dashboard.callbacks.appointment_modal_callbacks from config import SECRET_KEY, ENV server = Flask(__name__) @@ -19,21 +21,19 @@ app = Dash( __name__, server=server, use_pages=True, - # external_stylesheets=[dbc.themes.BOOTSTRAP], + external_stylesheets=[dbc.themes.BOOTSTRAP], suppress_callback_exceptions=True, serve_locally=True ) -app.layout = html.Div([ +app.layout = dmc.MantineProvider([ Header(), - dcc.Store(id="sidebar-state", data={"collapsed": False}), - # dcc.Store(id="calendar-click-store"), - # dcc.Store(id="calendar-event-store"), - html.Div(id="sidebar"), # Sidebar wird dynamisch gerendert - html.Div(page_container, className="page-content"), - html.Div("TEST", id="test-div"), + html.Div([ + html.Div(id="sidebar", className="sidebar"), + html.Div(page_container, className="page-content"), + dcc.Store(id="sidebar-state", data={"collapsed": False}), + ], style={"display": "flex"}), ]) - if __name__ == "__main__": app.run(host="0.0.0.0", port=8050, debug=(ENV=="development")) diff --git a/dashboard/assets/custom.css b/dashboard/assets/custom.css index 62754cf..8032785 100644 --- a/dashboard/assets/custom.css +++ b/dashboard/assets/custom.css @@ -10,8 +10,9 @@ body { /* page-content (rechts neben der Sidebar) */ .page-content { - margin-left: 220px; + 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 */ } @@ -69,10 +70,10 @@ body { background-color: #e4d5c1; color: black; height: calc(100vh - 60px); /* Höhe minus Header */ - position: fixed; top: 60px; /* Den gleichen Wert wie Header-Höhe verwenden */ left: 0; z-index: 1000; + position: relative; /* oder fixed, je nach Layout */ overflow-x: hidden; overflow-y: auto; } @@ -185,4 +186,34 @@ body { .sidebar.collapsed ~ .page-content { margin-left: 0; } -} \ No newline at end of file +} + +/* :root { + --mb-z-index: 2000 !important; + --mantine-z-index-popover: 2100 !important; + --mantine-z-index-overlay: 2100 !important; + --mantine-z-index-dropdown: 2100 !important; + --mantine-z-index-max: 9999 !important; +} + +.mantine-Modal-root, +.mantine-Modal-modal, +.mantine-Modal-overlay { + z-index: 3000 !important; +} + +.mantine-Modal-overlay { + z-index: 2000 !important; +} + +.mantine-Modal-modal { + z-index: 2100 !important; +} + +.mantine-Popover-dropdown, +.mantine-DatePicker-dropdown, +.mantine-Select-dropdown { + z-index: 2200 !important; +} */ + + diff --git a/dashboard/callbacks/appointment_modal_callbacks.py b/dashboard/callbacks/appointment_modal_callbacks.py new file mode 100644 index 0000000..2c4f09e --- /dev/null +++ b/dashboard/callbacks/appointment_modal_callbacks.py @@ -0,0 +1,371 @@ +from dash import Input, Output, State, callback, html, dcc, no_update +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): + ctx = callback.ctx + 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'), + 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/callbacks/appointments_callbacks.py b/dashboard/callbacks/appointments_callbacks.py index 0c0f524..f82510e 100644 --- a/dashboard/callbacks/appointments_callbacks.py +++ b/dashboard/callbacks/appointments_callbacks.py @@ -1,16 +1,17 @@ # dashboard/callbacks/appointments_callbacks.py +import requests +import json +from flask import session +from dash import Input, Output, State, callback, ctx, dash import os import sys sys.path.append('/workspace') -from dash import Input, Output, State, callback, ctx, dash -from flask import session -import json -import requests print("appointments_callbacks.py geladen") API_BASE_URL = os.getenv("API_BASE_URL", "http://192.168.43.100") + @callback( dash.Output('output', 'children'), dash.Input('calendar', 'lastDateClick') @@ -20,6 +21,7 @@ def display_date(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') @@ -29,6 +31,7 @@ def display_event(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') @@ -38,6 +41,7 @@ def display_select(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'), @@ -52,7 +56,8 @@ def load_events(view_dates): start = view_dates["start"] end = view_dates["end"] try: - resp = requests.get(f"{API_BASE_URL}/api/events", params={"start": start, "end": end}) + resp = requests.get(f"{API_BASE_URL}/api/events", + params={"start": start, "end": end}) resp.raise_for_status() events = resp.json() return events @@ -60,3 +65,23 @@ def load_events(view_dates): print("Fehler beim Laden der Events:", e) return [] +# --- Modalbox öffnen --- + + +@callback( + Output("appointment-modal", "opened"), + [ + Input("open-appointment-modal-btn", "n_clicks"), + Input("close-appointment-modal-btn", "n_clicks") + ], + State("appointment-modal", "opened"), + prevent_initial_call=True +) +def toggle_appointment_modal(open_click, close_click, is_open): + from dash import ctx + trigger = ctx.triggered_id + if trigger == "open-appointment-modal-btn" and open_click: + return True + if trigger == "close-appointment-modal-btn" and close_click: + return False + return is_open diff --git a/dashboard/components/appointment_modal.py b/dashboard/components/appointment_modal.py new file mode 100644 index 0000000..fedcb84 --- /dev/null +++ b/dashboard/components/appointment_modal.py @@ -0,0 +1,262 @@ +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" + # fullScreen=True, + # styles={ + # "modal": {"zIndex": 2001, "position": "relative"}, + # "overlay": {"zIndex": 2000} + # }, + 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": "other", "label": "Sonstiges"} + ], + id="type-input", + required=True, + style={"flex": 1} + ), + "Wählen Sie den Typ des Termins für bessere Kategorisierung" + ), + 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/pages/appointments.py b/dashboard/pages/appointments.py index 1a727f6..61555fa 100644 --- a/dashboard/pages/appointments.py +++ b/dashboard/pages/appointments.py @@ -3,6 +3,7 @@ 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") @@ -10,6 +11,17 @@ 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( @@ -43,6 +55,9 @@ layout = dbc.Container([ ]), dbc.Row([ dbc.Col(html.Div(id='select-output')) + ]), + dbc.Row([ + dbc.Col(html.Div(id='modal-output', children=get_appointment_modal())) ]) ], fluid=True)