This commit is contained in:
@@ -0,0 +1,332 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
export type ZipProcessingJobStatus = 'processing' | 'complete' | 'error';
|
||||||
|
|
||||||
|
export type ZipProcessingJob = {
|
||||||
|
id: string;
|
||||||
|
file: File;
|
||||||
|
status: ZipProcessingJobStatus;
|
||||||
|
archiveCount?: number;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Check, CircleDashed, TriangleAlert } from '@lucide/svelte';
|
||||||
|
import Button from '$lib/components/ui/button.svelte';
|
||||||
|
import * as Card from '$lib/components/ui/card/index';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
jobs: ZipProcessingJob[];
|
||||||
|
compact?: boolean;
|
||||||
|
onRetry: (jobId: string) => void;
|
||||||
|
onRemove: (jobId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { jobs, compact = false, onRetry, onRemove }: Props = $props();
|
||||||
|
|
||||||
|
let processingCount = $derived(jobs.filter((job) => job.status === 'processing').length);
|
||||||
|
let completedCount = $derived(jobs.filter((job) => job.status === 'complete').length);
|
||||||
|
let failedJobs = $derived(jobs.filter((job) => job.status === 'error'));
|
||||||
|
let completionPercent = $derived(jobs.length === 0 ? 0 : (completedCount / jobs.length) * 100);
|
||||||
|
|
||||||
|
const formatFileSize = (size: number) => {
|
||||||
|
if (size < 1024 * 1024) return `${Math.max(1, Math.round(size / 1024))} KB`;
|
||||||
|
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const archiveCountLabel = (count: number | undefined) => {
|
||||||
|
if (count === undefined) return 'Bereit';
|
||||||
|
return `${count} Archiv${count === 1 ? '' : 'e'} bereit`;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if compact}
|
||||||
|
<Card.Root size="sm" class="border-l-4 border-l-secondary">
|
||||||
|
<p class="sr-only" role="status" aria-live="polite">
|
||||||
|
{completedCount} von {jobs.length} Dateien bereit, {processingCount} in Bearbeitung,
|
||||||
|
{failedJobs.length} fehlgeschlagen.
|
||||||
|
</p>
|
||||||
|
<Card.Content class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div class="flex min-w-0 items-center gap-3">
|
||||||
|
<div class="relative grid size-10 shrink-0 place-items-center" aria-hidden="true">
|
||||||
|
<div class="compact-folder"></div>
|
||||||
|
{#if processingCount > 0}
|
||||||
|
<CircleDashed
|
||||||
|
class="absolute -right-0.5 -bottom-0.5 size-4 animate-spin text-secondary"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<TriangleAlert class="absolute -right-0.5 -bottom-0.5 size-4 text-destructive" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="font-bold text-primary-900">
|
||||||
|
{#if processingCount > 0}
|
||||||
|
{processingCount} ZIP-Datei{processingCount === 1 ? '' : 'en'}
|
||||||
|
{processingCount === 1 ? ' wird' : ' werden'} noch geöffnet
|
||||||
|
{:else}
|
||||||
|
{failedJobs.length} ZIP-Datei{failedJobs.length === 1 ? '' : 'en'} konnte{failedJobs.length ===
|
||||||
|
1
|
||||||
|
? ''
|
||||||
|
: 'n'} nicht geöffnet werden
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
<p class="truncate text-[13px] text-primary-700">
|
||||||
|
{jobs
|
||||||
|
.filter((job) =>
|
||||||
|
processingCount > 0 ? job.status === 'processing' : job.status === 'error'
|
||||||
|
)
|
||||||
|
.map((job) => job.file.name)
|
||||||
|
.join(', ')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="shrink-0 text-[12px] font-medium text-primary-700">
|
||||||
|
Verarbeitung nur in Ihrem Browser
|
||||||
|
</p>
|
||||||
|
</Card.Content>
|
||||||
|
|
||||||
|
{#if failedJobs.length > 0}
|
||||||
|
<Card.Footer class="flex flex-col items-stretch gap-2">
|
||||||
|
{#each failedJobs as job (job.id)}
|
||||||
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p class="min-w-0 text-[13px] font-medium text-destructive">
|
||||||
|
<span class="font-bold">{job.file.name}</span> konnte nicht geöffnet werden.
|
||||||
|
</p>
|
||||||
|
<div class="flex shrink-0 gap-1">
|
||||||
|
<Button variant="tertiary" class="h-8 min-w-0 px-3" onclick={() => onRetry(job.id)}
|
||||||
|
>Erneut versuchen</Button
|
||||||
|
>
|
||||||
|
<Button variant="tertiary" class="h-8 min-w-0 px-3" onclick={() => onRemove(job.id)}
|
||||||
|
>Entfernen</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</Card.Footer>
|
||||||
|
{/if}
|
||||||
|
</Card.Root>
|
||||||
|
{:else}
|
||||||
|
<Card.Root class="mx-auto w-full max-w-2xl shadow-sm">
|
||||||
|
<p class="sr-only" role="status" aria-live="polite">
|
||||||
|
{completedCount} von {jobs.length} Dateien bereit, {processingCount} in Bearbeitung,
|
||||||
|
{failedJobs.length} fehlgeschlagen.
|
||||||
|
</p>
|
||||||
|
<Card.Header class="place-items-center px-6 pt-4 text-center sm:px-10">
|
||||||
|
<div class="case-intake" aria-hidden="true">
|
||||||
|
<div class="folder-back"></div>
|
||||||
|
<div class="document-sheet">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="folder-front"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card.Title class="mt-3 text-[22px] font-extrabold text-primary-900">
|
||||||
|
{#if processingCount > 0}
|
||||||
|
beA-Archive werden vorbereitet
|
||||||
|
{:else if failedJobs.length > 0 && completedCount === 0}
|
||||||
|
Archive konnten nicht vorbereitet werden
|
||||||
|
{:else}
|
||||||
|
Archive sind bereit
|
||||||
|
{/if}
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description class="max-w-md font-medium text-primary-700">
|
||||||
|
Die Dateien werden ausschließlich in Ihrem Browser geöffnet und nicht hochgeladen.
|
||||||
|
</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
|
||||||
|
<Card.Content class="flex flex-col gap-4 px-6 sm:px-10">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex items-center justify-between gap-4 text-[13px] font-bold text-primary-900">
|
||||||
|
<span>{completedCount} von {jobs.length} Dateien bereit</span>
|
||||||
|
{#if processingCount > 0}
|
||||||
|
<span class="font-medium text-primary-700">Bitte kurz warten</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="h-1.5 overflow-hidden rounded-full bg-primary-100"
|
||||||
|
role="progressbar"
|
||||||
|
aria-label="Vorbereitete ZIP-Dateien"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax={jobs.length}
|
||||||
|
aria-valuenow={completedCount}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full bg-success transition-[width] duration-500"
|
||||||
|
style:width={`${completionPercent}%`}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="flex flex-col gap-2" aria-label="Status der ZIP-Dateien">
|
||||||
|
{#each jobs as job (job.id)}
|
||||||
|
<li
|
||||||
|
class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-3 rounded-xl border bg-background/55 px-3 py-3 sm:grid-cols-[auto_minmax(0,1fr)_auto]"
|
||||||
|
>
|
||||||
|
<div class="grid size-8 place-items-center rounded-full bg-card" aria-hidden="true">
|
||||||
|
{#if job.status === 'processing'}
|
||||||
|
<CircleDashed class="size-5 animate-spin text-secondary" />
|
||||||
|
{:else if job.status === 'complete'}
|
||||||
|
<Check class="size-5 text-success" />
|
||||||
|
{:else}
|
||||||
|
<TriangleAlert class="size-5 text-destructive" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-0">
|
||||||
|
<p class="truncate text-[14px] font-bold text-primary-900">{job.file.name}</p>
|
||||||
|
<p class="job-meta text-[12px] text-primary-700">{formatFileSize(job.file.size)}</p>
|
||||||
|
{#if job.status === 'error'}
|
||||||
|
<p class="mt-1 text-[12px] leading-snug text-destructive">
|
||||||
|
<span class="font-bold">Konnte nicht geöffnet werden.</span>
|
||||||
|
<span class="block">
|
||||||
|
{job.error ?? 'Prüfen Sie, ob die Datei ein gültiges beA-ZIP-Archiv ist.'}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="col-start-2 flex flex-wrap items-center gap-1 sm:col-start-3 sm:row-start-1"
|
||||||
|
>
|
||||||
|
{#if job.status === 'processing'}
|
||||||
|
<span class="text-[12px] font-bold text-secondary">Wird geöffnet</span>
|
||||||
|
{:else if job.status === 'complete'}
|
||||||
|
<span class="text-[12px] font-bold text-success">
|
||||||
|
{archiveCountLabel(job.archiveCount)}
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<Button variant="tertiary" class="h-8 min-w-0 px-3" onclick={() => onRetry(job.id)}
|
||||||
|
>Erneut versuchen</Button
|
||||||
|
>
|
||||||
|
<Button variant="tertiary" class="h-8 min-w-0 px-3" onclick={() => onRemove(job.id)}
|
||||||
|
>Entfernen</Button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.job-meta {
|
||||||
|
font-family: 'Geist Variable', ui-monospace, monospace;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.case-intake {
|
||||||
|
position: relative;
|
||||||
|
width: 7rem;
|
||||||
|
height: 5.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-back,
|
||||||
|
.folder-front,
|
||||||
|
.compact-folder {
|
||||||
|
position: absolute;
|
||||||
|
background: color-mix(in srgb, var(--primary) 12%, white);
|
||||||
|
border: 2px solid var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-back {
|
||||||
|
inset: 1.55rem 0 0;
|
||||||
|
border-radius: 0.65rem 0.65rem 0.8rem 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-back::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -2px;
|
||||||
|
top: -0.8rem;
|
||||||
|
width: 3.4rem;
|
||||||
|
height: 1rem;
|
||||||
|
border: 2px solid var(--primary);
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: 0.55rem 0.55rem 0 0;
|
||||||
|
background: color-mix(in srgb, var(--primary) 12%, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-sheet {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
left: 50%;
|
||||||
|
top: 0.2rem;
|
||||||
|
display: flex;
|
||||||
|
width: 3.7rem;
|
||||||
|
height: 4.6rem;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.42rem;
|
||||||
|
border: 2px solid var(--primary);
|
||||||
|
border-radius: 0.35rem;
|
||||||
|
background: var(--card);
|
||||||
|
padding: 0.8rem 0.65rem;
|
||||||
|
box-shadow: 0 0.4rem 1rem rgb(18 47 98 / 12%);
|
||||||
|
animation: file-intake 1.8s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-sheet span {
|
||||||
|
display: block;
|
||||||
|
height: 0.18rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: color-mix(in srgb, var(--primary) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-sheet span:nth-child(2) {
|
||||||
|
width: 72%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-sheet span:nth-child(3) {
|
||||||
|
width: 88%;
|
||||||
|
background: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-front {
|
||||||
|
z-index: 2;
|
||||||
|
inset: 2.7rem 0 0;
|
||||||
|
border-radius: 0.45rem 0.45rem 0.8rem 0.8rem;
|
||||||
|
background: color-mix(in srgb, var(--primary) 18%, white);
|
||||||
|
transform: perspective(8rem) rotateX(-5deg);
|
||||||
|
transform-origin: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-folder {
|
||||||
|
inset: 0.65rem 0.2rem 0.25rem;
|
||||||
|
border-radius: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-folder::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -2px;
|
||||||
|
top: -0.4rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border: 2px solid var(--primary);
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: 0.25rem 0.25rem 0 0;
|
||||||
|
background: color-mix(in srgb, var(--primary) 12%, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes file-intake {
|
||||||
|
0%,
|
||||||
|
18% {
|
||||||
|
transform: translate(-50%, -0.45rem);
|
||||||
|
}
|
||||||
|
55%,
|
||||||
|
100% {
|
||||||
|
transform: translate(-50%, 0.65rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.document-sheet,
|
||||||
|
:global(.animate-spin) {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
--accent: #d6dce5;
|
--accent: #d6dce5;
|
||||||
--accent-foreground: oklch(0.205 0 0);
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
--destructive: #ff0027;
|
--destructive: #ff0027;
|
||||||
|
--success: #4e8a72;
|
||||||
--border: oklch(0.922 0 0);
|
--border: oklch(0.922 0 0);
|
||||||
--input: oklch(0.922 0 0);
|
--input: oklch(0.922 0 0);
|
||||||
--ring: oklch(0.708 0 0);
|
--ring: oklch(0.708 0 0);
|
||||||
@@ -68,6 +69,7 @@
|
|||||||
--accent: oklch(0.269 0 0);
|
--accent: oklch(0.269 0 0);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: #ff0027;
|
--destructive: #ff0027;
|
||||||
|
--success: #79c4a3;
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.556 0 0);
|
||||||
@@ -105,6 +107,7 @@
|
|||||||
--color-input: var(--input);
|
--color-input: var(--input);
|
||||||
--color-border: var(--border);
|
--color-border: var(--border);
|
||||||
--color-destructive: var(--destructive);
|
--color-destructive: var(--destructive);
|
||||||
|
--color-success: var(--success);
|
||||||
--color-accent-foreground: var(--accent-foreground);
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
--color-accent: var(--accent);
|
--color-accent: var(--accent);
|
||||||
--color-muted-foreground: var(--muted-foreground);
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
import Button from '$lib/components/ui/button.svelte';
|
import Button from '$lib/components/ui/button.svelte';
|
||||||
import ProcessedZipArchiveEditor from '$lib/components/ProcessedZipArchiveEditor.svelte';
|
import ProcessedZipArchiveEditor from '$lib/components/ProcessedZipArchiveEditor.svelte';
|
||||||
import ZipDropzone from '$lib/components/ZipDropzone.svelte';
|
import ZipDropzone from '$lib/components/ZipDropzone.svelte';
|
||||||
|
import BeaArchiveProcessing, {
|
||||||
|
type ZipProcessingJob
|
||||||
|
} from '$lib/components/BeaArchiveProcessing.svelte';
|
||||||
import {
|
import {
|
||||||
extractZipArchives,
|
extractZipArchives,
|
||||||
mergeProcessedZipArchive,
|
mergeProcessedZipArchive,
|
||||||
@@ -16,7 +19,9 @@
|
|||||||
|
|
||||||
let selectedZipFiles: File[] = $state([]);
|
let selectedZipFiles: File[] = $state([]);
|
||||||
let processedZipFiles: ProcessedZipArchive[] = $state([]);
|
let processedZipFiles: ProcessedZipArchive[] = $state([]);
|
||||||
let pendingZipCount = $state(0);
|
let zipJobs: ZipProcessingJob[] = $state([]);
|
||||||
|
let pendingZipCount = $derived(zipJobs.filter((job) => job.status === 'processing').length);
|
||||||
|
let failedZipCount = $derived(zipJobs.filter((job) => job.status === 'error').length);
|
||||||
let thumbnailWidth = $state(200);
|
let thumbnailWidth = $state(200);
|
||||||
let thumbnailWidthHydrated = $state(false);
|
let thumbnailWidthHydrated = $state(false);
|
||||||
let archiveGeneration = 0;
|
let archiveGeneration = 0;
|
||||||
@@ -48,34 +53,67 @@
|
|||||||
localStorage.setItem(THUMBNAIL_WIDTH_STORAGE_KEY, String(thumbnailWidth));
|
localStorage.setItem(THUMBNAIL_WIDTH_STORAGE_KEY, String(thumbnailWidth));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const updateZipJob = (jobId: string, update: Partial<ZipProcessingJob>) => {
|
||||||
|
zipJobs = zipJobs.map((job) => (job.id === jobId ? { ...job, ...update } : job));
|
||||||
|
};
|
||||||
|
|
||||||
const handleFilesSelected = (files: File[]) => {
|
const handleFilesSelected = (files: File[]) => {
|
||||||
selectedZipFiles = [...selectedZipFiles, ...files];
|
selectedZipFiles = [...selectedZipFiles, ...files];
|
||||||
|
|
||||||
for (const file of files) {
|
const jobs = files.map((file) => ({
|
||||||
void processZipFile(file, archiveGeneration);
|
id: crypto.randomUUID(),
|
||||||
|
file,
|
||||||
|
status: 'processing' as const
|
||||||
|
}));
|
||||||
|
zipJobs = [...zipJobs, ...jobs];
|
||||||
|
|
||||||
|
for (const job of jobs) {
|
||||||
|
void processZipFile(job.id, job.file, archiveGeneration);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const processZipFile = async (file: File, generation: number) => {
|
const processZipFile = async (jobId: string, file: File, generation: number) => {
|
||||||
pendingZipCount += 1;
|
updateZipJob(jobId, { status: 'processing', error: undefined, archiveCount: undefined });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const processedZipArchives = await extractZipArchives(file);
|
const processedZipArchives = await extractZipArchives(file);
|
||||||
|
|
||||||
if (generation !== archiveGeneration) {
|
if (generation !== archiveGeneration || !zipJobs.some((job) => job.id === jobId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
processedZipFiles = [...processedZipFiles, ...processedZipArchives];
|
processedZipFiles = [...processedZipFiles, ...processedZipArchives];
|
||||||
|
updateZipJob(jobId, {
|
||||||
|
status: 'complete',
|
||||||
|
archiveCount: processedZipArchives.length
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to process ${file.name}`, error);
|
console.error(`Failed to process ${file.name}`, error);
|
||||||
} finally {
|
|
||||||
if (generation === archiveGeneration) {
|
if (generation === archiveGeneration && zipJobs.some((job) => job.id === jobId)) {
|
||||||
pendingZipCount -= 1;
|
updateZipJob(jobId, {
|
||||||
|
status: 'error',
|
||||||
|
error: 'Prüfen Sie, ob die Datei ein gültiges beA-ZIP-Archiv ist.'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const retryZipFile = (jobId: string) => {
|
||||||
|
const job = zipJobs.find((candidate) => candidate.id === jobId);
|
||||||
|
if (!job || job.status === 'processing') return;
|
||||||
|
|
||||||
|
void processZipFile(job.id, job.file, archiveGeneration);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeZipFile = (jobId: string) => {
|
||||||
|
const job = zipJobs.find((candidate) => candidate.id === jobId);
|
||||||
|
if (!job || job.status === 'processing') return;
|
||||||
|
|
||||||
|
zipJobs = zipJobs.filter((candidate) => candidate.id !== jobId);
|
||||||
|
selectedZipFiles = selectedZipFiles.filter((file) => file !== job.file);
|
||||||
|
};
|
||||||
|
|
||||||
const updateProcessedZipFile = (index: number, archive: ProcessedZipArchive) => {
|
const updateProcessedZipFile = (index: number, archive: ProcessedZipArchive) => {
|
||||||
processedZipFiles = processedZipFiles.map((existingArchive, existingIndex) =>
|
processedZipFiles = processedZipFiles.map((existingArchive, existingIndex) =>
|
||||||
existingIndex === index ? archive : existingArchive
|
existingIndex === index ? archive : existingArchive
|
||||||
@@ -97,7 +135,7 @@
|
|||||||
archiveGeneration += 1;
|
archiveGeneration += 1;
|
||||||
selectedZipFiles = [];
|
selectedZipFiles = [];
|
||||||
processedZipFiles = [];
|
processedZipFiles = [];
|
||||||
pendingZipCount = 0;
|
zipJobs = [];
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -166,12 +204,15 @@
|
|||||||
class="flex min-h-[calc(-180px+100vh)] w-full grow flex-col justify-center gap-4 px-4 py-6"
|
class="flex min-h-[calc(-180px+100vh)] w-full grow flex-col justify-center gap-4 px-4 py-6"
|
||||||
>
|
>
|
||||||
<div class="mx-auto flex w-full flex-col gap-3">
|
<div class="mx-auto flex w-full flex-col gap-3">
|
||||||
{#if pendingZipCount > 0}
|
{#if processedZipFiles.length === 0}
|
||||||
<div
|
<BeaArchiveProcessing jobs={zipJobs} onRetry={retryZipFile} onRemove={removeZipFile} />
|
||||||
class="rounded-container bg-primary-50 px-4 py-3 text-[14px] font-medium text-primary-900"
|
{:else if pendingZipCount > 0 || failedZipCount > 0}
|
||||||
>
|
<BeaArchiveProcessing
|
||||||
Noch {pendingZipCount} ZIP-Datei{pendingZipCount === 1 ? '' : 'en'} in Bearbeitung.
|
jobs={zipJobs}
|
||||||
</div>
|
compact
|
||||||
|
onRetry={retryZipFile}
|
||||||
|
onRemove={removeZipFile}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if processedZipFiles.length > 0}
|
{#if processedZipFiles.length > 0}
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ const trackPageErrors = (page: Page) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pdfDropzone = (page: Page) => page.getByRole('button', { name: 'PDF-Dateien hinzufügen' });
|
const pdfDropzone = (page: Page) => page.getByRole('button', { name: 'PDF-Dateien hinzufügen' });
|
||||||
|
const zipDropzone = (page: Page) =>
|
||||||
|
page.getByRole('button', { name: 'beA-ZIP-Dateien hinzufügen' });
|
||||||
|
|
||||||
test('start page exposes only working tools and forwards a dropped ZIP to beA', async ({
|
test('start page exposes only working tools and forwards a dropped ZIP to beA', async ({
|
||||||
page
|
page
|
||||||
@@ -97,6 +99,28 @@ test('start page exposes only working tools and forwards a dropped ZIP to beA',
|
|||||||
expect(pageErrors).toEqual([]);
|
expect(pageErrors).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('beA reports an invalid ZIP and lets the user remove it', async ({ page }) => {
|
||||||
|
const pageErrors = trackPageErrors(page);
|
||||||
|
|
||||||
|
await page.goto('/tools/bea');
|
||||||
|
await dropFiles(page, zipDropzone(page), [
|
||||||
|
{
|
||||||
|
name: 'defektes-archiv.zip',
|
||||||
|
mimeType: 'application/zip',
|
||||||
|
buffer: Buffer.from('kein gueltiges ZIP-Archiv')
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(page.getByText('defektes-archiv.zip', { exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByText('Konnte nicht geöffnet werden.')).toBeVisible();
|
||||||
|
await expect(page.getByRole('status')).toContainText('1 fehlgeschlagen');
|
||||||
|
await expect(page.getByRole('button', { name: 'Erneut versuchen' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Entfernen' }).click();
|
||||||
|
await expect(zipDropzone(page)).toBeVisible();
|
||||||
|
expect(pageErrors).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
test('short tool pages keep the footer flush with the viewport bottom', async ({ page }) => {
|
test('short tool pages keep the footer flush with the viewport bottom', async ({ page }) => {
|
||||||
const expectFooterAtViewportBottom = async (viewport: { width: number; height: number }) => {
|
const expectFooterAtViewportBottom = async (viewport: { width: number; height: number }) => {
|
||||||
await page.setViewportSize(viewport);
|
await page.setViewportSize(viewport);
|
||||||
|
|||||||
Reference in New Issue
Block a user