diff --git a/frontend/e2e/auth.spec.ts b/frontend/e2e/auth.spec.ts index da5c4118..9d3315ef 100644 --- a/frontend/e2e/auth.spec.ts +++ b/frontend/e2e/auth.spec.ts @@ -24,7 +24,7 @@ test.describe('Authentication', () => { }); test('protected routes redirect to /login without session', async ({ page }) => { - for (const url of ['/documents/new', '/persons', '/conversations']) { + for (const url of ['/documents/new', '/persons', '/briefwechsel']) { await page.goto(url); await expect(page).toHaveURL(/\/login/); } diff --git a/frontend/e2e/persons.spec.ts b/frontend/e2e/persons.spec.ts index 1b5aed2d..e392d4e8 100644 --- a/frontend/e2e/persons.spec.ts +++ b/frontend/e2e/persons.spec.ts @@ -181,132 +181,3 @@ test.describe('Person detail — sent and received documents', () => { // If no person has dated documents, the test is a no-op (year range is optional) }); }); - -test.describe('Person detail — conversations link', () => { - test('co-correspondent chips link to conversations pre-filled with both persons', async ({ - page - }) => { - await page.goto('/persons'); - const firstLink = page.locator('a[href^="/persons/"]:not([href="/persons/new"])').first(); - const href = await firstLink.getAttribute('href'); - const personId = href!.split('/persons/')[1]; - await firstLink.click(); - await page.waitForSelector('[data-hydrated]'); - - // Co-correspondent chips link to /conversations?senderId=X&receiverId=Y - const chip = page.locator(`a[href^="/conversations?senderId=${personId}&receiverId="]`).first(); - if ((await chip.count()) > 0) { - const chipHref = await chip.getAttribute('href'); - expect(chipHref).toMatch(/\/conversations\?senderId=.+&receiverId=.+/); - } - }); -}); - -test.describe('Conversations', () => { - test('shows the empty state when no persons are selected', async ({ page }) => { - await page.goto('/conversations'); - await expect(page.getByText(/Wählen Sie zwei Personen aus/i)).toBeVisible(); - await page.screenshot({ path: 'test-results/e2e/conversations-empty.png' }); - }); - - test('nav link is active on the conversations page', async ({ page }) => { - await page.goto('/conversations'); - const navLink = page.getByRole('link', { name: 'Konversationen' }); - await expect(navLink).toHaveClass(/bg-nav-active/); - }); - - test('sort toggle changes the button label', async ({ page }) => { - await page.goto('/conversations'); - await page.waitForSelector('[data-hydrated]'); - const btn = page.getByRole('button', { name: /Sortierung/i }); - await expect(btn).toContainText('Neueste zuerst'); - await btn.click(); - await expect(page).toHaveURL(/dir=ASC/); - await expect(btn).toContainText('Älteste zuerst'); - await page.screenshot({ path: 'test-results/e2e/conversations-sort.png' }); - }); -}); - -test.describe('Conversations — enhancements', () => { - // Hans→Anna (1923) and Anna→Hans (1965) are seeded in DataInitializer - // Navigate directly by URL so the test doesn't rely on typeahead interaction - async function loadHansAnnaConversation(page: import('@playwright/test').Page) { - // Resolve person IDs from the persons list - await page.goto('/persons'); - const hansLink = page.getByRole('link', { name: /Hans Müller/ }).first(); - const hansHref = await hansLink.getAttribute('href'); - const hansId = hansHref!.split('/').pop()!; - - const annaLink = page.getByRole('link', { name: /Anna Schmidt/ }).first(); - const annaHref = await annaLink.getAttribute('href'); - const annaId = annaHref!.split('/').pop()!; - - await page.goto(`/conversations?senderId=${hansId}&receiverId=${annaId}`); - await page.waitForURL(/senderId=/); - } - - test('shows document count and year range summary when both persons are selected', async ({ - page - }) => { - await loadHansAnnaConversation(page); - // Hans→Anna (1923) + Anna→Hans (1965) = 2 documents, range 1923–1965 - await expect(page.getByTestId('conv-summary')).toContainText('2'); - await expect(page.getByTestId('conv-summary')).toContainText('1923'); - await expect(page.getByTestId('conv-summary')).toContainText('1965'); - await page.screenshot({ path: 'test-results/e2e/conversations-summary.png' }); - }); - - test('shows year dividers between documents from different years', async ({ page }) => { - await loadHansAnnaConversation(page); - // Expect at least two year dividers (1923 and 1965) - await expect(page.getByTestId('year-divider').first()).toBeVisible(); - const dividers = page.getByTestId('year-divider'); - const texts = await dividers.allTextContents(); - expect(texts.some((t) => t.includes('1923'))).toBe(true); - expect(texts.some((t) => t.includes('1965'))).toBe(true); - await page.screenshot({ path: 'test-results/e2e/conversations-year-dividers.png' }); - }); - - test('swap button switches sender and receiver and reloads', async ({ page }) => { - await loadHansAnnaConversation(page); - const url = new URL(page.url()); - const originalSenderId = url.searchParams.get('senderId')!; - const originalReceiverId = url.searchParams.get('receiverId')!; - - await page.getByTestId('conv-swap-btn').click(); - // Wait for the URL to reflect the swapped IDs (not just any URL with senderId=) - await page.waitForURL( - (url) => new URL(url).searchParams.get('senderId') === originalReceiverId - ); - - const swappedUrl = new URL(page.url()); - expect(swappedUrl.searchParams.get('senderId')).toBe(originalReceiverId); - expect(swappedUrl.searchParams.get('receiverId')).toBe(originalSenderId); - await page.screenshot({ path: 'test-results/e2e/conversations-swap.png' }); - }); - - test('shows "new document" link pre-filled with both persons when conversation is loaded', async ({ - page - }) => { - await loadHansAnnaConversation(page); - const url = new URL(page.url()); - const senderId = url.searchParams.get('senderId')!; - const receiverId = url.searchParams.get('receiverId')!; - - const link = page.getByTestId('conv-new-doc-link'); - await expect(link).toBeVisible(); - const href = await link.getAttribute('href'); - expect(href).toContain(`senderId=${senderId}`); - expect(href).toContain(`receiverId=${receiverId}`); - await page.screenshot({ path: 'test-results/e2e/conversations-new-doc-link.png' }); - }); - - test('does not show swap button or new document link when only one person is selected', async ({ - page - }) => { - await page.goto('/conversations'); - await page.waitForURL('/conversations'); - await expect(page.getByTestId('conv-swap-btn')).not.toBeVisible(); - await expect(page.getByTestId('conv-new-doc-link')).not.toBeVisible(); - }); -}); diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 3ba37d97..20dba446 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -136,8 +136,6 @@ "person_co_correspondents_heading": "Häufige Korrespondenten", "person_correspondents_hint": "klicken für Konversation", "person_show_more": "+ {count} weitere anzeigen", - "conv_heading": "Briefwechsel", - "conv_subtitle": "Briefwechsel einer Person durchsuchen — mit oder ohne Korrespondent.", "conv_label_person_a": "Person A (Absender)", "conv_label_person_b": "Korrespondent", "conv_label_from": "Zeitraum von", @@ -146,30 +144,18 @@ "conv_sort_newest": "Neueste zuerst", "conv_sort_oldest": "Älteste zuerst", "conv_empty_heading": "Wessen Briefe möchten Sie lesen?", - "conv_empty_text": "Wähle eine Person aus dem Archiv um deren Briefe zu sehen — mit oder ohne Korrespondent.", "conv_hero_crosslink": "Suchen Sie ein bestimmtes Dokument? → Zur Dokumentensuche", "conv_no_results_heading": "Keine Dokumente gefunden.", "conv_no_results_text": "Versuchen Sie, den Zeitraum anzupassen.", "conv_swap_btn": "Personen tauschen", - "conv_summary": "{count} Dokumente · {yearFrom}–{yearTo}", "conv_new_doc_link": "Neues Dokument in diesem Briefwechsel", - "conv_label_correspondent_optional": "Korrespondent", - "conv_hint_single_person": "Alle Briefe von {name} — wähle einen Korrespondenten oben um einzugrenzen", - "conv_hint_single_person_filtered": "Alle Briefe von {name} · {from}–{to} · {sortLabel}", - "conv_strip_period": "Zeitraum", - "conv_strip_from_placeholder": "Von…", - "conv_strip_to_placeholder": "Bis…", - "conv_strip_all_correspondents": "Alle Korrespondenten", "conv_strip_sort_newest": "Neueste", "conv_strip_sort_oldest": "Älteste", "conv_suggestions_heading": "Häufigste Korrespondenten", "conv_suggestions_all_label": "Alle Korrespondenten von {name}", "conv_letters_count": "{count} Briefe", - "conv_empty_search_placeholder": "Person suchen…", "conv_hero_divider": "oder", "conv_empty_recent_label": "Zuletzt geöffnet", - "conv_asym_sent": "{count} von {name} →", - "conv_asym_received": "{count} von {name} ←", "conv_no_party": "—", "admin_heading": "Admin Dashboard", "admin_tab_users": "Benutzer", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 9ffc814c..901adb29 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -136,8 +136,6 @@ "person_co_correspondents_heading": "Frequent correspondents", "person_correspondents_hint": "click to view conversation", "person_show_more": "+ {count} more", - "conv_heading": "Letters", - "conv_subtitle": "Browse a person's letters — with or without a correspondent.", "conv_label_person_a": "Person A (Sender)", "conv_label_person_b": "Correspondent", "conv_label_from": "Period from", @@ -146,30 +144,18 @@ "conv_sort_newest": "Newest first", "conv_sort_oldest": "Oldest first", "conv_empty_heading": "Whose letters would you like to read?", - "conv_empty_text": "Choose a person from the archive to see their letters — with or without a correspondent.", "conv_hero_crosslink": "Looking for a specific document? → Go to document search", "conv_no_results_heading": "No documents found.", "conv_no_results_text": "Try adjusting the time period.", "conv_swap_btn": "Swap persons", - "conv_summary": "{count} documents · {yearFrom}–{yearTo}", "conv_new_doc_link": "New document in this exchange", - "conv_label_correspondent_optional": "Correspondent", - "conv_hint_single_person": "All letters from {name} — choose a correspondent above to narrow down", - "conv_hint_single_person_filtered": "All letters from {name} · {from}–{to} · {sortLabel}", - "conv_strip_period": "Period", - "conv_strip_from_placeholder": "From…", - "conv_strip_to_placeholder": "To…", - "conv_strip_all_correspondents": "All correspondents", "conv_strip_sort_newest": "Newest", "conv_strip_sort_oldest": "Oldest", "conv_suggestions_heading": "Top correspondents", "conv_suggestions_all_label": "All correspondents of {name}", "conv_letters_count": "{count} letters", - "conv_empty_search_placeholder": "Search person…", "conv_hero_divider": "or", "conv_empty_recent_label": "Recently opened", - "conv_asym_sent": "{count} from {name} →", - "conv_asym_received": "{count} from {name} ←", "conv_no_party": "—", "admin_heading": "Admin Dashboard", "admin_tab_users": "Users", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 213bf53e..53d4f1a0 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -136,8 +136,6 @@ "person_co_correspondents_heading": "Corresponsales frecuentes", "person_correspondents_hint": "clic para ver conversación", "person_show_more": "+ {count} más", - "conv_heading": "Cartas", - "conv_subtitle": "Explore las cartas de una persona — con o sin corresponsal.", "conv_label_person_a": "Persona A (Remitente)", "conv_label_person_b": "Corresponsal", "conv_label_from": "Período desde", @@ -146,30 +144,18 @@ "conv_sort_newest": "Más reciente primero", "conv_sort_oldest": "Más antiguo primero", "conv_empty_heading": "¿De quién desea leer las cartas?", - "conv_empty_text": "Elige una persona del archivo para ver sus cartas — con o sin corresponsal.", "conv_hero_crosslink": "¿Busca un documento en particular? → Ir a la búsqueda", "conv_no_results_heading": "No se encontraron documentos.", "conv_no_results_text": "Intente ajustar el período de tiempo.", "conv_swap_btn": "Intercambiar personas", - "conv_summary": "{count} documentos · {yearFrom}–{yearTo}", "conv_new_doc_link": "Nuevo documento en este intercambio", - "conv_label_correspondent_optional": "Corresponsal", - "conv_hint_single_person": "Todas las cartas de {name} — elige un corresponsal arriba para filtrar", - "conv_hint_single_person_filtered": "Todas las cartas de {name} · {from}–{to} · {sortLabel}", - "conv_strip_period": "Período", - "conv_strip_from_placeholder": "Desde…", - "conv_strip_to_placeholder": "Hasta…", - "conv_strip_all_correspondents": "Todos los corresponsales", "conv_strip_sort_newest": "Más reciente", "conv_strip_sort_oldest": "Más antiguo", "conv_suggestions_heading": "Corresponsales frecuentes", "conv_suggestions_all_label": "Todos los corresponsales de {name}", "conv_letters_count": "{count} cartas", - "conv_empty_search_placeholder": "Buscar persona…", "conv_hero_divider": "o", "conv_empty_recent_label": "Recientemente abiertos", - "conv_asym_sent": "{count} de {name} →", - "conv_asym_received": "{count} de {name} ←", "conv_no_party": "—", "admin_heading": "Panel de administración", "admin_tab_users": "Usuarios", diff --git a/frontend/src/routes/conversations/+page.server.ts b/frontend/src/routes/conversations/+page.server.ts deleted file mode 100644 index d6f4cfd6..00000000 --- a/frontend/src/routes/conversations/+page.server.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { components } from '$lib/generated/api'; -import { createApiClient } from '$lib/api.server'; - -export async function load({ url, fetch }) { - const senderId = url.searchParams.get('senderId') || ''; - const receiverId = url.searchParams.get('receiverId') || ''; - const from = url.searchParams.get('from') || ''; - const to = url.searchParams.get('to') || ''; - const dir = url.searchParams.get('dir') || 'DESC'; - - const api = createApiClient(fetch); - - let documents: components['schemas']['Document'][] = []; - let senderName = ''; - let receiverName = ''; - - const requests: Promise[] = []; - - if (senderId && receiverId) { - requests.push( - api - .GET('/api/documents/conversation', { - params: { - query: { - senderId, - receiverId, - dir, - from: from || undefined, - to: to || undefined - } - } - }) - .then(({ data }) => { - documents = data ?? []; - }) - ); - } - - if (senderId) { - requests.push( - api.GET('/api/persons/{id}', { params: { path: { id: senderId } } }).then(({ data }) => { - const p = data as { displayName: string } | undefined; - if (p) senderName = p.displayName; - }) - ); - } - - if (receiverId) { - requests.push( - api.GET('/api/persons/{id}', { params: { path: { id: receiverId } } }).then(({ data }) => { - const p = data as { displayName: string } | undefined; - if (p) receiverName = p.displayName; - }) - ); - } - - await Promise.all(requests); - - return { - documents, - initialValues: { senderName, receiverName }, - filters: { senderId, receiverId, from, to, dir } - }; -} diff --git a/frontend/src/routes/conversations/+page.svelte b/frontend/src/routes/conversations/+page.svelte deleted file mode 100644 index 05b4b522..00000000 --- a/frontend/src/routes/conversations/+page.svelte +++ /dev/null @@ -1,104 +0,0 @@ - - -
- -
-

{m.conv_heading()}

-

- {m.conv_subtitle()} -

-
- - - - - {#if !senderId || !receiverId} -
-
- -
-

{m.conv_empty_heading()}

-

{m.conv_empty_text()}

-
- {:else if data.documents.length === 0} -
-

{m.conv_no_results_heading()}

-

{m.conv_no_results_text()}

-
- {:else} - - {/if} -
diff --git a/frontend/src/routes/conversations/ConversationFilterBar.svelte b/frontend/src/routes/conversations/ConversationFilterBar.svelte deleted file mode 100644 index da7b33c1..00000000 --- a/frontend/src/routes/conversations/ConversationFilterBar.svelte +++ /dev/null @@ -1,142 +0,0 @@ - - -
-
- -
- onapplyFilters()} - /> -
- - -
- -
- - -
- onapplyFilters()} - /> -
-
- -
- -
- - onapplyFilters()} - class="block w-full border-line py-2.5 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" - /> -
- - -
- - onapplyFilters()} - class="block w-full border-line py-2.5 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" - /> -
- - -
- -
-
-
diff --git a/frontend/src/routes/conversations/ConversationTimeline.svelte b/frontend/src/routes/conversations/ConversationTimeline.svelte deleted file mode 100644 index ecc9d14a..00000000 --- a/frontend/src/routes/conversations/ConversationTimeline.svelte +++ /dev/null @@ -1,160 +0,0 @@ - - - -
- {#if yearFrom !== null && yearTo !== null} -

- {m.conv_summary({ count: documents.length, yearFrom, yearTo })} -

- {:else} -

- {documents.length} -

- {/if} - {#if canWrite} - - - - - {m.conv_new_doc_link()} - - {/if} -
- - -
- - - -
-
- {#each documentGroups as group (group.label)} - {#if group.label} - - {/if} - {#each group.documents as doc (doc.id)} - {@const isRight = doc.sender?.id === senderId} - - -
- -
- - - - - - -
-

- {doc.title || doc.originalFilename} -

- - - - -
- - -
- - {doc.documentDate ? formatDate(doc.documentDate) : '—'} - - {#if doc.location} - - • {doc.location} - - {/if} -
-
-
-
- {/each} - {/each} -
-
-
diff --git a/frontend/src/routes/conversations/page.svelte.spec.ts b/frontend/src/routes/conversations/page.svelte.spec.ts deleted file mode 100644 index 3b3ba356..00000000 --- a/frontend/src/routes/conversations/page.svelte.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { cleanup, render } from 'vitest-browser-svelte'; -import { page } from 'vitest/browser'; -import Page from './+page.svelte'; - -vi.mock('$app/navigation', () => ({ goto: vi.fn() })); - -afterEach(cleanup); - -// ─── Test data ──────────────────────────────────────────────────────────────── - -const baseData = { - user: undefined, - canWrite: true, - canAnnotate: false, - documents: [], - initialValues: { senderName: '', receiverName: '' }, - filters: { senderId: '', receiverId: '', from: '', to: '', dir: 'DESC' as const } -}; - -const withPersons = { - ...baseData, - filters: { ...baseData.filters, senderId: 'p1', receiverId: 'p2' } -}; - -const makeDoc = (overrides: Record = {}) => ({ - id: 'd1', - title: 'Testbrief', - originalFilename: 'testbrief.pdf', - status: 'UPLOADED' as const, - documentDate: '1923-04-12', - location: 'Berlin', - sender: { id: 'p1', firstName: 'Hans', lastName: 'Müller' }, - receivers: [{ id: 'p2', firstName: 'Anna', lastName: 'Schmidt' }], - tags: [], - transcription: undefined, - filePath: undefined, - createdAt: '1923-04-12T00:00:00Z', - updatedAt: '1923-04-12T00:00:00Z', - ...overrides -}); - -const withDocs = { - ...withPersons, - documents: [makeDoc()] -}; - -// ─── Empty state ────────────────────────────────────────────────────────────── - -describe('Conversations page – empty state', () => { - it('shows the empty-state heading when no persons are selected', async () => { - render(Page, { data: baseData }); - await expect.element(page.getByText(/Wessen Briefe möchten Sie lesen/i)).toBeInTheDocument(); - }); - - it('hides the swap button when no persons are selected', async () => { - render(Page, { data: baseData }); - // Button is always in the DOM (holds grid column width on desktop) but made invisible - await expect.element(page.getByTestId('conv-swap-btn')).toHaveClass('invisible'); - }); - - it('does not show the new document link when no persons are selected', async () => { - render(Page, { data: baseData }); - await expect.element(page.getByTestId('conv-new-doc-link')).not.toBeInTheDocument(); - }); -}); - -// ─── No results ─────────────────────────────────────────────────────────────── - -describe('Conversations page – no results', () => { - it('shows "no documents found" when both persons are selected but there are no documents', async () => { - render(Page, { data: withPersons }); - await expect.element(page.getByText(/Keine Dokumente gefunden/i)).toBeInTheDocument(); - }); -}); - -// ─── Swap button ────────────────────────────────────────────────────────────── - -describe('Conversations page – swap button', () => { - it('shows the swap button when both persons are selected', async () => { - render(Page, { data: withPersons }); - await expect.element(page.getByTestId('conv-swap-btn')).not.toHaveClass('invisible'); - }); - - it('calls goto with swapped sender and receiver when clicked', async () => { - const { goto } = await import('$app/navigation'); - vi.mocked(goto).mockClear(); - render(Page, { data: withPersons }); - document.querySelector('[data-testid="conv-swap-btn"]')!.click(); - expect(goto).toHaveBeenCalledWith(expect.stringContaining('senderId=p2'), expect.anything()); - expect(goto).toHaveBeenCalledWith(expect.stringContaining('receiverId=p1'), expect.anything()); - }); -}); - -// ─── Summary ────────────────────────────────────────────────────────────────── - -describe('Conversations page – summary', () => { - it('shows document count and year range when documents are loaded', async () => { - const data = { - ...withPersons, - documents: [ - makeDoc({ documentDate: '1923-04-12' }), - makeDoc({ id: 'd2', documentDate: '1965-08-03' }) - ] - }; - render(Page, { data }); - const summary = page.getByTestId('conv-summary'); - await expect.element(summary).toHaveTextContent('2'); - await expect.element(summary).toHaveTextContent('1923'); - await expect.element(summary).toHaveTextContent('1965'); - }); -}); - -// ─── Year dividers ──────────────────────────────────────────────────────────── - -describe('Conversations page – year dividers', () => { - it('renders a year divider for the first document', async () => { - render(Page, { data: withDocs }); - await expect.element(page.getByTestId('group-divider').first()).toHaveTextContent('1923'); - }); - - it('renders a divider for each new year in the document list', async () => { - const data = { - ...withPersons, - documents: [ - makeDoc({ documentDate: '1923-04-12' }), - makeDoc({ id: 'd2', documentDate: '1965-08-03' }) - ] - }; - render(Page, { data }); - await expect.element(page.getByTestId('group-divider').first()).toHaveTextContent('1923'); - await expect.element(page.getByTestId('group-divider').nth(1)).toHaveTextContent('1965'); - }); - - it('does not render a second divider for documents from the same year', async () => { - const data = { - ...withPersons, - documents: [ - makeDoc({ documentDate: '1923-04-12' }), - makeDoc({ id: 'd2', documentDate: '1923-09-01' }) - ] - }; - render(Page, { data }); - // Only one divider for 1923; 1965 divider should not appear - await expect.element(page.getByTestId('group-divider').first()).toHaveTextContent('1923'); - await expect.element(page.getByTestId('group-divider').nth(1)).not.toBeInTheDocument(); - }); -}); - -// ─── New document link ──────────────────────────────────────────────────────── - -describe('Conversations page – new document link', () => { - it('shows the link with correct href for a write user', async () => { - render(Page, { data: { ...withDocs, canWrite: true } }); - const link = page.getByTestId('conv-new-doc-link'); - await expect.element(link).toBeInTheDocument(); - await expect.element(link).toHaveAttribute('href', '/documents/new?senderId=p1&receiverId=p2'); - }); - - it('hides the link for a read-only user', async () => { - render(Page, { data: { ...withDocs, canWrite: false } }); - await expect.element(page.getByTestId('conv-new-doc-link')).not.toBeInTheDocument(); - }); -});