prepare intranet for first Dokploy deployment

This commit is contained in:
2026-08-26 12:59:51 +02:00
parent 5cfe688bdc
commit 29dfb85fef
1321 changed files with 227234 additions and 0 deletions
@@ -0,0 +1,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>