Quelltext des aktuellen Standes
<?php
$this->layout('layouts/main', [
'page_title' => 'Vertragskategorien',
'current_route' => 'contracts',
'full_width' => true,
'breadcrumbs' => [],
]);
?>
<div class="cv-wrap">
<!-- Content Header -->
<div class="content-header">
<div>
<h1><?= $this->te('Vertragskategorien') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item"><a href="/contracts"><?= $this->te('Verträge') ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('Kategorien') ?></li>
</ol>
</div>
<div class="content-header-actions">
<a href="/contracts" 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 Verträgen') ?>
</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" style="padding:1.5rem;margin:0;"><?= $this->te('Noch keine Kategorien angelegt. Legen Sie die erste Kategorie an.') ?></p>
<?php else: ?>
<div class="table-wrapper" style="border:none;border-radius:0;">
<table class="data-table data-table--fluid">
<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('Verträge') ?></th>
<?php if ($can_manage): ?>
<th scope="col" style="width:130px"></th>
<?php endif; ?>
</tr>
</thead>
<tbody>
<?php foreach ($categories as $cat): ?>
<tr class="cv-cat-row">
<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="/contracts?category=<?= $cat['id'] ?>" class="cv-cat-count-link">
<?= (int)$cat['contract_count'] ?>
</a>
</td>
<?php if ($can_manage): ?>
<td style="white-space:nowrap;">
<button type="button" class="btn btn-default btn-sm"
onclick="openEditForm(<?= $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="/contracts/categories/<?= $cat['id'] ?>" method="POST" style="display:inline;"
onsubmit="return confirm('Kategorie \'<?= addslashes($this->e($cat['name'])) ?>\' löschen?\nVerträge 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-outline-danger btn-sm"><?= $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="/contracts/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. Software, Hardware, Infrastruktur…') ?>">
</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="cat-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="resetForm()" style="display:none;"><?= $this->te('Abbrechen') ?></button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<script>
function openEditForm(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 = '/contracts/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('catForm').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
document.getElementById('cat_name').focus();
}
function resetForm() {
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 = '/contracts/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('.cat-color-preset').forEach(p => p.style.borderColor = 'transparent');
}
document.querySelectorAll('.cat-color-preset').forEach(function(el) {
el.addEventListener('click', function() {
document.getElementById('cat_color').value = this.dataset.color;
document.querySelectorAll('.cat-color-preset').forEach(p => p.style.borderColor = 'transparent');
this.style.borderColor = '#374151';
});
});
document.getElementById('cat_color').addEventListener('input', function() {
document.querySelectorAll('.cat-color-preset').forEach(p => p.style.borderColor = 'transparent');
});
</script>
<style>
/* ====================================================
Kategorien-Seite · teilt cv- Prefix mit index.php
==================================================== */
.cv-wrap { display: flex; flex-direction: column; }
/* ── Zweispaltiges Layout ─────────────────────────── */
.cv-cat-layout {
display: grid;
grid-template-columns: 1fr 360px;
gap: 1.5rem;
align-items: start;
}
.cv-cat-layout > * { min-width: 0; }
/* ── Cards ────────────────────────────────────────── */
.cv-card {
background: var(--color-bg);
border-radius: 0.25rem;
box-shadow: 0 1px 3px rgba(0,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; }
/* Tabelle: Standard .table-wrapper/.data-table aus app.css */
.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; }
.cv-cat-count-link {
display: inline-flex; align-items: center; justify-content: center;
min-width: 28px; height: 22px;
background: var(--color-primary-soft); color: var(--color-primary-strong);
border-radius: 9999px; font-size: 0.75rem; font-weight: 600;
text-decoration: none;
transition: background 0.15s;
}
.cv-cat-count-link:hover { background: var(--color-primary); color: var(--color-on-accent); }
/* ── Leer-Zustand ─────────────────────────────────── */
.cv-empty-hint { color: var(--gray-400); font-size: 0.85rem; }
/* ── Responsive ───────────────────────────────────── */
@media (max-width: 900px) {
.cv-cat-layout { grid-template-columns: 1fr; }
}
</style>