OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

16 KB
<?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'])): ?>
                                        &ndash; <?= 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: ?>
                                    &ndash;
                                <?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>