feat: Massenimport/Export JSON/CSV (All-or-Nothing) (#15) #296

Merged
niboer merged 2 commits from issue-15-massenimport into main 2026-07-11 22:30:03 +02:00
Owner

Allgemein

  • closes #15
  • REBASE-MERGE

Altes Verhalten

Keine Massenimport/Export-Funktion vorhanden. Personen muessen einzeln via Formular angelegt werden. Export nur als einzelner DSGVO-Datenexport (ZIP) pro Person verfuegbar.

Neues Verhalten

Administratoren mit edit-person-Rolle koennen in der erweiterten Ansicht unter /app/persons:

Import (All-or-Nothing via Prisma $transaction):

  • JSON (Content-Type: application/json) und CSV (Content-Type: text/csv)
  • Matching: UUID primaer (UPDATE vorhanden, CREATE bei neuer UUID), firstName+lastName case-insensitiv sekundaer
  • Gruppen-Handling: nicht existent → anlegen, existent → approvedBy setzen
  • Kontaktdaten werden mit AES-256-GCM verschluesselt gespeichert

Export:

  • JSON oder CSV mit archived-Filter (true/false/all)
  • Entschluesselte Kontaktdaten + Gruppenname

Frontend:

  • 5-Schritte-Import-Dialog (Upload → Parsing → Preview → Submit → Result)
  • Beispiel-Popup mit Copy-Button
  • Export-Buttons: JSON + CSV

Begruendung

Issue #15 — Massenimport/Export fuer Administratoren, damit Personendaten nicht einzeln angelegt werden muessen.

Test-Hinweise

Backend Unit-Tests: 18 Tests in src/__tests__/import-export.test.ts
E2E-Tests: 14 Test-Cases in 3 Dateien unter e2e/specs/test-3-workflows/

Manuell:

  • /app/persons?archived=false → Erweiterte Ansicht → Import/Export-Buttons sichtbar
  • JSON-Import via [Importieren] → Datei aus e2e/test-fixtures/import-export/ auswaehlen
  • Export via [JSON exportieren] / [CSV exportieren] → Download mit allen/aktiven/archivierten Personen
  • Beispiel-Datei via [Beispiel-Datei] → Popup mit Copy-Button

Bekannte Einschraenkungen

  • Export nur gefiltert nach archived (true/false/all) — kein freies Feld-Filter
  • Import erwartet festes Schema (kein Mapping, keine Drop-Felder)
  • E2E-Tests benoetigen CI-Umgebung (Docker) fuer volle Abdeckung

Checkliste

  • use:testid an neuen <button>, <a>, <form>-Elementen gesetzt?
  • Korrespondierender E2E-Test in e2e/specs/ angelegt?
### Allgemein - closes #15 - REBASE-MERGE ### Altes Verhalten Keine Massenimport/Export-Funktion vorhanden. Personen muessen einzeln via Formular angelegt werden. Export nur als einzelner DSGVO-Datenexport (ZIP) pro Person verfuegbar. ### Neues Verhalten Administratoren mit `edit-person`-Rolle koennen in der erweiterten Ansicht unter `/app/persons`: **Import (All-or-Nothing via Prisma $transaction):** - JSON (Content-Type: application/json) und CSV (Content-Type: text/csv) - Matching: UUID primaer (UPDATE vorhanden, CREATE bei neuer UUID), `firstName+lastName` case-insensitiv sekundaer - Gruppen-Handling: nicht existent → anlegen, existent → approvedBy setzen - Kontaktdaten werden mit AES-256-GCM verschluesselt gespeichert **Export:** - JSON oder CSV mit `archived`-Filter (true/false/all) - Entschluesselte Kontaktdaten + Gruppenname **Frontend:** - 5-Schritte-Import-Dialog (Upload → Parsing → Preview → Submit → Result) - Beispiel-Popup mit Copy-Button - Export-Buttons: JSON + CSV ### Begruendung Issue #15 — Massenimport/Export fuer Administratoren, damit Personendaten nicht einzeln angelegt werden muessen. ### Test-Hinweise **Backend Unit-Tests:** 18 Tests in `src/__tests__/import-export.test.ts` **E2E-Tests:** 14 Test-Cases in 3 Dateien unter `e2e/specs/test-3-workflows/` Manuell: - `/app/persons?archived=false` → Erweiterte Ansicht → Import/Export-Buttons sichtbar - JSON-Import via `[Importieren]` → Datei aus `e2e/test-fixtures/import-export/` auswaehlen - Export via `[JSON exportieren]` / `[CSV exportieren]` → Download mit allen/aktiven/archivierten Personen - Beispiel-Datei via `[Beispiel-Datei]` → Popup mit Copy-Button ### Bekannte Einschraenkungen - Export nur gefiltert nach `archived` (true/false/all) — kein freies Feld-Filter - Import erwartet festes Schema (kein Mapping, keine Drop-Felder) - E2E-Tests benoetigen CI-Umgebung (Docker) fuer volle Abdeckung ### Checkliste - [x] `use:testid` an neuen `<button>`, `<a>`, `<form>`-Elementen gesetzt? - [x] Korrespondierender E2E-Test in `e2e/specs/` angelegt?
refs #15

Aktuelles: OpenAPI-Spezifikation hat keine Import/Export-Endpunkte
Neu: PersonImportRow, PersonImportRequest, PersonImportResponse, PersonExportRow
als Schemas + POST /api/v1/persons/import und GET /api/v1/persons/export als Pfade
refs #15

Aktuelles: Backend kann keine CSV-Daten parsen
Neu: papaparse (Backend-Dependency) fuer CSV-Import und -Export
refs #15

Aktuelles: Keine Massenimport/Export-Endpunkte vorhanden
Neu: Import-Endpunkt (JSON/CSV, All-or-Nothing via Prisma ,
UUID/Name-Matching, Group-Handling) und Export-Endpunkt (JSON/CSV,
archived-Filter, Kontaktdaten-Entschluesselung)
refs #15

Aktuelles: Person-Mock fehlt findFirst-Methode
Neu: findFirst ergaenzt, benoetigt fuer Import-Name-Matching
refs #15

Aktuelles: Keine Tests fuer Import/Export-Endpunkte
Neu: 18 Tests fuer Auth, Validierung, UUID-Matching, Name-Matching,
CSV-Parsing, JSON/CSV-Export und archived-Filter
refs #15

Aktuelles: Keine Import/Export-UI vorhanden
Neu: Beispiel-Popup mit Copy-Button, 5-Schritte-Import-Dialog,
Import/Export-Button-Leiste, i18n-Texte (de+en), Integration
in Personen-Seite (nur erweiterte Ansicht + edit-person-Rolle)
refs #15

Aktuelles: Keine E2E-Tests fuer Import/Export
Neu: 14 Test Cases in 3 Workflows (Export, CSV-Import, JSON-Import)
mit Seed-Daten-Validierung (15/12/3 Personen)
refs #15

Aktuelles: Kein Design-Dokument fuer Massenimport/Export
Neu: Vollstaendige Spezifikation (Dateiformat, Matching, All-or-Nothing,
UI-Integration, Testabdeckung)
refs #15

Aktuelles: Design-Dokumente verweisen nicht auf Massenimport/Export
Neu: design.md-Tabelle aktualisiert, 02-datenmodell mit Verweis auf
10-massenimport.md, offene Fragen entfernt; 03-autorisierungsmatrix
mit 2 neuen Eintraegen; 03-api-design ergaenzt
refs #15

Aktuelles: await import('../../svelte.config.js') haengt im Branch
(vermutlich durch SvelteKit-Vite-Plugin-Init mit 3 neuen Komponenten)
Neu: 4. Test nutzt readFileSync + toContain wie die anderen 3 Tests;
kein dynamischer Import mehr, kein Timeout
refs #15

Aktuelles: Export-Schema erzwingt boolean fuer archived,
schlaegt mit 400 bei ?archived=all fehl. Import-Dialog hat
kein data-testid fuer E2E-Selektor.

Neu: archived als string (true/false/all) im E2E-Test verwendbar;
Dialog.Content hat data-testid=dialog-person-import
refs #15

Aktuelles: t(path, {step}) gibt den unveraenderten Text zurueck,
weil {step} nicht ersetzt wird.

Neu: Optionaler params-Parameter mit {key}-Ersetzung im String.
Beispiele: t('importStep', {step}) -> 'Schritt 1 von 5',
t('importSuccess', {count}) -> '1 Person(en) erfolgreich importiert.'
refs #15

Aktuelles: Toast hat feste testid toast-success/toast-error;
Import-Dialog hat input-import-file statt input-file-upload
und btn-import-done statt btn-import-close.

Neu: Toast akzeptiert optionales testId-Prop (Fallback auf alte IDs);
Import-Dialog testids an e2e-Erwartungen angepasst.
refs #15

Aktuelles: CSV ohne Header testet toBeDisabled() auf Button
der nicht im DOM ist (parseError-Branch). Ungueltiges JSON
sucht nach 'ungueltig' aber i18n-Text ist 'gueltiges'.

Neu: toBeHidden() fuer nicht existenten Submit-Button;
Suchtext von 'ungueltig' auf 'gueltiges' korrigiert.
refs #15

Aktuelles: importResult und step werden synchron gesetzt;
Svelte 5 batched updates unvollstaendig → Dialog zeigt
Kombination aus step=5-Description und step=4-Content.

Neu: await tick() zwischen den Zuweisungen stellt sicher,
dass Svelte den ersten Render-Durchlauf abschliesst bevor
step wechselt. Ergebnis-Content wird zuverlaessig gerendert.
refs #15

Aktuelles: Keine vordefinierten Test-Dateien fuer manuelles
Import-Testing.

Neu: 10 Fixture-Dateien in e2e/test-fixtures/import-export/
nummeriert nach Test-Reihenfolge (01-08 + 2 example-*).
refs #15

Aktuelles: BFF-Proxy hardcodiert Content-Type=application/json
fuer jeden Request mit Body. CSV-String wird als JSON
interpretiert → Backend 400.

Neu: Originaler Content-Type aus dem Client-Request wird
durchgereicht. Fallback auf application/json wenn keiner
gesetzt ist.
refs #15

Aktuelles: Bei HTTP 400+ wird res.json() zwar ausgefuehrt,
aber das Fastify-Error-Objekt hat kein errors-Feld.
Der Fehler wird nicht angezeigt, die GUI bleibt leer.

Neu: res.ok-Check nach fetch. Bei Fehlern wird message/error
aus dem Response-Body extrahiert und als importResult.errors
gesetzt. Template hat optional-chaining gegen undefined errors.
refs #15

Aktuelles: Alle Import/Export-Tests in einer Datei mit
shared context. Fehler in einem Test killen alle folgenden
via afterAll-context-close.

Neu: Drei eigenstaendige Dateien mit je eigenem context,
beforeAll/afterAll. Isolation verhindert Cascade-Failures.
- export.spec.ts (6 Tests)
- import-csv.spec.ts (3 Tests)
- import-json.spec.ts (5 Tests)
refs #15

Aktuelles: Negative Tests versuchen btn-import-close zu klicken,
der nur in Schritt 5 existiert. Bei Schritt 3 (parseError/
hasSchemaError) gibt es diesen Button nicht → context closed.

Neu: Dialog via Escape schliessen. HandleClose() in der
Komponente resettet den State sauber unabhaengig vom Schritt.
refs #15

Aktuelles: Leere CSV hat nach .trim() nur 1 Zeile → Frontend
blockiert mit parseError. Leeres JSON [] hat length=0 →
submitImport kehrt frueh zurueck. Beide nie API-Call.

Neu: CSV 'firstName,lastName\n,' (trailing comma als 2. Zeile).
JSON [{}] (leeres Objekt mit Length 1). Beide erreichen das
Backend → 422 → Fehler-Text wie erwartet.
fix(e2e): rewrite empty csv/json tests for frontend validation
All checks were successful
CI Backend / lint-backend (pull_request) Has been skipped
CI Frontend / lint-frontend (pull_request) Has been skipped
CI Meta / conventional-commit (pull_request) Has been skipped
E2E / e2e (pull_request) Has been skipped
CI Backend / test-backend (pull_request) Has been skipped
CI Frontend / test-frontend (pull_request) Has been skipped
9f7a0e61ba
refs #15

Aktuelles: Leeres CSV/JSON versuchen API-Call via
uploadAndImport, aber Frontend blockiert vorher
(parseError bzw. parsedRows.length===0). waitForResponse
haengt.

Neu: Direktes Dialog-Handling ohne uploadAndImport.
Toast-Text auf Frontend-Fehler prüfen, dann Escape.
Kein API-Call noetig — korrektes Verhalten.
niboer force-pushed issue-15-massenimport from 9f7a0e61ba
All checks were successful
CI Backend / lint-backend (pull_request) Has been skipped
CI Frontend / lint-frontend (pull_request) Has been skipped
CI Meta / conventional-commit (pull_request) Has been skipped
E2E / e2e (pull_request) Has been skipped
CI Backend / test-backend (pull_request) Has been skipped
CI Frontend / test-frontend (pull_request) Has been skipped
to 9e9044ea03
All checks were successful
CI Backend / lint-backend (pull_request) Has been skipped
CI Frontend / lint-frontend (pull_request) Has been skipped
CI Meta / conventional-commit (pull_request) Has been skipped
E2E / e2e (pull_request) Has been skipped
CI Backend / test-backend (pull_request) Has been skipped
CI Frontend / test-frontend (pull_request) Has been skipped
2026-07-11 21:57:05 +02:00
Compare
niboer force-pushed issue-15-massenimport from 9e9044ea03
All checks were successful
CI Backend / lint-backend (pull_request) Has been skipped
CI Frontend / lint-frontend (pull_request) Has been skipped
CI Meta / conventional-commit (pull_request) Has been skipped
E2E / e2e (pull_request) Has been skipped
CI Backend / test-backend (pull_request) Has been skipped
CI Frontend / test-frontend (pull_request) Has been skipped
to 61445571dd
All checks were successful
CI Frontend / lint-frontend (pull_request) Has been skipped
CI Meta / conventional-commit (pull_request) Has been skipped
CI Frontend / test-frontend (pull_request) Has been skipped
E2E / e2e (pull_request) Has been skipped
CI Backend / lint-backend (pull_request) Has been skipped
CI Backend / test-backend (pull_request) Has been skipped
CI Backend / lint-backend (push) Successful in 56s
CI Frontend / lint-frontend (push) Successful in 1m16s
CI Meta / conventional-commit (push) Has been skipped
CI Backend / test-backend (push) Successful in 3m49s
CI Frontend / test-frontend (push) Successful in 1m58s
CD / build-and-push (push) Successful in 2m23s
2026-07-11 21:59:20 +02:00
Compare
niboer changed title from WIP: feat: Massenimport/Export JSON/CSV (All-or-Nothing) (#15) to feat: Massenimport/Export JSON/CSV (All-or-Nothing) (#15) 2026-07-11 22:08:33 +02:00
niboer changed title from feat: Massenimport/Export JSON/CSV (All-or-Nothing) (#15) to WIP: feat: Massenimport/Export JSON/CSV (All-or-Nothing) (#15) 2026-07-11 22:10:25 +02:00
niboer changed title from WIP: feat: Massenimport/Export JSON/CSV (All-or-Nothing) (#15) to feat: Massenimport/Export JSON/CSV (All-or-Nothing) (#15) 2026-07-11 22:10:28 +02:00
niboer merged commit 61445571dd into main 2026-07-11 22:30:03 +02:00
niboer deleted branch issue-15-massenimport 2026-07-11 22:30:03 +02:00
Sign in to join this conversation.
No description provided.