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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-29 19:50:30 +02:00
parent 7b03aada3b
commit 3abdf9bb68
8 changed files with 151 additions and 3 deletions

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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<BackendError | n
/** Returns a localised message for the given error code via Paraglide. Falls back to INTERNAL_ERROR. */
export function getErrorMessage(code: ErrorCode | string | undefined): string {
switch (code) {
case 'PERSON_NOT_FOUND':
return m.error_person_not_found();
case 'DOCUMENT_NOT_FOUND':
return m.error_document_not_found();
case 'DOCUMENT_NO_FILE':

View File

@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest';
import { formatDocumentStatus } from './documentStatusLabel';
describe('formatDocumentStatus', () => {
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');
});
});

View File

@@ -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();
}
}

View File

@@ -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('');
});
});

View File

@@ -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 '';
}