fix(person): expose life dates on PersonSummaryDTO projection

The mention dropdown renders precise life dates but receives
PersonSummaryDTO items from /api/persons, which only carried the derived
years - the date fields were silently undefined at runtime. Add
birth/death date + precision to the projection and all four native
queries (searchWithDocumentCount's GROUP BY already listed the columns).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-12 19:18:55 +02:00
parent f2f9006548
commit 92672dbb71
4 changed files with 115 additions and 4 deletions

View File

@@ -67,7 +67,9 @@ public interface PersonRepository extends JpaRepository<Person, UUID> {
SELECT p.id, p.title, p.first_name AS firstName, p.last_name AS lastName,
p.person_type AS personType,
p.alias, CAST(EXTRACT(YEAR FROM p.birth_date) AS int) AS birthYear,
CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear, p.notes,
CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear,
p.birth_date AS birthDate, p.birth_date_precision AS birthDatePrecision,
p.death_date AS deathDate, p.death_date_precision AS deathDatePrecision, p.notes,
p.family_member AS familyMember, p.provisional AS provisional,
(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
@@ -81,7 +83,9 @@ public interface PersonRepository extends JpaRepository<Person, UUID> {
SELECT p.id, p.title, p.first_name AS firstName, p.last_name AS lastName,
p.person_type AS personType,
p.alias, CAST(EXTRACT(YEAR FROM p.birth_date) AS int) AS birthYear,
CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear, p.notes,
CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear,
p.birth_date AS birthDate, p.birth_date_precision AS birthDatePrecision,
p.death_date AS deathDate, p.death_date_precision AS deathDatePrecision, p.notes,
p.family_member AS familyMember, p.provisional AS provisional,
(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
@@ -103,7 +107,9 @@ public interface PersonRepository extends JpaRepository<Person, UUID> {
SELECT p.id, p.title, p.first_name AS firstName, p.last_name AS lastName,
p.person_type AS personType,
p.alias, CAST(EXTRACT(YEAR FROM p.birth_date) AS int) AS birthYear,
CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear, p.notes,
CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear,
p.birth_date AS birthDate, p.birth_date_precision AS birthDatePrecision,
p.death_date AS deathDate, p.death_date_precision AS deathDatePrecision, p.notes,
p.family_member AS familyMember, p.provisional AS provisional,
(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
@@ -143,7 +149,9 @@ public interface PersonRepository extends JpaRepository<Person, UUID> {
SELECT p.id, p.title, p.first_name AS firstName, p.last_name AS lastName,
p.person_type AS personType,
p.alias, CAST(EXTRACT(YEAR FROM p.birth_date) AS int) AS birthYear,
CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear, p.notes,
CAST(EXTRACT(YEAR FROM p.death_date) AS int) AS deathYear,
p.birth_date AS birthDate, p.birth_date_precision AS birthDatePrecision,
p.death_date AS deathDate, p.death_date_precision AS deathDatePrecision, p.notes,
p.family_member AS familyMember, p.provisional AS provisional,
(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

View File

@@ -1,5 +1,8 @@
package org.raddatz.familienarchiv.person;
import org.raddatz.familienarchiv.document.DatePrecision;
import java.time.LocalDate;
import java.util.UUID;
/**
@@ -16,6 +19,13 @@ public interface PersonSummaryDTO {
String getAlias();
Integer getBirthYear();
Integer getDeathYear();
// Full date + precision alongside the derived years: list consumers that render
// precise life dates (mention dropdown) read these; year-only consumers keep
// the cheaper getBirthYear/getDeathYear.
LocalDate getBirthDate();
DatePrecision getBirthDatePrecision();
LocalDate getDeathDate();
DatePrecision getDeathDatePrecision();
String getNotes();
boolean isFamilyMember();
boolean isProvisional();

View File

@@ -217,6 +217,14 @@ class PersonControllerTest {
public String getAlias() { return null; }
public Integer getBirthYear() { return null; }
public Integer getDeathYear() { return null; }
public java.time.LocalDate getBirthDate() { return null; }
public org.raddatz.familienarchiv.document.DatePrecision getBirthDatePrecision() {
return org.raddatz.familienarchiv.document.DatePrecision.UNKNOWN;
}
public java.time.LocalDate getDeathDate() { return null; }
public org.raddatz.familienarchiv.document.DatePrecision getDeathDatePrecision() {
return org.raddatz.familienarchiv.document.DatePrecision.UNKNOWN;
}
public String getNotes() { return null; }
public boolean isFamilyMember() { return false; }
public boolean isProvisional() { return false; }

View File

@@ -970,4 +970,89 @@ class PersonRepositoryTest {
assertThat(found.get(0).getBirthYear()).isEqualTo(1920);
assertThat(found.get(0).getDeathYear()).isNull();
}
// ─── #773 follow-up: full date + precision exposed on the summary projection ──
// (the mention dropdown renders precise life dates from the list endpoint)
@Test
void findAllWithDocumentCount_exposesDateAndPrecisionFields() {
personRepository.save(Person.builder()
.firstName("Maria").lastName("Praezise")
.birthDate(LocalDate.of(1901, 3, 14)).birthDatePrecision(DatePrecision.DAY)
.deathDate(LocalDate.of(1972, 1, 1)).deathDatePrecision(DatePrecision.YEAR)
.build());
personRepository.save(Person.builder()
.firstName("Nora").lastName("Datenlos")
.build());
entityManager.flush();
List<PersonSummaryDTO> all = personRepository.findAllWithDocumentCount();
PersonSummaryDTO dated = all.stream()
.filter(p -> "Praezise".equals(p.getLastName())).findFirst().orElseThrow();
assertThat(dated.getBirthDate()).isEqualTo(LocalDate.of(1901, 3, 14));
assertThat(dated.getBirthDatePrecision()).isEqualTo(DatePrecision.DAY);
assertThat(dated.getDeathDate()).isEqualTo(LocalDate.of(1972, 1, 1));
assertThat(dated.getDeathDatePrecision()).isEqualTo(DatePrecision.YEAR);
PersonSummaryDTO undated = all.stream()
.filter(p -> "Datenlos".equals(p.getLastName())).findFirst().orElseThrow();
assertThat(undated.getBirthDate()).isNull();
assertThat(undated.getBirthDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
assertThat(undated.getDeathDate()).isNull();
assertThat(undated.getDeathDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
}
@Test
void searchWithDocumentCount_exposesDateAndPrecisionFields() {
personRepository.save(Person.builder()
.firstName("Herbert").lastName("Suchbar")
.birthDate(LocalDate.of(1899, 1, 1)).birthDatePrecision(DatePrecision.YEAR)
.deathDate(LocalDate.of(1972, 6, 12)).deathDatePrecision(DatePrecision.DAY)
.build());
entityManager.flush();
List<PersonSummaryDTO> found = personRepository.searchWithDocumentCount("Suchbar");
assertThat(found).hasSize(1);
assertThat(found.get(0).getBirthDate()).isEqualTo(LocalDate.of(1899, 1, 1));
assertThat(found.get(0).getBirthDatePrecision()).isEqualTo(DatePrecision.YEAR);
assertThat(found.get(0).getDeathDate()).isEqualTo(LocalDate.of(1972, 6, 12));
assertThat(found.get(0).getDeathDatePrecision()).isEqualTo(DatePrecision.DAY);
}
@Test
void findTopByDocumentCount_exposesDateAndPrecisionFields() {
personRepository.save(Person.builder()
.firstName("Top").lastName("Dokumentiert")
.birthDate(LocalDate.of(1910, 5, 1)).birthDatePrecision(DatePrecision.MONTH)
.build());
entityManager.flush();
List<PersonSummaryDTO> top = personRepository.findTopByDocumentCount(10);
PersonSummaryDTO found = top.stream()
.filter(p -> "Dokumentiert".equals(p.getLastName())).findFirst().orElseThrow();
assertThat(found.getBirthDate()).isEqualTo(LocalDate.of(1910, 5, 1));
assertThat(found.getBirthDatePrecision()).isEqualTo(DatePrecision.MONTH);
assertThat(found.getDeathDate()).isNull();
assertThat(found.getDeathDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
}
@Test
void findByFilter_exposesDateAndPrecisionFields() {
personRepository.save(Person.builder()
.firstName("Gefiltert").lastName("Genau")
.deathDate(LocalDate.of(1944, 11, 2)).deathDatePrecision(DatePrecision.DAY)
.build());
entityManager.flush();
List<PersonSummaryDTO> found = personRepository.findByFilter(
null, null, null, null, false, "Gefiltert", 10, 0);
assertThat(found).hasSize(1);
assertThat(found.get(0).getBirthDate()).isNull();
assertThat(found.get(0).getBirthDatePrecision()).isEqualTo(DatePrecision.UNKNOWN);
assertThat(found.get(0).getDeathDate()).isEqualTo(LocalDate.of(1944, 11, 2));
assertThat(found.get(0).getDeathDatePrecision()).isEqualTo(DatePrecision.DAY);
}
}