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_role_receiver": "Empfangen",
"person_co_correspondents_heading": "Häufige Korrespondenten", "person_co_correspondents_heading": "Häufige Korrespondenten",
"person_correspondents_hint": "klicken für Konversation", "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", "person_show_more": "+ {count} weitere anzeigen",
"conv_label_person_a": "Person A (Absender)", "conv_label_person_a": "Person A (Absender)",
"conv_label_person_b": "Korrespondent", "conv_label_person_b": "Korrespondent",

View File

@@ -190,6 +190,9 @@
"person_role_receiver": "Received", "person_role_receiver": "Received",
"person_co_correspondents_heading": "Frequent correspondents", "person_co_correspondents_heading": "Frequent correspondents",
"person_correspondents_hint": "click to view conversation", "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", "person_show_more": "+ {count} more",
"conv_label_person_a": "Person A (Sender)", "conv_label_person_a": "Person A (Sender)",
"conv_label_person_b": "Correspondent", "conv_label_person_b": "Correspondent",

View File

@@ -190,6 +190,9 @@
"person_role_receiver": "Recibido", "person_role_receiver": "Recibido",
"person_co_correspondents_heading": "Corresponsales frecuentes", "person_co_correspondents_heading": "Corresponsales frecuentes",
"person_correspondents_hint": "clic para ver conversación", "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", "person_show_more": "+ {count} más",
"conv_label_person_a": "Persona A (Remitente)", "conv_label_person_a": "Persona A (Remitente)",
"conv_label_person_b": "Corresponsal", "conv_label_person_b": "Corresponsal",

View File

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

View File

@@ -3,10 +3,12 @@ import { m } from '$lib/paraglide/messages.js';
let { let {
coCorrespondents, coCorrespondents,
personId personId,
personName
}: { }: {
coCorrespondents: { id: string; name: string; count: number }[]; coCorrespondents: { id: string; name: string; count: number }[];
personId: string; personId: string;
personName: string;
} = $props(); } = $props();
function initials(name: string): string { 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"> <h3 class="text-[10px] font-bold tracking-widest text-ink-3 uppercase">
{m.person_co_correspondents_heading()} {m.person_co_correspondents_heading()}
</h3> </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>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{#each coCorrespondents as c (c.id)} {#each coCorrespondents as c (c.id)}
<a <a
href="/briefwechsel?senderId={personId}&receiverId={c.id}" href="/documents?senderId={personId}&receiverId={c.id}"
title={m.doc_conversation_title()} 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" 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 --> <!-- Initials circle -->
@@ -41,8 +45,11 @@ function initials(name: string): string {
{initials(c.name)} {initials(c.name)}
</span> </span>
{c.name} {c.name}
<span class="text-[10px] font-normal text-ink-3">×{c.count}</span> <span
<!-- Chat icon --> class="text-[10px] font-normal text-ink-3"
title={m.person_correspondents_badge_title()}>×{c.count}</span
>
<!-- Search icon -->
<svg <svg
class="h-3 w-3 flex-shrink-0 text-ink-3" class="h-3 w-3 flex-shrink-0 text-ink-3"
fill="none" fill="none"
@@ -54,7 +61,7 @@ function initials(name: string): string {
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
stroke-width="2" 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> </svg>
</a> </a>

View File

@@ -1,6 +1,7 @@
import { describe, it, expect, afterEach } from 'vitest'; import { describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte'; import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser'; import { page } from 'vitest/browser';
import { m } from '$lib/paraglide/messages.js';
import CoCorrespondentsList from './CoCorrespondentsList.svelte'; import CoCorrespondentsList from './CoCorrespondentsList.svelte';
afterEach(cleanup); afterEach(cleanup);
@@ -8,7 +9,7 @@ afterEach(cleanup);
describe('CoCorrespondentsList', () => { describe('CoCorrespondentsList', () => {
it('renders nothing when the coCorrespondents list is empty', async () => { it('renders nothing when the coCorrespondents list is empty', async () => {
render(CoCorrespondentsList, { render(CoCorrespondentsList, {
props: { coCorrespondents: [], personId: 'p-1' } props: { coCorrespondents: [], personId: 'p-1', personName: 'Anna Schmidt' }
}); });
await expect await expect
@@ -16,18 +17,19 @@ describe('CoCorrespondentsList', () => {
.not.toBeInTheDocument(); .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, { render(CoCorrespondentsList, {
props: { props: {
coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann', count: 3 }], coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann', count: 3 }],
personId: 'p-1' personId: 'p-1',
personName: 'Anna Schmidt'
} }
}); });
await expect await expect
.element(page.getByRole('heading', { name: /häufige korrespondenten/i })) .element(page.getByRole('heading', { name: /häufige korrespondenten/i }))
.toBeVisible(); .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 () => { 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-1', name: 'Max Mustermann', count: 3 },
{ id: 'c-2', name: 'Erika Beispiel', count: 1 } { 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(); 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, { render(CoCorrespondentsList, {
props: { props: {
coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann', count: 3 }], coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann', count: 3 }],
personId: 'p-1' personId: 'p-1',
personName: 'Anna Schmidt'
} }
}); });
await expect await expect
.element(page.getByRole('link', { name: /max mustermann/i })) .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 () => { it('builds initials from up to two name parts', async () => {
render(CoCorrespondentsList, { render(CoCorrespondentsList, {
props: { props: {
coCorrespondents: [{ id: 'c-1', name: 'Max Mustermann Beispiel', count: 1 }], 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, { render(CoCorrespondentsList, {
props: { props: {
coCorrespondents: [{ id: 'c-1', name: 'Cher', count: 2 }], 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, { render(CoCorrespondentsList, {
props: { props: {
coCorrespondents: [{ id: 'c-1', name: ' Max', count: 1 }], coCorrespondents: [{ id: 'c-1', name: ' Max', count: 1 }],
personId: 'p-1' personId: 'p-1',
personName: 'Anna Schmidt'
} }
}); });