370 lines
13 KiB
Python
370 lines
13 KiB
Python
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)"
|
|
) |