From 101c351d1449a3b3d373d3122213da65976a498a Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 7 May 2026 18:37:33 +0200 Subject: [PATCH] feat(person): add findTopByDocumentCount endpoint for reader dashboard PersonController GET /api/persons?sort=documentCount&size=N returns the top N persons by combined sender+receiver document count for the reader dashboard. Co-Authored-By: Claude Sonnet 4.6 --- .../familienarchiv/person/PersonController.java | 8 +++++++- .../familienarchiv/person/PersonRepository.java | 14 ++++++++++++++ .../familienarchiv/person/PersonService.java | 4 ++++ .../person/PersonControllerTest.java | 11 +++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonController.java b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonController.java index b59fa759..c082ca0b 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonController.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonController.java @@ -35,7 +35,13 @@ public class PersonController { @GetMapping @RequirePermission(Permission.READ_ALL) - public ResponseEntity> getPersons(@RequestParam(required = false) String q) { + public ResponseEntity> getPersons( + @RequestParam(required = false) String q, + @RequestParam(required = false, defaultValue = "0") int size, + @RequestParam(required = false) String sort) { + if ("documentCount".equals(sort) && size > 0 && q == null) { + return ResponseEntity.ok(personService.findTopByDocumentCount(size)); + } return ResponseEntity.ok(personService.findAll(q)); } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java index 8b7b524d..a0e3fb16 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonRepository.java @@ -69,6 +69,20 @@ public interface PersonRepository extends JpaRepository { nativeQuery = true) List searchWithDocumentCount(@Param("query") String query); + @Query(value = """ + SELECT p.id, p.title, p.first_name AS firstName, p.last_name AS lastName, + p.person_type AS personType, + p.alias, p.birth_year AS birthYear, p.death_year AS deathYear, p.notes, + p.family_member AS familyMember, + (SELECT COUNT(*) FROM documents d WHERE d.sender_id = p.id) + + (SELECT COUNT(*) FROM document_receivers dr WHERE dr.person_id = p.id) AS documentCount + FROM persons p + ORDER BY documentCount DESC + LIMIT :limit + """, + nativeQuery = true) + List findTopByDocumentCount(@Param("limit") int limit); + // --- Correspondent queries --- @Query(value = """ diff --git a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java index 007008b1..89b11ef3 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java @@ -41,6 +41,10 @@ public class PersonService { return personRepository.searchWithDocumentCount(q.trim()); } + public List findTopByDocumentCount(int limit) { + return personRepository.findTopByDocumentCount(limit); + } + public Person getById(UUID id) { return personRepository.findById(id) .orElseThrow(() -> DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found: " + id)); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java index 45454794..7d8044c7 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonControllerTest.java @@ -81,6 +81,17 @@ class PersonControllerTest { .andExpect(jsonPath("$[0].firstName").value("Hans")); } + @Test + @WithMockUser(authorities = "READ_ALL") + void getPersons_delegatesTopByDocumentCount_whenSortAndSizeGiven() throws Exception { + PersonSummaryDTO top = mockPersonSummary("Käthe", "Raddatz"); + when(personService.findTopByDocumentCount(4)).thenReturn(List.of(top)); + + mockMvc.perform(get("/api/persons").param("sort", "documentCount").param("size", "4")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$[0].firstName").value("Käthe")); + } + private PersonSummaryDTO mockPersonSummary(String firstName, String lastName) { return new PersonSummaryDTO() { public java.util.UUID getId() { return UUID.randomUUID(); }