Quelltext des aktuellen Standes
<?php
$this->layout('layouts/main', [
'page_title' => 'Budgetkategorien',
'current_route' => 'budget',
'full_width' => true,
'breadcrumbs' => [],
]);
?>
<div class="cv-wrap">
<!-- Content Header -->
<div class="content-header">
<div>
<h1><?= $this->te('Budgetkategorien') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item"><a href="/budget"><?= $this->te('Budget') ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('Kategorien') ?></li>
</ol>
</div>
<div class="content-header-actions">
<a href="/budget" class="btn btn-default">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="15 18 9 12 15 6"/></svg>
<?= $this->te('Zurück zu Budgets') ?>
</a>
</div>
</div>
<div class="page-body">
<div class="cv-cat-layout">
<!-- ── Kategorien-Liste ───────────────────────────── -->
<div class="cv-cat-list-col">
<div class="cv-card">
<div class="cv-card-head">
<h2 class="cv-card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><path d="M4 6h16M4 12h8m-8 6h16"/></svg>
<?= $this->te('Kategorien') ?>
<span class="cv-count-badge"><?= count($categories) ?></span>
</h2>
</div>
<?php if (empty($categories)): ?>
<p class="cv-empty-hint"><?= $this->te('Noch keine Kategorien angelegt.') ?></p>
<?php else: ?>
<div class="cv-cat-table-wrap">
<table class="data-table">
<thead>
<tr>
<th scope="col" style="width:36px;"></th>
<th scope="col"><?= $this->te('Name') ?></th>
<th scope="col"><?= $this->te('Beschreibung') ?></th>
<th scope="col" style="width:80px;text-align:center;"><?= $this->te('Positionen') ?></th>
<?php if ($can_manage): ?>
<th scope="col" style="width:140px;"></th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($categories as $cat): ?>
<tr>
<td style="text-align:center;">
<span class="cv-cat-dot" style="background:<?= $this->e($cat['color']) ?>;"></span>
</td>
<td><strong><?= $this->e($cat['name']) ?></strong></td>
<td class="cv-cat-desc"><?= $this->e($cat['description'] ?? '–') ?></td>
<td style="text-align:center;">
<a href="/budget?category=<?= (int)$cat['id'] ?>" class="cv-cat-count-link">
<?= (int)$cat['position_count'] ?>
</a>
</td>
<?php if ($can_manage): ?>
<td style="white-space:nowrap;">
<button type="button" class="btn btn-xs btn-default"
onclick="openBudgetEditForm(
<?= (int)$cat['id'] ?>,
'<?= addslashes($this->e($cat['name'])) ?>',
'<?= addslashes($this->e($cat['color'])) ?>',
'<?= addslashes($this->e($cat['description'] ?? '')) ?>',
<?= (int)$cat['sort_order'] ?>
)">
<?= $this->te('Bearbeiten') ?>
</button>
<form action="/budget/categories/<?= (int)$cat['id'] ?>" method="POST"
style="display:inline;"
onsubmit="return confirm('Kategorie \'<?= addslashes($this->e($cat['name'])) ?>\' löschen?\nPositionen behalten ihre Daten, verlieren aber die Kategorie.');">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="btn btn-xs btn-danger"><?= $this->te('Löschen') ?></button>
</form>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
<!-- ── Formular: Neue Kategorie / Bearbeiten ─────── -->
<?php if ($can_manage): ?>
<div class="cv-cat-form-col">
<div class="cv-card" id="catFormCard">
<div class="cv-card-head cv-card-head-green" id="catFormHeadEl">
<h2 class="cv-card-title" id="catFormTitle">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<?= $this->te('Neue Kategorie') ?>
</h2>
</div>
<div class="cv-card-body">
<form id="catForm" action="/budget/categories" method="POST">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" name="_method" id="catFormMethod" value="">
<div class="form-group">
<label for="cat_name"><?= $this->te('Name') ?> <span class="text-danger">*</span></label>
<input type="text" id="cat_name" name="name" class="form-control" required maxlength="100"
placeholder="<?= $this->te('z.B. Hardware, Software, Personal…') ?>">
</div>
<div class="form-group">
<label for="cat_color"><?= $this->te('Farbe') ?></label>
<div style="display:flex;gap:0.75rem;align-items:center;flex-wrap:wrap;">
<input type="color" id="cat_color" name="color" value="#3c8dbc"
style="width:44px;height:32px;padding:2px;border:1px solid var(--gray-300);border-radius:0.25rem;cursor:pointer;">
<div style="display:flex;gap:0.35rem;flex-wrap:wrap;">
<?php foreach (['#3c8dbc','#16a34a','#dc2626','#d97706','#7c3aed','#0891b2','#db2777','#6b7280','#1a4a7a','#065f46'] as $pc): ?>
<span class="bcat-color-preset" data-color="<?= $pc ?>"
style="display:inline-block;width:22px;height:22px;border-radius:50%;background:<?= $pc ?>;cursor:pointer;border:2px solid transparent;"
title="<?= $pc ?>"></span>
<?php endforeach; ?>
</div>
</div>
</div>
<div class="form-group">
<label for="cat_description"><?= $this->te('Beschreibung') ?></label>
<input type="text" id="cat_description" name="description" class="form-control" maxlength="255"
placeholder="<?= $this->te('Optional: kurze Erläuterung') ?>">
</div>
<div class="form-group">
<label for="cat_sort_order"><?= $this->te('Sortierreihenfolge') ?></label>
<input type="number" id="cat_sort_order" name="sort_order" class="form-control"
value="0" min="0" max="999" style="width:90px;">
<span class="form-hint"><?= $this->te('Kleinere Zahlen erscheinen zuerst') ?></span>
</div>
<div style="display:flex;gap:0.5rem;margin-top:0.25rem;">
<button type="submit" class="btn btn-primary" id="catFormSubmit"><?= $this->te('Speichern') ?></button>
<button type="button" id="catFormReset" class="btn btn-default"
onclick="resetBudgetForm()" style="display:none;"><?= $this->te('Abbrechen') ?></button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<script>
function openBudgetEditForm(id, name, color, description, sortOrder) {
var iconEdit = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>';
document.getElementById('catFormTitle').innerHTML = iconEdit + ' Kategorie bearbeiten';
document.getElementById('catFormHeadEl').className = 'cv-card-head cv-card-head-orange';
document.getElementById('catForm').action = '/budget/categories/' + id;
document.getElementById('catFormMethod').value = 'PUT';
document.getElementById('cat_name').value = name;
document.getElementById('cat_color').value = color;
document.getElementById('cat_description').value = description;
document.getElementById('cat_sort_order').value = sortOrder;
document.getElementById('catFormReset').style.display = '';
document.getElementById('catFormSubmit').textContent = 'Aktualisieren';
document.getElementById('catFormCard').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
document.getElementById('cat_name').focus();
}
function resetBudgetForm() {
var iconNew = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
document.getElementById('catFormTitle').innerHTML = iconNew + ' Neue Kategorie';
document.getElementById('catFormHeadEl').className = 'cv-card-head cv-card-head-green';
document.getElementById('catForm').action = '/budget/categories';
document.getElementById('catFormMethod').value = '';
document.getElementById('cat_name').value = '';
document.getElementById('cat_color').value = '#3c8dbc';
document.getElementById('cat_description').value = '';
document.getElementById('cat_sort_order').value = '0';
document.getElementById('catFormReset').style.display = 'none';
document.getElementById('catFormSubmit').textContent = 'Speichern';
document.querySelectorAll('.bcat-color-preset').forEach(p => p.style.borderColor = 'transparent');
}
document.querySelectorAll('.bcat-color-preset').forEach(function(el) {
el.addEventListener('click', function() {
document.getElementById('cat_color').value = this.dataset.color;
document.querySelectorAll('.bcat-color-preset').forEach(p => p.style.borderColor = 'transparent');
this.style.borderColor = '#374151';
});
});
document.getElementById('cat_color').addEventListener('input', function() {
document.querySelectorAll('.bcat-color-preset').forEach(p => p.style.borderColor = 'transparent');
});
</script>
<style>
/* ====================================================
Budget-Kategorien · teilt cv- Prefix mit Contracts
==================================================== */
.cv-wrap { display:flex; flex-direction:column; }
.cv-cat-layout { display:grid; grid-template-columns:1fr 360px; gap:1.5rem; align-items:start; }
.cv-cat-layout > * { min-width:0; }
.cv-card { background:var(--color-bg); border-radius:0.25rem; box-shadow:0 1px 3px rgba(0,0,0,.07); overflow:hidden; }
.cv-card-head { display:flex; align-items:center; justify-content:space-between; padding:0.875rem 1.25rem; border-bottom:1px solid var(--gray-100); }
.cv-card-head-green { border-left:3px solid var(--green-600); }
.cv-card-head-orange { border-left:3px solid var(--amber-600); }
.cv-card-title { display:flex; align-items:center; gap:0.45rem; margin:0; font-size:0.82rem; font-weight:600; text-transform:uppercase; letter-spacing:0.04em; color:var(--gray-700); }
.cv-card-title svg { color:var(--gray-400); flex-shrink:0; }
.cv-count-badge { background:var(--color-primary-soft); color:var(--color-primary-strong); border-radius:9999px; font-size:0.7rem; font-weight:600; padding:0.1rem 0.45rem; margin-left:0.25rem; }
.cv-card-body { padding:1.25rem; }
.cv-cat-table-wrap { overflow-x:auto; }
.cv-cat-dot { display:inline-block; width:14px; height:14px; border-radius:50%; vertical-align:middle; }
.cv-cat-desc { color:var(--gray-500); font-size:0.82rem; max-width:260px; }
.cv-cat-count-link { color:var(--color-primary); font-weight:600; text-decoration:none; }
.cv-cat-count-link:hover { text-decoration:underline; }
.cv-empty-hint { padding:1.5rem; margin:0; color:var(--gray-400); font-size:0.85rem; }
/* Formular: Standard .form-group/.form-control/.form-hint aus app.css */
@media (max-width:900px) {
.cv-cat-layout { grid-template-columns:1fr; }
}
</style>