diff --git a/backend/src/main/java/org/raddatz/familienarchiv/timeline/Kind.java b/backend/src/main/java/org/raddatz/familienarchiv/timeline/Kind.java
new file mode 100644
index 00000000..4ac07440
--- /dev/null
+++ b/backend/src/main/java/org/raddatz/familienarchiv/timeline/Kind.java
@@ -0,0 +1,7 @@
+package org.raddatz.familienarchiv.timeline;
+
+/** Discriminates curated/derived events from archive letters in {@link TimelineEntryDTO}. */
+public enum Kind {
+ EVENT,
+ LETTER
+}
diff --git a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEntryDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEntryDTO.java
index 9f729be9..44cf88ec 100644
--- a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEntryDTO.java
+++ b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEntryDTO.java
@@ -4,28 +4,39 @@ import io.swagger.v3.oas.annotations.media.Schema;
import org.raddatz.familienarchiv.document.DatePrecision;
import java.time.LocalDate;
+import java.util.List;
+import java.util.UUID;
/**
- * Unified DTO for timeline entries — covers both curated {@link TimelineEvent} rows
- * ({@code derived=false}) and derived life-events assembled from Person/relationship data
- * ({@code derived=true}).
+ * Unified DTO for timeline entries — covers curated {@link TimelineEvent} rows, derived
+ * life-events ({@link DerivedEventType}), and archive letters (Documents).
*
- *
The {@code id} field is typed {@code String}, not {@code UUID}, because derived events
- * carry synthetic prefixed ids ({@code birth:{uuid}}, {@code death:{uuid}},
- * {@code marriage:{uuid}}) that are structurally non-UUID by construction. Any write endpoint
- * must reject ids that do not parse as {@code UUID} — enforced and tested in issue #5.
+ *
Edit-affordance contract (for issue #7): {@code derived == true || eventId == null}
+ * means no edit link should be rendered by the frontend.
*
- *
Callers of {@code TimelineService.assembleDerivedEvents()} must independently enforce
+ *
Letter display fields: {@code senderName} — {@code ""} means unknown/unlinked
+ * correspondent; frontend renders {@code 'Unbekannt'} fallback. Only populated for
+ * {@link Kind#LETTER} entries.
+ *
+ *
Type field: {@code null} for {@link Kind#LETTER} entries; frontend must not render
+ * an event-type badge for letters.
+ *
+ *
Callers of {@code TimelineEventService.assembleDerivedEvents()} must independently enforce
* {@code READ_ALL} authorization before invoking that method (see ADR-043).
*/
public record TimelineEntryDTO(
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String id,
- @Schema(requiredMode = Schema.RequiredMode.REQUIRED) EventType type,
- LocalDate eventDate,
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) Kind kind,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) DatePrecision precision,
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) boolean derived,
- DerivedEventType derivedType,
- String primaryPersonName,
- String relatedPersonName
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String senderName,
+ @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String receiverName,
+ LocalDate eventDate,
+ LocalDate eventDateEnd,
+ String title,
+ EventType type,
+ UUID eventId,
+ UUID documentId,
+ List linkedPersonIds,
+ DerivedEventType derivedType
) {
}
diff --git a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEventService.java b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEventService.java
index 3a50d4bf..75803bb0 100644
--- a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEventService.java
+++ b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEventService.java
@@ -263,14 +263,10 @@ public class TimelineEventService {
return persons.stream()
.filter(p -> p.getBirthDate() != null)
.map(p -> new TimelineEntryDTO(
- "birth:" + p.getId(),
- EventType.PERSONAL,
- p.getBirthDate(),
- p.getBirthDatePrecision(),
- true,
- DerivedEventType.BIRTH,
- p.getDisplayName(),
- null))
+ Kind.EVENT, p.getBirthDatePrecision(), true, "", "",
+ p.getBirthDate(), null,
+ p.getDisplayName(), EventType.PERSONAL,
+ null, null, List.of(p.getId()), DerivedEventType.BIRTH))
.toList();
}
@@ -278,14 +274,10 @@ public class TimelineEventService {
return persons.stream()
.filter(p -> p.getDeathDate() != null)
.map(p -> new TimelineEntryDTO(
- "death:" + p.getId(),
- EventType.PERSONAL,
- p.getDeathDate(),
- p.getDeathDatePrecision(),
- true,
- DerivedEventType.DEATH,
- p.getDisplayName(),
- null))
+ Kind.EVENT, p.getDeathDatePrecision(), true, "", "",
+ p.getDeathDate(), null,
+ p.getDisplayName(), EventType.PERSONAL,
+ null, null, List.of(p.getId()), DerivedEventType.DEATH))
.toList();
}
@@ -303,15 +295,15 @@ public class TimelineEventService {
DatePrecision precision = r.getFromYear() != null
? DatePrecision.YEAR
: DatePrecision.UNKNOWN;
+ String title = r.getPerson().getDisplayName()
+ + " & " + r.getRelatedPerson().getDisplayName();
result.add(new TimelineEntryDTO(
- "marriage:" + r.getId(),
- EventType.PERSONAL,
- eventDate,
- precision,
- true,
- DerivedEventType.MARRIAGE,
- r.getPerson().getDisplayName(),
- r.getRelatedPerson().getDisplayName()));
+ Kind.EVENT, precision, true, "", "",
+ eventDate, null,
+ title, EventType.PERSONAL,
+ null, null,
+ List.of(r.getPerson().getId(), r.getRelatedPerson().getId()),
+ DerivedEventType.MARRIAGE));
}
}
return result;
diff --git a/backend/src/test/java/org/raddatz/familienarchiv/timeline/DerivedEventsAssemblyTest.java b/backend/src/test/java/org/raddatz/familienarchiv/timeline/DerivedEventsAssemblyTest.java
index 9566b1fd..ddef7bd6 100644
--- a/backend/src/test/java/org/raddatz/familienarchiv/timeline/DerivedEventsAssemblyTest.java
+++ b/backend/src/test/java/org/raddatz/familienarchiv/timeline/DerivedEventsAssemblyTest.java
@@ -18,7 +18,6 @@ import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -108,7 +107,7 @@ class DerivedEventsAssemblyTest {
assertThat(event.derivedType()).isEqualTo(DerivedEventType.BIRTH);
assertThat(event.eventDate()).isEqualTo(LocalDate.of(1901, 3, 12));
assertThat(event.precision()).isEqualTo(DatePrecision.DAY);
- assertThat(event.primaryPersonName()).isEqualTo(anna.getDisplayName());
+ assertThat(event.title()).isEqualTo(anna.getDisplayName());
}
// --- REQ-003: null birthDate → no Geburt event ---
@@ -162,7 +161,7 @@ class DerivedEventsAssemblyTest {
assertThat(event.derivedType()).isEqualTo(DerivedEventType.DEATH);
assertThat(event.eventDate()).isEqualTo(LocalDate.of(1965, 7, 4));
assertThat(event.precision()).isEqualTo(DatePrecision.DAY);
- assertThat(event.primaryPersonName()).isEqualTo(hans.getDisplayName());
+ assertThat(event.title()).isEqualTo(hans.getDisplayName());
}
// --- REQ-002 + REQ-003 combined ---
@@ -285,10 +284,10 @@ class DerivedEventsAssemblyTest {
List result = service.assembleDerivedEvents();
assertThat(result).hasSize(1);
- String id = result.get(0).id();
- assertThat(id).startsWith("birth:");
- assertThatThrownBy(() -> UUID.fromString(id))
- .isInstanceOf(IllegalArgumentException.class);
+ TimelineEntryDTO entry = result.get(0);
+ assertThat(entry.derived()).isTrue();
+ assertThat(entry.eventId()).isNull();
+ assertThat(entry.documentId()).isNull();
}
// --- REQ-010: display names on Heirat ---
@@ -307,8 +306,8 @@ class DerivedEventsAssemblyTest {
assertThat(heiraten).hasSize(1);
TimelineEntryDTO heirat = heiraten.get(0);
- assertThat(heirat.primaryPersonName()).isNotNull().isNotBlank();
- assertThat(heirat.relatedPersonName()).isNotNull().isNotBlank();
+ assertThat(heirat.title()).isNotNull().isNotBlank();
+ assertThat(heirat.linkedPersonIds()).hasSize(2);
}
// --- REQ-007 note: assumption/documentation test ---