Quelltext des aktuellen Standes
<?php $this->layout('layouts/main', ['page_title' => $page_title, 'current_route' => 'deadlines', 'full_width' => true]); ?>
<?php
/** $items, $stats, $within, $horizons, $source, $sources, $hide_overdue, $can_settings */
$sevPill = [
'overdue' => ['status-pill--danger', 'fa-exclamation-triangle', 'bg-danger'],
'critical' => ['status-pill--orange', 'fa-bolt', 'bg-warning'],
'warning' => ['status-pill--warning', 'fa-clock', 'bg-primary'],
'info' => ['status-pill--info', 'fa-calendar', 'bg-info'],
];
$kpiMeta = [
'overdue' => ['Überfällig', 'fa-exclamation-triangle', 'bg-danger'],
'critical' => ['≤ 7 Tage', 'fa-bolt', 'bg-warning'],
'warning' => ['≤ 30 Tage', 'fa-clock', 'bg-primary'],
'info' => ['Später', 'fa-calendar', 'bg-info'],
];
$whenText = static function (int $d): string {
if ($d < 0) return abs($d) . ' Tag' . (abs($d) === 1 ? '' : 'e') . ' überfällig';
if ($d === 0) return 'heute fällig';
return 'in ' . $d . ' Tag' . ($d === 1 ? '' : 'en');
};
?>
<div>
<div class="content-header">
<div>
<h1><?= $this->te('Fristen-Cockpit') ?></h1>
<ol class="breadcrumb"><li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li><li class="breadcrumb-item active"><?= $this->te('Fristen') ?></li></ol>
</div>
<div class="content-header-actions">
<?php if (($ical_token ?? '') === ''): ?>
<form action="/deadlines/calendar/subscribe" method="POST" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<button type="submit" class="btn btn-default"><i class="fas fa-calendar-alt"></i> <?= $this->te('Kalender abonnieren') ?></button>
</form>
<?php else: ?>
<button type="button" class="btn btn-default" onclick="document.getElementById('dlIcalBox').style.display = document.getElementById('dlIcalBox').style.display === 'none' ? 'block' : 'none';">
<i class="fas fa-calendar-alt"></i> <?= $this->te('Kalender-Abo') ?>
</button>
<?php endif; ?>
<?php if ($can_settings): ?>
<a href="/admin/deadlines/settings" class="btn btn-default"><i class="fas fa-envelope"></i> <?= $this->te('Digest-Mail') ?></a>
<?php endif; ?>
</div>
</div>
<?php if (($ical_token ?? '') !== ''): ?>
<div id="dlIcalBox" class="card card-outline card-primary mb-3" style="display:none;">
<div class="filter-card-body">
<label class="form-group" style="display:block;margin-bottom:.35rem;font-weight:600;"><?= $this->te('Kalender-Abo-Link (iCal)') ?></label>
<div style="display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;">
<input type="text" class="form-control form-control-sm" style="flex:1;min-width:260px;" readonly
value="<?= $this->e(rtrim($this->baseUrl(), '/')) ?>/deadlines/calendar/<?= $this->e($ical_token) ?>.ics"
onclick="this.select();">
<form action="/deadlines/calendar/subscribe" method="POST" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<button type="submit" class="btn btn-outline-secondary btn-sm"><i class="fas fa-sync"></i> <?= $this->te('Neu erzeugen') ?></button>
</form>
<form action="/deadlines/calendar/revoke" method="POST" style="display:inline;"
onsubmit="return confirm('Kalender-Abo widerrufen? Der Link wird ungültig.')">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<button type="submit" class="btn btn-outline-danger btn-sm"><i class="fas fa-times"></i> <?= $this->te('Widerrufen') ?></button>
</form>
</div>
<small class="form-hint"><?= $this->te('Diesen Link in Outlook/Apple Kalender/Google Kalender als Abo-Kalender hinzufügen. Er enthält alle Fristen, die du sehen darfst.') ?></small>
</div>
</div>
<?php endif; ?>
<div class="page-body">
<!-- KPIs (über der Filterleiste – siehe STYLEGUIDE Abschnitt 6/8) -->
<div class="kpi-row">
<?php foreach (['overdue','critical','warning','info'] as $sev):
[$label, $icon, $bg] = $kpiMeta[$sev];
?>
<div class="kpi-col">
<div class="small-box <?= $bg ?>">
<div class="inner"><h3><?= (int)($stats[$sev] ?? 0) ?></h3><p><?= $this->e($label) ?></p></div>
<div class="icon"><i class="fas <?= $icon ?>"></i></div>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Filter -->
<div class="card card-outline card-primary mb-3">
<div class="filter-card-body">
<form action="/deadlines" method="GET" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;">
<select name="within" class="form-control form-control-sm" style="width:auto;">
<?php foreach ($horizons as $d => $label): ?>
<option value="<?= (int)$d ?>" <?= $within === (int)$d ? 'selected' : '' ?>><?= $this->te('Zeitraum: {spanne}', ['spanne' => $label]) ?></option>
<?php endforeach; ?>
</select>
<select name="source" class="form-control form-control-sm" style="width:auto;">
<option value=""><?= $this->te('Alle Bereiche') ?></option>
<?php foreach ($sources as $key => $meta): ?>
<option value="<?= $this->e($key) ?>" <?= $source === $key ? 'selected' : '' ?>><?= $this->e($meta['label']) ?></option>
<?php endforeach; ?>
</select>
<label style="display:inline-flex;align-items:center;gap:0.4rem;font-size:0.85rem;cursor:pointer;white-space:nowrap;">
<input type="checkbox" name="hide_overdue" value="1" <?= $hide_overdue ? 'checked' : '' ?>>
<?= $this->te('Überfällige ausblenden') ?>
</label>
<button type="submit" class="btn btn-primary btn-sm"><?= $this->te('Anzeigen') ?></button>
<?php if ($source !== '' || $within !== 90 || $hide_overdue): ?>
<a href="/deadlines?reset=1" class="btn btn-secondary btn-sm"><?= $this->te('Zurücksetzen') ?></a>
<?php endif; ?>
</form>
</div>
</div>
<!-- Liste -->
<?php if (empty($items)): ?>
<div class="empty-state">
<span class="empty-icon">✅</span>
<strong><?= $this->te('Keine Fristen im gewählten Zeitraum.') ?></strong>
</div>
<?php else: ?>
<div class="table-wrapper">
<table class="data-table">
<thead>
<tr>
<th scope="col" style="width:180px;"><?= $this->te('Bereich') ?></th>
<th scope="col"><?= $this->te('Eintrag') ?></th>
<th scope="col" style="width:120px;"><?= $this->te('Fällig am') ?></th>
<th scope="col" style="width:170px;"><?= $this->te('Status') ?></th>
<th scope="col" style="width:190px;"></th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $it):
[$pill, , ] = $sevPill[$it['severity']] ?? ['status-pill--neutral', '', ''];
?>
<tr>
<td style="color:var(--gray-500);font-size:.85rem;white-space:nowrap;"><?= $this->e($it['category']) ?></td>
<td>
<a href="<?= $this->e($it['url']) ?>" class="table-link" style="font-weight:600;"><?= $this->e($it['title']) ?></a>
<?php if ($it['context'] !== ''): ?><span style="color:var(--gray-400);"> · <?= $this->e($it['context']) ?></span><?php endif; ?>
</td>
<td style="color:var(--gray-500);font-size:.85rem;white-space:nowrap;">
<?= $this->e($it['due_date']) ?>
<?php if (($it['postponed_to'] ?? '') !== ''): ?>
<br><small style="color:var(--gray-400);">
→ <?= $this->e(date('d.m.Y', strtotime((string)$it['postponed_to']))) ?>
</small>
<?php endif; ?>
</td>
<td>
<span class="status-pill <?= $pill ?>"><?= $this->e($whenText((int)$it['days_left'])) ?></span>
<?php if (($it['action'] ?? '') !== ''): ?>
<br><small style="color:var(--gray-500);" title="<?= $this->e(trim(
($it['action_note'] ?? '')
. (($it['action_by'] ?? '') !== '' ? ' – ' . $it['action_by'] : '')
)) ?>">
<?= $this->te($it['action'] === 'postponed' ? 'verschoben' : 'quittiert') ?>
<?php if (($it['action_by'] ?? '') !== ''): ?>
<?= $this->te('von {person}', ['person' => $it['action_by']], 'herkunft') ?>
<?php endif; ?>
</small>
<?php endif; ?>
</td>
<td style="white-space:nowrap;text-align:right;">
<?php if (!empty($can_decide)): ?>
<?php if (($it['action'] ?? '') !== ''): ?>
<form method="POST" action="/deadlines/revoke" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" name="source" value="<?= $this->e($it['source']) ?>">
<input type="hidden" name="object_id" value="<?= (int)$it['object_id'] ?>">
<input type="hidden" name="due_date" value="<?= $this->e($it['due_date']) ?>">
<button type="submit" class="btn btn-default btn-xs"><?= $this->te('Zurücknehmen') ?></button>
</form>
<?php else: ?>
<form method="POST" action="/deadlines/acknowledge" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" name="source" value="<?= $this->e($it['source']) ?>">
<input type="hidden" name="object_id" value="<?= (int)$it['object_id'] ?>">
<input type="hidden" name="due_date" value="<?= $this->e($it['due_date']) ?>">
<button type="submit" class="btn btn-default btn-xs" title="<?= $this->te('Zur Kenntnis genommen') ?>"><?= $this->te('Quittieren') ?></button>
</form>
<button type="button" class="btn btn-default btn-xs"
onclick="dlVerschieben(this)"
data-source="<?= $this->e($it['source']) ?>"
data-object="<?= (int)$it['object_id'] ?>"
data-due="<?= $this->e($it['due_date']) ?>"
data-titel="<?= $this->e($it['title']) ?>"><?= $this->te('Verschieben') ?></button>
<?php endif; ?>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
<p style="font-size:.78rem;color:var(--gray-400);margin-top:.75rem;">
<?= $this->te('Modulübergreifend: SSL, Verträge & Kündigungsfristen, Lizenzen, Asset-Garantien, Leihgeräte-Rückgaben, Wartungstermine, ISMS-Reviews und geplante Changes. Es werden nur Fristen aus Bereichen angezeigt, auf die du Zugriff hast. Überfällige bis 180 Tage rückblickend.') ?>
</p>
</div>
</div>
<!-- Frist verschieben: Datum und Begruendung -->
<div class="modal-overlay" id="dl-postpone-modal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="dl-postpone-title">
<div class="modal-card" style="max-width:480px;">
<div class="modal-header">
<h2 id="dl-postpone-title"><i class="fas fa-calendar-day" aria-hidden="true"></i> <?= $this->te('Frist verschieben') ?></h2>
<button type="button" class="modal-close" data-dl-close aria-label="<?= $this->te('Schließen') ?>">×</button>
</div>
<form method="POST" action="/deadlines/postpone">
<input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
<input type="hidden" name="source" id="dl-pp-source">
<input type="hidden" name="object_id" id="dl-pp-object">
<input type="hidden" name="due_date" id="dl-pp-due">
<div class="modal-body">
<p style="margin-top:0;">
<strong id="dl-pp-titel"></strong> <?= $this->te('– ursprünglich fällig am') ?>
<span id="dl-pp-duetext"></span>.
</p>
<div class="form-group">
<label for="dl-pp-date"><?= $this->te('Neues Datum') ?> <span class="text-danger">*</span></label>
<input type="date" name="postponed_to" id="dl-pp-date" class="form-control form-control-sm" required>
<small class="form-text text-muted">
<?= $this->t('Höchstens') ?> <?= $this->te('{tage} Tage in der Zukunft.', ['tage' => (int)($max_postpone ?? 365)]) ?>
</small>
</div>
<div class="form-group">
<label for="dl-pp-note"><?= $this->te('Begründung') ?> <span class="text-danger">*</span></label>
<textarea name="note" id="dl-pp-note" class="form-control form-control-sm" rows="3" required
placeholder="<?= $this->te('Warum wird die Frist verschoben?') ?>"></textarea>
<small class="form-text text-muted">
<?= $this->te('Ein verschobenes Datum ohne Grund ist für eine Prüfung wertlos.') ?>
</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dl-close><?= $this->te('Abbrechen') ?></button>
<button type="submit" class="btn btn-primary"><?= $this->te('Verschieben') ?></button>
</div>
</form>
</div>
</div>
<script>
/**
* Oeffnet den Dialog zum Verschieben und uebernimmt die Frist, an der geklickt
* wurde. Vorbelegt ist eine Woche nach dem urspruenglichen Datum, mindestens
* aber morgen - ein Datum in der Vergangenheit weist der Server ab.
*/
function dlVerschieben(knopf) {
var m = document.getElementById('dl-postpone-modal');
document.getElementById('dl-pp-source').value = knopf.dataset.source;
document.getElementById('dl-pp-object').value = knopf.dataset.object;
document.getElementById('dl-pp-due').value = knopf.dataset.due;
document.getElementById('dl-pp-titel').textContent = knopf.dataset.titel;
document.getElementById('dl-pp-duetext').textContent = knopf.dataset.due;
document.getElementById('dl-pp-note').value = '';
var vorschlag = new Date(knopf.dataset.due + 'T00:00:00');
vorschlag.setDate(vorschlag.getDate() + 7);
var morgen = new Date();
morgen.setDate(morgen.getDate() + 1);
if (vorschlag < morgen) { vorschlag = morgen; }
var feld = document.getElementById('dl-pp-date');
feld.value = vorschlag.toISOString().slice(0, 10);
feld.min = new Date().toISOString().slice(0, 10);
m.style.display = 'flex';
}
(function () {
var m = document.getElementById('dl-postpone-modal');
if (!m) { return; }
function zu() { m.style.display = 'none'; }
m.querySelectorAll('[data-dl-close]').forEach(function (b) { b.addEventListener('click', zu); });
m.addEventListener('click', function (e) { if (e.target === m) { zu(); } });
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && m.style.display !== 'none') { zu(); }
});
})();
</script>