initial feature/react-migration commit
This commit is contained in:
892
dashboard-dash-backup/manitine_test.py
Normal file
892
dashboard-dash-backup/manitine_test.py
Normal file
@@ -0,0 +1,892 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user