initial feature/react-migration commit
This commit is contained in:
6
.stylelintrc.json
Normal file
6
.stylelintrc.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"stylelint-config-standard",
|
||||||
|
"stylelint-config-tailwindcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
38
dashboard-dash-backup/Dockerfile
Normal file
38
dashboard-dash-backup/Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# dashboard/Dockerfile
|
||||||
|
# Produktions-Dockerfile für die Dash-Applikation
|
||||||
|
|
||||||
|
# --- Basis-Image ---
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
# --- Arbeitsverzeichnis im Container ---
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# --- Systemabhängigkeiten installieren (falls benötigt) ---
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
build-essential git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# --- Python-Abhängigkeiten kopieren und installieren ---
|
||||||
|
COPY dash_using_fullcalendar-0.1.0.tar.gz ./
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# --- Applikationscode kopieren ---
|
||||||
|
COPY dashboard/ /app
|
||||||
|
|
||||||
|
# --- Non-Root-User anlegen und Rechte setzen ---
|
||||||
|
ARG USER_ID=1000
|
||||||
|
ARG GROUP_ID=1000
|
||||||
|
RUN groupadd -g ${GROUP_ID} infoscreen_taa \
|
||||||
|
&& useradd -u ${USER_ID} -g ${GROUP_ID} \
|
||||||
|
--shell /bin/bash --create-home infoscreen_taa \
|
||||||
|
&& chown -R infoscreen_taa:infoscreen_taa /app
|
||||||
|
USER infoscreen_taa
|
||||||
|
|
||||||
|
# --- Port für Dash exposed ---
|
||||||
|
EXPOSE 8050
|
||||||
|
|
||||||
|
# --- Startbefehl: Gunicorn mit Dash-Server ---
|
||||||
|
# "app.py" enthält: app = dash.Dash(...); server = app.server
|
||||||
|
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:8050", "app:server"]
|
||||||
51
dashboard-dash-backup/Dockerfile.dev
Normal file
51
dashboard-dash-backup/Dockerfile.dev
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# dashboard/Dockerfile.dev
|
||||||
|
# Entwicklungs-Dockerfile für das Dash-Dashboard
|
||||||
|
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
# Build args für UID/GID
|
||||||
|
ARG USER_ID=1000
|
||||||
|
ARG GROUP_ID=1000
|
||||||
|
|
||||||
|
# Systemabhängigkeiten (falls nötig)
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends locales curl \
|
||||||
|
&& sed -i 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen \
|
||||||
|
&& locale-gen \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Locale setzen
|
||||||
|
ENV LANG=de_DE.UTF-8 \
|
||||||
|
LANGUAGE=de_DE:de \
|
||||||
|
LC_ALL=de_DE.UTF-8
|
||||||
|
|
||||||
|
# Non-root User anlegen
|
||||||
|
RUN groupadd -g ${GROUP_ID} infoscreen_taa \
|
||||||
|
&& useradd -u ${USER_ID} -g ${GROUP_ID} --shell /bin/bash --create-home infoscreen_taa
|
||||||
|
|
||||||
|
# Arbeitsverzeichnis
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Kopiere nur Requirements für schnellen Rebuild
|
||||||
|
COPY dash_using_fullcalendar-0.1.0.tar.gz ./
|
||||||
|
COPY requirements.txt ./
|
||||||
|
COPY requirements-dev.txt ./
|
||||||
|
|
||||||
|
# Installiere Abhängigkeiten
|
||||||
|
RUN pip install --upgrade pip \
|
||||||
|
&& pip install --no-cache-dir -r requirements.txt \
|
||||||
|
&& pip install --no-cache-dir -r requirements-dev.txt
|
||||||
|
|
||||||
|
# Setze Entwicklungs-Modus
|
||||||
|
ENV DASH_DEBUG_MODE=True
|
||||||
|
ENV API_URL=http://server:8000/api
|
||||||
|
|
||||||
|
# Ports für Dash und optional Live-Reload
|
||||||
|
EXPOSE 8050
|
||||||
|
EXPOSE 5678
|
||||||
|
|
||||||
|
# Wechsle zum non-root User
|
||||||
|
USER infoscreen_taa
|
||||||
|
|
||||||
|
# Dev-Start: Dash mit Hot-Reload
|
||||||
|
CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5637", "app.py"]
|
||||||
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 225 KiB |
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)
|
||||||
5
dashboard-dash-backup/pages/test.py
Normal file
5
dashboard-dash-backup/pages/test.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import dash
|
||||||
|
from dash import html
|
||||||
|
|
||||||
|
dash.register_page(__name__, path="/test", name="Testseite")
|
||||||
|
layout = html.Div("Testseite funktioniert!")
|
||||||
193
dashboard-dash-backup/sidebar_test.py
Normal file
193
dashboard-dash-backup/sidebar_test.py
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
"""
|
||||||
|
This app creates a collapsible, responsive sidebar layout with
|
||||||
|
dash-bootstrap-components and some custom css with media queries.
|
||||||
|
|
||||||
|
When the screen is small, the sidebar moved to the top of the page, and the
|
||||||
|
links get hidden in a collapse element. We use a callback to toggle the
|
||||||
|
collapse when on a small screen, and the custom CSS to hide the toggle, and
|
||||||
|
force the collapse to stay open when the screen is large.
|
||||||
|
|
||||||
|
dcc.Location is used to track the current location, a callback uses the current
|
||||||
|
location to render the appropriate page content. The active prop of each
|
||||||
|
NavLink is set automatically according to the current pathname. To use this
|
||||||
|
feature you must install dash-bootstrap-components >= 0.11.0.
|
||||||
|
|
||||||
|
For more details on building multi-page Dash applications, check out the Dash
|
||||||
|
documentation: https://dash.plotly.com/urls
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
sys.path.append('/workspace')
|
||||||
|
import dash
|
||||||
|
import dash_bootstrap_components as dbc
|
||||||
|
from dash import Input, Output, State, dcc, html, page_container
|
||||||
|
from dash_iconify import DashIconify
|
||||||
|
# import callbacks.ui_callbacks
|
||||||
|
import dashboard.callbacks.appointments_callbacks
|
||||||
|
import dashboard.callbacks.appointment_modal_callbacks
|
||||||
|
import dash_mantine_components as dmc
|
||||||
|
|
||||||
|
app = dash.Dash(
|
||||||
|
external_stylesheets=[dbc.themes.BOOTSTRAP],
|
||||||
|
# these meta_tags ensure content is scaled correctly on different devices
|
||||||
|
# see: https://www.w3schools.com/css/css_rwd_viewport.asp for more
|
||||||
|
meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}],
|
||||||
|
use_pages=True,
|
||||||
|
suppress_callback_exceptions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
nav_items = [
|
||||||
|
{"label": "Übersicht", "href": "/overview", "icon": "mdi:view-dashboard"},
|
||||||
|
{"label": "Termine", "href": "/appointments","icon": "mdi:calendar"},
|
||||||
|
{"label": "Bildschirme", "href": "/clients", "icon": "mdi:monitor"},
|
||||||
|
{"label": "Einstellungen","href": "/settings", "icon": "mdi:cog"},
|
||||||
|
{"label": "Benutzer", "href": "/users", "icon": "mdi:account"},
|
||||||
|
]
|
||||||
|
|
||||||
|
nav_links = []
|
||||||
|
|
||||||
|
for item in nav_items:
|
||||||
|
# Create a NavLink for each item
|
||||||
|
link_id = {"type": "nav-item", "index": item["label"]}
|
||||||
|
nav_link = dbc.NavLink(
|
||||||
|
[
|
||||||
|
DashIconify(icon=item["icon"], width=24),
|
||||||
|
html.Span(item["label"], className="ms-2 sidebar-label"),
|
||||||
|
],
|
||||||
|
href=item["href"],
|
||||||
|
active="exact",
|
||||||
|
className="sidebar-item",
|
||||||
|
id=link_id,
|
||||||
|
)
|
||||||
|
nav_links.append(
|
||||||
|
html.Div(
|
||||||
|
children=nav_link,
|
||||||
|
className="nav-item-container"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# we use the Row and Col components to construct the sidebar header
|
||||||
|
# it consists of a title, and a toggle, the latter is hidden on large screens
|
||||||
|
sidebar_header = dbc.Row(
|
||||||
|
[
|
||||||
|
dbc.Col(html.H2("Sidebar", className="display-4")),
|
||||||
|
dbc.Col(
|
||||||
|
[
|
||||||
|
html.Button(
|
||||||
|
# use the Bootstrap navbar-toggler classes to style
|
||||||
|
html.Span(className="navbar-toggler-icon"),
|
||||||
|
className="navbar-toggler",
|
||||||
|
# the navbar-toggler classes don't set color
|
||||||
|
style={
|
||||||
|
"color": "rgba(0,0,0,.5)",
|
||||||
|
"border-color": "rgba(0,0,0,.1)",
|
||||||
|
},
|
||||||
|
id="navbar-toggle",
|
||||||
|
),
|
||||||
|
html.Button(
|
||||||
|
# use the Bootstrap navbar-toggler classes to style
|
||||||
|
html.Span(className="navbar-toggler-icon"),
|
||||||
|
className="navbar-toggler",
|
||||||
|
# the navbar-toggler classes don't set color
|
||||||
|
style={
|
||||||
|
"color": "rgba(0,0,0,.5)",
|
||||||
|
"border-color": "rgba(0,0,0,.1)",
|
||||||
|
},
|
||||||
|
id="sidebar-toggle",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
# the column containing the toggle will be only as wide as the
|
||||||
|
# toggle, resulting in the toggle being right aligned
|
||||||
|
width="auto",
|
||||||
|
# vertically align the toggle in the center
|
||||||
|
align="center",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
sidebar = html.Div(
|
||||||
|
[
|
||||||
|
sidebar_header,
|
||||||
|
# we wrap the horizontal rule and short blurb in a div that can be
|
||||||
|
# hidden on a small screen
|
||||||
|
html.Div(
|
||||||
|
[
|
||||||
|
html.Hr(),
|
||||||
|
html.P(
|
||||||
|
"A responsive sidebar layout with collapsible navigation " "links.",
|
||||||
|
className="lead",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
id="blurb",
|
||||||
|
),
|
||||||
|
# use the Collapse component to animate hiding / revealing links
|
||||||
|
dbc.Collapse(
|
||||||
|
dbc.Nav(
|
||||||
|
nav_links, # <-- Korrigiert: keine zusätzliche Liste
|
||||||
|
vertical=True,
|
||||||
|
pills=True,
|
||||||
|
),
|
||||||
|
id="collapse",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
id="sidebar",
|
||||||
|
)
|
||||||
|
|
||||||
|
content = dmc.MantineProvider([
|
||||||
|
html.Div(
|
||||||
|
html.Div(page_container, className="page-content"),style={"flex": "1", "padding": "20px"}
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
app.layout = html.Div([dcc.Location(id="url"), sidebar, content])
|
||||||
|
|
||||||
|
|
||||||
|
# @app.callback(Output("page-content", "children"), [Input("url", "pathname")])
|
||||||
|
# def render_page_content(pathname):
|
||||||
|
# if pathname == "/":
|
||||||
|
# return html.P("This is the content of the home page!")
|
||||||
|
# elif pathname == "/page-1":
|
||||||
|
# return html.P("This is the content of page 1. Yay!")
|
||||||
|
# elif pathname == "/page-2":
|
||||||
|
# return html.P("Oh cool, this is page 2!")
|
||||||
|
# # If the user tries to reach a different page, return a 404 message
|
||||||
|
# return html.Div(
|
||||||
|
# [
|
||||||
|
# html.H1("404: Not found", className="text-danger"),
|
||||||
|
# html.Hr(),
|
||||||
|
# html.P(f"The pathname {pathname} was not recognised..."),
|
||||||
|
# ],
|
||||||
|
# className="p-3 bg-light rounded-3",
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
@app.callback(
|
||||||
|
[Output("sidebar", "className"), Output("collapse", "is_open")],
|
||||||
|
[
|
||||||
|
Input("sidebar-toggle", "n_clicks"),
|
||||||
|
Input("navbar-toggle", "n_clicks"),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
State("sidebar", "className"),
|
||||||
|
State("collapse", "is_open"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def toggle_sidebar_and_collapse(sidebar_n, navbar_n, classname, is_open):
|
||||||
|
ctx = dash.callback_context
|
||||||
|
if not ctx.triggered:
|
||||||
|
return classname, is_open
|
||||||
|
trigger_id = ctx.triggered[0]["prop_id"].split(".")[0]
|
||||||
|
if trigger_id == "sidebar-toggle":
|
||||||
|
# Toggle sidebar collapse
|
||||||
|
if sidebar_n and classname == "":
|
||||||
|
return "collapsed", is_open
|
||||||
|
return "", is_open
|
||||||
|
elif trigger_id == "navbar-toggle":
|
||||||
|
# Toggle collapse
|
||||||
|
if navbar_n:
|
||||||
|
return classname, not is_open
|
||||||
|
return classname, is_open
|
||||||
|
return classname, is_open
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(port=8888, debug=True)
|
||||||
1
dashboard/.dockerignore
Normal file
1
dashboard/.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
34
dashboard/.eslintrc.cjs
Normal file
34
dashboard/.eslintrc.cjs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended'
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: ['react', '@typescript-eslint'],
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// Beispiele für sinnvolle Anpassungen
|
||||||
|
'react/react-in-jsx-scope': 'off', // nicht nötig mit React 17+
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
9
dashboard/.prettierrc
Normal file
9
dashboard/.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100,
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
9
dashboard/.stylelintrc.json
Normal file
9
dashboard/.stylelintrc.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"stylelint-config-standard",
|
||||||
|
"stylelint-config-tailwindcss"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"at-rule-no-unknown": null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +1,39 @@
|
|||||||
# dashboard/Dockerfile
|
# ==========================================
|
||||||
# Produktions-Dockerfile für die Dash-Applikation
|
# dashboard/Dockerfile (Production)
|
||||||
|
# ==========================================
|
||||||
|
FROM node:lts-alpine AS builder
|
||||||
|
|
||||||
# --- Basis-Image ---
|
|
||||||
FROM python:3.13-slim
|
|
||||||
|
|
||||||
# --- Arbeitsverzeichnis im Container ---
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# --- Systemabhängigkeiten installieren (falls benötigt) ---
|
# Copy package files
|
||||||
RUN apt-get update \
|
COPY package*.json ./
|
||||||
&& apt-get install -y --no-install-recommends \
|
COPY pnpm-lock.yaml* ./
|
||||||
build-essential git \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# --- Python-Abhängigkeiten kopieren und installieren ---
|
# Install pnpm and dependencies
|
||||||
COPY dash_using_fullcalendar-0.1.0.tar.gz ./
|
RUN npm install -g pnpm
|
||||||
COPY requirements.txt ./
|
RUN pnpm install --frozen-lockfile
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# --- Applikationscode kopieren ---
|
# Copy source code
|
||||||
COPY dashboard/ /app
|
COPY . .
|
||||||
|
|
||||||
# --- Non-Root-User anlegen und Rechte setzen ---
|
# Build arguments
|
||||||
ARG USER_ID=1000
|
ARG NODE_ENV=production
|
||||||
ARG GROUP_ID=1000
|
ARG VITE_API_URL
|
||||||
RUN groupadd -g ${GROUP_ID} infoscreen_taa \
|
|
||||||
&& useradd -u ${USER_ID} -g ${GROUP_ID} \
|
|
||||||
--shell /bin/bash --create-home infoscreen_taa \
|
|
||||||
&& chown -R infoscreen_taa:infoscreen_taa /app
|
|
||||||
USER infoscreen_taa
|
|
||||||
|
|
||||||
# --- Port für Dash exposed ---
|
# Build the application
|
||||||
EXPOSE 8050
|
RUN pnpm build
|
||||||
|
|
||||||
# --- Startbefehl: Gunicorn mit Dash-Server ---
|
# Production stage with nginx
|
||||||
# "app.py" enthält: app = dash.Dash(...); server = app.server
|
FROM nginx:alpine
|
||||||
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:8050", "app:server"]
|
|
||||||
|
# Copy built files to nginx
|
||||||
|
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Copy custom nginx config (optional)
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Start nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
@@ -1,51 +1,24 @@
|
|||||||
# dashboard/Dockerfile.dev
|
# ==========================================
|
||||||
# Entwicklungs-Dockerfile für das Dash-Dashboard
|
# dashboard/Dockerfile.dev (Development)
|
||||||
|
# ==========================================
|
||||||
|
FROM node:lts-alpine
|
||||||
|
|
||||||
FROM python:3.13-slim
|
WORKDIR /workspace/dashboard
|
||||||
|
|
||||||
# Build args für UID/GID
|
# Install dependencies manager (pnpm optional, npm reicht für Compose-Setup)
|
||||||
ARG USER_ID=1000
|
# RUN npm install -g pnpm
|
||||||
ARG GROUP_ID=1000
|
|
||||||
|
|
||||||
# Systemabhängigkeiten (falls nötig)
|
# Copy package files
|
||||||
RUN apt-get update \
|
COPY package*.json ./
|
||||||
&& apt-get install -y --no-install-recommends locales curl \
|
|
||||||
&& sed -i 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen \
|
|
||||||
&& locale-gen \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Locale setzen
|
# Install dependencies (nutze npm, da Compose "npm run dev" nutzt)
|
||||||
ENV LANG=de_DE.UTF-8 \
|
RUN npm install
|
||||||
LANGUAGE=de_DE:de \
|
|
||||||
LC_ALL=de_DE.UTF-8
|
|
||||||
|
|
||||||
# Non-root User anlegen
|
# Copy source code
|
||||||
RUN groupadd -g ${GROUP_ID} infoscreen_taa \
|
COPY . .
|
||||||
&& useradd -u ${USER_ID} -g ${GROUP_ID} --shell /bin/bash --create-home infoscreen_taa
|
|
||||||
|
|
||||||
# Arbeitsverzeichnis
|
# Expose ports
|
||||||
WORKDIR /app
|
EXPOSE 3000 9229
|
||||||
|
|
||||||
# Kopiere nur Requirements für schnellen Rebuild
|
# Standard-Dev-Command (wird von Compose überschrieben)
|
||||||
COPY dash_using_fullcalendar-0.1.0.tar.gz ./
|
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "3000"]
|
||||||
COPY requirements.txt ./
|
|
||||||
COPY requirements-dev.txt ./
|
|
||||||
|
|
||||||
# Installiere Abhängigkeiten
|
|
||||||
RUN pip install --upgrade pip \
|
|
||||||
&& pip install --no-cache-dir -r requirements.txt \
|
|
||||||
&& pip install --no-cache-dir -r requirements-dev.txt
|
|
||||||
|
|
||||||
# Setze Entwicklungs-Modus
|
|
||||||
ENV DASH_DEBUG_MODE=True
|
|
||||||
ENV API_URL=http://server:8000/api
|
|
||||||
|
|
||||||
# Ports für Dash und optional Live-Reload
|
|
||||||
EXPOSE 8050
|
|
||||||
EXPOSE 5678
|
|
||||||
|
|
||||||
# Wechsle zum non-root User
|
|
||||||
USER infoscreen_taa
|
|
||||||
|
|
||||||
# Dev-Start: Dash mit Hot-Reload
|
|
||||||
CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5637", "app.py"]
|
|
||||||
54
dashboard/README.md
Normal file
54
dashboard/README.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
extends: [
|
||||||
|
// Remove ...tseslint.configs.recommended and replace with this
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
plugins: {
|
||||||
|
// Add the react-x and react-dom plugins
|
||||||
|
'react-x': reactX,
|
||||||
|
'react-dom': reactDom,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended typescript rules
|
||||||
|
...reactX.configs['recommended-typescript'].rules,
|
||||||
|
...reactDom.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
28
dashboard/eslint.config.js
Normal file
28
dashboard/eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
13
dashboard/index.html
Normal file
13
dashboard/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
7968
dashboard/package-lock.json
generated
Normal file
7968
dashboard/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
49
dashboard/package.json
Normal file
49
dashboard/package.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"name": "dashboard",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@syncfusion/ej2-react-buttons": "^29.2.5",
|
||||||
|
"@syncfusion/ej2-react-calendars": "^29.2.11",
|
||||||
|
"@syncfusion/ej2-react-grids": "^29.2.11",
|
||||||
|
"@syncfusion/ej2-react-schedule": "^29.2.10",
|
||||||
|
"cldr-data": "^36.0.4",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.25.0",
|
||||||
|
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||||
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@types/react": "^19.1.8",
|
||||||
|
"@types/react-dom": "^19.1.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.34.1",
|
||||||
|
"@typescript-eslint/parser": "^8.34.1",
|
||||||
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "^9.29.0",
|
||||||
|
"eslint-config-prettier": "^10.1.5",
|
||||||
|
"eslint-plugin-prettier": "^5.5.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"stylelint": "^16.21.0",
|
||||||
|
"stylelint-config-standard": "^38.0.0",
|
||||||
|
"stylelint-config-tailwindcss": "^1.0.0",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.30.1",
|
||||||
|
"vite": "^6.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
2310
dashboard/pnpm-lock.yaml
generated
Normal file
2310
dashboard/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
dashboard/postcss.config.cjs
Normal file
6
dashboard/postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
1
dashboard/public/vite.svg
Normal file
1
dashboard/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
11
dashboard/src/App.css
Normal file
11
dashboard/src/App.css
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
@import "../node_modules/@syncfusion/ej2-base/styles/material.css";
|
||||||
|
@import "../node_modules/@syncfusion/ej2-buttons/styles/material.css";
|
||||||
|
@import "../node_modules/@syncfusion/ej2-calendars/styles/material.css";
|
||||||
|
@import "../node_modules/@syncfusion/ej2-dropdowns/styles/material.css";
|
||||||
|
@import "../node_modules/@syncfusion/ej2-inputs/styles/material.css";
|
||||||
|
@import "../node_modules/@syncfusion/ej2-lists/styles/material.css";
|
||||||
|
@import "../node_modules/@syncfusion/ej2-navigations/styles/material.css";
|
||||||
|
@import "../node_modules/@syncfusion/ej2-popups/styles/material.css";
|
||||||
|
@import "../node_modules/@syncfusion/ej2-splitbuttons/styles/material.css";
|
||||||
|
@import "../node_modules/@syncfusion/ej2-react-schedule/styles/material.css";
|
||||||
|
|
||||||
113
dashboard/src/App.tsx
Normal file
113
dashboard/src/App.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
// import 'react-app-polyfill/ie11'; // optional, falls benötigt
|
||||||
|
import './App.css';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
ScheduleComponent,
|
||||||
|
Day,
|
||||||
|
Week,
|
||||||
|
WorkWeek,
|
||||||
|
Month,
|
||||||
|
Agenda,
|
||||||
|
TimelineViews,
|
||||||
|
TimelineMonth,
|
||||||
|
Inject,
|
||||||
|
ViewsDirective,
|
||||||
|
ViewDirective,
|
||||||
|
ResourcesDirective,
|
||||||
|
ResourceDirective,
|
||||||
|
} from '@syncfusion/ej2-react-schedule';
|
||||||
|
import { L10n, loadCldr, setCulture } from '@syncfusion/ej2-base';
|
||||||
|
import * as de from 'cldr-data/main/de/ca-gregorian.json';
|
||||||
|
import * as numbers from 'cldr-data/main/de/numbers.json';
|
||||||
|
import * as timeZoneNames from 'cldr-data/main/de/timeZoneNames.json';
|
||||||
|
import * as numberingSystems from 'cldr-data/supplemental/numberingSystems.json';
|
||||||
|
|
||||||
|
// CLDR-Daten laden
|
||||||
|
loadCldr(
|
||||||
|
(de as unknown as { default: object }).default,
|
||||||
|
(numbers as unknown as { default: object }).default,
|
||||||
|
(timeZoneNames as unknown as { default: object }).default,
|
||||||
|
(numberingSystems as unknown as { default: object }).default
|
||||||
|
);
|
||||||
|
|
||||||
|
// Deutsche Lokalisierung für den Scheduler
|
||||||
|
L10n.load({
|
||||||
|
de: {
|
||||||
|
schedule: {
|
||||||
|
day: 'Tag',
|
||||||
|
week: 'Woche',
|
||||||
|
workWeek: 'Arbeitswoche',
|
||||||
|
month: 'Monat',
|
||||||
|
agenda: 'Agenda',
|
||||||
|
today: 'Heute',
|
||||||
|
noEvents: 'Keine Termine',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kultur setzen
|
||||||
|
setCulture('de');
|
||||||
|
|
||||||
|
// Ressourcen-Daten
|
||||||
|
const resources = [
|
||||||
|
{ text: 'Raum A', id: 1, color: '#1aaa55' },
|
||||||
|
{ text: 'Raum B', id: 2, color: '#357cd2' },
|
||||||
|
{ text: 'Raum C', id: 3, color: '#7fa900' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Dummy-Termine generieren
|
||||||
|
const now = new Date();
|
||||||
|
const appointments = Array.from({ length: 10 }).map((_, i) => {
|
||||||
|
const dayOffset = Math.floor(i * 1.4); // verteilt auf 14 Tage
|
||||||
|
const start = new Date(now);
|
||||||
|
start.setDate(now.getDate() + dayOffset);
|
||||||
|
start.setHours(9 + (i % 4), 0, 0, 0);
|
||||||
|
const end = new Date(start);
|
||||||
|
end.setHours(start.getHours() + 1);
|
||||||
|
|
||||||
|
return {
|
||||||
|
Id: i + 1,
|
||||||
|
Subject: `Termin ${i + 1}`,
|
||||||
|
StartTime: start,
|
||||||
|
EndTime: end,
|
||||||
|
ResourceId: (i % 3) + 1,
|
||||||
|
Location: resources[i % 3].text,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="p-8 bg-gray-100 min-h-screen">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Infoscreen Kalendersteuerung</h1>
|
||||||
|
<ScheduleComponent
|
||||||
|
height="650px"
|
||||||
|
locale="de"
|
||||||
|
currentView="TimelineWeek"
|
||||||
|
eventSettings={{ dataSource: appointments }}
|
||||||
|
group={{ resources: ['Räume'] }}
|
||||||
|
>
|
||||||
|
<ViewsDirective>
|
||||||
|
<ViewDirective option="Week" />
|
||||||
|
<ViewDirective option="TimelineWeek" />
|
||||||
|
<ViewDirective option="Month" />
|
||||||
|
<ViewDirective option="TimelineMonth" />
|
||||||
|
</ViewsDirective>
|
||||||
|
<ResourcesDirective>
|
||||||
|
<ResourceDirective
|
||||||
|
field="ResourceId"
|
||||||
|
title="Räume"
|
||||||
|
name="Räume"
|
||||||
|
allowMultiple={false}
|
||||||
|
dataSource={resources}
|
||||||
|
textField="text"
|
||||||
|
idField="id"
|
||||||
|
colorField="color"
|
||||||
|
/>
|
||||||
|
</ResourcesDirective>
|
||||||
|
<Inject services={[Day, Week, WorkWeek, Month, Agenda, TimelineViews, TimelineMonth]} />
|
||||||
|
</ScheduleComponent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
1
dashboard/src/assets/react.svg
Normal file
1
dashboard/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
75
dashboard/src/index.css
Normal file
75
dashboard/src/index.css
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* :root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgb(255 255 255 / 87%);
|
||||||
|
background-color: #242424;
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizelegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
} */
|
||||||
|
|
||||||
|
/* @media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
} */
|
||||||
14
dashboard/src/main.tsx
Normal file
14
dashboard/src/main.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { StrictMode } from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import './index.css';
|
||||||
|
import App from './App.tsx';
|
||||||
|
import { registerLicense } from '@syncfusion/ej2-base';
|
||||||
|
|
||||||
|
// Setze hier deinen Lizenzschlüssel ein
|
||||||
|
registerLicense('Ngo9BigBOggjHTQxAR8/V1NNaF1cWWhPYVFxWmFZfVtgd19FaFZRQ2Y/P1ZhSXxWdkNhWX5bc3xVQWZUUkF9XUs=');
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
4
dashboard/src/types/json.d.ts
vendored
Normal file
4
dashboard/src/types/json.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
declare module "*.json" {
|
||||||
|
const value: unknown;
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
1
dashboard/src/vite-env.d.ts
vendored
Normal file
1
dashboard/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
10
dashboard/tailwind.config.cjs
Normal file
10
dashboard/tailwind.config.cjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
module.exports = {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
corePlugins: {
|
||||||
|
preflight: false,
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
27
dashboard/tsconfig.app.json
Normal file
27
dashboard/tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
10
dashboard/tsconfig.json
Normal file
10
dashboard/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"typeRoots": ["./src/types", "./node_modules/@types"]
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
25
dashboard/tsconfig.node.json
Normal file
25
dashboard/tsconfig.node.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
7
dashboard/vite.config.ts
Normal file
7
dashboard/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
@@ -17,6 +17,7 @@ services:
|
|||||||
- dashboard
|
- dashboard
|
||||||
networks:
|
networks:
|
||||||
- infoscreen-net
|
- infoscreen-net
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: mariadb:11.4.7
|
image: mariadb:11.4.7
|
||||||
container_name: infoscreen-db
|
container_name: infoscreen-db
|
||||||
@@ -32,7 +33,6 @@ services:
|
|||||||
- "3306:3306"
|
- "3306:3306"
|
||||||
networks:
|
networks:
|
||||||
- infoscreen-net
|
- infoscreen-net
|
||||||
# ✅ HINZUGEFÜGT: Healthcheck für MariaDB
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -48,13 +48,11 @@ services:
|
|||||||
- ./mosquitto/config:/mosquitto/config
|
- ./mosquitto/config:/mosquitto/config
|
||||||
- ./mosquitto/data:/mosquitto/data
|
- ./mosquitto/data:/mosquitto/data
|
||||||
- ./mosquitto/log:/mosquitto/log
|
- ./mosquitto/log:/mosquitto/log
|
||||||
# ✅ HINZUGEFÜGT: MQTT-Ports explizit exponiert
|
|
||||||
ports:
|
ports:
|
||||||
- "1883:1883" # Standard MQTT
|
- "1883:1883" # Standard MQTT
|
||||||
- "9001:9001" # WebSocket (falls benötigt)
|
- "9001:9001" # WebSocket (falls benötigt)
|
||||||
networks:
|
networks:
|
||||||
- infoscreen-net
|
- infoscreen-net
|
||||||
# ✅ HINZUGEFÜGT: Healthcheck für MQTT
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "mosquitto_pub -h localhost -t test -m 'health' || exit 1"]
|
test: ["CMD-SHELL", "mosquitto_pub -h localhost -t test -m 'health' || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -70,7 +68,6 @@ services:
|
|||||||
container_name: infoscreen-api
|
container_name: infoscreen-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
# ✅ GEÄNDERT: Erweiterte depends_on mit Healthcheck-Conditions
|
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
mqtt:
|
mqtt:
|
||||||
@@ -93,33 +90,36 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
|
# ✅ GEÄNDERT: Dashboard jetzt mit Node.js/React statt Python/Dash
|
||||||
dashboard:
|
dashboard:
|
||||||
build:
|
build:
|
||||||
context: ./dashboard
|
context: ./dashboard
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
# ✅ HINZUGEFÜGT: Build-Args für React Production Build
|
||||||
|
args:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- VITE_API_URL=${API_URL}
|
||||||
image: infoscreen-dashboard:latest
|
image: infoscreen-dashboard:latest
|
||||||
container_name: infoscreen-dashboard
|
container_name: infoscreen-dashboard
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
# ✅ GEÄNDERT: Healthcheck-Condition für Server
|
|
||||||
server:
|
server:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
environment:
|
environment:
|
||||||
API_URL: ${API_URL}
|
# ✅ GEÄNDERT: React-spezifische Umgebungsvariablen
|
||||||
|
- VITE_API_URL=${API_URL}
|
||||||
|
- NODE_ENV=production
|
||||||
ports:
|
ports:
|
||||||
- "8050:8050"
|
- "3000:3000" # ✅ GEÄNDERT: Standard React/Vite Port
|
||||||
networks:
|
networks:
|
||||||
- infoscreen-net
|
- infoscreen-net
|
||||||
|
# ✅ GEÄNDERT: Healthcheck für React App
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8050/_alive"]
|
test: ["CMD", "curl", "-f", "http://localhost:3000/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 45s
|
start_period: 30s
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
db-data:
|
db-data:
|
||||||
# mosquitto-conf:
|
|
||||||
# mosquitto-data:
|
|
||||||
# # ✅ HINZUGEFÜGT: Log-Volume für MQTT
|
|
||||||
# mosquitto-logs:
|
|
||||||
12
nginx.conf
12
nginx.conf
@@ -1,14 +1,22 @@
|
|||||||
events {}
|
events {}
|
||||||
http {
|
http {
|
||||||
upstream dashboard {
|
upstream dashboard {
|
||||||
server infoscreen-dashboard:8050;
|
server 127.0.0.1:3000;
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
# Optional: HTTP auf HTTPS weiterleiten
|
# Optional: HTTP auf HTTPS weiterleiten
|
||||||
return 301 https://$host$request_uri;
|
# return 301 https://$host$request_uri;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://dashboard;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
|
|||||||
Reference in New Issue
Block a user