From 3abdf9bb6809f6e0e551e199d9517b04b4388d17 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 29 Mar 2026 19:50:30 +0200 Subject: [PATCH] feat(persons): add formatLifeDateRange + formatDocumentStatus utility functions Unit tests for both; i18n keys for doc status and person stats bar; PERSON_NOT_FOUND added to frontend ErrorCode type. Co-Authored-By: Claude Sonnet 4.6 --- frontend/messages/de.json | 19 ++++++++++++- frontend/messages/en.json | 19 ++++++++++++- frontend/messages/es.json | 19 ++++++++++++- frontend/src/lib/errors.ts | 3 ++ .../src/lib/utils/documentStatusLabel.spec.ts | 28 +++++++++++++++++++ frontend/src/lib/utils/documentStatusLabel.ts | 22 +++++++++++++++ .../src/lib/utils/personLifeDates.spec.ts | 24 ++++++++++++++++ frontend/src/lib/utils/personLifeDates.ts | 20 +++++++++++++ 8 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 frontend/src/lib/utils/documentStatusLabel.spec.ts create mode 100644 frontend/src/lib/utils/documentStatusLabel.ts create mode 100644 frontend/src/lib/utils/personLifeDates.spec.ts create mode 100644 frontend/src/lib/utils/personLifeDates.ts diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 9424d65f..3931b8a3 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -320,5 +320,22 @@ "dashboard_needs_metadata_show_all": "Alle anzeigen", "dashboard_recent_heading": "Zuletzt aktiv", "dashboard_resume_label": "Zuletzt geöffnet:", - "dashboard_resume_fallback": "Unbekanntes Dokument" + "dashboard_resume_fallback": "Unbekanntes Dokument", + "doc_status_placeholder": "Platzhalter", + "doc_status_uploaded": "Hochgeladen", + "doc_status_transcribed": "Transkribiert", + "doc_status_reviewed": "Geprüft", + "doc_status_archived": "Archiviert", + "doc_status_unknown": "Unbekannt", + "persons_stats_persons_one": "1 Person", + "persons_stats_persons_many": "{count} Personen", + "persons_stats_documents_one": "1 Dokument", + "persons_stats_documents_many": "{count} Dokumente", + "error_person_not_found": "Die Person wurde nicht gefunden.", + "person_btn_edit": "Bearbeiten", + "person_discard_changes": "Änderungen verwerfen", + "person_danger_zone_heading": "Gefahrenzone", + "persons_new_birth_year": "Geburtsjahr", + "persons_new_death_year": "Todesjahr", + "persons_new_notes": "Notizen" } diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 4e1e071d..07dd1e6c 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -320,5 +320,22 @@ "dashboard_needs_metadata_show_all": "Show all", "dashboard_recent_heading": "Recent Activity", "dashboard_resume_label": "Last opened:", - "dashboard_resume_fallback": "Unknown document" + "dashboard_resume_fallback": "Unknown document", + "doc_status_placeholder": "Placeholder", + "doc_status_uploaded": "Uploaded", + "doc_status_transcribed": "Transcribed", + "doc_status_reviewed": "Reviewed", + "doc_status_archived": "Archived", + "doc_status_unknown": "Unknown", + "persons_stats_persons_one": "1 person", + "persons_stats_persons_many": "{count} persons", + "persons_stats_documents_one": "1 document", + "persons_stats_documents_many": "{count} documents", + "error_person_not_found": "Person not found.", + "person_btn_edit": "Edit", + "person_discard_changes": "Discard changes", + "person_danger_zone_heading": "Danger zone", + "persons_new_birth_year": "Birth year", + "persons_new_death_year": "Death year", + "persons_new_notes": "Notes" } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 14c9f868..fdb95ee4 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -320,5 +320,22 @@ "dashboard_needs_metadata_show_all": "Ver todos", "dashboard_recent_heading": "Actividad reciente", "dashboard_resume_label": "Último abierto:", - "dashboard_resume_fallback": "Documento desconocido" + "dashboard_resume_fallback": "Documento desconocido", + "doc_status_placeholder": "Marcador", + "doc_status_uploaded": "Cargado", + "doc_status_transcribed": "Transcrito", + "doc_status_reviewed": "Revisado", + "doc_status_archived": "Archivado", + "doc_status_unknown": "Desconocido", + "persons_stats_persons_one": "1 persona", + "persons_stats_persons_many": "{count} personas", + "persons_stats_documents_one": "1 documento", + "persons_stats_documents_many": "{count} documentos", + "error_person_not_found": "Persona no encontrada.", + "person_btn_edit": "Editar", + "person_discard_changes": "Descartar cambios", + "person_danger_zone_heading": "Zona de peligro", + "persons_new_birth_year": "Año de nacimiento", + "persons_new_death_year": "Año de fallecimiento", + "persons_new_notes": "Notas" } diff --git a/frontend/src/lib/errors.ts b/frontend/src/lib/errors.ts index 3abe2b0c..d5964198 100644 --- a/frontend/src/lib/errors.ts +++ b/frontend/src/lib/errors.ts @@ -5,6 +5,7 @@ import * as m from '$lib/paraglide/messages.js'; * Keep in sync with backend/src/main/java/org/raddatz/familienarchiv/exception/ErrorCode.java */ export type ErrorCode = + | 'PERSON_NOT_FOUND' | 'DOCUMENT_NOT_FOUND' | 'DOCUMENT_NO_FILE' | 'FILE_NOT_FOUND' @@ -47,6 +48,8 @@ export async function parseBackendError(res: Response): Promise { + it('maps PLACEHOLDER to correct label', () => { + expect(formatDocumentStatus('PLACEHOLDER')).toBe('Platzhalter'); + }); + + it('maps UPLOADED to correct label', () => { + expect(formatDocumentStatus('UPLOADED')).toBe('Hochgeladen'); + }); + + it('maps TRANSCRIBED to correct label', () => { + expect(formatDocumentStatus('TRANSCRIBED')).toBe('Transkribiert'); + }); + + it('maps REVIEWED to correct label', () => { + expect(formatDocumentStatus('REVIEWED')).toBe('Geprüft'); + }); + + it('maps ARCHIVED to correct label', () => { + expect(formatDocumentStatus('ARCHIVED')).toBe('Archiviert'); + }); + + it('returns fallback for unknown status', () => { + expect(formatDocumentStatus('SOMETHING_NEW')).toBe('Unbekannt'); + }); +}); diff --git a/frontend/src/lib/utils/documentStatusLabel.ts b/frontend/src/lib/utils/documentStatusLabel.ts new file mode 100644 index 00000000..715fae30 --- /dev/null +++ b/frontend/src/lib/utils/documentStatusLabel.ts @@ -0,0 +1,22 @@ +import { m } from '$lib/paraglide/messages.js'; + +/** + * Maps a document status string to a localised human-readable label. + * Falls back to "Unknown" for unrecognised values. + */ +export function formatDocumentStatus(status: string): string { + switch (status) { + case 'PLACEHOLDER': + return m.doc_status_placeholder(); + case 'UPLOADED': + return m.doc_status_uploaded(); + case 'TRANSCRIBED': + return m.doc_status_transcribed(); + case 'REVIEWED': + return m.doc_status_reviewed(); + case 'ARCHIVED': + return m.doc_status_archived(); + default: + return m.doc_status_unknown(); + } +} diff --git a/frontend/src/lib/utils/personLifeDates.spec.ts b/frontend/src/lib/utils/personLifeDates.spec.ts new file mode 100644 index 00000000..cbe23ab8 --- /dev/null +++ b/frontend/src/lib/utils/personLifeDates.spec.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import { formatLifeDateRange } from './personLifeDates'; + +describe('formatLifeDateRange', () => { + it('returns both dates when birth and death year are given', () => { + expect(formatLifeDateRange(1882, 1944)).toBe('* 1882 – † 1944'); + }); + + it('returns only birth year when only birthYear is given', () => { + expect(formatLifeDateRange(1882, undefined)).toBe('* 1882'); + }); + + it('returns only death year when only deathYear is given', () => { + expect(formatLifeDateRange(undefined, 1944)).toBe('† 1944'); + }); + + it('returns empty string when neither year is given', () => { + expect(formatLifeDateRange(undefined, undefined)).toBe(''); + }); + + it('returns empty string when both are null', () => { + expect(formatLifeDateRange(null, null)).toBe(''); + }); +}); diff --git a/frontend/src/lib/utils/personLifeDates.ts b/frontend/src/lib/utils/personLifeDates.ts new file mode 100644 index 00000000..4adbb2fa --- /dev/null +++ b/frontend/src/lib/utils/personLifeDates.ts @@ -0,0 +1,20 @@ +/** + * Formats the life date range for a person. + * Examples: + * * 1882 – † 1944 (both) + * * 1882 (birth only) + * † 1944 (death only) + * "" (neither) + */ +export function formatLifeDateRange(birthYear?: number | null, deathYear?: number | null): string { + if (birthYear && deathYear) { + return `* ${birthYear} – † ${deathYear}`; + } + if (birthYear) { + return `* ${birthYear}`; + } + if (deathYear) { + return `† ${deathYear}`; + } + return ''; +}