diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 14269eed..5590c6b7 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -655,6 +655,7 @@ "person_alias_btn_delete": "Entfernen", "error_alias_not_found": "Der Namensalias wurde nicht gefunden.", "error_invalid_person_type": "Der angegebene Personentyp ist ungültig.", + "error_invalid_date_range": "Das Enddatum darf nicht vor dem Startdatum liegen.", "validation_last_name_required": "Nachname ist Pflichtfeld.", "validation_first_name_required": "Vorname ist Pflichtfeld.", "error_ocr_service_unavailable": "Der OCR-Dienst ist nicht verfügbar.", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index b7de0948..5b7c2698 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -655,6 +655,7 @@ "person_alias_btn_delete": "Remove", "error_alias_not_found": "The name alias was not found.", "error_invalid_person_type": "The specified person type is not valid.", + "error_invalid_date_range": "The end date must not be before the start date.", "validation_last_name_required": "Last name is required.", "validation_first_name_required": "First name is required.", "error_ocr_service_unavailable": "The OCR service is not available.", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index ee584c40..4e856892 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -655,6 +655,7 @@ "person_alias_btn_delete": "Eliminar", "error_alias_not_found": "No se encontro el alias de nombre.", "error_invalid_person_type": "El tipo de persona especificado no es válido.", + "error_invalid_date_range": "La fecha final no puede ser anterior a la inicial.", "validation_last_name_required": "El apellido es obligatorio.", "validation_first_name_required": "El nombre es obligatorio.", "error_ocr_service_unavailable": "El servicio OCR no está disponible.", diff --git a/frontend/src/lib/document/WhoWhenSection.svelte b/frontend/src/lib/document/WhoWhenSection.svelte index 1a312e63..8c49a72b 100644 --- a/frontend/src/lib/document/WhoWhenSection.svelte +++ b/frontend/src/lib/document/WhoWhenSection.svelte @@ -70,6 +70,13 @@ onMount(() => { const dateInvalid = $derived(dateDirty && dateDisplay.length > 0 && dateIso === ''); +// Inline mirror of the server guard (#678). ISO YYYY-MM-DD strings compare +// lexicographically, so no Date object is needed. Server stays the gate — +// this only surfaces the error early; it never disables Save. +const endBeforeStart = $derived( + showEndDate && endDateIso !== '' && dateIso !== '' && endDateIso < dateIso +); + function handleDateInput(e: Event) { const result = handleGermanDateInput(e); dateDisplay = result.display; @@ -155,8 +162,19 @@ $effect(() => { oninput={handleEndDateInput} placeholder={m.form_placeholder_date()} maxlength="10" - class="block min-h-[48px] w-full rounded border border-line px-2 py-3 text-sm shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring" + aria-invalid={endBeforeStart ? 'true' : undefined} + aria-describedby={endBeforeStart ? 'end-date-error' : undefined} + class="block min-h-[48px] w-full rounded border border-line px-2 py-3 text-sm shadow-sm + {endBeforeStart + ? 'border-red-400 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500' + : 'focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring'}" /> + {#if endBeforeStart} + +

+ {m.error_invalid_date_range()} +

+ {/if} {/if} diff --git a/frontend/src/lib/document/WhoWhenSection.svelte.test.ts b/frontend/src/lib/document/WhoWhenSection.svelte.test.ts index d3a05147..f2d7746f 100644 --- a/frontend/src/lib/document/WhoWhenSection.svelte.test.ts +++ b/frontend/src/lib/document/WhoWhenSection.svelte.test.ts @@ -102,3 +102,49 @@ describe('WhoWhenSection — precision controls', () => { expect(raw?.querySelector('b')).toBeNull(); }); }); + +describe('WhoWhenSection — end-before-start inline validation (#678)', () => { + it('shows an inline error on the end-date field when end is before start (AC1)', async () => { + render(WhoWhenSection, { + precision: 'RANGE', + dateIso: '1917-01-11', + endDateIso: '1917-01-10' + }); + + const end = document.querySelector('input#metaDateEnd') as HTMLInputElement; + await vi.waitFor(() => { + expect(document.querySelector('#end-date-error')).not.toBeNull(); + expect(end.getAttribute('aria-invalid')).toBe('true'); + expect(end.className).toContain('border-red-400'); + }); + }); + + it('clears the inline error once the end date is corrected, without reload (AC5)', async () => { + render(WhoWhenSection, { + precision: 'RANGE', + dateIso: '1917-01-11', + endDateIso: '1917-01-10' + }); + + await vi.waitFor(() => expect(document.querySelector('#end-date-error')).not.toBeNull()); + + const end = document.querySelector('input#metaDateEnd') as HTMLInputElement; + end.value = '12.01.1917'; // now after the start + end.dispatchEvent(new Event('input', { bubbles: true })); + + await vi.waitFor(() => { + expect(document.querySelector('#end-date-error')).toBeNull(); + expect(end.getAttribute('aria-invalid')).not.toBe('true'); + }); + }); + + it('does not show the inline error when precision is not RANGE', async () => { + render(WhoWhenSection, { + precision: 'DAY', + dateIso: '1917-01-11', + endDateIso: '1917-01-10' + }); + + expect(document.querySelector('#end-date-error')).toBeNull(); + }); +}); diff --git a/frontend/src/lib/shared/errors.ts b/frontend/src/lib/shared/errors.ts index dcdb9f25..6efec2a7 100644 --- a/frontend/src/lib/shared/errors.ts +++ b/frontend/src/lib/shared/errors.ts @@ -8,6 +8,7 @@ export type ErrorCode = | 'PERSON_NOT_FOUND' | 'ALIAS_NOT_FOUND' | 'INVALID_PERSON_TYPE' + | 'INVALID_DATE_RANGE' | 'DOCUMENT_NOT_FOUND' | 'DOCUMENT_NO_FILE' | 'FILE_NOT_FOUND' @@ -87,6 +88,8 @@ export function getErrorMessage(code: ErrorCode | string | undefined): string { return m.error_alias_not_found(); case 'INVALID_PERSON_TYPE': return m.error_invalid_person_type(); + case 'INVALID_DATE_RANGE': + return m.error_invalid_date_range(); case 'DOCUMENT_NOT_FOUND': return m.error_document_not_found(); case 'DOCUMENT_NO_FILE':