174 lines
6.7 KiB
TypeScript
174 lines
6.7 KiB
TypeScript
import { Document, Page, Text, View, StyleSheet, pdf, Image } from '@react-pdf/renderer';
|
|
import type { AuditReport, ModuleAuditResult, ScenarioResult, CheckStatus } from './types';
|
|
|
|
const styles = StyleSheet.create({
|
|
page: {
|
|
flexDirection: 'column',
|
|
padding: 40,
|
|
fontSize: 10,
|
|
fontFamily: 'Helvetica',
|
|
color: '#333',
|
|
},
|
|
header: {
|
|
marginBottom: 20,
|
|
borderBottom: '2pt solid #6b21a8',
|
|
paddingBottom: 10,
|
|
},
|
|
title: { fontSize: 20, fontWeight: 'bold', color: '#6b21a8' },
|
|
subtitle: { fontSize: 10, color: '#666', marginTop: 4 },
|
|
section: { marginTop: 15, marginBottom: 10 },
|
|
sectionTitle: { fontSize: 14, fontWeight: 'bold', marginBottom: 8, color: '#111' },
|
|
summaryRow: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
marginBottom: 4,
|
|
},
|
|
summaryLabel: { fontWeight: 'bold', width: 200 },
|
|
summaryValue: { flex: 1 },
|
|
moduleCard: {
|
|
marginTop: 12,
|
|
padding: 10,
|
|
border: '1pt solid #ddd',
|
|
borderRadius: 4,
|
|
},
|
|
moduleTitle: { fontSize: 12, fontWeight: 'bold', marginBottom: 6, color: '#6b21a8' },
|
|
moduleStats: { fontSize: 9, color: '#555', marginBottom: 4 },
|
|
scenarioItem: {
|
|
marginTop: 6,
|
|
padding: 6,
|
|
backgroundColor: '#f9fafb',
|
|
borderRadius: 3,
|
|
},
|
|
scenarioId: { fontSize: 9, fontWeight: 'bold', marginBottom: 3, color: '#111' },
|
|
checkRow: { flexDirection: 'row', marginBottom: 2, fontSize: 8 },
|
|
statusBadge: { width: 40, fontWeight: 'bold' },
|
|
checkLabel: { width: 120 },
|
|
checkValue: { flex: 1 },
|
|
footer: {
|
|
position: 'absolute',
|
|
bottom: 30,
|
|
left: 40,
|
|
right: 40,
|
|
textAlign: 'center',
|
|
color: '#999',
|
|
fontSize: 8,
|
|
borderTop: '1pt solid #eaeaea',
|
|
paddingTop: 10,
|
|
},
|
|
});
|
|
|
|
function statusColor(s: CheckStatus): string {
|
|
switch (s) {
|
|
case 'PASS': return '#16a34a';
|
|
case 'WARN': return '#d97706';
|
|
case 'FAIL': return '#dc2626';
|
|
}
|
|
}
|
|
|
|
interface ReportPDFProps {
|
|
report: AuditReport;
|
|
}
|
|
|
|
function AuditReportDocument({ report }: ReportPDFProps) {
|
|
return (
|
|
<Document>
|
|
<Page size="A4" style={styles.page}>
|
|
<View style={[styles.header, { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }]}>
|
|
<View>
|
|
<Text style={styles.title}>Relatório de Auditoria</Text>
|
|
<Text style={styles.subtitle}>
|
|
Auditor IA: {report.provider} ({report.model}) | {new Date(report.timestamp).toLocaleString('pt-BR')}
|
|
</Text>
|
|
</View>
|
|
{typeof window !== 'undefined' && window.location && (
|
|
<Image src={`${window.location.origin}/logo_brainwind.png`} style={{ width: 120, height: 40, objectFit: 'contain' }} />
|
|
)}
|
|
</View>
|
|
|
|
<View style={styles.section}>
|
|
<Text style={styles.sectionTitle}>Resumo</Text>
|
|
<View style={styles.summaryRow}>
|
|
<Text style={styles.summaryLabel}>Total de Cenários:</Text>
|
|
<Text style={styles.summaryValue}>{report.totalScenarios}</Text>
|
|
</View>
|
|
<View style={styles.summaryRow}>
|
|
<Text style={[styles.summaryLabel, { color: '#16a34a' }]}>Aprovado:</Text>
|
|
<Text style={styles.summaryValue}>{report.totalPassed}</Text>
|
|
</View>
|
|
<View style={styles.summaryRow}>
|
|
<Text style={[styles.summaryLabel, { color: '#d97706' }]}>Atenção:</Text>
|
|
<Text style={styles.summaryValue}>{report.totalWarnings}</Text>
|
|
</View>
|
|
<View style={styles.summaryRow}>
|
|
<Text style={[styles.summaryLabel, { color: '#dc2626' }]}>Falha:</Text>
|
|
<Text style={styles.summaryValue}>{report.totalFailed}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{report.modules.map((mod: ModuleAuditResult) => (
|
|
<View key={mod.module} style={styles.moduleCard}>
|
|
<Text style={styles.moduleTitle}>
|
|
{mod.moduleLabel} — {mod.passed}/{mod.totalScenarios} (Alertas: {mod.warnings} | Falhas: {mod.failed})
|
|
</Text>
|
|
{mod.scenarios.map((sc: ScenarioResult) => {
|
|
const summaryTranslated = (sc.summary || '')
|
|
.replace(/Verdict PASS\.?/gi, 'Veredito: APROVADO.')
|
|
.replace(/Verdict WARN\.?/gi, 'Veredito: ATENÇÃO.')
|
|
.replace(/Verdict FAIL\.?/gi, 'Veredito: FALHA.');
|
|
|
|
return (
|
|
<View key={sc.scenarioId} style={styles.scenarioItem}>
|
|
<Text style={styles.scenarioId}>
|
|
{sc.scenarioId} — {sc.verdict === 'PASS' ? '✓' : sc.verdict === 'WARN' ? '⚠' : '✗'} {summaryTranslated}
|
|
</Text>
|
|
{sc.enunciado_problema && (
|
|
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Enunciado: {sc.enunciado_problema}</Text>
|
|
)}
|
|
{sc.tabelas_nbr_consultadas && (
|
|
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Tabelas: {sc.tabelas_nbr_consultadas}</Text>
|
|
)}
|
|
{sc.resultado_app && (
|
|
<Text style={{ fontSize: 8, color: '#444', marginBottom: 2 }}>Avaliação do App: {sc.resultado_app}</Text>
|
|
)}
|
|
{sc.calculation_steps && (
|
|
<Text style={{ fontSize: 8, color: '#444', marginBottom: 4, fontStyle: 'italic' }}>
|
|
CoT: {sc.calculation_steps.substring(0, 300)}{sc.calculation_steps.length > 300 ? '...' : ''}
|
|
</Text>
|
|
)}
|
|
{sc.checks.map((chk, i) => (
|
|
<View key={i} style={styles.checkRow}>
|
|
<Text style={[styles.statusBadge, { color: statusColor(chk.status) }]}>{chk.status}</Text>
|
|
<Text style={styles.checkLabel}>{chk.name}</Text>
|
|
<Text style={styles.checkValue}>{chk.actual} ({chk.expected})</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
))}
|
|
|
|
<Text style={styles.footer}>
|
|
Gerado por BrainWind v1.0 — Ferramenta de Auditoria NBR 6123:2023
|
|
</Text>
|
|
</Page>
|
|
</Document>
|
|
);
|
|
}
|
|
|
|
export async function exportAuditReportToPDF(report: AuditReport): Promise<void> {
|
|
const blob = await pdf(<AuditReportDocument report={report} />).toBlob();
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
|
|
const dateStr = new Date(report.timestamp).toLocaleDateString('pt-BR').replace(/\//g, '-');
|
|
const safeModel = report.model.split('/').pop()?.replace(/[^a-zA-Z0-9_-]/g, '-') || 'LLM';
|
|
const filename = `Audit-${safeModel}_${dateStr}.pdf`;
|
|
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', filename);
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|