OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

17 KB
<?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&uuml;cksetzen') ?></a>
                    <?php endif; ?>
                </form>
            </div>
        </div>

        <!-- Liste -->
        <?php if (empty($items)): ?>
            <div class="empty-state">
                <span class="empty-icon">&#9989;</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);">
                                &rarr; <?= $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 &amp; 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') ?>">&times;</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('&ndash; 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>