Files
intranet/web/intranet_repo/projects/adminpanel/admin_notices.php
T

472 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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>