feat: implement i18n — extract all UI strings, add EN + ES-MX translations, add language selector
Extract all hardcoded German strings from every .svelte file and component into Paraglide message keys. Add complete translations for all keys in messages/en.json (English) and messages/es.json (Spanish/Mexico). Changes: - messages/de.json: 100+ keys covering navigation, buttons, form labels, placeholders, section headings, empty states, and error messages - messages/en.json, messages/es.json: complete translations for all keys - project.inlang/settings.json: change baseLocale from "en" to "de" - +layout.svelte: add DE/EN/ES language selector in header using setLocale(); active language is bold, choice persists via Paraglide cookie strategy - All 10 route pages + 3 shared components: replace hardcoded German with m.key() - e2e/lang.spec.ts: E2E tests for language selector visibility, switching, persistence across navigation, and active state highlighting Closes #2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #9.
This commit is contained in:
37
frontend/e2e/lang.spec.ts
Normal file
37
frontend/e2e/lang.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Language selector', () => {
|
||||
test('shows DE, EN, ES buttons in the header', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('banner').getByRole('button', { name: 'DE' })).toBeVisible();
|
||||
await expect(page.getByRole('banner').getByRole('button', { name: 'EN' })).toBeVisible();
|
||||
await expect(page.getByRole('banner').getByRole('button', { name: 'ES' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('switching to EN translates the navigation', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('banner').getByRole('button', { name: 'EN' }).click();
|
||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Documents' })).toBeVisible();
|
||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Persons' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('language choice persists after navigation', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('banner').getByRole('button', { name: 'EN' }).click();
|
||||
await page.goto('/persons');
|
||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Documents' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('switching back to DE restores German', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.getByRole('banner').getByRole('button', { name: 'EN' }).click();
|
||||
await page.getByRole('banner').getByRole('button', { name: 'DE' }).click();
|
||||
await expect(page.getByRole('navigation').getByRole('link', { name: 'Dokumente' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('active language button is visually highlighted', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const deBtn = page.getByRole('banner').getByRole('button', { name: 'DE' });
|
||||
await expect(deBtn).toHaveClass(/font-bold/);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from de!",
|
||||
|
||||
"error_document_not_found": "Das Dokument wurde nicht gefunden.",
|
||||
"error_document_no_file": "Diesem Dokument ist noch keine Datei zugeordnet.",
|
||||
"error_file_not_found": "Die Datei konnte im Speicher nicht gefunden werden.",
|
||||
@@ -10,5 +10,159 @@
|
||||
"error_unauthorized": "Sie sind nicht angemeldet.",
|
||||
"error_forbidden": "Sie haben keine Berechtigung für diese Aktion.",
|
||||
"error_validation_error": "Die Eingabe ist ungültig.",
|
||||
"error_internal_error": "Ein unerwarteter Fehler ist aufgetreten."
|
||||
"error_internal_error": "Ein unerwarteter Fehler ist aufgetreten.",
|
||||
|
||||
"nav_documents": "Dokumente",
|
||||
"nav_persons": "Personen",
|
||||
"nav_conversations": "Konversationen",
|
||||
"nav_admin": "Admin",
|
||||
"nav_logout": "Abmelden",
|
||||
|
||||
"btn_save": "Speichern",
|
||||
"btn_cancel": "Abbrechen",
|
||||
"btn_edit": "Bearbeiten",
|
||||
"btn_create": "Erstellen",
|
||||
"btn_delete": "Löschen",
|
||||
"btn_back_to_overview": "Zurück zur Übersicht",
|
||||
"btn_back": "Zurück",
|
||||
"btn_back_to_document": "Zurück zum Dokument",
|
||||
|
||||
"form_label_first_name": "Vorname",
|
||||
"form_label_last_name": "Nachname",
|
||||
"form_label_alias": "Rufname / Alias",
|
||||
"form_placeholder_alias": "z.B. Oma Frieda, Onkel Karl…",
|
||||
"form_label_date": "Datum",
|
||||
"form_placeholder_date": "TT.MM.JJJJ",
|
||||
"form_date_error": "Bitte im Format TT.MM.JJJJ eingeben, z.B. 20.12.2026",
|
||||
"form_label_location": "Ort",
|
||||
"form_placeholder_location": "z.B. Berlin, Wien…",
|
||||
"form_label_sender": "Absender",
|
||||
"form_label_receivers": "Empfänger",
|
||||
"form_label_title": "Titel *",
|
||||
"form_label_tags": "Schlagworte",
|
||||
"form_label_content": "Inhalt",
|
||||
"form_placeholder_content": "Kurze Beschreibung des Inhalts…",
|
||||
"form_label_transcription": "Transkription",
|
||||
"form_placeholder_transcription": "Vollständiger Text des Dokuments…",
|
||||
"form_label_archive_location": "Aufbewahrungsort",
|
||||
"form_placeholder_archive_location": "z.B. Schrank 3, Mappe B",
|
||||
"form_helper_archive_location": "Wo befindet sich das Originaldokument?",
|
||||
|
||||
"login_heading": "Anmelden",
|
||||
"login_label_username": "Benutzername",
|
||||
"login_label_password": "Passwort",
|
||||
"login_btn_submit": "Anmelden",
|
||||
|
||||
"docs_search_placeholder": "Suche in Titel, Inhalt, Ort...",
|
||||
"docs_btn_filter": "Filter",
|
||||
"docs_btn_reset_title": "Filter zurücksetzen",
|
||||
"docs_filter_label_tags": "Schlagworte",
|
||||
"docs_filter_label_sender": "Absender",
|
||||
"docs_filter_label_receivers": "Empfänger",
|
||||
"docs_filter_label_from": "Von",
|
||||
"docs_filter_label_to": "Bis",
|
||||
"docs_btn_new": "Neues Dokument",
|
||||
"docs_empty_heading": "Keine Dokumente gefunden",
|
||||
"docs_empty_text": "Versuchen Sie, die Filter anzupassen oder den Suchbegriff zu ändern.",
|
||||
"docs_empty_btn_clear": "Alle Filter löschen",
|
||||
"docs_list_from": "Von",
|
||||
"docs_list_to": "An",
|
||||
"docs_list_unknown": "Unbekannt",
|
||||
|
||||
"doc_section_who_when": "Wer & Wann",
|
||||
"doc_section_description": "Beschreibung",
|
||||
"doc_section_file": "Datei",
|
||||
"doc_file_upload_label": "Datei hochladen",
|
||||
"doc_file_upload_note": "(optional)",
|
||||
"doc_file_replace_label": "Neue Datei hochladen",
|
||||
"doc_file_replace_note": "(ersetzt die aktuelle Datei)",
|
||||
"doc_current_file_label": "Aktuelle Datei:",
|
||||
"doc_new_heading": "Neues Dokument",
|
||||
"doc_edit_heading": "Bearbeiten",
|
||||
|
||||
"doc_section_details": "Details",
|
||||
"doc_label_document_date": "Dokumentendatum",
|
||||
"doc_label_creation_location": "Erstellungsort",
|
||||
"doc_label_archive_location_original": "Aufbewahrungsort (Original)",
|
||||
"doc_section_persons": "Personen",
|
||||
"doc_sender_not_specified": "Nicht angegeben",
|
||||
"doc_no_receivers": "Keine Empfänger",
|
||||
"doc_section_content": "Inhalt",
|
||||
"doc_label_summary": "Zusammenfassung",
|
||||
"doc_loading": "Lade Dokument...",
|
||||
"doc_download_link": "Direkter Download versuchen",
|
||||
"doc_no_scan": "Kein Scan vorhanden",
|
||||
|
||||
"persons_heading": "Personenverzeichnis",
|
||||
"persons_subtitle": "Durchsuchen Sie den Index aller erfassten Personen im Familienarchiv.",
|
||||
"persons_btn_new": "Neue Person",
|
||||
"persons_search_placeholder": "Namen suchen...",
|
||||
"persons_empty_heading": "Keine Personen gefunden.",
|
||||
"persons_empty_text": "Versuchen Sie einen anderen Suchbegriff.",
|
||||
|
||||
"persons_new_heading": "Neue Person",
|
||||
"persons_section_details": "Angaben zur Person",
|
||||
|
||||
"person_edit_heading": "Person bearbeiten",
|
||||
"person_label_full_name": "Voller Name",
|
||||
"person_merge_heading": "Person zusammenführen",
|
||||
"person_merge_description": "Diese Person wird in die gewählte Zielperson überführt. Alle Dokumente und Verknüpfungen werden übertragen, danach wird diese Person gelöscht.",
|
||||
"person_merge_target_label": "Zusammenführen mit",
|
||||
"person_btn_merge": "Zusammenführen",
|
||||
"person_btn_merge_confirm": "Ja, zusammenführen",
|
||||
"person_merge_warning": "Achtung: Diese Aktion ist nicht rückgängig zu machen.",
|
||||
"person_docs_heading": "Gesendete Dokumente",
|
||||
"person_no_docs": "Diese Person ist noch nicht als Absender verknüpft.",
|
||||
|
||||
"conv_heading": "Konversationen",
|
||||
"conv_subtitle": "Verfolgen Sie den Schriftverkehr zwischen zwei Personen chronologisch.",
|
||||
"conv_label_person_a": "Person A (Absender)",
|
||||
"conv_label_person_b": "Person B (Empfänger)",
|
||||
"conv_label_from": "Zeitraum von",
|
||||
"conv_label_to": "Zeitraum bis",
|
||||
"conv_sort_label": "Sortierung:",
|
||||
"conv_sort_newest": "Neueste zuerst",
|
||||
"conv_sort_oldest": "Älteste zuerst",
|
||||
"conv_empty_heading": "Wählen Sie zwei Personen aus",
|
||||
"conv_empty_text": "Die Korrespondenz wird hier angezeigt.",
|
||||
"conv_no_results_heading": "Keine Dokumente gefunden.",
|
||||
"conv_no_results_text": "Versuchen Sie, den Zeitraum anzupassen.",
|
||||
|
||||
"admin_heading": "Admin Dashboard",
|
||||
"admin_tab_users": "Benutzer",
|
||||
"admin_tab_groups": "Gruppen",
|
||||
"admin_tab_tags": "Schlagworte",
|
||||
"admin_section_users": "Benutzerverwaltung",
|
||||
"admin_col_login": "Login",
|
||||
"admin_col_groups": "Gruppen",
|
||||
"admin_col_password": "Passwort",
|
||||
"admin_multiselect_hint": "Strg+Klick für Auswahl",
|
||||
"admin_password_placeholder": "Neues PW (optional)",
|
||||
"admin_no_groups": "Keine Gruppen",
|
||||
"admin_btn_delete_user_title": "Benutzer löschen",
|
||||
"admin_section_new_user": "Neuen Benutzer anlegen",
|
||||
"admin_multiselect_hint_multi": "Strg+Klick für mehrere",
|
||||
"admin_multiselect_hint_full": "Strg+Klick für Mehrfachauswahl",
|
||||
"admin_section_tags": "Schlagworte",
|
||||
"admin_tags_warning": "Warnung: Umbenennen oder Löschen wirkt sich auf alle verknüpften Dokumente aus.",
|
||||
"admin_btn_edit_tag_label": "Schlagwort bearbeiten",
|
||||
"admin_tag_delete_confirm": "Wirklich löschen? Das Schlagwort wird aus allen Dokumenten entfernt.",
|
||||
"admin_btn_delete_tag_label": "Schlagwort löschen",
|
||||
"admin_section_groups": "Gruppenverwaltung",
|
||||
"admin_col_name": "Name",
|
||||
"admin_col_permissions": "Berechtigungen",
|
||||
"admin_col_actions": "Aktionen",
|
||||
"admin_group_delete_confirm": "Gruppe wirklich löschen?",
|
||||
"admin_section_new_group": "Neue Gruppe anlegen",
|
||||
"admin_group_name_placeholder": "Gruppenname (z.B. Editoren)",
|
||||
|
||||
"comp_typeahead_placeholder": "Namen tippen...",
|
||||
"comp_typeahead_loading": "Suche...",
|
||||
"comp_multiselect_placeholder": "Namen tippen...",
|
||||
"comp_multiselect_remove": "Entfernen",
|
||||
"comp_multiselect_loading": "Suche...",
|
||||
"comp_taginput_placeholder_create": "Schlagworte hinzufügen...",
|
||||
"comp_taginput_placeholder_filter": "Nach Schlagworten filtern...",
|
||||
"comp_taginput_remove": "Schlagwort entfernen",
|
||||
"comp_taginput_create_hint": "Enter drücken um Schlagwort zu erstellen."
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from en!",
|
||||
|
||||
"error_document_not_found": "Document not found.",
|
||||
"error_document_no_file": "No file is associated with this document.",
|
||||
"error_file_not_found": "The file could not be found in storage.",
|
||||
@@ -10,5 +10,159 @@
|
||||
"error_unauthorized": "You are not logged in.",
|
||||
"error_forbidden": "You do not have permission for this action.",
|
||||
"error_validation_error": "The input is invalid.",
|
||||
"error_internal_error": "An unexpected error occurred."
|
||||
"error_internal_error": "An unexpected error occurred.",
|
||||
|
||||
"nav_documents": "Documents",
|
||||
"nav_persons": "Persons",
|
||||
"nav_conversations": "Conversations",
|
||||
"nav_admin": "Admin",
|
||||
"nav_logout": "Sign out",
|
||||
|
||||
"btn_save": "Save",
|
||||
"btn_cancel": "Cancel",
|
||||
"btn_edit": "Edit",
|
||||
"btn_create": "Create",
|
||||
"btn_delete": "Delete",
|
||||
"btn_back_to_overview": "Back to overview",
|
||||
"btn_back": "Back",
|
||||
"btn_back_to_document": "Back to document",
|
||||
|
||||
"form_label_first_name": "First name",
|
||||
"form_label_last_name": "Last name",
|
||||
"form_label_alias": "Nickname / Alias",
|
||||
"form_placeholder_alias": "e.g. Grandma Frieda, Uncle Karl…",
|
||||
"form_label_date": "Date",
|
||||
"form_placeholder_date": "DD.MM.YYYY",
|
||||
"form_date_error": "Please enter in DD.MM.YYYY format, e.g. 20.12.2026",
|
||||
"form_label_location": "Location",
|
||||
"form_placeholder_location": "e.g. Berlin, Vienna…",
|
||||
"form_label_sender": "Sender",
|
||||
"form_label_receivers": "Recipients",
|
||||
"form_label_title": "Title *",
|
||||
"form_label_tags": "Tags",
|
||||
"form_label_content": "Content",
|
||||
"form_placeholder_content": "Brief description of the content…",
|
||||
"form_label_transcription": "Transcription",
|
||||
"form_placeholder_transcription": "Full text of the document…",
|
||||
"form_label_archive_location": "Storage location",
|
||||
"form_placeholder_archive_location": "e.g. Cabinet 3, Folder B",
|
||||
"form_helper_archive_location": "Where is the original document stored?",
|
||||
|
||||
"login_heading": "Sign in",
|
||||
"login_label_username": "Username",
|
||||
"login_label_password": "Password",
|
||||
"login_btn_submit": "Sign in",
|
||||
|
||||
"docs_search_placeholder": "Search in title, content, location...",
|
||||
"docs_btn_filter": "Filter",
|
||||
"docs_btn_reset_title": "Reset filter",
|
||||
"docs_filter_label_tags": "Tags",
|
||||
"docs_filter_label_sender": "Sender",
|
||||
"docs_filter_label_receivers": "Recipients",
|
||||
"docs_filter_label_from": "From",
|
||||
"docs_filter_label_to": "To",
|
||||
"docs_btn_new": "New document",
|
||||
"docs_empty_heading": "No documents found",
|
||||
"docs_empty_text": "Try adjusting the filters or changing the search term.",
|
||||
"docs_empty_btn_clear": "Clear all filters",
|
||||
"docs_list_from": "From",
|
||||
"docs_list_to": "To",
|
||||
"docs_list_unknown": "Unknown",
|
||||
|
||||
"doc_section_who_when": "Who & When",
|
||||
"doc_section_description": "Description",
|
||||
"doc_section_file": "File",
|
||||
"doc_file_upload_label": "Upload file",
|
||||
"doc_file_upload_note": "(optional)",
|
||||
"doc_file_replace_label": "Upload new file",
|
||||
"doc_file_replace_note": "(replaces the current file)",
|
||||
"doc_current_file_label": "Current file:",
|
||||
"doc_new_heading": "New document",
|
||||
"doc_edit_heading": "Edit",
|
||||
|
||||
"doc_section_details": "Details",
|
||||
"doc_label_document_date": "Document date",
|
||||
"doc_label_creation_location": "Place of creation",
|
||||
"doc_label_archive_location_original": "Storage location (original)",
|
||||
"doc_section_persons": "Persons",
|
||||
"doc_sender_not_specified": "Not specified",
|
||||
"doc_no_receivers": "No recipients",
|
||||
"doc_section_content": "Content",
|
||||
"doc_label_summary": "Summary",
|
||||
"doc_loading": "Loading document...",
|
||||
"doc_download_link": "Try direct download",
|
||||
"doc_no_scan": "No scan available",
|
||||
|
||||
"persons_heading": "Person directory",
|
||||
"persons_subtitle": "Browse the index of all recorded persons in the family archive.",
|
||||
"persons_btn_new": "New person",
|
||||
"persons_search_placeholder": "Search names...",
|
||||
"persons_empty_heading": "No persons found.",
|
||||
"persons_empty_text": "Try a different search term.",
|
||||
|
||||
"persons_new_heading": "New person",
|
||||
"persons_section_details": "Person details",
|
||||
|
||||
"person_edit_heading": "Edit person",
|
||||
"person_label_full_name": "Full name",
|
||||
"person_merge_heading": "Merge person",
|
||||
"person_merge_description": "This person will be merged into the selected target person. All documents and links will be transferred, then this person will be deleted.",
|
||||
"person_merge_target_label": "Merge with",
|
||||
"person_btn_merge": "Merge",
|
||||
"person_btn_merge_confirm": "Yes, merge",
|
||||
"person_merge_warning": "Warning: This action cannot be undone.",
|
||||
"person_docs_heading": "Sent documents",
|
||||
"person_no_docs": "This person has not yet been linked as a sender.",
|
||||
|
||||
"conv_heading": "Conversations",
|
||||
"conv_subtitle": "Follow the correspondence between two persons chronologically.",
|
||||
"conv_label_person_a": "Person A (Sender)",
|
||||
"conv_label_person_b": "Person B (Recipient)",
|
||||
"conv_label_from": "Period from",
|
||||
"conv_label_to": "Period to",
|
||||
"conv_sort_label": "Sort:",
|
||||
"conv_sort_newest": "Newest first",
|
||||
"conv_sort_oldest": "Oldest first",
|
||||
"conv_empty_heading": "Select two persons",
|
||||
"conv_empty_text": "The correspondence will be shown here.",
|
||||
"conv_no_results_heading": "No documents found.",
|
||||
"conv_no_results_text": "Try adjusting the time period.",
|
||||
|
||||
"admin_heading": "Admin Dashboard",
|
||||
"admin_tab_users": "Users",
|
||||
"admin_tab_groups": "Groups",
|
||||
"admin_tab_tags": "Tags",
|
||||
"admin_section_users": "User management",
|
||||
"admin_col_login": "Login",
|
||||
"admin_col_groups": "Groups",
|
||||
"admin_col_password": "Password",
|
||||
"admin_multiselect_hint": "Ctrl+Click to select",
|
||||
"admin_password_placeholder": "New PW (optional)",
|
||||
"admin_no_groups": "No groups",
|
||||
"admin_btn_delete_user_title": "Delete user",
|
||||
"admin_section_new_user": "Create new user",
|
||||
"admin_multiselect_hint_multi": "Ctrl+Click for multiple",
|
||||
"admin_multiselect_hint_full": "Ctrl+Click for multiple selection",
|
||||
"admin_section_tags": "Tags",
|
||||
"admin_tags_warning": "Warning: Renaming or deleting affects all linked documents.",
|
||||
"admin_btn_edit_tag_label": "Edit tag",
|
||||
"admin_tag_delete_confirm": "Really delete? The tag will be removed from all documents.",
|
||||
"admin_btn_delete_tag_label": "Delete tag",
|
||||
"admin_section_groups": "Group management",
|
||||
"admin_col_name": "Name",
|
||||
"admin_col_permissions": "Permissions",
|
||||
"admin_col_actions": "Actions",
|
||||
"admin_group_delete_confirm": "Really delete group?",
|
||||
"admin_section_new_group": "Create new group",
|
||||
"admin_group_name_placeholder": "Group name (e.g. Editors)",
|
||||
|
||||
"comp_typeahead_placeholder": "Type a name...",
|
||||
"comp_typeahead_loading": "Searching...",
|
||||
"comp_multiselect_placeholder": "Type a name...",
|
||||
"comp_multiselect_remove": "Remove",
|
||||
"comp_multiselect_loading": "Searching...",
|
||||
"comp_taginput_placeholder_create": "Add tags...",
|
||||
"comp_taginput_placeholder_filter": "Filter by tags...",
|
||||
"comp_taginput_remove": "Remove tag",
|
||||
"comp_taginput_create_hint": "Press Enter to create tag."
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://inlang.com/schema/inlang-message-format",
|
||||
"hello_world": "Hello, {name} from es!",
|
||||
|
||||
"error_document_not_found": "Documento no encontrado.",
|
||||
"error_document_no_file": "No hay ningún archivo asociado a este documento.",
|
||||
"error_file_not_found": "El archivo no pudo encontrarse en el almacenamiento.",
|
||||
@@ -10,5 +10,159 @@
|
||||
"error_unauthorized": "No ha iniciado sesión.",
|
||||
"error_forbidden": "No tiene permiso para realizar esta acción.",
|
||||
"error_validation_error": "La entrada no es válida.",
|
||||
"error_internal_error": "Se ha producido un error inesperado."
|
||||
"error_internal_error": "Se ha producido un error inesperado.",
|
||||
|
||||
"nav_documents": "Documentos",
|
||||
"nav_persons": "Personas",
|
||||
"nav_conversations": "Conversaciones",
|
||||
"nav_admin": "Admin",
|
||||
"nav_logout": "Cerrar sesión",
|
||||
|
||||
"btn_save": "Guardar",
|
||||
"btn_cancel": "Cancelar",
|
||||
"btn_edit": "Editar",
|
||||
"btn_create": "Crear",
|
||||
"btn_delete": "Eliminar",
|
||||
"btn_back_to_overview": "Volver al resumen",
|
||||
"btn_back": "Volver",
|
||||
"btn_back_to_document": "Volver al documento",
|
||||
|
||||
"form_label_first_name": "Nombre",
|
||||
"form_label_last_name": "Apellido",
|
||||
"form_label_alias": "Apodo / Alias",
|
||||
"form_placeholder_alias": "p.ej. Abuela Frieda, Tío Karl…",
|
||||
"form_label_date": "Fecha",
|
||||
"form_placeholder_date": "DD.MM.AAAA",
|
||||
"form_date_error": "Introduzca en formato DD.MM.AAAA, p.ej. 20.12.2026",
|
||||
"form_label_location": "Lugar",
|
||||
"form_placeholder_location": "p.ej. Berlín, Viena…",
|
||||
"form_label_sender": "Remitente",
|
||||
"form_label_receivers": "Destinatarios",
|
||||
"form_label_title": "Título *",
|
||||
"form_label_tags": "Etiquetas",
|
||||
"form_label_content": "Contenido",
|
||||
"form_placeholder_content": "Breve descripción del contenido…",
|
||||
"form_label_transcription": "Transcripción",
|
||||
"form_placeholder_transcription": "Texto completo del documento…",
|
||||
"form_label_archive_location": "Ubicación de almacenamiento",
|
||||
"form_placeholder_archive_location": "p.ej. Armario 3, Carpeta B",
|
||||
"form_helper_archive_location": "¿Dónde se encuentra el documento original?",
|
||||
|
||||
"login_heading": "Iniciar sesión",
|
||||
"login_label_username": "Usuario",
|
||||
"login_label_password": "Contraseña",
|
||||
"login_btn_submit": "Iniciar sesión",
|
||||
|
||||
"docs_search_placeholder": "Buscar en título, contenido, lugar...",
|
||||
"docs_btn_filter": "Filtrar",
|
||||
"docs_btn_reset_title": "Restablecer filtro",
|
||||
"docs_filter_label_tags": "Etiquetas",
|
||||
"docs_filter_label_sender": "Remitente",
|
||||
"docs_filter_label_receivers": "Destinatarios",
|
||||
"docs_filter_label_from": "Desde",
|
||||
"docs_filter_label_to": "Hasta",
|
||||
"docs_btn_new": "Nuevo documento",
|
||||
"docs_empty_heading": "No se encontraron documentos",
|
||||
"docs_empty_text": "Intente ajustar los filtros o cambiar el término de búsqueda.",
|
||||
"docs_empty_btn_clear": "Borrar todos los filtros",
|
||||
"docs_list_from": "De",
|
||||
"docs_list_to": "Para",
|
||||
"docs_list_unknown": "Desconocido",
|
||||
|
||||
"doc_section_who_when": "Quién & Cuándo",
|
||||
"doc_section_description": "Descripción",
|
||||
"doc_section_file": "Archivo",
|
||||
"doc_file_upload_label": "Subir archivo",
|
||||
"doc_file_upload_note": "(opcional)",
|
||||
"doc_file_replace_label": "Subir nuevo archivo",
|
||||
"doc_file_replace_note": "(reemplaza el archivo actual)",
|
||||
"doc_current_file_label": "Archivo actual:",
|
||||
"doc_new_heading": "Nuevo documento",
|
||||
"doc_edit_heading": "Editar",
|
||||
|
||||
"doc_section_details": "Detalles",
|
||||
"doc_label_document_date": "Fecha del documento",
|
||||
"doc_label_creation_location": "Lugar de creación",
|
||||
"doc_label_archive_location_original": "Ubicación de almacenamiento (original)",
|
||||
"doc_section_persons": "Personas",
|
||||
"doc_sender_not_specified": "No especificado",
|
||||
"doc_no_receivers": "Sin destinatarios",
|
||||
"doc_section_content": "Contenido",
|
||||
"doc_label_summary": "Resumen",
|
||||
"doc_loading": "Cargando documento...",
|
||||
"doc_download_link": "Intentar descarga directa",
|
||||
"doc_no_scan": "No hay escaneo disponible",
|
||||
|
||||
"persons_heading": "Directorio de personas",
|
||||
"persons_subtitle": "Explore el índice de todas las personas registradas en el archivo familiar.",
|
||||
"persons_btn_new": "Nueva persona",
|
||||
"persons_search_placeholder": "Buscar nombres...",
|
||||
"persons_empty_heading": "No se encontraron personas.",
|
||||
"persons_empty_text": "Pruebe con otro término de búsqueda.",
|
||||
|
||||
"persons_new_heading": "Nueva persona",
|
||||
"persons_section_details": "Datos de la persona",
|
||||
|
||||
"person_edit_heading": "Editar persona",
|
||||
"person_label_full_name": "Nombre completo",
|
||||
"person_merge_heading": "Fusionar persona",
|
||||
"person_merge_description": "Esta persona se fusionará con la persona de destino seleccionada. Todos los documentos y enlaces se transferirán y esta persona será eliminada.",
|
||||
"person_merge_target_label": "Fusionar con",
|
||||
"person_btn_merge": "Fusionar",
|
||||
"person_btn_merge_confirm": "Sí, fusionar",
|
||||
"person_merge_warning": "Atención: Esta acción no se puede deshacer.",
|
||||
"person_docs_heading": "Documentos enviados",
|
||||
"person_no_docs": "Esta persona aún no está vinculada como remitente.",
|
||||
|
||||
"conv_heading": "Conversaciones",
|
||||
"conv_subtitle": "Siga la correspondencia entre dos personas cronológicamente.",
|
||||
"conv_label_person_a": "Persona A (Remitente)",
|
||||
"conv_label_person_b": "Persona B (Destinatario)",
|
||||
"conv_label_from": "Período desde",
|
||||
"conv_label_to": "Período hasta",
|
||||
"conv_sort_label": "Ordenar:",
|
||||
"conv_sort_newest": "Más reciente primero",
|
||||
"conv_sort_oldest": "Más antiguo primero",
|
||||
"conv_empty_heading": "Seleccione dos personas",
|
||||
"conv_empty_text": "La correspondencia se mostrará aquí.",
|
||||
"conv_no_results_heading": "No se encontraron documentos.",
|
||||
"conv_no_results_text": "Intente ajustar el período de tiempo.",
|
||||
|
||||
"admin_heading": "Panel de administración",
|
||||
"admin_tab_users": "Usuarios",
|
||||
"admin_tab_groups": "Grupos",
|
||||
"admin_tab_tags": "Etiquetas",
|
||||
"admin_section_users": "Gestión de usuarios",
|
||||
"admin_col_login": "Login",
|
||||
"admin_col_groups": "Grupos",
|
||||
"admin_col_password": "Contraseña",
|
||||
"admin_multiselect_hint": "Ctrl+Clic para seleccionar",
|
||||
"admin_password_placeholder": "Nueva contraseña (opcional)",
|
||||
"admin_no_groups": "Sin grupos",
|
||||
"admin_btn_delete_user_title": "Eliminar usuario",
|
||||
"admin_section_new_user": "Crear nuevo usuario",
|
||||
"admin_multiselect_hint_multi": "Ctrl+Clic para varios",
|
||||
"admin_multiselect_hint_full": "Ctrl+Clic para selección múltiple",
|
||||
"admin_section_tags": "Etiquetas",
|
||||
"admin_tags_warning": "Advertencia: Renombrar o eliminar afecta a todos los documentos vinculados.",
|
||||
"admin_btn_edit_tag_label": "Editar etiqueta",
|
||||
"admin_tag_delete_confirm": "¿Realmente eliminar? La etiqueta se eliminará de todos los documentos.",
|
||||
"admin_btn_delete_tag_label": "Eliminar etiqueta",
|
||||
"admin_section_groups": "Gestión de grupos",
|
||||
"admin_col_name": "Nombre",
|
||||
"admin_col_permissions": "Permisos",
|
||||
"admin_col_actions": "Acciones",
|
||||
"admin_group_delete_confirm": "¿Realmente eliminar el grupo?",
|
||||
"admin_section_new_group": "Crear nuevo grupo",
|
||||
"admin_group_name_placeholder": "Nombre del grupo (p.ej. Editores)",
|
||||
|
||||
"comp_typeahead_placeholder": "Escriba un nombre...",
|
||||
"comp_typeahead_loading": "Buscando...",
|
||||
"comp_multiselect_placeholder": "Escriba un nombre...",
|
||||
"comp_multiselect_remove": "Eliminar",
|
||||
"comp_multiselect_loading": "Buscando...",
|
||||
"comp_taginput_placeholder_create": "Añadir etiquetas...",
|
||||
"comp_taginput_placeholder_filter": "Filtrar por etiquetas...",
|
||||
"comp_taginput_remove": "Eliminar etiqueta",
|
||||
"comp_taginput_create_hint": "Pulse Enter para crear etiqueta."
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"plugin.inlang.messageFormat": {
|
||||
"pathPattern": "./messages/{locale}.json"
|
||||
},
|
||||
"baseLocale": "en",
|
||||
"baseLocale": "de",
|
||||
"locales": [
|
||||
"de",
|
||||
"en",
|
||||
"es",
|
||||
"de"
|
||||
"es"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { components } from '$lib/generated/api';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
type Person = components['schemas']['Person'];
|
||||
|
||||
interface Props {
|
||||
@@ -76,7 +77,7 @@
|
||||
type="button"
|
||||
onclick={() => removePerson(person.id)}
|
||||
class="text-brand-navy/50 hover:text-red-500 focus:outline-none ml-0.5"
|
||||
aria-label="Entfernen"
|
||||
aria-label={m.comp_multiselect_remove()}
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
@@ -92,7 +93,7 @@
|
||||
bind:value={searchTerm}
|
||||
oninput={handleInput}
|
||||
onfocus={() => { updateDropdownPosition(); showDropdown = true; }}
|
||||
placeholder={selectedPersons.length === 0 ? 'Namen tippen...' : ''}
|
||||
placeholder={selectedPersons.length === 0 ? m.comp_multiselect_placeholder() : ''}
|
||||
class="flex-1 min-w-[120px] border-none p-1 focus:ring-0 text-sm bg-transparent outline-none"
|
||||
/>
|
||||
</div>
|
||||
@@ -103,7 +104,7 @@
|
||||
class="z-50 bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm"
|
||||
>
|
||||
{#if loading}
|
||||
<div class="p-2 text-gray-500 text-sm">Suche...</div>
|
||||
<div class="p-2 text-gray-500 text-sm">{m.comp_multiselect_loading()}</div>
|
||||
{:else}
|
||||
{#each results as person}
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { components } from '$lib/generated/api';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
type Person = components['schemas']['Person'];
|
||||
|
||||
interface Props {
|
||||
@@ -95,7 +96,7 @@
|
||||
bind:value={searchTerm}
|
||||
oninput={handleInput}
|
||||
onfocus={() => { updateDropdownPosition(); showDropdown = true; }}
|
||||
placeholder="Namen tippen..."
|
||||
placeholder={m.comp_typeahead_placeholder()}
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm border p-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
|
||||
@@ -105,7 +106,7 @@
|
||||
class="z-50 bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
|
||||
>
|
||||
{#if loading}
|
||||
<div class="p-2 text-gray-500 text-sm">Suche...</div>
|
||||
<div class="p-2 text-gray-500 text-sm">{m.comp_typeahead_loading()}</div>
|
||||
{:else}
|
||||
{#each results as person}
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
interface Props {
|
||||
tags?: string[];
|
||||
allowCreation?: boolean;
|
||||
@@ -88,7 +90,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => removeTag(i)}
|
||||
aria-label="Schlagwort entfernen"
|
||||
aria-label={m.comp_taginput_remove()}
|
||||
class="text-brand-navy/50 hover:text-red-500 focus:outline-none"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
@@ -113,8 +115,8 @@
|
||||
onfocus={() => fetchSuggestions(inputVal)}
|
||||
placeholder={tags.length === 0
|
||||
? allowCreation
|
||||
? 'Schlagworte hinzufügen...'
|
||||
: 'Nach Schlagworten filtern...'
|
||||
? m.comp_taginput_placeholder_create()
|
||||
: m.comp_taginput_placeholder_filter()
|
||||
: ''}
|
||||
class="w-full h-full border-none p-1 focus:ring-0 text-sm bg-transparent outline-none"
|
||||
/>
|
||||
@@ -143,6 +145,6 @@
|
||||
</div>
|
||||
</div>
|
||||
{#if allowCreation}
|
||||
<p class="text-xs text-gray-400 mt-1">Enter drücken um Schlagwort zu erstellen.</p>
|
||||
<p class="text-xs text-gray-400 mt-1">{m.comp_taginput_create_hint()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
import { enhance } from '$app/forms';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { setLocale, getLocale } from '$lib/paraglide/runtime';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const locales = ['DE', 'EN', 'ES'] as const;
|
||||
const localeMap = { DE: 'de', EN: 'en', ES: 'es' } as const;
|
||||
const activeLocale = $derived(getLocale().toUpperCase());
|
||||
|
||||
const isAdmin = $derived(page.data.user?.groups.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN')));
|
||||
|
||||
// Set after client-side hydration completes. Used by E2E tests to know the
|
||||
@@ -40,7 +46,7 @@
|
||||
? 'text-brand-navy bg-brand-purple/15 rounded'
|
||||
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
|
||||
>
|
||||
Dokumente
|
||||
{m.nav_documents()}
|
||||
</a>
|
||||
|
||||
<a
|
||||
@@ -50,7 +56,7 @@
|
||||
? 'text-brand-navy bg-brand-purple/15 rounded'
|
||||
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
|
||||
>
|
||||
Personen
|
||||
{m.nav_persons()}
|
||||
</a>
|
||||
|
||||
<a
|
||||
@@ -60,7 +66,7 @@
|
||||
? 'text-brand-navy bg-brand-purple/15 rounded'
|
||||
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
|
||||
>
|
||||
Konversationen
|
||||
{m.nav_conversations()}
|
||||
</a>
|
||||
{#if isAdmin}
|
||||
<a
|
||||
@@ -70,25 +76,41 @@
|
||||
? 'text-brand-navy bg-brand-purple/15 rounded'
|
||||
: 'text-gray-500 hover:text-brand-navy hover:bg-brand-sand/60 rounded'}"
|
||||
>
|
||||
Admin
|
||||
{m.nav_admin()}
|
||||
</a>
|
||||
{/if}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Right Side -->
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Language selector -->
|
||||
<div class="flex items-center gap-1 border-r border-gray-200 pr-3">
|
||||
{#each locales as locale}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => setLocale(localeMap[locale])}
|
||||
class="text-xs font-sans tracking-widest px-1.5 py-1 transition-colors
|
||||
{activeLocale === locale
|
||||
? 'font-bold text-brand-navy'
|
||||
: 'font-normal text-gray-400 hover:text-brand-navy'}"
|
||||
>
|
||||
{locale}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<form action="/logout" method="POST" use:enhance>
|
||||
<button
|
||||
type="submit"
|
||||
class="inline-flex items-center gap-1.5 text-xs text-gray-400 hover:text-brand-navy font-bold uppercase font-sans tracking-widest px-3 py-2 transition-colors"
|
||||
>
|
||||
<img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Account-SM.svg" alt="" aria-hidden="true" class="w-4 h-4 opacity-50" />
|
||||
Abmelden
|
||||
{m.nav_logout()}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
{/if}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { goto } from '$app/navigation';
|
||||
import TagInput from '$lib/components/TagInput.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { untrack } from 'svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -82,7 +83,7 @@ $effect(() => {
|
||||
type="text"
|
||||
bind:value={q}
|
||||
oninput={handleTextSearch}
|
||||
placeholder="Suche in Titel, Inhalt, Ort..."
|
||||
placeholder={m.docs_search_placeholder()}
|
||||
class="block w-full border-gray-300 py-2.5 pr-10 pl-3 placeholder-gray-400 shadow-sm focus:border-brand-navy focus:ring-brand-navy"
|
||||
/>
|
||||
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
@@ -96,14 +97,14 @@ $effect(() => {
|
||||
class="flex items-center gap-2 border border-gray-300 bg-gray-50 px-4 py-2.5 text-sm font-bold tracking-wide text-gray-600 uppercase transition hover:bg-gray-100 hover:text-brand-navy"
|
||||
>
|
||||
<img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Chevron/Chevron-Down-SM.svg" alt="" aria-hidden="true" class="h-4 w-4 transform transition-transform duration-200 {showAdvanced ? 'rotate-180' : ''}" />
|
||||
Filter
|
||||
{m.docs_btn_filter()}
|
||||
</button>
|
||||
|
||||
<!-- Reset Button -->
|
||||
<a
|
||||
href="/"
|
||||
class="flex items-center justify-center border border-transparent px-3 py-2.5 text-gray-400 transition hover:text-red-500"
|
||||
title="Filter zurücksetzen"
|
||||
title={m.docs_btn_reset_title()}
|
||||
>
|
||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Close-MD.svg" alt="" aria-hidden="true" class="h-5 w-5 opacity-40" />
|
||||
</a>
|
||||
@@ -118,7 +119,7 @@ $effect(() => {
|
||||
<!-- Tag Filter -->
|
||||
<div class="md:col-span-12">
|
||||
<p class="mb-2 block text-xs font-bold tracking-widest text-gray-500 uppercase">
|
||||
Schlagworte
|
||||
{m.docs_filter_label_tags()}
|
||||
</p>
|
||||
<TagInput bind:tags={tagNames} allowCreation={false} />
|
||||
</div>
|
||||
@@ -130,7 +131,7 @@ $effect(() => {
|
||||
>
|
||||
<PersonTypeahead
|
||||
name="senderId"
|
||||
label="Absender"
|
||||
label={m.docs_filter_label_sender()}
|
||||
bind:value={senderId}
|
||||
initialName={data.initialValues?.senderName}
|
||||
onchange={triggerSearch}
|
||||
@@ -145,7 +146,7 @@ $effect(() => {
|
||||
>
|
||||
<PersonTypeahead
|
||||
name="receiverId"
|
||||
label="Empfänger"
|
||||
label={m.docs_filter_label_receivers()}
|
||||
bind:value={receiverId}
|
||||
initialName={data.initialValues?.receiverName}
|
||||
onchange={triggerSearch}
|
||||
@@ -159,7 +160,7 @@ $effect(() => {
|
||||
<label
|
||||
for="from"
|
||||
class="mb-2 block text-xs font-bold tracking-widest text-gray-500 uppercase"
|
||||
>Von</label
|
||||
>{m.docs_filter_label_from()}</label
|
||||
>
|
||||
<input
|
||||
type="date"
|
||||
@@ -173,7 +174,7 @@ $effect(() => {
|
||||
<label
|
||||
for="to"
|
||||
class="mb-2 block text-xs font-bold tracking-widest text-gray-500 uppercase"
|
||||
>Bis</label
|
||||
>{m.docs_filter_label_to()}</label
|
||||
>
|
||||
<input
|
||||
type="date"
|
||||
@@ -195,7 +196,7 @@ $effect(() => {
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-brand-navy/60 transition-colors hover:text-brand-navy"
|
||||
>
|
||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Add/Add-General-MD.svg" alt="" aria-hidden="true" class="h-4 w-4" />
|
||||
Neues Dokument
|
||||
{m.docs_btn_new()}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -252,27 +253,27 @@ $effect(() => {
|
||||
<div class="flex items-baseline">
|
||||
<span
|
||||
class="w-10 font-sans text-xs font-bold tracking-wide text-gray-400 uppercase"
|
||||
>Von</span
|
||||
>{m.docs_list_from()}</span
|
||||
>
|
||||
{#if doc.sender}
|
||||
<span class="text-gray-900"
|
||||
>{doc.sender.firstName} {doc.sender.lastName}</span
|
||||
>
|
||||
{:else}
|
||||
<span class="text-gray-400 italic">Unbekannt</span>
|
||||
<span class="text-gray-400 italic">{m.docs_list_unknown()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-baseline">
|
||||
<span
|
||||
class="w-10 font-sans text-xs font-bold tracking-wide text-gray-400 uppercase"
|
||||
>An</span
|
||||
>{m.docs_list_to()}</span
|
||||
>
|
||||
{#if doc.receivers && doc.receivers.length > 0}
|
||||
<span class="text-gray-900">
|
||||
{doc.receivers.map((p) => p.firstName + ' ' + p.lastName).join(', ')}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-gray-400 italic">Unbekannt</span>
|
||||
<span class="text-gray-400 italic">{m.docs_list_unknown()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -312,15 +313,15 @@ $effect(() => {
|
||||
>
|
||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Mag-Glass-MD.svg" alt="" aria-hidden="true" class="h-6 w-6" />
|
||||
</div>
|
||||
<h3 class="font-serif text-lg font-medium text-brand-navy">Keine Dokumente gefunden</h3>
|
||||
<h3 class="font-serif text-lg font-medium text-brand-navy">{m.docs_empty_heading()}</h3>
|
||||
<p class="mt-1 font-sans text-sm text-gray-500">
|
||||
Versuchen Sie, die Filter anzupassen oder den Suchbegriff zu ändern.
|
||||
{m.docs_empty_text()}
|
||||
</p>
|
||||
<button
|
||||
onclick={() => goto('/')}
|
||||
class="mt-6 text-sm font-bold tracking-wide text-brand-mint uppercase transition hover:text-brand-navy"
|
||||
>
|
||||
Alle Filter löschen
|
||||
{m.docs_empty_btn_clear()}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
@@ -41,7 +42,7 @@
|
||||
|
||||
<div class="max-w-7xl mx-auto py-8 sm:px-6 lg:px-8 font-sans">
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-3xl font-serif text-brand-navy">Admin Dashboard</h1>
|
||||
<h1 class="text-3xl font-serif text-brand-navy">{m.admin_heading()}</h1>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex bg-white rounded-lg p-1 shadow-sm border border-gray-200">
|
||||
@@ -50,21 +51,21 @@
|
||||
'users'
|
||||
? 'bg-brand-navy text-white'
|
||||
: 'text-gray-500 hover:text-brand-navy'}"
|
||||
onclick={() => (activeTab = 'users')}>Benutzer</button
|
||||
onclick={() => (activeTab = 'users')}>{m.admin_tab_users()}</button
|
||||
>
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
|
||||
'groups'
|
||||
? 'bg-brand-navy text-white'
|
||||
: 'text-gray-500 hover:text-brand-navy'}"
|
||||
onclick={() => (activeTab = 'groups')}>Gruppen</button
|
||||
onclick={() => (activeTab = 'groups')}>{m.admin_tab_groups()}</button
|
||||
>
|
||||
<button
|
||||
class="px-4 py-2 text-sm font-bold uppercase tracking-wide rounded-md transition {activeTab ===
|
||||
'tags'
|
||||
? 'bg-brand-navy text-white'
|
||||
: 'text-gray-500 hover:text-brand-navy'}"
|
||||
onclick={() => (activeTab = 'tags')}>Schlagworte</button
|
||||
onclick={() => (activeTab = 'tags')}>{m.admin_tab_tags()}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,22 +79,22 @@
|
||||
{#if activeTab === 'users'}
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
||||
<h2 class="text-lg font-bold text-gray-700">Benutzerverwaltung</h2>
|
||||
<h2 class="text-lg font-bold text-gray-700">{m.admin_section_users()}</h2>
|
||||
</div>
|
||||
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Login</th
|
||||
>{m.admin_col_login()}</th
|
||||
>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Gruppen</th
|
||||
>{m.admin_col_groups()}</th
|
||||
>
|
||||
{#if editingUserId}
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Passwort</th
|
||||
>{m.admin_col_password()}</th
|
||||
>
|
||||
{/if}
|
||||
</tr>
|
||||
@@ -129,7 +130,7 @@
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
<p class="text-[10px] text-gray-400 mt-1">Strg+Klick für Auswahl</p>
|
||||
<p class="text-[10px] text-gray-400 mt-1">{m.admin_multiselect_hint()}</p>
|
||||
</td>
|
||||
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right align-top">
|
||||
@@ -147,7 +148,7 @@
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Neues PW (optional)"
|
||||
placeholder={m.admin_password_placeholder()}
|
||||
class="w-32 py-1 px-2 text-xs border border-brand-mint rounded"
|
||||
/>
|
||||
|
||||
@@ -156,14 +157,14 @@
|
||||
type="submit"
|
||||
class="bg-green-600 text-white px-2 py-1 rounded text-xs font-bold uppercase hover:bg-green-700"
|
||||
>
|
||||
Speichern
|
||||
{m.btn_save()}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={cancelEditUser}
|
||||
class="bg-gray-200 text-gray-600 px-2 py-1 rounded text-xs font-bold uppercase hover:bg-gray-300"
|
||||
>
|
||||
Abbrechen
|
||||
{m.btn_cancel()}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -184,7 +185,7 @@
|
||||
</span>
|
||||
{/each}
|
||||
{:else}
|
||||
<span class="text-xs text-gray-400 italic">Keine Gruppen</span>
|
||||
<span class="text-xs text-gray-400 italic">{m.admin_no_groups()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
@@ -194,7 +195,7 @@
|
||||
onclick={() => startEditUser(user.id)}
|
||||
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
|
||||
>
|
||||
Bearbeiten
|
||||
{m.btn_edit()}
|
||||
</button>
|
||||
|
||||
<form
|
||||
@@ -213,7 +214,7 @@
|
||||
<input type="hidden" name="id" value={user.id} />
|
||||
<button
|
||||
class="text-gray-300 hover:text-red-600 transition-colors p-1"
|
||||
title="Benutzer löschen"
|
||||
title={m.admin_btn_delete_user_title()}
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -236,7 +237,7 @@
|
||||
<!-- Create User Form -->
|
||||
<div class="p-6 bg-gray-50 border-t border-gray-200">
|
||||
<h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide">
|
||||
Neuen Benutzer anlegen
|
||||
{m.admin_section_new_user()}
|
||||
</h3>
|
||||
<form
|
||||
method="POST"
|
||||
@@ -254,7 +255,7 @@
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Passwort"
|
||||
placeholder={m.admin_col_password()}
|
||||
required
|
||||
class="rounded border-gray-300 text-sm w-full"
|
||||
/>
|
||||
@@ -265,19 +266,19 @@
|
||||
multiple
|
||||
class="rounded border-gray-300 text-sm w-full h-[42px] py-1"
|
||||
required
|
||||
title="Strg+Klick für mehrere"
|
||||
title={m.admin_multiselect_hint_multi()}
|
||||
>
|
||||
{#each data.groups as group}
|
||||
<option value={group.id}>{group.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<p class="text-[10px] text-gray-400 mt-1">Strg+Klick für Mehrfachauswahl</p>
|
||||
<p class="text-[10px] text-gray-400 mt-1">{m.admin_multiselect_hint_full()}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="bg-brand-navy text-white h-[42px] rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full"
|
||||
>Anlegen</button
|
||||
>{m.btn_create()}</button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
@@ -285,9 +286,9 @@
|
||||
{:else if activeTab === 'tags'}
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
|
||||
<div class="p-6 border-b border-gray-100 bg-yellow-50/50">
|
||||
<h2 class="text-lg font-bold text-gray-700">Schlagworte</h2>
|
||||
<h2 class="text-lg font-bold text-gray-700">{m.admin_section_tags()}</h2>
|
||||
<p class="text-xs text-yellow-800 mt-1">
|
||||
Warnung: Umbenennen oder Löschen wirkt sich auf alle verknüpften Dokumente aus.
|
||||
{m.admin_tags_warning()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -312,7 +313,7 @@
|
||||
bind:value={editingTagName}
|
||||
class="flex-1 border-brand-mint ring-1 ring-brand-mint rounded px-2 py-1 text-sm"
|
||||
/>
|
||||
<button aria-label="Speichern" class="text-green-600 hover:text-green-800"
|
||||
<button aria-label={m.btn_save()} class="text-green-600 hover:text-green-800"
|
||||
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
@@ -325,7 +326,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={cancelEditTag}
|
||||
aria-label="Abbrechen"
|
||||
aria-label={m.btn_cancel()}
|
||||
class="text-gray-400 hover:text-gray-600"
|
||||
><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
@@ -346,7 +347,7 @@
|
||||
>
|
||||
<button
|
||||
onclick={() => startEditTag(tag)}
|
||||
aria-label="Schlagwort bearbeiten"
|
||||
aria-label={m.admin_btn_edit_tag_label()}
|
||||
class="p-1 text-gray-400 hover:text-brand-navy"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
@@ -363,9 +364,7 @@
|
||||
action="?/deleteTag"
|
||||
use:enhance={({ cancel }) => {
|
||||
if (
|
||||
!confirm(
|
||||
'Wirklich löschen? Das Schlagwort wird aus allen Dokumenten entfernt.'
|
||||
)
|
||||
!confirm(m.admin_tag_delete_confirm())
|
||||
) {
|
||||
cancel();
|
||||
}
|
||||
@@ -376,7 +375,7 @@
|
||||
class="inline"
|
||||
>
|
||||
<input type="hidden" name="id" value={tag.id} />
|
||||
<button aria-label="Schlagwort löschen" class="p-1 text-gray-400 hover:text-red-600">
|
||||
<button aria-label={m.admin_btn_delete_tag_label()} class="p-1 text-gray-400 hover:text-red-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
@@ -396,21 +395,21 @@
|
||||
{:else if activeTab === 'groups'}
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-lg overflow-hidden" in:slide>
|
||||
<div class="p-6 border-b border-gray-100 flex justify-between items-center">
|
||||
<h2 class="text-lg font-bold text-gray-700">Gruppenverwaltung</h2>
|
||||
<h2 class="text-lg font-bold text-gray-700">{m.admin_section_groups()}</h2>
|
||||
</div>
|
||||
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Name</th
|
||||
>{m.admin_col_name()}</th
|
||||
>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Berechtigungen</th
|
||||
>{m.admin_col_permissions()}</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-bold text-gray-500 uppercase tracking-wider"
|
||||
>Aktionen</th
|
||||
>{m.admin_col_actions()}</th
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -460,7 +459,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 self-start sm:self-center">
|
||||
<button type="submit" aria-label="Speichern" class="text-green-600 hover:text-green-800 p-1">
|
||||
<button type="submit" aria-label={m.btn_save()} class="text-green-600 hover:text-green-800 p-1">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
@@ -473,7 +472,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={cancelEditGroup}
|
||||
aria-label="Abbrechen"
|
||||
aria-label={m.btn_cancel()}
|
||||
class="text-gray-400 hover:text-red-500 p-1"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
@@ -513,14 +512,14 @@
|
||||
onclick={() => startEditGroup(group.id)}
|
||||
class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide"
|
||||
>
|
||||
Bearbeiten
|
||||
{m.btn_edit()}
|
||||
</button>
|
||||
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteGroup"
|
||||
use:enhance={({ cancel }) => {
|
||||
if (!confirm('Gruppe wirklich löschen?')) {
|
||||
if (!confirm(m.admin_group_delete_confirm())) {
|
||||
cancel();
|
||||
}
|
||||
return async ({ update }) => {
|
||||
@@ -531,7 +530,7 @@
|
||||
<input type="hidden" name="id" value={group.id} />
|
||||
<button
|
||||
class="text-gray-300 hover:text-red-600 p-1 transition-colors"
|
||||
title="Löschen"
|
||||
title={m.btn_delete()}
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
@@ -554,7 +553,7 @@
|
||||
<!-- CREATE GROUP FORM -->
|
||||
<div class="p-6 bg-gray-50 border-t border-gray-200">
|
||||
<h3 class="text-xs font-bold uppercase text-gray-500 mb-4 tracking-wide">
|
||||
Neue Gruppe anlegen
|
||||
{m.admin_section_new_group()}
|
||||
</h3>
|
||||
<form
|
||||
method="POST"
|
||||
@@ -566,7 +565,7 @@
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Gruppenname (z.B. Editoren)"
|
||||
placeholder={m.admin_group_name_placeholder()}
|
||||
required
|
||||
class="rounded border-gray-300 text-sm w-full"
|
||||
/>
|
||||
@@ -590,7 +589,7 @@
|
||||
type="submit"
|
||||
class="bg-brand-navy text-white px-6 py-2 rounded text-sm font-bold uppercase hover:bg-brand-mint hover:text-brand-navy w-full md:w-auto"
|
||||
>
|
||||
Anlegen
|
||||
{m.btn_create()}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -39,9 +40,9 @@
|
||||
<div class="max-w-5xl mx-auto py-10 px-4">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-8 border-b border-brand-navy/10 pb-4">
|
||||
<h1 class="text-3xl font-serif font-medium text-brand-navy">Konversationen</h1>
|
||||
<h1 class="text-3xl font-serif font-medium text-brand-navy">{m.conv_heading()}</h1>
|
||||
<p class="text-brand-navy/60 font-sans text-sm mt-2">
|
||||
Verfolgen Sie den Schriftverkehr zwischen zwei Personen chronologisch.
|
||||
{m.conv_subtitle()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -54,7 +55,7 @@
|
||||
>
|
||||
<PersonTypeahead
|
||||
name="senderId"
|
||||
label="Person A (Absender)"
|
||||
label={m.conv_label_person_a()}
|
||||
bind:value={senderId}
|
||||
initialName={data.initialValues.senderName}
|
||||
onchange={() => applyFilters()}
|
||||
@@ -67,7 +68,7 @@
|
||||
>
|
||||
<PersonTypeahead
|
||||
name="receiverId"
|
||||
label="Person B (Empfänger)"
|
||||
label={m.conv_label_person_b()}
|
||||
bind:value={receiverId}
|
||||
initialName={data.initialValues.receiverName}
|
||||
onchange={() => applyFilters()}
|
||||
@@ -81,7 +82,7 @@
|
||||
<label
|
||||
for="dateFrom"
|
||||
class="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-2"
|
||||
>Zeitraum von</label
|
||||
>{m.conv_label_from()}</label
|
||||
>
|
||||
<input
|
||||
id="dateFrom"
|
||||
@@ -97,7 +98,7 @@
|
||||
<label
|
||||
for="dateTo"
|
||||
class="block text-xs font-bold uppercase tracking-widest text-gray-500 mb-2"
|
||||
>Zeitraum bis</label
|
||||
>{m.conv_label_to()}</label
|
||||
>
|
||||
<input
|
||||
id="dateTo"
|
||||
@@ -114,8 +115,8 @@
|
||||
onclick={toggleSort}
|
||||
class="w-full flex items-center justify-center h-[42px] border border-brand-sand text-xs font-bold uppercase tracking-wide text-brand-navy hover:bg-brand-navy hover:text-white transition-colors"
|
||||
>
|
||||
<span class="mr-2">Sortierung:</span>
|
||||
<span>{sortDir === 'DESC' ? 'Neueste zuerst' : 'Älteste zuerst'}</span>
|
||||
<span class="mr-2">{m.conv_sort_label()}</span>
|
||||
<span>{sortDir === 'DESC' ? m.conv_sort_newest() : m.conv_sort_oldest()}</span>
|
||||
<svg
|
||||
class="w-4 h-4 ml-2 transform {sortDir === 'ASC'
|
||||
? 'rotate-180'
|
||||
@@ -147,15 +148,15 @@
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
<p class="text-brand-navy font-serif text-lg">Wählen Sie zwei Personen aus</p>
|
||||
<p class="text-gray-500 font-sans text-sm mt-1">Die Korrespondenz wird hier angezeigt.</p>
|
||||
<p class="text-brand-navy font-serif text-lg">{m.conv_empty_heading()}</p>
|
||||
<p class="text-gray-500 font-sans text-sm mt-1">{m.conv_empty_text()}</p>
|
||||
</div>
|
||||
{:else if data.documents.length === 0}
|
||||
<div
|
||||
class="flex flex-col items-center justify-center py-24 bg-white border border-brand-sand rounded-sm text-center shadow-sm"
|
||||
>
|
||||
<p class="text-brand-navy font-serif">Keine Dokumente gefunden.</p>
|
||||
<p class="text-gray-400 text-sm mt-2">Versuchen Sie, den Zeitraum anzupassen.</p>
|
||||
<p class="text-brand-navy font-serif">{m.conv_no_results_heading()}</p>
|
||||
<p class="text-gray-400 text-sm mt-2">{m.conv_no_results_text()}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- CHAT CONTAINER -->
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
|
||||
|
||||
<h1>{m.hello_world({ name: 'SvelteKit User' })}</h1>
|
||||
<h1>{m.nav_documents()}</h1>
|
||||
<div>
|
||||
<button onclick={() => setLocale('en')}>en</button>
|
||||
<button onclick={() => setLocale('es')}>es</button>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const doc = $derived(data.document);
|
||||
@@ -53,7 +55,7 @@
|
||||
>
|
||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" alt="" aria-hidden="true" class="w-4 h-4" />
|
||||
</div>
|
||||
<span>Zurück</span>
|
||||
<span>{m.btn_back()}</span>
|
||||
</a>
|
||||
|
||||
<div class="flex items-center gap-4 overflow-hidden border-l border-gray-200 pl-6">
|
||||
@@ -77,7 +79,7 @@
|
||||
class="text-brand-navy bg-transparent border border-brand-navy hover:bg-brand-navy hover:text-white px-4 py-2 rounded text-sm font-medium transition flex items-center gap-2"
|
||||
>
|
||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Edit-Content-MD.svg" alt="" aria-hidden="true" class="w-4 h-4" />
|
||||
Bearbeiten
|
||||
{m.btn_edit()}
|
||||
</a>
|
||||
|
||||
{#if doc.filePath}
|
||||
@@ -105,7 +107,7 @@
|
||||
<h3
|
||||
class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2"
|
||||
>
|
||||
Details
|
||||
{m.doc_section_details()}
|
||||
</h3>
|
||||
<div class="space-y-5">
|
||||
<!-- Date -->
|
||||
@@ -117,7 +119,7 @@
|
||||
<span class="block font-serif text-lg text-brand-navy">
|
||||
{doc.documentDate ? new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }).format(new Date(doc.documentDate + 'T12:00:00')) : '—'}
|
||||
</span>
|
||||
<span class="text-xs font-sans text-gray-500">Dokumentendatum</span>
|
||||
<span class="text-xs font-sans text-gray-500">{m.doc_label_document_date()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -130,7 +132,7 @@
|
||||
<span class="block font-serif text-lg text-brand-navy">
|
||||
{doc.location ? doc.location : '—'}
|
||||
</span>
|
||||
<span class="text-xs font-sans text-gray-500">Erstellungsort</span>
|
||||
<span class="text-xs font-sans text-gray-500">{m.doc_label_creation_location()}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -144,7 +146,7 @@
|
||||
<span class="block font-serif text-lg text-brand-navy">
|
||||
{doc.documentLocation}
|
||||
</span>
|
||||
<span class="text-xs font-sans text-gray-500">Aufbewahrungsort (Original)</span>
|
||||
<span class="text-xs font-sans text-gray-500">{m.doc_label_archive_location_original()}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -167,7 +169,7 @@
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<span class="text-xs font-sans text-gray-500">Schlagworte</span>
|
||||
<span class="text-xs font-sans text-gray-500">{m.form_label_tags()}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -179,11 +181,11 @@
|
||||
<h3
|
||||
class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2"
|
||||
>
|
||||
Personen
|
||||
{m.doc_section_persons()}
|
||||
</h3>
|
||||
|
||||
<div class="mb-6">
|
||||
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Absender</span>
|
||||
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.form_label_sender()}</span>
|
||||
{#if doc.sender}
|
||||
<a
|
||||
href="/persons/{doc.sender.id}"
|
||||
@@ -209,12 +211,12 @@
|
||||
</div>
|
||||
</a>
|
||||
{:else}
|
||||
<span class="text-sm font-serif text-gray-400 italic">Nicht angegeben</span>
|
||||
<span class="text-sm font-serif text-gray-400 italic">{m.doc_sender_not_specified()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Empfänger</span>
|
||||
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.form_label_receivers()}</span>
|
||||
{#if doc.receivers && doc.receivers.length > 0}
|
||||
<div class="space-y-2">
|
||||
{#each doc.receivers as receiver}
|
||||
@@ -248,7 +250,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-sm font-serif text-gray-400 italic">Keine Empfänger</span>
|
||||
<span class="text-sm font-serif text-gray-400 italic">{m.doc_no_receivers()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -259,13 +261,13 @@
|
||||
<h3
|
||||
class="text-xs font-sans font-bold text-brand-navy uppercase tracking-widest mb-4 border-b border-brand-sand pb-2"
|
||||
>
|
||||
Inhalt
|
||||
{m.doc_section_content()}
|
||||
</h3>
|
||||
|
||||
<div class="space-y-6">
|
||||
{#if doc.summary}
|
||||
<div>
|
||||
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Zusammenfassung</span>
|
||||
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.doc_label_summary()}</span>
|
||||
<div
|
||||
class="bg-brand-sand/30 p-5 rounded border border-brand-sand text-sm font-serif text-brand-navy leading-relaxed whitespace-pre-wrap"
|
||||
>
|
||||
@@ -276,7 +278,7 @@
|
||||
|
||||
{#if doc.transcription}
|
||||
<div>
|
||||
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">Transkription</span>
|
||||
<span class="text-xs font-sans text-gray-400 block mb-2 uppercase">{m.form_label_transcription()}</span>
|
||||
<div
|
||||
class="bg-brand-sand/30 p-5 rounded border border-brand-sand text-sm font-serif text-brand-navy leading-relaxed whitespace-pre-wrap"
|
||||
>
|
||||
@@ -314,7 +316,7 @@
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="font-sans text-sm tracking-wide">Lade Dokument...</span>
|
||||
<span class="font-sans text-sm tracking-wide">{m.doc_loading()}</span>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="text-gray-400 text-center px-4">
|
||||
@@ -325,7 +327,7 @@
|
||||
target="_blank"
|
||||
class="underline hover:text-white text-sm"
|
||||
>
|
||||
Direkter Download versuchen
|
||||
{m.doc_download_link()}
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -334,7 +336,7 @@
|
||||
<div class="bg-white/5 p-8 rounded-full mb-6">
|
||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/PDF-Document-MD.svg" alt="" aria-hidden="true" class="w-12 h-12 opacity-50 invert" />
|
||||
</div>
|
||||
<p class="font-sans text-sm tracking-wide uppercase">Kein Scan vorhanden</p>
|
||||
<p class="font-sans text-sm tracking-wide uppercase">{m.doc_no_scan()}</p>
|
||||
</div>
|
||||
{:else if fileUrl && doc.originalFilename.toLowerCase().endsWith('.pdf')}
|
||||
<iframe
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import PersonMultiSelect from '$lib/components/PersonMultiSelect.svelte';
|
||||
import { untrack } from 'svelte';
|
||||
import { isoToGerman, germanToIso } from '$lib/utils';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
@@ -43,10 +44,10 @@
|
||||
<div class="mb-6">
|
||||
<a href="/documents/{doc.id}" class="inline-flex items-center text-xs font-bold uppercase tracking-widest text-gray-500 hover:text-brand-navy transition-colors group mb-4">
|
||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" alt="" aria-hidden="true" class="w-4 h-4 mr-2 transform group-hover:-translate-x-1 transition-transform" />
|
||||
Zurück zum Dokument
|
||||
{m.btn_back_to_document()}
|
||||
</a>
|
||||
<h1 class="text-3xl font-serif text-brand-navy">
|
||||
Bearbeiten — <span class="text-brand-navy/70">{doc.title || doc.originalFilename}</span>
|
||||
{m.doc_edit_heading()} — <span class="text-brand-navy/70">{doc.title || doc.originalFilename}</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -58,20 +59,20 @@
|
||||
|
||||
<!-- ── Section 1: Wer & Wann ── -->
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
|
||||
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Wer & Wann</h2>
|
||||
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.doc_section_who_when()}</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
|
||||
<!-- Datum -->
|
||||
<div>
|
||||
<label for="documentDate" class="block text-sm font-medium text-gray-700 mb-1">Datum</label>
|
||||
<label for="documentDate" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_date()}</label>
|
||||
<input
|
||||
id="documentDate"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
value={dateDisplay}
|
||||
oninput={handleDateInput}
|
||||
placeholder="TT.MM.JJJJ"
|
||||
placeholder={m.form_placeholder_date()}
|
||||
maxlength="10"
|
||||
class="block w-full rounded border-gray-300 shadow-sm p-2 border text-sm
|
||||
{dateInvalid ? 'border-red-400 focus:border-red-500 focus:ring-red-500' : 'focus:border-brand-navy focus:ring-brand-navy'}"
|
||||
@@ -79,19 +80,19 @@
|
||||
/>
|
||||
<input type="hidden" name="documentDate" value={dateIso} />
|
||||
{#if dateInvalid}
|
||||
<p id="date-error" class="mt-1 text-xs text-red-600">Bitte im Format TT.MM.JJJJ eingeben, z.B. 20.12.2026</p>
|
||||
<p id="date-error" class="mt-1 text-xs text-red-600">{m.form_date_error()}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Ort -->
|
||||
<div>
|
||||
<label for="location" class="block text-sm font-medium text-gray-700 mb-1">Ort</label>
|
||||
<label for="location" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_location()}</label>
|
||||
<input
|
||||
id="location"
|
||||
type="text"
|
||||
name="location"
|
||||
value={doc.location || ''}
|
||||
placeholder="z.B. Berlin, Wien…"
|
||||
placeholder={m.form_placeholder_location()}
|
||||
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm"
|
||||
/>
|
||||
</div>
|
||||
@@ -100,7 +101,7 @@
|
||||
<div>
|
||||
<PersonTypeahead
|
||||
name="senderId"
|
||||
label="Absender"
|
||||
label={m.form_label_sender()}
|
||||
bind:value={senderId}
|
||||
initialName={doc.sender ? `${doc.sender.firstName} ${doc.sender.lastName}` : ''}
|
||||
/>
|
||||
@@ -108,7 +109,7 @@
|
||||
|
||||
<!-- Empfänger -->
|
||||
<div>
|
||||
<p class="block text-sm font-medium text-gray-700 mb-1">Empfänger</p>
|
||||
<p class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_receivers()}</p>
|
||||
<PersonMultiSelect bind:selectedPersons={selectedReceivers} />
|
||||
</div>
|
||||
|
||||
@@ -117,13 +118,13 @@
|
||||
|
||||
<!-- ── Section 2: Beschreibung ── -->
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
|
||||
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Beschreibung</h2>
|
||||
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.doc_section_description()}</h2>
|
||||
|
||||
<div class="space-y-5">
|
||||
|
||||
<!-- Titel -->
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Titel *</label>
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_title()} *</label>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
@@ -136,33 +137,33 @@
|
||||
|
||||
<!-- Aufbewahrungsort -->
|
||||
<div>
|
||||
<label for="documentLocation" class="block text-sm font-medium text-gray-700 mb-1">Aufbewahrungsort</label>
|
||||
<label for="documentLocation" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_archive_location()}</label>
|
||||
<input
|
||||
id="documentLocation"
|
||||
type="text"
|
||||
name="documentLocation"
|
||||
value={doc.documentLocation || ''}
|
||||
placeholder="z.B. Schrank 3, Mappe B"
|
||||
placeholder={m.form_placeholder_archive_location()}
|
||||
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-400">Wo befindet sich das Originaldokument?</p>
|
||||
<p class="mt-1 text-xs text-gray-400">{m.form_helper_archive_location()}</p>
|
||||
</div>
|
||||
|
||||
<!-- Schlagworte -->
|
||||
<div>
|
||||
<p class="block text-sm font-medium text-gray-700 mb-1">Schlagworte</p>
|
||||
<p class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_tags()}</p>
|
||||
<TagInput bind:tags />
|
||||
<input type="hidden" name="tags" value={tags.join(',')} />
|
||||
</div>
|
||||
|
||||
<!-- Inhalt -->
|
||||
<div>
|
||||
<label for="summary" class="block text-sm font-medium text-gray-700 mb-1">Inhalt</label>
|
||||
<label for="summary" class="block text-sm font-medium text-gray-700 mb-1">{m.form_label_content()}</label>
|
||||
<textarea
|
||||
id="summary"
|
||||
name="summary"
|
||||
rows="5"
|
||||
placeholder="Kurze Beschreibung des Inhalts…"
|
||||
placeholder={m.form_placeholder_content()}
|
||||
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm font-serif"
|
||||
>{doc.summary || ''}</textarea>
|
||||
</div>
|
||||
@@ -172,27 +173,27 @@
|
||||
|
||||
<!-- ── Section 3: Transkription ── -->
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
|
||||
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Transkription</h2>
|
||||
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.form_label_transcription()}</h2>
|
||||
<textarea
|
||||
id="transcription"
|
||||
name="transcription"
|
||||
rows="12"
|
||||
placeholder="Vollständiger Text des Dokuments…"
|
||||
placeholder={m.form_placeholder_transcription()}
|
||||
class="block w-full rounded border-gray-300 shadow-sm p-2 border focus:border-brand-navy focus:ring-brand-navy text-sm font-serif"
|
||||
>{doc.transcription || ''}</textarea>
|
||||
</div>
|
||||
|
||||
<!-- ── Section 4: Datei ── -->
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
|
||||
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Datei</h2>
|
||||
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">{m.doc_section_file()}</h2>
|
||||
|
||||
<div class="flex items-center gap-3 mb-4 text-sm text-gray-600 bg-brand-sand/20 rounded px-3 py-2">
|
||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/PDF-Document-MD.svg" alt="" aria-hidden="true" class="w-4 h-4 flex-shrink-0" />
|
||||
<span>Aktuelle Datei: <strong class="text-brand-navy font-medium">{doc.originalFilename}</strong></span>
|
||||
<span>{m.doc_current_file_label()} <strong class="text-brand-navy font-medium">{doc.originalFilename}</strong></span>
|
||||
</div>
|
||||
|
||||
<label for="file-upload" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Neue Datei hochladen <span class="font-normal text-gray-400">(ersetzt die aktuelle Datei)</span>
|
||||
{m.doc_file_replace_label()} <span class="font-normal text-gray-400">({m.doc_file_replace_note()})</span>
|
||||
</label>
|
||||
<input
|
||||
id="file-upload"
|
||||
@@ -213,13 +214,13 @@
|
||||
href="/documents/{doc.id}"
|
||||
class="text-sm text-gray-500 hover:text-brand-navy transition-colors font-medium"
|
||||
>
|
||||
Abbrechen
|
||||
{m.btn_cancel()}
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-6 py-2 bg-brand-navy text-white text-sm font-bold uppercase tracking-widest rounded hover:bg-brand-navy/80 transition-colors"
|
||||
>
|
||||
Speichern
|
||||
{m.btn_save()}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { enhance } from '$app/forms';
|
||||
import TagInput from '$lib/components/TagInput.svelte';
|
||||
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||
import PersonMultiSelect from '$lib/components/PersonMultiSelect.svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let { form } = $props();
|
||||
|
||||
@@ -61,9 +62,9 @@ function handleDateInput(e: Event) {
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
Zurück zur Übersicht
|
||||
{m.btn_back_to_overview()}
|
||||
</a>
|
||||
<h1 class="font-serif text-3xl text-brand-navy">Neues Dokument</h1>
|
||||
<h1 class="font-serif text-3xl text-brand-navy">{m.doc_new_heading()}</h1>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
@@ -73,13 +74,13 @@ function handleDateInput(e: Event) {
|
||||
<form method="POST" enctype="multipart/form-data" use:enhance class="space-y-6 pb-20">
|
||||
<!-- ── Section 1: Wer & Wann ── -->
|
||||
<div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm">
|
||||
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">Wer & Wann</h2>
|
||||
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">{m.doc_section_who_when()}</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
<!-- Datum -->
|
||||
<div>
|
||||
<label for="documentDate" class="mb-1 block text-sm font-medium text-gray-700"
|
||||
>Datum</label
|
||||
>{m.form_label_date()}</label
|
||||
>
|
||||
<input
|
||||
id="documentDate"
|
||||
@@ -87,7 +88,7 @@ function handleDateInput(e: Event) {
|
||||
inputmode="numeric"
|
||||
value={dateDisplay}
|
||||
oninput={handleDateInput}
|
||||
placeholder="TT.MM.JJJJ"
|
||||
placeholder={m.form_placeholder_date()}
|
||||
maxlength="10"
|
||||
class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm
|
||||
{dateInvalid ? 'border-red-400 focus:border-red-500 focus:ring-red-500' : 'focus:border-brand-navy focus:ring-brand-navy'}"
|
||||
@@ -96,31 +97,31 @@ function handleDateInput(e: Event) {
|
||||
<input type="hidden" name="documentDate" value={dateIso} />
|
||||
{#if dateInvalid}
|
||||
<p id="date-error" class="mt-1 text-xs text-red-600">
|
||||
Bitte im Format TT.MM.JJJJ eingeben, z.B. 20.12.2026
|
||||
{m.form_date_error()}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Ort -->
|
||||
<div>
|
||||
<label for="location" class="mb-1 block text-sm font-medium text-gray-700">Ort</label>
|
||||
<label for="location" class="mb-1 block text-sm font-medium text-gray-700">{m.form_label_location()}</label>
|
||||
<input
|
||||
id="location"
|
||||
type="text"
|
||||
name="location"
|
||||
placeholder="z.B. Berlin, Wien…"
|
||||
placeholder={m.form_placeholder_location()}
|
||||
class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Absender -->
|
||||
<div>
|
||||
<PersonTypeahead name="senderId" label="Absender" bind:value={senderId} />
|
||||
<PersonTypeahead name="senderId" label={m.form_label_sender()} bind:value={senderId} />
|
||||
</div>
|
||||
|
||||
<!-- Empfänger -->
|
||||
<div>
|
||||
<p class="mb-1 block text-sm font-medium text-gray-700">Empfänger</p>
|
||||
<p class="mb-1 block text-sm font-medium text-gray-700">{m.form_label_receivers()}</p>
|
||||
<PersonMultiSelect bind:selectedPersons={selectedReceivers} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,12 +129,12 @@ function handleDateInput(e: Event) {
|
||||
|
||||
<!-- ── Section 2: Beschreibung ── -->
|
||||
<div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm">
|
||||
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">Beschreibung</h2>
|
||||
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">{m.doc_section_description()}</h2>
|
||||
|
||||
<div class="space-y-5">
|
||||
<!-- Titel -->
|
||||
<div>
|
||||
<label for="title" class="mb-1 block text-sm font-medium text-gray-700">Titel *</label>
|
||||
<label for="title" class="mb-1 block text-sm font-medium text-gray-700">{m.form_label_title()} *</label>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
@@ -146,33 +147,33 @@ function handleDateInput(e: Event) {
|
||||
<!-- Aufbewahrungsort -->
|
||||
<div>
|
||||
<label for="documentLocation" class="mb-1 block text-sm font-medium text-gray-700"
|
||||
>Aufbewahrungsort</label
|
||||
>{m.form_label_archive_location()}</label
|
||||
>
|
||||
<input
|
||||
id="documentLocation"
|
||||
type="text"
|
||||
name="documentLocation"
|
||||
placeholder="z.B. Schrank 3, Mappe B"
|
||||
placeholder={m.form_placeholder_archive_location()}
|
||||
class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-gray-400">Wo befindet sich das Originaldokument?</p>
|
||||
<p class="mt-1 text-xs text-gray-400">{m.form_helper_archive_location()}</p>
|
||||
</div>
|
||||
|
||||
<!-- Schlagworte -->
|
||||
<div>
|
||||
<p class="mb-1 block text-sm font-medium text-gray-700">Schlagworte</p>
|
||||
<p class="mb-1 block text-sm font-medium text-gray-700">{m.form_label_tags()}</p>
|
||||
<TagInput bind:tags={tags} />
|
||||
<input type="hidden" name="tags" value={tags.join(',')} />
|
||||
</div>
|
||||
|
||||
<!-- Inhalt -->
|
||||
<div>
|
||||
<label for="summary" class="mb-1 block text-sm font-medium text-gray-700">Inhalt</label>
|
||||
<label for="summary" class="mb-1 block text-sm font-medium text-gray-700">{m.form_label_content()}</label>
|
||||
<textarea
|
||||
id="summary"
|
||||
name="summary"
|
||||
rows="5"
|
||||
placeholder="Kurze Beschreibung des Inhalts…"
|
||||
placeholder={m.form_placeholder_content()}
|
||||
class="block w-full rounded border border-gray-300 p-2 font-serif text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy"
|
||||
></textarea>
|
||||
</div>
|
||||
@@ -181,22 +182,22 @@ function handleDateInput(e: Event) {
|
||||
|
||||
<!-- ── Section 3: Transkription ── -->
|
||||
<div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm">
|
||||
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">Transkription</h2>
|
||||
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">{m.form_label_transcription()}</h2>
|
||||
<textarea
|
||||
id="transcription"
|
||||
name="transcription"
|
||||
rows="12"
|
||||
placeholder="Vollständiger Text des Dokuments…"
|
||||
placeholder={m.form_placeholder_transcription()}
|
||||
class="block w-full rounded border border-gray-300 p-2 font-serif text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- ── Section 4: Datei ── -->
|
||||
<div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm">
|
||||
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">Datei</h2>
|
||||
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">{m.doc_section_file()}</h2>
|
||||
|
||||
<label for="file-upload" class="mb-1 block text-sm font-medium text-gray-700">
|
||||
Datei hochladen <span class="font-normal text-gray-400">(optional)</span>
|
||||
{m.doc_file_upload_label()} <span class="font-normal text-gray-400">({m.doc_file_upload_note()})</span>
|
||||
</label>
|
||||
<input
|
||||
id="file-upload"
|
||||
@@ -216,13 +217,13 @@ function handleDateInput(e: Event) {
|
||||
class="sticky bottom-0 z-10 -mx-4 flex items-center justify-between border-t border-brand-sand bg-white px-6 py-4 shadow-[0_-2px_8px_rgba(0,0,0,0.06)]"
|
||||
>
|
||||
<a href="/" class="text-sm font-medium text-gray-500 transition-colors hover:text-brand-navy">
|
||||
Abbrechen
|
||||
{m.btn_cancel()}
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded bg-brand-navy px-6 py-2 text-sm font-bold tracking-widest text-white uppercase transition-colors hover:bg-brand-navy/80"
|
||||
>
|
||||
Speichern
|
||||
{m.btn_save()}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
let { form }: { form?: { error?: string; success?: boolean } } = $props();
|
||||
</script>
|
||||
|
||||
@@ -17,17 +18,17 @@
|
||||
|
||||
<!-- Card -->
|
||||
<div class="bg-white border border-brand-sand rounded-sm shadow-sm p-8">
|
||||
<h1 class="font-sans text-sm font-bold uppercase tracking-widest text-brand-navy mb-6">Anmelden</h1>
|
||||
<h1 class="font-sans text-sm font-bold uppercase tracking-widest text-brand-navy mb-6">{m.login_heading()}</h1>
|
||||
|
||||
<form method="POST" action="?/login" class="space-y-5">
|
||||
<div>
|
||||
<label for="username" class="block text-xs font-bold font-sans uppercase tracking-widest text-gray-500 mb-1.5">Benutzername</label>
|
||||
<label for="username" class="block text-xs font-bold font-sans uppercase tracking-widest text-gray-500 mb-1.5">{m.login_label_username()}</label>
|
||||
<input type="text" name="username" id="username" required autocomplete="username"
|
||||
class="block w-full border border-gray-300 py-2.5 px-3 text-sm font-serif text-brand-navy placeholder-gray-400 focus:border-brand-navy focus:ring-1 focus:ring-brand-navy focus:outline-none" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-xs font-bold font-sans uppercase tracking-widest text-gray-500 mb-1.5">Passwort</label>
|
||||
<label for="password" class="block text-xs font-bold font-sans uppercase tracking-widest text-gray-500 mb-1.5">{m.login_label_password()}</label>
|
||||
<input type="password" name="password" id="password" required autocomplete="current-password"
|
||||
class="block w-full border border-gray-300 py-2.5 px-3 text-sm font-serif text-brand-navy placeholder-gray-400 focus:border-brand-navy focus:ring-1 focus:ring-brand-navy focus:outline-none" />
|
||||
</div>
|
||||
@@ -38,7 +39,7 @@
|
||||
|
||||
<button type="submit"
|
||||
class="w-full bg-brand-navy text-white py-2.5 text-xs font-bold uppercase tracking-widest font-sans hover:bg-brand-navy/90 transition-colors mt-2">
|
||||
Anmelden
|
||||
{m.login_btn_submit()}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
@@ -20,16 +21,16 @@ function handleSearch(e: Event) {
|
||||
class="mb-10 flex flex-col justify-between gap-6 border-b border-brand-navy/10 pb-6 md:flex-row md:items-end"
|
||||
>
|
||||
<div>
|
||||
<h1 class="font-serif text-3xl font-medium text-brand-navy">Personenverzeichnis</h1>
|
||||
<h1 class="font-serif text-3xl font-medium text-brand-navy">{m.persons_heading()}</h1>
|
||||
<p class="mt-2 max-w-xl font-sans text-sm text-brand-navy/60">
|
||||
Durchsuchen Sie den Index aller erfassten Personen im Familienarchiv.
|
||||
{m.persons_subtitle()}
|
||||
</p>
|
||||
<a
|
||||
href="/persons/new"
|
||||
class="mt-3 inline-flex items-center gap-1 text-sm font-medium text-brand-navy/60 transition-colors hover:text-brand-navy"
|
||||
>
|
||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Add/Add-General-MD.svg" alt="" aria-hidden="true" class="h-4 w-4" />
|
||||
Neue Person
|
||||
{m.persons_btn_new()}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -40,7 +41,7 @@ function handleSearch(e: Event) {
|
||||
<input
|
||||
id="search"
|
||||
type="text"
|
||||
placeholder="Namen suchen..."
|
||||
placeholder={m.persons_search_placeholder()}
|
||||
value={data.q || ''}
|
||||
oninput={handleSearch}
|
||||
class="block w-full rounded-sm border border-gray-300 bg-white py-2.5 pr-10 pl-4 font-sans text-sm text-brand-navy placeholder-gray-400 shadow-sm focus:border-brand-navy focus:ring-1 focus:ring-brand-navy focus:outline-none"
|
||||
@@ -63,8 +64,8 @@ function handleSearch(e: Event) {
|
||||
>
|
||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Account-MD.svg" alt="" aria-hidden="true" class="h-6 w-6" />
|
||||
</div>
|
||||
<p class="font-serif text-lg text-brand-navy">Keine Personen gefunden.</p>
|
||||
<p class="mt-1 font-sans text-sm text-gray-500">Versuchen Sie einen anderen Suchbegriff.</p>
|
||||
<p class="font-serif text-lg text-brand-navy">{m.persons_empty_heading()}</p>
|
||||
<p class="mt-1 font-sans text-sm text-gray-500">{m.persons_empty_text()}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import PersonTypeahead from '$lib/components/PersonTypeahead.svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
@@ -29,7 +30,7 @@
|
||||
<div class="mb-6">
|
||||
<a href="/persons" class="inline-flex items-center text-xs font-bold uppercase tracking-widest text-gray-500 hover:text-brand-navy transition-colors group">
|
||||
<img src="/degruyter-icons/Simple/Medium-24px/SVG/Action/Arrow/Arrow-Left-MD.svg" alt="" aria-hidden="true" class="w-4 h-4 mr-2 transform group-hover:-translate-x-1 transition-transform" />
|
||||
Zurück zur Übersicht
|
||||
{m.btn_back_to_overview()}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +43,7 @@
|
||||
<!-- Edit Form -->
|
||||
<form method="POST" action="?/update" use:enhance>
|
||||
<div class="flex flex-col gap-6">
|
||||
<h2 class="text-xl font-serif text-brand-navy border-b border-gray-100 pb-3">Person bearbeiten</h2>
|
||||
<h2 class="text-xl font-serif text-brand-navy border-b border-gray-100 pb-3">{m.person_edit_heading()}</h2>
|
||||
|
||||
{#if form?.updateError}
|
||||
<p class="text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">{form.updateError}</p>
|
||||
@@ -50,7 +51,7 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="firstName" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">Vorname *</label>
|
||||
<label for="firstName" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">{m.form_label_first_name()} *</label>
|
||||
<input
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
@@ -61,7 +62,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lastName" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">Nachname *</label>
|
||||
<label for="lastName" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">{m.form_label_last_name()} *</label>
|
||||
<input
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
@@ -72,7 +73,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="alias" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">Rufname / Alias</label>
|
||||
<label for="alias" class="block text-xs font-bold uppercase tracking-widest text-gray-400 mb-1">{m.form_label_alias()}</label>
|
||||
<input
|
||||
id="alias"
|
||||
name="alias"
|
||||
@@ -85,10 +86,10 @@
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit" class="px-5 py-2 bg-brand-navy text-white text-sm font-bold uppercase tracking-widest rounded hover:bg-brand-navy/80 transition-colors">
|
||||
Speichern
|
||||
{m.btn_save()}
|
||||
</button>
|
||||
<button type="button" onclick={() => (editMode = false)} class="px-5 py-2 border border-gray-300 text-gray-600 text-sm font-bold uppercase tracking-widest rounded hover:bg-gray-50 transition-colors">
|
||||
Abbrechen
|
||||
{m.btn_cancel()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,19 +110,19 @@
|
||||
</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">
|
||||
<img src="/degruyter-icons/Simple/Small-16px/SVG/Action/Edit-Content-SM.svg" alt="" aria-hidden="true" class="w-3.5 h-3.5" />
|
||||
Bearbeiten
|
||||
{m.btn_edit()}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div>
|
||||
<span class="block text-xs font-bold font-sans text-gray-400 uppercase tracking-widest mb-1">Voller Name</span>
|
||||
<span class="block text-xs font-bold font-sans text-gray-400 uppercase tracking-widest mb-1">{m.person_label_full_name()}</span>
|
||||
<span class="block text-lg font-serif text-brand-navy">{person.firstName} {person.lastName}</span>
|
||||
</div>
|
||||
|
||||
{#if person.alias}
|
||||
<div>
|
||||
<span class="block text-xs font-bold font-sans text-gray-400 uppercase tracking-widest mb-1">Rufname / Alias</span>
|
||||
<span class="block text-xs font-bold font-sans text-gray-400 uppercase tracking-widest mb-1">{m.form_label_alias()}</span>
|
||||
<span class="block text-lg font-serif text-brand-navy italic">"{person.alias}"</span>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -136,9 +137,9 @@
|
||||
{#key person.id}
|
||||
<div class="bg-white shadow-sm border border-brand-sand rounded-sm overflow-hidden mb-10">
|
||||
<div class="p-6 md:p-8">
|
||||
<h2 class="text-lg font-serif text-brand-navy mb-1">Person zusammenführen</h2>
|
||||
<h2 class="text-lg font-serif text-brand-navy mb-1">{m.person_merge_heading()}</h2>
|
||||
<p class="text-sm text-gray-500 font-sans mb-5">
|
||||
Diese Person wird in die gewählte Zielperson überführt. Alle Dokumente und Verknüpfungen werden übertragen, danach wird diese Person gelöscht.
|
||||
{m.person_merge_description()}
|
||||
</p>
|
||||
|
||||
{#if form?.mergeError}
|
||||
@@ -152,7 +153,7 @@
|
||||
<div class="flex-1">
|
||||
<PersonTypeahead
|
||||
name="_targetPersonDisplay"
|
||||
label="Zusammenführen mit"
|
||||
label={m.person_merge_target_label()}
|
||||
value={mergeTargetId}
|
||||
onchange={(value) => { mergeTargetId = value; showMergeConfirm = false; }}
|
||||
/>
|
||||
@@ -165,7 +166,7 @@
|
||||
onclick={() => (showMergeConfirm = true)}
|
||||
class="px-4 py-2 text-sm font-bold uppercase tracking-widest border border-red-300 text-red-600 rounded hover:bg-red-50 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
Zusammenführen
|
||||
{m.person_btn_merge()}
|
||||
</button>
|
||||
{:else}
|
||||
<div class="flex gap-2">
|
||||
@@ -173,14 +174,14 @@
|
||||
type="submit"
|
||||
class="px-4 py-2 text-sm font-bold uppercase tracking-widest bg-red-600 text-white rounded hover:bg-red-700 transition-colors"
|
||||
>
|
||||
Ja, zusammenführen
|
||||
{m.person_btn_merge_confirm()}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showMergeConfirm = false)}
|
||||
class="px-4 py-2 text-sm font-bold uppercase tracking-widest border border-gray-300 text-gray-500 rounded hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
{m.btn_cancel()}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -188,7 +189,7 @@
|
||||
|
||||
{#if showMergeConfirm}
|
||||
<p class="mt-3 text-sm text-red-700 bg-red-50 border border-red-200 rounded px-3 py-2">
|
||||
Achtung: Diese Aktion ist nicht rückgängig zu machen. <strong>{person.firstName} {person.lastName}</strong> wird gelöscht.
|
||||
{m.person_merge_warning()} <strong>{person.firstName} {person.lastName}</strong> wird gelöscht.
|
||||
</p>
|
||||
{/if}
|
||||
</form>
|
||||
@@ -199,7 +200,7 @@
|
||||
<!-- Linked Documents Section -->
|
||||
<div>
|
||||
<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">Gesendete Dokumente</h2>
|
||||
<h2 class="text-xl font-serif text-brand-navy">{m.person_docs_heading()}</h2>
|
||||
<span class="bg-brand-navy text-white text-xs font-bold px-2 py-1 rounded-full">
|
||||
{documents.length}
|
||||
</span>
|
||||
@@ -207,7 +208,7 @@
|
||||
|
||||
{#if documents.length === 0}
|
||||
<div class="p-12 text-center bg-white border border-brand-sand border-dashed rounded-sm">
|
||||
<p class="text-gray-500 font-sans">Diese Person ist noch nicht als Absender verknüpft.</p>
|
||||
<p class="text-gray-500 font-sans">{m.person_no_docs()}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="space-y-3">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
let { form } = $props();
|
||||
</script>
|
||||
|
||||
@@ -22,9 +23,9 @@ let { form } = $props();
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
Zurück zur Übersicht
|
||||
{m.btn_back_to_overview()}
|
||||
</a>
|
||||
<h1 class="font-serif text-3xl text-brand-navy">Neue Person</h1>
|
||||
<h1 class="font-serif text-3xl text-brand-navy">{m.persons_new_heading()}</h1>
|
||||
</div>
|
||||
|
||||
{#if form?.error}
|
||||
@@ -34,13 +35,13 @@ let { form } = $props();
|
||||
<form method="POST">
|
||||
<div class="rounded-sm border border-brand-sand bg-white p-6 shadow-sm">
|
||||
<h2 class="mb-5 text-xs font-bold tracking-widest text-gray-400 uppercase">
|
||||
Angaben zur Person
|
||||
{m.persons_section_details()}
|
||||
</h2>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="firstName" class="mb-1 block text-sm font-medium text-gray-700"
|
||||
>Vorname *</label
|
||||
>{m.form_label_first_name()} *</label
|
||||
>
|
||||
<input
|
||||
id="firstName"
|
||||
@@ -53,7 +54,7 @@ let { form } = $props();
|
||||
|
||||
<div>
|
||||
<label for="lastName" class="mb-1 block text-sm font-medium text-gray-700"
|
||||
>Nachname *</label
|
||||
>{m.form_label_last_name()} *</label
|
||||
>
|
||||
<input
|
||||
id="lastName"
|
||||
@@ -66,13 +67,13 @@ let { form } = $props();
|
||||
|
||||
<div class="md:col-span-2">
|
||||
<label for="alias" class="mb-1 block text-sm font-medium text-gray-700"
|
||||
>Rufname / Alias</label
|
||||
>{m.form_label_alias()}</label
|
||||
>
|
||||
<input
|
||||
id="alias"
|
||||
name="alias"
|
||||
type="text"
|
||||
placeholder="z.B. Oma Frieda, Onkel Karl…"
|
||||
placeholder={m.form_placeholder_alias()}
|
||||
class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm focus:border-brand-navy focus:ring-brand-navy"
|
||||
/>
|
||||
</div>
|
||||
@@ -87,13 +88,13 @@ let { form } = $props();
|
||||
href="/persons"
|
||||
class="text-sm font-medium text-gray-500 transition-colors hover:text-brand-navy"
|
||||
>
|
||||
Abbrechen
|
||||
{m.btn_cancel()}
|
||||
</a>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded bg-brand-navy px-6 py-2 text-sm font-bold tracking-widest text-white uppercase transition-colors hover:bg-brand-navy/80"
|
||||
>
|
||||
Erstellen
|
||||
{m.btn_create()}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user