OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

show.php

66 KB
<?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'])): ?>
                                &ndash; <?= 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 &middot; {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 &middot; {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 &middot; {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">&times;</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">&times;</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">&times;</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">&times;</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>