diff --git a/frontend/e2e/persons.spec.ts b/frontend/e2e/persons.spec.ts index 071d2af4..78e43c27 100644 --- a/frontend/e2e/persons.spec.ts +++ b/frontend/e2e/persons.spec.ts @@ -72,14 +72,35 @@ test.describe('New person', () => { }); }); +test.describe('Person detail — sort toggle', () => { + test('sort toggle changes the button label when person has documents', async ({ page }) => { + await page.goto('/persons'); + const firstPerson = page.locator('a[href^="/persons/"]').first(); + await firstPerson.click(); + await page.waitForSelector('[data-hydrated]'); + + const sortBtn = page.getByRole('button', { name: /Neueste zuerst|Älteste zuerst/i }); + if (await sortBtn.isVisible()) { + await expect(sortBtn).toContainText('Neueste zuerst'); + await sortBtn.click(); + await expect(sortBtn).toContainText('Älteste zuerst'); + await sortBtn.click(); + await expect(sortBtn).toContainText('Neueste zuerst'); + await page.screenshot({ path: 'test-results/e2e/person-sort-toggle.png' }); + } + }); +}); + test.describe('Person detail — conversations link', () => { test('has a conversations link that pre-fills the person', async ({ page }) => { await page.goto('/persons'); - const firstLink = page.locator('a[href^="/persons/"]').first(); + // Exclude /persons/new to avoid matching the "New person" button + 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(); - const convLink = page.getByRole('link', { name: /Konversationen/i }); + // Use the specific person-detail link text, not the nav "Konversationen" link + const convLink = page.getByRole('link', { name: /Konversationen anzeigen/i }); await expect(convLink).toBeVisible(); await expect(convLink).toHaveAttribute('href', `/conversations?senderId=${personId}`); }); diff --git a/frontend/src/lib/utils/sort.spec.ts b/frontend/src/lib/utils/sort.spec.ts new file mode 100644 index 00000000..488fd8d8 --- /dev/null +++ b/frontend/src/lib/utils/sort.spec.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest'; +import { sortDocumentsByDate } from './sort'; + +const doc = (id: string, documentDate: string | null) => + ({ id, documentDate } as { id: string; documentDate: string | null }); + +describe('sortDocumentsByDate', () => { + it('sorts DESC by default — newest first', () => { + const docs = [doc('a', '1920-01-01'), doc('b', '1950-06-15'), doc('c', '1935-03-10')]; + const result = sortDocumentsByDate(docs, 'DESC'); + expect(result.map((d) => d.id)).toEqual(['b', 'c', 'a']); + }); + + it('sorts ASC — oldest first', () => { + const docs = [doc('a', '1920-01-01'), doc('b', '1950-06-15'), doc('c', '1935-03-10')]; + const result = sortDocumentsByDate(docs, 'ASC'); + expect(result.map((d) => d.id)).toEqual(['a', 'c', 'b']); + }); + + it('places documents without a date last in DESC', () => { + const docs = [doc('a', null), doc('b', '1940-01-01'), doc('c', null)]; + const result = sortDocumentsByDate(docs, 'DESC'); + expect(result[0].id).toBe('b'); + expect(result.slice(1).map((d) => d.id)).toContain('a'); + expect(result.slice(1).map((d) => d.id)).toContain('c'); + }); + + it('places documents without a date last in ASC', () => { + const docs = [doc('a', null), doc('b', '1940-01-01'), doc('c', null)]; + const result = sortDocumentsByDate(docs, 'ASC'); + expect(result[0].id).toBe('b'); + }); + + it('does not mutate the original array', () => { + const docs = [doc('a', '1950-01-01'), doc('b', '1920-01-01')]; + const original = [...docs]; + sortDocumentsByDate(docs, 'ASC'); + expect(docs).toEqual(original); + }); + + it('returns an empty array unchanged', () => { + expect(sortDocumentsByDate([], 'DESC')).toEqual([]); + }); +}); diff --git a/frontend/src/lib/utils/sort.ts b/frontend/src/lib/utils/sort.ts new file mode 100644 index 00000000..eccd573a --- /dev/null +++ b/frontend/src/lib/utils/sort.ts @@ -0,0 +1,19 @@ +export type SortDir = 'ASC' | 'DESC'; + +/** + * Returns a new array of documents sorted by documentDate. + * Documents without a date are always placed last, regardless of direction. + */ +export function sortDocumentsByDate( + docs: T[], + dir: SortDir +): T[] { + return [...docs].sort((a, b) => { + const da = a.documentDate ?? ''; + const db = b.documentDate ?? ''; + if (!da && !db) return 0; + if (!da) return 1; + if (!db) return -1; + return dir === 'DESC' ? db.localeCompare(da) : da.localeCompare(db); + }); +} diff --git a/frontend/src/routes/persons/[id]/+page.svelte b/frontend/src/routes/persons/[id]/+page.svelte index e29e227f..f78d2132 100644 --- a/frontend/src/routes/persons/[id]/+page.svelte +++ b/frontend/src/routes/persons/[id]/+page.svelte @@ -2,12 +2,16 @@ import { enhance } from '$app/forms'; import PersonTypeahead from '$lib/components/PersonTypeahead.svelte'; import { m } from '$lib/paraglide/messages.js'; + import { sortDocumentsByDate, type SortDir } from '$lib/utils/sort'; let { data, form } = $props(); const person = $derived(data.person); const documents = $derived(data.documents); + let sortDir = $state('DESC'); + const sortedDocuments = $derived(sortDocumentsByDate(documents, sortDir)); + let editMode = $state(false); let mergeTargetId = $state(''); let showMergeConfirm = $state(false); @@ -206,10 +210,21 @@
-

{m.person_docs_heading()}

- - {documents.length} - +
+

{m.person_docs_heading()}

+ + {documents.length} + +
+ {#if documents.length > 0} + + {/if}
{#if documents.length === 0} @@ -218,7 +233,7 @@
{:else}