feat(persons): retarget frequent-correspondents card to document search

The "Häufige Korrespondenten" card linked into the standalone Briefwechsel
view. Retarget each chip to the existing document search pre-filtered by
sender and receiver (/documents?senderId=A&receiverId=B), naming both
persons in a search-action title, swapping the chat-bubble icon for a
magnifier, and clarifying that the ×N badge counts shared letters in both
directions (not the unidirectional search result count).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-02 20:32:52 +02:00
parent 03e2615fa7
commit 2c23c75d89
6 changed files with 60 additions and 19 deletions

View File

@@ -190,6 +190,9 @@
"person_role_receiver": "Empfangen",
"person_co_correspondents_heading": "Häufige Korrespondenten",
"person_correspondents_hint": "klicken für Konversation",
"person_correspondents_search_title": "Briefe von {A} an {B} durchsuchen",
"person_correspondents_search_hint": "klicken, um Briefe zu durchsuchen",
"person_correspondents_badge_title": "Gemeinsame Briefe in beide Richtungen",
"person_show_more": "+ {count} weitere anzeigen",
"conv_label_person_a": "Person A (Absender)",
"conv_label_person_b": "Korrespondent",

View File

@@ -190,6 +190,9 @@
"person_role_receiver": "Received",
"person_co_correspondents_heading": "Frequent correspondents",
"person_correspondents_hint": "click to view conversation",
"person_correspondents_search_title": "Search letters from {A} to {B}",
"person_correspondents_search_hint": "click to search letters",
"person_correspondents_badge_title": "Shared letters in both directions",
"person_show_more": "+ {count} more",
"conv_label_person_a": "Person A (Sender)",
"conv_label_person_b": "Correspondent",

View File

@@ -190,6 +190,9 @@
"person_role_receiver": "Recibido",
"person_co_correspondents_heading": "Corresponsales frecuentes",
"person_correspondents_hint": "clic para ver conversación",
"person_correspondents_search_title": "Buscar cartas de {A} a {B}",
"person_correspondents_search_hint": "haz clic para buscar cartas",
"person_correspondents_badge_title": "Cartas compartidas en ambas direcciones",
"person_show_more": "+ {count} más",
"conv_label_person_a": "Persona A (Remitente)",
"conv_label_person_b": "Corresponsal",

View File

@@ -68,7 +68,11 @@ const coCorrespondents = $derived.by(() => {
<!-- Right column: correspondents + relationships + documents -->
<div>
<CoCorrespondentsList coCorrespondents={coCorrespondents} personId={person.id} />
<CoCorrespondentsList
coCorrespondents={coCorrespondents}
personId={person.id}
personName={person.displayName}
/>
<div class="mt-6">
<PersonRelationshipsCard

View File

@@ -3,10 +3,12 @@ import { m } from '$lib/paraglide/messages.js';
let {
coCorrespondents,
personId
personId,
personName
}: {
coCorrespondents: { id: string; name: string; count: number }[];
personId: string;
personName: string;
} = $props();
function initials(name: string): string {
@@ -25,13 +27,15 @@ function initials(name: string): string {
<h3 class="text-[10px] font-bold tracking-widest text-ink-3 uppercase">
{m.person_co_correspondents_heading()}
</h3>
<span class="font-sans text-[10px] text-ink-3 italic">{m.person_correspondents_hint()}</span>
<span class="font-sans text-[10px] text-ink-3 italic"
>{m.person_correspondents_search_hint()}</span
>
</div>
<div class="flex flex-wrap gap-2">
{#each coCorrespondents as c (c.id)}
<a
href="/briefwechsel?senderId={personId}&receiverId={c.id}"
title={m.doc_conversation_title()}
href="/documents?senderId={personId}&receiverId={c.id}"
title={m.person_correspondents_search_title({ A: personName, B: c.name })}
class="inline-flex items-center gap-1.5 rounded-full border border-line bg-muted px-3 py-1.5 font-sans text-xs font-bold text-ink transition-colors hover:border-primary hover:bg-surface"
>
<!-- Initials circle -->
@@ -41,8 +45,11 @@ function initials(name: string): string {
{initials(c.name)}
</span>
{c.name}
<span class="text-[10px] font-normal text-ink-3">×{c.count}</span>
<!-- Chat icon -->
<span
class="text-[10px] font-normal text-ink-3"
title={m.person_correspondents_badge_title()}>×{c.count}</span
>
<!-- Search icon -->
<svg
class="h-3 w-3 flex-shrink-0 text-ink-3"
fill="none"
@@ -54,7 +61,7 @@ function initials(name: string): string {
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
</a>

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import { m } from '$lib/paraglide/messages.js';
import CoCorrespondentsList from './CoCorrespondentsList.svelte';
afterEach(cleanup);
@@ -8,7 +9,7 @@ afterEach(cleanup);
describe('CoCorrespondentsList', () => {
it('renders nothing when the coCorrespondents list is empty', async () => {
render(CoCorrespondentsList, {
props: { coCorrespondents: [], personId: 'p-1' }
props: { coCorrespondents: [], personId: 'p-1', personName: 'Anna Schmidt' }
});
await expect
@@ -16,18 +17,19 @@ describe('CoCorrespondentsList', () => {
.not.toBeInTheDocument();
});
it('renders the heading and hint when there is at least one co-correspondent', async () => {
it('renders the heading and search hint when there is at least one co-correspondent', async () => {
render(CoCorrespondentsList, {
props: {
coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann', count: 3 }],
personId: 'p-1'
personId: 'p-1',
personName: 'Anna Schmidt'
}
});
await expect
.element(page.getByRole('heading', { name: /häufige korrespondenten/i }))
.toBeVisible();
await expect.element(page.getByText('klicken für Konversation')).toBeVisible();
await expect.element(page.getByText(m.person_correspondents_search_hint())).toBeVisible();
});
it('renders one chip per co-correspondent with name and count', async () => {
@@ -37,7 +39,8 @@ describe('CoCorrespondentsList', () => {
{ id: 'c-1', name: 'Max Mustermann', count: 3 },
{ id: 'c-2', name: 'Erika Beispiel', count: 1 }
],
personId: 'p-1'
personId: 'p-1',
personName: 'Anna Schmidt'
}
});
@@ -47,24 +50,40 @@ describe('CoCorrespondentsList', () => {
await expect.element(page.getByText('×1')).toBeVisible();
});
it('points each chip to the bilateral conversation route with the correct ids', async () => {
it('points each chip to the document search pre-filtered by sender and receiver', async () => {
render(CoCorrespondentsList, {
props: {
coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann', count: 3 }],
personId: 'p-1'
personId: 'p-1',
personName: 'Anna Schmidt'
}
});
await expect
.element(page.getByRole('link', { name: /max mustermann/i }))
.toHaveAttribute('href', '/briefwechsel?senderId=p-1&receiverId=c-1');
.toHaveAttribute('href', '/documents?senderId=p-1&receiverId=c-1');
});
it('labels the link as a search action naming both persons', async () => {
render(CoCorrespondentsList, {
props: {
coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann', count: 3 }],
personId: 'p-1',
personName: 'Anna Schmidt'
}
});
const link = page.getByRole('link', { name: /max mustermann/i });
await expect.element(link).toHaveAttribute('title', /Anna Schmidt/);
await expect.element(link).toHaveAttribute('title', /Max Mustermann/);
});
it('builds initials from up to two name parts', async () => {
render(CoCorrespondentsList, {
props: {
coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann Beispiel', count: 1 }],
personId: 'p-1'
personId: 'p-1',
personName: 'Anna Schmidt'
}
});
@@ -75,7 +94,8 @@ describe('CoCorrespondentsList', () => {
render(CoCorrespondentsList, {
props: {
coCorrespondents: [{ id: 'c-1', name: 'Cher', count: 2 }],
personId: 'p-1'
personId: 'p-1',
personName: 'Anna Schmidt'
}
});
@@ -88,7 +108,8 @@ describe('CoCorrespondentsList', () => {
render(CoCorrespondentsList, {
props: {
coCorrespondents: [{ id: 'c-1', name: ' Max', count: 1 }],
personId: 'p-1'
personId: 'p-1',
personName: 'Anna Schmidt'
}
});