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)