feat(edit): unify edit page with enrich split-panel layout
Extract DocumentEditLayout shared component for the PDF+form split-panel UI, replacing the old scrolling layout on /documents/[id]/edit with the same fixed-panel structure used by /enrich/[id]. Removes TranscriptionSection and FileSectionEdit from the edit page; file upload/replace is now handled by the shared layout. Delete SaveBar and FileSectionEdit as dead code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,101 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { onMount, onDestroy, untrack } from 'svelte';
|
||||
import { createFileLoader } from '$lib/hooks/useFileLoader.svelte';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { countRequiredFilled } from '$lib/utils/requiredFields';
|
||||
import DocumentViewer from '$lib/components/DocumentViewer.svelte';
|
||||
import UploadZone from '$lib/components/document/UploadZone.svelte';
|
||||
import WhoWhenSection from '$lib/components/document/WhoWhenSection.svelte';
|
||||
import DescriptionSection from '$lib/components/document/DescriptionSection.svelte';
|
||||
import DocumentEditLayout from '$lib/components/document/DocumentEditLayout.svelte';
|
||||
|
||||
let { data, form } = $props();
|
||||
|
||||
const doc = $derived(data.document);
|
||||
|
||||
// File preview state
|
||||
const fileLoader = createFileLoader();
|
||||
|
||||
let navHeight = $state(0);
|
||||
onMount(() => {
|
||||
navHeight = document.querySelector('header')?.getBoundingClientRect().height ?? 0;
|
||||
});
|
||||
|
||||
// Dummy bindable state required by DocumentViewer
|
||||
let annotateMode = $state(false);
|
||||
let activeAnnotationId = $state<string | null>(null);
|
||||
let activeAnnotationPage = $state<number | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (doc?.id && doc?.filePath) {
|
||||
fileLoader.loadFile(`/api/documents/${doc.id}/file`);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => fileLoader.destroy());
|
||||
|
||||
// Form state
|
||||
let tags = $state(untrack(() => doc.tags ?? []));
|
||||
let senderId = $state(untrack(() => doc.sender?.id ?? ''));
|
||||
let selectedReceivers = $state(untrack(() => doc.receivers ?? []));
|
||||
|
||||
// Progress bar state — bound from child components
|
||||
let dateIso = $state(untrack(() => doc.documentDate ?? ''));
|
||||
let currentTitle = $state(untrack(() => doc.title ?? ''));
|
||||
|
||||
const requiredFilled = $derived(countRequiredFilled(currentTitle, dateIso, senderId));
|
||||
const requiredPct = $derived((requiredFilled / 3) * 100);
|
||||
|
||||
// Upload state machine
|
||||
let isUploading = $state(false);
|
||||
let isDragging = $state(false);
|
||||
let uploadError = $state<string | null>(null);
|
||||
let abortController = $state<AbortController | null>(null);
|
||||
|
||||
async function handleFile(file: File) {
|
||||
uploadError = null;
|
||||
isUploading = true;
|
||||
const controller = new AbortController();
|
||||
abortController = controller;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await fetch(`/api/documents/${doc.id}/file`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
signal: controller.signal
|
||||
});
|
||||
if (!res.ok) throw new Error('Upload fehlgeschlagen');
|
||||
await invalidate('app:document');
|
||||
} catch (e) {
|
||||
if ((e as Error).name === 'AbortError') return;
|
||||
uploadError = m.error_file_upload_failed();
|
||||
} finally {
|
||||
isUploading = false;
|
||||
abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelUpload() {
|
||||
abortController?.abort();
|
||||
isUploading = false;
|
||||
}
|
||||
|
||||
async function handleReplaceFile(e: Event) {
|
||||
const file = (e.currentTarget as HTMLInputElement).files?.[0];
|
||||
if (!file) return;
|
||||
await handleFile(file);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{doc.title || doc.originalFilename || 'Dokument'} — Anreicherung</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="fixed inset-x-0 bottom-0 flex flex-col" style="top: {navHeight}px">
|
||||
<!-- Top bar -->
|
||||
<div class="flex items-center justify-between border-b border-line bg-surface px-6 py-3">
|
||||
<DocumentEditLayout doc={doc} formId="save-form" formAction="?/save" formError={form?.error}>
|
||||
{#snippet topbar()}
|
||||
<a
|
||||
href="/enrich"
|
||||
class="group inline-flex items-center font-sans text-xs font-bold tracking-widest text-ink-2 uppercase transition-colors hover:text-ink"
|
||||
@@ -116,133 +34,37 @@ async function handleReplaceFile(e: Event) {
|
||||
<p class="font-sans text-xs text-ink-3">
|
||||
{m.enrich_progress({ count: data.incompleteCount })}
|
||||
</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<!-- Required-fields progress bar -->
|
||||
<div class="flex items-center gap-3 border-b border-line bg-surface px-6 py-1.5">
|
||||
<span class="text-xs font-bold tracking-widest text-ink-3 uppercase">Pflichtfelder</span>
|
||||
<div
|
||||
class="h-0.5 flex-1 rounded-full bg-line"
|
||||
role="progressbar"
|
||||
aria-valuenow={requiredFilled}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={3}
|
||||
aria-label="Pflichtfelder"
|
||||
{#snippet actionbar()}
|
||||
<button
|
||||
type="submit"
|
||||
form="skip-form"
|
||||
class="font-sans text-sm font-medium text-ink-2 transition-colors hover:text-ink"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-full bg-brand-navy transition-all duration-300"
|
||||
style="width:{requiredPct}%"
|
||||
></div>
|
||||
</div>
|
||||
<span class="text-xs font-bold text-brand-navy">{requiredFilled} / 3</span>
|
||||
</div>
|
||||
{m.enrich_skip()}
|
||||
</button>
|
||||
|
||||
<!-- Main content -->
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<!-- Left: PDF preview / upload zone (60%) -->
|
||||
<div class="relative flex flex-[6] flex-col overflow-hidden border-r border-line">
|
||||
{#if !doc.filePath}
|
||||
<UploadZone
|
||||
filename={doc.originalFilename}
|
||||
isUploading={isUploading}
|
||||
bind:isDragging={isDragging}
|
||||
error={uploadError}
|
||||
onFile={handleFile}
|
||||
onCancel={cancelUpload}
|
||||
/>
|
||||
{:else}
|
||||
<!-- Datei ersetzen toolbar (ghost button above PDF) -->
|
||||
<div class="flex shrink-0 items-center border-b border-white/10 bg-pdf-bg px-4 py-1.5">
|
||||
<label
|
||||
class="ml-auto flex min-h-[44px] cursor-pointer items-center text-xs font-bold tracking-widest text-white/40 uppercase transition-colors hover:text-white/70"
|
||||
>
|
||||
Datei ersetzen
|
||||
<input type="file" class="sr-only" onchange={handleReplaceFile} />
|
||||
</label>
|
||||
</div>
|
||||
<div class="relative flex-1 overflow-hidden">
|
||||
<DocumentViewer
|
||||
doc={doc}
|
||||
fileUrl={fileLoader.fileUrl}
|
||||
isLoading={fileLoader.isLoading}
|
||||
error={fileLoader.fileError}
|
||||
bind:annotateMode={annotateMode}
|
||||
bind:activeAnnotationId={activeAnnotationId}
|
||||
bind:activeAnnotationPage={activeAnnotationPage}
|
||||
onAnnotationClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Right: form (40%) -->
|
||||
<div class="flex flex-[4] flex-col overflow-hidden">
|
||||
{#if form?.error}
|
||||
<div class="border-b border-red-200 bg-red-50 px-6 py-3 text-sm text-red-700">
|
||||
{form.error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form
|
||||
id="save-form"
|
||||
method="POST"
|
||||
action="?/save"
|
||||
enctype="multipart/form-data"
|
||||
use:enhance
|
||||
class="flex-1 space-y-5 overflow-y-auto p-6"
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
form="save-form"
|
||||
formaction="?/save"
|
||||
class="rounded-sm border border-gray-300 px-5 py-2 font-sans text-xs font-bold tracking-widest text-gray-600 uppercase transition-colors hover:bg-gray-50"
|
||||
>
|
||||
<WhoWhenSection
|
||||
bind:senderId={senderId}
|
||||
bind:selectedReceivers={selectedReceivers}
|
||||
bind:dateIso={dateIso}
|
||||
initialDateIso={doc.documentDate ?? ''}
|
||||
initialLocation={doc.location ?? ''}
|
||||
initialSenderName={doc.sender ? doc.sender.displayName : ''}
|
||||
/>
|
||||
<DescriptionSection
|
||||
bind:tags={tags}
|
||||
bind:currentTitle={currentTitle}
|
||||
initialTitle={doc.title ?? ''}
|
||||
titleRequired={true}
|
||||
/>
|
||||
</form>
|
||||
{m.btn_save()}
|
||||
</button>
|
||||
|
||||
<!-- Skip form (outside main form to avoid nesting) -->
|
||||
<form id="skip-form" method="POST" action="?/skip" use:enhance></form>
|
||||
|
||||
<!-- Action bar -->
|
||||
<div class="flex items-center justify-between gap-3 border-t border-line bg-surface p-4">
|
||||
<!-- Skip button linked to skip-form -->
|
||||
<button
|
||||
type="submit"
|
||||
form="skip-form"
|
||||
class="font-sans text-sm font-medium text-ink-2 transition-colors hover:text-ink"
|
||||
>
|
||||
{m.enrich_skip()}
|
||||
</button>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<!-- Save -->
|
||||
<button
|
||||
type="submit"
|
||||
form="save-form"
|
||||
formaction="?/save"
|
||||
class="rounded-sm border border-gray-300 px-5 py-2 font-sans text-xs font-bold tracking-widest text-gray-600 uppercase transition-colors hover:bg-gray-50"
|
||||
>
|
||||
{m.btn_save()}
|
||||
</button>
|
||||
|
||||
<!-- Save & mark as reviewed -->
|
||||
<button
|
||||
type="submit"
|
||||
form="save-form"
|
||||
formaction="?/saveAndReview"
|
||||
class="rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{m.btn_save_and_mark_reviewed()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
form="save-form"
|
||||
formaction="?/saveAndReview"
|
||||
class="rounded-sm bg-primary px-5 py-2 font-sans text-xs font-bold tracking-widest text-primary-fg uppercase transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{m.btn_save_and_mark_reviewed()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
</DocumentEditLayout>
|
||||
|
||||
<form id="skip-form" method="POST" action="?/skip" use:enhance></form>
|
||||
|
||||
Reference in New Issue
Block a user