176 lines
6.6 KiB
TypeScript
176 lines
6.6 KiB
TypeScript
import { useId, useRef, useState } from 'react';
|
|
import { profileJson } from '@/lib/profile-json';
|
|
|
|
export type ReusableDeskDocument = {
|
|
id: string | null;
|
|
document_id: string;
|
|
file_name: string;
|
|
version_no: number | null;
|
|
source_reference: string | null;
|
|
category: string | null;
|
|
stage: string | null;
|
|
valid_until: string | null;
|
|
origin_label: string | null;
|
|
status: string;
|
|
malware_scan_status: string;
|
|
media_type: string | null;
|
|
size_bytes: number | null;
|
|
};
|
|
type Props = {
|
|
caseId: string;
|
|
disabled: boolean;
|
|
selected: ReusableDeskDocument | null;
|
|
onSelect: (document: ReusableDeskDocument | null) => void;
|
|
};
|
|
const key = (doc: ReusableDeskDocument) =>
|
|
doc.id ?? `upload:${doc.document_id}`;
|
|
|
|
export default function DeskDocumentPicker(props: Props) {
|
|
const group = useId();
|
|
const [query, setQuery] = useState('');
|
|
const [documents, setDocuments] = useState<ReusableDeskDocument[]>([]);
|
|
const [next, setNext] = useState<number | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
const [message, setMessage] = useState('');
|
|
const latest = useRef(props);
|
|
latest.current = props;
|
|
const sequence = useRef(0);
|
|
|
|
async function search(offset = 0) {
|
|
if (busy || props.disabled) return;
|
|
const ticket = ++sequence.current;
|
|
const caseId = props.caseId;
|
|
setBusy(true);
|
|
setMessage('A pesquisar documentos autorizados…');
|
|
try {
|
|
const params = new URLSearchParams({
|
|
q: query,
|
|
offset: String(offset),
|
|
});
|
|
const result = await profileJson(
|
|
`/balcao-virtual/dossiers/${caseId}/documentos-disponiveis?${params}`,
|
|
{},
|
|
'GET',
|
|
);
|
|
if (ticket !== sequence.current || latest.current.caseId !== caseId)
|
|
return;
|
|
if (
|
|
!Array.isArray(result.documents) ||
|
|
(result.next_offset !== null &&
|
|
!Number.isInteger(result.next_offset))
|
|
)
|
|
throw new Error(
|
|
'Não foi possível confirmar os resultados. A selecção anterior foi conservada.',
|
|
);
|
|
setDocuments(result.documents);
|
|
setNext(result.next_offset);
|
|
setMessage(
|
|
result.documents.length
|
|
? 'Seleccione a origem que pretende reutilizar.'
|
|
: 'Sem documentos nesta página. Pode ajustar a pesquisa ou continuar, se houver mais resultados.',
|
|
);
|
|
} catch (error) {
|
|
if (ticket === sequence.current && latest.current.caseId === caseId)
|
|
setMessage(
|
|
error instanceof Error
|
|
? error.message
|
|
: 'Não foi possível pesquisar documentos.',
|
|
);
|
|
} finally {
|
|
if (ticket === sequence.current) setBusy(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-2 rounded border p-3">
|
|
<p>Reutilizar um documento autorizado</p>
|
|
<p className="text-sm">
|
|
Pesquise pelo nome do ficheiro, dossier ou referência. A
|
|
selecção conserva a origem e a validade declarada; a associação
|
|
exige confirmação.
|
|
</p>
|
|
<label className="block">
|
|
Nome ou referência do documento
|
|
<input
|
|
value={query}
|
|
maxLength={120}
|
|
disabled={props.disabled || busy}
|
|
onChange={(event) => {
|
|
setQuery(event.target.value);
|
|
setDocuments([]);
|
|
setNext(null);
|
|
setMessage(
|
|
'Pesquisa alterada. Pesquise para actualizar os resultados.',
|
|
);
|
|
}}
|
|
onKeyDown={(event) => {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
void search();
|
|
}
|
|
}}
|
|
/>
|
|
</label>
|
|
<button
|
|
type="button"
|
|
disabled={props.disabled || busy}
|
|
onClick={() => void search()}
|
|
>
|
|
Pesquisar documentos
|
|
</button>
|
|
<p role="status">{message}</p>
|
|
{documents.map((doc) => (
|
|
<label className="block" key={key(doc)}>
|
|
<input
|
|
type="radio"
|
|
name={`document-origin-${group}`}
|
|
checked={
|
|
props.selected
|
|
? key(props.selected) === key(doc)
|
|
: false
|
|
}
|
|
disabled={props.disabled || busy}
|
|
onChange={() => props.onSelect(doc)}
|
|
/>{' '}
|
|
{doc.file_name} · versão {doc.version_no ?? 'por confirmar'}{' '}
|
|
· {doc.origin_label ?? 'Ficheiro enviado por si'} ·{' '}
|
|
{doc.source_reference || 'Referência por completar'}
|
|
{doc.stage ? ` · origem ${doc.stage}` : ''}
|
|
{doc.valid_until
|
|
? ` · validade declarada ${doc.valid_until}`
|
|
: ''}{' '}
|
|
· {doc.malware_scan_status}
|
|
</label>
|
|
))}
|
|
{next !== null && (
|
|
<button
|
|
type="button"
|
|
disabled={props.disabled || busy}
|
|
onClick={() => void search(next)}
|
|
>
|
|
Próxima página de documentos
|
|
</button>
|
|
)}
|
|
{props.selected && (
|
|
<div>
|
|
<p>
|
|
Origem seleccionada: {props.selected.file_name} ·{' '}
|
|
{props.selected.source_reference ||
|
|
'Referência por completar'}
|
|
{props.selected.valid_until
|
|
? ` · validade declarada ${props.selected.valid_until}`
|
|
: ''}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
disabled={props.disabled || busy}
|
|
onClick={() => props.onSelect(null)}
|
|
>
|
|
Retirar origem seleccionada
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|