refactor(#240): deduplicate formatDate, use generated types, always-visible strip

- Add formatMCDate() to $lib/utils/date.ts (locale-aware, medium format);
  remove duplicated inline formatDate() from all three column components
- Replace local TranscriptionQueueItemDTO/TranscriptionWeeklyStatsDTO type
  declarations with imports from $lib/generated/api across all four components
- Add dashed empty states to SegmentationColumn and TranscriptionColumn
  (ReadyColumn already had one)
- Remove outer {#if} from MissionControlStrip so the section is always
  visible — each column owns its own empty state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-16 12:28:20 +02:00
parent d78685c5a4
commit 06eb1cada8
5 changed files with 58 additions and 83 deletions

View File

@@ -1,23 +1,12 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages.js';
import type { components } from '$lib/generated/api';
import SegmentationColumn from './SegmentationColumn.svelte';
import TranscriptionColumn from './TranscriptionColumn.svelte';
import ReadyColumn from './ReadyColumn.svelte';
type TranscriptionQueueItemDTO = {
id: string;
title: string;
documentDate?: string;
annotationCount: number;
textedBlockCount: number;
reviewedBlockCount: number;
};
type TranscriptionWeeklyStatsDTO = {
segmentationCount: number;
transcriptionCount: number;
readyCount: number;
};
type TranscriptionQueueItemDTO = components['schemas']['TranscriptionQueueItemDTO'];
type TranscriptionWeeklyStatsDTO = components['schemas']['TranscriptionWeeklyStatsDTO'];
interface Props {
segmentationDocs: TranscriptionQueueItemDTO[];
@@ -29,21 +18,16 @@ interface Props {
let { segmentationDocs, transcriptionDocs, readyDocs, weeklyStats }: Props = $props();
</script>
{#if segmentationDocs.length > 0 || transcriptionDocs.length > 0 || readyDocs.length > 0}
<section class="mt-4 rounded-sm border border-line bg-surface p-6">
<h2 class="mb-4 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
{m.mission_control_heading()}
</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<SegmentationColumn
docs={segmentationDocs}
weeklyCount={weeklyStats?.segmentationCount ?? 0}
/>
<TranscriptionColumn
docs={transcriptionDocs}
weeklyCount={weeklyStats?.transcriptionCount ?? 0}
/>
<ReadyColumn docs={readyDocs} weeklyCount={weeklyStats?.readyCount ?? 0} />
</div>
</section>
{/if}
<section class="mt-4 rounded-sm border border-line bg-surface p-6">
<h2 class="mb-4 font-sans text-xs font-bold tracking-widest text-ink-3 uppercase">
{m.mission_control_heading()}
</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<SegmentationColumn docs={segmentationDocs} weeklyCount={weeklyStats?.segmentationCount ?? 0} />
<TranscriptionColumn
docs={transcriptionDocs}
weeklyCount={weeklyStats?.transcriptionCount ?? 0}
/>
<ReadyColumn docs={readyDocs} weeklyCount={weeklyStats?.readyCount ?? 0} />
</div>
</section>

View File

@@ -1,15 +1,10 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages.js';
import { getLocale } from '$lib/paraglide/runtime.js';
import { formatMCDate } from '$lib/utils/date.js';
import type { components } from '$lib/generated/api';
type TranscriptionQueueItemDTO = {
id: string;
title: string;
documentDate?: string;
annotationCount: number;
textedBlockCount: number;
reviewedBlockCount: number;
};
type TranscriptionQueueItemDTO = components['schemas']['TranscriptionQueueItemDTO'];
interface Props {
docs: TranscriptionQueueItemDTO[];
@@ -18,14 +13,6 @@ interface Props {
let { docs, weeklyCount }: Props = $props();
function formatDate(dateStr: string): string {
return new Intl.DateTimeFormat(getLocale(), {
day: 'numeric',
month: 'short',
year: 'numeric'
}).format(new Date(dateStr + 'T12:00:00'));
}
function reviewedPct(doc: TranscriptionQueueItemDTO): number {
if (doc.annotationCount === 0) return 0;
return Math.round((doc.reviewedBlockCount / doc.annotationCount) * 100);
@@ -61,7 +48,8 @@ function reviewedPct(doc: TranscriptionQueueItemDTO): number {
<span class="font-serif text-sm text-ink">{doc.title}</span>
<div class="mt-0.5 flex items-center gap-2">
{#if doc.documentDate}
<span class="text-xs text-ink-3">{formatDate(doc.documentDate)}</span>
<span class="text-xs text-ink-3">{formatMCDate(doc.documentDate, getLocale())}</span
>
{/if}
{#if doc.textedBlockCount > 0}
<span class="text-xs font-semibold text-ink">

View File

@@ -1,15 +1,10 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages.js';
import { getLocale } from '$lib/paraglide/runtime.js';
import { formatMCDate } from '$lib/utils/date.js';
import type { components } from '$lib/generated/api';
type TranscriptionQueueItemDTO = {
id: string;
title: string;
documentDate?: string;
annotationCount: number;
textedBlockCount: number;
reviewedBlockCount: number;
};
type TranscriptionQueueItemDTO = components['schemas']['TranscriptionQueueItemDTO'];
interface Props {
docs: TranscriptionQueueItemDTO[];
@@ -17,14 +12,6 @@ interface Props {
}
let { docs, weeklyCount }: Props = $props();
function formatDate(dateStr: string): string {
return new Intl.DateTimeFormat(getLocale(), {
day: 'numeric',
month: 'short',
year: 'numeric'
}).format(new Date(dateStr + 'T12:00:00'));
}
</script>
{#if docs.length > 0}
@@ -53,11 +40,19 @@ function formatDate(dateStr: string): string {
>
<span class="font-serif text-sm text-ink">{doc.title}</span>
{#if doc.documentDate}
<span class="mt-0.5 text-xs text-ink-3">{formatDate(doc.documentDate)}</span>
<span class="mt-0.5 text-xs text-ink-3"
>{formatMCDate(doc.documentDate, getLocale())}</span
>
{/if}
</a>
</li>
{/each}
</ul>
</div>
{:else}
<div
class="flex min-h-[120px] flex-col items-center justify-center rounded-sm border border-dashed border-line bg-surface/50 p-6 text-center"
>
<p class="text-xs text-ink-3">{m.mission_control_segmentation_empty()}</p>
</div>
{/if}

View File

@@ -1,15 +1,10 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages.js';
import { getLocale } from '$lib/paraglide/runtime.js';
import { formatMCDate } from '$lib/utils/date.js';
import type { components } from '$lib/generated/api';
type TranscriptionQueueItemDTO = {
id: string;
title: string;
documentDate?: string;
annotationCount: number;
textedBlockCount: number;
reviewedBlockCount: number;
};
type TranscriptionQueueItemDTO = components['schemas']['TranscriptionQueueItemDTO'];
interface Props {
docs: TranscriptionQueueItemDTO[];
@@ -18,14 +13,6 @@ interface Props {
let { docs, weeklyCount }: Props = $props();
function formatDate(dateStr: string): string {
return new Intl.DateTimeFormat(getLocale(), {
day: 'numeric',
month: 'short',
year: 'numeric'
}).format(new Date(dateStr + 'T12:00:00'));
}
function blockProgress(doc: TranscriptionQueueItemDTO): number {
if (doc.annotationCount === 0) return 0;
return (doc.textedBlockCount / doc.annotationCount) * 100;
@@ -58,7 +45,9 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number {
>
<span class="font-serif text-sm text-ink">{doc.title}</span>
{#if doc.documentDate}
<span class="mt-0.5 text-xs text-ink-3">{formatDate(doc.documentDate)}</span>
<span class="mt-0.5 text-xs text-ink-3"
>{formatMCDate(doc.documentDate, getLocale())}</span
>
{/if}
{#if doc.textedBlockCount > 0}
<div class="mt-1.5 flex items-center gap-2">
@@ -83,4 +72,10 @@ function blockProgress(doc: TranscriptionQueueItemDTO): number {
{/each}
</ul>
</div>
{:else}
<div
class="flex min-h-[120px] flex-col items-center justify-center rounded-sm border border-dashed border-line bg-surface/50 p-6 text-center"
>
<p class="text-xs text-ink-3">{m.mission_control_transcription_empty()}</p>
</div>
{/if}

View File

@@ -19,6 +19,19 @@ export function formatDate(isoDate: string, format: 'short' | 'long' = 'long'):
}).format(date);
}
/**
* Format an ISO date string for medium-length display (e.g. "15. Jun. 1920").
* Uses T12:00:00 to avoid UTC timezone off-by-one.
* Pass an explicit BCP 47 locale tag to respect the app locale; defaults to 'de-DE'.
*/
export function formatMCDate(isoDate: string, locale: string = 'de-DE'): string {
return new Intl.DateTimeFormat(locale, {
day: 'numeric',
month: 'short',
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.