feat(admin): surface skipped file count in ImportStatusCard
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m4s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m2s
CI / fail2ban Regex (pull_request) Successful in 39s
CI / Semgrep Security Scan (pull_request) Successful in 18s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m3s

Adds SkippedFile to the local ImportStatus type and updates
ImportStatusCard to show an amber skipped-count section with a
collapsible filename list in the DONE state. Only rendered when
skipped > 0. i18n keys added for de/en/es.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-18 13:14:32 +02:00
parent e124c68cf4
commit 22589e4729
6 changed files with 76 additions and 0 deletions

View File

@@ -350,6 +350,7 @@
"admin_system_import_status_running": "Import läuft…",
"admin_system_import_status_done": "Import abgeschlossen",
"admin_system_import_status_done_label": "Dokumente verarbeitet",
"admin_system_import_skipped_label": "übersprungen",
"admin_system_import_status_failed": "Import fehlgeschlagen",
"admin_system_import_failed_no_spreadsheet": "Keine Tabellendatei gefunden.",
"admin_system_import_failed_internal": "Interner Fehler beim Import.",

View File

@@ -350,6 +350,7 @@
"admin_system_import_status_running": "Import running…",
"admin_system_import_status_done": "Import complete",
"admin_system_import_status_done_label": "Documents processed",
"admin_system_import_skipped_label": "skipped",
"admin_system_import_status_failed": "Import failed",
"admin_system_import_failed_no_spreadsheet": "No spreadsheet file found.",
"admin_system_import_failed_internal": "Import failed due to an internal error.",

View File

@@ -350,6 +350,7 @@
"admin_system_import_status_running": "Importación en curso…",
"admin_system_import_status_done": "Importación completada",
"admin_system_import_status_done_label": "Documentos procesados",
"admin_system_import_skipped_label": "omitidos",
"admin_system_import_status_failed": "Importación fallida",
"admin_system_import_failed_no_spreadsheet": "No se encontró ninguna hoja de cálculo.",
"admin_system_import_failed_internal": "Error interno durante la importación.",

View File

@@ -48,6 +48,21 @@ const failureMessage = $derived(
</p>
<p class="mt-1 text-xs text-green-800">{m.admin_system_import_status_done()}</p>
</div>
{#if importStatus.skipped > 0}
<details class="mb-4 rounded-sm border border-amber-200 bg-amber-50 p-4 text-amber-700">
<summary class="cursor-pointer list-none">
<p data-testid="skipped-count" class="text-base font-bold">{importStatus.skipped}</p>
<p class="font-sans text-xs font-bold tracking-widest text-amber-800 uppercase">
{m.admin_system_import_skipped_label()}
</p>
</summary>
<ul class="mt-3 space-y-1">
{#each importStatus.skippedFiles as { filename, reason } (filename)}
<li class="font-mono text-xs text-ink-2">{filename}{reason}</li>
{/each}
</ul>
</details>
{/if}
<button
data-import-trigger
onclick={ontrigger}

View File

@@ -8,6 +8,8 @@ const makeStatus = (overrides: Partial<ImportStatus> = {}): ImportStatus => ({
state: 'IDLE',
statusCode: 'IMPORT_IDLE',
processed: 0,
skipped: 0,
skippedFiles: [],
startedAt: null,
...overrides
});
@@ -128,4 +130,53 @@ describe('ImportStatusCard', () => {
await getByRole('button').click();
expect(ontrigger).toHaveBeenCalledOnce();
});
it('shows skipped count when DONE and skipped > 0', async () => {
const { getByTestId } = render(ImportStatusCard, {
props: {
importStatus: makeStatus({
state: 'DONE',
statusCode: 'IMPORT_DONE',
processed: 10,
skipped: 3,
skippedFiles: [
{ filename: 'fake.pdf', reason: 'Keine gültige PDF-Signatur' },
{ filename: 'other.pdf', reason: 'Keine gültige PDF-Signatur' },
{ filename: 'tiny.pdf', reason: 'Keine gültige PDF-Signatur' }
]
}),
ontrigger: () => {}
}
});
await expect.element(getByTestId('skipped-count')).toHaveTextContent('3');
});
it('shows skipped filenames in collapsible list when DONE and skipped > 0', async () => {
const { getByText } = render(ImportStatusCard, {
props: {
importStatus: makeStatus({
state: 'DONE',
statusCode: 'IMPORT_DONE',
processed: 5,
skipped: 1,
skippedFiles: [{ filename: 'fake.pdf', reason: 'Keine gültige PDF-Signatur' }]
}),
ontrigger: () => {}
}
});
await expect.element(getByText('fake.pdf')).toBeInTheDocument();
});
it('does not show skipped section when DONE and skipped is 0', async () => {
const { getByTestId } = render(ImportStatusCard, {
props: {
importStatus: makeStatus({ state: 'DONE', statusCode: 'IMPORT_DONE', processed: 5 }),
ontrigger: () => {}
}
});
await expect.element(getByTestId('skipped-count')).not.toBeInTheDocument();
});
});

View File

@@ -1,6 +1,13 @@
export type SkippedFile = {
filename: string;
reason: string;
};
export type ImportStatus = {
state: 'IDLE' | 'RUNNING' | 'DONE' | 'FAILED';
statusCode: string;
processed: number;
skipped: number;
skippedFiles: SkippedFile[];
startedAt: string | null;
};