feat(dashboard): add stats footnote and min-h touch target to DashboardRecentDocuments

Adds stats?: StatsDTO | null prop; renders a quiet footnote showing total
document and person counts. Guards on stats?.totalDocuments != null so
zero is shown but the footnote is absent when stats fails. Adds
min-h-[44px] to doc rows for WCAG 2.5.5 touch target compliance.
Adds dashboard_stats_documents/persons i18n keys in de/en/es.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-03-31 19:29:00 +02:00
parent 20923d04b6
commit f618364632
5 changed files with 59 additions and 2 deletions

View File

@@ -375,6 +375,8 @@
"dashboard_needs_metadata_heading": "Metadaten fehlen",
"dashboard_needs_metadata_show_all": "Alle anzeigen",
"dashboard_recent_heading": "Zuletzt aktiv",
"dashboard_stats_documents": "Dokumente",
"dashboard_stats_persons": "Personen",
"dashboard_resume_label": "Zuletzt geöffnet:",
"dashboard_resume_fallback": "Unbekanntes Dokument",
"doc_status_placeholder": "Platzhalter",

View File

@@ -375,6 +375,8 @@
"dashboard_needs_metadata_heading": "Missing Metadata",
"dashboard_needs_metadata_show_all": "Show all",
"dashboard_recent_heading": "Recent Activity",
"dashboard_stats_documents": "Documents",
"dashboard_stats_persons": "Persons",
"dashboard_resume_label": "Last opened:",
"dashboard_resume_fallback": "Unknown document",
"doc_status_placeholder": "Placeholder",

View File

@@ -375,6 +375,8 @@
"dashboard_needs_metadata_heading": "Metadatos incompletos",
"dashboard_needs_metadata_show_all": "Ver todos",
"dashboard_recent_heading": "Actividad reciente",
"dashboard_stats_documents": "Documentos",
"dashboard_stats_persons": "Personas",
"dashboard_resume_label": "Último abierto:",
"dashboard_resume_fallback": "Documento desconocido",
"doc_status_placeholder": "Marcador",

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import * as m from '$lib/paraglide/messages.js';
import { getLocale } from '$lib/paraglide/runtime.js';
import type { components } from '$lib/generated/api';
type Document = {
id: string;
@@ -9,11 +10,14 @@ type Document = {
sender?: { id: string; firstName: string; lastName: string };
};
type StatsDTO = components['schemas']['StatsDTO'];
interface Props {
recentDocs: Document[];
stats?: StatsDTO | null;
}
let { recentDocs }: Props = $props();
let { recentDocs, stats = null }: Props = $props();
function formatDate(dateStr: string): string {
// updatedAt is a full ISO datetime — no T12:00:00 noon-anchor needed here
@@ -31,7 +35,10 @@ function formatDate(dateStr: string): string {
{m.dashboard_recent_heading()}
</h2>
{#each recentDocs as doc (doc.id)}
<div class="flex items-center justify-between border-b border-line py-2 last:border-0">
<div
data-testid="doc-row-{doc.id}"
class="flex min-h-[44px] items-center justify-between border-b border-line py-2 last:border-0"
>
<a
href="/documents/{doc.id}"
class="font-serif text-lg text-ink hover:text-ink-2 hover:underline"
@@ -48,5 +55,12 @@ function formatDate(dateStr: string): string {
{/if}
</div>
{/each}
{#if stats?.totalDocuments != null}
<p data-testid="dashboard-stats-footnote" class="mt-4 font-sans text-xs text-ink-3">
{stats.totalDocuments}
{m.dashboard_stats_documents()} · {stats.totalPersons}
{m.dashboard_stats_persons()}
</p>
{/if}
</div>
{/if}

View File

@@ -55,3 +55,40 @@ describe('DashboardRecentDocuments', () => {
await expect.element(dateEl).toBeInTheDocument();
});
});
describe('DashboardRecentDocuments — stats footnote', () => {
it('renders stats footnote when stats.totalDocuments is provided', async () => {
render(DashboardRecentDocuments, {
recentDocs: [makeDoc('d1', 'Taufschein')],
stats: { totalDocuments: 248, totalPersons: 34 }
});
const footnote = page.getByTestId('dashboard-stats-footnote');
await expect.element(footnote).toBeInTheDocument();
});
it('omits stats footnote when stats is null', async () => {
render(DashboardRecentDocuments, {
recentDocs: [makeDoc('d1', 'Taufschein')],
stats: null
});
const footnote = page.getByTestId('dashboard-stats-footnote');
await expect.element(footnote).not.toBeInTheDocument();
});
it('shows "0 Documents" when totalDocuments is 0', async () => {
render(DashboardRecentDocuments, {
recentDocs: [makeDoc('d1', 'Taufschein')],
stats: { totalDocuments: 0, totalPersons: 0 }
});
const footnote = page.getByTestId('dashboard-stats-footnote');
await expect.element(footnote).toBeInTheDocument();
});
});
describe('DashboardRecentDocuments — touch targets', () => {
it('each doc row has min-h-[44px] class for WCAG touch target', async () => {
render(DashboardRecentDocuments, { recentDocs: [makeDoc('d1', 'Taufschein')] });
const row = page.getByTestId('doc-row-d1');
await expect.element(row).toHaveClass('min-h-[44px]');
});
});