Quelltext des aktuellen Standes
<?php $this->layout('layouts/admin', [
'page_title' => 'Leistungsnachweise',
'current_admin_page' => 'tickets',
]); ?>
<?php
/**
* Einstellungen, Monatslauf und Übersicht der Leistungsnachweise.
* Erwartete Variablen: $settings, $units, $reports, $templates, $last_month.
*/
$s = $settings ?? [];
$statusLabel = [
'draft' => 'Entwurf',
'awaiting_signature' => 'Wartet auf Unterschrift',
'signed' => 'Unterschrieben',
'sent' => 'Versendet',
'failed' => 'Fehlgeschlagen',
'cancelled' => 'Zurückgezogen',
];
$statusClass = [
'draft' => 'status-pill',
'awaiting_signature' => 'status-pill status-pill--warning',
'signed' => 'status-pill status-pill--success',
'sent' => 'status-pill status-pill--info',
'failed' => 'status-pill status-pill--danger',
'cancelled' => 'status-pill',
];
$modeLabel = [
'info' => 'Nur Information',
'signature_link' => 'Unterschrift per Link',
'signature_tablet' => 'Unterschrift am Tablet',
];
$dauer = static fn(int $min): string => intdiv($min, 60) . ':' . str_pad((string)($min % 60), 2, '0', STR_PAD_LEFT) . ' h';
?>
<div class="settings-page">
<section class="settings-card">
<div class="settings-card-header">
<span class="settings-card-icon">📋</span>
<div>
<h2><?= $this->te('Einstellungen') ?></h2>
<p class="settings-card-desc"><?= $this->te('Was beim Abschluss eines Tickets geschieht und wie der Monatslauf arbeitet.') ?></p>
</div>
</div>
<div class="settings-card-body">
<form method="post" action="/admin/tickets/service-reports/settings" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="<?= $this->e($this->csrf()) ?>">
<div class="form-group">
<label class="checkbox-inline">
<input type="checkbox" name="enabled" value="1"<?= !empty($s['enabled']) ? ' checked' : '' ?>>
<?= $this->te('Leistungsnachweise verwenden') ?>
</label>
<small class="form-hint"><?= $this->te('Solange dies aus ist, erscheint beim Schließen eines Tickets keine Abfrage und der Monatslauf bleibt still.') ?></small>
</div>
<hr style="margin:1.25rem 0;border:none;border-top:1px solid var(--gray-200);">
<div class="form-group">
<label for="sr-ticket-mode"><?= $this->te('Beim Abschluss eines Tickets') ?></label>
<select id="sr-ticket-mode" name="ticket_mode" class="form-control">
<option value=""<?= ($s['ticket_mode'] ?? '') === '' ? ' selected' : '' ?>><?= $this->te('Nachfragen (Vorgabe)') ?></option>
<option value="info"<?= ($s['ticket_mode'] ?? '') === 'info' ? ' selected' : '' ?>><?= $this->te('Immer nur zur Information versenden') ?></option>
<option value="signature_link"<?= ($s['ticket_mode'] ?? '') === 'signature_link' ? ' selected' : '' ?>><?= $this->te('Immer mit Link zur Unterschrift versenden') ?></option>
<option value="signature_tablet"<?= ($s['ticket_mode'] ?? '') === 'signature_tablet' ? ' selected' : '' ?>><?= $this->te('Immer am Signatur-Tablet unterschreiben lassen') ?></option>
<option value="none"<?= ($s['ticket_mode'] ?? '') === 'none' ? ' selected' : '' ?>><?= $this->te('Beim Abschluss keinen Nachweis erstellen') ?></option>
</select>
<small class="form-hint"><?= $this->te('Ohne festgelegte Vorgabe fragt das Schließen-Fenster nach – dort lässt sich auch die Empfängeradresse ändern.') ?></small>
</div>
<hr style="margin:1.25rem 0;border:none;border-top:1px solid var(--gray-200);">
<div class="form-group">
<label class="checkbox-inline">
<input type="checkbox" name="monthly_enabled" value="1"<?= !empty($s['monthly_enabled']) ? ' checked' : '' ?>>
<?= $this->te('Monatlichen Sammelnachweis automatisch erstellen') ?>
</label>
</div>
<div class="form-row-2">
<div class="form-group">
<label for="sr-monthly-mode"><?= $this->te('Monatslauf versendet') ?></label>
<select id="sr-monthly-mode" name="monthly_mode" class="form-control">
<option value="info"<?= ($s['monthly_mode'] ?? 'info') === 'info' ? ' selected' : '' ?>><?= $this->te('nur zur Information') ?></option>
<option value="signature_link"<?= ($s['monthly_mode'] ?? '') === 'signature_link' ? ' selected' : '' ?>><?= $this->te('mit Link zur Unterschrift') ?></option>
</select>
</div>
<div class="form-group">
<label for="sr-monthly-day"><?= $this->te('Stichtag im Monat') ?></label>
<input type="number" id="sr-monthly-day" name="monthly_day" class="form-control" min="1" max="28"
value="<?= (int)($s['monthly_day'] ?? 1) ?>">
<small class="form-hint"><?= $this->te('Ab diesem Tag wird der Vormonat abgerechnet.') ?></small>
</div>
</div>
<div class="form-group">
<label for="sr-scope"><?= $this->te('Umfang des Monatsnachweises') ?></label>
<select id="sr-scope" name="monthly_scope" class="form-control">
<option value="time_in_month"<?= ($s['monthly_scope'] ?? '') === 'time_in_month' ? ' selected' : '' ?>><?= $this->te('Alle im Monat erfassten Zeiten – unabhängig vom Ticket-Status') ?></option>
<option value="closed_in_month"<?= ($s['monthly_scope'] ?? '') === 'closed_in_month' ? ' selected' : '' ?>><?= $this->te('Nur im Monat geschlossene Tickets – dafür mit ihrer gesamten Zeit') ?></option>
</select>
</div>
<div class="form-row-2">
<div class="form-group">
<label for="sr-link-days"><?= $this->te('Unterschriften-Link gültig (Tage)') ?></label>
<input type="number" id="sr-link-days" name="link_days" class="form-control" min="1" max="365"
value="<?= (int)($s['link_days'] ?? 30) ?>">
</div>
<div class="form-group">
<label for="sr-prefix"><?= $this->te('Kennzeichen der Nummer') ?></label>
<input type="text" id="sr-prefix" name="number_prefix" class="form-control" maxlength="10"
value="<?= $this->e((string)($s['number_prefix'] ?? 'LN')) ?>">
<small class="form-hint"><?= $this->t('Ergibt Nummern wie <code>{beispiel}</code>.', ['beispiel' => ($s['number_prefix'] ?? 'LN') . '-' . date('Y') . '-0001']) ?></small>
</div>
</div>
<hr style="margin:1.25rem 0;border:none;border-top:1px solid var(--gray-200);">
<h3 style="font-size:1rem;font-weight:700;margin:0 0 .35rem;"><?= $this->te('Aufbau des Dokuments') ?></h3>
<p class="form-hint" style="margin-top:0;">
<?= $this->t('Texte und Anordnung stehen in den <a href="#sr-vorlagen">Vorlagen</a>; hier stehen Logo und Spalten der Positionstabelle.') ?>
</p>
<div class="form-group">
<label for="sr-logo"><?= $this->te('Logo im Dokument') ?></label>
<select id="sr-logo" name="logo" class="form-control">
<option value="app"<?= ($s['logo'] ?? 'app') === 'app' ? ' selected' : '' ?>><?= $this->te('Logo der Installation') ?></option>
<option value="custom"<?= ($s['logo'] ?? '') === 'custom' ? ' selected' : '' ?>><?= $this->te('Eigenes Logo (unten hochladen)') ?></option>
<option value="none"<?= ($s['logo'] ?? '') === 'none' ? ' selected' : '' ?>><?= $this->te('Kein Logo') ?></option>
</select>
<small class="form-hint"><?= $this->t('Eingesetzt wird es über den Platzhalter <code>{{logo}}</code> in der Vorlage.') ?></small>
</div>
<div class="form-row-2">
<div class="form-group">
<label for="sr-logo-file"><?= $this->te('Eigenes Logo hochladen') ?></label>
<input type="file" id="sr-logo-file" name="logo_file" class="form-control" accept="image/png,image/jpeg,image/gif,image/webp">
<?php if (!empty($s['logo_path'])): ?>
<small class="form-hint">
<?= $this->t('Hinterlegt: <code>{pfad}</code>', ['pfad' => $s['logo_path']]) ?>
<label class="checkbox-inline" style="margin-left:.5rem;">
<input type="checkbox" name="logo_remove" value="1"> <?= $this->te('entfernen') ?>
</label>
</small>
<?php endif; ?>
</div>
<div class="form-group">
<label for="sr-logo-height"><?= $this->te('Höhe des Logos (Pixel)') ?></label>
<input type="number" id="sr-logo-height" name="logo_height" class="form-control" min="16" max="200"
value="<?= (int)($s['logo_height'] ?? 48) ?>">
</div>
</div>
<div class="form-group">
<label><?= $this->te('Spalten der Positionstabelle') ?></label>
<div style="display:flex;flex-wrap:wrap;gap:.75rem 1.25rem;">
<?php foreach (($all_columns ?? []) as $key => $titel): ?>
<label class="checkbox-inline">
<input type="checkbox" name="columns[]" value="<?= $this->e($key) ?>"
<?= in_array($key, (array)($s['columns'] ?? []), true) ? 'checked' : '' ?>
<?= $key === 'duration' ? 'checked disabled' : '' ?>>
<?= $this->e($titel) ?>
</label>
<?php endforeach; ?>
</div>
<input type="hidden" name="columns[]" value="duration">
<small class="form-hint"><?= $this->te('Die Dauer trägt den Nachweis und bleibt immer stehen.') ?></small>
</div>
<hr style="margin:1.25rem 0;border:none;border-top:1px solid var(--gray-200);">
<h3 style="font-size:1rem;font-weight:700;margin:0 0 .35rem;"><?= $this->te('Aufwand ausweisen') ?></h3>
<div class="form-group">
<label for="sr-time-mode"><?= $this->te('Darstellung') ?></label>
<select id="sr-time-mode" name="time_mode" class="form-control">
<option value="time"<?= ($s['time_mode'] ?? 'time') === 'time' ? ' selected' : '' ?>><?= $this->te('Gemessene Zeit (z. B. 2:05 h)') ?></option>
<option value="units"<?= ($s['time_mode'] ?? '') === 'units' ? ' selected' : '' ?>><?= $this->te('Zeiteinheiten (z. B. 9)') ?></option>
</select>
</div>
<div class="form-row-2">
<div class="form-group">
<label for="sr-unit-minutes"><?= $this->te('Länge einer Zeiteinheit (Minuten)') ?></label>
<input type="number" id="sr-unit-minutes" name="unit_minutes" class="form-control" min="1" max="480"
value="<?= (int)($s['unit_minutes'] ?? 15) ?>">
</div>
<div class="form-group">
<label for="sr-unit-rounding"><?= $this->te('Angefangene Einheit') ?></label>
<select id="sr-unit-rounding" name="unit_rounding" class="form-control">
<?php foreach (($all_roundings ?? []) as $key => $titel): ?>
<option value="<?= $this->e($key) ?>"<?= ($s['unit_rounding'] ?? 'up') === $key ? ' selected' : '' ?>><?= $this->e($titel) ?></option>
<?php endforeach; ?>
</select>
<small class="form-hint"><?= $this->te('Gerundet wird je Position; die Summen entstehen aus den gerundeten Werten.') ?></small>
</div>
</div>
<div class="form-group">
<label for="sr-unit-label"><?= $this->te('Beschriftung der Spalte') ?></label>
<input type="text" id="sr-unit-label" name="unit_label" class="form-control" maxlength="40"
value="<?= $this->e((string)($s['unit_label'] ?? '')) ?>" placeholder="<?= $this->te('Einheiten') ?>">
</div>
<hr style="margin:1.25rem 0;border:none;border-top:1px solid var(--gray-200);">
<h3 style="font-size:1rem;font-weight:700;margin:0 0 .35rem;"><?= $this->te('Absender') ?></h3>
<p class="form-hint" style="margin-top:0;">
<?= $this->te('Steht als einzeilige Rücksendeangabe über dem Anschriftenfeld und in der Fußzeile des Briefes. Was leer bleibt, entfällt.') ?>
</p>
<div class="form-group">
<label for="sr-sender-company"><?= $this->te('Firma') ?></label>
<input type="text" id="sr-sender-company" name="sender_company" class="form-control" maxlength="255"
value="<?= $this->e((string)($s['sender']['company'] ?? '')) ?>">
</div>
<div class="form-row-2">
<div class="form-group">
<label for="sr-sender-street"><?= $this->te('Straße und Hausnummer') ?></label>
<input type="text" id="sr-sender-street" name="sender_street" class="form-control" maxlength="255"
value="<?= $this->e((string)($s['sender']['street'] ?? '')) ?>">
</div>
<div class="form-group">
<label for="sr-sender-city"><?= $this->te('PLZ und Ort') ?></label>
<div style="display:flex;gap:.5rem;">
<input type="text" id="sr-sender-zip" name="sender_zip" class="form-control" maxlength="20" style="width:7rem;"
value="<?= $this->e((string)($s['sender']['zip'] ?? '')) ?>" aria-label="<?= $this->te('PLZ') ?>">
<input type="text" id="sr-sender-city" name="sender_city" class="form-control" maxlength="255"
value="<?= $this->e((string)($s['sender']['city'] ?? '')) ?>">
</div>
</div>
</div>
<div class="form-row-2">
<div class="form-group">
<label for="sr-sender-phone"><?= $this->te('Telefon') ?></label>
<input type="text" id="sr-sender-phone" name="sender_phone" class="form-control" maxlength="255"
value="<?= $this->e((string)($s['sender']['phone'] ?? '')) ?>">
</div>
<div class="form-group">
<label for="sr-sender-email"><?= $this->te('E-Mail') ?></label>
<input type="text" id="sr-sender-email" name="sender_email" class="form-control" maxlength="255"
value="<?= $this->e((string)($s['sender']['email'] ?? '')) ?>">
</div>
</div>
<div class="form-group">
<label for="sr-sender-web"><?= $this->te('Web') ?></label>
<input type="text" id="sr-sender-web" name="sender_web" class="form-control" maxlength="255"
value="<?= $this->e((string)($s['sender']['web'] ?? '')) ?>">
</div>
<div class="form-row-2">
<div class="form-group">
<label for="sr-contact-mode"><?= $this->te('Ansprechpartner im Brief') ?></label>
<select id="sr-contact-mode" name="contact_mode" class="form-control">
<option value="assignee"<?= ($s['contact_mode'] ?? 'assignee') === 'assignee' ? ' selected' : '' ?>><?= $this->te('Bearbeiter des Tickets') ?></option>
<option value="fixed"<?= ($s['contact_mode'] ?? '') === 'fixed' ? ' selected' : '' ?>><?= $this->te('Fest hinterlegt') ?></option>
</select>
<small class="form-hint"><?= $this->te('Beim Monatsnachweis der Techniker mit dem größten Anteil.') ?></small>
</div>
<div class="form-group">
<label for="sr-contact-name"><?= $this->te('Fester Ansprechpartner') ?></label>
<input type="text" id="sr-contact-name" name="contact_name" class="form-control" maxlength="120"
value="<?= $this->e((string)($s['contact_name'] ?? '')) ?>">
<small class="form-hint"><?= $this->te('Gilt auch als Rückfall, wenn kein Bearbeiter zu ermitteln ist.') ?></small>
</div>
</div>
<button type="submit" class="btn btn-primary"><?= $this->te('Speichern') ?></button>
</form>
</div>
</section>
<section class="settings-card">
<div class="settings-card-header">
<span class="settings-card-icon">📅</span>
<div>
<h2><?= $this->te('Monatslauf von Hand starten') ?></h2>
<p class="settings-card-desc"><?= $this->te('Erstellt die Nachweise eines Monats für alle Standorte, die noch keinen haben. Ohne Versand entstehen Entwürfe, die sich vorher ansehen lassen.') ?></p>
</div>
</div>
<div class="settings-card-body">
<form method="post" action="/admin/tickets/service-reports/run" style="display:flex;gap:0.75rem;align-items:flex-end;flex-wrap:wrap;">
<input type="hidden" name="_csrf" value="<?= $this->e($this->csrf()) ?>">
<div class="form-group" style="margin:0;">
<label for="sr-month"><?= $this->te('Monat') ?></label>
<input type="month" id="sr-month" name="month" class="form-control" value="<?= $this->e((string)$last_month) ?>">
</div>
<label class="checkbox-inline" style="margin-bottom:0.4rem;">
<input type="checkbox" name="dispatch" value="1" checked>
<?= $this->te('gleich versenden') ?>
</label>
<button type="submit" class="btn btn-default"><?= $this->te('Nachweise erstellen') ?></button>
</form>
</div>
</section>
<section class="settings-card" id="sr-vorlagen">
<div class="settings-card-header">
<span class="settings-card-icon">✏</span>
<div>
<h2><?= $this->te('Vorlagen') ?></h2>
<p class="settings-card-desc"><?= $this->t('Aufbau und Texte der Dokumente. Die Anschreiben stehen bei den <a href="/admin/tickets/templates">Ticket-E-Mails</a> – dort mit demselben Editor und denselben Platzhaltern wie jede andere Ticket-Mail.') ?></p>
</div>
</div>
<div class="settings-card-body">
<div class="table-wrapper">
<table class="data-table">
<thead><tr>
<th scope="col"><?= $this->te('Vorlage') ?></th>
<th scope="col"><?= $this->te('Zuletzt geändert') ?></th>
<th scope="col"></th>
</tr></thead>
<tbody>
<?php foreach (($templates ?? []) as $tpl): ?>
<tr>
<td><?= $this->e((string)$tpl['name']) ?></td>
<td><?= $this->e(date('d.m.Y H:i', strtotime((string)$tpl['updated_at']))) ?></td>
<td style="text-align:right;">
<a class="btn btn-default btn-sm" href="/admin/tickets/service-reports/template/<?= $this->e((string)$tpl['template_key']) ?>"><?= $this->te('Bearbeiten') ?></a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</section>
<section class="settings-card">
<div class="settings-card-header">
<span class="settings-card-icon">🏢</span>
<div>
<h2><?= $this->te('Empfangende Standorte') ?></h2>
<p class="settings-card-desc"><?= $this->t('Ein Standort empfängt Nachweise, sobald unter <a href="/admin/locations">Standorte</a> eine Adresse hinterlegt ist. Unterstandorte ohne eigene Adresse werden dem nächsthöheren Standort mit Adresse zugerechnet.') ?></p>
</div>
</div>
<div class="settings-card-body">
<?php if (empty($units)): ?>
<div class="empty-state"><?= $this->te('Noch kein Standort mit E-Mail-Adresse – es kann nichts versendet werden.') ?></div>
<?php else: ?>
<div class="table-wrapper">
<table class="data-table">
<thead><tr>
<th scope="col"><?= $this->te('Standort') ?></th>
<th scope="col"><?= $this->te('Adresse') ?></th>
<th scope="col"><?= $this->te('Zugerechnete Standorte') ?></th>
<th scope="col"><?= $this->te('Monatslauf') ?></th>
</tr></thead>
<tbody>
<?php foreach ($units as $unit): ?>
<tr>
<td><strong><?= $this->e((string)$unit['location']['name']) ?></strong></td>
<td><?= $this->e((string)$unit['location']['report_email']) ?></td>
<td><?= count($unit['members']) ?></td>
<td>
<?php if ((int)($unit['location']['report_monthly'] ?? 1) === 1): ?>
<span class="status-pill status-pill--success"><?= $this->te('ja') ?></span>
<?php else: ?>
<span class="status-pill"><?= $this->te('nein') ?></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</section>
<section class="settings-card">
<div class="settings-card-header">
<span class="settings-card-icon">📄</span>
<div>
<h2><?= $this->te('Zuletzt erstellt') ?></h2>
<p class="settings-card-desc"><?= $this->te('Die 50 jüngsten Leistungsnachweise.') ?></p>
</div>
</div>
<div class="settings-card-body">
<?php if (empty($reports)): ?>
<div class="empty-state"><?= $this->te('Noch kein Leistungsnachweis erstellt.') ?></div>
<?php else: ?>
<div class="table-wrapper">
<table class="data-table">
<thead><tr>
<th scope="col"><?= $this->te('Nummer') ?></th>
<th scope="col"><?= $this->te('Art') ?></th>
<th scope="col"><?= $this->te('Standort') ?></th>
<th scope="col"><?= $this->te('Zeitraum') ?></th>
<th scope="col" style="text-align:right;"><?= $this->te('Aufwand') ?></th>
<th scope="col"><?= $this->te('Versand') ?></th>
<th scope="col"><?= $this->te('Status') ?></th>
<th scope="col"></th>
</tr></thead>
<tbody>
<?php foreach ($reports as $r): ?>
<tr>
<td><a class="table-link" href="/admin/tickets/service-reports/<?= (int)$r['id'] ?>/preview"><?= $this->e((string)$r['number']) ?></a></td>
<td>
<?php if ((string)$r['kind'] === 'monthly'): ?>
<?= $this->te('Monat') ?>
<?php else: ?>
<?= $this->t('Ticket #{id}', ['id' => (int)$r['ticket_id']]) ?>
<?php endif; ?>
</td>
<td><?= $this->e((string)($r['location_name'] ?? '—')) ?></td>
<td><?= $this->e(date('d.m.Y', strtotime((string)$r['period_start']))) ?> – <?= $this->e(date('d.m.Y', strtotime((string)$r['period_end']))) ?></td>
<td style="text-align:right;"><?= $this->e($dauer((int)$r['total_minutes'])) ?></td>
<td><?= $this->e($modeLabel[(string)$r['mode']] ?? (string)$r['mode']) ?></td>
<td><span class="<?= $statusClass[(string)$r['status']] ?? 'status-pill' ?>"><?= $this->e($statusLabel[(string)$r['status']] ?? (string)$r['status']) ?></span></td>
<td style="text-align:right;white-space:nowrap;">
<a class="btn btn-default btn-sm" href="/service-reports/<?= (int)$r['id'] ?>/pdf" target="_blank" rel="noopener"><?= $this->te('PDF') ?></a>
<?php if (in_array((string)$r['status'], ['draft', 'failed', 'awaiting_signature'], true)): ?>
<form method="post" action="/service-reports/<?= (int)$r['id'] ?>/send" style="display:inline;">
<input type="hidden" name="_csrf" value="<?= $this->e($this->csrf()) ?>">
<button class="btn btn-default btn-sm"><?= $this->te('Erneut senden') ?></button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</section>
</div>