prepare intranet for first Dokploy deployment
This commit is contained in:
@@ -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 & 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;
|
||||
?>
|
||||
Reference in New Issue
Block a user