OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

sign.php

7 KB
<?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>