'use client' import { useCallback, useRef, useState, useTransition } from 'react' import { attachmentText } from '@/lib/i18n/documents' import { MAX_FILE_BYTES, formatBytes } from '@/lib/files/limits' import { releaseAttachmentAction } from '@/app/(app)/dateien/actions' import { useScanEvents, type ScanEvent } from './useScanEvents' export type AttachmentView = { id: string originalName: string mimeType: string sizeBytes: number scanStatus: string scanMessage: string | null blurhash: string | null width: number | null height: number | null hasThumb: boolean createdAt: string } /** Welchem Besitzer die Datei angehängt wird. Genau eines der drei Felder. */ export type UploadTarget = { caseNumber?: string entryId?: string documentNumber?: string } type Laufend = { key: string; name: string; progress: number; error?: string } /** * Anhänge einer Akte, eines Eintrags oder eines Dokuments. * * Der Ablauf folgt docs/09-performance-und-ux.md: die Datei erscheint sofort * in der Liste, der Fortschrittsbalken zeigt nur die **Übertragung**, und * danach steht „Wird geprüft", bis das Ereignis aus `/api/events` kommt. Kein * Balken wartet auf einen Virenscanner, und kein Spinner blockiert die Seite. */ export function Attachments({ target, initial, canUpload, canRelease, }: { target: UploadTarget initial: AttachmentView[] canUpload: boolean canRelease: boolean }) { const [dateien, setDateien] = useState(initial) const [laufend, setLaufend] = useState([]) const [pending, startTransition] = useTransition() const eingabe = useRef(null) const uebernehmen = useCallback((event: ScanEvent) => { setDateien((bisher) => bisher.map((datei) => datei.id === event.id ? { ...datei, scanStatus: event.scanStatus, scanMessage: event.scanMessage, mimeType: event.mimeType, sizeBytes: event.sizeBytes, blurhash: event.blurhash, width: event.width, height: event.height, hasThumb: event.hasThumb, } : datei, ), ) }, []) useScanEvents(uebernehmen) function hochladen(datei: File) { const key = `${datei.name}-${Date.now()}-${Math.random()}` if (datei.size > MAX_FILE_BYTES) { setLaufend((l) => [ ...l, { key, name: datei.name, progress: 0, error: attachmentText.errors['file-too-large'] }, ]) return } setLaufend((l) => [...l, { key, name: datei.name, progress: 0 }]) const form = new FormData() form.set('file', datei) for (const [feld, wert] of Object.entries(target)) { if (wert) form.set(feld, wert) } // XMLHttpRequest und nicht fetch: nur so gibt es einen echten // Fortschritt beim Senden. `fetch` meldet ihn bis heute nicht. const anfrage = new XMLHttpRequest() anfrage.open('POST', '/api/files') anfrage.upload.addEventListener('progress', (event) => { if (!event.lengthComputable) return const anteil = Math.round((event.loaded / event.total) * 100) setLaufend((l) => l.map((e) => (e.key === key ? { ...e, progress: anteil } : e))) }) anfrage.addEventListener('load', () => { const fertig = () => setLaufend((l) => l.filter((e) => e.key !== key)) if (anfrage.status === 201) { const { id } = JSON.parse(anfrage.responseText) as { id: string } setDateien((bisher) => [ ...bisher, { id, originalName: datei.name, mimeType: datei.type || 'application/octet-stream', sizeBytes: datei.size, scanStatus: 'PENDING', scanMessage: null, blurhash: null, width: null, height: null, hasThumb: false, createdAt: new Date().toISOString(), }, ]) fertig() return } let meldung = attachmentText.errors.transfer! try { meldung = (JSON.parse(anfrage.responseText) as { error?: string }).error ?? meldung } catch { // Antwort ohne JSON, dann bleibt die allgemeine Meldung. } setLaufend((l) => l.map((e) => (e.key === key ? { ...e, error: meldung } : e))) }) anfrage.addEventListener('error', () => { setLaufend((l) => l.map((e) => (e.key === key ? { ...e, error: attachmentText.errors.transfer! } : e)), ) }) anfrage.send(form) } return (
{dateien.length === 0 && laufend.length === 0 ? (

{attachmentText.empty}

) : (
    {dateien.map((datei) => (
  • {datei.hasThumb ? ( // eslint-disable-next-line @next/next/no-img-element ) : null} {datei.originalName} {formatBytes(datei.sizeBytes)} {attachmentText.statusNames[datei.scanStatus] ?? datei.scanStatus} {datei.scanStatus === 'CLEAN' ? ( {attachmentText.download} ) : null} {datei.scanStatus === 'FAILED' && canRelease ? ( ) : null}
    {datei.scanMessage ? (

    {datei.scanMessage}

    ) : null}
  • ))} {laufend.map((eintrag) => (
  • {eintrag.name} {eintrag.error ?? `${attachmentText.uploading} ${eintrag.progress} %`}
    {!eintrag.error ? (
    ) : null}
  • ))}
)} {canUpload ? (
{ for (const datei of event.target.files ?? []) hochladen(datei) event.target.value = '' }} className="hidden" />

{attachmentText.allowed}

) : null}
) }