Quelltext des aktuellen Standes
<?php
$this->layout('layouts/main', [
'page_title' => 'Budgetverwaltung',
'current_route' => 'budget',
'full_width' => true,
'breadcrumbs' => [],
]);
$statusLabels = [
'draft' => 'Entwurf',
'active' => 'Aktiv',
'closed' => 'Abgeschlossen',
'archived' => 'Archiviert',
];
$typeLabels = ['department' => 'Abteilung', 'project' => 'Projekt'];
$statusPills = [
'draft' => 'status-pill--neutral',
'active' => 'status-pill--success',
'closed' => 'status-pill--info',
'archived' => 'status-pill--neutral',
];
$typePills = ['department' => 'status-pill--info', 'project' => 'status-pill--success'];
$hasFilters = ($search !== '' || $status_filter !== '' || $type_filter !== '' || $year_filter !== null || ($category_filter ?? null) !== null);
?>
<div class="bm-wrap">
<!-- Content Header -->
<div class="content-header">
<div>
<h1><?= $this->te('Budgetverwaltung') ?></h1>
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
<li class="breadcrumb-item active"><?= $this->te('Budget') ?></li>
</ol>
</div>
<div class="content-header-actions">
<a href="/budget/reports" class="btn btn-default">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg>
<?= $this->te('Auswertung') ?>
</a>
<a href="/budget/export" class="btn btn-default">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
<?= $this->te('CSV-Export') ?>
</a>
<a href="/budget/categories" class="btn btn-default">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M4 6h16M4 12h8m-8 6h16"/></svg>
<?= $this->te('Kategorien') ?>
</a>
<?php if ($can_manage): ?>
<a href="/budget/create" class="btn btn-primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<?= $this->te('Neues Budget') ?>
</a>
<?php endif; ?>
</div>
</div>
<div class="page-body">
<!-- KPI-Kacheln -->
<div class="kpi-row">
<div class="kpi-col">
<div class="small-box bg-info">
<div class="inner">
<h3><?= (int)$stats['active_budgets'] ?></h3>
<p><?= $this->te('Aktive Budgets') ?></p>
</div>
<div class="icon"><i class="fas fa-chart-bar"></i></div>
<a href="/budget?status=active" class="small-box-footer"><?= $this->t('Details <i class="fas fa-arrow-circle-right"></i>') ?></a>
</div>
</div>
<div class="kpi-col">
<div class="small-box bg-primary">
<div class="inner">
<h3><?= $this->money($stats['total_planned'], 0) ?></h3>
<p><?= $this->te('Geplantes Gesamtbudget') ?></p>
</div>
<div class="icon"><i class="fas fa-euro-sign"></i></div>
<a href="/budget" class="small-box-footer"><?= $this->te('Alle Budgets') ?> <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="kpi-col">
<div class="small-box <?= $stats['total_actual'] > $stats['total_planned'] ? 'bg-danger' : 'bg-success' ?>">
<div class="inner">
<h3><?= $this->money($stats['total_actual'], 0) ?></h3>
<p><?= $this->te('Tatsächliche Kosten (brutto)') ?><br>
<span style="font-weight:600;opacity:.9;"><?= $this->te('Netto: {betrag}', ['betrag' => $this->money($stats['total_actual_net'] ?? $stats['total_actual'], 0)]) ?></span>
</p>
</div>
<div class="icon"><i class="fas fa-receipt"></i></div>
<a href="/budget" class="small-box-footer"><?= $this->te('Übersicht') ?> <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
<div class="kpi-col">
<div class="small-box <?= $stats['over_budget_count'] > 0 ? 'bg-danger' : 'bg-secondary' ?>">
<div class="inner">
<h3><?= (int)$stats['over_budget_count'] ?></h3>
<p><?= $this->te('Budgets überschritten') ?></p>
</div>
<div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
<a href="/budget" class="small-box-footer"><?= $this->te('Anzeigen') ?> <i class="fas fa-arrow-circle-right"></i></a>
</div>
</div>
</div>
<!-- Filterleiste -->
<div class="card card-outline card-primary mb-3">
<div class="filter-card-body">
<form action="/budget" method="GET" class="bm-filter-form">
<div class="filter-input-wrap">
<svg class="filter-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input type="search" name="q" class="form-control" placeholder="<?= $this->te('Budget suchen…') ?>"
value="<?= $this->e($search) ?>">
</div>
<select name="status" class="form-control bm-filter-select" onchange="this.form.submit()">
<option value=""><?= $this->te('Alle Status') ?></option>
<?php foreach ($statusLabels as $val => $lbl): ?>
<option value="<?= $val ?>" <?= $status_filter === $val ? 'selected' : '' ?>><?= $lbl ?></option>
<?php endforeach; ?>
</select>
<select name="type" class="form-control bm-filter-select" onchange="this.form.submit()">
<option value=""><?= $this->te('Alle Typen') ?></option>
<?php foreach ($typeLabels as $val => $lbl): ?>
<option value="<?= $val ?>" <?= $type_filter === $val ? 'selected' : '' ?>><?= $lbl ?></option>
<?php endforeach; ?>
</select>
<select name="year" class="form-control bm-filter-select" onchange="this.form.submit()">
<option value=""><?= $this->te('Alle Jahre') ?></option>
<?php foreach ($fiscal_years as $fy): ?>
<option value="<?= (int)$fy['fiscal_year'] ?>"
<?= $year_filter === (int)$fy['fiscal_year'] ? 'selected' : '' ?>>
<?= (int)$fy['fiscal_year'] ?>
</option>
<?php endforeach; ?>
</select>
<?php if (!empty($categories)): ?>
<select name="category" class="form-control bm-filter-select" onchange="this.form.submit()">
<option value=""><?= $this->te('Alle Kategorien') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= (int)$cat['id'] ?>"
<?= ($category_filter ?? null) === (int)$cat['id'] ? 'selected' : '' ?>>
<?= $this->e($cat['name']) ?>
</option>
<?php endforeach; ?>
</select>
<?php endif; ?>
<button type="submit" class="btn btn-default"><?= $this->te('Suchen') ?></button>
<?php if ($hasFilters): ?>
<a href="/budget" class="btn btn-default"><?= $this->te('Filter zurücksetzen') ?></a>
<?php endif; ?>
</form>
</div>
</div>
<!-- Tabelle / Leerseite -->
<?php if (empty($budgets)): ?>
<div class="empty-state">
<svg viewBox="0 0 64 64" fill="none" stroke="#cbd5e1" stroke-width="2" width="56" height="56"><rect x="8" y="16" width="48" height="36" rx="3"/><path d="M8 24h48M20 16v-4a4 4 0 0 1 8 0v4M36 16v-4a4 4 0 0 1 8 0v4"/></svg>
<p><?= $this->te('Keine Budgets gefunden.') ?></p>
<?php if ($can_manage && !$hasFilters): ?>
<a href="/budget/create" class="btn btn-primary btn-sm"><?= $this->te('Erstes Budget anlegen') ?></a>
<?php endif; ?>
</div>
<?php else: ?>
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th scope="col"><?= $this->te('Titel') ?></th>
<th scope="col"><?= $this->te('Typ') ?></th>
<th scope="col"><?= $this->te('Zeitraum') ?></th>
<th scope="col" class="text-right"><?= $this->te('Geplant') ?></th>
<th scope="col" class="text-right"><?= $this->te('Tatsächlich') ?></th>
<th scope="col" style="min-width:120px;"><?= $this->te('Auslastung') ?></th>
<th scope="col"><?= $this->te('Status') ?></th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<?php foreach ($budgets as $b):
$planned = (float)$b['total_planned'];
$actual = (float)$b['total_actual'];
$actualNet = max(0.0, (float)($b['total_actual_net'] ?? $actual));
$utilPct = $planned > 0 ? (int)round($actual / $planned * 100) : 0;
$barPct = min($utilPct, 100);
$isOver = $actual > $planned && $planned > 0;
?>
<tr>
<td>
<a href="/budget/<?= (int)$b['id'] ?>" class="bm-title-link"><?= $this->e($b['title']) ?></a>
<?php if (!empty($b['department'])): ?>
<small class="bm-meta-sub"><?= $this->e($b['department']) ?></small>
<?php endif; ?>
<?php if (!empty($b['responsible_name'])): ?>
<small class="bm-meta-sub">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="11" height="11"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<?= $this->e($b['responsible_name']) ?>
</small>
<?php endif; ?>
</td>
<td>
<span class="status-pill <?= $typePills[$b['budget_type']] ?? 'status-pill--neutral' ?>">
<?= $typeLabels[$b['budget_type']] ?? $this->e($b['budget_type']) ?>
</span>
</td>
<td class="bm-td-period">
<?php if (!empty($b['period_start'])): ?>
<?= date('d.m.Y', strtotime($b['period_start'])) ?>
<?php if (!empty($b['period_end'])): ?>
– <?= date('d.m.Y', strtotime($b['period_end'])) ?>
<?php endif; ?>
<?php elseif (!empty($b['fiscal_year'])): ?>
<?= $this->te('GJ {jahr}', ['jahr' => (int)$b['fiscal_year']]) ?>
<?php else: ?>
–
<?php endif; ?>
</td>
<td class="text-right bm-td-mono"><?= $this->money($planned, 2) ?></td>
<td class="text-right bm-td-mono <?= $isOver ? 'bm-over' : '' ?>">
<?= $this->money($actual, 2) ?>
<small class="bm-meta-sub"><?= $this->te('netto {betrag}', ['betrag' => $this->money($actualNet, 2)]) ?></small>
</td>
<td>
<div class="bm-util-bar">
<div class="bm-util-fill <?= $isOver ? 'bm-util-over' : '' ?>" style="width:<?= $barPct ?>%"></div>
</div>
<span class="bm-util-pct <?= $isOver ? 'bm-over' : '' ?>"><?= $utilPct ?>%</span>
</td>
<td>
<span class="status-pill <?= $statusPills[$b['status']] ?? 'status-pill--neutral' ?>">
<?= $statusLabels[$b['status']] ?? $this->e($b['status']) ?>
</span>
</td>
<td class="bm-td-actions">
<a href="/budget/<?= (int)$b['id'] ?>" class="btn btn-xs btn-default"><?= $this->te('Detail') ?></a>
<?php if ($can_manage): ?>
<a href="/budget/<?= (int)$b['id'] ?>/edit" class="btn btn-xs btn-default"><?= $this->te('Bearbeiten') ?></a>
<form action="/budget/<?= (int)$b['id'] ?>" method="POST" style="display:inline;"
onsubmit="return confirm(<?= htmlspecialchars(json_encode("Budget wirklich löschen?\n\n" . $b['title'], JSON_UNESCAPED_UNICODE), ENT_QUOTES) ?>);">
<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>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div><!-- /page-body -->
</div><!-- /bm-wrap -->
<style>
/* ====================================================
Budget-Modul – Index · Prefix: bm-
==================================================== */
.bm-wrap { display:flex; flex-direction:column; }
/* Filter */
.bm-filter-form { display:flex; gap:0.5rem; flex-wrap:wrap; align-items:center; }
.bm-filter-select { width:auto !important; }
/* Tabelle – Zell-Inhalte */
.bm-title-link { font-weight:600; color:var(--color-gray-900); text-decoration:none; display:block; }
.bm-title-link:hover { color:var(--color-primary); }
.bm-meta-sub { display:block; font-size:0.72rem; color:var(--gray-500); margin-top:1px; }
.bm-td-period { font-size:0.82rem; white-space:nowrap; }
.bm-td-mono { font-variant-numeric:tabular-nums; white-space:nowrap; }
.bm-td-actions { white-space:nowrap; }
/* Auslastungsbalken */
.bm-util-bar { height:6px; background:var(--gray-200); border-radius:9999px; overflow:hidden; margin-bottom:2px; }
.bm-util-fill { height:100%; background:var(--green-600); border-radius:9999px; }
.bm-util-over { background:var(--red-600); }
.bm-util-pct { font-size:0.72rem; color:var(--gray-500); }
.bm-over { color:var(--red-600); font-weight:600; }
</style>