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 ab776822..5d45300a 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/controller/DocumentController.java @@ -18,9 +18,11 @@ import jakarta.validation.constraints.Min; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.validation.annotation.Validated; +import org.raddatz.familienarchiv.dto.BatchMetadataRequest; import org.raddatz.familienarchiv.dto.BulkEditError; import org.raddatz.familienarchiv.dto.BulkEditResult; import org.raddatz.familienarchiv.dto.DocumentBatchMetadataDTO; +import org.raddatz.familienarchiv.dto.DocumentBatchSummary; import org.raddatz.familienarchiv.dto.DocumentBulkEditDTO; import org.raddatz.familienarchiv.dto.DocumentSearchResult; import org.raddatz.familienarchiv.dto.DocumentUpdateDTO; @@ -279,6 +281,15 @@ public class DocumentController { return new BulkEditResult(updated, errors); } + @PostMapping(value = "/batch-metadata", consumes = MediaType.APPLICATION_JSON_VALUE) + @RequirePermission(Permission.READ_ALL) + public List batchMetadata(@RequestBody BatchMetadataRequest request) { + if (request == null || request.ids() == null || request.ids().isEmpty()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "ids is required"); + } + return documentService.batchMetadata(request.ids()); + } + @GetMapping("/incomplete-count") @RequirePermission(Permission.WRITE_ALL) public Map getIncompleteCount() { 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 f53a60b3..c5832b0b 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -350,6 +350,22 @@ public class DocumentService { return documentRepository.save(doc); } + /** + * Returns lightweight summaries (id, title, server PDF URL) for the given + * document IDs. Unknown IDs are silently dropped — the consumer is the + * bulk-edit page's left strip, where missing previews would already be + * obvious; surfacing them as errors here adds no value. + */ + public List batchMetadata(List ids) { + if (ids == null || ids.isEmpty()) return List.of(); + return documentRepository.findAllById(ids).stream() + .map(d -> new org.raddatz.familienarchiv.dto.DocumentBatchSummary( + d.getId(), + d.getTitle() != null ? d.getTitle() : d.getOriginalFilename(), + "/api/documents/" + d.getId() + "/file")) + .toList(); + } + /** * Applies a bulk-edit DTO to a single document atomically. * Tags and receivers are additive (merged into existing sets); sender and the diff --git a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java index bc67dd21..a4075835 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.java @@ -1016,6 +1016,41 @@ class DocumentControllerTest { verify(documentService).applyBulkEditToDocument(eq(id2), any()); } + // ─── POST /api/documents/batch-metadata ────────────────────────────────── + + @Test + void batchMetadata_returns401_whenUnauthenticated() throws Exception { + mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/api/documents/batch-metadata") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"ids\":[\"" + UUID.randomUUID() + "\"]}")) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser(authorities = "READ_ALL") + void batchMetadata_returns400_whenIdsEmpty() throws Exception { + mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/api/documents/batch-metadata") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"ids\":[]}")) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(authorities = "READ_ALL") + void batchMetadata_returnsSummaries_forExistingIds() throws Exception { + UUID id = UUID.randomUUID(); + when(documentService.batchMetadata(any())).thenReturn(List.of( + new org.raddatz.familienarchiv.dto.DocumentBatchSummary(id, "Brief", "/api/documents/" + id + "/file"))); + + mockMvc.perform(org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post("/api/documents/batch-metadata") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"ids\":[\"" + id + "\"]}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].id").value(id.toString())) + .andExpect(jsonPath("$[0].title").value("Brief")) + .andExpect(jsonPath("$[0].pdfUrl").value("/api/documents/" + id + "/file")); + } + @Test @WithMockUser(authorities = "WRITE_ALL") void patchBulk_returnsPartialFailureShape_whenServiceThrowsForOneDocument() throws Exception { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java index 3065391c..034a30bf 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java @@ -2089,6 +2089,58 @@ class DocumentServiceTest { assertThat(doc.getDocumentLocation()).isEqualTo("NewLocation"); } + // ─── batchMetadata ─────────────────────────────────────────────────────── + + @Test + void batchMetadata_returnsEmpty_whenIdsIsNull() { + assertThat(documentService.batchMetadata(null)).isEmpty(); + } + + @Test + void batchMetadata_returnsEmpty_whenIdsIsEmpty() { + assertThat(documentService.batchMetadata(List.of())).isEmpty(); + } + + @Test + void batchMetadata_returnsSummariesWithPdfUrl_forExistingIds() { + UUID id1 = UUID.randomUUID(); + UUID id2 = UUID.randomUUID(); + Document d1 = Document.builder().id(id1).title("Brief 1").build(); + Document d2 = Document.builder().id(id2).title("Brief 2").build(); + when(documentRepository.findAllById(List.of(id1, id2))).thenReturn(List.of(d1, d2)); + + var result = documentService.batchMetadata(List.of(id1, id2)); + + assertThat(result).hasSize(2); + assertThat(result.get(0).id()).isEqualTo(id1); + assertThat(result.get(0).title()).isEqualTo("Brief 1"); + assertThat(result.get(0).pdfUrl()).isEqualTo("/api/documents/" + id1 + "/file"); + } + + @Test + void batchMetadata_silentlyDropsUnknownIds() { + UUID known = UUID.randomUUID(); + UUID missing = UUID.randomUUID(); + Document d = Document.builder().id(known).title("Found").build(); + when(documentRepository.findAllById(List.of(known, missing))).thenReturn(List.of(d)); + + var result = documentService.batchMetadata(List.of(known, missing)); + + assertThat(result).hasSize(1); + assertThat(result.get(0).id()).isEqualTo(known); + } + + @Test + void batchMetadata_fallsBackToOriginalFilename_whenTitleIsNull() { + UUID id = UUID.randomUUID(); + Document d = Document.builder().id(id).originalFilename("scan001.pdf").build(); + when(documentRepository.findAllById(List.of(id))).thenReturn(List.of(d)); + + var result = documentService.batchMetadata(List.of(id)); + + assertThat(result.get(0).title()).isEqualTo("scan001.pdf"); + } + @Test void applyBulkEditToDocument_skipsLocationFields_whenBlankOrNull() { UUID id = UUID.randomUUID();