Quelltext des aktuellen Standes
<?php
/**
* Öffentliche Seite zum Unterschreiben eines Leistungsnachweises.
*
* Standalone-Seite ohne Anmeldung – sie ist die Außenseite für Empfänger.
* Erwartete Variablen: $report, $document, $token.
*/
$csrf = $this->csrf();
?><!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title><?= $this->e($page_title ?? 'Leistungsnachweis') ?></title>
<link rel="stylesheet" href="/assets/css/fonts.css">
<link rel="stylesheet" href="/assets/css/tokens.css">
<link rel="stylesheet" href="/assets/css/app.css">
<style>
:root { color-scheme: light; }
body { padding: 2rem 1rem; }
.sr-wrap { max-width: 880px; margin: 0 auto; }
.sr-card {
background: var(--color-bg); border-radius: 0.85rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.08);
padding: 1.75rem 2rem; margin-bottom: 1.25rem;
}
.sr-doc { font-size: 0.9rem; overflow-x: auto; }
.sr-doc table { width: 100%; border-collapse: collapse; margin-top: 0.75rem; }
.sr-doc th { background: var(--gray-700); color: var(--color-bg); font-size: 0.78rem; padding: 0.4rem 0.5rem; text-align: left; }
.sr-doc td { font-size: 0.8rem; padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--gray-200); }
.sr-doc tr.sr-group td { background: var(--gray-100); font-weight: 700; }
.sr-doc tr.sr-sum td { background: var(--blue-50); font-weight: 700; }
.sr-doc .sr-num { text-align: right; white-space: nowrap; }
.sr-doc .sr-sign { display: none; }
.sr-pad {
position: relative; border: 2px dashed var(--gray-300); border-radius: 0.6rem;
height: 190px; background: var(--color-bg); touch-action: none;
}
.sr-pad.active { border-style: solid; border-color: var(--color-primary); }
.sr-pad canvas { display: block; width: 100%; height: 100%; touch-action: none; }
.sr-pad-hint {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
color: var(--gray-400); font-size: 0.9rem; pointer-events: none;
}
.sr-actions { display: flex; gap: 0.75rem; align-items: center; margin-top: 1rem; flex-wrap: wrap; }
.sr-meta { color: var(--gray-500); font-size: 0.82rem; }
</style>
</head>
<body>
<div class="sr-wrap">
<div class="sr-card">
<div class="sr-doc"><?= $document ?></div>
</div>
<form class="sr-card" method="POST" action="/leistungsnachweis/<?= $this->e($token) ?>" id="srForm">
<input type="hidden" name="_csrf" value="<?= $this->e($csrf) ?>">
<input type="hidden" name="signature" id="srSignature">
<h2 style="font-size:1.05rem;margin:0 0 0.75rem;"><?= $this->te('Leistungen bestätigen') ?></h2>
<p class="sr-meta" style="margin-bottom:1rem;">
<?= $this->te('Bitte unterschreiben Sie im Feld unten. Mit dem Absenden bestätigen Sie die aufgeführten Leistungen; eine Ausfertigung geht anschließend per E-Mail an Sie.') ?>
</p>
<div class="form-group">
<label class="form-label" for="srName"><?= $this->te('Name in Druckbuchstaben') ?></label>
<input type="text" class="form-control" id="srName" name="signer_name" maxlength="120"
autocomplete="name" placeholder="<?= $this->te('Vor- und Nachname') ?>">
</div>
<div class="sr-pad" id="srPad">
<canvas id="srCanvas"></canvas>
<div class="sr-pad-hint" id="srHint"><?= $this->te('Hier unterschreiben') ?></div>
</div>
<div class="sr-actions">
<button type="button" class="btn btn-default" id="srClear"><?= $this->te('Neu zeichnen') ?></button>
<button type="submit" class="btn btn-primary" id="srSubmit" disabled><?= $this->te('Unterschrift absenden') ?></button>
<span class="sr-meta"><?= $this->t('Gültig bis {datum}', ['datum' => date('d.m.Y', strtotime((string)$report['signature_expires_at']))]) ?></span>
</div>
</form>
</div>
<script>
(function () {
'use strict';
var pad = document.getElementById('srPad');
var canvas = document.getElementById('srCanvas');
var ctx = canvas.getContext('2d');
var hint = document.getElementById('srHint');
var submit = document.getElementById('srSubmit');
var zeichnet = false, gezeichnet = false;
function groesse() {
var rect = pad.getBoundingClientRect();
if (!rect.width || !rect.height) { return; }
var dpr = window.devicePixelRatio || 1;
var alt = gezeichnet ? canvas.toDataURL() : null;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
ctx.strokeStyle = '#0f172a';
ctx.lineWidth = 2.4;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
if (alt) {
var bild = new Image();
bild.onload = function () { ctx.drawImage(bild, 0, 0, rect.width, rect.height); };
bild.src = alt;
}
}
function punkt(e) {
var rect = canvas.getBoundingClientRect();
var q = e.touches ? e.touches[0] : e;
return { x: q.clientX - rect.left, y: q.clientY - rect.top };
}
function start(e) {
e.preventDefault();
zeichnet = true;
var p = punkt(e);
ctx.beginPath();
ctx.moveTo(p.x, p.y);
if (!gezeichnet) {
gezeichnet = true;
hint.style.display = 'none';
pad.classList.add('active');
submit.disabled = false;
}
}
function bewegen(e) {
if (!zeichnet) { return; }
e.preventDefault();
var p = punkt(e);
ctx.lineTo(p.x, p.y);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(p.x, p.y);
}
function ende(e) { if (zeichnet) { e.preventDefault(); } zeichnet = false; }
['mousedown', 'touchstart'].forEach(function (n) { canvas.addEventListener(n, start, { passive: false }); });
['mousemove', 'touchmove'].forEach(function (n) { canvas.addEventListener(n, bewegen, { passive: false }); });
['mouseup', 'mouseleave', 'touchend'].forEach(function (n) { canvas.addEventListener(n, ende, { passive: false }); });
document.getElementById('srClear').addEventListener('click', function () {
var rect = pad.getBoundingClientRect();
ctx.clearRect(0, 0, rect.width, rect.height);
gezeichnet = false;
hint.style.display = '';
pad.classList.remove('active');
submit.disabled = true;
});
document.getElementById('srForm').addEventListener('submit', function (e) {
if (!gezeichnet) { e.preventDefault(); return; }
document.getElementById('srSignature').value = canvas.toDataURL('image/png');
submit.disabled = true;
});
window.addEventListener('resize', groesse);
groesse();
}());
</script>
</body>
</html>