import { useRef, useState } from 'react'; import { profileJson, ProfileValidationError } from '@/lib/profile-json'; import { deskChoices } from '@/lib/desk-field-values'; import type { DeskDocument } from './desk-internal-operations'; import DeskDocumentPicker, { type ReusableDeskDocument, } from './desk-document-picker'; export type AttachmentField = { label: string; required: boolean; mime_types?: string[]; max_size_kb?: number; max_files?: number; }; export type AttachmentInventory = { documents: DeskDocument[]; case_version: number; link_id?: string | null; reused_link?: boolean; reusable_documents?: DeskDocument[]; }; type Props = { caseId: string; requestId: string; caseVersion: number; fieldKey: string; field: AttachmentField; value: unknown; documents: DeskDocument[]; reusableDocuments?: DeskDocument[]; disabled: boolean; uploading: boolean; onChange: (ids: string[]) => void; onBusy: (busy: boolean) => void; onInventory: (result: AttachmentInventory, select: boolean) => void; }; const formatLabels: Record = { 'application/pdf': 'PDF', 'image/png': 'PNG', 'image/jpeg': 'JPEG', 'text/plain': 'Texto simples', }; export default function DeskAttachmentField(props: Props) { const latest = useRef(props); const fileInput = useRef(null); latest.current = props; const [file, setFile] = useState(null), [source, setSource] = useState(''), [consent, setConsent] = useState(false); const [busy, setBusy] = useState(false), [message, setMessage] = useState(''), [errors, setErrors] = useState>({}); const [reuseId, setReuseId] = useState(''); const [remoteOrigin, setRemoteOrigin] = useState(null); const reusable = [ ...new Map( (props.reusableDocuments ?? []) .filter( (doc) => !props.documents.some( (current) => current.document_id === doc.document_id, ), ) .map((doc) => [ JSON.stringify([ doc.document_id, doc.source_reference, doc.stage ?? null, doc.valid_until ?? null, ]), doc, ]), ).values(), ]; const origin = remoteOrigin ?? reusable.find((doc) => doc.id === reuseId); const selected = deskChoices(props.value); const unique = [ ...new Map( props.documents.map((doc) => [ doc.document_id, props.documents.find( (d) => d.document_id === doc.document_id && selected.includes(d.id), ) ?? doc, ]), ).values(), ]; async function refresh() { if (busy) return; setBusy(true); try { const data = await profileJson( `/balcao-virtual/pedidos/${props.requestId}`, {}, 'GET', ); if ( !Array.isArray(data.documents) || !Number.isInteger(data.caseVersion) ) throw new Error( 'Não foi possível confirmar a lista dos documentos. As respostas desta página foram conservadas.', ); latest.current.onInventory( { documents: data.documents, case_version: data.caseVersion, reusable_documents: data.reusableDocuments, }, false, ); setMessage( 'Estado e lista dos documentos actualizados. As respostas desta página foram conservadas.', ); } catch (error) { setMessage( error instanceof Error ? error.message : 'Não foi possível actualizar os documentos.', ); } finally { setBusy(false); } } async function upload() { if ( props.disabled || props.uploading || busy || (!file && !origin) || !consent || (!origin?.source_reference?.trim() && !source.trim()) ) return; if ( !origin && file && file.size > (props.field.max_size_kb ?? 10240) * 1024 ) { setErrors({ file: 'O ficheiro excede o tamanho máximo desta pergunta.', }); return; } setBusy(true); props.onBusy(true); setErrors({}); setMessage('A enviar o documento privado…'); const data = new FormData(); data.set('version', String(props.caseVersion)); data.set('request_id', props.requestId); data.set('field_key', props.fieldKey); if (origin) { data.set('document_id', origin.document_id); if (origin.id) data.set('reuse_link_id', origin.id); if (!origin.source_reference?.trim()) data.set('source_reference', source); } else if (file) { data.set('file', file); data.set('source_reference', source); } data.set('consent', '1'); try { const result: AttachmentInventory = await profileJson( `/balcao-virtual/dossiers/${props.caseId}/documentos`, data, ); if ( !Array.isArray(result.documents) || !Number.isInteger(result.case_version) || typeof result.link_id !== 'string' || !result.documents.some( (document) => document.id === result.link_id, ) ) throw new Error( 'O envio não confirmou o documento associado. Actualize a lista antes de repetir; o ficheiro seleccionado continua nesta página.', ); latest.current.onInventory(result, true); setFile(null); if (fileInput.current) fileInput.current.value = ''; setConsent(false); setReuseId(''); setRemoteOrigin(null); setMessage( result.reused_link ? 'Este documento já estava associado. Conservou-se a referência registada; pode seleccioná-lo nesta pergunta.' : 'Documento associado ao pedido. A análise de segurança e a selecção nesta pergunta são apresentadas abaixo.', ); } catch (error) { setMessage( error instanceof Error ? error.message : 'O envio não foi confirmado. Actualize a lista antes de repetir.', ); if (error instanceof ProfileValidationError) setErrors(error.fields); } finally { setBusy(false); latest.current.onBusy(false); } } return (
{props.field.label} {props.field.required ? ' *' : ''}

Seleccione documentos já associados a este dossier ou envie um ficheiro. O mesmo original pode apoiar várias perguntas, sem novo envio.

Formatos:{' '} {(props.field.mime_types ?? []) .map((mime) => formatLabels[mime] ?? mime) .join(', ')} . Até {props.field.max_size_kb} KB por ficheiro e{' '} {props.field.max_files} ficheiro(s) seleccionados.

{unique.map((doc) => { const checked = selected.includes(doc.id); const mime = doc.media_type?.split(';')[0].trim().toLowerCase(); const known = !!mime && doc.size_bytes !== null && doc.size_bytes !== undefined; const fits = (!mime || props.field.mime_types?.includes(mime)) && (doc.size_bytes === null || doc.size_bytes === undefined || doc.size_bytes <= (props.field.max_size_kb ?? 0) * 1024); return ( ); })} {selected .filter((id) => !props.documents.some((doc) => doc.id === id)) .map((id) => (

Existe uma selecção cuja disponibilidade precisa de ser actualizada.{' '}

))} {!unique.length && (

Ainda não existem documentos associados a este pedido ou ao dossier para seleccionar.

)}

Pode guardar o rascunho enquanto a análise está pendente. Para submeter uma pergunta aplicável, os suportes seleccionados têm de estar disponíveis, autorizados e com análise de segurança válida; isso não os torna documentos oficialmente aprovados.

Enviar novo suporte para esta pergunta

Escolher um ficheiro ou um documento de origem não o associa ao pedido. Confirme a associação nesta pergunta antes de submeter.

{reusable.length > 0 && ( )} { setRemoteOrigin(document); setReuseId(''); setConsent(false); if (document) { setFile(null); if (fileInput.current) fileInput.current.value = ''; } }} /> {!origin && ( )} {!origin || !origin.source_reference?.trim() ? ( ) : (

Referência já registada: {origin.source_reference}. Será conservada com a versão deste original.

)}
{message && (

{message}

)} {Object.entries(errors).map(([key, error]) => (

{error}

))}
); }