From 73229077beca2510c06e207a31f6d0804ab3dc44 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 13 Apr 2026 13:59:35 +0200 Subject: [PATCH] feat(transcription): add sticky review progress counter to TranscriptionEditView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shows 'X / Y geprüft' with a brand-mint progress bar at the top of the transcription panel. Derived from the blocks prop — no extra state. Co-Authored-By: Claude Sonnet 4.6 --- .../components/TranscriptionEditView.svelte | 149 ++++++++++-------- .../TranscriptionEditView.svelte.spec.ts | 28 +++- 2 files changed, 109 insertions(+), 68 deletions(-) diff --git a/frontend/src/lib/components/TranscriptionEditView.svelte b/frontend/src/lib/components/TranscriptionEditView.svelte index a8f27ec3..e93471ac 100644 --- a/frontend/src/lib/components/TranscriptionEditView.svelte +++ b/frontend/src/lib/components/TranscriptionEditView.svelte @@ -50,6 +50,9 @@ let debounceTimers = new SvelteMap>(); let pendingTexts = new SvelteMap(); let sortedBlocks = $derived([...blocks].sort((a, b) => a.sortOrder - b.sortOrder)); let hasBlocks = $derived(blocks.length > 0); +let reviewedCount = $derived(blocks.filter((b) => b.reviewed).length); +let totalCount = $derived(blocks.length); +let reviewProgress = $derived(totalCount > 0 ? (reviewedCount / totalCount) * 100 : 0); function getSaveState(blockId: string): SaveState { return saveStates.get(blockId) ?? 'idle'; @@ -263,78 +266,92 @@ $effect(() => { }); -
+
{#if hasBlocks} - -
- {#each sortedBlocks as block, i (block.id)} - {#if dropTargetIdx === i} -
- {/if} - + +
+

+ {reviewedCount} / {totalCount} geprüft +

+
handleGripDown(e, block.id)} - class="relative transition-all duration-150 {draggedBlockId === block.id ? 'z-10 rounded-lg shadow-xl ring-2 ring-turquoise/40' : ''}" - style={draggedBlockId === block.id ? `transform: translateY(${dragOffsetY}px) scale(1.02); opacity: 0.9;` : ''} - > - handleTextChange(block.id, text)} - onFocus={() => handleFocus(block.id)} - onDeleteClick={() => handleDelete(block.id)} - onRetry={() => handleRetry(block.id)} - onReviewToggle={() => onReviewToggle(block.id)} - onMoveUp={() => handleMoveUp(block.id)} - onMoveDown={() => handleMoveDown(block.id)} - isFirst={i === 0} - isLast={i === sortedBlocks.length - 1} - /> -
- {/each} - - {#if dropTargetIdx === sortedBlocks.length} -
- {/if} - - -
- {m.transcription_next_block_cta({ number: sortedBlocks.length + 1 })} + class="h-full rounded-full bg-brand-mint transition-all duration-300" + style="width: {reviewProgress}%" + >
- - {#if canRunOcr && onTriggerOcr} -
- +
+ +
+ {#each sortedBlocks as block, i (block.id)} + {#if dropTargetIdx === i} +
+ {/if} + +
handleGripDown(e, block.id)} + class="relative transition-all duration-150 {draggedBlockId === block.id ? 'z-10 rounded-lg shadow-xl ring-2 ring-turquoise/40' : ''}" + style={draggedBlockId === block.id ? `transform: translateY(${dragOffsetY}px) scale(1.02); opacity: 0.9;` : ''} > - {m.ocr_rerun_label()} -
-
- handleTextChange(block.id, text)} + onFocus={() => handleFocus(block.id)} + onDeleteClick={() => handleDelete(block.id)} + onRetry={() => handleRetry(block.id)} + onReviewToggle={() => onReviewToggle(block.id)} + onMoveUp={() => handleMoveUp(block.id)} + onMoveDown={() => handleMoveDown(block.id)} + isFirst={i === 0} + isLast={i === sortedBlocks.length - 1} />
-
- {/if} + {/each} + + {#if dropTargetIdx === sortedBlocks.length} +
+ {/if} + + +
+ {m.transcription_next_block_cta({ number: sortedBlocks.length + 1 })} +
+ + {#if canRunOcr && onTriggerOcr} +
+ + {m.ocr_rerun_label()} + +
+ +
+
+ {/if} +
{:else}
diff --git a/frontend/src/lib/components/TranscriptionEditView.svelte.spec.ts b/frontend/src/lib/components/TranscriptionEditView.svelte.spec.ts index c7fd211e..a6490eb5 100644 --- a/frontend/src/lib/components/TranscriptionEditView.svelte.spec.ts +++ b/frontend/src/lib/components/TranscriptionEditView.svelte.spec.ts @@ -13,7 +13,9 @@ const block1 = { text: 'Block eins', label: null, sortOrder: 0, - version: 0 + version: 0, + source: 'MANUAL' as const, + reviewed: false }; const block2 = { id: 'b2', @@ -22,7 +24,9 @@ const block2 = { text: 'Block zwei', label: null, sortOrder: 1, - version: 0 + version: 0, + source: 'OCR' as const, + reviewed: true }; function renderView(overrides: Record = {}, service = createConfirmService()) { @@ -232,3 +236,23 @@ describe('TranscriptionEditView — delete block', () => { expect(onDeleteBlock).not.toHaveBeenCalled(); }); }); + +// ─── Review progress counter ────────────────────────────────────────────────── + +describe('TranscriptionEditView — review progress counter', () => { + it('shows reviewed count and total when blocks exist', async () => { + // block1: reviewed=false, block2: reviewed=true → "1 / 2 geprüft" + renderView(); + await expect.element(page.getByText(/1 \/ 2 geprüft/)).toBeInTheDocument(); + }); + + it('shows 0 reviewed when no blocks are reviewed', async () => { + renderView({ blocks: [block1] }); // block1.reviewed = false + await expect.element(page.getByText(/0 \/ 1 geprüft/)).toBeInTheDocument(); + }); + + it('does not show progress counter when there are no blocks', async () => { + renderView({ blocks: [] }); + await expect.element(page.getByText(/geprüft/)).not.toBeInTheDocument(); + }); +});