merge(feat/person-birth-death-years): resolve conflicts with main, bump migration to V6
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 1m45s
CI / Backend Unit Tests (pull_request) Successful in 2m4s
CI / E2E Tests (pull_request) Failing after 18m40s
CI / Unit & Component Tests (push) Successful in 1m57s
CI / Backend Unit Tests (push) Successful in 2m13s
CI / E2E Tests (push) Failing after 18m39s
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 1m45s
CI / Backend Unit Tests (pull_request) Successful in 2m4s
CI / E2E Tests (pull_request) Failing after 18m40s
CI / Unit & Component Tests (push) Successful in 1m57s
CI / Backend Unit Tests (push) Successful in 2m13s
CI / E2E Tests (push) Failing after 18m39s
Resolves merge conflicts with main (feat/person-notes merged first). Combines both features: birth/death years and notes field on person detail. Renames migration V5__add_birth_death_years to V6 to avoid Flyway conflict. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #28.
This commit is contained in:
@@ -59,7 +59,7 @@ public class PersonController {
|
|||||||
? Integer.parseInt(body.get("birthYear")) : null;
|
? Integer.parseInt(body.get("birthYear")) : null;
|
||||||
Integer deathYear = body.get("deathYear") != null && !body.get("deathYear").isBlank()
|
Integer deathYear = body.get("deathYear") != null && !body.get("deathYear").isBlank()
|
||||||
? Integer.parseInt(body.get("deathYear")) : null;
|
? Integer.parseInt(body.get("deathYear")) : null;
|
||||||
return ResponseEntity.ok(personService.updatePerson(id, firstName.trim(), lastName.trim(), body.get("alias"), birthYear, deathYear));
|
return ResponseEntity.ok(personService.updatePerson(id, firstName.trim(), lastName.trim(), body.get("alias"), body.get("notes"), birthYear, deathYear));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/{id}/merge")
|
@PostMapping("/{id}/merge")
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ public class Person {
|
|||||||
// Optional: Aliasse für die Suche (z.B. "Opa Hans")
|
// Optional: Aliasse für die Suche (z.B. "Opa Hans")
|
||||||
private String alias;
|
private String alias;
|
||||||
|
|
||||||
|
// Optional: Free-text biographical notes
|
||||||
|
@Column(columnDefinition = "TEXT")
|
||||||
|
private String notes;
|
||||||
|
|
||||||
private Integer birthYear;
|
private Integer birthYear;
|
||||||
private Integer deathYear;
|
private Integer deathYear;
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ public class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public Person updatePerson(UUID id, String firstName, String lastName, String alias, Integer birthYear, Integer deathYear) {
|
public Person updatePerson(UUID id, String firstName, String lastName, String alias, String notes, Integer birthYear, Integer deathYear) {
|
||||||
if (birthYear != null && deathYear != null && birthYear > deathYear) {
|
if (birthYear != null && deathYear != null && birthYear > deathYear) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Geburtsjahr darf nicht nach dem Todesjahr liegen");
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Geburtsjahr darf nicht nach dem Todesjahr liegen");
|
||||||
}
|
}
|
||||||
@@ -67,6 +67,7 @@ public class PersonService {
|
|||||||
person.setFirstName(firstName);
|
person.setFirstName(firstName);
|
||||||
person.setLastName(lastName);
|
person.setLastName(lastName);
|
||||||
person.setAlias(alias == null || alias.isBlank() ? null : alias.trim());
|
person.setAlias(alias == null || alias.isBlank() ? null : alias.trim());
|
||||||
|
person.setNotes(notes == null || notes.isBlank() ? null : notes.trim());
|
||||||
person.setBirthYear(birthYear);
|
person.setBirthYear(birthYear);
|
||||||
person.setDeathYear(deathYear);
|
person.setDeathYear(deathYear);
|
||||||
return personRepository.save(person);
|
return personRepository.save(person);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE persons ADD COLUMN notes TEXT;
|
||||||
@@ -83,6 +83,32 @@ class PersonServiceTest {
|
|||||||
verify(personRepository).findByAliasIgnoreCase("Clara Cram");
|
verify(personRepository).findByAliasIgnoreCase("Clara Cram");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── updatePerson (notes) ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updatePerson_persistsNotes() {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").build();
|
||||||
|
when(personRepository.findById(id)).thenReturn(Optional.of(person));
|
||||||
|
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Person result = personService.updatePerson(id, "Anna", "Alt", null, "Some notes here.", null, null);
|
||||||
|
|
||||||
|
assertThat(result.getNotes()).isEqualTo("Some notes here.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updatePerson_clearsNotes_whenBlank() {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
Person person = Person.builder().id(id).firstName("Anna").lastName("Alt").notes("old notes").build();
|
||||||
|
when(personRepository.findById(id)).thenReturn(Optional.of(person));
|
||||||
|
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
|
Person result = personService.updatePerson(id, "Anna", "Alt", null, " ", null, null);
|
||||||
|
|
||||||
|
assertThat(result.getNotes()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
// ─── updatePerson (birth/death years) ────────────────────────────────────
|
// ─── updatePerson (birth/death years) ────────────────────────────────────
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -92,7 +118,7 @@ class PersonServiceTest {
|
|||||||
when(personRepository.findById(id)).thenReturn(Optional.of(person));
|
when(personRepository.findById(id)).thenReturn(Optional.of(person));
|
||||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
Person result = personService.updatePerson(id, "Anna", "Alt", null, 1890, 1965);
|
Person result = personService.updatePerson(id, "Anna", "Alt", null, null, 1890, 1965);
|
||||||
|
|
||||||
assertThat(result.getBirthYear()).isEqualTo(1890);
|
assertThat(result.getBirthYear()).isEqualTo(1890);
|
||||||
assertThat(result.getDeathYear()).isEqualTo(1965);
|
assertThat(result.getDeathYear()).isEqualTo(1965);
|
||||||
@@ -102,7 +128,7 @@ class PersonServiceTest {
|
|||||||
void updatePerson_throwsBadRequest_whenBirthYearAfterDeathYear() {
|
void updatePerson_throwsBadRequest_whenBirthYearAfterDeathYear() {
|
||||||
UUID id = UUID.randomUUID();
|
UUID id = UUID.randomUUID();
|
||||||
|
|
||||||
assertThatThrownBy(() -> personService.updatePerson(id, "Anna", "Alt", null, 1970, 1950))
|
assertThatThrownBy(() -> personService.updatePerson(id, "Anna", "Alt", null, null, 1970, 1950))
|
||||||
.isInstanceOf(ResponseStatusException.class)
|
.isInstanceOf(ResponseStatusException.class)
|
||||||
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
|
.extracting(e -> ((ResponseStatusException) e).getStatusCode().value())
|
||||||
.isEqualTo(400);
|
.isEqualTo(400);
|
||||||
@@ -115,7 +141,7 @@ class PersonServiceTest {
|
|||||||
when(personRepository.findById(id)).thenReturn(Optional.of(person));
|
when(personRepository.findById(id)).thenReturn(Optional.of(person));
|
||||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
|
||||||
Person result = personService.updatePerson(id, "Anna", "Alt", null, 1900, 1900);
|
Person result = personService.updatePerson(id, "Anna", "Alt", null, null, 1900, 1900);
|
||||||
|
|
||||||
assertThat(result.getBirthYear()).isEqualTo(1900);
|
assertThat(result.getBirthYear()).isEqualTo(1900);
|
||||||
assertThat(result.getDeathYear()).isEqualTo(1900);
|
assertThat(result.getDeathYear()).isEqualTo(1900);
|
||||||
|
|||||||
@@ -94,6 +94,40 @@ 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');
|
||||||
|
// 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();
|
||||||
|
// 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}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test.describe('Conversations', () => {
|
test.describe('Conversations', () => {
|
||||||
test('shows the empty state when no persons are selected', async ({ page }) => {
|
test('shows the empty state when no persons are selected', async ({ page }) => {
|
||||||
await page.goto('/conversations');
|
await page.goto('/conversations');
|
||||||
|
|||||||
@@ -111,6 +111,8 @@
|
|||||||
"person_btn_merge": "Zusammenführen",
|
"person_btn_merge": "Zusammenführen",
|
||||||
"person_btn_merge_confirm": "Ja, zusammenführen",
|
"person_btn_merge_confirm": "Ja, zusammenführen",
|
||||||
"person_merge_warning": "Achtung: Diese Aktion ist nicht rückgängig zu machen.",
|
"person_merge_warning": "Achtung: Diese Aktion ist nicht rückgängig zu machen.",
|
||||||
|
"person_label_notes": "Notizen",
|
||||||
|
"person_placeholder_notes": "Biographische Hinweise, Besonderheiten…",
|
||||||
"person_label_birth_year": "Geburtsjahr",
|
"person_label_birth_year": "Geburtsjahr",
|
||||||
"person_label_death_year": "Todesjahr",
|
"person_label_death_year": "Todesjahr",
|
||||||
"person_placeholder_year": "z.B. 1923",
|
"person_placeholder_year": "z.B. 1923",
|
||||||
|
|||||||
@@ -111,6 +111,8 @@
|
|||||||
"person_btn_merge": "Merge",
|
"person_btn_merge": "Merge",
|
||||||
"person_btn_merge_confirm": "Yes, merge",
|
"person_btn_merge_confirm": "Yes, merge",
|
||||||
"person_merge_warning": "Warning: This action cannot be undone.",
|
"person_merge_warning": "Warning: This action cannot be undone.",
|
||||||
|
"person_label_notes": "Notes",
|
||||||
|
"person_placeholder_notes": "Biographical notes, remarks…",
|
||||||
"person_label_birth_year": "Birth year",
|
"person_label_birth_year": "Birth year",
|
||||||
"person_label_death_year": "Death year",
|
"person_label_death_year": "Death year",
|
||||||
"person_placeholder_year": "e.g. 1923",
|
"person_placeholder_year": "e.g. 1923",
|
||||||
@@ -180,5 +182,6 @@
|
|||||||
"comp_taginput_placeholder_create": "Add tags...",
|
"comp_taginput_placeholder_create": "Add tags...",
|
||||||
"comp_taginput_placeholder_filter": "Filter by tags...",
|
"comp_taginput_placeholder_filter": "Filter by tags...",
|
||||||
"comp_taginput_remove": "Remove tag",
|
"comp_taginput_remove": "Remove tag",
|
||||||
"comp_taginput_create_hint": "Press Enter to create tag."
|
"comp_taginput_create_hint": "Press Enter to create tag.",
|
||||||
|
"person_btn_conversations": "View conversations"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,8 @@
|
|||||||
"person_btn_merge": "Fusionar",
|
"person_btn_merge": "Fusionar",
|
||||||
"person_btn_merge_confirm": "Sí, fusionar",
|
"person_btn_merge_confirm": "Sí, fusionar",
|
||||||
"person_merge_warning": "Atención: Esta acción no se puede deshacer.",
|
"person_merge_warning": "Atención: Esta acción no se puede deshacer.",
|
||||||
|
"person_label_notes": "Notas",
|
||||||
|
"person_placeholder_notes": "Notas biográficas, observaciones…",
|
||||||
"person_label_birth_year": "Año de nacimiento",
|
"person_label_birth_year": "Año de nacimiento",
|
||||||
"person_label_death_year": "Año de fallecimiento",
|
"person_label_death_year": "Año de fallecimiento",
|
||||||
"person_placeholder_year": "p.ej. 1923",
|
"person_placeholder_year": "p.ej. 1923",
|
||||||
@@ -180,5 +182,6 @@
|
|||||||
"comp_taginput_placeholder_create": "Añadir etiquetas...",
|
"comp_taginput_placeholder_create": "Añadir etiquetas...",
|
||||||
"comp_taginput_placeholder_filter": "Filtrar por etiquetas...",
|
"comp_taginput_placeholder_filter": "Filtrar por etiquetas...",
|
||||||
"comp_taginput_remove": "Eliminar etiqueta",
|
"comp_taginput_remove": "Eliminar etiqueta",
|
||||||
"comp_taginput_create_hint": "Pulse Enter para crear etiqueta."
|
"comp_taginput_create_hint": "Pulse Enter para crear etiqueta.",
|
||||||
|
"person_btn_conversations": "Ver conversaciones"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ export interface components {
|
|||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
alias?: string;
|
alias?: string;
|
||||||
|
notes?: string;
|
||||||
birthYear?: number;
|
birthYear?: number;
|
||||||
deathYear?: number;
|
deathYear?: number;
|
||||||
};
|
};
|
||||||
|
|||||||
44
frontend/src/lib/utils/sort.spec.ts
Normal file
44
frontend/src/lib/utils/sort.spec.ts
Normal file
@@ -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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
19
frontend/src/lib/utils/sort.ts
Normal file
19
frontend/src/lib/utils/sort.ts
Normal file
@@ -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<T extends { documentDate?: string | null }>(
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ export const actions = {
|
|||||||
const firstName = formData.get('firstName')?.toString().trim();
|
const firstName = formData.get('firstName')?.toString().trim();
|
||||||
const lastName = formData.get('lastName')?.toString().trim();
|
const lastName = formData.get('lastName')?.toString().trim();
|
||||||
const alias = formData.get('alias')?.toString().trim() || undefined;
|
const alias = formData.get('alias')?.toString().trim() || undefined;
|
||||||
|
const notes = formData.get('notes')?.toString().trim() || undefined;
|
||||||
const birthYear = formData.get('birthYear')?.toString().trim() || undefined;
|
const birthYear = formData.get('birthYear')?.toString().trim() || undefined;
|
||||||
const deathYear = formData.get('deathYear')?.toString().trim() || undefined;
|
const deathYear = formData.get('deathYear')?.toString().trim() || undefined;
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ export const actions = {
|
|||||||
body: {
|
body: {
|
||||||
firstName, lastName,
|
firstName, lastName,
|
||||||
...(alias ? { alias } : {}),
|
...(alias ? { alias } : {}),
|
||||||
|
...(notes ? { notes } : {}),
|
||||||
...(birthYear ? { birthYear } : {}),
|
...(birthYear ? { birthYear } : {}),
|
||||||
...(deathYear ? { deathYear } : {})
|
...(deathYear ? { deathYear } : {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,16 @@
|
|||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
|
import { sortDocumentsByDate, type SortDir } from '$lib/utils/sort';
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
const person = $derived(data.person);
|
const person = $derived(data.person);
|
||||||
const documents = $derived(data.documents);
|
const documents = $derived(data.documents);
|
||||||
|
|
||||||
|
let sortDir = $state<SortDir>('DESC');
|
||||||
|
const sortedDocuments = $derived(sortDocumentsByDate(documents, sortDir));
|
||||||
|
|
||||||
let editMode = $state(false);
|
let editMode = $state(false);
|
||||||
let mergeTargetId = $state('');
|
let mergeTargetId = $state('');
|
||||||
let showMergeConfirm = $state(false);
|
let showMergeConfirm = $state(false);
|
||||||
@@ -108,6 +112,16 @@
|
|||||||
class="block w-full border border-gray-300 rounded px-3 py-2 font-serif text-brand-navy focus:outline-none focus:border-brand-navy"
|
class="block w-full border border-gray-300 rounded px-3 py-2 font-serif text-brand-navy focus:outline-none focus:border-brand-navy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<label for="notes" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">{m.person_label_notes()}</label>
|
||||||
|
<textarea
|
||||||
|
id="notes"
|
||||||
|
name="notes"
|
||||||
|
rows="4"
|
||||||
|
placeholder={m.person_placeholder_notes()}
|
||||||
|
class="block w-full border border-gray-300 rounded px-3 py-2 font-serif text-brand-navy focus:outline-none focus:border-brand-navy resize-y"
|
||||||
|
>{person.notes ?? ''}</textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
@@ -134,10 +148,16 @@
|
|||||||
<h1 class="text-4xl font-serif text-brand-navy">
|
<h1 class="text-4xl font-serif text-brand-navy">
|
||||||
{person.firstName} {person.lastName}
|
{person.firstName} {person.lastName}
|
||||||
</h1>
|
</h1>
|
||||||
<button onclick={() => (editMode = true)} class="ml-4 flex-shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold uppercase tracking-widest border border-gray-300 text-gray-500 rounded hover:border-brand-navy hover:text-brand-navy transition-colors">
|
<div class="ml-4 flex-shrink-0 flex items-center gap-2">
|
||||||
<img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Edit-Content-SM.svg" alt="" aria-hidden="true" class="w-3.5 h-3.5" />
|
<a href="/conversations?senderId={person.id}" class="inline-flex items-center gap-1 text-sm font-medium text-brand-navy/60 hover:text-brand-navy transition-colors">
|
||||||
{m.btn_edit()}
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path 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"/></svg>
|
||||||
</button>
|
{m.person_btn_conversations()}
|
||||||
|
</a>
|
||||||
|
<button onclick={() => (editMode = true)} class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-bold uppercase tracking-widest border border-gray-300 text-gray-500 rounded hover:border-brand-navy hover:text-brand-navy transition-colors">
|
||||||
|
<img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Edit-Content-SM.svg" alt="" aria-hidden="true" class="w-3.5 h-3.5" />
|
||||||
|
{m.btn_edit()}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
@@ -163,6 +183,13 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if person.notes}
|
||||||
|
<div class="md:col-span-2">
|
||||||
|
<span class="block text-xs font-bold font-sans text-gray-400 uppercase tracking-widest mb-1">{m.person_label_notes()}</span>
|
||||||
|
<p class="text-base font-serif text-brand-navy whitespace-pre-wrap">{person.notes}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -237,10 +264,21 @@
|
|||||||
<!-- Linked Documents Section -->
|
<!-- Linked Documents Section -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between mb-6 border-b border-brand-navy/10 pb-2">
|
<div class="flex items-center justify-between mb-6 border-b border-brand-navy/10 pb-2">
|
||||||
<h2 class="text-xl font-serif text-brand-navy">{m.person_docs_heading()}</h2>
|
<div class="flex items-center gap-3">
|
||||||
<span class="bg-brand-navy text-white text-xs font-bold px-2 py-1 rounded-full">
|
<h2 class="text-xl font-serif text-brand-navy">{m.person_docs_heading()}</h2>
|
||||||
{documents.length}
|
<span class="bg-brand-navy text-white text-xs font-bold px-2 py-1 rounded-full">
|
||||||
</span>
|
{documents.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{#if documents.length > 0}
|
||||||
|
<button
|
||||||
|
onclick={() => (sortDir = sortDir === 'DESC' ? 'ASC' : 'DESC')}
|
||||||
|
class="text-xs font-bold uppercase tracking-widest text-gray-400 hover:text-brand-navy transition-colors"
|
||||||
|
aria-label={m.conv_sort_label()}
|
||||||
|
>
|
||||||
|
{sortDir === 'DESC' ? m.conv_sort_newest() : m.conv_sort_oldest()}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if documents.length === 0}
|
{#if documents.length === 0}
|
||||||
@@ -249,7 +287,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="space-y-3">
|
<ul class="space-y-3">
|
||||||
{#each documents as doc}
|
{#each sortedDocuments as doc}
|
||||||
<li class="group">
|
<li class="group">
|
||||||
<a href="/documents/{doc.id}" class="block bg-white border border-brand-sand p-4 hover:border-brand-navy hover:shadow-md transition-all duration-200">
|
<a href="/documents/{doc.id}" class="block bg-white border border-brand-sand p-4 hover:border-brand-navy hover:shadow-md transition-all duration-200">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user