feat: implement package 1 session and validation feedback

- add session validation_errors bootstrap initialization

- render global flash + validation messages in header

- remove footer alert-based flash handling

- persist structured validation errors across save handlers

- mark NEXT_STEPS package 1 tasks as done
This commit is contained in:
2026-02-18 09:40:59 +01:00
parent ec20fa2f96
commit f4ce7f360d
17 changed files with 162 additions and 55 deletions

View File

@@ -77,6 +77,42 @@ main {
min-height: calc(100vh - 200px);
}
.flash-stack {
display: grid;
gap: 10px;
margin: 0 auto 18px;
max-width: 1200px;
}
.flash-message {
border-radius: 8px;
border: 1px solid transparent;
padding: 12px 14px;
background: #f8fafc;
}
.flash-message--success {
border-color: #99dfba;
background: #ebf9f1;
color: #165938;
}
.flash-message--error {
border-color: #efb4b4;
background: #fff1f1;
color: #8a1f1f;
}
.flash-message__text {
margin: 0;
font-weight: 600;
}
.flash-message__list {
margin: 8px 0 0 18px;
padding: 0;
}
/* Shared components -------------------------------------------------- */
.filter-form {
margin: 20px 0;
@@ -192,4 +228,4 @@ main {
footer>p {
margin-bottom: 0;
}
}

View File

@@ -12,6 +12,10 @@ if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}
if (!isset($_SESSION['validation_errors']) || !is_array($_SESSION['validation_errors'])) {
$_SESSION['validation_errors'] = [];
}
require_once __DIR__ . '/lib/_sql.php';
$sql = new SQL();

View File

@@ -25,6 +25,7 @@ if ($locationId <= 0) {
if (!empty($errors)) {
$_SESSION['error'] = implode(', ', $errors);
$_SESSION['validation_errors'] = $errors;
$redirectUrl = $buildingId ? "?module=buildings&action=edit&id=$buildingId" : "?module=buildings&action=edit";
header("Location: $redirectUrl");
exit;

View File

@@ -88,6 +88,7 @@ if ($isEndpointUsed($portBType, $portBId)) {
if (!empty($errors)) {
$_SESSION['error'] = implode(', ', $errors);
$_SESSION['validation_errors'] = $errors;
$redirectUrl = $connId ? "?module=connections&action=edit&id=$connId" : "?module=connections&action=edit";
header("Location: $redirectUrl");
exit;
@@ -123,6 +124,7 @@ if ($connId > 0) {
if ($connectionTypeId <= 0) {
$_SESSION['error'] = "Kein Verbindungstyp verfuegbar";
$_SESSION['validation_errors'] = ["Kein Verbindungstyp verfuegbar"];
header("Location: ?module=connections&action=edit");
exit;
}

View File

@@ -49,6 +49,7 @@ if (!in_array($category, ['switch', 'server', 'patchpanel', 'other'])) {
// Falls Fehler: zurück zum Edit-Formular
if (!empty($errors)) {
$_SESSION['error'] = implode(', ', $errors);
$_SESSION['validation_errors'] = $errors;
header('Location: ?module=device_types&action=edit' . ($deviceTypeId ? "&id=$deviceTypeId" : ""));
exit;
}
@@ -67,6 +68,7 @@ if (!empty($_FILES['image']['name'])) {
// Nur SVG, JPG, PNG erlaubt
if (!in_array($fileExt, ['svg', 'jpg', 'jpeg', 'png'])) {
$_SESSION['error'] = "Nur SVG, JPG und PNG sind erlaubt";
$_SESSION['validation_errors'] = ["Nur SVG, JPG und PNG sind erlaubt"];
header('Location: ?module=device_types&action=edit' . ($deviceTypeId ? "&id=$deviceTypeId" : ""));
exit;
}
@@ -86,6 +88,7 @@ if (!empty($_FILES['image']['name'])) {
$imageType = $fileExt === 'svg' ? 'svg' : 'bitmap';
} else {
$_SESSION['error'] = "Datei-Upload fehlgeschlagen";
$_SESSION['validation_errors'] = ["Datei-Upload fehlgeschlagen"];
header('Location: ?module=device_types&action=edit' . ($deviceTypeId ? "&id=$deviceTypeId" : ""));
exit;
}

View File

@@ -57,6 +57,7 @@ if ($rackHeightHe < 1) {
// Falls Fehler: zurück zum Edit-Formular
if (!empty($errors)) {
$_SESSION['error'] = implode(', ', $errors);
$_SESSION['validation_errors'] = $errors;
$redirectUrl = $deviceId ? "?module=devices&action=edit&id=$deviceId" : "?module=devices&action=edit";
header("Location: $redirectUrl");
exit;

View File

@@ -20,6 +20,24 @@ if ($type === 'patchpanel') {
$height = $fixedPanelHeight;
$portCount = (int)($_POST['port_count'] ?? 0);
$comment = trim($_POST['comment'] ?? '');
$errors = [];
if ($name === '') {
$errors[] = 'Name ist erforderlich';
}
if ($floorId <= 0) {
$errors[] = 'Stockwerk ist erforderlich';
}
if ($portCount < 0) {
$errors[] = 'Port-Anzahl darf nicht negativ sein';
}
if (!empty($errors)) {
$_SESSION['error'] = implode(', ', $errors);
$_SESSION['validation_errors'] = $errors;
$redirectUrl = $id > 0 ? "?module=floor_infrastructure&action=edit&type=patchpanel&id=$id" : "?module=floor_infrastructure&action=edit&type=patchpanel";
header("Location: $redirectUrl");
exit;
}
$panelId = $id;
@@ -55,6 +73,7 @@ if ($type === 'patchpanel') {
}
}
}
$_SESSION['success'] = $id > 0 ? 'Patchpanel gespeichert' : 'Patchpanel erstellt';
} elseif ($type === 'outlet') {
$name = trim($_POST['name'] ?? '');
$roomId = (int)($_POST['room_id'] ?? 0);
@@ -62,6 +81,21 @@ if ($type === 'patchpanel') {
$y = (int)($_POST['y'] ?? 0);
$comment = trim($_POST['comment'] ?? '');
$outletId = $id;
$errors = [];
if ($name === '') {
$errors[] = 'Name ist erforderlich';
}
if ($roomId <= 0) {
$errors[] = 'Raum ist erforderlich';
}
if (!empty($errors)) {
$_SESSION['error'] = implode(', ', $errors);
$_SESSION['validation_errors'] = $errors;
$redirectUrl = $id > 0 ? "?module=floor_infrastructure&action=edit&type=outlet&id=$id" : "?module=floor_infrastructure&action=edit&type=outlet";
header("Location: $redirectUrl");
exit;
}
if ($id > 0) {
$sql->set(
@@ -93,6 +127,10 @@ if ($type === 'patchpanel') {
);
}
}
$_SESSION['success'] = $id > 0 ? 'Wandbuchse gespeichert' : 'Wandbuchse erstellt';
} else {
$_SESSION['error'] = 'Ungueltiger Infrastrukturobjekt-Typ';
$_SESSION['validation_errors'] = ['Ungueltiger Infrastrukturobjekt-Typ'];
}
header('Location: ?module=floor_infrastructure&action=list');

View File

@@ -37,6 +37,7 @@ if ($buildingId <= 0) {
// Falls Fehler: zurück zum Edit-Formular
if (!empty($errors)) {
$_SESSION['error'] = implode(', ', $errors);
$_SESSION['validation_errors'] = $errors;
$redirectUrl = $floorId ? "?module=floors&action=edit&id=$floorId" : "?module=floors&action=edit";
header("Location: $redirectUrl");
exit;
@@ -50,6 +51,7 @@ if ($floorSvgContent !== '') {
$storedSvgPath = storeSvgEditorContent($sql, $floorId, $floorSvgContent);
if ($storedSvgPath === false) {
$_SESSION['error'] = "SVG aus dem Editor konnte nicht gespeichert werden";
$_SESSION['validation_errors'] = ["SVG aus dem Editor konnte nicht gespeichert werden"];
$redirectUrl = $floorId ? "?module=floors&action=edit&id=$floorId" : "?module=floors&action=edit";
header("Location: $redirectUrl");
exit;

View File

@@ -20,6 +20,7 @@ if (empty($name)) {
if (!empty($errors)) {
$_SESSION['error'] = implode(', ', $errors);
$_SESSION['validation_errors'] = $errors;
$redirectUrl = $locationId ? "?module=locations&action=edit&id=$locationId" : "?module=locations&action=edit";
header("Location: $redirectUrl");
exit;

View File

@@ -23,20 +23,11 @@ $isEdit = !empty($portType);
$pageTitle = $isEdit ? "Porttyp bearbeiten: " . htmlspecialchars($portType['name']) : "Neuen Porttyp anlegen";
$mediaOptions = ['copper' => 'Kupfer', 'fiber' => 'Lichtwelle', 'coax' => 'Koax', 'other' => 'Sonstiges'];
$error = $_SESSION['error'] ?? '';
unset($_SESSION['error']);
?>
<div class="port-type-edit">
<h1><?php echo $pageTitle; ?></h1>
<?php if ($error): ?>
<div class="error-message">
<?php echo htmlspecialchars($error); ?>
</div>
<?php endif; ?>
<form method="post" action="?module=port_types&action=save" class="edit-form">
<?php if ($isEdit): ?>
@@ -157,12 +148,4 @@ unset($_SESSION['error']);
opacity: 0.8;
}
.error-message {
background: #ffe3e3;
color: #a73737;
border: 1px solid #f5c2c2;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
}
</style>

View File

@@ -34,20 +34,11 @@ $portTypes = $sql->get(
$params
);
$success = $_SESSION['success'] ?? '';
unset($_SESSION['success']);
?>
<div class="port-types-container">
<h1>Porttypen</h1>
<?php if ($success): ?>
<div class="success-message">
<?php echo htmlspecialchars($success); ?>
</div>
<?php endif; ?>
<div class="toolbar">
<form method="GET" class="filter-form">
<input type="hidden" name="module" value="port_types">
@@ -164,8 +155,7 @@ unset($_SESSION['success']);
font-weight: bold;
}
.empty-state,
.success-message {
.empty-state {
margin: 20px 0;
padding: 15px;
border-radius: 6px;
@@ -177,12 +167,6 @@ unset($_SESSION['success']);
text-align: center;
}
.success-message {
background: #e9f8f1;
border: 1px solid #c7eedc;
color: #2c7d59;
}
.actions {
white-space: nowrap;
}

View File

@@ -29,6 +29,7 @@ if (!in_array($medium, $allowedMediums, true)) {
if (!empty($errors)) {
$_SESSION['error'] = implode(', ', $errors);
$_SESSION['validation_errors'] = $errors;
$redirect = $portTypeId ? "?module=port_types&action=edit&id=$portTypeId" : "?module=port_types&action=edit";
header("Location: $redirect");
exit;

View File

@@ -40,6 +40,7 @@ if ($heightHe < 1) {
// Falls Fehler: zurück zum Edit-Formular
if (!empty($errors)) {
$_SESSION['error'] = implode(', ', $errors);
$_SESSION['validation_errors'] = $errors;
$redirectUrl = $rackId ? "?module=racks&action=edit&id=$rackId" : "?module=racks&action=edit";
header("Location: $redirectUrl");
exit;

View File

@@ -18,6 +18,15 @@ $comment = trim((string)($_POST['comment'] ?? ''));
$rawPolygon = trim((string)($_POST['polygon_points'] ?? ''));
if ($name === '' || $floorId <= 0) {
$errors = [];
if ($name === '') {
$errors[] = 'Name ist erforderlich';
}
if ($floorId <= 0) {
$errors[] = 'Stockwerk ist erforderlich';
}
$_SESSION['error'] = implode(', ', $errors);
$_SESSION['validation_errors'] = $errors;
$redirect = $roomId > 0 ? "?module=rooms&action=edit&id=$roomId" : "?module=rooms&action=edit&floor_id=$floorId";
header("Location: $redirect");
exit;
@@ -100,6 +109,7 @@ if (roomsHasPolygonColumn($sql)) {
}
}
$_SESSION['success'] = $roomId > 0 ? 'Raum gespeichert' : 'Raum erstellt';
header('Location: ?module=locations&action=list');
exit;

View File

@@ -12,21 +12,5 @@
| Session: <?php echo session_id() !== '' ? 'aktiv' : 'inaktiv'; ?>
</p>
</footer>
<?php if (!empty($_SESSION['success']) || !empty($_SESSION['error'])): ?>
<script>
(function () {
const success = <?php echo json_encode($_SESSION['success'] ?? '', JSON_UNESCAPED_UNICODE); ?>;
const error = <?php echo json_encode($_SESSION['error'] ?? '', JSON_UNESCAPED_UNICODE); ?>;
if (success) {
alert(success);
}
if (error) {
alert(error);
}
})();
</script>
<?php unset($_SESSION['success'], $_SESSION['error']); ?>
<?php endif; ?>
</body>
</html>

View File

@@ -64,4 +64,60 @@
</nav>
</header>
<?php
$flashMessages = [];
$successMessage = trim((string)($_SESSION['success'] ?? ''));
if ($successMessage !== '') {
$flashMessages[] = [
'type' => 'success',
'text' => $successMessage,
];
}
$errorMessage = trim((string)($_SESSION['error'] ?? ''));
if ($errorMessage !== '') {
$flashMessages[] = [
'type' => 'error',
'text' => $errorMessage,
];
}
$validationErrors = $_SESSION['validation_errors'] ?? [];
if (!is_array($validationErrors)) {
$validationErrors = [];
}
$validationErrors = array_values(array_filter(array_map(static function ($entry) {
return trim((string)$entry);
}, $validationErrors), static function ($entry) {
return $entry !== '';
}));
if (!empty($validationErrors)) {
$flashMessages[] = [
'type' => 'error',
'text' => 'Bitte pruefe die Eingaben:',
'details' => $validationErrors,
];
}
unset($_SESSION['success'], $_SESSION['error'], $_SESSION['validation_errors']);
?>
<main>
<?php if (!empty($flashMessages)): ?>
<section class="flash-stack" aria-live="polite">
<?php foreach ($flashMessages as $message): ?>
<article class="flash-message flash-message--<?php echo htmlspecialchars((string)$message['type']); ?>">
<p class="flash-message__text"><?php echo htmlspecialchars((string)($message['text'] ?? '')); ?></p>
<?php if (!empty($message['details']) && is_array($message['details'])): ?>
<ul class="flash-message__list">
<?php foreach ($message['details'] as $detail): ?>
<li><?php echo htmlspecialchars((string)$detail); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</article>
<?php endforeach; ?>
</section>
<?php endif; ?>