modalbox for appoiments
This commit is contained in:
371
dashboard/callbacks/appointment_modal_callbacks.py
Normal file
371
dashboard/callbacks/appointment_modal_callbacks.py
Normal file
@@ -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)"
|
||||
)
|
||||
Reference in New Issue
Block a user