prepare intranet for first Dokploy deployment

This commit is contained in:
2026-08-26 12:59:51 +02:00
parent 5cfe688bdc
commit 29dfb85fef
1321 changed files with 227234 additions and 0 deletions
@@ -0,0 +1,32 @@
<?php
/* Zentrale Reiternavigation des Adminpanels.
* Erwartet: $pdo (PDO) und optional $adminActiveTab.
*/
$adminActiveTab = $adminActiveTab ?? '';
$adminInboxBadgeCount = 0;
try {
$adminInboxBadgeCount = (int)$pdo->query("SELECT COUNT(*) FROM admin_messages WHERE status = 'pending'")->fetchColumn();
} catch (Throwable $e) {
// Die Navigation soll auch dann benutzbar bleiben, wenn das Postfach temporär nicht verfügbar ist.
$adminInboxBadgeCount = 0;
}
$adminTabs = [
'users' => ['label' => 'Nutzerverwaltung', 'href' => 'admin.php'],
'inbox' => ['label' => 'Postfach', 'href' => 'admin_inbox.php', 'badge' => $adminInboxBadgeCount],
'kalli' => ['label' => 'Kalli', 'href' => 'admin_kalli.php'],
'brandschutz' => ['label' => 'Brandschutz', 'href' => 'admin_brandschutz.php'],
'notices' => ['label' => 'Meldungen', 'href' => 'admin_notices.php'],
];
?>
<div class="content-tab-bar">
<?php foreach ($adminTabs as $tabKey => $tab): ?>
<a class="content-tab-bar__item<?= $adminActiveTab === $tabKey ? ' active' : '' ?>" href="<?= htmlspecialchars($tab['href'], ENT_QUOTES, 'UTF-8') ?>">
<?= htmlspecialchars($tab['label'], ENT_QUOTES, 'UTF-8') ?>
<?php if (!empty($tab['badge'])): ?>
<span class="content-tab-bar__badge"><?= (int)$tab['badge'] ?></span>
<?php endif; ?>
</a>
<?php endforeach; ?>
</div>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,677 @@
<?php /* Name: admin_brandschutz.php
Beschreibung: Brandschutz-Schulungsverwaltung im Adminpanel
- Übersicht aller Nutzer mit Brandschutz-Rolle
- Fälligkeitsstatus und Details
- Manuelle Anpassung des Schulungsdatums */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
// Admin-Check
$stmt = $pdo->prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
$stmt->execute([$_SESSION["user_id"]]);
$isAdmin = $stmt->fetchColumn();
if ((int)$isAdmin !== 1) {
header("Location: {$g_httpHomePath}");
exit();
}
// Hilfsfunktionen
function getProfileImageUrl_bs(array $row, string $g_profilePicturePath): string {
$profileImg = glob($g_profilePicturePath."{$row['user_id']}-*.jpg");
if (!empty($profileImg)) {
usort($profileImg, function($a, $b) {
return filemtime($b) - filemtime($a);
});
return str_replace($_SERVER['DOCUMENT_ROOT'], '', $profileImg[0]);
}
return '/intranet_repo/general_files/profile_pictures/default.jpg';
}
function getDisplayName_bs(array $row): string {
$fullName = trim(($row['first_name'] ?? '') . ' ' . ($row['last_name'] ?? ''));
return $fullName !== '' ? $fullName : ($row['mail'] ?? 'Unbekannt');
}
// Schulungsdatum manuell anpassen (POST)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['bs_update_date'])) {
$editUserId = (int)($_POST['bs_user_id'] ?? 0);
$newDate = $_POST['bs_new_date'] ?? '';
if ($editUserId > 0 && preg_match('/^\d{4}-\d{2}-\d{2}$/', $newDate)) {
// Prüfen ob das neue Datum innerhalb der Frist liegt (365 Tage ab Datum > heute)
$newDueDate = date('Y-m-d', strtotime($newDate . ' +365 days'));
$isWithinDeadline = (strtotime($newDueDate) >= strtotime(date('Y-m-d')));
if ($isWithinDeadline) {
// Datum in der Frist: Zähler zurücksetzen
$stmtUpsert = $pdo->prepare("INSERT INTO brandschutz_training (user_id, last_training_date, reminder_count, admin_reminder_count) VALUES (?, ?, 0, 0) ON CONFLICT (user_id) DO UPDATE SET last_training_date = EXCLUDED.last_training_date, reminder_count = 0, admin_reminder_count = 0");
} else {
// Datum außerhalb der Frist: Nur Datum aktualisieren, Zähler beibehalten
$stmtUpsert = $pdo->prepare("INSERT INTO brandschutz_training (user_id, last_training_date, reminder_count, admin_reminder_count) VALUES (?, ?, 0, 0) ON CONFLICT (user_id) DO UPDATE SET last_training_date = EXCLUDED.last_training_date");
}
$stmtUpsert->execute([$editUserId, $newDate]);
// Systemnachricht in erledigte Admin-Nachrichten
$adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
if (empty($adminName)) $adminName = 'Ein Administrator';
$stmtUser = $pdo->prepare("SELECT first_name, last_name, mail FROM user_general_information WHERE user_id = ?");
$stmtUser->execute([$editUserId]);
$editedUser = $stmtUser->fetch(PDO::FETCH_ASSOC);
$editedName = trim(($editedUser['first_name'] ?? '') . ' ' . ($editedUser['last_name'] ?? ''));
if (empty($editedName)) $editedName = $editedUser['mail'] ?? 'Unbekannt';
$formattedDate = date('d.m.Y', strtotime($newDate));
$sysSubject = "Brandschutz-Schulungsdatum angepasst: {$editedName}";
$sysMessage = "{$adminName} hat das Schulungsdatum von {$editedName} manuell auf den {$formattedDate} gesetzt.";
$stmtMsg = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
$senderMail = $editedUser['mail'] ?? '';
$stmtMsg->execute([$adminName, $senderMail, $sysSubject, $sysMessage]);
header("Location: admin_brandschutz.php?message=5");
exit();
}
}
// Brandschutz-Nutzer laden
$sql = "SELECT ugi.user_id, ugi.first_name, ugi.last_name, ugi.mail,
bt.last_training_date, bt.reminder_count, bt.admin_reminder_count,
CASE WHEN ul.user_id IS NOT NULL THEN 1 ELSE 0 END AS IstRegistriert
FROM user_permissions up
INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id
LEFT JOIN brandschutz_training bt ON ugi.user_id = bt.user_id
LEFT JOIN user_login ul ON ugi.user_id = ul.user_id
WHERE up.\"Brandschutz\" = 1
ORDER BY bt.last_training_date ASC, ugi.last_name ASC, ugi.first_name ASC";
$bsUsers = $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
?>
<!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%);
}
/* 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">
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php include $g_phpSnippetsPath.'overlay_message.php'; ?>
<div class="header_title ht_admin text_white unselectable" id="header_title">
<span class="header_title_text">Adminpanel</span>
</div>
<div class="content" id="fullsize">
<?php $adminActiveTab = 'brandschutz'; require __DIR__.'/_tabs.php'; ?>
<div class="admin-users-shell">
<div class="admin-table-card">
<div class="admin-list-header">
<div>
<span class="admin-list-title">Brandschutz-Schulungen</span>
<div class="admin-list-hint"><?= count($bsUsers) ?> Nutzer mit Brandschutz-Rolle</div>
</div>
</div>
<table class="admin-user-table" id="bsTable">
<thead>
<tr>
<th>Profil</th>
<th>Name</th>
<th>Nächste Fälligkeit</th>
<th>Status</th>
</tr>
</thead>
<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));
}
}
?>
<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>
<button class="admin-profile-button" type="button" tabindex="-1" aria-hidden="true">
<img src="<?= htmlspecialchars($profileUrl) ?>" alt="Profilbild von <?= htmlspecialchars($nameOutput) ?>">
</button>
</td>
<td>
<span class="admin-user-name"><?= htmlspecialchars($nameOutput) ?></span>
</td>
<td>
<span class="bs-due-date"><?= $dueDateDisplay ?></span>
</td>
<td>
<span class="bs-status-badge <?= $statusClass ?>"><?= $statusText ?></span>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($bsUsers)): ?>
<tr><td colspan="4" style="padding:24px !important;text-align:center;color:hsl(210,20%,45%);background:#FFF !important;">Keine Nutzer mit der Rolle "Brandschutz" gefunden.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
<!-- Modals für jeden Brandschutz-Nutzer -->
<?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);
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';
}
}
?>
<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">
<button type="button" class="exit" aria-label="Schließen" onclick="closeBsModal(<?= (int)$bsUser['user_id'] ?>)">×</button>
<div class="admin-modal-hero">
<img class="admin-modal-profile-img" src="<?= htmlspecialchars($profileUrl) ?>" alt="Profilbild von <?= htmlspecialchars($nameOutput) ?>">
<div>
<div class="admin-modal-title"><?= htmlspecialchars($nameOutput) ?></div>
<div class="admin-modal-subtitle">
<span><?= htmlspecialchars($bsUser['mail'] ?? '') ?></span>
<span class="admin-modal-chip">Brandschutz</span>
</div>
<div class="admin-modal-subtitle" style="margin-bottom:0;">Schulungsdetails &amp; Fälligkeiten</div>
</div>
</div>
<div class="admin-modal-body">
<div class="admin-section-title">Schulungsstatus</div>
<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>
<div class="bs-detail-card">
<div class="bs-detail-label">Nächste Fälligkeit</div>
<div class="bs-detail-value"><?= $dueDateDisplay ?></div>
</div>
<div class="bs-detail-card">
<div class="bs-detail-label">Überfälligkeit</div>
<div class="bs-detail-value <?= $overdueClass ?>"><?= $overdueText ?></div>
</div>
<div class="bs-detail-card">
<div class="bs-detail-label">Erinnerungsmails gesendet</div>
<div class="bs-detail-value"><?= $reminderCount ?></div>
</div>
<?php if ($adminReminderCount > 0): ?>
<div class="bs-detail-card">
<div class="bs-detail-label">Admin-Eskalationen</div>
<div class="bs-detail-value overdue"><?= $adminReminderCount ?></div>
</div>
<?php endif; ?>
</div>
<div class="admin-section-title">Schulungsdatum anpassen</div>
<form method="post" action="">
<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;">
<label style="display:block;font-size:var(--font-size-xs);font-weight:600;color:var(--gray-600);margin-bottom:4px;">Datum der letzten Schulung</label>
<input type="date" name="bs_new_date" class="bs-date-input" value="<?= htmlspecialchars($lastTraining ?? '') ?>" required>
</div>
</div>
<p style="margin:0 0 16px 0;font-size:0.82em;color:hsl(210,14%,46%);font-weight:600;">Setzt das Schulungsdatum und alle Erinnerungszähler zurück.</p>
<div class="admin-modal-actions">
<div>
<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>
</div>
</div>
</form>
</div>
</div>
</div>
<?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'; ?>
</body>
</html>
@@ -0,0 +1,564 @@
<?php /* Name: admin_inbox.php
Beschreibung: Admin-Nachrichteninbox
- Registrierungsanfragen
- Kontaktformular-Anfragen
- Systemnachrichten
- Seelsorge-Nachrichten
- Info-Nachrichten */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
// Admin-Check
$stmt = $pdo->prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
$stmt->execute([$_SESSION["user_id"]]);
$isAdmin = $stmt->fetchColumn();
if ((int)$isAdmin !== 1) {
header("Location: {$g_httpHomePath}");
exit();
}
// Typ-Konfiguration für Inbox
$typeConfig = [
'registration' => ['label' => 'Registrierung', 'class' => 'inbox-type-registration'],
'contact' => ['label' => 'Kontakt', 'class' => 'inbox-type-contact'],
'system' => ['label' => 'System', 'class' => 'inbox-type-system'],
];
// Filter
$activeFilter = $_GET['filter'] ?? 'all';
$validFilters = array_merge(['all'], array_keys($typeConfig));
if (!in_array($activeFilter, $validFilters, true)) $activeFilter = 'all';
function getTypeLabel($type, $typeConfig) {
return $typeConfig[$type]['label'] ?? ucfirst($type);
}
function getTypeClass($type, $typeConfig) {
return $typeConfig[$type]['class'] ?? 'inbox-type-system';
}
// Kontaktnachricht als erledigt markieren
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["mark_done"])) {
$msgId = (int)$_POST["mark_done"];
$stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
$stmt->execute([$msgId]);
header("Location: admin_inbox.php?inbox_status=done");
exit();
}
// Brandschutz-Eskalation: Admin bestätigt persönliche Erinnerung
if ($_SERVER["REQUEST_METHOD"] === "POST" && isset($_POST["bs_escalation_confirm"])) {
$msgId = (int)$_POST["bs_escalation_msg_id"];
$bsUserId = (int)$_POST["bs_escalation_user_id"];
// Nachricht als erledigt markieren
$stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
$stmt->execute([$msgId]);
// Log-Eintrag erstellen
$adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
if (empty($adminName)) $adminName = 'Ein Administrator';
$stmtUser = $pdo->prepare("SELECT first_name, last_name, mail FROM user_general_information WHERE user_id = ?");
$stmtUser->execute([$bsUserId]);
$bsUserData = $stmtUser->fetch(PDO::FETCH_ASSOC);
$bsUserName = trim(($bsUserData['first_name'] ?? '') . ' ' . ($bsUserData['last_name'] ?? ''));
if (empty($bsUserName)) $bsUserName = $bsUserData['mail'] ?? 'Unbekannt';
$logSubject = "Persönliche Brandschutz-Erinnerung bestätigt: {$bsUserName}";
$logMessage = "{$adminName} hat bestätigt, {$bsUserName} persönlich an die ausstehende Brandschutzunterweisung erinnert zu haben.";
$stmtLog = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
$senderMail = $bsUserData['mail'] ?? '';
$stmtLog->execute([$adminName, $senderMail, $logSubject, $logMessage]);
header("Location: admin_inbox.php?inbox_status=done");
exit();
}
// Nachrichten laden (mit optionalem Filter)
$filterClause = '';
if ($activeFilter !== 'all') {
$filterClause = " AND type = '" . $activeFilter . "'";
}
$pendingMessages = $pdo->query("SELECT * FROM admin_messages WHERE status = 'pending'{$filterClause} ORDER BY created_at DESC")->fetchAll(PDO::FETCH_ASSOC);
$doneMessages = $pdo->query("SELECT * FROM admin_messages WHERE status = 'done'{$filterClause} ORDER BY processed_at DESC LIMIT 20")->fetchAll(PDO::FETCH_ASSOC);
// Rollen laden für Registrierungs-Modal
$permissionRoles = [];
$rolesResult = $pdo->query("SELECT column_name FROM information_schema.columns WHERE table_name = 'user_permissions' AND table_schema = 'public'");
while ($col = $rolesResult->fetch(PDO::FETCH_ASSOC)) {
if (!in_array($col['column_name'], ['p_id', 'user_id'], true)) {
$permissionRoles[] = $col['column_name'];
}
}
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<style>
.inbox-shell {
padding: 18px;
background: linear-gradient(135deg, var(--gray-50) 0%, var(--theme-light, var(--nak-blue-light)) 100%);
min-height: 100%;
}
.inbox-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;
margin-bottom: 18px;
}
.inbox-header {
display: flex;
align-items: center;
justify-content: space-between;
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));
}
.inbox-title {
font-size: 1.05em;
font-weight: 800;
color: hsl(210, 20%, 15%);
}
.inbox-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
padding: 0 7px;
background: hsl(0, 78%, 47%);
color: #FFFFFF;
font-size: 0.78em;
font-weight: 850;
}
.inbox-item {
padding: 16px 18px;
border-bottom: 1px solid rgba(120, 140, 160, 0.1);
transition: background 0.18s ease;
}
.inbox-item:last-child { border-bottom: none; }
.inbox-item:hover { background: hsl(210, 57%, 97%); }
.inbox-item-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 6px;
}
.inbox-item-type {
display: inline-flex;
align-items: center;
padding: 3px 8px;
font-size: 0.72em;
font-weight: 800;
}
.inbox-type-registration {
color: hsl(210, 57%, 34%);
background: hsl(210, 57%, 94%);
border: 1px solid hsl(210, 57%, 88%);
}
.inbox-type-contact {
color: hsl(145, 50%, 32%);
background: hsl(145, 50%, 93%);
border: 1px solid hsl(145, 50%, 85%);
}
.inbox-type-system {
color: hsl(30, 70%, 36%);
background: hsl(30, 70%, 94%);
border: 1px solid hsl(30, 70%, 85%);
}
.inbox-item-date {
font-size: 0.82em;
color: hsl(210, 14%, 50%);
font-weight: 600;
}
.inbox-item-subject {
font-weight: 800;
color: hsl(210, 20%, 15%);
margin-bottom: 4px;
}
.inbox-item-sender {
font-size: 0.9em;
color: hsl(210, 14%, 40%);
margin-bottom: 8px;
}
.inbox-item-message {
font-size: 0.9em;
color: hsl(210, 12%, 35%);
white-space: pre-wrap;
background: hsl(210, 12%, 98%);
padding: 10px 12px;
border: 1px solid hsl(210, 12%, 92%);
margin-bottom: 10px;
}
.inbox-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.inbox-actions form { display: inline; }
.inbox-btn {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 32px;
padding: 6px 12px;
border: 1px solid hsl(210, 18%, 84%);
background: #FFFFFF;
color: hsl(210, 20%, 22%);
font-size: 0.82em;
font-weight: 850;
cursor: pointer;
text-decoration: none;
box-shadow: 0 4px 12px rgba(31,38,46,0.06);
transition: background 0.18s ease;
}
.inbox-btn:hover { background: hsl(210, 57%, 96%); }
.inbox-btn-approve {
color: #FFFFFF !important;
background: linear-gradient(135deg, var(--nak-darkgreen), #005a10) !important;
border-color: #005a10 !important;
}
.inbox-btn-reject {
color: var(--nak-red) !important;
border-color: hsl(352, 72%, 88%) !important;
background: hsl(352, 72%, 97%) !important;
}
.inbox-btn-done {
color: hsl(210, 14%, 46%) !important;
background: hsl(210, 12%, 95%) !important;
border-color: hsl(210, 12%, 88%) !important;
}
.inbox-empty {
padding: 24px;
text-align: center;
color: hsl(210, 14%, 50%);
font-weight: 600;
}
.inbox-section-title {
margin: 18px 0 10px 0;
font-size: 0.88em;
font-weight: 850;
text-transform: uppercase;
letter-spacing: 0.04em;
color: hsl(210, 14%, 46%);
}
.inbox-done-item {
opacity: 0.6;
}
@media (max-width: 780px) {
.inbox-shell { padding: 10px; }
.inbox-item-header { flex-direction: column; align-items: flex-start; gap: 4px; }
.inbox-filter-bar { gap: 6px; }
.inbox-filter-btn { font-size: 0.72em; padding: 5px 10px; }
}
.inbox-filter-bar {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 14px;
}
.inbox-filter-btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 14px;
border: 1px solid hsl(210, 18%, 84%);
background: #FFF;
color: hsl(210, 18%, 42%);
font-size: 0.82em;
font-weight: 750;
cursor: pointer;
transition: all 0.18s ease;
text-decoration: none;
}
.inbox-filter-btn:hover { background: hsl(210, 57%, 96%); }
.inbox-filter-btn.active {
background: hsl(210, 57%, 94%);
border-color: hsl(210, 57%, 72%);
color: hsl(210, 57%, 30%);
font-weight: 850;
}
</style>
</head>
<body class="theme-admin">
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php include $g_phpSnippetsPath.'overlay_message.php'; ?>
<?php if (isset($_GET["inbox_status"])): ?>
<?php
$inboxStatusMap = [
'done' => ['type' => 'success', 'text' => 'Nachricht als erledigt markiert.'],
'approved' => ['type' => 'success', 'text' => 'Registrierung freigegeben.'],
'rejected' => ['type' => 'success', 'text' => 'Registrierung abgelehnt.'],
'error' => ['type' => 'error', 'text' => 'Ein Fehler ist aufgetreten.'],
];
$statusKey = $_GET["inbox_status"];
$statusInfo = $inboxStatusMap[$statusKey] ?? ['type' => 'success', 'text' => 'Aktion ausgeführt.'];
?>
<div class="nak-toast nak-toast--<?= $statusInfo['type'] ?>" id="nakToast">
<div class="nak-toast__icon">
<?php if ($statusInfo['type'] === 'success'): ?>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><path d="M20 6L9 17l-5-5"/></svg>
<?php else: ?>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<?php endif; ?>
</div>
<span class="nak-toast__text"><?= htmlspecialchars($statusInfo['text']) ?></span>
<button class="nak-toast__close" onclick="dismissNakToast()" aria-label="Schließen">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="square" stroke-linejoin="miter"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<script>
(function() {
var toast = document.getElementById('nakToast');
if (!toast) return;
requestAnimationFrame(function() { toast.classList.add('nak-toast--visible'); });
var timer = setTimeout(function() { dismissNakToast(); }, 4000);
toast._timer = timer;
if (window.history && window.history.replaceState) {
window.history.replaceState({}, '', 'admin_inbox.php');
}
})();
function dismissNakToast() {
var toast = document.getElementById('nakToast');
if (!toast || toast.classList.contains('nak-toast--hiding')) return;
clearTimeout(toast._timer);
toast.classList.add('nak-toast--hiding');
toast.classList.remove('nak-toast--visible');
setTimeout(function() { toast.remove(); }, 350);
}
</script>
<?php endif; ?>
<div class="header_title ht_admin text_white" id="header_title">
<span class="header_title_text">Adminpanel</span>
</div>
<div class="content" id="fullsize">
<?php $adminActiveTab = 'inbox'; require __DIR__.'/_tabs.php'; ?>
<div class="inbox-shell">
<!-- Filter-Leiste -->
<div class="inbox-filter-bar">
<a href="?filter=all" class="inbox-filter-btn <?= $activeFilter === 'all' ? 'active' : '' ?>">Alle</a>
<?php foreach ($typeConfig as $typeKey => $typeCfg): ?>
<a href="?filter=<?= $typeKey ?>" class="inbox-filter-btn <?= $activeFilter === $typeKey ? 'active' : '' ?>"><?= $typeCfg['label'] ?></a>
<?php endforeach; ?>
</div>
<!-- Offene Nachrichten -->
<div class="inbox-card">
<div class="inbox-header">
<div class="inbox-title">Offene Anfragen</div>
<?php if (count($pendingMessages) > 0): ?>
<span class="inbox-count"><?= count($pendingMessages) ?></span>
<?php endif; ?>
</div>
<?php if (count($pendingMessages) === 0): ?>
<div class="inbox-empty">Keine offenen Anfragen vorhanden.</div>
<?php else: ?>
<?php foreach ($pendingMessages as $msg): ?>
<div class="inbox-item">
<div class="inbox-item-header">
<div>
<span class="inbox-item-type <?= getTypeClass($msg['type'], $typeConfig) ?>">
<?= getTypeLabel($msg['type'], $typeConfig) ?>
</span>
</div>
<span class="inbox-item-date"><?= date('d.m.Y, H:i', strtotime($msg['created_at'])) ?> Uhr</span>
</div>
<div class="inbox-item-subject"><?= htmlspecialchars($msg['subject'] ?? '') ?></div>
<div class="inbox-item-sender">
Von: <?= htmlspecialchars($msg['sender_name'] ?? 'Unbekannt') ?>
(<?= htmlspecialchars($msg['sender_email'] ?? '') ?>)
</div>
<div class="inbox-item-message"><?= htmlspecialchars($msg['message'] ?? '') ?></div>
<div class="inbox-actions">
<?php if ($msg['type'] === 'registration'): ?>
<?php $data = json_decode($msg['data_json'], true); ?>
<button type="button" class="inbox-btn inbox-btn-approve" onclick="openRegistrationModal(<?= (int)($data['pending_id'] ?? 0) ?>, <?= (int)$msg['id'] ?>, '<?= htmlspecialchars($msg['sender_name'] ?? '', ENT_QUOTES) ?>', '<?= htmlspecialchars($msg['sender_email'] ?? '', ENT_QUOTES) ?>')">Anfrage bearbeiten</button>
<?php elseif ($msg['type'] === 'system' && !empty($msg['data_json'])): ?>
<?php
$data = json_decode($msg['data_json'], true);
if (isset($data['action']) && $data['action'] === 'brandschutz_escalation'):
?>
<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="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="mark_done" value="<?= (int)$msg['id'] ?>">
<button type="submit" class="inbox-btn inbox-btn-done">Als erledigt markieren</button>
</form>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
<!-- Erledigte Nachrichten -->
<?php if (count($doneMessages) > 0): ?>
<div class="inbox-section-title">Erledigte Anfragen (letzte 20)</div>
<div class="inbox-card">
<?php foreach ($doneMessages as $msg): ?>
<div class="inbox-item inbox-done-item">
<div class="inbox-item-header">
<div>
<span class="inbox-item-type <?= getTypeClass($msg['type'], $typeConfig) ?>">
<?= getTypeLabel($msg['type'], $typeConfig) ?>
</span>
</div>
<span class="inbox-item-date">Erledigt: <?= date('d.m.Y, H:i', strtotime($msg['processed_at'])) ?> Uhr</span>
</div>
<div class="inbox-item-subject"><?= htmlspecialchars($msg['subject'] ?? '') ?></div>
<div class="inbox-item-sender">
Von: <?= htmlspecialchars($msg['sender_name'] ?? 'Unbekannt') ?>
(<?= htmlspecialchars($msg['sender_email'] ?? '') ?>)
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div><!-- inbox-shell -->
</div><!-- content -->
<!-- Registrierungs-Modal -->
<div class="standard_overlay" id="registrationModal" onclick="if(event.target.id==='registrationModal')closeRegistrationModal()">
<div style="position:relative;width:min(560px,calc(100vw - 24px));max-height:min(700px,calc(100vh - 24px));overflow:auto;background:#FFF;box-shadow:0 24px 70px rgba(0,0,0,0.36);">
<button type="button" aria-label="Schließen" onclick="closeRegistrationModal()" style="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:#FFF;font-size:1.35em;cursor:pointer;z-index:1;">×</button>
<div style="padding:24px;color:#FFF;background:radial-gradient(circle at 10% 0%,rgba(255,255,255,0.24),transparent 32%),linear-gradient(135deg,var(--gray-800),var(--gray-900));">
<div style="font-size:1.3em;font-weight:850;">Registrierung bearbeiten</div>
<div id="regModalUserInfo" style="margin-top:6px;color:rgba(255,255,255,0.8);font-weight:600;"></div>
</div>
<div style="padding:22px 24px 24px 24px;">
<form method="post" action="approve-registration.php" id="regModalForm">
<input type="hidden" name="pending_id" id="regModalPendingId">
<input type="hidden" name="message_id" id="regModalMessageId">
<input type="hidden" name="action" id="regModalAction">
<div style="margin-bottom:16px;">
<label style="display:block;margin-bottom:8px;font-weight:750;font-size:0.92em;color:hsl(210,18%,36%);">Rollen zuweisen:</label>
<div style="display:flex;flex-wrap:wrap;gap:8px;">
<?php foreach ($permissionRoles as $role): ?>
<label style="display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border:1px solid hsl(210,18%,84%);background:hsl(210,12%,98%);cursor:pointer;font-weight:650;font-size:0.88em;hyphens:auto;-webkit-hyphens:auto;overflow-wrap:break-word;">
<input type="checkbox" name="roles[]" value="<?= htmlspecialchars($role) ?>" style="accent-color:hsl(210,57%,45%);flex-shrink:0;min-width:16px;min-height:16px;">
<?= htmlspecialchars(genderRole($role)) ?>
</label>
<?php endforeach; ?>
</div>
</div>
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:20px;">
<button type="button" onclick="submitRegistrationAction('reject')" class="inbox-btn inbox-btn-reject" style="padding:10px 18px;font-size:0.9em;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" style="vertical-align:middle;margin-right:4px;"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>Ablehnen</button>
<button type="button" onclick="submitRegistrationAction('approve')" class="inbox-btn inbox-btn-approve" style="padding:10px 18px;font-size:0.9em;"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" style="vertical-align:middle;margin-right:4px;"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>Freischalten</button>
</div>
</form>
</div>
</div>
</div>
<script>
function openRegistrationModal(pendingId, messageId, name, email) {
document.getElementById('regModalPendingId').value = pendingId;
document.getElementById('regModalMessageId').value = messageId;
document.getElementById('regModalUserInfo').textContent = name + ' (' + email + ')';
document.getElementById('registrationModal').style.display = 'flex';
}
function closeRegistrationModal() {
document.getElementById('registrationModal').style.display = 'none';
}
function submitRegistrationAction(action) {
var confirmMsg = action === 'approve' ? 'Nutzer mit den gewählten Rollen freischalten?' : 'Registrierung ablehnen?';
if (!confirm(confirmMsg)) return;
document.getElementById('regModalAction').value = action;
document.getElementById('regModalForm').submit();
}
// Brandschutz Eskalation Modal
function openBsEscalationModal(msgId, userId, userName, daysOverdue) {
document.getElementById('bsEscMsgId').value = msgId;
document.getElementById('bsEscUserId').value = userId;
document.getElementById('bsEscUserName').textContent = userName;
document.getElementById('bsEscDaysOverdue').textContent = daysOverdue + ' Tage';
document.getElementById('bsEscalationModal').style.display = 'flex';
}
function closeBsEscalationModal() {
document.getElementById('bsEscalationModal').style.display = 'none';
}
function submitBsEscalation() {
document.getElementById('bsEscForm').submit();
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
if (document.getElementById('registrationModal').style.display === 'flex') {
closeRegistrationModal();
}
if (document.getElementById('bsEscalationModal').style.display === 'flex') {
closeBsEscalationModal();
}
}
});
</script>
<!-- Brandschutz Eskalation Modal -->
<div class="standard_overlay" id="bsEscalationModal" onclick="if(event.target.id==='bsEscalationModal')closeBsEscalationModal()">
<div style="position:relative;width:min(520px,calc(100vw - 24px));max-height:min(500px,calc(100vh - 24px));overflow:auto;background:#FFF;box-shadow:0 24px 70px rgba(0,0,0,0.36);">
<button type="button" aria-label="Schließen" onclick="closeBsEscalationModal()" style="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:#FFF;font-size:1.35em;cursor:pointer;z-index:1;">×</button>
<div style="padding:24px;color:#FFF;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%));">
<div style="font-size:1.3em;font-weight:850;">Persönliche Erinnerung bestätigen</div>
<div style="margin-top:6px;color:rgba(255,255,255,0.8);font-weight:600;">Brandschutzunterweisung – Eskalation</div>
</div>
<div style="padding:22px 24px 24px 24px;">
<p style="margin:0 0 14px 0;font-size:0.95em;color:hsl(210,20%,25%);line-height:1.6;">
Bitte bestätigen Sie, dass Sie <strong id="bsEscUserName"></strong> persönlich an die ausstehende Brandschutzunterweisung erinnert haben.
</p>
<div style="padding:12px 14px;background:hsl(0,65%,95%);border:1px solid hsl(0,65%,85%);margin-bottom:18px;">
<div style="font-size:0.78em;font-weight:850;text-transform:uppercase;letter-spacing:0.04em;color:hsl(0,65%,38%);margin-bottom:4px;">Überfälligkeit</div>
<div style="font-size:1.1em;font-weight:850;color:hsl(0,65%,35%);" id="bsEscDaysOverdue"></div>
</div>
<form method="post" action="" id="bsEscForm">
<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">
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:16px;">
<button type="button" onclick="closeBsEscalationModal()" class="inbox-btn" style="padding:10px 18px;font-size:0.9em;">Abbrechen</button>
<button type="submit" class="inbox-btn inbox-btn-approve" style="padding:10px 18px;font-size:0.9em;background:linear-gradient(135deg,var(--nak-orange),hsl(25,95%,40%)) !important;border-color:hsl(25,95%,40%) !important;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" style="vertical-align:middle;margin-right:4px;"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
Bestätigen
</button>
</div>
</form>
</div>
</div>
</div>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,471 @@
<?php
/* Name: admin_notices.php
Beschreibung: Verwaltung der Meldungen auf dem Intranet-Hauptmenü */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
// Admin-Check
$stmt = $pdo->prepare('SELECT "Admin" FROM user_permissions WHERE user_id = ?');
$stmt->execute([$_SESSION['user_id']]);
$isAdmin = $stmt->fetchColumn();
if ((int)$isAdmin !== 1) {
header("Location: {$g_httpHomePath}");
exit();
}
const NOTICE_MAX_TITLE_LENGTH = 180;
const NOTICE_MAX_MESSAGE_LENGTH = 4000;
$noticeTypes = [
'info' => 'Information',
'warning' => 'Warnung',
'success' => 'Erfolg',
'error' => 'Wichtig',
];
if (empty($_SESSION['admin_notices_csrf'])) {
$_SESSION['admin_notices_csrf'] = bin2hex(random_bytes(32));
}
$csrfToken = $_SESSION['admin_notices_csrf'];
function noticeRedirect(string $status): void {
header('Location: admin_notices.php?status=' . rawurlencode($status));
exit();
}
function noticeVerifyCsrf(): void {
$postedToken = $_POST['csrf_token'] ?? '';
$sessionToken = $_SESSION['admin_notices_csrf'] ?? '';
if (!is_string($postedToken) || !is_string($sessionToken) || $sessionToken === '' || !hash_equals($sessionToken, $postedToken)) {
http_response_code(400);
exit('Ungültige Anfrage. Bitte laden Sie die Seite neu.');
}
}
function noticeParseLocalDateTime(?string $value): ?string {
$value = trim((string)$value);
if ($value === '') {
return null;
}
$tz = new DateTimeZone('Europe/Berlin');
$date = DateTimeImmutable::createFromFormat('!Y-m-d\TH:i', $value, $tz);
$errors = DateTimeImmutable::getLastErrors();
if (!$date || ($errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0))) {
throw new InvalidArgumentException('Ungültiges Datum.');
}
// Mit explizitem Offset an PostgreSQL senden, damit TIMESTAMPTZ eindeutig ist.
return $date->format('Y-m-d H:i:sP');
}
function noticeFormatLocalDateTime(?string $value): string {
if (empty($value)) {
return '';
}
try {
return (new DateTimeImmutable($value))->setTimezone(new DateTimeZone('Europe/Berlin'))->format('Y-m-d\TH:i');
} catch (Throwable $e) {
return '';
}
}
function noticeDisplayDate(?string $value): string {
if (empty($value)) {
return 'unbegrenzt';
}
try {
return (new DateTimeImmutable($value))->setTimezone(new DateTimeZone('Europe/Berlin'))->format('d.m.Y, H:i') . ' Uhr';
} catch (Throwable $e) {
return '–';
}
}
function noticeDbBool($value): bool {
return $value === true || $value === 1 || $value === '1' || $value === 't' || $value === 'true';
}
function noticeTextLength(string $value): int {
return function_exists('mb_strlen') ? mb_strlen($value, 'UTF-8') : strlen($value);
}
function noticeValidateInput(array $noticeTypes): array {
$title = trim((string)($_POST['title'] ?? ''));
$message = trim((string)($_POST['message'] ?? ''));
$type = (string)($_POST['type'] ?? 'info');
$sortOrder = filter_var($_POST['sort_order'] ?? 0, FILTER_VALIDATE_INT);
$isActive = isset($_POST['is_active']);
if ($title === '' || noticeTextLength($title) > NOTICE_MAX_TITLE_LENGTH) {
throw new InvalidArgumentException('Titel fehlt oder ist zu lang.');
}
if ($message === '' || noticeTextLength($message) > NOTICE_MAX_MESSAGE_LENGTH) {
throw new InvalidArgumentException('Meldung fehlt oder ist zu lang.');
}
if (!array_key_exists($type, $noticeTypes)) {
throw new InvalidArgumentException('Ungültiger Meldungstyp.');
}
if ($sortOrder === false || $sortOrder < -9999 || $sortOrder > 9999) {
throw new InvalidArgumentException('Ungültige Sortierung.');
}
$startsAt = noticeParseLocalDateTime($_POST['starts_at'] ?? null);
$endsAt = noticeParseLocalDateTime($_POST['ends_at'] ?? null);
if ($startsAt !== null && $endsAt !== null) {
$start = new DateTimeImmutable($startsAt);
$end = new DateTimeImmutable($endsAt);
if ($end <= $start) {
throw new InvalidArgumentException('Das Ende muss nach dem Beginn liegen.');
}
}
return [
'title' => $title,
'message' => $message,
'type' => $type,
'sort_order' => (int)$sortOrder,
'is_active' => $isActive,
'starts_at' => $startsAt,
'ends_at' => $endsAt,
];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
noticeVerifyCsrf();
$action = (string)($_POST['action'] ?? '');
try {
if ($action === 'create') {
$data = noticeValidateInput($noticeTypes);
$stmt = $pdo->prepare(
'INSERT INTO mainpage_notices
(title, message, type, is_active, starts_at, ends_at, sort_order, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())'
);
$stmt->execute([
$data['title'], $data['message'], $data['type'], $data['is_active'] ? 'true' : 'false',
$data['starts_at'], $data['ends_at'], $data['sort_order'], (int)$_SESSION['user_id'],
]);
noticeRedirect('created');
}
if ($action === 'update') {
$id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
if (!$id || $id < 1) {
throw new InvalidArgumentException('Ungültige Meldungs-ID.');
}
$data = noticeValidateInput($noticeTypes);
$stmt = $pdo->prepare(
'UPDATE mainpage_notices
SET title = ?, message = ?, type = ?, is_active = ?, starts_at = ?, ends_at = ?, sort_order = ?, updated_at = NOW()
WHERE id = ?'
);
$stmt->execute([
$data['title'], $data['message'], $data['type'], $data['is_active'] ? 'true' : 'false',
$data['starts_at'], $data['ends_at'], $data['sort_order'], (int)$id,
]);
noticeRedirect('updated');
}
if ($action === 'toggle') {
$id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
if (!$id || $id < 1) {
throw new InvalidArgumentException('Ungültige Meldungs-ID.');
}
$stmt = $pdo->prepare('UPDATE mainpage_notices SET is_active = NOT is_active, updated_at = NOW() WHERE id = ?');
$stmt->execute([(int)$id]);
noticeRedirect('toggled');
}
if ($action === 'delete') {
$id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
if (!$id || $id < 1) {
throw new InvalidArgumentException('Ungültige Meldungs-ID.');
}
$stmt = $pdo->prepare('DELETE FROM mainpage_notices WHERE id = ?');
$stmt->execute([(int)$id]);
noticeRedirect('deleted');
}
throw new InvalidArgumentException('Unbekannte Aktion.');
} catch (InvalidArgumentException $e) {
$_SESSION['admin_notices_error'] = $e->getMessage();
noticeRedirect('error');
} catch (PDOException $e) {
$_SESSION['admin_notices_error'] = 'Datenbankfehler. Prüfen Sie, ob die Tabelle mainpage_notices angelegt wurde.';
noticeRedirect('error');
}
}
$noticeError = $_SESSION['admin_notices_error'] ?? '';
unset($_SESSION['admin_notices_error']);
$status = (string)($_GET['status'] ?? '');
$statusMessages = [
'created' => 'Meldung wurde angelegt.',
'updated' => 'Meldung wurde gespeichert.',
'toggled' => 'Status der Meldung wurde geändert.',
'deleted' => 'Meldung wurde gelöscht.',
];
$tableAvailable = true;
$notices = [];
try {
$notices = $pdo->query(
'SELECT id, title, message, type, is_active, starts_at, ends_at, sort_order, created_at, updated_at
FROM mainpage_notices
ORDER BY sort_order ASC, created_at DESC, id DESC'
)->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
$tableAvailable = false;
$noticeError = 'Die Tabelle mainpage_notices fehlt. Führen Sie zuerst general_files/ini/mainpage_notices.sql aus.';
}
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<style>
.notice-admin-shell { padding: 18px; min-height: 100%; }
.notice-admin-card { background:#fff; border:1px solid var(--gray-200); box-shadow:var(--shadow-md); margin-bottom:18px; }
.notice-admin-header { display:flex; align-items:center; justify-content:space-between; gap:16px; padding:16px 18px; border-bottom:1px solid var(--gray-200); }
.notice-admin-title { font-size:1.05em; font-weight:800; color:var(--gray-900); }
.notice-admin-hint { margin-top:3px; font-size:.86em; color:var(--gray-500); }
.notice-admin-button { display:inline-flex; align-items:center; justify-content:center; min-height:36px; padding:7px 14px; border:1px solid var(--gray-800); background:var(--gray-900); color:#fff; font:inherit; font-size:.86em; font-weight:750; cursor:pointer; text-decoration:none; }
.notice-admin-button:hover { color:#fff; background:#111; text-decoration:none; }
.notice-admin-button--secondary { color:var(--gray-800); background:#fff; border-color:var(--gray-300); }
.notice-admin-button--secondary:hover { color:var(--gray-900); background:var(--gray-50); }
.notice-admin-button--danger { color:var(--nak-red); background:#fff; border-color:hsl(352,72%,84%); }
.notice-admin-button--danger:hover { color:#fff; background:var(--nak-red); }
.notice-admin-grid { display:grid; gap:12px; padding:16px 18px 18px; }
.notice-admin-item { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:16px; padding:16px; border:1px solid var(--gray-200); background:var(--gray-50); }
.notice-admin-item.is-disabled { opacity:.62; }
.notice-admin-item-title { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:6px; font-weight:800; color:var(--gray-900); }
.notice-admin-chip { display:inline-flex; align-items:center; padding:3px 8px; font-size:.72em; font-weight:800; border-left:3px solid currentColor; background:#fff; }
.notice-admin-chip--info { color:var(--nak-blue); }
.notice-admin-chip--warning { color:var(--nak-orange); }
.notice-admin-chip--success { color:var(--nak-darkgreen); }
.notice-admin-chip--error { color:var(--nak-red); }
.notice-admin-message { white-space:pre-wrap; line-height:1.45; color:var(--gray-700); font-size:.92em; }
.notice-admin-meta { display:flex; gap:14px; flex-wrap:wrap; margin-top:10px; color:var(--gray-500); font-size:.8em; }
.notice-admin-actions { display:flex; align-items:flex-start; gap:7px; flex-wrap:wrap; justify-content:flex-end; }
.notice-admin-actions form { margin:0; }
.notice-admin-empty { padding:28px 18px; text-align:center; color:var(--gray-500); }
.notice-admin-alert { margin:16px 18px 0; padding:11px 13px; border-left:4px solid var(--nak-darkgreen); background:hsl(145,50%,94%); color:hsl(145,50%,28%); font-size:.9em; }
.notice-admin-alert--error { border-color:var(--nak-red); background:hsl(352,72%,96%); color:hsl(352,62%,35%); }
.notice-modal { position:fixed; inset:0; z-index:5000; display:none; align-items:center; justify-content:center; padding:20px; background:rgba(16,24,32,.62); }
.notice-modal.open { display:flex; }
.notice-modal-card { width:min(680px,100%); max-height:calc(100dvh - 40px); overflow:auto; background:#fff; box-shadow:var(--shadow-xl); }
.notice-modal-header { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:18px 20px; border-bottom:1px solid var(--gray-200); }
.notice-modal-title { font-size:1.08em; font-weight:800; color:var(--gray-900); }
.notice-modal-close { border:0; background:transparent; font-size:1.5em; line-height:1; cursor:pointer; color:var(--gray-500); }
.notice-modal-body { padding:20px; }
.notice-form-grid { display:grid; grid-template-columns:1fr 1fr; gap:14px; }
.notice-field { display:flex; flex-direction:column; gap:6px; }
.notice-field--full { grid-column:1 / -1; }
.notice-field label { font-size:.83em; font-weight:750; color:var(--gray-700); }
.notice-field input, .notice-field select, .notice-field textarea { width:100%; box-sizing:border-box; padding:10px 11px; border:1px solid var(--gray-300); background:#fff; color:var(--gray-900); font:inherit; font-size:.9em; }
.notice-field textarea { min-height:120px; resize:vertical; }
.notice-checkbox { display:flex; align-items:center; gap:8px; min-height:40px; }
.notice-checkbox input { width:auto; }
.notice-form-note { margin-top:5px; font-size:.78em; color:var(--gray-500); }
.notice-modal-actions { display:flex; justify-content:flex-end; gap:8px; padding:0 20px 20px; }
@media (max-width:760px) {
.notice-admin-shell { padding:10px; }
.notice-admin-header, .notice-admin-item { grid-template-columns:1fr; display:grid; }
.notice-admin-actions { justify-content:flex-start; }
.notice-form-grid { grid-template-columns:1fr; }
.notice-field--full { grid-column:auto; }
.content-tab-bar { overflow-x:auto; }
}
</style>
</head>
<body class="theme-admin">
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php include $g_phpSnippetsPath.'overlay_message.php'; ?>
<div class="header_title ht_admin text_white unselectable" id="header_title">
<span class="header_title_text">Adminpanel</span>
</div>
<div class="content" id="fullsize">
<?php $adminActiveTab = 'notices'; require __DIR__.'/_tabs.php'; ?>
<div class="notice-admin-shell">
<div class="notice-admin-card">
<div class="notice-admin-header">
<div>
<div class="notice-admin-title">Meldungen im Hauptmenü</div>
<div class="notice-admin-hint">Meldungen erscheinen ausschließlich oberhalb der Kacheln auf der Intranet-Hauptseite.</div>
</div>
<?php if ($tableAvailable): ?>
<button type="button" class="notice-admin-button" onclick="openNoticeModal()">+ Neue Meldung</button>
<?php endif; ?>
</div>
<?php if ($status === 'error' && $noticeError !== ''): ?>
<div class="notice-admin-alert notice-admin-alert--error"><?= htmlspecialchars($noticeError, ENT_QUOTES, 'UTF-8') ?></div>
<?php elseif (!$tableAvailable && $noticeError !== ''): ?>
<div class="notice-admin-alert notice-admin-alert--error"><?= htmlspecialchars($noticeError, ENT_QUOTES, 'UTF-8') ?></div>
<?php elseif (isset($statusMessages[$status])): ?>
<div class="notice-admin-alert"><?= htmlspecialchars($statusMessages[$status], ENT_QUOTES, 'UTF-8') ?></div>
<?php endif; ?>
<?php if ($tableAvailable): ?>
<?php if (empty($notices)): ?>
<div class="notice-admin-empty">Noch keine Meldungen vorhanden.</div>
<?php else: ?>
<div class="notice-admin-grid">
<?php foreach ($notices as $notice): ?>
<?php
$noticeId = (int)$notice['id'];
$noticeType = array_key_exists($notice['type'], $noticeTypes) ? $notice['type'] : 'info';
$editPayload = [
'id' => $noticeId,
'title' => $notice['title'],
'message' => $notice['message'],
'type' => $noticeType,
'is_active' => noticeDbBool($notice['is_active']),
'starts_at' => noticeFormatLocalDateTime($notice['starts_at']),
'ends_at' => noticeFormatLocalDateTime($notice['ends_at']),
'sort_order' => (int)$notice['sort_order'],
];
?>
<div class="notice-admin-item<?= noticeDbBool($notice['is_active']) ? '' : ' is-disabled' ?>">
<div>
<div class="notice-admin-item-title">
<?= htmlspecialchars($notice['title'], ENT_QUOTES, 'UTF-8') ?>
<span class="notice-admin-chip notice-admin-chip--<?= htmlspecialchars($noticeType, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($noticeTypes[$noticeType], ENT_QUOTES, 'UTF-8') ?></span>
<?php if (!noticeDbBool($notice['is_active'])): ?><span class="notice-admin-chip">Deaktiviert</span><?php endif; ?>
</div>
<div class="notice-admin-message"><?= htmlspecialchars($notice['message'], ENT_QUOTES, 'UTF-8') ?></div>
<div class="notice-admin-meta">
<span>Von: <?= htmlspecialchars(noticeDisplayDate($notice['starts_at']), ENT_QUOTES, 'UTF-8') ?></span>
<span>Bis: <?= htmlspecialchars(noticeDisplayDate($notice['ends_at']), ENT_QUOTES, 'UTF-8') ?></span>
<span>Sortierung: <?= (int)$notice['sort_order'] ?></span>
</div>
</div>
<div class="notice-admin-actions">
<button type="button" class="notice-admin-button notice-admin-button--secondary" onclick='openNoticeModal(<?= json_encode($editPayload, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?>)'>Bearbeiten</button>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="action" value="toggle">
<input type="hidden" name="id" value="<?= $noticeId ?>">
<button type="submit" class="notice-admin-button notice-admin-button--secondary"><?= noticeDbBool($notice['is_active']) ? 'Deaktivieren' : 'Aktivieren' ?></button>
</form>
<form method="post" onsubmit="return confirm('Meldung wirklich dauerhaft löschen?');">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= $noticeId ?>">
<button type="submit" class="notice-admin-button notice-admin-button--danger">Löschen</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="notice-modal" id="noticeModal" aria-hidden="true">
<div class="notice-modal-card" role="dialog" aria-modal="true" aria-labelledby="noticeModalTitle">
<form method="post" id="noticeForm">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>">
<input type="hidden" name="action" id="noticeAction" value="create">
<input type="hidden" name="id" id="noticeId" value="">
<div class="notice-modal-header">
<div class="notice-modal-title" id="noticeModalTitle">Neue Meldung</div>
<button type="button" class="notice-modal-close" onclick="closeNoticeModal()" aria-label="Schließen">×</button>
</div>
<div class="notice-modal-body">
<div class="notice-form-grid">
<div class="notice-field notice-field--full">
<label for="noticeTitle">Titel</label>
<input id="noticeTitle" name="title" type="text" maxlength="180" required>
</div>
<div class="notice-field notice-field--full">
<label for="noticeMessage">Meldung</label>
<textarea id="noticeMessage" name="message" maxlength="4000" required></textarea>
</div>
<div class="notice-field">
<label for="noticeType">Typ</label>
<select id="noticeType" name="type">
<?php foreach ($noticeTypes as $typeKey => $typeLabel): ?>
<option value="<?= htmlspecialchars($typeKey, ENT_QUOTES, 'UTF-8') ?>"><?= htmlspecialchars($typeLabel, ENT_QUOTES, 'UTF-8') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="notice-field">
<label for="noticeSortOrder">Sortierung</label>
<input id="noticeSortOrder" name="sort_order" type="number" min="-9999" max="9999" value="0" required>
<div class="notice-form-note">Kleinere Zahlen erscheinen zuerst.</div>
</div>
<div class="notice-field">
<label for="noticeStartsAt">Anzeigen ab</label>
<input id="noticeStartsAt" name="starts_at" type="datetime-local">
<div class="notice-form-note">Leer = sofort.</div>
</div>
<div class="notice-field">
<label for="noticeEndsAt">Anzeigen bis</label>
<input id="noticeEndsAt" name="ends_at" type="datetime-local">
<div class="notice-form-note">Leer = ohne Ablaufdatum.</div>
</div>
<div class="notice-field notice-field--full">
<label class="notice-checkbox">
<input id="noticeIsActive" name="is_active" type="checkbox" value="1" checked>
Meldung aktiv
</label>
</div>
</div>
</div>
<div class="notice-modal-actions">
<button type="button" class="notice-admin-button notice-admin-button--secondary" onclick="closeNoticeModal()">Abbrechen</button>
<button type="submit" class="notice-admin-button">Speichern</button>
</div>
</form>
</div>
</div>
<script>
function openNoticeModal(data) {
data = data || null;
var modal = document.getElementById('noticeModal');
document.getElementById('noticeAction').value = data ? 'update' : 'create';
document.getElementById('noticeId').value = data ? data.id : '';
document.getElementById('noticeTitle').value = data ? data.title : '';
document.getElementById('noticeMessage').value = data ? data.message : '';
document.getElementById('noticeType').value = data ? data.type : 'warning';
document.getElementById('noticeSortOrder').value = data ? data.sort_order : 0;
document.getElementById('noticeStartsAt').value = data ? data.starts_at : '';
document.getElementById('noticeEndsAt').value = data ? data.ends_at : '';
document.getElementById('noticeIsActive').checked = data ? !!data.is_active : true;
document.getElementById('noticeModalTitle').textContent = data ? 'Meldung bearbeiten' : 'Neue Meldung';
modal.classList.add('open');
modal.setAttribute('aria-hidden', 'false');
document.getElementById('noticeTitle').focus();
}
function closeNoticeModal() {
var modal = document.getElementById('noticeModal');
modal.classList.remove('open');
modal.setAttribute('aria-hidden', 'true');
}
document.getElementById('noticeModal').addEventListener('click', function(event) {
if (event.target === this) closeNoticeModal();
});
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') closeNoticeModal();
});
</script>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,53 @@
<?php /* Name: admin_profiles.php
Beschreibung: Administratoren-Seite zur Nutzerverwaltung */
// Initialisierung
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
?>
<!DOCTYPE html>
<html lang="de" dir="ltr">
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
</head>
<body class="theme-admin">
<?php /* Besondere Admin-Rechte checken */
// Fetch user details
$stmt = $pdo->prepare("SELECT * FROM user_permissions up INNER JOIN user_general_information ugi ON up.user_id = ugi.user_id WHERE up.user_id = ?");
$stmt->execute([$_SESSION["user_id"]]);
$isAdmin = $stmt->fetch(PDO::FETCH_ASSOC);
// Check if the user has admin access (Admin = 1)
if ($isAdmin["Admin"] == 1) {
$sql_groups = "SELECT * FROM user_permissions";
$stmt = $pdo->prepare($sql_groups);
$stmt->execute([]);
$groups = $stmt->fetchAll(PDO::FETCH_ASSOC);
} else {
header("Location: {$g_httpHomePath}");
exit();
}
?>
<?php // Es folgen ab hier die HTML-Ausgaben beginnend mit dem HTML-Header: ?>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/header.php'; ?>
<?php // Header (einfach): Titel der Seite ?>
<div class="header_title ht_admin text_white" id="header_title">
<div class="flexrow fullwidth" style="justify-content:space-between !important;">
<div class="header_title_text">Nutzerverwaltung</div>
</div>
</div>
<?php // Profilanzeige
require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/projects/intranet/show_profile.php"); ?>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/footer.php'; ?>
</body>
</html>
@@ -0,0 +1,182 @@
<?php /* Name: approve-registration.php
Beschreibung: Admin genehmigt oder lehnt eine Registrierungsanfrage ab.
- Setzt Rollen bei Freischaltung
- Sendet Mails an Nutzer UND Admins
POST-Parameter: action (approve/reject), pending_id, message_id, roles[] */
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/initialise.php';
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
// Admin-Check
$stmt = $pdo->prepare("SELECT \"Admin\" FROM user_permissions WHERE user_id = ?");
$stmt->execute([$_SESSION["user_id"]]);
$isAdmin = $stmt->fetchColumn();
if ((int)$isAdmin !== 1) {
header("Location: {$g_httpHomePath}");
exit();
}
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
header("Location: admin_inbox.php");
exit();
}
// Admin-Name ermitteln
$adminName = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
if (empty($adminName)) $adminName = 'Ein Administrator';
$action = $_POST["action"] ?? '';
$pending_id = (int)($_POST["pending_id"] ?? 0);
$message_id = (int)($_POST["message_id"] ?? 0);
$selectedRoles = $_POST["roles"] ?? [];
if (!is_array($selectedRoles)) $selectedRoles = [];
if ($pending_id === 0) {
header("Location: admin_inbox.php?inbox_status=error");
exit();
}
// Pending Registration laden
$stmt = $pdo->prepare("SELECT * FROM pending_registrations WHERE id = ? AND status = 'pending'");
$stmt->execute([$pending_id]);
$pending = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$pending) {
header("Location: admin_inbox.php?inbox_status=error");
exit();
}
// Rollen-Spalten laden
$permissionRoles = [];
$rolesResult = $pdo->query("SELECT column_name FROM information_schema.columns WHERE table_name = 'user_permissions' AND table_schema = 'public'");
while ($col = $rolesResult->fetch(PDO::FETCH_ASSOC)) {
if (!in_array($col['column_name'], ['p_id', 'user_id'], true)) {
$permissionRoles[] = $col['column_name'];
}
}
$loginUrl = "https://" . ($_SERVER['HTTP_HOST'] ?? intranet_env('INTRANET_FQDN')) . "/intranet_repo/projects/authorization/login.php";
if ($action === 'approve') {
$pdo->beginTransaction();
try {
// Nutzer in user_general_information anlegen
$stmt = $pdo->prepare("INSERT INTO user_general_information (mail, first_name, last_name, phone) VALUES (?, ?, ?, ?)");
$stmt->execute([$pending['email'], $pending['first_name'], $pending['last_name'], $pending['phone']]);
$newUserId = $pdo->lastInsertId();
if (!$newUserId) {
$checkStmt = $pdo->prepare("SELECT user_id FROM user_general_information WHERE mail = ?");
$checkStmt->execute([$pending['email']]);
$newUserId = $checkStmt->fetchColumn();
}
// Login-Daten anlegen
$stmt = $pdo->prepare("INSERT INTO user_login (user_id, password_hash) VALUES (?, ?)");
$stmt->execute([$newUserId, $pending['password_hash']]);
// Berechtigungen mit gewählten Rollen anlegen
$roleColumns = array_map(function($r) { return '"' . str_replace('"', '""', $r) . '"'; }, $permissionRoles);
$columns = array_merge(['"user_id"'], $roleColumns);
$placeholders = array_fill(0, count($columns), '?');
$roleValues = [];
foreach ($permissionRoles as $role) {
$roleValues[] = in_array($role, $selectedRoles, true) ? 1 : 0;
}
$params = array_merge([(int)$newUserId], $roleValues);
$insertSql = "INSERT INTO user_permissions (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
$stmt = $pdo->prepare($insertSql);
$stmt->execute($params);
// Pending-Status aktualisieren
$stmt = $pdo->prepare("UPDATE pending_registrations SET status = 'approved' WHERE id = ?");
$stmt->execute([$pending_id]);
// Inbox-Nachricht als erledigt markieren
if ($message_id > 0) {
$stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
$stmt->execute([$message_id]);
}
$pdo->commit();
// Zugewiesene Rollen als Text
$assignedRoles = !empty($selectedRoles) ? implode(', ', $selectedRoles) : 'Keine besonderen Rollen';
// Mail an den Nutzer
$userBetreff = "Registrierung freigeschaltet";
$userNachricht = "Hallo {$pending['first_name']},\n\n";
$userNachricht .= "Ihr Account für das Intranet der NAK Reinickendorf wurde freigeschaltet!\n\n";
$userNachricht .= "Sie können sich ab sofort mit Ihrer E-Mail-Adresse und dem von Ihnen gewählten Passwort anmelden.";
send_intranet_mail($pending['email'], $userBetreff, $userNachricht, null, "intranet@nak-reinickendorf.de", "Jetzt anmelden", $loginUrl);
// Mail an Admin-Team
$adminBetreff = "Registrierung freigegeben: {$pending['first_name']} {$pending['last_name']}";
$adminNachricht = "{$adminName} hat die Registrierung bearbeitet:\n\n";
$adminNachricht .= "Nutzer: {$pending['first_name']} {$pending['last_name']}\n";
$adminNachricht .= "E-Mail: {$pending['email']}\n";
$adminNachricht .= "Aktion: Freigeschaltet\n";
$adminNachricht .= "Zugewiesene Rollen: {$assignedRoles}";
send_admin_team_mail($pdo, $adminBetreff, $adminNachricht);
// Systemnachricht im Posteingang
$sysSubject = "Nutzer freigeschaltet: {$pending['first_name']} {$pending['last_name']}";
$sysMessage = "{$adminName} hat {$pending['first_name']} {$pending['last_name']} ({$pending['email']}) freigeschaltet.\nZugewiesene Rollen: {$assignedRoles}";
$stmtSys = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
$stmtSys->execute([$adminName, $pending['email'], $sysSubject, $sysMessage]);
header("Location: admin_inbox.php?inbox_status=approved");
exit();
} catch (Throwable $e) {
$pdo->rollBack();
error_log("approve-registration error: " . $e->getMessage());
header("Location: admin_inbox.php?inbox_status=error");
exit();
}
} elseif ($action === 'reject') {
// Ablehnen
$stmt = $pdo->prepare("UPDATE pending_registrations SET status = 'rejected' WHERE id = ?");
$stmt->execute([$pending_id]);
// Inbox-Nachricht als erledigt markieren
if ($message_id > 0) {
$stmt = $pdo->prepare("UPDATE admin_messages SET status = 'done', processed_at = NOW() WHERE id = ?");
$stmt->execute([$message_id]);
}
// Mail an den Nutzer
$userBetreff = "Registrierung abgelehnt";
$userNachricht = "Hallo {$pending['first_name']},\n\n";
$userNachricht .= "Ihre Registrierungsanfrage für das Intranet der NAK Reinickendorf wurde leider abgelehnt.\n\n";
$userNachricht .= "Bei Fragen wenden Sie sich bitte an den Administrator über das Kontaktformular auf der Anmeldeseite.";
send_intranet_mail($pending['email'], $userBetreff, $userNachricht);
// Mail an Admin-Team
$adminBetreff = "Registrierung abgelehnt: {$pending['first_name']} {$pending['last_name']}";
$adminNachricht = "{$adminName} hat die Registrierung bearbeitet:\n\n";
$adminNachricht .= "Nutzer: {$pending['first_name']} {$pending['last_name']}\n";
$adminNachricht .= "E-Mail: {$pending['email']}\n";
$adminNachricht .= "Aktion: Abgelehnt";
send_admin_team_mail($pdo, $adminBetreff, $adminNachricht);
// Systemnachricht im Posteingang
$sysSubject = "Registrierung abgelehnt: {$pending['first_name']} {$pending['last_name']}";
$sysMessage = "{$adminName} hat die Registrierung von {$pending['first_name']} {$pending['last_name']} ({$pending['email']}) abgelehnt.";
$stmtSys = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message, status, processed_at, created_at) VALUES ('system', ?, ?, ?, ?, 'done', NOW(), NOW())");
$stmtSys->execute([$adminName, $pending['email'], $sysSubject, $sysMessage]);
header("Location: admin_inbox.php?inbox_status=rejected");
exit();
}
header("Location: admin_inbox.php");
exit();
?>
@@ -0,0 +1,56 @@
<?php /* Name: process_contact.php
Beschreibung: Verarbeitung von Kontaktformular-Anfragen
- Speichert in admin_messages
- Sendet Mail an technik@nak-reinickendorf.de */
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$pdo = require_once($_SERVER['DOCUMENT_ROOT']."/intranet_repo/general_files/ini/database.php");
require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/general_files/ini/send_mail.php';
if ($_SERVER["REQUEST_METHOD"] !== "POST") {
http_response_code(405);
echo json_encode(['success' => false, 'error' => 'Methode nicht erlaubt']);
exit;
}
// Felder auslesen
$name = trim($_POST["contact_name"] ?? '');
$email = trim($_POST["contact_email"] ?? '');
$subject = trim($_POST["contact_subject"] ?? 'Kontaktanfrage');
$message = trim($_POST["contact_message"] ?? '');
// Validierung
if (empty($name) || empty($email) || empty($message)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Bitte alle Felder ausfüllen.']);
exit;
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Ungültige E-Mail-Adresse.']);
exit;
}
// In admin_messages speichern
$stmt = $pdo->prepare("INSERT INTO admin_messages (type, sender_name, sender_email, subject, message) VALUES ('contact', ?, ?, ?, ?)");
$stmt->execute([$name, $email, $subject, $message]);
// Mail an Admin-Team (Admin + Technik)
$mail_betreff = "Kontaktanfrage: {$subject}";
$mail_nachricht = "Neue Kontaktanfrage über das Intranet:\n\n";
$mail_nachricht .= "Name: {$name}\n";
$mail_nachricht .= "E-Mail: {$email}\n";
$mail_nachricht .= "Betreff: {$subject}\n\n";
$mail_nachricht .= "Nachricht:\n{$message}";
send_admin_team_mail($pdo, $mail_betreff, $mail_nachricht);
// JSON-Antwort für AJAX
header('Content-Type: application/json');
echo json_encode(['success' => true, 'message' => 'Nachricht wurde gesendet.']);
exit;
?>