Files
familienarchiv/frontend/src/routes/documents/[id]/+page.svelte
Marcel ecfd80bf9a
Some checks failed
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m34s
CI / Backend Unit Tests (pull_request) Successful in 2m16s
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (pull_request) Failing after 24m11s
feat(frontend): add discussion sub-tab navigation for annotation threads
Within the Diskussion panel tab, show two sub-tabs when an annotation is
active: «Diskussion» (document-level thread, with comment-count badge) and
«Annotation · Seite N» (annotation-specific thread).

Behaviour:
- Clicking an annotation auto-switches to the Annotation sub-tab
- Clicking the Diskussion sub-tab deselects the annotation and returns to
  the document thread
- Escape clears the active annotation (or collapses the panel if none)
- activeAnnotationPage is now lifted from PdfViewer → DocumentViewer →
  page → DocumentBottomPanel → PanelDiscussion so the tab label shows the
  correct page number

Closes #60
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 22:45:35 +01:00

170 lines
4.8 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
import DocumentTopBar from '$lib/components/DocumentTopBar.svelte';
import DocumentViewer from '$lib/components/DocumentViewer.svelte';
import DocumentBottomPanel from '$lib/components/DocumentBottomPanel.svelte';
type Tab = 'metadata' | 'transcription' | 'discussion' | 'history';
let { data } = $props();
const doc = $derived(data.document);
const canComment = $derived((data.canAnnotate || data.canWrite) ?? false);
const canAdmin = $derived(
(data.user?.groups as Array<{ permissions: string[] }> | undefined)?.some((g) =>
g.permissions.includes('ADMIN')
) ?? false
);
const currentUserId = $derived((data.user?.id as string | undefined) ?? null);
// ── File loading ──────────────────────────────────────────────────────────────
let fileUrl = $state('');
let isLoading = $state(false);
let fileError = $state('');
$effect(() => {
if (doc?.id && doc?.filePath) {
loadFile(doc.id);
}
});
async function loadFile(id: string) {
isLoading = true;
fileError = '';
fileUrl = '';
try {
const response = await fetch(`/api/documents/${id}/file`);
if (!response.ok) {
if (response.status === 401) throw new Error('Nicht eingeloggt');
throw new Error('Fehler beim Laden der Datei');
}
const blob = await response.blob();
fileUrl = URL.createObjectURL(blob);
} catch (e) {
console.error(e);
fileError = 'Vorschau konnte nicht geladen werden.';
} finally {
isLoading = false;
}
}
// ── Annotation state (lifted from PdfViewer) ──────────────────────────────────
let annotateMode = $state(false);
let activeAnnotationId = $state<string | null>(null);
let activeAnnotationPage = $state<number | null>(null);
// When an annotation is clicked, open the Diskussion tab.
$effect(() => {
if (activeAnnotationId) {
activeTab = 'discussion';
panelOpen = true;
}
});
// ── Bottom panel state ────────────────────────────────────────────────────────
const LS_KEY_OPEN = 'doc-panel-open';
const LS_KEY_HEIGHT = 'doc-panel-height';
const LS_KEY_TAB = 'doc-panel-tab';
let panelOpen = $state(false);
let panelHeight = $state(320);
let activeTab = $state<Tab>('metadata');
let localStorageRestored = $state(false);
onMount(() => {
const savedOpen = localStorage.getItem(LS_KEY_OPEN);
const savedHeight = localStorage.getItem(LS_KEY_HEIGHT);
const savedTab = localStorage.getItem(LS_KEY_TAB);
if (savedTab && ['metadata', 'transcription', 'discussion', 'history'].includes(savedTab)) {
activeTab = savedTab as Tab;
}
if (savedHeight) {
const h = parseInt(savedHeight, 10);
if (!isNaN(h) && h >= 80) panelHeight = h;
}
if (savedOpen !== null) {
panelOpen = savedOpen === 'true';
} else if (!doc.filePath) {
// No previous state and no file → open to Metadaten by default
panelOpen = true;
activeTab = 'metadata';
}
localStorageRestored = true;
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
if (activeAnnotationId) {
activeAnnotationId = null;
activeAnnotationPage = null;
} else if (panelOpen) {
panelOpen = false;
}
}
}
document.addEventListener('keydown', onKeyDown);
return () => document.removeEventListener('keydown', onKeyDown);
});
// Persist panel state whenever it changes (after initial restore).
$effect(() => {
if (!localStorageRestored) return;
localStorage.setItem(LS_KEY_OPEN, String(panelOpen));
localStorage.setItem(LS_KEY_HEIGHT, String(panelHeight));
localStorage.setItem(LS_KEY_TAB, activeTab);
});
</script>
<svelte:head>
<title>{doc.title || doc.originalFilename || 'Dokument'}</title>
</svelte:head>
<div class="flex h-screen flex-col overflow-hidden bg-white" data-hydrated>
<DocumentTopBar
doc={doc}
canWrite={data.canWrite ?? false}
canAnnotate={data.canAnnotate ?? false}
fileUrl={fileUrl}
bind:annotateMode={annotateMode}
/>
<div class="relative flex-1 overflow-hidden">
<DocumentViewer
doc={doc}
fileUrl={fileUrl}
isLoading={isLoading}
error={fileError}
bind:annotateMode={annotateMode}
bind:activeAnnotationId={activeAnnotationId}
bind:activeAnnotationPage={activeAnnotationPage}
onAnnotationClick={(id) => {
activeAnnotationId = id;
}}
/>
</div>
</div>
<DocumentBottomPanel
doc={doc}
comments={(data.comments ?? []) as never[]}
canComment={canComment}
currentUserId={currentUserId}
canAdmin={canAdmin}
bind:open={panelOpen}
bind:height={panelHeight}
bind:activeTab={activeTab}
activeAnnotationId={activeAnnotationId}
activeAnnotationPage={activeAnnotationPage}
onClearAnnotation={() => {
activeAnnotationId = null;
activeAnnotationPage = null;
}}
/>