Bladeren bron

adding form generation for minor-parent signature

Medowar 1 week geleden
bovenliggende
commit
d65cf215e3
6 gewijzigde bestanden met toevoegingen van 249 en 23 verwijderingen
  1. 5 6
      admin/application.php
  2. 21 0
      api/submit.php
  3. 84 11
      src/mail/mailer.php
  4. 126 0
      src/mail/pdfgenerator.php
  5. 12 6
      src/mail/submissionformatter.php
  6. 1 0
      src/storage/jsonstore.php

+ 5 - 6
admin/application.php

@@ -72,6 +72,11 @@ $csrf = Csrf::token();
                         <button type="submit" class="btn btn-small">Alle Uploads als ZIP herunterladen</button>
                     </form>
                 <?php endif; ?>
+                <form method="post" action="<?= htmlspecialchars(Bootstrap::url('admin/delete.php')) ?>" onsubmit="return confirm('Antrag wirklich löschen? Der Antrag wird für alle Benutzer unwiederbringlich entfernt.');">
+                    <input type="hidden" name="csrf" value="<?= htmlspecialchars($csrf) ?>">
+                    <input type="hidden" name="id" value="<?= htmlspecialchars((string) ($submission['application_key'] ?? '')) ?>">
+                    <button type="submit" class="btn btn-small">Antrag löschen</button>
+                </form>
             </div>
         </div>
 
@@ -161,12 +166,6 @@ $csrf = Csrf::token();
             <?php endforeach; ?>
         <?php endif; ?>
 
-        <h2>Löschen</h2>
-        <form method="post" action="<?= htmlspecialchars(Bootstrap::url('admin/delete.php')) ?>" onsubmit="return confirm('Antrag wirklich löschen? Der Antrag wird für alle Benutzer unwiederbringlich entfernt.');">
-            <input type="hidden" name="csrf" value="<?= htmlspecialchars($csrf) ?>">
-            <input type="hidden" name="id" value="<?= htmlspecialchars((string) ($submission['application_key'] ?? '')) ?>">
-            <button type="submit" class="btn">Antrag löschen</button>
-        </form>
     </section>
 </main>
 </body>

+ 21 - 0
api/submit.php

@@ -30,6 +30,26 @@ function resolveSubmitSuccessMessage(array $app): string
     return trim($message);
 }
 
+function isMinorBirthdate(string $birthdate): bool
+{
+    $birthdate = trim($birthdate);
+    if ($birthdate === '') {
+        return false;
+    }
+
+    $date = DateTimeImmutable::createFromFormat('!Y-m-d', $birthdate);
+    if (!$date || $date->format('Y-m-d') !== $birthdate) {
+        return false;
+    }
+
+    $today = new DateTimeImmutable('today');
+    if ($date > $today) {
+        return false;
+    }
+
+    return $date->diff($today)->y < 18;
+}
+
 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
     Bootstrap::jsonResponse([
         'ok' => false,
@@ -158,6 +178,7 @@ try {
             'step' => 4,
             'form_data' => $mergedFormData,
             'uploads' => $mergedUploads,
+            'is_minor_submission' => isMinorBirthdate((string) ($mergedFormData['geburtsdatum'] ?? '')),
         ]);
 
         return [

+ 84 - 11
src/mail/mailer.php

@@ -61,10 +61,12 @@ final class Mailer
         $formDataPdf = $this->pdfGenerator->generateFormDataPdf($submission);
         $attachmentsPdf = $this->pdfGenerator->generateAttachmentsPdf($submission);
         $pdfAttachments = $this->pdfGenerator->collectPdfAttachments($submission);
+        $minorSignaturePdf = $this->pdfGenerator->generateMinorSignaturePdf($submission);
+        $isMinorSubmission = $this->isMinorSubmission($submission);
 
         try {
-            $this->sendAdminMails($submission, $formDataPdf, $attachmentsPdf, $pdfAttachments);
-            $this->sendApplicantMail($submission);
+            $this->sendAdminMails($submission, $formDataPdf, $attachmentsPdf, $pdfAttachments, $isMinorSubmission);
+            $this->sendApplicantMail($submission, $minorSignaturePdf);
         } finally {
             if ($formDataPdf !== null) {
                 @unlink($formDataPdf);
@@ -72,6 +74,9 @@ final class Mailer
             if ($attachmentsPdf !== null) {
                 @unlink($attachmentsPdf);
             }
+            if ($minorSignaturePdf !== null) {
+                @unlink($minorSignaturePdf);
+            }
         }
     }
 
@@ -84,12 +89,13 @@ final class Mailer
         ?string $formDataPdf,
         ?string $attachmentsPdf,
         array $pdfAttachments,
+        bool $isMinorSubmission,
     ): void {
         $recipients = (array) ($this->mailConfig['recipients'] ?? []);
         $subject = (string) ($this->mailConfig['subjects']['admin'] ?? 'Neuer Mitgliedsantrag');
 
-        $htmlBody = $this->renderAdminHtml($submission);
-        $textBody = $this->renderAdminText($submission);
+        $htmlBody = $this->renderAdminHtml($submission, $isMinorSubmission);
+        $textBody = $this->renderAdminText($submission, $isMinorSubmission);
 
         foreach ($recipients as $recipient) {
             if (!is_string($recipient) || $recipient === '') {
@@ -123,16 +129,17 @@ final class Mailer
     }
 
     /** @param array<string, mixed> $submission */
-    private function sendApplicantMail(array $submission): void
+    private function sendApplicantMail(array $submission, ?string $minorSignaturePdf): void
     {
         $email = (string) ($submission['email'] ?? '');
         if ($email === '') {
             return;
         }
 
+        $isMinorSubmission = $this->isMinorSubmission($submission);
         $subject = (string) ($this->mailConfig['subjects']['applicant'] ?? 'Bestätigung Mitgliedsantrag');
-        $htmlBody = $this->renderApplicantHtml($submission);
-        $textBody = $this->renderApplicantText($submission);
+        $htmlBody = $this->renderApplicantHtml($submission, $isMinorSubmission);
+        $textBody = $this->renderApplicantText($submission, $isMinorSubmission);
 
         try {
             $mail = $this->createMailBuilder();
@@ -141,6 +148,10 @@ final class Mailer
                 ->setHtmlBody($htmlBody)
                 ->setTextBody($textBody);
 
+            if ($minorSignaturePdf !== null && is_file($minorSignaturePdf)) {
+                $mail->addAttachment($minorSignaturePdf, 'Einverstaendniserklaerung-Minderjaehrige.pdf', 'application/pdf');
+            }
+
             if (!$mail->send()) {
                 Bootstrap::log('mail', 'Versand an Antragsteller fehlgeschlagen: ' . $email . ' - ' . $mail->getErrorInfo());
             }
@@ -166,7 +177,7 @@ final class Mailer
     // ---------------------------------------------------------------
 
     /** @param array<string, mixed> $submission */
-    private function renderAdminHtml(array $submission): string
+    private function renderAdminHtml(array $submission, bool $isMinorSubmission): string
     {
         $steps = $this->formatter->formatSteps($submission);
         $uploads = $this->formatter->formatUploads($submission);
@@ -175,6 +186,9 @@ final class Mailer
         $h .= '<h2 style="color:#c0392b">Neuer Mitgliedsantrag</h2>';
         $h .= '<p>Eingereicht: ' . $this->esc($this->formatTimestamp($submission)) . '<br>';
         $h .= 'E-Mail: ' . $this->esc((string) ($submission['email'] ?? '')) . '</p>';
+        if ($isMinorSubmission) {
+            $h .= $this->renderMinorAdminNoticeHtml();
+        }
 
         foreach ($steps as $step) {
             $h .= '<h3 style="border-bottom:2px solid #c0392b;padding-bottom:4px">' . $this->esc($step['title']) . '</h3>';
@@ -203,7 +217,7 @@ final class Mailer
     }
 
     /** @param array<string, mixed> $submission */
-    private function renderAdminText(array $submission): string
+    private function renderAdminText(array $submission, bool $isMinorSubmission): string
     {
         $steps = $this->formatter->formatSteps($submission);
         $uploads = $this->formatter->formatUploads($submission);
@@ -211,6 +225,9 @@ final class Mailer
         $t = "NEUER MITGLIEDSANTRAG\n";
         $t .= 'Eingereicht: ' . $this->formatTimestamp($submission) . "\n";
         $t .= 'E-Mail: ' . (string) ($submission['email'] ?? '') . "\n\n";
+        if ($isMinorSubmission) {
+            $t .= $this->renderMinorAdminNoticeText() . "\n\n";
+        }
 
         foreach ($steps as $step) {
             $t .= strtoupper($step['title']) . "\n" . str_repeat('-', 40) . "\n";
@@ -237,7 +254,7 @@ final class Mailer
     // ---------------------------------------------------------------
 
     /** @param array<string, mixed> $submission */
-    private function renderApplicantHtml(array $submission): string
+    private function renderApplicantHtml(array $submission, bool $isMinorSubmission): string
     {
         $steps = $this->formatter->formatSteps($submission);
         $uploads = $this->formatter->formatUploads($submission);
@@ -245,6 +262,11 @@ final class Mailer
         $h = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:Arial,sans-serif;color:#333;max-width:700px;margin:0 auto">';
         $h .= '<h2 style="color:#c0392b">Vielen Dank für Ihren Mitgliedsantrag!</h2>';
         $h .= '<p>Ihre Daten wurden erfolgreich übermittelt. Nachfolgend finden Sie eine Zusammenfassung.</p>';
+        if ($isMinorSubmission) {
+            $h .= '<p><strong>Hinweis für Minderjährige:</strong> '
+                . 'Das angehängte Formular ist auszudrucken, von Antragsteller/in und Erziehungsberechtigten zu unterschreiben '
+                . 'und persönlich einzureichen.</p>';
+        }
 
         foreach ($steps as $step) {
             $h .= '<h3>' . $this->esc($step['title']) . '</h3>';
@@ -274,13 +296,17 @@ final class Mailer
     }
 
     /** @param array<string, mixed> $submission */
-    private function renderApplicantText(array $submission): string
+    private function renderApplicantText(array $submission, bool $isMinorSubmission): string
     {
         $steps = $this->formatter->formatSteps($submission);
         $uploads = $this->formatter->formatUploads($submission);
 
         $t = "Vielen Dank für Ihren Mitgliedsantrag!\n\n";
         $t .= "Ihre Daten wurden erfolgreich übermittelt. Nachfolgend finden Sie eine Zusammenfassung.\n\n";
+        if ($isMinorSubmission) {
+            $t .= "Hinweis für Minderjährige: Das angehängte Formular ist auszudrucken, von Antragsteller/in und "
+                . "Erziehungsberechtigten zu unterschreiben und persönlich einzureichen.\n\n";
+        }
 
         foreach ($steps as $step) {
             $t .= strtoupper($step['title']) . "\n";
@@ -321,6 +347,53 @@ final class Mailer
         return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
     }
 
+    private function renderMinorAdminNoticeHtml(): string
+    {
+        return '<div style="background:#fff3cd;border:1px solid #f0c36d;padding:10px 12px;margin:12px 0">'
+            . '<strong>Wichtiger Hinweis (Minderjaehrig):</strong> '
+            . 'Die Unterschrift eines Erziehungsberechtigten liegt digital noch nicht vor. '
+            . 'Bitte die Bearbeitung erst nach Eingang des handschriftlich unterschriebenen Formulars fortsetzen.'
+            . '</div>';
+    }
+
+    private function renderMinorAdminNoticeText(): string
+    {
+        return "WICHTIGER HINWEIS (MINDERJAEHRIG)\n"
+            . 'Die Unterschrift eines Erziehungsberechtigten liegt digital noch nicht vor. '
+            . 'Bitte die Bearbeitung erst nach Eingang des handschriftlich unterschriebenen Formulars fortsetzen.';
+    }
+
+    /** @param array<string, mixed> $submission */
+    private function isMinorSubmission(array $submission): bool
+    {
+        if (array_key_exists('is_minor_submission', $submission)) {
+            return (bool) $submission['is_minor_submission'];
+        }
+
+        $formData = (array) ($submission['form_data'] ?? []);
+        return $this->deriveIsMinorFromBirthdate((string) ($formData['geburtsdatum'] ?? ''));
+    }
+
+    private function deriveIsMinorFromBirthdate(string $birthdate): bool
+    {
+        $birthdate = trim($birthdate);
+        if ($birthdate === '') {
+            return false;
+        }
+
+        $date = \DateTimeImmutable::createFromFormat('!Y-m-d', $birthdate);
+        if (!$date || $date->format('Y-m-d') !== $birthdate) {
+            return false;
+        }
+
+        $today = new \DateTimeImmutable('today');
+        if ($date > $today) {
+            return false;
+        }
+
+        return $date->diff($today)->y < 18;
+    }
+
     private function renderOtpText(string $code, int $ttlSeconds): string
     {
         $configured = (string) ($this->mailConfig['otp']['text_template'] ?? '');

+ 126 - 0
src/mail/pdfgenerator.php

@@ -80,6 +80,71 @@ final class PdfGenerator
         }
     }
 
+    /**
+     * Generates a printable consent/signature document for minors.
+     * Returns null for adult submissions.
+     *
+     * @param array<string, mixed> $submission
+     */
+    public function generateMinorSignaturePdf(array $submission): ?string
+    {
+        if (!$this->isMinorSubmission($submission)) {
+            return null;
+        }
+
+        try {
+            $pdf = $this->createPdf();
+            $pdf->SetTitle($this->enc('Einverstaendniserklaerung Minderjaehrige'), true);
+            $pdf->AddPage();
+
+            $pdf->SetFont('Helvetica', 'B', 15);
+            $pdf->Cell(0, 10, $this->enc('Einverstaendniserklaerung fuer Minderjaehrige'), 0, 1);
+            $pdf->SetFont('Helvetica', '', 9);
+            $pdf->Cell(0, 5, $this->enc('Eingereicht: ' . $this->formatTimestamp($submission)), 0, 1);
+            $pdf->Cell(0, 5, $this->enc('E-Mail: ' . (string) ($submission['email'] ?? '')), 0, 1);
+            $pdf->Ln(3);
+
+            $pdf->SetFont('Helvetica', '', 10);
+            $pdf->MultiCell(
+                0,
+                5,
+                $this->enc('Dieses Dokument ist auszudrucken, handschriftlich zu unterschreiben und persoenlich einzureichen.')
+            );
+            $pdf->Ln(2);
+
+            $steps = $this->formatter->formatSteps($submission);
+            foreach ($steps as $step) {
+                $this->renderStepSection($pdf, $step);
+            }
+
+            $uploads = $this->formatter->formatUploads($submission);
+            if ($uploads !== []) {
+                $this->ensureSpace($pdf, 20);
+                $pdf->SetFont('Helvetica', 'B', 12);
+                $pdf->Cell(0, 8, $this->enc('Hochgeladene Dateien'), 0, 1);
+                $pdf->SetFont('Helvetica', '', 10);
+                foreach ($uploads as $group) {
+                    $pdf->SetFont('Helvetica', 'B', 10);
+                    $pdf->Cell(0, 6, $this->enc($group['label'] . ':'), 0, 1);
+                    $pdf->SetFont('Helvetica', '', 10);
+                    foreach ($group['files'] as $name) {
+                        $pdf->Cell(5);
+                        $pdf->Cell(0, 5, $this->enc('- ' . $name), 0, 1);
+                    }
+                }
+            }
+
+            $this->renderMinorSignatureSection($pdf);
+
+            $tmpPath = $this->tempPath('minderjaehrige_einverstaendnis');
+            $pdf->Output('F', $tmpPath);
+            return $tmpPath;
+        } catch (\Throwable $e) {
+            Bootstrap::log('mail', 'PDF-Erstellung (Minderjaehrigen-Erklaerung) fehlgeschlagen: ' . $e->getMessage());
+            return null;
+        }
+    }
+
     /**
      * Compiles all non-portrait image uploads into a single PDF.
      * Returns null if there are no image uploads.
@@ -240,6 +305,36 @@ final class PdfGenerator
         }
     }
 
+    private function renderMinorSignatureSection(\FPDF $pdf): void
+    {
+        $this->ensureSpace($pdf, 55);
+        $pdf->Ln(6);
+        $pdf->SetFont('Helvetica', 'B', 11);
+        $pdf->Cell(0, 8, $this->enc('Unterschriften'), 0, 1);
+        $pdf->SetFont('Helvetica', '', 10);
+        $pdf->MultiCell(
+            0,
+            5,
+            $this->enc('Hiermit bestaetigen Antragsteller/in und Erziehungsberechtigte/r die Richtigkeit der oben aufgefuehrten Angaben.')
+        );
+        $pdf->Ln(10);
+
+        $lineWidth = 80.0;
+        $leftX = self::MARGIN;
+        $rightX = $pdf->GetPageWidth() - self::MARGIN - $lineWidth;
+        $lineY = $pdf->GetY();
+
+        $pdf->Line($leftX, $lineY, $leftX + $lineWidth, $lineY);
+        $pdf->Line($rightX, $lineY, $rightX + $lineWidth, $lineY);
+
+        $pdf->SetY($lineY + 2);
+        $pdf->SetFont('Helvetica', '', 9);
+        $pdf->SetX($leftX);
+        $pdf->Cell($lineWidth, 5, $this->enc('Antragsteller/in (minderjaehrig)'), 0, 0);
+        $pdf->SetX($rightX);
+        $pdf->Cell($lineWidth, 5, $this->enc('Erziehungsberechtigte/r (Eltern)'), 0, 1);
+    }
+
     private function embedImage(\FPDF $pdf, string $path): void
     {
         $size = @getimagesize($path);
@@ -355,6 +450,37 @@ final class PdfGenerator
     // Utilities
     // ---------------------------------------------------------------
 
+    /** @param array<string, mixed> $submission */
+    private function isMinorSubmission(array $submission): bool
+    {
+        if (array_key_exists('is_minor_submission', $submission)) {
+            return (bool) $submission['is_minor_submission'];
+        }
+
+        $formData = (array) ($submission['form_data'] ?? []);
+        return $this->deriveIsMinorFromBirthdate((string) ($formData['geburtsdatum'] ?? ''));
+    }
+
+    private function deriveIsMinorFromBirthdate(string $birthdate): bool
+    {
+        $birthdate = trim($birthdate);
+        if ($birthdate === '') {
+            return false;
+        }
+
+        $date = \DateTimeImmutable::createFromFormat('!Y-m-d', $birthdate);
+        if (!$date || $date->format('Y-m-d') !== $birthdate) {
+            return false;
+        }
+
+        $today = new \DateTimeImmutable('today');
+        if ($date > $today) {
+            return false;
+        }
+
+        return $date->diff($today)->y < 18;
+    }
+
     private function createPdf(): \FPDF
     {
         $pdf = new \FPDF('P', 'mm', 'A4');

+ 12 - 6
src/mail/submissionformatter.php

@@ -203,15 +203,21 @@ final class SubmissionFormatter
     /** @param array<string, mixed> $formData */
     private function deriveIsMinor(array $formData): string
     {
-        $dob = (string) ($formData['geburtsdatum'] ?? '');
-        if ($dob === '') {
+        $birthdate = trim((string) ($formData['geburtsdatum'] ?? ''));
+        if ($birthdate === '') {
             return '0';
         }
-        $ts = strtotime($dob);
-        if ($ts === false) {
+
+        $date = \DateTimeImmutable::createFromFormat('!Y-m-d', $birthdate);
+        if (!$date || $date->format('Y-m-d') !== $birthdate) {
+            return '0';
+        }
+
+        $today = new \DateTimeImmutable('today');
+        if ($date > $today) {
             return '0';
         }
-        $age = (int) ((time() - $ts) / 31557600);
-        return $age < 18 ? '1' : '0';
+
+        return $date->diff($today)->y < 18 ? '1' : '0';
     }
 }

+ 1 - 0
src/storage/jsonstore.php

@@ -126,6 +126,7 @@ final class JsonStore
             'submitted_at' => $now,
             'expires_at' => $expires,
             'step' => $submission['step'] ?? ($draft['step'] ?? null),
+            'is_minor_submission' => (bool) ($submission['is_minor_submission'] ?? false),
             'form_data' => (array) ($submission['form_data'] ?? []),
             'uploads' => (array) ($submission['uploads'] ?? []),
         ];