+ {#each annotations as annotation (annotation.id)}
+
+ {#if canAnnotate}
+
+ {/if}
+
+ {/each}
+
+ {#if drawRect && drawRect.width > 0}
+
+ {/if}
+
diff --git a/frontend/src/lib/components/AnnotationLayer.svelte.spec.ts b/frontend/src/lib/components/AnnotationLayer.svelte.spec.ts
new file mode 100644
index 00000000..e25f95c4
--- /dev/null
+++ b/frontend/src/lib/components/AnnotationLayer.svelte.spec.ts
@@ -0,0 +1,74 @@
+import { describe, it, expect, afterEach } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+
+import AnnotationLayer from './AnnotationLayer.svelte';
+
+afterEach(cleanup);
+
+type Annotation = {
+ id: string;
+ documentId: string;
+ pageNumber: number;
+ x: number;
+ y: number;
+ width: number;
+ height: number;
+ color: string;
+ createdAt: string;
+};
+
+function makeAnnotation(id = 'ann-1'): Annotation {
+ return {
+ id,
+ documentId: 'doc-1',
+ pageNumber: 1,
+ x: 0.1,
+ y: 0.1,
+ width: 0.3,
+ height: 0.2,
+ color: '#ff0000',
+ createdAt: new Date().toISOString()
+ };
+}
+
+describe('AnnotationLayer', () => {
+ it('renders a colored element for each annotation', async () => {
+ render(AnnotationLayer, {
+ annotations: [makeAnnotation('ann-1'), makeAnnotation('ann-2')],
+ canAnnotate: false,
+ color: '#ff0000',
+ onDraw: () => {},
+ onDelete: () => {}
+ });
+
+ await expect.element(page.getByTestId('annotation-ann-1')).toBeInTheDocument();
+ await expect.element(page.getByTestId('annotation-ann-2')).toBeInTheDocument();
+ });
+
+ it('shows a delete button for each annotation when canAnnotate is true', async () => {
+ render(AnnotationLayer, {
+ annotations: [makeAnnotation('ann-1')],
+ canAnnotate: true,
+ color: '#ff0000',
+ onDraw: () => {},
+ onDelete: () => {}
+ });
+
+ await expect
+ .element(page.getByRole('button', { name: /annotation löschen/i }))
+ .toBeInTheDocument();
+ });
+
+ it('does not show delete buttons when canAnnotate is false', async () => {
+ render(AnnotationLayer, {
+ annotations: [makeAnnotation('ann-1')],
+ canAnnotate: false,
+ color: '#ff0000',
+ onDraw: () => {},
+ onDelete: () => {}
+ });
+
+ expect(page.getByRole('button', { name: /annotation löschen/i }).query()).toBeNull();
+ });
+});
diff --git a/frontend/src/lib/components/PdfViewer.svelte b/frontend/src/lib/components/PdfViewer.svelte
new file mode 100644
index 00000000..7863143c
--- /dev/null
+++ b/frontend/src/lib/components/PdfViewer.svelte
@@ -0,0 +1,422 @@
+
+
+{#if !url}
+
+
Fehler beim Laden der PDF
+
+ Direkt öffnen
+
+
+{:else}
+

diff --git a/frontend/src/routes/documents/new/page.svelte.spec.ts b/frontend/src/routes/documents/new/page.svelte.spec.ts
index 0a269279..dcc3b387 100644
--- a/frontend/src/routes/documents/new/page.svelte.spec.ts
+++ b/frontend/src/routes/documents/new/page.svelte.spec.ts
@@ -10,6 +10,7 @@ afterEach(cleanup);
const baseData = {
user: undefined,
canWrite: true,
+ canAnnotate: false,
persons: [],
initialSenderId: '',
initialSenderName: '',
diff --git a/frontend/src/routes/layout.svelte.spec.ts b/frontend/src/routes/layout.svelte.spec.ts
index add63655..6103089c 100644
--- a/frontend/src/routes/layout.svelte.spec.ts
+++ b/frontend/src/routes/layout.svelte.spec.ts
@@ -22,6 +22,7 @@ const makeData = (overrides = {}) => ({
createdAt: ''
},
canWrite: true,
+ canAnnotate: false,
...overrides
});
diff --git a/frontend/src/routes/page.svelte.spec.ts b/frontend/src/routes/page.svelte.spec.ts
index df6eddd4..d50ce18b 100644
--- a/frontend/src/routes/page.svelte.spec.ts
+++ b/frontend/src/routes/page.svelte.spec.ts
@@ -20,6 +20,7 @@ afterEach(cleanup);
const emptyData = {
user: undefined,
canWrite: true,
+ canAnnotate: false,
filters: { q: '', from: '', to: '', senderId: '', receiverId: '', tags: [] },
documents: [],
initialValues: { senderName: '', receiverName: '' },
diff --git a/frontend/src/routes/persons/page.svelte.spec.ts b/frontend/src/routes/persons/page.svelte.spec.ts
index a2c1f8d5..9dc5fc75 100644
--- a/frontend/src/routes/persons/page.svelte.spec.ts
+++ b/frontend/src/routes/persons/page.svelte.spec.ts
@@ -14,7 +14,7 @@ const makePerson = (overrides = {}) => ({
...overrides
});
-const emptyData = { user: undefined, canWrite: true, q: '', persons: [] };
+const emptyData = { user: undefined, canWrite: true, canAnnotate: false, q: '', persons: [] };
const dataWithPersons = { ...emptyData, persons: [makePerson()] };
afterEach(cleanup);
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index 76be0fe1..c5f71a26 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -6,6 +6,9 @@ import { playwright } from '@vitest/browser-playwright';
import { sveltekit } from '@sveltejs/kit/vite';
export default defineConfig({
+ optimizeDeps: {
+ include: ['pdfjs-dist']
+ },
server: {
host: '0.0.0.0', // Erlaubt Zugriff von außen
port: 5173, // Standard SvelteKit Port
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 386ec56f..c75d7088 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -207,6 +207,28 @@
resolved "https://registry.npmjs.org/@lix-js/server-protocol-schema/-/server-protocol-schema-0.1.1.tgz"
integrity sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==
+"@napi-rs/canvas-linux-x64-gnu@0.1.97":
+ version "0.1.97"
+ resolved "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.97.tgz"
+ integrity sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==
+
+"@napi-rs/canvas@^0.1.95":
+ version "0.1.97"
+ resolved "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.97.tgz"
+ integrity sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==
+ optionalDependencies:
+ "@napi-rs/canvas-android-arm64" "0.1.97"
+ "@napi-rs/canvas-darwin-arm64" "0.1.97"
+ "@napi-rs/canvas-darwin-x64" "0.1.97"
+ "@napi-rs/canvas-linux-arm-gnueabihf" "0.1.97"
+ "@napi-rs/canvas-linux-arm64-gnu" "0.1.97"
+ "@napi-rs/canvas-linux-arm64-musl" "0.1.97"
+ "@napi-rs/canvas-linux-riscv64-gnu" "0.1.97"
+ "@napi-rs/canvas-linux-x64-gnu" "0.1.97"
+ "@napi-rs/canvas-linux-x64-musl" "0.1.97"
+ "@napi-rs/canvas-win32-arm64-msvc" "0.1.97"
+ "@napi-rs/canvas-win32-x64-msvc" "0.1.97"
+
"@playwright/test@^1.58.2":
version "1.58.2"
resolved "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz"
@@ -1462,6 +1484,11 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
+node-readable-to-web-readable-stream@^0.4.2:
+ version "0.4.2"
+ resolved "https://registry.npmjs.org/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz"
+ integrity sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==
+
obug@^2.1.0, obug@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz"
@@ -1553,6 +1580,14 @@ pathe@^2.0.3:
resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz"
integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
+pdfjs-dist@^5.5.207:
+ version "5.5.207"
+ resolved "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz"
+ integrity sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==
+ optionalDependencies:
+ "@napi-rs/canvas" "^0.1.95"
+ node-readable-to-web-readable-stream "^0.4.2"
+
picocolors@^1.0.0, picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
diff --git a/scripts/rebuild-frontend.sh b/scripts/rebuild-frontend.sh
new file mode 100755
index 00000000..7e50b474
--- /dev/null
+++ b/scripts/rebuild-frontend.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+# Rebuilds the frontend Docker container and refreshes the node_modules volume.
+# Run this after adding or updating npm dependencies.
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+
+echo "Stopping frontend container..."
+docker compose stop frontend
+
+echo "Removing frontend container..."
+docker compose rm -f frontend
+
+echo "Removing stale node_modules volume..."
+docker volume rm familienarchiv_frontend_node_modules 2>/dev/null || true
+
+echo "Rebuilding image and starting container..."
+docker compose up -d --build frontend
+
+echo "Done. Tailing logs (Ctrl+C to exit)..."
+docker compose logs -f frontend