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:
co-authored by
Claude Sonnet 5
parent
8beb0fcf52
commit
b6c1389c55
@@ -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'">×</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">←</a>
|
||||
<h2 class="dp-nav-title"><?= e($months_de[$month] . ' ' . $year) ?></h2>
|
||||
<a href="<?= e($next_url) ?>" class="dp-nav-btn">→</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'">×</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'">×</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">📍 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">←</a>
|
||||
<h2 class="dp-nav-title"><?= e($months_de[$month] . ' ' . $year) ?></h2>
|
||||
<a href="<?= e($next_url) ?>" class="dp-nav-btn">→</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­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">↓ <?= 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 — 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">↓ 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">
|
||||
↓ <?= 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'">×</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­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">↓ 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 — 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 — 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'">×</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 — 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'">×</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(); ?>
|
||||
Reference in New Issue
Block a user