90 lines
2.9 KiB
TypeScript
90 lines
2.9 KiB
TypeScript
import {
|
|
validateDossierAttachments,
|
|
type DossierAttachment,
|
|
} from './project-dossier-report.ts';
|
|
|
|
export type DossierExportOperation =
|
|
| 'word'
|
|
| 'package'
|
|
| 'pdf'
|
|
| 'csv'
|
|
| 'attachments';
|
|
const messages: Record<DossierExportOperation, string> = {
|
|
word: 'Não foi possível gerar o relatório Word.',
|
|
package: 'Não foi possível preparar o pacote de anexos.',
|
|
pdf: 'Não foi possível preparar a versão para PDF.',
|
|
csv: 'Não foi possível preparar o ficheiro CSV.',
|
|
attachments: 'Não foi possível ler os anexos seleccionados.',
|
|
};
|
|
let failureSequence = 0;
|
|
|
|
export function dossierExportFailure(
|
|
operation: DossierExportOperation,
|
|
cause: unknown,
|
|
log: (message: string, detail: unknown) => void = console.error,
|
|
): string {
|
|
const reference = `EXP-${Date.now().toString(36)}-${++failureSequence}`;
|
|
// Local diagnostics only: never send dossier values or file contents.
|
|
log('[InvesteMoz exportação]', { reference, operation, cause });
|
|
return `${messages[operation]} Os dados, as secções e os anexos foram mantidos. Tente novamente pelo mesmo botão. Referência: ${reference}.`;
|
|
}
|
|
|
|
/** Synchronous acquisition prevents a second click before React renders busy. */
|
|
export class DossierExportGate {
|
|
private active = false;
|
|
async run(task: () => Promise<void>): Promise<boolean> {
|
|
if (this.active) return false;
|
|
this.active = true;
|
|
try {
|
|
await task();
|
|
return true;
|
|
} finally {
|
|
this.active = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function prepareDossierAttachments(
|
|
current: DossierAttachment[],
|
|
incoming: File[],
|
|
): Promise<{
|
|
attachments: DossierAttachment[];
|
|
duplicateCount: number;
|
|
problem?: string;
|
|
}> {
|
|
for (const file of incoming) {
|
|
const problem = validateDossierAttachments([file]);
|
|
if (problem)
|
|
return { attachments: current, duplicateCount: 0, problem };
|
|
}
|
|
const identity = async (file: File) => {
|
|
const hash = await crypto.subtle.digest(
|
|
'SHA-256',
|
|
await file.arrayBuffer(),
|
|
);
|
|
return `${file.name}:${Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, '0')).join('')}`;
|
|
};
|
|
const known = new Set(
|
|
await Promise.all(current.map((item) => identity(item.file))),
|
|
);
|
|
const added: DossierAttachment[] = [];
|
|
let duplicateCount = 0;
|
|
for (const file of incoming) {
|
|
const key = await identity(file);
|
|
if (known.has(key)) {
|
|
duplicateCount++;
|
|
continue;
|
|
}
|
|
known.add(key);
|
|
added.push({ file, include: true, caption: '' });
|
|
}
|
|
const problem = validateDossierAttachments(
|
|
[...current, ...added].map((item) => item.file),
|
|
);
|
|
return {
|
|
attachments: problem ? current : [...current, ...added],
|
|
duplicateCount,
|
|
problem: problem ?? undefined,
|
|
};
|
|
}
|