diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..f5f54f3
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,10 @@
+.git
+.gitignore
+.env
+*.dump
+backup.sql
+*.zip
+**/__pycache__/
+**/*.pyc
+worker/ckal_upload_client/xlsx/*.xlsx
+worker/pdf_python/terminuebersicht.pdf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0c46a78
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,28 @@
+# Environment / Secrets
+.env
+
+# Datenbank-Backups
+*.dump
+backup.sql
+
+# Archive
+*.zip
+
+# CKalender Runtime-Dateien
+worker/ckal_upload_client/xlsx/*.xlsx
+worker/pdf_python/terminuebersicht.pdf
+
+# Benutzer-Profilbilder
+# default.jpg ist ein statisches Fallback-Asset und bleibt in Git
+web/intranet_repo/general_files/profile_pictures/*
+!web/intranet_repo/general_files/profile_pictures/default.jpg
+
+# Contentpanel / News
+# Hochgeladene PDFs sowie daraus erzeugte PNGs sind Produktivdaten
+web/intranet_repo/projects/contentpanel/insertion/news/*
+
+# Generierte Dashboard-Terminbilder
+web/intranet_repo/projects/dashboard/month/*
+
+# Lokale Backup-Dateien
+*.bak
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..8c990b9
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,55 @@
+FROM php:8.3.33-apache-bookworm
+
+ENV TZ=Europe/Berlin \
+ PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
+ PATH=/opt/intranet-venv/bin:$PATH
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ locales tzdata \
+ python3 python3-venv \
+ openssh-client ghostscript poppler-utils pngquant \
+ libpq-dev libzip-dev \
+ libpng-dev libjpeg62-turbo-dev libfreetype6-dev \
+ && echo 'de_DE.UTF-8 UTF-8' > /etc/locale.gen \
+ && locale-gen \
+ && docker-php-ext-configure gd --with-freetype --with-jpeg \
+ && docker-php-ext-install -j"$(nproc)" pdo_pgsql pgsql zip gd \
+ && php -r '$r=["pdo_pgsql","pgsql","curl","mbstring","dom","SimpleXML","xml","xmlreader","xmlwriter","zip","gd","Zend OPcache"]; foreach($r as $e){if(!extension_loaded($e)){fwrite(STDERR,"missing PHP ext: $e\n"); exit(1);}}' \
+ && python3 -m venv /opt/intranet-venv \
+ && /opt/intranet-venv/bin/pip install --no-cache-dir --upgrade pip \
+ && /opt/intranet-venv/bin/pip install --no-cache-dir \
+ beautifulsoup4==4.13.4 fpdf==1.7.2 requests==2.32.4 playwright==1.62.0 \
+ && /opt/intranet-venv/bin/python -m playwright install --with-deps chromium \
+ && chmod -R a+rX /ms-playwright \
+ && rm -rf /var/lib/apt/lists/*
+
+ENV LANG=de_DE.UTF-8 \
+ LC_ALL=de_DE.UTF-8
+
+COPY docker/intranet.ini /usr/local/etc/php/conf.d/99-intranet.ini
+COPY web/ /var/www/html/
+COPY worker/ /opt/intranet/worker/
+COPY docker/scheduler.py /opt/intranet/docker/scheduler.py
+
+RUN mkdir -p \
+ /data/ckal-xlsx /data/pdf /data/state \
+ /var/www/html/intranet_repo/general_files/profile_pictures \
+ /var/www/html/intranet_repo/projects/contentpanel/insertion/news \
+ /var/www/html/intranet_repo/projects/developerpanel/work \
+ /var/www/html/intranet_repo/projects/seelsorge/data \
+ /var/www/html/intranet_repo/projects/dashboard/month \
+ && chown -R www-data:www-data \
+ /data \
+ /var/www/html/intranet_repo/general_files/profile_pictures \
+ /var/www/html/intranet_repo/projects/contentpanel/insertion/news \
+ /var/www/html/intranet_repo/projects/developerpanel/work \
+ /var/www/html/intranet_repo/projects/seelsorge/data \
+ /var/www/html/intranet_repo/projects/dashboard/month \
+ && chmod +x /opt/intranet/docker/scheduler.py /opt/intranet/worker/pdf_python/pdf_to_png.sh
+
+EXPOSE 80
+
+HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
+ CMD curl -fsS http://127.0.0.1/ >/dev/null || exit 1
+
+CMD ["apache2-foreground"]
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..c411b76
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,100 @@
+services:
+ web:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ restart: unless-stopped
+ expose:
+ - "80"
+ environment:
+ TZ: Europe/Berlin
+ INTRANET_FQDN: ${INTRANET_FQDN:?set INTRANET_FQDN}
+ DB_HOST: ${DB_HOST:?set DB_HOST}
+ DB_PORT: ${DB_PORT:-5432}
+ DB_NAME: ${DB_NAME:?set DB_NAME}
+ DB_USER: ${DB_USER:?set DB_USER}
+ DB_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD}
+ SMTP_HOST: ${SMTP_HOST:-smtp.strato.de}
+ SMTP_PORT: ${SMTP_PORT:-587}
+ SMTP_USER: ${SMTP_USER:?set SMTP_USER}
+ SMTP_PASSWORD: ${SMTP_PASSWORD:?set SMTP_PASSWORD}
+ ADMIN_EMAIL: ${ADMIN_EMAIL:-technik@nak-reinickendorf.de}
+ SMS_GATEWAY_URL: ${SMS_GATEWAY_URL:?set SMS_GATEWAY_URL}
+ HA_BASE_URL: ${HA_BASE_URL:?set HA_BASE_URL}
+ HA_TOKEN: ${HA_TOKEN:?set HA_TOKEN}
+ OPENWEATHER_API_KEY: ${OPENWEATHER_API_KEY:?set OPENWEATHER_API_KEY}
+ CKAL_XLSX_DIR: /data/ckal-xlsx
+ volumes:
+ - profile_pictures:/var/www/html/intranet_repo/general_files/profile_pictures
+ - content_news:/var/www/html/intranet_repo/projects/contentpanel/insertion/news
+ - developer_work:/var/www/html/intranet_repo/projects/developerpanel/work
+ - seelsorge_data:/var/www/html/intranet_repo/projects/seelsorge/data
+ - dashboard_month:/var/www/html/intranet_repo/projects/dashboard/month
+ - ckal_xlsx:/data/ckal-xlsx
+ networks:
+ - app
+ - dokploy-network
+
+ scheduler:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ restart: unless-stopped
+ init: true
+ user: "33:33"
+ command: ["/opt/intranet/docker/scheduler.py"]
+ shm_size: "512m"
+ environment:
+ TZ: Europe/Berlin
+ HOME: /tmp/intranet-worker
+ XDG_CACHE_HOME: /tmp/intranet-worker-cache
+ PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
+ INTRANET_FQDN: ${INTRANET_FQDN:?set INTRANET_FQDN}
+ DB_HOST: ${DB_HOST:?set DB_HOST}
+ DB_PORT: ${DB_PORT:-5432}
+ DB_NAME: ${DB_NAME:?set DB_NAME}
+ DB_USER: ${DB_USER:?set DB_USER}
+ DB_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD}
+ SMTP_HOST: ${SMTP_HOST:-smtp.strato.de}
+ SMTP_PORT: ${SMTP_PORT:-587}
+ SMTP_USER: ${SMTP_USER:?set SMTP_USER}
+ SMTP_PASSWORD: ${SMTP_PASSWORD:?set SMTP_PASSWORD}
+ ADMIN_EMAIL: ${ADMIN_EMAIL:-technik@nak-reinickendorf.de}
+ CKAL_USERNAME: ${CKAL_USERNAME:?set CKAL_USERNAME}
+ CKAL_PASSWORD: ${CKAL_PASSWORD:?set CKAL_PASSWORD}
+ CKAL_CALENDAR_URL: ${CKAL_CALENDAR_URL:?set CKAL_CALENDAR_URL}
+ CKAL_XLSX_DIR: /data/ckal-xlsx
+ PDF_OUTPUT: /data/pdf/terminuebersicht.pdf
+ DASHBOARD_MONTH_DIR: /var/www/html/intranet_repo/projects/dashboard/month
+ CALENDAR_INTERVAL_SECONDS: ${CALENDAR_INTERVAL_SECONDS:-60}
+ STRATO_SSH_HOST: ${STRATO_SSH_HOST:-ssh.strato.de}
+ STRATO_SSH_PORT: ${STRATO_SSH_PORT:-22}
+ STRATO_SSH_USER: ${STRATO_SSH_USER:?set STRATO_SSH_USER}
+ STRATO_REMOTE_PATH: ${STRATO_REMOTE_PATH:?set STRATO_REMOTE_PATH}
+ STRATO_SSH_KEY_FILE: /run/secrets/strato_ed25519
+ STRATO_KNOWN_HOSTS_FILE: /run/secrets/known_hosts
+ volumes:
+ - dashboard_month:/var/www/html/intranet_repo/projects/dashboard/month
+ - ckal_xlsx:/data/ckal-xlsx
+ - calendar_pdf:/data/pdf
+ - scheduler_state:/data/state
+ - ../files/strato_ed25519:/run/secrets/strato_ed25519:ro
+ - ../files/known_hosts:/run/secrets/known_hosts:ro
+ networks:
+ - app
+ - dokploy-network
+
+volumes:
+ profile_pictures:
+ content_news:
+ developer_work:
+ seelsorge_data:
+ dashboard_month:
+ ckal_xlsx:
+ calendar_pdf:
+ scheduler_state:
+
+networks:
+ app:
+ dokploy-network:
+ external: true
diff --git a/docker/intranet.ini b/docker/intranet.ini
new file mode 100644
index 0000000..66453cf
--- /dev/null
+++ b/docker/intranet.ini
@@ -0,0 +1,11 @@
+date.timezone = Europe/Berlin
+upload_max_filesize = 64M
+post_max_size = 70M
+memory_limit = 512M
+max_execution_time = 180
+display_errors = Off
+log_errors = On
+session.cookie_httponly = 1
+session.cookie_samesite = Lax
+session.cookie_secure = 1
+expose_php = Off
diff --git a/docker/scheduler.py b/docker/scheduler.py
new file mode 100644
index 0000000..3cf2ad9
--- /dev/null
+++ b/docker/scheduler.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+import fcntl
+import os
+import shutil
+import subprocess
+import time
+from datetime import datetime
+from pathlib import Path
+
+PYTHON = '/opt/intranet-venv/bin/python'
+ROOT = Path('/opt/intranet/worker')
+STATE_DIR = Path('/data/state')
+PDF_OUTPUT = Path(os.getenv('PDF_OUTPUT', '/data/pdf/terminuebersicht.pdf'))
+INTERVAL = int(os.getenv('CALENDAR_INTERVAL_SECONDS', '60'))
+
+
+def required(name: str) -> str:
+ value = os.getenv(name, '').strip()
+ if not value:
+ raise RuntimeError(f'Missing environment variable: {name}')
+ return value
+
+
+def run(cmd, cwd=None):
+ print('+', ' '.join(map(str, cmd)), flush=True)
+ subprocess.run(cmd, cwd=cwd, check=True, env=os.environ.copy())
+
+
+def prepare_ssh_key() -> Path:
+ source = Path(os.getenv('STRATO_SSH_KEY_FILE', '/run/secrets/strato_ed25519'))
+ if not source.is_file():
+ raise RuntimeError(f'SSH key file missing: {source}')
+ target = Path('/tmp/strato_ed25519')
+ shutil.copyfile(source, target)
+ target.chmod(0o600)
+ return target
+
+
+def calendar_sync():
+ run([PYTHON, str(ROOT / 'ckal_upload_client/client.py')], cwd=ROOT / 'ckal_upload_client')
+ run([PYTHON, str(ROOT / 'pdf_python/create_pdf.py')], cwd=ROOT / 'pdf_python')
+ run(['/bin/bash', str(ROOT / 'pdf_python/pdf_to_png.sh')], cwd=ROOT / 'pdf_python')
+
+ required('STRATO_SSH_USER')
+ required('STRATO_REMOTE_PATH')
+ host = os.getenv('STRATO_SSH_HOST', 'ssh.strato.de')
+ port = os.getenv('STRATO_SSH_PORT', '22')
+ known_hosts = Path(os.getenv('STRATO_KNOWN_HOSTS_FILE', '/run/secrets/known_hosts'))
+ if not known_hosts.is_file():
+ raise RuntimeError(f'known_hosts missing: {known_hosts}')
+ key = prepare_ssh_key()
+ if not PDF_OUTPUT.is_file() or PDF_OUTPUT.stat().st_size == 0:
+ raise RuntimeError(f'PDF missing/empty: {PDF_OUTPUT}')
+
+ run([
+ '/usr/bin/scp', '-O', '-q', '-P', port,
+ '-i', str(key),
+ '-o', 'IdentitiesOnly=yes',
+ '-o', 'BatchMode=yes',
+ '-o', 'StrictHostKeyChecking=yes',
+ '-o', f'UserKnownHostsFile={known_hosts}',
+ str(PDF_OUTPUT),
+ f"{required('STRATO_SSH_USER')}@{host}:{required('STRATO_REMOTE_PATH')}",
+ ])
+ print('Calendar Sync erfolgreich', flush=True)
+
+
+def brandschutz_once_per_month():
+ now = datetime.now()
+ if now.day != 1 or now.hour < 8:
+ return
+
+ marker = STATE_DIR / 'brandschutz-last-attempt'
+ month = now.strftime('%Y-%m')
+ if marker.exists() and marker.read_text().strip() == month:
+ return
+
+ # At-most-once: erst markieren, dann ausführen. Bei Fehler manuell prüfen und Marker löschen.
+ marker.write_text(month + '\n')
+ print(f'Brandschutz-Lauf {month}', flush=True)
+ run(['/usr/local/bin/php', '/var/www/html/intranet_repo/projects/brandschutz/brandschutz_cron.php'])
+
+
+def main():
+ STATE_DIR.mkdir(parents=True, exist_ok=True)
+ lock_handle = (STATE_DIR / 'scheduler.lock').open('w')
+ try:
+ fcntl.flock(lock_handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
+ except BlockingIOError as exc:
+ raise RuntimeError('Ein anderer Scheduler läuft bereits.') from exc
+
+ if INTERVAL < 30:
+ raise RuntimeError('CALENDAR_INTERVAL_SECONDS muss >= 30 sein.')
+ for name in ('CKAL_USERNAME', 'CKAL_PASSWORD', 'CKAL_CALENDAR_URL'):
+ required(name)
+ PDF_OUTPUT.parent.mkdir(parents=True, exist_ok=True)
+
+ next_run = 0.0
+ while True:
+ try:
+ brandschutz_once_per_month()
+ except Exception as exc:
+ print(f'FEHLER Brandschutz: {exc}', flush=True)
+
+ now_mono = time.monotonic()
+ if now_mono >= next_run:
+ try:
+ calendar_sync()
+ except Exception as exc:
+ print(f'FEHLER Calendar Sync: {exc}', flush=True)
+ next_run = time.monotonic() + INTERVAL
+
+ (STATE_DIR / 'heartbeat').write_text(str(int(time.time())) + '\n')
+ time.sleep(1)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/web/dashboard.php b/web/dashboard.php
new file mode 100644
index 0000000..78b2874
--- /dev/null
+++ b/web/dashboard.php
@@ -0,0 +1,3 @@
+ Format: "Y-m-d"
+*/
+
+require_once('util/connector_feiertage.php');
+ $connector_feiertage = lib_Feiertage_Connector::getInstance();
+require_once('util/connector_kalendertage.php');
+ $connector_kalendertage = lib_Kalendertage_Connector::getInstance();
+
+ $pdo = require($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
+?>
+
+
+
+
+
+
+ View
+
+
+ Edit
+
+
+
diff --git a/web/intranet_repo/_recycle_bin/kalli.css b/web/intranet_repo/_recycle_bin/kalli.css
new file mode 100644
index 0000000..82ada37
--- /dev/null
+++ b/web/intranet_repo/_recycle_bin/kalli.css
@@ -0,0 +1,756 @@
+/*
+ Name: kalli.css
+ Beschreibung: Spezielles CSS für KALLI
+
+ TODO: LÖSCHEN - WIRD NICHT MEHR EINGESETZT
+*/
+a {
+ color: black;
+ text-decoration: none;
+}
+
+a:hover{
+ color: white;
+}
+
+.header_menu {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ height: 7%;
+ width: 100vw;
+ background-color: #5793c9;
+ border-bottom: 4px solid #c3e2ff;
+ z-index: 1000; /* Z-Index erhöhen, um sicherzustellen, dass der Header über anderen Elementen liegt */
+ position: fixed;
+}
+.header_menu a {
+ height: 100%;
+}
+.header_menu img {
+ height: 100%;
+}
+.content {
+ position: absolute;
+ width: 100vw;;
+ top: 8%;
+}
+.profile {
+ display: flex;
+ flex-direction: row;
+}
+.center {
+ height: 100%;
+}
+
+.center a {
+ padding: 0 10px 0 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.center img {
+ height: 80%;
+ max-height: 50px;
+ border-radius: 50%;
+ overflow: hidden;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+}
+
+h1, h2 {
+ text-align: center;
+}
+
+.alert{
+ background-color: #f44336 !important;
+}
+
+.spacer{
+ height: 6em;
+}
+
+.content-spacer {
+ min-height: calc( 100vh - 67px);
+}
+
+.legal {
+ margin: auto;
+ max-width: 63em;
+ min-width: 0em;
+ text-align: justify;
+}
+
+.legal a {
+ color: #5793c9;
+}
+
+.legal a:hover {
+ color: #1c5282;
+}
+
+.pdf {
+ margin: auto;
+ width: 70vw;
+ height: 50em;
+ min-width: 600px;
+}
+
+#pdf {
+ display: none;
+ text-align: center;
+ font-size: 20px;
+ margin: 3em;
+ }
+
+#pdf:hover{
+ color: #1c5282;
+}
+
+@media screen and (max-width:650px) {
+ .pdf{
+ display: none;
+ }
+ #pdf{
+ display: block;
+ }
+}
+
+.event-box {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #c3e2ff;
+ padding: 10px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box0 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #c3e2ff;
+ padding: 10px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ min-width: 20em;
+ justify-content: center; /* Center horizontally */
+ align-items: center; /* Center vertically */
+}
+
+
+.event-box1 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #c3e2ff;
+ padding: 0px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box2 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #c3e2ff;
+ padding: 0px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box3 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #ffffff;
+ padding: 0px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box4 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #c3e2ff;
+ padding: 10px 1em;
+ display: flex;
+
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box5 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #1c5282;
+ padding: 0px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box6 {
+ overflow-x: scroll; /*ACHTUNG!*/
+ max-width: 80vw;
+ background-color: #c3e2ff;
+ margin: auto;
+ padding: 5px 1em;
+}
+
+.group-column {
+ flex-grow: 10; /* default 0 */
+ padding: 10px 1em;
+}
+
+.event {
+ background-color: #5793c9;
+ margin: 15px 0;
+ padding-left: 10px;
+ border-radius: 5px;
+ display: flex;
+ flex-direction: row;
+}
+
+.event1 {
+ background-color: #5793c9;
+ margin: 15px 0;
+ padding-left: 10px;
+ padding-right: 10px;
+ border-radius: 5px;
+ display: flex;
+ flex-direction: row;
+}
+
+.event-box a {
+ margin: 0 1em;
+}
+
+.svg1 {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 0 15px 0 20px;
+}
+
+.news {
+ width: fit-content;
+ padding: 1em;
+ background-color: #5793c9;
+ border-radius: 5px;
+ margin: 3em auto;
+}
+
+.news1 {
+
+ background-color: #5793c9;
+ margin: 15px 0;
+ padding-left: 10px;
+ padding-right: 10px;
+ border-radius: 5px;
+ display: flex;
+ flex-direction: row;
+}
+.news2 {
+
+ background-color: #5793c9;
+ margin: 15px 0;
+ padding-left: 10px;
+ padding-right: 10px;
+ border-radius: 5px;
+ display: flex;
+ flex-direction: row;
+}
+
+.news3 {
+ width: fit-content;
+ padding: 0.5em;
+ background-color: #5793c9;
+ border-radius: 5px;
+ margin: auto;
+}
+
+.news2:hover {
+
+ background-color: #1c5282;
+
+}
+
+.news3:hover {
+
+ background-color: #1c5282;
+
+}
+
+.null {
+ margin: 15px 0;
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
+
+
+.news a{
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+ align-content: flex-end;
+}
+
+.svg-news {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 0 0 0 10px;
+}
+
+.form-div {
+ margin: auto;
+ max-width: 70vw;
+ width: 30em;
+ padding: 1em;
+ background-color: #c3e2ff;
+ border-radius: 5px;
+}
+
+.form-div1 {
+ margin: auto;
+ max-width: 70vw;
+ width: 30em;
+ padding: 1em;
+ text-align: center;
+
+}
+
+input[type=text], select {
+ width: 100%;
+ padding: 12px 20px;
+ margin: 8px 0;
+ display: inline-block;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+
+textarea, select {
+ width: 100%;
+ padding: 12px 20px;
+ margin: 8px 0;
+ display: inline-block;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+
+input[type=number], select {
+ width: 100%;
+ padding: 5px 1px 5px 5px;
+ margin: 8px 0;
+ display: inline-block;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+
+input[type=submit] {
+ width: 100%;
+ background-color: #5793c9;
+ color: white;
+ padding: 14px 20px;
+ margin: 8px 0;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+input[type=submit]:hover {
+ background-color: #2a6193;
+}
+
+
+footer {
+ height: 3em;
+ border-top: #5793c9 3px solid;
+ margin-top: 1em;
+ display: flex;
+ flex-direction: column;
+ width: 100vw;
+}
+
+footer a:hover{
+ color: #5793c9;
+}
+
+.legal-links {
+ width: fit-content;
+ margin: auto;
+}
+
+.legal-links a {
+ margin: 0 1em;
+ font-size: 16px;
+}
+
+
+#mySidenav a {
+ position: absolute; /* Position them relative to the browser window */
+ position: fixed;
+ right: -80px; /* Position them outside of the screen */
+ transition: 0.3s; /* Add transition on hover */
+ padding: 15px; /* 15px padding */
+ width: 100px; /* Set a specific width */
+ text-decoration: none; /* Remove underline */
+ font-size: 20px; /* Increase font size */
+ color: white; /* White text color */
+ border-radius: 5px 0 0 5px; /* Rounded corners on the top right and bottom right side */
+ text-align: end;
+}
+
+#mySidenav a:hover {
+ right: 0; /* On mouse-over, make the elements appear as they should */
+}
+
+/* The about link: 20px from the top with a green background */
+#admin {
+ top: 15px;
+ background-color: #5793c9;
+}
+
+
+/* Style the Image Used to Trigger the Modal */
+#myImg {
+ border-radius: 5px;
+ box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
+ cursor: pointer;
+}
+
+/* On mouse-over, add a deeper shadow */
+#myImg:hover {
+ box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
+}
+
+
+
+.box2 {
+ width: 100%;
+ padding: 5px 0;
+ max-width: 600px;
+ margin: auto;
+}
+
+.box2:hover {
+ max-width: 800px;
+ font-size: 20pt;
+ transition: 1s;
+}
+
+.box3 {
+ max-width: 100%;
+}
+
+/* Add some padding inside the card container */
+.container {
+ padding: 2px 16px;
+ text-align: center;
+}
+
+.container_flex {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ max-width: 550px;
+ min-width: 120px;
+ background-color: #35374b;
+ margin: 0.5em auto;
+ padding: 0px;
+ color: #fff;
+ border-radius: 7px
+}
+
+.container_flex_row {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ width: 90%;
+ margin: 0.75em auto;
+ padding: 7px;
+ border-radius: 7px
+}
+
+.container_flex_row_2 {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-evenly;
+ padding: 0;
+ margin: auto;
+}
+
+.container_flex_row_bg {
+ background-color: #c3e2ff;
+ border-radius: 7px;
+ margin: 1em auto 1em auto;
+ width: 90%;
+ padding: 3px 3px 3px 3px;
+}
+
+.time_group {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ width: 300px;
+}
+
+.container_today {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: center;
+ max-width: 550px;
+ min-width: 90px;
+ background-color: #35374b;
+ margin: 0.75em auto;
+ padding: 7px;
+ color: #fff;
+ border-radius: 7px
+}
+
+.container_flex_title {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: center;
+ max-width: 550px;
+ background-color: #3478a2;
+ margin: 0.75em auto;
+ color: #fff;
+ border-radius: 7px
+}
+
+.container_flex_casual {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ max-width: 650px;
+ background-color: #35374b;
+ padding: 0 1em 0 1em;
+ color: #fff;
+ border-radius: 7px;
+ margin: 0 0.75em 0 0.75em;
+}
+
+.container_flex_casual_2 {
+ background-color: #35374b;
+ padding: 1em 1em 1em 1em;
+ color: #fff;
+ border-radius: 7px;
+ max-height: 100px;
+ justify-content: center;
+}
+
+.container_flex_casual_2 img {
+ display: block;
+ justify-content: center;
+ margin: auto;
+}
+
+.container_flex_time {
+ justify-content: center;
+ margin: auto;
+ background-color: #35374b;
+ margin: 5px;
+ font-size: 25;
+ padding: 12px 20px 12px 20px;
+ color: #fff;
+ border-radius: 7px;
+}
+
+.container_flex_time p {
+ margin: 0;
+}
+
+.container_flex_topbar {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ padding: 10px 10px 0 10px;
+ border-radius: 7px;
+}
+
+.vertical_spacer {
+ height: 100%;
+ margin: auto 0 auto 0;
+}
+
+.container_flex_2 {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ padding: 10px 0px 10px 0px;
+ border-radius: 7px;
+ margin: auto;
+}
+
+.container_flex_weather {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ padding: 10px 10px 10px 10px;
+ border-radius: 7px;
+}
+
+.container_flex_3 {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-evenly;
+ background-color: #3b3c4e;
+ width: 593px;
+ padding: 10px 10px 10px 10px;
+ color: #fff;
+ border-radius: 7px;
+
+}
+
+.container_flex_bg {
+ background-color: #c3e2ff;
+ border-radius: 7px;
+ padding: 3px 10px 3px 10px;
+}
+
+.item_flex_2 {
+ margin: 0.2em;
+}
+
+.item_flex_oben {
+ margin: 0.75em auto auto auto;
+}
+
+.item_flex_rechts {
+ margin: auto 0.2em auto auto;
+}
+
+.item_flex_links {
+ margin: auto auto auto 0.2em;
+}
+
+.opnv-icon {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 0 0 0 20px;
+}
+
+.temp-icon {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+.temp-icon-house {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 7px 0 7px 5px;
+}
+
+.temp-icon-today img {
+ display: block;
+ justify-content: center;
+ padding: 5px 5px 5px 5px;
+ margin: auto;
+}
+
+.temp-icon-house img {
+ display: block;
+ justify-content: center;
+ margin: auto 2em auto auto;
+}
+
+.temp-line {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 0 10 0 0px;
+}
+
+
+.item_flex.line {
+ max-width: 300px;
+ padding: 0px 10px 0px 10px;
+}
+
+.h2_test {
+ font-size:25px;
+ padding: 15px 0 0 0;
+}
+
+.item_flex.line p{
+ text-align: center;
+}
+
+.item_flex.time-remain{
+ text-align: center;
+ margin: 0 20px 0 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+.item_flex.time-remain *{
+ margin: 0;
+}
+table {
+ border-collapse: separate;
+ border-spacing: 0.2em;
+}
+
+th, td {
+ padding: .2em .5em;
+ border-radius: .1em;
+}
+
+
+
+td {
+ font-style: italic;
+ text-align: right;
+ box-shadow: inset 1px 3px 5px -3px rgb(0 0 0 / .5);
+}
+
+td:empty{
+ box-shadow: none;
+}
+
+tbody tr:nth-child(even) {
+ background-color: #e4ebf2;
+ color: #000;
+}
+
+.rechtsbuendig {
+ text-align: right;
+}
+
+.bosskoddes {
+ max-width: 100%;
+ height: auto;
+ display: none;
+}
diff --git a/web/intranet_repo/_recycle_bin/main.css b/web/intranet_repo/_recycle_bin/main.css
new file mode 100644
index 0000000..ddcaaa4
--- /dev/null
+++ b/web/intranet_repo/_recycle_bin/main.css
@@ -0,0 +1,789 @@
+body, html {
+ margin: 0;
+ padding: 0;
+ min-height: 100vh;
+ position: relative;
+ max-width: 100vw;
+ overflow-x: hidden;
+}
+*{
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
+}
+a {
+ color: black;
+ text-decoration: none;
+}
+
+a:hover{
+ color: white;
+}
+
+.header_menu {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ height: 7%;
+ width: 100vw;
+ background-color: #5793c9;
+ border-bottom: 4px solid #c3e2ff;
+ z-index: 1000; /* Z-Index erhöhen, um sicherzustellen, dass der Header über anderen Elementen liegt */
+ position: fixed;
+}
+.header_menu a {
+ height: 100%;
+}
+.header_menu img {
+ height: 100%;
+}
+.content {
+ position: absolute;
+ width: 100vw;;
+ top: 8%;
+}
+.profile {
+ display: flex;
+ flex-direction: row;
+}
+.center {
+ height: 100%;
+}
+
+.center a {
+ padding: 0 10px 0 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.center img {
+ height: 80%;
+ max-height: 50px;
+ border-radius: 50%;
+ overflow: hidden;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+}
+
+h1, h2 {
+ text-align: center;
+}
+
+.error-box {
+
+ background-color: #04AA6D;
+ color: #fefefe;
+ margin: auto;
+ height: 3em;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ border-radius: 2px;
+ min-width: fit-content;
+ padding: 0 1em;
+}
+
+.error-box1 {
+
+ background-color: #f44336;
+ color: #fefefe;
+ margin: auto;
+ height: 3em;
+ text-align: center;
+ display: flex;
+ justify-content: center;
+ border-radius: 2px;
+ min-width: fit-content;
+ padding: 0 1em;
+}
+
+.alert{
+ background-color: #f44336 !important;
+}
+
+.spacer{
+ height: 6em;
+}
+
+.content-spacer {
+ min-height: calc( 100vh - 67px);
+}
+
+.legal {
+ margin: auto;
+ max-width: 63em;
+ min-width: 0em;
+ text-align: justify;
+}
+
+.legal a {
+ color: #5793c9;
+}
+
+.legal a:hover {
+ color: #1c5282;
+}
+
+.pdf {
+ margin: auto;
+ width: 70vw;
+ height: 50em;
+ min-width: 600px;
+}
+
+#pdf {
+ display: none;
+ text-align: center;
+ font-size: 20px;
+ margin: 3em;
+ }
+
+#pdf:hover{
+ color: #1c5282;
+}
+
+@media screen and (max-width:650px) {
+ .pdf{
+ display: none;
+ }
+ #pdf{
+ display: block;
+ }
+}
+
+.event-box {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #c3e2ff;
+ padding: 10px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box0 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #c3e2ff;
+ padding: 10px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ min-width: 20em;
+ justify-content: center; /* Center horizontally */
+ align-items: center; /* Center vertically */
+}
+
+
+.event-box1 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #c3e2ff;
+ padding: 0px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box2 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #c3e2ff;
+ padding: 0px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box3 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #ffffff;
+ padding: 0px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box4 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #c3e2ff;
+ padding: 10px 1em;
+ display: flex;
+
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box5 {
+ max-width: 60vw;
+ margin: auto;
+ background-color: #1c5282;
+ padding: 0px 1em;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: space-evenly;
+ min-width: 20em;
+}
+
+.event-box6 {
+ overflow-x: scroll; /*ACHTUNG!*/
+ max-width: 80vw;
+ background-color: #c3e2ff;
+ margin: auto;
+ padding: 5px 1em;
+}
+
+.group-column {
+ flex-grow: 10; /* default 0 */
+ padding: 10px 1em;
+}
+
+.event {
+ background-color: #5793c9;
+ margin: 15px 0;
+ padding-left: 10px;
+ border-radius: 5px;
+ display: flex;
+ flex-direction: row;
+}
+
+.event1 {
+ background-color: #5793c9;
+ margin: 15px 0;
+ padding-left: 10px;
+ padding-right: 10px;
+ border-radius: 5px;
+ display: flex;
+ flex-direction: row;
+}
+
+.event-box a {
+ margin: 0 1em;
+}
+
+.svg1 {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 0 15px 0 20px;
+}
+
+.news {
+ width: fit-content;
+ padding: 1em;
+ background-color: #5793c9;
+ border-radius: 5px;
+ margin: 3em auto;
+}
+
+.news1 {
+
+ background-color: #5793c9;
+ margin: 15px 0;
+ padding-left: 10px;
+ padding-right: 10px;
+ border-radius: 5px;
+ display: flex;
+ flex-direction: row;
+}
+.news2 {
+
+ background-color: #5793c9;
+ margin: 15px 0;
+ padding-left: 10px;
+ padding-right: 10px;
+ border-radius: 5px;
+ display: flex;
+ flex-direction: row;
+}
+
+.news3 {
+ width: fit-content;
+ padding: 0.5em;
+ background-color: #5793c9;
+ border-radius: 5px;
+ margin: auto;
+}
+
+.news2:hover {
+
+ background-color: #1c5282;
+
+}
+
+.news3:hover {
+
+ background-color: #1c5282;
+
+}
+
+.null {
+ margin: 15px 0;
+ padding-left: 10px;
+ padding-right: 10px;
+}
+
+
+
+.news a{
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+ align-content: flex-end;
+}
+
+.svg-news {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 0 0 0 10px;
+}
+
+.form-div {
+ margin: auto;
+ max-width: 70vw;
+ width: 30em;
+ padding: 1em;
+ background-color: #c3e2ff;
+ border-radius: 5px;
+}
+
+.form-div1 {
+ margin: auto;
+ max-width: 70vw;
+ width: 30em;
+ padding: 1em;
+ text-align: center;
+
+}
+
+input[type=text], select {
+ width: 100%;
+ padding: 12px 20px;
+ margin: 8px 0;
+ display: inline-block;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+
+textarea, select {
+ width: 100%;
+ padding: 12px 20px;
+ margin: 8px 0;
+ display: inline-block;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+
+input[type=number], select {
+ width: 100%;
+ padding: 5px 1px 5px 5px;
+ margin: 8px 0;
+ display: inline-block;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+
+input[type=submit] {
+ width: 100%;
+ background-color: #5793c9;
+ color: white;
+ padding: 14px 20px;
+ margin: 8px 0;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+input[type=submit]:hover {
+ background-color: #2a6193;
+}
+
+
+footer {
+ height: 3em;
+ border-top: #5793c9 3px solid;
+ margin-top: 1em;
+ display: flex;
+ flex-direction: column;
+ width: 100vw;
+}
+
+footer a:hover{
+ color: #5793c9;
+}
+
+.legal-links {
+ width: fit-content;
+ margin: auto;
+}
+
+.legal-links a {
+ margin: 0 1em;
+ font-size: 16px;
+}
+
+
+#mySidenav a {
+ position: absolute; /* Position them relative to the browser window */
+ position: fixed;
+ right: -80px; /* Position them outside of the screen */
+ transition: 0.3s; /* Add transition on hover */
+ padding: 15px; /* 15px padding */
+ width: 100px; /* Set a specific width */
+ text-decoration: none; /* Remove underline */
+ font-size: 20px; /* Increase font size */
+ color: white; /* White text color */
+ border-radius: 5px 0 0 5px; /* Rounded corners on the top right and bottom right side */
+ text-align: end;
+}
+
+#mySidenav a:hover {
+ right: 0; /* On mouse-over, make the elements appear as they should */
+}
+
+/* The about link: 20px from the top with a green background */
+#admin {
+ top: 15px;
+ background-color: #5793c9;
+}
+
+
+/* Style the Image Used to Trigger the Modal */
+#myImg {
+ border-radius: 5px;
+ box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
+ cursor: pointer;
+}
+
+/* On mouse-over, add a deeper shadow */
+#myImg:hover {
+ box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2);
+}
+
+
+
+.box2 {
+ width: 100%;
+ padding: 5px 0;
+ max-width: 600px;
+ margin: auto;
+}
+
+.box2:hover {
+ max-width: 800px;
+ font-size: 20pt;
+ transition: 1s;
+}
+
+.box3 {
+ max-width: 100%;
+}
+
+/* Add some padding inside the card container */
+.container {
+ padding: 2px 16px;
+ text-align: center;
+}
+
+.container_flex {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ max-width: 550px;
+ min-width: 120px;
+ background-color: #35374b;
+ margin: 0.5em auto;
+ padding: 0px;
+ color: #fff;
+ border-radius: 7px
+}
+
+.container_flex_row {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ width: 90%;
+ margin: 0.75em auto;
+ padding: 7px;
+ border-radius: 7px
+}
+
+.container_flex_row_2 {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-evenly;
+ padding: 0;
+ margin: auto;
+}
+
+.container_flex_row_bg {
+ background-color: #c3e2ff;
+ border-radius: 7px;
+ margin: 1em auto 1em auto;
+ width: 90%;
+ padding: 3px 3px 3px 3px;
+}
+
+.time_group {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ width: 300px;
+}
+
+.container_today {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: center;
+ max-width: 550px;
+ min-width: 90px;
+ background-color: #35374b;
+ margin: 0.75em auto;
+ padding: 7px;
+ color: #fff;
+ border-radius: 7px
+}
+
+.container_flex_title {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: center;
+ max-width: 550px;
+ background-color: #3478a2;
+ margin: 0.75em auto;
+ color: #fff;
+ border-radius: 7px
+}
+
+.container_flex_casual {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ max-width: 650px;
+ background-color: #35374b;
+ padding: 0 1em 0 1em;
+ color: #fff;
+ border-radius: 7px;
+ margin: 0 0.75em 0 0.75em;
+}
+
+.container_flex_casual_2 {
+ background-color: #35374b;
+ padding: 1em 1em 1em 1em;
+ color: #fff;
+ border-radius: 7px;
+ max-height: 100px;
+ justify-content: center;
+}
+
+.container_flex_casual_2 img {
+ display: block;
+ justify-content: center;
+ margin: auto;
+}
+
+.container_flex_time {
+ justify-content: center;
+ margin: auto;
+ background-color: #35374b;
+ margin: 5px;
+ font-size: 25;
+ padding: 12px 20px 12px 20px;
+ color: #fff;
+ border-radius: 7px;
+}
+
+.container_flex_time p {
+ margin: 0;
+}
+
+.container_flex_topbar {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ padding: 10px 10px 0 10px;
+ border-radius: 7px;
+}
+
+.vertical_spacer {
+ height: 100%;
+ margin: auto 0 auto 0;
+}
+
+.container_flex_2 {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ padding: 10px 0px 10px 0px;
+ border-radius: 7px;
+ margin: auto;
+}
+
+.container_flex_weather {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ padding: 10px 10px 10px 10px;
+ border-radius: 7px;
+}
+
+.container_flex_3 {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-evenly;
+ background-color: #3b3c4e;
+ width: 593px;
+ padding: 10px 10px 10px 10px;
+ color: #fff;
+ border-radius: 7px;
+
+}
+
+.container_flex_bg {
+ background-color: #c3e2ff;
+ border-radius: 7px;
+ padding: 3px 10px 3px 10px;
+}
+
+.item_flex_2 {
+ margin: 0.2em;
+}
+
+.item_flex_oben {
+ margin: 0.75em auto auto auto;
+}
+
+.item_flex_rechts {
+ margin: auto 0.2em auto auto;
+}
+
+.item_flex_links {
+ margin: auto auto auto 0.2em;
+}
+
+.opnv-icon {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 0 0 0 20px;
+}
+
+.temp-icon {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+.temp-icon-house {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 7px 0 7px 5px;
+}
+
+.temp-icon-today img {
+ display: block;
+ justify-content: center;
+ padding: 5px 5px 5px 5px;
+ margin: auto;
+}
+
+.temp-icon-house img {
+ display: block;
+ justify-content: center;
+ margin: auto 2em auto auto;
+}
+
+.temp-line {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ margin: 0 10 0 0px;
+}
+
+
+.item_flex.line {
+ max-width: 300px;
+ padding: 0px 10px 0px 10px;
+}
+
+.h2_test {
+ font-size:25px;
+ padding: 15px 0 0 0;
+}
+
+.item_flex.line p{
+ text-align: center;
+}
+
+.item_flex.time-remain{
+ text-align: center;
+ margin: 0 20px 0 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+.item_flex.time-remain *{
+ margin: 0;
+}
+table {
+ border-collapse: separate;
+ border-spacing: 0.2em;
+}
+
+th, td {
+ padding: .2em .5em;
+ border-radius: .1em;
+}
+
+
+
+td {
+ font-style: italic;
+ text-align: right;
+ box-shadow: inset 1px 3px 5px -3px rgb(0 0 0 / .5);
+}
+
+td:empty{
+ box-shadow: none;
+}
+
+tbody tr:nth-child(even) {
+ background-color: #e4ebf2;
+ color: #000;
+}
+
+.rechtsbuendig {
+ text-align: right;
+}
+
+.bosskoddes {
+ max-width: 100%;
+ height: auto;
+ display: none;
+}
diff --git a/web/intranet_repo/_recycle_bin/nak_logo.svg b/web/intranet_repo/_recycle_bin/nak_logo.svg
new file mode 100644
index 0000000..f9bae34
--- /dev/null
+++ b/web/intranet_repo/_recycle_bin/nak_logo.svg
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/_recycle_bin/seelsorge copy.css b/web/intranet_repo/_recycle_bin/seelsorge copy.css
new file mode 100644
index 0000000..055f23c
--- /dev/null
+++ b/web/intranet_repo/_recycle_bin/seelsorge copy.css
@@ -0,0 +1,774 @@
+/* Name: seelsorge.css
+ Beschreibung: Spezielles CSS für Seelsorge Planer */
+
+#seelsorge.menue {
+ position:relative;
+ top: 0px;
+ display: flex;
+ margin: 0px 0px 0px 0px;
+ height: fit-content;
+ width: 100%;
+ justify-content: space-evenly;
+ align-items: center;
+ background-color: transparent;
+ transition: top 0.9s ease-in-out;
+}
+#seelsorge.menue .list {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: left;
+ align-items: center;
+ margin: 0 1.8vw 0 1.8vw;
+ padding: 0px;
+ width: 100%;
+}
+
+#seelsorge.menue .menu-item:link {color: hsl(210, 60%, 50%);}
+#seelsorge.menue .menu-item:hover {color: #FEFEFE; opacity: 1.0; transition: top 0.6s ease-in-out;}
+#seelsorge.menue .active {
+ /*background-color: hsl(210, 80%, 35%) !important;*/
+ color: #FEFEFE !important;
+ font-weight: 400 !important;
+ /*box-shadow: 0 0 2px 1px rgba(255, 255, 255, 0.4),
+ 0 1px 12px 0 rgba(0, 0, 0, 0.5) !important;*/
+ opacity: 1.0 !important;
+}
+
+#seelsorge.menue .menu-item {
+ padding: 0px;
+ margin: 0px 0px 0px 30px;
+ display: block;
+ width: 175px;
+ max-width: 24vw;
+ height: 100%;
+ text-align: center;
+ /*border-radius: 9999px;*/
+ color: #DDDDDD;
+ font-size: 100%;
+ font-weight: 100;
+ cursor: pointer;
+ opacity: 0.9;
+
+ transition: background-color 0.5s ease;
+ /*box-shadow: 0 1px 1px 0 rgba(255, 255, 255, 0.3),
+ 0 1px 6px 0 rgba(0, 0, 0, 0.2);*/
+}
+#seelsorge.menue .indicator {
+ position: absolute;
+ left: 0px;
+ bottom: -3px;
+ width: 0px;
+ border-bottom: 2px solid #FEFEFE;
+ transition: width 0.9s ease-in-out, left 0.6s ease-in-out;
+}
+
+.content_seelsorge {
+ position: relative;
+ display: none; /* Ausblenden, wird per Javascript gesteuert */
+ justify-content: center;
+ justify-self: center;
+ padding: 5px;
+ margin-top: 0px;
+ width: calc(100% - 20px - 2px); /* Abzug von Padding+Border */
+ min-height: 100%;
+ border: 1px solid #DDDDDD;
+ border-radius: 5px;
+ background-color: hsl( 60, 33%, 100%);
+}
+.content .active {
+ display: flex;
+}
+
+p {font-size: 10pt;}
+ .long {text-align: justify;}
+ .up {text-transform: uppercase;}
+ .low {text-transform: lowercase;}
+ .capital {text-transform: capitalize;}
+
+table, div.table {
+ display: table;
+ padding: 0px;
+ margin: 0px;
+ align-items: center;
+ border: 0px solid #000000;
+}
+
+/* Spezielle Styles */
+div.title {
+ display: table;
+ -ms-flex: 100%;
+ flex: 100%;
+ margin: 5px;
+ background-color: transparent;
+ border-radius: 6px;
+ text-align: center;
+ box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.3);
+ background: linear-gradient(to top left,
+ hsl(60, 33%, 70%) 0%,
+ hsl(195, 33%, 70%) 100%);
+}
+p.title {
+ margin: 0px;
+ background-color: hsl(297, 50%, 20%);
+ -webkit-background-clip: text;
+ -moz-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ text-shadow: rgba(255,255,255,0.5) 0px 3px 3px;
+ font-size: 31pt;
+ font-weight: bold;
+}
+
+/* Wiederholbare Styles*/
+
+.hidden {
+ display: none;
+}
+.left {
+ text-align: left;
+}
+.right {
+ text-align : right;
+}
+
+img.header {
+ padding: 0px;
+ margin: 0px;
+ height: 100px;
+ width: auto;
+ max-height: 100px;
+ border: 0px solid rgba(0, 0, 0, 0.2);
+ border-radius: 5px;
+}
+
+/* div.menue {
+ -ms-flex: 80px;
+ flex: 80px;
+ flex-flow: column wrap;
+ padding: 15px 5px 5px 5px;
+ margin: 5px;
+ height: auto;
+ align-items: center;
+
+ border: 6px solid hsl(60, 33%, 90%) ;
+ border-radius: 6px;
+ background-color: hsl(60, 33%, 75%);
+ box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2),
+ 0 6px 20px 0 rgba(0, 0, 0, 0.3);
+
+ text-align: left;
+ font-family: Bahnschrift, Arial;
+ font-size: 12pt;
+ font-weight: bold;
+} */
+/* div.menue a:link {color: hsl(195, 60%, 50%);}
+div.menue a:visited {color: hsl(195, 60%, 50%);}
+div.menue a:hover {color: hsl(195, 100%, 100%);}
+div.menue a:active {color: hsl(195, 20%, 50%);}
+div.menue a.currentPage {background-color: hsl(208, 50%, 70%); color: hsl(195, 50%, 20%);}
+div.menue ul {
+ display: flex;
+ flex-flow: column wrap;
+
+ list-style-type: none;
+ width: 100%;
+ padding: 0px;
+ margin: 0px;
+} */
+/* div.menue ul a {
+ padding: 10px;
+ padding-left: 20px;
+ margin: 5px;
+ margin-left: 0px;
+ display: block;
+ width: auto;
+ height: auto;
+ border-radius: 20px;
+ text-decoration: none;
+ background-color: hsl(60, 33%, 100%);
+ border: 0px solid hsl(195, 0%, 80%);
+ transition: background-color 0.5s ease;
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.2), 0 1px 6px 0 rgba(0, 0, 0, 0.2);
+} */
+/* div.menue ul a:hover {
+ border: 0px solid hsl(60, 33%, 100%);
+ opacity: 1;
+ color: hsl(60, 50%, 80%);
+ background-color: hsl(208, 60%, 35%);
+ box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.4), 0 1px 12px 0 rgba(0, 0, 0, 0.4);
+
+}
+img.menue_bg {
+ width: 90px;
+ padding-top: 15px;
+ opacity: 0.7;
+} */
+
+/* Content-Styles: Output und Panels */
+/* div.content {
+ display: flex;
+ -ms-flex: 70%;
+ flex: 70%;
+ flex-flow: row wrap;
+ padding: 5px;
+ margin: 5px;
+ width: auto;
+ height: auto;
+ max-height: calc(100vh - 37px - 140px);
+ overflow: auto;
+ background-color: hsl(60, 33%, 90%);
+ border-radius: 6px;
+ box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.3);
+} */
+
+div.content_panel form {
+ padding: 0px;
+ padding-top: 0px;
+ margin: 0px;
+ margin-top: 10px;
+ /*overflow: auto;*/
+ width: calc(100% - 0px); /* Abzug von Abständen */
+ height: calc(100vh - 37px - 140px - 100px + 2px); /* Abzug von Abständen */
+ /*background-color: hsl( 60, 33%, 95%);*/
+}
+div.content_panel input {
+ /* margin-top: 5px; */
+ margin-left: 0px;
+ padding: 10px;
+ padding-left: 20px;
+ padding-right: 20px;
+ width: 120px;
+ color: hsl(60, 33%, 75%);
+ background-color: hsl(208, 50%, 30%);
+ border: 2px solid #AAA;
+ border-radius: 5px;
+ font-weight: bold;
+}
+
+div.content_output {
+ padding: 15px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ margin: 0px;
+ margin-top: 10px;
+ overflow: auto;
+ width: calc(100% - 31px); /* Abzug von Abständen */
+ max-height: calc(100vh - 37px - 140px - 100px); /* Abzug von Abständen */
+ background-color: hsl( 60, 33%, 95%);
+ border: 1px solid #DDDDDD;
+ border-radius: 4px;
+}
+div.content_output ul {
+ margin-left: 0px;
+ padding-left: 0px;
+}
+div.content_output li {
+ margin-left: 20px;
+ padding-left: 0px;
+}
+
+/*Pofilgalerie*/
+ul.galerie {
+ display: block;
+ padding:0px;
+ margin:10px;
+ width:auto;
+ list-style: none;
+}
+li.galerie {
+ position: relative;
+ float: left;
+ padding: 0px;
+ margin: 6px;
+ width: 122px;
+ height: auto;
+ border: 1px solid hsl(60, 33%, 50%);
+ border-radius: 6px;
+ /*background-color: #000;*/
+ opacity: 0.9;
+ background: linear-gradient(to top left,
+ hsl(297, 70%, 10%) 0%,
+ hsl(297, 60%, 40%) 30%,
+ hsl(297, 40%, 30%) 60%,
+ hsl(297, 70%, 30%) 100%
+ );
+}
+li.galerie:hover {
+ border: 1px solid hsl(60, 33%, 30%);
+ opacity: 1;
+
+ background: linear-gradient(to top left,
+ hsl(297, 70%, 20%) 0%,
+ hsl(297, 60%, 50%) 30%,
+ hsl(297, 40%, 40%) 60%,
+ hsl(297, 70%, 40%) 100%
+ );
+ z-index: 0;
+}
+div.galerie_green {
+ position: absolute;
+ left: 50%;
+ top: 66px;
+ transform: translate(-50%, -50%);
+ width: 100%;
+ height: 70%;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ border-bottom-left-radius: 50% 17%;
+ border-bottom-right-radius: 50% 17%;
+ opacity: 1;
+
+ background: radial-gradient(closest-corner at 30% 50%, hsl(146,70%,50%), hsl(146,70%,20%));
+
+ box-shadow: 0 0 2px 1px hsl(146,70%,50%), 0 3px 3px 1px hsl(146,70%,50%,0.7), 0 4px 6px hsl(146,70%,50%,0.5);
+
+ z-index: -3;
+}
+div.galerie_red {
+ position: absolute;
+ left: 50%;
+ top: 66px;
+ transform: translate(-50%, -50%);
+ width: 100%;
+ height: 70%;
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ border-bottom-left-radius: 50% 17%;
+ border-bottom-right-radius: 50% 17%;
+ opacity: 1;
+
+ background: radial-gradient(closest-corner at 10% 50%, hsl(360,70%,50%), hsl(360,70%,20%));
+
+ box-shadow: 0 0 2px 1px hsl(360,70%,50%), 0 3px 3px 1px hsl(360,70%,50%,0.7), 0 4px 6px hsl(360,70%,50%,0.5);
+
+ z-index: -3;
+}
+img.galerie_image {
+ padding: 0px;
+ margin: 0px;
+ width: 100%;
+ height: auto;
+ border: 0px solid hsl(60, 33%, 60%);
+ border-top-left-radius: 5px;
+ border-top-right-radius: 5px;
+ border-bottom-left-radius: 50% 17%;
+ border-bottom-right-radius: 50% 17%;
+
+ z-index: -2;
+}
+div.galerie_name {
+ display: table;
+ padding: 0px;
+ margin: 0px;
+ width: 100%;
+ height: 60px;
+ text-align: center;
+ font-size: 11pt;
+ font-weight: bold;
+
+ z-index: -1;
+}
+
+/* Profildetails
+--------------------------------------------------------------------------------
+ */
+div.profil_overlay {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ align-items: center;
+
+ background-color: hsl(195, 60%, 10%,0.9);
+ z-index: 7;
+}
+div.profil_container {
+ display: none;
+ flex-flow: row wrap;
+ justify-content: stretch;
+ align-items: stretch;
+ align-content: center;
+ position: absolute;
+ padding: 10px;
+ margin: 10px;
+ max-width: 60%;
+ border: 0px solid #DDDDDD;
+ background-color: transparent;
+ border-radius: 20px;
+ box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 12px 20px 0 rgba(0, 0, 0, 0.5);
+
+ z-index: -1;
+}
+div.profil_container a:link {color: hsl(297, 33%, 33%);}
+div.profil_container a:visited {color: hsl(297, 33%, 33%);}
+div.profil_container a:hover {color: hsl(297, 50%, 60%);}
+div.profil_container a:active {color: hsl(297, 50%, 60%);}
+div.profil_container_bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+
+ background-color: rgba(255, 255, 255, 0.4);
+ -webkit-backdrop-filter: blur(5px);
+ backdrop-filter: blur(5px);
+ border-radius: 20px;
+
+ z-index: -2;
+}
+.profil_name {
+ margin: 10px;
+ width: 100%;
+ text-align: left;
+ font-size: 16pt;
+ font-weight: bold;
+ color: hsl(195, 30%, 15%);
+ text-shadow: 1px 1px 2px #888888;
+}
+.profil_name_sub {
+ width: 100%;
+ text-align: left;
+ font-size: 12pt;
+ font-weight: bold;
+ color: hsl(297, 50%, 40%);
+}
+.profil_exit {
+ display: table;
+ position: absolute;
+ top: 15px;
+ right: 15px;
+ width: 30px;
+ height: 30px;
+
+ text-align: center;
+ font-size: 16pt;
+
+ background-color: #FFFFFF;
+ border: 0px solid #DDDDDD;
+ border-radius: 50%;
+ box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.2), 0 1px 3px 0 rgba(0, 0, 0, 0.4);
+}
+.profil_exit:hover {
+ cursor: pointer;
+ background-color: hsl(297, 70%, 40%, 0.7);
+}
+div.profil_content_small {
+ padding: 5px;
+ margin: 5px;
+ width: 250px;
+ text-align: center;
+
+ background-color: #FFFFFF;
+ border: 5px solid hsl(60, 33%, 80%);
+ border-radius: 10px;
+ box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.3), 0 3px 6px 0 rgba(0, 0, 0, 0.4);
+}
+div.profil_content_small img {
+ padding: 0px;
+ margin: 0px;
+ width: 100%;
+ border-radius: 3px;
+}
+div.profil_data {
+ -ms-flex: 50%;
+ flex: 50%;
+ padding: 30px;
+ margin: 5px;
+
+ border: 0px solid #DDDDDD;
+ background-color: hsl(60, 33%, 80%);
+ border-radius: 10px;
+ box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.3), 0 3px 6px 0 rgba(0, 0, 0, 0.4);
+}
+div.profil_data td {
+ padding-top: 2px;
+ padding-left: 5px;
+ padding-right: 5px;
+ padding-bottom: 2px;
+ margin: 0px;
+ width: auto;
+
+ border-spacing: 0px;
+ border-bottom: 1px solid #AAAAAA;
+
+}
+div.profil_calendar {
+ -ms-flex: 50%;
+ flex: 50%;
+ width: auto;
+ height: auto;
+ min-width: 600px;
+ padding: 5px;
+ margin: 5px;
+ background-color: #FFFFFF;
+ border: 1px solid #DDDDDD;
+ border-radius: 10px 10px 10px 10px;
+ box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.3), 0 3px 6px 0 rgba(0, 0, 0, 0.4);
+}
+
+/* Kalenderblatt*/
+.page_calendar {
+ position: relative;
+ display: none; /* Ausblenden, wird per Javascript gesteuert */
+ height: calc(100vh - 20px - 2px);
+ max-height: 750px;
+ max-width: 525px;
+ padding: 0px;
+ margin: 5px;
+ background-color: #FEFEFE;
+ border: 1px solid hsl(210, 0%, 90%);
+ border-radius: 10px;
+ box-shadow: 0px 1px 1px 0 rgba(0, 0, 0, 0.3), 0 3px 6px 0 rgba(0, 0, 0, 0.4);
+
+ aspect-ratio: 7/10;
+
+ /* Bild */
+ /*background-image: url('img/bg_sun.jpg');*/
+ background-repeat: no-repeat;
+ background-position: center;
+ background-size: cover;
+
+}
+.page_calendar .active { display: flex; }
+
+.page_calendar button {
+ position: absolute;
+ right: 5px;
+ top: 5px;
+ height: 30px;
+ border-radius: 5px;
+ border: 1px solid transparent;
+ background-color: hsl(297, 10%, 95%);
+ z-index: 10;
+}
+.page_calendar button:hover {
+ border: 1px solid hsl(297, 40%, 90%);
+ background-color: hsl(297, 80%, 95%);
+}
+.calendar_header {
+ position: relative;
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: center;
+ align-items: center;
+ height: 34px;
+ margin: 0px 0px 0px 0px;
+ padding: 5px 5px 5px 5px;
+ background: linear-gradient(to bottom,
+ hsl(210, 57%, 80%) 0%,
+ hsl(210, 57%, 97%) 90%);
+ border: 0px solid #DDDDDD;
+ border-bottom: 1px solid #DDDDDD;
+ border-top-left-radius: 9px;
+ border-top-right-radius: 9px;
+}
+.calendar_title {
+ position: relative;
+ height: fit-content;
+ width: 150px;
+ font-size: 14.0pt;
+ text-align: center;
+}
+.arrow_item {
+ padding: 5px;
+ border-radius: 5px;
+ border: 1px solid transparent;
+ background-color: transparent;
+}
+.arrow_item:hover {
+ border: 1px solid hsla(297, 0%, 50%, 0.5);
+ background-color: hsl(297, 0%, 90%, 0.5);
+}
+.arrow_item:hover svg path { fill: hsl(210, 0%, 100%); }
+.arrow_light svg path { fill: hsl(210, 30%, 70%); }
+.arrow_dark svg path { fill: hsl(210, 100%, 30%); }
+
+.calendar_content {
+ position: relative;
+ display: flex;
+ flex-flow: column nowrap;
+ justify-content: flex-start;
+ padding: 0px;
+ margin: 0px;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+
+ font-size: 1.0em;
+ font-weight: 600;
+
+ border-spacing: 0px;
+ border: 0px solid #DDDDDD;
+ border-radius: 10px;
+}
+.calendar_content table {
+ padding: 0px;
+ margin: 0px;
+ width: 100%;
+ height: 100%;
+ /* border-spacing: 9px; */
+}
+.calendar_content thead {
+ width: 100%;
+ height: 100%;
+ /* border-spacing: 9px; */
+}
+.calendar_content table td {
+ text-align: center;
+ vertical-align: middle;
+}
+td.calendar_head {
+ padding-bottom: 5px;
+ border-bottom: 2px solid hsl(195, 40%, 70%);
+}
+td.calendar_head_sat {
+ padding-bottom: 10px;
+ border-bottom: 2px solid hsl(297, 10%, 70%);
+}
+td.calendar_head_sun {
+ padding-bottom: 10px;
+ border-bottom: 2px solid hsl(297, 80%, 70%);
+}
+td.calendar_day_area {
+ align-content: baseline;
+ padding-top: 2px;
+ width: 100%;
+}
+td.calendar_day_area:hover {
+ cursor: default;
+}
+td.calendar_day_area a {
+ text-decoration: none;
+}
+#calendar_day_item:hover, #calendar_day_item *:hover {
+ color: hsl(195, 0%, 90%);
+ background-color: hsl(195, 80%, 60%);
+ box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.3);
+}
+.calendar_day_item {
+ width: auto;
+ height: 100%;
+ aspect-ratio: 1/1;
+ vertical-align: middle;
+ border-radius: 100%;
+ box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.3);
+}
+ /*Kombi mit -> calendar_day_item*/
+ .availableday {
+ color: #FFFFFF;
+ background-color: hsl(132, 100%, 27%);
+ }
+ .normalday {
+ background-color: hsl(297, 20%, 100%);
+ }
+ .today {
+ color: #FFFFFF;
+ background-color: hsl(195, 100%, 30%);
+ box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.6);
+ }
+ /*td.today:hover {
+ color: hsl(195, 0%, 90%);
+ background-color: hsl(195, 80%, 60%);
+ box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.3);
+ } */
+ .saturday {
+ color: #FFFFFF;
+ background-color: #555555;
+ }
+ .sunday { /* auch für gesetzliche Feiertage in Berlin */
+ color: #FFFFFF;
+ background-color: hsl(297, 80%, 70%);
+ }
+ .localholiday { /* sonstige regionale Feiertage */
+ background-color: hsl(297, 20%, 70%);
+ }
+div.calendar_day {
+ display: flex;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ justify-content: stretch;
+ align-items: center;
+ align-content: center;
+ height: 60px;
+ border: 0px solid #000;
+}
+div.feiertagsname {
+ position: absolute;
+ bottom: 0px;
+ display: flex;
+ -ms-flex-direction: column;
+ flex-direction: column;
+ justify-content: stretch;
+ padding: 0px;
+ margin-top: 0px;
+ font-size: 7.5pt;
+ text-align: center;
+ width: 100%;
+ color: hsl(297, 30%, 50%);
+ border: 1px solid hsl(297, 30%, 90%);
+ z-index:5;
+}
+
+/* Verfügbarkeit: Kalendertag */
+td.greenbox {
+ box-shadow: 4px 4px 4px hsl(146,50%,50%),
+ 4px -4px 4px hsl(146,70%,30%),
+ -4px 4px 4px hsl(146,30%,70%),
+ -4px -4px 4px hsl(146,50%,30%);
+}
+
+@media (max-width: 750px) {
+ #seelsorge.menue .list {
+ justify-content: space-evenly;
+ }
+ #seelsorge.menue .menu-item {
+ padding: 5px 2vw 5px 2vw;
+ margin: 1vw;
+ max-width: 24vw;
+ font-size: 80%;
+ }
+ .content_seelsorge {
+ padding: 0px;
+ margin-top: 0px;
+ width: calc(100% - 0px - 0px); /* Abzug von Padding+Border */
+ border: 0px solid #DDDDDD;
+ border-radius: 5px;
+ background-color: transparent;
+ }
+ .page_calendar {
+ width: calc(100vw - 30px - 2px);
+ max-width: 700px;
+
+ padding: 0px;
+ margin: 0px;
+ transform: scale(0.9);
+ }
+}
+@media (max-width: 650px) {
+ .page_calendar {
+ width: calc(100vw - 10px - 2px);
+
+ max-width: 600px;
+ transform: scale(0.8);
+ }
+}
+@media (max-width: 550px) {
+ .page_calendar {
+ max-height: 550px;
+ max-width: 550px;
+ transform: scale(0.9);
+ }
+}
+@media (max-width: 475px) {
+ .page_calendar {
+
+ max-height: 475px;
+ max-width: 475px;
+
+ transform: scale(0.6);
+ }
+}
\ No newline at end of file
diff --git a/web/intranet_repo/_templates/template.php b/web/intranet_repo/_templates/template.php
new file mode 100644
index 0000000..5062c8e
--- /dev/null
+++ b/web/intranet_repo/_templates/template.php
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/_templates/template_admin.php b/web/intranet_repo/_templates/template_admin.php
new file mode 100644
index 0000000..739c032
--- /dev/null
+++ b/web/intranet_repo/_templates/template_admin.php
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+ query($sql);
+ $isAdmin = $result->fetch(PDO::FETCH_ASSOC);
+
+ // Check if the user has admin access (Admin = 1)
+ if ($isAdmin["Admin"] == 1) {
+ $sql_groups = "SELECT * FROM user_permissions";
+ $stmt = $pdo->prepare($sql_groups);
+ $stmt->execute();
+ $groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ } else {
+ header("Location: {$g_httpHomePath}");
+ exit();
+ }
+ ?>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/design-guidelines.md b/web/intranet_repo/design-guidelines.md
new file mode 100644
index 0000000..c468eaf
--- /dev/null
+++ b/web/intranet_repo/design-guidelines.md
@@ -0,0 +1,130 @@
+# NAK Intranet – Design-Richtlinien
+## Basierend auf dem CD-Manual NAK 2026
+
+---
+
+## 1. Farben (Digital-Palette)
+
+| Farbe | HEX-Wert | Verwendung |
+|-----------------|-----------|---------------------------------------|
+| NAK Blau | `#2786cf` | Primärfarbe, Buttons, Links, Header |
+| Schwarz | `#000000` | Texte, Icons |
+| Sonnengelb | `#ffdc0f` | Akzente, Warnungen |
+| Orange | `#f08200` | Akzente |
+| Rot | `#d21400` | Fehlermeldungen, Warnstufen |
+| Himbeere | `#d73282` | Akzente |
+| Lila | `#731e7d` | Seelsorge-Bereich |
+| Dunkelblau | `#1243a0` | Akzente |
+| Türkisblau | `#00a091` | Akzente |
+| Dunkelgrün | `#006e14` | Entwickler-Bereich, Erfolg |
+| Gelbgrün | `#82be23` | Akzente |
+
+### Neutrale Farben (Graustufen)
+- Hintergrund: `#f8f9fa` (Gray-50)
+- Karten/Panels: `#ffffff`
+- Trennlinien: `#e9ecef` (Gray-200)
+- Sekundärer Text: `#6c757d` (Gray-600)
+- Primärer Text: `#212529` (Gray-900)
+
+---
+
+## 2. Typografie
+
+- **Hausschrift**: Suisse Int'l (als woff2 eingebunden)
+ - Light (300) – Fließtext
+ - Medium (500) – Überschriften, Labels
+ - Semibold (600) – Buttons, Hervorhebungen
+- **Fallback**: Arial, sans-serif
+
+### Schriftgrößen
+| Token | Größe |
+|-------------|-----------|
+| xs | 0.75rem |
+| sm | 0.875rem |
+| base | 1rem |
+| md | 1.125rem |
+| lg | 1.25rem |
+| xl | 1.5rem |
+| 2xl | 2rem |
+| 3xl | 2.5rem |
+
+---
+
+## 3. Formsprache: Eckiges Design
+
+Das CD-Manual schreibt eine **eckige Formsprache** vor:
+- **Keine abgerundeten Ecken** (border-radius: 0) für UI-Elemente
+- Ausnahme: Profilbilder (kreisförmig, border-radius: 50%)
+- Ausnahme: Pillentags/Badges (border-radius: 9999px)
+- Klare, geradlinige Kanten
+- Scharfe Konturen bei Karten, Buttons, Eingabefeldern, Modals
+
+---
+
+## 4. Logo – Wort-Bild-Marke
+
+- Das offizielle NAK-Logo wird immer zusammen mit dem Schriftzug verwendet
+- Anordnung: Logo-Symbol links, daneben:
+ - Zeile 1: **Neuapostolische Kirche** (Medium, 500)
+ - Zeile 2: *Berlin-Reinickendorf* (Light, 300)
+- Mindestgröße: Logo-Symbol 44px (Desktop), 36px (Mobile)
+- Das Logo darf nicht verzerrt, eingefärbt oder manipuliert werden
+
+---
+
+## 5. Header
+
+- Weißer Hintergrund (`#ffffff`)
+- 3px NAK-Blau-Linie am unteren Rand
+- Logo + Wordmark links
+- Profilbild rechts (kreisrund)
+ - **Desktop**: Hover → Name + Abmelden ausfahren
+ - **Mobile**: Klick → Name + Abmelden ausfahren
+- Sticky mit Hide-on-scroll-down, Show-on-scroll-up
+
+---
+
+## 6. Footer
+
+- Weißer Hintergrund
+- 2px NAK-Blau-Linie oben
+- Zentrierte Links: Impressum | Admin kontaktieren | Datenschutz
+- Kein Release-Marker (entfernt)
+
+---
+
+## 7. Hauptmenü (Intranet Startseite)
+
+- **Desktop**: Volle Bildschirmbreite, Grid-Layout
+- **Mobile**: Zentriert, angepasstes Grid (2 Spalten)
+- Buttons: Eckig, mit Schatten, Hover-Effekt (leichte Vergrößerung)
+- Farbige Buttons gemäß Bereichszugehörigkeit (Blau, Lila, Rot, Schwarz, Grün)
+
+---
+
+## 8. Authorization-Seiten (Login, Registrierung, Passwort-Reset)
+
+- Zentriertes Card-Layout
+- NAK-Logo mit Wordmark oben
+- Eckige Eingabefelder
+- Eckige Buttons (Pillform für primäre Actions beibehalten als Ausnahme)
+- Hintergrund: Leichter Gradient (Gray-50 → NAK-Blau-Light)
+
+---
+
+## 9. Profil-Seiten
+
+- Zentriertes Layout, max. 600px Breite
+- Großes, kreisrundes Profilbild mit Hover-Effekt
+- Eckige Eingabefelder und Karten
+- Klare Gruppenübersicht
+
+---
+
+## 10. Allgemeine Regeln
+
+- **Schatten**: Dezent, mehrstufig (sm, md, lg, xl)
+- **Übergänge**: 150ms (schnell), 250ms (standard), 400ms (langsam)
+- **Abstände**: Konsistentes Spacing-System (0.25rem bis 4rem)
+- **Responsive**: Mobile-first, Breakpoints bei 480px und 768px
+- **Barrierefreiheit**: Focus-Styles beibehalten, ausreichender Kontrast
diff --git a/web/intranet_repo/general_files/ini/database.php b/web/intranet_repo/general_files/ini/database.php
new file mode 100644
index 0000000..f79b6b1
--- /dev/null
+++ b/web/intranet_repo/general_files/ini/database.php
@@ -0,0 +1,27 @@
+ PDO::ERRMODE_EXCEPTION,
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
+ PDO::ATTR_EMULATE_PREPARES => false,
+ ]
+ );
+} catch (PDOException $e) {
+ error_log('Database connection failed: ' . $e->getMessage());
+ http_response_code(500);
+ exit('Database connection failed.');
+}
+
+return $pdo;
diff --git a/web/intranet_repo/general_files/ini/functions.php b/web/intranet_repo/general_files/ini/functions.php
new file mode 100644
index 0000000..c93453f
--- /dev/null
+++ b/web/intranet_repo/general_files/ini/functions.php
@@ -0,0 +1,124 @@
+";
+ echo file_get_contents($p_file);
+ if ($p_text) echo $p_text;
+ echo "";
+ }
+}
+
+
+/** Infopanel mit einer zweispaltigen Tabelle
+ * @param string $title Überschrift des Panels
+ * @param array $data_array Strukturdaten mit Schlüssel und Wert
+ * @return string Die formatierte Zeichenkette.
+ */
+function infopanel_table($title, $data_array) {
+ // Ausgabe des Panels mit ZWEI Spalten
+ echo '';
+ echo '
';
+ echo ' ';
+ echo ' ';
+ echo ' ';
+ echo ' ' . $title . ' ';
+ echo ' ';
+ echo ' ';
+ echo ' ';
+ echo ' ';
+
+ foreach ($data_array as $key => $value) {
+
+ echo ' ';
+ echo ' ' . $key . ' ';
+ echo ' ' . nvl($value, '---') . ' ';
+ echo ' ';
+ }
+ echo ' ';
+ echo '
';
+ echo '
';
+}
+
+
+/** Markdown-Formatierung für Schnelleingaben
+ * @param string $text Die zu formatierende Zeichenkette.
+ * @return string Die formatierte Zeichenkette.
+ */
+function markdownFormat($text) {
+ $text = nl2br(rtrim($text, "\n")); // Umbrüche formatieren ( )
+
+ $pattern = "/(\::::)( .+)/";
+ $replacement = "$2 "; // :::: {Einrücken 2. Ebene}
+ $text = preg_replace($pattern, $replacement, $text);
+
+ $pattern = "/(\::)( .+)/";
+ $replacement = "$2 "; // :: {Einrücken 1. Ebene}
+ $text = preg_replace($pattern, $replacement, $text);
+
+ $pattern = "/(\-\-)( .+)/";
+ $replacement = "$2 "; // -- {Listenelement 2. Ebene}
+ $text = preg_replace($pattern, $replacement, $text);
+
+ $pattern = "/(\-)( .+)/";
+ $replacement = "$2 "; // - {Listenelement 1. Ebene}
+ $text = preg_replace($pattern, $replacement, $text);
+
+ $text = preg_replace("/\*\*(.*?)\*\*/s", "$1 ", $text); // **{Fett}**
+ $text = preg_replace("/\*(.*?)\*/s", "$1 ", $text); // *{Kursiv}*
+
+ $text = ""; // Listenumschluss
+
+ return $text;
+}
+
+
+ /////////////////////////////////////////
+ // EIGENE PRÜFFUNKTIONEN //
+/////////////////////////////////////////
+
+
+/** Checken einer Zeichenkette auf das Vorhandensein.
+ * @param string|null $value Die zu überprüfende Zeichenkette.
+ * @param mixed $default Der Ersatzwert, der zurückgegeben wird, wenn die Zeichenkette leer ist.
+ * @return mixed Der ursprüngliche Wert oder der Ersatzwert.
+ */
+function nvl(string $value = null, $default) {
+ return isset($value) && $value !== '' ? $value : $default;
+}
+
+
+/** Gegenderte Anzeige eines Rollennamens (Datenbankwert → Anzeige).
+ * @param string $role Der Rollenname aus der Datenbank
+ * @return string Der gegenderte Anzeigename
+ */
+function genderRole(string $role): string {
+ static $map = [
+ 'Admin' => 'Admin',
+ 'Technik' => 'Technik',
+ 'Entwickler' => 'Entwickler*in',
+ 'Amtsgeschwister' => 'Amtsgeschwister',
+ 'Diakon' => 'Diakon*in',
+ 'DiakonIn' => 'Diakon*in',
+ 'Priester' => 'Priester*in',
+ 'PriesterIn' => 'Priester*in',
+ 'Brandschutz' => 'Brandschutz',
+ 'Brandschutzbeauftragter' => 'Brandschutzbeauftrage*r',
+ ];
+ return $map[$role] ?? $role;
+}
+?>
\ No newline at end of file
diff --git a/web/intranet_repo/general_files/ini/parameters.php b/web/intranet_repo/general_files/ini/parameters.php
new file mode 100644
index 0000000..4adb7cf
--- /dev/null
+++ b/web/intranet_repo/general_files/ini/parameters.php
@@ -0,0 +1,80 @@
+ $p_project,
+ 'Gemeinde' => $p_nac_base,
+ 'Version' => $p_version,
+ 'Status' => $p_status,
+ ];
+ $array_dev = [
+ 'Host' => $_SERVER['HTTP_HOST'],
+ 'IPv4-Adresse' => $_SERVER['SERVER_ADDR'],
+ 'PHP-Version' => phpversion(),
+ 'Server-Software' => $_SERVER['SERVER_SOFTWARE'],
+ 'Betriebssystem' => php_uname('s'),
+ 'Zeitzone' => date_default_timezone_get(),
+ 'Locale-Einstellung' => $g_locale,
+ 'Aktivierte PHP-Erweiterungen' => implode(', ', get_loaded_extensions()),
+ 'Maximale Upload-Dateigröße' => ini_get('upload_max_filesize'),
+ 'Speicherlimit für PHP-Skripte' => ini_get('memory_limit'),
+ ];
+
+
+ // Prefixe
+ $isHttps = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on')
+ || (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
+ && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https');
+ $g_httpPrefix = ($isHttps ? 'https' : 'http') . '://';
+
+ // Absolute Pfade
+ $g_generalFilePath = $_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/";
+ $g_projectsPath = $_SERVER['DOCUMENT_ROOT']."/intranet_repo/projects/";
+
+ $g_httpCurrentUrl = $g_httpPrefix.$publicHost.$_SERVER['REQUEST_URI'];
+ $g_httpCurrentCleanUrl = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
+ $g_httpCurrentUrl = $g_httpPrefix.$publicHost.$_SERVER['REQUEST_URI'];
+ $g_httpHomePath = $g_httpPrefix.$publicHost."/intranet_repo/projects/intranet/index.php";
+ $g_httpLoginPath = $g_httpPrefix.$publicHost."/intranet_repo/projects/authorization/login.php";
+ $g_httpLogoutPath = $g_httpPrefix.$publicHost."/intranet_repo/projects/authorization/logout.php";
+
+
+ $g_imagePath = $g_projectsPath."_central/img/";
+ $g_imageButtonPath = $g_projectsPath."_central/img/picto/";
+ $g_phpSnippetsPath = $g_projectsPath."_central/php/snippets/";
+ $g_profilePicturePath = $g_generalFilePath."profile_pictures/";
+
+ // Relative Pfade
+ $g_imageRelativePath = "/intranet_repo/projects/_central/img/";
+ $g_imageButtonRelativePath = "/intranet_repo/projects/_central/img/picto/";
+ $g_profilePictureRelativePath = "/intranet_repo/general_files/profile_pictures/";
+
+
+ // Buttons verwalten (SVG-Grafiken)
+ $p_buttons = array(
+ 'add' => $g_imageButtonPath.'add.svg',
+ 'arrow_left' => $g_imageButtonPath.'arrow_left.svg',
+ 'arrow_right' => $g_imageButtonPath.'arrow_right.svg',
+ 'arrow_up' => $g_imageButtonPath.'arrow_up.svg',
+ 'cancel' => $g_imageButtonPath.'cancel.svg',
+ 'edit' => $g_imageButtonPath.'edit.svg',
+ 'calendar' => $g_imageButtonPath.'calendar.svg'
+ );
+?>
diff --git a/web/intranet_repo/general_files/ini/runtime_env.php b/web/intranet_repo/general_files/ini/runtime_env.php
new file mode 100644
index 0000000..b27d465
--- /dev/null
+++ b/web/intranet_repo/general_files/ini/runtime_env.php
@@ -0,0 +1,14 @@
+
+
+ ' . htmlspecialchars($buttonText) . '
+
+ ';
+ }
+
+ // Inhalt: Zeilenumbrüche in umwandeln, aber vorhandenes HTML beibehalten
+ $inhaltHtml = nl2br(htmlspecialchars($inhalt));
+
+ $datum = date('d.m.Y, H:i') . ' Uhr';
+
+ // Logo-URL dynamisch ermitteln
+ $host = $_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN');
+ $logoUrl = 'https://' . $host . '/intranet_repo/projects/_central/img/nak-logo.svg';
+
+ return '
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Neuapostolische Kirche
+ Gemeinde Reinickendorf
+
+
+
+
+
+ ' . htmlspecialchars($betreff) . '
+
+
+
+ ' . $inhaltHtml . '
+
+ ' . $buttonHtml . '
+
+
+
+ Gesendet am ' . $datum . '
+ Intranet NAK Reinickendorf · Automatische Benachrichtigung
+
+
+
+
+
+
+';
+}
+
+/**
+ * Sendet eine HTML-E-Mail über SMTP.
+ *
+ * @param string $empfaenger Empfänger
+ * @param string $betreff Betreff
+ * @param string $nachricht Klartext-Nachricht (wird ins HTML-Template eingebettet)
+ * @param string|null $cc CC-Adresse
+ * @param string $absender Absender
+ * @param string|null $buttonText Optionaler Button-Text
+ * @param string|null $buttonUrl Optionale Button-URL
+ * @return bool
+ */
+function send_intranet_mail(
+ string $empfaenger,
+ string $betreff,
+ string $nachricht,
+ ?string $cc = null,
+ string $absender = "intranet@nak-reinickendorf.de",
+ ?string $buttonText = null,
+ ?string $buttonUrl = null
+): bool {
+ $htmlBody = build_html_mail($betreff, $nachricht, $buttonText, $buttonUrl);
+
+ // MIME-Header
+ $boundary = 'boundary_' . bin2hex(random_bytes(16));
+ $msgId = '<' . bin2hex(random_bytes(16)) . '@nak-reinickendorf.de>';
+ $headers = "From: Intranet NAK Reinickendorf <{$absender}>\r\n";
+ $headers .= "Reply-To: {$absender}\r\n";
+ $headers .= "To: {$empfaenger}\r\n";
+ if (!empty($cc)) {
+ $headers .= "Cc: {$cc}\r\n";
+ }
+ $headers .= "Subject: =?UTF-8?B?" . base64_encode($betreff) . "?=\r\n";
+ $headers .= "Message-ID: {$msgId}\r\n";
+ $headers .= "MIME-Version: 1.0\r\n";
+ $headers .= "Content-Type: multipart/alternative; boundary=\"{$boundary}\"\r\n";
+ $headers .= "X-Mailer: Intranet-NAK-Reinickendorf/1.0\r\n";
+ $headers .= "Date: " . date('r') . "\r\n";
+
+ // Plaintext-Fallback
+ $plainBody = strip_tags($nachricht);
+
+ // Multipart Body
+ $body = "--{$boundary}\r\n";
+ $body .= "Content-Type: text/plain; charset=utf-8\r\n";
+ $body .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
+ $body .= $plainBody . "\r\n\r\n";
+ $body .= "--{$boundary}\r\n";
+ $body .= "Content-Type: text/html; charset=utf-8\r\n";
+ $body .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
+ $body .= $htmlBody . "\r\n\r\n";
+ $body .= "--{$boundary}--\r\n";
+
+ $message = $headers . "\r\n" . $body;
+
+ $recipients = [$empfaenger];
+ if (!empty($cc)) {
+ $recipients[] = $cc;
+ }
+
+ return smtp_send(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, $absender, $recipients, $message);
+}
+
+/**
+ * Direkter SMTP-Versand mit STARTTLS und AUTH LOGIN.
+ */
+function smtp_send(string $host, int $port, string $user, string $pass, string $from, array $to, string $data): bool {
+ $errno = 0;
+ $errstr = '';
+ $socket = @stream_socket_client("tcp://{$host}:{$port}", $errno, $errstr, 30);
+ if (!$socket) {
+ error_log("SMTP connect failed: {$errstr} ({$errno})");
+ return false;
+ }
+ stream_set_timeout($socket, 30);
+
+ $response = smtp_read($socket);
+ if (substr($response, 0, 3) !== '220') { fclose($socket); return false; }
+
+ smtp_write($socket, "EHLO " . gethostname());
+ $response = smtp_read($socket);
+ if (substr($response, 0, 3) !== '250') { fclose($socket); return false; }
+
+ smtp_write($socket, "STARTTLS");
+ $response = smtp_read($socket);
+ if (substr($response, 0, 3) !== '220') { fclose($socket); return false; }
+
+ $crypto = stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT);
+ if (!$crypto) {
+ error_log("SMTP STARTTLS failed");
+ fclose($socket);
+ return false;
+ }
+
+ smtp_write($socket, "EHLO " . gethostname());
+ $response = smtp_read($socket);
+ if (substr($response, 0, 3) !== '250') { fclose($socket); return false; }
+
+ smtp_write($socket, "AUTH LOGIN");
+ $response = smtp_read($socket);
+ if (substr($response, 0, 3) !== '334') { fclose($socket); return false; }
+
+ smtp_write($socket, base64_encode($user));
+ $response = smtp_read($socket);
+ if (substr($response, 0, 3) !== '334') { fclose($socket); return false; }
+
+ smtp_write($socket, base64_encode($pass));
+ $response = smtp_read($socket);
+ if (substr($response, 0, 3) !== '235') {
+ error_log("SMTP auth failed: {$response}");
+ fclose($socket);
+ return false;
+ }
+
+ smtp_write($socket, "MAIL FROM:<{$from}>");
+ $response = smtp_read($socket);
+ if (substr($response, 0, 3) !== '250') { fclose($socket); return false; }
+
+ foreach ($to as $recipient) {
+ smtp_write($socket, "RCPT TO:<{$recipient}>");
+ $response = smtp_read($socket);
+ if (substr($response, 0, 3) !== '250') { fclose($socket); return false; }
+ }
+
+ smtp_write($socket, "DATA");
+ $response = smtp_read($socket);
+ if (substr($response, 0, 3) !== '354') { fclose($socket); return false; }
+
+ $data = str_replace("\r\n.", "\r\n..", $data);
+ fwrite($socket, $data . "\r\n.\r\n");
+ $response = smtp_read($socket);
+ if (substr($response, 0, 3) !== '250') { fclose($socket); return false; }
+
+ smtp_write($socket, "QUIT");
+ smtp_read($socket);
+ fclose($socket);
+
+ return true;
+}
+
+function smtp_write($socket, string $cmd): void {
+ fwrite($socket, $cmd . "\r\n");
+}
+
+function smtp_read($socket): string {
+ $response = '';
+ while ($line = fgets($socket, 512)) {
+ $response .= $line;
+ if (isset($line[3]) && $line[3] === ' ') break;
+ if (strlen($line) < 4) break;
+ }
+ return $response;
+}
+
+/**
+ * Ermittelt alle E-Mail-Adressen von Nutzern, die BEIDE angegebenen Rollen besitzen.
+ * Benötigt eine bestehende $pdo-Verbindung.
+ *
+ * @param PDO $pdo Datenbankverbindung
+ * @param array $roles Array von Rollennamen, die alle erfüllt sein müssen
+ * @return array Liste der E-Mail-Adressen
+ */
+function get_emails_by_roles(PDO $pdo, array $roles): array {
+ if (empty($roles)) return [];
+
+ $conditions = [];
+ foreach ($roles as $role) {
+ $conditions[] = '"' . str_replace('"', '""', $role) . '" = 1';
+ }
+ $where = implode(' AND ', $conditions);
+
+ $sql = "SELECT ugi.mail FROM user_permissions up
+ INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id
+ WHERE {$where} AND ugi.mail IS NOT NULL AND ugi.mail != ''";
+ $result = $pdo->query($sql);
+ $emails = [];
+ if ($result) {
+ while ($row = $result->fetch()) {
+ $emails[] = $row['mail'];
+ }
+ }
+ return $emails;
+}
+
+/**
+ * Sendet eine Mail an alle Nutzer mit den Rollen Admin UND Technik (Sammelmail).
+ *
+ * @param PDO $pdo Datenbankverbindung
+ * @param string $betreff Betreff
+ * @param string $nachricht Nachricht
+ * @param string|null $buttonText Optionaler Button-Text
+ * @param string|null $buttonUrl Optionale Button-URL
+ * @return bool True wenn mindestens eine Mail gesendet wurde
+ */
+function send_admin_team_mail(
+ PDO $pdo,
+ string $betreff,
+ string $nachricht,
+ ?string $buttonText = null,
+ ?string $buttonUrl = null
+): bool {
+ $emails = get_emails_by_roles($pdo, ['Admin', 'Technik']);
+ if (empty($emails)) {
+ // Fallback auf ADMIN_EMAIL
+ return send_intranet_mail(ADMIN_EMAIL, $betreff, $nachricht, null, "intranet@nak-reinickendorf.de", $buttonText, $buttonUrl);
+ }
+ $success = false;
+ foreach ($emails as $email) {
+ if (send_intranet_mail($email, $betreff, $nachricht, null, "intranet@nak-reinickendorf.de", $buttonText, $buttonUrl)) {
+ $success = true;
+ }
+ }
+ return $success;
+}
+
+/**
+ * Sendet eine Mail an alle Nutzer mit der Rolle Brandschutzbeauftragter.
+ *
+ * @param PDO $pdo Datenbankverbindung
+ * @param string $betreff Betreff
+ * @param string $nachricht Nachricht
+ * @param string|null $buttonText Optionaler Button-Text
+ * @param string|null $buttonUrl Optionale Button-URL
+ * @return bool True wenn mindestens eine Mail gesendet wurde
+ */
+function send_brandschutzbeauftragter_mail(
+ PDO $pdo,
+ string $betreff,
+ string $nachricht,
+ ?string $buttonText = null,
+ ?string $buttonUrl = null
+): bool {
+ $emails = get_emails_by_roles($pdo, ['Brandschutzbeauftragter']);
+ if (empty($emails)) return false;
+ $success = false;
+ foreach ($emails as $email) {
+ if (send_intranet_mail($email, $betreff, $nachricht, null, "intranet@nak-reinickendorf.de", $buttonText, $buttonUrl)) {
+ $success = true;
+ }
+ }
+ return $success;
+}
+?>
diff --git a/web/intranet_repo/general_files/initialise.php b/web/intranet_repo/general_files/initialise.php
new file mode 100644
index 0000000..6b62867
--- /dev/null
+++ b/web/intranet_repo/general_files/initialise.php
@@ -0,0 +1,42 @@
+prepare("SELECT * FROM user_general_information WHERE user_id = ?");
+ $stmt->execute([$_SESSION["user_id"]]);
+ $user = $stmt->fetch();
+
+ // Authentication
+ if (isset($user["mail"])) {
+ $stmt_groups = $pdo->prepare("SELECT * FROM user_permissions WHERE user_id = ?");
+ $stmt_groups->execute([$_SESSION["user_id"]]);
+ $groups = $stmt_groups->fetch();
+ }
+
+ } else {
+ header("Location: {$g_httpLoginPath}");
+ exit();
+ }
+?>
\ No newline at end of file
diff --git a/web/intranet_repo/general_files/profile_pictures/47-2024.jpg b/web/intranet_repo/general_files/profile_pictures/47-2024.jpg
new file mode 100644
index 0000000..fd24fbe
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/47-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/48-2024.jpg b/web/intranet_repo/general_files/profile_pictures/48-2024.jpg
new file mode 100644
index 0000000..dfa30e5
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/48-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/50-20241019070946.jpg b/web/intranet_repo/general_files/profile_pictures/50-20241019070946.jpg
new file mode 100644
index 0000000..4598a45
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/50-20241019070946.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/52-2024.jpg b/web/intranet_repo/general_files/profile_pictures/52-2024.jpg
new file mode 100644
index 0000000..51d5769
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/52-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/54-2024.jpg b/web/intranet_repo/general_files/profile_pictures/54-2024.jpg
new file mode 100644
index 0000000..1c280ad
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/54-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/56-2024.jpg b/web/intranet_repo/general_files/profile_pictures/56-2024.jpg
new file mode 100644
index 0000000..9fb4e85
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/56-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/58-2024.jpg b/web/intranet_repo/general_files/profile_pictures/58-2024.jpg
new file mode 100644
index 0000000..c9001f9
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/58-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/6-20260520160955.jpg b/web/intranet_repo/general_files/profile_pictures/6-20260520160955.jpg
new file mode 100644
index 0000000..3a97d7e
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/6-20260520160955.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/61-2026.jpg b/web/intranet_repo/general_files/profile_pictures/61-2026.jpg
new file mode 100644
index 0000000..e1774b1
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/61-2026.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/62-2024.jpg b/web/intranet_repo/general_files/profile_pictures/62-2024.jpg
new file mode 100644
index 0000000..3b9a0b8
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/62-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/63-2024.jpg b/web/intranet_repo/general_files/profile_pictures/63-2024.jpg
new file mode 100644
index 0000000..6e83e5e
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/63-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/64-2024.jpg b/web/intranet_repo/general_files/profile_pictures/64-2024.jpg
new file mode 100644
index 0000000..c197ba3
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/64-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/67-2024.jpg b/web/intranet_repo/general_files/profile_pictures/67-2024.jpg
new file mode 100644
index 0000000..ee5a577
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/67-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/68-2024.jpg b/web/intranet_repo/general_files/profile_pictures/68-2024.jpg
new file mode 100644
index 0000000..c94f148
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/68-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/69-2024.jpg b/web/intranet_repo/general_files/profile_pictures/69-2024.jpg
new file mode 100644
index 0000000..e0a5012
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/69-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/70-2024.jpg b/web/intranet_repo/general_files/profile_pictures/70-2024.jpg
new file mode 100644
index 0000000..afbd842
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/70-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/71-2024.jpg b/web/intranet_repo/general_files/profile_pictures/71-2024.jpg
new file mode 100644
index 0000000..2e6e802
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/71-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/72-2024.jpg b/web/intranet_repo/general_files/profile_pictures/72-2024.jpg
new file mode 100644
index 0000000..18d674b
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/72-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/8-20240506143203.jpg b/web/intranet_repo/general_files/profile_pictures/8-20240506143203.jpg
new file mode 100644
index 0000000..ed314d9
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/8-20240506143203.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/82-2024.jpg b/web/intranet_repo/general_files/profile_pictures/82-2024.jpg
new file mode 100644
index 0000000..f84a9a1
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/82-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/85-2024.jpg b/web/intranet_repo/general_files/profile_pictures/85-2024.jpg
new file mode 100644
index 0000000..3b9a0b8
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/85-2024.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/88-20260520161354.jpg b/web/intranet_repo/general_files/profile_pictures/88-20260520161354.jpg
new file mode 100644
index 0000000..9707c85
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/88-20260520161354.jpg differ
diff --git a/web/intranet_repo/general_files/profile_pictures/default.jpg b/web/intranet_repo/general_files/profile_pictures/default.jpg
new file mode 100644
index 0000000..bfb8d80
Binary files /dev/null and b/web/intranet_repo/general_files/profile_pictures/default.jpg differ
diff --git a/web/intranet_repo/projects/_central/fonts/SuisseIntl-Light.woff2 b/web/intranet_repo/projects/_central/fonts/SuisseIntl-Light.woff2
new file mode 100644
index 0000000..d0c3ed0
Binary files /dev/null and b/web/intranet_repo/projects/_central/fonts/SuisseIntl-Light.woff2 differ
diff --git a/web/intranet_repo/projects/_central/fonts/SuisseIntl-LightIt.woff2 b/web/intranet_repo/projects/_central/fonts/SuisseIntl-LightIt.woff2
new file mode 100644
index 0000000..d5ffb98
Binary files /dev/null and b/web/intranet_repo/projects/_central/fonts/SuisseIntl-LightIt.woff2 differ
diff --git a/web/intranet_repo/projects/_central/fonts/SuisseIntl-Medium.woff2 b/web/intranet_repo/projects/_central/fonts/SuisseIntl-Medium.woff2
new file mode 100644
index 0000000..d1a4f04
Binary files /dev/null and b/web/intranet_repo/projects/_central/fonts/SuisseIntl-Medium.woff2 differ
diff --git a/web/intranet_repo/projects/_central/fonts/SuisseIntl-Semibold.woff2 b/web/intranet_repo/projects/_central/fonts/SuisseIntl-Semibold.woff2
new file mode 100644
index 0000000..7e18ddc
Binary files /dev/null and b/web/intranet_repo/projects/_central/fonts/SuisseIntl-Semibold.woff2 differ
diff --git a/web/intranet_repo/projects/_central/img/favicon.ico b/web/intranet_repo/projects/_central/img/favicon.ico
new file mode 100644
index 0000000..4b5ad66
Binary files /dev/null and b/web/intranet_repo/projects/_central/img/favicon.ico differ
diff --git a/web/intranet_repo/projects/_central/img/intranet.svg b/web/intranet_repo/projects/_central/img/intranet.svg
new file mode 100644
index 0000000..067a97c
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/intranet.svg
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/_central/img/nak-logo.svg b/web/intranet_repo/projects/_central/img/nak-logo.svg
new file mode 100644
index 0000000..4ebe92f
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/nak-logo.svg
@@ -0,0 +1,865 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/add.svg b/web/intranet_repo/projects/_central/img/picto/add.svg
new file mode 100644
index 0000000..1d7c839
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/add.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/arrow_left.svg b/web/intranet_repo/projects/_central/img/picto/arrow_left.svg
new file mode 100644
index 0000000..c287167
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/arrow_left.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/arrow_right.svg b/web/intranet_repo/projects/_central/img/picto/arrow_right.svg
new file mode 100644
index 0000000..cfb3af3
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/arrow_right.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/arrow_up.svg b/web/intranet_repo/projects/_central/img/picto/arrow_up.svg
new file mode 100644
index 0000000..faf4c06
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/arrow_up.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/calendar.svg b/web/intranet_repo/projects/_central/img/picto/calendar.svg
new file mode 100644
index 0000000..a06768f
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/calendar.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/cancel.svg b/web/intranet_repo/projects/_central/img/picto/cancel.svg
new file mode 100644
index 0000000..16f11c9
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/cancel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/development.svg b/web/intranet_repo/projects/_central/img/picto/development.svg
new file mode 100644
index 0000000..68f831c
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/development.svg
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/edit.svg b/web/intranet_repo/projects/_central/img/picto/edit.svg
new file mode 100644
index 0000000..8b081c0
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/edit.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/folder_green.svg b/web/intranet_repo/projects/_central/img/picto/folder_green.svg
new file mode 100644
index 0000000..b51ce9d
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/folder_green.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/_central/img/picto/folder_grey.svg b/web/intranet_repo/projects/_central/img/picto/folder_grey.svg
new file mode 100644
index 0000000..ab0866b
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/folder_grey.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/_central/img/picto/folder_orange.svg b/web/intranet_repo/projects/_central/img/picto/folder_orange.svg
new file mode 100644
index 0000000..a8a9e64
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/folder_orange.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/_central/img/picto/folder_red.svg b/web/intranet_repo/projects/_central/img/picto/folder_red.svg
new file mode 100644
index 0000000..6f2f767
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/folder_red.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/_central/img/picto/folder_yellow.svg b/web/intranet_repo/projects/_central/img/picto/folder_yellow.svg
new file mode 100644
index 0000000..aa90890
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/folder_yellow.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/_central/img/picto/gear.svg b/web/intranet_repo/projects/_central/img/picto/gear.svg
new file mode 100644
index 0000000..1fc1ec6
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/gear.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/logout.svg b/web/intranet_repo/projects/_central/img/picto/logout.svg
new file mode 100644
index 0000000..a7c4681
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/logout.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/pdf_extern.svg b/web/intranet_repo/projects/_central/img/picto/pdf_extern.svg
new file mode 100644
index 0000000..10ccab0
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/pdf_extern.svg
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/reload.svg b/web/intranet_repo/projects/_central/img/picto/reload.svg
new file mode 100644
index 0000000..bdf76f9
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/reload.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/img/picto/video.svg b/web/intranet_repo/projects/_central/img/picto/video.svg
new file mode 100644
index 0000000..079efdc
--- /dev/null
+++ b/web/intranet_repo/projects/_central/img/picto/video.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/layouts/nak-design.css b/web/intranet_repo/projects/_central/layouts/nak-design.css
new file mode 100644
index 0000000..0e4bfcc
--- /dev/null
+++ b/web/intranet_repo/projects/_central/layouts/nak-design.css
@@ -0,0 +1,2591 @@
+/* ==========================================================================
+ NAK Intranet – Corporate Design Stylesheet
+ Basierend auf dem CD-Manual NAK 2026
+ Gemeinde: Berlin-Reinickendorf
+ ========================================================================== */
+
+/* --------------------------------------------------------------------------
+ 0. FONT FACES – Suisse Int'l (Hausschrift der NAK)
+ -------------------------------------------------------------------------- */
+ @font-face {
+ font-family: 'Suisse Intl';
+ src: url('/intranet_repo/projects/_central/fonts/SuisseIntl-Light.woff2') format('woff2');
+ font-weight: 300;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Suisse Intl';
+ src: url('/intranet_repo/projects/_central/fonts/SuisseIntl-LightIt.woff2') format('woff2');
+ font-weight: 300;
+ font-style: italic;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Suisse Intl';
+ src: url('/intranet_repo/projects/_central/fonts/SuisseIntl-Medium.woff2') format('woff2');
+ font-weight: 500;
+ font-style: normal;
+ font-display: swap;
+}
+@font-face {
+ font-family: 'Suisse Intl';
+ src: url('/intranet_repo/projects/_central/fonts/SuisseIntl-Semibold.woff2') format('woff2');
+ font-weight: 600;
+ font-style: normal;
+ font-display: swap;
+}
+
+/* --------------------------------------------------------------------------
+ 1. CSS CUSTOM PROPERTIES (Design Tokens)
+ -------------------------------------------------------------------------- */
+:root {
+ /* NAK Primärfarben – Digital */
+ --nak-blue: #2786cf;
+ --nak-blue-90: rgba(39, 134, 207, 0.9);
+ --nak-blue-80: rgba(39, 134, 207, 0.8);
+ --nak-blue-70: rgba(39, 134, 207, 0.7);
+ --nak-blue-60: rgba(39, 134, 207, 0.6);
+ --nak-blue-50: rgba(39, 134, 207, 0.5);
+ --nak-blue-40: rgba(39, 134, 207, 0.4);
+ --nak-blue-30: rgba(39, 134, 207, 0.3);
+ --nak-blue-20: rgba(39, 134, 207, 0.2);
+ --nak-blue-10: rgba(39, 134, 207, 0.1);
+ --nak-blue-light: #e8f4fd;
+ --nak-blue-hover: #1e6fad;
+ --nak-black: #000000;
+
+ /* NAK Sekundärfarben – Digital */
+ --nak-yellow: #ffdc0f;
+ --nak-orange: #f08200;
+ --nak-red: #d21400;
+ --nak-raspberry: #d73282;
+ --nak-purple: #731e7d;
+ --nak-darkblue: #1243a0;
+ --nak-teal: #00a091;
+ --nak-darkgreen: #006e14;
+ --nak-lime: #82be23;
+
+ /* Neutrale Farben */
+ --gray-50: #f8f9fa;
+ --gray-100: #f1f3f5;
+ --gray-200: #e9ecef;
+ --gray-300: #dee2e6;
+ --gray-400: #ced4da;
+ --gray-500: #adb5bd;
+ --gray-600: #6c757d;
+ --gray-700: #495057;
+ --gray-800: #343a40;
+ --gray-900: #212529;
+
+ /* Typografie */
+ --font-primary: 'Suisse Intl', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
+ --font-size-xs: 0.75rem;
+ --font-size-sm: 0.875rem;
+ --font-size-base: 1rem;
+ --font-size-md: 1.125rem;
+ --font-size-lg: 1.25rem;
+ --font-size-xl: 1.5rem;
+ --font-size-2xl: 2rem;
+ --font-size-3xl: 2.5rem;
+
+ /* Abstände */
+ --space-xs: 0.25rem;
+ --space-sm: 0.5rem;
+ --space-md: 1rem;
+ --space-lg: 1.5rem;
+ --space-xl: 2rem;
+ --space-2xl: 3rem;
+ --space-3xl: 4rem;
+
+ /* Radien */
+ --radius-sm: 0;
+ --radius-md: 0;
+ --radius-lg: 0;
+ --radius-xl: 0;
+ --radius-full: 9999px;
+
+ /* Schatten */
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
+ --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12);
+ --shadow-xl: 0 20px 60px rgba(0, 0, 0, 0.15);
+
+ /* Layout */
+ --header-height: 84px;
+ --header-title-height: 48px;
+ --footer-height: 40px;
+ --content-max-width: 1200px;
+ --sidebar-width: 280px;
+
+ /* Transitions */
+ --transition-fast: 150ms ease;
+ --transition-base: 250ms ease;
+ --transition-slow: 400ms ease;
+
+ --theme-color: var(--nak-blue);
+ --theme-hover: var(--nak-blue-hover);
+ --theme-shadow: rgba(39, 134, 207, 0.3);
+ --theme-focus: rgba(39, 134, 207, 0.2);
+ --theme-light: var(--nak-blue-light);
+}
+
+/* --------------------------------------------------------------------------
+ 2. RESET & BASE
+ -------------------------------------------------------------------------- */
+*, *::before, *::after {
+ box-sizing: border-box;
+ font-family: inherit;
+}
+
+html {
+ scroll-behavior: smooth;
+ -webkit-text-size-adjust: 100%;
+ overflow-x: hidden;
+ scrollbar-width: none; /* Firefox */
+ -ms-overflow-style: none; /* IE/Edge */
+}
+html::-webkit-scrollbar {
+ display: none; /* Chrome, Safari, Opera */
+}
+
+body, html {
+ margin: 0;
+ padding: 0;
+ min-height: 100vh;
+ max-width: 100vw;
+ background-color: var(--gray-50);
+}
+
+body {
+ font-family: var(--font-primary);
+ font-size: var(--font-size-base);
+ font-weight: 300;
+ line-height: 1.6;
+ color: var(--gray-900);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ display: flex;
+ flex-direction: column;
+ padding-bottom: var(--footer-height);
+ overflow: visible;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-weight: 500;
+ line-height: 1.3;
+ color: var(--gray-900);
+ margin-top: 0;
+}
+
+h1 { font-size: var(--font-size-3xl); margin-bottom: var(--space-lg); }
+h2 { font-size: var(--font-size-2xl); margin-bottom: var(--space-md); }
+h3 { font-size: var(--font-size-xl); margin-bottom: var(--space-md); }
+h4 { font-size: var(--font-size-lg); margin-bottom: var(--space-sm); color: var(--nak-blue); }
+h5 { font-size: var(--font-size-md); margin-bottom: var(--space-sm); color: var(--gray-600); }
+h6 { font-size: var(--font-size-base); margin-bottom: var(--space-sm); color: var(--gray-700); }
+
+a {
+ color: var(--theme-color, var(--nak-blue));
+ text-decoration: none;
+ transition: color var(--transition-fast);
+}
+a:hover {
+ color: var(--theme-hover, var(--nak-blue-hover));
+ text-decoration: none;
+}
+a:visited {
+ color: var(--theme-color, var(--nak-blue));
+}
+
+p {
+ margin: 0 0 var(--space-md) 0;
+}
+
+img {
+ max-width: 100%;
+ height: auto;
+}
+
+/* --------------------------------------------------------------------------
+ 3. HEADER
+ -------------------------------------------------------------------------- */
+header {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ z-index: 1000;
+ margin: 0;
+ padding: 0;
+}
+
+.header_menue {
+ position: fixed;
+ top: 0;
+ left: 0;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ height: var(--header-height);
+ width: 100%;
+ box-sizing: border-box;
+ background: #ffffff;
+ border-bottom: 3px solid var(--nak-blue);
+ box-shadow: var(--shadow-md);
+ transition: top 0.3s ease-in-out;
+ z-index: 1001;
+ padding: 0 var(--space-lg);
+}
+
+.header_menue .nak-logo-link {
+ display: flex;
+ align-items: flex-end;
+ height: 100%;
+ gap: var(--space-md);
+ text-decoration: none;
+ cursor: pointer;
+ padding-bottom: 12px;
+}
+
+.header_menue .nak-logo-link:hover {
+ text-decoration: none;
+}
+
+.header_menue .nak-logo-icon {
+ height: 62px;
+ width: 62px;
+ flex-shrink: 0;
+}
+
+.header_menue .nak-logo-text {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ line-height: 1.2;
+}
+
+.header_menue .nak-logo-text .nak-name {
+ font-family: var(--font-primary);
+ font-weight: 500;
+ font-size: 1rem;
+ color: var(--gray-900);
+ white-space: nowrap;
+}
+
+.header_menue .nak-logo-text .nak-gemeinde {
+ font-family: var(--font-primary);
+ font-weight: 300;
+ font-size: var(--font-size-sm);
+ color: var(--gray-600);
+ white-space: nowrap;
+}
+
+/* Legacy logo support (hide old SVG logo) */
+.header_menue img.intranet {
+ display: none;
+}
+
+/* Profile Widget in Header */
+.header_profile {
+ position: relative;
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: center;
+ height: 56px;
+ max-width: 56px;
+ border-radius: 9999px;
+ overflow: hidden;
+ background-color: transparent;
+ box-shadow: none;
+ transition: max-width var(--transition-base), background-color var(--transition-base), box-shadow var(--transition-base), border-radius var(--transition-base), padding var(--transition-base);
+ z-index: 1005;
+ cursor: pointer;
+}
+
+.header_profile img {
+ width: 56px;
+ min-width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ border: 2px solid var(--gray-200);
+ transition: border-color var(--transition-fast);
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.header_profile img:hover {
+ border-color: var(--nak-blue);
+}
+
+.header_profile_slideLeft {
+ max-width: 300px;
+ padding-right: 0;
+ padding-left: var(--space-md);
+ background-color: #ffffff;
+ box-shadow: var(--shadow-lg);
+ border-radius: 28px;
+ border: 1px solid var(--gray-200);
+}
+
+/* Desktop: Profile expands on hover */
+@media (min-width: 769px) {
+ .header_profile:hover {
+ max-width: 300px;
+ padding-left: var(--space-md);
+ background-color: #ffffff;
+ box-shadow: var(--shadow-lg);
+ border: 1px solid var(--gray-200);
+ border-radius: 28px;
+ }
+}
+
+.header_profile_bar {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 6px;
+ padding: 0 var(--space-sm);
+ height: 100%;
+ white-space: nowrap;
+ transition: opacity var(--transition-base), color var(--transition-fast);
+ cursor: pointer;
+ font-size: var(--font-size-xs);
+ font-weight: 500;
+ color: var(--gray-600);
+}
+
+.header_profile_bar:hover {
+ color: var(--nak-blue);
+}
+
+.header_profile_bar svg {
+ flex-shrink: 0;
+}
+
+/* --------------------------------------------------------------------------
+ 4. HEADER TITLE BAR (Seitentitel)
+ -------------------------------------------------------------------------- */
+.header_title {
+ position: fixed;
+ top: var(--header-height);
+ display: flex;
+ align-items: center;
+ width: 100%;
+ height: var(--header-title-height);
+ background: linear-gradient(135deg, var(--nak-blue) 0%, #1e6fad 100%);
+ box-shadow: 0 2px 8px rgba(39, 134, 207, 0.3);
+ transition: top 0.3s ease-in-out;
+ z-index: 999;
+ padding: 0 var(--space-lg);
+ border: none;
+ border-radius: 0;
+}
+
+.header_title_text {
+ font-size: var(--font-size-md);
+ font-weight: 500;
+ color: #ffffff;
+ margin: 0;
+}
+
+/* Title bar Variants */
+.ht_admin {
+ background: linear-gradient(135deg, var(--gray-800) 0%, var(--gray-900) 100%) !important;
+}
+.ht_dev {
+ background: linear-gradient(135deg, var(--nak-darkgreen) 0%, #005a10 100%) !important;
+}
+.ht_pastoral {
+ background: linear-gradient(135deg, var(--nak-purple) 0%, #5c1865 100%) !important;
+}
+.ht_kalli {
+ background: linear-gradient(135deg, var(--nak-blue) 0%, var(--nak-blue-hover) 100%) !important;
+}
+.ht_orange {
+ background: linear-gradient(135deg, var(--nak-orange) 0%, #d47200 100%) !important;
+}
+.ht_raspberry {
+ background: linear-gradient(135deg, var(--nak-raspberry) 0%, #b8296e 100%) !important;
+}
+.ht_teal {
+ background: linear-gradient(135deg, var(--nak-teal) 0%, #008577 100%) !important;
+}
+.ht_darkblue {
+ background: linear-gradient(135deg, var(--nak-darkblue) 0%, #0e3580 100%) !important;
+}
+.ht_lime {
+ background: linear-gradient(135deg, var(--nak-lime) 0%, #6fa31e 100%) !important;
+}
+
+/* Per-project theme color overrides */
+.theme-blue { --theme-color: var(--nak-blue); --theme-hover: #1e6fad; --theme-shadow: rgba(39,134,207,0.3); --theme-focus: rgba(39,134,207,0.2); --theme-light: #e8f4fd; }
+.theme-purple { --theme-color: var(--nak-purple); --theme-hover: #5c1865; --theme-shadow: rgba(115,30,125,0.3); --theme-focus: rgba(115,30,125,0.2); --theme-light: #f3e6f5; }
+.theme-admin { --theme-color: var(--gray-800); --theme-hover: var(--gray-900); --theme-shadow: rgba(0,0,0,0.2); --theme-focus: rgba(0,0,0,0.15); --theme-light: #eceef0; }
+.theme-green { --theme-color: var(--nak-darkgreen); --theme-hover: #005a10; --theme-shadow: rgba(0,110,20,0.3); --theme-focus: rgba(0,110,20,0.2); --theme-light: #e4f5e7; }
+.theme-teal { --theme-color: var(--nak-teal); --theme-hover: #008577; --theme-shadow: rgba(0,160,145,0.3); --theme-focus: rgba(0,160,145,0.2); --theme-light: #e2f5f3; }
+.theme-orange { --theme-color: var(--nak-orange); --theme-hover: #d47200; --theme-shadow: rgba(240,130,0,0.3); --theme-focus: rgba(240,130,0,0.2); --theme-light: #fef0e0; }
+.theme-raspberry { --theme-color: var(--nak-raspberry); --theme-hover: #b8296e; --theme-shadow: rgba(215,50,130,0.3); --theme-focus: rgba(215,50,130,0.2); --theme-light: #fce8f2; }
+.theme-darkblue{ --theme-color: var(--nak-darkblue); --theme-hover: #0e3580; --theme-shadow: rgba(18,67,160,0.3); --theme-focus: rgba(18,67,160,0.2); --theme-light: #e4eaf6; }
+.theme-lime { --theme-color: var(--nak-lime); --theme-hover: #6fa31e; --theme-shadow: rgba(130,190,35,0.3); --theme-focus: rgba(130,190,35,0.2); --theme-light: #f1f8e4; }
+
+/* Apply theme color to interactive elements inside themed containers */
+[class*="theme-"] .content input.button,
+[class*="theme-"] .content input.button_fullwidth,
+[class*="theme-"] .button_fullwidth {
+ background: var(--theme-color);
+}
+[class*="theme-"] .content input.button:hover,
+[class*="theme-"] .content input.button_fullwidth:hover,
+[class*="theme-"] .button_fullwidth:hover {
+ background: var(--theme-hover);
+}
+[class*="theme-"] .content input.field_fullwidth:focus,
+[class*="theme-"] .content input.field:focus,
+[class*="theme-"] .phone-input:focus {
+ border-color: var(--theme-color);
+ box-shadow: 0 0 0 3px var(--theme-focus);
+}
+[class*="theme-"] .header_title {
+ background: linear-gradient(135deg, var(--theme-color) 0%, var(--theme-hover) 100%);
+ box-shadow: 0 2px 8px var(--theme-shadow);
+}
+
+/* --------------------------------------------------------------------------
+ 5. CONTENT AREA
+ -------------------------------------------------------------------------- */
+.content {
+ width: 100%;
+ box-sizing: border-box;
+}
+
+#base.content {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: 0;
+ padding-bottom: var(--footer-height);
+ margin: calc(var(--header-height) + var(--header-title-height)) 0 calc(-1 * var(--footer-height)) 0;
+ width: 100%;
+ box-sizing: border-box;
+ min-height: calc(100vh - var(--header-height) - var(--header-title-height));
+ background: linear-gradient(135deg, var(--gray-50) 0%, var(--theme-light, var(--nak-blue-light)) 100%);
+ z-index: unset;
+}
+
+#base.content > .content-tab-bar {
+ width: 100%;
+ flex-shrink: 0;
+}
+
+/* Pages without tab-bar get padding via their inner wrappers (.profile, .kalli-wizard, .content_flexcontainer) */
+
+#fullsize.content {
+ position: relative;
+ top: 0;
+ margin-top: calc(var(--header-height) + var(--header-title-height));
+ margin-bottom: calc(-1 * var(--footer-height));
+ padding: 0;
+ padding-bottom: var(--footer-height);
+ min-height: calc(100vh - var(--header-height) - var(--header-title-height));
+ width: 100%;
+ box-sizing: border-box;
+ border: none;
+ border-radius: 0;
+ overflow: visible;
+ background: linear-gradient(135deg, var(--gray-50) 0%, var(--theme-light, var(--nak-blue-light)) 100%);
+ z-index: unset;
+}
+
+#visit.content {
+ position: relative;
+ top: 0;
+ margin-top: calc(var(--header-height) + var(--header-title-height) + var(--header-title-height));
+ margin-bottom: calc(-1 * var(--footer-height));
+ padding: 0;
+ padding-bottom: var(--footer-height);
+ min-height: calc(100vh - var(--header-height) - var(--header-title-height) - var(--header-title-height));
+ width: 100%;
+ box-sizing: border-box;
+ border: none;
+ border-radius: 0;
+ overflow: visible;
+ background: linear-gradient(135deg, var(--gray-50) 0%, var(--theme-light, var(--nak-blue-light)) 100%);
+ z-index: 1;
+}
+
+.content_panel {
+ position: relative;
+ padding: var(--space-xl);
+ width: 100%;
+ min-height: 100%;
+ border: 1px solid var(--gray-200);
+ border-radius: 0;
+ background: #ffffff;
+ box-shadow: var(--shadow-sm);
+}
+
+/* Content Tab Bar – einheitliche Reiternavigation im Content-Bereich */
+.content-tab-bar {
+ display: flex;
+ align-items: stretch;
+ gap: 0;
+ width: 100%;
+ background: #ffffff;
+ border-bottom: 1px solid var(--gray-200);
+ box-shadow: 0 2px 4px rgba(0,0,0,0.04);
+ padding: 0 var(--space-lg);
+ box-sizing: border-box;
+ position: relative;
+ z-index: 90;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+.content-tab-bar__item {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 14px 20px;
+ font-family: var(--font-primary);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ color: var(--gray-500);
+ text-decoration: none;
+ cursor: pointer;
+ border: none;
+ background: transparent;
+ white-space: nowrap;
+ flex-shrink: 0;
+ transition: color var(--transition-fast);
+}
+.content-tab-bar__item:hover {
+ color: var(--gray-800);
+ text-decoration: none;
+}
+.content-tab-bar__item.active {
+ color: var(--gray-900);
+ font-weight: 600;
+}
+.content-tab-bar__item.active::after {
+ content: '';
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background: var(--theme-color, var(--nak-blue));
+}
+.content-tab-bar__badge {
+ min-width: 18px;
+ height: 18px;
+ padding: 0 5px;
+ border-radius: 9999px;
+ background: var(--nak-red);
+ color: #FFF;
+ font-size: 0.72em;
+ font-weight: 900;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+@media (max-width: 480px) {
+ .content-tab-bar {
+ padding: 0 var(--space-sm);
+ }
+ .content-tab-bar__item {
+ padding: 12px 14px;
+ font-size: var(--font-size-xs);
+ }
+ .profile-modal-hero {
+ grid-template-columns: 72px minmax(0, 1fr);
+ gap: 12px;
+ padding: 18px 16px;
+ }
+ .profile-modal-img {
+ width: 68px;
+ height: 68px;
+ }
+ .profile-modal-title {
+ font-size: 1.1em;
+ margin-right: 40px;
+ }
+ .profile-modal-body {
+ padding: 16px;
+ }
+}
+
+/* Tab content panels (for #base pages like profile) */
+.profile-tab-content {
+ display: none;
+ width: 100%;
+ padding: var(--space-lg);
+ box-sizing: border-box;
+}
+.profile-tab-content.active {
+ display: block;
+}
+
+/* --------------------------------------------------------------------------
+ 6. FOOTER
+ -------------------------------------------------------------------------- */
+footer {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 40px;
+ min-height: 40px;
+ max-height: 40px;
+ padding: 0 var(--space-lg);
+ margin-top: auto;
+ background: #ffffff;
+ border-top: 2px solid var(--nak-blue);
+ transition: all var(--transition-base);
+ overflow: hidden;
+ flex-shrink: 0;
+ z-index: 1000;
+}
+
+.release {
+ position: absolute;
+ top: -20px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2px 16px;
+ height: 20px;
+ font-size: var(--font-size-xs);
+ font-weight: 300;
+ color: #ffffff;
+ border-radius: 0;
+ white-space: nowrap;
+}
+
+.legal-links {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: var(--space-lg);
+ flex-wrap: wrap;
+}
+
+.legal-links a {
+ color: var(--gray-600);
+ text-decoration: none;
+ font-size: var(--font-size-sm);
+ font-weight: 300;
+ transition: color var(--transition-fast);
+}
+
+.legal-links a:hover {
+ color: var(--theme-color, var(--nak-blue));
+ text-decoration: none;
+}
+
+/* --------------------------------------------------------------------------
+ 7. MAIN PAGE (Intranet Hauptmenü)
+ -------------------------------------------------------------------------- */
+.mainpage {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ padding-top: calc(var(--header-height) + var(--header-title-height) + var(--space-sm));
+ padding-bottom: calc(var(--footer-height) + var(--space-lg));
+ width: 100%;
+ min-height: 100vh;
+ margin-bottom: calc(-1 * var(--footer-height));
+ overflow-x: hidden;
+ background: linear-gradient(135deg, var(--gray-50) 0%, var(--theme-light, var(--nak-blue-light)) 100%);
+}
+
+.mainpage_container {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(180px, 1fr));
+ grid-auto-rows: auto;
+ gap: var(--space-lg);
+ padding: var(--space-lg) var(--space-3xl);
+ margin: 0 auto;
+ max-width: 900px;
+ width: 100%;
+ justify-items: stretch;
+}
+
+/* Meldungen ausschließlich im Hauptmenü */
+.mainpage_notice {
+ grid-column: 1 / -1;
+ width: 100%;
+ box-sizing: border-box;
+ padding: 16px 20px;
+ border-left: 4px solid var(--nak-blue);
+ background: #ffffff;
+ box-shadow: var(--shadow-sm);
+}
+
+.mainpage_notice_title {
+ margin-bottom: 4px;
+ font-size: var(--font-size-base);
+ font-weight: 700;
+ color: var(--gray-900);
+}
+
+.mainpage_notice_text {
+ font-size: var(--font-size-sm);
+ line-height: 1.5;
+ color: var(--gray-700);
+}
+
+.mainpage_notice--info {
+ border-left-color: var(--nak-blue);
+ background: hsl(210, 57%, 97%);
+}
+
+.mainpage_notice--warning {
+ border-left-color: var(--nak-orange);
+ background: hsl(34, 90%, 96%);
+}
+
+.mainpage_notice--success {
+ border-left-color: var(--nak-darkgreen);
+ background: hsl(145, 50%, 96%);
+}
+
+.mainpage_notice--error {
+ border-left-color: var(--nak-red);
+ background: hsl(352, 72%, 97%);
+}
+
+.button_mainpage {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-sm);
+ padding: var(--space-xl) var(--space-lg);
+ min-height: 120px;
+ cursor: pointer;
+ color: #ffffff;
+ background: var(--nak-blue);
+ border: none;
+ border-radius: 0;
+ box-shadow: var(--shadow-md);
+ transition: transform var(--transition-fast), box-shadow var(--transition-fast);
+ overflow: hidden;
+ width: 100%;
+}
+
+.button_mainpage:hover {
+ transform: translateY(-4px) scale(1.02);
+ box-shadow: var(--shadow-xl);
+}
+
+.button_mainpage:active {
+ transform: translateY(-1px) scale(0.98);
+}
+
+.button_mainpage::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
+ transition: left 0.5s ease;
+}
+
+.button_mainpage:hover::before {
+ left: 100%;
+}
+
+.button_mainpage .text {
+ font-size: var(--font-size-md);
+ font-weight: 500;
+ text-align: center;
+ line-height: 1.3;
+}
+
+.button_mainpage svg {
+ fill: #ffffff;
+ width: 32px;
+ height: 32px;
+ opacity: 0.9;
+}
+
+#spin.button_mainpage:hover svg {
+ animation: spin 4s linear infinite;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Button color variants */
+.bg_blue { background: var(--nak-blue); }
+.bg_blue:hover { background: var(--nak-blue-hover); }
+.bg_darkblue { background: var(--nak-darkblue); }
+.bg_darkblue:hover { background: #0e3580; }
+.bg_purple { background: var(--nak-purple); }
+.bg_purple:hover { background: #5c1865; }
+.bg_orange { background: var(--nak-orange); }
+.bg_orange:hover { background: #d47200; }
+.bg_red { background: var(--nak-red); }
+.bg_red:hover { background: #b01000; }
+.bg_black { background: var(--gray-800); }
+.bg_black:hover { background: var(--gray-900); }
+.bg_green { background: var(--nak-darkgreen); }
+.bg_green:hover { background: #005a10; }
+.bg_teal { background: var(--nak-teal); }
+.bg_teal:hover { background: #008577; }
+.bg_raspberry { background: var(--nak-raspberry); }
+.bg_raspberry:hover { background: #b8296e; }
+.bg_lime { background: var(--nak-lime); }
+.bg_lime:hover { background: #6fa31e; }
+
+/* --------------------------------------------------------------------------
+ 8. PROFILE PAGES
+ -------------------------------------------------------------------------- */
+.profile {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ max-width: 600px;
+ margin: 0 auto;
+ padding: var(--space-xl) 0;
+}
+
+.profile img {
+ display: block;
+ margin: 0 auto var(--space-lg) auto;
+ width: 160px;
+ height: 160px;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ border-radius: 50%;
+ border: 4px solid #ffffff;
+ box-shadow: 0 4px 20px rgba(39, 134, 207, 0.2);
+ transition: transform var(--transition-base), box-shadow var(--transition-base);
+}
+
+.profile img:hover {
+ transform: scale(1.05);
+ box-shadow: 0 8px 30px rgba(39, 134, 207, 0.3);
+}
+
+.profile h2 {
+ text-align: center;
+ font-weight: 600;
+ margin-bottom: var(--space-xl);
+ color: var(--gray-900);
+}
+
+.profile form {
+ width: 100%;
+ text-align: center;
+}
+
+.profile .content_panel {
+ text-align: center;
+ padding: var(--space-2xl);
+}
+
+/* Profile container (overlay) */
+.profile_container {
+ display: none;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ width: min(520px, calc(100vw - 24px));
+ max-height: min(700px, calc(100dvh - 24px));
+ overflow: auto;
+ padding: 0;
+ background: #FFFFFF;
+ border-radius: 0;
+ box-shadow: 0 24px 70px rgba(0,0,0,0.36);
+ z-index: 2000;
+}
+
+.profile-modal-card {
+ position: relative;
+}
+
+.profile-modal-hero {
+ position: relative;
+ display: grid;
+ grid-template-columns: 96px minmax(0, 1fr);
+ gap: 16px;
+ align-items: center;
+ padding: 24px;
+ color: #FFFFFF;
+ background:
+ radial-gradient(circle at 10% 0%, rgba(255,255,255,0.24), transparent 32%),
+ linear-gradient(135deg, var(--nak-teal), #008577);
+}
+
+.profile-modal-img {
+ width: 88px;
+ height: 88px;
+ object-fit: cover;
+ border-radius: 16px;
+ box-shadow: 0 12px 28px rgba(0,0,0,0.32), 0 0 0 4px rgba(255,255,255,0.18);
+}
+
+.profile-modal-title {
+ margin: 0 42px 4px 0;
+ font-size: 1.35em;
+ font-weight: 850;
+ color: #FFFFFF;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-break: break-word;
+}
+
+.profile-modal-subtitle {
+ color: rgba(255,255,255,0.86);
+ font-size: 0.88em;
+ font-weight: 600;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-break: break-word;
+}
+
+.profile-modal-body {
+ padding: 22px 24px 24px 24px;
+}
+
+.profile-modal-field {
+ margin-bottom: 16px;
+}
+
+.profile-modal-field-label {
+ font-size: 0.82em;
+ font-weight: 850;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: hsl(210, 18%, 36%);
+ margin-bottom: 6px;
+}
+
+.profile-modal-field-value {
+ font-size: var(--font-size-sm);
+ color: var(--gray-800);
+ word-break: break-word;
+}
+
+.profile-modal-field-value a {
+ color: var(--theme-color, var(--nak-blue));
+ text-decoration: none;
+ font-weight: 600;
+}
+
+.profile-modal-field-value a:hover {
+ text-decoration: none;
+}
+
+.profile-modal-chips {
+ display: flex;
+ flex-flow: row wrap;
+ gap: 6px;
+}
+
+.profile-modal-chip {
+ display: inline-flex;
+ align-items: center;
+ padding: 5px 10px;
+ border-radius: 9999px;
+ background: hsl(174, 100%, 96%);
+ border: 1px solid hsl(174, 50%, 80%);
+ color: hsl(174, 60%, 28%);
+ font-size: 0.8em;
+ font-weight: 700;
+}
+
+.profile_container .exit {
+ position: absolute;
+ top: 14px;
+ right: 14px;
+ z-index: 1;
+ width: 34px;
+ height: 34px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 0;
+ border: 1px solid rgba(255,255,255,0.25);
+ background: rgba(255,255,255,0.16);
+ color: #FFFFFF;
+ font-size: 1.35em;
+ line-height: 1;
+ cursor: pointer;
+ padding: 0;
+ transition: background 0.18s ease;
+}
+
+.profile_container .exit:hover {
+ background: rgba(255,255,255,0.3);
+}
+
+div.profile_container a:link,
+div.profile_container a:visited { color: var(--theme-color, var(--nak-blue)); }
+div.profile_container a:hover,
+div.profile_container a:active { color: var(--theme-hover, var(--nak-blue-hover)); }
+
+/* --------------------------------------------------------------------------
+ 9. FORMS
+ -------------------------------------------------------------------------- */
+form {
+ position: relative;
+ display: flex;
+ flex-flow: column nowrap;
+ margin: 0;
+ padding: 0;
+ overflow: visible;
+ background-color: transparent;
+}
+
+.content label {
+ display: block;
+ margin: 0;
+ padding: 0;
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ color: var(--gray-600);
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+}
+
+.content input.field,
+.content input.field_fullwidth,
+.content input.field_date {
+ display: block;
+ position: relative;
+ margin: var(--space-sm) 0;
+ padding: 12px 14px;
+ width: 100%;
+ font-size: var(--font-size-base);
+ font-family: var(--font-primary);
+ font-weight: 300;
+ border: 1px solid var(--gray-300);
+ border-radius: 0;
+ background: var(--gray-50);
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
+ outline: none;
+}
+
+.content input.field:focus,
+.content input.field_fullwidth:focus,
+.content input.field_date:focus {
+ border-color: var(--nak-blue);
+ box-shadow: 0 0 0 3px var(--nak-blue-20);
+ background: #ffffff;
+}
+
+.content input.button,
+.content input.button_fullwidth,
+input.button_long {
+ position: relative;
+ margin: var(--space-md) 0;
+ padding: 14px 24px;
+ width: 100%;
+ height: auto;
+ font-size: var(--font-size-base);
+ font-weight: 500;
+ font-family: var(--font-primary);
+ color: #ffffff;
+ background: var(--nak-blue);
+ border: none;
+ border-radius: 0;
+ box-shadow: var(--shadow-sm);
+ cursor: pointer;
+ line-height: 1.4;
+ transition: background var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast);
+}
+
+.content input.button:hover,
+.content input.button_fullwidth:hover,
+input.button_long:hover {
+ background: var(--nak-blue-hover);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.content input[type="checkbox"] {
+ margin: var(--space-sm) var(--space-sm) var(--space-sm) 0;
+ width: 20px;
+ height: 20px;
+ accent-color: var(--nak-blue);
+}
+
+.content textarea {
+ margin: var(--space-sm) 0;
+ padding: 12px 14px;
+ width: 100%;
+ resize: vertical;
+ font-size: var(--font-size-base);
+ font-family: var(--font-primary);
+ font-weight: 300;
+ border: 1px solid var(--gray-300);
+ border-radius: 0;
+ background: var(--gray-50);
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
+ outline: none;
+}
+
+.content textarea:focus {
+ border-color: var(--nak-blue);
+ box-shadow: 0 0 0 3px var(--nak-blue-20);
+ background: #ffffff;
+}
+
+/* Eventbox (Form-Container) */
+.eventbox_mini {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-self: center;
+ margin: var(--space-lg) 0;
+ padding: var(--space-lg);
+ width: 100%;
+ max-width: 400px;
+ background: var(--gray-50);
+ border: 1px solid var(--gray-200);
+ border-radius: 0;
+}
+
+.eventbox_mini input.field {
+ margin: var(--space-sm) 0;
+ padding: 10px 14px;
+ width: 100%;
+ font-size: var(--font-size-base);
+ border: 1px solid var(--gray-300);
+ border-radius: 0;
+ outline: none;
+ transition: border-color var(--transition-fast);
+}
+
+.eventbox_mini input.field:focus {
+ border-color: var(--nak-blue);
+ box-shadow: 0 0 0 3px var(--nak-blue-20);
+}
+
+.eventbox_mini input.button {
+ margin: var(--space-md) 0 0 0;
+ padding: 12px;
+ width: 100%;
+ color: #ffffff;
+ font-weight: 500;
+ background: var(--nak-blue);
+ border: none;
+ border-radius: 0;
+ cursor: pointer;
+ transition: background var(--transition-fast);
+}
+
+.eventbox_mini input.button:hover {
+ background: var(--nak-blue-hover);
+}
+
+/* --------------------------------------------------------------------------
+ 10. LOGIN
+ -------------------------------------------------------------------------- */
+.login_panel {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ margin-bottom: calc(-1 * var(--footer-height));
+ padding: var(--space-xl);
+ background: linear-gradient(135deg, var(--gray-50) 0%, var(--theme-light, var(--nak-blue-light)) 100%);
+}
+
+.login_content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: var(--space-2xl);
+ width: 100%;
+ max-width: 420px;
+ background: #ffffff;
+ border-radius: 0;
+ box-shadow: var(--shadow-lg);
+}
+
+.login_content h1 {
+ font-size: var(--font-size-xl);
+ font-weight: 600;
+ color: var(--gray-900);
+ text-align: center;
+ margin-bottom: var(--space-sm);
+}
+
+.login_content h2 {
+ font-size: var(--font-size-lg);
+ font-weight: 500;
+ color: var(--gray-600);
+ text-align: center;
+ margin-bottom: var(--space-sm);
+}
+
+/* Logo/Wordmark auf Authorization-Seiten */
+.login_content .auth-logo {
+ display: flex;
+ align-items: flex-end;
+ align-self: flex-start;
+ gap: 14px;
+ width: 100%;
+ margin: 0 0 var(--space-lg) 0;
+ text-decoration: none;
+}
+
+.login_content .auth-logo__icon {
+ width: 64px;
+ height: 64px;
+ flex-shrink: 0;
+}
+
+.login_content .auth-logo__text {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ line-height: 1.2;
+ padding-bottom: 2px;
+}
+
+.login_content .auth-logo__name {
+ font-family: var(--font-primary);
+ font-weight: 500;
+ font-size: 1rem;
+ color: var(--gray-900);
+ white-space: nowrap;
+}
+
+.login_content .auth-logo__gemeinde {
+ font-family: var(--font-primary);
+ font-weight: 300;
+ font-size: 0.875rem;
+ color: var(--gray-600);
+ white-space: nowrap;
+}
+
+@media (max-width: 480px) {
+ .login_content .auth-logo {
+ gap: 10px;
+ }
+
+ .login_content .auth-logo__icon {
+ width: 48px;
+ height: 48px;
+ }
+
+ .login_content .auth-logo__name {
+ font-size: 0.875rem;
+ }
+
+ .login_content .auth-logo__gemeinde {
+ font-size: 0.75rem;
+ }
+}
+
+.login_panel form {
+ width: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+.login_panel .item {
+ position: relative;
+ display: flex;
+ flex-flow: column-reverse wrap;
+ justify-content: center;
+ align-items: flex-start;
+ width: 100%;
+}
+
+.login_panel label {
+ display: block;
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ color: var(--gray-600);
+ margin-bottom: var(--space-xs);
+}
+
+.login_field {
+ margin: 0 0 var(--space-md) 0;
+ padding: 14px 16px;
+ width: 100%;
+ font-size: var(--font-size-base);
+ font-family: var(--font-primary);
+ font-weight: 300;
+ border: 1px solid var(--gray-300);
+ border-radius: 0;
+ background: var(--gray-50);
+ outline: none;
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
+ box-shadow: none;
+}
+
+.login_field:focus {
+ border-color: var(--nak-blue);
+ box-shadow: 0 0 0 3px var(--nak-blue-20);
+ background: #ffffff;
+}
+
+.login_button {
+ margin: var(--space-md) 0 0 0;
+ padding: 14px;
+ width: 100%;
+ font-size: var(--font-size-base);
+ font-weight: 500;
+ font-family: var(--font-primary);
+ color: #ffffff;
+ background: var(--nak-blue);
+ border: none;
+ border-radius: 0;
+ cursor: pointer;
+ box-shadow: var(--shadow-sm);
+ transition: background var(--transition-fast), transform var(--transition-fast);
+}
+
+.login_button:hover {
+ background: var(--nak-blue-hover);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.login_panel .just-validate-error-label {
+ color: var(--nak-red) !important;
+ font-size: var(--font-size-xs);
+ font-weight: 500;
+}
+
+/* --------------------------------------------------------------------------
+ 11. MESSAGES
+ -------------------------------------------------------------------------- */
+.messagebox {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin: var(--space-md);
+ padding: var(--space-sm) var(--space-lg);
+ min-height: 36px;
+ width: fit-content;
+ color: #ffffff;
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ text-align: center;
+ border-radius: var(--radius-full);
+}
+
+.messagebox_short {
+ display: inline-flex;
+ align-items: center;
+ padding: var(--space-xs) var(--space-md);
+ height: 28px;
+ color: #ffffff;
+ font-size: var(--font-size-xs);
+ font-weight: 500;
+ border-radius: var(--radius-full);
+}
+
+/* Message Colors */
+.bg_red { background-color: var(--nak-red); }
+.bg_green { background-color: var(--nak-darkgreen); }
+.bg_black { background-color: var(--gray-800); }
+.bg_blue { background-color: var(--nak-blue); }
+.bg_purple { background-color: var(--nak-purple); }
+.text_white { color: #ffffff; }
+.text_black { color: var(--gray-900); }
+.text_red { color: var(--nak-red); }
+
+/* Toast Notifications */
+.nak-toast {
+ position: fixed;
+ top: 18px;
+ left: 50%;
+ transform: translateX(-50%) translateY(-120%);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 14px 20px;
+ min-width: 280px;
+ max-width: 520px;
+ background: #ffffff;
+ border: 1px solid var(--gray-200);
+ box-shadow: 0 8px 32px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06);
+ z-index: 10000;
+ opacity: 0;
+ pointer-events: none;
+ transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1),
+ opacity 0.35s cubic-bezier(0.22, 1, 0.36, 1);
+ font-family: var(--font-primary);
+}
+.nak-toast--visible {
+ transform: translateX(-50%) translateY(0);
+ opacity: 1;
+ pointer-events: auto;
+}
+.nak-toast--hiding {
+ transform: translateX(-50%) translateY(-120%);
+ opacity: 0;
+}
+.nak-toast__icon {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ border-radius: 0;
+}
+.nak-toast__icon svg {
+ width: 20px;
+ height: 20px;
+}
+.nak-toast--success .nak-toast__icon {
+ background: var(--nak-darkgreen);
+ color: #ffffff;
+}
+.nak-toast--error .nak-toast__icon {
+ background: var(--nak-red);
+ color: #ffffff;
+}
+.nak-toast__text {
+ flex: 1;
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ color: var(--gray-800);
+ line-height: 1.4;
+}
+.nak-toast__close {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ padding: 0;
+ margin: 0;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ color: var(--gray-400);
+ transition: color var(--transition-fast);
+}
+.nak-toast__close:hover {
+ color: var(--gray-700);
+}
+.nak-toast__close svg {
+ width: 16px;
+ height: 16px;
+}
+.nak-toast--success {
+ border-left: 4px solid var(--nak-darkgreen);
+}
+.nak-toast--error {
+ border-left: 4px solid var(--nak-red);
+}
+
+/* Inline variant for auth pages (not fixed, positioned in flow) */
+.nak-toast--inline {
+ position: relative;
+ top: auto;
+ left: auto;
+ transform: translateY(-20px);
+ margin: 0 auto var(--space-md) auto;
+ z-index: 10;
+}
+.nak-toast--inline.nak-toast--visible {
+ transform: translateY(0);
+}
+.nak-toast--inline.nak-toast--hiding {
+ transform: translateY(-20px);
+}
+
+@media (max-width: 600px) {
+ .nak-toast:not(.nak-toast--inline) {
+ left: 12px;
+ right: 12px;
+ transform: translateX(0) translateY(-120%);
+ min-width: auto;
+ max-width: none;
+ }
+ .nak-toast:not(.nak-toast--inline).nak-toast--visible {
+ transform: translateX(0) translateY(0);
+ }
+ .nak-toast:not(.nak-toast--inline).nak-toast--hiding {
+ transform: translateX(0) translateY(-120%);
+ }
+}
+
+/* --------------------------------------------------------------------------
+ 12. TABLES
+ -------------------------------------------------------------------------- */
+.eventbox_table {
+ position: relative;
+ display: block;
+ width: 100%;
+ background-color: unset;
+ border-collapse: collapse;
+ overflow: visible;
+ scroll-behavior: smooth;
+}
+
+.eventbox_table:after {
+ content: "";
+ display: block;
+ height: 40px;
+}
+
+.eventbox_table th {
+ padding: var(--space-sm) var(--space-md);
+ font-size: var(--font-size-sm);
+ font-weight: 500;
+ color: var(--gray-700);
+ text-align: center;
+ background: var(--gray-100);
+ border: none;
+ border-bottom: 2px solid var(--gray-200);
+ z-index: 10;
+}
+
+.eventbox_table th::after {
+ content: '';
+ position: absolute;
+ top: -1px;
+ left: 0;
+ right: 0;
+ margin: 0;
+ height: 1px;
+ background-color: #fefefe;
+ z-index: -1;
+}
+
+.eventbox_table td {
+ padding: var(--space-sm) var(--space-md);
+ background-color: unset;
+ border: none;
+ border-bottom: 1px solid var(--gray-100);
+ font-size: var(--font-size-sm);
+}
+
+.eventbox_table tr:hover td {
+ background-color: var(--nak-blue-10);
+}
+
+.eventbox_table tr:focus {
+ outline: 2px solid var(--nak-blue);
+ outline-offset: -2px;
+}
+
+.eventbox_table tr:focus td.person {
+ outline: 2px solid var(--nak-blue);
+ outline-offset: -2px;
+}
+
+.eventbox_table img {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ padding: 4px;
+ width: calc(100% - 8px);
+ height: auto;
+ object-fit: cover;
+ aspect-ratio: 1/1;
+ border-radius: 0;
+}
+
+#data.content_panel table {
+ margin-bottom: var(--space-xl);
+ width: 100%;
+ border-collapse: collapse;
+ background: #ffffff;
+}
+
+#data.content_panel th,
+#data.content_panel td {
+ text-align: left;
+ font-family: var(--font-primary);
+ border: 1px solid var(--gray-200);
+ padding: var(--space-sm) var(--space-md);
+}
+
+#data.content_panel th {
+ background-color: var(--gray-100);
+ font-weight: 500;
+}
+
+/* --------------------------------------------------------------------------
+ 13. INPUT PREFIX (Phone)
+ -------------------------------------------------------------------------- */
+.input-prefix {
+ display: flex;
+ align-items: center;
+ margin: 0;
+ padding: 12px 14px;
+ font-size: var(--font-size-base);
+ font-weight: 500;
+ color: var(--gray-700);
+ background: var(--gray-200);
+ border: 1px solid var(--gray-300);
+ border-right: none;
+ border-radius: 0;
+}
+
+.phone-input {
+ display: flex;
+ align-items: center;
+ flex: 1;
+ width: auto;
+ font-size: var(--font-size-base);
+ font-family: var(--font-primary);
+ margin: 0;
+ padding: 12px 14px;
+ border: 1px solid var(--gray-300);
+ border-left: none;
+ border-radius: 0;
+ outline: none;
+ transition: border-color var(--transition-fast);
+}
+
+.phone-input:focus {
+ border-color: var(--nak-blue);
+ box-shadow: 0 0 0 3px var(--nak-blue-20);
+}
+
+
+/* --------------------------------------------------------------------------
+ 15. UTILITY CLASSES
+ -------------------------------------------------------------------------- */
+.unselectable {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.center { display: flex; align-items: center; justify-content: center; }
+.center a { display: flex; justify-content: center; align-items: center; }
+.fullheight { height: 100%; }
+.fullwidth { width: 100%; }
+.left { text-align: left; }
+.right { text-align: right; }
+.right a { display: flex; justify-content: center; align-items: center; }
+.small_text { font-size: var(--font-size-sm); }
+.text_black { color: var(--gray-900); }
+
+.flexrow_left {
+ display: flex;
+ flex-direction: row;
+ align-items: stretch;
+}
+
+.flexcolumn_left {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.flexcolumn_center {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+}
+
+.content_flexcontainer {
+ display: flex;
+ flex-flow: row nowrap;
+ padding: var(--space-lg);
+ width: 100%;
+ box-sizing: border-box;
+}
+
+.flexleft {
+ flex: 70%;
+ order: 1;
+ padding: var(--space-sm);
+}
+
+.flexright {
+ flex: 30%;
+ order: 2;
+ padding: var(--space-sm);
+}
+
+@media (max-width: 768px) {
+ .content_flexcontainer {
+ flex-flow: column wrap;
+ }
+ .flexleft { order: 2; }
+ .flexright { order: 1; }
+}
+
+/* --------------------------------------------------------------------------
+ 16. RESPONSIVE
+ -------------------------------------------------------------------------- */
+@media (max-width: 768px) {
+
+ .mainpage_container {
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
+ gap: var(--space-md);
+ padding: var(--space-lg);
+ max-width: 700px;
+ }
+
+ .button_mainpage {
+ min-height: 100px;
+ padding: var(--space-lg) var(--space-md);
+ }
+
+ .button_mainpage .text {
+ font-size: var(--font-size-base);
+ }
+
+ #base.content {
+ padding: var(--space-md);
+ }
+
+ .content_panel {
+ padding: var(--space-lg);
+ }
+
+ .profile img {
+ width: 120px;
+ height: 120px;
+ }
+
+ .profile {
+ padding: var(--space-lg) 0;
+ }
+
+ .profile .content_panel {
+ padding: var(--space-lg);
+ }
+
+ .profile h2 {
+ font-size: var(--font-size-lg);
+ margin-bottom: var(--space-lg);
+ }
+
+ .eventbox_mini {
+ max-width: 100%;
+ padding: var(--space-md);
+ margin: var(--space-md) 0;
+ }
+
+ .eventbox_item {
+ min-width: 0;
+ width: 100%;
+ padding: var(--space-xs) 0;
+ white-space: normal;
+ }
+
+ .login_content {
+ padding: var(--space-xl);
+ max-width: 100%;
+ overflow-x: hidden;
+ }
+
+ footer {
+ height: auto;
+ min-height: 36px;
+ max-height: none;
+ padding: var(--space-xs) var(--space-md);
+ }
+
+ .legal-links {
+ gap: var(--space-md);
+ }
+
+ .legal-links a {
+ font-size: var(--font-size-xs);
+ }
+}
+
+@media (max-width: 480px) {
+ .mainpage_container {
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-sm);
+ padding: var(--space-md);
+ }
+
+ .button_mainpage {
+ min-height: 80px;
+ padding: var(--space-md);
+ }
+
+ .button_mainpage .text {
+ font-size: var(--font-size-sm);
+ }
+
+ .header_profile_slideLeft {
+ max-width: 300px;
+ padding-left: var(--space-sm);
+ }
+
+ .profile {
+ padding: var(--space-md) 0;
+ }
+
+ .profile img {
+ width: 100px;
+ height: 100px;
+ margin-bottom: var(--space-md);
+ }
+
+ .profile .content_panel {
+ padding: var(--space-md);
+ }
+
+ .profile h2 {
+ font-size: var(--font-size-base);
+ margin-bottom: var(--space-md);
+ }
+
+ .profile form {
+ width: 100%;
+ }
+
+ .content label {
+ font-size: var(--font-size-xs);
+ }
+
+ .content input.field_fullwidth {
+ padding: 10px 12px;
+ font-size: var(--font-size-sm);
+ }
+
+ .content input.button_fullwidth {
+ padding: 12px 16px;
+ font-size: var(--font-size-sm);
+ }
+
+ .flexrow_left.fullwidth {
+ flex-wrap: nowrap;
+ }
+
+ .input-prefix {
+ padding: 10px 10px;
+ font-size: var(--font-size-sm);
+ min-width: auto;
+ }
+
+ .phone-input {
+ padding: 10px 12px;
+ font-size: var(--font-size-sm);
+ }
+
+ .profile p {
+ font-size: var(--font-size-sm);
+ word-break: break-word;
+ }
+
+ .profile input[type="file"] {
+ font-size: var(--font-size-xs);
+ max-width: 100%;
+ }
+
+ .flexcolumn_left label {
+ font-size: var(--font-size-xs);
+ }
+
+ #base.content {
+ padding: var(--space-sm);
+ }
+
+ .content_panel {
+ padding: var(--space-md);
+ }
+
+ .eventbox_mini {
+ padding: var(--space-sm);
+ }
+
+ .eventbox_item {
+ margin: var(--space-xs) 0;
+ }
+
+ footer {
+ padding: var(--space-xs) var(--space-sm);
+ }
+
+ .legal-links {
+ gap: var(--space-sm);
+ }
+
+ .legal-links a {
+ font-size: 0.65rem;
+ }
+}
+
+/* --------------------------------------------------------------------------
+ 17. SPECIAL FORMS (display_within, kalli, etc.)
+ -------------------------------------------------------------------------- */
+form.display_within, form.display_within_admin, form.display_within_visit {
+ position: relative;
+ display: flex;
+ flex-flow: column nowrap;
+ justify-content: flex-start;
+ margin: 0;
+ padding: 0;
+ overflow: visible;
+ background-color: transparent;
+ border-top: 1px solid var(--gray-200);
+ border-radius: 0;
+ min-height: calc(100vh - var(--header-height) - var(--header-title-height) - var(--footer-height));
+ width: 100%;
+}
+
+form.display_within_dev {
+ position: relative;
+ display: flex;
+ flex-flow: column nowrap;
+ justify-content: flex-start;
+ margin: var(--space-md) 0 0 0;
+ padding: var(--space-sm);
+ overflow: visible;
+ background-color: transparent;
+ border-top: 1px solid var(--gray-200);
+ min-height: calc(100vh - var(--header-height) - var(--header-title-height) - 80px - var(--footer-height));
+ width: 100%;
+}
+
+.display_within_dev textarea {
+ margin: 5px;
+ padding: 5px;
+ width: auto;
+ resize: vertical;
+ border: 1px solid var(--gray-300);
+ border-radius: 0;
+ height: calc(100vh - var(--header-height) - var(--header-title-height));
+ font-size: var(--font-size-base);
+}
+
+form.kalli {
+ padding-top: var(--space-xl);
+ padding-bottom: var(--space-xl);
+}
+
+/* --------------------------------------------------------------------------
+ 18. EVENTBOX KALLI
+ -------------------------------------------------------------------------- */
+.eventbox_kalli {
+ position: relative;
+ display: flex;
+ flex-flow: row wrap;
+ align-items: stretch;
+ align-self: center;
+ margin: 0 0 var(--space-xl) 0;
+ padding: var(--space-md);
+ width: calc(85% - 10px);
+ background: var(--gray-50);
+ border: 1px solid var(--gray-200);
+ border-radius: 0;
+ font-size: var(--font-size-md) !important;
+}
+
+.eventbox_kalli a {
+ font-size: var(--font-size-sm);
+}
+
+.eventbox_kalli .label {
+ margin: 0 0 var(--space-sm) 0;
+ color: var(--gray-600);
+ font-weight: 500;
+}
+
+.eventbox_item {
+ flex: 1 0 20%;
+ margin: var(--space-sm);
+ padding: var(--space-sm) var(--space-md);
+ display: inline-block;
+ min-width: 300px;
+ box-sizing: border-box;
+ white-space: nowrap;
+ border: none;
+ border-radius: 0;
+}
+
+@media (max-width: 768px) {
+ .eventbox_item {
+ min-width: 0;
+ width: 100%;
+ padding: var(--space-xs) 0;
+ white-space: normal;
+ }
+}
+
+@media (max-width: 480px) {
+ .eventbox_item {
+ margin: var(--space-xs) 0;
+ }
+}
+
+/* --------------------------------------------------------------------------
+ 19. TABLE EXTRAS (sticky columns etc.)
+ -------------------------------------------------------------------------- */
+.eventbox_table th.cut_off_75 {
+ padding: 2px;
+ width: 75px !important;
+ min-width: 75px !important;
+ max-width: 75px !important;
+}
+
+.cut_off_text {
+ overflow: hidden;
+ display: inline-block;
+ vertical-align: middle;
+ padding: 2px;
+ width: calc(100% - 4px);
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.eventbox_table th:first-child,
+.eventbox_table td:first-child {
+ position: sticky;
+ left: 0;
+ margin: 0;
+ padding: 0;
+ width: auto;
+ height: auto;
+ background-color: inherit;
+ border: none;
+ z-index: 5;
+}
+
+.eventbox_table th:first-child {
+ z-index: 20;
+}
+
+.eventbox_table td.person {
+ padding: 4px !important;
+}
+
+.eventbox_table td.column_img_fixed {
+ margin: 0;
+ padding: 0;
+ padding-right: 4px;
+ height: 75px;
+ min-height: 75px;
+ max-height: 75px;
+ width: 75px;
+ min-width: 75px;
+ max-width: 75px;
+ background-color: unset;
+ border: none;
+ z-index: 3;
+}
+
+#visittable.eventbox_table td.column_img_fixed {
+ border-right: unset !important;
+}
+
+.eventbox_table input[type=submit] {
+ margin: 2px 4px;
+ padding: 4px 8px;
+ width: fit-content;
+ font-size: var(--font-size-xs) !important;
+ background: var(--nak-blue);
+ color: #ffffff;
+ border: none;
+ border-radius: 0;
+ cursor: pointer;
+}
+
+.eventbox_table input[type=text] {
+ display: inline-block;
+ margin: 0;
+ padding: 8px;
+ width: calc(100% - 20px);
+ border-radius: 0;
+ text-align: center;
+ border: 1px solid var(--gray-300);
+}
+
+.eventbox_table input[type=text]:focus {
+ border-color: var(--nak-blue);
+ box-shadow: 0 0 0 2px var(--nak-blue-20);
+ outline: none;
+}
+
+/* Focus styles for table rows */
+.eventbox_table tr:focus img {
+ border: 2px solid var(--nak-blue) !important;
+ padding: 3px;
+}
+
+#admintable.eventbox_table tr:focus {
+ background-color: #ffffff;
+}
+
+#visittable.eventbox_table tr:focus img {
+ border: 2px solid transparent !important;
+ padding: 3px;
+}
+
+/* --------------------------------------------------------------------------
+ 20. CONTENT PANEL OUTPUT
+ -------------------------------------------------------------------------- */
+.content_panel_output {
+ position: relative;
+ margin: var(--space-md) 0 0 0;
+ padding: var(--space-md) var(--space-lg);
+ width: 100%;
+ background: var(--gray-50);
+ border-top: 1px solid var(--gray-200);
+ border-radius: 0;
+}
+
+.content_panel_output ul { margin-left: 0; padding-left: 0; }
+.content_panel_output li { margin-left: var(--space-lg); padding-left: 0; }
+
+/* --------------------------------------------------------------------------
+ 21. SVG BUTTONS
+ -------------------------------------------------------------------------- */
+.reload {
+ position: absolute;
+ width: 40px;
+ height: 40px;
+ top: 0;
+ left: 4px;
+ opacity: 1;
+ cursor: pointer;
+ z-index: 5;
+}
+
+.reload svg {
+ fill: var(--nak-blue);
+ width: 100%;
+ height: 100%;
+}
+
+.button_picto {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+a .button_picto {
+ fill: var(--nak-blue-60);
+}
+
+a .button_picto:hover svg {
+ fill: var(--nak-blue);
+}
+
+.button_picto svg {
+ margin: 0 2px;
+ width: 18px;
+ height: 18px;
+}
+
+/* --------------------------------------------------------------------------
+ 22. COUNTDOWN
+ -------------------------------------------------------------------------- */
+.countdown {
+ position: absolute;
+ width: 100%;
+ top: 14.5px;
+ left: -2.5px;
+ text-align: center;
+ color: var(--nak-blue);
+ font-size: 6.5pt;
+ font-family: 'Courier New', monospace;
+ z-index: 6;
+}
+
+/* --------------------------------------------------------------------------
+ 23. PRINT
+ -------------------------------------------------------------------------- */
+@media print {
+ header, footer, .header_title { display: none; }
+ #base.content { margin-top: 0; padding: 0; }
+ .content_panel { box-shadow: none; border: none; }
+}
+
+/* --------------------------------------------------------------------------
+ 24. ANIMATIONS
+ -------------------------------------------------------------------------- */
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(10px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes slideDown {
+ from { opacity: 0; transform: translateY(-20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.fade-in {
+ animation: fadeIn 0.4s ease forwards;
+}
+
+/* ==========================================================================
+ 25. LEGACY STYLES (migrated from intranet.css)
+ ========================================================================== */
+
+/* --- Generic SVG hover --- */
+a:hover svg { fill: var(--theme-color, var(--nak-blue)); }
+
+/* --- Sticky table headers --- */
+thead {
+ position: sticky;
+ z-index: 10;
+}
+
+/* --- Header menu link height --- */
+.header_menue a {
+ height: 100%;
+}
+
+/* --- Data panel first-column fix --- */
+#data.content_panel td:first-child {
+ -webkit-column-width: auto;
+ -moz-column-width: auto;
+ column-width: auto;
+}
+
+/* --- Eventbox mini extras --- */
+.eventbox_mini input[type="file"] {
+ position: relative;
+ padding: 5px;
+}
+
+.eventbox_mini .item {
+ position: relative;
+ margin: 5px;
+ width: 100%;
+}
+
+/* --- Eventbox kalli text --- */
+.eventbox_kalli .text_after_input {
+ margin: 0;
+ padding: 0;
+ vertical-align: text-bottom;
+}
+
+/* --- Signup form --- */
+form#signup {
+ margin: 20px 5px;
+}
+
+/* --- Login label color --- */
+.login_label {
+ color: var(--gray-500);
+}
+
+/* --- Overlay styles --- */
+.standard_overlay {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ justify-content: center;
+ align-items: center;
+ background-color: hsla(210, 57%, 25%, 0.8);
+ z-index: 9999;
+}
+
+.standard_overlay .exit {
+ position: absolute;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ top: 15px;
+ right: 15px;
+ padding: 0;
+ width: 28px;
+ height: 28px;
+ background-color: #fefefe;
+ border: none;
+ border-radius: 50%;
+ box-shadow: 0 1px 1px 0 rgba(0,0,0,0.2), 0 1px 3px 0 rgba(0,0,0,0.4);
+}
+
+.standard_overlay .exit:hover {
+ cursor: pointer;
+ background-color: #dddddd;
+}
+
+.standard_overlay .exit svg {
+ margin: 1px 0 0 0;
+ width: 16px;
+ height: 16px;
+ fill: #000;
+}
+
+.standard_overlay .exit:hover svg {
+ margin: 1px 0 0 0;
+ width: 16px;
+ height: 16px;
+ fill: #fefefe !important;
+}
+
+.profile-modal-card .exit {
+ border-radius: 0;
+ border: 1px solid rgba(255,255,255,0.25);
+ background: rgba(255,255,255,0.16);
+ color: #FFFFFF;
+ box-shadow: none;
+}
+.profile-modal-card .exit:hover {
+ background: rgba(255,255,255,0.3);
+}
+
+/* --- Backdrop blur --- */
+.bg_blur {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ -webkit-backdrop-filter: blur(4px);
+ backdrop-filter: blur(4px);
+ z-index: -2;
+}
+
+/* --- Flex helpers --- */
+.flexrow {
+ position: absolute;
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: center;
+}
+
+.flexrow_right {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ justify-content: right;
+ align-content: center;
+}
+
+/* --- Table cell middle --- */
+.cell_middle {
+ display: table-cell;
+ align-items: center;
+ text-align: center;
+ vertical-align: middle;
+}
+
+/* --- Placeholder --- */
+.placeholder {
+ padding-top: 65px;
+}
+
+/* --- Utility resets --- */
+.no_margins { margin: 0 !important; }
+.no_paddings { padding: 0 !important; }
+.no_wrap { flex-wrap: nowrap !important; }
+
+/* --- Background colors --- */
+.bg_greyred { background-color: hsl(0, 15%, 90%) !important; }
+.bg_grey { background-color: hsl(210, 15%, 80%) !important; }
+
+/* --- Linear gradient backgrounds (eventbox_table) --- */
+.bg_blue_linear2left {
+ background: linear-gradient(to left,
+ hsla(220, 12%, 95%, 1.0) 0px,
+ hsla(210, 57%, 50%, 0.6) 1px,
+ hsla(210, 57%, 50%, 1.0) 30%,
+ hsla(210, 57%, 58%, 1.0) 100%) !important;
+}
+
+.bg_red_linear2left {
+ background: linear-gradient(to left,
+ hsla(220, 12%, 95%, 1.0) 0px,
+ hsla(342, 65%, 45%, 0.6) 1px,
+ hsla(342, 65%, 45%, 1.0) 30%,
+ hsla(342, 65%, 58%, 1.0) 100%) !important;
+}
+
+.bg_orange_linear2left {
+ background: linear-gradient(to left,
+ hsla(220, 12%, 95%, 1.0) 0px,
+ hsla(27, 70%, 55%, 0.6) 1px,
+ hsla(27, 70%, 55%, 1.0) 30%,
+ hsla(27, 70%, 68%, 1.0) 100%) !important;
+}
+
+.bg_green_linear2left {
+ background: linear-gradient(to left,
+ hsla(220, 12%, 95%, 1.0) 0px,
+ hsla(150, 30%, 48%, 0.6) 1px,
+ hsla(150, 30%, 48%, 1.0) 30%,
+ hsla(150, 30%, 61%, 1.0) 100%) !important;
+}
+
+/* --- Text colors --- */
+.text_grey { color: hsl(210, 15%, 50%) !important; }
+.text_green { color: hsl(145, 40%, 45%) !important; }
+.text_yellow { color: hsl(45, 70%, 45%) !important; }
+
+/* --- Tiny text --- */
+.tiny_text {
+ font-size: 0.78em;
+ font-weight: 400;
+}
+
+/* --- Hyphenation --- */
+.hyphens {
+ hyphens: auto !important;
+}
+
+/* --- Tooltip --- */
+.tooltip {
+ position: relative;
+ display: inline-block;
+}
+
+.tooltip .tooltiptext {
+ position: absolute;
+ visibility: hidden;
+ width: fit-content;
+ margin-top: 30px;
+ padding: 3px 7px;
+ color: hsl(220, 12%, 95%);
+ text-align: center;
+ font-size: 0.7em;
+ font-weight: 300;
+ background-color: var(--gray-900);
+ border-radius: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ transition: visibility 0.3s linear, opacity 0.6s linear;
+ opacity: 0;
+ z-index: 5000;
+}
+
+.tooltip:hover .tooltiptext {
+ visibility: visible;
+ transition: visibility 0.3s linear, opacity 0.3s linear;
+ opacity: 1;
+}
+
+/* --- Border radius round --- */
+.borderradius_round {
+ border-radius: 100% !important;
+}
+
+/* ==========================================================================
+ 26. LEGACY RESPONSIVE (migrated from intranet.css @media max-width: 650px)
+ ========================================================================== */
+@media (max-width: 650px) {
+
+ .content {
+ padding-left: 0;
+ padding-right: 0;
+ }
+
+ #data.content_panel {
+ background-color: transparent;
+ }
+
+ .eventbox_table::after {
+ content: "";
+ display: block;
+ height: 80px;
+ }
+
+ .eventbox_kalli {
+ margin: 0 0 25px 0;
+ padding: 0;
+ height: auto;
+ width: 100%;
+ background-color: hsl(210, 57%, 90%);
+ border-radius: 0;
+ }
+
+ .eventbox_kalli_item {
+ flex: 1 0 48%;
+ margin: 0;
+ padding: 10px 5px 10px 10px;
+ box-sizing: border-box;
+ white-space: nowrap;
+ border: none;
+ border-top: 1px solid #fefefe;
+ border-radius: 0;
+ }
+
+ .login_button {
+ font-size: 14pt;
+ }
+
+ .profile_container {
+ align-content: flex-start;
+ width: calc(100vw - 12px);
+ max-height: calc(100dvh - 12px);
+ border-radius: 0;
+ background: #FFFFFF;
+ }
+
+ .tooltip .tooltiptext {
+ left: unset;
+ right: 20px;
+ transform: translateX(20px);
+ }
+}
+
+@media (max-height: 430px) {
+ .tooltip .tooltiptext {
+ padding: 1px 3px;
+ font-size: 0.6em;
+ font-weight: 300;
+ }
+}
+
+/* --- Sleek modal scrollbar --- */
+.standard_overlay > div,
+.visit-detail-card,
+.visit-modal {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(0,0,0,0.12) transparent;
+}
+.standard_overlay > div::-webkit-scrollbar,
+.visit-detail-card::-webkit-scrollbar,
+.visit-modal::-webkit-scrollbar {
+ width: 5px;
+}
+.standard_overlay > div::-webkit-scrollbar-track,
+.visit-detail-card::-webkit-scrollbar-track,
+.visit-modal::-webkit-scrollbar-track {
+ background: transparent;
+}
+.standard_overlay > div::-webkit-scrollbar-thumb,
+.visit-detail-card::-webkit-scrollbar-thumb,
+.visit-modal::-webkit-scrollbar-thumb {
+ background: rgba(0,0,0,0.12);
+ border-radius: 3px;
+}
+.standard_overlay > div::-webkit-scrollbar-thumb:hover,
+.visit-detail-card::-webkit-scrollbar-thumb:hover,
+.visit-modal::-webkit-scrollbar-thumb:hover {
+ background: rgba(0,0,0,0.22);
+}
+.standard_overlay > div::-webkit-scrollbar-button,
+.visit-detail-card::-webkit-scrollbar-button,
+.visit-modal::-webkit-scrollbar-button {
+ display: none;
+}
diff --git a/web/intranet_repo/projects/_central/layouts/seelsorge.css b/web/intranet_repo/projects/_central/layouts/seelsorge.css
new file mode 100644
index 0000000..6907749
--- /dev/null
+++ b/web/intranet_repo/projects/_central/layouts/seelsorge.css
@@ -0,0 +1,1004 @@
+/* Name: seelsorge.css
+ Beschreibung: Spezielles CSS für Seelsorge Planer */
+
+#seelsorge.menue {
+ position: relative;
+ top: 0px;
+ display: flex;
+ margin: 0px 0px 0px 0px;
+ height: auto;
+ width: 100%;
+ justify-content: space-evenly;
+ align-items: center;
+ background-color: transparent;
+ transition: top 0.9s ease-in-out;
+ -webkit-transition: top 0.9s ease-in-out;
+}
+#seelsorge.menue .list {
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: left;
+ align-items: center;
+ margin: 0 1.8vw 0 1.8vw;
+ padding: 0px;
+ width: 100%;
+}
+
+#seelsorge.menue .menu-item:link { color: hsl(210, 60%, 50%);}
+#seelsorge.menue .menu-item:hover { color: #FFFFFF;
+ opacity: 1.0;
+ transition: top 0.6s ease-in-out;
+ -webkit-transition: top 0.6s ease-in-out;}
+#seelsorge.menue .active {
+ /*background-color: hsl(210, 80%, 35%) !important;*/
+ color: #FEFEFE !important;
+ font-weight: 400 !important;
+ /*box-shadow: 0 0 2px 1px rgba(255, 255, 255, 0.4),
+ 0 1px 12px 0 rgba(0, 0, 0, 0.5) !important;*/
+ opacity: 1.0 !important;
+}
+
+#seelsorge.menue .menu-item {
+ padding: 0px;
+ margin: 0px 0px 0px 30px;
+ display: block;
+ width: 175px;
+ max-width: 24vw;
+ height: 100%;
+ text-align: center;
+ /*border-radius: 9999px;*/
+ color: #FEFEFE;
+ font-size: 100%;
+ font-weight: 100;
+ cursor: pointer;
+ opacity: 0.9;
+
+ transition: background-color 0.5s ease;
+ -webkit-transition: background-color 0.5s ease;
+ /*box-shadow: 0 1px 1px 0 rgba(255, 255, 255, 0.3),
+ 0 1px 6px 0 rgba(0, 0, 0, 0.2);*/
+}
+#seelsorge.menue .indicator {
+ position: absolute;
+ left: 0px;
+ bottom: -3px;
+ width: 0px;
+ border-bottom: 2px solid #FEFEFE;
+ transition: width 0.9s ease-in-out, left 0.6s ease-in-out;
+}
+
+.content_seelsorge {
+ position: relative;
+ display: none;
+ flex-flow: row wrap;
+ justify-content: center;
+ align-content: space-around;
+ align-items: flex-start;
+ padding: 0;
+ margin-top: 0;
+ width: 100%;
+ min-height: 100%;
+ border-radius: 0;
+}
+.content .active { /* bezieht sich damit auf .content_seelsorge*/
+ display: flex;
+}
+
+/* Besuchsstellen */
+.dereservation-button, .prioritize-button, .reservation-button {
+ padding: 5px 10px;
+ margin: 5px;
+ border: 0px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.10);
+ border-radius: 0;
+ cursor: pointer;
+}
+.dereservation-button {
+ background-color: hsl(130, 80%, 40%) !important;
+ color: white;
+}
+.prioritize-button {
+ background-color: hsl(0, 100%, 50%) !important;
+ color: white;
+}
+.reservation-button {
+
+ background-color: hsl(210, 57%, 50%)!important;
+ color: white;
+}
+.highlighted {
+ border: 2px solid hsl(0, 100%, 50%);
+}
+.reserver_profile_image {
+ margin: auto;
+ width: 50px;
+ height: 50px;
+}
+
+div.content_panel input {
+ /* margin-top: 5px; */
+ margin-left: 0px;
+ padding: 10px;
+ padding-left: 20px;
+ padding-right: 20px;
+ width: 120px;
+ color: hsl(60, 33%, 75%);
+ background-color: hsl(208, 50%, 30%);
+ border: 2px solid #AAA;
+ border-radius: 0;
+ font-weight: bold;
+}
+
+div.content_output {
+ padding: 15px;
+ padding-top: 0px;
+ padding-bottom: 0px;
+ margin: 0px;
+ margin-top: 10px;
+ overflow: visible;
+ width: calc(100% - 31px);
+ background-color: hsl( 60, 33%, 95%);
+ border: 1px solid #DDDDDD;
+ border-radius: 0;
+}
+div.content_output ul {
+ margin-left: 0px;
+ padding-left: 0px;
+}
+div.content_output li {
+ margin-left: 20px;
+ padding-left: 0px;
+}
+
+
+.eventbox_maxi {
+ position: relative;
+ display: flex;
+ flex-flow: column nowrap;
+ align-content: center;
+ justify-content: center;
+ align-items: stretch;
+ align-self: center;
+ margin: 0px 10px 10px 10px;
+ padding: 5px 5px 5px 5px;
+ height: auto;
+ width: calc(100% - 30px);
+ background-color: hsla(0, 0%, 100%, 0.3);
+ border-radius: 0;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.08);
+}
+
+/* Pofilgalerie */
+ul.galery {
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: flex-start;
+ padding: 0px;
+ margin: 0px;
+ width:auto;
+ list-style: none;
+}
+li.galery {
+ position: relative;
+ float: left;
+ padding: 0px;
+ margin: 10px;
+ width: 112px;
+ height: 137px;
+ background-color: #FEFEFE;
+ border-radius: 0;
+ overflow: hidden;
+ cursor: pointer;
+ opacity: 1;
+}
+li.galery:hover {
+ filter:contrast(1.1) saturate(1.1);
+ opacity: 1;
+}
+li.galery:hover .galery_name{
+ color: hsl(297, 60%, 45%);
+ text-decoration: none;
+}
+ul.galery .available {
+ box-shadow: 0 0 2px 1px hsl(132, 100%, 35%);
+}
+ul.galery .notavailable {
+ box-shadow: 0 0 2px 1px hsl(0,0%,50%);
+}
+img.galery_image {
+ padding: 0px;
+ margin: 0px;
+ width: 112px;
+ height: 112px;
+ object-fit: cover; /* Bild ohne Verzerrung beschneiden, um es auszufüllen */
+ object-position: center; /* Bildmittelpunkt als Zentrum für den Zuschnitt verwenden */
+}
+.galery_name {
+ position: relative;
+ display: flex;
+ flex-flow: column nowrap;
+ align-content: center;
+ justify-content: center;
+ align-items: center;
+ align-self: center;
+
+ padding: 0px 5px 0px 5px;
+ margin: 0px;
+ height: 17px;
+ text-align: center;
+ font-size: 0.78em;
+ font-weight: 500;
+ color: hsl(297, 40%, 45%);
+ background-color: transparent;
+}
+
+/* .profile_name {
+ margin: 10px;
+ width: 100%;
+ text-align: left;
+ font-size: 16pt;
+ font-weight: bold;
+ color: hsl(195, 30%, 15%);
+ text-shadow: 1px 1px 2px #888888;
+}
+.profile_name_sub {
+ width: 100%;
+ text-align: left;
+ font-size: 12pt;
+ font-weight: bold;
+ color: hsl(297, 50%, 40%);
+} */
+
+
+
+/* .profile_content_small {
+ padding: 5px;
+ margin: 5px;
+ width: 250px;
+ text-align: center;
+
+ background-color: #FFFFFF;
+ border: 5px solid hsl(60, 33%, 80%);
+ border-radius: 10px;
+ box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.3), 0 3px 6px 0 rgba(0, 0, 0, 0.4);
+}
+.profile_content_small img {
+ padding: 0px;
+ margin: 0px;
+ width: 100%;
+ border-radius: 3px;
+} */
+/* .profile_data {
+ -ms-flex: 50%;
+ flex: 50%;
+ padding: 30px;
+ margin: 5px;
+
+ border: 0px solid #DDDDDD;
+ background-color: hsl(60, 33%, 80%);
+ border-radius: 10px;
+ box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.3), 0 3px 6px 0 rgba(0, 0, 0, 0.4);
+} */
+/* .profile_data td {
+ padding-top: 2px;
+ padding-left: 5px;
+ padding-right: 5px;
+ padding-bottom: 2px;
+ margin: 0px;
+ width: auto;
+
+ border-spacing: 0px;
+ border-bottom: 1px solid #AAAAAA;
+
+} */
+/* .profile_calendar {
+ -ms-flex: 50%;
+ flex: 50%;
+ width: auto;
+ height: auto;
+ min-width: 600px;
+ padding: 5px;
+ margin: 5px;
+ background-color: #FFFFFF;
+ border: 1px solid #DDDDDD;
+ border-radius: 10px 10px 10px 10px;
+ box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.3), 0 3px 6px 0 rgba(0, 0, 0, 0.4);
+} */
+
+/* Kalenderblatt*/
+.page_calendar {
+ position: relative;
+ display: none;
+ flex-flow: column wrap;
+ align-items: center;
+ min-height: auto;
+ min-width: 35%;
+ max-height: 600px;
+ max-width: 600px;
+ padding: 0;
+ margin: 5px;
+ background-color: #FFFFFF;
+ border: 1px solid #e9ecef;
+ border-radius: 0;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.10), 0 1px 0 rgba(255,255,255,0.9) inset;
+}
+.page_calendar .active { display: flex; }
+
+.page_calendar .button {
+ position: absolute;
+ display: flex;
+ right: 5px;
+ top: 5px;
+ align-content: center;
+ justify-content: center;
+ height: 40px;
+ width: 40px;
+ border-radius: 0;
+ border: 1px solid transparent;
+ background-color: hsl(297, 10%, 95%);
+ z-index: 50;
+}
+.page_calendar .button:hover {
+ border: 1px solid hsl(297, 40%, 90%);
+ background-color: hsl(297, 80%, 95%);
+}
+.calendar_header {
+ position: relative;
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: center;
+ align-items: center;
+ height: 48px;
+ width: 100%;
+ margin: 0;
+ padding: 8px 12px;
+ background: linear-gradient(135deg, hsl(297, 60%, 35%), hsl(297, 80%, 22%));
+ border-bottom: none;
+ border-radius: 0;
+ color: #FFFFFF;
+}
+.calendar-help-btn {
+ position: absolute;
+ right: 10px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid rgba(255,255,255,0.3);
+ border-radius: 0;
+ background: rgba(255,255,255,0.12);
+ color: #FFFFFF;
+ font-size: 0.9em;
+ font-weight: 700;
+ cursor: pointer;
+ padding: 0;
+ transition: background 0.18s ease;
+}
+.calendar-help-btn:hover {
+ background: rgba(255,255,255,0.28);
+}
+.calendar_title {
+ position: relative;
+ height: auto;
+ width: 200px;
+ font-size: 1.15em;
+ font-weight: 700;
+ text-align: center;
+ color: #FFFFFF;
+ letter-spacing: 0.02em;
+}
+.calendar_title_modus {
+ position: absolute;
+ top: 2px;
+ width: 100%;
+ color: rgba(255,255,255,0.7);
+ font-size: 0.8em;
+ text-align: center;
+ z-index: 5;
+}
+.arrow_item {
+ padding: 6px;
+ border-radius: 0;
+ background-color: rgba(255,255,255,0.12);
+ cursor: pointer;
+ transition: background 0.18s ease;
+}
+.arrow_item:hover {
+ background-color: rgba(255,255,255,0.24);
+}
+.arrow_item:hover svg path { fill: #FFFFFF; }
+.arrow_light svg path { fill: rgba(255,255,255,0.4); }
+.arrow_dark svg path { fill: #FFFFFF; }
+
+.calendar_content {
+ position: relative;
+ display: flex;
+ flex-flow: column nowrap;
+ justify-content: flex-start;
+ padding: 0;
+ margin: 0;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+ font-size: 1.0em;
+ font-weight: 600;
+ border-spacing: 0;
+ border: none;
+ border-radius: 0;
+}
+.calendar_content table {
+ padding: 0px;
+ margin: 0px 0px 0px 0px;
+ width: 100%;
+ height: auto;
+ table-layout: fixed;
+ border-spacing: 5px;
+ border: 0px solid #DDDDDD;
+}
+.calendar_content table:after {
+ content: "";
+ display: block;
+ height: 15px; /* Abstand nach der letzten Zeile für unseren Scrollbutton */
+}
+.calendar_content table td {
+ text-align: center;
+ vertical-align: middle;
+}
+td.calendar_head {
+ padding: 5px 0px 5px 0px;
+ border-bottom: 2px solid hsl(297, 30%, 70%);
+ font-size: 0.82em;
+ font-weight: 700;
+ color: hsl(297, 20%, 40%);
+ letter-spacing: 0.04em;
+}
+td.calendar_head_sat {
+ padding: 5px 0px 5px 0px;
+ border-bottom: 2px solid hsl(297, 40%, 55%);
+ font-size: 0.82em;
+ font-weight: 700;
+ color: hsl(297, 30%, 40%);
+ letter-spacing: 0.04em;
+}
+td.calendar_head_sun {
+ padding: 5px 0px 5px 0px;
+ border-bottom: 2px solid hsl(297, 80%, 40%);
+ font-size: 0.82em;
+ font-weight: 700;
+ color: hsl(297, 60%, 35%);
+ letter-spacing: 0.04em;
+}
+td.calendar_day_area {
+ align-content: center;
+ padding: 5px;
+ width: calc(100% - 10px);
+}
+td.calendar_day_area:hover {
+ cursor: default;
+}
+td.calendar_day_area a {
+ text-decoration: none;
+}
+#calendar_day_item:hover, #calendar_day_item *:hover {
+ filter: brightness(0.82);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
+}
+.calendar_day_item {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ align-content: center;
+ width: auto;
+ height: 100%;
+ aspect-ratio: 1/1;
+ vertical-align: middle;
+ border-radius: 0;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.12);
+ transition: background 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
+}
+ .available_single {
+ color: hsl(210, 20%, 15%);
+ background-color: #ffdc0f;
+ }
+ .available_multiple {
+ color: #FFFFFF;
+ background-color: #006e14;
+ }
+ .normalday {
+ background-color: hsl(297, 10%, 98%);
+ }
+ .today {
+ color: #FFFFFF;
+ background-color: #1243a0;
+ box-shadow: 0 4px 16px rgba(18,67,160,0.35);
+ }
+ /*td.today:hover {
+ color: hsl(195, 0%, 90%);
+ background-color: hsl(195, 80%, 60%);
+ box-shadow: 1px 1px 2px 1px rgba(0, 0, 0, 0.3);
+ } */
+ .saturday {
+ color: #FFFFFF;
+ background-color: hsl(297, 25%, 45%);
+ }
+ .sunday, .localholiday {
+ color: #FFFFFF;
+ background-color: hsl(297, 80%, 40%);
+ }
+ .past {
+ color: hsl(0, 0%, 55%);
+ background-color: hsl(0, 0%, 90%);
+ }
+
+ .editmode {
+ border-radius: 0 !important;
+ box-shadow: 0 4px 16px rgba(0,0,0,0.2) !important;
+ }
+
+.calendar_day {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-content: center;
+ width: auto;
+ height: 43px;
+ border: 0px solid #000;
+}
+.feiertagsname {
+ position: absolute;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ margin-top: 43px;
+ padding: 0px 1px;
+ /*yphens: auto; */ /* Textumbruch erstmal nicht*/
+ font-size: 0.55em;
+ text-align: center;
+ width: auto;
+ color: hsl(297, 100%, 20%);
+ border: 1px solid hsl(297, 30%, 90%);
+ z-index: 5;
+}
+
+
+
+/* Verfügbarkeit: Kalendertag */
+td.greenbox {
+ box-shadow: 4px 4px 4px hsl(146,50%,50%),
+ 4px -4px 4px hsl(146,70%,30%),
+ -4px 4px 4px hsl(146,30%,70%),
+ -4px -4px 4px hsl(146,50%,30%);
+}
+
+
+/* Media Query für breite Bildschirme */
+
+@media (min-width: 900px) {
+ .content .active {
+ flex-direction: row;
+ }
+ .page_calendar {
+ max-width: 600px;
+ margin: 5px auto;
+ }
+}
+
+/* Anpassungen für kleinere Bildschirme */
+@media (max-width: 900px) {
+ #seelsorge.menue .list {
+ justify-content: space-evenly;
+ padding: 0;
+ margin: 0;
+
+ }
+ #seelsorge.menue .menu-item {
+ padding: 5px 2vw 5px 2vw;
+ margin: 0.125vh 1vw 0.0125vh 1vw;
+ max-width: 24vw;
+ font-size: 80%;
+ }
+ .content_seelsorge {
+ padding: 0px;
+ margin-top: 0px;
+ width: calc(100% - 0px - 0px);
+ border: 0px solid #DDDDDD;
+ border-radius: 0;
+ background-color: transparent;
+ }
+ .content .active {
+ flex-direction: column;
+ align-items: center;
+ }
+ .page_calendar {
+ width: calc(100vw - 30px - 2px);
+ max-width: 750px;
+ flex: 0 0 100%;
+ padding: 0px;
+ transform: scale(0.99);
+ overflow-x: hidden;
+ }
+
+ .calendar_content {
+ font-size: 1.2em;
+ }
+ .feiertagsname {
+ font-size: 0.5em;
+ border: 1px solid hsl(297, 30%, 90%);
+ }
+}
+@media (max-width: 600px) {
+ ul.galery {
+ justify-content: space-evenly;
+ padding: 0px;
+ margin: 5px;
+ width:auto;
+ list-style: none;
+ }
+}
+@media (max-width: 400px) {
+ #seelsorge.menue .list {
+ justify-content: space-evenly;
+ }
+ #seelsorge.menue .menu-item {
+ padding: 5px 5px 5px 5px;
+ margin: 0px 5px 0px 5px;
+ max-width: 24vw;
+ font-size: 77%;
+ }
+ .page_calendar {
+ width: calc(100vw - 10px - 2px);
+
+ max-width: 400px;
+ transform: scale(0.98);
+ overflow-x: hidden;
+ }
+ .calendar_content {
+ font-size: 1.4em;
+ }
+ .calendar_day_item {
+ width: auto;
+ height: 90%;
+ aspect-ratio: 1/1;
+ vertical-align: middle;
+ border-radius: 0;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.12);
+ }
+ .feiertagsname {
+ font-size: 0.30em;
+ text-align: center;
+ max-width: 51px;
+ border: 0px solid hsl(297, 30%, 90%);
+ }
+
+}
+/*
+@media (max-width: 550px) {
+ .page_calendar {
+ max-height: 550px;
+ max-width: 550px;
+ transform: scale(0.9);
+ }
+}
+@media (max-width: 475px) {
+ .page_calendar {
+
+ max-height: 475px;
+ max-width: 475px;
+
+ transform: scale(0.6);
+ }
+} */
+
+.loading-spinner {
+ border: 4px solid hsl(297, 20%, 85%);
+ border-radius: 50%;
+ border-top: 4px solid hsl(297, 60%, 35%);
+ width: 20px;
+ height: 20px;
+ animation: spin 1s linear infinite;
+ margin: auto;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+
+/* ===== Seelsorge Tages-Modal ===== */
+.seelsorge-modal-card {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ width: min(520px, calc(100vw - 24px));
+ max-height: min(780px, calc(100vh - 24px));
+ overflow: hidden;
+ padding: 0;
+ background: #FFFFFF;
+ border-radius: 0;
+ box-shadow: 0 24px 70px rgba(0,0,0,0.36);
+}
+.seelsorge-modal-hero {
+ position: relative;
+ display: grid;
+ grid-template-columns: 64px 1fr;
+ gap: 16px;
+ align-items: center;
+ padding: 24px;
+ color: #FFFFFF;
+ flex-shrink: 0;
+ background:
+ radial-gradient(circle at 10% 0%, rgba(255,255,255,0.24), transparent 32%),
+ linear-gradient(135deg, hsl(297, 60%, 35%), hsl(297, 80%, 22%));
+}
+.seelsorge-modal-hero-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 56px;
+ height: 56px;
+ border-radius: 14px;
+ background: rgba(255,255,255,0.15);
+ box-shadow: 0 8px 20px rgba(0,0,0,0.2);
+}
+.seelsorge-modal-title {
+ margin: 0 42px 4px 0;
+ font-size: 1.35em;
+ font-weight: 850;
+ color: #FFFFFF;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-break: break-word;
+}
+.seelsorge-modal-subtitle {
+ display: flex;
+ flex-flow: row wrap;
+ gap: 8px;
+ align-items: center;
+ margin-top: 4px;
+ color: rgba(255,255,255,0.86);
+ font-weight: 600;
+}
+.seelsorge-modal-chip {
+ display: inline-flex;
+ align-items: center;
+ padding: 4px 9px;
+ border-radius: 9999px;
+ background: rgba(255,255,255,0.14);
+ border: 1px solid rgba(255,255,255,0.18);
+ font-size: 0.82em;
+}
+.seelsorge-chip-holiday {
+ background: rgba(255,200,50,0.2);
+ border-color: rgba(255,200,50,0.3);
+}
+.seelsorge-modal-card .exit {
+ position: absolute;
+ top: 14px;
+ right: 14px;
+ z-index: 1;
+ width: 34px;
+ height: 34px;
+ border-radius: 0;
+ border: 1px solid rgba(255,255,255,0.25);
+ background: rgba(255,255,255,0.16);
+ color: #FFFFFF;
+ font-size: 1.35em;
+ line-height: 1;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.seelsorge-modal-card .exit:hover {
+ background: rgba(255,255,255,0.3);
+}
+.seelsorge-modal-body {
+ padding: 22px 24px 24px 24px;
+ overflow-y: auto;
+ flex: 1;
+ min-height: 0;
+}
+.seelsorge-modal-section {
+ margin-bottom: 18px;
+}
+.seelsorge-modal-field {
+ margin-bottom: 12px;
+}
+.seelsorge-modal-field-label {
+ font-size: 0.82em;
+ font-weight: 850;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: hsl(297, 20%, 40%);
+ margin-bottom: 4px;
+}
+.seelsorge-modal-field-value {
+ font-size: 0.95em;
+ color: hsl(210, 20%, 20%);
+ word-break: break-word;
+}
+.seelsorge-section-title {
+ margin: 0 0 10px 0;
+ font-size: 0.9em;
+ font-weight: 850;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: hsl(297, 20%, 40%);
+}
+.seelsorge-modal-empty {
+ padding: 16px 0;
+ text-align: center;
+ color: hsl(210, 12%, 50%);
+ font-weight: 600;
+}
+.seelsorge-modal-loading {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: 40px 0;
+}
+.seelsorge-modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+ border-top: 1px solid hsl(297, 18%, 90%);
+ padding-top: 16px;
+ margin-top: 8px;
+}
+.seelsorge-action-button {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 40px;
+ padding: 10px 20px;
+ border: 1px solid transparent;
+ border-radius: 0;
+ font-size: 0.9em;
+ font-weight: 850;
+ cursor: pointer;
+ transition: background 0.18s ease, transform 0.18s ease;
+}
+.seelsorge-add-button {
+ color: #FFFFFF;
+ background: linear-gradient(135deg, hsl(297, 60%, 35%), hsl(297, 80%, 28%));
+ border-color: hsl(297, 80%, 28%);
+ box-shadow: 0 4px 12px rgba(115,30,125,0.2);
+}
+.seelsorge-add-button:hover {
+ background: linear-gradient(135deg, hsl(297, 65%, 40%), hsl(297, 80%, 32%));
+ transform: translateY(-1px);
+}
+.seelsorge-remove-button {
+ color: hsl(352, 70%, 40%);
+ background: hsl(352, 72%, 97%);
+ border-color: hsl(352, 72%, 88%);
+ box-shadow: 0 4px 12px rgba(0,0,0,0.06);
+}
+.seelsorge-remove-button:hover {
+ background: hsl(352, 72%, 93%);
+ transform: translateY(-1px);
+}
+
+/* Galerie im Modal - Legacy (unused, kept for compatibility) */
+.seelsorge-modal-body ul.galery {
+ justify-content: flex-start;
+}
+
+/* ===== Diakon-Karten im Modal ===== */
+.diakon-grid {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+.diakon-card {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 10px 14px;
+ background: #FFFFFF;
+ border: 1px solid #e9ecef;
+ border-radius: 0;
+ cursor: pointer;
+ transition: background 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
+ box-shadow: 0 2px 6px rgba(0,0,0,0.04);
+}
+.diakon-card:hover {
+ background: hsl(297, 40%, 97%);
+ box-shadow: 0 4px 16px rgba(115,30,125,0.12);
+ transform: translateY(-1px);
+}
+.diakon-card-img {
+ width: 48px;
+ height: 48px;
+ object-fit: cover;
+ border-radius: 6px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.10);
+ flex-shrink: 0;
+}
+.diakon-card-info {
+ flex: 1;
+ min-width: 0;
+}
+.diakon-card-name {
+ font-size: 0.92em;
+ font-weight: 700;
+ color: #212529;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.diakon-card-role {
+ font-size: 0.78em;
+ font-weight: 600;
+ color: hsl(297, 30%, 50%);
+ margin-top: 2px;
+}
+.diakon-card-arrow {
+ flex-shrink: 0;
+ color: hsl(297, 20%, 65%);
+ display: flex;
+ align-items: center;
+ transition: color 0.18s ease;
+}
+.diakon-card:hover .diakon-card-arrow {
+ color: hsl(297, 60%, 35%);
+}
+
+/* ===== Hilfe-Modal Farblegende ===== */
+.calendar-help-legend {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+.calendar-help-legend-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ font-size: 0.88em;
+ color: #374151;
+}
+.calendar-help-swatch {
+ flex-shrink: 0;
+ width: 28px;
+ height: 28px;
+ border-radius: 0;
+ box-shadow: 0 2px 6px rgba(0,0,0,0.12);
+}
+
+@media (max-width: 480px) {
+ .seelsorge-modal-card {
+ width: calc(100vw - 12px);
+ max-height: min(80vh, calc(100vh - 40px));
+ }
+ .seelsorge-modal-hero {
+ grid-template-columns: 48px 1fr;
+ gap: 12px;
+ padding: 18px;
+ }
+ .seelsorge-modal-hero-icon {
+ width: 44px;
+ height: 44px;
+ }
+ .seelsorge-modal-title {
+ font-size: 1.15em;
+ }
+ .seelsorge-modal-body {
+ padding: 16px;
+ }
+ .diakon-card {
+ gap: 10px;
+ padding: 8px 10px;
+ }
+ .diakon-card-img {
+ width: 40px;
+ height: 40px;
+ }
+ .diakon-card-name {
+ font-size: 0.85em;
+ }
+}
diff --git a/web/intranet_repo/projects/_central/php/footer.php b/web/intranet_repo/projects/_central/php/footer.php
new file mode 100644
index 0000000..4920841
--- /dev/null
+++ b/web/intranet_repo/projects/_central/php/footer.php
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/php/header.php b/web/intranet_repo/projects/_central/php/header.php
new file mode 100644
index 0000000..7f8b8a4
--- /dev/null
+++ b/web/intranet_repo/projects/_central/php/header.php
@@ -0,0 +1,47 @@
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/php/html_head.php b/web/intranet_repo/projects/_central/php/html_head.php
new file mode 100644
index 0000000..f28f27e
--- /dev/null
+++ b/web/intranet_repo/projects/_central/php/html_head.php
@@ -0,0 +1,18 @@
+... */
+
+
+?>
+
+
+
+
+
+
+
+
+
+
+ NAK Berlin-Reinickendorf
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/php/snippets/button_edit.php b/web/intranet_repo/projects/_central/php/snippets/button_edit.php
new file mode 100644
index 0000000..2e84b38
--- /dev/null
+++ b/web/intranet_repo/projects/_central/php/snippets/button_edit.php
@@ -0,0 +1,26 @@
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/php/snippets/contact_modal.php b/web/intranet_repo/projects/_central/php/snippets/contact_modal.php
new file mode 100644
index 0000000..96b1ac6
--- /dev/null
+++ b/web/intranet_repo/projects/_central/php/snippets/contact_modal.php
@@ -0,0 +1,119 @@
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/_central/php/snippets/countdown.php b/web/intranet_repo/projects/_central/php/snippets/countdown.php
new file mode 100644
index 0000000..f0d7061
--- /dev/null
+++ b/web/intranet_repo/projects/_central/php/snippets/countdown.php
@@ -0,0 +1,30 @@
+
+
+120
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/php/snippets/overlay_addNewUser.php b/web/intranet_repo/projects/_central/php/snippets/overlay_addNewUser.php
new file mode 100644
index 0000000..101f1f0
--- /dev/null
+++ b/web/intranet_repo/projects/_central/php/snippets/overlay_addNewUser.php
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
Neuen Nutzer anlegen
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/php/snippets/overlay_message.php b/web/intranet_repo/projects/_central/php/snippets/overlay_message.php
new file mode 100644
index 0000000..723c7a9
--- /dev/null
+++ b/web/intranet_repo/projects/_central/php/snippets/overlay_message.php
@@ -0,0 +1,73 @@
+-Beginn. */
+
+
+ // Mapping aller Intranet-Messageboxen mit Typ und Ausgabetext
+ function getMessageInfo($message) {
+ return match ($message) {
+ 1 => ['type' => 'success', 'text' => "Passwort zurückgesetzt."],
+ 2 => ['type' => 'success', 'text' => "Nutzer entfernt."],
+ 3 => ['type' => 'success', 'text' => "Nutzer angelegt."],
+ 4 => ['type' => 'error', 'text' => "Nutzer existiert bereits."],
+ 5 => ['type' => 'success', 'text' => "Speicherung erfolgt."],
+ 6 => ['type' => 'success', 'text' => "Einladungsmail gesendet."],
+ 7 => ['type' => 'error', 'text' => "Bitte Name und E-Mail ausfüllen."],
+ 8 => ['type' => 'success', 'text' => "Brandschutzschulung abgeschlossen. Vielen Dank für deine Teilnahme!"],
+
+ default => ['type' => 'error', 'text' => "Unbekannter Fehler."],
+ };
+ }
+
+
+ // Laden der Message
+ if (isset($_GET["message"])) {
+ $message = intval($_GET["message"]);
+ $messageInfo = getMessageInfo($message);
+ $toastType = $messageInfo['type'];
+ $toastText = $messageInfo['text'];
+
+?>
+
+
+
= htmlspecialchars($toastText) ?>
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/php/snippets/overlay_profile.php b/web/intranet_repo/projects/_central/php/snippets/overlay_profile.php
new file mode 100644
index 0000000..ee36d82
--- /dev/null
+++ b/web/intranet_repo/projects/_central/php/snippets/overlay_profile.php
@@ -0,0 +1,81 @@
+
+
+query($sql);
+ $partner = $result->fetch(PDO::FETCH_ASSOC);
+
+ if (isset($partner["mail"])) {
+ $sql_groups = "SELECT * FROM user_permissions WHERE user_id = ?";
+ $stmt = $pdo->prepare($sql_groups);
+ $stmt->execute([$_SESSION["profile_id"]]);
+ $groups = $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+ }
+ else {
+ header("Location:{$g_httpLoginPath}");
+ exit();
+ }
+
+ // Profile image
+ $profileImg = glob($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/profile_pictures/{$_SESSION["profile_id"]}-*.jpg");
+ if (!empty($profileImg)) {
+ usort($profileImg, function($a, $b) { return filemtime($b) - filemtime($a); });
+ $profileImgUrl = str_replace($_SERVER['DOCUMENT_ROOT'], '', $profileImg[0]);
+ } else {
+ $profileImgUrl = "/intranet_repo/general_files/profile_pictures/default.jpg";
+ }
+
+ $fullName = trim(($partner["first_name"] ?? '') . ' ' . ($partner["last_name"] ?? ''));
+ $isRegistered = !empty($partner["first_name"]);
+?>
+
+
+
×
+
+
+
+
+
+
= htmlspecialchars($fullName) ?>
+
+
Nicht registriert
+
+
+ = htmlspecialchars($partner["mail"]) ?>
+
+
+
+
+
+
+
+
+
+
+
+
Gruppen
+
+ $value) {
+ if ($value == 1) {
+ echo '' . htmlspecialchars($key) . ' ';
+ }
+ }
+ ?>
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/scripts/intranet.js b/web/intranet_repo/projects/_central/scripts/intranet.js
new file mode 100644
index 0000000..530a4a1
--- /dev/null
+++ b/web/intranet_repo/projects/_central/scripts/intranet.js
@@ -0,0 +1,199 @@
+ ////==========================================//
+ //// GLOBALER BEREICH //
+ ////////////////////////////////////////////////
+//// Globale Konstanten
+ const url = new URL(window.location.href);
+ const basisUrl = url.origin + url.pathname;
+ ////
+ ////////////////////////////////////////////////
+////==========================================//
+
+
+
+ ////==========================================//
+ //// FUNKTIONEN und LISTENER //
+ ////////////////////////////////////////////////
+////
+
+// Position der Elemente – min-height wird jetzt vollständig per CSS gelöst (nak-design.css)
+// Die alte JS-Berechnung wurde entfernt, da sie nach dem Laden die Höhe reduzierte.
+
+
+// JavaScript zum Erkennen der Scroll-Richtung und zum Umschalten der Klasse
+// Funktioniert sowohl bei Body-Scroll als auch bei internem Container-Scroll (z.B. eventbox_table)
+var lastScrollTop = 0;
+var lastContainerScrollTop = 0;
+
+function handleHeaderScroll(scrollTop, lastScroll) {
+ const elemHeaderMenue = document.getElementById('header_menue');
+ const elemHeaderTitle = document.getElementById('header_title');
+
+ // Dynamische Höhe aus CSS-Variable lesen
+ const headerHeight = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 64;
+
+ // Kein Einfahren, wenn der Content auf die Seite passt (kein Scrollen nötig)
+ if (document.documentElement.scrollHeight <= window.innerHeight) {
+ return;
+ }
+
+ // Am unteren Ende der Seite: Header wieder einblenden
+ const atBottom = (window.innerHeight + window.scrollY) >= (document.documentElement.scrollHeight - 5);
+
+ if (scrollTop > lastScroll && !atBottom) {
+ // Menü raus bei Scroll nach unten
+ if (elemHeaderMenue) {
+ elemHeaderMenue.style.top = `-${headerHeight}px`;
+ elemHeaderMenue.style.transition = "top 0.3s ease-in-out";
+ }
+ if (elemHeaderTitle) {
+ elemHeaderTitle.style.top = "0";
+ elemHeaderTitle.style.transition = "top 0.3s ease-in-out";
+ }
+ } else {
+ // Menü wieder rein bei Scroll nach oben
+ if (elemHeaderMenue) {
+ elemHeaderMenue.style.top = "0";
+ elemHeaderMenue.style.transition = "top 0.3s ease-in-out";
+ }
+ if (elemHeaderTitle) {
+ elemHeaderTitle.style.top = `${headerHeight}px`;
+ elemHeaderTitle.style.transition = "top 0.3s ease-in-out";
+ }
+ }
+}
+
+// Body/Document Scroll
+document.addEventListener("scroll", function() {
+ var st = window.scrollY || document.documentElement.scrollTop;
+ handleHeaderScroll(st, lastScrollTop);
+ lastScrollTop = st <= 0 ? 0 : st;
+}, false);
+
+// Auch bei internem Container-Scroll (eventbox_table, display_within_dev)
+document.addEventListener("DOMContentLoaded", function() {
+ const scrollContainers = document.querySelectorAll('.eventbox_table, form.display_within_dev');
+ scrollContainers.forEach(function(container) {
+ var lastCST = 0;
+ container.addEventListener("scroll", function() {
+ var st = container.scrollTop;
+ handleHeaderScroll(st, lastCST);
+ lastCST = st <= 0 ? 0 : st;
+ }, false);
+ });
+});
+
+
+
+
+
+
+function openCloseHeaderProfile() {
+ if (window.innerWidth >= 769) {
+ // Desktop: click on profile image goes to profile page
+ window.location.href = '/intranet_repo/projects/intranet/profile.php';
+ } else {
+ // Mobile: toggle expand/collapse
+ const hpElem = document.getElementById('header_profile');
+ hpElem.classList.toggle('header_profile_slideLeft');
+ }
+}
+
+function openCloseOverlay(id) {
+ //Overlay ein-/ausblenden - beim Ausblenden zurück auf eigene Seite
+ const elem = document.getElementById(id);
+
+ if (elem.style.display === "flex") {
+ elem.style.display = "none";
+ } else {
+ elem.style.display = "flex";
+ }
+}
+
+function openCloseProfile(id, name, overlay) {
+// Profil per Klick öffnen/schließen, alle anderen immer schließen
+
+ const profiles = document.getElementsByName(name); //Ausgewählter Profilbereich (visit, calendar, ...)
+ const oProfile = document.getElementById(overlay); //Ausgewähltes Overlay
+ const aProfile = document.getElementById(id); //Ausgewähltes Profil
+ const aId = (id > 0) ? aProfile.id : 0; //Ausweichen, wenn kein Profil gewählt
+
+ console.log('id:' + id);
+ console.log('overlay:' + overlay);
+ console.log('name:' + name);
+
+ if (oProfile.style.display === "flex") { //Overlay ein-/ausblenden
+ oProfile.style.display = "none";
+ } else {
+ oProfile.style.display = "flex";
+ //TODO: folgender ScrollTop greift nicht, WARUM?
+ //oProfile.scrollTop = 0; //Hinscrollen zum Beginn des Overlays, vor allem bei @Media
+ }
+
+ for (let i = 0; i < profiles.length; i++) {
+
+ const cProfile = profiles[i]; //Aktuelles Profil (Style: profile_container)
+
+ if (cProfile.id === aId && cProfile.style.display !== "block") {
+ profiles[i].style.display = "block";
+ } else {
+ profiles[i].style.display = "none";
+ }
+ }
+ document.activeElement.blur(); //Fokus entfernen
+
+ // TEST console.log(id);
+}
+
+
+
+function mix(content) { //wird aktuell nicht eingesetzt, Daten werden per SQL randomized
+ const list = document.getElementById(content);
+ const items = Array.from(list.children);
+ console.log('Vor dem Mischen:', items.map(item => item.textContent || item.innerText));
+
+ for (let i = items.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [items[i], items[j]] = [items[j], items[i]];
+ }
+
+ console.log('Nach dem Mischen:', items.map(item => item.textContent || item.innerText));
+ items.forEach(item => list.appendChild(item));
+}
+
+
+function showHideText(element) {
+ element.classList.toggle("open");
+}
+
+
+
+// Per Taste Overlay verlassen
+document.addEventListener('keydown', function(event) {
+ const profileVisitOverlay = document.getElementById('profile_visit_overlay');
+ const profileCalendarOverlay = document.getElementById('profile_calendar_overlay');
+ const addNewUser = document.getElementById('addNewUser');
+ const showMessage = document.getElementById('showMessage');
+
+ if (event.key === 'Escape') {
+ if (profileVisitOverlay && profileVisitOverlay.style.display === 'flex') {
+ openCloseProfile(0, 'profile_visit', 'profile_visit_overlay');
+ }
+ if (profileCalendarOverlay && profileCalendarOverlay.style.display === 'flex') {
+ openCloseProfile(0, 'profile_calendar', 'profile_calendar_overlay');
+ }
+ if (addNewUser && addNewUser.style.display === 'flex') {
+ openCloseOverlay('addNewUser');
+ }
+ } else if (event.key === 'Enter' || event.key === 'Return' || event.key === " ") {
+ if (showMessage && showMessage.style.display === 'flex') {
+ openCloseOverlay('showMessage'); // ausblenden
+ window.location.href=basisUrl; // zur aktuellen Seite zurückkehren
+ }
+ }
+});
+
+// Fokus auf passendes Element beim Laden der Intranet-Seiten
+document.addEventListener("DOMContentLoaded", function() {
+ document.querySelector("#admintable tr[tabindex='0']")?.focus(); // Adminpanel
+ document.querySelector("#visittable tr[tabindex='0']")?.focus(); // Besuchsstellen
+});
\ No newline at end of file
diff --git a/web/intranet_repo/projects/_central/scripts/seelsorge.js b/web/intranet_repo/projects/_central/scripts/seelsorge.js
new file mode 100644
index 0000000..5544a1f
--- /dev/null
+++ b/web/intranet_repo/projects/_central/scripts/seelsorge.js
@@ -0,0 +1,247 @@
+ ////==========================================//
+ ////GLOBALER BEREICH//
+ ////////////////////////////////////////////////
+////Globale Konstanten
+
+////
+////////////////////////////////////////////////
+////==========================================//
+
+
+
+ ////==========================================//
+ ////FUNKTIONEN und LISTENER//
+ ////////////////////////////////////////////////
+////
+
+
+// Warten, bis das DOM vollstaendig geladen ist
+document.addEventListener('DOMContentLoaded', function() {
+ var menuItems = document.querySelectorAll('.content-tab-bar__item[data-target]');
+
+ // Event-Listener fuer Klickereignisse auf Menueintraege
+ menuItems.forEach(function(item) {
+ item.addEventListener('click', function() {
+ // Entferne die aktive Klasse von allen Menueintraegen
+ menuItems.forEach(function(menuItem) {
+ menuItem.classList.remove('active');
+ });
+
+ // Fuege die aktive Klasse zum geklickten Menuelement hinzu
+ item.classList.add('active');
+
+ // Verstecke alle Inhaltsbereiche
+ document.querySelectorAll('.content_seelsorge').forEach(function(section) {
+ section.classList.remove('active');
+ });
+
+ // Zeige den entsprechenden Inhalt an
+ var contentId = item.getAttribute('data-target');
+ document.getElementById(contentId).classList.add('active');
+ });
+ });
+
+ // ESC-Taste schliesst offene Modals
+ document.addEventListener('keydown', function(e) {
+ if (e.key === 'Escape') {
+ closeCalendarHelp();
+ closeDayModal();
+ }
+ });
+});
+
+
+
+// Wechsel der Kalenderblaetter Monat 1 und 2
+function switchCalendar(type) {
+ const calendar_header_1 = document.querySelector('#calendar_header_1');
+ const calendar_header_2 = document.querySelector('#calendar_header_2');
+ const calendar_1 = document.querySelectorAll('[name="calendar_1"]');
+ const calendar_2 = document.querySelectorAll('[name="calendar_2"]');
+
+ const isLeft = type === "left";
+ calendar_header_1.style.display = isLeft ? "flex" : "none";
+ calendar_header_2.style.display = isLeft ? "none" : "flex";
+
+ calendar_1.forEach(cal => cal.style.visibility = isLeft ? "visible" : "collapse");
+ calendar_2.forEach(cal => cal.style.visibility = isLeft ? "collapse" : "visible");
+}
+
+
+// Tages-Modal oeffnen
+async function openDayModal(calendarDate) {
+ if (!calendarDate) return;
+
+ const modal = document.getElementById('seelsorge-day-modal');
+ const body = document.getElementById('modal-day-body');
+ const title = document.getElementById('modal-day-title');
+ const subtitle = document.getElementById('modal-day-subtitle');
+
+ if (!modal || !body || !title || !subtitle) {
+ console.error('openDayModal: Modal-Elemente nicht gefunden.');
+ return;
+ }
+
+ // Modal sofort anzeigen mit Ladezustand
+ title.textContent = 'Laden...';
+ subtitle.innerHTML = '';
+ body.innerHTML = '';
+ modal.style.display = 'flex';
+
+ try {
+ const response = await fetch('content/calendar_day.php', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ body: 'calendarDate=' + encodeURIComponent(calendarDate)
+ });
+
+ const html = await response.text();
+
+ if (response.ok) {
+ body.innerHTML = html;
+
+ // Metadaten aus dem eingebetteten JSON lesen
+ const metaEl = body.querySelector('#modal-meta');
+ if (metaEl) {
+ const meta = JSON.parse(metaEl.textContent);
+ title.textContent = meta.title;
+
+ let chips = 'KW ' + meta.kw + ' ';
+ if (meta.feiertag) {
+ chips += '' + meta.feiertag + ' ';
+ }
+ subtitle.innerHTML = chips;
+ metaEl.remove();
+ }
+ } else {
+ // PHP-Fehler direkt im Modal anzeigen
+ title.textContent = 'Fehler ' + response.status;
+ body.innerHTML = ''
+ + (html || 'Keine Antwort vom Server.') + '
';
+ }
+ } catch (error) {
+ body.innerHTML = 'Fehler beim Laden der Daten: ' + error.message + '
';
+ console.error('Fehler beim Laden der Tagesdetails:', error);
+ }
+}
+
+
+// Modal schliessen
+function closeDayModal() {
+ const modal = document.getElementById('seelsorge-day-modal');
+ if (modal) modal.style.display = 'none';
+}
+
+// Backdrop-Klick schliesst Modal
+function closeDayModalOnBackdrop(event) {
+ if (event.target === event.currentTarget) {
+ closeDayModal();
+ }
+}
+
+
+// Verfuegbarkeit im Modal togglen (Modal bleibt offen)
+async function toggleAvailabilityModal(date) {
+ const btn = document.getElementById('modal-toggle-btn');
+ if (!btn) return;
+
+ const originalText = btn.textContent;
+ btn.disabled = true;
+ btn.textContent = 'Wird gespeichert\u2026';
+
+ try {
+ const response = await fetch("toggle_availability.php", {
+ method: "POST",
+ headers: { "Content-type": "application/x-www-form-urlencoded" },
+ body: "date=" + encodeURIComponent(date)
+ });
+
+ const data = await response.json();
+
+ if (data.ok) {
+ // Modal-Inhalt neu laden um aktuellen Stand zu zeigen
+ await openDayModal(date);
+
+ // Kalender-Tageszelle visuell aktualisieren
+ updateCalendarCell(date, data.nowAvailable, data.availCount);
+ } else {
+ btn.textContent = originalText;
+ btn.disabled = false;
+ console.error("Fehler:", data.error);
+ }
+ } catch (error) {
+ btn.textContent = originalText;
+ btn.disabled = false;
+ console.error("Fehler beim Aktualisieren der Verfuegbarkeit:", error);
+ }
+}
+
+// Kalender-Tageszelle visuell aktualisieren ohne Page-Reload
+function updateCalendarCell(date, nowAvailable, availCount) {
+ var cells = document.querySelectorAll('#calendar_day_item');
+ cells.forEach(function(cell) {
+ var onclick = cell.getAttribute('onclick') || '';
+ if (onclick.indexOf(date) === -1) return;
+
+ // Alle Verfuegbarkeits-Klassen entfernen
+ cell.classList.remove('available_single', 'available_multiple', 'normalday');
+
+ if (availCount >= 2) {
+ cell.classList.add('available_multiple');
+ } else if (availCount === 1) {
+ cell.classList.add('available_single');
+ } else {
+ cell.classList.add('normalday');
+ }
+ });
+}
+
+
+// Profil eines Diakons aus dem Kalender-Modal oeffnen
+var _lastCalendarDate = null;
+
+function openCalendarProfile(userId) {
+ // Datum merken, damit wir beim Schliessen zurueckkehren koennen
+ var metaEl = document.getElementById('modal-meta');
+ var bodyEl = document.getElementById('modal-day-body');
+ if (!_lastCalendarDate && bodyEl) {
+ // Datum aus dem Toggle-Button extrahieren
+ var toggleBtn = bodyEl.querySelector('#modal-toggle-btn');
+ if (toggleBtn) {
+ var onclickAttr = toggleBtn.getAttribute('onclick') || '';
+ var match = onclickAttr.match(/'(\d{4}-\d{2}-\d{2})'/);
+ if (match) _lastCalendarDate = match[1];
+ }
+ }
+ closeDayModal();
+ openCloseProfile(userId, 'profile_calendar', 'profile_calendar_overlay');
+}
+
+// Globalen Listener fuer Kalender-Profil-Overlay: bei Schliessen Tagesmodal wieder oeffnen
+document.addEventListener('DOMContentLoaded', function() {
+ // MutationObserver auf das Kalender-Profil-Overlay
+ var calOverlay = document.getElementById('profile_calendar_overlay');
+ if (calOverlay) {
+ var observer = new MutationObserver(function(mutations) {
+ mutations.forEach(function(m) {
+ if (m.attributeName === 'style' && calOverlay.style.display === 'none' && _lastCalendarDate) {
+ var date = _lastCalendarDate;
+ _lastCalendarDate = null;
+ openDayModal(date);
+ }
+ });
+ });
+ observer.observe(calOverlay, { attributes: true, attributeFilter: ['style'] });
+ }
+});
+
+
+// Hilfe-Modal oeffnen/schliessen
+function openCalendarHelp() {
+ var modal = document.getElementById('calendar-help-modal');
+ if (modal) modal.style.display = 'flex';
+}
+function closeCalendarHelp() {
+ var modal = document.getElementById('calendar-help-modal');
+ if (modal) modal.style.display = 'none';
+}
\ No newline at end of file
diff --git a/web/intranet_repo/projects/adminpanel/_tabs.php b/web/intranet_repo/projects/adminpanel/_tabs.php
new file mode 100644
index 0000000..06d0f29
--- /dev/null
+++ b/web/intranet_repo/projects/adminpanel/_tabs.php
@@ -0,0 +1,32 @@
+query("SELECT COUNT(*) FROM admin_messages WHERE status = 'pending'")->fetchColumn();
+} catch (Throwable $e) {
+ // Die Navigation soll auch dann benutzbar bleiben, wenn das Postfach temporär nicht verfügbar ist.
+ $adminInboxBadgeCount = 0;
+}
+
+$adminTabs = [
+ 'users' => ['label' => 'Nutzerverwaltung', 'href' => 'admin.php'],
+ 'inbox' => ['label' => 'Postfach', 'href' => 'admin_inbox.php', 'badge' => $adminInboxBadgeCount],
+ 'kalli' => ['label' => 'Kalli', 'href' => 'admin_kalli.php'],
+ 'brandschutz' => ['label' => 'Brandschutz', 'href' => 'admin_brandschutz.php'],
+ 'notices' => ['label' => 'Meldungen', 'href' => 'admin_notices.php'],
+];
+?>
+
diff --git a/web/intranet_repo/projects/adminpanel/admin.php b/web/intranet_repo/projects/adminpanel/admin.php
new file mode 100644
index 0000000..321d76e
--- /dev/null
+++ b/web/intranet_repo/projects/adminpanel/admin.php
@@ -0,0 +1,1304 @@
+query($sql);
+
+ while ($column = $result->fetch(PDO::FETCH_ASSOC)) {
+ $field = $column['column_name'];
+ if (!in_array($field, ['p_id', 'user_id'], true)) {
+ $roles[] = $field;
+ }
+ }
+
+ return $roles;
+ }
+
+ function quoteIdentifier(string $identifier): string {
+ return '"' . str_replace('"', '""', $identifier) . '"';
+ }
+
+ function getProfileImageUrl(array $group, string $g_profilePicturePath): string {
+ $profileImg = glob($g_profilePicturePath."{$group['user_id']}-*.jpg");
+ if (!empty($profileImg)) {
+ usort($profileImg, function($a, $b) {
+ return filemtime($b) - filemtime($a);
+ });
+ $latestProfileImg = $profileImg[0];
+ return str_replace($_SERVER['DOCUMENT_ROOT'], '', $latestProfileImg);
+ }
+
+ return '/intranet_repo/general_files/profile_pictures/default.jpg';
+ }
+
+ function getDisplayName(array $group): string {
+ $fullName = trim(($group['first_name'] ?? '') . ' ' . ($group['last_name'] ?? ''));
+ return $fullName !== '' ? $fullName : ($group['mail'] ?? 'Unbekannt');
+ }
+
+ $permissionRoles = getPermissionRoles($pdo);
+
+ /* Besondere Admin-Rechte checken */
+ $stmt = $pdo->prepare("SELECT up.* FROM user_permissions up WHERE up.user_id = ? LIMIT 1");
+ $stmt->execute([$_SESSION["user_id"]]);
+ $isAdmin = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!$isAdmin || (int)($isAdmin["Admin"] ?? 0) !== 1) {
+ header("Location: {$g_httpHomePath}");
+ exit();
+ }
+
+ // Neuen Nutzer inklusive Rollen anlegen
+ if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["addRecord"])) {
+ $newMail = trim($_POST["newMail"] ?? '');
+ $newFirstName = trim($_POST["newFirstName"] ?? '');
+ $newLastName = trim($_POST["newLastName"] ?? '');
+ $selectedRoles = $_POST["newRoles"] ?? [];
+ if (!is_array($selectedRoles)) {
+ $selectedRoles = [];
+ }
+
+ if (empty($newMail) || empty($newFirstName) || empty($newLastName)) {
+ header("Location: admin.php?message=7");
+ exit();
+ }
+
+ $checkSql = "SELECT COUNT(*) FROM user_general_information WHERE mail=?";
+ $checkStmt = $pdo->prepare($checkSql);
+ $checkStmt->execute([$newMail]);
+ $count = $checkStmt->fetchColumn();
+
+ if ($count > 0) {
+ header("Location: admin.php?message=4");
+ exit();
+ }
+
+ $pdo->beginTransaction();
+ try {
+ $insertSql = "INSERT INTO user_general_information (mail, first_name, last_name) VALUES (?, ?, ?)";
+ $stmt = $pdo->prepare($insertSql);
+ $stmt->execute([$newMail, $newFirstName, $newLastName]);
+ $newId = $pdo->lastInsertId();
+
+ // Fallback, falls insert_id wegen DB-Konfiguration nicht gesetzt ist
+ if (!$newId) {
+ $checkSql = "SELECT user_id FROM user_general_information WHERE mail=?";
+ $checkStmt = $pdo->prepare($checkSql);
+ $checkStmt->execute([$newMail]);
+ $row = $checkStmt->fetch(PDO::FETCH_ASSOC);
+ $newId = $row['user_id'];
+ }
+
+ $roleColumns = array_map('quoteIdentifier', $permissionRoles);
+ $columns = array_merge(['user_id'], $roleColumns);
+ $placeholders = array_fill(0, count($columns), '?');
+ $roleValues = [];
+ foreach ($permissionRoles as $role) {
+ $roleValues[] = in_array($role, $selectedRoles, true) ? 1 : 0;
+ }
+ $params = array_merge([(int)$newId], $roleValues);
+
+ $insertPermissionsSql = "INSERT INTO user_permissions (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
+ $stmt = $pdo->prepare($insertPermissionsSql);
+ $stmt->execute($params);
+
+ $pdo->commit();
+ } catch (Throwable $e) {
+ $pdo->rollBack();
+ throw $e;
+ }
+
+ // Einladungsmail an den neuen Nutzer senden
+ require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
+ $displayName = trim("{$newFirstName} {$newLastName}");
+ $greeting = !empty($displayName) ? "Hallo {$newFirstName}" : "Hallo";
+ $signupUrl = "https://" . $_SERVER['HTTP_HOST'] . "/intranet_repo/projects/authorization/signup.php";
+
+ $userBetreff = "Einladung zum Intranet NAK Reinickendorf";
+ $userNachricht = "{$greeting},\n\n";
+ $userNachricht .= "Sie wurden zum Intranet der NAK Reinickendorf eingeladen.\n\n";
+ $userNachricht .= "Bitte registrieren Sie sich mit der E-Mail-Adresse {$newMail} über den folgenden Link, um Ihren Zugang zu aktivieren.";
+
+ send_intranet_mail($newMail, $userBetreff, $userNachricht, null, "intranet@nak-reinickendorf.de", "Jetzt registrieren", $signupUrl);
+
+ // Info-Mail an Admins
+ $adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
+ if (empty($adminName)) $adminName = 'Ein Administrator';
+ $nutzerAnzeige = !empty($displayName) ? $displayName : $newMail;
+
+ $adminBetreff = "Neuer Nutzer angelegt: {$nutzerAnzeige}";
+ $adminNachricht = "{$adminName} hat einen neuen Nutzer angelegt:\n\n";
+ $adminNachricht .= "Name: " . (!empty($displayName) ? $displayName : '(kein Name)') . "\n";
+ $adminNachricht .= "E-Mail: {$newMail}\n";
+ $adminNachricht .= "Rollen: " . (!empty($selectedRoles) ? implode(', ', $selectedRoles) : 'Keine');
+
+ send_admin_team_mail($pdo, $adminBetreff, $adminNachricht);
+
+ header("Location: admin.php?message=3");
+ exit();
+ }
+
+ // Einladungsmail erneut senden
+ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["resendInvite"])) {
+ $resendUserId = (int)($_GET["resendInvite"] ?? 0);
+ if ($resendUserId > 0) {
+ require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
+ $stmt = $pdo->prepare("SELECT mail, first_name, last_name FROM user_general_information WHERE user_id = ?");
+ $stmt->execute([$resendUserId]);
+ $invUser = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($invUser) {
+ $displayName = trim(($invUser['first_name'] ?? '') . ' ' . ($invUser['last_name'] ?? ''));
+ $greeting = !empty($invUser['first_name']) ? "Hallo {$invUser['first_name']}" : "Hallo";
+ $signupUrl = "https://" . $_SERVER['HTTP_HOST'] . "/intranet_repo/projects/authorization/signup.php";
+
+ $userBetreff = "Erinnerung: Einladung zum Intranet NAK Reinickendorf";
+ $userNachricht = "{$greeting},\n\n";
+ $userNachricht .= "Sie wurden zum Intranet der NAK Reinickendorf eingeladen und haben sich noch nicht registriert.\n\n";
+ $userNachricht .= "Bitte registrieren Sie sich mit der E-Mail-Adresse {$invUser['mail']} über den folgenden Link, um Ihren Zugang zu aktivieren.";
+
+ send_intranet_mail($invUser['mail'], $userBetreff, $userNachricht, null, "intranet@nak-reinickendorf.de", "Jetzt registrieren", $signupUrl);
+ }
+ }
+ header("Location: admin.php?message=6");
+ exit();
+ }
+
+ // Rollen speichern
+ if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["editRecord"])) {
+ $editedID = (int)($_POST["editedID"] ?? 0);
+ $selectedRoles = $_POST["roles"] ?? [];
+ if (!is_array($selectedRoles)) {
+ $selectedRoles = [];
+ }
+
+ // Update name, email and phone in user_general_information
+ $editFirstName = trim($_POST["edit_first_name"] ?? '');
+ $editLastName = trim($_POST["edit_last_name"] ?? '');
+ $editMail = trim($_POST["edit_mail"] ?? '');
+ $editPhone = trim($_POST["edit_phone"] ?? '');
+ $editPhoneVal = ($editPhone !== '') ? $editPhone : null;
+
+ if ($editMail !== '') {
+ $stmtInfo = $pdo->prepare("UPDATE user_general_information SET first_name=?, last_name=?, mail=?, phone=? WHERE user_id=?");
+ $stmtInfo->execute([$editFirstName, $editLastName, $editMail, $editPhoneVal, $editedID]);
+ }
+
+ // Update roles in user_permissions
+ $setParts = [];
+ $params = [];
+ foreach ($permissionRoles as $role) {
+ $setParts[] = quoteIdentifier($role) . "=?";
+ $params[] = in_array($role, $selectedRoles, true) ? 1 : 0;
+ }
+ $params[] = $editedID;
+
+ $updateSql = "UPDATE user_permissions SET " . implode(', ', $setParts) . " WHERE user_id=?";
+ $stmt = $pdo->prepare($updateSql);
+ $stmt->execute($params);
+
+ // Vermerk in erledigte Tasks
+ $stmt = $pdo->prepare("SELECT mail, first_name, last_name FROM user_general_information WHERE user_id = ?");
+ $stmt->execute([$editedID]);
+ $editedUser = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ $adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
+ if (empty($adminName)) $adminName = 'Ein Administrator';
+ $editedUserName = trim(($editedUser['first_name'] ?? '') . ' ' . ($editedUser['last_name'] ?? ''));
+ if (empty($editedUserName)) $editedUserName = $editedUser['mail'] ?? 'Unbekannt';
+ $assignedRoles = !empty($selectedRoles) ? implode(', ', $selectedRoles) : 'Keine';
+
+ $taskSubject = "Profil aktualisiert: {$editedUserName}";
+ $taskMessage = "{$adminName} hat das Profil von {$editedUserName} aktualisiert.\nNeue Rollen: {$assignedRoles}";
+
+ $stmt = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
+ $senderMail = $editedUser['mail'] ?? '';
+ $stmt->execute([$adminName, $senderMail, $taskSubject, $taskMessage]);
+
+ header("Location: admin.php?message=5");
+ exit();
+ }
+
+ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["delete"])) {
+ $deleteUserId = (int)$_GET["delete"];
+
+ $deleteSql = "DELETE FROM user_general_information WHERE user_id=?";
+ $stmt = $pdo->prepare($deleteSql);
+ $stmt->execute([$deleteUserId]);
+
+ header("Location: admin.php?message=2");
+ exit();
+ }
+
+ if ($_SERVER["REQUEST_METHOD"] === "GET" && isset($_GET["resetPassword"])) {
+ $resetUserId = (int)$_GET["resetPassword"];
+
+ // Login-Daten löschen (Name und Telefon bleiben erhalten)
+ $deleteSql = "DELETE FROM user_login WHERE user_id=?";
+ $stmt = $pdo->prepare($deleteSql);
+ $stmt->execute([$resetUserId]);
+
+ // User-Daten für Mail laden
+ $stmt = $pdo->prepare("SELECT mail, first_name, last_name FROM user_general_information WHERE user_id = ?");
+ $stmt->execute([$resetUserId]);
+ $resetUser = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($resetUser) {
+ require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
+ $signupUrl = "https://" . $_SERVER['HTTP_HOST'] . "/intranet_repo/projects/authorization/signup.php";
+ $greeting = !empty($resetUser['first_name']) ? "Hallo {$resetUser['first_name']}" : "Hallo";
+
+ $userBetreff = "Passwort zurückgesetzt";
+ $userNachricht = "{$greeting},\n\n";
+ $userNachricht .= "Ihr Passwort für das Intranet der NAK Reinickendorf wurde von einem Administrator zurückgesetzt.\n\n";
+ $userNachricht .= "Bitte registrieren Sie sich erneut über den folgenden Link, um ein neues Passwort festzulegen.";
+
+ send_intranet_mail($resetUser['mail'], $userBetreff, $userNachricht, null, "intranet@nak-reinickendorf.de", "Jetzt neu registrieren", $signupUrl);
+
+ // Vermerk in erledigte Tasks schreiben
+ $adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
+ if (empty($adminName)) $adminName = 'Ein Administrator';
+ $resetUserName = trim(($resetUser['first_name'] ?? '') . ' ' . ($resetUser['last_name'] ?? ''));
+ if (empty($resetUserName)) $resetUserName = $resetUser['mail'];
+
+ $taskSubject = "Passwort zurückgesetzt: {$resetUserName}";
+ $taskMessage = "{$adminName} hat das Passwort von {$resetUserName} ({$resetUser['mail']}) zurückgesetzt. Eine Registrierungsmail wurde versendet.";
+
+ $stmt = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
+ $stmt->execute([$adminName, $resetUser['mail'], $taskSubject, $taskMessage]);
+ }
+
+ header("Location: admin.php?message=1");
+ exit();
+ }
+
+ if (isset($_GET['partner_id'])) {
+ $_SESSION["profile_id"] = (int)$_GET['partner_id'];
+ header("Location: admin_profiles.php");
+ exit();
+ }
+
+ // Nutzer inklusive aller Rechte laden: Registrierte Nutzer zuerst, dann Name, dann Mail.
+ $sql_groups = "SELECT up.*, ugi.mail, ugi.first_name, ugi.last_name, ugi.phone,
+ CASE WHEN ul.user_id IS NOT NULL THEN 1 ELSE 0 END AS \"IstRegistriert\"
+ FROM user_permissions up
+ LEFT JOIN user_general_information ugi ON up.user_id = ugi.user_id
+ LEFT JOIN user_login ul ON up.user_id = ul.user_id
+ ORDER BY \"IstRegistriert\" DESC, COALESCE(ugi.last_name, ''), COALESCE(ugi.first_name, ''), ugi.mail";
+ $result_groups = $pdo->query($sql_groups);
+ $groups = $result_groups->fetchAll(PDO::FETCH_ASSOC);
+
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Profil
+ Name
+ E-Mail-Adresse
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = htmlspecialchars($nameOutput) ?>
+
+
+
+ = $isRegistered ? 'registriert' : 'nicht registriert' ?>
+
+
+
+ = htmlspecialchars($mailOutput) ?>
+
+
+
+ Keine Nutzer mit dieser Rolle gefunden.
+
+
+
+
+
+
+
+
+
×
+
+
+
+
+
Neuen Nutzer anlegen
+
+ neues Profil
+
+
+
+
+
+
+ Name
+
+
+
+
+
+ E-Mail-Adresse
+
+ Der Nutzer erhält eine Einladungsmail zur Registrierung.
+
+ Rollen vergeben
+
+
+
+
+ = htmlspecialchars(genderRole($role)) ?>
+
+
+
+
+
+
+
+ Abbrechen
+ Nutzer anlegen
+
+
+
+
+
+
+
+
+
+
+
+
×
+
+
+
+
+
= htmlspecialchars($nameOutput) ?>
+
+ = htmlspecialchars($mailOutput) ?>
+ = $isRegistered ? 'Registriertes Profil' : 'Noch nicht registriert' ?>
+ ID = (int)$group['user_id'] ?>
+
+
Rollen und Kontoaktionen verwalten
+
+
+
+
+
+
+
+ Nutzerdaten
+
+
+
+ E-Mail-Adresse
+
+
+
+
+
+ Rollen
+
+
+
+
+ >
+ = htmlspecialchars(genderRole($role)) ?>
+
+
+
+
+
+
+
+ Abbrechen
+ Änderungen speichern
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/adminpanel/admin_brandschutz.php b/web/intranet_repo/projects/adminpanel/admin_brandschutz.php
new file mode 100644
index 0000000..9c19c2e
--- /dev/null
+++ b/web/intranet_repo/projects/adminpanel/admin_brandschutz.php
@@ -0,0 +1,677 @@
+prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
+ $stmt->execute([$_SESSION["user_id"]]);
+ $isAdmin = $stmt->fetchColumn();
+
+ if ((int)$isAdmin !== 1) {
+ header("Location: {$g_httpHomePath}");
+ exit();
+ }
+
+ // Hilfsfunktionen
+ function getProfileImageUrl_bs(array $row, string $g_profilePicturePath): string {
+ $profileImg = glob($g_profilePicturePath."{$row['user_id']}-*.jpg");
+ if (!empty($profileImg)) {
+ usort($profileImg, function($a, $b) {
+ return filemtime($b) - filemtime($a);
+ });
+ return str_replace($_SERVER['DOCUMENT_ROOT'], '', $profileImg[0]);
+ }
+ return '/intranet_repo/general_files/profile_pictures/default.jpg';
+ }
+
+ function getDisplayName_bs(array $row): string {
+ $fullName = trim(($row['first_name'] ?? '') . ' ' . ($row['last_name'] ?? ''));
+ return $fullName !== '' ? $fullName : ($row['mail'] ?? 'Unbekannt');
+ }
+
+ // Schulungsdatum manuell anpassen (POST)
+ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bs_update_date'])) {
+ $editUserId = (int)($_POST['bs_user_id'] ?? 0);
+ $newDate = $_POST['bs_new_date'] ?? '';
+
+ if ($editUserId > 0 && preg_match('/^\d{4}-\d{2}-\d{2}$/', $newDate)) {
+ // Prüfen ob das neue Datum innerhalb der Frist liegt (365 Tage ab Datum > heute)
+ $newDueDate = date('Y-m-d', strtotime($newDate . ' +365 days'));
+ $isWithinDeadline = (strtotime($newDueDate) >= strtotime(date('Y-m-d')));
+
+ if ($isWithinDeadline) {
+ // Datum in der Frist: Zähler zurücksetzen
+ $stmtUpsert = $pdo->prepare("INSERT INTO brandschutz_training (user_id, last_training_date, reminder_count, admin_reminder_count) VALUES (?, ?, 0, 0) ON CONFLICT (user_id) DO UPDATE SET last_training_date = EXCLUDED.last_training_date, reminder_count = 0, admin_reminder_count = 0");
+ } else {
+ // Datum außerhalb der Frist: Nur Datum aktualisieren, Zähler beibehalten
+ $stmtUpsert = $pdo->prepare("INSERT INTO brandschutz_training (user_id, last_training_date, reminder_count, admin_reminder_count) VALUES (?, ?, 0, 0) ON CONFLICT (user_id) DO UPDATE SET last_training_date = EXCLUDED.last_training_date");
+ }
+ $stmtUpsert->execute([$editUserId, $newDate]);
+
+ // Systemnachricht in erledigte Admin-Nachrichten
+ $adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
+ if (empty($adminName)) $adminName = 'Ein Administrator';
+
+ $stmtUser = $pdo->prepare("SELECT first_name, last_name, mail FROM user_general_information WHERE user_id = ?");
+ $stmtUser->execute([$editUserId]);
+ $editedUser = $stmtUser->fetch(PDO::FETCH_ASSOC);
+
+ $editedName = trim(($editedUser['first_name'] ?? '') . ' ' . ($editedUser['last_name'] ?? ''));
+ if (empty($editedName)) $editedName = $editedUser['mail'] ?? 'Unbekannt';
+
+ $formattedDate = date('d.m.Y', strtotime($newDate));
+ $sysSubject = "Brandschutz-Schulungsdatum angepasst: {$editedName}";
+ $sysMessage = "{$adminName} hat das Schulungsdatum von {$editedName} manuell auf den {$formattedDate} gesetzt.";
+
+ $stmtMsg = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
+ $senderMail = $editedUser['mail'] ?? '';
+ $stmtMsg->execute([$adminName, $senderMail, $sysSubject, $sysMessage]);
+
+ header("Location: admin_brandschutz.php?message=5");
+ exit();
+ }
+ }
+
+ // Brandschutz-Nutzer laden
+ $sql = "SELECT ugi.user_id, ugi.first_name, ugi.last_name, ugi.mail,
+ bt.last_training_date, bt.reminder_count, bt.admin_reminder_count,
+ CASE WHEN ul.user_id IS NOT NULL THEN 1 ELSE 0 END AS IstRegistriert
+ FROM user_permissions up
+ INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id
+ LEFT JOIN brandschutz_training bt ON ugi.user_id = bt.user_id
+ LEFT JOIN user_login ul ON ugi.user_id = ul.user_id
+ WHERE up.\"Brandschutz\" = 1
+ ORDER BY bt.last_training_date ASC, ugi.last_name ASC, ugi.first_name ASC";
+ $bsUsers = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
+
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Profil
+ Name
+ Nächste Fälligkeit
+ Status
+
+
+
+
+ 30) {
+ $statusText = 'Aktuell';
+ $statusClass = 'bs-status-ok';
+ $dueDateDisplay = date('d.m.Y', strtotime($dueDate));
+ } elseif ($diff > 0) {
+ $statusText = 'Bald fällig';
+ $statusClass = 'bs-status-warning';
+ $dueDateDisplay = date('d.m.Y', strtotime($dueDate));
+ } elseif ($diff > -120) {
+ $daysOverdue = abs((int)$diff);
+ $statusText = "{$daysOverdue} Tage überfällig";
+ $statusClass = 'bs-status-overdue';
+ $dueDateDisplay = date('d.m.Y', strtotime($dueDate));
+ } else {
+ $daysOverdue = abs((int)$diff);
+ $statusText = "{$daysOverdue} Tage überfällig";
+ $statusClass = 'bs-status-critical';
+ $dueDateDisplay = date('d.m.Y', strtotime($dueDate));
+ }
+ }
+ ?>
+
+
+
+
+
+
+
+ = htmlspecialchars($nameOutput) ?>
+
+
+ = $dueDateDisplay ?>
+
+
+ = $statusText ?>
+
+
+
+
+ Keine Nutzer mit der Rolle "Brandschutz" gefunden.
+
+
+
+
+
+
+
+
+ 0) {
+ $overdueText = 'Nicht überfällig (' . (int)$diff . ' Tage verbleibend)';
+ $overdueClass = 'ok';
+ } else {
+ $daysOverdue = abs((int)$diff);
+ $overdueText = "{$daysOverdue} Tage überfällig";
+ $overdueClass = 'overdue';
+ }
+ }
+ ?>
+
+
+
×
+
+
+
+
+
= htmlspecialchars($nameOutput) ?>
+
+ = htmlspecialchars($bsUser['mail'] ?? '') ?>
+ Brandschutz
+
+
Schulungsdetails & Fälligkeiten
+
+
+
+
+
Schulungsstatus
+
+
+
Letzte Schulung
+
= $lastTrainingDisplay ?>
+
+
+
Nächste Fälligkeit
+
= $dueDateDisplay ?>
+
+
+
Überfälligkeit
+
= $overdueText ?>
+
+
+
Erinnerungsmails gesendet
+
= $reminderCount ?>
+
+ 0): ?>
+
+
Admin-Eskalationen
+
= $adminReminderCount ?>
+
+
+
+
+
Schulungsdatum anpassen
+
+
+
+
+ Datum der letzten Schulung
+
+
+
+ Setzt das Schulungsdatum und alle Erinnerungszähler zurück.
+
+
+
+ Schließen
+
+
+ Datum speichern
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/adminpanel/admin_inbox.php b/web/intranet_repo/projects/adminpanel/admin_inbox.php
new file mode 100644
index 0000000..e5e23b5
--- /dev/null
+++ b/web/intranet_repo/projects/adminpanel/admin_inbox.php
@@ -0,0 +1,564 @@
+prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
+ $stmt->execute([$_SESSION["user_id"]]);
+ $isAdmin = $stmt->fetchColumn();
+
+ if ((int)$isAdmin !== 1) {
+ header("Location: {$g_httpHomePath}");
+ exit();
+ }
+
+ // Typ-Konfiguration für Inbox
+ $typeConfig = [
+ 'registration' => ['label' => 'Registrierung', 'class' => 'inbox-type-registration'],
+ 'contact' => ['label' => 'Kontakt', 'class' => 'inbox-type-contact'],
+ 'system' => ['label' => 'System', 'class' => 'inbox-type-system'],
+ ];
+
+ // Filter
+ $activeFilter = $_GET['filter'] ?? 'all';
+ $validFilters = array_merge(['all'], array_keys($typeConfig));
+ if (!in_array($activeFilter, $validFilters, true)) $activeFilter = 'all';
+
+ function getTypeLabel($type, $typeConfig) {
+ return $typeConfig[$type]['label'] ?? ucfirst($type);
+ }
+ function getTypeClass($type, $typeConfig) {
+ return $typeConfig[$type]['class'] ?? 'inbox-type-system';
+ }
+
+ // Kontaktnachricht als erledigt markieren
+ if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["mark_done"])) {
+ $msgId = (int)$_POST["mark_done"];
+ $stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
+ $stmt->execute([$msgId]);
+ header("Location: admin_inbox.php?inbox_status=done");
+ exit();
+ }
+
+ // Brandschutz-Eskalation: Admin bestätigt persönliche Erinnerung
+ if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["bs_escalation_confirm"])) {
+ $msgId = (int)$_POST["bs_escalation_msg_id"];
+ $bsUserId = (int)$_POST["bs_escalation_user_id"];
+
+ // Nachricht als erledigt markieren
+ $stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
+ $stmt->execute([$msgId]);
+
+ // Log-Eintrag erstellen
+ $adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
+ if (empty($adminName)) $adminName = 'Ein Administrator';
+
+ $stmtUser = $pdo->prepare("SELECT first_name, last_name, mail FROM user_general_information WHERE user_id = ?");
+ $stmtUser->execute([$bsUserId]);
+ $bsUserData = $stmtUser->fetch(PDO::FETCH_ASSOC);
+
+ $bsUserName = trim(($bsUserData['first_name'] ?? '') . ' ' . ($bsUserData['last_name'] ?? ''));
+ if (empty($bsUserName)) $bsUserName = $bsUserData['mail'] ?? 'Unbekannt';
+
+ $logSubject = "Persönliche Brandschutz-Erinnerung bestätigt: {$bsUserName}";
+ $logMessage = "{$adminName} hat bestätigt, {$bsUserName} persönlich an die ausstehende Brandschutzunterweisung erinnert zu haben.";
+
+ $stmtLog = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
+ $senderMail = $bsUserData['mail'] ?? '';
+ $stmtLog->execute([$adminName, $senderMail, $logSubject, $logMessage]);
+
+ header("Location: admin_inbox.php?inbox_status=done");
+ exit();
+ }
+
+ // Nachrichten laden (mit optionalem Filter)
+ $filterClause = '';
+ if ($activeFilter !== 'all') {
+ $filterClause = " AND type = '" . $activeFilter . "'";
+ }
+ $pendingMessages = $pdo->query("SELECT * FROM admin_messages WHERE status = 'pending'{$filterClause} ORDER BY created_at DESC")->fetchAll(PDO::FETCH_ASSOC);
+ $doneMessages = $pdo->query("SELECT * FROM admin_messages WHERE status = 'done'{$filterClause} ORDER BY processed_at DESC LIMIT 20")->fetchAll(PDO::FETCH_ASSOC);
+
+
+ // Rollen laden für Registrierungs-Modal
+ $permissionRoles = [];
+ $rolesResult = $pdo->query("SELECT column_name FROM information_schema.columns WHERE table_name = 'user_permissions' AND table_schema = 'public'");
+ while ($col = $rolesResult->fetch(PDO::FETCH_ASSOC)) {
+ if (!in_array($col['column_name'], ['p_id', 'user_id'], true)) {
+ $permissionRoles[] = $col['column_name'];
+ }
+ }
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ['type' => 'success', 'text' => 'Nachricht als erledigt markiert.'],
+ 'approved' => ['type' => 'success', 'text' => 'Registrierung freigegeben.'],
+ 'rejected' => ['type' => 'success', 'text' => 'Registrierung abgelehnt.'],
+ 'error' => ['type' => 'error', 'text' => 'Ein Fehler ist aufgetreten.'],
+ ];
+ $statusKey = $_GET["inbox_status"];
+ $statusInfo = $inboxStatusMap[$statusKey] ?? ['type' => 'success', 'text' => 'Aktion ausgeführt.'];
+ ?>
+
+
+
= htmlspecialchars($statusInfo['text']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Keine offenen Anfragen vorhanden.
+
+
+
+
+
= htmlspecialchars($msg['subject'] ?? '') ?>
+
+ Von: = htmlspecialchars($msg['sender_name'] ?? 'Unbekannt') ?>
+ (= htmlspecialchars($msg['sender_email'] ?? '') ?>)
+
+
= htmlspecialchars($msg['message'] ?? '') ?>
+
+
+
+
+ Anfrage bearbeiten
+
+
+ Persönliche Erinnerung bestätigen
+
+
+
+ Als erledigt markieren
+
+
+
+
+
+ Als erledigt markieren
+
+
+
+
+
+
+
+
+
+ 0): ?>
+
Erledigte Anfragen (letzte 20)
+
+
+
+
+
= htmlspecialchars($msg['subject'] ?? '') ?>
+
+ Von: = htmlspecialchars($msg['sender_name'] ?? 'Unbekannt') ?>
+ (= htmlspecialchars($msg['sender_email'] ?? '') ?>)
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
+
+
Registrierung bearbeiten
+
+
+
+
+
+
+
+
+
+
+
Rollen zuweisen:
+
+
+
+
+ = htmlspecialchars(genderRole($role)) ?>
+
+
+
+
+
+
+
Ablehnen
+
Freischalten
+
+
+
+
+
+
+
+
+
+
+
+
×
+
+
+
Persönliche Erinnerung bestätigen
+
Brandschutzunterweisung – Eskalation
+
+
+
+
+ Bitte bestätigen Sie, dass Sie persönlich an die ausstehende Brandschutzunterweisung erinnert haben.
+
+
+
+
+
+
+
+
+
+
Abbrechen
+
+
+ Bestätigen
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/adminpanel/admin_kalli.php b/web/intranet_repo/projects/adminpanel/admin_kalli.php
new file mode 100644
index 0000000..165250b
--- /dev/null
+++ b/web/intranet_repo/projects/adminpanel/admin_kalli.php
@@ -0,0 +1,1072 @@
+prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
+ $stmt->execute([$_SESSION["user_id"]]);
+ $isAdmin = $stmt->fetchColumn();
+
+ if ((int)$isAdmin !== 1) {
+ header("Location: {$g_httpHomePath}");
+ exit();
+ }
+
+ // Berechtigungsrollen aus user_permissions laden
+ function getPermissionRoles(PDO $pdo): array {
+ $roles = [];
+ $result = $pdo->query("SELECT column_name FROM information_schema.columns WHERE table_name = 'user_permissions' AND table_schema = 'public'");
+ while ($column = $result->fetch(PDO::FETCH_ASSOC)) {
+ $field = $column['column_name'];
+ if (!in_array($field, ['p_id', 'user_id'], true)) {
+ $roles[] = $field;
+ }
+ }
+ return $roles;
+ }
+
+ $permissionRoles = getPermissionRoles($pdo);
+
+ // ── POST-Handler ──────────────────────────────────────────────────
+
+ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+
+ // Kategorie anlegen
+ if (isset($_POST['addCategory'])) {
+ $name = trim($_POST['cat_name'] ?? '');
+ $role = trim($_POST['cat_permission_role'] ?? '');
+ if ($name !== '') {
+ $stmt = $pdo->prepare("INSERT INTO kalli_categories (name, permission_role, created_at, updated_at) VALUES (?, ?, NOW(), NOW())");
+ $stmt->execute([$name, $role]);
+ header("Location: admin_kalli.php?kalli_msg=1");
+ exit();
+ }
+ header("Location: admin_kalli.php?kalli_msg=5");
+ exit();
+ }
+
+ // Kategorie bearbeiten
+ if (isset($_POST['editCategory'])) {
+ $id = (int)($_POST['cat_id'] ?? 0);
+ $name = trim($_POST['cat_name'] ?? '');
+ $role = trim($_POST['cat_permission_role'] ?? '');
+ if ($id > 0 && $name !== '') {
+ $stmt = $pdo->prepare("UPDATE kalli_categories SET name = ?, permission_role = ?, updated_at = NOW() WHERE id = ?");
+ $stmt->execute([$name, $role, $id]);
+ header("Location: admin_kalli.php?kalli_msg=1");
+ exit();
+ }
+ header("Location: admin_kalli.php?kalli_msg=5");
+ exit();
+ }
+
+ // Kategorie löschen (CASCADE)
+ if (isset($_POST['deleteCategory'])) {
+ $id = (int)($_POST['cat_id'] ?? 0);
+ if ($id > 0) {
+ // Zuerst zugehörige Terminarten löschen
+ $stmt = $pdo->prepare("DELETE FROM kalli_appointment_types WHERE category_id = ?");
+ $stmt->execute([$id]);
+
+ $stmt = $pdo->prepare("DELETE FROM kalli_categories WHERE id = ?");
+ $stmt->execute([$id]);
+ header("Location: admin_kalli.php?kalli_msg=2");
+ exit();
+ }
+ header("Location: admin_kalli.php?kalli_msg=5");
+ exit();
+ }
+
+ // Terminart anlegen
+ if (isset($_POST['addType'])) {
+ $catId = (int)($_POST['type_category_id'] ?? 0);
+ $name = trim($_POST['type_name'] ?? '');
+ $displayLabel = trim($_POST['type_display_label'] ?? '');
+ $planungsstatus = trim($_POST['type_planungsstatus'] ?? 'fix');
+ $anzeigeziel = trim($_POST['type_anzeigeziel'] ?? 'intern');
+ $gruppenzuordnung = trim($_POST['type_gruppenzuordnung'] ?? '');
+ $anzeigezielOrt = trim($_POST['type_anzeigeziel_ort'] ?? '');
+ $anzeigezielNotiz = trim($_POST['type_anzeigeziel_notiz'] ?? '');
+ $anzeigezielVerantw = trim($_POST['type_anzeigeziel_verantwortlicher'] ?? '');
+ $defaultText = trim($_POST['type_default_text'] ?? '');
+
+ if ($catId > 0 && $name !== '') {
+ $stmt = $pdo->prepare("INSERT INTO kalli_appointment_types (category_id, name, display_label, planungsstatus, anzeigeziel, gruppenzuordnung, anzeigeziel_ort, anzeigeziel_notiz, anzeigeziel_verantwortlicher, default_text, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())");
+ $stmt->execute([$catId, $name, $displayLabel, $planungsstatus, $anzeigeziel, $gruppenzuordnung, $anzeigezielOrt, $anzeigezielNotiz, $anzeigezielVerantw, $defaultText]);
+ header("Location: admin_kalli.php?kalli_msg=3");
+ exit();
+ }
+ header("Location: admin_kalli.php?kalli_msg=5");
+ exit();
+ }
+
+ // Terminart bearbeiten
+ if (isset($_POST['editType'])) {
+ $id = (int)($_POST['type_id'] ?? 0);
+ $catId = (int)($_POST['type_category_id'] ?? 0);
+ $name = trim($_POST['type_name'] ?? '');
+ $displayLabel = trim($_POST['type_display_label'] ?? '');
+ $planungsstatus = trim($_POST['type_planungsstatus'] ?? 'fix');
+ $anzeigeziel = trim($_POST['type_anzeigeziel'] ?? 'intern');
+ $gruppenzuordnung = trim($_POST['type_gruppenzuordnung'] ?? '');
+ $anzeigezielOrt = trim($_POST['type_anzeigeziel_ort'] ?? '');
+ $anzeigezielNotiz = trim($_POST['type_anzeigeziel_notiz'] ?? '');
+ $anzeigezielVerantw = trim($_POST['type_anzeigeziel_verantwortlicher'] ?? '');
+ $defaultText = trim($_POST['type_default_text'] ?? '');
+
+ if ($id > 0 && $name !== '') {
+ $stmt = $pdo->prepare("UPDATE kalli_appointment_types SET category_id = ?, name = ?, display_label = ?, planungsstatus = ?, anzeigeziel = ?, gruppenzuordnung = ?, anzeigeziel_ort = ?, anzeigeziel_notiz = ?, anzeigeziel_verantwortlicher = ?, default_text = ?, updated_at = NOW() WHERE id = ?");
+ $stmt->execute([$catId, $name, $displayLabel, $planungsstatus, $anzeigeziel, $gruppenzuordnung, $anzeigezielOrt, $anzeigezielNotiz, $anzeigezielVerantw, $defaultText, $id]);
+ header("Location: admin_kalli.php?kalli_msg=3");
+ exit();
+ }
+ header("Location: admin_kalli.php?kalli_msg=5");
+ exit();
+ }
+
+ // Terminart löschen
+ if (isset($_POST['deleteType'])) {
+ $id = (int)($_POST['type_id'] ?? 0);
+ if ($id > 0) {
+ $stmt = $pdo->prepare("DELETE FROM kalli_appointment_types WHERE id = ?");
+ $stmt->execute([$id]);
+ header("Location: admin_kalli.php?kalli_msg=4");
+ exit();
+ }
+ header("Location: admin_kalli.php?kalli_msg=5");
+ exit();
+ }
+ }
+
+ // ── Daten laden ───────────────────────────────────────────────────
+
+ $categories = $pdo->query("SELECT * FROM kalli_categories ORDER BY sort_order ASC, name ASC")->fetchAll(PDO::FETCH_ASSOC);
+
+ $typesRaw = $pdo->query("SELECT * FROM kalli_appointment_types ORDER BY sort_order ASC, name ASC")->fetchAll(PDO::FETCH_ASSOC);
+ $typesByCategory = [];
+ foreach ($typesRaw as $t) {
+ $typesByCategory[(int)$t['category_id']][] = $t;
+ }
+
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Noch keine Kategorien vorhanden. Erstelle eine neue Kategorie, um Terminarten zu verwalten.
+
+
+
+
+
+
+
+
+
+
+
+
+ Name
+ Anzeige-Label
+ Planungsstatus
+ Anzeigeziel
+ Gruppe
+ Ort
+ Notiz
+ Verantw.
+ Standardtext
+
+
+
+
+
+
+ = htmlspecialchars($type['name']) ?>
+ = htmlspecialchars($type['display_label']) ?>
+ = htmlspecialchars($type['planungsstatus']) ?>
+ = htmlspecialchars($type['anzeigeziel']) ?>
+ = htmlspecialchars($type['gruppenzuordnung']) ?>
+ = htmlspecialchars($type['anzeigeziel_ort'] ?? '') ?>
+ = htmlspecialchars($type['anzeigeziel_notiz'] ?? '') ?>
+ = htmlspecialchars($type['anzeigeziel_verantwortlicher'] ?? '') ?>
+ = htmlspecialchars(mb_strimwidth($type['default_text'] ?? '', 0, 40, '…')) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Keine Terminarten in dieser Kategorie.
+
+
+
+ Neue Terminart
+
+
+
+
+
+
+
+
+
+
×
+
+
Neue Kategorie
+
Kategorie für Kalli-Terminarten anlegen
+
+
+
+
+ Name *
+
+
+
+ Berechtigungsrolle
+
+ – Keine –
+
+ = htmlspecialchars($role) ?>
+
+
+
+
+
Abbrechen
+
Kategorie speichern
+
+
+
+
+
+
+
+
+
+
×
+
+
Kategorie bearbeiten
+
Änderungen an der Kategorie vornehmen
+
+
+
+
+
+ Name *
+
+
+
+ Berechtigungsrolle
+
+ – Keine –
+
+ = htmlspecialchars($role) ?>
+
+
+
+
+
Abbrechen
+
Änderungen speichern
+
+
+
+
+
+
+
+
+
+
×
+
+
Kategorie löschen
+
Achtung: Alle zugehörigen Terminarten werden ebenfalls gelöscht!
+
+
+
Soll die Kategorie wirklich gelöscht werden?
+
Dieser Vorgang kann nicht rückgängig gemacht werden.
+
+
+
+
Abbrechen
+
Endgültig löschen
+
+
+
+
+
+
+
+
+
+
×
+
+
Neue Terminart
+
Terminart anlegen
+
+
+
+
+
+
+
+ Planungsstatus
+
+ fix
+ geplant
+
+
+
+ Anzeigeziel
+
+ intern
+ öffentlich
+
+
+
+
+ Gruppenzuordnung
+
+
+
+
+ Anzeigeziel Ort
+
+ – Keine –
+ intern
+ öffentlich
+
+
+
+ Anzeigeziel Notiz
+
+ – Keine –
+ intern
+ öffentlich
+
+
+
+
+ Anzeigeziel Verantwortlicher
+
+ – Keine –
+ intern
+ öffentlich
+
+
+
+ Standardtext
+
+
+
+
Abbrechen
+
Terminart speichern
+
+
+
+
+
+
+
+
+
+
×
+
+
Terminart bearbeiten
+
Änderungen an der Terminart vornehmen
+
+
+
+
+
+
+
+
+ Planungsstatus
+
+ fix
+ geplant
+
+
+
+ Anzeigeziel
+
+ intern
+ öffentlich
+
+
+
+
+ Gruppenzuordnung
+
+
+
+
+ Anzeigeziel Ort
+
+ – Keine –
+ intern
+ öffentlich
+
+
+
+ Anzeigeziel Notiz
+
+ – Keine –
+ intern
+ öffentlich
+
+
+
+
+ Anzeigeziel Verantwortlicher
+
+ – Keine –
+ intern
+ öffentlich
+
+
+
+ Standardtext
+
+
+
+
Abbrechen
+
Änderungen speichern
+
+
+
+
+
+
+
+
+
+
×
+
+
Terminart löschen
+
Soll diese Terminart wirklich gelöscht werden?
+
+
+
Soll die Terminart wirklich gelöscht werden?
+
Dieser Vorgang kann nicht rückgängig gemacht werden.
+
+
+
+
Abbrechen
+
Endgültig löschen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/adminpanel/admin_notices.php b/web/intranet_repo/projects/adminpanel/admin_notices.php
new file mode 100644
index 0000000..c1a137b
--- /dev/null
+++ b/web/intranet_repo/projects/adminpanel/admin_notices.php
@@ -0,0 +1,471 @@
+prepare('SELECT "Admin" FROM user_permissions WHERE user_id = ?');
+$stmt->execute([$_SESSION['user_id']]);
+$isAdmin = $stmt->fetchColumn();
+
+if ((int)$isAdmin !== 1) {
+ header("Location: {$g_httpHomePath}");
+ exit();
+}
+
+const NOTICE_MAX_TITLE_LENGTH = 180;
+const NOTICE_MAX_MESSAGE_LENGTH = 4000;
+
+$noticeTypes = [
+ 'info' => 'Information',
+ 'warning' => 'Warnung',
+ 'success' => 'Erfolg',
+ 'error' => 'Wichtig',
+];
+
+if (empty($_SESSION['admin_notices_csrf'])) {
+ $_SESSION['admin_notices_csrf'] = bin2hex(random_bytes(32));
+}
+$csrfToken = $_SESSION['admin_notices_csrf'];
+
+function noticeRedirect(string $status): void {
+ header('Location: admin_notices.php?status=' . rawurlencode($status));
+ exit();
+}
+
+function noticeVerifyCsrf(): void {
+ $postedToken = $_POST['csrf_token'] ?? '';
+ $sessionToken = $_SESSION['admin_notices_csrf'] ?? '';
+ if (!is_string($postedToken) || !is_string($sessionToken) || $sessionToken === '' || !hash_equals($sessionToken, $postedToken)) {
+ http_response_code(400);
+ exit('Ungültige Anfrage. Bitte laden Sie die Seite neu.');
+ }
+}
+
+function noticeParseLocalDateTime(?string $value): ?string {
+ $value = trim((string)$value);
+ if ($value === '') {
+ return null;
+ }
+
+ $tz = new DateTimeZone('Europe/Berlin');
+ $date = DateTimeImmutable::createFromFormat('!Y-m-d\TH:i', $value, $tz);
+ $errors = DateTimeImmutable::getLastErrors();
+
+ if (!$date || ($errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0))) {
+ throw new InvalidArgumentException('Ungültiges Datum.');
+ }
+
+ // Mit explizitem Offset an PostgreSQL senden, damit TIMESTAMPTZ eindeutig ist.
+ return $date->format('Y-m-d H:i:sP');
+}
+
+function noticeFormatLocalDateTime(?string $value): string {
+ if (empty($value)) {
+ return '';
+ }
+ try {
+ return (new DateTimeImmutable($value))->setTimezone(new DateTimeZone('Europe/Berlin'))->format('Y-m-d\TH:i');
+ } catch (Throwable $e) {
+ return '';
+ }
+}
+
+function noticeDisplayDate(?string $value): string {
+ if (empty($value)) {
+ return 'unbegrenzt';
+ }
+ try {
+ return (new DateTimeImmutable($value))->setTimezone(new DateTimeZone('Europe/Berlin'))->format('d.m.Y, H:i') . ' Uhr';
+ } catch (Throwable $e) {
+ return '–';
+ }
+}
+
+function noticeDbBool($value): bool {
+ return $value === true || $value === 1 || $value === '1' || $value === 't' || $value === 'true';
+}
+
+function noticeTextLength(string $value): int {
+ return function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
+}
+
+function noticeValidateInput(array $noticeTypes): array {
+ $title = trim((string)($_POST['title'] ?? ''));
+ $message = trim((string)($_POST['message'] ?? ''));
+ $type = (string)($_POST['type'] ?? 'info');
+ $sortOrder = filter_var($_POST['sort_order'] ?? 0, FILTER_VALIDATE_INT);
+ $isActive = isset($_POST['is_active']);
+
+ if ($title === '' || noticeTextLength($title) > NOTICE_MAX_TITLE_LENGTH) {
+ throw new InvalidArgumentException('Titel fehlt oder ist zu lang.');
+ }
+ if ($message === '' || noticeTextLength($message) > NOTICE_MAX_MESSAGE_LENGTH) {
+ throw new InvalidArgumentException('Meldung fehlt oder ist zu lang.');
+ }
+ if (!array_key_exists($type, $noticeTypes)) {
+ throw new InvalidArgumentException('Ungültiger Meldungstyp.');
+ }
+ if ($sortOrder === false || $sortOrder < -9999 || $sortOrder > 9999) {
+ throw new InvalidArgumentException('Ungültige Sortierung.');
+ }
+
+ $startsAt = noticeParseLocalDateTime($_POST['starts_at'] ?? null);
+ $endsAt = noticeParseLocalDateTime($_POST['ends_at'] ?? null);
+
+ if ($startsAt !== null && $endsAt !== null) {
+ $start = new DateTimeImmutable($startsAt);
+ $end = new DateTimeImmutable($endsAt);
+ if ($end <= $start) {
+ throw new InvalidArgumentException('Das Ende muss nach dem Beginn liegen.');
+ }
+ }
+
+ return [
+ 'title' => $title,
+ 'message' => $message,
+ 'type' => $type,
+ 'sort_order' => (int)$sortOrder,
+ 'is_active' => $isActive,
+ 'starts_at' => $startsAt,
+ 'ends_at' => $endsAt,
+ ];
+}
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ noticeVerifyCsrf();
+ $action = (string)($_POST['action'] ?? '');
+
+ try {
+ if ($action === 'create') {
+ $data = noticeValidateInput($noticeTypes);
+ $stmt = $pdo->prepare(
+ 'INSERT INTO mainpage_notices
+ (title, message, type, is_active, starts_at, ends_at, sort_order, created_by, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())'
+ );
+ $stmt->execute([
+ $data['title'], $data['message'], $data['type'], $data['is_active'] ? 'true' : 'false',
+ $data['starts_at'], $data['ends_at'], $data['sort_order'], (int)$_SESSION['user_id'],
+ ]);
+ noticeRedirect('created');
+ }
+
+ if ($action === 'update') {
+ $id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
+ if (!$id || $id < 1) {
+ throw new InvalidArgumentException('Ungültige Meldungs-ID.');
+ }
+ $data = noticeValidateInput($noticeTypes);
+ $stmt = $pdo->prepare(
+ 'UPDATE mainpage_notices
+ SET title = ?, message = ?, type = ?, is_active = ?, starts_at = ?, ends_at = ?, sort_order = ?, updated_at = NOW()
+ WHERE id = ?'
+ );
+ $stmt->execute([
+ $data['title'], $data['message'], $data['type'], $data['is_active'] ? 'true' : 'false',
+ $data['starts_at'], $data['ends_at'], $data['sort_order'], (int)$id,
+ ]);
+ noticeRedirect('updated');
+ }
+
+ if ($action === 'toggle') {
+ $id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
+ if (!$id || $id < 1) {
+ throw new InvalidArgumentException('Ungültige Meldungs-ID.');
+ }
+ $stmt = $pdo->prepare('UPDATE mainpage_notices SET is_active = NOT is_active, updated_at = NOW() WHERE id = ?');
+ $stmt->execute([(int)$id]);
+ noticeRedirect('toggled');
+ }
+
+ if ($action === 'delete') {
+ $id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
+ if (!$id || $id < 1) {
+ throw new InvalidArgumentException('Ungültige Meldungs-ID.');
+ }
+ $stmt = $pdo->prepare('DELETE FROM mainpage_notices WHERE id = ?');
+ $stmt->execute([(int)$id]);
+ noticeRedirect('deleted');
+ }
+
+ throw new InvalidArgumentException('Unbekannte Aktion.');
+ } catch (InvalidArgumentException $e) {
+ $_SESSION['admin_notices_error'] = $e->getMessage();
+ noticeRedirect('error');
+ } catch (PDOException $e) {
+ $_SESSION['admin_notices_error'] = 'Datenbankfehler. Prüfen Sie, ob die Tabelle mainpage_notices angelegt wurde.';
+ noticeRedirect('error');
+ }
+}
+
+$noticeError = $_SESSION['admin_notices_error'] ?? '';
+unset($_SESSION['admin_notices_error']);
+$status = (string)($_GET['status'] ?? '');
+$statusMessages = [
+ 'created' => 'Meldung wurde angelegt.',
+ 'updated' => 'Meldung wurde gespeichert.',
+ 'toggled' => 'Status der Meldung wurde geändert.',
+ 'deleted' => 'Meldung wurde gelöscht.',
+];
+
+$tableAvailable = true;
+$notices = [];
+try {
+ $notices = $pdo->query(
+ 'SELECT id, title, message, type, is_active, starts_at, ends_at, sort_order, created_at, updated_at
+ FROM mainpage_notices
+ ORDER BY sort_order ASC, created_at DESC, id DESC'
+ )->fetchAll(PDO::FETCH_ASSOC);
+} catch (PDOException $e) {
+ $tableAvailable = false;
+ $noticeError = 'Die Tabelle mainpage_notices fehlt. Führen Sie zuerst general_files/ini/mainpage_notices.sql aus.';
+}
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
= htmlspecialchars($noticeError, ENT_QUOTES, 'UTF-8') ?>
+
+
= htmlspecialchars($noticeError, ENT_QUOTES, 'UTF-8') ?>
+
+
= htmlspecialchars($statusMessages[$status], ENT_QUOTES, 'UTF-8') ?>
+
+
+
+
+
Noch keine Meldungen vorhanden.
+
+
+
+ $noticeId,
+ 'title' => $notice['title'],
+ 'message' => $notice['message'],
+ 'type' => $noticeType,
+ 'is_active' => noticeDbBool($notice['is_active']),
+ 'starts_at' => noticeFormatLocalDateTime($notice['starts_at']),
+ 'ends_at' => noticeFormatLocalDateTime($notice['ends_at']),
+ 'sort_order' => (int)$notice['sort_order'],
+ ];
+ ?>
+
+
+
+ = htmlspecialchars($notice['title'], ENT_QUOTES, 'UTF-8') ?>
+ = htmlspecialchars($noticeTypes[$noticeType], ENT_QUOTES, 'UTF-8') ?>
+ Deaktiviert
+
+
= htmlspecialchars($notice['message'], ENT_QUOTES, 'UTF-8') ?>
+
+ Von: = htmlspecialchars(noticeDisplayDate($notice['starts_at']), ENT_QUOTES, 'UTF-8') ?>
+ Bis: = htmlspecialchars(noticeDisplayDate($notice['ends_at']), ENT_QUOTES, 'UTF-8') ?>
+ Sortierung: = (int)$notice['sort_order'] ?>
+
+
+
+ Bearbeiten
+
+
+
+
+ = noticeDbBool($notice['is_active']) ? 'Deaktivieren' : 'Aktivieren' ?>
+
+
+
+
+
+ Löschen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Abbrechen
+ Speichern
+
+
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/adminpanel/admin_profiles.php b/web/intranet_repo/projects/adminpanel/admin_profiles.php
new file mode 100644
index 0000000..73c25a8
--- /dev/null
+++ b/web/intranet_repo/projects/adminpanel/admin_profiles.php
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+ prepare("SELECT * FROM user_permissions up INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id WHERE up.user_id = ?");
+ $stmt->execute([$_SESSION["user_id"]]);
+ $isAdmin = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ // Check if the user has admin access (Admin = 1)
+ if ($isAdmin["Admin"] == 1) {
+ $sql_groups = "SELECT * FROM user_permissions";
+ $stmt = $pdo->prepare($sql_groups);
+ $stmt->execute([]);
+ $groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ } else {
+ header("Location: {$g_httpHomePath}");
+ exit();
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/adminpanel/approve-registration.php b/web/intranet_repo/projects/adminpanel/approve-registration.php
new file mode 100644
index 0000000..0921342
--- /dev/null
+++ b/web/intranet_repo/projects/adminpanel/approve-registration.php
@@ -0,0 +1,182 @@
+prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
+ $stmt->execute([$_SESSION["user_id"]]);
+ $isAdmin = $stmt->fetchColumn();
+
+ if ((int)$isAdmin !== 1) {
+ header("Location: {$g_httpHomePath}");
+ exit();
+ }
+
+ if ($_SERVER["REQUEST_METHOD"] !== "POST") {
+ header("Location: admin_inbox.php");
+ exit();
+ }
+
+ // Admin-Name ermitteln
+ $adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
+ if (empty($adminName)) $adminName = 'Ein Administrator';
+
+ $action = $_POST["action"] ?? '';
+ $pending_id = (int)($_POST["pending_id"] ?? 0);
+ $message_id = (int)($_POST["message_id"] ?? 0);
+ $selectedRoles = $_POST["roles"] ?? [];
+ if (!is_array($selectedRoles)) $selectedRoles = [];
+
+ if ($pending_id === 0) {
+ header("Location: admin_inbox.php?inbox_status=error");
+ exit();
+ }
+
+ // Pending Registration laden
+ $stmt = $pdo->prepare("SELECT * FROM pending_registrations WHERE id = ? AND status = 'pending'");
+ $stmt->execute([$pending_id]);
+ $pending = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!$pending) {
+ header("Location: admin_inbox.php?inbox_status=error");
+ exit();
+ }
+
+ // Rollen-Spalten laden
+ $permissionRoles = [];
+ $rolesResult = $pdo->query("SELECT column_name FROM information_schema.columns WHERE table_name = 'user_permissions' AND table_schema = 'public'");
+ while ($col = $rolesResult->fetch(PDO::FETCH_ASSOC)) {
+ if (!in_array($col['column_name'], ['p_id', 'user_id'], true)) {
+ $permissionRoles[] = $col['column_name'];
+ }
+ }
+
+ $loginUrl = "https://" . ($_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN')) . "/intranet_repo/projects/authorization/login.php";
+
+ if ($action === 'approve') {
+ $pdo->beginTransaction();
+ try {
+ // Nutzer in user_general_information anlegen
+ $stmt = $pdo->prepare("INSERT INTO user_general_information (mail, first_name, last_name, phone) VALUES (?, ?, ?, ?)");
+ $stmt->execute([$pending['email'], $pending['first_name'], $pending['last_name'], $pending['phone']]);
+ $newUserId = $pdo->lastInsertId();
+
+ if (!$newUserId) {
+ $checkStmt = $pdo->prepare("SELECT user_id FROM user_general_information WHERE mail = ?");
+ $checkStmt->execute([$pending['email']]);
+ $newUserId = $checkStmt->fetchColumn();
+ }
+
+ // Login-Daten anlegen
+ $stmt = $pdo->prepare("INSERT INTO user_login (user_id, password_hash) VALUES (?, ?)");
+ $stmt->execute([$newUserId, $pending['password_hash']]);
+
+ // Berechtigungen mit gewählten Rollen anlegen
+ $roleColumns = array_map(function($r) { return '"' . str_replace('"', '""', $r) . '"'; }, $permissionRoles);
+ $columns = array_merge(['"user_id"'], $roleColumns);
+ $placeholders = array_fill(0, count($columns), '?');
+ $roleValues = [];
+ foreach ($permissionRoles as $role) {
+ $roleValues[] = in_array($role, $selectedRoles, true) ? 1 : 0;
+ }
+ $params = array_merge([(int)$newUserId], $roleValues);
+
+ $insertSql = "INSERT INTO user_permissions (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
+ $stmt = $pdo->prepare($insertSql);
+ $stmt->execute($params);
+
+ // Pending-Status aktualisieren
+ $stmt = $pdo->prepare("UPDATE pending_registrations SET status = 'approved' WHERE id = ?");
+ $stmt->execute([$pending_id]);
+
+ // Inbox-Nachricht als erledigt markieren
+ if ($message_id > 0) {
+ $stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
+ $stmt->execute([$message_id]);
+ }
+
+ $pdo->commit();
+
+ // Zugewiesene Rollen als Text
+ $assignedRoles = !empty($selectedRoles) ? implode(', ', $selectedRoles) : 'Keine besonderen Rollen';
+
+ // Mail an den Nutzer
+ $userBetreff = "Registrierung freigeschaltet";
+ $userNachricht = "Hallo {$pending['first_name']},\n\n";
+ $userNachricht .= "Ihr Account für das Intranet der NAK Reinickendorf wurde freigeschaltet!\n\n";
+ $userNachricht .= "Sie können sich ab sofort mit Ihrer E-Mail-Adresse und dem von Ihnen gewählten Passwort anmelden.";
+
+ send_intranet_mail($pending['email'], $userBetreff, $userNachricht, null, "intranet@nak-reinickendorf.de", "Jetzt anmelden", $loginUrl);
+
+ // Mail an Admin-Team
+ $adminBetreff = "Registrierung freigegeben: {$pending['first_name']} {$pending['last_name']}";
+ $adminNachricht = "{$adminName} hat die Registrierung bearbeitet:\n\n";
+ $adminNachricht .= "Nutzer: {$pending['first_name']} {$pending['last_name']}\n";
+ $adminNachricht .= "E-Mail: {$pending['email']}\n";
+ $adminNachricht .= "Aktion: Freigeschaltet\n";
+ $adminNachricht .= "Zugewiesene Rollen: {$assignedRoles}";
+
+ send_admin_team_mail($pdo, $adminBetreff, $adminNachricht);
+
+ // Systemnachricht im Posteingang
+ $sysSubject = "Nutzer freigeschaltet: {$pending['first_name']} {$pending['last_name']}";
+ $sysMessage = "{$adminName} hat {$pending['first_name']} {$pending['last_name']} ({$pending['email']}) freigeschaltet.\nZugewiesene Rollen: {$assignedRoles}";
+ $stmtSys = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
+ $stmtSys->execute([$adminName, $pending['email'], $sysSubject, $sysMessage]);
+
+ header("Location: admin_inbox.php?inbox_status=approved");
+ exit();
+
+ } catch (Throwable $e) {
+ $pdo->rollBack();
+ error_log("approve-registration error: " . $e->getMessage());
+ header("Location: admin_inbox.php?inbox_status=error");
+ exit();
+ }
+
+ } elseif ($action === 'reject') {
+ // Ablehnen
+ $stmt = $pdo->prepare("UPDATE pending_registrations SET status = 'rejected' WHERE id = ?");
+ $stmt->execute([$pending_id]);
+
+ // Inbox-Nachricht als erledigt markieren
+ if ($message_id > 0) {
+ $stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
+ $stmt->execute([$message_id]);
+ }
+
+ // Mail an den Nutzer
+ $userBetreff = "Registrierung abgelehnt";
+ $userNachricht = "Hallo {$pending['first_name']},\n\n";
+ $userNachricht .= "Ihre Registrierungsanfrage für das Intranet der NAK Reinickendorf wurde leider abgelehnt.\n\n";
+ $userNachricht .= "Bei Fragen wenden Sie sich bitte an den Administrator über das Kontaktformular auf der Anmeldeseite.";
+
+ send_intranet_mail($pending['email'], $userBetreff, $userNachricht);
+
+ // Mail an Admin-Team
+ $adminBetreff = "Registrierung abgelehnt: {$pending['first_name']} {$pending['last_name']}";
+ $adminNachricht = "{$adminName} hat die Registrierung bearbeitet:\n\n";
+ $adminNachricht .= "Nutzer: {$pending['first_name']} {$pending['last_name']}\n";
+ $adminNachricht .= "E-Mail: {$pending['email']}\n";
+ $adminNachricht .= "Aktion: Abgelehnt";
+
+ send_admin_team_mail($pdo, $adminBetreff, $adminNachricht);
+
+ // Systemnachricht im Posteingang
+ $sysSubject = "Registrierung abgelehnt: {$pending['first_name']} {$pending['last_name']}";
+ $sysMessage = "{$adminName} hat die Registrierung von {$pending['first_name']} {$pending['last_name']} ({$pending['email']}) abgelehnt.";
+ $stmtSys = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
+ $stmtSys->execute([$adminName, $pending['email'], $sysSubject, $sysMessage]);
+
+ header("Location: admin_inbox.php?inbox_status=rejected");
+ exit();
+ }
+
+ header("Location: admin_inbox.php");
+ exit();
+?>
diff --git a/web/intranet_repo/projects/adminpanel/process_contact.php b/web/intranet_repo/projects/adminpanel/process_contact.php
new file mode 100644
index 0000000..9b6d7ed
--- /dev/null
+++ b/web/intranet_repo/projects/adminpanel/process_contact.php
@@ -0,0 +1,56 @@
+ false, 'error' => 'Methode nicht erlaubt']);
+ exit;
+ }
+
+ // Felder auslesen
+ $name = trim($_POST["contact_name"] ?? '');
+ $email = trim($_POST["contact_email"] ?? '');
+ $subject = trim($_POST["contact_subject"] ?? 'Kontaktanfrage');
+ $message = trim($_POST["contact_message"] ?? '');
+
+ // Validierung
+ if (empty($name) || empty($email) || empty($message)) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'error' => 'Bitte alle Felder ausfüllen.']);
+ exit;
+ }
+
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ http_response_code(400);
+ echo json_encode(['success' => false, 'error' => 'Ungültige E-Mail-Adresse.']);
+ exit;
+ }
+
+ // In admin_messages speichern
+ $stmt = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message) VALUES ('contact', ?, ?, ?, ?)");
+ $stmt->execute([$name, $email, $subject, $message]);
+
+ // Mail an Admin-Team (Admin + Technik)
+ $mail_betreff = "Kontaktanfrage: {$subject}";
+ $mail_nachricht = "Neue Kontaktanfrage über das Intranet:\n\n";
+ $mail_nachricht .= "Name: {$name}\n";
+ $mail_nachricht .= "E-Mail: {$email}\n";
+ $mail_nachricht .= "Betreff: {$subject}\n\n";
+ $mail_nachricht .= "Nachricht:\n{$message}";
+
+ send_admin_team_mail($pdo, $mail_betreff, $mail_nachricht);
+
+ // JSON-Antwort für AJAX
+ header('Content-Type: application/json');
+ echo json_encode(['success' => true, 'message' => 'Nachricht wurde gesendet.']);
+ exit;
+?>
diff --git a/web/intranet_repo/projects/authorization/just-validate.js b/web/intranet_repo/projects/authorization/just-validate.js
new file mode 100644
index 0000000..10bd93e
--- /dev/null
+++ b/web/intranet_repo/projects/authorization/just-validate.js
@@ -0,0 +1 @@
+var __defProp=Object.defineProperty,__defNormalProp=(e,i,t)=>i in e?__defProp(e,i,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[i]=t,__publicField=(e,i,t)=>(__defNormalProp(e,"symbol"!=typeof i?i+"":i,t),t);!function(e,i){"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(e="undefined"!=typeof globalThis?globalThis:e||self).JustValidate=i()}(this,(function(){"use strict";const e=/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,i=/^-?[0-9]\d*$/,t=/^(?=.*[A-Za-z])(?=.*\d).{8,}$/,s=/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/,l=e=>"string"!=typeof e||""===e;var r=(e=>(e.Required="required",e.Email="email",e.MinLength="minLength",e.MaxLength="maxLength",e.Password="password",e.Number="number",e.Integer="integer",e.MaxNumber="maxNumber",e.MinNumber="minNumber",e.StrongPassword="strongPassword",e.CustomRegexp="customRegexp",e.MinFilesCount="minFilesCount",e.MaxFilesCount="maxFilesCount",e.Files="files",e))(r||{}),o=(e=>(e.Required="required",e))(o||{}),a=(e=>(e.Label="label",e.LabelArrow="labelArrow",e))(a||{});const n=[{key:r.Required,dict:{en:"The field is required"}},{key:r.Email,dict:{en:"Email has invalid format"}},{key:r.MaxLength,dict:{en:"The field must contain a maximum of :value characters"}},{key:r.MinLength,dict:{en:"The field must contain a minimum of :value characters"}},{key:r.Password,dict:{en:"Password must contain minimum eight characters, at least one letter and one number"}},{key:r.StrongPassword,dict:{en:"Password should contain minimum eight characters, at least one uppercase letter, one lowercase letter, one number and one special character"}},{key:r.Number,dict:{en:"Value should be a number"}},{key:r.MaxNumber,dict:{en:"Number should be less or equal than :value"}},{key:r.MinNumber,dict:{en:"Number should be more or equal than :value"}},{key:r.MinFilesCount,dict:{en:"Files count should be more or equal than :value"}},{key:r.MaxFilesCount,dict:{en:"Files count should be less or equal than :value"}},{key:r.Files,dict:{en:"Uploaded files have one or several invalid properties (extension/size/type etc)."}}],d=e=>"object"==typeof e&&null!==e&&"then"in e&&"function"==typeof e.then,c=e=>Array.isArray(e)?e.filter((e=>e.length>0)):"string"==typeof e&&e.trim()?[...e.split(" ").filter((e=>e.length>0))]:[],u=e=>e instanceof Element||e instanceof HTMLDocument,h={errorFieldStyle:{color:"#b81111",border:"1px solid #B81111"},errorFieldCssClass:"just-validate-error-field",successFieldCssClass:"just-validate-success-field",errorLabelStyle:{color:"#b81111"},errorLabelCssClass:"just-validate-error-label",successLabelCssClass:"just-validate-success-label",focusInvalidField:!0,lockForm:!0,testingMode:!1,validateBeforeSubmitting:!1,submitFormAutomatically:!1};return class{constructor(e,i,t){__publicField(this,"form",null),__publicField(this,"fields",{}),__publicField(this,"groupFields",{}),__publicField(this,"errors",{}),__publicField(this,"isValid",!1),__publicField(this,"isSubmitted",!1),__publicField(this,"globalConfig",h),__publicField(this,"errorLabels",{}),__publicField(this,"successLabels",{}),__publicField(this,"eventListeners",[]),__publicField(this,"dictLocale",n),__publicField(this,"currentLocale","en"),__publicField(this,"customStyleTags",{}),__publicField(this,"onSuccessCallback"),__publicField(this,"onFailCallback"),__publicField(this,"onValidateCallback"),__publicField(this,"tooltips",[]),__publicField(this,"lastScrollPosition"),__publicField(this,"isScrollTick"),__publicField(this,"fieldIds",new Map),__publicField(this,"getKeyByFieldSelector",(e=>this.fieldIds.get(e))),__publicField(this,"getFieldSelectorByKey",(e=>{for(const[i,t]of this.fieldIds)if(e===t)return i})),__publicField(this,"getCompatibleFields",(()=>{const e={};return Object.keys(this.fields).forEach((i=>{let t=i;const s=this.getFieldSelectorByKey(i);"string"==typeof s&&(t=s),e[t]={...this.fields[i]}})),e})),__publicField(this,"setKeyByFieldSelector",(e=>{if(this.fieldIds.has(e))return this.fieldIds.get(e);const i=String(this.fieldIds.size+1);return this.fieldIds.set(e,i),i})),__publicField(this,"refreshAllTooltips",(()=>{this.tooltips.forEach((e=>{e.refresh()}))})),__publicField(this,"handleDocumentScroll",(()=>{this.lastScrollPosition=window.scrollY,this.isScrollTick||(window.requestAnimationFrame((()=>{this.refreshAllTooltips(),this.isScrollTick=!1})),this.isScrollTick=!0)})),__publicField(this,"formSubmitHandler",(e=>{e.preventDefault(),this.isSubmitted=!0,this.validateHandler(e)})),__publicField(this,"handleFieldChange",(e=>{let i;for(const t in this.fields){if(this.fields[t].elem===e){i=t;break}}i&&(this.fields[i].touched=!0,this.validateField(i,!0))})),__publicField(this,"handleGroupChange",(e=>{let i;for(const t in this.groupFields){if(this.groupFields[t].elems.find((i=>i===e))){i=t;break}}i&&(this.groupFields[i].touched=!0,this.validateGroup(i,!0))})),__publicField(this,"handlerChange",(e=>{e.target&&(this.handleFieldChange(e.target),this.handleGroupChange(e.target),this.renderErrors())})),this.initialize(e,i,t)}initialize(e,i,t){if(this.form=null,this.errors={},this.isValid=!1,this.isSubmitted=!1,this.globalConfig=h,this.errorLabels={},this.successLabels={},this.eventListeners=[],this.customStyleTags={},this.tooltips=[],this.currentLocale="en","string"==typeof e){const i=document.querySelector(e);if(!i)throw Error(`Form with ${e} selector not found! Please check the form selector`);this.setForm(i)}else{if(!(e instanceof HTMLFormElement))throw Error("Form selector is not valid. Please specify a string selector or a DOM element.");this.setForm(e)}if(this.globalConfig={...h,...i},t&&(this.dictLocale=[...t,...n]),this.isTooltip()){const e=document.createElement("style");e.textContent=".just-validate-error-label[data-tooltip=true]{position:fixed;padding:4px 8px;background:#423f3f;color:#fff;white-space:nowrap;z-index:10;border-radius:4px;transform:translateY(-5px)}.just-validate-error-label[data-tooltip=true]:before{content:'';width:0;height:0;border-left:solid 5px transparent;border-right:solid 5px transparent;border-bottom:solid 5px #423f3f;position:absolute;z-index:3;display:block;bottom:-5px;transform:rotate(180deg);left:calc(50% - 5px)}.just-validate-error-label[data-tooltip=true][data-direction=left]{transform:translateX(-5px)}.just-validate-error-label[data-tooltip=true][data-direction=left]:before{right:-7px;bottom:auto;left:auto;top:calc(50% - 2px);transform:rotate(90deg)}.just-validate-error-label[data-tooltip=true][data-direction=right]{transform:translateX(5px)}.just-validate-error-label[data-tooltip=true][data-direction=right]:before{right:auto;bottom:auto;left:-7px;top:calc(50% - 2px);transform:rotate(-90deg)}.just-validate-error-label[data-tooltip=true][data-direction=bottom]{transform:translateY(5px)}.just-validate-error-label[data-tooltip=true][data-direction=bottom]:before{right:auto;bottom:auto;left:calc(50% - 5px);top:-5px;transform:rotate(0)}",this.customStyleTags[a.Label]=document.head.appendChild(e),this.addListener("scroll",document,this.handleDocumentScroll)}}getLocalisedString(e,i,t){var s;const l=null!=t?t:e;let o=null==(s=this.dictLocale.find((e=>e.key===l)))?void 0:s.dict[this.currentLocale];if(o||t&&(o=t),o&&void 0!==i)switch(e){case r.MaxLength:case r.MinLength:case r.MaxNumber:case r.MinNumber:case r.MinFilesCount:case r.MaxFilesCount:o=o.replace(":value",String(i))}return o||t||"Value is incorrect"}getFieldErrorMessage(e,i){const t="function"==typeof e.errorMessage?e.errorMessage(this.getElemValue(i),this.fields):e.errorMessage;return this.getLocalisedString(e.rule,e.value,t)}getFieldSuccessMessage(e,i){const t="function"==typeof e?e(this.getElemValue(i),this.fields):e;return this.getLocalisedString(void 0,void 0,t)}getGroupErrorMessage(e){return this.getLocalisedString(e.rule,void 0,e.errorMessage)}getGroupSuccessMessage(e){if(e.successMessage)return this.getLocalisedString(void 0,void 0,e.successMessage)}setFieldInvalid(e,i){this.fields[e].isValid=!1,this.fields[e].errorMessage=this.getFieldErrorMessage(i,this.fields[e].elem)}setFieldValid(e,i){this.fields[e].isValid=!0,void 0!==i&&(this.fields[e].successMessage=this.getFieldSuccessMessage(i,this.fields[e].elem))}setGroupInvalid(e,i){this.groupFields[e].isValid=!1,this.groupFields[e].errorMessage=this.getGroupErrorMessage(i)}setGroupValid(e,i){this.groupFields[e].isValid=!0,this.groupFields[e].successMessage=this.getGroupSuccessMessage(i)}getElemValue(e){switch(e.type){case"checkbox":return e.checked;case"file":return e.files;default:return e.value}}validateGroupRule(e,i,t){if(t.rule===o.Required)i.every((e=>!e.checked))?this.setGroupInvalid(e,t):this.setGroupValid(e,t)}validateFieldRule(o,a,n,c=!1){const u=n.value,h=this.getElemValue(a);if(n.plugin){n.plugin(h,this.getCompatibleFields())||this.setFieldInvalid(o,n)}else{switch(n.rule){case r.Required:(e=>{let i=e;return"string"==typeof e&&(i=e.trim()),!i})(h)&&this.setFieldInvalid(o,n);break;case r.Email:if(l(h))break;f=h,e.test(f)||this.setFieldInvalid(o,n);break;case r.MaxLength:if(void 0===u){console.error(`Value for ${n.rule} rule for [${o}] field is not defined. The field will be always invalid.`),this.setFieldInvalid(o,n);break}if("number"!=typeof u){console.error(`Value for ${n.rule} rule for [${o}] should be a number. The field will be always invalid.`),this.setFieldInvalid(o,n);break}if(l(h))break;((e,i)=>e.length>i)(h,u)&&this.setFieldInvalid(o,n);break;case r.MinLength:if(void 0===u){console.error(`Value for ${n.rule} rule for [${o}] field is not defined. The field will be always invalid.`),this.setFieldInvalid(o,n);break}if("number"!=typeof u){console.error(`Value for ${n.rule} rule for [${o}] should be a number. The field will be always invalid.`),this.setFieldInvalid(o,n);break}if(l(h))break;((e,i)=>e.lengtht.test(e))(h)||this.setFieldInvalid(o,n);break;case r.StrongPassword:if(l(h))break;(e=>s.test(e))(h)||this.setFieldInvalid(o,n);break;case r.Number:if(l(h))break;(e=>"string"==typeof e&&!isNaN(+e)&&!isNaN(parseFloat(e)))(h)||this.setFieldInvalid(o,n);break;case r.Integer:if(l(h))break;(e=>i.test(e))(h)||this.setFieldInvalid(o,n);break;case r.MaxNumber:{if(void 0===u){console.error(`Value for ${n.rule} rule for [${o}] field is not defined. The field will be always invalid.`),this.setFieldInvalid(o,n);break}if("number"!=typeof u){console.error(`Value for ${n.rule} rule for [${o}] field should be a number. The field will be always invalid.`),this.setFieldInvalid(o,n);break}if(l(h))break;const e=+h;(Number.isNaN(e)||((e,i)=>e>i)(e,u))&&this.setFieldInvalid(o,n);break}case r.MinNumber:{if(void 0===u){console.error(`Value for ${n.rule} rule for [${o}] field is not defined. The field will be always invalid.`),this.setFieldInvalid(o,n);break}if("number"!=typeof u){console.error(`Value for ${n.rule} rule for [${o}] field should be a number. The field will be always invalid.`),this.setFieldInvalid(o,n);break}if(l(h))break;const e=+h;(Number.isNaN(e)||((e,i)=>eu){this.setFieldInvalid(o,n);break}break;case r.Files:{if(void 0===u)return console.error(`Value for ${n.rule} rule for [${o}] field is not defined. This field will be always invalid.`),void this.setFieldInvalid(o,n);if("object"!=typeof u)return console.error(`Value for ${n.rule} rule for [${o}] field should be an object. This field will be always invalid.`),void this.setFieldInvalid(o,n);const e=u.files;if("object"!=typeof e)return console.error(`Value for ${n.rule} rule for [${o}] field should be an object with files array. This field will be always invalid.`),void this.setFieldInvalid(o,n);const i=(e,i)=>{const t=Number.isFinite(i.minSize)&&e.sizei.maxSize,l=Array.isArray(i.names)&&!i.names.includes(e.name),r=Array.isArray(i.extensions)&&!i.extensions.includes(e.name.split(".")[e.name.split(".").length-1]),o=Array.isArray(i.types)&&!i.types.includes(e.type);return t||s||l||r||o};if("object"==typeof h&&null!==h)for(let t=0,s=h.length;t{e||this.setFieldInvalid(o,n)})).catch((()=>{this.setFieldInvalid(o,n)})):(console.error(`Validator function for custom rule for [${o}] field should return a Promise. This field will be always invalid.`),void this.setFieldInvalid(o,n))}this.fields[o].asyncCheckPending=!0}e||this.setFieldInvalid(o,n)}}var f}}isFormValid(){let e=!0;for(let i=0,t=Object.values(this.fields).length;i{const r=this.validateFieldRule(e,s.elem,t,i);d(r)&&l.push(r)})),s.isValid&&this.setFieldValid(e,null==(t=s.config)?void 0:t.successMessage),Promise.allSettled(l).finally((()=>{var e;i&&(null==(e=this.onValidateCallback)||e.call(this,{isValid:this.isFormValid(),isSubmitted:this.isSubmitted,fields:this.getCompatibleFields(),groups:{...this.groupFields}}))}))}revalidateField(e){if("string"!=typeof e&&!u(e))throw Error("Field selector is not valid. Please specify a string selector or a valid DOM element.");const i=this.getKeyByFieldSelector(e);return i&&this.fields[i]?new Promise((e=>{this.validateField(i,!0).finally((()=>{this.clearFieldStyle(i),this.clearFieldLabel(i),this.renderFieldError(i,!0),e(!!this.fields[i].isValid)}))})):(console.error("Field not found. Check the field selector."),Promise.reject())}revalidateGroup(e){if("string"!=typeof e&&!u(e))throw Error("Group selector is not valid. Please specify a string selector or a valid DOM element.");const i=this.getKeyByFieldSelector(e);return i&&this.groupFields[i]?new Promise((e=>{this.validateGroup(i).finally((()=>{this.clearFieldLabel(i),this.renderGroupError(i,!0),e(!!this.groupFields[i].isValid)}))})):(console.error("Group not found. Check the group selector."),Promise.reject())}validateGroup(e,i=!1){const t=this.groupFields[e],s=[];return[...t.rules].reverse().forEach((i=>{const l=this.validateGroupRule(e,t.elems,i);d(l)&&s.push(l)})),Promise.allSettled(s).finally((()=>{var e;i&&(null==(e=this.onValidateCallback)||e.call(this,{isValid:this.isFormValid(),isSubmitted:this.isSubmitted,fields:this.getCompatibleFields(),groups:{...this.groupFields}}))}))}focusInvalidField(){for(const e in this.fields){const i=this.fields[e];if(!i.isValid){setTimeout((()=>i.elem.focus()),0);break}}}afterSubmitValidation(e=!1){this.renderErrors(e),this.globalConfig.focusInvalidField&&this.focusInvalidField()}validate(e=!1){return new Promise((i=>{const t=[];Object.keys(this.fields).forEach((e=>{const i=this.validateField(e);d(i)&&t.push(i)})),Object.keys(this.groupFields).forEach((e=>{const i=this.validateGroup(e);d(i)&&t.push(i)})),Promise.allSettled(t).then((()=>{var s;this.afterSubmitValidation(e),null==(s=this.onValidateCallback)||s.call(this,{isValid:this.isFormValid(),isSubmitted:this.isSubmitted,fields:this.getCompatibleFields(),groups:{...this.groupFields}}),i(!!t.length)}))}))}revalidate(){return new Promise((e=>{this.validateHandler(void 0,!0).finally((()=>{this.globalConfig.focusInvalidField&&this.focusInvalidField(),e(this.isValid)}))}))}validateHandler(e,i=!1){return this.globalConfig.lockForm&&this.lockForm(),this.validate(i).finally((()=>{var i,t,s;this.globalConfig.lockForm&&this.unlockForm(),this.isValid?(null==(i=this.onSuccessCallback)||i.call(this,e),this.globalConfig.submitFormAutomatically&&(null==(t=null==e?void 0:e.currentTarget)||t.submit())):null==(s=this.onFailCallback)||s.call(this,this.getCompatibleFields(),this.groupFields)}))}setForm(e){this.form=e,this.form.setAttribute("novalidate","novalidate"),this.removeListener("submit",this.form,this.formSubmitHandler),this.addListener("submit",this.form,this.formSubmitHandler)}addListener(e,i,t){i.addEventListener(e,t),this.eventListeners.push({type:e,elem:i,func:t})}removeListener(e,i,t){i.removeEventListener(e,t),this.eventListeners=this.eventListeners.filter((t=>t.type!==e||t.elem!==i))}addField(e,i,t){if("string"!=typeof e&&!u(e))throw Error("Field selector is not valid. Please specify a string selector or a valid DOM element.");let s;if(s="string"==typeof e?this.form.querySelector(e):e,!s)throw Error("Field doesn't exist in the DOM! Please check the field selector.");if(!Array.isArray(i)||!i.length)throw Error("Rules argument should be an array and should contain at least 1 element.");i.forEach((e=>{if(!("rule"in e||"validator"in e||"plugin"in e))throw Error("Rules argument must contain at least one rule or validator property.");if(!(e.validator||e.plugin||e.rule&&Object.values(r).includes(e.rule)))throw Error(`Rule should be one of these types: ${Object.values(r).join(", ")}. Provided value: ${e.rule}`)}));const l=this.setKeyByFieldSelector(e);return this.fields[l]={elem:s,rules:i,isValid:void 0,touched:!1,config:t},this.setListeners(s),(this.isSubmitted||this.globalConfig.validateBeforeSubmitting)&&this.validateField(l),this}removeField(e){if("string"!=typeof e&&!u(e))throw Error("Field selector is not valid. Please specify a string selector or a valid DOM element.");const i=this.getKeyByFieldSelector(e);if(!i||!this.fields[i])return console.error("Field not found. Check the field selector."),this;const t=this.getListenerType(this.fields[i].elem.type);return this.removeListener(t,this.fields[i].elem,this.handlerChange),this.clearErrors(),delete this.fields[i],this}removeGroup(e){if("string"!=typeof e)throw Error("Group selector is not valid. Please specify a string selector.");const i=this.getKeyByFieldSelector(e);return i&&this.groupFields[i]?(this.groupFields[i].elems.forEach((e=>{const i=this.getListenerType(e.type);this.removeListener(i,e,this.handlerChange)})),this.clearErrors(),delete this.groupFields[i],this):(console.error("Group not found. Check the group selector."),this)}addRequiredGroup(e,i,t,s){if("string"!=typeof e&&!u(e))throw Error("Group selector is not valid. Please specify a string selector or a valid DOM element.");let l;if(l="string"==typeof e?this.form.querySelector(e):e,!l)throw Error("Group selector not found! Please check the group selector.");const r=l.querySelectorAll("input"),a=Array.from(r).filter((e=>{const i=((e,i)=>{const t=[...i].reverse();for(let s=0,l=t.length;s{let i=e;const t=[];for(;i;)t.unshift(i),i=i.parentNode;return t})(e));return!i||i[1].elems.find((i=>i!==e))})),n=this.setKeyByFieldSelector(e);return this.groupFields[n]={rules:[{rule:o.Required,errorMessage:i,successMessage:s}],groupElem:l,elems:a,touched:!1,isValid:void 0,config:t},r.forEach((e=>{this.setListeners(e)})),this}getListenerType(e){switch(e){case"checkbox":case"select-one":case"file":case"radio":return"change";default:return"input"}}setListeners(e){const i=this.getListenerType(e.type);this.removeListener(i,e,this.handlerChange),this.addListener(i,e,this.handlerChange)}clearFieldLabel(e){var i,t;null==(i=this.errorLabels[e])||i.remove(),null==(t=this.successLabels[e])||t.remove()}clearFieldStyle(e){var i,t,s,l;const r=this.fields[e],o=(null==(i=r.config)?void 0:i.errorFieldStyle)||this.globalConfig.errorFieldStyle;Object.keys(o).forEach((e=>{r.elem.style[e]=""}));const a=(null==(t=r.config)?void 0:t.successFieldStyle)||this.globalConfig.successFieldStyle||{};Object.keys(a).forEach((e=>{r.elem.style[e]=""})),r.elem.classList.remove(...c((null==(s=r.config)?void 0:s.errorFieldCssClass)||this.globalConfig.errorFieldCssClass),...c((null==(l=r.config)?void 0:l.successFieldCssClass)||this.globalConfig.successFieldCssClass))}clearErrors(){var e,i;Object.keys(this.errorLabels).forEach((e=>this.errorLabels[e].remove())),Object.keys(this.successLabels).forEach((e=>this.successLabels[e].remove()));for(const t in this.fields)this.clearFieldStyle(t);for(const t in this.groupFields){const s=this.groupFields[t],l=(null==(e=s.config)?void 0:e.errorFieldStyle)||this.globalConfig.errorFieldStyle;Object.keys(l).forEach((e=>{s.elems.forEach((i=>{var t;i.style[e]="",i.classList.remove(...c((null==(t=s.config)?void 0:t.errorFieldCssClass)||this.globalConfig.errorFieldCssClass))}))}));const r=(null==(i=s.config)?void 0:i.successFieldStyle)||this.globalConfig.successFieldStyle||{};Object.keys(r).forEach((e=>{s.elems.forEach((i=>{var t;i.style[e]="",i.classList.remove(...c((null==(t=s.config)?void 0:t.successFieldCssClass)||this.globalConfig.successFieldCssClass))}))}))}this.tooltips=[]}isTooltip(){return!!this.globalConfig.tooltip}lockForm(){const e=this.form.querySelectorAll("input, textarea, button, select");for(let i=0,t=e.length;i{this.renderTooltip(e,i,t)}}}createErrorLabelElem(e,i,t){const s=document.createElement("div");s.innerHTML=i;const l=this.isTooltip()?null==t?void 0:t.errorLabelStyle:(null==t?void 0:t.errorLabelStyle)||this.globalConfig.errorLabelStyle;return Object.assign(s.style,l),s.classList.add(...c((null==t?void 0:t.errorLabelCssClass)||this.globalConfig.errorLabelCssClass),"just-validate-error-label"),this.isTooltip()&&(s.dataset.tooltip="true"),this.globalConfig.testingMode&&(s.dataset.testId=`error-label-${e}`),this.errorLabels[e]=s,s}createSuccessLabelElem(e,i,t){if(void 0===i)return null;const s=document.createElement("div");s.innerHTML=i;const l=(null==t?void 0:t.successLabelStyle)||this.globalConfig.successLabelStyle;return Object.assign(s.style,l),s.classList.add(...c((null==t?void 0:t.successLabelCssClass)||this.globalConfig.successLabelCssClass),"just-validate-success-label"),this.globalConfig.testingMode&&(s.dataset.testId=`success-label-${e}`),this.successLabels[e]=s,s}renderErrorsContainer(e,i){const t=i||this.globalConfig.errorsContainer;if("string"==typeof t){const i=this.form.querySelector(t);if(i)return i.appendChild(e),!0;console.error(`Error container with ${t} selector not found. Errors will be rendered as usual`)}return t instanceof Element?(t.appendChild(e),!0):(void 0!==t&&console.error("Error container not found. It should be a string or existing Element. Errors will be rendered as usual"),!1)}renderGroupLabel(e,i,t,s){if(!s){if(this.renderErrorsContainer(i,t))return}e.appendChild(i)}renderFieldLabel(e,i,t,s){var l,r,o,a,n,d,c;if(!s){if(this.renderErrorsContainer(i,t))return}if("checkbox"===e.type||"radio"===e.type){const t=document.querySelector(`label[for="${e.getAttribute("id")}"]`);"label"===(null==(r=null==(l=e.parentElement)?void 0:l.tagName)?void 0:r.toLowerCase())?null==(a=null==(o=e.parentElement)?void 0:o.parentElement)||a.appendChild(i):t?null==(n=t.parentElement)||n.appendChild(i):null==(d=e.parentElement)||d.appendChild(i)}else null==(c=e.parentElement)||c.appendChild(i)}showLabels(e,i){Object.keys(e).forEach(((t,s)=>{const l=e[t],r=this.getKeyByFieldSelector(t);if(!r||!this.fields[r])return void console.error("Field not found. Check the field selector.");const o=this.fields[r];o.isValid=!i,this.clearFieldStyle(r),this.clearFieldLabel(r),this.renderFieldError(r,!1,l),0===s&&this.globalConfig.focusInvalidField&&setTimeout((()=>o.elem.focus()),0)}))}showErrors(e){if("object"!=typeof e)throw Error("[showErrors]: Errors should be an object with key: value format");this.showLabels(e,!0)}showSuccessLabels(e){if("object"!=typeof e)throw Error("[showSuccessLabels]: Labels should be an object with key: value format");this.showLabels(e,!1)}renderFieldError(e,i=!1,t){var s,l,r,o,a,n;const d=this.fields[e];if(!1===d.isValid&&(this.isValid=!1),void 0===d.isValid||!i&&!this.isSubmitted&&!d.touched&&void 0===t)return;if(d.isValid){if(!d.asyncCheckPending){const i=this.createSuccessLabelElem(e,void 0!==t?t:d.successMessage,d.config);i&&this.renderFieldLabel(d.elem,i,null==(s=d.config)?void 0:s.errorsContainer,!0),d.elem.classList.add(...c((null==(l=d.config)?void 0:l.successFieldCssClass)||this.globalConfig.successFieldCssClass))}return}d.elem.classList.add(...c((null==(r=d.config)?void 0:r.errorFieldCssClass)||this.globalConfig.errorFieldCssClass));const u=this.createErrorLabelElem(e,void 0!==t?t:d.errorMessage,d.config);this.renderFieldLabel(d.elem,u,null==(o=d.config)?void 0:o.errorsContainer),this.isTooltip()&&this.tooltips.push(this.renderTooltip(d.elem,u,null==(n=null==(a=d.config)?void 0:a.tooltip)?void 0:n.position))}renderGroupError(e,i=!0){var t,s,l,r;const o=this.groupFields[e];if(!1===o.isValid&&(this.isValid=!1),void 0===o.isValid||!i&&!this.isSubmitted&&!o.touched)return;if(o.isValid){o.elems.forEach((e=>{var i,t;Object.assign(e.style,(null==(i=o.config)?void 0:i.successFieldStyle)||this.globalConfig.successFieldStyle),e.classList.add(...c((null==(t=o.config)?void 0:t.successFieldCssClass)||this.globalConfig.successFieldCssClass))}));const i=this.createSuccessLabelElem(e,o.successMessage,o.config);return void(i&&this.renderGroupLabel(o.groupElem,i,null==(t=o.config)?void 0:t.errorsContainer,!0))}this.isValid=!1,o.elems.forEach((e=>{var i,t;Object.assign(e.style,(null==(i=o.config)?void 0:i.errorFieldStyle)||this.globalConfig.errorFieldStyle),e.classList.add(...c((null==(t=o.config)?void 0:t.errorFieldCssClass)||this.globalConfig.errorFieldCssClass))}));const a=this.createErrorLabelElem(e,o.errorMessage,o.config);this.renderGroupLabel(o.groupElem,a,null==(s=o.config)?void 0:s.errorsContainer),this.isTooltip()&&this.tooltips.push(this.renderTooltip(o.groupElem,a,null==(r=null==(l=o.config)?void 0:l.tooltip)?void 0:r.position))}renderErrors(e=!1){if(this.isSubmitted||e||this.globalConfig.validateBeforeSubmitting){this.clearErrors(),this.isValid=!0;for(const e in this.groupFields)this.renderGroupError(e);for(const e in this.fields)this.renderFieldError(e)}}destroy(){this.eventListeners.forEach((e=>{this.removeListener(e.type,e.elem,e.func)})),Object.keys(this.customStyleTags).forEach((e=>{this.customStyleTags[e].remove()})),this.clearErrors(),this.globalConfig.lockForm&&this.unlockForm()}refresh(){this.destroy(),this.form?(this.initialize(this.form,this.globalConfig),Object.keys(this.fields).forEach((e=>{const i=this.getFieldSelectorByKey(e);i&&this.addField(i,[...this.fields[e].rules],this.fields[e].config)}))):console.error("Cannot initialize the library! Form is not defined")}setCurrentLocale(e){"string"==typeof e||void 0===e?(this.currentLocale=e,this.isSubmitted&&this.validate()):console.error("Current locale should be a string")}onSuccess(e){return this.onSuccessCallback=e,this}onFail(e){return this.onFailCallback=e,this}onValidate(e){return this.onValidateCallback=e,this}}}));
\ No newline at end of file
diff --git a/web/intranet_repo/projects/authorization/login.php b/web/intranet_repo/projects/authorization/login.php
new file mode 100644
index 0000000..7e9bbd1
--- /dev/null
+++ b/web/intranet_repo/projects/authorization/login.php
@@ -0,0 +1,106 @@
+prepare($sql);
+ $stmt->execute([$_POST["email"]]);
+ $user = $stmt->fetch();
+
+ if ($user) {
+
+ if (isset($user['password_hash']) && password_verify($_POST["password"], $user["password_hash"])) {
+
+ session_start();
+ session_regenerate_id();
+ $_SESSION["user_id"] = $user["user_id"];
+
+ header("Location: {$g_httpHomePath}");
+ exit;
+ }
+ }
+
+ $is_invalid = true;
+}
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Ungültiger Login!
+
+
+
+
+
+
+
+
Passwort erfolgreich geändert. Sie können sich jetzt anmelden.
+
+
+
+
+
+
+
+
Neuapostolische Kirche
+
Berlin-Reinickendorf
+
+
+
Anmelden
+
+
+
+ ">
+
+
+
+
+
+ Sie haben noch keinen Account für das Intranet?
+
Registrieren
+
+ Passwort vergessen?
+
Passwort zurücksetzen
+
+ Probleme bei der Anmeldung?
+
Admin kontaktieren
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/authorization/logout.php b/web/intranet_repo/projects/authorization/logout.php
new file mode 100644
index 0000000..6cc5f09
--- /dev/null
+++ b/web/intranet_repo/projects/authorization/logout.php
@@ -0,0 +1,13 @@
+exec("SET timezone = 'Europe/Berlin'");
+
+if ($_SERVER["REQUEST_METHOD"] !== "POST") {
+ header("Location: reset-password.php");
+ exit;
+}
+
+$action = $_POST["action"] ?? '';
+
+// =============================================
+// SCHRITT 1: OTP anfordern
+// =============================================
+if ($action === "request_otp") {
+ $email = trim($_POST["email"] ?? '');
+
+ if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
+ header("Location: reset-password.php?error=1");
+ exit;
+ }
+
+ // Nutzer suchen (muss registriert sein = user_login existiert)
+ $stmt = $pdo->prepare("SELECT ugi.user_id, ugi.phone, ugi.first_name FROM user_general_information ugi INNER JOIN user_login ul ON ugi.user_id = ul.user_id WHERE ugi.mail = ?");
+ $stmt->execute([$email]);
+ $user = $stmt->fetch();
+
+ if (!$user) {
+ header("Location: reset-password.php?error=1");
+ exit;
+ }
+
+ if (empty($user['phone'])) {
+ header("Location: reset-password.php?error=2");
+ exit;
+ }
+
+ // OTP generieren (6-stellig)
+ $otp = str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT);
+
+ // Ablaufzeit als UNIX-Timestamp speichern (vermeidet Timezone-Probleme)
+ $expires_timestamp = time() + 600; // 10 Minuten
+ $expires_at = date('Y-m-d H:i:s', $expires_timestamp);
+
+ // Alte OTPs für diesen Nutzer invalidieren
+ $stmt = $pdo->prepare("UPDATE password_reset_otp SET used = 1 WHERE user_id = ? AND used = 0");
+ $stmt->execute([$user['user_id']]);
+
+ // Neues OTP speichern
+ $stmt = $pdo->prepare("INSERT INTO password_reset_otp (user_id, otp_code, expires_at) VALUES (?, ?, ?)");
+ $stmt->execute([$user['user_id'], $otp, $expires_at]);
+
+ // SMS senden (wie im Seelsorgeplaner)
+ $phone_full = "+49" . $user['phone'];
+ $messageText = "Intranet NAK Reinickendorf\n\nIhr Bestaetigungscode: {$otp}\n\nDer Code ist 10 Minuten gueltig.";
+
+ $smsData = array(
+ "phone" => $phone_full,
+ "message" => $messageText
+ );
+
+ $options = array(
+ 'http' => array(
+ 'header' => "Content-Type: application/json; charset=utf-8",
+ 'method' => 'POST',
+ 'content' => json_encode($smsData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
+ 'timeout' => 10,
+ ),
+ );
+
+ $context = stream_context_create($options);
+ $smsResult = @file_get_contents(rtrim(intranet_env('SMS_GATEWAY_URL'), '/') . '/send_sms', false, $context);
+
+ if ($smsResult === FALSE) {
+ header("Location: reset-password.php?error=4");
+ exit;
+ }
+
+ // OTP zusätzlich per Mail senden
+ $greeting = !empty($user['first_name']) ? "Hallo {$user['first_name']}" : "Hallo";
+ $mailBetreff = "Ihr Bestätigungscode";
+ $mailNachricht = "{$greeting},\n\n";
+ $mailNachricht .= "Sie haben das Zurücksetzen Ihres Passworts angefordert.\n\n";
+ $mailNachricht .= "Ihr Bestätigungscode lautet: {$otp}\n\n";
+ $mailNachricht .= "Der Code ist 10 Minuten gültig.\n\n";
+ $mailNachricht .= "Falls Sie diese Anfrage nicht gestellt haben, können Sie diese Nachricht ignorieren.";
+
+ send_intranet_mail($email, $mailBetreff, $mailNachricht);
+
+ // Telefonnummer maskieren für Anzeige (nur letzte 3 Ziffern zeigen)
+ $phone_masked = str_repeat('*', max(0, strlen($user['phone']) - 3)) . substr($user['phone'], -3);
+
+ header("Location: reset-password.php?step=verify&uid={$user['user_id']}&phone=" . urlencode($phone_masked));
+ exit;
+}
+
+// =============================================
+// SCHRITT 2: OTP verifizieren → Reset-Token erzeugen
+// =============================================
+if ($action === "verify_otp") {
+ $user_id = (int)($_POST["user_id"] ?? 0);
+ $otp_code = trim($_POST["otp_code"] ?? '');
+
+ if ($user_id === 0 || strlen($otp_code) !== 6) {
+ header("Location: reset-password.php?error=3");
+ exit;
+ }
+
+ // OTP prüfen – Vergleich mit PHP-Zeit statt DB NOW()
+ $now = date('Y-m-d H:i:s');
+ $stmt = $pdo->prepare("SELECT id FROM password_reset_otp WHERE user_id = ? AND otp_code = ? AND used = 0 AND expires_at > ?");
+ $stmt->execute([$user_id, $otp_code, $now]);
+ $otpRecord = $stmt->fetch();
+
+ if (!$otpRecord) {
+ header("Location: reset-password.php?step=verify&uid={$user_id}&error=3");
+ exit;
+ }
+
+ // OTP als verwendet markieren
+ $stmt = $pdo->prepare("UPDATE password_reset_otp SET used = 1 WHERE id = ?");
+ $stmt->execute([$otpRecord['id']]);
+
+ // Einmal-Token für Passwort-Änderung generieren (in Session speichern)
+ $reset_token = bin2hex(random_bytes(32));
+ if (session_status() == PHP_SESSION_NONE) {
+ session_start();
+ }
+ $_SESSION['reset_token'] = $reset_token;
+ $_SESSION['reset_user_id'] = $user_id;
+ $_SESSION['reset_token_expires'] = time() + 600; // 10 Minuten
+
+ header("Location: reset-password.php?step=newpassword&token=" . urlencode($reset_token));
+ exit;
+}
+
+// =============================================
+// SCHRITT 3: Neues Passwort setzen (nach OTP-Verifizierung)
+// =============================================
+if ($action === "set_password") {
+ if (session_status() == PHP_SESSION_NONE) {
+ session_start();
+ }
+
+ $reset_token = $_POST["reset_token"] ?? '';
+ $new_password = $_POST["new_password"] ?? '';
+ $new_password_confirm = $_POST["new_password_confirm"] ?? '';
+
+ // Token validieren
+ if (empty($reset_token)
+ || !isset($_SESSION['reset_token'])
+ || $reset_token !== $_SESSION['reset_token']
+ || !isset($_SESSION['reset_token_expires'])
+ || $_SESSION['reset_token_expires'] < time()) {
+ // Session-Daten aufräumen
+ unset($_SESSION['reset_token'], $_SESSION['reset_user_id'], $_SESSION['reset_token_expires']);
+ header("Location: reset-password.php?error=7");
+ exit;
+ }
+
+ $user_id = (int)$_SESSION['reset_user_id'];
+
+ // Passwort-Validierung
+ if (strlen($new_password) < 8 || !preg_match("/[a-z]/i", $new_password) || !preg_match("/[0-9]/", $new_password)) {
+ header("Location: reset-password.php?step=newpassword&token=" . urlencode($reset_token) . "&error=5");
+ exit;
+ }
+
+ if ($new_password !== $new_password_confirm) {
+ header("Location: reset-password.php?step=newpassword&token=" . urlencode($reset_token) . "&error=6");
+ exit;
+ }
+
+ // Neues Passwort setzen
+ $password_hash = password_hash($new_password, PASSWORD_DEFAULT);
+ $stmt = $pdo->prepare("UPDATE user_login SET password_hash = ? WHERE user_id = ?");
+ $stmt->execute([$password_hash, $user_id]);
+
+ // User-Daten laden für Mail und Vermerk
+ $stmt = $pdo->prepare("SELECT mail, first_name, last_name FROM user_general_information WHERE user_id = ?");
+ $stmt->execute([$user_id]);
+ $resetUserData = $stmt->fetch();
+
+ if ($resetUserData) {
+ $loginUrl = "https://" . ($_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN')) . "/intranet_repo/projects/authorization/login.php";
+ $greeting = !empty($resetUserData['first_name']) ? "Hallo {$resetUserData['first_name']}" : "Hallo";
+
+ $mailBetreff = "Passwort erfolgreich geändert";
+ $mailNachricht = "{$greeting},\n\n";
+ $mailNachricht .= "Ihr Passwort für das Intranet der NAK Reinickendorf wurde erfolgreich geändert.\n\n";
+ $mailNachricht .= "Sie können sich ab sofort mit Ihrem neuen Passwort anmelden.";
+
+ send_intranet_mail($resetUserData['mail'], $mailBetreff, $mailNachricht, null, "intranet@nak-reinickendorf.de", "Jetzt anmelden", $loginUrl);
+
+ // Vermerk in erledigte Tasks
+ $userName = trim(($resetUserData['first_name'] ?? '') . ' ' . ($resetUserData['last_name'] ?? ''));
+ if (empty($userName)) $userName = $resetUserData['mail'];
+
+ $taskSubject = "Passwort selbst zurückgesetzt: {$userName}";
+ $taskMessage = "{$userName} ({$resetUserData['mail']}) hat das eigene Passwort erfolgreich zurückgesetzt.";
+
+ $stmt = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
+ $stmt->execute([$userName, $resetUserData['mail'], $taskSubject, $taskMessage]);
+ }
+
+ // Session-Daten aufräumen
+ unset($_SESSION['reset_token'], $_SESSION['reset_user_id'], $_SESSION['reset_token_expires']);
+
+ header("Location: login.php?message=1");
+ exit;
+}
+
+header("Location: reset-password.php");
+exit;
+?>
diff --git a/web/intranet_repo/projects/authorization/reset-password.php b/web/intranet_repo/projects/authorization/reset-password.php
new file mode 100644
index 0000000..9d93673
--- /dev/null
+++ b/web/intranet_repo/projects/authorization/reset-password.php
@@ -0,0 +1,159 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ "E-Mail-Adresse nicht gefunden oder nicht registriert.",
+ 2 => "Keine Telefonnummer hinterlegt. Bitte wenden Sie sich an den Administrator.",
+ 3 => "Ungültiger oder abgelaufener Code.",
+ 4 => "SMS konnte nicht gesendet werden. Bitte versuchen Sie es erneut.",
+ 5 => "Passwort muss mindestens 8 Zeichen, einen Buchstaben und eine Zahl enthalten.",
+ 6 => "Passwörter stimmen nicht überein.",
+ 7 => "Sitzung abgelaufen. Bitte starten Sie den Vorgang erneut."
+ ];
+ $errorNum = (int)$_GET["error"];
+ $errorText = $errorMessages[$errorNum] ?? "Unbekannter Fehler.";
+ ?>
+
+
+
= htmlspecialchars($errorText) ?>
+
+
+
+
+
+
+
+
Passwort erfolgreich geändert. Sie können sich jetzt anmelden.
+
+
+
+
+
+
+
+
+
Neuapostolische Kirche
+
Berlin-Reinickendorf
+
+
+
Passwort zurücksetzen
+
+
+
+
Geben Sie Ihre E-Mail-Adresse ein. Ein Bestätigungscode wird an Ihre hinterlegte Telefonnummer und per Mail gesendet.
+
+
+
+ E-Mailadresse
+
+
+
+ Code anfordern
+
+
+
+
+
+
+
Ein 6-stelliger Code wurde an die Nummer +49 = $phone_masked ?> und an Ihre E-Mail-Adresse gesendet.
+
+
+
+ ">
+
+ Bestätigungscode
+
+
+
+
+
+
+
+
+
+
Ihr Code wurde bestätigt. Geben Sie nun Ihr neues Passwort ein.
+
+
+
+ ">
+
+ Neues Passwort
+
+
+
+
+ Neues Passwort wiederholen
+
+
+
+
+
+
+
+
+
+
+
+
Zurück zur Anmeldung
+
+ Probleme beim Zurücksetzen?
+
Admin kontaktieren
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/authorization/self-signup-process.php b/web/intranet_repo/projects/authorization/self-signup-process.php
new file mode 100644
index 0000000..ca86ca7
--- /dev/null
+++ b/web/intranet_repo/projects/authorization/self-signup-process.php
@@ -0,0 +1,97 @@
+prepare($checkSql);
+$stmt->execute([$email]);
+$count1 = $stmt->fetchColumn();
+
+$checkSql2 = "SELECT COUNT(*) FROM pending_registrations WHERE email = ? AND status = 'pending'";
+$stmt2 = $pdo->prepare($checkSql2);
+$stmt2->execute([$email]);
+$count2 = $stmt2->fetchColumn();
+
+if ($count1 > 0 || $count2 > 0) {
+ header("Location: self-signup.php?error=2");
+ exit;
+}
+
+// Passwort hashen und Token generieren
+$password_hash = password_hash($_POST["password"], PASSWORD_DEFAULT);
+$token = bin2hex(random_bytes(32));
+
+// In pending_registrations speichern
+$insertSql = "INSERT INTO pending_registrations (first_name, last_name, email, password_hash, token) VALUES (?, ?, ?, ?, ?)";
+$stmt = $pdo->prepare($insertSql);
+if (!$stmt) {
+ header("Location: self-signup.php?error=1");
+ exit;
+}
+
+if (!$stmt->execute([$first_name, $last_name, $email, $password_hash, $token])) {
+ header("Location: self-signup.php?error=1");
+ exit;
+}
+$pending_id = $pdo->lastInsertId();
+
+// Admin-Nachricht in Inbox speichern
+$subject = "Neue Registrierungsanfrage: {$first_name} {$last_name}";
+$message_text = "Neue Selbstregistrierung:\n\nName: {$first_name} {$last_name}\nE-Mail: {$email}\n\nBitte im Adminpanel freischalten.";
+$data_json = json_encode(['pending_id' => $pending_id, 'token' => $token]);
+
+$inboxSql = "INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, data_json) VALUES ('registration', ?, ?, ?, ?, ?)";
+$stmt = $pdo->prepare($inboxSql);
+$sender_name = "{$first_name} {$last_name}";
+$stmt->execute([$sender_name, $email, $subject, $message_text, $data_json]);
+
+// Mail an Admin-Team (Admin + Technik)
+$admin_email = ADMIN_EMAIL;
+$mail_betreff = "Intranet: Neue Registrierungsanfrage von {$first_name} {$last_name}";
+$mail_nachricht = "Hallo Admin,\n\n";
+$mail_nachricht .= "Es liegt eine neue Registrierungsanfrage vor:\n\n";
+$mail_nachricht .= "Name: {$first_name} {$last_name}\n";
+$mail_nachricht .= "E-Mail: {$email}\n\n";
+$mail_nachricht .= "Bitte melden Sie sich im Adminpanel an, um die Anfrage zu bearbeiten.\n\n";
+$mail_nachricht .= "-- Intranet NAK Reinickendorf";
+
+send_admin_team_mail($pdo, $mail_betreff, $mail_nachricht);
+
+// Erfolg
+header("Location: self-signup.php?success=1");
+exit;
+?>
diff --git a/web/intranet_repo/projects/authorization/self-signup.php b/web/intranet_repo/projects/authorization/self-signup.php
new file mode 100644
index 0000000..dc8f546
--- /dev/null
+++ b/web/intranet_repo/projects/authorization/self-signup.php
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.
+
+
+
+
+
+
Diese E-Mailadresse ist bereits registriert oder es liegt bereits eine Anfrage vor.
+
+
+
+
+
+
+
+
+
Ihre Registrierungsanfrage wurde gesendet. Sie erhalten eine Benachrichtigung, sobald Ihr Konto freigeschaltet wurde.
+
+
+
+
+
+
+
+
+
Neuapostolische Kirche
+
Berlin-Reinickendorf
+
+
+
Registrierung beantragen
+
Ihr Antrag wird von einem Administrator geprüft und freigeschaltet.
+
+
+ Vorname
+
+
+
+
+ Nachname
+
+
+
+
+ E-Mailadresse
+
+
+
+
+ Passwort
+
+
+
+
+ Passwort wiederholen
+
+
+
+
+
+
+
+ Sie haben bereits einen Account?
+
+
Anmelden
+
+ Sie wurden bereits vom Admin vorregistriert?
+
+
Direkt registrieren
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/authorization/signup-process.php b/web/intranet_repo/projects/authorization/signup-process.php
new file mode 100644
index 0000000..b2c6999
--- /dev/null
+++ b/web/intranet_repo/projects/authorization/signup-process.php
@@ -0,0 +1,118 @@
+prepare("SELECT user_id FROM user_general_information WHERE mail = ?");
+$stmt->execute([$email]);
+$user_id = $stmt->fetchColumn();
+
+if ($user_id) {
+ // Nutzer ist vorregistriert – prüfen ob bereits Login existiert
+ $stmt = $pdo->prepare("SELECT COUNT(*) FROM user_login WHERE user_id = ?");
+ $stmt->execute([$user_id]);
+ $loginExists = $stmt->fetchColumn();
+
+ if ($loginExists > 0) {
+ // Bereits vollständig registriert
+ header("Location: signup.php?error=2");
+ exit;
+ }
+
+ // Direkte Registrierung durchführen
+ $stmt = $pdo->prepare("INSERT INTO user_login (user_id, password_hash) VALUES (?, ?)");
+
+ if ($stmt->execute([$user_id, $password_hash])) {
+
+ // Namen und Telefonnummer aktualisieren
+ $stmt = $pdo->prepare("UPDATE user_general_information SET first_name=?, last_name=?, phone=? WHERE user_id=?");
+ $stmt->execute([$first_name, $last_name, $phone, $user_id]);
+
+ header("Location: login.php");
+ exit;
+ } else {
+ header("Location: signup.php?error=1");
+ exit;
+ }
+}
+
+// === Prüfung 2: Liegt bereits ein Antrag vor? ===
+$stmt = $pdo->prepare("SELECT COUNT(*) FROM pending_registrations WHERE email = ? AND status = 'pending'");
+$stmt->execute([$email]);
+$pendingCount = $stmt->fetchColumn();
+
+if ($pendingCount > 0) {
+ header("Location: signup.php?error=2");
+ exit;
+}
+
+// === Antrag stellen (Nutzer nicht vorregistriert) ===
+$token = bin2hex(random_bytes(32));
+
+$stmt = $pdo->prepare("INSERT INTO pending_registrations (first_name, last_name, email, phone, password_hash, token) VALUES (?, ?, ?, ?, ?, ?)");
+
+if (!$stmt->execute([$first_name, $last_name, $email, $phone, $password_hash, $token])) {
+ header("Location: signup.php?error=1");
+ exit;
+}
+$pending_id = $pdo->lastInsertId();
+
+// Admin-Nachricht in Inbox speichern
+$subject = "Neue Registrierungsanfrage: {$first_name} {$last_name}";
+$message_text = "Neue Registrierungsanfrage:\n\nName: {$first_name} {$last_name}\nE-Mail: {$email}\n\nBitte im Adminpanel freischalten.";
+$data_json = json_encode(['pending_id' => $pending_id, 'token' => $token]);
+
+$stmt = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, data_json) VALUES ('registration', ?, ?, ?, ?, ?)");
+$sender_name = "{$first_name} {$last_name}";
+$stmt->execute([$sender_name, $email, $subject, $message_text, $data_json]);
+
+// Mail an Admin-Team (Admin + Technik)
+$mail_betreff = "Neue Registrierungsanfrage von {$first_name} {$last_name}";
+$mail_nachricht = "Es liegt eine neue Registrierungsanfrage vor:\n\n";
+$mail_nachricht .= "Name: {$first_name} {$last_name}\n";
+$mail_nachricht .= "E-Mail: {$email}\n";
+$mail_nachricht .= "Telefon: {$phone}\n\n";
+$mail_nachricht .= "Bitte melden Sie sich im Adminpanel an, um die Anfrage zu bearbeiten.";
+
+send_admin_team_mail($pdo, $mail_betreff, $mail_nachricht, "Im Adminpanel bearbeiten", "https://" . ($_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN')) . "/intranet_repo/projects/adminpanel/admin_inbox.php");
+
+// Erfolg – gleiche Meldung wie bei direkter Registrierung (Nutzer merkt keinen Unterschied)
+header("Location: signup.php?success=1");
+exit;
+?>
diff --git a/web/intranet_repo/projects/authorization/signup.php b/web/intranet_repo/projects/authorization/signup.php
new file mode 100644
index 0000000..9863a4f
--- /dev/null
+++ b/web/intranet_repo/projects/authorization/signup.php
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.
+
+
+
+
+
+
Diese E-Mailadresse ist bereits registriert oder es liegt bereits eine Anfrage vor.
+
+
+
+
+
+
+
+
+
Ihre Registrierung wurde beantragt. Sie erhalten eine Benachrichtigung, sobald Ihr Konto freigeschaltet wurde.
+
+
+
+
+
+
+
+
+
Neuapostolische Kirche
+
Berlin-Reinickendorf
+
+
+
Registrieren
+
Erstellen Sie Ihren persönlichen Zugang zum Intranet.
+
+
+ Vorname
+
+
+
+
+ Nachname
+
+
+
+
+ E-Mailadresse
+
+
+
+
+ Telefonnummer
+
+
+ Passwort
+
+
+
+
+ Passwort wiederholen
+
+
+
+
+
+
+
+ Sie haben bereits einen Account?
+
+
Anmelden
+
+ Probleme bei der Registrierung?
+
Admin kontaktieren
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/authorization/validate-email.php b/web/intranet_repo/projects/authorization/validate-email.php
new file mode 100644
index 0000000..4340828
--- /dev/null
+++ b/web/intranet_repo/projects/authorization/validate-email.php
@@ -0,0 +1,40 @@
+ false]);
+ exit;
+}
+
+$email = trim($_GET["email"]);
+$is_available = true;
+
+// Prüfung 1: Gibt es einen vollständigen Login für diese E-Mail?
+$stmt = $pdo->prepare("SELECT u.user_id FROM user_general_information u INNER JOIN user_login l ON u.user_id = l.user_id WHERE u.mail = ?");
+$stmt->execute([$email]);
+
+if ($stmt->fetch()) {
+ $is_available = false;
+}
+
+// Prüfung 2: Liegt bereits ein offener Antrag vor?
+if ($is_available) {
+ $stmt = $pdo->prepare("SELECT COUNT(*) FROM pending_registrations WHERE email = ? AND status = 'pending'");
+ $stmt->execute([$email]);
+ $pendingCount = $stmt->fetchColumn();
+
+ if ($pendingCount > 0) {
+ $is_available = false;
+ }
+}
+
+echo json_encode(["available" => $is_available]);
+exit;
+?>
diff --git a/web/intranet_repo/projects/authorization/validation.js b/web/intranet_repo/projects/authorization/validation.js
new file mode 100644
index 0000000..5169dd7
--- /dev/null
+++ b/web/intranet_repo/projects/authorization/validation.js
@@ -0,0 +1,74 @@
+const validation = new JustValidate("#signup");
+validation
+ .addField("#first_name", [
+ {
+ rule: "required",
+ errorMessage: "Pflichtfeld"
+ }
+ ])
+ .addField("#last_name", [
+ {
+ rule: "required",
+ errorMessage: "Pflichtfeld"
+ }
+ ])
+ .addField("#email", [
+ {
+ rule: "required",
+ errorMessage: "Pflichtfeld"
+ },
+ {
+ rule: "email",
+ errorMessage: "Ungültig"
+ },
+ {
+ validator: (value) => () => {
+ return fetch("validate-email.php?email=" + encodeURIComponent(value))
+ .then(function(response) {
+ return response.json();
+ })
+ .then(function(json) {
+ return json.available;
+ });
+ },
+ errorMessage: "Bereits registriert"
+ }
+ ])
+ .addField("#phone", [
+ {
+ rule: "required",
+ errorMessage: "Pflichtfeld"
+ },
+ {
+ rule: "minLength",
+ value: 6,
+ errorMessage: "Ungültige Telefonnummer"
+ },
+ {
+ validator: (value) => {
+ return /^[1-9][0-9\s\-\/()]{4,}$/.test(value);
+ },
+ errorMessage: "Ohne führende 0, nur Ziffern"
+ }
+ ])
+ .addField("#password", [
+ {
+ rule: "required",
+ errorMessage: "Pflichtfeld"
+ },
+ {
+ rule: "password",
+ errorMessage: "Zu leicht"
+ }
+ ])
+ .addField("#password_confirmation", [
+ {
+ validator: (value, fields) => {
+ return value === fields["#password"].elem.value;
+ },
+ errorMessage: "Passwörter müssen übereinstimmen"
+ }
+ ])
+ .onSuccess((event) => {
+ document.getElementById("signup").submit();
+ });
diff --git a/web/intranet_repo/projects/brandschutz/Brandschutzordnung.pdf b/web/intranet_repo/projects/brandschutz/Brandschutzordnung.pdf
new file mode 100644
index 0000000..c9b9440
Binary files /dev/null and b/web/intranet_repo/projects/brandschutz/Brandschutzordnung.pdf differ
diff --git a/web/intranet_repo/projects/brandschutz/brandschutz.php b/web/intranet_repo/projects/brandschutz/brandschutz.php
new file mode 100644
index 0000000..7f20610
--- /dev/null
+++ b/web/intranet_repo/projects/brandschutz/brandschutz.php
@@ -0,0 +1,3070 @@
+prepare("SELECT \"Brandschutz\" FROM user_permissions WHERE user_id = ?");
+ $stmtCheck->execute([$userId]);
+ $hasBrandschutz = $stmtCheck->fetchColumn();
+
+ if ((int)$hasBrandschutz === 1) {
+ $today = date('Y-m-d');
+
+ // Tracking-Eintrag aktualisieren oder anlegen
+ $stmtUpsert = $pdo->prepare("INSERT INTO brandschutz_training (user_id, last_training_date, reminder_count, admin_reminder_count) VALUES (?, ?, 0, 0) ON CONFLICT (user_id) DO UPDATE SET last_training_date = EXCLUDED.last_training_date, reminder_count = 0, admin_reminder_count = 0");
+ $stmtUpsert->execute([$userId, $today]);
+
+ // Bestätigungsmail an den Nutzer senden
+ require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
+ $userName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
+ $greeting = !empty($user['first_name']) ? "Hallo {$user['first_name']}" : "Hallo";
+ $mailSubject = "Brandschutzunterweisung erfolgreich absolviert";
+ $mailMessage = "{$greeting},\n\nSie haben die Brandschutzunterweisung am " . date('d.m.Y') . " erfolgreich absolviert.\n\nIhre nächste Unterweisung ist bis zum " . date('d.m.Y', strtotime('+365 days')) . " fällig.\n\nVielen Dank für Ihre Teilnahme!";
+ send_intranet_mail($user['mail'], $mailSubject, $mailMessage);
+
+ // Systemnachricht ins Admin-Postfach (erledigt)
+ if (empty($userName)) $userName = $user['mail'] ?? 'Unbekannt';
+ $sysSubject = "Brandschutzschulung absolviert: {$userName}";
+ $sysMessage = "{$userName} hat die Brandschutzunterweisung am " . date('d.m.Y') . " erfolgreich absolviert.";
+ $senderMail = $user['mail'] ?? '';
+ $stmtMsg = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
+ $stmtMsg->execute([$userName, $senderMail, $sysSubject, $sysMessage]);
+
+ header("Location: /intranet_repo/projects/intranet/index.php?message=8");
+ exit();
+ }
+ }
+?>
+
+
+
+
+ Brandschutzunterweisung
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Willkommen zur Brandschutzunterweisung
+
+
+ Brandschutz geht uns alle an. Diese Unterweisung vermittelt Ihnen das notwendige Wissen, um Brände zu vermeiden,
+ im Ernstfall richtig zu handeln und Ihre Aufgaben im Rahmen des Brandschutzes wahrzunehmen.
+ Die Schulung vermittelt die Inhalte der für uns geltenden
Brandschutzordnung nach DIN 14096 und ist in fünf Säulen gegliedert.
+ Die Schulung dauert nur etwa
20 Minuten und kann bequem vom Handy oder PC aus durchgeführt werden.
+
+
+
+
+
+ Unser Brandschutz steht auf fünf Säulen : Brände vermeiden, im Brandfall richtig handeln,
+ sicherheitsrelevante Aufgaben pflichtbewusst erledigen, Feuerlöscher sicher einsetzen und die besonderen Gegebenheiten
+ unserer Kirchengemeinde kennen. Jede Säule trägt dazu bei, Menschen zu schützen, Sachschäden zu verhindern
+ und im Ernstfall klare Orientierung zu geben.
+
+
+
+ 1
+ Brände vermeiden
+
+
+ 2
+ Im Brandfall richtig handeln
+
+
+ 3
+ Aufgaben des vBsU
+
+
+ 4
+ Feuerlöscher nutzen
+
+
+ 5
+ Unsere Kircheneinrichtung kennen
+
+
+
+
+
+
+
+
+ Schulung starten >
+
+
+
+
+
+
+
+
+
Grundlagen der Brandverhütung
+
+ Die beste Brandbekämpfung ist die Brandverhütung. Durch Ordnung, Sauberkeit und umsichtiges Verhalten lassen sich die meisten Brände vermeiden.
+ Die folgenden Regeln gelten für alle Räumlichkeiten unserer Kirchengemeinde.
+
+
+
+
Grundregel
+ Es besteht
Rauchverbot in allen Gebäudeteilen. Brennbare Abfälle dürfen nicht in Gebäuden gelagert werden.
+
+
+
+ Ordnung und Sauberkeit : Fluchtwege, Treppen und Verkehrsflächen müssen stets freigehalten werden.
+ Brennbare Abfälle müssen regelmäßig und ordnungsgemäß entsorgt werden. Keine Lagerung in Gebäuden.
+ Brandbekämpfungseinrichtungen : Feuerlöscher, Brandmeldeeinrichtungen, Notausgänge, Hauptschalter und Absperrschieber müssen ständig freizugänglich und gekennzeichnet sein. Bei Mängeln sofort den Gemeindevorsteher informieren.
+ Flucht- und Rettungswege dürfen niemals zugestellt oder versperrt werden. Sicherheitsschilder und Fluchtpläne dürfen nicht entfernt, verdeckt oder zugestellt werden.
+ Brennbare Flüssigkeiten und Gase müssen getrennt und sicher gelagert werden (nur in zugelassenen Behältern).
+
+
+
+
+ < Einleitung
+
+
+ Weiter >
+
+
+
+
+
+
+
Sichere Nutzung elektrischer Geräte
+
+ Elektrische Defekte zählen zu den häufigsten Brandursachen. Die folgenden Regeln sind strikt einzuhalten:
+
+
+
+ Geräte abschalten : Alle elektrischen Geräte müssen nach Gebrauch abgeschaltet oder vom Netz getrennt werden.
+ Stand-by-Betrieb : Geräte dürfen nur im Stand-by laufen, wenn sie unter Aufsicht stehen.
+ Defekte Geräte : Beschädigte Kabel, Stecker oder Geräte sofort außer Betrieb nehmen und melden.
+ Mehrfachsteckdosen nicht hintereinanderschalten. Maximale Belastung beachten.
+ Private Elektrogeräte nur nach Genehmigung verwenden.
+
+
+
+
Tipp
+ Beim Verlassen eines Raumes immer prüfen: Sind alle Geräte ausgeschaltet? Sind Kaffeemaschine, Wasserkocher und Heizlüfter vom Netz getrennt?
+
+
+
+
+ < Zurück
+
+
+ Weiter >
+
+
+
+
+
+
+
Kerzen und Dekoration
+
+ Offenes Feuer ist eine der häufigsten Brandursachen in Versammlungsräumen. Für unsere Kirchengemeinde gelten besondere Regeln:
+
+
+
+ Adventskränze und -gestecke nur mit elektrischen Kerzen erlaubt.
+ Kerzen am Altar dürfen nur unter ständiger Aufsicht brennen. Löschgerät in Griffweite bereithalten.
+ Dekoration nur aus schwerentflammbarem Material. Abstand zu Wärmequellen einhalten.
+ Schweiß- und Trennschleifarbeiten nur mit schriftlicher Genehmigung und festgelegten Brandschutzmaßnahmen.
+
+
+
+
+ < Zurück
+
+
+ Weiter zum Quiz >
+
+
+
+
+
+
+
Testen Sie Ihr Wissen
+
+
+
1. Welche Aussage zur Lagerung brennbarer Materialien ist korrekt?
+
+
+
+ ●
+ Brennbare Abfälle dürfen kurzzeitig im Flur gelagert werden.
+
+
+
+ ●
+ Brennbare Flüssigkeiten und Gase müssen getrennt und in zugelassenen Behältern gelagert werden.
+
+
+
+ ●
+ Brennbare Materialien dürfen neben Feuerlöschern abgestellt werden.
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
2. Was gilt für Adventskränze in unseren Kirchenräumen?
+
+
+
+ ●
+ Echte Kerzen sind erlaubt, wenn jemand daneben steht.
+
+
+
+ ●
+ Nur elektrische Kerzen sind auf Adventskränzen und -gestecken erlaubt.
+
+
+
+ ●
+ Kerzen auf Adventskränzen sind generell verboten, auch elektrische.
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
3. Wann dürfen Schweiß- und Trennschleifarbeiten durchgeführt werden?
+
+
+
+ ●
+ Jederzeit, wenn ein Feuerlöscher in der Nähe steht.
+
+
+
+ ●
+ Nur mit schriftlicher Genehmigung und festgelegten Brandschutzmaßnahmen.
+
+
+
+ ●
+ Nur am Wochenende, wenn niemand im Gebäude ist.
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
+ < Zurück
+
+
+ Weiter zu Säule 2 >
+
+
+
+
+
+
+
+
+
+
Sofortmaßnahmen bei Brandentdeckung
+
+ Wird ein Brand entdeckt, ist schnelles, aber besonnenes Handeln entscheidend.
+ Panik ist der größte Feind, bleiben Sie ruhig und handeln Sie systematisch.
+
+
+
+
Oberste Regel
+
Ruhe bewahren! Überlegtes Handeln rettet Leben. Panik führt zu Fehlentscheidungen und gefährdet Sie und andere.
+
+
+
+ Folgende Sofortmaßnahmen sind bei Brandentdeckung zu ergreifen und sollten bei Möglichkeit an mehrere Personen delegiert werden.
+
+
+
+ A Alarmieren : Feuerwehr über Notruf 112 alarmieren.
+ L Leute warnen : Alle Anwesenden informieren, laut und deutlich warnen.
+ A Assistieren : Gefährdete und hilfebedürftige Personen unterstützen.
+ R Räumen : Gebäude über die gekennzeichneten Fluchtwege verlassen, Aufzug nicht benutzen und Sammelstelle aufsuchen.
+ M Möglichst löschen : Entstehungsbrand nur bekämpfen, wenn dies gefahrlos möglich ist; Feuerlöscher einsetzen.
+
+
+
+
+
+ < Säule 1
+
+
+ Weiter >
+
+
+
+
+
+
+
Der Notruf und die 5 W-Fragen
+
+ Die Feuerwehr benötigt bei jedem Notruf klare Informationen. Merken Sie sich die 5 W-Fragen :
+
+
+
+ Wo brennt es? (Adresse, Gebäudeteil, Stockwerk)
+ Was brennt? (Art und Umfang des Brandes)
+ Wie viele Personen sind betroffen/verletzt?
+ Welche Verletzungen liegen vor?
+ Warten auf Rückfragen der Leitstelle!
+
+
+
+
Unsere Adresse für den Notruf
+
Klixstraße 17, 13403 Berlin
+ Diese Adresse findest du innen an der rechten Tür des Haupteingangs (Fluchttür).
+
+
+
+
+ < Zurück
+
+
+ Weiter >
+
+
+
+
+
+
+
Evakuierung und Flucht
+
+ Im Brandfall ist die geordnete Evakuierung aller Personen oberstes Ziel.
+
+
+
+ Fluchtweg nutzen : Immer die gekennzeichneten Fluchtwege verwenden. Aufzüge nicht benutzen!
+ Türen schließen : Brandabschnittstüren und Rauchschutztüren schließen (nicht abschließen!).
+ Sammelplatz aufsuchen : Am vereinbarten Sammelplatz einfinden und auf Vollzähligkeitsprüfung warten.
+ Verrauchte Bereiche meiden : Bei Rauch: Gebückt gehen, Rauch steigt nach oben.
+ Einweiser für Feuerwehr : Wenn möglich, eine Person zur Einweisung der Feuerwehr abstellen.
+
+
+
+
Warum Türen schließen?
+
Geschlossene Türen bremsen Feuer und Rauch und halten Fluchtwege länger nutzbar.
+ Türen sollten deshalb geschlossen gehalten werden, aber nicht verkeilt oder blockiert werden!
+
+
+
+
Was tun, wenn der Fluchtweg versperrt ist?
+
+ Wenn ein Fluchtweg durch Rauch oder Feuer versperrt ist: Gehen Sie in einen rauchfreien Raum,
+ schließen Sie die Tür, dichten Sie Ritzen mit nassen Tüchern ab und machen Sie sich am Fenster
+ bemerkbar. Warten Sie auf die Feuerwehr. Springen Sie niemals aus oberen Stockwerken!
+
+
+
+
+ < Zurück
+
+
+ Weiter zu den Übungen >
+
+
+
+
+
+
+
Szenario-Übung
+
+
+
+
+
+ Szenario: Sie bemerken nach dem Gottesdienst Rauch im Treppenhaus. Mehrere Gemeindemitglieder,
+ darunter zwei ältere Personen mit Gehilfe, befinden sich noch im ersten Stock. Was tun Sie?
+
+
+
+ Welche Verhaltensweisen sind hier falsch ? Wählen Sie alle Antworten aus, die man nicht tun sollte.
+
+
+
+
+ ●
+ Schnell durch das verrauchte Treppenhaus laufen, um die älteren Personen herunterzubringen.
+
+
+
+ ●
+ Ruhe bewahren, 112 anrufen, alle warnen, mit den älteren Personen in einem rauchfreien Raum warten und sich am Fenster bemerkbar machen.
+
+
+
+ ●
+ Durch das verrauchte Treppenhaus gehen und versuchen, den Brand selbst zu löschen.
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
+
+
Ergänzen Sie die Lücken:
+
+ Die Notrufnummer lautet .
+ Die Adresse unserer Kirchengemeinde lautet
+ mit Postleitzahl .
+
+
+
+ Antwort prüfen
+
+
+
+
+
+ < Zurück
+
+
+ Weiter zu Säule 3 >
+
+
+
+
+
+
+
+
+
+
Wer ist der/die vBsU?
+
+ Der/die verantwortliche Brandschutzunterwiesene (vBsU) ist die Person, die bei einer Veranstaltung
+ in der Kirchengemeinde die Verantwortung für den Brandschutz übernimmt. Diese Rolle ist essenziell für die
+ Sicherheit aller Anwesenden.
+
+
+
+
Kernaufgabe
+ Der/die vBsU ist
30 Minuten vor Veranstaltungsbeginn anwesend und führt vor Ort eine Reihe von
+ Prüfungen und Vorbereitungen durch, um die Sicherheit zu gewährleisten.
+
+
+
+
+
+ Jedes volljährige Gemeindemitglied, das diese Brandschutzunterweisung erfolgreich absolviert hat und bereit ist,
+ die Verantwortung zu übernehmen. Eine Liste der aktuellen vBsU liegt beim Gemeindevorsteher aus.
+
+
+
+
+
+ < Säule 2
+
+
+ Weiter >
+
+
+
+
+
+
+
Vor der Veranstaltung
+
+ Der/die vBsU führt 30 Minuten vor Beginn folgende Prüfungen durch:
+
+
+
+
+ ●
+ Fluchtwege und Notausgänge frei und benutzbar?
+
+
+ ●
+ Feuerlöscher vorhanden und zugänglich?
+
+
+ ●
+ Notbeleuchtung funktionsfähig?
+
+
+ ●
+ Sammelplatz (Bürgersteig, Parkplatz) bekannt und zugänglich?
+
+
+ ●
+ Hilfebedürftige Personen identifiziert?
+
+
+ ●
+ Helfer eingeteilt (Evakuierung, Notruf, Einweisung)?
+
+
+ ●
+ Durchsagetext und Notrufadressen griffbereit?
+
+
+
+
+
+ < Zurück
+
+
+ Weiter >
+
+
+
+
+
+
+
Im Notfall
+
+ Im Brandfall übernimmt der/die vBsU die Koordination aller Maßnahmen:
+
+
+
+ Evakuierung einleiten : Durchsage vom Pult verlesen (rotes, laminiertes DIN-A4-Blatt).
+ Notruf veranlassen : 112 anrufen oder delegieren.
+ Helfer koordinieren : Aufgaben an vorher eingeteilte Helfer vergeben.
+ Gebäude kontrollieren : Sicherstellen, dass alle Räume geräumt sind.
+ Vollzähligkeitsprüfung : Am Sammelplatz die Anwesenheit prüfen.
+ Feuerwehr einweisen : Informationen zu Brandort, vermissten Personen und Gefahren geben.
+
+
+
+
Durchsage im Notfall
+ Der vorbereitete Text zur Räumung des Gebäudes wird vom Pult aus verlesen. So hören alle Anwesenden
+ gleichzeitig die gleiche, klare Ansage. Der Text liegt als rotes, laminiertes DIN-A4-Blatt bereit.
+
+
+
+
+ < Zurück
+
+
+ Weiter zu den Übungen >
+
+
+
+
+
+
+
Zuordnungsübung und Quiz
+
+
Übung: Ordnen Sie jede Aufgabe der richtigen Phase zu.
+
+
+
+ Fluchtwege prüfen
+
+ -- Bitte wählen --
+ Vor der Veranstaltung
+ Im Notfall
+
+
+
+ Evakuierungsdurchsage
+
+ -- Bitte wählen --
+ Vor der Veranstaltung
+ Im Notfall
+
+
+
+ Hilfebedürftige identifizieren
+
+ -- Bitte wählen --
+ Vor der Veranstaltung
+ Im Notfall
+
+
+
+ Vollzähligkeitsprüfung
+
+ -- Bitte wählen --
+ Vor der Veranstaltung
+ Im Notfall
+
+
+
+
+
+ Antwort prüfen
+
+
+
+
Wie lange vor Veranstaltungsbeginn muss der/die vBsU anwesend sein?
+
+
+
+ ●
+ 15 Minuten vorher
+
+
+
+ ●
+ 30 Minuten vorher
+
+
+
+ ●
+ 60 Minuten vorher
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
Was ist die erste Handlung des/der vBsU im Brandfall?
+
+
+
+ ●
+ Den Brand selbst löschen.
+
+
+
+ ●
+ Die Evakuierung einleiten und per Durchsage alle Anwesenden warnen.
+
+
+
+ ●
+ Warten, bis die Feuerwehr eintrifft.
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
+ < Zurück
+
+
+ Weiter zu Säule 4 >
+
+
+
+
+
+
+
+
+
+
Grundsätze der Brandbekämpfung
+
+ Feuerlöscher sind für die Bekämpfung von Entstehungsbränden gedacht, also für kleine,
+ gerade entstandene Brände. Bei einem ausgedehnten Brand ist sofortige Evakuierung Pflicht!
+
+
+
+
Wichtigster Grundsatz
+
Personenrettung geht IMMER vor Brandbekämpfung!
+ Bringen Sie zuerst alle Personen in Sicherheit, bevor Sie versuchen, einen Brand zu löschen.
+
+
+
+ Rückzugsweg freihalten : Löschen Sie immer mit dem Rücken zum Ausgang, damit Sie jederzeit fliehen können.
+ Feuerlöscher erst am Brandherd aktivieren : Nicht schon auf dem Weg zum Brand auslösen (begrenzte Löschdauer: ca. 10-15 Sekunden).
+ Windrichtung beachten : Immer mit dem Wind löschen, nicht gegen den Wind.
+ Stoßweise löschen : Kurze, gezielte Stöße sind effektiver als Dauerbetrieb.
+ Nicht in Flammen spritzen : An der Basis des Feuers ansetzen, von vorne nach hinten und von unten nach oben.
+
+
+
+
Wussten Sie?
+ Ein handelsüblicher 6-kg-Pulverlöscher hat nur ca. 10-15 Sekunden Löschdauer. Daher: Erst am Brand aktivieren und gezielt einsetzen!
+
+
+
+
+ < Säule 3
+
+
+ Weiter >
+
+
+
+
+
+
+
Richtige Löschtechnik
+
Die richtige Anwendung entscheidet über den Löscherfolg:
+
+
+
+ Wenn verfügbar, mehrere Feuerlöscher gleichzeitig (nicht nacheinander!) einsetzen.
+ Die Löschwirkung vervielfacht sich dadurch.
+
+
+
+
+
+ Von vorne nach hinten löschen. An der vorderen Kante des Feuers beginnen und
+ das Löschmittel gleichmäßig über die Fläche verteilen.
+
+
+
+
+
+ Von oben nach unten löschen. Der Schwerkraft folgen.
+
+
+
+
+
+ < Zurück
+
+
+ Weiter >
+
+
+
+
+
+
+
Besonderheiten und Gefahren
+
+
+ Elektrobrände : Niemals mit Wasser löschen! CO2-Löscher oder Pulverlöscher verwenden. Gerät wenn möglich vom Netz trennen.
+ Fettbrände : Niemals mit Wasser löschen (Fettexplosion)! Deckel drauf, Löschdecke oder speziellen Fettbrandlöscher verwenden.
+ Nach dem Löschen : Brandstelle nicht verlassen, Rückzündung möglich. Feuerwehr informieren. Benutzten Feuerlöscher nicht wieder aufhängen, sondern sofort zur Neubefüllung melden.
+
+
+
+
Nach dem Löschen
+ Einen benutzten Feuerlöscher immer sofort als benutzt kennzeichnen und die Neubefüllung veranlassen, auch wenn er nur kurz ausgelöst wurde.
+
+
+
+
+ < Zurück
+
+
+ Weiter zum Quiz >
+
+
+
+
+
+
+
Testen Sie Ihr Wissen
+
+
+
+
+
+
+
+ Szenario: Sie entdecken einen kleinen Brand an einer Steckdosenleiste im Gemeindesaal.
+ Die Steckdosenleiste steht unter Strom. In der Nähe befinden sich ein CO2-Löscher
+ und ein Eimer Wasser. Was tun Sie?
+
+
+
+
+
+
+
+ < Zurück
+
+
+ Weiter zu Säule 5 >
+
+
+
+
+
+
+
+
+
+
Unsere Räumlichkeiten und Ausstattung
+
+
+ In den vorangegangenen Säulen hast du die allgemeinen Grundlagen des Brandschutzes kennengelernt.
+ Jetzt wird es konkret: Wir schauen uns an, wie der Brandschutz in unserer Kirchengemeinde
+ organisiert ist, wo sich Feuerlöscher und Notleuchten befinden, wie die Räumung abläuft und
+ welche Besonderheiten unsere Rettungswege haben.
+
+
+
+ Unsere Gemeinde auf einen Blick:
+ Unsere Kirchengemeinde fasst 180 bis 200 Personen . Wir verfügen über
+ keine Brandmeldeanlage , umso wichtiger ist es, dass jeder die Abläufe
+ im Brandfall kennt und weiß, wo sich die wichtigsten Hilfsmittel befinden.
+
+
+
+ Standorte der Feuerlöscher:
+
+
+ Vorraum am seitlichen Ausgang
+ Treppenhaus 1. OG
+ Hinterer Kirchenanbau neben der Toilette
+
+
+
+ Wichtig: Die Adresse unserer Kirchengemeinde (für den Notruf) findest du
+ innen an der rechten Tür des Haupteingangs (Fluchttür): Klixstraße 17, 13403 Berlin .
+
+
+
+
+ < Säule 4
+
+
+ Weiter >
+
+
+
+
+
+
+
Rettungswege und Gefahrenstellen
+
+
+ Ausgänge aus dem Kirchenschiff:
+
+
+ Haupteingang (Achtung: Stufen!)
+ Hinterer Kirchenanbau (Achtung: Stufen!)
+ Seitlicher Eingang (Achtung: Gegenverkehr von der Empore!)
+
+
+
+ Potenzielle Gefahrenstellen auf den Rettungswegen:
+
+
+ Stufen zwischen Kirchenschiff und Anbau
+ Stufen vor dem Haupteingang
+ Treppenhaus an der Empore (Gegenverkehr)
+
+
+
+ Koordination im Brandfall: Die unmittelbaren Aktionen werden
+ ausschließlich durch die zuständige vBsU koordiniert. Eine klare Zuständigkeit
+ verhindert Chaos und widersprüchliche Anweisungen. An den potenziellen Gefahrendstellen sollte im Brandfall bei Möglichkeit eine BsU positioniert werden.
+
+
+
+
+ < Zurück
+
+
+ Weiter >
+
+
+
+
+
+
+
Quiz Teil 1
+
+
+
+
+
+
+
Verfügt unsere Kirchengemeinde über eine Brandmeldeanlage?
+
+
+
+ ●
+ Nein, wir haben keine Brandmeldeanlage.
+
+
+
+ ●
+ Ja, in jedem Raum sind Rauchmelder installiert.
+
+
+
+ ●
+ Ja, aber nur im Kirchenschiff.
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
Wo findest du die Adresse der Kirchengemeinde für den Notruf?
+
+
+
+ ●
+ Innen an der Haupteingangstür.
+
+
+
+ ●
+ Auf dem Altar.
+
+
+
+ ●
+ Im Treppenhaus 1. OG.
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
Wie viele Personen fasst unser Kirchenschiff?
+
+
+
+ ●
+ 180 bis 200 Personen.
+
+
+
+ ●
+ 200 bis 230 Personen.
+
+
+
+ ●
+ 150 bis 180 Personen.
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
+ < Zurück
+
+
+ Weiter >
+
+
+
+
+
+
+
Quiz Teil 2
+
+
+
+
+
Wer koordiniert im Brandfall die Aktionen?
+
+
+
+ ●
+ Die zuständige vBsU.
+
+
+
+ ●
+ Der Gemeindevorsteher.
+
+
+
+ ●
+ Anwesende BsU gemeinsam.
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
+
+
Wie wird die Räumung eingeleitet?
+
+
+
+ ●
+ Der vorbereitete Text wird vom Pult aus verlesen.
+
+
+
+ ●
+ Es gibt eine Alarmsirene.
+
+
+
+ ●
+ Die anwesenden BsU koordinieren die Räumung in ihrem erreichbaren Umfeld.
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
Wer übernimmt die weitere Erste Hilfe im Brandfall?
+
+
+
+ ●
+ Anwesende Ersthelfer*innen.
+
+
+
+ ●
+ Die vBsU.
+
+
+
+ ●
+ Niemand, wir warten auf den Rettungsdienst.
+
+
+
+
+ Antwort prüfen
+
+
+
+
+
+
+ < Zurück
+
+
+ Weiter >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/brandschutz/brandschutz.png b/web/intranet_repo/projects/brandschutz/brandschutz.png
new file mode 100644
index 0000000..c4e489e
Binary files /dev/null and b/web/intranet_repo/projects/brandschutz/brandschutz.png differ
diff --git a/web/intranet_repo/projects/brandschutz/brandschutz_cron.php b/web/intranet_repo/projects/brandschutz/brandschutz_cron.php
new file mode 100644
index 0000000..d4d30b2
--- /dev/null
+++ b/web/intranet_repo/projects/brandschutz/brandschutz_cron.php
@@ -0,0 +1,162 @@
+query($sql);
+$users = $result->fetchAll(PDO::FETCH_ASSOC);
+
+foreach ($users as $bsUser) {
+ $userId = (int)$bsUser['user_id'];
+ $firstName = $bsUser['first_name'] ?? '';
+ $lastName = $bsUser['last_name'] ?? '';
+ $mail = $bsUser['mail'] ?? '';
+ $fullName = trim("{$firstName} {$lastName}");
+ if (empty($fullName)) $fullName = $mail;
+ $greeting = !empty($firstName) ? "Hallo {$firstName}" : "Hallo";
+
+ $lastTraining = $bsUser['last_training_date'];
+ $reminderCount = (int)($bsUser['reminder_count'] ?? 0);
+ $adminReminderCount = (int)($bsUser['admin_reminder_count'] ?? 0);
+
+ // Sicherstellen dass ein Tracking-Eintrag existiert
+ $stmtEnsure = $pdo->prepare("INSERT INTO brandschutz_training (user_id) VALUES (?) ON CONFLICT (user_id) DO NOTHING");
+ $stmtEnsure->execute([$userId]);
+
+ // Fälligkeit berechnen
+ if (empty($lastTraining)) {
+ // Nie geschult = sofort fällig
+ $dueDate = $today;
+ $daysUntilDue = -1; // Bereits fällig
+ $daysOverdue = 1;
+ } else {
+ $dueDate = date('Y-m-d', strtotime($lastTraining . ' +365 days'));
+ $daysUntilDue = (int)((strtotime($dueDate) - strtotime($today)) / 86400);
+ $daysOverdue = $daysUntilDue < 0 ? abs($daysUntilDue) : 0;
+ }
+
+ // Szenario 1: Einen Monat vor Ablauf (0-30 Tage verbleibend) - Erste Erinnerung
+ if ($daysUntilDue >= 0 && $daysUntilDue <= 30 && $reminderCount === 0) {
+ $dueDateFormatted = date('d.m.Y', strtotime($dueDate));
+ $subject = "Erinnerung: Brandschutzunterweisung bis {$dueDateFormatted} fällig";
+ $message = "{$greeting},\n\nIhre Brandschutzunterweisung läuft am {$dueDateFormatted} ab.\n\n";
+ $message .= "Die Schulung dauert nur etwa 20 Minuten und kann bequem vom Handy oder PC aus durchgeführt werden.\n\n";
+ $message .= "Bitte absolvieren Sie die Unterweisung innerhalb der nächsten zwei Monate über das Intranet.\n\n";
+ $message .= "Vielen Dank!";
+
+ $brandschutzUrl = "https://" . $_SERVER['HTTP_HOST'] . "/intranet_repo/projects/brandschutz/brandschutz.php";
+ send_intranet_mail($mail, $subject, $message, null, "intranet@nak-reinickendorf.de", "Zur Brandschutzunterweisung", $brandschutzUrl);
+
+ // Zähler erhöhen
+ $stmtUpdate = $pdo->prepare("UPDATE brandschutz_training SET reminder_count = reminder_count + 1 WHERE user_id = ?");
+ $stmtUpdate->execute([$userId]);
+
+ // Log in erledigte Nachrichten
+ $sysSubject = "Brandschutz-Erinnerung gesendet: {$fullName}";
+ $sysMessage = "Automatische Erinnerungsmail an {$fullName} ({$mail}) gesendet. Schulung fällig am {$dueDateFormatted}.";
+ $stmtLog = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', 'System', ?, ?, ?, 'done', NOW(), NOW())");
+ $stmtLog->execute([$mail, $sysSubject, $sysMessage]);
+
+ $log[] = "Erste Erinnerung an {$fullName} gesendet (fällig am {$dueDateFormatted}).";
+ }
+
+ // Szenario 2: Frist überschritten - Monatliche Erinnerung an Nutzer
+ if ($daysOverdue > 0) {
+ $dueDateFormatted = !empty($lastTraining) ? date('d.m.Y', strtotime($dueDate)) : 'sofort';
+ $monthsOverdue = (int)ceil($daysOverdue / 30);
+
+ $subject = "Überfällig: Brandschutzunterweisung ({$daysOverdue} Tage)";
+ $message = "{$greeting},\n\nIhre Brandschutzunterweisung ist seit {$daysOverdue} Tagen überfällig";
+ if ($dueDateFormatted !== 'sofort') {
+ $message .= " (Frist war der {$dueDateFormatted})";
+ }
+ $message .= ".\n\nDie Schulung dauert nur etwa 20 Minuten und kann bequem vom Handy oder PC aus durchgeführt werden.\n\n";
+ $message .= "Bitte absolvieren Sie die Unterweisung umgehend über das Intranet.\n\n";
+ $message .= "Dies ist die {$reminderCount}. Erinnerung zu dieser Schulung.";
+
+ $brandschutzUrl = "https://" . $_SERVER['HTTP_HOST'] . "/intranet_repo/projects/brandschutz/brandschutz.php";
+ send_intranet_mail($mail, $subject, $message, null, "intranet@nak-reinickendorf.de", "Zur Brandschutzunterweisung", $brandschutzUrl);
+
+ // Zähler erhöhen
+ $stmtUpdate = $pdo->prepare("UPDATE brandschutz_training SET reminder_count = reminder_count + 1 WHERE user_id = ?");
+ $stmtUpdate->execute([$userId]);
+
+ // Log in erledigte Nachrichten
+ $sysSubject = "Brandschutz-Erinnerung gesendet: {$fullName} ({$daysOverdue} Tage überfällig)";
+ $sysMessage = "Automatische Erinnerungsmail (Nr. " . ($reminderCount + 1) . ") an {$fullName} ({$mail}) gesendet. Schulung seit {$daysOverdue} Tagen überfällig.";
+ $stmtLog = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', 'System', ?, ?, ?, 'done', NOW(), NOW())");
+ $stmtLog->execute([$mail, $sysSubject, $sysMessage]);
+
+ $log[] = "Erinnerung Nr." . ($reminderCount + 1) . " an {$fullName} ({$daysOverdue} Tage überfällig).";
+
+ // Szenario 3: 4+ Monate überfällig - Eskalation an Brandschutzbeauftragte + Admins
+ if ($daysOverdue >= 120) {
+ $adminSubject = "ESKALATION: Brandschutzunterweisung überfällig – {$fullName}";
+ $adminMessage = "Die Brandschutzunterweisung von {$fullName} ({$mail}) ist seit {$daysOverdue} Tagen überfällig (mehr als 4 Monate).\n\n";
+ $adminMessage .= "Bitte erinnern Sie die Person persönlich an die ausstehende Schulung.\n\n";
+ $adminMessage .= "Bisherige Erinnerungsmails an den Nutzer: " . ($reminderCount + 1) . "\n";
+ $adminMessage .= "Bisherige Admin-Eskalationen: " . ($adminReminderCount + 1);
+
+ // Mail an Brandschutzbeauftragte
+ send_brandschutzbeauftragter_mail($pdo, $adminSubject, $adminMessage);
+
+ // Aktive (pending) Systemnachricht im Postfach
+ $dataJson = json_encode([
+ 'action' => 'brandschutz_escalation',
+ 'user_id' => $userId,
+ 'user_name' => $fullName,
+ 'days_overdue' => $daysOverdue
+ ]);
+ $stmtAdmin = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, created_at, data_json) VALUES ('system', 'System', ?, ?, ?, 'pending', NOW(), ?)");
+ $stmtAdmin->execute([$mail, $adminSubject, $adminMessage, $dataJson]);
+
+ // Admin-Zähler erhöhen
+ $stmtAdminCount = $pdo->prepare("UPDATE brandschutz_training SET admin_reminder_count = admin_reminder_count + 1 WHERE user_id = ?");
+ $stmtAdminCount->execute([$userId]);
+
+ $log[] = "ESKALATION an Brandschutzbeauftragte für {$fullName} ({$daysOverdue} Tage überfällig).";
+ }
+ }
+}
+
+$pdo = null;
+
+// Ausgabe/Logging
+$output = "Brandschutz-Cron ausgeführt am " . date('d.m.Y H:i:s') . "\n";
+$output .= count($users) . " Nutzer mit Brandschutz-Rolle geprüft.\n";
+if (!empty($log)) {
+ $output .= "\nAktionen:\n" . implode("\n", $log);
+} else {
+ $output .= "\nKeine Aktionen erforderlich.";
+}
+
+echo $output . "\n";
diff --git a/web/intranet_repo/projects/contentpanel/insertion/insert.php b/web/intranet_repo/projects/contentpanel/insertion/insert.php
new file mode 100644
index 0000000..53c01d9
--- /dev/null
+++ b/web/intranet_repo/projects/contentpanel/insertion/insert.php
@@ -0,0 +1,1021 @@
+ Text per pdftotext (Poppler) */
+function pdfToText($pdfFullPath) {
+ $bin = trim(shell_exec('command -v pdftotext'));
+ if ($bin === '') { $_SESSION['meldung_gottesdienst'] = "Fehler: 'pdftotext' nicht gefunden."; return false; }
+ $tmpTxt = tempnam(sys_get_temp_dir(), 'lg_').'.txt';
+ $cmd = escapeshellcmd($bin).' -layout -enc UTF-8 '.escapeshellarg($pdfFullPath).' '.escapeshellarg($tmpTxt).' 2>&1';
+ exec($cmd, $out, $ret);
+ if ($ret !== 0) { @unlink($tmpTxt); $_SESSION['meldung_gottesdienst'] = "Fehler beim PDF-Import:\n".htmlspecialchars(implode("\n", $out)); return false; }
+ $txt = file_get_contents($tmpTxt);
+ @unlink($tmpTxt);
+ return $txt;
+}
+
+function parseLeitgedanken($textRaw) {
+ // Zeilenenden normalisieren + Silbentrennungen (…-\n…) zusammenziehen
+ $text = preg_replace("/\r\n?/", "\n", $textRaw);
+ $text = preg_replace("/(?<=\p{L})-\n(?=\p{L})/u", "", $text);
+ $lines = explode("\n", $text);
+
+ // Abschnittsüberschriften mit Datum am Zeilenende
+ $dateHeaderRegex = '/(?P.*?)(?P\d{1,2})(?:\.\s*\/\s*(?P\d{1,2}))?\.(?P\d{1,2})\.(?P\d{4})\s*$/u';
+
+ // Whitelist der (deutschen) Buchkürzel
+ $BOOKS = '(?:Mo|Sam|Kön|Chr|Dan|Ps|Spr|Pred|Hld|Hi|Jes|Jer|Klgl|Hes|Ez|Hos|Joel|Am|Ob|Mi|Nah|Hab|Zef|Hag|Sach|Mal|Rut|Ri|Jos|Esra|Neh|Est|'
+ . 'Mt|Mk|Lk|Joh|Apg|Röm|Kor|Gal|Eph|Phil|Kol|Thess|Tim|Tit|Phlm|Hebr|Jak|Petr|Joh|Jud|Offb)';
+
+ // Referenz am Zeilenende
+ $refRegex = '/(?P[(?:[1-3]\.?\s*)?' . $BOOKS . '\s*\d{1,3}\s*,\s*\d{1,3}(?:[–\-]\d{1,3}(?:[a-z])?|(?:\.\d{1,3})+(?:[a-z])?|[a-z])?)\s*$/u';
+
+ // Abschnittsgrenzen sammeln
+ $sections = [];
+ for ($i = 0; $i < count($lines); $i++) {
+ if (preg_match($dateHeaderRegex, $lines[$i], $m)) {
+ $d = str_pad((int)$m['d1'], 2, '0', STR_PAD_LEFT);
+ $mn = str_pad((int)$m['m'], 2, '0', STR_PAD_LEFT);
+ $y = (int)$m['y'];
+ $sections[] = ['start'=>$i, 'date'=>sprintf('%04d-%02d-%02d',$y,$mn,$d)];
+ }
+ }
+ if (empty($sections)) return [];
+ $sections[array_key_last($sections)]['end'] = count($lines);
+ for ($s = 0; $s < count($sections)-1; $s++) { $sections[$s]['end'] = $sections[$s+1]['start']; }
+
+ $out = [];
+ foreach ($sections as $sec) {
+ for ($i = $sec['start']; $i < $sec['end']; $i++) {
+ $line = rtrim($lines[$i]); if ($line === '') continue;
+ if (preg_match($refRegex, $line, $mRef)) {
+ $beforeRef = trim(preg_replace($refRegex, '', $line));
+ $blkPrev = [];
+ for ($k = $i - 1; $k >= $sec['start'] && $k >= $i - 15; $k--) {
+ $pl = rtrim($lines[$k]); if ($pl === '') break;
+ if (preg_match($refRegex, $pl)) break;
+ $blkPrev[] = $pl;
+ }
+ $block = array_reverse($blkPrev);
+ if ($beforeRef !== '') $block[] = $beforeRef;
+ $vers = trim(preg_replace('/\s+/', ' ', implode(' ', $block)));
+ if ($vers !== '') {
+ $out[] = ['bibeldatum'=>$sec['date'], 'bibelwort'=>trim($mRef['ref']), 'bibelvers'=>$vers];
+ break; // pro Abschnitt nur das erste Bibelwort
+ }
+ }
+ }
+ }
+ // Dubletten raus
+ $uniq=[]; $final=[];
+ foreach ($out as $e) { $key=$e['bibeldatum'].'|'.$e['bibelwort']; if (!isset($uniq[$key])) { $uniq[$key]=1; $final[]=$e; } }
+ return $final;
+}
+
+// Hilfsfunktion, um PDF in PNG umzuwandeln
+function convertPDFtoPNG($pdfFilePath, $outputDirectory, $baseFileName) {
+ if (!is_file($pdfFilePath)) {
+ error_log('PDF-Datei nicht gefunden: ' . $pdfFilePath);
+ return false;
+ }
+
+ $outputFilePath = $outputDirectory . '/' . $baseFileName . '_page_%d.png';
+
+ $command =
+ 'gs -dSAFER -dBATCH -dNOPAUSE '
+ . '-sDEVICE=png16m -r300 '
+ . '-sOutputFile=' . escapeshellarg($outputFilePath) . ' '
+ . escapeshellarg($pdfFilePath)
+ . ' 2>&1';
+
+ $output = [];
+ $returnCode = 0;
+ exec($command, $output, $returnCode);
+
+ if ($returnCode !== 0) {
+ error_log(
+ 'Ghostscript-Konvertierung fehlgeschlagen: '
+ . implode("\n", $output)
+ );
+ return false;
+ }
+
+ return true;
+}
+
+// LG-PDF importieren -> direkt alles in DB
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['leitgedanken_pdf'])) {
+ if ($_FILES['leitgedanken_pdf']['error'] !== UPLOAD_ERR_OK) {
+ $_SESSION['meldung_gottesdienst'] = 'Fehler beim Upload der Leitgedanken-PDF.';
+ } else {
+ $ext = strtolower(pathinfo($_FILES['leitgedanken_pdf']['name'], PATHINFO_EXTENSION));
+ if ($ext !== 'pdf') {
+ $_SESSION['meldung_gottesdienst'] = 'Nur PDF-Dateien sind für den Import erlaubt.';
+ } else {
+ $txt = pdfToText($_FILES['leitgedanken_pdf']['tmp_name']);
+ if ($txt !== false) {
+ $items = parseLeitgedanken($txt);
+
+ // vorbereitete Statements
+ $stmtExists = $pdo->prepare("SELECT 1 FROM gottesdienst WHERE bibeldatum = ? AND bibelwort = ? LIMIT 1");
+ $stmtInsert = $pdo->prepare("INSERT INTO gottesdienst (bibelwort, bibelvers, bibeldatum) VALUES (?, ?, ?)");
+
+ $ok=0; $dup=0;
+ foreach ($items as $it) {
+ $stmtExists->execute([$it['bibeldatum'], $it['bibelwort']]);
+ if ($stmtExists->fetchColumn() !== false) { $dup++; continue; }
+
+ $stmtInsert->execute([$it['bibelwort'], $it['bibelvers'], $it['bibeldatum']]); $ok++;
+ }
+
+ if ($ok > 0) {
+ $_SESSION['meldung_gottesdienst'] = "Leitgedanken importiert: $ok Einträge angelegt".($dup?(", $dup dupliziert übersprungen"):"").".";
+ } else {
+ $_SESSION['meldung_gottesdienst'] = "Keine neuen Einträge erkannt.".($dup?(" ($dup Duplikate)"):"");
+ }
+ }
+ }
+ }
+ header("Location: ".$_SERVER['PHP_SELF']);
+ exit();
+}
+
+// Aushang manuell löschen
+if (
+ $_SERVER['REQUEST_METHOD'] === 'POST' &&
+ isset($_POST['delete_file_base'])
+) {
+ $fileBase = (string) $_POST['delete_file_base'];
+
+ if (!isValidNewsFileBase($fileBase)) {
+ $_SESSION['meldung_dateien'] = 'Ungültiger Dateiname.';
+ } elseif (deleteNewsFiles($fileBase)) {
+ $_SESSION['meldung_dateien'] = 'Aushang wurde vollständig gelöscht.';
+ } else {
+ $_SESSION['meldung_dateien'] =
+ 'Aushang konnte nicht vollständig gelöscht werden. Bitte Logs prüfen.';
+ }
+
+ header('Location: insert.php#dateien');
+ exit();
+}
+
+// Neuen Aushang als PDF hochladen und in PNG-Seiten umwandeln
+if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['fileToUpload'])) {
+ $date = (string) ($_POST['date'] ?? '');
+ $dateObject = DateTimeImmutable::createFromFormat('!Y-m-d', $date);
+
+ if (
+ $dateObject === false ||
+ $dateObject->format('Y-m-d') !== $date
+ ) {
+ $_SESSION['meldung_dateien'] = 'Ungültiges Verfallsdatum.';
+ header('Location: insert.php#dateien');
+ exit();
+ }
+
+ // Bereinige den eingegebenen Namen.
+ $name = sanitizeFileName((string) ($_POST['name'] ?? ''));
+ $name = trim($name, '_-');
+
+ if ($name === '') {
+ $_SESSION['meldung_dateien'] = 'Bitte einen gültigen Titel eingeben.';
+ header('Location: insert.php#dateien');
+ exit();
+ }
+
+ if ($_FILES['fileToUpload']['error'] !== UPLOAD_ERR_OK) {
+ $_SESSION['meldung_dateien'] = 'Fehler beim Upload der PDF-Datei.';
+ header('Location: insert.php#dateien');
+ exit();
+ }
+
+ $fileExtension = strtolower(
+ pathinfo($_FILES['fileToUpload']['name'], PATHINFO_EXTENSION)
+ );
+
+ if ($fileExtension !== 'pdf') {
+ $_SESSION['meldung_dateien'] = 'Nur PDF-Dateien sind erlaubt.';
+ header('Location: insert.php#dateien');
+ exit();
+ }
+
+ $directoryPath =
+ '/var/www/html/intranet_repo/projects/contentpanel/insertion/news/';
+ $imageOutputDirectory = $directoryPath . 'png/';
+
+ if (
+ !is_dir($imageOutputDirectory) &&
+ !mkdir($imageOutputDirectory, 0755, true) &&
+ !is_dir($imageOutputDirectory)
+ ) {
+ $_SESSION['meldung_dateien'] =
+ 'Das Aushang-Verzeichnis konnte nicht angelegt werden.';
+ header('Location: insert.php#dateien');
+ exit();
+ }
+
+ $fileName = generateFileName($date, $name, 'pdf');
+ $targetFilePath = $directoryPath . $fileName;
+ $baseFileName = pathinfo($fileName, PATHINFO_FILENAME);
+
+ if (!isValidNewsFileBase($baseFileName)) {
+ $_SESSION['meldung_dateien'] = 'Ungültiger Dateiname.';
+ header('Location: insert.php#dateien');
+ exit();
+ }
+
+ if (!move_uploaded_file(
+ $_FILES['fileToUpload']['tmp_name'],
+ $targetFilePath
+ )) {
+ $_SESSION['meldung_dateien'] = 'Fehler beim Speichern der PDF-Datei.';
+ header('Location: insert.php#dateien');
+ exit();
+ }
+
+ if (!convertPDFtoPNG(
+ $targetFilePath,
+ $imageOutputDirectory,
+ $baseFileName
+ )) {
+ // Fehlgeschlagenen Upload vollständig aufräumen.
+ deleteNewsFiles($baseFileName);
+
+ $_SESSION['meldung_dateien'] =
+ 'Fehler bei der Umwandlung der PDF. '
+ . 'Die unvollständigen Dateien wurden entfernt.';
+
+ header('Location: insert.php#dateien');
+ exit();
+ }
+
+ $_SESSION['meldung_dateien'] = 'Aushang wurde erfolgreich hochgeladen.';
+ header('Location: insert.php#dateien');
+ exit();
+}
+
+// Löschen eines Gottesdienstes
+if (isset($_POST['delete_id'])) {
+ $delete_id = $_POST['delete_id'];
+
+ // Löschanfrage vorbereiten
+ try {
+ $sql_delete = "DELETE FROM gottesdienst WHERE id = ?";
+ $stmt = $pdo->prepare($sql_delete);
+ $stmt->execute([$delete_id]);
+ $_SESSION['meldung_gottesdienst'] = "Eintrag erfolgreich gelöscht!";
+ header("Location: " . $_SERVER['PHP_SELF']); // Seite neu laden
+ exit(); // Skript nach dem Redirect beenden
+ } catch (Exception $e) {
+ $_SESSION['meldung_gottesdienst'] = "Fehler beim Löschen: " . $e->getMessage();
+ }
+}
+
+// Gottesdienst anlegen (ohne EL)
+if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['bibelwort']) && isset($_POST['bibelvers']) && isset($_POST['bibeldatum'])) {
+ $bibelwort = $_POST['bibelwort'];
+ $bibelvers = $_POST['bibelvers'];
+ $bibeldatum = $_POST['bibeldatum'];
+
+ try {
+ $sql = "INSERT INTO gottesdienst (bibelwort, bibelvers, bibeldatum) VALUES (?, ?, ?)";
+ $stmt = $pdo->prepare($sql);
+ $stmt->execute([$bibelwort, $bibelvers, $bibeldatum]);
+ setlocale(LC_TIME, 'de_DE.UTF-8');
+ $formattedDate = strftime('%d. %B', strtotime($bibeldatum));
+ $_SESSION['meldung_gottesdienst'] = "Gottesdienst am " . $formattedDate . " erfolgreich angelegt!";
+ header("Location: " . $_SERVER['PHP_SELF']); exit();
+ } catch (Exception $e) {
+ $_SESSION['meldung_gottesdienst'] = "Fehler beim Einfügen: " . $e->getMessage();
+ }
+}
+
+
+// Ansage anlegen
+if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['ansage_datum_von'])) {
+ $ansage_datum_von = $_POST['ansage_datum_von'];
+ $ansage_datum_bis = !empty($_POST['ansage_datum_bis']) ? $_POST['ansage_datum_bis'] : NULL;
+ $ansage_uhrzeit = !empty($_POST['ansage_uhrzeit']) ? $_POST['ansage_uhrzeit'] : NULL;
+ $ansage_ort = $_POST['ansage_ort'];
+ $ansage_titel = $_POST['ansage_titel'];
+ $ansage_untertitel = $_POST['ansage_untertitel'];
+
+ try {
+ // SQL-Anweisung
+ $sql = "INSERT INTO ansagen (ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel)
+ VALUES (?, ?, ?, ?, ?, ?)";
+
+ // Statement vorbereiten
+ $stmt = $pdo->prepare($sql);
+
+ // Statement ausführen
+ $stmt->execute([$ansage_datum_von, $ansage_datum_bis, $ansage_uhrzeit, $ansage_ort, $ansage_titel, $ansage_untertitel]);
+ setlocale(LC_TIME, 'de_DE.UTF-8');
+ $formattedDate = strftime('%d. %B', strtotime($ansage_datum_von));
+ $_SESSION['meldung_ansage'] = $ansage_titel . " am " . $formattedDate . " erfolgreich angelegt!";
+ header("Location: " . $_SERVER['PHP_SELF']);
+ exit();
+ } catch (Exception $e) {
+ $_SESSION['meldung_ansage'] = "Fehler beim Einfügen: " . $e->getMessage();
+ }
+}
+
+// Löschen einer Ansage
+if (isset($_POST['delete_ansage_id'])) {
+ $delete_ansage_id = $_POST['delete_ansage_id'];
+
+ // Löschanfrage vorbereiten
+ try {
+ $sql_delete_ansage = "DELETE FROM ansagen WHERE id = ?";
+ $stmt = $pdo->prepare($sql_delete_ansage);
+ $stmt->execute([$delete_ansage_id]);
+ $_SESSION['meldung_ansage'] = "Ansage erfolgreich gelöscht!";
+ header("Location: " . $_SERVER['PHP_SELF']);
+ exit();
+ } catch (Exception $e) {
+ $_SESSION['meldung_ansage'] = "Fehler beim Löschen: " . $e->getMessage();
+ }
+}
+
+// Meldungen aus der Session übernehmen
+if (isset($_SESSION['meldung_gottesdienst'])) {
+ $meldung_gottesdienst = $_SESSION['meldung_gottesdienst'];
+ unset($_SESSION['meldung_gottesdienst']);
+}
+
+if (isset($_SESSION['meldung_ansage'])) {
+ $meldung_ansage = $_SESSION['meldung_ansage'];
+ unset($_SESSION['meldung_ansage']);
+}
+
+if (isset($_SESSION['meldung_dateien'])) {
+ $meldung_dateien = $_SESSION['meldung_dateien'];
+ unset($_SESSION['meldung_dateien']);
+}
+
+$heute = date('Y-m-d');
+
+// Gottesdienste aus der Datenbank abrufen
+$sql_fetch_gottesdienste = "SELECT id, bibelwort, bibelvers, TO_CHAR(bibeldatum, 'DD.MM.YYYY') AS datum FROM gottesdienst WHERE bibeldatum >= ? ORDER BY bibeldatum ASC";
+$stmt = $pdo->prepare($sql_fetch_gottesdienste);
+$stmt->execute([$heute]);
+$planung_gottesdienste = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+
+// Ansagen aus der Datenbank abrufen
+$sql_fetch_ansagen = "SELECT id, ansage_untertitel, ansage_titel, ansage_ort, ansage_uhrzeit, TO_CHAR(ansage_datum_von, 'DD.MM.YYYY') AS datum_von, TO_CHAR(ansage_datum_bis, 'DD.MM.YYYY') AS datum_bis FROM ansagen WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ? ORDER BY ansage_datum_von ASC";
+$stmt = $pdo->prepare($sql_fetch_ansagen);
+$stmt->execute([$heute, $heute]);
+$planung_ansagen = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+
+// Schließe die Datenbankverbindung
+$pdo = null;
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+ ]
+
+
+
+
Gottesdienst
+
Ansagen
+
Dateiverwaltung
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Geplante Gottesdienste
+
+
+
+
+
+
+
+
+ Leitgedanken importieren
+
+
+
+
+
+
+
+
+
+
+ Neuen Gottesdienst planen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0) : ?>
+
+
+
+
+
+ –
+
+
+
+
+
+
+
+ Uhr
+
+
+ ·
+
+
+
+
+
+ Löschen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ format('d.m.Y') : $expiryDate;
+
+ $numPages = 0;
+ foreach ($pngFiles as $png) {
+ if (strpos($png, $baseFileName . '_page_') !== false) $numPages++;
+ }
+
+ $aushangData[] = [
+ 'fileBase' => $baseFileName,
+ 'formattedExpiry' => $formattedExpiryDate,
+ 'title' => $title,
+ 'numPages' => $numPages,
+ ];
+ }
+ }
+
+ if (empty($aushangData)) {
+ echo '
Keine Aushänge vorhanden.
';
+ } else {
+ foreach ($aushangData as $aushang) {
+ echo '
';
+ echo '
';
+ echo '
' . htmlspecialchars($aushang['title']) . '
';
+ echo '
' . $aushang['numPages'] . ($aushang['numPages'] == 1 ? ' Seite' : ' Seiten') . ' · Verfällt ' . $aushang['formattedExpiry'] . '
';
+ echo '
';
+ echo ' ';
+ echo 'Löschen ';
+ echo ' ';
+ echo '
';
+ }
+ }
+ } else {
+ echo '
Verzeichnis nicht gefunden.
';
+ }
+ ?>
+
+
+
+
+
+
+
+ Neuen Aushang hochladen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-08-31-Kirchenmusik_3_page_1.png b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-08-31-Kirchenmusik_3_page_1.png
new file mode 100644
index 0000000..2c1f1bc
Binary files /dev/null and b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-08-31-Kirchenmusik_3_page_1.png differ
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-08-31-Kirchenmusik_3_page_2.png b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-08-31-Kirchenmusik_3_page_2.png
new file mode 100644
index 0000000..dfbddbe
Binary files /dev/null and b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-08-31-Kirchenmusik_3_page_2.png differ
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-05-Ansagen_Dampferfahrt_page_1.png b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-05-Ansagen_Dampferfahrt_page_1.png
new file mode 100644
index 0000000..f3e9dbc
Binary files /dev/null and b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-05-Ansagen_Dampferfahrt_page_1.png differ
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-05-Ansagen_Dampferfahrt_page_2.png b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-05-Ansagen_Dampferfahrt_page_2.png
new file mode 100644
index 0000000..2026547
Binary files /dev/null and b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-05-Ansagen_Dampferfahrt_page_2.png differ
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-05-Ansagen_Dampferfahrt_page_3.png b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-05-Ansagen_Dampferfahrt_page_3.png
new file mode 100644
index 0000000..fdec037
Binary files /dev/null and b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-05-Ansagen_Dampferfahrt_page_3.png differ
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-30-Wandkalender_page_1.png b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-30-Wandkalender_page_1.png
new file mode 100644
index 0000000..69ab8ae
Binary files /dev/null and b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-09-30-Wandkalender_page_1.png differ
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-12-31-Jahresmotto_page_1.png b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-12-31-Jahresmotto_page_1.png
new file mode 100644
index 0000000..f92bea2
Binary files /dev/null and b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-12-31-Jahresmotto_page_1.png differ
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-12-31-Kinderschutz_page_1.png b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-12-31-Kinderschutz_page_1.png
new file mode 100644
index 0000000..c7054b4
Binary files /dev/null and b/web/intranet_repo/projects/contentpanel/insertion/news/png/2026-12-31-Kinderschutz_page_1.png differ
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news/png/2030-12-31-Vision_page_1.png b/web/intranet_repo/projects/contentpanel/insertion/news/png/2030-12-31-Vision_page_1.png
new file mode 100644
index 0000000..7d79daa
Binary files /dev/null and b/web/intranet_repo/projects/contentpanel/insertion/news/png/2030-12-31-Vision_page_1.png differ
diff --git a/web/intranet_repo/projects/contentpanel/insertion/news_cleanup.php b/web/intranet_repo/projects/contentpanel/insertion/news_cleanup.php
new file mode 100644
index 0000000..d9b301f
--- /dev/null
+++ b/web/intranet_repo/projects/contentpanel/insertion/news_cleanup.php
@@ -0,0 +1,133 @@
+format('Y-m-d') !== $expiryDateString
+ ) {
+ error_log(
+ 'Ungültiges Ablaufdatum bei News-Datei: ' . $fileBase
+ );
+ continue;
+ }
+
+ /*
+ * Aushang bleibt den gesamten angegebenen Tag sichtbar.
+ * Beispiel Ablaufdatum 2026-09-05:
+ * Löschung ab 2026-09-06 00:00 Uhr.
+ */
+ $deleteFrom = $expiryDate->modify('+1 day');
+
+ if ($now >= $deleteFrom) {
+ if (!deleteNewsFiles($fileBase)) {
+ error_log(
+ 'Abgelaufene News konnte nicht vollständig gelöscht werden: '
+ . $fileBase
+ );
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/intranet_repo/projects/dashboard/cloudy.jpg b/web/intranet_repo/projects/dashboard/cloudy.jpg
new file mode 100644
index 0000000..4abf634
Binary files /dev/null and b/web/intranet_repo/projects/dashboard/cloudy.jpg differ
diff --git a/web/intranet_repo/projects/dashboard/config.local.php b/web/intranet_repo/projects/dashboard/config.local.php
new file mode 100644
index 0000000..2cb22a4
--- /dev/null
+++ b/web/intranet_repo/projects/dashboard/config.local.php
@@ -0,0 +1,4 @@
+prepare($sql);
+$stmt->execute([$heute]);
+$row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+if ($row) {
+ $bibelwort = $row['bibelwort'];
+ $bibelvers = $row['bibelvers'];
+ $eingangNummer = $row['eingang_nummer'];
+ $eingangTitel = $row['eingang_titel'];
+} else {
+ $bibelwort = "Bibelwort nicht eingetragen";
+ $bibelvers = "Bibelvers nicht gefunden.";
+ $eingangNummer = "N/A";
+ $eingangTitel = "Eingangslied nicht gefunden";
+}
+
+// --- Ansagen ---
+$sqlAnsagen = "SELECT ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel
+ FROM ansagen
+ WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ?
+ ORDER BY ansage_datum_von ASC";
+$stmtAnsagen = $pdo->prepare($sqlAnsagen);
+$stmtAnsagen->execute([$heute, $heute]);
+$ansagenResult = $stmtAnsagen->fetchAll(PDO::FETCH_ASSOC);
+
+// Verbindung schließen (optional, aber sauber)
+$pdo = null;
+
+// --- Aushänge ---
+$pngDirectoryUrl = '../contentpanel/insertion/news/png/';
+
+// Abgelaufene Aushänge einschließlich Original-PDF bereinigen
+cleanupExpiredNews();
+
+$pngDirectoryFs = realpath(
+ __DIR__ . '/../contentpanel/insertion/news/png'
+);
+
+$pngFiles = ($pngDirectoryFs && is_dir($pngDirectoryFs))
+ ? scandir($pngDirectoryFs)
+ : [];
+?>
+
+
+
+
+
+ NAK Reinickendorf
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ';
+ }
+ }
+ if (!$hasPng) {
+ echo '
Keine Aushänge vorgeplant.
';
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/dashboard/ha_temperature.php b/web/intranet_repo/projects/dashboard/ha_temperature.php
new file mode 100644
index 0000000..aab6f6e
--- /dev/null
+++ b/web/intranet_repo/projects/dashboard/ha_temperature.php
@@ -0,0 +1,60 @@
+ true,
+ CURLOPT_HTTPHEADER => [
+ 'Authorization: Bearer ' . HA_TOKEN,
+ 'Content-Type: application/json'
+ ],
+ CURLOPT_CONNECTTIMEOUT => 3,
+ CURLOPT_TIMEOUT => 5,
+]);
+$response = curl_exec($ch);
+$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+$error = curl_error($ch);
+curl_close($ch);
+
+if ($response === false || $httpCode < 200 || $httpCode >= 300) {
+ http_response_code(502);
+ echo json_encode([
+ 'error' => 'Home Assistant request failed',
+ 'details' => $error ?: ('HTTP ' . $httpCode)
+ ]);
+ exit;
+}
+
+$data = json_decode($response, true);
+if (!is_array($data) || !isset($data['state'])) {
+ http_response_code(500);
+ echo json_encode(['error' => 'Unexpected HA response']);
+ exit;
+}
+
+$val = $data['state'];
+$unit = $data['attributes']['unit_of_measurement'] ?? '°C';
+
+$CALIBRATION_OFFSET = +0;
+
+if (is_numeric($val)) {
+ $number = round((float)$val + $CALIBRATION_OFFSET, 1);
+} else {
+ $number = $val;
+}
+
+echo json_encode([
+ 'entity_id' => $entityId,
+ 'value' => $number,
+ 'unit' => $unit,
+ 'friendly_name' => $data['attributes']['friendly_name'] ?? null,
+ 'last_changed' => $data['last_changed'] ?? null,
+]);
diff --git a/web/intranet_repo/projects/dashboard/month/terminuebersicht-1.png b/web/intranet_repo/projects/dashboard/month/terminuebersicht-1.png
new file mode 100644
index 0000000..3420812
Binary files /dev/null and b/web/intranet_repo/projects/dashboard/month/terminuebersicht-1.png differ
diff --git a/web/intranet_repo/projects/dashboard/month/terminuebersicht-2.png b/web/intranet_repo/projects/dashboard/month/terminuebersicht-2.png
new file mode 100644
index 0000000..0774395
Binary files /dev/null and b/web/intranet_repo/projects/dashboard/month/terminuebersicht-2.png differ
diff --git a/web/intranet_repo/projects/dashboard/stylo.css b/web/intranet_repo/projects/dashboard/stylo.css
new file mode 100644
index 0000000..af53312
--- /dev/null
+++ b/web/intranet_repo/projects/dashboard/stylo.css
@@ -0,0 +1,340 @@
+/* Gesamtes Seiten-Layout */
+body {
+ margin: 0;
+ height: 100vh;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: 'SUSE', sans-serif;
+ font-size: 20px;
+ background-image: url('cloudy.jpg');
+ background-size: cover;
+ background-repeat: no-repeat;
+ background-position: center center;
+ overflow: hidden;
+}
+
+/* Content Board Layout */
+.content-board {
+ background-color: #a8a6a7a7;
+ width: 99%;
+ height: 97%;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ border-radius: 0;
+ padding: 0 6px 7px 3px;
+}
+
+.content-column-left {
+ width: 40%;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+.content-column-right {
+ width: 58%;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+.temperature {
+ display: flex;
+ justify-content: space-around;
+ align-items: center;
+ padding: 5px 0 0 0;
+ height: 70%;
+}
+
+.verse {
+ text-align: justify;
+ color: rgb(67, 67, 67);
+ padding: 5px 15px 5px 15px;
+}
+
+.date {
+ color: rgb(67, 67, 67);
+ font-weight: bold;
+}
+
+.content-column-right-upper-box-left {
+ width: 60%;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+.content-column-right-upper-box-right {
+ width: 39%;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+.content-column-right-upper-box {
+ width: 100%;
+ height: 51%;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.content-item-time {
+ border-radius: 0;
+ box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
+ background-color: #ffffffab;
+ width: 100%;
+ height: 30%;
+ margin: 15px 0 15px 0;
+ box-sizing: border-box;
+}
+
+.content-item-news {
+ border-radius: 0;
+ box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
+ background-color: #ffffffab;
+ width: 100%;
+ height: 63%;
+ margin: 15px 0 15px 0;
+ box-sizing: border-box;
+}
+
+.content-item-video {
+ border-radius: 0;
+ box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
+ background-color: #cbc5bc;
+ width: 100%;
+ height: 40%;
+ margin: 15px 0 15px 0;
+ box-sizing: border-box;
+}
+
+.content-item-middle-top {
+ position: relative;
+ border-radius: 0;
+ box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
+ background-color: #cbc5bc;
+ width: 100%;
+ height: 60%;
+ margin: 15px 0 15px 0;
+ box-sizing: border-box;
+ overflow: hidden;
+}
+
+/* Pseudoelement für den animierten border-bottom Effekt */
+.content-item-middle-top::after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ height: 15px;
+ background: linear-gradient(to bottom, transparent, #cbc5bc);
+}
+
+/* Monatsplan: jetzt mit weißem Hintergrund */
+.content-item-month {
+ border-radius: 0;
+ box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
+ background-color: #cbc5bc7d;
+ width: 100%;
+ height: 56%;
+ margin: 15px 0 15px 0;
+ box-sizing: border-box;
+ overflow: hidden;
+}
+.content-item-month-cell {
+ display: flex;
+ background-color: #ffffffe5;
+ justify-content: center; /* Zentriere die Bilder horizontal */
+ height: 94%;
+ align-items: center; /* Zentriere die Bilder vertikal */
+}
+
+.content-item-month-cell img {
+ max-width: 100%; /* Verhindert, dass das Bild größer als der Container wird */
+ max-height: 100%; /* Verhindert Verzerrungen, indem es die maximale Höhe auf die Containerhöhe beschränkt */
+ object-fit: contain; /* Stellt sicher, dass das Bild im Container bleibt, ohne gestreckt zu werden */
+}
+
+.content-item-daily {
+ border-radius: 0;
+ box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
+ background-color: #edebe8;
+ width: 100%;
+ height: 63%;
+ margin: 15px 0 15px 0;
+ box-sizing: border-box;
+ overflow: hidden;
+}
+
+.header-sticky-note {
+ font-size: 25px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ height: 20px;
+ width: 100%;
+ padding: 5px 0 15px 0;
+ border-bottom: 2px solid #38312430;
+ background-color: #cbc5bc;
+ border-radius: 0;
+}
+
+.time {
+ font-size: 80px;
+ text-align: center;
+ padding: 8px 0 0 0;
+}
+
+.bible {
+ text-align: center;
+ font-size: 60px;
+ padding: 10px 0 10px 0;
+ border-bottom: 2px solid #38312456;
+ background-color: #edebe8;
+}
+
+.GB {
+ text-align: center;
+ font-size: 60px;
+ padding: 5px 0 0 0;
+}
+
+.GB-title {
+ text-align: center;
+ color: rgb(67, 67, 67);
+}
+
+.content-column-double-item {
+ width: 100%;
+ height: 33%;
+ margin: 15px 0 15px 0;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+/* Elemente innerhalb des Content Boards */
+.content-item {
+ border-radius: 0;
+ box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
+ background-color: #edebe8;
+ height: 100%;
+ width: 49%;
+}
+
+.content-item-bottom-box {
+ border-radius: 0;
+ box-shadow: 5px 5px 2px rgba(0, 0, 0, 0.16);
+ background-color: #edebe8;
+ margin: 6px 0 0 0;
+ height: 47.5%;
+ width: 100%;
+ overflow: hidden;
+}
+
+.video-container {
+ border-radius: 0;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ position: relative;
+ border: 5px solid #cbc5bc;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.video-container video {
+ width: 100%;
+ height: 100%;
+ transform: scale(1.2);
+ object-fit: cover;
+ border-radius: 0;
+}
+
+.container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 40%;
+ background-color: #38312431;
+ border-radius: 0;
+ border: 2px solid #38312440;
+}
+
+.text-outdoor {
+ font-weight: bold;
+ padding: 9px 0 0 0;
+ margin: 0 0 15px 0;
+}
+
+.text-indoor {
+ font-weight: bold;
+ margin: 0 0 15px 0;
+}
+
+.news-box {
+ display: flex;
+ justify-content: flex-start;
+ padding: 5px 15px 5px 15px;
+ border-bottom: 2px solid #38312456;
+ background-color: #edebe8;
+ word-wrap: break-word;
+}
+
+.news-date {
+ font-weight: bold;
+ margin: 0 10px 0 0;
+}
+
+.news-subtitle {
+ color: rgba(67, 67, 67, 0.657);
+}
+
+.news-container {
+ height: 290px;
+ overflow: hidden;
+ position: relative;
+}
+.news-scroll {
+ display: flex;
+ flex-direction: column;
+ animation-name: scroll-v;
+ animation-timing-function: linear;
+ animation-iteration-count: infinite;
+ --scroll-distance-v: 0px;
+}
+@keyframes scroll-v {
+ 0% { transform: translateY(0); }
+ 100% { transform: translateY(var(--scroll-distance-v)); }
+}
+
+.aushang-flex img {
+ border-radius: 0;
+ margin: 0 10px 0 0;
+ border: 2px solid #38312440;
+ box-shadow: 4px 4px 2px rgba(0, 0, 0, 0.08);
+}
+
+.aushang-flex {
+ padding: 0 5px 0 5px;
+ display: flex;
+ justify-content: space-evenly;
+ align-items: center;
+ height: 90.5%;
+ border-radius: 0;
+
+ animation-name: scroll-h;
+ animation-timing-function: linear;
+ animation-iteration-count: infinite;
+ --scroll-distance-h: 0px;
+}
+@keyframes scroll-h {
+ 0% { transform: translateX(0); }
+ 100% { transform: translateX(var(--scroll-distance-h)); }
+}
diff --git a/web/intranet_repo/projects/dashboard/update_content.php b/web/intranet_repo/projects/dashboard/update_content.php
new file mode 100644
index 0000000..afd4cea
--- /dev/null
+++ b/web/intranet_repo/projects/dashboard/update_content.php
@@ -0,0 +1,54 @@
+prepare($sql);
+ $stmt->execute([$heute]);
+ $row = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if ($row) {
+ $bibelwort = $row['bibelwort'];
+ $bibelvers = $row['bibelvers'];
+ $eingangNummer = $row['eingang_nummer'];
+ $eingangTitel = $row['eingang_titel'];
+ } else {
+ $bibelwort = "Bibelwort nicht eingetragen";
+ $bibelvers = "Bibelvers nicht gefunden.";
+ $eingangNummer = "N/A";
+ $eingangTitel = "Eingangslied nicht gefunden";
+ }
+
+ // --- Ansagen ---
+ $sqlAnsagen = "SELECT ansage_datum_von, ansage_datum_bis, ansage_uhrzeit, ansage_ort, ansage_titel, ansage_untertitel
+ FROM ansagen
+ WHERE ansage_datum_von >= ? OR ansage_datum_bis >= ?
+ ORDER BY ansage_datum_von ASC";
+ $stmtAnsagen = $pdo->prepare($sqlAnsagen);
+ $stmtAnsagen->execute([$heute, $heute]);
+ $ansagenResult = $stmtAnsagen->fetchAll(PDO::FETCH_ASSOC);
+
+ // Rückgabe als JSON
+ echo json_encode([
+ 'bibelwort' => $bibelwort,
+ 'bibelvers' => $bibelvers,
+ 'eingang_nummer' => $eingangNummer,
+ 'eingang_titel' => $eingangTitel,
+ 'ansagen' => $ansagenResult
+ ], JSON_UNESCAPED_UNICODE);
+
+} catch (Throwable $e) {
+ // Optional: error_log($e->getMessage());
+ echo json_encode(['error' => 'Fehler bei der Verbindung zur Datenbank'], JSON_UNESCAPED_UNICODE);
+} finally {
+ $pdo = null;
+}
diff --git a/web/intranet_repo/projects/developerpanel/developer.php b/web/intranet_repo/projects/developerpanel/developer.php
new file mode 100644
index 0000000..cc572fd
--- /dev/null
+++ b/web/intranet_repo/projects/developerpanel/developer.php
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
+ query($sql);
+ $isDeveloper = $result->fetch(PDO::FETCH_ASSOC);
+
+ // Check if the user has developer access (Entwickler = 1)
+ if ($isDeveloper["Entwickler"] == 1) {
+ $sql_groups = "SELECT * FROM user_permissions";
+ $stmt = $pdo->prepare($sql_groups);
+ $stmt->execute();
+ $groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ } else {
+ header("Location: {$g_httpHomePath}");
+ exit();
+ }
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/developerpanel/developer_info.php b/web/intranet_repo/projects/developerpanel/developer_info.php
new file mode 100644
index 0000000..548d6a8
--- /dev/null
+++ b/web/intranet_repo/projects/developerpanel/developer_info.php
@@ -0,0 +1,70 @@
+
+
+
+ "Max Mustermann",
+ "Alter" => 30,
+ "Beruf" => "Softwareentwickler",
+ // Weitere Daten ...
+];
+?>
+
+
+
+ "12345",
+ "Name" => "Erika Musterfrau",
+ "E-Mail" => "erika@example.com",
+ // Weitere Daten ...
+];
+?>
+
+
+
+
+
+
+
+
+ Allgemeines
+
+
+
+
+ $value): ?>
+
+ = $key ?>
+ = nvl($value, '---') ?>
+
+
+
+
+
+
+
+
+
+ Serverinformationen
+
+
+
+
+ $value): ?>
+
+ = $key ?>
+ = nvl($value, '---') ?>
+
+
+
+
+
\ No newline at end of file
diff --git a/web/intranet_repo/projects/developerpanel/work/sources.txt b/web/intranet_repo/projects/developerpanel/work/sources.txt
new file mode 100644
index 0000000..d911de2
--- /dev/null
+++ b/web/intranet_repo/projects/developerpanel/work/sources.txt
@@ -0,0 +1,16 @@
+Grafiken
+
+**SVG**
+https://www.svgrepo.com/
+
+- Iconsax Bold Oval Icons Collection
+-- https://www.svgrepo.com/collection/iconsax-bold-oval-icons/
+
+- Lightning Design Utility Icons Collection
+-- https://www.svgrepo.com/collection/lightning-design-utility-icons/
+
+- Bootstrap UI Icons Collection
+-- https://www.svgrepo.com/collection/bootstrap-ui-icons/
+
+- Zest Interface Icons Collection
+-- https://www.svgrepo.com/collection/zest-interface-icons/
\ No newline at end of file
diff --git a/web/intranet_repo/projects/developerpanel/work/todo.txt b/web/intranet_repo/projects/developerpanel/work/todo.txt
new file mode 100644
index 0000000..236ac53
--- /dev/null
+++ b/web/intranet_repo/projects/developerpanel/work/todo.txt
@@ -0,0 +1,159 @@
+Relaunch: Einführung (Aufgaben Samu)
+**Umstrukturierung: SeelsorgePlaner**
+
+- projects\seelsorge\besuchsstellen.php (und die eingebundenen Unterseiten):
+:: Fortsetzung Technik:
+:::: 1. Bei Klicken auf Buttons und Bilder auf der aktuellen Ansicht verbleiben, anstelle auf die Startseite (active) zu wechseln (AJAX Funktion).
+:::: 2. Sicherung: Aufruf aller Seiten zu Seelsorge nur für Amtsgeschwister !!!
+- Alle Objekte die über den Browser theoretisch aufgerufen werden können: -> Sicherheit, dass kein Quellcode gelesen werden kann. Routing ggf. auf LogIn-Seite.
+- Web-Server: Kein Laden eines Objektordners. Damit keine Dateien gesichtet oder explizit heruntergeladen werden können.
+- Analyse: Mehrfach-Codes -> Optimierung: Auslagern in separate Files (snippets-Ordner)
+
+*Admin-Seite*
+- Profil als Pop-Up (analog Besuchsstellen-Seite)
+
+*Seelsorge-Seiten*
+- Workaround zu openCloseProfile ggf. optimieren oder besser neue Lösung schaffen.
+
+*Prüfung*
+- Alle Buttons prüfen. Ist die Textgröße in den Buttons für Bildschirm und Handy okay?
+
+**Fehlerbehebung (SAMU) **
+- Bugfix: *visit.php*
+-- Nach einer Aktion (Erneuern/Priorisieren) wird die Seite neu geladen für den Fokus auf Datensatz. Das funktioniert aktuell nicht wg. Seitenverlinkung auf seelsorge.php. Hier brauchen wir einen neuen Denkansatz. Wie gestalten wir das? Ist ggf. doch eine Aufteilung der Seitenbereiche zurück auf zwei extra Seiten notwendig oder gibt es die Möglichkeit dies per Javascript zu steuern, indem beim Neuladen der Seite die zu aktivierenden Elemente bekannt sind und ein-/ausgeblendet werden? Kann auf ein Neuladen der Seite verzichtet werden, indem per AJAX nur der Inhalt aktualisiert wird?
+- *calender_edit.php*
+-- Datum in der Vergangenheit sind anwählbar im Edit-Modus. Erst ab dem aktuellen Tag darf das möglich sein.
+
+** Datenbankanbindung der Verfügbarkeiten (Samu) --> "greenbox" auf Kalender-Item **
+- Datenabfrage: bei abgelaufenen Tagen keine Prüfung auf Verfügbarkeit nötig -> ohne "greenbox"
+
+** Fehlerbereinigung Browser: Alle bekannten Browser checken**
+- Style-Fehler/-Optimierung melden und ggf. von Markus korrigieren lassen
+
+**Bug auf Developer-Seite: (Hilfe durch Samu)**
+- Wenn Cookie *modus=edit* und Seite verlassen wird, bleibt das Cookie erhalten.
+-- Reset des Cookies beim Verlassen der Seite. Allerdings muss das Cookie bestehen bleiben, wenn weiterhin bearbeitet werden soll. Andere Cookies müssen ebenfalls resettet werden, die nichts mit anderen Seiten zu tun haben. Können wir dazu bestimmte Modi einsetzen? z. B. "modus=dev_edit" oder Kombi-Prüfung auf pageFile+modus?
+-- Fehler beim Speichern beheben
+-- Welche Lösungsansätze haben wir, um die einzelnen Aufgaben der ToDo-Liste zentral (und nur online) zu pflegen?
+:: Einzelne Datensätze?
+:: Einzelne Textdateien in einem geschützten Dateiordner?
+
+** Seite Kalender: (Mitarbeit durch Samu)**
+-- **Absprache:** Ist das Kalenderblatt final?
+-- Was wollen wir ggf. in der zweiten Übersicht der Verfügbarkeiten oder einer zusätzlichen Sidebar auflisten? (Feiertage, besondere Termine aus dem CKalender)
+:: >> Welche Informationen sind wichtig / welche sind nicht erforderlich?
+:: >> Was erledigen wir im nächsten Release?
+:: **Auflistung aller Besuchsstellen mit:**
+-- Speicherung von "Letzter Besuch"?
+
+
+Weiterentwicklung (Markus)
+** Layout verfeinern **
+- admin.php << Adminseite sowie seelsorge.php << Besuchsstellen-Seite:
+-- Tabelle, bei Cursor-Tasten drücken: rauf/runter -> Markierung der Tabellenzeile wechseln
+- Farben über Farbdefinitionen (var)
+- Farben vereinheitlichen z.b. #00000 -> black-variable
+
+** Kleine Optimierung:**
+- Wiederholte absolute Pfade in Variablen speichern:
+-- ../general_files
+-- ../_central
+-- ../intranet
+-- ../kalli
+-- ../seelsorge
+-- ../authorization
+- Wiederholte Codes in extra Code-Snippets auslagern:
+-- z. B. admin.php: Bildausgabe
+- Anstelle von "exit;" schreiben wir "exit();"
+
+**Kalender:**
+- Aufnahme Ferienzeiten Berlin/Brandenburg
+
+
+
+2. Release: Nice-to-have´s und Umsetzung von neuen Anforderungen
+** Neue Seite: Chat**
+- Implementierung des Chats
+- Chatverlauf mit Datum, Uhrzeit, Nachricht, Autor, Profilbild und Möglichkeit zur Texteingabe
+- Chatmöglichkeit mit jedem User oder nur Funktionsträger oder auschließlich nur für Amtsgeschwister?
+
+** Kalli **
+- Datum und Uhrzeit Felder direkt anwählbar? Denn es muss zweimal in das Feld geklickt werden.
+- Bei nicht Media Felder nebeneinander darstellen? Ggf. Padding in Statusmeldung-Feld erhöhen?
+
+** Seite Kalender:**
+- CKalender-Einbindung: Welche Infos zur entsprechenden Rolle (Chorsänger, Putzgruppe, Technik) wollen wir zur Ausgabe bringen?
+- Neben Kalenderblatt: Sidebar mit nächsten Feiertagen, Ferien, besondere Termine als Vorschau (mit welchem Zeitraum)
+- Bessere Darstellung der Feiertage bei @media?
+-- Zum Beispiel: Bei Media ausblenden der Feiertage und kleinen rosa Punkt anzeigen. Feiertag in Verfügbarkeit anzeigen.
+
+** Seite Besuchsstellen (bzw. erweiterte Adminseite) **
+- Sollen Administratoren Datensätze löschen und bearbeiten? Betrifft alle Daten: Priesterbezirk, Adresse, Kommunikationsdaten und Hinweis
+-- für Priesterbezirk benötigen wir eine Auswahlfunktion, um den neuen Priester einzusetzen.
+- Sollen Hinweistexte verkürtzt dargestellt und ausklappbar sein? Button mit Pfeil nach unten/oben.
+-- Umsetzungsmöglichkeit: Button über Wechsel der Klasse mit enthaltenen bgimage. Text: Mit Display none, opacity 0. wird dann zu Display Block mit opacity 1.
+- Aktions-Buttons nicht mit Text sondern Symbole und Toolbartext einblenden für eine kompaktere Darstellung? Oder dies bei @media für eine bessere Übersichtlichkeit?
+
+** Seitenbereich Kalendertag **
+- Macht eine Sidebar-Darstellung des Kalenderblatts Sinn und wäre es damit komfortabler? --> Mini-Kalender zum betreffenden Monat und Anklick-Möglichkeit zum Wechsel des Kalendertages in einer kleineren kompakten Ansicht. Oder sogar wählbar mit Vergrößern/Verkleinern-Button?
+
+** Datenausgabe formatiert anzeigen**
+- z. B. Telefonnummern. Was ist dafür notwendig, gibt es dazu verfügbare Funktionen (oder reguläre Ausdrücke)? Eventuell gibt es etwas externes, was wir einbinden können?
+
+** Optimierung profile.php**
+- Nach Bestätigung der Änderung Statuszeile/Overlay einblenden (ggf. nach ein paar Sekunden wegblenden)
+
+** Style- Optimierung **
+- Dunkelmodus anbieten und für jeden User speichern, ob hell/dunkel
+- Profilfotos in thumbnails dublizieren. Nur in der maximal notwendigen Auflösung werden für die Ausgabe verwendet. Reichen 100x100px aus?
+- Content-Bereich vergrößern/verschieben indem das Menü ausgeblendet wird. Beim Zeigen mit der Maus auf den Bereich soll es wieder angezeigt werden, Sliden. Oder durch Klicken kann der Bereich rein und raus geschoben werden, wobei sich der Content-Bereich anpasst.
+- Entweder beim Scrollen innerhalb von FORM (das mit content_fullsize umschlossen ist) und mindestens die Seitengröße hat (ggf. ist ein Scrollbalken vorhanden) Schaltfläche mit scrollToTop oder ähnliches einblenden. Alternativ festen Button einsetzen.
+- Layout: Farben: Prüfung/Korrektur auf richtigen Farbcode bzw. Farbverbesserung
+- overlay_message.php: OK-Button oder ähnliches anstelle des Exit-Buttons?
+-- Oder automatisches Ausblenden z. B. nach 2 bis 4 Sekunden (ggf. Message links unten kurz ein und wieder ausblenden - evntl. bei kleinen Bildschirmen anders formatiert anzeigen) - aös animierte Darstellung mit grünem oder weißen Haken.
+
+** Seite Admin **
+- Vorschlag: Nutzer als Blöcke untereinander bei Media. Onclick Element um Berechtigungen als Liste einzublenden. Bearbeitungs-Button um Einträge zu ändern, Toogle Button zum Bedienen. Entweder sofortige Speicherung oder Bestätigungs-Button?
+
+** Seite Developer **
+- Farbe azure harmony einsetzen für Schattierungen anstelle von #DDDDDD.
+
+3. Release: - IDEE - Eigenständiges KALLI und Ablösung von CKalender
+- Alle CKalender Funktionen werden adaptiert.
+- Generischer Aufbau der HTML-Ausgabe anhand der Datenbankinhalte.
+-- Online-Verwaltung von Terminarten
+-- Online-Verwaltung von Nutzergruppen
+-- Nutzerverwaltung: Ansicht und Bedienung vereinfachen und je nach Medium anders darstellen -> One-Button-Mechanik...
+-- Aufteilung der Administration in mehrere Ebenen und ansprechendes Seitenmenü.
+- Profil, eigenes: An/abwahl von Kontaktmedien: WhatsApp, Signal etc. -> Button mit Hintergrund mit Hovertext / oder als Text mit Tooglebar?
+- Admin . Tabelle nach Namen sortieren
+- w3schools.com CSS Templates einsetzen
+- Kalendarium: Brauchen wir einen Countdown zum Reload? Wenn nein, Funktionen, Content und SVG löschen
+- Seelsorge Menü - Javascript: auch bei Rezise nachjustieren
+-- Bottom von Indikatorstreifen an menu-item ausrichten, zumindest bei @media
+
+**Alle Codes optmieren**
+- Feiertage: Berliner und regionale Feiertage vervollständigen. Ggf. andere API nutzen.
+- Profilseite, persönliche: Alle Amtsgeschwister haben die Möglichkeit Ihre Zeitpräferenzen in Wochentagen (Buttons) und Uhrzeit von/bis (normale Uhrzeit-Felder) einzutragen - hier: Erweitern der Datenbank
+
+**Anfragemöglichkeit für Amtsgeschwister**
+-- Routenplaner: Eigene Adresse -> Amtsgeschwister-Adresse -> Besuchsstelle
+... Kontaktaufnahem über Chat ...
+
+- **profile.php**
+- Änderung des Passwortes ermöglichen?
+
+Ideen
+**Feedback-Formular am Seitenrand**
+- Jeder Nutzer kann den Entwicklern eine Info senden. Besonders am Anfang können Fehler auftreten. Beim Senden werden alle Entwickler per E-Mail und/oder Push-Notifikation informiert.
+- Ggf. Hilfe und Feedback mit Fragezeichen-Button innerhalb des Profilmenüs
+- Menü: Zurück-Button rechts oder links anbieten im Menütitel?
+
+** Suchfunktion **
+- Suche oder Filtermöglichkeit in den Tabellen von Besuchsstellen und/oder Admin anbieten?
+
+** Sidbar **
+Wenn mehr Seiten hinzu kommen werden: Header Menü als SideMenü. Menü-Symbol (3 Linien), beim Klicken Menü nach rechts anzeigen, Animation (rotieren). Bei erneuten klicken Pfeil nach links. Hoch runter Buttons bei möglichen Unterseiten mit Animation (rotieren).
+
+** Darstellung von Buttons für Aktionen **
+Wenn Anzahl der Buttons/Links auf einer Seite mehr werden, dann zumindest bei Media Aktionen und Hyperlinks nur als Symbol anzeigen und bei Hover Text einblenden.
diff --git a/web/intranet_repo/projects/intranet/index.php b/web/intranet_repo/projects/intranet/index.php
new file mode 100644
index 0000000..a099b46
--- /dev/null
+++ b/web/intranet_repo/projects/intranet/index.php
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Eintragung erfolgreich! Kalli wird Ihren Termin direkt vermerken.
+
+
+
+
+
+
+
+ prepare(
+ "SELECT id, title, message, type
+ FROM mainpage_notices
+ WHERE is_active = TRUE
+ AND (starts_at IS NULL OR starts_at <= CURRENT_TIMESTAMP)
+ AND (ends_at IS NULL OR ends_at > CURRENT_TIMESTAMP)
+ ORDER BY sort_order ASC, created_at ASC, id ASC"
+ );
+ $noticeStmt->execute();
+ $mainpageNotices = $noticeStmt->fetchAll(PDO::FETCH_ASSOC);
+ } catch (PDOException $e) {
+ $mainpageNotices = [];
+ }
+
+ $allowedNoticeTypes = ['info', 'warning', 'success', 'error'];
+ foreach ($mainpageNotices as $mainpageNotice):
+ $noticeType = in_array($mainpageNotice['type'], $allowedNoticeTypes, true) ? $mainpageNotice['type'] : 'info';
+ ?>
+
+
= htmlspecialchars($mainpageNotice['title'], ENT_QUOTES, 'UTF-8') ?>
+
= nl2br(htmlspecialchars($mainpageNotice['message'], ENT_QUOTES, 'UTF-8')) ?>
+
+
+
+ prepare("SELECT last_training_date FROM brandschutz_training WHERE user_id = ?");
+ $bsStmt->execute([$_SESSION['user_id']]);
+ $bsLastTraining = $bsStmt->fetchColumn();
+ if ($bsLastTraining === false) {
+ $bsOverdue = true;
+ } else {
+ if (empty($bsLastTraining) || strtotime($bsLastTraining . ' +365 days') < time()) {
+ $bsOverdue = true;
+ }
+ }
+ ?>
+
+
+
+
Brandschutzunterweisung fällig!
+
Bitte führen Sie zeitnah die
Online-Brandschutzunterweisung durch. Die Schulung dauert nur ca. 15 bis 20 Minuten. Eine regelmäßige Durchführung ist gesetzlich vorgeschrieben. Vielen Dank!
+
+
+
+
+
+
+ Kalli
+
+
+
+
+
+ Monatsplan
+
+
+
+
+
+ Seelsorge
+
+
+
+
+
+ Brandschutz
+
+ !
+
+
+
+
+
+
+ Konto
+
+
+
+
+
+ Schwarzes Brett
+
+
+
+ prepare("SELECT COUNT(*) FROM admin_messages WHERE status = 'pending'");
+ $badgeStmt->execute();
+ $unreadCount = $badgeStmt->fetchColumn();
+ ?>
+
+
+ Admin
+ 0): ?>
+ = $unreadCount ?>
+
+
+
+
+
+
+
+ Entwickler
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/intranet/profile.php b/web/intranet_repo/projects/intranet/profile.php
new file mode 100644
index 0000000..7b2d860
--- /dev/null
+++ b/web/intranet_repo/projects/intranet/profile.php
@@ -0,0 +1,261 @@
+
+
+
+
+
+
+
+
+prepare("SELECT password_hash FROM user_login WHERE user_id = ?");
+ $stmt->execute([$_SESSION["user_id"]]);
+ $pw_result = $stmt->fetch(PDO::FETCH_ASSOC);
+
+ if (!$pw_result || !password_verify($current_password, $pw_result['password_hash'])) {
+ $pw_error = "Das aktuelle Passwort ist falsch.";
+ } elseif (strlen($new_password) < 8 || !preg_match("/[a-z]/i", $new_password) || !preg_match("/[0-9]/", $new_password)) {
+ $pw_error = "Das neue Passwort muss mindestens 8 Zeichen, einen Buchstaben und eine Zahl enthalten.";
+ } elseif ($new_password !== $new_password_confirm) {
+ $pw_error = "Die neuen Passwörter stimmen nicht überein.";
+ } else {
+ $password_hash = password_hash($new_password, PASSWORD_DEFAULT);
+ $stmt = $pdo->prepare("UPDATE user_login SET password_hash = ? WHERE user_id = ?");
+ $stmt->execute([$password_hash, $_SESSION["user_id"]]);
+ $pw_success = "Passwort erfolgreich geändert.";
+ }
+ } else {
+ // Profildaten-Update
+ $mail = $_POST["mail"];
+ $phone = !empty($_POST["phone"]) ? $_POST["phone"] : null; // Wert auf NULL setzen, wenn das Feld leer ist
+
+ // Profilbild hochladen, wenn ein Bild ausgewählt wurde
+ if ($_FILES["profile_picture"]["error"] === UPLOAD_ERR_OK) {
+ $upload_dir = $_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/profile_pictures/";
+
+ // Lösche vorhandene Dateien mit übereinstimmender Benutzer-ID
+ $files = glob($upload_dir . $_SESSION["user_id"] . "-*.jpg");
+ foreach ($files as $file) {
+ unlink($file);
+ }
+
+ // Generiere einen eindeutigen Dateinamen basierend auf Benutzer-ID und Datum/Uhrzeit
+ $filename = $_SESSION["user_id"] . "-" . date("YmdHis") . ".jpg";
+ $upload_file = $upload_dir . $filename;
+
+ if (move_uploaded_file($_FILES["profile_picture"]["tmp_name"], $upload_file)) {
+ $message = "Profilbild erfolgreich hochgeladen.";
+ } else {
+ $message = "Beim Hochladen des Profilbildes ist ein Fehler aufgetreten.";
+ }
+ }
+
+ // Benutzerdaten aktualisieren
+ $sql_update = "UPDATE user_general_information
+ SET mail=?, phone=?
+ WHERE user_id=?";
+ $stmt = $pdo->prepare($sql_update);
+ $stmt->execute([$mail, $phone, $_SESSION["user_id"]]);
+
+ // Weiterleitung ohne Nachricht
+ header("Location: {$_SERVER['PHP_SELF']}");
+ exit();
+ } // Ende else (Profildaten-Update)
+ }
+
+ $sql = "SELECT * FROM user_general_information WHERE user_id = {$_SESSION["user_id"]}";
+ $result = $pdo->query($sql);
+ $user = $result->fetch(PDO::FETCH_ASSOC);
+
+ $sql = "SELECT * FROM user_permissions WHERE user_id = {$_SESSION["user_id"]}";
+ $result = $pdo->query($sql);
+ $groups = $result->fetch(PDO::FETCH_ASSOC);
+} else {
+
+ header("Location: {$g_httpLoginPath}");
+ exit;
+}
+
+// ACHTUNG: Die PHP-Header Weiterleitungen müssen hier abgeschlossen sein.
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Informationen
+
Sicherheit
+
+
+
+
+
+
+ ";
+ } else {
+ // Standardbild anzeigen, wenn keine passende Datei gefunden wurde
+ echo "
";
+ }
+ ?>
+
+
" enctype="multipart/form-data">
+
+
+
+
+
+
E-Mailadresse
+
" required>
+
+
Telefon
+
+
+
+
+
+
+ Profilbild ändern
+
+
+
+
+
+
+
+
+
Gruppen:
+ $value) {
+ if ($value == 1) {
+ $responsibilities[] = $key;
+ }
+ }
+ echo implode(", ", $responsibilities);
+ ?>
+
+
+
+
+
+
+
+
+
+
+
Passwort ändern
+
+
+
+ = htmlspecialchars($pw_error) ?>
+
+
+
+
+ = htmlspecialchars($pw_success) ?>
+
+
+
+
">
+
+
+
+
+
+ Passwort vergessen? Per SMS/Mail zurücksetzen
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/intranet_repo/projects/intranet/show_profile.php b/web/intranet_repo/projects/intranet/show_profile.php
new file mode 100644
index 0000000..70fa649
--- /dev/null
+++ b/web/intranet_repo/projects/intranet/show_profile.php
@@ -0,0 +1,88 @@
+
+
+
+query($sql);
+ $partner = $result->fetch(PDO::FETCH_ASSOC);
+
+ if (isset($partner["mail"])) {
+ $sql_groups = "SELECT * FROM user_permissions WHERE user_id = ?";
+ $stmt = $pdo->prepare($sql_groups);
+ $stmt->execute([$_SESSION["profile_id"]]);
+ $groups = $stmt->fetch(PDO::FETCH_ASSOC);
+ }
+ }
+ else {
+ header("Location:{$g_httpLoginPath}");
+ exit();
+ }
+?>
+
+
+
+
+
+ ";
+ } else {
+ // Standardbild anzeigen, wenn keine passende Datei gefunden wurde
+ echo "
";
+ }
+ ?>
+
+
+
Dieser Nutzer ist noch nicht registriert.
+
+
+
+
E-Mail: ">
+
+
Telefon:
+ ">0
+
+
+
+
Gruppen:
+ $value) {
+ if ($value == 1) {
+ $responsibilities[] = $key;
+ }
+ }
+ echo implode(", ", $responsibilities);
+ ?>
+
+
+
+
+
+
+
+