feat(stammbaum): show inferred relationship in the document drawer
- New presentational RelationshipBadge component (labelFromA → arrow → labelFromB) wired into DocumentMetadataDrawer's Personen column, rendered after the receivers block when both endpoints are family members. - DocumentTopBar gains an optional inferredRelationship prop and passes it through. - documents/[id]/+page.server.ts loads the badge: only when sender is a family member, exactly one receiver, and that receiver is also a family member; 404 (no path) → null. - relationshipLabels.ts maps the backend label keys (parent/child/...) to localised strings, so the server load returns badge-ready strings. Refs #358. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { m } from '$lib/paraglide/messages.js';
|
||||
import { formatDate } from '$lib/utils/date';
|
||||
import { formatDocumentStatus } from '$lib/utils/documentStatusLabel';
|
||||
import { getInitials, personAvatarColor } from '$lib/utils/personFormat';
|
||||
import RelationshipBadge from '$lib/components/RelationshipBadge.svelte';
|
||||
|
||||
type Person = { id: string; firstName?: string | null; lastName: string; displayName: string };
|
||||
type Tag = { id: string; name: string };
|
||||
@@ -14,9 +15,18 @@ type Props = {
|
||||
sender: Person | null;
|
||||
receivers: Person[];
|
||||
tags: Tag[];
|
||||
inferredRelationship?: { labelFromA: string; labelFromB: string } | null;
|
||||
};
|
||||
|
||||
let { documentDate, location, status, sender, receivers, tags }: Props = $props();
|
||||
let {
|
||||
documentDate,
|
||||
location,
|
||||
status,
|
||||
sender,
|
||||
receivers,
|
||||
tags,
|
||||
inferredRelationship = null
|
||||
}: Props = $props();
|
||||
|
||||
const VISIBLE_RECEIVER_LIMIT = 5;
|
||||
|
||||
@@ -112,6 +122,12 @@ function getFullName(person: Person): string {
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if inferredRelationship}
|
||||
<RelationshipBadge
|
||||
labelFromA={inferredRelationship.labelFromA}
|
||||
labelFromB={inferredRelationship.labelFromB}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="font-serif text-sm text-ink-3">{m.doc_details_no_persons()}</p>
|
||||
|
||||
@@ -30,9 +30,16 @@ type Props = {
|
||||
canWrite: boolean;
|
||||
fileUrl: string;
|
||||
transcribeMode: boolean;
|
||||
inferredRelationship?: { labelFromA: string; labelFromB: string } | null;
|
||||
};
|
||||
|
||||
let { doc, canWrite, fileUrl, transcribeMode = $bindable() }: Props = $props();
|
||||
let {
|
||||
doc,
|
||||
canWrite,
|
||||
fileUrl,
|
||||
transcribeMode = $bindable(),
|
||||
inferredRelationship = null
|
||||
}: Props = $props();
|
||||
|
||||
let detailsOpen = $state(false);
|
||||
|
||||
@@ -275,6 +282,7 @@ let mobileMenuOpen = $state(false);
|
||||
sender={doc.sender ?? null}
|
||||
receivers={doc.receivers ? [...doc.receivers] : []}
|
||||
tags={doc.tags ? [...doc.tags] : []}
|
||||
inferredRelationship={inferredRelationship}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
26
frontend/src/lib/components/RelationshipBadge.svelte
Normal file
26
frontend/src/lib/components/RelationshipBadge.svelte
Normal file
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
|
||||
type Props = { labelFromA: string; labelFromB: string };
|
||||
let { labelFromA, labelFromB }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p class="mb-1 font-sans text-xs font-medium text-ink-3">
|
||||
{m.doc_details_field_relationship()}
|
||||
</p>
|
||||
<div class="flex items-center gap-1.5 px-2 font-serif text-sm text-ink">
|
||||
<span class="font-semibold">{labelFromA}</span>
|
||||
<svg
|
||||
class="h-3 w-3 shrink-0 text-accent"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13 7l5 5-5 5M6 12h12" />
|
||||
</svg>
|
||||
<span class="font-semibold">{labelFromB}</span>
|
||||
</div>
|
||||
</div>
|
||||
44
frontend/src/lib/relationshipLabels.ts
Normal file
44
frontend/src/lib/relationshipLabels.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import * as m from '$lib/paraglide/messages.js';
|
||||
|
||||
/**
|
||||
* Maps a backend inferred-label key (parent, uncle_aunt, ...) to its
|
||||
* localised string. Unknown keys fall back to "distant".
|
||||
*/
|
||||
export function inferredRelationshipLabel(key: string): string {
|
||||
switch (key) {
|
||||
case 'parent':
|
||||
return m.relation_inferred_parent();
|
||||
case 'child':
|
||||
return m.relation_inferred_child();
|
||||
case 'spouse':
|
||||
return m.relation_inferred_spouse();
|
||||
case 'sibling':
|
||||
return m.relation_inferred_sibling();
|
||||
case 'grandparent':
|
||||
return m.relation_inferred_grandparent();
|
||||
case 'grandchild':
|
||||
return m.relation_inferred_grandchild();
|
||||
case 'great_grandparent':
|
||||
return m.relation_inferred_great_grandparent();
|
||||
case 'great_grandchild':
|
||||
return m.relation_inferred_great_grandchild();
|
||||
case 'uncle_aunt':
|
||||
return m.relation_inferred_uncle_aunt();
|
||||
case 'niece_nephew':
|
||||
return m.relation_inferred_niece_nephew();
|
||||
case 'great_uncle_aunt':
|
||||
return m.relation_inferred_great_uncle_aunt();
|
||||
case 'great_niece_nephew':
|
||||
return m.relation_inferred_great_niece_nephew();
|
||||
case 'inlaw_parent':
|
||||
return m.relation_inferred_inlaw_parent();
|
||||
case 'inlaw_child':
|
||||
return m.relation_inferred_inlaw_child();
|
||||
case 'sibling_inlaw':
|
||||
return m.relation_inferred_sibling_inlaw();
|
||||
case 'cousin_1':
|
||||
return m.relation_inferred_cousin_1();
|
||||
default:
|
||||
return m.relation_inferred_distant();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import { createApiClient } from '$lib/api.server';
|
||||
import { getErrorMessage } from '$lib/errors';
|
||||
import { inferredRelationshipLabel } from '$lib/relationshipLabels';
|
||||
|
||||
export async function load({ params, fetch }) {
|
||||
const { id } = params;
|
||||
@@ -15,5 +16,38 @@ export async function load({ params, fetch }) {
|
||||
throw error(docResult.response.status, getErrorMessage(code));
|
||||
}
|
||||
|
||||
return { document: docResult.data! };
|
||||
const document = docResult.data!;
|
||||
const inferredRelationship = await loadInferredRelationship(api, document);
|
||||
|
||||
return { document, inferredRelationship };
|
||||
}
|
||||
|
||||
async function loadInferredRelationship(
|
||||
api: ReturnType<typeof createApiClient>,
|
||||
document: {
|
||||
sender?: { id: string; familyMember?: boolean } | null;
|
||||
receivers?: { id: string; familyMember?: boolean }[];
|
||||
}
|
||||
): Promise<{ labelFromA: string; labelFromB: string } | null> {
|
||||
const sender = document.sender;
|
||||
const receivers = document.receivers ?? [];
|
||||
|
||||
// The badge is shown only when both endpoints are family members and the
|
||||
// document has exactly one receiver.
|
||||
if (!sender?.familyMember) return null;
|
||||
if (receivers.length !== 1) return null;
|
||||
const receiver = receivers[0];
|
||||
if (!receiver?.familyMember) return null;
|
||||
|
||||
const result = await api.GET('/api/persons/{aId}/relationship-to/{bId}', {
|
||||
params: { path: { aId: sender.id, bId: receiver.id } }
|
||||
});
|
||||
|
||||
if (result.response.status === 404) return null;
|
||||
if (!result.response.ok || !result.data) return null;
|
||||
|
||||
return {
|
||||
labelFromA: inferredRelationshipLabel(result.data.labelFromA),
|
||||
labelFromB: inferredRelationshipLabel(result.data.labelFromB)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -395,6 +395,7 @@ onMount(() => {
|
||||
canWrite={canWrite}
|
||||
fileUrl={fileLoader.fileUrl}
|
||||
bind:transcribeMode={transcribeMode}
|
||||
inferredRelationship={data.inferredRelationship}
|
||||
/>
|
||||
|
||||
<div class="relative flex-1 overflow-hidden {transcribeMode ? 'flex flex-col md:flex-row' : ''}">
|
||||
|
||||
Reference in New Issue
Block a user