web - refactor core modules, harden authorization, simplify endpoints

This commit is contained in:
2026-08-29 11:15:54 +02:00
parent e1109a07ef
commit 79d857d686
71 changed files with 8269 additions and 9119 deletions
@@ -1,457 +1,117 @@
<?php /* Name: admin_brandschutz.php
Beschreibung: Brandschutz-Schulungsverwaltung im Adminpanel
- Übersicht aller Nutzer mit Brandschutz-Rolle
- Fälligkeitsstatus und Details
- Manuelle Anpassung des Schulungsdatums */
<?php
/* Name: admin_brandschutz.php
Beschreibung: Brandschutz-Schulungsverwaltung im Adminpanel */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/initialise.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/brandschutz/util/brandschutz_helpers.php';
// Admin-Check
$stmt = $pdo->prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
$stmt->execute([$_SESSION["user_id"]]);
$isAdmin = $stmt->fetchColumn();
if ((int) ($groups['Admin'] ?? 0) !== 1) {
header("Location: {$g_httpHomePath}");
exit();
}
if ((int)$isAdmin !== 1) {
header("Location: {$g_httpHomePath}");
exit();
}
$csrfToken = brandschutz_csrf_token();
// Hilfsfunktionen
function getProfileImageUrl_bs(array $row, string $g_profilePicturePath): string {
$profileImg = glob($g_profilePicturePath."{$row['user_id']}-*.jpg");
if (!empty($profileImg)) {
usort($profileImg, function($a, $b) {
return filemtime($b) - filemtime($a);
});
return str_replace($_SERVER['DOCUMENT_ROOT'], '', $profileImg[0]);
}
return '/intranet_repo/general_files/profile_pictures/default.jpg';
}
function getProfileImageUrl_bs(array $row, string $g_profilePicturePath): string
{
$profileImages = glob($g_profilePicturePath . $row['user_id'] . '-*.jpg') ?: [];
if ($profileImages !== []) {
usort($profileImages, static function (string $a, string $b): int {
return filemtime($b) <=> filemtime($a);
});
return str_replace($_SERVER['DOCUMENT_ROOT'], '', $profileImages[0]);
}
function getDisplayName_bs(array $row): string {
$fullName = trim(($row['first_name'] ?? '') . ' ' . ($row['last_name'] ?? ''));
return $fullName !== '' ? $fullName : ($row['mail'] ?? 'Unbekannt');
}
return '/intranet_repo/general_files/profile_pictures/default.jpg';
}
// Schulungsdatum manuell anpassen (POST)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bs_update_date'])) {
$editUserId = (int)($_POST['bs_user_id'] ?? 0);
$newDate = $_POST['bs_new_date'] ?? '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
brandschutz_verify_csrf($_POST['csrf_token'] ?? null);
} catch (InvalidArgumentException $exception) {
http_response_code(400);
exit($exception->getMessage());
}
if ($editUserId > 0 && preg_match('/^\d{4}-\d{2}-\d{2}$/', $newDate)) {
// Prüfen ob das neue Datum innerhalb der Frist liegt (365 Tage ab Datum > heute)
$newDueDate = date('Y-m-d', strtotime($newDate . ' +365 days'));
$isWithinDeadline = (strtotime($newDueDate) >= strtotime(date('Y-m-d')));
if (($_POST['action'] ?? '') !== 'update_training_date') {
http_response_code(400);
exit('Ungültige Aktion.');
}
if ($isWithinDeadline) {
// Datum in der Frist: Zähler zurücksetzen
$stmtUpsert = $pdo->prepare("INSERT INTO brandschutz_training (user_id, last_training_date, reminder_count, admin_reminder_count) VALUES (?, ?, 0, 0) ON CONFLICT (user_id) DO UPDATE SET last_training_date = EXCLUDED.last_training_date, reminder_count = 0, admin_reminder_count = 0");
} else {
// Datum außerhalb der Frist: Nur Datum aktualisieren, Zähler beibehalten
$stmtUpsert = $pdo->prepare("INSERT INTO brandschutz_training (user_id, last_training_date, reminder_count, admin_reminder_count) VALUES (?, ?, 0, 0) ON CONFLICT (user_id) DO UPDATE SET last_training_date = EXCLUDED.last_training_date");
}
$stmtUpsert->execute([$editUserId, $newDate]);
$editUserId = filter_var($_POST['bs_user_id'] ?? null, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
$newDate = trim((string) ($_POST['bs_new_date'] ?? ''));
if ($editUserId === false || !brandschutz_is_valid_date($newDate)) {
http_response_code(400);
exit('Ungültige Eingaben.');
}
// Systemnachricht in erledigte Admin-Nachrichten
$adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
if (empty($adminName)) $adminName = 'Ein Administrator';
$stmtUser = $pdo->prepare(
'SELECT ugi.first_name, ugi.last_name, ugi.mail '
. 'FROM user_general_information ugi '
. 'INNER JOIN user_permissions up ON up.user_id = ugi.user_id '
. 'WHERE ugi.user_id = ? AND up."Brandschutz" = 1'
);
$stmtUser->execute([(int) $editUserId]);
$editedUser = $stmtUser->fetch(PDO::FETCH_ASSOC);
if (!$editedUser) {
http_response_code(404);
exit('Brandschutz-Nutzer nicht gefunden.');
}
$stmtUser = $pdo->prepare("SELECT first_name, last_name, mail FROM user_general_information WHERE user_id = ?");
$stmtUser->execute([$editUserId]);
$editedUser = $stmtUser->fetch(PDO::FETCH_ASSOC);
$newStatus = brandschutz_training_status($newDate);
$resetCounters = (int) $newStatus['days_until_due'] >= 0;
$adminName = brandschutz_display_name(is_array($user) ? $user : [], 'Ein Administrator');
$editedName = brandschutz_display_name($editedUser);
$editedName = trim(($editedUser['first_name'] ?? '') . ' ' . ($editedUser['last_name'] ?? ''));
if (empty($editedName)) $editedName = $editedUser['mail'] ?? 'Unbekannt';
$pdo->beginTransaction();
try {
brandschutz_store_training_date($pdo, (int) $editUserId, $newDate, $resetCounters);
brandschutz_log_admin_message(
$pdo,
$adminName,
(string) ($editedUser['mail'] ?? ''),
'Brandschutz-Schulungsdatum angepasst: ' . $editedName,
$adminName . ' hat das Schulungsdatum von ' . $editedName . ' manuell auf den '
. (new DateTimeImmutable($newDate))->format('d.m.Y') . ' gesetzt.'
);
$pdo->commit();
} catch (Throwable $exception) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $exception;
}
$formattedDate = date('d.m.Y', strtotime($newDate));
$sysSubject = "Brandschutz-Schulungsdatum angepasst: {$editedName}";
$sysMessage = "{$adminName} hat das Schulungsdatum von {$editedName} manuell auf den {$formattedDate} gesetzt.";
header('Location: admin_brandschutz.php?message=5', true, 303);
exit();
}
$stmtMsg = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
$senderMail = $editedUser['mail'] ?? '';
$stmtMsg->execute([$adminName, $senderMail, $sysSubject, $sysMessage]);
$sql = 'SELECT ugi.user_id, ugi.first_name, ugi.last_name, ugi.mail, '
. 'bt.last_training_date, bt.reminder_count, bt.admin_reminder_count, '
. 'CASE WHEN ul.user_id IS NOT NULL THEN 1 ELSE 0 END AS IstRegistriert '
. 'FROM user_permissions up '
. 'INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id '
. 'LEFT JOIN brandschutz_training bt ON ugi.user_id = bt.user_id '
. 'LEFT JOIN user_login ul ON ugi.user_id = ul.user_id '
. 'WHERE up."Brandschutz" = 1 '
. 'ORDER BY bt.last_training_date ASC, ugi.last_name ASC, ugi.first_name ASC';
$bsUsers = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
header("Location: admin_brandschutz.php?message=5");
exit();
}
}
// Brandschutz-Nutzer laden
$sql = "SELECT ugi.user_id, ugi.first_name, ugi.last_name, ugi.mail,
bt.last_training_date, bt.reminder_count, bt.admin_reminder_count,
CASE WHEN ul.user_id IS NOT NULL THEN 1 ELSE 0 END AS IstRegistriert
FROM user_permissions up
INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id
LEFT JOIN brandschutz_training bt ON ugi.user_id = bt.user_id
LEFT JOIN user_login ul ON ugi.user_id = ul.user_id
WHERE up.\"Brandschutz\" = 1
ORDER BY bt.last_training_date ASC, ugi.last_name ASC, ugi.first_name ASC";
$bsUsers = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
foreach ($bsUsers as &$bsUser) {
$bsUser['_display_name'] = brandschutz_display_name($bsUser);
$bsUser['_profile_url'] = getProfileImageUrl_bs($bsUser, $g_profilePicturePath);
$bsUser['_training_status'] = brandschutz_training_status($bsUser['last_training_date'] ?? null);
}
unset($bsUser);
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<style>
.admin-users-shell {
padding: 18px;
background: linear-gradient(135deg, var(--gray-50) 0%, var(--theme-light, var(--nak-blue-light)) 100%);
min-height: 100%;
}
.admin-table-card {
overflow: hidden;
border: 1px solid rgba(120, 140, 160, 0.22);
background: rgba(255,255,255,0.86);
box-shadow: 0 20px 45px rgba(31, 38, 46, 0.10), 0 1px 0 rgba(255,255,255,0.9) inset;
}
.admin-list-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px 12px 18px;
border-bottom: 1px solid rgba(120, 140, 160, 0.16);
background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.72));
}
.admin-list-title {
font-size: 1.05em;
font-weight: 800;
color: hsl(210, 20%, 15%);
}
.admin-list-hint {
font-size: 0.86em;
color: hsl(210, 15%, 46%);
}
table.admin-user-table {
display: table !important;
width: calc(100% - 24px);
margin: 8px 12px 14px 12px;
border-collapse: separate;
border-spacing: 0 8px;
padding: 0;
background: transparent;
overflow: visible;
}
.admin-user-table thead th {
position: sticky;
top: 0;
padding: 10px 14px;
color: hsl(210, 16%, 46%);
font-size: 0.78em;
font-weight: 800;
text-align: left;
letter-spacing: 0.04em;
text-transform: uppercase;
background: transparent !important;
border: none !important;
}
.admin-user-table tbody tr.admin-user-row {
position: relative;
cursor: pointer;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 6px 18px rgba(31, 38, 46, 0.06);
transition: transform 0.18s ease, box-shadow 0.18s ease, background 0.18s ease;
}
.admin-user-table tbody tr.admin-user-row:hover {
transform: translateY(-1px);
background: #FFFFFF;
box-shadow: 0 12px 26px rgba(31, 38, 46, 0.13);
}
.admin-user-table td {
padding: 12px 14px !important;
vertical-align: middle;
border: none !important;
background: transparent !important;
}
.admin-user-table tbody tr.admin-user-row td:first-child {
width: 82px;
min-width: 82px;
max-width: 82px;
}
.admin-profile-button {
display: block;
width: 58px;
height: 58px;
border-radius: 12px;
overflow: hidden;
border: 0;
padding: 0;
background: transparent;
cursor: pointer;
box-shadow: 0 8px 20px rgba(31, 38, 46, 0.18), 0 0 0 3px rgba(255,255,255,0.95);
}
.admin-profile-button img {
display: block;
width: 58px !important;
height: 58px !important;
object-fit: cover;
border-radius: 12px !important;
transition: transform 0.2s ease;
}
.admin-user-table tbody tr.admin-user-row:hover .admin-profile-button img {
transform: scale(1.08);
}
.admin-user-name {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 1em;
font-weight: 800;
color: hsl(210, 20%, 15%);
}
.bs-status-badge {
display: inline-flex;
align-items: center;
padding: 4px 10px;
font-size: 0.78em;
font-weight: 800;
}
.bs-status-ok {
color: hsl(145, 50%, 28%);
background: hsl(145, 50%, 93%);
border: 1px solid hsl(145, 50%, 82%);
}
.bs-status-warning {
color: hsl(35, 78%, 30%);
background: hsl(42, 90%, 93%);
border: 1px solid hsl(42, 90%, 78%);
}
.bs-status-overdue {
color: hsl(0, 65%, 38%);
background: hsl(0, 65%, 95%);
border: 1px solid hsl(0, 65%, 82%);
}
.bs-status-critical {
color: #FFFFFF;
background: hsl(0, 65%, 45%);
border: 1px solid hsl(0, 65%, 38%);
}
.bs-due-date {
font-size: 0.9em;
font-weight: 650;
color: hsl(210, 14%, 35%);
}
<link rel="stylesheet" href="/intranet_repo/projects/_central/layouts/admin_brandschutz.css">
<script src="/intranet_repo/projects/_central/scripts/admin_brandschutz.js" defer></script>
/* Modal */
.admin-modal-card {
position: relative;
width: min(720px, calc(100vw - 24px));
max-height: min(760px, calc(100vh - 24px));
overflow: auto;
padding: 0;
background: #FFFFFF;
box-shadow: 0 24px 70px rgba(0,0,0,0.36);
}
.admin-modal-hero {
position: relative;
display: grid;
grid-template-columns: 112px 1fr;
gap: 18px;
align-items: center;
padding: 24px;
color: #FFFFFF;
background:
radial-gradient(circle at 10% 0%, rgba(255,255,255,0.24), transparent 32%),
linear-gradient(135deg, hsl(25, 90%, 45%), hsl(25, 95%, 35%));
}
.admin-modal-profile-img {
width: 104px;
height: 104px;
object-fit: cover;
border-radius: 16px;
box-shadow: 0 12px 28px rgba(0,0,0,0.32), 0 0 0 4px rgba(255,255,255,0.18);
}
.admin-modal-title {
margin: 0 42px 6px 0;
font-size: 1.45em;
font-weight: 850;
color: #FFFFFF;
}
.admin-modal-subtitle {
display: flex;
flex-flow: row wrap;
gap: 8px;
align-items: center;
margin-bottom: 10px;
color: rgba(255,255,255,0.86);
font-weight: 600;
}
.admin-modal-chip {
display: inline-flex;
align-items: center;
padding: 4px 9px;
border-radius: 9999px;
background: rgba(255,255,255,0.14);
border: 1px solid rgba(255,255,255,0.18);
font-size: 0.82em;
}
.admin-modal-body {
padding: 22px 24px 24px 24px;
}
.admin-section-title {
margin: 0 0 10px 0;
font-size: 0.9em;
font-weight: 850;
letter-spacing: 0.04em;
text-transform: uppercase;
color: hsl(210, 18%, 36%);
}
.admin-modal-card .exit {
position: absolute;
top: 14px;
right: 14px;
width: 34px;
height: 34px;
border: 1px solid rgba(255,255,255,0.25);
background: rgba(255,255,255,0.16);
color: #FFFFFF;
font-size: 1.35em;
line-height: 1;
cursor: pointer;
}
.bs-detail-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 20px;
}
.bs-detail-card {
padding: 14px 16px;
border: 1px solid hsl(210, 18%, 88%);
background: linear-gradient(180deg, #FFFFFF, hsl(210, 12%, 98%));
}
.bs-detail-label {
margin-bottom: 3px;
color: hsl(210, 14%, 46%);
font-size: 0.74em;
font-weight: 850;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.bs-detail-value {
color: hsl(210, 20%, 18%);
font-size: 1.02em;
font-weight: 850;
}
.bs-detail-value.overdue {
color: hsl(0, 65%, 42%);
}
.bs-detail-value.ok {
color: hsl(145, 50%, 32%);
}
.admin-modal-actions {
display: flex;
justify-content: space-between;
gap: 10px;
border-top: 1px solid hsl(210, 18%, 90%);
padding-top: 16px;
}
.admin-modal-actions button {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 36px;
padding: 8px 13px;
border: 1px solid hsl(210, 18%, 84%);
background: #FFFFFF;
color: hsl(210, 20%, 22%);
font-size: 0.86em;
font-weight: 850;
cursor: pointer;
box-shadow: 0 4px 12px rgba(31,38,46,0.06);
}
.admin-modal-actions button:hover {
background: hsl(210, 57%, 96%);
}
.admin-save-button {
color: #FFFFFF !important;
background: linear-gradient(135deg, var(--nak-orange), hsl(25, 95%, 40%)) !important;
border-color: hsl(25, 95%, 40%) !important;
}
.bs-date-input {
padding: 10px 12px;
font-size: var(--font-size-sm);
font-family: var(--font-primary);
border: 1px solid var(--gray-300);
background: var(--gray-50);
width: 100%;
box-sizing: border-box;
font-weight: 650;
}
.bs-date-input:focus {
border-color: hsl(25, 80%, 55%);
background: #FFFFFF;
outline: none;
box-shadow: 0 0 0 4px hsla(25, 80%, 55%, 0.14);
}
@media (max-width: 780px) {
.admin-users-shell { padding: 10px; }
.admin-list-header {
flex-direction: column;
align-items: flex-start;
padding: 14px;
gap: 8px;
}
table.admin-user-table {
display: block !important;
width: auto;
margin: 8px;
}
.admin-user-table thead { display: none; }
.admin-user-table tbody {
display: grid;
gap: 10px;
}
.admin-user-table tbody tr.admin-user-row {
display: grid;
grid-template-columns: 72px minmax(0, 1fr);
gap: 0 12px;
padding: 12px;
border: 1px solid rgba(120, 140, 160, 0.16);
box-shadow: 0 8px 22px rgba(31, 38, 46, 0.08);
}
.admin-user-table tbody tr.admin-user-row td {
display: block;
padding: 0 !important;
}
.admin-user-table tbody tr.admin-user-row td:first-child {
grid-row: 1 / span 3;
width: 72px;
min-width: 72px;
max-width: 72px;
}
.admin-profile-button,
.admin-profile-button img {
width: 64px !important;
height: 64px !important;
border-radius: 12px !important;
}
.admin-modal-card {
width: calc(100vw - 12px);
max-height: calc(100dvh - 12px);
}
.admin-modal-hero {
grid-template-columns: 78px minmax(0, 1fr);
gap: 14px;
padding: 18px;
}
.admin-modal-profile-img {
width: 72px;
height: 72px;
border-radius: 14px;
}
.admin-modal-title {
font-size: 1.15em;
margin-right: 40px;
}
.admin-modal-body { padding: 18px; }
.bs-detail-grid { grid-template-columns: 1fr; }
.admin-modal-actions { flex-direction: column-reverse; }
.admin-modal-actions button { width: 100%; }
}
</style>
</head>
<body class="theme-admin">
@@ -487,45 +147,9 @@
<tbody>
<?php foreach ($bsUsers as $bsUser): ?>
<?php
$nameOutput = getDisplayName_bs($bsUser);
$profileUrl = getProfileImageUrl_bs($bsUser, $g_profilePicturePath);
$lastTraining = $bsUser['last_training_date'];
$reminderCount = (int)($bsUser['reminder_count'] ?? 0);
$adminReminderCount = (int)($bsUser['admin_reminder_count'] ?? 0);
// Berechne Fälligkeit
if (empty($lastTraining)) {
$dueDate = null;
$daysOverdue = null;
$statusText = 'Nie geschult';
$statusClass = 'bs-status-overdue';
$dueDateDisplay = 'Sofort fällig';
} else {
$dueDate = date('Y-m-d', strtotime($lastTraining . ' +365 days'));
$today = date('Y-m-d');
$diff = (strtotime($dueDate) - strtotime($today)) / 86400;
if ($diff > 30) {
$statusText = 'Aktuell';
$statusClass = 'bs-status-ok';
$dueDateDisplay = date('d.m.Y', strtotime($dueDate));
} elseif ($diff > 0) {
$statusText = 'Bald fällig';
$statusClass = 'bs-status-warning';
$dueDateDisplay = date('d.m.Y', strtotime($dueDate));
} elseif ($diff > -120) {
$daysOverdue = abs((int)$diff);
$statusText = "{$daysOverdue} Tage überfällig";
$statusClass = 'bs-status-overdue';
$dueDateDisplay = date('d.m.Y', strtotime($dueDate));
} else {
$daysOverdue = abs((int)$diff);
$statusText = "{$daysOverdue} Tage überfällig";
$statusClass = 'bs-status-critical';
$dueDateDisplay = date('d.m.Y', strtotime($dueDate));
}
}
$nameOutput = (string) $bsUser['_display_name'];
$profileUrl = (string) $bsUser['_profile_url'];
$trainingStatus = $bsUser['_training_status'];
?>
<tr class="admin-user-row" tabindex="0" role="button" aria-label="<?= htmlspecialchars($nameOutput) ?> Details" onclick="openBsModal(<?= (int)$bsUser['user_id'] ?>)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();openBsModal(<?= (int)$bsUser['user_id'] ?>)}">
<td>
@@ -537,10 +161,10 @@
<span class="admin-user-name"><?= htmlspecialchars($nameOutput) ?></span>
</td>
<td>
<span class="bs-due-date"><?= $dueDateDisplay ?></span>
<span class="bs-due-date"><?= htmlspecialchars((string) $trainingStatus['due_date_display']) ?></span>
</td>
<td>
<span class="bs-status-badge <?= $statusClass ?>"><?= $statusText ?></span>
<span class="bs-status-badge <?= htmlspecialchars((string) $trainingStatus['status_class']) ?>"><?= htmlspecialchars((string) $trainingStatus['status_text']) ?></span>
</td>
</tr>
<?php endforeach; ?>
@@ -555,34 +179,12 @@
<!-- Modals für jeden Brandschutz-Nutzer -->
<?php foreach ($bsUsers as $bsUser): ?>
<?php
$nameOutput = getDisplayName_bs($bsUser);
$profileUrl = getProfileImageUrl_bs($bsUser, $g_profilePicturePath);
$nameOutput = (string) $bsUser['_display_name'];
$profileUrl = (string) $bsUser['_profile_url'];
$lastTraining = $bsUser['last_training_date'];
$reminderCount = (int)($bsUser['reminder_count'] ?? 0);
$adminReminderCount = (int)($bsUser['admin_reminder_count'] ?? 0);
if (empty($lastTraining)) {
$dueDate = null;
$dueDateDisplay = 'Sofort fällig';
$lastTrainingDisplay = 'Nie';
$overdueText = 'Keine Schulung hinterlegt – sofort fällig';
$overdueClass = 'overdue';
} else {
$dueDate = date('Y-m-d', strtotime($lastTraining . ' +365 days'));
$dueDateDisplay = date('d.m.Y', strtotime($dueDate));
$lastTrainingDisplay = date('d.m.Y', strtotime($lastTraining));
$today = date('Y-m-d');
$diff = (strtotime($dueDate) - strtotime($today)) / 86400;
if ($diff > 0) {
$overdueText = 'Nicht überfällig (' . (int)$diff . ' Tage verbleibend)';
$overdueClass = 'ok';
} else {
$daysOverdue = abs((int)$diff);
$overdueText = "{$daysOverdue} Tage überfällig";
$overdueClass = 'overdue';
}
}
$reminderCount = (int) ($bsUser['reminder_count'] ?? 0);
$adminReminderCount = (int) ($bsUser['admin_reminder_count'] ?? 0);
$trainingStatus = $bsUser['_training_status'];
?>
<div class="standard_overlay" id="bsModal-<?= (int)$bsUser['user_id'] ?>" onclick="if(event.target.id==='bsModal-<?= (int)$bsUser['user_id'] ?>')closeBsModal(<?= (int)$bsUser['user_id'] ?>)">
<div class="admin-modal-card">
@@ -605,15 +207,15 @@
<div class="bs-detail-grid">
<div class="bs-detail-card">
<div class="bs-detail-label">Letzte Schulung</div>
<div class="bs-detail-value"><?= $lastTrainingDisplay ?></div>
<div class="bs-detail-value"><?= htmlspecialchars((string) $trainingStatus['last_training_display']) ?></div>
</div>
<div class="bs-detail-card">
<div class="bs-detail-label">Nächste Fälligkeit</div>
<div class="bs-detail-value"><?= $dueDateDisplay ?></div>
<div class="bs-detail-value"><?= htmlspecialchars((string) $trainingStatus['due_date_display']) ?></div>
</div>
<div class="bs-detail-card">
<div class="bs-detail-label">Überfälligkeit</div>
<div class="bs-detail-value <?= $overdueClass ?>"><?= $overdueText ?></div>
<div class="bs-detail-value <?= htmlspecialchars((string) $trainingStatus['overdue_class']) ?>"><?= htmlspecialchars((string) $trainingStatus['overdue_text']) ?></div>
</div>
<div class="bs-detail-card">
<div class="bs-detail-label">Erinnerungsmails gesendet</div>
@@ -629,6 +231,8 @@
<div class="admin-section-title">Schulungsdatum anpassen</div>
<form method="post" action="">
<input type="hidden" name="action" value="update_training_date">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="bs_user_id" value="<?= (int)$bsUser['user_id'] ?>">
<div style="display:flex;gap:10px;align-items:flex-end;margin-bottom:8px;">
<div style="flex:1;">
@@ -643,7 +247,7 @@
<button type="button" onclick="closeBsModal(<?= (int)$bsUser['user_id'] ?>)">Schließen</button>
</div>
<div>
<button type="submit" name="bs_update_date" value="1" class="admin-save-button">Datum speichern</button>
<button type="submit" class="admin-save-button">Datum speichern</button>
</div>
</div>
</form>
@@ -653,23 +257,7 @@
<?php endforeach; ?>
</div><!-- content -->
<script>
function openBsModal(userId) {
var modal = document.getElementById('bsModal-' + userId);
if (modal) modal.style.display = 'flex';
}
function closeBsModal(userId) {
var modal = document.getElementById('bsModal-' + userId);
if (modal) modal.style.display = 'none';
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.querySelectorAll('.standard_overlay[id^="bsModal-"]').forEach(function(m) {
m.style.display = 'none';
});
}
});
</script>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
@@ -7,6 +7,8 @@
- Info-Nachrichten */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/authorization/util/authorization_helpers.php';
$authorizationCsrfToken = authorization_csrf_token();
// Admin-Check
$stmt = $pdo->prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
@@ -18,6 +20,11 @@
exit();
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !authorization_csrf_is_valid($_POST['csrf_token'] ?? null)) {
header('Location: admin_inbox.php?inbox_status=error');
exit();
}
// Typ-Konfiguration für Inbox
$typeConfig = [
'registration' => ['label' => 'Registrierung', 'class' => 'inbox-type-registration'],
@@ -398,12 +405,14 @@
<button type="button" class="inbox-btn inbox-btn-approve" style="background:linear-gradient(135deg,var(--nak-orange),hsl(25,95%,40%)) !important;border-color:hsl(25,95%,40%) !important;" onclick="openBsEscalationModal(<?= (int)$msg['id'] ?>, <?= (int)$data['user_id'] ?>, '<?= htmlspecialchars($data['user_name'] ?? '', ENT_QUOTES) ?>', <?= (int)($data['days_overdue'] ?? 0) ?>)">Persönliche Erinnerung bestätigen</button>
<?php else: ?>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?= authorization_html($authorizationCsrfToken) ?>">
<input type="hidden" name="mark_done" value="<?= (int)$msg['id'] ?>">
<button type="submit" class="inbox-btn inbox-btn-done">Als erledigt markieren</button>
</form>
<?php endif; ?>
<?php else: ?>
<form method="post" action="">
<input type="hidden" name="csrf_token" value="<?= authorization_html($authorizationCsrfToken) ?>">
<input type="hidden" name="mark_done" value="<?= (int)$msg['id'] ?>">
<button type="submit" class="inbox-btn inbox-btn-done">Als erledigt markieren</button>
</form>
@@ -453,6 +462,7 @@
<div style="padding:22px 24px 24px 24px;">
<form method="post" action="approve-registration.php" id="regModalForm">
<input type="hidden" name="csrf_token" value="<?= authorization_html($authorizationCsrfToken) ?>">
<input type="hidden" name="pending_id" id="regModalPendingId">
<input type="hidden" name="message_id" id="regModalMessageId">
<input type="hidden" name="action" id="regModalAction">
@@ -542,6 +552,7 @@ document.addEventListener('keydown', function(e) {
</div>
<form method="post" action="" id="bsEscForm">
<input type="hidden" name="csrf_token" value="<?= authorization_html($authorizationCsrfToken) ?>">
<input type="hidden" name="bs_escalation_msg_id" id="bsEscMsgId">
<input type="hidden" name="bs_escalation_user_id" id="bsEscUserId">
<input type="hidden" name="bs_escalation_confirm" value="1">
@@ -6,6 +6,7 @@
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/authorization/util/authorization_helpers.php';
// Admin-Check
$stmt = $pdo->prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
@@ -22,6 +23,11 @@
exit();
}
if (!authorization_csrf_is_valid($_POST['csrf_token'] ?? null)) {
header("Location: admin_inbox.php?inbox_status=error");
exit();
}
// Admin-Name ermitteln
$adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
if (empty($adminName)) $adminName = 'Ein Administrator';