- Added `reimportar_e_analisar` method to `App` struct to handle reimporting and analyzing files. - Preserved previous analysis results to restore in case of errors or empty imports. - Updated UI to include a button for reanalyzing the current file. - Enhanced file selection logic to allow direct processing if a compatible preset is selected. - Refactored import logic to streamline the analysis process for both CSV and XLSX files. - Improved user feedback with appropriate error and success messages during file operations.
310 lines
9.4 KiB
Rust
310 lines
9.4 KiB
Rust
use crate::application::usecases::exportar_pdf::exportar_pdf;
|
||
use crate::domain::{
|
||
entities::{chave_serie::ChaveSerie, resultado_analise::ResultadoAnalise},
|
||
services::{detector_sequencia::agrupar_contiguos, parser_monetario::formatar_valor_br},
|
||
};
|
||
use crate::infrastructure::pdf_generator::GenpdfGenerator;
|
||
use crate::ui::app::{AcaoModal, App, EstadoApp};
|
||
use egui::{Context, Ui};
|
||
|
||
const OPCOES_PAGINA: &[usize] = &[50, 100, 200, 1000];
|
||
|
||
/// Renderiza a tela de resultados.
|
||
pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) {
|
||
// Extrair resultado do estado (sem mover)
|
||
let resultado = match &app.estado {
|
||
EstadoApp::ExibindoResultado(r) => r.clone(),
|
||
_ => return,
|
||
};
|
||
|
||
ui.heading("Resultado da Análise");
|
||
ui.add_space(8.0);
|
||
|
||
ui.horizontal(|ui| {
|
||
if ui.button("< Nova Análise").clicked() {
|
||
app.exibir_confirmacao(
|
||
"Nova Análise",
|
||
"Deseja iniciar uma nova análise? O resultado atual será descartado.",
|
||
AcaoModal::ConfirmarNovaAnalise,
|
||
);
|
||
}
|
||
|
||
if ui.button("⚙ Reconfigurar Colunas").clicked() {
|
||
app.estado = EstadoApp::ConfigurandoColunas;
|
||
return;
|
||
}
|
||
|
||
let pode_reanalisar = app.caminho_arquivo.is_some();
|
||
if ui
|
||
.add_enabled(pode_reanalisar, egui::Button::new("🔄 Reanalisar Arquivo"))
|
||
.on_hover_text("Reimporta o arquivo do disco com o layout atual e reanalisa")
|
||
.clicked()
|
||
{
|
||
app.reimportar_e_analisar(ctx);
|
||
}
|
||
|
||
if ui.button("📄 Exportar PDF").clicked() {
|
||
exportar_para_pdf(app, &resultado);
|
||
}
|
||
});
|
||
|
||
ui.add_space(8.0);
|
||
|
||
// Controle de itens por página
|
||
ui.horizontal(|ui| {
|
||
ui.label("Itens por página:");
|
||
for &opcao in OPCOES_PAGINA {
|
||
if ui
|
||
.selectable_label(app.itens_por_pagina == opcao, opcao.to_string())
|
||
.clicked()
|
||
{
|
||
app.itens_por_pagina = opcao;
|
||
app.pagina_faltantes = 0;
|
||
app.pagina_duplicatas = 0;
|
||
}
|
||
}
|
||
});
|
||
|
||
ui.add_space(12.0);
|
||
ui.separator();
|
||
|
||
egui::ScrollArea::vertical().show(ui, |ui| {
|
||
// Faltantes
|
||
renderizar_faltantes(ui, app, &resultado);
|
||
|
||
ui.add_space(12.0);
|
||
ui.separator();
|
||
|
||
// Duplicatas
|
||
renderizar_duplicatas(ui, app, &resultado);
|
||
|
||
ui.add_space(12.0);
|
||
ui.separator();
|
||
|
||
// Totais
|
||
renderizar_totais(ui, &resultado);
|
||
});
|
||
}
|
||
|
||
fn renderizar_totais(ui: &mut Ui, resultado: &ResultadoAnalise) {
|
||
ui.label(egui::RichText::new("Totais").heading().strong());
|
||
ui.add_space(4.0);
|
||
|
||
ui.label(format!(
|
||
"Total Geral: R$ {}",
|
||
formatar_valor_br(&resultado.soma_total)
|
||
));
|
||
|
||
let mut chaves: Vec<&ChaveSerie> = resultado.soma_por_serie.keys().collect();
|
||
chaves.sort();
|
||
|
||
for chave in chaves {
|
||
let soma = &resultado.soma_por_serie[chave];
|
||
let total_notas = resultado.total_por_serie.get(chave).copied().unwrap_or(0);
|
||
ui.label(format!(
|
||
" Série {}: {} nota(s) — R$ {}",
|
||
chave.label(),
|
||
total_notas,
|
||
formatar_valor_br(soma)
|
||
));
|
||
}
|
||
}
|
||
|
||
fn renderizar_faltantes(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalise) {
|
||
let total_faltantes = resultado.total_faltantes();
|
||
ui.label(
|
||
egui::RichText::new(format!("Notas Faltantes ({} total)", total_faltantes))
|
||
.heading()
|
||
.strong(),
|
||
);
|
||
ui.add_space(4.0);
|
||
|
||
if total_faltantes == 0 {
|
||
ui.label("✔ Nenhuma nota faltante.");
|
||
return;
|
||
}
|
||
|
||
let mut chaves: Vec<&ChaveSerie> = resultado.faltantes_por_serie.keys().collect();
|
||
chaves.sort();
|
||
|
||
for chave in chaves {
|
||
let faltantes = &resultado.faltantes_por_serie[chave];
|
||
if faltantes.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
// Estatística de completude por série
|
||
let total_notas = resultado.total_por_serie.get(chave).copied().unwrap_or(0);
|
||
let total_esperado = total_notas + faltantes.len();
|
||
let percentual = total_notas as f64 / total_esperado as f64 * 100.0;
|
||
|
||
ui.horizontal(|ui| {
|
||
ui.label(format!(
|
||
"Série {} — {} faltante(s) — {}/{} notas ({:.1}% completo):",
|
||
chave.label(),
|
||
faltantes.len(),
|
||
total_notas,
|
||
total_esperado,
|
||
percentual,
|
||
));
|
||
if ui
|
||
.button("📋 Copiar")
|
||
.on_hover_text("Copiar todos os números faltantes")
|
||
.clicked()
|
||
{
|
||
let texto = faltantes
|
||
.iter()
|
||
.map(|n| n.to_string())
|
||
.collect::<Vec<_>>()
|
||
.join(", ");
|
||
ui.ctx().copy_text(texto);
|
||
}
|
||
});
|
||
|
||
// Paginação (por faltante individual)
|
||
let total_paginas = (faltantes.len() + app.itens_por_pagina - 1) / app.itens_por_pagina;
|
||
if app.pagina_faltantes >= total_paginas {
|
||
app.pagina_faltantes = 0;
|
||
}
|
||
|
||
let inicio = app.pagina_faltantes * app.itens_por_pagina;
|
||
let fim = (inicio + app.itens_por_pagina).min(faltantes.len());
|
||
|
||
// Exibir grupos contíguos da página atual
|
||
for (a, b) in agrupar_contiguos(&faltantes[inicio..fim]) {
|
||
if a == b {
|
||
ui.label(format!(" • {}", a));
|
||
} else {
|
||
ui.label(format!(" • {}–{} ({} notas)", a, b, b - a + 1));
|
||
}
|
||
}
|
||
|
||
if total_paginas > 1 {
|
||
ui.horizontal(|ui| {
|
||
if ui.button("◀").clicked() && app.pagina_faltantes > 0 {
|
||
app.pagina_faltantes -= 1;
|
||
}
|
||
ui.label(format!(
|
||
"Página {} / {}",
|
||
app.pagina_faltantes + 1,
|
||
total_paginas
|
||
));
|
||
if ui.button("▶").clicked() && app.pagina_faltantes + 1 < total_paginas {
|
||
app.pagina_faltantes += 1;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
fn renderizar_duplicatas(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalise) {
|
||
let total_dup = resultado.total_duplicatas();
|
||
ui.label(
|
||
egui::RichText::new(format!("Notas Duplicadas ({} grupo(s))", total_dup))
|
||
.heading()
|
||
.strong(),
|
||
);
|
||
ui.add_space(4.0);
|
||
|
||
if total_dup == 0 {
|
||
ui.label("✔ Nenhuma nota duplicada.");
|
||
return;
|
||
}
|
||
|
||
let mut chaves: Vec<&ChaveSerie> = resultado.duplicadas_por_serie.keys().collect();
|
||
chaves.sort();
|
||
|
||
for chave in chaves {
|
||
let duplicatas = &resultado.duplicadas_por_serie[chave];
|
||
if duplicatas.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
ui.horizontal(|ui| {
|
||
ui.label(format!(
|
||
"Série {} — {} grupo(s) duplicado(s):",
|
||
chave.label(),
|
||
duplicatas.len()
|
||
));
|
||
if ui
|
||
.button("📋 Copiar")
|
||
.on_hover_text("Copiar números duplicados")
|
||
.clicked()
|
||
{
|
||
let texto = duplicatas
|
||
.iter()
|
||
.map(|(n, c)| format!("{} ({}x)", n, c))
|
||
.collect::<Vec<_>>()
|
||
.join(", ");
|
||
ui.ctx().copy_text(texto);
|
||
}
|
||
});
|
||
|
||
let total_paginas = (duplicatas.len() + app.itens_por_pagina - 1) / app.itens_por_pagina;
|
||
if app.pagina_duplicatas >= total_paginas {
|
||
app.pagina_duplicatas = 0;
|
||
}
|
||
|
||
let inicio = app.pagina_duplicatas * app.itens_por_pagina;
|
||
let fim = (inicio + app.itens_por_pagina).min(duplicatas.len());
|
||
|
||
for (numero, count) in &duplicatas[inicio..fim] {
|
||
ui.label(format!(
|
||
" • NF {} / Série {} — {} ocorrências",
|
||
numero,
|
||
chave.label(),
|
||
count
|
||
));
|
||
}
|
||
|
||
if total_paginas > 1 {
|
||
ui.horizontal(|ui| {
|
||
if ui.button("◀").clicked() && app.pagina_duplicatas > 0 {
|
||
app.pagina_duplicatas -= 1;
|
||
}
|
||
ui.label(format!(
|
||
"Página {} / {}",
|
||
app.pagina_duplicatas + 1,
|
||
total_paginas
|
||
));
|
||
if ui.button("▶").clicked() && app.pagina_duplicatas + 1 < total_paginas {
|
||
app.pagina_duplicatas += 1;
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
fn exportar_para_pdf(app: &mut App, resultado: &ResultadoAnalise) {
|
||
if let Some(caminho) = rfd::FileDialog::new()
|
||
.set_file_name("relatorio.pdf")
|
||
.add_filter("PDF", &["pdf"])
|
||
.save_file()
|
||
{
|
||
let gerador = GenpdfGenerator;
|
||
let nome_layout = if app.nome_layout_atual.is_empty() {
|
||
None
|
||
} else {
|
||
Some(app.nome_layout_atual.as_str())
|
||
};
|
||
|
||
match exportar_pdf(
|
||
&gerador,
|
||
resultado,
|
||
&app.nome_arquivo,
|
||
nome_layout,
|
||
&caminho,
|
||
) {
|
||
Ok(_) => {
|
||
app.exibir_aviso(
|
||
"Sucesso",
|
||
format!("PDF exportado para: {}", caminho.display()),
|
||
);
|
||
}
|
||
Err(e) => {
|
||
app.exibir_erro(format!("Erro ao exportar PDF: {}", e));
|
||
}
|
||
}
|
||
}
|
||
}
|