diff --git a/frontend/src/lib/components/PersonMultiSelect.svelte b/frontend/src/lib/components/PersonMultiSelect.svelte index cffbf38f..c98ba5ab 100644 --- a/frontend/src/lib/components/PersonMultiSelect.svelte +++ b/frontend/src/lib/components/PersonMultiSelect.svelte @@ -1,15 +1,20 @@ - + {#each selectedPersons as person} @@ -69,7 +74,7 @@ {person.firstName} {person.lastName} removePerson(person.id)} + onclick={() => removePerson(person.id)} class="text-brand-navy/50 hover:text-red-500 focus:outline-none ml-0.5" aria-label="Entfernen" > @@ -85,8 +90,8 @@ type="text" autocomplete="off" bind:value={searchTerm} - on:input={handleInput} - on:focus={() => { updateDropdownPosition(); showDropdown = true; }} + oninput={handleInput} + onfocus={() => { updateDropdownPosition(); showDropdown = true; }} placeholder={selectedPersons.length === 0 ? 'Namen tippen...' : ''} class="flex-1 min-w-[120px] border-none p-1 focus:ring-0 text-sm bg-transparent outline-none" /> @@ -101,10 +106,10 @@ Suche... {:else} {#each results as person} - selectPerson(person)} + onclick={() => selectPerson(person)} + onkeydown={(e) => e.key === 'Enter' && selectPerson(person)} role="button" tabindex="0" > diff --git a/frontend/src/lib/components/PersonTypeahead.svelte b/frontend/src/lib/components/PersonTypeahead.svelte index 8ec884cb..b274993a 100644 --- a/frontend/src/lib/components/PersonTypeahead.svelte +++ b/frontend/src/lib/components/PersonTypeahead.svelte @@ -1,30 +1,33 @@ - + {label} @@ -96,8 +93,8 @@ id="{name}-search" autocomplete="off" bind:value={searchTerm} - on:input={handleInput} - on:focus={() => { updateDropdownPosition(); showDropdown = true; }} + oninput={handleInput} + onfocus={() => { updateDropdownPosition(); showDropdown = true; }} placeholder="Namen tippen..." class="mt-1 block w-full rounded-md border-gray-300 shadow-sm border p-2 focus:ring-blue-500 focus:border-blue-500" /> @@ -111,10 +108,10 @@ Suche... {:else} {#each results as person} - selectPerson(person)} + onclick={() => selectPerson(person)} + onkeydown={(e) => e.key === 'Enter' && selectPerson(person)} role="button" tabindex="0" > diff --git a/frontend/src/lib/components/TagInput.svelte b/frontend/src/lib/components/TagInput.svelte index 63350213..b5df9b7b 100644 --- a/frontend/src/lib/components/TagInput.svelte +++ b/frontend/src/lib/components/TagInput.svelte @@ -1,13 +1,16 @@ - - - - - {#each tags as tag, i} - - {tag} - removeTag(i)} - aria-label="Schlagwort entfernen" - class="text-brand-navy/50 hover:text-red-500 focus:outline-none" - > - - - - {/each} + + + + + {#each tags as tag, i} + + {tag} + removeTag(i)} + aria-label="Schlagwort entfernen" + class="text-brand-navy/50 hover:text-red-500 focus:outline-none" + > + + + + {/each} - - - handleInput()} - on:blur={() => setTimeout(() => (showSuggestions = false), 200)} - placeholder={tags.length === 0 - ? allowCreation - ? 'Schlagworte hinzufügen...' - : 'Nach Schlagworten filtern...' - : ''} - class="w-full h-full border-none p-1 focus:ring-0 text-sm bg-transparent outline-none" - /> + + + fetchSuggestions(inputVal)} + onkeydown={handleKeydown} + onfocus={() => fetchSuggestions(inputVal)} + placeholder={tags.length === 0 + ? allowCreation + ? 'Schlagworte hinzufügen...' + : 'Nach Schlagworten filtern...' + : ''} + class="w-full h-full border-none p-1 focus:ring-0 text-sm bg-transparent outline-none" + /> - - {#if showSuggestions && suggestions.length > 0} - - {#each suggestions as suggestion, i} - addTag(suggestion)} - on:keydown={(e) => e.key === 'Enter' && addTag(suggestion)} - > - {suggestion} - - {/each} - - {/if} - - - {#if allowCreation} - Enter drücken um Schlagwort zu erstellen. - {/if} + + {#if showSuggestions && suggestions.length > 0} + + {#each suggestions as suggestion, i} + addTag(suggestion)} + onkeydown={(e) => e.key === 'Enter' && addTag(suggestion)} + > + {suggestion} + + {/each} + + {/if} + + + {#if allowCreation} + Enter drücken um Schlagwort zu erstellen. + {/if} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 50dc8338..d019a23d 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -2,116 +2,112 @@ import './layout.css'; import { enhance } from '$app/forms'; import { page } from '$app/state'; - $: user = page.data.user; - $: isAdmin = user?.groups.some(g => g.permissions.includes("ADMIN")) + + let { children } = $props(); + + const isAdmin = $derived(page.data.user?.groups.some((g: { permissions: string[] }) => g.permissions.includes('ADMIN'))); - - - {#if !page.url.pathname.startsWith('/login')} - - - - + {#if !page.url.pathname.startsWith('/login')} + + + - - - - - - - - - + + + + + + + - FAMILIENARCHIV - - - + FAMILIENARCHIV + + + - - - - Dokumente - + + + Dokumente + - - Personen - + + Personen + - - Konversationen - - {#if isAdmin} - - Admin - - {/if} - - + + Konversationen + + {#if isAdmin} + + Admin + + {/if} + + - - - - - Abmelden - - - - - - - {/if} + + + + + Abmelden + + + + + + + {/if} - - - + + {@render children()} + diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index ceae8171..f1ea2f67 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -3,21 +3,19 @@ import PersonTypeahead from '$lib/components/PersonTypeahead.svelte'; import { goto } from '$app/navigation'; import TagInput from '$lib/components/TagInput.svelte'; import { slide } from 'svelte/transition'; +import { untrack } from 'svelte'; -export let data; +let { data } = $props(); -// Local state variables -let q = data.filters?.q || ''; -let from = data.filters?.from || ''; -let to = data.filters?.to || ''; -let senderId = data.filters?.senderId || ''; -let receiverId = data.filters?.receiverId || ''; -let tagNames = data.filters?.tags || []; +let q = $state(untrack(() => data.filters?.q || '')); +let from = $state(untrack(() => data.filters?.from || '')); +let to = $state(untrack(() => data.filters?.to || '')); +let senderId = $state(untrack(() => data.filters?.senderId || '')); +let receiverId = $state(untrack(() => data.filters?.receiverId || '')); +let tagNames = $state(untrack(() => data.filters?.tags || [])); -// Debounce Timer -let searchTimer: any; - -let showAdvanced = false; +let searchTimer: ReturnType; +let showAdvanced = $state(false); function triggerSearch() { const params = new URLSearchParams(); @@ -42,27 +40,24 @@ function handleTextSearch() { }, 500); } -let previousTags = tagNames.join(','); -$: { - const currentTags = tagNames.join(','); - if (currentTags !== previousTags) { - previousTags = currentTags; +// Trigger search when tags change +let prevTagStr = untrack(() => tagNames.join(',')); +$effect(() => { + const cur = tagNames.join(','); + if (cur !== prevTagStr) { + prevTagStr = cur; triggerSearch(); } -} +}); -function toggleAdvanced() { - showAdvanced = !showAdvanced; -} - -// Sync with server data (e.g. after reset) -$: { +// Sync local state with server data after navigation +$effect(() => { q = data.filters?.q || ''; from = data.filters?.from || ''; to = data.filters?.to || ''; senderId = data.filters?.senderId || ''; receiverId = data.filters?.receiverId || ''; -} +}); @@ -76,7 +71,7 @@ $: { @@ -94,7 +89,7 @@ $: { (showAdvanced = !showAdvanced)} 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" > Schlagworte - + @@ -156,7 +151,7 @@ $: { label="Absender" bind:value={senderId} initialName={data.initialValues?.senderName} - on:change={triggerSearch} + onchange={triggerSearch} /> @@ -171,7 +166,7 @@ $: { label="Empfänger" bind:value={receiverId} initialName={data.initialValues?.receiverName} - on:change={triggerSearch} + onchange={triggerSearch} /> @@ -188,7 +183,7 @@ $: { type="date" id="from" bind:value={from} - on:change={triggerSearch} + onchange={triggerSearch} class="block w-full border-gray-300 py-2.5 text-sm shadow-sm" /> @@ -202,7 +197,7 @@ $: { type="date" id="to" bind:value={to} - on:change={triggerSearch} + onchange={triggerSearch} class="block w-full border-gray-300 py-2.5 text-sm shadow-sm" /> @@ -329,15 +324,14 @@ $: { - + {#if doc.tags && doc.tags.length > 0} {#each doc.tags as tag} - goto(`/?tag=${encodeURIComponent(tag.name)}`)} + onclick={(e) => { e.preventDefault(); e.stopPropagation(); goto(`/?tag=${encodeURIComponent(tag.name)}`); }} > {tag.name} @@ -384,7 +378,7 @@ $: { Versuchen Sie, die Filter anzupassen oder den Suchbegriff zu ändern. goto('/')} + onclick={() => goto('/')} class="mt-6 text-sm font-bold tracking-wide text-brand-mint uppercase transition hover:text-brand-navy" > Alle Filter löschen diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index 1405421d..b92c3f74 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -2,15 +2,17 @@ import { enhance } from '$app/forms'; import { slide } from 'svelte/transition'; - export let data; - export let form; + let { data, form } = $props(); - let activeTab = 'users'; - let editingTagId: string | null = null; - let editingTagName = ''; - let editingUserId: string | null = null; + let activeTab = $state('users'); + let editingTagId: string | null = $state(null); + let editingTagName = $state(''); + let editingUserId: string | null = $state(null); + let editingGroupId: string | null = $state(null); - function startEditTag(tag: any) { + const availablePermissions = ['WRITE_ALL', 'ADMIN', 'ADMIN_USER', 'ADMIN_TAG', 'ADMIN_PERMISSION']; + + function startEditTag(tag: { id: string; name: string }) { editingTagId = tag.id; editingTagName = tag.name; } @@ -20,7 +22,7 @@ editingTagName = ''; } - function startEditUser(id: string) { + function startEditUser(id: string) { editingUserId = id; } @@ -28,9 +30,6 @@ editingUserId = null; } - let editingGroupId: string | null = null; - const availablePermissions = ['WRITE_ALL', 'ADMIN', 'ADMIN_USER', 'ADMIN_TAG', 'ADMIN_PERMISSION']; - function startEditGroup(id: string) { editingGroupId = id; } @@ -51,21 +50,21 @@ 'users' ? 'bg-brand-navy text-white' : 'text-gray-500 hover:text-brand-navy'}" - on:click={() => (activeTab = 'users')}>Benutzer (activeTab = 'users')}>Benutzer (activeTab = 'groups')}>Gruppen (activeTab = 'groups')}>Gruppen (activeTab = 'tags')}>Schlagworte (activeTab = 'tags')}>Schlagworte @@ -106,7 +105,6 @@ {user.username} - - g.id === group.id)} + selected={user.groups.some((g: { id: string }) => g.id === group.id)} > {group.name} @@ -136,7 +133,6 @@ - Abbrechen @@ -194,15 +190,13 @@ - startEditUser(user.id)} + onclick={() => startEditUser(user.id)} class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide" > Bearbeiten - - {:else if activeTab === 'tags'} - Schlagworte @@ -332,9 +324,9 @@ > startEditTag(tag)} + onclick={() => startEditTag(tag)} aria-label="Schlagwort bearbeiten" class="p-1 text-gray-400 hover:text-brand-navy" > @@ -370,16 +362,13 @@ method="POST" action="?/deleteTag" use:enhance={({ cancel }) => { - // This runs BEFORE the request is sent if ( !confirm( 'Wirklich löschen? Das Schlagwort wird aus allen Dokumenten entfernt.' ) ) { - cancel(); // Stop the request + cancel(); } - - // This runs AFTER the server responds return async ({ update }) => { await update(); }; @@ -443,7 +432,6 @@ > - - {#each availablePermissions as perm} - startEditGroup(group.id)} + onclick={() => startEditGroup(group.id)} class="text-brand-mint hover:text-brand-navy text-sm font-bold uppercase tracking-wide" > Bearbeiten @@ -537,7 +523,6 @@ if (!confirm('Gruppe wirklich löschen?')) { cancel(); } - return async ({ update }) => { await update(); }; diff --git a/frontend/src/routes/conversations/+page.svelte b/frontend/src/routes/conversations/+page.svelte index 3b9051a2..1456a7e9 100644 --- a/frontend/src/routes/conversations/+page.svelte +++ b/frontend/src/routes/conversations/+page.svelte @@ -1,58 +1,39 @@ @@ -74,9 +55,9 @@ applyFilters()} /> @@ -87,9 +68,9 @@ applyFilters()} /> @@ -106,7 +87,7 @@ id="dateFrom" type="date" bind:value={fromDate} - on:change={() => applyFilters()} + onchange={() => applyFilters()} class="block w-full border-gray-300 shadow-sm focus:border-brand-navy focus:ring-brand-navy text-sm py-2.5" /> @@ -122,7 +103,7 @@ id="dateTo" type="date" bind:value={toDate} - on:change={() => applyFilters()} + onchange={() => applyFilters()} class="block w-full border-gray-300 shadow-sm focus:border-brand-navy focus:ring-brand-navy text-sm py-2.5" /> @@ -130,7 +111,7 @@ Sortierung: @@ -169,7 +150,7 @@ Wählen Sie zwei Personen aus Die Korrespondenz wird hier angezeigt. - {:else if documents.length === 0} + {:else if data.documents.length === 0} @@ -178,18 +159,15 @@ {:else} - - - - {#each documents as doc} + {#each data.documents as doc} {@const isRight = doc.sender?.id === senderId} @@ -200,7 +178,7 @@ ? 'flex-row-reverse' : 'flex-row'}" > - + - export let data; - $: doc = data.document; + let { data } = $props(); - // Instead of a direct link, we use a reactive variable for the Blob URL - let fileUrl = ''; - let isLoading = false; - let error = ''; + const doc = $derived(data.document); - // Reactive statement: Whenever the document ID changes, load the file - $: if (doc?.id && doc?.filePath) { - loadFile(doc.id); - } + let fileUrl = $state(''); + let isLoading = $state(false); + let error = $state(''); + + $effect(() => { + if (doc?.id && doc?.filePath) { + loadFile(doc.id); + } + }); async function loadFile(id: string) { isLoading = true; error = ''; - fileUrl = ''; // Reset previous URL + fileUrl = ''; try { - // 1. Fetch with current authentication const response = await fetch(`/api/documents/${id}/file`); if (!response.ok) { @@ -26,10 +26,7 @@ throw new Error('Fehler beim Laden der Datei'); } - // 2. Create a Blob from the data const blob = await response.blob(); - - // 3. Create a temporary URL for this Blob fileUrl = URL.createObjectURL(blob); } catch (e) { @@ -186,7 +183,6 @@ {#if doc.documentLocation} - 0} - - + {#if doc.summary || doc.transcription} - {#if doc.summary} - Zusammenfassung + Zusammenfassung @@ -348,12 +340,9 @@ {/if} - {#if doc.transcription} - Transkription + Transkription @@ -376,7 +365,6 @@ {#if isLoading} - {error} {#if doc.filePath} - {:else if !doc.filePath} - - Kein Scan vorhanden {:else if fileUrl && doc.originalFilename.toLowerCase().endsWith('.pdf')} - {:else if fileUrl} - t.name) : []; - let senderId = doc.sender?.id ?? ''; - let selectedReceivers = doc.receivers ?? []; + let { document: doc } = untrack(() => data); + let tags = $state(doc.tags ? doc.tags.map((t: { name: string }) => t.name) : []); + let senderId = $state(doc.sender?.id ?? ''); + let selectedReceivers = $state(doc.receivers ?? []); function isoToGerman(iso: string): string { if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ''; @@ -25,9 +25,11 @@ return `${y}-${m}-${d}`; } - let dateDisplay = isoToGerman(doc.documentDate ?? ''); - let dateIso = doc.documentDate ?? ''; - let dateDirty = false; + let dateDisplay = $state(isoToGerman(doc.documentDate ?? '')); + let dateIso = $state(doc.documentDate ?? ''); + let dateDirty = $state(false); + + const dateInvalid = $derived(dateDirty && dateDisplay.length > 0 && dateIso === ''); function handleDateInput(e: Event) { const input = e.target as HTMLInputElement; @@ -45,8 +47,6 @@ dateIso = germanToIso(formatted); dateDirty = true; } - - $: dateInvalid = dateDirty && dateDisplay.length > 0 && dateIso === ''; @@ -84,7 +84,7 @@ type="text" inputmode="numeric" value={dateDisplay} - on:input={handleDateInput} + oninput={handleDateInput} placeholder="TT.MM.JJJJ" maxlength="10" class="block w-full rounded border-gray-300 shadow-sm p-2 border text-sm diff --git a/frontend/src/routes/documents/new/+page.svelte b/frontend/src/routes/documents/new/+page.svelte index 091a3586..484b28e1 100644 --- a/frontend/src/routes/documents/new/+page.svelte +++ b/frontend/src/routes/documents/new/+page.svelte @@ -4,15 +4,17 @@ import TagInput from '$lib/components/TagInput.svelte'; import PersonTypeahead from '$lib/components/PersonTypeahead.svelte'; import PersonMultiSelect from '$lib/components/PersonMultiSelect.svelte'; -export let form; +let { form } = $props(); -let tags: string[] = []; -let senderId = ''; -let selectedReceivers: { id: string; firstName: string; lastName: string }[] = []; +let tags: string[] = $state([]); +let senderId = $state(''); +let selectedReceivers: { id: string; firstName: string; lastName: string }[] = $state([]); -let dateDisplay = ''; -let dateIso = ''; -let dateDirty = false; +let dateDisplay = $state(''); +let dateIso = $state(''); +let dateDirty = $state(false); + +const dateInvalid = $derived(dateDirty && dateDisplay.length > 0 && dateIso === ''); function germanToIso(german: string): string { const match = german.match(/^(\d{2})\.(\d{2})\.(\d{4})$/); @@ -37,8 +39,6 @@ function handleDateInput(e: Event) { dateIso = germanToIso(formatted); dateDirty = true; } - -$: dateInvalid = dateDirty && dateDisplay.length > 0 && dateIso === ''; @@ -86,7 +86,7 @@ $: dateInvalid = dateDirty && dateDisplay.length > 0 && dateIso === ''; type="text" inputmode="numeric" value={dateDisplay} - on:input={handleDateInput} + oninput={handleDateInput} placeholder="TT.MM.JJJJ" maxlength="10" class="block w-full rounded border border-gray-300 p-2 text-sm shadow-sm diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index cfd80b34..26d111f1 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -1,6 +1,5 @@ @@ -10,13 +9,13 @@ Benutzername - Passwort - @@ -24,7 +23,7 @@ {form.error} {/if} - Anmelden diff --git a/frontend/src/routes/persons/+page.svelte b/frontend/src/routes/persons/+page.svelte index f1ac7f88..e44a8a02 100644 --- a/frontend/src/routes/persons/+page.svelte +++ b/frontend/src/routes/persons/+page.svelte @@ -1,10 +1,10 @@ @@ -83,7 +87,7 @@ Speichern - + (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 @@ -103,7 +107,7 @@ {person.firstName} {person.lastName} - + (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"> Bearbeiten @@ -150,7 +154,7 @@ name="_targetPersonDisplay" label="Zusammenführen mit" value={mergeTargetId} - on:change={(e) => { mergeTargetId = e.detail.value; showMergeConfirm = false; }} + onchange={(value) => { mergeTargetId = value; showMergeConfirm = false; }} /> @@ -158,7 +162,7 @@ showMergeConfirm = true} + 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 @@ -173,7 +177,7 @@ showMergeConfirm = false} + 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 diff --git a/frontend/src/routes/persons/new/+page.svelte b/frontend/src/routes/persons/new/+page.svelte index 245ac1f5..e1e36618 100644 --- a/frontend/src/routes/persons/new/+page.svelte +++ b/frontend/src/routes/persons/new/+page.svelte @@ -1,5 +1,5 @@
Enter drücken um Schlagwort zu erstellen.
Wählen Sie zwei Personen aus
Die Korrespondenz wird hier angezeigt.
{error}