Quelltext des aktuellen Standes
<?php
$this->layout('layouts/main', [
'page_title' => $budget['title'],
'current_route' => 'budget',
'full_width' => true,
'breadcrumbs' => [],
]);
$statusCfg = [
'draft' => ['label' => 'Entwurf', 'cls' => 'status-pill--neutral'],
'active' => ['label' => 'Aktiv', 'cls' => 'status-pill--success'],
'closed' => ['label' => 'Abgeschlossen', 'cls' => 'status-pill--info'],
'archived' => ['label' => 'Archiviert', 'cls' => 'status-pill--neutral'],
];
$typeLabels = ['department' => 'Abteilung', 'project' => 'Projekt'];
$stCfg = $statusCfg[$budget['status']] ?? $statusCfg['active'];
// Planwerte werden netto erfasst, der Ist-Wert entsteht brutto aus den Belegen.
// Jede Kennzahl liegt deshalb in beiden Größen vor; verglichen wird jeweils
// Netto gegen Netto und Brutto gegen Brutto.
$isLumpSum = ($budget['budget_mode'] ?? 'structured') === 'lump_sum';
$taxRate = (float)($summary['tax_rate'] ?? 19.0);
$totalPlanned = (float)($summary['total_planned_net'] ?? $summary['total_planned']);
$totalPlannedGross = (float)($summary['total_planned_gross'] ?? $totalPlanned);
$totalActual = (float)($summary['total_actual_gross'] ?? $summary['total_actual']);
$totalActualNet = (float)($summary['total_actual_net'] ?? $totalActual);
$totalCommitted = (float)($summary['total_committed_net'] ?? $summary['total_committed'] ?? 0);
$totalCommittedGross = (float)($summary['total_committed_gross'] ?? $totalCommitted);
// Auslastung und Fortschritt auf Netto-Grundlage – beide Seiten ohne MwSt.
$utilPct = $totalPlanned > 0 ? round(($totalActualNet / $totalPlanned) * 100, 1) : 0;
$barPct = min($utilPct, 100);
$isOver = $totalActualNet > $totalPlanned && $totalPlanned > 0;
$variance = $totalPlanned - $totalActualNet;
$varianceGross = $totalPlannedGross - $totalActual;
$available = $totalPlanned - $totalActualNet - $totalCommitted;
$availableGross = $totalPlannedGross - $totalActual - $totalCommittedGross;
// Satz für die Anzeige: „19 %" statt „19,00 %".
$taxRateLabel = rtrim(rtrim($this->number($taxRate, 2), '0'), ',') . ' %';
// Vorschau-Button (Auge) für Belege – nutzt das globale Anhang-Modal
// (templates/partials/_attachment_modal.php, in main.php eingebunden).
require_once dirname(__DIR__) . '/partials/_attachment_helpers.php';
$bsMoney = function ($v): string {
return $this->money((float)$v, 2);
};
// Datei-Metadaten (Icon nach Typ + lesbare Größe).
$bsFileMeta = function (array $r): array {
$file = (string)($r['original_filename'] ?? '');
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
$mime = (string)($r['mime_type'] ?? '');
$isPdf = $ext === 'pdf' || $mime === 'application/pdf';
$isImg = in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true) || strpos($mime, 'image/') === 0;
$size = (int)($r['file_size'] ?? 0);
if ($size >= 1048576) $sizeStr = $this->number($size / 1048576, 1) . ' MB';
elseif ($size >= 1024) $sizeStr = $this->number($size / 1024, 0) . ' KB';
elseif ($size > 0) $sizeStr = $size . ' B';
else $sizeStr = '';
return [
'icon' => $isPdf ? 'fa-file-pdf' : ($isImg ? 'fa-file-image' : 'fa-file-alt'),
'kind' => $isPdf ? 'pdf' : ($isImg ? 'img' : 'file'),
'size' => $sizeStr,
'ext' => $ext !== '' ? strtoupper($ext) : '',
];
};
// Ein Beleg als ruhige Karten-Zeile: Typ-Icon · Titel (Notiz) + eine
// dezente Meta-Zeile · Betrag (Brutto groß, Netto klein) · Aktionen.
$bsReceiptRow = function (array $r) use ($bsFileMeta, $bsMoney, $can_manage) {
$id = (int)$r['id'];
$file = (string)($r['original_filename'] ?? '');
$title = trim((string)($r['notes'] ?? ''));
$m = $bsFileMeta($r);
$dl = '/budget/receipts/' . $id . '/download';
$meta = [];
if (!empty($r['vendor'])) $meta[] = $this->e($r['vendor']);
if (!empty($r['invoice_number'])) $meta[] = 'Re. ' . $this->e($r['invoice_number']);
if (!empty($r['invoice_date'])) $meta[] = date('d.m.Y', strtotime($r['invoice_date']));
$upBy = trim((string)($r['uploaded_by_name'] ?? ''));
$upDate = !empty($r['created_at']) ? date('d.m.Y', strtotime($r['created_at'])) : '';
$upInfo = trim($upBy . ($upBy && $upDate ? ' · ' : '') . $upDate);
ob_start(); ?>
<div class="bs-rc"<?= $upInfo !== '' ? ' title="Hochgeladen: ' . $this->e($upInfo) . '"' : '' ?>>
<div class="bs-rc-ico bs-rc-ico--<?= $m['kind'] ?>"><i class="fas <?= $m['icon'] ?>"></i></div>
<div class="bs-rc-main">
<div class="bs-rc-title"><?= $this->e($title !== '' ? $title : $file) ?></div>
<div class="bs-rc-meta">
<?php if ($meta): ?><span><?= implode(' · ', $meta) ?></span><span class="bs-rc-sep">·</span><?php endif; ?>
<a href="<?= $dl ?>" class="bs-rc-file" target="_blank" title="<?= $this->te('Öffnen / herunterladen') ?>"><?= $this->e($file) ?><?= $m['size'] !== '' ? ' · ' . $m['size'] : '' ?></a>
</div>
</div>
<div class="bs-rc-amt">
<?php if ($r['amount'] !== null || ($r['net_amount'] ?? null) !== null): ?>
<div class="bs-rc-gross"><?= $r['amount'] !== null ? $bsMoney($r['amount']) : '–' ?></div>
<?php if (($r['net_amount'] ?? null) !== null): ?>
<div class="bs-rc-net"><?= $this->te('netto {betrag}', ['betrag' => $bsMoney($r['net_amount'])]) ?><?php if (($r['tax_rate'] ?? null) !== null): ?> · <?= rtrim(rtrim($this->number((float)$r['tax_rate'], 2), '0'), ',') ?> %<?php endif; ?></div>
<?php endif; ?>
<?php endif; ?>
</div>
<div class="bs-rc-act">
<?= att_view_link('/budget/receipts/' . $id . '/view', $file, $r['mime_type'] ?? '', 'bs-file-btn') ?>
<a href="<?= $dl ?>" class="bs-file-btn" target="_blank" title="<?= $this->te('Herunterladen') ?>"><i class="fas fa-download"></i></a>
<?php if ($can_manage): ?>
<button type="button" class="bs-file-btn bs-receipt-edit-btn" title="<?= $this->te('Bearbeiten') ?>"
data-id="<?= $id ?>"
data-vendor="<?= $this->e($r['vendor'] ?? '') ?>"
data-invoice="<?= $this->e($r['invoice_number'] ?? '') ?>"
data-date="<?= $this->e($r['invoice_date'] ?? '') ?>"
data-gross="<?= $r['amount'] !== null ? $this->e($r['amount']) : '' ?>"
data-net="<?= ($r['net_amount'] ?? null) !== null ? $this->e($r['net_amount']) : '' ?>"
data-tax="<?= ($r['tax_rate'] ?? null) !== null ? $this->e((string)(float)$r['tax_rate']) : '' ?>"
data-notes="<?= $this->e($r['notes'] ?? '') ?>"
data-filename="<?= $this->e($file) ?>"><i class="fas fa-pen"></i></button>
<form action="/budget/receipts/<?= $id ?>" method="POST" class="bs-rc-del" onsubmit="return confirm('Beleg löschen?');">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="bs-file-btn bs-file-btn--danger" title="<?= $this->te('Löschen') ?>"><i class="fas fa-trash"></i></button>
</form>
<?php endif; ?>
</div>
</div>
<?php return ob_get_clean();
};
?>
<div class="bs-wrap">
<!-- Content Header -->
<div class="content-header">
<div>
<h1>
<span class="status-pill <?= $stCfg['cls'] ?>"><?= $stCfg['label'] ?></span>
<?= $this->e($budget['title']) ?>
</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->e($budget['title']) ?></li>
</ol>
</div>
<div class="content-header-actions">
<a href="/budget/<?= (int)$budget['id'] ?>/print" target="_blank" class="btn btn-default">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
<?= $this->te('Drucken / PDF') ?>
</a>
<a href="/budget/<?= (int)$budget['id'] ?>/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>
CSV
</a>
<?php if ($can_manage): ?>
<button type="button" class="btn btn-default" onclick="document.getElementById('dupBudgetModal').style.display='flex'">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
<?= $this->te('Duplizieren') ?>
</button>
<a href="/budget/<?= (int)$budget['id'] ?>/edit" class="btn btn-default"><?= $this->te('Bearbeiten') ?></a>
<form action="/budget/<?= (int)$budget['id'] ?>" method="POST" style="display:inline;"
onsubmit="return confirm(<?= htmlspecialchars(json_encode("Budget wirklich löschen?\n\n" . $budget['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-danger"><?= $this->te('Löschen') ?></button>
</form>
<?php endif; ?>
</div>
</div>
<div class="page-body">
<!-- KPI-Kacheln -->
<div class="kpi-row">
<div class="kpi-col">
<div class="small-box bg-primary">
<div class="inner">
<h3><?= $this->money($totalPlanned, 2) ?></h3>
<p><?= $this->te($isLumpSum ? 'Pauschalbudget' : 'Geplant gesamt') ?> <?= $this->te('(netto)') ?><br>
<span style="font-weight:600;opacity:.9;"><?= $this->te('Brutto:') ?> <?= $this->money($totalPlannedGross, 2) ?></span>
</p>
</div>
<div class="icon"><i class="fas fa-euro-sign"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box <?= $isOver ? 'bg-danger' : 'bg-success' ?>">
<div class="inner">
<h3><?= $this->money($totalActual, 2) ?></h3>
<p><?= $this->te('Ausgegeben (brutto)') ?><br>
<span style="font-weight:600;opacity:.9;"><?= $this->te('Netto:') ?> <?= $this->money($totalActualNet, 2) ?></span>
</p>
</div>
<div class="icon"><i class="fas fa-receipt"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box <?= $variance >= 0 ? 'bg-info' : 'bg-danger' ?>">
<div class="inner">
<h3><?= $this->money(abs($variance), 2) ?></h3>
<p><?= $this->te($variance >= 0 ? 'Restbudget' : 'Überschreitung') ?> <?= $this->te('(netto)') ?><br>
<span style="font-weight:600;opacity:.9;"><?= $this->te('Brutto:') ?> <?= $this->money(abs($varianceGross), 2) ?></span>
</p>
</div>
<div class="icon"><i class="fas fa-balance-scale"></i></div>
</div>
</div>
<div class="kpi-col">
<div class="small-box <?= $isOver ? 'bg-danger' : 'bg-info' ?>">
<div class="inner">
<h3><?= $utilPct ?>%</h3>
<p><?= $this->te('Auslastung (netto)') ?></p>
</div>
<div class="icon"><i class="fas fa-chart-bar"></i></div>
</div>
</div>
</div>
<!-- Budget-Info & Gesamtfortschritt -->
<div class="bs-card mb-3">
<div class="bs-card-body bs-info-grid">
<div class="bs-info-col">
<?php if (!empty($budget['department'])): ?>
<div class="bs-info-row">
<span class="bs-info-label"><?= $typeLabels[$budget['budget_type']] ?? 'Typ' ?></span>
<span><?= $this->e($budget['department']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($budget['responsible_name'])): ?>
<div class="bs-info-row">
<span class="bs-info-label"><?= $this->te('Verantwortlich') ?></span>
<span><?= $this->e($budget['responsible_name']) ?></span>
</div>
<?php endif; ?>
<?php if (!empty($budget['fiscal_year'])): ?>
<div class="bs-info-row">
<span class="bs-info-label"><?= $this->te('Geschäftsjahr') ?></span>
<span><?= (int)$budget['fiscal_year'] ?></span>
</div>
<?php endif; ?>
<?php if (!empty($budget['period_start'])): ?>
<div class="bs-info-row">
<span class="bs-info-label"><?= $this->te('Zeitraum') ?></span>
<span>
<?= date('d.m.Y', strtotime($budget['period_start'])) ?>
<?php if (!empty($budget['period_end'])): ?>
– <?= date('d.m.Y', strtotime($budget['period_end'])) ?>
<?php endif; ?>
</span>
</div>
<?php endif; ?>
<?php if (!empty($budget['notes'])): ?>
<div class="bs-info-row">
<span class="bs-info-label"><?= $this->te('Notizen') ?></span>
<span class="bs-notes"><?= nl2br($this->e($budget['notes'])) ?></span>
</div>
<?php endif; ?>
</div>
<div class="bs-progress-col">
<div class="bs-progress-label">
<span><?= $utilPct ?><?= $this->te('% ausgelastet') ?> <span class="bs-muted"><?= $this->te('(netto)') ?></span></span>
<?php if (!$isLumpSum && $summary['positions_over_budget'] > 0): ?>
<span class="bs-warning-badge">
<i class="fas fa-exclamation-triangle"></i>
<?= (int)$summary['positions_over_budget'] ?> <?= $this->te('Position(en) überschritten') ?>
</span>
<?php elseif ($isOver): ?>
<span class="bs-warning-badge">
<i class="fas fa-exclamation-triangle"></i>
<?= $this->te('Budget überschritten') ?>
</span>
<?php endif; ?>
</div>
<div class="bs-progress-bar">
<?php $commitBar = $totalPlanned > 0 ? min(round($totalCommitted / $totalPlanned * 100, 1), 100 - $barPct) : 0; ?>
<div class="bs-progress-fill <?= $isOver ? 'bs-prog-over' : '' ?>"
style="width:<?= $barPct ?>%"></div>
<?php if ($commitBar > 0): ?>
<div class="bs-progress-commit" style="width:<?= $commitBar ?>%" title="<?= $this->te('Verplant (zugesagt)') ?>"></div>
<?php endif; ?>
</div>
<div class="bs-progress-legend">
<span><?= $this->te('netto {ist} von {plan}', ['ist' => $this->money($totalActualNet, 2), 'plan' => $this->money($totalPlanned, 2)]) ?>
<span class="bs-muted"><?= $this->te('(brutto {ist} von {plan})', ['ist' => $this->money($totalActual, 2), 'plan' => $this->money($totalPlannedGross, 2)]) ?></span></span>
<?php if ($totalCommitted > 0): ?>
<span class="bs-muted"><?= $this->te('Verplant:') ?> <?= $this->te('{netto} netto · {brutto} brutto', ['netto' => $this->money($totalCommitted, 2), 'brutto' => $this->money($totalCommittedGross, 2)]) ?></span>
<span class="<?= $available >= 0 ? 'bs-pos' : 'bs-neg' ?>">
<?= $this->te('Frei verfügbar:') ?> <?= $this->te('{betrag} netto', ['betrag' => $this->money($available, 2)]) ?>
<span class="bs-muted"><?= $this->te('({betrag} brutto)', ['betrag' => $this->money($availableGross, 2)]) ?></span>
</span>
<?php else: ?>
<span class="<?= $variance >= 0 ? 'bs-pos' : 'bs-neg' ?>">
<?= $this->te($variance >= 0 ? 'Noch verfügbar:' : 'Überzogen:') ?> <?= $this->te('{betrag} netto', ['betrag' => $this->money(abs($variance), 2)]) ?>
<span class="bs-muted"><?= $this->te('({betrag} brutto)', ['betrag' => $this->money(abs($varianceGross), 2)]) ?></span>
</span>
<?php endif; ?>
<span class="bs-muted"><?= $this->te('MwSt-Satz für Planwerte:') ?> <?= $this->e($taxRateLabel) ?></span>
</div>
</div>
</div>
</div>
<?php if (($budget['budget_mode'] ?? 'structured') === 'lump_sum'): ?>
<!-- ══ PAUSCHALBUDGET – direkte Belege ══════════════════ -->
<div class="bs-card mb-3">
<div class="bs-card-head">
<h2 class="bs-card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<?= $this->te('Belege') ?>
<span class="bs-count-badge"><?= count($lump_sum_receipts) ?></span>
</h2>
</div>
<?php if (empty($lump_sum_receipts)): ?>
<p class="bs-empty-hint"><?= $this->te('Noch keine Belege gebucht.') ?></p>
<?php else: ?>
<div class="bs-receipts">
<?php foreach ($lump_sum_receipts as $r): ?>
<?= $bsReceiptRow($r) ?>
<?php endforeach; ?>
<div class="bs-receipts-foot">
<span><?= $this->te('Gesamt') ?></span>
<strong><?= $this->money($totalActual, 2) ?>
<span class="bs-muted" style="font-weight:400;"><?= $this->te('brutto · {betrag} netto', ['betrag' => $this->money($totalActualNet, 2)]) ?></span>
</strong>
</div>
</div>
<?php endif; ?>
<?php if ($can_manage && $lump_sum_position): ?>
<div class="bs-ruf-wrap">
<button type="button" class="btn btn-primary btn-sm bs-receipt-add-btn"
data-action="/budget/<?= (int)$budget['id'] ?>/receipts">
<i class="fas fa-plus"></i> <?= $this->te('Neuer Beleg') ?>
</button>
</div>
<?php endif; ?>
</div>
<?php else: ?>
<!-- ══ STRUKTURIERTES BUDGET – Positionen ═══════════════ -->
<!-- Positionen -->
<div class="bs-card mb-3">
<div class="bs-card-head">
<h2 class="bs-card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
<?= $this->te('Budgetpositionen') ?>
<span class="bs-count-badge"><?= count($positions) ?></span>
</h2>
</div>
<?php if (empty($positions)): ?>
<p class="bs-empty-hint"><?= $this->te('Noch keine Positionen vorhanden. Fügen Sie unten eine neue Position hinzu.') ?></p>
<?php else: ?>
<div class="bs-pos-table-wrap">
<table class="data-table">
<thead>
<tr>
<th scope="col"><?= $this->te('Position') ?></th>
<th scope="col"><?= $this->te('Kategorie') ?></th>
<th scope="col" class="text-right"><?= $this->te('Geplant (netto)') ?></th>
<th scope="col" class="text-right"><?= $this->te('Tatsächlich (netto)') ?></th>
<th scope="col" class="text-right"><?= $this->te('Differenz (netto)') ?></th>
<th scope="col" style="min-width:100px;"><?= $this->te('Auslastung') ?></th>
<th scope="col" style="text-align:center;width:70px;"><?= $this->te('Belege') ?></th>
<?php if ($can_manage): ?><th scope="col" style="width:120px;"></th><?php endif; ?>
</tr>
</thead>
<tbody>
<?php
$linkLabels = ['contract' => 'Vertrag', 'license' => 'Lizenz', 'order' => 'Bestellung'];
$linkUrls = ['contract' => '/contracts/', 'license' => '/licenses/', 'order' => '/orders/'];
foreach ($positions as $pos):
// Planwert netto erfasst, Ist brutto aus den Belegen –
// verglichen wird deshalb Netto gegen Netto.
$posPlanned = (float)$pos['planned_amount'];
$posPlannedGross = round($posPlanned * (1 + $taxRate / 100), 2);
$posActual = (float)$pos['actual_amount'];
$posActualNet = (float)($pos['actual_net'] ?? $posActual);
$posCommitted = (float)($pos['committed_amount'] ?? 0);
$posDiff = $posPlanned - $posActualNet;
$posDiffGross = $posPlannedGross - $posActual;
$posUtil = $posPlanned > 0 ? (int)round($posActualNet / $posPlanned * 100) : 0;
$posBar = min($posUtil, 100);
$posOver = $posActualNet > $posPlanned && $posPlanned > 0;
$posReceipts = $receipts[(int)$pos['id']] ?? [];
?>
<tr>
<td>
<strong><?= $this->e($pos['title']) ?></strong>
<?php if (!empty($pos['description'])): ?>
<small class="bm-meta-sub"><?= $this->e($pos['description']) ?></small>
<?php endif; ?>
<?php if (!empty($pos['linked_type'])): ?>
<small class="bm-meta-sub">
<a href="<?= $linkUrls[$pos['linked_type']] ?? '#' ?><?= (int)$pos['linked_id'] ?>" class="bs-link-ref">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="10" height="10"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
<?= ($linkLabels[$pos['linked_type']] ?? '') . ': ' . $this->e($pos['linked_label'] ?: ('#' . (int)$pos['linked_id'])) ?>
</a>
</small>
<?php endif; ?>
<?php if ($posCommitted > 0): ?>
<small class="bm-meta-sub"><?= $this->te('Verplant:') ?> <?= $this->te('{netto} netto · {brutto} brutto', ['netto' => $this->money($posCommitted, 2), 'brutto' => $this->money(round($posCommitted * (1 + $taxRate / 100), 2), 2)]) ?></small>
<?php endif; ?>
</td>
<td>
<?php if (!empty($pos['category_name'])): ?>
<span class="bs-cat-dot" style="background:<?= $this->e($pos['category_color'] ?? 'var(--color-gray-400)') ?>"></span>
<?= $this->e($pos['category_name']) ?>
<?php else: ?>
<span class="bs-muted">–</span>
<?php endif; ?>
</td>
<td class="text-right bs-td-mono">
<?= $this->money($posPlanned, 2) ?>
<small class="bs-gross"><?= $this->te('brutto {betrag}', ['betrag' => $this->money($posPlannedGross, 2)]) ?></small>
</td>
<td class="text-right bs-td-mono <?= $posOver ? 'bm-over' : '' ?>">
<?= $this->money($posActualNet, 2) ?>
<small class="bs-gross"><?= $this->te('brutto {betrag}', ['betrag' => $this->money($posActual, 2)]) ?></small>
</td>
<td class="text-right bs-td-mono <?= $posDiff < 0 ? 'bm-over' : 'bs-pos' ?>">
<?= $posDiff >= 0 ? '+' : '' ?><?= $this->money($posDiff, 2) ?>
<small class="bs-gross"><?= $this->te('brutto {betrag}', ['betrag' => ($posDiffGross >= 0 ? '+' : '') . $this->money($posDiffGross, 2)]) ?></small>
</td>
<td>
<div class="bm-util-bar">
<div class="bm-util-fill <?= $posOver ? 'bm-util-over' : '' ?>" style="width:<?= $posBar ?>%"></div>
</div>
<span class="bm-util-pct <?= $posOver ? 'bm-over' : '' ?>"><?= $posUtil ?>%</span>
</td>
<td style="text-align:center;">
<button type="button" class="btn btn-xs <?= count($posReceipts) > 0 ? 'btn-info' : 'btn-default' ?> bs-receipt-toggle"
data-pos="<?= (int)$pos['id'] ?>" title="<?= $this->te('Belege anzeigen/ausblenden') ?>">
<?= count($posReceipts) ?>
<i class="fas fa-paperclip"></i>
</button>
</td>
<?php if ($can_manage): ?>
<td style="white-space:nowrap;">
<button type="button" class="btn btn-xs btn-default bs-pos-edit-btn"
data-id="<?= (int)$pos['id'] ?>"
data-title="<?= $this->e($pos['title']) ?>"
data-desc="<?= $this->e($pos['description'] ?? '') ?>"
data-planned="<?= $this->e($pos['planned_amount']) ?>"
data-committed="<?= $this->e($pos['committed_amount'] ?? '0') ?>"
data-category="<?= (int)($pos['category_id'] ?? 0) ?>"
data-linktype="<?= $this->e($pos['linked_type'] ?? '') ?>"
data-linkid="<?= (int)($pos['linked_id'] ?? 0) ?>"
data-linklabel="<?= $this->e($pos['linked_label'] ?? '') ?>"
data-sort="<?= (int)$pos['sort_order'] ?>">
<?= $this->te('Bearbeiten') ?>
</button>
<form action="/budget/<?= (int)$budget['id'] ?>/positions/<?= (int)$pos['id'] ?>"
method="POST" style="display:inline;"
onsubmit="return confirm(<?= htmlspecialchars(json_encode("Position löschen?\n\n" . $pos['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>
</td>
<?php endif; ?>
</tr>
<!-- Belege-Unterzeile (initial ausgeblendet) -->
<tr id="bs-receipts-<?= (int)$pos['id'] ?>" class="bs-receipt-row" style="display:none;">
<td colspan="<?= $can_manage ? 8 : 7 ?>" style="padding:0;">
<div class="bs-receipt-panel">
<?php if (!empty($posReceipts)): ?>
<?php $posReceiptSum = array_sum(array_column($posReceipts, 'amount')); ?>
<div class="bs-receipts">
<?php foreach ($posReceipts as $r): ?>
<?= $bsReceiptRow($r) ?>
<?php endforeach; ?>
<div class="bs-receipts-foot">
<span><?= $this->te('Summe') ?></span>
<strong><?= $this->money($posReceiptSum, 2) ?></strong>
</div>
</div>
<?php else: ?>
<p class="bs-empty-hint" style="margin:0;padding:0.5rem 0 0;"><?= $this->te('Noch keine Belege für diese Position.') ?></p>
<?php endif; ?>
<?php if ($can_manage): ?>
<div class="bs-ruf-form">
<button type="button" class="btn btn-sm btn-primary bs-receipt-add-btn"
data-action="/budget/<?= (int)$budget['id'] ?>/positions/<?= (int)$pos['id'] ?>/receipts">
<i class="fas fa-plus"></i> <?= $this->te('Neuer Beleg') ?>
</button>
</div>
<?php endif; ?>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<?php endif; /* end lump_sum else (structured) */?>
<!-- Neue Position hinzufügen -->
<?php if ($can_manage && ($budget['budget_mode'] ?? 'structured') === 'structured'): ?>
<div class="bs-card mb-3" id="addPositionCard">
<div class="bs-card-head bs-card-head-green">
<h2 class="bs-card-title">
<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 Position') ?>
</h2>
</div>
<div class="bs-card-body">
<form action="/budget/<?= (int)$budget['id'] ?>/positions" method="POST" class="bs-pos-add-form">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="bs-pos-add-row">
<input type="text" name="title" required placeholder="<?= $this->te('Positionstitel *') ?>"
class="form-control" style="flex:2;min-width:180px;">
<select name="category_id" class="form-control" style="min-width:140px;">
<option value=""><?= $this->te('Keine Kategorie') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= (int)$cat['id'] ?>"><?= $this->e($cat['name']) ?></option>
<?php endforeach; ?>
</select>
<input type="number" name="planned_amount" step="0.01" min="0"
placeholder="<?= $this->te('Geplant ({cur}, netto)', ['cur' => $this->currency()]) ?>" class="form-control" style="width:140px;">
<input type="number" name="committed_amount" step="0.01" min="0"
placeholder="<?= $this->te('Verplant ({cur}, netto)', ['cur' => $this->currency()]) ?>" class="form-control" style="width:140px;"
title="<?= $this->te('Zugesagte/offene Mittel, die noch nicht als Beleg gebucht sind') ?>">
<input type="text" name="description" placeholder="<?= $this->te('Beschreibung (optional)') ?>"
class="form-control" style="flex:1;min-width:140px;">
<button type="submit" class="btn btn-primary"><?= $this->te('Position hinzufügen') ?></button>
</div>
</form>
</div>
</div>
<!-- Position bearbeiten (Modal) -->
<div id="editPositionModal" class="modal-overlay" style="display:none;">
<div class="modal-card">
<div class="modal-header">
<h2><?= $this->te('Position bearbeiten') ?></h2>
<button aria-label="<?= $this->te('Schließen') ?>" type="button" onclick="closeEditModal()" class="modal-close">×</button>
</div>
<div class="modal-body">
<form id="editPositionForm" method="POST">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" name="_method" value="PUT">
<div class="bs-edit-field">
<label for="ep_title"><?= $this->te('Titel *') ?></label>
<input type="text" name="title" id="ep_title" required class="form-control">
</div>
<div class="bs-edit-row">
<div class="bs-edit-field">
<label for="ep_category"><?= $this->te('Kategorie') ?></label>
<select name="category_id" id="ep_category" class="form-control">
<option value=""><?= $this->te('Keine Kategorie') ?></option>
<?php foreach ($categories as $cat): ?>
<option value="<?= (int)$cat['id'] ?>"><?= $this->e($cat['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="bs-edit-field">
<label for="ep_planned"><?= $this->te('Geplanter Betrag ({cur}, netto)', ['cur' => $this->currency()]) ?></label>
<input type="number" name="planned_amount" id="ep_planned" step="0.01" min="0" class="form-control">
</div>
</div>
<div class="bs-edit-row">
<div class="bs-edit-field">
<label for="ep_committed"><?= $this->te('Verplant / zugesagt ({cur}, netto)', ['cur' => $this->currency()]) ?></label>
<input type="number" name="committed_amount" id="ep_committed" step="0.01" min="0" class="form-control">
</div>
<div class="bs-edit-field">
<label for="ep_linktype"><?= $this->te('Verknüpfung') ?></label>
<select name="linked_type" id="ep_linktype" class="form-control">
<option value=""><?= $this->te('Keine') ?></option>
<option value="contract"><?= $this->te('Vertrag') ?></option>
<option value="license"><?= $this->te('Lizenz') ?></option>
<option value="order"><?= $this->te('Bestellung') ?></option>
</select>
</div>
</div>
<div class="bs-edit-row">
<div class="bs-edit-field">
<label><?= $this->te('Verknüpfte ID') ?></label>
<input type="number" name="linked_id" id="ep_linkid" min="0" class="form-control" placeholder="z.B. 42">
</div>
<div class="bs-edit-field">
<label><?= $this->te('Bezeichnung der Verknüpfung') ?></label>
<input type="text" name="linked_label" id="ep_linklabel" class="form-control" maxlength="255" placeholder="<?= $this->te('optional') ?>">
</div>
</div>
<div class="bs-edit-field">
<label for="ep_desc"><?= $this->te('Beschreibung') ?></label>
<input type="text" name="description" id="ep_desc" class="form-control">
</div>
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button type="submit" class="btn btn-primary"><?= $this->te('Speichern') ?></button>
<button type="button" onclick="closeEditModal()" class="btn btn-default"><?= $this->te('Abbrechen') ?></button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<?php if ($can_manage): ?>
<!-- Budget duplizieren (Modal) -->
<div id="dupBudgetModal" class="modal-overlay" style="display:none;">
<div class="modal-card">
<div class="modal-header">
<h2><?= $this->te('Budget duplizieren') ?></h2>
<button aria-label="<?= $this->te('Schließen') ?>" type="button" onclick="document.getElementById('dupBudgetModal').style.display='none'" class="modal-close">×</button>
</div>
<div class="modal-body">
<p style="font-size:0.85rem;color:var(--gray-500);margin-top:0;">
<?= $this->t('Erstellt eine Kopie inkl. aller Positionen (Status: <strong>Entwurf</strong>). Belege und Ist-Werte werden <strong>nicht</strong> übernommen.') ?>
</p>
<form action="/budget/<?= (int)$budget['id'] ?>/duplicate" method="POST">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="bs-edit-field">
<label for="f-c226-title"><?= $this->te('Titel der Kopie') ?></label>
<input id="f-c226-title" type="text" name="title" class="form-control" maxlength="255"
value="<?= $this->e($budget['title']) ?> (Kopie)">
</div>
<div class="bs-edit-field">
<label for="f-c226-fiscal_year"><?= $this->te('Geschäftsjahr') ?></label>
<input id="f-c226-fiscal_year" type="number" name="fiscal_year" class="form-control" min="2000" max="2099"
value="<?= !empty($budget['fiscal_year']) ? (int)$budget['fiscal_year'] + 1 : (int)date('Y') + 1 ?>">
<small class="bm-meta-sub"><?= $this->te('Standardmäßig auf das Folgejahr gesetzt (Rollover).') ?></small>
</div>
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button type="submit" class="btn btn-primary"><?= $this->te('Duplizieren') ?></button>
<button type="button" class="btn btn-default" onclick="document.getElementById('dupBudgetModal').style.display='none'"><?= $this->te('Abbrechen') ?></button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<?php if ($can_manage): ?>
<!-- Vorschläge für das Lieferant-Feld (beide Beleg-Modals) -->
<datalist id="bsVendorList">
<?php foreach (($companies ?? []) as $co): ?>
<option value="<?= $this->e($co['name']) ?>"></option>
<?php endforeach; ?>
</datalist>
<!-- Neuer Beleg (Modal) -->
<div id="newReceiptModal" class="modal-overlay" style="display:none;">
<div class="modal-card">
<div class="modal-header">
<h2><?= $this->te('Neuer Beleg') ?></h2>
<button aria-label="<?= $this->te('Schließen') ?>" type="button" onclick="closeReceiptModal('newReceiptModal')" class="modal-close">×</button>
</div>
<div class="modal-body">
<form id="newReceiptForm" method="POST" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<div class="bs-edit-field">
<label><?= $this->te('Datei') ?> <span class="text-danger">*</span></label>
<label class="bs-ruf-drop" for="nrf_file" style="width:100%;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15" height="15"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
<span id="nrf_fileinfo"><?= $this->te('Beleg hierher ziehen oder klicken') ?></span>
</label>
<input type="file" id="nrf_file" name="receipt_file" class="bs-ruf-hidden"
accept=".pdf,.jpg,.jpeg,.png,.gif,.webp" required>
</div>
<div class="bs-edit-field">
<label><?= $this->te('Bezeichnung') ?> <span class="bs-field-hint"><?= $this->te('(wird als Titel angezeigt)') ?></span></label>
<input type="text" name="notes" id="nrf_notes" class="form-control" maxlength="255"
placeholder="<?= $this->te('z. B. Büromaterial Mai') ?>">
</div>
<div class="bs-edit-row">
<div class="bs-edit-field">
<label><?= $this->te('Lieferant') ?></label>
<input type="text" name="vendor" id="nrf_vendor" class="form-control" list="bsVendorList" maxlength="255" placeholder="<?= $this->te('Lieferant') ?>">
</div>
<div class="bs-edit-field">
<label><?= $this->te('Rechnungsnummer') ?></label>
<input type="text" name="invoice_number" id="nrf_invoice" class="form-control" maxlength="100" placeholder="<?= $this->te('Re.-Nr.') ?>">
</div>
</div>
<div class="bs-edit-row">
<div class="bs-edit-field">
<label for="nrf_date"><?= $this->te('Datum') ?></label>
<input type="date" name="invoice_date" id="nrf_date" class="form-control">
</div>
<div class="bs-edit-field">
<label for="nrf_tax"><?= $this->te('MwSt-Satz') ?></label>
<select name="tax_rate" id="nrf_tax" class="form-control bs-tax-select">
<option value="19">19 %</option>
<option value="7">7 %</option>
<option value="0">0 %</option>
<option value=""><?= $this->te('ohne') ?></option>
</select>
</div>
</div>
<div class="bs-edit-row">
<div class="bs-edit-field">
<label><?= $this->te('Brutto ({cur})', ['cur' => $this->currency()]) ?></label>
<input type="number" name="amount" id="nrf_gross" class="form-control bs-amt-input" step="0.01" min="0" placeholder="0,00">
</div>
<div class="bs-edit-field">
<label><?= $this->te('Netto ({cur})', ['cur' => $this->currency()]) ?></label>
<input type="number" name="net_amount" id="nrf_net" class="form-control bs-amt-input" step="0.01" min="0" placeholder="0,00">
</div>
</div>
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button type="submit" class="btn btn-primary"><i class="fas fa-upload"></i> <?= $this->te('Hochladen') ?></button>
<button type="button" onclick="closeReceiptModal('newReceiptModal')" class="btn btn-default"><?= $this->te('Abbrechen') ?></button>
</div>
</form>
</div>
</div>
</div>
<!-- Beleg bearbeiten (Modal) -->
<div id="editReceiptModal" class="modal-overlay" style="display:none;">
<div class="modal-card">
<div class="modal-header">
<h2><?= $this->te('Beleg bearbeiten') ?></h2>
<button aria-label="<?= $this->te('Schließen') ?>" type="button" onclick="closeReceiptModal('editReceiptModal')" class="modal-close">×</button>
</div>
<div class="modal-body">
<form id="editReceiptForm" method="POST">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" name="_method" value="PUT">
<p class="bs-receipt-editfile"><?= $this->te('Datei:') ?> <span id="erf_filename"></span> <small><?= $this->te('(Datei bleibt unverändert)') ?></small></p>
<div class="bs-edit-field">
<label><?= $this->te('Bezeichnung') ?> <span class="bs-field-hint"><?= $this->te('(wird als Titel angezeigt)') ?></span></label>
<input type="text" name="notes" id="erf_notes" class="form-control" maxlength="255" placeholder="<?= $this->te('z. B. Büromaterial Mai') ?>">
</div>
<div class="bs-edit-row">
<div class="bs-edit-field">
<label><?= $this->te('Lieferant') ?></label>
<input type="text" name="vendor" id="erf_vendor" class="form-control" list="bsVendorList" maxlength="255" placeholder="<?= $this->te('Lieferant') ?>">
</div>
<div class="bs-edit-field">
<label><?= $this->te('Rechnungsnummer') ?></label>
<input type="text" name="invoice_number" id="erf_invoice" class="form-control" maxlength="100" placeholder="<?= $this->te('Re.-Nr.') ?>">
</div>
</div>
<div class="bs-edit-row">
<div class="bs-edit-field">
<label for="erf_date"><?= $this->te('Datum') ?></label>
<input type="date" name="invoice_date" id="erf_date" class="form-control">
</div>
<div class="bs-edit-field">
<label for="erf_tax"><?= $this->te('MwSt-Satz') ?></label>
<select name="tax_rate" id="erf_tax" class="form-control bs-tax-select">
<option value="19">19 %</option>
<option value="7">7 %</option>
<option value="0">0 %</option>
<option value=""><?= $this->te('ohne') ?></option>
</select>
</div>
</div>
<div class="bs-edit-row">
<div class="bs-edit-field">
<label><?= $this->te('Brutto ({cur})', ['cur' => $this->currency()]) ?></label>
<input type="number" name="amount" id="erf_gross" class="form-control bs-amt-input" step="0.01" min="0" placeholder="0,00">
</div>
<div class="bs-edit-field">
<label><?= $this->te('Netto ({cur})', ['cur' => $this->currency()]) ?></label>
<input type="number" name="net_amount" id="erf_net" class="form-control bs-amt-input" step="0.01" min="0" placeholder="0,00">
</div>
</div>
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button type="submit" class="btn btn-primary"><?= $this->te('Speichern') ?></button>
<button type="button" onclick="closeReceiptModal('editReceiptModal')" class="btn btn-default"><?= $this->te('Abbrechen') ?></button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
</div><!-- /page-body -->
</div><!-- /bs-wrap -->
<script>
// Beleg-Zeilen togglen
document.querySelectorAll('.bs-receipt-toggle').forEach(function(btn) {
btn.addEventListener('click', function() {
var row = document.getElementById('bs-receipts-' + this.dataset.pos);
if (!row) return;
var open = row.style.display !== 'none';
row.style.display = open ? 'none' : '';
this.classList.toggle('btn-info', !open);
this.classList.toggle('btn-default', open);
});
});
// Nach Upload: Panel der Position via URL-Hash automatisch öffnen (#receipts-{posId})
(function() {
var hash = window.location.hash;
if (hash && hash.indexOf('#receipts-') === 0) {
var posId = hash.replace('#receipts-', '');
var row = document.getElementById('bs-receipts-' + posId);
var btn = document.querySelector('.bs-receipt-toggle[data-pos="' + posId + '"]');
if (row) { row.style.display = ''; }
if (btn) { btn.classList.add('btn-info'); btn.classList.remove('btn-default'); }
if (row) { row.scrollIntoView({ behavior: 'smooth', block: 'center' }); }
history.replaceState(null, '', window.location.pathname + window.location.search);
}
})();
<?php if ($can_manage): ?>
// Position bearbeiten (nur vorhanden bei strukturierten Budgets)
var editModalEl = document.getElementById('editPositionModal');
document.querySelectorAll('.bs-pos-edit-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!editModalEl) return;
var id = this.dataset.id;
var budgetId = <?= (int)$budget['id'] ?>;
document.getElementById('editPositionForm').action = '/budget/' + budgetId + '/positions/' + id;
document.getElementById('ep_title').value = this.dataset.title;
document.getElementById('ep_desc').value = this.dataset.desc;
document.getElementById('ep_planned').value = this.dataset.planned;
document.getElementById('ep_committed').value = this.dataset.committed || '';
document.getElementById('ep_linktype').value = this.dataset.linktype || '';
document.getElementById('ep_linkid').value = (this.dataset.linkid && this.dataset.linkid !== '0') ? this.dataset.linkid : '';
document.getElementById('ep_linklabel').value = this.dataset.linklabel || '';
var catSel = document.getElementById('ep_category');
for (var i = 0; i < catSel.options.length; i++) {
catSel.options[i].selected = (catSel.options[i].value == this.dataset.category);
}
editModalEl.style.display = 'flex';
});
});
function closeEditModal() {
if (editModalEl) editModalEl.style.display = 'none';
}
if (editModalEl) {
editModalEl.addEventListener('click', function(e) {
if (e.target === this) closeEditModal();
});
}
// ── Belege: Brutto/Netto-Verrechnung + Modals (Neu/Bearbeiten) ──────────
function closeReceiptModal(id) {
var m = document.getElementById(id);
if (m) m.style.display = 'none';
}
// Brutto/Netto über den MwSt-Satz synchron halten.
function bsBindAmountCalc(grossId, netId, taxId) {
var g = document.getElementById(grossId),
n = document.getElementById(netId),
t = document.getElementById(taxId);
if (!g || !n || !t) return;
function calc(src) {
var rate = parseFloat(t.value);
if (isNaN(rate)) return; // "ohne" → keine Berechnung
var f = 1 + rate / 100;
if (src === 'net') {
var nv = parseFloat(n.value);
if (!isNaN(nv)) g.value = (nv * f).toFixed(2);
} else {
var gv = parseFloat(g.value);
if (!isNaN(gv) && f > 0) n.value = (gv / f).toFixed(2);
}
}
g.addEventListener('input', function () { calc('gross'); });
n.addEventListener('input', function () { calc('net'); });
t.addEventListener('change', function () { calc(g.value !== '' ? 'gross' : 'net'); });
}
bsBindAmountCalc('nrf_gross', 'nrf_net', 'nrf_tax');
bsBindAmountCalc('erf_gross', 'erf_net', 'erf_tax');
// Neuer Beleg
var newReceiptModal = document.getElementById('newReceiptModal');
var newReceiptForm = document.getElementById('newReceiptForm');
document.querySelectorAll('.bs-receipt-add-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
if (!newReceiptForm) return;
newReceiptForm.reset();
newReceiptForm.action = this.dataset.action;
var tax = document.getElementById('nrf_tax'); if (tax) tax.value = '19';
var info = document.getElementById('nrf_fileinfo');
if (info) { info.textContent = 'Beleg hierher ziehen oder klicken'; info.style.color = ''; }
newReceiptModal.style.display = 'flex';
});
});
var nrfFile = document.getElementById('nrf_file');
if (nrfFile) {
nrfFile.addEventListener('change', function () {
var info = document.getElementById('nrf_fileinfo');
if (!info) return;
info.textContent = this.files[0] ? this.files[0].name : 'Beleg hierher ziehen oder klicken';
info.style.color = this.files[0] ? '#374151' : '';
});
}
// Beleg bearbeiten
var editReceiptModal = document.getElementById('editReceiptModal');
var editReceiptForm = document.getElementById('editReceiptForm');
document.querySelectorAll('.bs-receipt-edit-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
if (!editReceiptForm) return;
var d = this.dataset;
editReceiptForm.action = '/budget/receipts/' + d.id;
var set = function (id, v) { var el = document.getElementById(id); if (el) el.value = v || ''; };
document.getElementById('erf_filename').textContent = d.filename || '';
set('erf_vendor', d.vendor);
set('erf_invoice', d.invoice);
set('erf_date', d.date);
set('erf_gross', d.gross);
set('erf_net', d.net);
set('erf_notes', d.notes);
var tax = document.getElementById('erf_tax');
if (tax) {
var tv = (d.tax !== undefined && d.tax !== '') ? String(parseFloat(d.tax)) : '';
tax.value = ['19', '7', '0', ''].indexOf(tv) !== -1 ? tv : '';
}
editReceiptModal.style.display = 'flex';
});
});
[newReceiptModal, editReceiptModal].forEach(function (m) {
if (!m) return;
m.addEventListener('click', function (e) { if (e.target === this) m.style.display = 'none'; });
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
if (newReceiptModal && newReceiptModal.style.display !== 'none') newReceiptModal.style.display = 'none';
if (editReceiptModal && editReceiptModal.style.display !== 'none') editReceiptModal.style.display = 'none';
});
<?php endif; ?>
</script>
<style>
/* ====================================================
Budget-Modul – Show-Seite · Prefix: bs-
==================================================== */
.bs-wrap { display:flex; flex-direction:column; }
/* Cards */
.bs-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);
}
.bs-card-head {
display:flex; align-items:center; justify-content:space-between;
padding:0.875rem 1.25rem; border-bottom:1px solid var(--gray-100);
}
.bs-card-head-green { border-top-color:var(--green-600); }
.bs-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);
}
.bs-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;
}
.bs-card-body { padding:1.25rem; }
/* Info-Grid */
.bs-info-grid { display:grid; grid-template-columns:auto 1fr; gap:1.5rem; align-items:start; }
.bs-info-col { display:flex; flex-direction:column; gap:0.5rem; }
.bs-info-row { display:flex; gap:0.75rem; font-size:0.875rem; }
.bs-info-label { font-weight:600; color:var(--gray-500); min-width:120px; }
.bs-notes { white-space:pre-line; color:var(--gray-700); }
/* Progress */
.bs-progress-col { display:flex; flex-direction:column; gap:0.5rem; }
.bs-progress-label { display:flex; gap:0.75rem; align-items:center; flex-wrap:wrap; font-size:0.82rem; color:var(--gray-700); }
.bs-warning-badge { background:var(--red-100); color:var(--red-600); border-radius:0.25rem; padding:0.15rem 0.5rem; font-size:0.75rem; font-weight:600; }
.bs-progress-bar { position:relative; height:14px; background:var(--gray-200); border-radius:9999px; overflow:hidden; display:flex; }
.bs-progress-fill { height:100%; background:var(--green-600); transition:width .3s; }
.bs-progress-commit{ height:100%; background:repeating-linear-gradient(45deg,var(--amber-500),var(--amber-500) 4px,var(--amber-400) 4px,var(--amber-400) 8px); }
.bs-prog-over { background:var(--red-600); }
.bs-link-ref { color:var(--color-primary); text-decoration:none; display:inline-flex; align-items:center; gap:3px; }
.bs-link-ref:hover { text-decoration:underline; }
.bs-progress-legend { display:flex; justify-content:space-between; font-size:0.78rem; color:var(--gray-500); }
.bs-pos { color:var(--green-600); font-weight:600; }
.bs-neg { color:var(--red-600); font-weight:600; }
/* Positionen-Tabelle – Zell-Inhalte (Tabelle selbst: .data-table aus app.css) */
.bs-pos-table-wrap { overflow-x:auto; }
.bs-td-mono { font-variant-numeric:tabular-nums; white-space:nowrap; }
/* Zweitgröße unter dem Hauptbetrag: dieselbe Zahl in der jeweils anderen Größe. */
.bs-gross { display:block; font-size:0.72rem; font-weight:400; color:var(--gray-500); margin-top:0.1rem; }
.bs-cat-dot { display:inline-block; width:10px; height:10px; border-radius:50%; vertical-align:middle; margin-right:4px; }
.bs-muted { color:var(--gray-400); }
/* Belege-Panel */
.bs-receipt-row > td { background:var(--slate-50) !important; }
.bs-receipt-panel { padding:0.75rem 1.25rem 1rem; }
.bs-receipt-dl { color:var(--color-primary); text-decoration:none; font-weight:500; }
.bs-receipt-dl:hover { text-decoration:underline; }
/* Beleg-Liste: ruhige Karten-Zeilen statt dichter Tabelle */
.bs-receipts { display:flex; flex-direction:column; }
.bs-rc { display:flex; align-items:center; gap:0.75rem; padding:0.6rem 0.4rem; border-bottom:1px solid var(--slate-100); border-radius:0.35rem; }
.bs-rc:last-of-type { border-bottom:none; }
.bs-rc:hover { background:var(--slate-50); }
.bs-rc-ico { flex-shrink:0; width:36px; height:36px; border-radius:0.45rem; display:flex; align-items:center; justify-content:center; font-size:1rem; background:var(--slate-100); color:var(--slate-500); }
.bs-rc-ico--pdf { background:var(--red-100); color:var(--red-600); }
.bs-rc-ico--img { background:var(--green-100); color:var(--green-600); }
.bs-rc-main { flex:1; min-width:0; display:flex; flex-direction:column; gap:0.1rem; }
.bs-rc-title { font-weight:600; color:var(--gray-800); line-height:1.3; word-break:break-word; }
.bs-rc-meta { display:flex; align-items:center; flex-wrap:wrap; gap:0.3rem; font-size:0.76rem; color:var(--gray-400); min-width:0; }
.bs-rc-sep { color:var(--gray-300); }
.bs-rc-file { color:var(--color-primary); text-decoration:none; max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.bs-rc-file:hover { text-decoration:underline; }
.bs-rc-amt { flex-shrink:0; text-align:right; display:flex; flex-direction:column; gap:0.05rem; min-width:90px; }
.bs-rc-gross { font-weight:700; color:var(--gray-800); font-variant-numeric:tabular-nums; }
.bs-rc-net { font-size:0.72rem; color:var(--gray-400); white-space:nowrap; }
.bs-rc-act { flex-shrink:0; display:flex; align-items:center; gap:0.25rem; }
.bs-rc-del { display:inline-flex; margin:0; }
.bs-receipts-foot { display:flex; justify-content:space-between; align-items:center; padding:0.55rem 0.4rem 0.1rem; margin-top:0.25rem; border-top:1px solid var(--gray-200); }
.bs-receipts-foot span { color:var(--gray-500); font-weight:600; text-transform:uppercase; letter-spacing:0.03em; font-size:0.72rem; }
.bs-receipts-foot strong { color:var(--gray-800); font-variant-numeric:tabular-nums; }
/* Datei-/Aktions-Buttons (klein, dezent) */
.bs-file-btn { display:inline-flex; align-items:center; justify-content:center; width:28px; height:28px; color:var(--gray-500); background:var(--slate-100); border:none; border-radius:0.35rem; text-decoration:none; cursor:pointer; transition:background .12s,color .12s; }
.bs-file-btn:hover { background:var(--blue-100); color:var(--blue-600); }
.bs-file-btn--danger:hover { background:var(--red-100); color:var(--red-600); }
.bs-field-hint { font-weight:400; font-size:0.72rem; color:var(--gray-400); }
/* Beleg-Modal: Dateihinweis im Bearbeiten-Dialog */
.bs-receipt-editfile { font-size:0.82rem; color:var(--gray-700); margin:0 0 0.875rem; padding:0.4rem 0.6rem; background:var(--gray-100); border-radius:0.25rem; word-break:break-all; }
.bs-receipt-editfile small { color:var(--gray-400); }
/* Responsive: Belege-Zeile bei wenig Platz umbrechen */
@media (max-width:640px) {
.bs-rc { flex-wrap:wrap; }
.bs-rc-amt { text-align:left; min-width:0; flex-direction:row; gap:0.4rem; align-items:baseline; }
.bs-rc-act { margin-left:auto; }
}
/* Upload-Formular (contracts-style) */
.bs-ruf-wrap { padding:1rem 1.25rem; border-top:1px dashed var(--gray-300); }
.bs-ruf-form { padding-top:0.75rem; border-top:1px dashed var(--gray-300); margin-top:0.5rem; }
.bs-ruf-hidden { display:none; }
.bs-ruf-droprow {
display:flex; align-items:center; gap:0.5rem; margin-bottom:0.75rem; flex-wrap:wrap;
}
.bs-ruf-drop {
flex:1; display:flex; align-items:center; gap:0.5rem;
padding:0.4rem 0.75rem; background:var(--gray-50); border:1px dashed var(--gray-300);
border-radius:0.25rem; cursor:pointer; font-size:0.8rem; color:var(--gray-400);
transition:all 0.15s; min-width:0;
}
.bs-ruf-drop:hover { border-color:var(--color-primary); background:var(--color-primary-pale); color:var(--color-primary); }
.bs-ruf-drop svg { flex-shrink:0; }
.bs-ruf-meta {
display:grid; grid-template-columns:2fr 1fr 1fr 1fr;
gap:0.5rem 0.75rem; align-items:end;
}
.bs-ruf-field { display:flex; flex-direction:column; gap:0.2rem; }
.bs-ruf-field label { font-size:0.75rem; font-weight:600; color:var(--gray-700); }
.bs-ruf-field input, .bs-ruf-field select {
padding:0.3rem 0.6rem; border:1px solid var(--color-gray-400); border-radius:0.25rem;
font-size:0.82rem; width:100%; box-sizing:border-box; background:var(--color-bg); color:var(--color-gray-700);
}
.bs-ruf-field input:focus, .bs-ruf-field select:focus {
border-color:var(--bs-primary-light); outline:0; box-shadow:0 0 0 .2rem rgba(0,123,255,.25);
}
.bs-ruf-field-wide { grid-column:span 2; }
/* Neue-Position-Formular */
.bs-pos-add-row { display:flex; gap:0.5rem; flex-wrap:wrap; align-items:center; }
/* Bearbeiten-Modal */
.bs-edit-field { margin-bottom:0.875rem; }
.bs-edit-field label { display:block; font-size:0.82rem; font-weight:600; color:var(--gray-700); margin-bottom:0.3rem; }
.bs-edit-row { display:grid; grid-template-columns:1fr 1fr; gap:0.75rem; }
/* Leer-Hinweis */
.bs-empty-hint { padding:1.5rem; color:var(--gray-400); font-size:0.85rem; margin:0; }
@media (max-width:768px) {
.bs-info-grid { grid-template-columns:1fr; }
.bs-edit-row { grid-template-columns:1fr; }
.bs-pos-add-row .form-control { flex:1 1 100%; width:100% !important; }
.bs-ruf-drop { flex-basis:100%; }
.bs-ruf-meta { grid-template-columns:1fr 1fr; }
.bs-ruf-field-wide { grid-column:1/-1; }
}
@media (max-width:480px) {
.bs-ruf-meta { grid-template-columns:1fr; }
.bs-ruf-field-wide { grid-column:1; }
}
</style>