Quelltext des aktuellen Standes
<?php
$isEdit = isset($topic) && $topic !== null;
$pageTitle = $isEdit ? 'Themengebiet bearbeiten' : 'Neues Themengebiet';
$this->layout('layouts/main', [
'page_title' => $pageTitle,
'breadcrumbs' => [], // Kein Breadcrumb
'full_width' => true
]);
// Parent ID von verschiedenen Quellen ermitteln
$selectedParentId = $topic['parent_id'] ?? $parent['id'] ?? (isset($_GET['parent_id']) ? (int)$_GET['parent_id'] : null) ?? (isset($_POST['parent_id']) ? (int)$_POST['parent_id'] : null);
?>
<div class="topic-form-fullwidth">
<!-- Content Header -->
<div class="content-header">
<div>
<h1><?= $this->e($pageTitle) ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item"><a href="/topics"><?= $this->te('Themengebiete') ?></a></li>
<?php if ($isEdit): ?>
<li class="breadcrumb-item"><a href="/topics/<?= $topic['id'] ?>"><?= $this->e($topic['name']) ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('Bearbeiten') ?></li>
<?php else: ?>
<li class="breadcrumb-item active"><?= $this->te('Neues Themengebiet') ?></li>
<?php endif; ?>
</ol>
</div>
</div>
<div class="form-body">
<form action="<?= $isEdit ? '/topics/' . $topic['id'] : '/topics' ?>" method="POST" class="topic-form" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<?php if ($isEdit): ?>
<input type="hidden" name="_method" value="PUT">
<?php endif; ?>
<div class="form-grid">
<!-- Linke Spalte: Hauptformular -->
<div class="form-main">
<!-- Grunddaten -->
<div class="form-card">
<div class="card-header">
<h2><span aria-hidden="true">📝</span> <?= $this->te('Grunddaten') ?></h2>
</div>
<div class="card-body">
<div class="form-group">
<label for="name" class="form-label required"><?= $this->te('Name des Themengebiets') ?></label>
<input type="text" id="name" name="name" class="form-control"
value="<?= $this->e($topic['name'] ?? $_POST['name'] ?? '') ?>"
required maxlength="255" autofocus
placeholder="<?= $this->te('z.B. Netzwerk, Server, Anwendungen...') ?>">
<small class="form-help"><?= $this->te('Der Name wird in der Navigation und Übersicht angezeigt') ?></small>
</div>
<div class="form-group">
<label for="description" class="form-label"><?= $this->te('Beschreibung') ?></label>
<textarea id="description" name="description" class="form-control" rows="4"
maxlength="1000"
placeholder="<?= $this->te('Kurze Beschreibung des Themengebiets...') ?>"><?= $this->e($topic['description'] ?? $_POST['description'] ?? '') ?></textarea>
<small class="form-help"><?= $this->te('Optionale Beschreibung zur besseren Orientierung (max. 1000 Zeichen)') ?></small>
</div>
</div>
</div>
<!-- Einordnung -->
<div class="form-card">
<div class="card-header">
<h2><span aria-hidden="true">📁</span> <?= $this->te('Einordnung') ?></h2>
</div>
<div class="card-body">
<div class="form-group">
<?php $gewaehlteStandorte = array_map('intval', $topic['location_ids'] ?? []); ?>
<label for="location_ids" class="form-label"><?= $this->te('Standorte') ?></label>
<select id="location_ids" name="location_ids[]" class="form-control form-select"
multiple size="8">
<?php foreach ($locations ?? [] as $loc): ?>
<option value="<?= (int)$loc['id'] ?>"
<?= in_array((int)$loc['id'], $gewaehlteStandorte, true) ? 'selected' : '' ?>>
<?= str_repeat(' ', (int)($loc['_depth'] ?? 0)) . $this->e($loc['name']) ?>
</option>
<?php endforeach; ?>
</select>
<small class="form-help">
<?= $this->te('Steuert, wer dieses Themengebiet sieht – im Backend wie im Self-Service-Portal. Nichts gewählt = alle Standorte. Mehrere Standorte sind möglich, wenn dieselbe Anleitung für mehrere Kunden gilt. Ein Oberstandort gilt auch für dessen Unterstandorte; untergeordnete Themengebiete ohne eigene Auswahl erben diese.') ?>
</small>
</div>
<div class="form-group">
<label for="parent_id" class="form-label"><?= $this->te('Übergeordnetes Themengebiet') ?></label>
<select id="parent_id" name="parent_id" class="form-control form-select">
<option value=""><?= $this->te('Kein übergeordnetes Themengebiet (Root-Ebene)') ?></option>
<?php foreach ($all_topics ?? [] as $t): ?>
<?php
// Aktuelles Topic und seine Kinder ausschließen bei Bearbeitung
if ($isEdit && (
$t['id'] === $topic['id'] ||
str_starts_with($t['path'], $topic['path'] . '/')
)) {
continue;
}
$indent = str_repeat('— ', $t['depth']);
$selected = ($t['id'] == $selectedParentId) ? 'selected' : '';
?>
<option value="<?= $t['id'] ?>" <?= $selected ?>>
<?= $indent ?><?= $this->e($t['name']) ?>
</option>
<?php endforeach; ?>
</select>
<small class="form-help"><?= $this->te('Wählen Sie ein übergeordnetes Themengebiet für hierarchische Organisation') ?></small>
</div>
</div>
</div>
</div>
<!-- Rechte Spalte: Sidebar -->
<div class="form-sidebar">
<!-- Titelbild -->
<div class="form-card">
<div class="card-header">
<h2><span aria-hidden="true">📷</span> <?= $this->te('Titelbild') ?></h2>
</div>
<div class="card-body">
<?php if ($isEdit && !empty($topic['image_path'])): ?>
<div class="current-image">
<img src="/uploads/topics/<?= $this->e($topic['image_path']) ?>"
alt="<?= $this->e($topic['name']) ?>"
class="image-preview">
<div class="image-actions">
<label class="btn btn-sm btn-default">
<input type="file" name="image" accept="image/jpeg,image/png,image/webp" class="hidden-input" id="image-input">
<?= $this->te('ändern') ?>
</label>
<label class="delete-image-label">
<input type="checkbox" name="delete_image" value="1">
<?= $this->te('entfernen') ?>
</label>
</div>
</div>
<?php else: ?>
<div class="image-dropzone" id="image-dropzone">
<input type="file" name="image" accept="image/jpeg,image/png,image/webp" class="hidden-input" id="image-input">
<div class="dropzone-content">
<span class="dropzone-icon" aria-hidden="true">📷</span>
<span class="dropzone-text"><?= $this->te('Bild hochladen') ?></span>
<span class="dropzone-hint"><?= $this->te('JPG, PNG oder WebP') ?><br><?= $this->te('(max. 2MB)') ?></span>
</div>
<img src="" alt="" class="image-preview hidden" id="image-preview">
</div>
<?php endif; ?>
<small class="form-help"><?= $this->te('Wird anstelle des Ordner-Icons angezeigt') ?></small>
</div>
</div>
<!-- Aktionen -->
<div class="form-card actions-card">
<div class="card-body">
<button type="submit" class="btn btn-primary btn-block">
<i class="fas fa-save"></i>
<?= $isEdit ? 'Speichern' : 'Erstellen' ?>
</button>
<a href="<?= $isEdit ? '/topics/' . $topic['id'] : '/topics' ?>" class="btn btn-default btn-block">
<?= $this->te('Abbrechen') ?>
</a>
</div>
</div>
</div>
</div>
</form>
<!-- Gefahrenzone: Separates Formular AUSSERHALB des Hauptformulars -->
<?php if ($isEdit): ?>
<div class="form-grid" style="max-width: 1000px; margin: 0 auto;">
<div></div>
<div class="form-sidebar">
<div class="form-card danger-card">
<div class="card-header">
<h2><span aria-hidden="true">⚠</span> <?= $this->te('Gefahrenzone') ?></h2>
</div>
<div class="card-body">
<?php $canDelete = ($topic['page_count'] ?? 0) === 0; ?>
<?php if (!$canDelete): ?>
<p class="danger-info">
<?= $this->t('<strong>Löschen nicht möglich:</strong><br> Enthält noch') ?> <?= $this->te('{anzahl} Seite(n).', ['anzahl' => $topic['page_count']]) ?>
</p>
<?php else: ?>
<p class="danger-info">
<?= $this->te('Das Themengebiet wird archiviert und ist nicht mehr sichtbar.') ?>
</p>
<?php endif; ?>
<form action="/topics/<?= $topic['id'] ?>" method="POST"
onsubmit="return confirm('Sind Sie sicher, dass Sie dieses Themengebiet l\u00f6schen m\u00f6chten?');">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="btn btn-danger btn-block btn-sm" <?= !$canDelete ? 'disabled' : '' ?>>
<i class="fas fa-trash"></i> <?= $this->te('Löschen') ?>
</button>
</form>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const imageInput = document.getElementById('image-input');
const dropzone = document.getElementById('image-dropzone');
const preview = document.getElementById('image-preview');
if (imageInput && dropzone) {
// Neues Topic: Dropzone-Modus
dropzone.addEventListener('click', function(e) {
if (e.target !== imageInput) {
imageInput.click();
}
});
dropzone.addEventListener('dragover', function(e) {
e.preventDefault();
dropzone.classList.add('dragover');
});
dropzone.addEventListener('dragleave', function() {
dropzone.classList.remove('dragover');
});
dropzone.addEventListener('drop', function(e) {
e.preventDefault();
dropzone.classList.remove('dragover');
if (e.dataTransfer.files.length) {
imageInput.files = e.dataTransfer.files;
showPreview(e.dataTransfer.files[0]);
}
});
imageInput.addEventListener('change', function() {
if (this.files.length) {
showPreview(this.files[0]);
}
});
function showPreview(file) {
if (!file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = function(e) {
preview.src = e.target.result;
preview.classList.remove('hidden');
dropzone.querySelector('.dropzone-content').classList.add('hidden');
};
reader.readAsDataURL(file);
}
} else if (imageInput) {
// Edit-Modus: Kein Dropzone, aber trotzdem Preview aktualisieren
imageInput.addEventListener('change', function() {
if (this.files.length) {
var file = this.files[0];
if (!file.type.startsWith('image/')) return;
var reader = new FileReader();
reader.onload = function(e) {
var existingPreview = document.querySelector('.current-image .image-preview');
if (existingPreview) {
existingPreview.src = e.target.result;
}
};
reader.readAsDataURL(file);
}
});
}
});
</script>
<style>
.topic-form-fullwidth {
}
/* Form Body */
.form-body {
padding: 2rem;
background: var(--slate-50);
min-height: calc(100vh - 250px);
}
.form-grid {
display: grid;
grid-template-columns: 1fr 320px;
gap: 1.5rem;
max-width: 1000px;
margin: 0 auto;
}
/* Form Cards */
.form-card {
background: var(--color-bg);
border-radius: 0.25rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
overflow: hidden;
margin-bottom: 1.5rem;
}
.form-card:last-child {
margin-bottom: 0;
}
.card-header {
padding: 1rem 1.25rem;
background: var(--gray-50);
border-bottom: 1px solid var(--gray-200);
}
.card-header h2 {
margin: 0;
font-size: 0.95rem;
font-weight: 600;
color: var(--gray-900);
}
.card-body {
padding: 1.25rem;
}
/* Form Elements */
.form-group:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-weight: 500;
color: var(--gray-700);
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.form-label.required::after {
content: " *";
color: var(--red-600);
}
textarea.form-control {
resize: vertical;
min-height: 100px;
}
.form-select {
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.75rem center;
background-repeat: no-repeat;
background-size: 1.25rem;
padding-right: 2.5rem;
}
.form-help {
display: block;
margin-top: 0.375rem;
font-size: 0.8rem;
color: var(--gray-500);
}
/* Image Upload */
.image-dropzone {
position: relative;
border: 2px dashed var(--gray-300);
border-radius: 0.25rem;
padding: 1.5rem;
text-align: center;
cursor: pointer;
transition: all 0.15s;
background: var(--gray-50);
margin-bottom: 0.5rem;
}
.image-dropzone:hover,
.image-dropzone.dragover {
border-color: var(--color-primary);
background: var(--blue-50);
}
.hidden-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.dropzone-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.dropzone-icon {
font-size: 2rem;
opacity: 0.6;
}
.dropzone-text {
font-weight: 500;
color: var(--gray-700);
font-size: 0.9rem;
}
.dropzone-hint {
font-size: 0.75rem;
color: var(--gray-400);
line-height: 1.4;
}
.image-preview {
max-width: 100%;
max-height: 150px;
border-radius: 0.25rem;
margin: 0 auto;
display: block;
}
.image-preview.hidden,
.dropzone-content.hidden {
display: none;
}
.current-image {
text-align: center;
margin-bottom: 0.5rem;
}
.current-image .image-preview {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 0.25rem;
margin-bottom: 0.75rem;
}
.image-actions {
display: flex;
justify-content: center;
gap: 1rem;
align-items: center;
}
.image-actions .hidden-input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
}
.delete-image-label {
display: flex;
align-items: center;
gap: 0.35rem;
font-size: 0.8rem;
color: var(--red-600);
cursor: pointer;
}
.delete-image-label input {
width: 0.9rem;
height: 0.9rem;
}
/* Actions Card */
.actions-card .card-body {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
/* Danger Card */
.danger-card {
border: 1px solid var(--red-200);
}
.danger-card .card-header {
background: var(--red-50);
}
.danger-card .card-header h2 {
color: var(--red-600);
}
.danger-info {
font-size: 0.85rem;
color: var(--red-800);
margin: 0 0 1rem 0;
line-height: 1.5;
}
/* Buttons – lokale Überschreibungen für diesen Kontext */
.actions-card .btn-block + .btn-block {
margin-top: 0;
}
/* Responsive */
@media (max-width: 900px) {
.form-grid {
grid-template-columns: 1fr;
}
.form-sidebar {
order: -1;
}
}
@media (max-width: 640px) {
.form-body {
padding: 1rem;
}
}
</style>