892 lines
35 KiB
Python
892 lines
35 KiB
Python
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) |