feat(conversations): filter person typeahead to correspondents of selected person
All checks were successful
CI / E2E Tests (push) Successful in 18m17s
CI / Unit & Component Tests (push) Successful in 3m37s
CI / Backend Unit Tests (push) Successful in 2m15s
CI / Unit & Component Tests (pull_request) Successful in 2m12s
CI / Backend Unit Tests (pull_request) Successful in 2m1s
CI / E2E Tests (pull_request) Successful in 15m17s
All checks were successful
CI / E2E Tests (push) Successful in 18m17s
CI / Unit & Component Tests (push) Successful in 3m37s
CI / Backend Unit Tests (push) Successful in 2m15s
CI / Unit & Component Tests (pull_request) Successful in 2m12s
CI / Backend Unit Tests (pull_request) Successful in 2m1s
CI / E2E Tests (pull_request) Successful in 15m17s
Closes #29 Backend: - Add PersonRepository.findCorrespondents / findCorrespondentsWithFilter (native SQL, orders by shared document count DESC, limit 10) - Add PersonService.findCorrespondents(personId, q) delegating to the correct repository method based on whether a query string is present - Expose GET /api/persons/{id}/correspondents?q= in PersonController Frontend: - Add optional restrictToCorrespondentsOf prop to PersonTypeahead - On focus with the prop set, fetch correspondents immediately (no typing required) — opens the dropdown showing top correspondents - On input with the prop set, hit the correspondents endpoint with q= param - Without the prop, keep existing /api/persons?q= behaviour unchanged - Wire the prop bidirectionally in /conversations: sender restricts receiver and vice versa Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #42.
This commit is contained in:
@@ -34,6 +34,13 @@ public class PersonController {
|
||||
return personService.getById(id);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/correspondents")
|
||||
public ResponseEntity<List<Person>> getCorrespondents(
|
||||
@PathVariable UUID id,
|
||||
@RequestParam(required = false) String q) {
|
||||
return ResponseEntity.ok(personService.findCorrespondents(id, q));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/documents")
|
||||
public List<Document> getPersonDocuments(@PathVariable UUID id) {
|
||||
return documentService.getDocumentsBySender(id);
|
||||
|
||||
@@ -28,6 +28,51 @@ public interface PersonRepository extends JpaRepository<Person, UUID> {
|
||||
// Lookup by full alias string, used during ODS mass import
|
||||
Optional<Person> findByAliasIgnoreCase(String alias);
|
||||
|
||||
// --- Correspondent queries ---
|
||||
|
||||
@Query(value = """
|
||||
SELECT p.* FROM persons p
|
||||
INNER JOIN (
|
||||
SELECT dr.person_id AS other_id, d.id AS doc_id
|
||||
FROM documents d
|
||||
JOIN document_receivers dr ON dr.document_id = d.id
|
||||
WHERE d.sender_id = :personId
|
||||
UNION ALL
|
||||
SELECT d.sender_id AS other_id, d.id AS doc_id
|
||||
FROM documents d
|
||||
JOIN document_receivers dr ON dr.document_id = d.id
|
||||
WHERE dr.person_id = :personId AND d.sender_id IS NOT NULL
|
||||
) shared ON shared.other_id = p.id
|
||||
WHERE p.id != :personId
|
||||
GROUP BY p.id
|
||||
ORDER BY COUNT(DISTINCT shared.doc_id) DESC
|
||||
LIMIT 10
|
||||
""", nativeQuery = true)
|
||||
List<Person> findCorrespondents(@Param("personId") UUID personId);
|
||||
|
||||
@Query(value = """
|
||||
SELECT p.* FROM persons p
|
||||
INNER JOIN (
|
||||
SELECT dr.person_id AS other_id, d.id AS doc_id
|
||||
FROM documents d
|
||||
JOIN document_receivers dr ON dr.document_id = d.id
|
||||
WHERE d.sender_id = :personId
|
||||
UNION ALL
|
||||
SELECT d.sender_id AS other_id, d.id AS doc_id
|
||||
FROM documents d
|
||||
JOIN document_receivers dr ON dr.document_id = d.id
|
||||
WHERE dr.person_id = :personId AND d.sender_id IS NOT NULL
|
||||
) shared ON shared.other_id = p.id
|
||||
WHERE p.id != :personId
|
||||
AND (LOWER(CONCAT(p.first_name,' ',p.last_name)) LIKE LOWER(CONCAT('%',:q,'%'))
|
||||
OR LOWER(CONCAT(p.last_name,' ',p.first_name)) LIKE LOWER(CONCAT('%',:q,'%'))
|
||||
OR LOWER(p.alias) LIKE LOWER(CONCAT('%',:q,'%')))
|
||||
GROUP BY p.id
|
||||
ORDER BY COUNT(DISTINCT shared.doc_id) DESC
|
||||
LIMIT 10
|
||||
""", nativeQuery = true)
|
||||
List<Person> findCorrespondentsWithFilter(@Param("personId") UUID personId, @Param("q") String q);
|
||||
|
||||
// --- Merge helpers (native SQL to bypass JPA entity layer) ---
|
||||
|
||||
@Modifying
|
||||
|
||||
@@ -31,6 +31,13 @@ public class PersonService {
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Person nicht gefunden"));
|
||||
}
|
||||
|
||||
public List<Person> findCorrespondents(UUID personId, String q) {
|
||||
if (q != null && !q.isBlank()) {
|
||||
return personRepository.findCorrespondentsWithFilter(personId, q);
|
||||
}
|
||||
return personRepository.findCorrespondents(personId);
|
||||
}
|
||||
|
||||
public List<Person> getAllById(List<UUID> ids) {
|
||||
return personRepository.findAllById(ids);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.raddatz.familienarchiv.model.Person;
|
||||
import org.raddatz.familienarchiv.repository.PersonRepository;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -158,6 +159,41 @@ class PersonServiceTest {
|
||||
assertThat(result.getDeathYear()).isEqualTo(1900);
|
||||
}
|
||||
|
||||
// ─── findCorrespondents ──────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void findCorrespondents_delegatesToRepository_withoutFilter() {
|
||||
UUID personId = UUID.randomUUID();
|
||||
List<Person> expected = List.of(Person.builder().id(UUID.randomUUID()).firstName("Anna").lastName("Muster").build());
|
||||
when(personRepository.findCorrespondents(personId)).thenReturn(expected);
|
||||
|
||||
assertThat(personService.findCorrespondents(personId, null)).isEqualTo(expected);
|
||||
verify(personRepository).findCorrespondents(personId);
|
||||
verify(personRepository, never()).findCorrespondentsWithFilter(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findCorrespondents_delegatesToRepository_withFilter() {
|
||||
UUID personId = UUID.randomUUID();
|
||||
List<Person> expected = List.of(Person.builder().id(UUID.randomUUID()).firstName("Anna").lastName("Muster").build());
|
||||
when(personRepository.findCorrespondentsWithFilter(personId, "Anna")).thenReturn(expected);
|
||||
|
||||
assertThat(personService.findCorrespondents(personId, "Anna")).isEqualTo(expected);
|
||||
verify(personRepository).findCorrespondentsWithFilter(personId, "Anna");
|
||||
verify(personRepository, never()).findCorrespondents(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findCorrespondents_delegatesToRepository_withBlankFilter() {
|
||||
UUID personId = UUID.randomUUID();
|
||||
when(personRepository.findCorrespondents(personId)).thenReturn(List.of());
|
||||
|
||||
personService.findCorrespondents(personId, " ");
|
||||
|
||||
verify(personRepository).findCorrespondents(personId);
|
||||
verify(personRepository, never()).findCorrespondentsWithFilter(any(), any());
|
||||
}
|
||||
|
||||
// ─── mergePersons ─────────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user