From 55ffaa1c5c4754e798db2c992082ce214c4d145b Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 20 Mar 2026 09:39:50 +0100 Subject: [PATCH] feat(person): show received docs, role badges, stats bar, co-correspondents - Split document list into Gesendete / Empfangene Dokumente sections - Add role badges (Gesendet / Empfangen) on each document card - Add statistics strip showing total count and year range - Add co-correspondents section with frequency-sorted chips - Single sort toggle applies to both sections Closes #1 Closes #19 Closes #21 Closes #22 Co-Authored-By: Claude Sonnet 4.6 --- frontend/e2e/persons.spec.ts | 13 ++ frontend/src/routes/persons/[id]/+page.svelte | 176 +++++++++++++++--- 2 files changed, 166 insertions(+), 23 deletions(-) diff --git a/frontend/e2e/persons.spec.ts b/frontend/e2e/persons.spec.ts index 2a2a577c..736e7fee 100644 --- a/frontend/e2e/persons.spec.ts +++ b/frontend/e2e/persons.spec.ts @@ -114,6 +114,19 @@ test.describe('Person detail — sort toggle', () => { }); }); +test.describe('Person detail — sent and received documents', () => { + test('shows both sent and received document sections', async ({ page }) => { + await page.goto('/persons'); + const firstPerson = page.locator('a[href^="/persons/"]:not([href="/persons/new"])').first(); + await firstPerson.click(); + await page.waitForSelector('[data-hydrated]'); + + await expect(page.getByRole('heading', { name: /Gesendete Dokumente/i })).toBeVisible(); + await expect(page.getByRole('heading', { name: /Empfangene Dokumente/i })).toBeVisible(); + await page.screenshot({ path: 'test-results/e2e/person-sent-received.png' }); + }); +}); + test.describe('Person detail — conversations link', () => { test('has a conversations link that pre-fills the person', async ({ page }) => { await page.goto('/persons'); diff --git a/frontend/src/routes/persons/[id]/+page.svelte b/frontend/src/routes/persons/[id]/+page.svelte index 605c848b..c50a5034 100644 --- a/frontend/src/routes/persons/[id]/+page.svelte +++ b/frontend/src/routes/persons/[id]/+page.svelte @@ -7,10 +7,48 @@ let { data, form } = $props(); const person = $derived(data.person); - const documents = $derived(data.documents); + const sentDocuments = $derived(data.sentDocuments); + const receivedDocuments = $derived(data.receivedDocuments); let sortDir = $state('DESC'); - const sortedDocuments = $derived(sortDocumentsByDate(documents, sortDir)); + const sortedSentDocuments = $derived(sortDocumentsByDate(sentDocuments, sortDir)); + const sortedReceivedDocuments = $derived(sortDocumentsByDate(receivedDocuments, sortDir)); + + const allDocuments = $derived([...sentDocuments, ...receivedDocuments]); + + const docStats = $derived(() => { + const dated = allDocuments.filter(d => d.documentDate); + const years = dated.map(d => parseInt(d.documentDate!.substring(0, 4))); + return { + total: allDocuments.length, + minYear: years.length ? Math.min(...years) : null, + maxYear: years.length ? Math.max(...years) : null, + }; + }); + + const coCorrespondents = $derived(() => { + const freq = new Map(); + + for (const doc of sentDocuments) { + for (const receiver of doc.receivers ?? []) { + const key = receiver.id; + const existing = freq.get(key); + if (existing) existing.count++; + else freq.set(key, { id: receiver.id, name: `${receiver.firstName} ${receiver.lastName}`, count: 1 }); + } + } + + for (const doc of receivedDocuments) { + if (doc.sender && doc.sender.id !== person.id) { + const key = doc.sender.id; + const existing = freq.get(key); + if (existing) existing.count++; + else freq.set(key, { id: doc.sender.id, name: `${doc.sender.firstName} ${doc.sender.lastName}`, count: 1 }); + } + } + + return [...freq.values()].sort((a, b) => b.count - a.count).slice(0, 5); + }); let editMode = $state(false); let mergeTargetId = $state(''); @@ -261,33 +299,66 @@ {/key} - -
-
-
-

{m.person_docs_heading()}

- - {documents.length} - -
- {#if documents.length > 0} - + + {#if coCorrespondents().length > 0} +
+

{m.person_co_correspondents_heading()}

+
+ {#each coCorrespondents() as c} + + {c.name} + ({c.count}) + + {/each} +
+
+ {/if} + + + {#if docStats().total > 0} +
+ {docStats().total} Dokumente + {#if docStats().minYear !== null} + · + {#if docStats().minYear === docStats().maxYear} + {docStats().minYear} + {:else} + {docStats().minYear} – {docStats().maxYear} {/if} + {/if} +
+ {/if} + + + {#if allDocuments.length > 0} +
+ +
+ {/if} + + +
+
+

{m.person_docs_heading()}

+ + {sentDocuments.length} +
- {#if documents.length === 0} + {#if sentDocuments.length === 0}

{m.person_no_docs()}

{:else}
-
+
+ - + +
+
+
+ + {/each} + + {/if} +
+ + +
+
+

{m.person_received_docs_heading()}

+ + {receivedDocuments.length} + +
+ + {#if receivedDocuments.length === 0} +
+

{m.person_no_received_docs()}

+
+ {:else} +