From d7f4f6f16330e231a1a79bf84c8ecba66c2c9fe9 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 27 Apr 2026 14:58:43 +0200 Subject: [PATCH] feat(stammbaum): person detail Beziehungen card - persons/[id]/+page.server.ts loads relationships and inferred-relationships in the existing parallel fetch. - New PersonRelationshipsCard renders direct chips (mint) and the top-5 derived chips (grey) on /persons/{id}, both linked to the other person's page. Empty state shows "Noch keine Beziehungen bekannt." in muted serif. - Card sits in the right column above the document lists. Refs #358. Co-Authored-By: Claude Sonnet 4.6 --- .../src/routes/persons/[id]/+page.server.ts | 15 ++- frontend/src/routes/persons/[id]/+page.svelte | 11 +- .../[id]/PersonRelationshipsCard.svelte | 100 ++++++++++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 frontend/src/routes/persons/[id]/PersonRelationshipsCard.svelte diff --git a/frontend/src/routes/persons/[id]/+page.server.ts b/frontend/src/routes/persons/[id]/+page.server.ts index 3c5f0f5c..33b3ae74 100644 --- a/frontend/src/routes/persons/[id]/+page.server.ts +++ b/frontend/src/routes/persons/[id]/+page.server.ts @@ -11,11 +11,20 @@ export async function load({ params, fetch, locals }) { g.permissions.includes('WRITE_ALL') ) ?? false; - const [personResult, sentDocsResult, receivedDocsResult, aliasesResult] = await Promise.all([ + const [ + personResult, + sentDocsResult, + receivedDocsResult, + aliasesResult, + relsResult, + inferredResult + ] = await Promise.all([ api.GET('/api/persons/{id}', { params: { path: { id } } }), api.GET('/api/persons/{id}/documents', { params: { path: { id } } }), api.GET('/api/persons/{id}/received-documents', { params: { path: { id } } }), - api.GET('/api/persons/{id}/aliases', { params: { path: { id } } }) + api.GET('/api/persons/{id}/aliases', { params: { path: { id } } }), + api.GET('/api/persons/{id}/relationships', { params: { path: { id } } }), + api.GET('/api/persons/{id}/inferred-relationships', { params: { path: { id } } }) ]); if (!personResult.response.ok) { @@ -28,6 +37,8 @@ export async function load({ params, fetch, locals }) { sentDocuments: sentDocsResult.data ?? [], receivedDocuments: receivedDocsResult.data ?? [], aliases: aliasesResult.data ?? [], + relationships: relsResult.data ?? [], + inferredRelationships: inferredResult.data ?? [], canWrite }; } diff --git a/frontend/src/routes/persons/[id]/+page.svelte b/frontend/src/routes/persons/[id]/+page.svelte index 64978be9..9a9cc581 100644 --- a/frontend/src/routes/persons/[id]/+page.svelte +++ b/frontend/src/routes/persons/[id]/+page.svelte @@ -6,6 +6,7 @@ import PersonCard from './PersonCard.svelte'; import NameHistoryCard from './NameHistoryCard.svelte'; import CoCorrespondentsList from './CoCorrespondentsList.svelte'; import PersonDocumentList from './PersonDocumentList.svelte'; +import PersonRelationshipsCard from './PersonRelationshipsCard.svelte'; let { data } = $props(); @@ -64,10 +65,18 @@ const coCorrespondents = $derived.by(() => { - +
+
+ +
+ +import { m } from '$lib/paraglide/messages.js'; +import { inferredRelationshipLabel } from '$lib/relationshipLabels'; +import type { components } from '$lib/generated/api'; + +type RelationshipDTO = components['schemas']['RelationshipDTO']; +type InferredRelationshipWithPersonDTO = components['schemas']['InferredRelationshipWithPersonDTO']; + +interface Props { + personId: string; + relationships: RelationshipDTO[]; + inferredRelationships: InferredRelationshipWithPersonDTO[]; +} + +let { personId, relationships, inferredRelationships }: Props = $props(); + +const topDerived = $derived(inferredRelationships.slice(0, 5)); + +function chipLabel(rel: RelationshipDTO): string { + const viewpointIsSubject = rel.personId === personId; + switch (rel.relationType) { + case 'PARENT_OF': + return viewpointIsSubject ? m.relation_parent_of() : m.relation_child_of(); + case 'SPOUSE_OF': + return m.relation_spouse_of(); + case 'SIBLING_OF': + return m.relation_sibling_of(); + case 'FRIEND': + return m.relation_friend(); + case 'COLLEAGUE': + return m.relation_colleague(); + case 'EMPLOYER': + return m.relation_employer(); + case 'DOCTOR': + return m.relation_doctor(); + case 'NEIGHBOR': + return m.relation_neighbor(); + default: + return m.relation_other(); + } +} + +function otherId(rel: RelationshipDTO): string { + return rel.personId === personId ? rel.relatedPersonId : rel.personId; +} + +function otherName(rel: RelationshipDTO): string { + return rel.personId === personId ? rel.relatedPersonDisplayName : rel.personDisplayName; +} + + +
+

+ {m.person_relationships_heading()} +

+ + {#if relationships.length === 0 && topDerived.length === 0} +

{m.person_relationships_empty()}

+ {:else} + {#if relationships.length > 0} + + {/if} + + {#if topDerived.length > 0} + + {/if} + {/if} +