Reorganize into monorepo layout, move mitarbeiter-app to legacy reference

Consolidates the previously separate omsorgapp and omsorgCore repos
(each had their own nested .git with GitHub history) plus the old
root-level website/mitarbeiter-app into a single monorepo, matching
the structure already documented in the root CLAUDE.md. Also moves
the PHP employee app aside as omsorgWeb/mitarbeiter-app-legacy/ to
serve as a template for a ground-up rewrite.

Fixes .gitignore in the same pass: the config-secrets/uploads/data
patterns were unanchored (relative to repo root, not depth-agnostic),
so they silently stopped matching once the app moved under omsorgWeb/.
Patterns are now **/-prefixed and cover both mitarbeiter-app and
mitarbeiter-app-legacy, keeping DB/SMTP credentials and uploaded
employee documents out of version control.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Felix Kemmler
2026-08-07 14:21:37 +02:00
co-authored by Claude Sonnet 5
parent 8beb0fcf52
commit b6c1389c55
355 changed files with 24348 additions and 361 deletions
@@ -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(); ?>
@@ -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
@@ -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(); ?>
@@ -0,0 +1,75 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
require_login();
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$current = $_POST['current_password'] ?? '';
$new = $_POST['new_password'] ?? '';
$repeat = $_POST['new_password_repeat'] ?? '';
if (strlen($new) < 8) {
$error = 'Das neue Passwort muss mindestens 8 Zeichen lang sein.';
} elseif ($new !== $repeat) {
$error = 'Die Passwörter stimmen nicht überein.';
} else {
$result = omsorgcore_change_password(_omsorgcore_config(), $_SESSION['omsorgcore_access_token'], $current, $new);
if ($result['ok']) {
// Serverseitig wurden alle Sessions widerrufen (siehe UserService.ChangeOwnPasswordAsync) -
// die aktuelle Session ist damit tot, neuer Login mit dem neuen Passwort ist nötig.
_clear_omsorgcore_session();
header('Location: ../index.php?pwchanged=1');
exit;
}
$error = $result['status'] === 401
? 'Aktuelles Passwort ist falsch.'
: 'Passwort konnte nicht geändert werden.';
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>OMSORG Felix Passwort ändern</title>
<link rel="stylesheet" href="../app.css">
</head>
<body>
<div class="login-page">
<div class="login-card glass">
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 6px">Passwort ändern erforderlich</h2>
<p class="hint" style="margin:0 0 22px">
Für deinen Account wurde ein initiales Passwort vergeben. Lege jetzt dein eigenes Passwort fest,
bevor du weiterarbeiten kannst.
</p>
<?php if ($error): ?>
<div class="error" style="margin-bottom:16px"><?= e($error) ?></div>
<?php endif; ?>
<form method="post">
<?= csrf_field() ?>
<label>
Aktuelles Passwort
<input type="password" name="current_password" autocomplete="current-password" required autofocus>
</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" style="width:100%;margin-top:6px">Passwort ändern</button>
</form>
</div>
</div>
</body>
</html>
@@ -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>
@@ -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();
@@ -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;
@@ -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(); ?>
@@ -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;
@@ -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(); ?>
@@ -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,125 @@
<?php
require_once __DIR__ . '/../lib/auth.php';
if (is_logged_in()) {
header('Location: dashboard.php');
exit;
}
$step = $_POST['step'] ?? 'request';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
if ($step === 'request') {
$username = trim($_POST['username'] ?? '');
if ($username === '') {
$error = 'Bitte Benutzername eingeben.';
} else {
omsorgcore_forgot_password_request(_omsorgcore_config(), $username);
// Bewusst immer weiter zu Schritt 2, unabhängig vom Ergebnis - kein Rückschluss darauf,
// ob der Username existiert (siehe PasswordResetService.RequestResetAsync in omsorgCore).
$step = 'verify';
}
} elseif ($step === 'verify') {
$username = trim($_POST['username'] ?? '');
$pin = trim($_POST['pin'] ?? '');
$result = omsorgcore_forgot_password_verify(_omsorgcore_config(), $username, $pin);
if ($result['ok']) {
$resetToken = $result['data']['resetToken'];
$step = 'reset';
} else {
$error = 'Code ungültig oder abgelaufen.';
}
} elseif ($step === 'reset') {
$resetToken = $_POST['reset_token'] ?? '';
$new = $_POST['new_password'] ?? '';
$repeat = $_POST['new_password_repeat'] ?? '';
if (strlen($new) < 8) {
$error = 'Das neue Passwort muss mindestens 8 Zeichen lang sein.';
} elseif ($new !== $repeat) {
$error = 'Die Passwörter stimmen nicht überein.';
} else {
$result = omsorgcore_forgot_password_reset(_omsorgcore_config(), $resetToken, $new);
if ($result['ok']) {
header('Location: ../index.php?pwchanged=1');
exit;
}
$error = 'Zurücksetzen fehlgeschlagen. Bitte Vorgang erneut starten.';
$step = 'request';
}
}
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>OMSORG Felix Passwort vergessen</title>
<link rel="stylesheet" href="../app.css">
</head>
<body>
<div class="login-page">
<div class="login-card glass">
<h2 style="font-family:'KindelSerif',Georgia,serif;font-size:1.5rem;margin:0 0 6px">Passwort vergessen</h2>
<?php if ($error): ?>
<div class="error" style="margin-bottom:16px"><?= e($error) ?></div>
<?php endif; ?>
<?php if ($step === 'request'): ?>
<p class="hint" style="margin:0 0 22px">Gib deinen Benutzernamen ein, wir senden dir einen Code per E-Mail.</p>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="step" value="request">
<label>
Benutzername
<input type="text" name="username" autocomplete="username" required autofocus>
</label>
<button type="submit" class="btn" style="width:100%;margin-top:6px">Code anfordern</button>
</form>
<?php elseif ($step === 'verify'): ?>
<p class="hint" style="margin:0 0 22px">Gib den Code ein, den wir dir per E-Mail geschickt haben.</p>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="step" value="verify">
<input type="hidden" name="username" value="<?= e($username ?? '') ?>">
<label>
Code
<input type="text" name="pin" inputmode="numeric" pattern="[0-9]*" maxlength="6" required autofocus>
</label>
<button type="submit" class="btn" style="width:100%;margin-top:6px">Code bestätigen</button>
</form>
<?php elseif ($step === 'reset'): ?>
<p class="hint" style="margin:0 0 22px">Lege dein neues Passwort fest.</p>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="step" value="reset">
<input type="hidden" name="reset_token" value="<?= e($resetToken ?? '') ?>">
<label>
Neues Passwort
<input type="password" name="new_password" autocomplete="new-password" required minlength="8" autofocus>
</label>
<label>
Neues Passwort wiederholen
<input type="password" name="new_password_repeat" autocomplete="new-password" required minlength="8">
</label>
<button type="submit" class="btn" style="width:100%;margin-top:6px">Passwort setzen</button>
</form>
<?php endif; ?>
<p class="hint" style="text-align:center;margin-top:14px">
<a href="../index.php">Zurück zum Login</a>
</p>
</div>
</div>
</body>
</html>
@@ -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(); ?>
@@ -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(); ?>
@@ -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(); ?>
@@ -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;
@@ -0,0 +1,147 @@
<?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" enctype="multipart/form-data">
<?= 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>
<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>
<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(); ?>
@@ -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(); ?>