diff --git a/frontend/src/lib/components/AnnotationLayer.svelte b/frontend/src/lib/components/AnnotationLayer.svelte index f2e3a993..34ca26d2 100644 --- a/frontend/src/lib/components/AnnotationLayer.svelte +++ b/frontend/src/lib/components/AnnotationLayer.svelte @@ -1,15 +1,5 @@ + +{#snippet commentEntry(comment: Comment | CommentReply, threadId: string, showReplyButton: boolean)} + {#if editingId === comment.id} +
+ +
+ + +
+
+ {:else} +
+
+
+ {comment.authorName} + {timeAgo(comment.createdAt)} + {#if wasEdited(comment)} + + {m.comment_edited_label()} + {timeAgo(comment.updatedAt)} + + {/if} +
+

{comment.content}

+
+ {#if canModify(comment)} +
+ + +
+ {/if} +
+ {#if showReplyButton && canComment} +
+ +
+ {/if} + {/if} +{/snippet} +
{#each comments as thread, ti (thread.id)}
0 ? 'border-t border-line pt-4' : ''}>
- {#if editingId === thread.id} -
- -
- - -
-
- {:else} -
-
-
- {thread.authorName} - {timeAgo(thread.createdAt)} - {#if wasEdited(thread)} - - {m.comment_edited_label()} - {timeAgo(thread.updatedAt)} - - {/if} -
-

{thread.content}

-
- {#if canModify(thread)} -
- - -
- {/if} -
- - {#if thread.replies.length === 0 && canComment} -
- -
- {/if} - {/if} + {@render commentEntry(thread, thread.id, thread.replies.length === 0)}
{#each thread.replies as reply, ri (reply.id)}
- {#if editingId === reply.id} -
- -
- - -
-
- {:else} -
-
-
- {reply.authorName} - {timeAgo(reply.createdAt)} - {#if wasEdited(reply)} - - {m.comment_edited_label()} - {timeAgo(reply.updatedAt)} - - {/if} -
-

{reply.content}

-
- {#if canModify(reply)} -
- - -
- {/if} -
- - {#if ri === thread.replies.length - 1 && canComment} -
- -
- {/if} - {/if} + {@render commentEntry(reply, thread.id, ri === thread.replies.length - 1)}
{/each} - + {#if replyingTo === thread.id}
+
+
+
diff --git a/frontend/src/lib/components/document/TranscriptionSection.svelte b/frontend/src/lib/components/document/TranscriptionSection.svelte new file mode 100644 index 00000000..142af5ae --- /dev/null +++ b/frontend/src/lib/components/document/TranscriptionSection.svelte @@ -0,0 +1,19 @@ + + +
+

+ {m.form_label_transcription()} +

+ +
diff --git a/frontend/src/lib/components/document/WhoWhenSection.svelte b/frontend/src/lib/components/document/WhoWhenSection.svelte new file mode 100644 index 00000000..71b2feaf --- /dev/null +++ b/frontend/src/lib/components/document/WhoWhenSection.svelte @@ -0,0 +1,102 @@ + + +
+

+ {m.doc_section_who_when()} +

+ +
+ +
+ + + + {#if dateInvalid} +

{m.form_date_error()}

+ {/if} +
+ + +
+ + +
+ + +
+ +
+ + +
+

{m.form_label_receivers()}

+ +
+
+
diff --git a/frontend/src/lib/components/user/UserGroupsSection.svelte b/frontend/src/lib/components/user/UserGroupsSection.svelte new file mode 100644 index 00000000..911cde23 --- /dev/null +++ b/frontend/src/lib/components/user/UserGroupsSection.svelte @@ -0,0 +1,24 @@ + + +
+ {#each groups as group (group.id)} + + {/each} +
diff --git a/frontend/src/lib/components/user/UserPasswordSection.svelte b/frontend/src/lib/components/user/UserPasswordSection.svelte new file mode 100644 index 00000000..cf25b239 --- /dev/null +++ b/frontend/src/lib/components/user/UserPasswordSection.svelte @@ -0,0 +1,31 @@ + + +
+ + + +
diff --git a/frontend/src/lib/components/user/UserProfileSection.svelte b/frontend/src/lib/components/user/UserProfileSection.svelte new file mode 100644 index 00000000..b3b8d214 --- /dev/null +++ b/frontend/src/lib/components/user/UserProfileSection.svelte @@ -0,0 +1,95 @@ + + +
+
+ + + +
+ + + + + + +
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts new file mode 100644 index 00000000..28e9da0b --- /dev/null +++ b/frontend/src/lib/types.ts @@ -0,0 +1,33 @@ +export type CommentReply = { + id: string; + authorId: string | null; + authorName: string; + content: string; + createdAt: string; + updatedAt: string; +}; + +export type Comment = { + id: string; + authorId: string | null; + authorName: string; + content: string; + createdAt: string; + updatedAt: string; + replies: CommentReply[]; +}; + +export type DocumentPanelTab = 'metadata' | 'transcription' | 'discussion' | 'history'; + +export type Annotation = { + id: string; + documentId: string; + pageNumber: number; + x: number; + y: number; + width: number; + height: number; + color: string; + createdAt: string; + fileHash?: string | null; +}; diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 8b4fe98d..51ca79f9 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,20 +1 @@ -/** - * Converts an ISO date string (YYYY-MM-DD) to German display format (DD.MM.YYYY). - * Returns an empty string for invalid or empty input. - */ -export function isoToGerman(iso: string): string { - if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ''; - const [y, m, d] = iso.split('-'); - return `${d}.${m}.${y}`; -} - -/** - * Converts a German date string (DD.MM.YYYY) to ISO format (YYYY-MM-DD). - * Returns an empty string for invalid or empty input. - */ -export function germanToIso(german: string): string { - const match = german.match(/^(\d{2})\.(\d{2})\.(\d{4})$/); - if (!match) return ''; - const [, d, m, y] = match; - return `${y}-${m}-${d}`; -} +export { isoToGerman, germanToIso } from '$lib/utils/date'; diff --git a/frontend/src/lib/utils/date.ts b/frontend/src/lib/utils/date.ts index 9f8b5c54..c2ab4e8e 100644 --- a/frontend/src/lib/utils/date.ts +++ b/frontend/src/lib/utils/date.ts @@ -9,3 +9,44 @@ export function formatDate(isoDate: string): string { year: 'numeric' }).format(new Date(isoDate + 'T12:00:00')); } + +/** + * Converts an ISO date string (YYYY-MM-DD) to German display format (DD.MM.YYYY). + * Returns an empty string for invalid or empty input. + */ +export function isoToGerman(iso: string): string { + if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ''; + const [y, m, d] = iso.split('-'); + return `${d}.${m}.${y}`; +} + +/** + * Converts a German date string (DD.MM.YYYY) to ISO format (YYYY-MM-DD). + * Returns an empty string for invalid or empty input. + */ +export function germanToIso(german: string): string { + const match = german.match(/^(\d{2})\.(\d{2})\.(\d{4})$/); + if (!match) return ''; + const [, d, m, y] = match; + return `${y}-${m}-${d}`; +} + +/** + * Handles a date input event for German-format date fields (DD.MM.YYYY). + * Strips non-digits, formats with dots, mutates the input's displayed value, + * and returns the display string and its ISO equivalent. + */ +export function handleGermanDateInput(e: Event): { display: string; iso: string } { + const input = e.target as HTMLInputElement; + const digits = input.value.replace(/\D/g, '').slice(0, 8); + let display: string; + if (digits.length <= 2) { + display = digits; + } else if (digits.length <= 4) { + display = `${digits.slice(0, 2)}.${digits.slice(2)}`; + } else { + display = `${digits.slice(0, 2)}.${digits.slice(2, 4)}.${digits.slice(4)}`; + } + input.value = display; + return { display, iso: germanToIso(display) }; +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 11f31185..32d85fe3 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,11 +1,11 @@
@@ -59,58 +45,7 @@ function clickOutside(node: HTMLElement) {
- +
@@ -134,64 +69,7 @@ function clickOutside(node: HTMLElement) { -
{ if (e.key === 'Escape') userMenuOpen = false; }} - role="none" - > - {#if userInitials} - - {:else} - - {/if} - - {#if userMenuOpen} - - {/if} -
+
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 71a5c93b..e5130ff1 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,13 +1,10 @@ -
- -
- -
- -
- (qFocused = true)} - onblur={() => (qFocused = false)} - placeholder={m.docs_search_placeholder()} - class="block w-full border-line py-2.5 pr-10 pl-3 placeholder-ink-3 shadow-sm focus:border-ink focus:ring-ink" - /> -
- -
-
- - - - - - - - -
- - - {#if showAdvanced} -
- -
-

- {m.docs_filter_label_tags()} -

- -
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
-
- {/if} -
+ (qFocused = true)} + onblur={() => (qFocused = false)} + /> {#if data.canWrite} - -
fileInput.click()} - onkeydown={(e) => e.key === 'Enter' && fileInput.click()} - > - - {#if isUploading} -
-
-
-
- {uploadProgress}% -
- {:else} - {m.upload_drop_hint()} - {m.upload_accepted_types()} - {/if} -
- - {#if uploadMessages.length > 0} -
- {#each uploadMessages as msg, i (i)} - - {/each} -
- {/if} + {/if} - -
- {#if data.canWrite} - - - {m.docs_btn_new()} - - {/if} -
- - - - +
diff --git a/frontend/src/routes/AppNav.svelte b/frontend/src/routes/AppNav.svelte new file mode 100644 index 00000000..49e6888e --- /dev/null +++ b/frontend/src/routes/AppNav.svelte @@ -0,0 +1,59 @@ + + +
+ + + +
diff --git a/frontend/src/routes/DocumentList.svelte b/frontend/src/routes/DocumentList.svelte new file mode 100644 index 00000000..9ac8537a --- /dev/null +++ b/frontend/src/routes/DocumentList.svelte @@ -0,0 +1,177 @@ + + + +
+ {#if canWrite} + + + {m.docs_btn_new()} + + {/if} +
+ + +
+ {#if error} +
+ {error} +
+ {:else if documents.length > 0} + + {:else} + +
+
+ +
+

{m.docs_empty_heading()}

+

+ {m.docs_empty_text()} +

+ +
+ {/if} +
diff --git a/frontend/src/routes/DropZone.svelte b/frontend/src/routes/DropZone.svelte new file mode 100644 index 00000000..d845d027 --- /dev/null +++ b/frontend/src/routes/DropZone.svelte @@ -0,0 +1,213 @@ + + +
fileInput.click()} + onkeydown={(e) => e.key === 'Enter' && fileInput.click()} +> + + {#if isUploading} +
+
+
+
+ {uploadProgress}% +
+ {:else} + {m.upload_drop_hint()} + {m.upload_accepted_types()} + {/if} +
+ +{#if uploadMessages.length > 0} +
+ {#each uploadMessages as msg, i (i)} + + {/each} +
+{/if} + + diff --git a/frontend/src/routes/SearchFilterBar.svelte b/frontend/src/routes/SearchFilterBar.svelte new file mode 100644 index 00000000..7b471b89 --- /dev/null +++ b/frontend/src/routes/SearchFilterBar.svelte @@ -0,0 +1,164 @@ + + +
+ +
+ +
+ +
+ +
+
+ + + + + + + + +
+ + + {#if showAdvanced} +
+ +
+

+ {m.docs_filter_label_tags()} +

+ +
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ {/if} +
diff --git a/frontend/src/routes/UserMenu.svelte b/frontend/src/routes/UserMenu.svelte new file mode 100644 index 00000000..cb954c84 --- /dev/null +++ b/frontend/src/routes/UserMenu.svelte @@ -0,0 +1,81 @@ + + +
{ + if (e.key === 'Escape') userMenuOpen = false; + }} + role="none" +> + {#if userInitials} + + {:else} + + {/if} + + {#if userMenuOpen} + + {/if} +
diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index a40ca891..75d458f2 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -1,66 +1,14 @@
@@ -107,466 +55,20 @@ async function backfillFileHashes() { {/if} {#if activeTab === 'users'} -
-
-

{m.admin_section_users()}

- - - - - {m.admin_btn_new_user()} - -
- - - - - - - - - - - - {#each data.users as user (user.id)} - - - - - - - {/each} - -
{m.admin_col_login()}{m.admin_col_full_name()}{m.admin_col_groups()}{m.admin_col_actions()}
- {user.username} - - {#if user.firstName || user.lastName} - {user.firstName ?? ''} {user.lastName ?? ''} - {:else} - - {/if} - -
- {#if user.groups && user.groups.length > 0} - {#each user.groups as group (group.id)} - - {group.name} - - {/each} - {:else} - {m.admin_no_groups()} - {/if} -
-
-
- - {m.btn_edit()} - - -
{ - if (!confirm(m.admin_user_delete_confirm({ username: user.username }))) { - cancel(); - } - return async ({ update }) => { - await update(); - }; - }} - class="flex items-center" - > - - -
-
-
+
+
{:else if activeTab === 'tags'} -
-
-

{m.admin_section_tags()}

-

- {m.admin_tags_warning()} -

-
- -
    - {#each data.tags as tag (tag.id)} -
  • - {#if editingTagId === tag.id} -
    - async ({ update }) => { - await update(); - cancelEditTag(); - }} - class="flex flex-1 items-center gap-2" - > - - - - -
    - {:else} - - {tag.name} - -
    - -
    { - if ( - !confirm(m.admin_tag_delete_confirm()) - ) { - cancel(); - } - return async ({ update }) => { - await update(); - }; - }} - class="inline" - > - - -
    -
    - {/if} -
  • - {/each} -
+
+
{:else if activeTab === 'groups'} -
-
-

{m.admin_section_groups()}

-
- - - - - - - - - - - {#each data.groups as group (group.id)} - - {#if editingGroupId === group.id} - - - {:else} - - - - - {/if} - - {/each} - -
{m.admin_col_name()}{m.admin_col_permissions()}{m.admin_col_actions()}
-
- async ({ update }) => { - await update(); - cancelEditGroup(); - }} - class="flex w-full flex-col items-start gap-4 sm:flex-row" - > - - -
- -
- -
- {#each availablePermissions as perm (perm)} - - {/each} -
- -
- - -
-
-
- {group.name} - -
- {#each group.permissions as perm (perm)} - - {perm} - - {/each} -
-
-
- - -
{ - if (!confirm(m.admin_group_delete_confirm())) { - cancel(); - } - return async ({ update }) => { - await update(); - }; - }} - > - - -
-
-
- - -
-

- {m.admin_section_new_group()} -

-
-
- -
- -
- {#each availablePermissions as perm (perm)} - - {/each} -
- - -
-
+
+
{:else if activeTab === 'system'} -
-

{m.admin_system_backfill_heading()}

-

{m.admin_system_backfill_description()}

- - {#if backfillResult !== null} -

- {m.admin_system_backfill_success({ count: backfillResult })} -

- {/if} -
- -
-

- {m.admin_system_backfill_hashes_heading()} -

-

{m.admin_system_backfill_hashes_description()}

- - {#if backfillHashesResult !== null} -

- {m.admin_system_backfill_hashes_success({ count: backfillHashesResult })} -

- {/if} +
+
{/if}
diff --git a/frontend/src/routes/admin/GroupsTab.svelte b/frontend/src/routes/admin/GroupsTab.svelte new file mode 100644 index 00000000..0f96cc22 --- /dev/null +++ b/frontend/src/routes/admin/GroupsTab.svelte @@ -0,0 +1,221 @@ + + +
+
+

{m.admin_section_groups()}

+
+ + + + + + + + + + + {#each groups as group (group.id)} + + {#if editingGroupId === group.id} + + + {:else} + + + + + {/if} + + {/each} + +
{m.admin_col_name()}{m.admin_col_permissions()}{m.admin_col_actions()}
+
+ async ({ update }) => { + await update(); + cancelEditGroup(); + }} + class="flex w-full flex-col items-start gap-4 sm:flex-row" + > + + +
+ +
+ +
+ {#each availablePermissions as perm (perm)} + + {/each} +
+ +
+ + +
+
+
+ {group.name} + +
+ {#each group.permissions as perm (perm)} + + {perm} + + {/each} +
+
+
+ + +
{ + if (!confirm(m.admin_group_delete_confirm())) { + cancel(); + } + return async ({ update }) => { + await update(); + }; + }} + > + + +
+
+
+ + +
+

+ {m.admin_section_new_group()} +

+
+
+ +
+ +
+ {#each availablePermissions as perm (perm)} + + {/each} +
+ + +
+
+
diff --git a/frontend/src/routes/admin/SystemTab.svelte b/frontend/src/routes/admin/SystemTab.svelte new file mode 100644 index 00000000..bd80e61c --- /dev/null +++ b/frontend/src/routes/admin/SystemTab.svelte @@ -0,0 +1,72 @@ + + +
+

{m.admin_system_backfill_heading()}

+

{m.admin_system_backfill_description()}

+ + {#if backfillResult !== null} +

+ {m.admin_system_backfill_success({ count: backfillResult })} +

+ {/if} +
+ +
+

+ {m.admin_system_backfill_hashes_heading()} +

+

{m.admin_system_backfill_hashes_description()}

+ + {#if backfillHashesResult !== null} +

+ {m.admin_system_backfill_hashes_success({ count: backfillHashesResult })} +

+ {/if} +
diff --git a/frontend/src/routes/admin/TagsTab.svelte b/frontend/src/routes/admin/TagsTab.svelte new file mode 100644 index 00000000..01a1fec0 --- /dev/null +++ b/frontend/src/routes/admin/TagsTab.svelte @@ -0,0 +1,127 @@ + + +
+
+

{m.admin_section_tags()}

+

+ {m.admin_tags_warning()} +

+
+ +
    + {#each tags as tag (tag.id)} +
  • + {#if editingTagId === tag.id} +
    + async ({ update }) => { + await update(); + cancelEditTag(); + }} + class="flex flex-1 items-center gap-2" + > + + + + +
    + {:else} + + {tag.name} + +
    + +
    { + if (!confirm(m.admin_tag_delete_confirm())) { + cancel(); + } + return async ({ update }) => { + await update(); + }; + }} + class="inline" + > + + +
    +
    + {/if} +
  • + {/each} +
+
diff --git a/frontend/src/routes/admin/UsersTab.svelte b/frontend/src/routes/admin/UsersTab.svelte new file mode 100644 index 00000000..de8dd02a --- /dev/null +++ b/frontend/src/routes/admin/UsersTab.svelte @@ -0,0 +1,120 @@ + + +
+
+

{m.admin_section_users()}

+ + + + + {m.admin_btn_new_user()} + +
+ + + + + + + + + + + + {#each users as user (user.id)} + + + + + + + {/each} + +
{m.admin_col_login()}{m.admin_col_full_name()}{m.admin_col_groups()}{m.admin_col_actions()}
+ {user.username} + + {#if user.firstName || user.lastName} + {user.firstName ?? ''} {user.lastName ?? ''} + {:else} + + {/if} + +
+ {#if user.groups && user.groups.length > 0} + {#each user.groups as group (group.id)} + + {group.name} + + {/each} + {:else} + {m.admin_no_groups()} + {/if} +
+
+
+ + {m.btn_edit()} + + +
{ + if (!confirm(m.admin_user_delete_confirm({ username: user.username }))) { + cancel(); + } + return async ({ update }) => { + await update(); + }; + }} + class="flex items-center" + > + + +
+
+
+
diff --git a/frontend/src/routes/admin/users/[id]/+page.svelte b/frontend/src/routes/admin/users/[id]/+page.svelte index 85369c6d..2159d6ed 100644 --- a/frontend/src/routes/admin/users/[id]/+page.svelte +++ b/frontend/src/routes/admin/users/[id]/+page.svelte @@ -1,41 +1,13 @@
@@ -76,77 +48,13 @@ function handleBirthDateInput(e: Event) {

{m.profile_section_personal()}

- -
-
- - - -
- - - - - - -
+
@@ -154,21 +62,7 @@ function handleBirthDateInput(e: Event) {

{m.admin_col_groups()}

- -
- {#each data.groups as group (group.id)} - - {/each} -
+
@@ -176,30 +70,7 @@ function handleBirthDateInput(e: Event) {

{m.admin_label_new_password_optional()}

- -
- - - -
+
diff --git a/frontend/src/routes/admin/users/new/+page.svelte b/frontend/src/routes/admin/users/new/+page.svelte index 01fe3e7f..ae79f96b 100644 --- a/frontend/src/routes/admin/users/new/+page.svelte +++ b/frontend/src/routes/admin/users/new/+page.svelte @@ -1,31 +1,11 @@
@@ -55,118 +35,19 @@ function handleBirthDateInput(e: Event) {
- -

- {m.admin_section_users()} -

- - - - +

{m.profile_section_personal()}

- -
- - - -
- - - - - - +

{m.admin_col_groups()}

- -
- {#each data.groups as group (group.id)} - - {/each} -
+
+import { m } from '$lib/paraglide/messages.js'; + + +

+ {m.admin_section_users()} +

+ + + + diff --git a/frontend/src/routes/conversations/+page.svelte b/frontend/src/routes/conversations/+page.svelte index 4ba259af..25665cdb 100644 --- a/frontend/src/routes/conversations/+page.svelte +++ b/frontend/src/routes/conversations/+page.svelte @@ -1,10 +1,10 @@
@@ -74,124 +55,18 @@ const enrichedDocuments = $derived(

- -
-
- -
- applyFilters()} - /> -
- - -
- -
- - -
- applyFilters()} - /> -
-
- -
- -
- - applyFilters()} - class="block w-full border-line py-2.5 text-sm shadow-sm focus:border-ink focus:ring-ink" - /> -
- - -
- - applyFilters()} - class="block w-full border-line py-2.5 text-sm shadow-sm focus:border-ink focus:ring-ink" - /> -
- - -
- -
-
-
+ {#if !senderId || !receiverId} @@ -219,127 +94,11 @@ const enrichedDocuments = $derived(

{m.conv_no_results_text()}

{:else} - -
- {#if yearFrom !== null && yearTo !== null} -

- {m.conv_summary({ count: data.documents.length, yearFrom, yearTo })} -

- {:else} -

- {data.documents.length} -

- {/if} - {#if data.canWrite} - - - - - {m.conv_new_doc_link()} - - {/if} -
- - -
- - - -
-
- {#each enrichedDocuments as { doc, year, showYearDivider } (doc.id)} - {#if showYearDivider} -
-
- {year} -
-
- {/if} - {@const isRight = doc.sender?.id === senderId} - - - - {/each} -
-
-
+ {/if}
diff --git a/frontend/src/routes/conversations/ConversationFilterBar.svelte b/frontend/src/routes/conversations/ConversationFilterBar.svelte new file mode 100644 index 00000000..1dfdd1f4 --- /dev/null +++ b/frontend/src/routes/conversations/ConversationFilterBar.svelte @@ -0,0 +1,142 @@ + + +
+
+ +
+ onapplyFilters()} + /> +
+ + +
+ +
+ + +
+ onapplyFilters()} + /> +
+
+ +
+ +
+ + onapplyFilters()} + class="block w-full border-line py-2.5 text-sm shadow-sm focus:border-ink focus:ring-ink" + /> +
+ + +
+ + onapplyFilters()} + class="block w-full border-line py-2.5 text-sm shadow-sm focus:border-ink focus:ring-ink" + /> +
+ + +
+ +
+
+
diff --git a/frontend/src/routes/conversations/ConversationTimeline.svelte b/frontend/src/routes/conversations/ConversationTimeline.svelte new file mode 100644 index 00000000..c1cac55f --- /dev/null +++ b/frontend/src/routes/conversations/ConversationTimeline.svelte @@ -0,0 +1,164 @@ + + + +
+ {#if yearFrom !== null && yearTo !== null} +

+ {m.conv_summary({ count: documents.length, yearFrom, yearTo })} +

+ {:else} +

+ {documents.length} +

+ {/if} + {#if canWrite} + + + + + {m.conv_new_doc_link()} + + {/if} +
+ + +
+ + + +
+
+ {#each enrichedDocuments as { doc, year, showYearDivider } (doc.id)} + {#if showYearDivider} +
+
+ {year} +
+
+ {/if} + {@const isRight = doc.sender?.id === senderId} + + + + {/each} +
+
+
diff --git a/frontend/src/routes/documents/[id]/+page.svelte b/frontend/src/routes/documents/[id]/+page.svelte index 543634f7..dbfdf827 100644 --- a/frontend/src/routes/documents/[id]/+page.svelte +++ b/frontend/src/routes/documents/[id]/+page.svelte @@ -4,8 +4,7 @@ import DocumentTopBar from '$lib/components/DocumentTopBar.svelte'; import DocumentViewer from '$lib/components/DocumentViewer.svelte'; import DocumentBottomPanel from '$lib/components/DocumentBottomPanel.svelte'; import AnnotationSidePanel from '$lib/components/AnnotationSidePanel.svelte'; - -type Tab = 'metadata' | 'transcription' | 'discussion' | 'history'; +import type { DocumentPanelTab } from '$lib/types'; let { data } = $props(); @@ -72,7 +71,7 @@ const LS_KEY_TAB = 'doc-panel-tab'; let panelOpen = $state(false); let panelHeight = $state(0); // set to full height on mount let navHeight = $state(0); -let activeTab = $state('metadata'); +let activeTab = $state('metadata'); let localStorageRestored = $state(false); onMount(() => { @@ -82,7 +81,7 @@ onMount(() => { const savedTab = localStorage.getItem(LS_KEY_TAB); if (savedTab && ['metadata', 'transcription', 'discussion', 'history'].includes(savedTab)) { - activeTab = savedTab as Tab; + activeTab = savedTab as DocumentPanelTab; } const topbar = document.querySelector('[data-topbar]'); panelHeight = window.innerHeight - navHeight - (topbar?.getBoundingClientRect().height ?? 0); diff --git a/frontend/src/routes/documents/[id]/edit/+page.svelte b/frontend/src/routes/documents/[id]/edit/+page.svelte index cac183a9..0adb8955 100644 --- a/frontend/src/routes/documents/[id]/edit/+page.svelte +++ b/frontend/src/routes/documents/[id]/edit/+page.svelte @@ -1,11 +1,12 @@
@@ -65,253 +42,30 @@ function handleDateInput(e: Event) { {/if} - -
-

- {m.doc_section_who_when()} -

- -
- -
- - - - {#if dateInvalid} -

{m.form_date_error()}

- {/if} -
- - -
- - -
- - -
- -
- - -
-

{m.form_label_receivers()}

- -
-
-
- - -
-

- {m.doc_section_description()} -

- -
- -
- - -
- - -
- - -

{m.form_helper_archive_location()}

-
- - -
-

{m.form_label_tags()}

- - -
- - -
- - -
-
-
- - -
-

- {m.form_label_transcription()} -

- -
- - -
-

- {m.doc_section_file()} -

- -
- - {m.doc_current_file_label()} - {doc.originalFilename} -
- - - -
- - -
- -
- {#if confirmDelete} - {m.doc_delete_confirm()} - - - {:else} - - {/if} -
- - -
- - {m.btn_cancel()} - - -
-
+ + + + +
diff --git a/frontend/src/routes/documents/[id]/edit/FileSectionEdit.svelte b/frontend/src/routes/documents/[id]/edit/FileSectionEdit.svelte new file mode 100644 index 00000000..1896861c --- /dev/null +++ b/frontend/src/routes/documents/[id]/edit/FileSectionEdit.svelte @@ -0,0 +1,40 @@ + + +
+

+ {m.doc_section_file()} +

+ +
+ + {m.doc_current_file_label()} + {originalFilename} +
+ + + +
diff --git a/frontend/src/routes/documents/[id]/edit/SaveBar.svelte b/frontend/src/routes/documents/[id]/edit/SaveBar.svelte new file mode 100644 index 00000000..8fd6547b --- /dev/null +++ b/frontend/src/routes/documents/[id]/edit/SaveBar.svelte @@ -0,0 +1,72 @@ + + +
+ +
+ {#if confirmDelete} + {m.doc_delete_confirm()} + + + {:else} + + {/if} +
+ + +
+ + {m.btn_cancel()} + + +
+
diff --git a/frontend/src/routes/documents/new/+page.svelte b/frontend/src/routes/documents/new/+page.svelte index 0b8355f6..b0eee120 100644 --- a/frontend/src/routes/documents/new/+page.svelte +++ b/frontend/src/routes/documents/new/+page.svelte @@ -1,10 +1,11 @@
@@ -75,167 +46,16 @@ function handleDateInput(e: Event) { {/if}
- -
-

- {m.doc_section_who_when()} -

+ + + + -
- -
- - - - {#if dateInvalid} -

- {m.form_date_error()} -

- {/if} -
- - -
- - -
- - -
- -
- - -
-

{m.form_label_receivers()}

- -
-
-
- - -
-

- {m.doc_section_description()} -

- -
- -
- - -
- - -
- - -

{m.form_helper_archive_location()}

-
- - -
-

{m.form_label_tags()}

- - -
- - -
- - -
-
-
- - -
-

- {m.form_label_transcription()} -

- -
- - -
-

- {m.doc_section_file()} -

- - - -
- - +
diff --git a/frontend/src/routes/documents/new/FileSectionNew.svelte b/frontend/src/routes/documents/new/FileSectionNew.svelte new file mode 100644 index 00000000..3e99fa47 --- /dev/null +++ b/frontend/src/routes/documents/new/FileSectionNew.svelte @@ -0,0 +1,25 @@ + + +
+

+ {m.doc_section_file()} +

+ + + +
diff --git a/frontend/src/routes/persons/[id]/+page.svelte b/frontend/src/routes/persons/[id]/+page.svelte index 5566055c..a37e87da 100644 --- a/frontend/src/routes/persons/[id]/+page.svelte +++ b/frontend/src/routes/persons/[id]/+page.svelte @@ -1,10 +1,10 @@
@@ -110,500 +64,25 @@ $effect(() => {
- -
-
+ -
- {#if editMode && data.canWrite} - - -
-

- {m.person_edit_heading()} -

- - {#if form?.updateError} -

- {form.updateError} -

- {/if} - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
- - {:else} - -
-
-
- {person.firstName[0]}{person.lastName[0]} -
-
- -
-
-

- {person.firstName} - {person.lastName} -

-
- {#if data.canWrite} - - {/if} -
-
- -
-
- {m.person_label_full_name()} - {person.firstName} {person.lastName} -
- - {#if person.alias} -
- {m.form_label_alias()} - "{person.alias}" -
- {/if} - - {#if person.birthYear || person.deathYear} -
- - {#if person.birthYear && person.deathYear}{m.person_label_birth_year()} / {m.person_label_death_year()}{:else if person.birthYear}{m.person_label_birth_year()}{:else}{m.person_label_death_year()}{/if} - - - {#if person.birthYear}* {person.birthYear}{/if}{#if person.birthYear && person.deathYear} -  {/if}{#if person.deathYear}† {person.deathYear}{/if} - -
- {/if} - - {#if person.notes} -
- {m.person_label_notes()} -

- {person.notes} -

-
- {/if} -
-
-
- {/if} -
-
- - {#if data.canWrite} {#key person.id} -
-
-

{m.person_merge_heading()}

-

- {m.person_merge_description()} -

- - {#if form?.mergeError} -

- {form.mergeError} -

- {/if} - -
- - -
-
- { mergeTargetId = value; showMergeConfirm = false; }} - /> -
- - {#if !showMergeConfirm} - - {:else} -
- - -
- {/if} -
- - {#if showMergeConfirm} -

- {m.person_merge_warning()} {person.firstName} {person.lastName} - {m.person_merge_will_be_deleted()} -

- {/if} -
-
-
+ {/key} {/if} - - {#if coCorrespondents.length > 0} -
-

- {m.person_co_correspondents_heading()} -

-
- {#each coCorrespondents as c (c.id)} - - {c.name} - ({c.count}) - - {/each} -
-
- {/if} + - -
-
-

{m.person_docs_heading()}

- - {sentDocuments.length} - - {#if sentYearRange} - {sentYearRange} - {/if} - {#if sentDocuments.length > 1} - - {/if} -
+ - {#if sentDocuments.length === 0} -
-

{m.person_no_docs()}

-
- {:else} - - {#if sentDocuments.length > DOCS_PREVIEW_LIMIT && !showAllSent} - - {/if} - {/if} -
- - -
-
-

{m.person_received_docs_heading()}

- - {receivedDocuments.length} - - {#if receivedYearRange} - {receivedYearRange} - {/if} - {#if receivedDocuments.length > 1} - - {/if} -
- - {#if receivedDocuments.length === 0} -
-

{m.person_no_received_docs()}

-
- {:else} - - {#if receivedDocuments.length > DOCS_PREVIEW_LIMIT && !showAllReceived} - - {/if} - {/if} -
+
diff --git a/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte b/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte new file mode 100644 index 00000000..5b0409eb --- /dev/null +++ b/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte @@ -0,0 +1,30 @@ + + +{#if coCorrespondents.length > 0} +
+

+ {m.person_co_correspondents_heading()} +

+
+ {#each coCorrespondents as c (c.id)} + + {c.name} + ({c.count}) + + {/each} +
+
+{/if} diff --git a/frontend/src/routes/persons/[id]/PersonCard.svelte b/frontend/src/routes/persons/[id]/PersonCard.svelte new file mode 100644 index 00000000..c5e2e9e0 --- /dev/null +++ b/frontend/src/routes/persons/[id]/PersonCard.svelte @@ -0,0 +1,246 @@ + + +
+
+ +
+ {#if editMode && canWrite} + +
+
+

+ {m.person_edit_heading()} +

+ + {#if form?.updateError} +

+ {form.updateError} +

+ {/if} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ {:else} + +
+
+
+ {person.firstName[0]}{person.lastName[0]} +
+
+ +
+
+

+ {person.firstName} + {person.lastName} +

+
+ {#if canWrite} + + {/if} +
+
+ +
+
+ {m.person_label_full_name()} + {person.firstName} {person.lastName} +
+ + {#if person.alias} +
+ {m.form_label_alias()} + "{person.alias}" +
+ {/if} + + {#if person.birthYear || person.deathYear} +
+ + {#if person.birthYear && person.deathYear}{m.person_label_birth_year()} / {m.person_label_death_year()}{:else if person.birthYear}{m.person_label_birth_year()}{:else}{m.person_label_death_year()}{/if} + + + {#if person.birthYear}* {person.birthYear}{/if}{#if person.birthYear && person.deathYear} +  {/if}{#if person.deathYear}† {person.deathYear}{/if} + +
+ {/if} + + {#if person.notes} +
+ {m.person_label_notes()} +

+ {person.notes} +

+
+ {/if} +
+
+
+ {/if} +
+
diff --git a/frontend/src/routes/persons/[id]/PersonDocumentList.svelte b/frontend/src/routes/persons/[id]/PersonDocumentList.svelte new file mode 100644 index 00000000..97e6a7e1 --- /dev/null +++ b/frontend/src/routes/persons/[id]/PersonDocumentList.svelte @@ -0,0 +1,132 @@ + + +
+
+

{heading}

+ + {documents.length} + + {#if yearRange} + {yearRange} + {/if} + {#if documents.length > 1} + + {/if} +
+ + {#if documents.length === 0} +
+

{emptyMessage}

+
+ {:else} + + {#if documents.length > DOCS_PREVIEW_LIMIT && !showAll} + + {/if} + {/if} +
diff --git a/frontend/src/routes/persons/[id]/PersonMergePanel.svelte b/frontend/src/routes/persons/[id]/PersonMergePanel.svelte new file mode 100644 index 00000000..0a771b0e --- /dev/null +++ b/frontend/src/routes/persons/[id]/PersonMergePanel.svelte @@ -0,0 +1,83 @@ + + +
+
+

{m.person_merge_heading()}

+

+ {m.person_merge_description()} +

+ + {#if form?.mergeError} +

+ {form.mergeError} +

+ {/if} + +
+ + +
+
+ { + mergeTargetId = value; + showMergeConfirm = false; + }} + /> +
+ + {#if !showMergeConfirm} + + {:else} +
+ + +
+ {/if} +
+ + {#if showMergeConfirm} +

+ {m.person_merge_warning()} {person.firstName} {person.lastName} + {m.person_merge_will_be_deleted()} +

+ {/if} +
+
+
diff --git a/frontend/src/routes/profile/+page.svelte b/frontend/src/routes/profile/+page.svelte index 5ae5e90c..3447118e 100644 --- a/frontend/src/routes/profile/+page.svelte +++ b/frontend/src/routes/profile/+page.svelte @@ -1,41 +1,9 @@
@@ -59,181 +27,7 @@ function handleBirthDateInput(e: Event) {

{m.profile_heading()}

- -
-

- {m.profile_section_personal()} -

- - {#if form?.updateSuccess} -
- {m.profile_saved()} -
- {/if} - {#if form?.updateError} -
- {form.updateError} -
- {/if} - -
-
- - - - - - - - - -
- - -
-
- - -
-

- {m.profile_section_password()} -

- - {#if form?.passwordSuccess} -
- {m.profile_password_changed()} -
- {/if} - {#if form?.passwordError} -
- {#if form.passwordError === 'PASSWORDS_DO_NOT_MATCH'} - {m.profile_password_mismatch()} - {:else} - {form.passwordError} - {/if} -
- {/if} - -
-
- - - - - -
- - -
-
+ +
diff --git a/frontend/src/routes/profile/PasswordChangeForm.svelte b/frontend/src/routes/profile/PasswordChangeForm.svelte new file mode 100644 index 00000000..27f22efd --- /dev/null +++ b/frontend/src/routes/profile/PasswordChangeForm.svelte @@ -0,0 +1,78 @@ + + +
+

+ {m.profile_section_password()} +

+ + {#if form?.passwordSuccess} +
+ {m.profile_password_changed()} +
+ {/if} + {#if form?.passwordError} +
+ {#if form.passwordError === 'PASSWORDS_DO_NOT_MATCH'} + {m.profile_password_mismatch()} + {:else} + {form.passwordError} + {/if} +
+ {/if} + +
+
+ + + + + +
+ + +
+
diff --git a/frontend/src/routes/profile/PersonalInfoForm.svelte b/frontend/src/routes/profile/PersonalInfoForm.svelte new file mode 100644 index 00000000..8cd37f7a --- /dev/null +++ b/frontend/src/routes/profile/PersonalInfoForm.svelte @@ -0,0 +1,123 @@ + + +
+

+ {m.profile_section_personal()} +

+ + {#if form?.updateSuccess} +
+ {m.profile_saved()} +
+ {/if} + {#if form?.updateError} +
+ {form.updateError} +
+ {/if} + +
+
+ + + + + + + + + +
+ + +
+