OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

41 KB
<?php
$this->layout('layouts/main', [
    'page_title' => 'Vertragsmanagement',
    'current_route' => 'contracts',
    'full_width' => true,
]);

$statusLabels = ['active' => 'Aktiv', 'expiring' => 'Kündigung', 'expired' => 'Abgelaufen', 'cancelled' => 'Gekündigt', 'future' => 'Zukünftig'];
// Hilfsfunktion: Label für 'expiring' je nach Vertragstyp
$getExpiringLabel = function(array $contract) use ($statusLabels): string {
    if ($contract['status'] === 'expiring'
        && empty($contract['auto_renew'])
        && empty($contract['cancellation_notice_date'])
    ) {
        return 'Läuft aus';
    }
    return $statusLabels[$contract['status']] ?? $contract['status'];
};
$view_filter             = $view_filter             ?? '';
$payment_this_month_sum  = $payment_this_month_sum  ?? 0.0;
$payment_this_month_count= $payment_this_month_count ?? 0;
$cancel_soon_count       = $cancel_soon_count       ?? 0;
$auto_renew_count        = $auto_renew_count        ?? 0;
$category_filter = $category_filter ?? null;
$price_display   = $price_display   ?? 'brutto';
$priceLabel      = $price_display === 'netto' ? 'Netto' : 'Brutto';
$show_archive    = $show_archive ?? false;
$sort            = $sort ?? null;
$dir             = $dir  ?? 'asc';
$hasFilters = ($search !== '' || $status_filter !== '' || $company_filter !== null || $category_filter !== null || $view_filter !== '');

// Aktuelle Filter für Sortier-Links erhalten
$baseQs = array_filter([
    'q'        => $search !== '' ? $search : null,
    'status'   => $status_filter !== '' ? $status_filter : null,
    'company'  => $company_filter !== null ? $company_filter : null,
    'category' => $category_filter !== null ? $category_filter : null,
    'archive'  => $show_archive ? '1' : null,
], static fn($v) => $v !== null && $v !== '');

// Sortierbarer Tabellenkopf (nur in der Standardansicht, nicht in Schnellansichten)
$sortHeader = function (string $col, string $label) use ($sort, $dir, $baseQs, $view_filter): string {
    if ($view_filter !== '') {
        return htmlspecialchars($label);
    }
    $nextDir = ($sort === $col && $dir === 'asc') ? 'desc' : 'asc';
    $href = '/contracts?' . http_build_query(array_merge($baseQs, ['sort' => $col, 'dir' => $nextDir]));
    $arrow = $sort === $col ? ($dir === 'asc' ? ' ▲' : ' ▼') : '';
    return '<a href="' . htmlspecialchars($href) . '" class="cv-sort' . ($sort === $col ? ' cv-sort-active' : '') . '">'
         . htmlspecialchars($label) . $arrow . '</a>';
};
?>

<div class="cv-wrap">

    <!-- Content Header -->
    <div class="content-header">
        <div>
            <h1><?= $this->te('Vertragsmanagement') ?></h1>
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
                <li class="breadcrumb-item active"><?= $this->te('Verträge') ?></li>
            </ol>
        </div>
        <div class="content-header-actions">
            <?php
            // Export-Buttons übernehmen die aktiven Filter (Suche/Status/Firma/Kategorie)
            $csvQs = [];
            if ($search !== '') $csvQs['q'] = $search;
            if ($status_filter !== '') $csvQs['status'] = $status_filter;
            if ($company_filter !== null) $csvQs['company'] = $company_filter;
            if ($category_filter !== null) $csvQs['category'] = $category_filter;
            $csvUrl = '/contracts/export/csv' . (!empty($csvQs) ? '?' . http_build_query($csvQs) : '');

            // PDF ist ein Kostenreport (aktive/laufende Verträge) – Status-Filter ist
            // dort nicht sinnvoll, Suche/Firma/Kategorie werden aber übernommen.
            $pdfQs = $csvQs;
            unset($pdfQs['status']);
            $pdfUrl = '/contracts/export/pdf' . (!empty($pdfQs) ? '?' . http_build_query($pdfQs) : '');
            ?>
            <?php if (!$show_archive): ?>
            <a href="<?= $pdfUrl ?>" target="_blank" class="btn btn-default" title="<?= $this->te('Verträge als PDF exportieren') ?>">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><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('PDF-Export') ?>
            </a>
            <a href="<?= $csvUrl ?>" class="btn btn-default" title="<?= $this->te('Aktuelle Auswahl als CSV exportieren') ?>">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><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"/><line x1="8" y1="13" x2="16" y2="13"/><line x1="8" y1="17" x2="13" y2="17"/></svg>
                CSV
            </a>
            <?php $ical_feed_url = $ical_feed_url ?? null; ?>
            <details class="cv-ical-menu">
                <summary class="btn btn-default" title="<?= $this->te('Kündigungsfristen als Kalender') ?>">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
                    <?= $this->te('Kalender') ?>
                </summary>
                <div class="cv-ical-pop">
                    <a href="/contracts/export/ical" class="cv-ical-row">
                        <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>
                        <span><?= $this->te('.ics herunterladen') ?> <small><?= $this->te('(einmalig)') ?></small></span>
                    </a>
                    <div class="cv-ical-sep"></div>
                    <?php if ($ical_feed_url): ?>
                    <div class="cv-ical-label"><?= $this->te('Abo-Link (in Kalender-App einfügen):') ?></div>
                    <div class="cv-ical-url-row">
                        <input type="text" id="cvIcalUrl" class="cv-ical-url" readonly value="<?= $this->e($ical_feed_url) ?>" onclick="this.select()">
                        <button type="button" class="cv-ical-copy" onclick="(function(b){navigator.clipboard&&navigator.clipboard.writeText(document.getElementById('cvIcalUrl').value);b.textContent='✓';setTimeout(function(){b.textContent='Kopieren';},1500);})(this)"><?= $this->te('Kopieren') ?></button>
                    </div>
                    <div class="cv-ical-actions">
                        <form action="/contracts/calendar/subscribe" method="POST" style="margin:0;">
                            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                            <button type="submit" class="cv-ical-link"><?= $this->te('Link erneuern') ?></button>
                        </form>
                        <form action="/contracts/calendar/revoke" method="POST" style="margin:0;" onsubmit="return confirm('Abo-Link widerrufen? Bestehende Abonnements funktionieren dann nicht mehr.');">
                            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                            <button type="submit" class="cv-ical-link cv-ical-link-danger"><?= $this->te('Widerrufen') ?></button>
                        </form>
                    </div>
                    <?php else: ?>
                    <div class="cv-ical-label"><?= $this->te('Kalender abonnieren – Fristen automatisch synchron halten:') ?></div>
                    <form action="/contracts/calendar/subscribe" method="POST" style="margin:0;">
                        <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                        <button type="submit" class="cv-ical-row cv-ical-row-btn">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M12 5v14M5 12h14"/></svg>
                            <span><?= $this->te('Abo-Link erstellen') ?></span>
                        </button>
                    </form>
                    <?php endif; ?>
                </div>
            </details>
            <?php endif; ?>
            <a href="/contracts/categories" class="btn btn-default" title="<?= $this->te('Kategorien verwalten') ?>">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M4 6h16M4 12h8m-8 6h16"/></svg>
                <?= $this->te('Kategorien') ?>
            </a>
            <?php if ($show_archive): ?>
            <a href="/contracts" class="btn btn-default">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="15 18 9 12 15 6"/></svg>
                <?= $this->te('Zur&uuml;ck zu Vertr&auml;gen') ?>
            </a>
            <?php else: ?>
            <a href="/contracts?archive=1" class="btn btn-default" title="<?= $this->te('Abgelaufene und beendete Verträge anzeigen') ?>">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>
                <?= $this->te('Archiv') ?>
            </a>
            <?php if ($can_manage): ?>
            <a href="/contracts/create" class="btn btn-primary">
                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="16" height="16"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
                <?= $this->te('Neuer Vertrag') ?>
            </a>
            <?php endif; ?>
            <?php endif; ?>
        </div>
    </div>

    <div class="page-body" style="min-height:200px;">

    <?php if ($show_archive): ?>
    <!-- Archiv-Banner -->
    <div class="cv-archive-banner">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>
        <strong><?= $this->te('Archiv') ?></strong> &ndash; <?= $this->te('Abgelaufene und beendete Vertr&auml;ge') ?>
        <span class="cv-archive-count"><?= $this->te(count($contracts) === 1 ? '{anzahl} Eintrag' : '{anzahl} Eintr&auml;ge', ['anzahl' => count($contracts)]) ?></span>
    </div>
    <?php endif; ?>

    <?php if (!$show_archive): ?>
    <!-- KPI Small Boxes -->
    <div class="kpi-row">
        <div class="kpi-col">
            <div class="small-box bg-info">
                <div class="inner">
                    <h3><?= $this->money($cost_summary['total_monthly'], 2) ?></h3>
                    <p><?= $this->te('Monatliche Kosten') ?> <span class="cv-price-badge"><?= $priceLabel ?></span></p>
                </div>
                <div class="icon"><i class="fas fa-euro-sign"></i></div>
                <span class="small-box-footer cv-kpi-footer-empty">&nbsp;</span>
            </div>
        </div>
        <div class="kpi-col">
            <div class="small-box bg-primary">
                <div class="inner">
                    <h3><?= $this->money($cost_summary['total_yearly'], 2) ?></h3>
                    <p><?= $this->te('Jährliche Kosten') ?> <span class="cv-price-badge"><?= $priceLabel ?></span></p>
                </div>
                <div class="icon"><i class="fas fa-calendar-alt"></i></div>
                <span class="small-box-footer cv-kpi-footer-empty">&nbsp;</span>
            </div>
        </div>
        <div class="kpi-col">
            <a href="/contracts?status=active" class="cv-kpi-link">
            <div class="small-box bg-success">
                <div class="inner">
                    <h3><?= (int)($status_counts['active'] ?? 0) ?></h3>
                    <p><?= $this->te('Aktive Verträge') ?></p>
                </div>
                <div class="icon"><i class="fas fa-file-contract"></i></div>
                <span class="small-box-footer"><?= $this->te('Anzeigen') ?> <i class="fas fa-arrow-circle-right"></i></span>
            </div>
            </a>
        </div>
        <div class="kpi-col">
            <a href="/contracts?status=expiring" class="cv-kpi-link">
            <div class="small-box <?= (int)($status_counts['expiring'] ?? 0) > 0 ? 'bg-warning' : 'bg-secondary' ?>">
                <div class="inner">
                    <h3><?= (int)($status_counts['expiring'] ?? 0) ?></h3>
                    <p><?= $this->te('Kündigung beachten') ?></p>
                </div>
                <div class="icon"><i class="fas fa-exclamation-triangle"></i></div>
                <span class="small-box-footer"><?= $this->te('Anzeigen') ?> <i class="fas fa-arrow-circle-right"></i></span>
            </div>
            </a>
        </div>
    </div>

    <?php if ($cancel_soon_count > 0 && $view_filter !== 'cancel_soon'): ?>
    <!-- Prominente Warnung: Kündigungsfristen laufen ab -->
    <a href="/contracts?view=cancel_soon" class="cv-alert-banner">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
        <span><?= $this->t($cancel_soon_count === 1 ? '<strong>{anzahl} Vertrag</strong> mit Kündigungsfrist in den nächsten 60 Tagen &ndash; jetzt prüfen' : '<strong>{anzahl} Verträge</strong> mit Kündigungsfrist in den nächsten 60 Tagen &ndash; jetzt prüfen', ['anzahl' => $cancel_soon_count]) ?></span>
        <span class="cv-alert-cta"><?= $this->te('Anzeigen &rarr;') ?></span>
    </a>
    <?php endif; ?>
    <?php endif; /* !$show_archive */ ?>

    <!-- Filterleiste -->
    <div class="card card-outline card-primary mb-3" >
        <div class="filter-card-body">
            <form action="/contracts" method="GET" id="cvFilterForm" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;">
                <?php if ($show_archive): ?><input type="hidden" name="archive" value="1"><?php endif; ?>
                <div style="position:relative;flex:1;min-width:200px;">
                                        <input type="search" name="q" class="form-control" style="padding-left:2rem;" placeholder="<?= $this->te('Verträge durchsuchen…') ?>" value="<?= $this->e($search) ?>" autocomplete="off">
                </div>

                <select name="status" class="form-control" style="width:auto;" onchange="this.form.submit()">
                    <option value=""><?= $this->te('Alle Status') ?></option>
                    <option value="active"    <?= $status_filter === 'active'    ? 'selected' : '' ?>><?= $this->te('Aktiv') ?></option>
                    <option value="future"    <?= $status_filter === 'future'    ? 'selected' : '' ?>><?= $this->te('Zukünftig') ?></option>
                    <option value="expiring"  <?= $status_filter === 'expiring'  ? 'selected' : '' ?>><?= $this->te('Kündigung beachten / Läuft aus') ?></option>
                    <option value="expired"   <?= $status_filter === 'expired'   ? 'selected' : '' ?>><?= $this->te('Abgelaufen') ?></option>
                    <option value="cancelled" <?= $status_filter === 'cancelled' ? 'selected' : '' ?>><?= $this->te('Gekündigt') ?></option>
                </select>

                <?php if (!empty($companies)): ?>
                <select name="company" class="form-control" style="width:auto;" onchange="this.form.submit()">
                    <option value=""><?= $this->te('Alle Firmen') ?></option>
                    <?php foreach ($companies as $comp): ?>
                        <option value="<?= $comp['id'] ?>" <?= $company_filter === (int)$comp['id'] ? 'selected' : '' ?>><?= $this->e($comp['name']) ?></option>
                    <?php endforeach; ?>
                </select>
                <?php endif; ?>

                <?php if (!empty($categories)): ?>
                <select name="category" class="form-control" style="width:auto;" onchange="this.form.submit()">
                    <option value=""><?= $this->te('Alle Kategorien') ?></option>
                    <option value="-1" <?= $category_filter === -1 ? 'selected' : '' ?>><?= $this->te('Ohne Kategorie') ?></option>
                    <?php foreach ($categories as $cat): ?>
                        <option value="<?= $cat['id'] ?>" <?= $category_filter === (int)$cat['id'] ? 'selected' : '' ?>>
                            <?= $this->e($cat['name']) ?> (<?= $cat['contract_count'] ?>)
                        </option>
                    <?php endforeach; ?>
                </select>
                <?php endif; ?>

                <?php if (!$show_archive): ?>
                <select name="view" class="form-control" style="width:auto;" onchange="this.form.submit()">
                    <option value=""><?= $this->te('Schnellansicht…') ?></option>
                    <option value="payment_this_month" <?= $view_filter === 'payment_this_month' ? 'selected' : '' ?>>
                        💳 <?= $this->te('Zahlung diesen Monat ({anzahl} · {summe} {art})', ['anzahl' => $payment_this_month_count, 'summe' => $this->money($payment_this_month_sum, 2), 'art' => $priceLabel]) ?>
                    </option>
                    <option value="cancel_soon" <?= $view_filter === 'cancel_soon' ? 'selected' : '' ?>>
                        ⚠️ <?= $this->te('Kündigung in 60 Tagen') ?><?= $cancel_soon_count > 0 ? ' (' . $cancel_soon_count . ')' : '' ?>
                    </option>
                    <option value="auto_renew" <?= $view_filter === 'auto_renew' ? 'selected' : '' ?>>
                        🔄 <?= $this->te('Auto-Verlängerung ({anzahl})', ['anzahl' => $auto_renew_count]) ?>
                    </option>
                </select>
                <?php endif; ?>

                <button type="submit" class="btn btn-default"><?= $this->te('Suchen') ?></button>

                <?php if ($hasFilters): ?>
                    <a href="/contracts" class="btn btn-default">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
                        <?= $this->te('Filter zurücksetzen') ?>
                    </a>
                <?php endif; ?>
            </form>
        </div>
    </div>

    <!-- Aktive Schnellansicht: Info-Banner -->
    <?php if ($view_filter === 'payment_this_month' && count($contracts) > 0): ?>
    <div class="cv-view-banner cv-view-banner-blue">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><rect x="1" y="4" width="22" height="16" rx="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>
        <?= $this->t(count($contracts) === 1 ? '<strong>{anzahl} Zahlung</strong> fällig im {monat}' : '<strong>{anzahl} Zahlungen</strong> fällig im {monat}', ['anzahl' => count($contracts), 'monat' => $this->e(date('F Y'))]) ?> &nbsp;·&nbsp;
        <?= $this->t('Gesamt: <strong>{summe}</strong>', ['summe' => $this->money($payment_this_month_sum, 2)]) ?>
    </div>
    <?php elseif ($view_filter === 'cancel_soon' && count($contracts) > 0): ?>
    <div class="cv-view-banner cv-view-banner-orange">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
        <?= $this->t(count($contracts) === 1 ? '<strong>{anzahl} Vertrag</strong> mit Kündigungsfrist in den nächsten 60 Tagen' : '<strong>{anzahl} Verträge</strong> mit Kündigungsfrist in den nächsten 60 Tagen', ['anzahl' => count($contracts)]) ?>
    </div>
    <?php elseif ($view_filter === 'auto_renew'): ?>
    <div class="cv-view-banner cv-view-banner-teal">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
        <?= $this->t(count($contracts) === 1 ? '<strong>{anzahl} Vertrag</strong> mit automatischer Verlängerung' : '<strong>{anzahl} Verträge</strong> mit automatischer Verlängerung', ['anzahl' => count($contracts)]) ?>
    </div>
    <?php endif; ?>

    <!-- Tabelle / Leer-Zustand -->
        <?php if (empty($contracts)): ?>
            <div class="empty-state">
                <div class="cv-empty-icon">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" width="44" height="44"><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"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
                </div>
                <p class="cv-empty-title"><?= $hasFilters ? 'Keine Verträge gefunden' : 'Noch keine Verträge' ?></p>
                <p class="cv-empty-hint"><?= $hasFilters ? 'Passen Sie die Filterkriterien an.' : 'Legen Sie den ersten Vertrag an.' ?></p>
                <?php if ($can_manage && !$hasFilters): ?>
                    <a href="/contracts/create" class="btn btn-primary" style="margin-top:1rem;">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="16" height="16"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
                        <?= $this->te('Neuer Vertrag') ?>
                    </a>
                <?php endif; ?>
            </div>
        <?php else: ?>
            <div class="table-wrapper">
                <table class="data-table">
                    <thead>
                        <tr>
                            <th scope="col" style="width:110px"><?= $sortHeader('status', 'Status') ?></th>
                            <th scope="col"><?= $sortHeader('title', 'Vertragsbezeichnung') ?></th>
                            <th scope="col" style="width:130px"><?= $sortHeader('category', 'Kategorie') ?></th>
                            <th scope="col"><?= $sortHeader('company', 'Firma') ?></th>
                            <th scope="col"><?= $this->te('Vertragsnr.') ?></th>
                            <th scope="col" class="cv-right" style="width:130px"><?= $sortHeader('cost', 'Kosten') ?> <span class="cv-th-price-label"><?= $priceLabel ?></span></th>
                            <?php if ($view_filter === 'payment_this_month'): ?>
                            <th scope="col" style="width:150px"><?= $this->te('Nächste Zahlung') ?></th>
                            <?php elseif ($view_filter === 'cancel_soon'): ?>
                            <th scope="col" style="width:150px"><?= $this->te('Kündigung bis') ?></th>
                            <?php elseif ($view_filter === 'auto_renew'): ?>
                            <th scope="col" style="width:150px"><?= $this->te('Verlängerung') ?></th>
                            <?php else: ?>
                            <th scope="col" style="width:195px"><?= $sortHeader('end_date', 'Laufzeit') ?></th>
                            <?php endif; ?>
                            <th scope="col" style="width:140px"><?= $this->te('Verantwortlich') ?></th>
                        </tr>
                    </thead>
                    <tbody>
                        <?php foreach ($contracts as $c): ?>
                        <tr class="cv-row clickable" onclick="window.location='/contracts/<?= $c['id'] ?>'" tabindex="0"
                            onkeydown="if(event.key==='Enter')window.location='/contracts/<?= $c['id'] ?>'">
                            <td>
                                <?php
                                $pillMap = ['active'=>'status-pill--success','expiring'=>'status-pill--warning','expired'=>'status-pill--danger','cancelled'=>'status-pill--neutral','future'=>'status-pill--purple'];
                                $dotMap  = ['active'=>'status-dot--success','expiring'=>'status-dot--warning','expired'=>'status-dot--danger','cancelled'=>'','future'=>''];
                                ?>
                                <span class="status-pill <?= $pillMap[$c['status']] ?? 'status-pill--neutral' ?>">
                                    <span class="status-dot <?= $dotMap[$c['status']] ?? '' ?>"></span>
                                    <?= $this->e($getExpiringLabel($c)) ?>
                                </span>
                            </td>
                            <td>
                                <a href="/contracts/<?= $c['id'] ?>" class="cv-link"><?= $this->e($c['title']) ?></a>
                            </td>
                            <td>
                                <?php if (!empty($c['category_name'])): ?>
                                <span class="cv-cat-pill" style="background:<?= $this->e($c['category_color']) ?>20;color:<?= $this->e($c['category_color']) ?>;border-color:<?= $this->e($c['category_color']) ?>40;">
                                    <?= $this->e($c['category_name']) ?>
                                </span>
                                <?php else: ?>
                                <span class="cv-muted" style="font-size:0.8rem;">–</span>
                                <?php endif; ?>
                            </td>
                            <td class="cv-muted"><?= $this->e($c['company_name'] ?? '–') ?></td>
                            <td class="cv-muted cv-mono"><?= $this->e($c['contract_number'] ?? '–') ?></td>
                            <td class="cv-right cv-cost">
                                <?php if (!empty($c['cost_amount'])): ?>
                                    <?php
                                    $interval  = $c['cost_interval'] ?? 'monthly';
                                    // Originalbetrag (Brutto), bei Bedarf auf Netto umrechnen
                                    $origAmount = (float)$c['cost_amount'];
                                    if ($price_display === 'netto' && !empty($c['tax_rate']) && (float)$c['tax_rate'] > 0) {
                                        $origAmount = $origAmount / (1 + (float)$c['tax_rate'] / 100);
                                    }
                                    ?>
                                    <?php if ($interval === 'one_time'): ?>
                                        <?= $this->money($origAmount, 2) ?><span class="cv-cost-interval"> <?= $this->te('einmalig') ?></span>
                                    <?php else: ?>
                                        <?php
                                        // Immer auf Monatsbasis anzeigen (vergleichbar über alle Zeilen)
                                        $monthlyAmount = match ($interval) {
                                            'yearly'    => $origAmount / 12,
                                            'quarterly' => $origAmount / 3,
                                            default     => $origAmount,
                                        };
                                        // Tatsächliche Zahlweise als Subzeile (nur wenn nicht monatlich)
                                        $cycleLabel = match ($interval) {
                                            'yearly'    => 'jährlich',
                                            'quarterly' => 'quartalsweise',
                                            default     => '',
                                        };
                                        ?>
                                        <?= $this->money($monthlyAmount, 2) ?><span class="cv-cost-interval"><?= $this->te('/Monat') ?></span>
                                        <?php if ($cycleLabel !== ''): ?>
                                            <span class="cv-cost-sub"><?= $this->money($origAmount, 2) ?>&nbsp;·&nbsp;<?= $cycleLabel ?></span>
                                        <?php endif; ?>
                                    <?php endif; ?>
                                <?php else: ?>
                                    <span class="cv-muted">–</span>
                                <?php endif; ?>
                            </td>
                            <?php if ($view_filter === 'payment_this_month'): ?>
                            <td class="cv-muted cv-dates">
                                <?php if (!empty($c['next_payment_date'])): ?>
                                    <?php
                                    $npd = $c['next_payment_date'];
                                    $daysLeft = (int)((strtotime($npd) - time()) / 86400);
                                    $color = $daysLeft <= 3 ? '#dc2626' : ($daysLeft <= 7 ? '#d97706' : '#374151');
                                    ?>
                                    <span style="color:<?= $color ?>;font-weight:600;"><?= date('d.m.Y', strtotime($npd)) ?></span>
                                    <span class="cv-cost-interval"><?= $this->t('(in {tage}&nbsp;Tagen)', ['tage' => $daysLeft]) ?></span>
                                <?php else: ?>
                                    <span class="cv-muted">–</span>
                                <?php endif; ?>
                            </td>
                            <?php elseif ($view_filter === 'cancel_soon'): ?>
                            <td class="cv-muted cv-dates">
                                <?php if (!empty($c['cancellation_notice_date'])): ?>
                                    <?php
                                    $cnd = $c['cancellation_notice_date'];
                                    $daysLeft = (int)((strtotime($cnd) - time()) / 86400);
                                    $color = $daysLeft <= 7 ? '#dc2626' : ($daysLeft <= 30 ? '#d97706' : '#374151');
                                    ?>
                                    <span style="color:<?= $color ?>;font-weight:600;"><?= date('d.m.Y', strtotime($cnd)) ?></span>
                                    <span class="cv-cost-interval"><?= $this->t('({tage}&nbsp;Tage)', ['tage' => $daysLeft]) ?></span>
                                <?php else: ?>
                                    <span class="cv-muted">–</span>
                                <?php endif; ?>
                            </td>
                            <?php elseif ($view_filter === 'auto_renew'): ?>
                            <td class="cv-muted cv-dates">
                                <?php if (!empty($c['end_date'])): ?>
                                    <span><?= $this->te('bis {datum}', ['datum' => date('d.m.Y', strtotime($c['end_date']))]) ?></span>
                                <?php else: ?>
                                    <span style="color:var(--gray-500);"><?= $this->te('unbefristet') ?></span>
                                <?php endif; ?>
                                <?php if (!empty($c['cancellation_period_months'])): ?>
                                    <br><span class="cv-cost-interval"><?= $this->t('Frist: {monate}&nbsp;Mon.', ['monate' => $c['cancellation_period_months']]) ?></span>
                                <?php endif; ?>
                            </td>
                            <?php else: ?>
                            <td class="cv-muted cv-dates">
                                <?php if (!empty($c['start_date']) || !empty($c['end_date'])): ?>
                                    <span><?= !empty($c['start_date']) ? date('d.m.Y', strtotime($c['start_date'])) : '?' ?></span>
                                    <span class="cv-date-sep">–</span>
                                    <span><?= !empty($c['end_date']) ? date('d.m.Y', strtotime($c['end_date'])) : 'unbefristet' ?></span>
                                <?php else: ?>
                                    –
                                <?php endif; ?>
                            </td>
                            <?php endif; ?>
                            <td class="cv-muted"><?= $this->e($c['responsible_name'] ?? '–') ?></td>
                        </tr>
                        <?php endforeach; ?>
                    </tbody>
                </table>
            </div>
            <div class="cv-footer">
                <span class="cv-count"><?= $this->te(count($contracts) === 1 ? '{anzahl} Vertrag' : '{anzahl} Verträge', ['anzahl' => count($contracts)]) ?></span>
                <?php if ($hasFilters): ?>
                    <a href="/contracts" class="cv-reset"><?= $this->te('Filter zurücksetzen') ?></a>
                <?php endif; ?>
            </div>
        <?php endif; ?>
    </div>
</div>

<style>
/* ====================================================
   Vertragsübersicht  ·  Prefix: cv-
   ==================================================== */

.cv-wrap {
    display: flex;
    flex-direction: column;
}

/* ── Body / Tabelle: Standard .table-wrapper/.data-table aus app.css ── */
.cv-sort {
    color: inherit;
    text-decoration: none;
    cursor: pointer;
}
.cv-sort:hover { color: var(--color-primary); }
.cv-sort-active { color: var(--color-primary-deep); }

/* ── Kalender-Abo Dropdown ────────────────────────── */
.cv-ical-menu { position: relative; display: inline-block; }
.cv-ical-menu > summary { list-style: none; cursor: pointer; }
.cv-ical-menu > summary::-webkit-details-marker { display: none; }
.cv-ical-pop {
    position: absolute; right: 0; top: calc(100% + 4px); z-index: 30;
    width: 320px; background: var(--color-bg); border: 1px solid var(--gray-200);
    border-radius: 0.375rem; box-shadow: 0 8px 24px rgba(0,0,0,0.12);
    padding: 0.75rem; text-align: left;
}
.cv-ical-row {
    display: flex; align-items: center; gap: 0.5rem; width: 100%;
    padding: 0.5rem 0.4rem; color: var(--gray-700); text-decoration: none;
    font-size: 0.85rem; border: none; background: transparent; cursor: pointer;
    border-radius: 0.25rem;
}
.cv-ical-row:hover { background: var(--gray-100); text-decoration: none; color: var(--gray-900); }
.cv-ical-row small { color: var(--gray-400); }
.cv-ical-row-btn { text-align: left; }
.cv-ical-sep { height: 1px; background: var(--gray-100); margin: 0.4rem 0; }
.cv-ical-label { font-size: 0.72rem; color: var(--gray-500); margin: 0.25rem 0.4rem 0.4rem; }
.cv-ical-url-row { display: flex; gap: 0.35rem; padding: 0 0.4rem; }
.cv-ical-url {
    flex: 1; min-width: 0; font-size: 0.72rem; padding: 0.35rem 0.5rem;
    border: 1px solid var(--gray-300); border-radius: 0.25rem; color: var(--gray-700); background: var(--gray-50);
}
.cv-ical-copy {
    flex-shrink: 0; font-size: 0.72rem; padding: 0.35rem 0.6rem;
    border: 1px solid var(--color-primary); background: var(--color-primary); color: var(--color-on-accent);
    border-radius: 0.25rem; cursor: pointer;
}
.cv-ical-copy:hover { background: var(--color-primary-dark); }
.cv-ical-actions { display: flex; gap: 0.75rem; padding: 0.5rem 0.4rem 0; }
.cv-ical-link {
    font-size: 0.75rem; background: none; border: none; cursor: pointer;
    color: var(--color-primary); padding: 0; text-decoration: underline;
}
.cv-ical-link-danger { color: var(--red-600); }

.cv-row:focus { outline: 2px solid var(--blue-500); outline-offset: -2px; }

/* ── Status-Badge ─────────────────────────────────── */

/* ── Zell-Styles ──────────────────────────────────── */
.cv-link {
    color: var(--gray-900);
    font-weight: 500;
    text-decoration: none;
}
.cv-link:hover { color: var(--color-primary); text-decoration: underline; }
.cv-cat-pill {
    display: inline-block; font-size: 0.67rem; font-weight: 600;
    padding: 0.1rem 0.45rem; border-radius: 9999px;
    border: 1px solid; margin-left: 0.35rem; vertical-align: middle;
    letter-spacing: 0.02em; white-space: nowrap;
}

.cv-muted { color: var(--gray-500); }
.cv-right { text-align: right; }

.cv-cost {
    font-weight: 600;
    color: var(--gray-900);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.cv-cost-interval {
    font-size: 0.75rem;
    font-weight: 400;
    color: var(--gray-400);
}
.cv-cost-sub {
    display: block;
    margin-top: 0.1rem;
    font-size: 0.72rem;
    font-weight: 400;
    color: var(--gray-400);
    white-space: nowrap;
}

.cv-mono { font-variant-numeric: tabular-nums; font-size: 0.825rem; }

.cv-dates {
    font-size: 0.825rem;
    white-space: nowrap;
}
.cv-date-sep { margin: 0 0.25rem; color: var(--gray-300); }

/* ── Footer ───────────────────────────────────────── */
.cv-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 0.875rem;
    padding: 0 0.25rem;
}
.cv-count { font-size: 0.8rem; color: var(--gray-400); }

/* ── Leer-Zustand ─────────────────────────────────── */
.cv-empty-icon {
    width: 72px;
    height: 72px;
    background: var(--gray-100);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--gray-400);
    margin-bottom: 1.25rem;
}
.cv-empty-title {
    font-size: 1rem;
    font-weight: 600;
    color: var(--gray-700);
    margin: 0 0 0.375rem;
}
.cv-empty-hint {
    font-size: 0.85rem;
    color: var(--gray-400);
    margin: 0;
}

/* ── Aktive Ansicht Banner ────────────────────────── */
.cv-view-banner {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.65rem 1rem;
    border-radius: 0.375rem;
    font-size: 0.85rem;
    margin-bottom: 0.75rem;
}
.cv-view-banner-blue   { background: var(--blue-50); color: var(--blue-800); border: 1px solid var(--blue-200); }
.cv-view-banner-orange { background: var(--amber-50); color: var(--amber-800); border: 1px solid var(--amber-200); }
.cv-view-banner-teal   { background: var(--teal-50); color: var(--emerald-800); border: 1px solid var(--emerald-200); }

/* ── KPI-Kachel als Link ──────────────────────────── */
.cv-kpi-link { display: block; text-decoration: none; color: inherit; }
.cv-kpi-link:hover { text-decoration: none; }
.cv-kpi-link:hover .small-box { transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0,0,0,0.12); transition: all 0.15s; }
/* Leerer Footer auf den Kosten-Karten: nur als Höhen-Platzhalter, nicht interaktiv */
.cv-kpi-footer-empty { cursor: default; }

/* ── Alarm-Banner (Kündigungsfristen) ─────────────── */
.cv-alert-banner {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.75rem 1rem;
    background: var(--orange-50);
    border: 1px solid var(--orange-300);
    border-left: 4px solid var(--orange-600);
    border-radius: 0.375rem;
    margin-bottom: 1rem;
    font-size: 0.9rem;
    color: var(--orange-800);
    text-decoration: none;
    transition: background 0.15s;
}
.cv-alert-banner:hover { background: var(--orange-100); text-decoration: none; color: var(--orange-800); }
.cv-alert-banner svg { flex-shrink: 0; color: var(--orange-600); }
.cv-alert-cta { margin-left: auto; font-weight: 600; white-space: nowrap; }

/* ── Archiv-Banner ────────────────────────────────── */
.cv-archive-banner {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.75rem 1rem;
    background: var(--gray-100);
    border: 1px solid var(--gray-300);
    border-radius: 0.375rem;
    margin-bottom: 1rem;
    font-size: 0.875rem;
    color: var(--gray-700);
}
.cv-archive-banner svg { flex-shrink: 0; color: var(--gray-500); }
.cv-archive-count {
    margin-left: auto;
    background: var(--gray-200);
    color: var(--gray-700);
    padding: 0.1rem 0.5rem;
    border-radius: 9999px;
    font-size: 0.78rem;
    font-weight: 600;
}

/* ── Preis-Badge (Brutto/Netto) ───────────────────── */
.cv-price-badge {
    display: inline-block;
    font-size: 0.6rem;
    font-weight: 700;
    background: rgba(255,255,255,0.25);
    border: 1px solid rgba(255,255,255,0.4);
    border-radius: 3px;
    padding: 0.05rem 0.3rem;
    letter-spacing: 0.04em;
    vertical-align: middle;
    margin-left: 0.3rem;
    text-transform: uppercase;
}
.cv-th-price-label {
    font-size: 0.6rem;
    font-weight: 600;
    background: var(--blue-100);
    color: var(--blue-800);
    border: 1px solid var(--blue-200);
    border-radius: 3px;
    padding: 0.05rem 0.3rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    margin-left: 0.25rem;
    vertical-align: middle;
}

/* ── Responsive ───────────────────────────────────── */
@media (max-width: 768px) {
    .cv-select { width: 100%; }

}

@media (max-width: 480px) {

}
</style>