Initial commit: OMSORG website + Mitarbeiter-App

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Felix Kemmler
2026-06-13 20:03:22 +02:00
co-authored by Claude Sonnet 4.6
commit 8beb0fcf52
103 changed files with 7384 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
# Config with credentials
mitarbeiter-app/config.php
mitarbeiter-app/config.secret.php
# Uploaded files
mitarbeiter-app/uploads/
mitarbeiter-app/downloads/
mitarbeiter-app/fortbildung-materials/
mitarbeiter-app/assets/avatars/
mitarbeiter-app/data/
# OS / editor
.DS_Store
Thumbs.db
.idea/
.vscode/
*.swp
*.swo
# Logs
*.log
+28
View File
@@ -0,0 +1,28 @@
# HTTPS erzwingen
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# Verzeichnis-Listing verbieten
Options -Indexes
# Security-Header
<IfModule mod_headers.c>
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" env=HTTPS
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
</IfModule>
# Direkte Aufrufe sensibler Dateien blockieren
<FilesMatch "^(config\.php|config\.secret\.php|config\.secret\.example\.php|users\.json|README\.txt|\.env)$">
Require all denied
</FilesMatch>
# PHP-Konfiguration für Session-Sicherheit
<IfModule mod_php.c>
php_flag session.cookie_httponly On
php_flag session.cookie_secure On
php_value session.cookie_samesite "Lax"
</IfModule>
+116
View File
@@ -0,0 +1,116 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is the OMSORG website plus **OMSORG Connect**, an internal employee web app ("Mitarbeiter-App") for a German nursing care company. There is no build system — everything is plain PHP, HTML, CSS, and vanilla JS deployed directly to a web server (`htdocs`).
## Deployment
Upload the full directory contents to the server's `htdocs` folder and overwrite existing files. After deploying, clear the browser cache or test in an incognito window.
There are no build steps, package managers, or test runners.
## Architecture
### Public website (`/`)
Static HTML with a contact form:
- `index.html` — main landing page
- `send-form.php` — handles the public contact form, sends email via PHP `mail()`
- `send-status-template.php` — reusable status page template for form results
- `datenschutz.html`, `impressum.html` — legal pages
### Employee app (`/mitarbeiter-app/`)
A session-based PHP app with no framework, backed by a **MySQL database** (PDO).
#### Entry point
- `index.php` — login page. Redirects to `pages/dashboard.php` if already logged in. Includes rate limiting: 5 failed attempts trigger a 10-minute lockout.
- `setup.php` — one-time setup script (run once after first deploy to bootstrap the DB)
#### Library (`lib/`)
- `auth.php` — included at the top of every protected page. Provides `require_login()`, `require_admin()`, `is_logged_in()`, `is_admin()`, `current_user()`, `current_name()`, `current_username()`, `csrf_token()`, `csrf_field()`, `verify_csrf()`, `e()`. Users are stored in the `users` DB table. Login is case-insensitive on username.
- `db.php` — provides `db(): PDO` (singleton). Reads credentials from `config.php`, runs pending migrations on first connection, and sets `PDO::ERRMODE_EXCEPTION`.
- `config.php` — returns array with MySQL DSN/credentials, SMTP settings, and mail recipient addresses. Import with `$config = require __DIR__ . '/config.php';`.
- `layout.php` — shared sidebar layout. Call `layout_start($title, $current_page)` and `layout_end()` to wrap page content. Renders the sidebar nav, user avatar, and injects the service worker.
- `mail.php` — provides `smtp_send($cfg, $to, $subject, $body, $attach = [])`. Raw SMTP implementation (no PHPMailer), supports SSL (port 465) and STARTTLS (port 587), and optional file attachments.
#### Pages (`pages/`)
Each page is a standalone PHP file using `layout_start`/`layout_end`:
- `dashboard.php` — main landing page after login; shows news and quick links
- `stundennachweis.php` — upload monthly timesheet (PDF/image)
- `urlaubsantrag.php` — vacation request form
- `abwesenheitsantrag.php` — absence request form (sick leave, etc.)
- `benefitsantrag.php` — employee benefits request
- `fortbildungsantrag.php` — training request with optional file upload
- `einsatzbewertung.php` — rate a deployment/assignment
- `einsatzanweisung.php` — view/download personal assignment instructions (PDF)
- `dokumentenarchiv.php` — personal document archive (upload/view own documents)
- `downloads.php` — company-wide file downloads (admin-managed)
- `dienstplan.php` — view personal shift schedule
- `werben.php` — refer a new employee
- `settings.php` — profile settings: change password, upload avatar
- `admin.php` — admin-only: tabs for Anträge, Nutzer, Dienstplan, Downloads, Fortbildungsmaterial, Stundennachweis, Bewertungen, News, Einsatzanweisung, Login-Versuche (failed login attempts: lists recent attempts, shows currently locked IPs, and clears the `login_attempts` table)
- `admin-dienstplan.php` — admin shift planner view
- Serve pages (`*-serve.php`, `dokument-serve.php`, `download-serve.php`, etc.) — stream protected files from `uploads/`, `downloads/`, `fortbildung-materials/` with auth check
#### Actions (`actions/`)
POST-only endpoints, each does one thing and redirects back:
- `submit-urlaubsantrag.php`, `submit-abwesenheitsantrag.php`, `submit-benefitsantrag.php`, `submit-fortbildungsantrag.php`, `submit-stundennachweis.php`, `submit-einsatzbewertung.php`, `submit-werben.php` — insert into the matching `requests_*` DB table, send notification email via `smtp_send()`
- `admin-action.php` — admin status updates (accept/reject requests), user management (add/delete/reset password), news CRUD
- `save-dienstplan.php` — admin saves shift entries
- `downloads-action.php`, `downloads-reorder.php` — admin manages downloadable files
- `fortbildung-material-action.php`, `fortbildung-material-reorder.php` — admin manages training materials
- `einsatzanweisung-action.php` — admin uploads/assigns personal instruction PDFs
- `upload-dokument.php`, `delete-dokument.php` — user document archive management
- `upload-avatar.php` — user uploads profile picture (stored in `assets/avatars/`)
- `save-profile.php` — user changes own password
- `logout.php` — destroys session, redirects to `../index.php`
#### Frontend
- `app.css` — all styles (no framework)
- `manifest.webmanifest` + `service-worker.js` — PWA support ("Add to Home Screen")
### Database schema (MySQL)
Managed via migrations in `migrations/`. Key tables:
- `users` — id, username, name, role, password_hash, active, created_at, email, telefon, avatar
- `requests_urlaubsantrag` — vacation requests (von, bis, vertretung, nachricht, status, admin_note)
- `requests_abwesenheitsantrag` — absence requests (von, bis, grund, vertretung, nachricht, status, admin_note)
- `requests_benefitsantrag` — benefits requests
- `requests_fortbildungsantrag` — training requests (with optional file upload)
- `requests_stundennachweis` — timesheet uploads (monat, filename)
- `requests_werben` — employee referrals
- `dienstplan` — shift schedule (user_id, date, schicht; UNIQUE on user_id+date)
- `downloads` — company download files (title, filename, sort_order)
- `fortbildung_materials` — training material files (title, filename, sort_order)
- `dokumente` — user personal document archive (user_id, kategorie, filename)
- `einsatzbewertungen` — deployment ratings
- `einsatzanweisung` — one PDF per user (UNIQUE on user_id)
- `news` — company news posts (title, text, date)
- `schema_migrations` — tracks applied migrations
### Migration system
`db.php` runs `_run_migrations()` on every connection. Migrations are PHP files in `migrations/` returning `['description' => ..., 'up' => [...SQL...]]`. They are applied in filename order and tracked in `schema_migrations`. Already-existing DBs without the migrations table are stamped as fully applied on first run.
### File storage
Uploaded files are stored outside webroot or protected by `.htaccess`:
- `uploads/` — user form attachments (stundennachweis, fortbildung, einsatzanweisung). Filename pattern: `{date}_{username}_{type}_{randomhex}.{ext}`
- `downloads/` — admin-managed company downloads (hashed filenames)
- `fortbildung-materials/` — admin-managed training materials (hashed filenames)
- `assets/avatars/` — user profile pictures (`{username}_{randomhex}.{ext}`)
Allowed upload types: pdf, doc, docx, jpg, jpeg, png. Max size: 12 MB.
### Security conventions
- `lib/` is blocked from direct HTTP access via `lib/.htaccess`
- `uploads/`, `downloads/`, `fortbildung-materials/`, `data/`, `migrations/` all have `.htaccess` denying direct access; files are served only through `*-serve.php` pages with auth checks
- All output uses `e()` (alias for `htmlspecialchars`) to prevent XSS
- Every POST form includes a CSRF token (`csrf_field()` in form, `verify_csrf()` in action)
- Login rate-limiting: 5 failures → 10-minute session lockout
- Uploads are renamed to random hex filenames before storage
### Email (configured in `config.php`)
- SMTP via `smtp_send()` in `lib/mail.php`; credentials in `config.php`
- `mail_info``info@omsorg-pflegedienste.de` (Geschäftsführung)
- `mail_sabrina``s.berggoetz@omsorg-pflegedienste.de` (Disposition)
- `mail_from``no-reply@omsorg-pflegedienste.de`
+16
View File
@@ -0,0 +1,16 @@
OMSORG Website + OMSORG Connect Mitarbeiter-App
Aktualisierung:
- Mitarbeiter-App im übersichtlichen OMSORG-Connect/Discord-Stil neu angeordnet
- Linke Seitenleiste mit Bereichen: Dashboard, Dienstplan, Chat, News, Dokumente, Fortbildung, Benefits, Ansprechpartner
- Mobile Ansicht bleibt bedienbar und responsive
- Bestehende Funktionen, Logins, Uploads, Formulare, E-Mail-Weiterleitung, Downloads und Adminbereich bleiben erhalten
Upload:
Den kompletten Inhalt dieses Ordners in den Server-Ordner htdocs hochladen und vorhandene Dateien ersetzen.
Wichtig:
Nach dem Hochladen Browser-Cache leeren oder im Inkognito-Fenster testen.
Zugangsdaten werden nicht in dieser Datei gespeichert.
Passwörter nur direkt in der App unter Admin vergeben.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><defs><linearGradient id="b" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#fff"/><stop offset="1" stop-color="#dff9fc"/></linearGradient></defs><rect width="900" height="620" rx="36" fill="url(#b)"/><rect x="95" y="135" width="710" height="355" rx="20" fill="#fff" stroke="#caeef3" stroke-width="8"/><rect x="190" y="225" width="120" height="95" rx="10" fill="#68d2df"/><rect x="390" y="225" width="120" height="95" rx="10" fill="#68d2df"/><rect x="590" y="225" width="120" height="95" rx="10" fill="#68d2df"/><rect x="190" y="365" width="120" height="125" rx="10" fill="#11111f"/><rect x="390" y="365" width="120" height="95" rx="10" fill="#68d2df"/><rect x="590" y="365" width="120" height="95" rx="10" fill="#68d2df"/><rect x="355" y="78" width="190" height="96" rx="18" fill="#11111f"/><path d="M450 104v46M427 127h46" stroke="#68d2df" stroke-width="18" stroke-linecap="round"/></svg>

After

Width:  |  Height:  |  Size: 951 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><defs><linearGradient id="a" x1="0" y1="0" x2="1" y2="1"><stop stop-color="#68d2df"/><stop offset="1" stop-color="#f5feff"/></linearGradient></defs><rect width="900" height="620" rx="36" fill="url(#a)"/><rect x="80" y="90" width="740" height="420" rx="38" fill="#fff" opacity=".88"/><circle cx="275" cy="260" r="72" fill="#11111f"/><path d="M145 455c45-135 215-135 260 0z" fill="#68d2df"/><rect x="475" y="170" width="250" height="32" rx="16" fill="#11111f"/><rect x="475" y="245" width="300" height="24" rx="12" fill="#c9f4f8"/><rect x="475" y="300" width="250" height="24" rx="12" fill="#c9f4f8"/><rect x="475" y="370" width="180" height="58" rx="29" fill="#68d2df"/></svg>

After

Width:  |  Height:  |  Size: 737 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 620"><rect width="900" height="620" rx="36" fill="#11111f"/><circle cx="170" cy="130" r="95" fill="#68d2df" opacity=".45"/><circle cx="760" cy="500" r="120" fill="#68d2df" opacity=".18"/><g fill="#fff"><circle cx="295" cy="250" r="58"/><circle cx="450" cy="225" r="70"/><circle cx="610" cy="250" r="58"/></g><g fill="#68d2df"><path d="M185 465c35-115 185-115 220 0z"/><path d="M305 490c42-145 248-145 290 0z"/><path d="M500 465c35-115 185-115 220 0z"/></g><text x="90" y="95" font-family="Arial" font-size="34" font-weight="800" fill="#fff">Menschen im Mittelpunkt</text></svg>

After

Width:  |  Height:  |  Size: 634 B

+130
View File
@@ -0,0 +1,130 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Datenschutz | OMSORG</title>
<meta name="description" content="Datenschutzerklärung der OMSORG Dautovic & Neumann GbR." />
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="topbar">
<a class="logo" href="index.html" aria-label="OMSORG Startseite">
<img src="assets/omsorg-wordmark-new.png" alt="OMSORG Dautovic & Neumann GbR">
</a>
<nav class="mainnav" aria-label="Hauptnavigation">
<a href="index.html">Startseite</a>
<a href="impressum.html">Impressum</a>
</nav>
<div class="quick"><a class="cta top-cta" href="index.html#kontakt">Kontakt</a></div>
</header>
<main class="wrap" style="padding:80px 0">
<section class="form-card privacy-text">
<p class="kicker">Rechtliches</p>
<h1 style="font-family:Georgia,serif;font-size:3rem;margin:0 0 20px">Datenschutzerklärung</h1>
<h2>1. Verantwortlicher</h2>
<p>Verantwortlich für die Datenverarbeitung auf dieser Website ist:</p>
<p><strong>OMSORG Dautovic &amp; Neumann GbR</strong><br>
Bismarckstraße 32<br>
75179 Pforzheim</p>
<p>Telefon: <a href="tel:+491773402429">0177 3402429</a><br>
E-Mail: <a href="mailto:info@omsorg-pflegedienste.de">info@omsorg-pflegedienste.de</a></p>
<h2>2. Allgemeine Hinweise zum Datenschutz</h2>
<p>Der Schutz Ihrer personenbezogenen Daten ist uns ein besonderes Anliegen. Wir behandeln Ihre Daten vertraulich und entsprechend den gesetzlichen Datenschutzvorschriften, insbesondere der Datenschutz-Grundverordnung (DSGVO), sowie dieser Datenschutzerklärung.</p>
<p>Die Nutzung unserer Website ist grundsätzlich ohne Angabe personenbezogener Daten möglich. Soweit auf dieser Website personenbezogene Daten erhoben werden, erfolgt dies nur, wenn Sie uns diese freiwillig mitteilen, zum Beispiel über ein Kontakt-, Bewerbungs- oder Anfrageformular.</p>
<h2>3. Aufruf der Website und Server-Logfiles</h2>
<p>Beim Besuch unserer Website werden durch den Hosting-Anbieter automatisch technische Informationen erfasst, die für den sicheren und stabilen Betrieb der Website erforderlich sind. Hierzu können insbesondere gehören:</p>
<ul>
<li>IP-Adresse</li>
<li>Datum und Uhrzeit des Zugriffs</li>
<li>aufgerufene Seite oder Datei</li>
<li>Browsertyp und Browserversion</li>
<li>verwendetes Betriebssystem</li>
<li>Referrer-URL</li>
<li>Hostname des zugreifenden Rechners</li>
</ul>
<p>Die Verarbeitung dieser Daten erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der technischen Bereitstellung, Sicherheit und Optimierung unserer Website.</p>
<h2>4. Hosting</h2>
<p>Unsere Website wird über United Domains gehostet. Der Hosting-Anbieter verarbeitet technische Zugriffsdaten, damit die Website sicher und zuverlässig bereitgestellt werden kann. Eine Zusammenführung dieser Daten mit anderen Datenquellen nehmen wir nicht vor.</p>
<h2>5. Kontaktaufnahme per E-Mail oder Formular</h2>
<p>Wenn Sie uns per E-Mail oder über ein Formular kontaktieren, verarbeiten wir die von Ihnen übermittelten Angaben ausschließlich zur Bearbeitung Ihrer Anfrage und für mögliche Anschlussfragen.</p>
<p>Hierzu können insbesondere folgende Daten gehören:</p>
<ul>
<li>Name und Vorname</li>
<li>Telefonnummer</li>
<li>E-Mail-Adresse</li>
<li>Nachrichteninhalt</li>
<li>Angaben zu Ihrer Einrichtung oder Ihrem Anliegen</li>
</ul>
<p>Die Verarbeitung erfolgt je nach Inhalt der Anfrage auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO, wenn Ihre Anfrage mit vorvertraglichen oder vertraglichen Maßnahmen zusammenhängt, oder auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO aufgrund unseres berechtigten Interesses an der Bearbeitung Ihrer Anfrage.</p>
<h2>6. Bewerbungen und Bewerbungsunterlagen</h2>
<p>Wenn Sie sich bei OMSORG Dautovic &amp; Neumann GbR bewerben, verarbeiten wir die von Ihnen übermittelten personenbezogenen Daten ausschließlich zum Zwecke der Durchführung des Bewerbungsverfahrens.</p>
<p>Hierzu können insbesondere gehören:</p>
<ul>
<li>Kontaktdaten</li>
<li>Lebenslauf</li>
<li>Qualifikationsnachweise</li>
<li>Zeugnisse</li>
<li>Berufserfahrung</li>
<li>Nachrichten und weitere Bewerbungsunterlagen</li>
</ul>
<p>Die Verarbeitung erfolgt auf Grundlage von Art. 6 Abs. 1 lit. b DSGVO sowie § 26 BDSG. Ihre Bewerbungsdaten werden nur für das Bewerbungsverfahren verwendet und nicht ohne Ihre Einwilligung an unbefugte Dritte weitergegeben.</p>
<p>Nach Abschluss des Bewerbungsverfahrens werden Bewerbungsdaten gelöscht, sobald keine gesetzlichen Aufbewahrungspflichten oder berechtigten Interessen mehr bestehen. Eine längere Speicherung erfolgt nur, wenn Sie hierzu eingewilligt haben oder gesetzliche Gründe dies erfordern.</p>
<h2>7. Anfragen von Einrichtungen</h2>
<p>Wenn Sie als Einrichtung Personal anfragen, verarbeiten wir die von Ihnen übermittelten Daten zur Bearbeitung Ihrer Anfrage, zur Kontaktaufnahme und zur Abstimmung möglicher Personaleinsätze.</p>
<p>Hierzu können insbesondere Angaben zur Einrichtung, Ansprechpartner, Kontaktdaten, gewünschte Qualifikation, Einsatzzeitraum und weitere Informationen zum Personalbedarf gehören.</p>
<h2>8. Datei-Upload</h2>
<p>Sofern Sie über unsere Website Dateien hochladen, zum Beispiel einen Lebenslauf oder Qualifikationsnachweise, werden diese ausschließlich zur Bearbeitung Ihrer Bewerbung oder Anfrage verwendet. Bitte übermitteln Sie nur Unterlagen, die für Ihr Anliegen erforderlich sind.</p>
<h2>9. Weitergabe personenbezogener Daten</h2>
<p>Eine Weitergabe personenbezogener Daten an Dritte erfolgt nur, wenn dies zur Bearbeitung Ihrer Anfrage erforderlich ist, eine gesetzliche Verpflichtung besteht oder Sie ausdrücklich eingewilligt haben.</p>
<h2>10. Speicherdauer</h2>
<p>Wir speichern personenbezogene Daten nur so lange, wie dies für den jeweiligen Zweck erforderlich ist oder gesetzliche Aufbewahrungspflichten bestehen. Nach Wegfall des Zwecks oder Ablauf gesetzlicher Fristen werden die Daten gelöscht oder gesperrt.</p>
<h2>10a. Mitarbeiter-App / interner Bereich</h2>
<p>Sofern Mitarbeiterinnen und Mitarbeiter den internen OMSORG-Mitarbeiterbereich nutzen, verarbeiten wir die dort freiwillig eingegebenen Daten zur Bearbeitung interner Anliegen. Hierzu können insbesondere Krankmeldungen, Abwesenheitsmeldungen, Urlaubsanträge, Stundennachweise, hochgeladene Dokumente, Kontaktdaten, Einsatzzeiträume und Nachrichteninhalte gehören.</p>
<p>Die Verarbeitung erfolgt zur Durchführung des Beschäftigungsverhältnisses, zur internen Organisation, zur Personalplanung und zur Bearbeitung arbeitsbezogener Anliegen. Rechtsgrundlage ist insbesondere Art. 6 Abs. 1 lit. b DSGVO in Verbindung mit § 26 BDSG sowie unser berechtigtes Interesse an einer ordnungsgemäßen internen Kommunikation und Personalverwaltung.</p>
<p>Die übermittelten Daten werden ausschließlich an die jeweils zuständigen internen Ansprechpartner, insbesondere Geschäftsführung und/oder Disposition, weitergeleitet.</p>
<h2>11. Ihre Rechte</h2>
<p>Sie haben im Rahmen der gesetzlichen Vorgaben jederzeit das Recht auf:</p>
<ul>
<li>Auskunft über die bei uns gespeicherten personenbezogenen Daten</li>
<li>Berichtigung unrichtiger oder unvollständiger Daten</li>
<li>Löschung Ihrer Daten</li>
<li>Einschränkung der Verarbeitung</li>
<li>Datenübertragbarkeit</li>
<li>Widerspruch gegen die Verarbeitung</li>
<li>Widerruf einer erteilten Einwilligung mit Wirkung für die Zukunft</li>
</ul>
<p>Zur Ausübung Ihrer Rechte können Sie sich jederzeit an uns wenden: <a href="mailto:info@omsorg-pflegedienste.de">info@omsorg-pflegedienste.de</a></p>
<h2>12. Beschwerderecht bei einer Aufsichtsbehörde</h2>
<p>Wenn Sie der Ansicht sind, dass die Verarbeitung Ihrer personenbezogenen Daten gegen Datenschutzrecht verstößt, haben Sie das Recht, sich bei einer Datenschutzaufsichtsbehörde zu beschweren.</p>
<h2>13. Datensicherheit</h2>
<p>Wir setzen technische und organisatorische Maßnahmen ein, um Ihre personenbezogenen Daten vor Verlust, Manipulation, unbefugtem Zugriff und Missbrauch zu schützen. Unsere Sicherheitsmaßnahmen werden entsprechend der technischen Entwicklung fortlaufend angepasst.</p>
<h2>14. Cookies und externe Dienste</h2>
<p>Unsere Website verwendet nach aktuellem Stand keine Analyse-Tools, Tracking-Dienste oder extern eingebundenen Marketing-Dienste. Sollten solche Dienste zukünftig eingesetzt werden, wird diese Datenschutzerklärung entsprechend angepasst.</p>
<h2>15. Änderungen dieser Datenschutzerklärung</h2>
<p>Wir behalten uns vor, diese Datenschutzerklärung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder Änderungen unserer Leistungen berücksichtigt.</p>
<p><a class="btn slim" href="index.html">Zurück zur Startseite</a></p>
</section>
</main>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Impressum | OMSORG</title><link rel="stylesheet" href="style.css"></head><body><header class="topbar"><a class="logo" href="index.html"><img src="assets/omsorg-logo-new.png" alt="OMSORG Unterstützend Humanitär / Dautovic &amp; Neumann GbR"></a><nav class="mainnav"><a href="index.html">Startseite</a><a href="datenschutz.html">Datenschutz</a></nav><div class="quick"><a class="cta" href="index.html#kontakt">Kontakt</a></div></header><main class="wrap" style="padding:80px 0"><section class="form-card"><p class="kicker">Rechtliches</p><h1 style="font-family:Georgia,serif;font-size:3rem;margin:0 0 20px">Impressum</h1><p><strong>OMSORG Dautovic &amp; Neumann GbR</strong><br>Bismarckstraße 32<br>75179 Pforzheim</p><p><strong>Vertreten durch:</strong><br>Sabina Dautovic &amp; Malik Neumann</p><p><strong>Kontakt:</strong><br>Telefon: <a href="tel:+491773402429">0177 3402429</a><br>E-Mail: <a href="mailto:info@omsorg-pflegedienste.de">info@omsorg-pflegedienste.de</a></p><p><a class="btn slim" href="index.html">Zurück zur Startseite</a></p></section></main></body></html>
+318
View File
@@ -0,0 +1,318 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OMSORG Dautovic & Neumann GbR | Pflegepersonal Pforzheim</title>
<meta name="description" content="OMSORG Dautovic & Neumann GbR Arbeitnehmerüberlassung in der Pflege. Qualifizierte Pflegekräfte für Einrichtungen und flexible Perspektiven für Pflegekräfte." />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="desktop-exact" aria-label="OMSORG Webseite im Desktop-Layout">
<a class="hot nav-start" href="#start" aria-label="Startseite"></a>
<a class="hot nav-pflege" href="#pflegekraefte" aria-label="Für Pflegekräfte"></a>
<a class="hot nav-einrichtungen" href="#einrichtungen" aria-label="Für Einrichtungen"></a>
<a class="hot nav-ueber" href="#ueber-uns" aria-label="Über uns"></a>
<a class="hot nav-kontakt" href="#kontakt" aria-label="Kontakt"></a>
<button class="hot nav-cta" type="button" data-open-modal="einrichtung" aria-label="Jetzt anfragen"></button>
<button class="hot hero-pflege" type="button" data-open-modal="bewerbung" aria-label="Jetzt bewerben"></button>
<button class="hot hero-einrichtungen" type="button" data-open-modal="einrichtung" aria-label="Personal anfragen"></button>
<a class="hot card-pflege" href="#pflegekraefte" aria-label="Für Pflegekräfte"></a>
<a class="hot card-einrichtungen" href="#einrichtungen" aria-label="Für Einrichtungen"></a>
<a class="hot about-link" href="#ueber-uns" aria-label="Mehr über uns"></a>
<a class="hot phone-link" href="tel:+491773402429" aria-label="0177 3402429 anrufen"></a>
<a class="hot email-link" href="mailto:info@omsorg-pflegedienste.de" aria-label="E-Mail schreiben"></a>
<a class="hot imprint-link" href="impressum.html" aria-label="Impressum"></a>
<a class="hot privacy-link" href="datenschutz.html" aria-label="Datenschutz"></a>
</div>
<header class="topbar">
<a class="logo" href="#start" aria-label="OMSORG Startseite"><img src="assets/omsorg-logo-new.png" alt="OMSORG Unterstützend Humanitär / Dautovic &amp; Neumann GbR"></a>
<button class="menu" type="button" aria-label="Menü öffnen" aria-expanded="false" onclick="const o=document.body.classList.toggle('nav-open');this.textContent=o?'✕':'☰';this.setAttribute('aria-expanded',o)"></button>
<nav class="mainnav" aria-label="Hauptnavigation">
<a class="active" href="#start">Startseite</a>
<a href="#pflegekraefte">Für Pflegekräfte</a>
<a href="#einrichtungen">Für Einrichtungen</a>
<a href="#ueber-uns">Über uns</a>
<a href="#kontakt">Kontakt</a>
<a href="mitarbeiter-app/index.php">Mitarbeiter-App</a>
</nav>
<button class="cta top-cta" type="button" data-open-modal="einrichtung">Jetzt anfragen</button>
</header>
<main id="start">
<section class="hero wrap">
<div class="hero-copy">
<p class="kicker">Verlässliche Pflege. Flexible Lösungen. Menschlich.</p>
<p class="welcome">Willkommen bei</p>
<img class="hero-wordmark" src="assets/omsorg-wordmark-new.png" alt="OMSORG">
<h2>Dautovic &amp; Neumann GbR</h2>
<p class="intro">Ihr Partner für Arbeitnehmerüberlassung in der Pflege.<br>Wir unterstützen Einrichtungen mit qualifizierten Pflegekräften und bieten Pflegekräften sichere und flexible Einsätze.</p>
<div class="hero-actions">
<button class="btn" type="button" data-open-modal="bewerbung"><span>👤</span><b>Für Pflegekräfte</b><small>Jetzt bewerben</small><i></i></button>
<button class="btn ghost" type="button" data-open-modal="einrichtung"><span></span><b>Für Einrichtungen</b><small>Personal anfragen</small><i></i></button>
</div>
</div>
<div class="team-stage" aria-hidden="true"><img src="assets/omsorg-team-transparent.png" alt=""></div>
<aside class="contact-float">
<div class="float-row"><i></i><p><strong>Bismarckstraße 32</strong><br>75179 Pforzheim</p></div>
<div class="float-row"><i>👤</i><p><strong>Sabina Dautovic &amp;<br>Malik Neumann</strong><br>Geschäftsführer<br><a href="tel:+491773402429">0177 3402429</a></p></div>
<div class="float-row"><i></i><p><strong>Sabrina Berggötz</strong><br>Leitung Disposition<br><a href="tel:+4917661464262">0176 61464262</a></p></div>
<button class="float-btn" type="button" data-open-modal="einrichtung">Jetzt unverbindlich anfragen</button>
</aside>
</section>
<section class="benefit-strip wrap">
<article><span>🛡</span><div><h3>Verlässlich</h3><p>Auf uns können Sie zählen. Wir stehen für Verbindlichkeit, Transparenz und Vertrauen.</p></div></article>
<article><span></span><div><h3>Flexibel</h3><p>Wir bieten schnelle und passgenaue Lösungen genau dann, wenn Sie uns brauchen.</p></div></article>
<article><span></span><div><h3>Menschlich</h3><p>Bei uns stehen Menschen im Mittelpunkt mit Empathie, Respekt und Wertschätzung.</p></div></article>
<article><span></span><div><h3>Qualifiziert</h3><p>Unsere Pflegekräfte sind fachlich kompetent, erfahren und engagiert.</p></div></article>
</section>
<section class="services wrap">
<div class="section-head center"><p class="kicker">Unsere Leistungen</p><h2>Passgenaue Lösungen<br>für Pflege &amp; Betreuung</h2><div class="mini-line"></div></div>
<div class="service-grid">
<article class="glass-card"><span>👥</span><h3>Arbeitnehmerüberlassung</h3><p>Qualifizierte Pflegekräfte flexibel und zuverlässig für Ihre Einrichtung.</p></article>
<article class="glass-card"><span>🛡</span><h3>Qualifizierte Fachkräfte</h3><p>Erfahrene Pflegekräfte mit Fachkompetenz und sozialer Verantwortung.</p></article>
<article class="glass-card"><span>📅</span><h3>Flexible Einsatzplanung</h3><p>Bedarfsgerechte Lösungen kurzfristig, langfristig oder projektbezogen.</p></article>
<article class="glass-card"><span></span><h3>Menschlich &amp; Verlässlich</h3><p>Wir stehen für Vertrauen, Wertschätzung und eine partnerschaftliche Zusammenarbeit.</p></article>
</div>
</section>
<section class="feature-row wrap">
<article id="pflegekraefte" class="feature-card"><div><p class="kicker">Für Pflegekräfte</p><h2>Arbeiten mit Wertschätzung und Perspektive</h2><ul><li>Flexible Arbeitszeitmodelle</li><li>Faire Vergütung</li><li>Persönliche Betreuung</li><li>Vielfältige Einsatzmöglichkeiten</li></ul><button class="btn slim" type="button" data-open-modal="pflegeinfo">Mehr erfahren <i></i></button></div><img src="assets/pflege-bewerbung.jpg" alt="Pflegekraft"></article>
<article id="einrichtungen" class="feature-card"><div><p class="kicker">Für Einrichtungen</p><h2>Qualifiziertes Personal genau dann, wenn Sie es benötigen</h2><ul><li>Schnelle Verfügbarkeit</li><li>Geprüfte Fachkräfte</li><li>Entlastung Ihres Teams</li><li>Langfristige Partnerschaft</li></ul><button class="btn slim" type="button" data-open-modal="einrichtungsinfo">Mehr erfahren <i></i></button></div><img src="assets/pflege-einrichtungen.jpg" alt="Pflegefachkraft"></article>
</section>
<section id="ueber-uns" class="about wrap">
<div class="about-photo"><img src="assets/pflege-training.jpg" alt="OMSORG Team"></div>
<div class="about-copy"><p class="kicker">Über uns</p><h2>Erfahrung. Engagement. Leidenschaft für Pflege.</h2><p>OMSORG Dautovic &amp; Neumann GbR wurde aus der Praxis heraus gegründet. Unsere Geschäftsführung und Disposition kennen die Pflege aus eigener Erfahrung und wissen, worauf es im Alltag wirklich ankommt.</p><button class="btn slim" type="button" data-open-modal="ueberuns">Mehr über uns <i></i></button></div>
</section>
<section id="kontakt" class="contact-bar wrap">
<div><i></i><b>Adresse</b><span>Bismarckstraße 32<br>75179 Pforzheim</span></div>
<div><i></i><b>Geschäftsführung</b><span>Sabina Dautovic &amp; Malik Neumann<br><small>Geschäftsführer</small><br><a href="tel:+491773402429">0177 3402429</a></span></div>
<div><i></i><b>Leitung Disposition</b><span>Sabrina Berggötz<br><a href="tel:+4917661464262">0176 61464262</a></span></div>
<div><i></i><b>E-Mail</b><span><a href="mailto:info@omsorg-pflegedienste.de">info@omsorg-pflegedienste.de</a></span></div>
<div><i></i><b>Bürozeiten</b><span>Mo Fr: 08:00 16:00 Uhr</span></div>
</section>
<section class="phase2-grid wrap" aria-label="Standort und Bewertung">
<article class="map-card">
<div class="section-head compact"><p class="kicker">Standort</p><h2>OMSORG in Pforzheim</h2><div class="mini-line left"></div></div>
<p>Bismarckstraße 32, 75179 Pforzheim</p>
<div class="map-frame" id="map-container">
<div id="map-consent" style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;background:rgba(10,20,40,.7);gap:12px;padding:24px;text-align:center">
<p style="margin:0;color:#c8d8f0;font-size:.95rem">Durch das Laden der Karte werden Daten an Google übertragen.<br>Mehr dazu in unserer <a href="datenschutz.html" style="color:var(--cyan,#7ecff6)">Datenschutzerklärung</a>.</p>
<button onclick="loadMap()" style="background:#1a7acc;color:#fff;border:none;border-radius:8px;padding:10px 22px;cursor:pointer;font-size:.95rem">Karte laden</button>
</div>
</div>
<script>
function loadMap() {
var c = document.getElementById('map-consent');
var frame = document.createElement('iframe');
frame.title = 'Google Maps Karte OMSORG Pforzheim';
frame.loading = 'lazy';
frame.referrerPolicy = 'no-referrer-when-downgrade';
frame.src = 'https://www.google.com/maps?q=Bismarckstra%C3%9Fe%2032%2C%2075179%20Pforzheim&output=embed';
frame.style.cssText = 'width:100%;height:100%;border:0';
c.replaceWith(frame);
}
</script>
<a class="btn slim phase2-btn" href="https://www.google.com/maps/search/?api=1&query=Bismarckstra%C3%9Fe%2032%2C%2075179%20Pforzheim" target="_blank" rel="noopener"><span></span><b>Route öffnen</b><i></i></a>
</article>
<article class="review-card">
<p class="kicker">Ihre Meinung zählt</p>
<h2>Google Bewertung abgeben</h2>
<p>Wir freuen uns über ehrliches Feedback von Pflegekräften, Einrichtungen und Partnern. Ihre Bewertung hilft anderen dabei, OMSORG besser kennenzulernen.</p>
<a class="btn slim phase2-btn" href="https://www.google.com/search?q=OMSORG+Dautovic+%26+Neumann+GbR+Bewertung" target="_blank" rel="noopener"><span></span><b>Auf Google bewerten</b><i></i></a>
<a class="whatsapp-inline" href="https://wa.me/491773402429" target="_blank" rel="noopener">Per WhatsApp an die Geschäftsführung schreiben</a>
</article>
</section>
</main>
<div class="modal-backdrop" id="modal-pflegeinfo" aria-hidden="true">
<div class="modal-dialog benefits-dialog" role="dialog" aria-modal="true" aria-labelledby="pflegeinfo-title">
<button class="modal-close" type="button" data-close-modal aria-label="Fenster schließen">×</button>
<p class="kicker">Für Pflegekräfte</p>
<h2 id="pflegeinfo-title">Warum OMSORG?</h2>
<p class="modal-note">Bei OMSORG Dautovic &amp; Neumann GbR stehen unsere Mitarbeiterinnen und Mitarbeiter im Mittelpunkt. Wir wissen, dass professionelle Pflege nur dann möglich ist, wenn sich Pflegekräfte wertgeschätzt, unterstützt und sicher fühlen.</p>
<p class="modal-note">Deshalb bieten wir mehr als nur einen Arbeitsplatz wir bieten Perspektiven, Sicherheit und ein starkes Team.</p>
<div class="benefits-highlight">
<article><strong>Sicherheit</strong><span>Unbefristete Festanstellung und langfristige Perspektive.</span></article>
<article><strong>Wertschätzung</strong><span>Persönliche Betreuung und respektvoller Umgang auf Augenhöhe.</span></article>
<article><strong>Entwicklung</strong><span>Fortbildungen, neue Einsatzmöglichkeiten und echte Karrierechancen.</span></article>
</div>
<ul class="benefits-list">
<li>Unbefristeter Arbeitsvertrag</li>
<li>Attraktive und faire Vergütung</li>
<li>Flexible Arbeitszeitmodelle</li>
<li>Persönliche Betreuung durch feste Ansprechpartner</li>
<li>Wertschätzende Unternehmenskultur</li>
<li>Fort- und Weiterbildungsmöglichkeiten</li>
<li>Unterstützung in persönlichen Lebenssituationen</li>
<li>Verlässliche Dienstplanung</li>
<li>Familiäres Betriebsklima</li>
<li>Moderne Arbeitsbedingungen</li>
<li>Vielfältige Einsatzmöglichkeiten</li>
<li>Entwicklungsmöglichkeiten für die berufliche Zukunft</li>
<li>Schnelle und unkomplizierte Kommunikation</li>
<li>Respektvoller Umgang auf Augenhöhe</li>
<li>Unterstützung bei beruflichen Herausforderungen</li>
<li>Individuelle Karriereplanung</li>
</ul>
<p class="modal-note">Bei OMSORG sind Sie nicht nur eine Personalnummer. Wir begleiten Sie persönlich, unterstützen Sie in Ihrem Berufsalltag und schaffen gemeinsam die Grundlage für eine langfristige und erfolgreiche Zusammenarbeit.</p>
<button class="btn modal-submit" type="button" data-open-modal="bewerbung"><span>👤</span><b>Jetzt bewerben</b><small>Teil des Teams werden</small><i></i></button>
</div>
</div>
<div class="modal-backdrop" id="modal-einrichtungsinfo" aria-hidden="true">
<div class="modal-dialog benefits-dialog" role="dialog" aria-modal="true" aria-labelledby="einrichtungsinfo-title">
<button class="modal-close" type="button" data-close-modal aria-label="Fenster schließen">×</button>
<p class="kicker">Für Einrichtungen</p>
<h2 id="einrichtungsinfo-title">Qualifiziertes Personal genau dann, wenn Sie es benötigen</h2>
<p class="modal-note">Personelle Engpässe, kurzfristige Ausfälle und steigende Anforderungen gehören heute zum Alltag vieler Pflegeeinrichtungen und Krankenhäuser. OMSORG Dautovic &amp; Neumann GbR unterstützt Sie dabei, diese Herausforderungen zuverlässig und professionell zu meistern.</p>
<p class="modal-note">Wir stellen qualifizierte Pflegekräfte flexibel und bedarfsgerecht zur Verfügung kurzfristig, langfristig oder projektbezogen. Unser Ziel ist es, Ihre Einrichtung nachhaltig zu entlasten und die Versorgung Ihrer Bewohner und Patienten jederzeit sicherzustellen.</p>
<div class="benefits-highlight">
<article><strong>Schnelle Verfügbarkeit</strong><span>Qualifiziertes Personal auch bei kurzfristigen Ausfällen.</span></article>
<article><strong>Planungssicherheit</strong><span>Verlässliche Personallösungen für eine stabile Versorgung.</span></article>
<article><strong>Feste Ansprechpartner</strong><span>Direkte Kommunikation ohne lange Wege.</span></article>
</div>
<ul class="benefits-list">
<li>Kurzfristige Verfügbarkeit qualifizierter Pflegekräfte</li>
<li>Examinierte Fachkräfte sowie einjährig examinierte Pflegekräfte</li>
<li>Flexible Einsatzmodelle nach Ihrem individuellen Bedarf</li>
<li>Entlastung Ihres Stammpersonals</li>
<li>Hohe Ausfallsicherheit und schnelle Reaktionszeiten</li>
<li>Feste Ansprechpartner für eine unkomplizierte Kommunikation</li>
<li>Transparente und verlässliche Zusammenarbeit</li>
<li>Sorgfältige Auswahl unserer Mitarbeiter</li>
<li>Fachliche Kompetenz und soziale Verantwortung</li>
<li>Unterstützung bei kurzfristigen Personalengpässen</li>
<li>Langfristige Partnerschaften statt kurzfristiger Lösungen</li>
<li>Planungssicherheit für Ihre Einrichtung</li>
</ul>
<p class="modal-note">Wir verstehen die Herausforderungen des Gesundheitswesens und entwickeln gemeinsam mit Ihnen passgenaue Personallösungen. Durch unsere Erfahrung im Pflegebereich und unser Verständnis für die Bedürfnisse von Einrichtungen schaffen wir nachhaltige Unterstützung dort, wo sie gebraucht wird.</p>
<p class="modal-note">Verlassen Sie sich auf einen Partner, der Professionalität, Flexibilität und Menschlichkeit miteinander verbindet.</p>
<button class="btn modal-submit" type="button" data-open-modal="einrichtung"><span></span><b>Personal anfragen</b><small>Bedarf mitteilen</small><i></i></button>
</div>
</div>
<div class="modal-backdrop" id="modal-ueberuns" aria-hidden="true">
<div class="modal-dialog benefits-dialog" role="dialog" aria-modal="true" aria-labelledby="ueberuns-title">
<button class="modal-close" type="button" data-close-modal aria-label="Fenster schließen">×</button>
<p class="kicker">Über uns</p>
<h2 id="ueberuns-title">Erfahrung. Engagement. Leidenschaft für Pflege.</h2>
<p class="modal-note">OMSORG Dautovic &amp; Neumann GbR wurde aus der Praxis heraus gegründet von Menschen, die die Pflege nicht nur kennen, sondern täglich leben.</p>
<p class="modal-note">Die Geschäftsführer Sabina Dautovic und Malik Neumann sind examinierte Pflegefachkräfte und verfügen über langjährige Erfahrung im Gesundheits- und Pflegewesen. Auch unsere Leitung der Disposition, Sabrina Berggötz, stammt aus der Pflegepraxis und kennt die täglichen Herausforderungen von Pflegekräften und Einrichtungen aus eigener Erfahrung.</p>
<p class="modal-note">Wir wissen, wie belastend Personalmangel, kurzfristige Ausfälle und hohe Anforderungen im Pflegealltag sein können. Deshalb verfolgen wir einen anderen Ansatz: Wir möchten nicht nur Personal vermitteln, sondern nachhaltige Lösungen schaffen für Pflegekräfte ebenso wie für Einrichtungen.</p>
<p class="modal-note">Was uns besonders macht: Unsere Geschäftsführung ist nicht ausschließlich im Büro tätig. Auch heute sind wir regelmäßig selbst im pflegerischen Einsatz und bleiben dadurch nah an den Menschen, den Herausforderungen und den tatsächlichen Bedürfnissen des Pflegealltags.</p>
<p class="modal-note">Diese Nähe zur Praxis ermöglicht es uns, die Anforderungen unserer Kunden und Mitarbeiter besser zu verstehen und individuelle Lösungen zu entwickeln. Wir kennen die Belastungen, die Verantwortung und die Erwartungen an professionelle Pflege aus eigener Erfahrung.</p>
<div class="benefits-highlight">
<article><strong>Praxisnah</strong><span>Geschäftsführung und Disposition kommen selbst aus der Pflege.</span></article>
<article><strong>Verlässlich</strong><span>Wir kennen Personalmangel aus eigener Erfahrung und handeln lösungsorientiert.</span></article>
<article><strong>Auf Augenhöhe</strong><span>Direkte Kommunikation, kurze Wege und persönliche Betreuung.</span></article>
</div>
<ul class="benefits-list">
<li>Menschlichkeit und Respekt</li>
<li>Vertrauen und Verlässlichkeit</li>
<li>Fachliche Kompetenz</li>
<li>Persönliche Betreuung</li>
<li>Ehrliche und transparente Kommunikation</li>
<li>Partnerschaftliche Zusammenarbeit</li>
</ul>
<p class="modal-note">Für uns stehen Menschen im Mittelpunkt. Deshalb legen wir großen Wert auf persönliche Beziehungen, kurze Entscheidungswege und eine vertrauensvolle Zusammenarbeit auf Augenhöhe.</p>
<p class="modal-note">OMSORG steht für professionelle Arbeitnehmerüberlassung mit Herz, Verstand und echter Praxiserfahrung.</p>
<button class="btn modal-submit" type="button" data-open-modal="bewerbung"><span>👤</span><b>Jetzt Kontakt aufnehmen</b><small>mit OMSORG</small><i></i></button>
</div>
</div>
<div class="modal-backdrop" id="modal-bewerbung" aria-hidden="true">
<div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="bewerbung-title">
<button class="modal-close" type="button" data-close-modal aria-label="Fenster schließen">×</button>
<p class="kicker">Für Pflegekräfte</p>
<h2 id="bewerbung-title">Jetzt bewerben</h2>
<p class="modal-note">Bitte hinterlassen Sie Ihre Kontaktdaten und laden Sie Ihren Lebenslauf hoch. Die Nachricht wird an <strong>info@omsorg-pflegedienste.de</strong> gesendet.</p>
<form class="modal-form" action="send-form.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="form_type" value="bewerbung">
<label>Vor- und Nachname*<input name="name" type="text" required autocomplete="name"></label>
<label>Telefon*<input name="telefon" type="tel" required autocomplete="tel"></label>
<label>E-Mail<input name="email" type="email" autocomplete="email"></label>
<label>Nachricht<textarea name="nachricht" rows="5" placeholder="Kurz vorstellen, Erfahrung, gewünschter Einsatzort oder Rückrufzeit"></textarea></label>
<label>Lebenslauf / Datei hochladen<input name="anlage" type="file" accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"></label>
<small class="form-hint">Erlaubt: PDF, DOC, DOCX, JPG, PNG bis 10 MB.</small>
<button class="btn modal-submit" type="submit"><span></span><b>Bewerbung senden</b><small>an OMSORG</small><i></i></button>
</form>
</div>
</div>
<div class="modal-backdrop" id="modal-einrichtung" aria-hidden="true">
<div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="einrichtung-title">
<button class="modal-close" type="button" data-close-modal aria-label="Fenster schließen">×</button>
<p class="kicker">Für Einrichtungen</p>
<h2 id="einrichtung-title">Personal anfragen</h2>
<p class="modal-note">Wählen Sie aus, welche Unterstützung Sie benötigen und für welchen Zeitraum. Die Anfrage wird an <strong>info@omsorg-pflegedienste.de</strong> gesendet.</p>
<form class="modal-form" action="send-form.php" method="post">
<input type="hidden" name="form_type" value="einrichtung">
<label>Einrichtung / Unternehmen*<input name="einrichtung" type="text" required autocomplete="organization"></label>
<label>Ansprechpartner*<input name="name" type="text" required autocomplete="name"></label>
<label>Telefon*<input name="telefon" type="tel" required autocomplete="tel"></label>
<label>E-Mail<input name="email" type="email" autocomplete="email"></label>
<label>Benötigte Unterstützung*
<select name="qualifikation" required>
<option value="">Bitte auswählen</option>
<option>1-jährige Pflegekraft</option>
<option>Fachkraft</option>
<option>Beides / noch offen</option>
</select>
</label>
<div class="form-split">
<label>Von*<input name="zeitraum_von" type="date" required></label>
<label>Bis*<input name="zeitraum_bis" type="date" required></label>
</div>
<label>Weitere Angaben<textarea name="nachricht" rows="5" placeholder="Schichten, Anzahl der Kräfte, Station/Bereich, gewünschte Rückrufzeit"></textarea></label>
<button class="btn modal-submit" type="submit"><span></span><b>Anfrage senden</b><small>an OMSORG</small><i></i></button>
</form>
</div>
</div>
<a class="whatsapp-float" href="https://wa.me/491773402429" target="_blank" rel="noopener" aria-label="OMSORG per WhatsApp kontaktieren">WhatsApp</a>
<footer class="footer wrap"><p>© 2026 OMSORG Dautovic &amp; Neumann GbR</p><nav><a href="impressum.html">Impressum</a><a href="datenschutz.html">Datenschutz</a></nav></footer>
<script>
document.querySelectorAll('a[href^="#"]').forEach(a=>a.addEventListener('click',()=>{document.body.classList.remove('nav-open');const m=document.querySelector('.menu');if(m){m.textContent='☰';m.setAttribute('aria-expanded','false');}}));
document.addEventListener('keydown',e=>{if(e.key==='Escape'&&document.body.classList.contains('nav-open')){document.body.classList.remove('nav-open');const m=document.querySelector('.menu');if(m){m.textContent='☰';m.setAttribute('aria-expanded','false');}}});
const openModal = (name) => {
const modal = document.getElementById('modal-' + name);
if (!modal) return;
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
document.body.classList.add('modal-open');
const firstField = modal.querySelector('input, select, textarea, button');
if (firstField) setTimeout(() => firstField.focus(), 60);
};
const closeModals = () => {
document.querySelectorAll('.modal-backdrop.is-open').forEach(modal => {
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
});
document.body.classList.remove('modal-open');
};
document.querySelectorAll('[data-open-modal]').forEach(btn => btn.addEventListener('click', () => openModal(btn.dataset.openModal)));
document.querySelectorAll('[data-close-modal]').forEach(btn => btn.addEventListener('click', closeModals));
document.querySelectorAll('.modal-backdrop').forEach(modal => modal.addEventListener('click', (event) => { if (event.target === modal) closeModals(); }));
document.addEventListener('keydown', (event) => { if (event.key === 'Escape') closeModals(); });
</script>
</body>
</html>
View File
+171
View File
@@ -0,0 +1,171 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_admin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/admin.php');
exit;
}
/* ── Login attempts ────────────────────────────────────────────────── */
if (($_POST['_login_action'] ?? '') === 'clear') {
db()->exec('DELETE FROM login_attempts');
header('Location: ../pages/admin.php?tab=login&cleared=1');
exit;
}
/* ── User actions ──────────────────────────────────────────────────── */
if (!empty($_POST['_user_action'])) {
$action = $_POST['_user_action'];
$user_id = (int)($_POST['user_id'] ?? 0);
$back = '../pages/admin.php?tab=nutzer';
switch ($action) {
case 'add_user': {
$username = trim($_POST['username'] ?? '');
$uname = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$telefon = trim($_POST['telefon'] ?? '');
$role = in_array($_POST['role'] ?? '', ['user', 'admin'], true) ? $_POST['role'] : 'user';
$password = $_POST['password'] ?? '';
if ($username === '' || $uname === '' || strlen($password) < 6) {
header('Location: ' . $back . '&user_msg=' . urlencode('Benutzername, Name und Passwort (mind. 6 Zeichen) sind Pflichtfelder.'));
exit;
}
try {
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
$stmt = db()->prepare(
'INSERT INTO users (username, name, role, password_hash, active, created_at, email, telefon)
VALUES (?, ?, ?, ?, 1, ?, ?, ?)'
);
$stmt->execute([$username, $uname, $role, $hash, date('c'), $email, $telefon]);
} catch (\PDOException $ex) {
$msg = str_contains($ex->getMessage(), 'UNIQUE') ? 'Benutzername bereits vergeben.' : 'Datenbankfehler.';
header('Location: ' . $back . '&user_msg=' . urlencode($msg));
exit;
}
header('Location: ' . $back . '&user_ok=1&user_msg=' . urlencode('Nutzer erstellt.'));
exit;
}
case 'edit_user': {
$uname = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$telefon = trim($_POST['telefon'] ?? '');
$role = in_array($_POST['role'] ?? '', ['user', 'admin'], true) ? $_POST['role'] : 'user';
$active = (int)(bool)($_POST['active'] ?? 0);
if ($user_id < 1 || $uname === '') {
header('Location: ' . $back . '&user_msg=' . urlencode('Ungültige Eingabe.'));
exit;
}
$stmt = db()->prepare(
'UPDATE users SET name=?, email=?, telefon=?, role=?, active=? WHERE id=?'
);
$stmt->execute([$uname, $email, $telefon, $role, $active, $user_id]);
header('Location: ' . $back . '&user_ok=1&user_msg=' . urlencode('Nutzer gespeichert.'));
exit;
}
case 'change_password': {
$password = $_POST['new_password'] ?? '';
if ($user_id < 1 || strlen($password) < 6) {
header('Location: ' . $back . '&user_msg=' . urlencode('Passwort muss mindestens 6 Zeichen lang sein.'));
exit;
}
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
$stmt = db()->prepare('UPDATE users SET password_hash=? WHERE id=?');
$stmt->execute([$hash, $user_id]);
header('Location: ' . $back . '&user_ok=1&user_msg=' . urlencode('Passwort geändert.'));
exit;
}
case 'delete_user': {
if ($user_id < 1) {
header('Location: ' . $back . '&user_msg=' . urlencode('Ungültige Nutzer-ID.'));
exit;
}
$stmt = db()->prepare('SELECT username FROM users WHERE id=?');
$stmt->execute([$user_id]);
$row = $stmt->fetch();
if (!$row) {
header('Location: ' . $back . '&user_msg=' . urlencode('Nutzer nicht gefunden.'));
exit;
}
if ($row['username'] === current_username()) {
header('Location: ' . $back . '&user_msg=' . urlencode('Den eigenen Account kann man nicht löschen.'));
exit;
}
$stmt = db()->prepare('DELETE FROM users WHERE id=?');
$stmt->execute([$user_id]);
header('Location: ' . $back . '&user_ok=1&user_msg=' . urlencode('Nutzer gelöscht.'));
exit;
}
default:
header('Location: ' . $back . '&user_msg=' . urlencode('Unbekannte Aktion.'));
exit;
}
}
/* ── Request actions ───────────────────────────────────────────────── */
$request_id = (int)($_POST['request_id'] ?? 0);
$action = $_POST['action'] ?? '';
$admin_note = trim($_POST['admin_note'] ?? '');
$type = $_POST['type'] ?? '';
$allowed_types = ['urlaubsantrag', 'abwesenheitsantrag', 'benefitsantrag', 'werben', 'fortbildungsantrag', 'stundennachweis'];
if (!in_array($type, $allowed_types, true)) {
header('Location: ../pages/admin.php?error=' . urlencode('Ungültiger Antragstyp.'));
exit;
}
/* ── Delete request ────────────────────────────────────────────────── */
if ($action === 'delete') {
if ($request_id < 1) {
header('Location: ../pages/admin.php?error=' . urlencode('Ungültige Antrags-ID.'));
exit;
}
$table = 'requests_' . $type;
$stmt = db()->prepare("SELECT * FROM $table WHERE id = ?");
$stmt->execute([$request_id]);
$row = $stmt->fetch();
if (!$row) {
header('Location: ../pages/admin.php?error=' . urlencode('Antrag nicht gefunden.'));
exit;
}
// Delete associated file for types that have uploads
if (in_array($type, ['stundennachweis', 'fortbildungsantrag'], true) && !empty($row['filename'])) {
$file_path = __DIR__ . '/../uploads/' . $row['filename'];
if (is_file($file_path)) {
unlink($file_path);
}
}
$stmt = db()->prepare("DELETE FROM $table WHERE id = ?");
$stmt->execute([$request_id]);
header('Location: ../pages/admin.php?deleted=1');
exit;
}
/* ── Status update ─────────────────────────────────────────────────── */
if (!in_array($action, ['accepted', 'rejected'], true) || $request_id < 1) {
header('Location: ../pages/admin.php?error=' . urlencode('Ungültige Aktion.'));
exit;
}
$table = 'requests_' . $type;
$stmt = db()->prepare("SELECT id FROM $table WHERE id = ?");
$stmt->execute([$request_id]);
if (!$stmt->fetch()) {
header('Location: ../pages/admin.php?error=' . urlencode('Antrag nicht gefunden.'));
exit;
}
$stmt = db()->prepare("UPDATE $table SET status = ?, admin_note = ?, updated_at = ? WHERE id = ?");
$stmt->execute([$action, $admin_note, date('c'), $request_id]);
header('Location: ../pages/admin.php?type=' . urlencode($type) . '&id=' . $request_id . '&updated=1');
exit;
@@ -0,0 +1,42 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/dokumentenarchiv.php');
exit;
}
$id = (int) ($_POST['id'] ?? 0);
if ($id <= 0) {
http_response_code(400);
echo 'Ungültige Anfrage.';
exit;
}
$stmt = db()->prepare('SELECT * FROM dokumente WHERE id = ?');
$stmt->execute([$id]);
$doc = $stmt->fetch();
if (!$doc) {
http_response_code(404);
echo 'Dokument nicht gefunden.';
exit;
}
$user = current_user();
if ((int) $doc['user_id'] !== (int) $user['id'] && !is_admin()) {
http_response_code(403);
echo 'Keine Berechtigung.';
exit;
}
$file_path = dirname(__DIR__) . '/uploads/' . $doc['filename'];
if (is_file($file_path)) {
unlink($file_path);
}
db()->prepare('DELETE FROM dokumente WHERE id = ?')->execute([$id]);
header('Location: ../pages/dokumentenarchiv.php?deleted=1');
exit;
@@ -0,0 +1,77 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/upload.php';
require_admin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/admin.php?tab=downloads');
exit;
}
$action = $_POST['_action'] ?? '';
/* ── Add ──────────────────────────────────────────────────────────────── */
if ($action === 'add') {
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
if ($title === '') {
header('Location: ../pages/admin.php?tab=downloads&err=notitle');
exit;
}
$up = handle_upload($_FILES['file'] ?? [], [
'allowed' => UPLOAD_TYPES_OFFICE,
'dir' => dirname(__DIR__) . '/downloads/',
'naming' => 'hash',
'max_bytes' => 20 * 1024 * 1024,
'messages' => [
'missing' => 'nofile', 'upload' => 'nofile',
'ext' => 'badext', 'mime' => 'badmime',
'size' => 'toobig', 'move' => 'upload',
],
]);
if (!$up['ok']) {
header('Location: ../pages/admin.php?tab=downloads&err=' . $up['error']);
exit;
}
$safe_name = $up['filename'];
$original_name = $up['original_name'];
$sort_order = (int)db()->query('SELECT COALESCE(MAX(sort_order), 0) + 10 FROM downloads')->fetchColumn();
$now = date('c');
$stmt = db()->prepare(
'INSERT INTO downloads (title, description, filename, original_name, sort_order, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)'
);
$stmt->execute([$title, $description, $safe_name, $original_name, $sort_order, $now, $now]);
header('Location: ../pages/admin.php?tab=downloads&ok=added');
exit;
}
/* ── Delete ───────────────────────────────────────────────────────────── */
if ($action === 'delete') {
$id = (int)($_POST['download_id'] ?? 0);
if ($id <= 0) {
header('Location: ../pages/admin.php?tab=downloads&err=invalid');
exit;
}
$stmt = db()->prepare('SELECT filename FROM downloads WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch();
if ($row) {
db()->prepare('DELETE FROM downloads WHERE id = ?')->execute([$id]);
@unlink(dirname(__DIR__) . '/downloads/' . $row['filename']);
}
header('Location: ../pages/admin.php?tab=downloads&ok=deleted');
exit;
}
header('Location: ../pages/admin.php?tab=downloads');
exit;
@@ -0,0 +1,45 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_admin();
header('Content-Type: application/json');
if (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') !== 'XMLHttpRequest') {
echo json_encode(['ok' => false, 'error' => 'forbidden']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['ok' => false, 'error' => 'method']);
exit;
}
$body = file_get_contents('php://input');
$data = json_decode($body, true);
$order = $data['order'] ?? null;
if (!is_array($order)) {
echo json_encode(['ok' => false, 'error' => 'invalid']);
exit;
}
$ids = array_values(array_filter(array_map('intval', $order), fn($v) => $v > 0));
if (empty($ids)) {
echo json_encode(['ok' => true]);
exit;
}
$now = date('c');
$pos = 10;
$stmt = db()->prepare('UPDATE downloads SET sort_order = ?, updated_at = ? WHERE id = ?');
db()->beginTransaction();
foreach ($ids as $id) {
$stmt->execute([$pos, $now, $id]);
$pos += 10;
}
db()->commit();
echo json_encode(['ok' => true]);
exit;
@@ -0,0 +1,92 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/upload.php';
require_admin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/admin.php?tab=einsatzanweisung');
exit;
}
verify_csrf();
$back = '../pages/admin.php?tab=einsatzanweisung';
$action = $_POST['_action'] ?? '';
$user_id = (int)($_POST['target_user_id'] ?? 0);
if ($user_id <= 0) {
header('Location: ' . $back . '&err=' . urlencode('Ungültiger Nutzer.'));
exit;
}
$stmt = db()->prepare('SELECT id FROM users WHERE id = ?');
$stmt->execute([$user_id]);
if (!$stmt->fetch()) {
header('Location: ' . $back . '&err=' . urlencode('Nutzer nicht gefunden.'));
exit;
}
if ($action === 'update_ort') {
$ort = trim($_POST['ort'] ?? '');
db()->prepare(
'INSERT INTO einsatzanweisung (user_id, ort)
VALUES (?, ?)
ON CONFLICT(user_id) DO UPDATE SET ort = excluded.ort'
)->execute([$user_id, $ort]);
header('Location: ' . $back . '&ok=' . urlencode('Einsatzort gespeichert.'));
exit;
}
if ($action === 'upload_pdf') {
$stmt = db()->prepare('SELECT u.username, e.filename AS old_filename FROM users u LEFT JOIN einsatzanweisung e ON e.user_id = u.id WHERE u.id = ?');
$stmt->execute([$user_id]);
$row = $stmt->fetch();
$dir = dirname(__DIR__) . '/uploads/';
$up = handle_upload($_FILES['pdf'] ?? [], [
'allowed' => UPLOAD_TYPES_PDF,
'dir' => $dir,
'prefix' => 'einsatzanweisung',
'username' => $row['username'],
'messages' => [
'missing' => 'Bitte eine PDF-Datei auswählen.',
'upload' => 'Fehler beim Hochladen.',
'ext' => 'Nur PDF-Dateien erlaubt.',
'mime' => 'Nur PDF-Dateien erlaubt.',
'size' => 'Datei ist zu groß (max. 12 MB).',
'move' => 'Fehler beim Speichern der Datei.',
],
]);
if (!$up['ok']) {
header('Location: ' . $back . '&err=' . urlencode($up['error']));
exit;
}
$original_name = $up['original_name'];
$safe_name = $up['filename'];
if (!empty($row['old_filename'])) {
$old_path = $dir . basename($row['old_filename']);
if (is_file($old_path)) {
unlink($old_path);
}
}
$now = date('c');
db()->prepare(
'INSERT INTO einsatzanweisung (user_id, filename, original_name, uploaded_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET
filename = excluded.filename,
original_name = excluded.original_name,
uploaded_at = excluded.uploaded_at'
)->execute([$user_id, $safe_name, $original_name, $now]);
header('Location: ' . $back . '&ok=' . urlencode('PDF erfolgreich hochgeladen.'));
exit;
}
header('Location: ' . $back);
exit;
@@ -0,0 +1,77 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/upload.php';
require_admin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/admin.php?tab=fortbildung');
exit;
}
$action = $_POST['_action'] ?? '';
/* ── Add ──────────────────────────────────────────────────────────────── */
if ($action === 'add') {
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
if ($title === '') {
header('Location: ../pages/admin.php?tab=fortbildung&err=notitle');
exit;
}
$up = handle_upload($_FILES['file'] ?? [], [
'allowed' => UPLOAD_TYPES_OFFICE,
'dir' => dirname(__DIR__) . '/fortbildung-materials/',
'naming' => 'hash',
'max_bytes' => 20 * 1024 * 1024,
'messages' => [
'missing' => 'nofile', 'upload' => 'nofile',
'ext' => 'badext', 'mime' => 'badmime',
'size' => 'toobig', 'move' => 'upload',
],
]);
if (!$up['ok']) {
header('Location: ../pages/admin.php?tab=fortbildung&err=' . $up['error']);
exit;
}
$safe_name = $up['filename'];
$original_name = $up['original_name'];
$sort_order = (int)db()->query('SELECT COALESCE(MAX(sort_order), 0) + 10 FROM fortbildung_materials')->fetchColumn();
$now = date('c');
$stmt = db()->prepare(
'INSERT INTO fortbildung_materials (title, description, filename, original_name, sort_order, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)'
);
$stmt->execute([$title, $description, $safe_name, $original_name, $sort_order, $now, $now]);
header('Location: ../pages/admin.php?tab=fortbildung&ok=added');
exit;
}
/* ── Delete ───────────────────────────────────────────────────────────── */
if ($action === 'delete') {
$id = (int)($_POST['material_id'] ?? 0);
if ($id <= 0) {
header('Location: ../pages/admin.php?tab=fortbildung&err=invalid');
exit;
}
$stmt = db()->prepare('SELECT filename FROM fortbildung_materials WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch();
if ($row) {
db()->prepare('DELETE FROM fortbildung_materials WHERE id = ?')->execute([$id]);
@unlink(dirname(__DIR__) . '/fortbildung-materials/' . $row['filename']);
}
header('Location: ../pages/admin.php?tab=fortbildung&ok=deleted');
exit;
}
header('Location: ../pages/admin.php?tab=fortbildung');
exit;
@@ -0,0 +1,45 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_admin();
header('Content-Type: application/json');
if (($_SERVER['HTTP_X_REQUESTED_WITH'] ?? '') !== 'XMLHttpRequest') {
echo json_encode(['ok' => false, 'error' => 'forbidden']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['ok' => false, 'error' => 'method']);
exit;
}
$body = file_get_contents('php://input');
$data = json_decode($body, true);
$order = $data['order'] ?? null;
if (!is_array($order)) {
echo json_encode(['ok' => false, 'error' => 'invalid']);
exit;
}
$ids = array_values(array_filter(array_map('intval', $order), fn($v) => $v > 0));
if (empty($ids)) {
echo json_encode(['ok' => true]);
exit;
}
$now = date('c');
$pos = 10;
$stmt = db()->prepare('UPDATE fortbildung_materials SET sort_order = ?, updated_at = ? WHERE id = ?');
db()->beginTransaction();
foreach ($ids as $id) {
$stmt->execute([$pos, $now, $id]);
$pos += 10;
}
db()->commit();
echo json_encode(['ok' => true]);
exit;
+11
View File
@@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$p = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$p['path'], $p['domain'], $p['secure'], $p['httponly']);
}
session_destroy();
header('Location: ../index.php');
exit;
+34
View File
@@ -0,0 +1,34 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_admin();
$action = $_POST['_action'] ?? '';
if ($action === 'add') {
$title = trim((string)($_POST['title'] ?? ''));
$text = trim((string)($_POST['text'] ?? ''));
$date = trim((string)($_POST['date'] ?? '')) ?: date('Y-m-d');
if ($title === '' || $text === '') {
header('Location: ../pages/admin.php?tab=news&err=empty');
exit;
}
$stmt = db()->prepare(
'INSERT INTO news (title, text, date, created_at) VALUES (?, ?, ?, ?)'
);
$stmt->execute([$title, $text, $date, date('c')]);
header('Location: ../pages/admin.php?tab=news&ok=added');
exit;
}
if ($action === 'delete') {
$id = (int)($_POST['news_id'] ?? 0);
if ($id > 0) {
db()->prepare('DELETE FROM news WHERE id = ?')->execute([$id]);
}
header('Location: ../pages/admin.php?tab=news&ok=deleted');
exit;
}
header('Location: ../pages/admin.php?tab=news');
exit;
@@ -0,0 +1,74 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_login();
$ajax = !empty($_POST['_ajax']);
function ajax_error(string $msg): never {
header('Content-Type: application/json');
echo json_encode(['ok' => false, 'error' => $msg]);
exit;
}
function ajax_ok(): never {
header('Content-Type: application/json');
echo json_encode(['ok' => true]);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
if ($ajax) ajax_error('Ungültige Anfrage.');
header('Location: ../pages/dienstplan.php'); exit;
}
$user_id = current_user()['id'];
$year = (int)($_POST['year'] ?? 0);
$month = (int)($_POST['month'] ?? 0);
if ($year < 2020 || $year > 2099 || $month < 1 || $month > 12) {
if ($ajax) ajax_error('Ungültiger Monat.');
header('Location: ../pages/dienstplan.php'); exit;
}
$back = '../pages/dienstplan.php?year=' . $year . '&month=' . $month;
$month_str = sprintf('%04d-%02d', $year, $month);
$schicht_ok = ['frueh', 'spaet', 'nacht'];
$submitted = $_POST['schicht'] ?? [];
if (!is_array($submitted)) {
header('Location: ' . $back . '&saved=1'); exit;
}
$now = date('c');
db()->beginTransaction();
try {
foreach ($submitted as $date => $schicht) {
if (!is_string($date) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) continue;
if (substr($date, 0, 7) !== $month_str) continue;
[$y, $m, $d] = array_map('intval', explode('-', $date));
if (!checkdate($m, $d, $y)) continue;
if (in_array($schicht, $schicht_ok, true)) {
db()->prepare(
'INSERT INTO dienstplan (user_id, date, schicht, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE schicht=VALUES(schicht), updated_at=VALUES(updated_at)'
)->execute([$user_id, $date, $schicht, $now, $now]);
} else {
db()->prepare(
'DELETE FROM dienstplan WHERE user_id = ? AND date = ?'
)->execute([$user_id, $date]);
}
}
db()->commit();
} catch (\PDOException $ex) {
if (db()->inTransaction()) db()->rollBack();
$msg = 'Datenbankfehler: ' . $ex->getMessage();
if ($ajax) ajax_error($msg);
header('Location: ' . $back . '&error=' . urlencode($msg));
exit;
}
if ($ajax) ajax_ok();
header('Location: ' . $back . '&saved=1');
exit;
+53
View File
@@ -0,0 +1,53 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_login();
$action = $_POST['action'] ?? '';
$username = current_username();
if ($action === 'profile') {
$email = trim($_POST['email'] ?? '');
$telefon = trim($_POST['telefon'] ?? '');
if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) {
header('Location: ../pages/settings.php?error=' . urlencode('Ungültige E-Mail-Adresse.'));
exit;
}
$stmt = db()->prepare('UPDATE users SET email = ?, telefon = ? WHERE username = ?');
$stmt->execute([$email, $telefon, $username]);
header('Location: ../pages/settings.php?saved=1');
exit;
}
if ($action === 'password') {
$old = $_POST['old_password'] ?? '';
$new = $_POST['new_password'] ?? '';
$repeat = $_POST['new_password_repeat'] ?? '';
$user = current_user();
if (!$user || !password_verify($old, $user['password_hash'])) {
header('Location: ../pages/settings.php?error=' . urlencode('Aktuelles Passwort ist falsch.'));
exit;
}
if (mb_strlen($new) < 8) {
header('Location: ../pages/settings.php?error=' . urlencode('Neues Passwort muss mindestens 8 Zeichen haben.'));
exit;
}
if ($new !== $repeat) {
header('Location: ../pages/settings.php?error=' . urlencode('Die neuen Passwörter stimmen nicht überein.'));
exit;
}
$hash = password_hash($new, PASSWORD_BCRYPT, ['cost' => 12]);
$stmt = db()->prepare('UPDATE users SET password_hash = ? WHERE username = ?');
$stmt->execute([$hash, $username]);
header('Location: ../pages/settings.php?pwchanged=1');
exit;
}
header('Location: ../pages/settings.php');
exit;
@@ -0,0 +1,73 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/mail.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/abwesenheitsantrag.php');
exit;
}
$back = '../pages/abwesenheitsantrag.php';
$von = trim($_POST['von'] ?? '');
$bis = trim($_POST['bis'] ?? '');
$grund = trim($_POST['grund'] ?? '');
$vertretung = trim($_POST['vertretung'] ?? '');
$nachricht = trim($_POST['nachricht'] ?? '');
if ($von === '' || $bis === '' || $grund === '') {
header('Location: ' . $back . '?error=' . urlencode('Von, Bis und Grund sind Pflichtfelder.'));
exit;
}
$allowed_grund = ['krankheit', 'arztbesuch', 'behoerdengang', 'sonderurlaub', 'elternzeit_pflegezeit', 'sonstiges'];
if (!in_array($grund, $allowed_grund, true)) {
header('Location: ' . $back . '?error=' . urlencode('Ungültiger Grund.'));
exit;
}
if (!strtotime($von) || !strtotime($bis)) {
header('Location: ' . $back . '?error=' . urlencode('Ungültiges Datum.'));
exit;
}
if (strtotime($von) > strtotime($bis)) {
header('Location: ' . $back . '?error=' . urlencode('Das Startdatum muss vor dem Enddatum liegen.'));
exit;
}
$user_id = current_user()['id'];
$now = date('c');
db()->prepare(
'INSERT INTO requests_abwesenheitsantrag (user_id, status, admin_note, created_at, updated_at, von, bis, grund, vertretung, nachricht)
VALUES (?, \'pending\', \'\', ?, ?, ?, ?, ?, ?, ?)'
)->execute([$user_id, $now, $now, $von, $bis, $grund, $vertretung, $nachricht]);
$grund_labels = [
'krankheit' => 'Krankheit',
'arztbesuch' => 'Arztbesuch',
'behoerdengang' => 'Behördengang',
'sonderurlaub' => 'Sonderurlaub',
'elternzeit_pflegezeit' => 'Elternzeit / Pflegezeit',
'sonstiges' => 'Sonstiges',
];
$config = require __DIR__ . '/../lib/config.php';
smtp_send(
$config,
$config['mail_sabrina'],
'Neuer Abwesenheitsantrag: ' . current_name(),
"Ein Abwesenheitsantrag wurde eingereicht.\n\n"
. 'Mitarbeiter: ' . current_name() . "\n"
. 'Von: ' . $von . "\n"
. 'Bis: ' . $bis . "\n"
. 'Grund: ' . ($grund_labels[$grund] ?? $grund) . "\n"
. 'Vertretung: ' . ($vertretung ?: '—') . "\n"
. 'Nachricht: ' . ($nachricht ?: '—') . "\n\n"
. "---\nGesendet über OMSORG Connect"
);
header('Location: ' . $back . '?success=1');
exit;
@@ -0,0 +1,63 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/mail.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/benefitsantrag.php');
exit;
}
$back = '../pages/benefitsantrag.php';
$benefit = trim($_POST['benefit'] ?? '');
$nachricht = trim($_POST['nachricht'] ?? '');
$allowed_benefits = ['yoga', 'autogenes_training', 'massage', 'tankgutschein', 'online_gutscheine'];
if (!in_array($benefit, $allowed_benefits, true)) {
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
header('Content-Type: application/json');
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Bitte einen Benefit auswählen.']);
exit;
}
header('Location: ' . $back . '?error=' . urlencode('Bitte einen Benefit auswählen.'));
exit;
}
$user_id = current_user()['id'];
$now = date('c');
db()->prepare(
'INSERT INTO requests_benefitsantrag (user_id, status, admin_note, created_at, updated_at, benefit, nachricht)
VALUES (?, \'pending\', \'\', ?, ?, ?, ?)'
)->execute([$user_id, $now, $now, $benefit, $nachricht]);
$benefit_labels = [
'yoga' => 'Yoga',
'autogenes_training' => 'Autogenes Training',
'massage' => 'Massage',
'tankgutschein' => 'Tankgutschein',
'online_gutscheine' => 'Online-Gutscheine',
];
$config = require __DIR__ . '/../lib/config.php';
smtp_send(
$config,
$config['mail_info'],
'Neuer Benefitsantrag: ' . current_name(),
"Ein Benefitsantrag wurde eingereicht.\n\n"
. 'Mitarbeiter: ' . current_name() . "\n"
. 'Benefit: ' . ($benefit_labels[$benefit] ?? $benefit) . "\n"
. 'Nachricht: ' . ($nachricht ?: '—') . "\n\n"
. "---\nGesendet über OMSORG Connect"
);
if (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
header('Content-Type: application/json');
echo json_encode(['ok' => true, 'benefit' => $benefit]);
exit;
}
header('Location: ' . $back . '?success=1');
exit;
@@ -0,0 +1,54 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/einsatzbewertung.php');
exit;
}
$back = '../pages/einsatzbewertung.php';
$einsatzort = trim($_POST['einsatzort'] ?? '');
$von = trim($_POST['von'] ?? '');
$bis = trim($_POST['bis'] ?? '');
$bewertung = (int)($_POST['bewertung'] ?? 0);
$wieder = isset($_POST['wieder']) ? 1 : 0;
$feedback = trim($_POST['feedback'] ?? '');
if ($einsatzort === '') {
header('Location: ' . $back . '?error=' . urlencode('Bitte den Einsatzort angeben.'));
exit;
}
if ($von === '' || $bis === '') {
header('Location: ' . $back . '?error=' . urlencode('Von und Bis sind Pflichtfelder.'));
exit;
}
if (!strtotime($von) || !strtotime($bis)) {
header('Location: ' . $back . '?error=' . urlencode('Ungültiges Datum.'));
exit;
}
if (strtotime($von) > strtotime($bis)) {
header('Location: ' . $back . '?error=' . urlencode('Das Startdatum muss vor dem Enddatum liegen.'));
exit;
}
if ($bewertung < 1 || $bewertung > 5) {
header('Location: ' . $back . '?error=' . urlencode('Bitte eine Bewertung von 1 bis 5 Sternen auswählen.'));
exit;
}
$user_id = current_user()['id'];
$now = date('c');
db()->prepare(
'INSERT INTO einsatzbewertungen (user_id, einsatzort, von, bis, bewertung, wieder, feedback, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
)->execute([$user_id, $einsatzort, $von, $bis, $bewertung, $wieder, $feedback, $now]);
header('Location: ' . $back . '?success=1');
exit;
@@ -0,0 +1,73 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/upload.php';
require_once __DIR__ . '/../lib/mail.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/fortbildungsantrag.php');
exit;
}
$back = '../pages/fortbildungsantrag.php';
$anliegen = trim($_POST['anliegen'] ?? '');
$thema = trim($_POST['thema'] ?? '');
$nachricht = trim($_POST['nachricht'] ?? '');
$allowed_anliegen = ['wbl_pdl', 'fortbildung', 'pflichtschulung'];
if (!in_array($anliegen, $allowed_anliegen, true)) {
redirect_error($back, 'Bitte ein Anliegen auswählen.');
}
if ($thema === '') {
redirect_error($back, 'Bitte ein Thema angeben.');
}
$up = handle_upload($_FILES['datei'] ?? [], [
'allowed' => UPLOAD_TYPES_DOCS,
'dir' => dirname(__DIR__) . '/uploads/',
'prefix' => 'fortbildung',
'required' => false,
]);
if (!$up['ok']) {
redirect_error($back, $up['error']);
}
$filename = $up['filename'] ?? '';
$original_name = $up['original_name'] ?? '';
$mime = $up['mime'] ?? null;
$user_id = current_user()['id'];
$now = date('c');
db()->prepare(
'INSERT INTO requests_fortbildungsantrag
(user_id, status, admin_note, anliegen, thema, nachricht, filename, original_name, created_at, updated_at)
VALUES (?, \'pending\', \'\', ?, ?, ?, ?, ?, ?, ?)'
)->execute([$user_id, $anliegen, $thema, $nachricht, $filename, $original_name, $now, $now]);
$anliegen_labels = [
'wbl_pdl' => 'WBL / PDL',
'fortbildung' => 'Fortbildung',
'pflichtschulung' => 'Pflichtschulung',
];
$config = require __DIR__ . '/../lib/config.php';
$attach = ($filename !== '')
? ['path' => dirname(__DIR__) . '/uploads/' . $filename, 'name' => $original_name, 'mime' => $mime]
: [];
smtp_send(
$config,
$config['mail_info'],
'Neuer Fortbildungsantrag: ' . current_name(),
"Ein Fortbildungsantrag wurde eingereicht.\n\n"
. 'Mitarbeiter: ' . current_name() . "\n"
. 'Anliegen: ' . ($anliegen_labels[$anliegen] ?? $anliegen) . "\n"
. 'Thema: ' . $thema . "\n"
. 'Nachricht: ' . ($nachricht ?: '—') . "\n\n"
. "---\nGesendet über OMSORG Connect",
$attach
);
redirect_ok($back);
@@ -0,0 +1,63 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/upload.php';
require_once __DIR__ . '/../lib/mail.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/stundennachweis.php');
exit;
}
$back = '../pages/stundennachweis.php';
$monat_m = trim($_POST['monat_m'] ?? '');
$monat_j = trim($_POST['monat_j'] ?? '');
if (!preg_match('/^\d{2}$/', $monat_m) || !preg_match('/^\d{4}$/', $monat_j)
|| (int)$monat_m < 1 || (int)$monat_m > 12
|| (int)$monat_j < 2020 || (int)$monat_j > 2099) {
redirect_error($back, 'Bitte einen gültigen Monat und ein gültiges Jahr auswählen.');
}
$monat = $monat_j . '-' . $monat_m;
$dir = dirname(__DIR__) . '/uploads/';
$up = handle_upload($_FILES['datei'] ?? [], [
'allowed' => UPLOAD_TYPES_PDF_IMG,
'dir' => $dir,
'prefix' => 'stundennachweis',
'messages' => [
'missing' => 'Bitte eine Datei hochladen.',
'ext' => 'Dateityp nicht erlaubt. Erlaubt: PDF, JPG, PNG.',
],
]);
if (!$up['ok']) {
redirect_error($back, $up['error']);
}
$safe_name = $up['filename'];
$original_name = $up['original_name'];
$mime = $up['mime'];
$user_id = current_user()['id'];
$now = date('c');
db()->prepare(
'INSERT INTO requests_stundennachweis
(user_id, status, admin_note, monat, filename, original_name, created_at, updated_at)
VALUES (?, \'pending\', \'\', ?, ?, ?, ?, ?)'
)->execute([$user_id, $monat, $safe_name, $original_name, $now, $now]);
$config = require __DIR__ . '/../lib/config.php';
smtp_send(
$config,
$config['mail_sabrina'],
'Neuer Stundennachweis: ' . current_name(),
"Ein Stundennachweis wurde hochgeladen.\n\n"
. 'Mitarbeiter: ' . current_name() . "\n"
. 'Monat: ' . $monat . "\n"
. 'Datei: ' . $original_name . "\n\n"
. "---\nGesendet über OMSORG Connect",
['path' => $dir . $safe_name, 'name' => $original_name, 'mime' => $mime]
);
redirect_ok($back);
@@ -0,0 +1,56 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/mail.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/urlaubsantrag.php');
exit;
}
$back = '../pages/urlaubsantrag.php';
$von = trim($_POST['von'] ?? '');
$bis = trim($_POST['bis'] ?? '');
$vertretung = trim($_POST['vertretung'] ?? '');
$nachricht = trim($_POST['nachricht'] ?? '');
if ($von === '' || $bis === '') {
header('Location: ' . $back . '?error=' . urlencode('Von und Bis sind Pflichtfelder.'));
exit;
}
if (!strtotime($von) || !strtotime($bis)) {
header('Location: ' . $back . '?error=' . urlencode('Ungültiges Datum.'));
exit;
}
if (strtotime($von) > strtotime($bis)) {
header('Location: ' . $back . '?error=' . urlencode('Das Startdatum muss vor dem Enddatum liegen.'));
exit;
}
$user_id = current_user()['id'];
$now = date('c');
db()->prepare(
'INSERT INTO requests_urlaubsantrag (user_id, status, admin_note, created_at, updated_at, von, bis, vertretung, nachricht)
VALUES (?, \'pending\', \'\', ?, ?, ?, ?, ?, ?)'
)->execute([$user_id, $now, $now, $von, $bis, $vertretung, $nachricht]);
$config = require __DIR__ . '/../lib/config.php';
smtp_send(
$config,
$config['mail_sabrina'],
'Neuer Urlaubsantrag: ' . current_name(),
"Ein Urlaubsantrag wurde eingereicht.\n\n"
. 'Mitarbeiter: ' . current_name() . "\n"
. 'Von: ' . $von . "\n"
. 'Bis: ' . $bis . "\n"
. 'Vertretung: ' . ($vertretung ?: '—') . "\n"
. 'Nachricht: ' . ($nachricht ?: '—') . "\n\n"
. "---\nGesendet über OMSORG Connect"
);
header('Location: ' . $back . '?success=1');
exit;
+62
View File
@@ -0,0 +1,62 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/mail.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/werben.php');
exit;
}
$back = '../pages/werben.php';
$name = trim($_POST['name'] ?? '');
$email = trim($_POST['email'] ?? '');
$qualifikation = trim($_POST['qualifikation'] ?? '');
$nachricht = trim($_POST['nachricht'] ?? '');
if ($name === '' || $email === '' || $qualifikation === '') {
header('Location: ' . $back . '?error=' . urlencode('Name, E-Mail und Qualifikation sind Pflichtfelder.'));
exit;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
header('Location: ' . $back . '?error=' . urlencode('Bitte eine gültige E-Mail-Adresse eingeben.'));
exit;
}
$allowed_qual = ['1jahr', '3jahr'];
if (!in_array($qualifikation, $allowed_qual, true)) {
header('Location: ' . $back . '?error=' . urlencode('Ungültige Qualifikation.'));
exit;
}
$user_id = current_user()['id'];
$now = date('c');
db()->prepare(
'INSERT INTO requests_werben (user_id, status, admin_note, created_at, updated_at, name, email, qualifikation, nachricht)
VALUES (?, \'pending\', \'\', ?, ?, ?, ?, ?, ?)'
)->execute([$user_id, $now, $now, $name, $email, $qualifikation, $nachricht]);
$qual_labels = [
'1jahr' => '1 Jährige Examinierte Pflegekraft',
'3jahr' => '3 Jährige Examinierte Pflegekraft',
];
$config = require __DIR__ . '/../lib/config.php';
smtp_send(
$config,
$config['mail_info'],
'Mitarbeiter werben: ' . current_name(),
"Ein Mitarbeiter wurde empfohlen.\n\n"
. 'Empfohlen von: ' . current_name() . "\n"
. 'Name der empfohlenen Person: ' . $name . "\n"
. 'E-Mail: ' . $email . "\n"
. 'Art der Stelle: ' . ($qual_labels[$qualifikation] ?? $qualifikation) . "\n"
. 'Nachricht: ' . ($nachricht ?: '—') . "\n\n"
. "---\nGesendet über OMSORG Connect"
);
header('Location: ' . $back . '?success=1');
exit;
+58
View File
@@ -0,0 +1,58 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/upload.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/settings.php');
exit;
}
$back = '../pages/settings.php';
$username = current_username();
$user = current_user();
$dir = dirname(__DIR__) . '/assets/avatars/';
// Remove avatar
if (!empty($_POST['remove_avatar'])) {
if (!empty($user['avatar'])) {
$old = $dir . basename($user['avatar']);
if (is_file($old)) {
unlink($old);
}
}
db()->prepare('UPDATE users SET avatar = NULL WHERE username = ?')->execute([$username]);
redirect_ok($back, 'avatar_saved');
}
$up = handle_upload($_FILES['avatar'] ?? [], [
'allowed' => UPLOAD_TYPES_IMAGES,
'dir' => $dir,
'naming' => 'plain',
'max_bytes' => 3 * 1024 * 1024,
'messages' => [
'missing' => 'Bitte ein Bild auswählen.',
'upload' => 'Fehler beim Hochladen des Bildes.',
'ext' => 'Nur Bilder erlaubt (JPG, PNG, GIF, WebP).',
'mime' => 'Nur Bilder erlaubt (JPG, PNG, GIF, WebP).',
'size' => 'Bild ist zu groß (max. 3 MB).',
'move' => 'Fehler beim Speichern des Bildes.',
],
]);
if (!$up['ok']) {
redirect_error($back, $up['error']);
}
$safe_name = $up['filename'];
// Delete old avatar after successful upload
if (!empty($user['avatar'])) {
$old = $dir . basename($user['avatar']);
if (is_file($old)) {
unlink($old);
}
}
db()->prepare('UPDATE users SET avatar = ? WHERE username = ?')->execute([$safe_name, $username]);
redirect_ok($back, 'avatar_saved');
@@ -0,0 +1,42 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/upload.php';
require_login();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('Location: ../pages/dokumentenarchiv.php');
exit;
}
$back = '../pages/dokumentenarchiv.php';
$allowed_kategorien = ['Fortbildungsnachweis', 'Zeugnis', 'Bescheinigung', 'Sonstiges'];
$kategorie = trim($_POST['kategorie'] ?? '');
$beschreibung = trim($_POST['beschreibung'] ?? '');
if (!in_array($kategorie, $allowed_kategorien, true)) {
redirect_error($back, 'Bitte eine Kategorie auswählen.');
}
$filesize = $_FILES['datei']['size'] ?? 0;
$up = handle_upload($_FILES['datei'] ?? [], [
'allowed' => UPLOAD_TYPES_DOCS,
'dir' => dirname(__DIR__) . '/uploads/',
'prefix' => 'dokument',
]);
if (!$up['ok']) {
redirect_error($back, $up['error']);
}
$safe_name = $up['filename'];
$original_name = $up['original_name'];
$user_id = current_user()['id'];
$now = date('c');
db()->prepare(
'INSERT INTO dokumente (user_id, kategorie, beschreibung, filename, original_name, filesize, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)'
)->execute([$user_id, $kategorie, $beschreibung, $safe_name, $original_name, $filesize, $now]);
redirect_ok($back, 'ok');
+381
View File
@@ -0,0 +1,381 @@
/* ── Fonts ──────────────────────────────────────────────────────────── */
@font-face{font-family:'Visby';src:url('font/visby/VisbyThin.otf') format('opentype');font-weight:100;font-style:normal;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyThin Italic.otf') format('opentype');font-weight:100;font-style:italic;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyLight.otf') format('opentype');font-weight:300;font-style:normal;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyLight Italic.otf') format('opentype');font-weight:300;font-style:italic;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyRegular.otf') format('opentype');font-weight:400;font-style:normal;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyItalic.otf') format('opentype');font-weight:400;font-style:italic;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyMedium.otf') format('opentype');font-weight:500;font-style:normal;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyMedium Italic.otf') format('opentype');font-weight:500;font-style:italic;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbySemibold.otf') format('opentype');font-weight:600;font-style:normal;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbySemibold Italic.otf') format('opentype');font-weight:600;font-style:italic;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyBold.otf') format('opentype');font-weight:700;font-style:normal;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyBold Italic.otf') format('opentype');font-weight:700;font-style:italic;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyExtrabold.otf') format('opentype');font-weight:800;font-style:normal;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyExtrabold Italic.otf') format('opentype');font-weight:800;font-style:italic;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyHeavy.otf') format('opentype');font-weight:900;font-style:normal;font-display:swap}
@font-face{font-family:'Visby';src:url('font/visby/VisbyHeavy Italic.otf') format('opentype');font-weight:900;font-style:italic;font-display:swap}
@font-face{font-family:'KindelSerif';src:url('font/kindelserif/KindelSerif_PersonalUse.otf') format('opentype');font-weight:400;font-style:normal;font-display:swap}
/* ── Design tokens (identical to main app) ─────────────────────────── */
:root{
--cyan:#10d7e6;--cyan2:#36f2ff;--dark:#031522;
--glass:rgba(0,35,51,.74);--line:rgba(170,240,250,.42);
--text:#f7ffff;--muted:#d7f6f8;
--sidebar-w:240px;
}
/* ── Reset & base ───────────────────────────────────────────────────── */
*{box-sizing:border-box}
html{scroll-behavior:smooth;background:#031522;overflow-x:hidden}
body{
margin:0;min-height:100vh;color:var(--text);
font-family:'Visby',Arial,Helvetica,sans-serif;line-height:1.45;
background:#031522 url('assets/omsorg-bg-full.png') center top/cover fixed no-repeat;
overflow-x:hidden;
}
body::before{
content:"";position:fixed;inset:0;z-index:-2;
background:linear-gradient(180deg,rgba(0,9,23,.08),rgba(0,9,23,.28) 58%,rgba(0,9,23,.52));
pointer-events:none;
}
body::after{
content:"";position:fixed;inset:0;z-index:-1;opacity:.35;
background-image:radial-gradient(circle,rgba(0,216,230,.75) 0 2px,transparent 3px);
background-size:24px 24px;
mask-image:radial-gradient(circle at 0% 34%,#000 0 0,transparent 18%),
radial-gradient(circle at 100% 88%,#000 0 0,transparent 17%);
pointer-events:none;
}
a{color:inherit;text-decoration:none}
img{display:block;max-width:100%}
/* ── Glass card ─────────────────────────────────────────────────────── */
.glass{
border:1px solid var(--line);border-radius:22px;
background:linear-gradient(145deg,rgba(0,35,51,.78),rgba(8,105,122,.28));
box-shadow:0 22px 70px rgba(0,0,0,.28);
backdrop-filter:blur(16px);
}
/* ── Buttons ────────────────────────────────────────────────────────── */
.btn{
display:inline-flex;align-items:center;justify-content:center;
gap:10px;border:1px solid rgba(255,255,255,.24);border-radius:13px;
background:linear-gradient(135deg,#25e2e9,#079aaa);
color:#fff;font-weight:900;padding:14px 18px;cursor:pointer;
box-shadow:0 14px 38px rgba(0,215,230,.18);font:inherit;
}
/* ── Form elements ──────────────────────────────────────────────────── */
form{display:grid;gap:14px}
label{display:grid;gap:7px;font-weight:800;color:#fff}
input,select,textarea{
width:100%;border:1px solid rgba(170,240,250,.36);border-radius:12px;
padding:14px;background:rgba(2,11,22,.54);color:#fff;font:inherit;
}
/* ── Alerts ─────────────────────────────────────────────────────────── */
.error{
padding:12px 14px;border:1px solid rgba(255,130,130,.45);
border-radius:12px;background:rgba(255,0,0,.13);color:#fff;
}
.success{
padding:12px 14px;border:1px solid rgba(54,242,255,.45);
border-radius:12px;background:rgba(16,215,230,.1);color:#fff;
}
.hint{color:var(--muted);font-size:.9rem}
.settings-section{margin-top:36px;padding-top:28px;border-top:1px solid rgba(170,240,250,.18)}
.settings-avatar-preview{width:80px;height:80px;border-radius:50%;object-fit:cover;border:2px solid rgba(170,240,250,.3)}
.settings-avatar-placeholder{width:80px;height:80px;border-radius:50%;display:grid;place-items:center;background:linear-gradient(135deg,#20e1ea,#078fa3);font-weight:900;font-size:2rem;color:#fff;flex-shrink:0}
/* ── Login page ─────────────────────────────────────────────────────── */
.login-page{display:grid;place-items:center;min-height:100vh;padding:26px}
.login-card{width:min(100%,450px);padding:30px}
.nav-toggle{display:none}
.nav-backdrop{display:none;position:fixed;inset:0;z-index:39;background:rgba(0,0,0,.55)}
/* ── Dashboard layout ───────────────────────────────────────────────── */
.dashboard-layout{
display:grid;
grid-template-columns:1fr;
padding-left:var(--sidebar-w);
min-height:100vh;
}
/* ── Sidebar ────────────────────────────────────────────────────────── */
.sidebar{
position:fixed;top:0;left:0;width:var(--sidebar-w);height:100vh;overflow-y:auto;
display:flex;flex-direction:column;gap:0;
border-radius:0;border-top:none;border-bottom:none;border-left:none;
padding:0;
}
.sidebar-brand{
padding:22px 20px 16px;
border-bottom:1px solid rgba(170,240,250,.18);
}
.sidebar-brand img{width:160px;height:auto}
.sidebar-app-name{
display:block;margin-top:6px;
color:var(--cyan2);text-transform:uppercase;
letter-spacing:.16em;font-size:.74rem;font-weight:900;
}
.sidebar-user{
display:grid;grid-template-columns:40px 1fr;gap:12px;
align-items:center;padding:16px 20px;
border-bottom:1px solid rgba(170,240,250,.12);
}
.sidebar-avatar{
width:40px;height:40px;display:grid;place-items:center;
border-radius:50%;background:linear-gradient(135deg,#20e1ea,#078fa3);
font-weight:900;font-size:1.1rem;color:#fff;
}
.sidebar-avatar img{width:100%;height:100%;border-radius:50%;object-fit:cover}
.sidebar-user b{display:block;font-size:.92rem}
.sidebar-user small{display:block;color:var(--muted);font-size:.78rem;margin-top:2px}
.sidebar-nav{padding:16px 12px 0;flex:1}
.sidebar-nav-label{
margin:0 8px 8px;color:var(--cyan2);
text-transform:uppercase;letter-spacing:.12em;font-size:.7rem;font-weight:900;
}
.sidebar-nav a{
display:flex;align-items:center;gap:10px;
border-radius:13px;padding:11px 12px;
color:#ebfdff;font-weight:800;font-size:.92rem;
border:1px solid transparent;margin-bottom:4px;
transition:background .15s,border-color .15s;
}
.sidebar-nav a:hover{
background:rgba(255,255,255,.075);
border-color:rgba(170,240,250,.18);
}
.sidebar-nav a.active{
background:linear-gradient(135deg,rgba(32,225,234,.22),rgba(7,143,163,.18));
border-color:rgba(54,242,255,.45);
color:#fff;
}
.nav-icon{font-size:1rem;opacity:.85}
.sidebar-footer{
padding:16px 20px;
border-top:1px solid rgba(170,240,250,.12);
margin-top:auto;
}
.sidebar-logout{
display:block;text-align:center;
border:1px solid rgba(170,240,250,.28);
border-radius:13px;padding:11px;
background:rgba(255,255,255,.06);
font-weight:900;font-size:.88rem;
transition:background .15s;
}
.sidebar-logout:hover{background:rgba(255,255,255,.12)}
/* ── Main content ───────────────────────────────────────────────────── */
.main-content{
overflow-y:auto;padding:48px 40px;min-height:100vh;
}
.main-inner{max-width:860px}
.main-inner:has(.page-split){max-width:none}
.main-inner:has(.dp-grid){max-width:none}
.main-title{
font-family:'KindelSerif',Georgia,serif;font-size:2.8rem;
line-height:1;margin:0 0 12px;color:#fff;
}
.main-title span{color:var(--cyan)}
.main-sub{color:var(--muted);font-size:1.05rem;margin:0}
/* ── Requests ───────────────────────────────────────────────────────── */
.badge{display:inline-block;padding:3px 10px;border-radius:8px;font-size:.82rem;font-weight:800}
.badge-pending {background:rgba(255,200,0,.18);border:1px solid rgba(255,200,0,.6);color:#ffe566}
.badge-accepted{background:rgba(16,215,230,.15);border:1px solid rgba(54,242,255,.55);color:var(--cyan2)}
.badge-rejected{background:rgba(255,60,60,.15);border:1px solid rgba(255,100,100,.55);color:#ff8080}
.table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
.monat-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.requests-table{width:100%;border-collapse:collapse;margin-top:20px}
.requests-table th{
text-align:left;padding:10px 12px;color:var(--cyan2);
font-size:.78rem;text-transform:uppercase;letter-spacing:.08em;
border-bottom:1px solid rgba(170,240,250,.2);font-weight:900;
}
.requests-table td{padding:12px;border-bottom:1px solid rgba(170,240,250,.08);vertical-align:middle}
.requests-table tr:hover td{background:rgba(255,255,255,.03)}
.type-tabs{display:flex;gap:8px;flex-wrap:wrap;margin:24px 0 0}
.type-tab{
padding:9px 18px;border-radius:11px;border:1px solid rgba(170,240,250,.22);
background:rgba(255,255,255,.05);color:#ebfdff;font-weight:800;font-size:.9rem;
cursor:pointer;transition:background .15s,border-color .15s;
}
.type-tab:hover{background:rgba(255,255,255,.1);border-color:rgba(170,240,250,.4)}
.type-tab.active{
background:linear-gradient(135deg,rgba(32,225,234,.22),rgba(7,143,163,.18));
border-color:rgba(54,242,255,.45);color:#fff;
}
.status-tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:24px}
.status-tab{
padding:8px 16px;border-radius:10px;border:1px solid rgba(170,240,250,.2);
background:rgba(255,255,255,.05);color:#ebfdff;font-weight:800;font-size:.88rem;
transition:background .15s,border-color .15s;
}
.status-tab:hover{background:rgba(255,255,255,.1)}
.status-tab.active{
background:rgba(16,215,230,.18);border-color:rgba(54,242,255,.55);color:#fff;
}
.detail-card{padding:28px;margin-top:24px}
.detail-field{margin-bottom:16px}
.detail-field-label{font-size:.8rem;text-transform:uppercase;letter-spacing:.1em;color:var(--cyan2);font-weight:900;margin-bottom:4px}
.detail-field-value{color:#fff;font-size:.97rem}
.btn-danger{background:linear-gradient(135deg,#e94560,#a01a2e)}
.btn-ghost{background:transparent;border:1px solid rgba(170,240,250,.35);color:#cff;box-shadow:none}
/* ── Split layout (list left, form right) ───────────────────────────── */
.page-split{
display:grid;
grid-template-columns:1fr 400px;
gap:32px;
align-items:start;
margin-top:28px;
}
.page-split-list h2{
font-family:'KindelSerif',Georgia,serif;font-size:1.3rem;margin:0 0 16px;color:#fff;
}
.page-split-form h2{
font-family:'KindelSerif',Georgia,serif;font-size:1.3rem;margin:0 0 20px;
}
/* ── Responsive ─────────────────────────────────────────────────────── */
@media(max-width:860px){
/* Body-Offset für fixierten Header */
body{padding-top:54px}
.dashboard-layout{grid-template-columns:1fr;padding-left:0;width:100%;max-width:100%;overflow-x:hidden}
/* Sidebar = fixierter kompakter Header, 54px hoch */
.sidebar{
position:fixed;top:0;left:0;right:0;z-index:40;
height:54px;overflow:hidden;
flex-direction:column;gap:0;
border-radius:0;border-right:none;
border-bottom:1px solid var(--line);
}
/* Brand-Zeile: Logo + Hamburger-Button */
.sidebar-brand{
display:flex;align-items:center;
height:54px;flex-shrink:0;
padding:0 14px;
border-bottom:none;
}
.sidebar-brand img{width:80px}
.sidebar-app-name{display:none}
/* Hamburger-Button */
.nav-toggle{
display:flex;align-items:center;justify-content:center;
margin-left:auto;flex-shrink:0;
width:44px;height:44px;
border:1px solid var(--line);border-radius:12px;
background:rgba(255,255,255,.08);
color:#fff;font-size:1.3rem;cursor:pointer;
transition:background .15s;
}
.nav-toggle:hover{background:rgba(255,255,255,.15)}
/* Nutzerblock + Nav + Footer: standardmäßig versteckt */
.sidebar-user{display:none}
.sidebar-nav{display:none;padding:12px 12px 0;flex-direction:column}
.sidebar-nav-label{display:block}
.sidebar-footer{display:none}
.nav-label{display:inline}
/* Geöffnet: Vollbild-Overlay */
body.nav-open .sidebar{height:100vh;overflow-y:auto}
body.nav-open .sidebar-user{display:grid}
body.nav-open .sidebar-nav{display:flex}
body.nav-open .sidebar-footer{display:block}
body.nav-open .nav-backdrop{display:block}
.main-content{padding:20px 16px;min-height:auto;width:100%;max-width:100%;overflow-x:hidden}
.main-inner{width:100%;max-width:100%}
.main-title{font-size:1.9rem}
.page-split{grid-template-columns:1fr}
.page-split-list,.page-split-form{min-width:0;width:100%}
.page-split-form{order:-1}
.requests-table,.users-table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}
}
/* ── Downloads page ─────────────────────────────────────────────────── */
.downloads-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:20px;margin-top:28px}
.download-card{padding:24px;display:flex;flex-direction:column;gap:14px}
.download-card-title{font-family:'KindelSerif',Georgia,serif;font-size:1.15rem;margin:0;color:#fff}
.download-card-desc{color:var(--muted);font-size:.92rem;margin:0;flex:1}
.download-btn{align-self:flex-start;padding:10px 18px;font-size:.9rem}
/* ── Downloads admin list ─────────────────────────────────────────────── */
.dl-sort-item{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:12px;border:1px solid rgba(170,240,250,.14);background:rgba(0,35,51,.44);margin-bottom:8px;cursor:grab;transition:background .15s}
.dl-sort-item:active{cursor:grabbing}
.dl-sort-item:hover{background:rgba(0,35,51,.7)}
.dl-sort-item.drag-over{border-color:rgba(54,242,255,.55);background:rgba(16,215,230,.08)}
.dl-drag-handle{color:var(--muted);font-size:1.2rem;user-select:none;flex-shrink:0;line-height:1}
.dl-item-info{flex:1;min-width:0}
.dl-item-title{font-weight:800;font-size:.93rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.dl-item-filename{color:var(--muted);font-size:.8rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px}
/* ── Dienstplan calendar ──────────────────────────────────────────────── */
.dp-ort{margin:0 0 20px;font-size:.95rem;color:var(--muted)}
.dp-ort strong{color:#ebfdff}
.dp-nav{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;flex-wrap:wrap;gap:12px}
.dp-nav-title{font-family:'KindelSerif',Georgia,serif;font-size:1.6rem;color:#fff;margin:0}
.dp-nav-btn{padding:8px 18px;border-radius:10px;border:1px solid rgba(170,240,250,.3);background:rgba(255,255,255,.06);color:#ebfdff;font-weight:800;font-size:.88rem;cursor:pointer;text-decoration:none;transition:background .15s}
.dp-nav-btn:hover{background:rgba(255,255,255,.12)}
.dp-save-bar{display:flex;justify-content:flex-end;align-items:center;gap:16px;margin-bottom:20px}
.dp-grid-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
.dp-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px;min-width:480px}
.dp-weekday{text-align:center;padding:8px 4px;color:var(--cyan2);font-size:.72rem;text-transform:uppercase;letter-spacing:.1em;font-weight:900}
.dp-cell{min-height:80px;border-radius:12px;padding:8px;border:1px solid rgba(170,240,250,.14);background:rgba(0,35,51,.44);display:flex;flex-direction:column;gap:6px}
.dp-cell-empty{background:transparent;border-color:transparent;min-height:0}
.dp-cell-today{border-color:rgba(54,242,255,.55);background:rgba(16,215,230,.08)}
.dp-cell-frueh{background:rgba(54,242,100,.1);border-color:rgba(54,242,100,.38)}
.dp-cell-spaet{background:rgba(255,180,50,.1);border-color:rgba(255,180,50,.38)}
.dp-cell-nacht{background:rgba(130,80,255,.14);border-color:rgba(130,80,255,.42)}
.dp-day-num{font-size:.78rem;font-weight:900;color:var(--muted);line-height:1}
.dp-cell-today .dp-day-num{color:var(--cyan2)}
.dp-select{width:100%;border:1px solid rgba(170,240,250,.28);border-radius:8px;padding:5px 6px;background:rgba(2,11,22,.62);color:#fff;font:inherit;font-size:.8rem;cursor:pointer}
.dp-badge{display:block;padding:3px 8px;border-radius:7px;font-size:.75rem;font-weight:900;text-align:center;word-break:break-word}
.dp-badge-frueh{background:rgba(54,242,100,.15);border:1px solid rgba(54,242,100,.5);color:#6eff9a}
.dp-badge-spaet{background:rgba(255,180,50,.15);border:1px solid rgba(255,180,50,.5);color:#ffc84a}
.dp-badge-nacht{background:rgba(130,80,255,.18);border:1px solid rgba(130,80,255,.5);color:#b89cff}
.dp-overlay-urlaub{background:rgba(16,215,230,.18);border:1px solid rgba(54,242,255,.5);color:var(--cyan2)}
.dp-overlay-abwesenheit{background:rgba(255,200,0,.15);border:1px solid rgba(255,200,0,.5);color:#ffe566}
.dp-user-select-wrap{margin-bottom:28px}
.dp-user-select-wrap select{max-width:320px;border-radius:12px;padding:12px;font-weight:800}
.dp-free{font-size:.75rem;color:rgba(215,246,248,.35)}
@media(max-width:680px){
.dp-grid{gap:3px}
.dp-cell{min-height:60px;padding:5px}
.dp-weekday{font-size:.62rem}
.dp-day-num{font-size:.7rem}
.dp-select{font-size:.72rem;padding:4px}
}
@media(max-width:480px){
.monat-grid{grid-template-columns:1fr}
.main-content{padding:18px 14px}
.main-title{font-size:1.6rem}
.btn-sm{padding:11px 13px}
.detail-card{padding:16px}
.modal-box{padding:20px 16px}
.admin-tabs{flex-wrap:wrap;gap:6px;margin-bottom:20px}
.admin-tab{padding:8px 14px;font-size:.82rem}
.type-tab,.status-tab{padding:7px 12px;font-size:.82rem}
.dp-cell{min-height:44px;padding:4px 3px}
.dp-weekday{font-size:.58rem;padding:6px 2px}
.dp-badge{font-size:.65rem;padding:2px 4px}
.dp-select{font-size:.68rem;padding:3px 2px}
.requests-table,.users-table{display:block;overflow-x:auto;-webkit-overflow-scrolling:touch}
.login-card{padding:22px 18px}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

@@ -0,0 +1,38 @@
1001Fonts Free For Personal Use License (FFP)
Preamble
In this license, 'KindelSerif' refers to the given .zip file, which may contain one or numerous fonts. These fonts can be of any type (.ttf, .otf, ...) and together they form a 'font family' or in short a 'typeface'.
1. Copyright
KindelSerif is the intellectual property of its respective author, provided it is original, and is protected by copyright laws in many parts of the world.
2. Personal Use
KindelSerif may be downloaded and used free of charge for personal use, as long as the usage is not racist or illegal. Personal use refers to all usage that does not generate financial income in a business manner, for instance:
- personal scrapbooking for yourself
- recreational websites and blogs for friends and family
- prints such as flyers, posters, t-shirts for churches, charities, and non-profit organizations
3. Commercial Use
Commercial use is not allowed without prior written permission from the respective author. Please contact the author to ask for commercial licensing. Commercial use refers to usage in a business environment, including:
- business cards, logos, advertising, websites, mobile apps for companies
- t-shirts, books, apparel that will be sold for money
- flyers, posters for events that charge admission
- freelance graphic design work
- anything that will generate direct or indirect income
4. Modification
KindelSerif may not be modified, altered, adapted or built upon without written permission by its respective author. This pertains all files within the downloadable font zip-file.
5. Conversion
KindelSerif may be converted to other formats such as WOFF, SVG or EOT webfonts, as long as the font is not modified in any other way, such as changing names or altering individual glyphs.
6. Distribution
While KindelSerif may freely be copied and passed along to other individuals for private use as its original downloadable zip-file, it may not be sold or published without written permission by its respective author.
7. Embedding
KindelSerif may be embedded into an application such as a web- or mobile app, as long as the application is of personal use and does not distribute KindelSerif, such as offering it as a download.
8. Disclaimer
KindelSerif is offered 'as is' without any warranty. 1001fonts.com and the respective author of KindelSerif shall not be liable for any damage derived from using this typeface. By using KindelSerif you agree to the terms of this license.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+94
View File
@@ -0,0 +1,94 @@
<?php
require_once __DIR__ . '/lib/auth.php';
if (is_logged_in()) {
header('Location: pages/dashboard.php');
exit;
}
$error = '';
$locked = false;
$MAX_FAILS = 5;
$LOCKOUT_SEC = 600;
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$fails = login_recent_fails($ip, $LOCKOUT_SEC);
if ($fails >= $MAX_FAILS) {
$locked = true;
$error = 'Zu viele Fehlversuche. Bitte warte ca. 10 Minuten.';
}
if (!$locked && $_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
if ($username !== '' && $password !== '') {
$stmt = db()->prepare(
'SELECT * FROM users WHERE LOWER(username) = LOWER(?) AND active = 1'
);
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
login_clear_fails($ip);
session_regenerate_id(true);
$_SESSION['felix_logged_in'] = true;
$_SESSION['felix_username'] = $user['username'];
$_SESSION['felix_name'] = $user['name'];
$_SESSION['felix_role'] = $user['role'];
header('Location: pages/dashboard.php');
exit;
}
}
login_record_fail($ip, $username ?? '');
$error = 'Benutzername oder Passwort falsch.';
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>OMSORG Felix Login</title>
<link rel="stylesheet" href="app.css">
<link rel="manifest" href="manifest.webmanifest">
<meta name="theme-color" content="#1a3a5c">
</head>
<body>
<div class="login-page">
<div class="login-card glass">
<div class="brand" style="text-align:center">
<img src="assets/omsorg-wordmark-new.png" alt="OMSORG" style="width:210px;margin:0 auto 22px">
</div>
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.7rem;margin:0 0 6px">Mitarbeiter-App</h2>
<p class="hint" style="margin:0 0 22px">Melde dich mit deinem Benutzerkonto an.</p>
<?php if ($error): ?>
<div class="error" style="margin-bottom:16px"><?= e($error) ?></div>
<?php endif; ?>
<form method="post">
<?= csrf_field() ?>
<label>
Benutzername
<input type="text" name="username" autocomplete="username" required autofocus
value="<?= e($_POST['username'] ?? '') ?>">
</label>
<label>
Passwort
<input type="password" name="password" autocomplete="current-password" required>
</label>
<button type="submit" class="btn" style="width:100%;margin-top:6px">Anmelden</button>
</form>
</div>
</div>
<script>
if ('serviceWorker' in navigator) navigator.serviceWorker.register('service-worker.js');
</script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
Require all denied
+111
View File
@@ -0,0 +1,111 @@
<?php
require_once __DIR__ . '/db.php';
$secure = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| (($_SERVER['SERVER_PORT'] ?? null) == 443);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'httponly' => true,
'secure' => $secure,
'samesite' => 'Lax',
]);
session_name('OMSORG_FELIX_APP');
session_start();
function is_logged_in(): bool {
if (empty($_SESSION['felix_logged_in']) || empty($_SESSION['felix_username'])) {
return false;
}
$stmt = db()->prepare('SELECT active FROM users WHERE username = ?');
$stmt->execute([$_SESSION['felix_username']]);
$row = $stmt->fetch();
return $row && (int)$row['active'] === 1;
}
function require_login(): void {
if (!is_logged_in()) {
header('Location: ../index.php');
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST)) {
verify_csrf();
}
}
function csrf_token(): string {
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
function csrf_field(): string {
return '<input type="hidden" name="csrf_token" value="' . csrf_token() . '">';
}
function verify_csrf(): void {
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals(csrf_token(), $token)) {
http_response_code(403);
exit('Ungültige Anfrage.');
}
}
function require_admin(): void {
require_login();
if (!is_admin()) {
header('Location: ../pages/dashboard.php');
exit;
}
}
function is_admin(): bool {
return isset($_SESSION['felix_role']) && $_SESSION['felix_role'] === 'admin';
}
function current_user(): array|false {
static $cache = null;
static $cachedFor = null;
$username = $_SESSION['felix_username'] ?? '';
if ($username === '') return false;
if ($cache !== null && $cachedFor === $username) return $cache;
$stmt = db()->prepare('SELECT * FROM users WHERE username = ?');
$stmt->execute([$username]);
$cache = $stmt->fetch();
$cachedFor = $username;
return $cache;
}
function current_username(): string {
return $_SESSION['felix_username'] ?? '';
}
function current_name(): string {
return $_SESSION['felix_name'] ?? '';
}
function e(string $s): string {
return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
// --- Login rate limiting (IP-based, persisted in DB) ---
function login_recent_fails(string $ip, int $windowSec = 600): int {
$cutoff = date('c', time() - $windowSec);
$stmt = db()->prepare(
'SELECT COUNT(*) FROM login_attempts WHERE ip = ? AND attempted_at >= ?'
);
$stmt->execute([$ip, $cutoff]);
return (int) $stmt->fetchColumn();
}
function login_record_fail(string $ip, string $username): void {
db()->prepare(
'INSERT INTO login_attempts (ip, username, attempted_at) VALUES (?, ?, ?)'
)->execute([$ip, $username, date('c')]);
}
function login_clear_fails(string $ip): void {
db()->prepare('DELETE FROM login_attempts WHERE ip = ?')->execute([$ip]);
}
@@ -0,0 +1,8 @@
<?php
// Vorlage: nach config.secret.php kopieren und mit echten Werten füllen.
// config.secret.php niemals ins Repo/Backup geben.
return [
'db_user' => 'DEIN_DB_BENUTZER',
'db_password' => 'DEIN_DB_PASSWORT',
'smtp_password' => 'DEIN_SMTP_PASSWORT',
];
+90
View File
@@ -0,0 +1,90 @@
<?php
function db(): PDO {
static $pdo = null;
if ($pdo === null) {
$c = require __DIR__ . '/config.php';
$pdo = new PDO(
$c['db_dsn'],
$c['db_user'],
$c['db_password'],
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
if ($c['db_driver'] === 'sqlite') {
$pdo->exec('PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;');
}
_run_migrations($pdo);
}
return $pdo;
}
function _table_exists(PDO $pdo, string $table, string $driver): bool {
if ($driver === 'sqlite') {
return (bool) $pdo->query(
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=" . $pdo->quote($table)
)->fetchColumn();
}
return (bool) $pdo->query(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema=DATABASE() AND table_name=" . $pdo->quote($table)
)->fetchColumn();
}
function _run_migrations(PDO $pdo): void {
$c = require __DIR__ . '/config.php';
$driver = $c['db_driver'];
$migrationTableExisted = _table_exists($pdo, 'schema_migrations', $driver);
$pdo->exec("
CREATE TABLE IF NOT EXISTS schema_migrations (
version VARCHAR(255) NOT NULL PRIMARY KEY,
applied_at VARCHAR(32) NOT NULL
)
");
$files = glob(__DIR__ . '/../migrations/*.php');
if (!$files) return;
sort($files);
// Existing DB without migration table: stamp all known migrations as applied.
if (!$migrationTableExisted && _table_exists($pdo, 'users', $driver)) {
$insertIgnore = $driver === 'sqlite' ? 'INSERT OR IGNORE' : 'INSERT IGNORE';
$stmt = $pdo->prepare(
"$insertIgnore INTO schema_migrations (version, applied_at) VALUES (?, ?)"
);
foreach ($files as $file) {
$stmt->execute([basename($file, '.php'), date('c')]);
}
return;
}
$applied = $pdo->query('SELECT version FROM schema_migrations')
->fetchAll(PDO::FETCH_COLUMN);
$applied = array_flip($applied);
$useTransactions = ($driver === 'sqlite');
foreach ($files as $file) {
$version = basename($file, '.php');
if (isset($applied[$version])) continue;
$migration = require $file;
if ($useTransactions) $pdo->beginTransaction();
try {
foreach ((array) $migration['up'] as $sql) {
$pdo->exec($sql);
}
$pdo->prepare(
'INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)'
)->execute([$version, date('c')]);
if ($useTransactions) $pdo->commit();
} catch (Throwable $e) {
if ($useTransactions && $pdo->inTransaction()) $pdo->rollBack();
throw new RuntimeException(
"Migration {$version} failed: " . $e->getMessage(), 0, $e
);
}
}
}
+110
View File
@@ -0,0 +1,110 @@
<?php
function layout_start(string $title, string $current_page, string $extra_head = ''): void {
$name = current_name();
$admin = is_admin();
$user = current_user();
$nav = [
'dashboard' => ['href' => 'dashboard.php', 'icon' => '⊞', 'label' => 'Dashboard'],
'stundennachweis' => ['href' => 'stundennachweis.php', 'icon' => '🕐', 'label' => 'Stundennachweis'],
'urlaubsantrag' => ['href' => 'urlaubsantrag.php', 'icon' => '🌴', 'label' => 'Urlaubsantrag'],
'dienstplan' => ['href' => 'dienstplan.php', 'icon' => '📋', 'label' => 'Dienstplan'],
'downloads' => ['href' => 'downloads.php', 'icon' => '📥', 'label' => 'Downloads'],
'einsatzanweisung' => ['href' => 'einsatzanweisung.php', 'icon' => '📄', 'label' => 'Einsatzanweisung'],
'benefitsantrag' => ['href' => 'benefitsantrag.php', 'icon' => '🎁', 'label' => 'Benefitsantrag'],
'abwesenheitsantrag' => ['href' => 'abwesenheitsantrag.php', 'icon' => '📅', 'label' => 'Abwesenheitsantrag'],
'fortbildungsantrag' => ['href' => 'fortbildungsantrag.php', 'icon' => '🎓', 'label' => 'Fortbildungsantrag'],
'dokumentenarchiv' => ['href' => 'dokumentenarchiv.php', 'icon' => '🗂', 'label' => 'Dokumentenarchiv'],
'einsatzbewertung' => ['href' => 'einsatzbewertung.php', 'icon' => '⭐', 'label' => 'Einsatzbewertung'],
'werben' => ['href' => 'werben.php', 'icon' => '🤝', 'label' => 'Mitarbeiter werben'],
'settings' => ['href' => 'settings.php', 'icon' => '⚙', 'label' => 'Einstellungen'],
];
if ($admin) {
$nav['admin'] = ['href' => 'admin.php', 'icon' => '🛡', 'label' => 'Verwaltung'];
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title><?= e($title) ?></title>
<link rel="stylesheet" href="../app.css">
<link rel="manifest" href="../manifest.webmanifest">
<meta name="theme-color" content="#1a3a5c">
<meta name="csrf-token" content="<?= csrf_token() ?>">
<?= $extra_head ?>
</head>
<body>
<div class="dashboard-layout">
<aside class="sidebar glass">
<div class="sidebar-brand">
<img src="../assets/omsorg-wordmark-new.png" alt="OMSORG">
<span class="sidebar-app-name">Mitarbeiter-App</span>
<button class="nav-toggle" type="button" aria-label="Menü öffnen" aria-expanded="false">☰</button>
</div>
<div class="sidebar-user">
<div class="sidebar-avatar">
<?php if (!empty($user['avatar'])): ?>
<img src="../assets/avatars/<?= e($user['avatar']) ?>" alt="<?= e($name) ?>">
<?php else: ?>
<?= e(mb_substr($name, 0, 1)) ?>
<?php endif; ?>
</div>
<div>
<b><?= e($name) ?></b>
<small><?= $admin ? 'Admin' : 'Mitarbeiter' ?></small>
</div>
</div>
<nav class="sidebar-nav">
<p class="sidebar-nav-label">Navigation</p>
<?php foreach ($nav as $key => $item): ?>
<a href="<?= e($item['href']) ?>"<?= $current_page === $key ? ' class="active"' : '' ?>>
<span class="nav-icon"><?= $item['icon'] ?></span><span class="nav-label"> <?= e($item['label']) ?></span>
</a>
<?php endforeach; ?>
</nav>
<div class="sidebar-footer">
<a href="../actions/logout.php" class="sidebar-logout">Abmelden</a>
</div>
</aside>
<div class="nav-backdrop" onclick="closeNav()"></div>
<main class="main-content">
<div class="main-inner">
<?php
}
function layout_end(): void {
?>
</div>
</main>
</div>
<script>
if ('serviceWorker' in navigator) navigator.serviceWorker.register('../service-worker.js');
function closeNav(){
document.body.classList.remove('nav-open');
const btn=document.querySelector('.nav-toggle');
if(btn){btn.textContent='☰';btn.setAttribute('aria-expanded','false');}
}
const navToggle=document.querySelector('.nav-toggle');
if(navToggle){
navToggle.addEventListener('click',function(){
const open=document.body.classList.toggle('nav-open');
this.textContent=open?'✕':'☰';
this.setAttribute('aria-expanded',open);
});
}
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeNav();});
</script>
</body>
</html>
<?php
}
+137
View File
@@ -0,0 +1,137 @@
<?php
/**
* Sends an email with an optional file attachment via SMTP.
*
* @param array $cfg Config array with smtp_host, smtp_port, smtp_user, smtp_password, mail_from
* @param string $to Recipient address
* @param string $subject Subject (plain UTF-8, will be encoded)
* @param string $body Plain-text body
* @param array $attach Optional: ['path' => '/abs/path', 'name' => 'display.pdf', 'mime' => 'application/pdf']
* @return array ['ok' => bool, 'log' => string]
*/
function smtp_send(array $cfg, string $to, string $subject, string $body, array $attach = []): array
{
$log = '';
$read = function ($conn) use (&$log) {
$line = '';
while ($chunk = fgets($conn, 512)) {
$log .= '<< ' . $chunk;
$line = $chunk;
if ($chunk[3] === ' ') break; // last line of multi-line response
}
return $line;
};
$write = function ($conn, string $cmd) use (&$log) {
$log .= '>> ' . $cmd;
fwrite($conn, $cmd);
};
$code = fn(string $line) => (int)substr($line, 0, 3);
try {
$host = trim($cfg['smtp_host']);
$port = (int)$cfg['smtp_port'];
$ssl = ($port === 465);
$conn = @fsockopen(($ssl ? 'ssl://' : '') . $host, $port, $errno, $errstr, 10);
if (!$conn) {
return ['ok' => false, 'log' => "Connect failed: $errstr ($errno)"];
}
stream_set_timeout($conn, 10);
$line = $read($conn);
if ($code($line) !== 220) throw new RuntimeException("Unexpected greeting: $line");
$write($conn, "EHLO omsorg-connect\r\n");
$capabilities = '';
while ($chunk = fgets($conn, 512)) {
$log .= '<< ' . $chunk;
$capabilities .= $chunk;
if ($chunk[3] === ' ') break;
}
if ($code($capabilities) !== 250) throw new RuntimeException("EHLO failed");
if (!$ssl && str_contains($capabilities, 'STARTTLS')) {
$write($conn, "STARTTLS\r\n");
$line = $read($conn);
if ($code($line) !== 220) throw new RuntimeException("STARTTLS failed: $line");
if (!stream_socket_enable_crypto($conn, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
throw new RuntimeException("TLS handshake failed");
}
$write($conn, "EHLO omsorg-connect\r\n");
while ($chunk = fgets($conn, 512)) {
$log .= '<< ' . $chunk;
if ($chunk[3] === ' ') break;
}
}
$write($conn, "AUTH LOGIN\r\n");
$line = $read($conn);
if ($code($line) !== 334) throw new RuntimeException("AUTH LOGIN failed: $line");
$write($conn, base64_encode($cfg['smtp_user']) . "\r\n");
$line = $read($conn);
if ($code($line) !== 334) throw new RuntimeException("Username rejected: $line");
$write($conn, base64_encode($cfg['smtp_password']) . "\r\n");
$line = $read($conn);
if ($code($line) !== 235) throw new RuntimeException("Password rejected: $line");
$write($conn, "MAIL FROM:<{$cfg['mail_from']}>\r\n");
$line = $read($conn);
if ($code($line) !== 250) throw new RuntimeException("MAIL FROM failed: $line");
$write($conn, "RCPT TO:<$to>\r\n");
$line = $read($conn);
if ($code($line) !== 250) throw new RuntimeException("RCPT TO failed: $line");
$write($conn, "DATA\r\n");
$line = $read($conn);
if ($code($line) !== 354) throw new RuntimeException("DATA failed: $line");
$enc_subject = '=?UTF-8?B?' . base64_encode($subject) . '?=';
$boundary = 'omsorg_' . md5(uniqid((string)mt_rand(), true));
$headers = "From: OMSORG Connect <{$cfg['mail_from']}>\r\n";
$headers .= "To: $to\r\n";
$headers .= "Subject: $enc_subject\r\n";
$headers .= "MIME-Version: 1.0\r\n";
if ($attach) {
$headers .= "Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n";
$msg = $headers . "\r\n";
$msg .= "--$boundary\r\n";
$msg .= "Content-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: 8bit\r\n\r\n";
$msg .= $body . "\r\n\r\n";
$safe_name = preg_replace('/[^A-Za-z0-9._-]/', '_', $attach['name']);
$file_data = chunk_split(base64_encode(file_get_contents($attach['path'])));
$msg .= "--$boundary\r\n";
$msg .= "Content-Type: {$attach['mime']}; name=\"$safe_name\"\r\n";
$msg .= "Content-Transfer-Encoding: base64\r\n";
$msg .= "Content-Disposition: attachment; filename=\"$safe_name\"\r\n\r\n";
$msg .= $file_data . "\r\n";
$msg .= "--$boundary--";
} else {
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
$msg = $headers . "\r\n" . $body;
}
// Dot-stuffing: lines starting with '.' must be doubled
$msg = str_replace("\n.", "\n..", $msg);
$write($conn, $msg . "\r\n.\r\n");
$line = $read($conn);
if ($code($line) !== 250) throw new RuntimeException("Message rejected: $line");
$write($conn, "QUIT\r\n");
$read($conn);
fclose($conn);
return ['ok' => true, 'log' => $log];
} catch (RuntimeException $e) {
if (isset($conn) && is_resource($conn)) fclose($conn);
return ['ok' => false, 'log' => $log . "\nError: " . $e->getMessage()];
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
require_once __DIR__ . '/auth.php';
// Gemeinsame Endung→MIME-Whitelists.
const UPLOAD_TYPES_DOCS = [
'pdf' => ['application/pdf'],
'doc' => ['application/msword'],
'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
];
const UPLOAD_TYPES_PDF_IMG = [
'pdf' => ['application/pdf'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
];
const UPLOAD_TYPES_IMAGES = [
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'gif' => ['image/gif'],
'webp' => ['image/webp'],
];
const UPLOAD_TYPES_PDF = [
'pdf' => ['application/pdf'],
];
const UPLOAD_TYPES_OFFICE = [
'pdf' => ['application/pdf'],
'doc' => ['application/msword'],
'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
'xls' => ['application/vnd.ms-excel'],
'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
'ppt' => ['application/vnd.ms-powerpoint'],
'pptx' => ['application/vnd.openxmlformats-officedocument.presentationml.presentation'],
'txt' => ['text/plain'],
'zip' => ['application/zip', 'application/x-zip-compressed'],
];
/**
* Validiert und speichert eine hochgeladene Datei.
*
* @param array $file Eintrag aus $_FILES, z.B. $_FILES['datei']
* @param array $opts allowed (ext=>mimes, Pflicht), dir (Pflicht), prefix,
* max_bytes (Default 12 MB), required (Default true),
* naming ('dated' [Default] = Datum_Username_Prefix_Rand,
* 'plain' = Username_Rand, 'hash' = nur Rand),
* username (Default current_username(); für 'dated'/'plain'),
* messages (Überschreibt Standardtexte: missing/upload/ext/mime/size/move)
* @return array ['ok'=>bool, 'error'=>?string, 'filename'=>?string,
* 'original_name'=>?string, 'mime'=>?string]
*/
function handle_upload(array $file, array $opts): array {
$allowed = $opts['allowed'];
$dir = rtrim($opts['dir'], '/') . '/';
$maxBytes = $opts['max_bytes'] ?? 12 * 1024 * 1024;
$required = $opts['required'] ?? true;
$naming = $opts['naming'] ?? 'dated';
$prefix = $opts['prefix'] ?? 'datei';
$username = $opts['username'] ?? current_username();
$msg = array_merge([
'missing' => 'Bitte eine Datei auswählen.',
'upload' => 'Fehler beim Hochladen der Datei.',
'ext' => 'Dateityp nicht erlaubt.',
'mime' => 'Dateityp nicht erlaubt.',
'size' => 'Datei ist zu groß (max. 12 MB).',
'move' => 'Fehler beim Hochladen der Datei.',
], $opts['messages'] ?? []);
$err = $file['error'] ?? UPLOAD_ERR_NO_FILE;
if ($err === UPLOAD_ERR_NO_FILE || !isset($file['tmp_name'])) {
if ($required) return ['ok' => false, 'error' => $msg['missing']];
return ['ok' => true, 'error' => null, 'filename' => null, 'original_name' => null, 'mime' => null];
}
if ($err !== UPLOAD_ERR_OK) {
return ['ok' => false, 'error' => $msg['upload']];
}
$originalName = $file['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!isset($allowed[$ext])) {
return ['ok' => false, 'error' => $msg['ext']];
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $allowed[$ext], true)) {
return ['ok' => false, 'error' => $msg['mime']];
}
if ($file['size'] > $maxBytes) {
return ['ok' => false, 'error' => $msg['size']];
}
$safeName = match ($naming) {
'hash' => bin2hex(random_bytes(16)) . '.' . $ext,
'plain' => $username . '_' . bin2hex(random_bytes(8)) . '.' . $ext,
default => date('Y-m-d') . '_' . $username . '_' . $prefix . '_' . bin2hex(random_bytes(8)) . '.' . $ext,
};
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
if (!move_uploaded_file($file['tmp_name'], $dir . $safeName)) {
return ['ok' => false, 'error' => $msg['move']];
}
return [
'ok' => true,
'error' => null,
'filename' => $safeName,
'original_name' => $originalName,
'mime' => $mime,
];
}
function redirect_error(string $back, string $msg): never {
header('Location: ' . $back . '?error=' . urlencode($msg));
exit;
}
function redirect_ok(string $back, string $param = 'success', string $val = '1'): never {
header('Location: ' . $back . '?' . $param . '=' . $val);
exit;
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "OMSORG Mitarbeiter-App",
"short_name": "OMSORG",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#1a3a5c",
"icons": [
{ "src": "../assets/omsorg-logo-new.png", "sizes": "192x192", "type": "image/png" },
{ "src": "../assets/omsorg-logo-new.png", "sizes": "512x512", "type": "image/png" }
]
}
+1
View File
@@ -0,0 +1 @@
Require all denied
@@ -0,0 +1,145 @@
<?php
return [
'description' => 'Initial schema',
'up' => [
"CREATE TABLE IF NOT EXISTS users (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
role VARCHAR(32) NOT NULL DEFAULT 'user',
password_hash VARCHAR(255) NOT NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at VARCHAR(32) NOT NULL,
email VARCHAR(255) NOT NULL DEFAULT '',
telefon VARCHAR(64) NOT NULL DEFAULT ''
)",
"CREATE TABLE IF NOT EXISTS requests_urlaubsantrag (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
admin_note TEXT NOT NULL,
created_at VARCHAR(32) NOT NULL,
updated_at VARCHAR(32) NOT NULL,
von VARCHAR(32) NOT NULL DEFAULT '',
bis VARCHAR(32) NOT NULL DEFAULT '',
vertretung VARCHAR(255) NOT NULL DEFAULT '',
nachricht TEXT NOT NULL
)",
"CREATE TABLE IF NOT EXISTS requests_abwesenheitsantrag (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
admin_note TEXT NOT NULL,
created_at VARCHAR(32) NOT NULL,
updated_at VARCHAR(32) NOT NULL,
von VARCHAR(32) NOT NULL DEFAULT '',
bis VARCHAR(32) NOT NULL DEFAULT '',
grund VARCHAR(64) NOT NULL DEFAULT '',
vertretung VARCHAR(255) NOT NULL DEFAULT '',
nachricht TEXT NOT NULL
)",
"CREATE TABLE IF NOT EXISTS requests_benefitsantrag (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
admin_note TEXT NOT NULL,
created_at VARCHAR(32) NOT NULL,
updated_at VARCHAR(32) NOT NULL,
benefit VARCHAR(64) NOT NULL DEFAULT '',
nachricht TEXT NOT NULL
)",
"CREATE TABLE IF NOT EXISTS requests_werben (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
admin_note TEXT NOT NULL,
created_at VARCHAR(32) NOT NULL,
updated_at VARCHAR(32) NOT NULL,
name VARCHAR(255) NOT NULL DEFAULT '',
email VARCHAR(255) NOT NULL DEFAULT '',
qualifikation VARCHAR(64) NOT NULL DEFAULT '',
nachricht TEXT NOT NULL
)",
"CREATE TABLE IF NOT EXISTS dienstplan (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
date VARCHAR(16) NOT NULL,
schicht VARCHAR(16) NOT NULL,
created_at VARCHAR(32) NOT NULL,
updated_at VARCHAR(32) NOT NULL,
UNIQUE KEY uq_user_date (user_id, date)
)",
"CREATE TABLE IF NOT EXISTS downloads (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at VARCHAR(32) NOT NULL,
updated_at VARCHAR(32) NOT NULL
)",
"CREATE TABLE IF NOT EXISTS fortbildung_materials (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT NOT NULL,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
created_at VARCHAR(32) NOT NULL,
updated_at VARCHAR(32) NOT NULL
)",
"CREATE TABLE IF NOT EXISTS requests_fortbildungsantrag (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
admin_note TEXT NOT NULL,
anliegen TEXT NOT NULL,
thema TEXT NOT NULL,
nachricht TEXT NOT NULL,
filename VARCHAR(255) NOT NULL DEFAULT '',
original_name VARCHAR(255) NOT NULL DEFAULT '',
created_at VARCHAR(32) NOT NULL,
updated_at VARCHAR(32) NOT NULL
)",
"CREATE TABLE IF NOT EXISTS requests_stundennachweis (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
admin_note TEXT NOT NULL,
monat VARCHAR(16) NOT NULL,
filename VARCHAR(255) NOT NULL DEFAULT '',
original_name VARCHAR(255) NOT NULL DEFAULT '',
created_at VARCHAR(32) NOT NULL,
updated_at VARCHAR(32) NOT NULL
)",
"CREATE TABLE IF NOT EXISTS einsatzbewertungen (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
einsatzort VARCHAR(255) NOT NULL,
von VARCHAR(32) NOT NULL,
bis VARCHAR(32) NOT NULL,
bewertung TINYINT NOT NULL,
wieder TINYINT(1) NOT NULL DEFAULT 0,
feedback TEXT NOT NULL,
created_at VARCHAR(32) NOT NULL
)",
"CREATE TABLE IF NOT EXISTS dokumente (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
kategorie VARCHAR(255) NOT NULL DEFAULT 'Sonstiges',
beschreibung TEXT NOT NULL,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
filesize INT NOT NULL DEFAULT 0,
created_at VARCHAR(32) NOT NULL
)",
"CREATE TABLE IF NOT EXISTS news (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
text TEXT NOT NULL,
date VARCHAR(16) NOT NULL,
created_at VARCHAR(32) NOT NULL
)",
],
];
@@ -0,0 +1,7 @@
<?php
return [
'description' => 'Add avatar column to users',
'up' => [
'ALTER TABLE users ADD COLUMN avatar VARCHAR(255) DEFAULT NULL',
],
];
@@ -0,0 +1,14 @@
<?php
return [
'description' => 'Einsatzanweisung table',
'up' => [
"CREATE TABLE IF NOT EXISTS einsatzanweisung (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL UNIQUE,
filename VARCHAR(255) NOT NULL DEFAULT '',
original_name VARCHAR(255) NOT NULL DEFAULT '',
ort VARCHAR(255) NOT NULL DEFAULT '',
uploaded_at VARCHAR(32) NOT NULL DEFAULT ''
)",
],
];
@@ -0,0 +1,13 @@
<?php
return [
'description' => 'Login attempts table for IP-based rate limiting',
'up' => [
"CREATE TABLE IF NOT EXISTS login_attempts (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
ip VARCHAR(45) NOT NULL DEFAULT '',
username VARCHAR(190) NOT NULL DEFAULT '',
attempted_at VARCHAR(32) NOT NULL DEFAULT ''
)",
"CREATE INDEX idx_login_attempts_ip_time ON login_attempts (ip, attempted_at)",
],
];
@@ -0,0 +1,165 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$user_id = current_user()['id'];
$stmt = db()->prepare('SELECT * FROM requests_abwesenheitsantrag WHERE user_id = ? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
$requests = $stmt->fetchAll();
$grund_labels = [
'krankheit' => 'Krankheit',
'arztbesuch' => 'Arztbesuch',
'behoerdengang' => 'Behördengang',
'sonderurlaub' => 'Sonderurlaub',
'elternzeit_pflegezeit' => 'Elternzeit / Pflegezeit',
'sonstiges' => 'Sonstiges',
];
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
layout_start('OMSORG Abwesenheitsantrag', 'abwesenheitsantrag');
?>
<style>
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:1000}
.modal-box{background:var(--glass-bg,#1e2235);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:28px 32px;max-width:480px;width:90%;position:relative}
.modal-box h3{margin:0 0 20px;font-family:'KindelSerif',Georgia,serif;font-size:1.3rem}
.modal-close{position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted,#888);line-height:1}
.modal-dl{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:.95rem}
.modal-dl dt{color:var(--muted,#888);white-space:nowrap}
.modal-dl dd{margin:0;font-weight:500}
.btn-sm{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-weight:600;font-size:.82rem;cursor:pointer;font:inherit;transition:background .15s}
.btn-sm:hover{background:rgba(255,255,255,.15)}
</style>
<h1 class="main-title">📅 Abwesenheitsantrag</h1>
<p class="main-sub">Melde eine geplante Abwesenheit. Das Startdatum muss vor dem Enddatum liegen.</p>
<?php if (isset($_GET['success'])): ?>
<div class="success" style="margin-top:16px">Antrag erfolgreich eingereicht.</div>
<?php elseif (isset($_GET['error'])): ?>
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
<?php endif; ?>
<div id="abw-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
<div class="modal-box glass">
<button class="modal-close" onclick="document.getElementById('abw-modal').style.display='none'">&times;</button>
<h3>Antrag Details</h3>
<dl class="modal-dl">
<dt>Von</dt> <dd id="abw-von"></dd>
<dt>Bis</dt> <dd id="abw-bis"></dd>
<dt>Grund</dt> <dd id="abw-grund"></dd>
<dt>Vertretung</dt> <dd id="abw-vertretung"></dd>
<dt>Nachricht</dt> <dd id="abw-nachricht"></dd>
<dt>Eingereicht</dt><dd id="abw-eingereicht"></dd>
<dt>Status</dt> <dd id="abw-status"></dd>
<dt>Admin-Notiz</dt><dd id="abw-note"></dd>
</dl>
</div>
</div>
<div class="page-split">
<div class="page-split-list">
<h2>Meine Anträge</h2>
<?php if (empty($requests)): ?>
<p style="color:var(--muted)">Noch keine Anträge gestellt.</p>
<?php else: ?>
<table class="requests-table">
<thead>
<tr>
<th>Von</th>
<th>Bis</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($requests as $r): ?>
<tr>
<td><?= e(date('d.m.Y', strtotime($r['von']))) ?></td>
<td><?= e(date('d.m.Y', strtotime($r['bis']))) ?></td>
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
<td>
<button class="btn-sm js-abw-details"
data-von="<?= e(date('d.m.Y', strtotime($r['von']))) ?>"
data-bis="<?= e(date('d.m.Y', strtotime($r['bis']))) ?>"
data-grund="<?= e($grund_labels[$r['grund']] ?? $r['grund']) ?>"
data-vertretung="<?= e($r['vertretung'] !== '' ? $r['vertretung'] : '') ?>"
data-nachricht="<?= e($r['nachricht'] !== '' ? $r['nachricht'] : '') ?>"
data-eingereicht="<?= e(date('d.m.Y', strtotime($r['created_at']))) ?>"
data-status-label="<?= e($status_labels[$r['status']] ?? $r['status']) ?>"
data-status-key="<?= e($r['status']) ?>"
data-note="<?= e($r['admin_note'] !== '' ? $r['admin_note'] : '') ?>"
>Details</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class="page-split-form">
<div class="glass detail-card">
<h2>Neuer Abwesenheitsantrag</h2>
<form action="../actions/submit-abwesenheitsantrag.php" method="post">
<?= csrf_field() ?>
<label>
<span>Von <span style="color:#ff8080">*</span></span>
<input type="date" name="von" required>
</label>
<label>
<span>Bis <span style="color:#ff8080">*</span></span>
<input type="date" name="bis" required>
</label>
<label>
<span>Grund <span style="color:#ff8080">*</span></span>
<select name="grund" required>
<option value=""> Bitte wählen </option>
<option value="krankheit">Krankheit</option>
<option value="arztbesuch">Arztbesuch</option>
<option value="behoerdengang">Behördengang</option>
<option value="sonderurlaub">Sonderurlaub</option>
<option value="elternzeit_pflegezeit">Elternzeit / Pflegezeit</option>
<option value="sonstiges">Sonstiges</option>
</select>
</label>
<label>
<span>Vertretung</span>
<input type="text" name="vertretung" placeholder="Name der Vertretung">
</label>
<label>
Nachricht
<textarea name="nachricht" rows="4" placeholder="Optionale Anmerkungen"></textarea>
</label>
<button type="submit" class="btn" style="margin-top:6px">Antrag einreichen</button>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('click', function(e) {
var btn = e.target.closest('.js-abw-details');
if (!btn) return;
var d = btn.dataset;
document.getElementById('abw-von').textContent = d.von;
document.getElementById('abw-bis').textContent = d.bis;
document.getElementById('abw-grund').textContent = d.grund;
document.getElementById('abw-vertretung').textContent = d.vertretung;
document.getElementById('abw-nachricht').textContent = d.nachricht;
document.getElementById('abw-eingereicht').textContent = d.eingereicht;
document.getElementById('abw-status').innerHTML =
'<span class="badge badge-' + d.statusKey + '">' + d.statusLabel + '</span>';
document.getElementById('abw-note').textContent = d.note;
document.getElementById('abw-modal').style.display = 'flex';
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') document.getElementById('abw-modal').style.display = 'none';
});
</script>
<?php layout_end(); ?>
+162
View File
@@ -0,0 +1,162 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/layout.php';
require_admin();
// Fetch all active users
$users = db()->query(
'SELECT id, name FROM users WHERE active = 1 ORDER BY name ASC'
)->fetchAll();
$now = new DateTimeImmutable('today');
$year = isset($_GET['year']) ? (int)$_GET['year'] : (int)$now->format('Y');
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)$now->format('n');
$year = max(2020, min(2099, $year));
$month = max(1, min(12, $month));
// Resolve selected user
$selected_id = isset($_GET['user_id']) ? (int)$_GET['user_id'] : 0;
$selected_user = null;
foreach ($users as $u) {
if ((int)$u['id'] === $selected_id) { $selected_user = $u; break; }
}
if (!$selected_user && !empty($users)) {
$selected_user = $users[0];
$selected_id = (int)$selected_user['id'];
}
$first = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
$month_start = $first->format('Y-m-d');
$month_end = $first->modify('last day of this month')->format('Y-m-d');
$days = (int)$first->format('t');
$prev = $first->modify('-1 month');
$next = $first->modify('+1 month');
$prev_url = '?user_id=' . $selected_id . '&year=' . $prev->format('Y') . '&month=' . $prev->format('n');
$next_url = '?user_id=' . $selected_id . '&year=' . $next->format('Y') . '&month=' . $next->format('n');
// Load shifts
$schichten = [];
if ($selected_id) {
$stmt = db()->prepare(
'SELECT date, schicht FROM dienstplan WHERE user_id = ? AND date >= ? AND date <= ?'
);
$stmt->execute([$selected_id, $month_start, $month_end]);
foreach ($stmt->fetchAll() as $row) {
$schichten[$row['date']] = $row['schicht'];
}
}
// Load overlays
$overlays = [];
if ($selected_id) {
$stmt = db()->prepare("
SELECT von, bis, 'urlaub' AS typ
FROM requests_urlaubsantrag
WHERE user_id = ? AND status = 'accepted' AND bis >= ? AND von <= ?
UNION ALL
SELECT von, bis, 'abwesenheit' AS typ
FROM requests_abwesenheitsantrag
WHERE user_id = ? AND status = 'accepted' AND bis >= ? AND von <= ?
");
$stmt->execute([$selected_id, $month_start, $month_end, $selected_id, $month_start, $month_end]);
foreach ($stmt->fetchAll() as $row) {
$cursor = new DateTimeImmutable($row['von']);
$end = new DateTimeImmutable($row['bis']);
while ($cursor <= $end) {
$key = $cursor->format('Y-m-d');
if ($key >= $month_start && $key <= $month_end) {
$overlays[$key] = $row['typ'];
}
$cursor = $cursor->modify('+1 day');
}
}
}
$today_str = $now->format('Y-m-d');
$schicht_labels = ['frueh' => 'Früh', 'spaet' => 'Spät', 'nacht' => 'Nacht'];
$weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
$months_de = ['', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
layout_start('Dienstpläne', 'admin-dienstplan');
?>
<h1 class="main-title">Dienstpläne</h1>
<div class="dp-user-select-wrap">
<form method="get">
<input type="hidden" name="year" value="<?= $year ?>">
<input type="hidden" name="month" value="<?= $month ?>">
<label class="form-label" for="user_id">Mitarbeiter</label>
<select name="user_id" id="user_id" onchange="this.form.submit()">
<?php foreach ($users as $u): ?>
<option value="<?= (int)$u['id'] ?>" <?= (int)$u['id'] === $selected_id ? 'selected' : '' ?>>
<?= e($u['name']) ?>
</option>
<?php endforeach; ?>
</select>
</form>
</div>
<?php if ($selected_user): ?>
<div class="dp-nav">
<div style="display:flex;gap:10px;align-items:center">
<a href="<?= e($prev_url) ?>" class="dp-nav-btn">&larr;</a>
<h2 class="dp-nav-title"><?= e($months_de[$month] . ' ' . $year) ?></h2>
<a href="<?= e($next_url) ?>" class="dp-nav-btn">&rarr;</a>
</div>
<span style="color:var(--muted);font-size:.9rem"><?= e($selected_user['name']) ?></span>
</div>
<div class="dp-grid">
<?php foreach ($weekdays as $wd): ?>
<div class="dp-weekday"><?= e($wd) ?></div>
<?php endforeach; ?>
<?php
$lead = (int)$first->format('N') - 1;
for ($i = 0; $i < $lead; $i++): ?>
<div class="dp-cell dp-cell-empty"></div>
<?php endfor; ?>
<?php for ($d = 1; $d <= $days; $d++):
$date_key = sprintf('%04d-%02d-%02d', $year, $month, $d);
$is_today = ($date_key === $today_str);
$overlay = $overlays[$date_key] ?? null;
$schicht = $schichten[$date_key] ?? null;
$cell_cls = 'dp-cell' . ($is_today ? ' dp-cell-today' : '');
?>
<div class="<?= $cell_cls ?>">
<span class="dp-day-num"><?= $d ?></span>
<?php if ($overlay): ?>
<span class="dp-badge dp-overlay-<?= e($overlay) ?>">
<?= $overlay === 'urlaub' ? 'Urlaub' : 'Abwesenheit' ?>
</span>
<?php endif; ?>
<?php if ($schicht): ?>
<span class="dp-badge dp-badge-<?= e($schicht) ?>">
<?= e($schicht_labels[$schicht]) ?>
</span>
<?php elseif (!$overlay): ?>
<span class="dp-free"></span>
<?php endif; ?>
</div>
<?php endfor; ?>
<?php
$total = $lead + $days;
$trail = (7 - ($total % 7)) % 7;
for ($i = 0; $i < $trail; $i++): ?>
<div class="dp-cell dp-cell-empty"></div>
<?php endfor; ?>
</div>
<?php else: ?>
<p style="color:var(--muted)">Keine aktiven Mitarbeiter vorhanden.</p>
<?php endif; ?>
<?php
layout_end();
File diff suppressed because it is too large Load Diff
+183
View File
@@ -0,0 +1,183 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$user_id = current_user()['id'];
$stmt = db()->prepare('SELECT * FROM requests_benefitsantrag WHERE user_id = ? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
$requests = $stmt->fetchAll();
$benefit_labels = [
'yoga' => 'Yoga',
'autogenes_training' => 'Autogenes Training',
'massage' => 'Massage',
'tankgutschein' => 'Tankgutschein',
'online_gutscheine' => 'Online-Gutscheine',
];
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
layout_start('OMSORG Benefitsantrag', 'benefitsantrag');
?>
<style>
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:1000}
.modal-box{background:var(--glass-bg,#1e2235);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:28px 32px;max-width:480px;width:90%;position:relative}
.modal-box h3{margin:0 0 20px;font-family:'KindelSerif',Georgia,serif;font-size:1.3rem}
.modal-close{position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted,#888);line-height:1}
.modal-dl{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:.95rem}
.modal-dl dt{color:var(--muted,#888);white-space:nowrap}
.modal-dl dd{margin:0;font-weight:500}
.btn-sm{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-weight:600;font-size:.82rem;cursor:pointer;font:inherit;transition:background .15s}
.btn-sm:hover{background:rgba(255,255,255,.15)}
</style>
<h1 class="main-title">🎁 Benefitsantrag</h1>
<p class="main-sub">Beantrage einen Mitarbeiterbenefit deiner Wahl.</p>
<?php if (isset($_GET['success'])): ?>
<div class="success" style="margin-top:16px">Antrag erfolgreich eingereicht.</div>
<?php elseif (isset($_GET['error'])): ?>
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
<?php endif; ?>
<div id="ben-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
<div class="modal-box glass">
<button class="modal-close" onclick="document.getElementById('ben-modal').style.display='none'">&times;</button>
<h3>Antrag Details</h3>
<dl class="modal-dl">
<dt>Benefit</dt> <dd id="ben-benefit"></dd>
<dt>Nachricht</dt> <dd id="ben-nachricht"></dd>
<dt>Eingereicht</dt><dd id="ben-eingereicht"></dd>
<dt>Status</dt> <dd id="ben-status"></dd>
<dt>Admin-Notiz</dt><dd id="ben-note"></dd>
</dl>
</div>
</div>
<div id="liberty-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
<div class="modal-box glass" style="max-width:600px;padding:16px;text-align:center">
<button class="modal-close" onclick="document.getElementById('liberty-modal').style.display='none'">&times;</button>
<div class="success" style="margin:0 0 14px">Antrag erfolgreich eingereicht.</div>
<img src="../assets/Liberty.jpeg" alt="Liberty" style="width:100%;border-radius:10px;display:block">
</div>
</div>
<div class="page-split">
<div class="page-split-list">
<h2>Meine Anträge</h2>
<?php if (empty($requests)): ?>
<p style="color:var(--muted)">Noch keine Anträge gestellt.</p>
<?php else: ?>
<table class="requests-table">
<thead>
<tr>
<th>Benefit</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($requests as $r): ?>
<tr>
<td style="font-weight:800"><?= e($benefit_labels[$r['benefit']] ?? $r['benefit']) ?></td>
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
<td>
<button class="btn-sm js-ben-details"
data-benefit="<?= e($benefit_labels[$r['benefit']] ?? $r['benefit']) ?>"
data-nachricht="<?= e($r['nachricht'] !== '' ? $r['nachricht'] : '') ?>"
data-eingereicht="<?= e(date('d.m.Y', strtotime($r['created_at']))) ?>"
data-status-label="<?= e($status_labels[$r['status']] ?? $r['status']) ?>"
data-status-key="<?= e($r['status']) ?>"
data-note="<?= e($r['admin_note'] !== '' ? $r['admin_note'] : '') ?>"
>Details</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class="page-split-form">
<div class="glass detail-card">
<h2>Neuer Benefitsantrag</h2>
<form id="benefits-form" action="../actions/submit-benefitsantrag.php" method="post">
<?= csrf_field() ?>
<label>
<span>Benefit <span style="color:#ff8080">*</span></span>
<select name="benefit" required>
<option value=""> Bitte wählen </option>
<option value="yoga">Yoga</option>
<option value="autogenes_training">Autogenes Training</option>
<option value="massage">Massage</option>
<option value="tankgutschein">Tankgutschein</option>
<option value="online_gutscheine">Online-Gutscheine</option>
</select>
</label>
<label>
Nachricht
<textarea name="nachricht" rows="4" placeholder="Optionale Anmerkungen"></textarea>
</label>
<div id="benefits-form-error" class="error" style="display:none;margin-top:8px"></div>
<button type="submit" class="btn" style="margin-top:6px">Antrag einreichen</button>
</form>
</div>
</div>
</div>
<script>
var LIBERTY_BENEFITS = ['autogenes_training', 'massage'];
document.getElementById('benefits-form').addEventListener('submit', function(e) {
var benefit = this.querySelector('[name="benefit"]').value;
if (!LIBERTY_BENEFITS.includes(benefit)) return; // normal submit for other benefits
e.preventDefault();
var form = this;
var btn = form.querySelector('[type="submit"]');
btn.disabled = true;
var errEl = document.getElementById('benefits-form-error');
errEl.style.display = 'none';
fetch(form.action, {
method: 'POST',
body: new FormData(form),
headers: { 'X-Requested-With': 'XMLHttpRequest' }
}).then(function(r) { return r.json(); }).then(function(data) {
btn.disabled = false;
if (data.ok) {
form.reset();
document.getElementById('liberty-modal').style.display = 'flex';
} else {
errEl.textContent = data.error || 'Ein Fehler ist aufgetreten.';
errEl.style.display = 'block';
}
}).catch(function() {
btn.disabled = false;
errEl.textContent = 'Ein Fehler ist aufgetreten. Bitte erneut versuchen.';
errEl.style.display = 'block';
});
});
document.addEventListener('click', function(e) {
var btn = e.target.closest('.js-ben-details');
if (!btn) return;
var d = btn.dataset;
document.getElementById('ben-benefit').textContent = d.benefit;
document.getElementById('ben-nachricht').textContent = d.nachricht;
document.getElementById('ben-eingereicht').textContent= d.eingereicht;
document.getElementById('ben-status').innerHTML =
'<span class="badge badge-' + d.statusKey + '">' + d.statusLabel + '</span>';
document.getElementById('ben-note').textContent = d.note;
document.getElementById('ben-modal').style.display = 'flex';
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.getElementById('ben-modal').style.display = 'none';
document.getElementById('liberty-modal').style.display = 'none';
}
});
</script>
<?php layout_end(); ?>
+189
View File
@@ -0,0 +1,189 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$name = current_name();
$user_id = current_user()['id'];
$news_items = db()->query('SELECT * FROM news ORDER BY date DESC, id DESC')->fetchAll();
$grund_labels = [
'arztbesuch' => 'Arztbesuch',
'behoerdengang' => 'Behördengang',
'sonderurlaub' => 'Sonderurlaub',
'elternzeit_pflegezeit' => 'Elternzeit / Pflegezeit',
'sonstiges' => 'Sonstiges',
];
$benefit_labels = [
'massage' => 'Massage',
'fitnessstudio' => 'Fitnessstudio',
'tankgutschein' => 'Tankgutschein',
'fortbildung' => 'Fortbildung',
];
$rows = [];
$stmt = db()->prepare('SELECT id, status, created_at, von, bis, vertretung, nachricht FROM requests_urlaubsantrag WHERE user_id=? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
foreach ($stmt->fetchAll() as $r) {
$rows[] = ['type' => 'urlaub', 'type_label' => 'Urlaubsantrag', 'icon' => '🌴'] + $r + ['detail' => $r['von'] . ' ' . $r['bis']];
}
$stmt = db()->prepare('SELECT id, status, created_at, von, bis, grund, vertretung, nachricht FROM requests_abwesenheitsantrag WHERE user_id=? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
foreach ($stmt->fetchAll() as $r) {
$rows[] = ['type' => 'abwesenheit', 'type_label' => 'Abwesenheitsantrag', 'icon' => '📅'] + $r + ['detail' => ($grund_labels[$r['grund']] ?? $r['grund']) . ': ' . $r['von'] . ' ' . $r['bis']];
}
$stmt = db()->prepare('SELECT id, status, created_at, benefit, nachricht FROM requests_benefitsantrag WHERE user_id=? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
foreach ($stmt->fetchAll() as $r) {
$rows[] = ['type' => 'benefits', 'type_label' => 'Benefitsantrag', 'icon' => '🎁'] + $r + ['detail' => $benefit_labels[$r['benefit']] ?? $r['benefit']];
}
$stmt = db()->prepare('SELECT id, status, created_at, name, email, qualifikation, nachricht FROM requests_werben WHERE user_id=? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
foreach ($stmt->fetchAll() as $r) {
$rows[] = ['type' => 'werben', 'type_label' => 'Mitarbeiter werben', 'icon' => '🤝'] + $r + ['detail' => $r['name'] . ($r['qualifikation'] ? ' (' . $r['qualifikation'] . ')' : '')];
}
usort($rows, fn($a, $b) => strcmp($b['created_at'], $a['created_at']));
$pending_count = count(array_filter($rows, fn($r) => $r['status'] === 'pending'));
$status_map = ['pending' => 'Ausstehend', 'accepted' => 'Angenommen', 'rejected' => 'Abgelehnt'];
layout_start('OMSORG Felix Dashboard', 'dashboard');
?>
<h1 class="main-title">Willkommen, <span><?= e($name) ?></span>!</h1>
<p class="main-sub">Hier siehst du alle deine Anträge auf einen Blick.</p>
<?php if (!empty($news_items)): ?>
<div style="margin:28px 0 8px">
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 14px">📢 News & Ankündigungen</h2>
<?php foreach ($news_items as $n): ?>
<div class="glass" style="padding:20px 24px;border-radius:18px;margin-bottom:12px">
<div style="color:var(--cyan2);font-size:.82rem;font-weight:900;margin-bottom:5px">
<?= e(date('d.m.Y', strtotime($n['date']))) ?>
</div>
<strong style="font-size:1.05rem;display:block;margin-bottom:6px"><?= e($n['title']) ?></strong>
<p style="margin:0;color:var(--muted);line-height:1.6;font-size:.95rem"><?= nl2br(e($n['text'])) ?></p>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($pending_count > 0): ?>
<p style="margin:18px 0 0;color:var(--muted)">
<?= $pending_count === 1 ? '1 Antrag ist noch' : $pending_count . ' Anträge sind noch' ?> <strong style="color:#ffe566">ausstehend</strong>.
</p>
<?php endif; ?>
<?php if (empty($rows)): ?>
<p style="margin-top:32px;color:var(--muted)">Du hast noch keine Anträge gestellt.</p>
<?php else: ?>
<div class="type-tabs" id="type-tabs">
<button class="type-tab active" data-type="all">Alle (<?= count($rows) ?>)</button>
<?php
$types = [];
foreach ($rows as $r) {
$types[$r['type']] = ['label' => $r['type_label'], 'icon' => $r['icon'], 'count' => ($types[$r['type']]['count'] ?? 0) + 1];
}
foreach ($types as $type => $info): ?>
<button class="type-tab" data-type="<?= e($type) ?>"><?= $info['icon'] ?> <?= e($info['label']) ?> (<?= $info['count'] ?>)</button>
<?php endforeach; ?>
</div>
<table class="requests-table" style="margin-top:16px">
<thead>
<tr>
<th>Art</th>
<th>Details</th>
<th>Eingereicht</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr data-type="<?= e($r['type']) ?>">
<td><?= $r['icon'] ?> <?= e($r['type_label']) ?></td>
<td style="color:var(--muted);font-size:.93rem"><?= e($r['detail']) ?></td>
<td style="color:var(--muted);font-size:.88rem;white-space:nowrap"><?= e(date('d.m.Y', strtotime($r['created_at']))) ?></td>
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_map[$r['status']] ?? $r['status']) ?></span></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<script>
document.getElementById('type-tabs').addEventListener('click', function(e) {
const btn = e.target.closest('.type-tab');
if (!btn) return;
this.querySelectorAll('.type-tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const type = btn.dataset.type;
document.querySelectorAll('.requests-table tbody tr').forEach(row => {
row.style.display = (type === 'all' || row.dataset.type === type) ? '' : 'none';
});
});
</script>
<?php endif; ?>
<?php layout_end(); ?>
<div id="pwa-install-banner" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#1a3a5c;color:#fff;padding:.85rem 1rem;z-index:8000;align-items:center;gap:.75rem;box-shadow:0 -2px 12px rgba(0,0,0,.25);">
<span style="flex:1;font-size:.95rem;">📲 <strong>OMSORG App</strong> zum Home-Bildschirm hinzufügen</span>
<button id="pwa-install-confirm" type="button" style="background:#fff;color:#1a3a5c;border:none;border-radius:8px;padding:.45rem 1rem;font-weight:700;cursor:pointer;white-space:nowrap;">Installieren</button>
<button id="pwa-install-dismiss" type="button" style="background:transparent;color:#fff;border:none;font-size:1.2rem;cursor:pointer;padding:0 .25rem;line-height:1;">✕</button>
</div>
<div id="pwa-install-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:flex-end;justify-content:center;padding:1rem;" role="dialog" aria-modal="true">
<div style="background:#fff;border-radius:16px;padding:1.5rem;width:100%;max-width:440px;color:#1a3a5c;">
<p style="margin:0 0 1.25rem;font-weight:700;font-size:1.1rem;text-align:center;">📲 App installieren</p>
<div style="background:#f0f4ff;border-radius:12px;padding:1rem 1.1rem;margin-bottom:.75rem;">
<p style="margin:0 0 .4rem;font-weight:700;font-size:.95rem;">🤖 Android (Chrome)</p>
<p style="margin:0;font-size:.9rem;line-height:1.6;color:#444;">Tippe oben rechts auf das <strong>Menü ⋮</strong> und dann auf<br><strong>„App installieren"</strong> oder <strong>„Zum Startbildschirm hinzufügen"</strong>.</p>
</div>
<div style="background:#f0f4ff;border-radius:12px;padding:1rem 1.1rem;margin-bottom:1.25rem;">
<p style="margin:0 0 .4rem;font-weight:700;font-size:.95rem;">🍎 iPhone / iPad (Safari)</p>
<p style="margin:0;font-size:.9rem;line-height:1.6;color:#444;">Tippe unten auf das <strong>Teilen-Symbol ⬆</strong> und dann auf<br><strong>„Zum Home-Bildschirm"</strong>.</p>
</div>
<button id="pwa-modal-close" style="display:block;width:100%;background:#1a3a5c;color:#fff;border:none;border-radius:8px;padding:.65rem;cursor:pointer;font-size:.95rem;font-weight:600;">Schließen</button>
</div>
</div>
<script>
(function(){
if(window.matchMedia('(display-mode: standalone)').matches)return;
if(document.cookie.split(';').some(c=>c.trim()==='pwa_dismissed=1'))return;
const banner=document.getElementById('pwa-install-banner');
const modal=document.getElementById('pwa-install-modal');
let deferred=null;
window.addEventListener('beforeinstallprompt',function(e){
e.preventDefault();
deferred=e;
});
banner.style.display='flex';
document.getElementById('pwa-install-confirm').addEventListener('click',function(){
if(deferred){
deferred.prompt();
banner.style.display='none';
} else {
banner.style.display='none';
modal.style.display='flex';
}
});
function dismiss(){
banner.style.display='none';
document.cookie='pwa_dismissed=1;path=/;max-age='+60*60*24*365;
}
document.getElementById('pwa-install-dismiss').addEventListener('click',dismiss);
document.getElementById('pwa-modal-close').addEventListener('click',function(){
modal.style.display='none';
dismiss();
});
})();
</script>
+184
View File
@@ -0,0 +1,184 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$user = current_user();
$user_id = $user['id'];
$now = new DateTimeImmutable('today');
$year = isset($_GET['year']) ? (int)$_GET['year'] : (int)$now->format('Y');
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)$now->format('n');
$year = max(2020, min(2099, $year));
$month = max(1, min(12, $month));
$first = new DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
$month_start = $first->format('Y-m-d');
$month_end = $first->modify('last day of this month')->format('Y-m-d');
$days = (int)$first->format('t');
$prev = $first->modify('-1 month');
$next = $first->modify('+1 month');
$prev_url = '?year=' . $prev->format('Y') . '&month=' . $prev->format('n');
$next_url = '?year=' . $next->format('Y') . '&month=' . $next->format('n');
$stmt = db()->prepare(
'SELECT date, schicht FROM dienstplan WHERE user_id = ? AND date >= ? AND date <= ?'
);
$stmt->execute([$user_id, $month_start, $month_end]);
$schichten = [];
foreach ($stmt->fetchAll() as $row) {
$schichten[$row['date']] = $row['schicht'];
}
$stmt = db()->prepare("
SELECT von, bis, 'urlaub' AS typ
FROM requests_urlaubsantrag
WHERE user_id = ? AND status = 'accepted' AND bis >= ? AND von <= ?
UNION ALL
SELECT von, bis, 'abwesenheit' AS typ
FROM requests_abwesenheitsantrag
WHERE user_id = ? AND status = 'accepted' AND bis >= ? AND von <= ?
");
$stmt->execute([$user_id, $month_start, $month_end, $user_id, $month_start, $month_end]);
$overlays = [];
foreach ($stmt->fetchAll() as $row) {
$cursor = new DateTimeImmutable($row['von']);
$end = new DateTimeImmutable($row['bis']);
while ($cursor <= $end) {
$key = $cursor->format('Y-m-d');
if ($key >= $month_start && $key <= $month_end) {
$overlays[$key] = $row['typ'];
}
$cursor = $cursor->modify('+1 day');
}
}
$today_str = $now->format('Y-m-d');
$ea_stmt = db()->prepare('SELECT ort FROM einsatzanweisung WHERE user_id = ?');
$ea_stmt->execute([$user_id]);
$ea_row = $ea_stmt->fetch();
$ea_ort = ($ea_row && $ea_row['ort'] !== '') ? $ea_row['ort'] : '';
$schicht_labels = ['frueh' => 'Früh', 'spaet' => 'Spät', 'nacht' => 'Nacht'];
$weekdays = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
$months_de = ['', 'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'];
layout_start('Dienstplan', 'dienstplan');
?>
<h1 class="main-title">Dienstplan</h1>
<?php if ($ea_ort !== ''): ?>
<p class="dp-ort">&#128205; Einsatzort: <strong><?= e($ea_ort) ?></strong></p>
<?php endif; ?>
<div class="dp-nav">
<div style="display:flex;gap:10px;align-items:center">
<a href="<?= e($prev_url) ?>" class="dp-nav-btn">&larr;</a>
<h2 class="dp-nav-title"><?= e($months_de[$month] . ' ' . $year) ?></h2>
<a href="<?= e($next_url) ?>" class="dp-nav-btn">&rarr;</a>
</div>
<span id="dp-status" style="font-size:.85rem;color:var(--muted);transition:opacity .4s"></span>
</div>
<div class="dp-grid-wrap">
<div class="dp-grid">
<?php foreach ($weekdays as $wd): ?>
<div class="dp-weekday"><?= e($wd) ?></div>
<?php endforeach; ?>
<?php
$lead = (int)$first->format('N') - 1;
for ($i = 0; $i < $lead; $i++): ?>
<div class="dp-cell dp-cell-empty"></div>
<?php endfor; ?>
<?php for ($d = 1; $d <= $days; $d++):
$date_key = sprintf('%04d-%02d-%02d', $year, $month, $d);
$is_today = ($date_key === $today_str);
$overlay = $overlays[$date_key] ?? null;
$schicht = $schichten[$date_key] ?? 'leer';
$cell_cls = 'dp-cell';
if (!$overlay && $schicht !== 'leer') $cell_cls .= ' dp-cell-' . $schicht;
if ($is_today) $cell_cls .= ' dp-cell-today';
?>
<div class="<?= $cell_cls ?>">
<span class="dp-day-num"><?= $d ?></span>
<?php if ($overlay): ?>
<span class="dp-badge dp-overlay-<?= e($overlay) ?>">
<?= $overlay === 'urlaub' ? 'Urlaub' : 'Abwesenheit' ?>
</span>
<?php if ($schicht !== 'leer'): ?>
<span class="dp-badge dp-badge-<?= e($schicht) ?>" style="opacity:.45">
<?= e($schicht_labels[$schicht]) ?>
</span>
<?php endif; ?>
<?php else: ?>
<select data-date="<?= e($date_key) ?>" class="dp-select">
<option value="leer" <?= $schicht === 'leer' ? 'selected' : '' ?>>Frei</option>
<option value="frueh" <?= $schicht === 'frueh' ? 'selected' : '' ?>>Früh</option>
<option value="spaet" <?= $schicht === 'spaet' ? 'selected' : '' ?>>Spät</option>
<option value="nacht" <?= $schicht === 'nacht' ? 'selected' : '' ?>>Nacht</option>
</select>
<?php endif; ?>
</div>
<?php endfor; ?>
<?php
$total = $lead + $days;
$trail = (7 - ($total % 7)) % 7;
for ($i = 0; $i < $trail; $i++): ?>
<div class="dp-cell dp-cell-empty"></div>
<?php endfor; ?>
</div>
</div>
<script>
(function(){
var YEAR = <?= (int)$year ?>;
var MONTH = <?= (int)$month ?>;
function colorCell(sel){
var cell = sel.closest('.dp-cell');
cell.classList.remove('dp-cell-frueh','dp-cell-spaet','dp-cell-nacht');
if(sel.value !== 'leer') cell.classList.add('dp-cell-' + sel.value);
}
var statusEl = document.getElementById('dp-status');
var statusTimer;
function showStatus(ok){
clearTimeout(statusTimer);
statusEl.style.opacity = '1';
statusEl.style.color = ok ? '#6eff9a' : '#ff9a9a';
statusEl.textContent = ok ? 'Gespeichert' : 'Fehler beim Speichern';
statusTimer = setTimeout(function(){ statusEl.style.opacity = '0'; }, 1800);
}
document.querySelectorAll('.dp-select').forEach(function(sel){
colorCell(sel);
sel.addEventListener('change', function(){
var date = this.dataset.date;
var schicht = this.value;
colorCell(this);
var body = new URLSearchParams();
body.append('year', YEAR);
body.append('month', MONTH);
body.append('schicht[' + date + ']', schicht);
body.append('_ajax', '1');
body.append('csrf_token', document.querySelector('meta[name="csrf-token"]').content);
fetch('../actions/save-dienstplan.php', {method:'POST', body:body})
.then(function(r){ return r.json(); })
.then(function(d){ showStatus(d.ok); })
.catch(function(){ showStatus(false); });
});
});
})();
</script>
<?php
layout_end();
+34
View File
@@ -0,0 +1,34 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_login();
$id = (int) ($_GET['id'] ?? 0);
if ($id <= 0) { http_response_code(400); echo 'Ungültige Anfrage.'; exit; }
$stmt = db()->prepare('SELECT * FROM dokumente WHERE id = ?');
$stmt->execute([$id]);
$doc = $stmt->fetch();
if (!$doc) { http_response_code(404); echo 'Dokument nicht gefunden.'; exit; }
$user = current_user();
if ((int) $doc['user_id'] !== (int) $user['id'] && !is_admin()) {
http_response_code(403);
echo 'Keine Berechtigung.';
exit;
}
$path = dirname(__DIR__) . '/uploads/' . $doc['filename'];
if (!is_file($path)) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
$display = $doc['original_name'];
$safe_display = addslashes($display);
$safe_encoded = rawurlencode($display);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $safe_display . '"; filename*=UTF-8\'\'' . $safe_encoded);
header('Content-Length: ' . filesize($path));
header('X-Content-Type-Options: nosniff');
header('Cache-Control: private, no-store');
readfile($path);
exit;
+110
View File
@@ -0,0 +1,110 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$user_id = current_user()['id'];
$admin = is_admin();
if ($admin) {
$docs = db()->query(
'SELECT d.*, u.name AS user_name
FROM dokumente d
JOIN users u ON u.id = d.user_id
ORDER BY d.created_at DESC'
)->fetchAll();
} else {
$stmt = db()->prepare('SELECT * FROM dokumente WHERE user_id = ? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
$docs = $stmt->fetchAll();
}
layout_start('OMSORG Dokumentenarchiv', 'dokumentenarchiv');
?>
<h1 class="main-title">🗂 Dokumenten&shy;archiv</h1>
<p class="main-sub">Eigene Nachweise und Dokumente hochladen und verwalten.</p>
<?php if (isset($_GET['ok'])): ?>
<div class="success" style="margin-top:16px">Dokument erfolgreich hochgeladen.</div>
<?php elseif (isset($_GET['deleted'])): ?>
<div class="success" style="margin-top:16px">Dokument wurde gelöscht.</div>
<?php elseif (isset($_GET['error'])): ?>
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
<?php endif; ?>
<div class="page-split" style="margin-top:24px">
<div class="page-split-list">
<h2><?= $admin ? 'Alle Dokumente' : 'Meine Dokumente' ?></h2>
<?php if (empty($docs)): ?>
<p style="color:var(--muted)">Noch keine Dokumente vorhanden.</p>
<?php else: ?>
<table class="requests-table">
<thead>
<tr>
<?php if ($admin): ?><th>Mitarbeiter</th><?php endif; ?>
<th>Kategorie</th>
<th>Beschreibung</th>
<th>Datei</th>
<th>Hochgeladen</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($docs as $d): ?>
<tr>
<?php if ($admin): ?>
<td style="font-weight:800"><?= e($d['user_name']) ?></td>
<?php endif; ?>
<td><?= e($d['kategorie']) ?></td>
<td style="color:var(--muted);font-size:.9rem"><?= $d['beschreibung'] !== '' ? e($d['beschreibung']) : '' ?></td>
<td>
<a href="dokument-serve.php?id=<?= (int)$d['id'] ?>" class="btn download-btn" style="padding:6px 14px;font-size:.85rem">&#8595; <?= e($d['original_name']) ?></a>
</td>
<td style="color:var(--muted);font-size:.9rem"><?= e(date('d.m.Y', strtotime($d['created_at']))) ?></td>
<td>
<form method="post" action="../actions/delete-dokument.php" onsubmit="return confirm('Dokument wirklich löschen?')">
<?= csrf_field() ?>
<input type="hidden" name="id" value="<?= (int)$d['id'] ?>">
<button type="submit" class="btn" style="background:rgba(220,50,50,.25);padding:6px 14px;font-size:.85rem">Löschen</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class="page-split-form">
<div class="glass detail-card">
<h2>Dokument hochladen</h2>
<form action="../actions/upload-dokument.php" method="post" enctype="multipart/form-data">
<?= csrf_field() ?>
<label>
<span>Kategorie <span style="color:#ff8080">*</span></span>
<select name="kategorie" required>
<option value=""> Bitte wählen </option>
<option value="Fortbildungsnachweis">Fortbildungsnachweis</option>
<option value="Zeugnis">Zeugnis</option>
<option value="Bescheinigung">Bescheinigung</option>
<option value="Sonstiges">Sonstiges</option>
</select>
</label>
<label>
Beschreibung
<input type="text" name="beschreibung" placeholder="z. B. Erste-Hilfe-Kurs 2025">
</label>
<label>
<span>Datei <span style="color:#ff8080">*</span></span>
<input type="file" name="datei" accept=".pdf,.doc,.docx,.jpg,.jpeg,.png" required>
<span class="hint">PDF, Word, JPG, PNG &mdash; max. 12 MB</span>
</label>
<button type="submit" class="btn" style="margin-top:6px">Hochladen</button>
</form>
</div>
</div>
</div>
<?php layout_end(); ?>
+25
View File
@@ -0,0 +1,25 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_login();
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) { http_response_code(400); echo 'Ungültige Anfrage.'; exit; }
$stmt = db()->prepare('SELECT filename, original_name FROM downloads WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
$path = dirname(__DIR__) . '/downloads/' . $row['filename'];
if (!is_file($path)) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
$safe_display = addslashes($row['original_name']);
$safe_encoded = rawurlencode($row['original_name']);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $safe_display . '"; filename*=UTF-8\'\'' . $safe_encoded);
header('Content-Length: ' . filesize($path));
header('X-Content-Type-Options: nosniff');
header('Cache-Control: private, no-store');
readfile($path);
exit;
+28
View File
@@ -0,0 +1,28 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$downloads = db()->query('SELECT * FROM downloads ORDER BY sort_order ASC, id ASC')->fetchAll();
layout_start('OMSORG Downloads', 'downloads');
?>
<h1 class="main-title">Down<span>loads</span></h1>
<p class="main-sub">Dokumente und Formulare zum Herunterladen.</p>
<?php if (empty($downloads)): ?>
<p style="color:var(--muted);margin-top:40px">Noch keine Downloads verfügbar.</p>
<?php else: ?>
<div class="downloads-grid">
<?php foreach ($downloads as $dl): ?>
<div class="glass download-card">
<h3 class="download-card-title"><?= e($dl['title']) ?></h3>
<?php if ($dl['description'] !== ''): ?>
<p class="download-card-desc"><?= e($dl['description']) ?></p>
<?php endif; ?>
<a href="download-serve.php?id=<?= (int)$dl['id'] ?>" class="btn download-btn">&#8595; Herunterladen</a>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php layout_end(); ?>
@@ -0,0 +1,40 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_login();
$current = current_user();
if (is_admin() && isset($_GET['user_id'])) {
$target_id = (int)$_GET['user_id'];
} else {
$target_id = (int)$current['id'];
}
$stmt = db()->prepare('SELECT * FROM einsatzanweisung WHERE user_id = ?');
$stmt->execute([$target_id]);
$ea = $stmt->fetch();
if (!$ea || $ea['filename'] === '') {
http_response_code(404);
echo 'Keine Datei gefunden.';
exit;
}
$path = dirname(__DIR__) . '/uploads/' . basename($ea['filename']);
if (!is_file($path)) {
http_response_code(404);
echo 'Datei nicht gefunden.';
exit;
}
$display = $ea['original_name'];
$safe_display = addslashes($display);
$safe_encoded = rawurlencode($display);
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' . $safe_display . '"; filename*=UTF-8\'\'' . $safe_encoded);
header('Content-Length: ' . filesize($path));
header('X-Content-Type-Options: nosniff');
header('Cache-Control: private, no-store');
readfile($path);
exit;
@@ -0,0 +1,42 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$user_id = current_user()['id'];
$stmt = db()->prepare('SELECT * FROM einsatzanweisung WHERE user_id = ?');
$stmt->execute([$user_id]);
$ea = $stmt->fetch();
layout_start('OMSORG Einsatzanweisung', 'einsatzanweisung');
?>
<h1 class="main-title">Einsatz<span>anweisung</span></h1>
<p class="main-sub">Deine persönliche Einsatzanweisung als PDF-Dokument.</p>
<?php if (!$ea || ($ea['filename'] === '' && $ea['ort'] === '')): ?>
<p style="color:var(--muted);margin-top:40px">Noch keine Einsatzanweisung hinterlegt.</p>
<?php else: ?>
<div class="glass" style="max-width:520px;padding:28px;border-radius:20px;margin-top:24px">
<?php if ($ea['ort'] !== ''): ?>
<div style="margin-bottom:24px">
<div style="color:var(--cyan2);font-size:.78rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;margin-bottom:6px">Einsatzort</div>
<div style="font-size:1.15rem;font-weight:800"><?= e($ea['ort']) ?></div>
</div>
<?php endif; ?>
<?php if ($ea['filename'] !== ''): ?>
<div>
<div style="color:var(--cyan2);font-size:.78rem;font-weight:900;text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px">Dokument</div>
<a href="einsatzanweisung-serve.php" class="btn" style="display:inline-flex;align-items:center;gap:8px">
&#8595; <?= e($ea['original_name']) ?>
</a>
<?php if ($ea['uploaded_at'] !== ''): ?>
<div style="color:var(--muted);font-size:.82rem;margin-top:8px">
Hochgeladen: <?= e(date('d.m.Y', strtotime($ea['uploaded_at']))) ?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<?php endif; ?>
<?php layout_end(); ?>
+181
View File
@@ -0,0 +1,181 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$user_id = current_user()['id'];
$stmt = db()->prepare('SELECT * FROM einsatzbewertungen WHERE user_id = ? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
$bewertungen = $stmt->fetchAll();
$extra_head = <<<'CSS'
<style>
.star-rating{display:flex;flex-direction:row-reverse;gap:4px;justify-content:flex-end}
.star-rating input{display:none}
.star-rating label{
font-size:2rem;cursor:pointer;color:rgba(170,240,250,.25);
transition:color .1s;line-height:1;
}
.star-rating input:checked ~ label,
.star-rating label:hover,
.star-rating label:hover ~ label{color:#f5c518}
.star-display{color:#f5c518;letter-spacing:2px;font-size:1.1rem}
.star-display .empty{color:rgba(170,240,250,.2)}
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:1000}
.modal-box{background:var(--glass-bg,#1e2235);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:28px 32px;max-width:480px;width:90%;position:relative}
.modal-box h3{margin:0 0 20px;font-family:'KindelSerif',Georgia,serif;font-size:1.3rem}
.modal-close{position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted,#888);line-height:1}
.modal-dl{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:.95rem}
.modal-dl dt{color:var(--muted,#888);white-space:nowrap}
.modal-dl dd{margin:0;font-weight:500}
.btn-sm{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-weight:600;font-size:.82rem;cursor:pointer;font:inherit;transition:background .15s}
.btn-sm:hover{background:rgba(255,255,255,.15)}
</style>
CSS;
layout_start('OMSORG Einsatzbewertung', 'einsatzbewertung', $extra_head);
?>
<h1 class="main-title">⭐ Einsatzbewertung</h1>
<p class="main-sub">Bewerte deinen Einsatzort. Dein Feedback hilft uns, die Einsatzplanung zu verbessern.</p>
<?php if (isset($_GET['success'])): ?>
<div class="success" style="margin-top:16px">Bewertung erfolgreich abgegeben.</div>
<?php elseif (isset($_GET['error'])): ?>
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
<?php endif; ?>
<div id="bew-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
<div class="modal-box glass">
<button class="modal-close" onclick="document.getElementById('bew-modal').style.display='none'">&times;</button>
<h3>Bewertung Details</h3>
<dl class="modal-dl">
<dt>Einsatzort</dt> <dd id="bew-ort"></dd>
<dt>Von</dt> <dd id="bew-von"></dd>
<dt>Bis</dt> <dd id="bew-bis"></dd>
<dt>Bewertung</dt> <dd id="bew-sterne"></dd>
<dt>Wieder?</dt> <dd id="bew-wieder"></dd>
<dt>Feedback</dt> <dd id="bew-feedback"></dd>
<dt>Datum</dt> <dd id="bew-datum"></dd>
</dl>
</div>
</div>
<div class="page-split">
<div class="page-split-list">
<h2>Meine Bewertungen</h2>
<?php if (empty($bewertungen)): ?>
<p style="color:var(--muted)">Noch keine Bewertungen abgegeben.</p>
<?php else: ?>
<table class="requests-table">
<thead>
<tr>
<th>Einsatzort</th>
<th>Bewertung</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($bewertungen as $b): ?>
<tr>
<td style="font-weight:800"><?= e($b['einsatzort']) ?></td>
<td>
<span class="star-display">
<?php for ($i = 1; $i <= 5; $i++): ?>
<span<?= $i > $b['bewertung'] ? ' class="empty"' : '' ?>>★</span>
<?php endfor; ?>
</span>
</td>
<td>
<button class="btn-sm js-bew-details"
data-ort="<?= e($b['einsatzort']) ?>"
data-von="<?= e(date('d.m.Y', strtotime($b['von']))) ?>"
data-bis="<?= e(date('d.m.Y', strtotime($b['bis']))) ?>"
data-bewertung="<?= (int)$b['bewertung'] ?>"
data-wieder="<?= (int)$b['wieder'] ?>"
data-feedback="<?= e($b['feedback'] !== '' ? $b['feedback'] : '') ?>"
data-datum="<?= e(date('d.m.Y', strtotime($b['created_at']))) ?>"
>Details</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class="page-split-form">
<div class="glass detail-card">
<h2>Neue Bewertung</h2>
<form action="../actions/submit-einsatzbewertung.php" method="post">
<?= csrf_field() ?>
<label>
<span>Einsatzort / Einrichtung <span style="color:#ff8080">*</span></span>
<input type="text" name="einsatzort" required placeholder="z. B. Pflegeheim Sonnenhof">
</label>
<label>
<span>Von <span style="color:#ff8080">*</span></span>
<input type="date" name="von" required>
</label>
<label>
<span>Bis <span style="color:#ff8080">*</span></span>
<input type="date" name="bis" required>
</label>
<div style="margin-bottom:14px">
<span style="display:block;margin-bottom:8px;font-weight:700">
Bewertung <span style="color:#ff8080">*</span>
</span>
<div class="star-rating">
<input type="radio" id="star5" name="bewertung" value="5" required>
<label for="star5" title="5 Sterne">★</label>
<input type="radio" id="star4" name="bewertung" value="4">
<label for="star4" title="4 Sterne">★</label>
<input type="radio" id="star3" name="bewertung" value="3">
<label for="star3" title="3 Sterne">★</label>
<input type="radio" id="star2" name="bewertung" value="2">
<label for="star2" title="2 Sterne">★</label>
<input type="radio" id="star1" name="bewertung" value="1">
<label for="star1" title="1 Stern">★</label>
</div>
</div>
<div style="margin-bottom:14px">
<label style="display:flex;align-items:center;gap:10px;flex-direction:row;font-weight:700;cursor:pointer">
<input type="checkbox" name="wieder" value="1" style="width:18px;height:18px;cursor:pointer">
Würdest du wieder dort arbeiten?
</label>
</div>
<label>
Feedback
<textarea name="feedback" rows="4" placeholder="Optionale Anmerkungen zu diesem Einsatz"></textarea>
</label>
<button type="submit" class="btn" style="margin-top:6px">Bewertung abgeben</button>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('click', function(e) {
var btn = e.target.closest('.js-bew-details');
if (!btn) return;
var d = btn.dataset;
document.getElementById('bew-ort').textContent = d.ort;
document.getElementById('bew-von').textContent = d.von;
document.getElementById('bew-bis').textContent = d.bis;
var n = parseInt(d.bewertung, 10);
var stars = '';
for (var i = 1; i <= 5; i++) stars += '<span' + (i > n ? ' class="empty"' : '') + '>★</span>';
document.getElementById('bew-sterne').innerHTML = '<span class="star-display">' + stars + '</span>';
document.getElementById('bew-wieder').innerHTML =
parseInt(d.wieder) ? '<span style="color:#6eff9a;font-weight:800">Ja</span>' : '<span style="color:var(--muted)">Nein</span>';
document.getElementById('bew-feedback').textContent = d.feedback;
document.getElementById('bew-datum').textContent = d.datum;
document.getElementById('bew-modal').style.display = 'flex';
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') document.getElementById('bew-modal').style.display = 'none';
});
</script>
<?php layout_end(); ?>
@@ -0,0 +1,25 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_login();
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) { http_response_code(400); echo 'Ungültige Anfrage.'; exit; }
$stmt = db()->prepare('SELECT filename, original_name FROM fortbildung_materials WHERE id = ?');
$stmt->execute([$id]);
$row = $stmt->fetch();
if (!$row) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
$path = dirname(__DIR__) . '/fortbildung-materials/' . $row['filename'];
if (!is_file($path)) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
$safe_display = addslashes($row['original_name']);
$safe_encoded = rawurlencode($row['original_name']);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $safe_display . '"; filename*=UTF-8\'\'' . $safe_encoded);
header('Content-Length: ' . filesize($path));
header('X-Content-Type-Options: nosniff');
header('Cache-Control: private, no-store');
readfile($path);
exit;
@@ -0,0 +1,116 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$user_id = current_user()['id'];
$materials = db()->query('SELECT * FROM fortbildung_materials ORDER BY sort_order ASC, id ASC')->fetchAll();
$stmt = db()->prepare('SELECT * FROM requests_fortbildungsantrag WHERE user_id = ? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
$requests = $stmt->fetchAll();
$anliegen_labels = [
'wbl_pdl' => 'Anfrage WBL/PDL Weiterbildung',
'fortbildung' => 'Fortbildung anfragen',
'pflichtschulung' => 'Pflichtschulung',
];
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
layout_start('OMSORG Fortbildungsantrag', 'fortbildungsantrag');
?>
<h1 class="main-title">🎓 Fortbildungs&shy;antrag</h1>
<p class="main-sub">Unterlagen herunterladen und Fortbildungsanträge einreichen.</p>
<?php if (isset($_GET['success'])): ?>
<div class="success" style="margin-top:16px">Antrag erfolgreich eingereicht.</div>
<?php elseif (isset($_GET['error'])): ?>
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
<?php endif; ?>
<div class="page-split" style="margin-top:24px">
<div class="page-split-list">
<h2>Unterlagen</h2>
<?php if (empty($materials)): ?>
<p style="color:var(--muted)">Noch keine Unterlagen verfügbar.</p>
<?php else: ?>
<div class="downloads-grid">
<?php foreach ($materials as $m): ?>
<div class="glass download-card">
<h3 class="download-card-title"><?= e($m['title']) ?></h3>
<?php if ($m['description'] !== ''): ?>
<p class="download-card-desc"><?= e($m['description']) ?></p>
<?php endif; ?>
<a href="fortbildung-serve.php?id=<?= (int)$m['id'] ?>" class="btn download-btn">&#8595; Herunterladen</a>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div class="page-split-form">
<div class="glass detail-card">
<h2>Neuer Antrag</h2>
<form action="../actions/submit-fortbildungsantrag.php" method="post" enctype="multipart/form-data">
<?= csrf_field() ?>
<label>
<span>Anliegen <span style="color:#ff8080">*</span></span>
<select name="anliegen" required>
<option value=""> Bitte wählen </option>
<option value="wbl_pdl">Anfrage WBL/PDL Weiterbildung</option>
<option value="fortbildung">Fortbildung anfragen</option>
<option value="pflichtschulung">Pflichtschulung</option>
</select>
</label>
<label>
<span>Thema <span style="color:#ff8080">*</span></span>
<input type="text" name="thema" required placeholder="z. B. Erste Hilfe, Demenzpflege …">
</label>
<label>
Nachricht
<textarea name="nachricht" rows="4" placeholder="Weitere Informationen oder Wünsche"></textarea>
</label>
<label>
Anhang
<input type="file" name="datei" accept=".pdf,.doc,.docx,.jpg,.jpeg,.png">
<span class="hint">PDF, Word, JPG, PNG &mdash; max. 12 MB (optional)</span>
</label>
<button type="submit" class="btn" style="margin-top:6px">Antrag einreichen</button>
</form>
</div>
</div>
</div>
<?php if (!empty($requests)): ?>
<div style="margin-top:40px">
<h2>Meine Anträge</h2>
<table class="requests-table">
<thead>
<tr>
<th>Anliegen</th>
<th>Thema</th>
<th>Eingereicht</th>
<th>Status</th>
<th>Notiz</th>
</tr>
</thead>
<tbody>
<?php foreach ($requests as $r): ?>
<tr>
<td><?= e($anliegen_labels[$r['anliegen']] ?? $r['anliegen']) ?></td>
<td style="font-weight:800"><?= e($r['thema']) ?></td>
<td style="color:var(--muted);font-size:.9rem"><?= e(date('d.m.Y', strtotime($r['created_at']))) ?></td>
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
<td style="color:var(--muted);font-size:.9rem"><?= $r['admin_note'] !== '' ? e($r['admin_note']) : '' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php layout_end(); ?>
+134
View File
@@ -0,0 +1,134 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$username = current_username();
$user = current_user();
$saved = isset($_GET['saved']);
$pwchanged = isset($_GET['pwchanged']);
$avatar_saved = isset($_GET['avatar_saved']);
$error = isset($_GET['error']) ? urldecode($_GET['error']) : '';
layout_start('OMSORG Einstellungen', 'settings');
?>
<h1 class="main-title">Einstellungen</h1>
<p class="main-sub">Deine persönlichen Daten und Passwort.</p>
<?php if ($saved): ?>
<div class="success" style="margin-top:20px">Profil erfolgreich gespeichert.</div>
<?php elseif ($pwchanged): ?>
<div class="success" style="margin-top:20px">Passwort erfolgreich geändert.</div>
<?php elseif ($avatar_saved): ?>
<div class="success" style="margin-top:20px">Profilbild erfolgreich gespeichert.</div>
<?php elseif ($error): ?>
<div class="error" style="margin-top:20px"><?= e($error) ?></div>
<?php endif; ?>
<!-- Profilbild -->
<div class="settings-section">
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 18px">Profilbild</h2>
<div style="display:flex;align-items:center;gap:24px;flex-wrap:wrap;margin-bottom:20px">
<?php if (!empty($user['avatar'])): ?>
<img id="avatar-preview" src="../assets/avatars/<?= e($user['avatar']) ?>"
alt="Profilbild" class="settings-avatar-preview">
<?php else: ?>
<div id="avatar-preview-placeholder" class="settings-avatar-placeholder">
<?= e(mb_substr($user['name'], 0, 1)) ?>
</div>
<img id="avatar-preview" src="" alt="Vorschau" class="settings-avatar-preview"
style="display:none">
<?php endif; ?>
<div>
<form action="../actions/upload-avatar.php" method="post" enctype="multipart/form-data"
style="display:flex;flex-direction:column;gap:10px;align-items:flex-start">
<?= csrf_field() ?>
<label class="btn" style="cursor:pointer;margin:0">
Bild auswählen
<input type="file" name="avatar" accept="image/*" id="avatar-input"
style="display:none" onchange="previewAvatar(this)">
</label>
<button type="submit" class="btn" id="avatar-upload-btn" style="display:none">
Hochladen
</button>
</form>
<?php if (!empty($user['avatar'])): ?>
<form action="../actions/upload-avatar.php" method="post" style="margin-top:8px">
<?= csrf_field() ?>
<input type="hidden" name="remove_avatar" value="1">
<button type="submit" class="btn btn-ghost" style="font-size:.85rem;padding:6px 14px">
Bild entfernen
</button>
</form>
<?php endif; ?>
</div>
</div>
<p style="color:var(--muted);font-size:.82rem;margin:0">JPG, PNG, GIF oder WebP · max. 3 MB</p>
</div>
<!-- Profildaten -->
<div class="settings-section">
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 18px">Profildaten</h2>
<form action="../actions/save-profile.php" method="post" style="max-width:480px">
<?= csrf_field() ?>
<input type="hidden" name="action" value="profile">
<label>
Name
<input type="text" value="<?= e($user['name']) ?>" disabled>
</label>
<label>
Benutzername
<input type="text" value="<?= e($username) ?>" disabled>
</label>
<label>
E-Mail
<input type="email" name="email" autocomplete="email"
value="<?= e($user['email'] ?? '') ?>">
</label>
<label>
Telefon
<input type="tel" name="telefon" autocomplete="tel"
value="<?= e($user['telefon'] ?? '') ?>">
</label>
<button type="submit" class="btn">Speichern</button>
</form>
</div>
<!-- Passwort ändern -->
<div class="settings-section">
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 18px">Passwort ändern</h2>
<form action="../actions/save-profile.php" method="post" style="max-width:480px">
<?= csrf_field() ?>
<input type="hidden" name="action" value="password">
<label>
Aktuelles Passwort
<input type="password" name="old_password" autocomplete="current-password" required>
</label>
<label>
Neues Passwort
<input type="password" name="new_password" autocomplete="new-password" required minlength="8">
</label>
<label>
Neues Passwort wiederholen
<input type="password" name="new_password_repeat" autocomplete="new-password" required minlength="8">
</label>
<button type="submit" class="btn">Passwort ändern</button>
</form>
</div>
<script>
function previewAvatar(input) {
if (!input.files || !input.files[0]) return;
const reader = new FileReader();
reader.onload = e => {
const img = document.getElementById('avatar-preview');
const ph = document.getElementById('avatar-preview-placeholder');
img.src = e.target.result;
img.style.display = '';
if (ph) ph.style.display = 'none';
document.getElementById('avatar-upload-btn').style.display = '';
};
reader.readAsDataURL(input.files[0]);
}
</script>
<?php layout_end(); ?>
+85
View File
@@ -0,0 +1,85 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$user_id = current_user()['id'];
$stmt = db()->prepare('SELECT * FROM requests_stundennachweis WHERE user_id = ? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
$requests = $stmt->fetchAll();
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
layout_start('OMSORG Stundennachweis', 'stundennachweis');
?>
<h1 class="main-title">🕐 Stundennachweis</h1>
<p class="main-sub">Monatlichen Stundennachweis einreichen.</p>
<?php if (isset($_GET['success'])): ?>
<div class="success" style="margin-top:16px">Nachweis erfolgreich eingereicht.</div>
<?php elseif (isset($_GET['error'])): ?>
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
<?php endif; ?>
<div class="glass detail-card" style="margin-top:24px;max-width:480px">
<h2>Nachweis einreichen</h2>
<form action="../actions/submit-stundennachweis.php" method="post" enctype="multipart/form-data">
<?= csrf_field() ?>
<div class="monat-grid">
<label>
<span>Monat <span style="color:#ff8080">*</span></span>
<select name="monat_m" required>
<option value=""> Monat </option>
<?php foreach (['01'=>'Januar','02'=>'Februar','03'=>'März','04'=>'April','05'=>'Mai','06'=>'Juni','07'=>'Juli','08'=>'August','09'=>'September','10'=>'Oktober','11'=>'November','12'=>'Dezember'] as $v => $l): ?>
<option value="<?= $v ?>"><?= $l ?></option>
<?php endforeach; ?>
</select>
</label>
<label>
<span>Jahr <span style="color:#ff8080">*</span></span>
<input type="number" name="monat_j" min="2020" max="2099"
value="<?= date('Y') ?>" required style="width:100%">
</label>
</div>
<label>
<span>Nachweis <span style="color:#ff8080">*</span></span>
<input type="file" name="datei" accept=".pdf,.jpg,.jpeg,.png" required>
<span class="hint">PDF, JPG, PNG &mdash; max. 12 MB</span>
</label>
<button type="submit" class="btn" style="margin-top:6px">Einreichen</button>
</form>
</div>
<?php if (!empty($requests)): ?>
<div style="margin-top:40px">
<h2>Meine Nachweise</h2>
<table class="requests-table">
<thead>
<tr>
<th>Monat</th>
<th>Datei</th>
<th>Eingereicht</th>
<th>Status</th>
<th>Notiz</th>
</tr>
</thead>
<tbody>
<?php foreach ($requests as $r): ?>
<tr>
<td style="font-weight:800"><?= e(date('F Y', strtotime($r['monat'] . '-01'))) ?></td>
<td>
<a href="upload-serve.php?file=<?= e(rawurlencode($r['filename'])) ?>&name=<?= e(rawurlencode($r['original_name'])) ?>"
style="color:var(--cyan2)"><?= e($r['original_name']) ?></a>
</td>
<td style="color:var(--muted);font-size:.9rem"><?= e(date('d.m.Y', strtotime($r['created_at']))) ?></td>
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
<td style="color:var(--muted);font-size:.9rem"><?= $r['admin_note'] !== '' ? e($r['admin_note']) : '' ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<?php layout_end(); ?>
+21
View File
@@ -0,0 +1,21 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_admin();
$file = basename($_GET['file'] ?? '');
if ($file === '') { http_response_code(400); echo 'Ungültige Anfrage.'; exit; }
$path = dirname(__DIR__) . '/uploads/' . $file;
if (!is_file($path)) { http_response_code(404); echo 'Datei nicht gefunden.'; exit; }
$display = $_GET['name'] ?? $file;
$safe_display = addslashes($display);
$safe_encoded = rawurlencode($display);
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $safe_display . '"; filename*=UTF-8\'\'' . $safe_encoded);
header('Content-Length: ' . filesize($path));
header('X-Content-Type-Options: nosniff');
header('Cache-Control: private, no-store');
readfile($path);
exit;
+142
View File
@@ -0,0 +1,142 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$user_id = current_user()['id'];
$stmt = db()->prepare('SELECT * FROM requests_urlaubsantrag WHERE user_id = ? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
$requests = $stmt->fetchAll();
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
layout_start('OMSORG Urlaubsantrag', 'urlaubsantrag');
?>
<style>
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:1000}
.modal-box{background:var(--glass-bg,#1e2235);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:28px 32px;max-width:480px;width:90%;position:relative}
.modal-box h3{margin:0 0 20px;font-family:'KindelSerif',Georgia,serif;font-size:1.3rem}
.modal-close{position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted,#888);line-height:1}
.modal-dl{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:.95rem}
.modal-dl dt{color:var(--muted,#888);white-space:nowrap}
.modal-dl dd{margin:0;font-weight:500}
.btn-sm{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-weight:600;font-size:.82rem;cursor:pointer;font:inherit;transition:background .15s}
.btn-sm:hover{background:rgba(255,255,255,.15)}
</style>
<h1 class="main-title">🌴 Urlaubsantrag</h1>
<p class="main-sub">Beantrage deinen Urlaub. Das Startdatum muss vor dem Enddatum liegen.</p>
<?php if (isset($_GET['success'])): ?>
<div class="success" style="margin-top:16px">Antrag erfolgreich eingereicht.</div>
<?php elseif (isset($_GET['error'])): ?>
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
<?php endif; ?>
<!-- Details-Modal -->
<div id="urlaub-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
<div class="modal-box glass">
<button class="modal-close" onclick="document.getElementById('urlaub-modal').style.display='none'">&times;</button>
<h3>Antrag Details</h3>
<dl class="modal-dl">
<dt>Von</dt> <dd id="md-von"></dd>
<dt>Bis</dt> <dd id="md-bis"></dd>
<dt>Vertretung</dt><dd id="md-vertretung"></dd>
<dt>Nachricht</dt> <dd id="md-nachricht"></dd>
<dt>Eingereicht</dt><dd id="md-eingereicht"></dd>
<dt>Status</dt> <dd id="md-status"></dd>
<dt>Admin-Notiz</dt><dd id="md-note"></dd>
</dl>
</div>
</div>
<div class="page-split">
<div class="page-split-list">
<h2>Meine Anträge</h2>
<?php if (empty($requests)): ?>
<p style="color:var(--muted)">Noch keine Anträge gestellt.</p>
<?php else: ?>
<table class="requests-table">
<thead>
<tr>
<th>Von</th>
<th>Bis</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($requests as $r): ?>
<tr>
<td><?= e(date('d.m.Y', strtotime($r['von']))) ?></td>
<td><?= e(date('d.m.Y', strtotime($r['bis']))) ?></td>
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
<td>
<button class="btn-sm js-urlaub-details"
data-von="<?= e(date('d.m.Y', strtotime($r['von']))) ?>"
data-bis="<?= e(date('d.m.Y', strtotime($r['bis']))) ?>"
data-vertretung="<?= e($r['vertretung'] !== '' ? $r['vertretung'] : '') ?>"
data-nachricht="<?= e($r['nachricht'] !== '' ? $r['nachricht'] : '') ?>"
data-eingereicht="<?= e(date('d.m.Y', strtotime($r['created_at']))) ?>"
data-status-label="<?= e($status_labels[$r['status']] ?? $r['status']) ?>"
data-status-key="<?= e($r['status']) ?>"
data-note="<?= e($r['admin_note'] !== '' ? $r['admin_note'] : '') ?>"
>Details</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class="page-split-form">
<div class="glass detail-card">
<h2>Neuer Urlaubsantrag</h2>
<form action="../actions/submit-urlaubsantrag.php" method="post">
<?= csrf_field() ?>
<label>
<span>Von <span style="color:#ff8080">*</span></span>
<input type="date" name="von" required>
</label>
<label>
<span>Bis <span style="color:#ff8080">*</span></span>
<input type="date" name="bis" required>
</label>
<label>
Vertretung
<input type="text" name="vertretung" placeholder="Name der Vertretung">
</label>
<label>
Nachricht
<textarea name="nachricht" rows="4" placeholder="Optionale Anmerkungen"></textarea>
</label>
<button type="submit" class="btn" style="margin-top:6px">Antrag einreichen</button>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('click', function(e) {
var btn = e.target.closest('.js-urlaub-details');
if (!btn) return;
var d = btn.dataset;
document.getElementById('md-von').textContent = d.von;
document.getElementById('md-bis').textContent = d.bis;
document.getElementById('md-vertretung').textContent = d.vertretung;
document.getElementById('md-nachricht').textContent = d.nachricht;
document.getElementById('md-eingereicht').textContent = d.eingereicht;
document.getElementById('md-status').innerHTML =
'<span class="badge badge-' + d.statusKey + '">' + d.statusLabel + '</span>';
document.getElementById('md-note').textContent = d.note;
document.getElementById('urlaub-modal').style.display = 'flex';
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') document.getElementById('urlaub-modal').style.display = 'none';
});
</script>
<?php layout_end(); ?>
+148
View File
@@ -0,0 +1,148 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_once __DIR__ . '/../lib/db.php';
require_once __DIR__ . '/../lib/layout.php';
require_login();
$user_id = current_user()['id'];
$stmt = db()->prepare('SELECT * FROM requests_werben WHERE user_id = ? ORDER BY created_at DESC');
$stmt->execute([$user_id]);
$requests = $stmt->fetchAll();
$qual_labels = [
'1jahr' => '1 Jährige Examinierte Pflegekraft',
'3jahr' => '3 Jährige Examinierte Pflegekraft',
];
$status_labels = ['pending' => 'Ausstehend', 'accepted' => 'Akzeptiert', 'rejected' => 'Abgelehnt'];
layout_start('OMSORG Mitarbeiter werben', 'werben');
?>
<style>
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:1000}
.modal-box{background:var(--glass-bg,#1e2235);border:1px solid var(--border,rgba(255,255,255,.1));border-radius:14px;padding:28px 32px;max-width:480px;width:90%;position:relative}
.modal-box h3{margin:0 0 20px;font-family:'KindelSerif',Georgia,serif;font-size:1.3rem}
.modal-close{position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.5rem;cursor:pointer;color:var(--muted,#888);line-height:1}
.modal-dl{display:grid;grid-template-columns:auto 1fr;gap:8px 16px;font-size:.95rem}
.modal-dl dt{color:var(--muted,#888);white-space:nowrap}
.modal-dl dd{margin:0;font-weight:500}
.btn-sm{display:inline-flex;align-items:center;gap:6px;padding:7px 13px;border-radius:9px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;font-weight:600;font-size:.82rem;cursor:pointer;font:inherit;transition:background .15s}
.btn-sm:hover{background:rgba(255,255,255,.15)}
</style>
<h1 class="main-title">🤝 Mitarbeiter werben</h1>
<p class="main-sub">Empfehle einen Kandidaten für eine offene Stelle bei OMSORG.</p>
<?php if (isset($_GET['success'])): ?>
<div class="success" style="margin-top:16px">Empfehlung erfolgreich eingereicht.</div>
<?php elseif (isset($_GET['error'])): ?>
<div class="error" style="margin-top:16px"><?= e(urldecode($_GET['error'])) ?></div>
<?php endif; ?>
<div id="werb-modal" class="modal-backdrop" onclick="if(event.target===this)this.style.display='none'">
<div class="modal-box glass">
<button class="modal-close" onclick="document.getElementById('werb-modal').style.display='none'">&times;</button>
<h3>Empfehlung Details</h3>
<dl class="modal-dl">
<dt>Name</dt> <dd id="werb-name"></dd>
<dt>E-Mail</dt> <dd id="werb-email"></dd>
<dt>Qualifikation</dt> <dd id="werb-qual"></dd>
<dt>Nachricht</dt> <dd id="werb-nachricht"></dd>
<dt>Eingereicht</dt> <dd id="werb-eingereicht"></dd>
<dt>Status</dt> <dd id="werb-status"></dd>
<dt>Admin-Notiz</dt> <dd id="werb-note"></dd>
</dl>
</div>
</div>
<div class="page-split">
<div class="page-split-list">
<h2>Meine Empfehlungen</h2>
<?php if (empty($requests)): ?>
<p style="color:var(--muted)">Noch keine Empfehlungen eingereicht.</p>
<?php else: ?>
<table class="requests-table">
<thead>
<tr>
<th>Name</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($requests as $r): ?>
<tr>
<td style="font-weight:800"><?= e($r['name']) ?></td>
<td><span class="badge badge-<?= e($r['status']) ?>"><?= e($status_labels[$r['status']] ?? $r['status']) ?></span></td>
<td>
<button class="btn-sm js-werb-details"
data-name="<?= e($r['name']) ?>"
data-email="<?= e($r['email']) ?>"
data-qual="<?= e($qual_labels[$r['qualifikation']] ?? $r['qualifikation']) ?>"
data-nachricht="<?= e($r['nachricht'] !== '' ? $r['nachricht'] : '') ?>"
data-eingereicht="<?= e(date('d.m.Y', strtotime($r['created_at']))) ?>"
data-status-label="<?= e($status_labels[$r['status']] ?? $r['status']) ?>"
data-status-key="<?= e($r['status']) ?>"
data-note="<?= e($r['admin_note'] !== '' ? $r['admin_note'] : '') ?>"
>Details</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<div class="page-split-form">
<div class="glass detail-card">
<h2>Kandidaten empfehlen</h2>
<form action="../actions/submit-werben.php" method="post">
<?= csrf_field() ?>
<label>
<span>Name des Kandidaten <span style="color:#ff8080">*</span></span>
<input type="text" name="name" required placeholder="Vorname Nachname">
</label>
<label>
<span>E-Mail des Kandidaten <span style="color:#ff8080">*</span></span>
<input type="email" name="email" required placeholder="kandidat@example.com">
</label>
<label>
<span>Qualifikation <span style="color:#ff8080">*</span></span>
<select name="qualifikation" required>
<option value=""> Bitte wählen </option>
<option value="1jahr">1 Jährige Examinierte Pflegekraft</option>
<option value="3jahr">3 Jährige Examinierte Pflegekraft</option>
</select>
</label>
<label>
Nachricht
<textarea name="nachricht" rows="4" placeholder="Warum empfiehlst du diese Person?"></textarea>
</label>
<button type="submit" class="btn" style="margin-top:6px">Empfehlung einreichen</button>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('click', function(e) {
var btn = e.target.closest('.js-werb-details');
if (!btn) return;
var d = btn.dataset;
document.getElementById('werb-name').textContent = d.name;
document.getElementById('werb-email').textContent = d.email;
document.getElementById('werb-qual').textContent = d.qual;
document.getElementById('werb-nachricht').textContent = d.nachricht;
document.getElementById('werb-eingereicht').textContent= d.eingereicht;
document.getElementById('werb-status').innerHTML =
'<span class="badge badge-' + d.statusKey + '">' + d.statusLabel + '</span>';
document.getElementById('werb-note').textContent = d.note;
document.getElementById('werb-modal').style.display = 'flex';
});
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') document.getElementById('werb-modal').style.display = 'none';
});
</script>
<?php layout_end(); ?>
+13
View File
@@ -0,0 +1,13 @@
const CACHE = 'omsorg-v1';
const STATIC = ['./app.css'];
self.addEventListener('install', e =>
e.waitUntil(caches.open(CACHE).then(c => c.addAll(STATIC)))
);
self.addEventListener('fetch', e => {
if (e.request.url.endsWith('.php') || e.request.method !== 'GET') return;
e.respondWith(
fetch(e.request).catch(() => caches.match(e.request))
);
});
+41
View File
@@ -0,0 +1,41 @@
<?php
ini_set('display_errors', '1');
error_reporting(E_ALL);
echo "<pre>\n";
try {
echo "1. Lade config...\n";
$c = require __DIR__ . '/lib/config.php';
echo " driver={$c['db_driver']} dsn={$c['db_dsn']}\n";
echo "2. Verbinde Datenbank...\n";
require_once __DIR__ . '/lib/db.php';
$pdo = db();
echo " Verbindung OK\n";
echo "3. Zaehle User...\n";
$count = (int) $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
echo " $count User gefunden\n";
if ($count === 0) {
echo "4. Erstelle Admin-User...\n";
$hash = password_hash('adminadmin', PASSWORD_BCRYPT, ['cost' => 12]);
$stmt = $pdo->prepare(
'INSERT INTO users (username, name, role, password_hash, active, created_at)
VALUES (?, ?, ?, ?, 1, ?)'
);
$stmt->execute(['admin', 'Administrator', 'admin', $hash, date('c')]);
echo " Admin erstellt: username=admin password=adminadmin\n";
} else {
echo "4. DB hat bereits User — kein Admin eingefuegt.\n";
}
echo "\nSetup erfolgreich abgeschlossen.\n";
} catch (Throwable $e) {
echo "\nFEHLER: " . $e->getMessage() . "\n";
echo "Datei: " . $e->getFile() . " Zeile " . $e->getLine() . "\n";
echo "\nStack trace:\n" . $e->getTraceAsString() . "\n";
}
echo "</pre>\n";

Some files were not shown because too many files have changed in this diff Show More