'use client' import { useCallback, useRef, useState } from 'react' import { attachmentText, documentText } from '@/lib/i18n/documents' import { MAX_FILE_BYTES } from '@/lib/files/limits' import { useStream, type ScanEvent } from '@/components/realtime/useStream' import type { SaveSection } from '@/components/sections/types' /** * Das Lichtbild im Aktenblatt. * * Der Wert des Feldes ist die **Kennung eines geprüften Anhangs**, nie das * Bild selbst. Ein Bild als Data-URL im Abschnittswert liefe an der ganzen * Prüfkette vorbei und wäre nachträglich kaum wieder herauszubekommen. * Siehe docs/07-dateien-und-sicherheit.md. * * Deshalb zwei Schritte hintereinander: hochladen, dann die Kennung in den * Abschnitt schreiben. Angezeigt wird erst, wenn die Prüfung durch ist. */ export function PhotoField({ save, fieldKey, label, documentNumber, topic, initialAttachmentId, initialUrl, initialVersion, readOnly, }: { save: SaveSection fieldKey: string label: string documentNumber: string /** Thema des Besitzers, damit das Prüfergebnis ankommt. */ topic: string initialAttachmentId: string | null initialUrl: string | null initialVersion: number readOnly: boolean }) { const [url, setUrl] = useState(initialUrl) const [version, setVersion] = useState(initialVersion) const [wartetAuf, setWartetAuf] = useState( initialAttachmentId && !initialUrl ? initialAttachmentId : null, ) const [fortschritt, setFortschritt] = useState(null) const [fehler, setFehler] = useState(null) const eingabe = useRef(null) const geprueft = useCallback( (event: ScanEvent) => { if (event.id !== wartetAuf) return if (event.scanStatus === 'CLEAN') { setUrl(`/api/files/${event.id}?variant=thumb`) setWartetAuf(null) return } if (event.scanStatus !== 'PENDING') { setFehler(event.scanMessage ?? attachmentText.statusNames[event.scanStatus]!) setWartetAuf(null) } }, [wartetAuf], ) useStream([topic], { 'attachment.scanned': (data) => geprueft(data as ScanEvent) }) function waehlen(datei: File) { setFehler(null) if (datei.size > MAX_FILE_BYTES) { setFehler(attachmentText.errors['file-too-large']!) return } const form = new FormData() form.set('file', datei) form.set('documentNumber', documentNumber) const anfrage = new XMLHttpRequest() anfrage.open('POST', '/api/files') setFortschritt(0) anfrage.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { setFortschritt(Math.round((event.loaded / event.total) * 100)) } }) anfrage.addEventListener('load', async () => { setFortschritt(null) if (anfrage.status !== 201) { try { setFehler( (JSON.parse(anfrage.responseText) as { error?: string }).error ?? attachmentText.errors.transfer!, ) } catch { setFehler(attachmentText.errors.transfer!) } return } const { id } = JSON.parse(anfrage.responseText) as { id: string } // Die Kennung sofort in den Abschnitt schreiben, auch wenn die Prüfung // noch läuft: sonst ginge die Zuordnung verloren, wenn der Nutzer die // Seite verlässt. const ergebnis = await save({ fieldKey, value: id, baseVersion: version }) if (ergebnis.ok) { setVersion(ergebnis.version) setWartetAuf(id) setUrl(null) } else if (!ergebnis.conflict) { setFehler(ergebnis.error) } }) anfrage.addEventListener('error', () => { setFortschritt(null) setFehler(attachmentText.errors.transfer!) }) anfrage.send(form) } return (

{label}

{url ? ( // eslint-disable-next-line @next/next/no-img-element {label} ) : wartetAuf ? ( attachmentText.checking ) : ( documentText.photoEmpty )}
{!readOnly ? (
{ const datei = event.target.files?.[0] if (datei) waehlen(datei) event.target.value = '' }} className="hidden" /> {fehler ? (

{fehler}

) : null}
) : null}
) }