OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

reports.php

8 KB
<?php
$this->layout('layouts/main', [
    'page_title'    => 'Budget-Auswertung',
    'current_route' => 'budget',
    'full_width'    => true,
    'breadcrumbs'   => [],
]);

// Maximalwert für die Balkenskalierung
$maxVal = 0.0;
foreach ($breakdown as $row) {
    $maxVal = max($maxVal, (float)$row['planned'], (float)$row['actual']);
}
$sumPlanned = array_sum(array_map(static fn($r) => (float)$r['planned'], $breakdown));
$sumActual  = array_sum(array_map(static fn($r) => (float)$r['actual'],  $breakdown));
?>

<div class="br-wrap">

    <div class="content-header">
        <div>
            <h1><?= $this->te('Budget-Auswertung') ?></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('Auswertung') ?></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') ?>
            </a>
        </div>
    </div>

    <div class="page-body">

        <!-- Filter -->
        <div class="card card-outline card-primary mb-3">
            <div class="filter-card-body">
                <form action="/budget/reports" method="GET" class="br-filter-form">
                    <label for="f-1599-year" style="font-weight:600;color:var(--gray-700);font-size:0.85rem;"><?= $this->te('Geschäftsjahr:') ?></label>
                    <select id="f-1599-year" name="year" class="form-control" style="width:auto;" 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>
                </form>
            </div>
        </div>

        <!-- KPI -->
        <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>
                </div>
            </div>
            <div class="kpi-col">
                <div class="small-box bg-primary">
                    <div class="inner"><h3><?= $this->money($sumPlanned, 0) ?></h3><p><?= $this->te('Geplant (gefiltert)') ?></p></div>
                    <div class="icon"><i class="fas fa-euro-sign"></i></div>
                </div>
            </div>
            <div class="kpi-col">
                <div class="small-box <?= $sumActual > $sumPlanned ? 'bg-danger' : 'bg-success' ?>">
                    <div class="inner"><h3><?= $this->money($sumActual, 0) ?></h3><p><?= $this->te('Tatsächlich (gefiltert)') ?></p></div>
                    <div class="icon"><i class="fas fa-receipt"></i></div>
                </div>
            </div>
            <div class="kpi-col">
                <div class="small-box bg-secondary">
                    <div class="inner"><h3><?= $sumPlanned > 0 ? (int)round($sumActual / $sumPlanned * 100) : 0 ?>%</h3><p><?= $this->te('Gesamtauslastung') ?></p></div>
                    <div class="icon"><i class="fas fa-percent"></i></div>
                </div>
            </div>
        </div>

        <!-- Kategorie-Aufschlüsselung -->
        <div class="br-card">
            <div class="br-card-head">
                <h2 class="br-card-title">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><path d="M3 3v18h18"/><path d="M18 17V9M13 17V5M8 17v-3"/></svg>
                    <?= $this->te('Ausgaben nach Kategorie (Plan vs. Ist)') ?>
                </h2>
            </div>

            <?php if (empty($breakdown)): ?>
                <p class="br-empty"><?= $this->te('Keine Daten für den gewählten Zeitraum.') ?></p>
            <?php else: ?>
            <div class="br-chart">
                <?php foreach ($breakdown as $row):
                    $planned = (float)$row['planned'];
                    $actual  = (float)$row['actual'];
                    $pPct    = $maxVal > 0 ? round($planned / $maxVal * 100, 1) : 0;
                    $aPct    = $maxVal > 0 ? round($actual  / $maxVal * 100, 1) : 0;
                    $over    = $actual > $planned && $planned > 0;
                ?>
                <div class="br-row">
                    <div class="br-row-label">
                        <span class="br-dot" style="background:<?= $this->e($row['category_color']) ?>"></span>
                        <?= $this->e($row['category_name']) ?>
                        <small>(<?= (int)$row['position_count'] ?>)</small>
                    </div>
                    <div class="br-bars">
                        <div class="br-bar-line">
                            <div class="br-bar br-bar-plan" style="width:<?= $pPct ?>%"></div>
                            <span class="br-bar-val"><?= $this->money($planned, 2) ?></span>
                        </div>
                        <div class="br-bar-line">
                            <div class="br-bar <?= $over ? 'br-bar-over' : 'br-bar-actual' ?>" style="width:<?= $aPct ?>%"></div>
                            <span class="br-bar-val <?= $over ? 'br-over' : '' ?>"><?= $this->money($actual, 2) ?></span>
                        </div>
                    </div>
                </div>
                <?php endforeach; ?>
            </div>
            <div class="br-legend">
                <span><span class="br-key br-bar-plan"></span> <?= $this->te('Geplant') ?></span>
                <span><span class="br-key br-bar-actual"></span> <?= $this->te('Tatsächlich') ?></span>
                <span><span class="br-key br-bar-over"></span> <?= $this->te('Überschritten') ?></span>
            </div>
            <?php endif; ?>
        </div>

    </div><!-- /page-body -->
</div><!-- /br-wrap -->

<style>
.br-wrap { display:flex; flex-direction:column; }
.br-filter-form { display:flex; gap:0.5rem; align-items:center; }

.br-card { background:var(--color-bg); border-radius:0.25rem; border:1px solid rgba(0,0,0,.125); border-top:3px solid var(--bs-primary); box-shadow:0 1px 3px rgba(0,0,0,.1); }
.br-card-head { padding:0.875rem 1.25rem; border-bottom:1px solid var(--gray-100); }
.br-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); }
.br-empty { padding:2rem; color:var(--gray-400); text-align:center; margin:0; }

.br-chart { padding:1.25rem; display:flex; flex-direction:column; gap:1rem; }
.br-row { display:grid; grid-template-columns:200px 1fr; gap:1rem; align-items:center; }
.br-row-label { font-size:0.85rem; font-weight:600; color:var(--gray-700); display:flex; align-items:center; gap:0.4rem; }
.br-row-label small { color:var(--gray-400); font-weight:400; }
.br-dot { width:11px; height:11px; border-radius:50%; flex-shrink:0; }
.br-bars { display:flex; flex-direction:column; gap:0.35rem; }
.br-bar-line { display:flex; align-items:center; gap:0.5rem; }
.br-bar { height:14px; border-radius:3px; min-width:2px; transition:width .3s; }
.br-bar-plan   { background:var(--blue-300); }
.br-bar-actual { background:var(--green-600); }
.br-bar-over   { background:var(--red-600); }
.br-bar-val { font-size:0.78rem; color:var(--gray-500); font-variant-numeric:tabular-nums; white-space:nowrap; }
.br-over { color:var(--red-600); font-weight:600; }

.br-legend { display:flex; gap:1.5rem; padding:0 1.25rem 1.25rem; font-size:0.78rem; color:var(--gray-500); }
.br-legend span { display:inline-flex; align-items:center; gap:0.35rem; }
.br-key { display:inline-block; width:14px; height:10px; border-radius:2px; }

@media (max-width:768px) {
    .br-row { grid-template-columns:1fr; gap:0.4rem; }
}
</style>