From 6fcff3355d850edc5fde141c7e02768269a558df Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 16 Mar 2026 16:04:56 +0100 Subject: [PATCH] feat: add create document feature via web interface - Backend: new POST /api/documents endpoint with DocumentService.createDocument() reusing DocumentUpdateDTO; handles file upload, tags, sender, receivers - Frontend: new /documents/new route with same four-section form as edit page (Wer & Wann, Beschreibung, Transkription, Datei) but with empty fields - Home page: subtle '+ Neues Dokument' link above the document list Co-Authored-By: Claude Sonnet 4.6 --- .../controller/DocumentController.java | 13 + .../service/DocumentService.java | 51 ++++ frontend/src/lib/generated/api.ts | 40 +++ frontend/src/routes/+page.svelte | 204 ++++++++-------- .../src/routes/documents/new/+page.server.ts | 35 +++ .../src/routes/documents/new/+page.svelte | 229 ++++++++++++++++++ 6 files changed, 476 insertions(+), 96 deletions(-) create mode 100644 frontend/src/routes/documents/new/+page.server.ts create mode 100644 frontend/src/routes/documents/new/+page.svelte diff --git a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java index 0c6f6c4d..0a2d00b4 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -22,6 +22,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -78,6 +79,18 @@ public class DocumentController { .orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id)); } + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @RequirePermission(Permission.WRITE_ALL) + public Document createDocument( + @ModelAttribute DocumentUpdateDTO dto, + @RequestPart(value = "file", required = false) MultipartFile file) { + try { + return documentService.createDocument(dto, file); + } catch (IOException e) { + throw DomainException.internal(ErrorCode.FILE_UPLOAD_FAILED, "Failed to upload file: " + e.getMessage()); + } + } + @PutMapping(value = "/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @RequirePermission(Permission.WRITE_ALL) public Document updateDocument( diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java index 14c05d38..c007aa61 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -77,6 +77,57 @@ public class DocumentService { return documentRepository.save(document); } + @Transactional + public Document createDocument(DocumentUpdateDTO dto, MultipartFile file) throws IOException { + String filename = (file != null && !file.isEmpty()) + ? file.getOriginalFilename() + : (dto.getTitle() != null ? dto.getTitle() : "Unbenanntes Dokument"); + + Document doc = Document.builder() + .originalFilename(filename) + .title(dto.getTitle()) + .documentDate(dto.getDocumentDate()) + .location(dto.getLocation()) + .documentLocation(dto.getDocumentLocation()) + .transcription(dto.getTranscription()) + .summary(dto.getSummary()) + .status(DocumentStatus.PLACEHOLDER) + .build(); + + doc = documentRepository.save(doc); + + // Tags + List tags = new ArrayList<>(); + if (dto.getTags() != null && !dto.getTags().isBlank()) { + tags = Arrays.stream(dto.getTags().split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toList(); + } + updateDocumentTags(doc.getId(), tags); + doc = documentRepository.findById(doc.getId()).orElseThrow(); + + // Sender + if (dto.getSenderId() != null) { + doc.setSender(personRepository.findById(dto.getSenderId()).orElse(null)); + } + + // Empfänger + if (dto.getReceiverIds() != null && !dto.getReceiverIds().isEmpty()) { + doc.setReceivers(new HashSet<>(personRepository.findAllById(dto.getReceiverIds()))); + } + + // Datei + if (file != null && !file.isEmpty()) { + String s3Key = fileService.uploadFile(file, file.getOriginalFilename()); + doc.setFilePath(s3Key); + doc.setContentType(file.getContentType()); + doc.setStatus(DocumentStatus.UPLOADED); + } + + return documentRepository.save(doc); + } + @Transactional public Document updateDocument(UUID id, DocumentUpdateDTO dto, MultipartFile newFile) throws IOException { Document doc = documentRepository.findById(id) diff --git a/frontend/src/lib/generated/api.ts b/frontend/src/lib/generated/api.ts index 37cd968f..e0445130 100644 --- a/frontend/src/lib/generated/api.ts +++ b/frontend/src/lib/generated/api.ts @@ -100,6 +100,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/documents": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createDocument"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/admin/trigger-import": { parameters: { query?: never; @@ -635,6 +651,30 @@ export interface operations { }; }; }; + createDocument: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "multipart/form-data": components["schemas"]["DocumentUpdateDTO"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["Document"]; + }; + }; + }; + }; triggerMassImport: { parameters: { query?: never; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 8464b5fa..ceae8171 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,89 +1,86 @@ -
+
-
+
-
+
-
+
-
+
-

Schlagworte

+

+ Schlagworte +

-
+
@@ -212,42 +211,55 @@ {/if}
+ + + -
+
{#if data.error} -
+
{data.error}
{:else if data.documents && data.documents.length > 0}