import { useRef, useState } from 'react'; import { Alert, Badge, Button, Card, FileButton, Group, Loader, Stack, Text, } from '@mantine/core'; import { IconAlertTriangle, IconCheck, IconFileUpload, } from '@tabler/icons-react'; import { conditionHolds, useLocalized, uploadDocument, type Attachment, type DocumentRequirement, } from '@ema-platform/api'; import { useTranslation } from 'react-i18next'; import { PdfPreviewModal } from '@ema-platform/ui'; const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024; interface Props { requirements: DocumentRequirement[]; attachments: Attachment[]; formData: Record>; ownerType: 'APPLICATION' | 'APPLICATION_STAFF'; ownerId: string; /** Document keys the officer flagged; these render as "needs fixing". */ flagged?: Record; /** When set, only flagged slots accept a new upload. */ restrictToFlagged?: boolean; /** * Requirement keys opened because a flagged section drives their condition — * a category correction can make documents newly required, and those have to * be uploadable even though the officer flagged no document. */ alsoUnlocked?: string[]; onUploaded: () => void; readOnly?: boolean; } /** * The upload slots for an application, driven by the configured document * requirements. Conditional slots appear only when their rule matches the * answers given — an owned vehicle asks for a libre, a rented one for the * rental agreement. */ export function DocumentSlots({ requirements, attachments, formData, ownerType, ownerId, flagged = {}, restrictToFlagged = false, alsoUnlocked = [], onUploaded, readOnly, }: Props) { const localized = useLocalized(); const { t } = useTranslation(); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const [preview, setPreview] = useState<{ url: string; title: string } | null>( null, ); const resetRefs = useRef void>>({}); const required = requirements.filter( (r) => r.mode === 'ALWAYS' || r.mode === 'OPTIONAL' || (r.mode === 'CONDITIONAL' && conditionHolds(r.conditionExpression, formData)), ); async function handle(documentKey: string, file: File | null) { if (!file) return; if (file.size > MAX_FILE_SIZE_BYTES) { setError( t('licensing.msg.fileTooLarge', { size: `${(file.size / 1024 / 1024).toFixed(1)}MB`, }), ); resetRefs.current[documentKey]?.(); return; } setBusy(documentKey); setError(null); const result = await uploadDocument({ ownerType, ownerId, documentKey, file }); setBusy(null); resetRefs.current[documentKey]?.(); if (result.ok) onUploaded(); else setError(result.error); } return ( {error && ( }> {error} )} {required.map((requirement) => { const existing = attachments.find((a) => a.documentKey === requirement.key); const uploaded = Boolean(existing?.files?.length); const fileUrl = existing?.files?.[0]?.url; const flagRemark = flagged[requirement.key]; const locked = readOnly || (restrictToFlagged && !flagRemark && !alsoUnlocked.includes(requirement.key)); return (
{localized(requirement.name)} {requirement.mode === 'CONDITIONAL' && ( {t('licensing.documents.conditional')} )} {requirement.mode === 'OPTIONAL' && ( {t('common.optional')} )} {uploaded && !flagRemark && ( }> {t('licensing.documents.uploaded')} )} {requirement.description && ( {localized(requirement.description)} )} {existing?.files?.[0] && ( {existing.files[0].originalName} ·{' '} {(existing.files[0].sizeBytes / 1024).toFixed(0)} KB )} {flagRemark && ( {t('licensing.documents.officerRemark', { name: flagRemark })} )}
{fileUrl && ( )} {!locked && ( { if (r) resetRefs.current[requirement.key] = r; }} onChange={(file) => handle(requirement.key, file)} accept={requirement.allowedMimeTypes?.join(',')} > {(props) => ( )} )}
); })} setPreview(null)} url={preview?.url ?? ''} title={preview?.title} />
); }