camera - (real) remote control, brandschutz - configurable course, dashboard - vbb licensed API (a real vbb license, how cool)

This commit is contained in:
2026-09-08 18:21:51 +02:00
parent 3f98cf2663
commit ce90820094
24 changed files with 3044 additions and 2065 deletions
@@ -4,6 +4,7 @@
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/general_files/initialise.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/brandschutz/util/brandschutz_helpers.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/intranet_repo/projects/brandschutz/util/brandschutz_course.php';
if ((int) ($groups['Admin'] ?? 0) !== 1) {
header("Location: {$g_httpHomePath}");
@@ -25,6 +26,8 @@ function getProfileImageUrl_bs(array $row, string $g_profilePicturePath): string
return '/intranet_repo/general_files/profile_pictures/default.jpg';
}
$editorError = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
brandschutz_verify_csrf($_POST['csrf_token'] ?? null);
@@ -33,59 +36,93 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit($exception->getMessage());
}
if (($_POST['action'] ?? '') !== 'update_training_date') {
$action = (string) ($_POST['action'] ?? '');
if (in_array($action, ['upload_image', 'delete_image'], true)) {
header('Content-Type: application/json; charset=UTF-8');
try {
$courseState = brandschutz_course_load($pdo);
if ($action === 'upload_image') {
$image = brandschutz_media_upload($_FILES['image'] ?? []);
echo json_encode(['ok' => true, 'image' => $image], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
} else {
brandschutz_media_delete((string) ($_POST['file'] ?? ''), $courseState['config']);
echo json_encode(['ok' => true], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
}
} catch (Throwable $exception) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => $exception->getMessage()], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
}
exit();
}
if ($action === 'save_course') {
header('Content-Type: application/json; charset=UTF-8');
try {
$postedConfig = json_decode((string) ($_POST['course_config'] ?? ''), true, 512, JSON_THROW_ON_ERROR);
$expectedRevision = filter_var($_POST['course_revision'] ?? null, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
if ($expectedRevision === false) throw new InvalidArgumentException('Ungültiger Bearbeitungsstand.');
$newRevision = brandschutz_course_save($pdo, $postedConfig, (int) $expectedRevision, (int) $_SESSION['user_id']);
echo json_encode(['ok' => true, 'revision' => $newRevision], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
} catch (Throwable $exception) {
http_response_code($exception instanceof RuntimeException ? 409 : 400);
$message = $exception instanceof JsonException ? 'Die Editor-Daten konnten nicht verarbeitet werden.' : $exception->getMessage();
echo json_encode(['ok' => false, 'error' => $message], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
}
exit();
} elseif ($action === 'update_training_date') {
$editUserId = filter_var($_POST['bs_user_id'] ?? null, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
$newDate = trim((string) ($_POST['bs_new_date'] ?? ''));
if ($editUserId === false || !brandschutz_is_valid_date($newDate)) {
http_response_code(400);
exit('Ungültige Eingaben.');
}
$stmtUser = $pdo->prepare(
'SELECT ugi.first_name, ugi.last_name, ugi.mail FROM user_general_information ugi '
. 'INNER JOIN user_permissions up ON up.user_id = ugi.user_id '
. 'WHERE ugi.user_id = ? AND up."Brandschutz" = 1'
);
$stmtUser->execute([(int) $editUserId]);
$editedUser = $stmtUser->fetch(PDO::FETCH_ASSOC);
if (!$editedUser) {
http_response_code(404);
exit('Brandschutz-Nutzer nicht gefunden.');
}
$newStatus = brandschutz_training_status($newDate);
$resetCounters = (int) $newStatus['days_until_due'] >= 0;
$adminName = brandschutz_display_name(is_array($user) ? $user : [], 'Ein Administrator');
$editedName = brandschutz_display_name($editedUser);
$pdo->beginTransaction();
try {
brandschutz_store_training_date($pdo, (int) $editUserId, $newDate, $resetCounters);
brandschutz_log_admin_message(
$pdo,
$adminName,
(string) ($editedUser['mail'] ?? ''),
'Brandschutz-Schulungsdatum angepasst: ' . $editedName,
$adminName . ' hat das Schulungsdatum von ' . $editedName . ' manuell auf den ' . (new DateTimeImmutable($newDate))->format('d.m.Y') . ' gesetzt.'
);
$pdo->commit();
} catch (Throwable $exception) {
if ($pdo->inTransaction()) $pdo->rollBack();
throw $exception;
}
header('Location: admin_brandschutz.php?message=5', true, 303);
exit();
} elseif ($action !== 'save_course') {
http_response_code(400);
exit('Ungültige Aktion.');
}
$editUserId = filter_var($_POST['bs_user_id'] ?? null, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
$newDate = trim((string) ($_POST['bs_new_date'] ?? ''));
if ($editUserId === false || !brandschutz_is_valid_date($newDate)) {
http_response_code(400);
exit('Ungültige Eingaben.');
}
$stmtUser = $pdo->prepare(
'SELECT ugi.first_name, ugi.last_name, ugi.mail '
. 'FROM user_general_information ugi '
. 'INNER JOIN user_permissions up ON up.user_id = ugi.user_id '
. 'WHERE ugi.user_id = ? AND up."Brandschutz" = 1'
);
$stmtUser->execute([(int) $editUserId]);
$editedUser = $stmtUser->fetch(PDO::FETCH_ASSOC);
if (!$editedUser) {
http_response_code(404);
exit('Brandschutz-Nutzer nicht gefunden.');
}
$newStatus = brandschutz_training_status($newDate);
$resetCounters = (int) $newStatus['days_until_due'] >= 0;
$adminName = brandschutz_display_name(is_array($user) ? $user : [], 'Ein Administrator');
$editedName = brandschutz_display_name($editedUser);
$pdo->beginTransaction();
try {
brandschutz_store_training_date($pdo, (int) $editUserId, $newDate, $resetCounters);
brandschutz_log_admin_message(
$pdo,
$adminName,
(string) ($editedUser['mail'] ?? ''),
'Brandschutz-Schulungsdatum angepasst: ' . $editedName,
$adminName . ' hat das Schulungsdatum von ' . $editedName . ' manuell auf den '
. (new DateTimeImmutable($newDate))->format('d.m.Y') . ' gesetzt.'
);
$pdo->commit();
} catch (Throwable $exception) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $exception;
}
header('Location: admin_brandschutz.php?message=5', true, 303);
exit();
}
$courseState = ['config' => ['title' => 'Brandschutzunterweisung', 'settings' => [], 'chapters' => []], 'revision' => 1, 'updated_at' => null];
try {
$courseState = brandschutz_course_load($pdo);
} catch (Throwable $exception) {
$editorError = $editorError ?? ('Die Schulungskonfiguration konnte nicht geladen werden: ' . $exception->getMessage());
}
$courseConfig = $courseState['config'];
$courseImages = brandschutz_media_list();
$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 '
@@ -110,6 +147,12 @@ unset($bsUser);
<head>
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/intranet_repo/projects/_central/php/html_head.php'; ?>
<link rel="stylesheet" href="/intranet_repo/projects/_central/layouts/admin_brandschutz.css">
<script id="bsEditorData" type="application/json"><?= json_encode([
'config' => $courseConfig,
'revision' => (int) $courseState['revision'],
'csrfToken' => $csrfToken,
'images' => $courseImages,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_THROW_ON_ERROR) ?></script>
<script src="/intranet_repo/projects/_central/scripts/admin_brandschutz.js" defer></script>
</head>
@@ -126,6 +169,51 @@ unset($bsUser);
<div class="content" id="fullsize">
<?php $adminActiveTab = 'brandschutz'; require __DIR__.'/_tabs.php'; ?>
<div class="standard_overlay bs-course-editor-overlay" id="bsCourseEditorOverlay" aria-hidden="true">
<section class="bs-editor-shell" id="bs-course-editor" role="dialog" aria-modal="true" aria-labelledby="bsCourseEditorTitle">
<div class="bs-editor-header">
<div>
<h1 id="bsCourseEditorTitle">Schulungseditor</h1>
<p>Kapitel, Seiten, Inhalte und Prüfungsfragen zentral verwalten.</p>
</div>
<div class="bs-editor-header-actions">
<button type="button" class="bs-editor-button" id="bsEditSettings">Einstellungen</button>
<button type="button" class="bs-course-editor-close" id="bsCloseCourseEditor" aria-label="Schulungseditor schließen">×</button>
</div>
</div>
<?php if ($editorError !== null): ?><div class="bs-editor-notice bs-editor-notice--error"><?= htmlspecialchars($editorError, ENT_QUOTES, 'UTF-8') ?></div><?php endif; ?>
<div class="bs-editor-notice" id="bsEditorStatus" role="status" aria-live="polite" hidden></div>
<div class="bs-editor-grid">
<aside class="bs-editor-panel">
<div class="bs-editor-panel-title"><span>Kapitel</span><button type="button" id="bsAddChapter" aria-label="Kapitel hinzufügen">+ Hinzufügen</button></div>
<div class="bs-editor-list" id="bsChapterList"></div>
</aside>
<aside class="bs-editor-panel">
<div class="bs-editor-panel-title"><span>Seiten</span><button type="button" id="bsAddPage" aria-label="Seite hinzufügen">+ Hinzufügen</button></div>
<div class="bs-editor-list" id="bsPageList"></div>
</aside>
<main class="bs-editor-panel bs-elements-panel">
<div class="bs-editor-panel-title"><span id="bsElementPanelTitle">Elemente</span><button type="button" id="bsAddElement">+ Baustein</button></div>
<div class="bs-element-list" id="bsElementList"></div>
</main>
</div>
</section>
</div>
<div class="standard_overlay bs-editor-overlay" id="bsEditorModal" aria-hidden="true">
<div class="bs-editor-modal" role="dialog" aria-modal="true" aria-labelledby="bsEditorModalTitle">
<button type="button" class="bs-editor-modal-close" id="bsEditorModalClose" aria-label="Schließen">×</button>
<h2 id="bsEditorModalTitle">Baustein bearbeiten</h2>
<div id="bsEditorModalBody"></div>
<div class="bs-editor-modal-actions">
<button type="button" class="bs-editor-button" id="bsEditorModalCancel">Abbrechen</button>
<button type="button" class="bs-editor-button bs-editor-button--primary" id="bsEditorModalSave">Übernehmen</button>
</div>
</div>
</div>
<div class="admin-users-shell">
<div class="admin-table-card">
<div class="admin-list-header">
@@ -133,6 +221,7 @@ unset($bsUser);
<span class="admin-list-title">Brandschutz-Schulungen</span>
<div class="admin-list-hint"><?= count($bsUsers) ?> Nutzer mit Brandschutz-Rolle</div>
</div>
<button type="button" class="bs-editor-button bs-editor-button--primary" id="bsOpenCourseEditor">Schulungseditor öffnen</button>
</div>
<table class="admin-user-table" id="bsTable">