From ed270f68e1c2578f8582d49b1effb47c4223a9bd Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 2 May 2026 18:01:19 +0200 Subject: [PATCH] feat(geschichten): wire discovery integrations on Person and Document pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Person detail (/persons/[id]): - Server load fetches GET /api/geschichten?status=PUBLISHED&personId={id} in parallel with the existing person/document queries. - Renders below the received-documents list when the person has at least one published story. Document detail (/documents/[id]): - Server load adds the same parallel call with documentId={id}. - DocumentTopBar gains geschichten + canBlogWrite props that flow through to DocumentMetadataDrawer. - DocumentMetadataDrawer's grid expands to lg:grid-cols-4 when the Geschichten column should appear (stories exist OR user can author), and shows "+ Geschichte anhängen" / "Alle anzeigen" links following the >= 3-story threshold from issue comment #5758. Co-Authored-By: Claude Opus 4.7 --- .../components/DocumentMetadataDrawer.svelte | 79 ++++++++++++++++++- .../src/lib/components/DocumentTopBar.svelte | 16 +++- .../src/routes/documents/[id]/+page.server.ts | 10 ++- .../src/routes/documents/[id]/+page.svelte | 2 + .../src/routes/persons/[id]/+page.server.ts | 9 ++- frontend/src/routes/persons/[id]/+page.svelte | 12 +++ 6 files changed, 121 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/components/DocumentMetadataDrawer.svelte b/frontend/src/lib/components/DocumentMetadataDrawer.svelte index ff8e177c..e30f87c8 100644 --- a/frontend/src/lib/components/DocumentMetadataDrawer.svelte +++ b/frontend/src/lib/components/DocumentMetadataDrawer.svelte @@ -7,6 +7,12 @@ import RelationshipPill from '$lib/components/RelationshipPill.svelte'; type Person = { id: string; firstName?: string | null; lastName: string; displayName: string }; type Tag = { id: string; name: string }; +type GeschichteSummary = { + id: string; + title: string; + publishedAt?: string; + author?: { firstName?: string; lastName?: string; email: string }; +}; type Props = { documentDate: string | null; @@ -16,6 +22,9 @@ type Props = { receivers: Person[]; tags: Tag[]; inferredRelationship?: { labelFromA: string; labelFromB: string } | null; + geschichten?: GeschichteSummary[]; + documentId?: string; + canBlogWrite?: boolean; }; let { @@ -25,10 +34,30 @@ let { sender, receivers, tags, - inferredRelationship = null + inferredRelationship = null, + geschichten = [], + documentId, + canBlogWrite = false }: Props = $props(); const VISIBLE_RECEIVER_LIMIT = 5; +const VISIBLE_GESCHICHTEN_LIMIT = 3; +const showGeschichtenColumn = $derived(geschichten.length > 0 || canBlogWrite); +const visibleGeschichten = $derived(geschichten.slice(0, VISIBLE_GESCHICHTEN_LIMIT)); +const hasGeschichtenOverflow = $derived(geschichten.length >= VISIBLE_GESCHICHTEN_LIMIT); +const gridClass = $derived(showGeschichtenColumn ? 'lg:grid-cols-4' : 'lg:grid-cols-3'); + +function formatGeschichteAuthor(g: GeschichteSummary): string { + const a = g.author; + if (!a) return ''; + const full = [a.firstName, a.lastName].filter(Boolean).join(' ').trim(); + return full || a.email || ''; +} + +function formatGeschichteDate(g: GeschichteSummary): string { + if (!g.publishedAt) return ''; + return formatDate(g.publishedAt.slice(0, 10), 'short'); +} const formattedDate = $derived(documentDate ? formatDate(documentDate) : '—'); const displayLocation = $derived(location ?? '—'); @@ -67,7 +96,7 @@ function getFullName(person: Person): string { {/snippet}
-
+

@@ -159,5 +188,51 @@ function getFullName(person: Person): string {

{m.doc_details_no_tags()}

{/if}

+ + + {#if showGeschichtenColumn} +
+
+

+ {m.geschichten_card_heading()} +

+ {#if canBlogWrite && documentId} + + {m.geschichten_card_attach_action()} + + {/if} +
+ + {#if geschichten.length === 0} +

+ {:else} +
    + {#each visibleGeschichten as g (g.id)} +
  • + + {g.title} + +

    + {formatGeschichteAuthor(g)} + {#if formatGeschichteDate(g)}· {formatGeschichteDate(g)}{/if} +

    +
  • + {/each} +
+ + {#if hasGeschichtenOverflow && documentId} + + {m.geschichten_card_show_all()} → + + {/if} + {/if} +
+ {/if}
diff --git a/frontend/src/lib/components/DocumentTopBar.svelte b/frontend/src/lib/components/DocumentTopBar.svelte index 15cadd6b..5e480a55 100644 --- a/frontend/src/lib/components/DocumentTopBar.svelte +++ b/frontend/src/lib/components/DocumentTopBar.svelte @@ -25,12 +25,21 @@ type Doc = { tags?: Tag[] | null; }; +type GeschichteSummary = { + id: string; + title: string; + publishedAt?: string; + author?: { firstName?: string; lastName?: string; email: string }; +}; + type Props = { doc: Doc; canWrite: boolean; fileUrl: string; transcribeMode: boolean; inferredRelationship?: { labelFromA: string; labelFromB: string } | null; + geschichten?: GeschichteSummary[]; + canBlogWrite?: boolean; }; let { @@ -38,7 +47,9 @@ let { canWrite, fileUrl, transcribeMode = $bindable(), - inferredRelationship = null + inferredRelationship = null, + geschichten = [], + canBlogWrite = false }: Props = $props(); let detailsOpen = $state(false); @@ -283,6 +294,9 @@ let mobileMenuOpen = $state(false); receivers={doc.receivers ? [...doc.receivers] : []} tags={doc.tags ? [...doc.tags] : []} inferredRelationship={inferredRelationship} + geschichten={geschichten} + documentId={doc.id} + canBlogWrite={canBlogWrite} />
{/if} diff --git a/frontend/src/routes/documents/[id]/+page.server.ts b/frontend/src/routes/documents/[id]/+page.server.ts index e601dd44..6f3d297b 100644 --- a/frontend/src/routes/documents/[id]/+page.server.ts +++ b/frontend/src/routes/documents/[id]/+page.server.ts @@ -7,7 +7,12 @@ export async function load({ params, fetch }) { const { id } = params; const api = createApiClient(fetch); - const docResult = await api.GET('/api/documents/{id}', { params: { path: { id } } }); + const [docResult, geschichtenResult] = await Promise.all([ + api.GET('/api/documents/{id}', { params: { path: { id } } }), + api.GET('/api/geschichten', { + params: { query: { status: 'PUBLISHED', documentId: id } } + }) + ]); if (docResult.response.status === 401) throw redirect(302, '/login'); @@ -18,8 +23,9 @@ export async function load({ params, fetch }) { const document = docResult.data!; const inferredRelationship = await loadInferredRelationship(api, document); + const geschichten = geschichtenResult.data ?? []; - return { document, inferredRelationship }; + return { document, inferredRelationship, geschichten }; } async function loadInferredRelationship( diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index 69f24b50..49810b90 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -424,6 +424,8 @@ onMount(() => { fileUrl={fileLoader.fileUrl} bind:transcribeMode={transcribeMode} inferredRelationship={data.inferredRelationship} + geschichten={data.geschichten ?? []} + canBlogWrite={data.canBlogWrite ?? false} />
diff --git a/frontend/src/routes/persons/[id]/+page.server.ts b/frontend/src/routes/persons/[id]/+page.server.ts index 33b3ae74..1fa9db55 100644 --- a/frontend/src/routes/persons/[id]/+page.server.ts +++ b/frontend/src/routes/persons/[id]/+page.server.ts @@ -17,14 +17,18 @@ export async function load({ params, fetch, locals }) { receivedDocsResult, aliasesResult, relsResult, - inferredResult + inferredResult, + geschichtenResult ] = 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}/relationships', { params: { path: { id } } }), - api.GET('/api/persons/{id}/inferred-relationships', { params: { path: { id } } }) + api.GET('/api/persons/{id}/inferred-relationships', { params: { path: { id } } }), + api.GET('/api/geschichten', { + params: { query: { status: 'PUBLISHED', personId: id } } + }) ]); if (!personResult.response.ok) { @@ -39,6 +43,7 @@ export async function load({ params, fetch, locals }) { aliases: aliasesResult.data ?? [], relationships: relsResult.data ?? [], inferredRelationships: inferredResult.data ?? [], + geschichten: geschichtenResult.data ?? [], canWrite }; } diff --git a/frontend/src/routes/persons/[id]/+page.svelte b/frontend/src/routes/persons/[id]/+page.svelte index 58c398b4..3a627892 100644 --- a/frontend/src/routes/persons/[id]/+page.svelte +++ b/frontend/src/routes/persons/[id]/+page.svelte @@ -7,6 +7,7 @@ import NameHistoryCard from './NameHistoryCard.svelte'; import CoCorrespondentsList from './CoCorrespondentsList.svelte'; import PersonDocumentList from './PersonDocumentList.svelte'; import PersonRelationshipsCard from './PersonRelationshipsCard.svelte'; +import GeschichtenCard from '$lib/components/GeschichtenCard.svelte'; let { data } = $props(); @@ -92,6 +93,17 @@ const coCorrespondents = $derived.by(() => { emptyMessage={m.person_no_received_docs()} />
+ + {#if data.geschichten && data.geschichten.length > 0} +
+ +
+ {/if}