Ver código fonte

Merge branch 'main' of Medowar/feuerwehr-freising-antragsformular into new-intro-flow

Medowar 1 semana atrás
pai
commit
961b6f0168
6 arquivos alterados com 107 adições e 64 exclusões
  1. 11 2
      admin/download-zip.php
  2. 64 50
      admin/index.php
  3. 10 1
      docs/ai_overview.md
  4. 21 2
      src/storage/fileuploadstore.php
  5. 1 0
      src/storage/jsonstore.php
  6. 0 9
      todo.md

+ 11 - 2
admin/download-zip.php

@@ -29,7 +29,16 @@ if ($submission === null) {
 
 $app = Bootstrap::config('app');
 $baseUploads = rtrim((string) $app['storage']['uploads'], '/');
-$zipPath = sys_get_temp_dir() . '/antrag_' . $id . '_' . bin2hex(random_bytes(4)) . '.zip';
+
+$formData = (array) ($submission['form_data'] ?? []);
+$vorname = preg_replace('/[^a-zA-Z0-9-]/', '_', (string) ($formData['vorname'] ?? ''));
+$nachname = preg_replace('/[^a-zA-Z0-9-]/', '_', (string) ($formData['nachname'] ?? ''));
+$namePart = trim($vorname . '_' . $nachname, '_');
+if ($namePart === '') {
+    $namePart = $id;
+}
+
+$zipPath = sys_get_temp_dir() . '/antrag_' . $namePart . '_' . bin2hex(random_bytes(4)) . '.zip';
 
 $zip = new ZipArchive();
 if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
@@ -68,7 +77,7 @@ $zip->close();
 
 header('Content-Type: application/zip');
 header('Content-Length: ' . (string) filesize($zipPath));
-header('Content-Disposition: attachment; filename="antrag_' . $id . '.zip"');
+header('Content-Disposition: attachment; filename="antrag_' . $namePart . '.zip"');
 readfile($zipPath);
 unlink($zipPath);
 exit;

+ 64 - 50
admin/index.php

@@ -37,6 +37,7 @@ if ($query !== '') {
 }
 ?><!doctype html>
 <html lang="de">
+
 <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -44,59 +45,72 @@ if ($query !== '') {
     <link rel="stylesheet" href="<?= htmlspecialchars(Bootstrap::url('assets/css/tokens.css')) ?>">
     <link rel="stylesheet" href="<?= htmlspecialchars(Bootstrap::url('assets/css/base.css')) ?>">
 </head>
+
 <body class="admin-page">
-<header class="site-header">
-    <div class="container header-inner">
-        <a class="brand" href="<?= htmlspecialchars(Bootstrap::url('admin/index.php')) ?>">
-            <img class="brand-logo" src="<?= htmlspecialchars(Bootstrap::url('assets/images/feuerwehr-logo-invers.webp')) ?>" alt="Feuerwehr Logo">
-            <div class="brand-title"><?= htmlspecialchars((string) ($app['project_name'] ?? 'Admin')) ?></div>
-        </a>
-    </div>
-</header>
-<main class="container">
-    <section class="card">
-        <div class="admin-toolbar">
-            <div>
-                <h1>Abgeschlossene Anträge</h1>
-                <a href="<?= htmlspecialchars(Bootstrap::url('admin/login.php?logout=1')) ?>">Abmelden</a>
-            </div>
-            <form method="get" class="field">
-                <label for="q">Suche Name oder E-Mail</label>
-                <input id="q" name="q" value="<?= htmlspecialchars($query) ?>">
-            </form>
+    <header class="site-header">
+        <div class="container header-inner">
+            <a class="brand" href="<?= htmlspecialchars(Bootstrap::url('admin/index.php')) ?>">
+                <img class="brand-logo"
+                    src="<?= htmlspecialchars(Bootstrap::url('assets/images/feuerwehr-logo-invers.webp')) ?>"
+                    alt="Feuerwehr Logo">
+                <div class="brand-title"><?= htmlspecialchars((string) ($app['project_name'] ?? 'Admin')) ?></div>
+            </a>
         </div>
+    </header>
+    <main class="container">
+        <section class="card">
+            <div class="admin-toolbar">
+                <div>
+                    <h1>Abgeschlossene Anträge</h1>
+                    <a href="<?= htmlspecialchars(Bootstrap::url('admin/login.php?logout=1')) ?>">Abmelden</a>
+                </div>
+                <form method="get" class="field">
+                    <label for="q">Suche Name oder E-Mail</label>
+                    <input id="q" name="q" value="<?= htmlspecialchars($query) ?>">
+                </form>
+            </div>
 
-        <?php if (empty($list)): ?>
-            <p>Keine Anträge vorhanden.</p>
-        <?php else: ?>
-            <div class="table-responsive">
-                <table class="responsive-table table-dense admin-submissions-table">
-                    <thead>
-                        <tr>
-                            <th>Vorname</th>
-                            <th>Nachname</th>
-                            <th>E-Mail</th>
-                            <th>Eingereicht</th>
-                            <th>Aktion</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        <?php foreach ($list as $item):
-                            $formData = (array) ($item['form_data'] ?? []);
-                        ?>
+            <div class="alert alert-warning">
+                <strong>Hinweis:</strong> Anträge werden nach <?= (int) ($app['retention']['submission_days'] ?? 90) ?>
+                Tagen aus Datenschutzgründen automatisch gelöscht.
+            </div>
+
+            <?php if (empty($list)): ?>
+                <p>Keine Anträge vorhanden.</p>
+            <?php else: ?>
+                <div class="table-responsive">
+                    <table class="responsive-table table-dense admin-submissions-table">
+                        <thead>
                             <tr>
-                                <td data-label="Vorname"><?= htmlspecialchars((string) ($formData['vorname'] ?? '')) ?></td>
-                                <td data-label="Nachname"><?= htmlspecialchars((string) ($formData['nachname'] ?? '')) ?></td>
-                                <td data-label="E-Mail"><?= htmlspecialchars((string) ($item['email'] ?? '')) ?></td>
-                                <td data-label="Eingereicht"><?= htmlspecialchars((string) ($item['submitted_at'] ?? '')) ?></td>
-                                <td data-label="Aktion"><a href="<?= htmlspecialchars(Bootstrap::url('admin/application.php?id=' . urlencode((string) ($item['application_key'] ?? '')))) ?>">Details</a></td>
+                                <th>Vorname</th>
+                                <th>Nachname</th>
+                                <th>E-Mail</th>
+                                <th>Eingereicht</th>
+                                <th>Aktion</th>
                             </tr>
-                        <?php endforeach; ?>
-                    </tbody>
-                </table>
-            </div>
-        <?php endif; ?>
-    </section>
-</main>
+                        </thead>
+                        <tbody>
+                            <?php foreach ($list as $item):
+                                $formData = (array) ($item['form_data'] ?? []);
+                                ?>
+                                <tr>
+                                    <td data-label="Vorname"><?= htmlspecialchars((string) ($formData['vorname'] ?? '')) ?></td>
+                                    <td data-label="Nachname"><?= htmlspecialchars((string) ($formData['nachname'] ?? '')) ?>
+                                    </td>
+                                    <td data-label="E-Mail"><?= htmlspecialchars((string) ($item['email'] ?? '')) ?></td>
+                                    <td data-label="Eingereicht"><?= htmlspecialchars((string) ($item['submitted_at'] ?? '')) ?>
+                                    </td>
+                                    <td data-label="Aktion"><a
+                                            href="<?= htmlspecialchars(Bootstrap::url('admin/application.php?id=' . urlencode((string) ($item['application_key'] ?? '')))) ?>">Details</a>
+                                    </td>
+                                </tr>
+                            <?php endforeach; ?>
+                        </tbody>
+                    </table>
+                </div>
+            <?php endif; ?>
+        </section>
+    </main>
 </body>
-</html>
+
+</html>

+ 10 - 1
docs/ai_overview.md

@@ -18,16 +18,25 @@ Digitaler Mitgliedsantrag für Feuerwehrverein mit Flatfile-Speicherung und Admi
   - `api/save-draft.php`
   - `api/submit.php`
   - `api/reset.php`
+  - `api/upload-preview.php`
+  - `api/delete-upload.php`
 - Admin:
   - `admin/login.php`
+  - `admin/auth.php`
   - `admin/index.php`
   - `admin/application.php`
   - `admin/download.php`
   - `admin/download-zip.php`
+  - `admin/export-pdf.php`
   - `admin/delete.php`
-- Kernlogik:
+  - `admin/test-mail.php`
+  - `admin/cleanup.php`
+- Kernlogik (`src/`-Ordner strukturiert nach Domains):
+  - `src/autoload.php` (Basis Autoloader)
+  - `src/app/bootstrap.php` (App-Initialisierung und Konfiguration)
   - `src/storage/jsonstore.php`
   - `src/storage/fileuploadstore.php`
+  - `src/storage/filesystem.php`
   - `src/form/validator.php`
   - `src/security/csrf.php`
   - `src/security/ratelimiter.php`

+ 21 - 2
src/storage/fileuploadstore.php

@@ -107,8 +107,8 @@ final class FileUploadStore
     /** @param array<string, mixed> $files */
     private function pickUploadedFile(array $files, string $fieldKey): ?array
     {
-        $primary = $files[$fieldKey] ?? null;
-        $camera = $files[$fieldKey . '__camera'] ?? null;
+        $primary = $this->extractFirstFile($files[$fieldKey] ?? null);
+        $camera = $this->extractFirstFile($files[$fieldKey . '__camera'] ?? null);
 
         if (is_array($primary) && ((int) ($primary['error'] ?? UPLOAD_ERR_NO_FILE) === UPLOAD_ERR_OK)) {
             return $primary;
@@ -127,6 +127,25 @@ final class FileUploadStore
         return null;
     }
 
+    private function extractFirstFile(mixed $fileArray): ?array
+    {
+        if (!is_array($fileArray)) {
+            return null;
+        }
+        
+        if (isset($fileArray['name']) && is_array($fileArray['name'])) {
+            return [
+                'name' => $fileArray['name'][0] ?? '',
+                'type' => $fileArray['type'][0] ?? '',
+                'tmp_name' => $fileArray['tmp_name'][0] ?? '',
+                'error' => $fileArray['error'][0] ?? UPLOAD_ERR_NO_FILE,
+                'size' => $fileArray['size'][0] ?? 0,
+            ];
+        }
+        
+        return $fileArray;
+    }
+
     private function sanitizeFilename(string $name): string
     {
         $name = str_replace(["\0", '/', '\\'], '', $name);

+ 1 - 0
src/storage/jsonstore.php

@@ -228,6 +228,7 @@ final class JsonStore
         } finally {
             flock($handle, LOCK_UN);
             fclose($handle);
+            @unlink($lockFile);
         }
     }
 

+ 0 - 9
todo.md

@@ -1,14 +1,5 @@
 Offene Themen:
 
-- Mail
-Mail Antrag an Antragsteller
-
-Mail Vorstand/Verwaltung:
-- Anhänge
-- Formatierung der Daten verbessern
-
-Mail Zustellung mehrere Empfänger
-
 Admin Interface
 Überarbeiten