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)" )