feat: adiciona suporte para análise em background e processamento de resultados pendentes

This commit is contained in:
FelipeCN
2026-03-03 10:16:50 -03:00
parent dedb6a72c2
commit 2c08f237e9
3 changed files with 181 additions and 43 deletions
+127 -5
View File
@@ -21,6 +21,7 @@ use crate::infrastructure::{
use egui::Context; use egui::Context;
use rusqlite::Connection; use rusqlite::Connection;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::mpsc;
/// Estado global da aplicação. /// Estado global da aplicação.
pub enum EstadoApp { pub enum EstadoApp {
@@ -39,6 +40,8 @@ pub enum EstadoApp {
}, },
/// Gerenciamento de layouts. /// Gerenciamento de layouts.
GerenciandoLayouts, GerenciandoLayouts,
/// Análise em execução em background (thread separada).
Analisando,
} }
/// Modal bloqueante a ser exibido sobre qualquer tela. /// Modal bloqueante a ser exibido sobre qualquer tela.
@@ -75,6 +78,29 @@ pub enum AcaoModal {
SalvarLayoutConfig, SalvarLayoutConfig,
} }
/// Resultado enviado pela thread de análise de volta para a UI.
pub enum ResultadoPendente {
/// Análise concluída com sucesso.
Concluido {
resultado: ResultadoAnalise,
/// Avisos de importação (None quando vier do path de expansão).
avisos: Option<ResumoAvisos>,
/// Notas importadas (None quando o App já as possui).
notas: Option<Vec<Nota>>,
},
/// Pré-análise concluída mas precisa de confirmação do usuário.
AguardandoConfirmacao {
pre: ResultadoPreAnalise,
series_excessivas: Vec<(String, u64)>,
avisos: ResumoAvisos,
notas: Vec<Nota>,
},
/// Arquivo importado não continha notas válidas.
Vazio,
/// Erro durante importação ou análise.
Erro(String),
}
/// Struct principal da aplicação egui. /// Struct principal da aplicação egui.
pub struct App { pub struct App {
pub estado: EstadoApp, pub estado: EstadoApp,
@@ -111,6 +137,8 @@ pub struct App {
// Pré-visualização das primeiras linhas do arquivo // Pré-visualização das primeiras linhas do arquivo
pub preview_arquivo: Option<Vec<Vec<String>>>, pub preview_arquivo: Option<Vec<Vec<String>>>,
// Canal para receber resultado da análise em background
pub resultado_pendente: Option<mpsc::Receiver<ResultadoPendente>>,
} }
impl Default for App { impl Default for App {
@@ -134,6 +162,7 @@ impl Default for App {
pagina_duplicatas: 0, pagina_duplicatas: 0,
itens_por_pagina: 100, itens_por_pagina: 100,
preview_arquivo: None, preview_arquivo: None,
resultado_pendente: None,
} }
} }
} }
@@ -148,6 +177,7 @@ impl App {
EstadoApp::ExibindoResultado(_) => 3, EstadoApp::ExibindoResultado(_) => 3,
EstadoApp::ConfirmandoIntervalo { .. } => 4, EstadoApp::ConfirmandoIntervalo { .. } => 4,
EstadoApp::GerenciandoLayouts => 5, EstadoApp::GerenciandoLayouts => 5,
EstadoApp::Analisando => 6,
} }
} }
@@ -295,12 +325,20 @@ impl App {
fn executar_acao_modal(&mut self, acao: AcaoModal) { fn executar_acao_modal(&mut self, acao: AcaoModal) {
match acao { match acao {
AcaoModal::ConfirmarExpansaoFaltantes => { AcaoModal::ConfirmarExpansaoFaltantes => {
// Retirar pre-análise do estado e expandir
if let EstadoApp::ConfirmandoIntervalo { pre, .. } = if let EstadoApp::ConfirmandoIntervalo { pre, .. } =
std::mem::replace(&mut self.estado, EstadoApp::Importando) std::mem::replace(&mut self.estado, EstadoApp::Analisando)
{ {
let resultado = expandir_analise(pre, &self.notas_importadas); let notas = self.notas_importadas.clone();
self.estado = EstadoApp::ExibindoResultado(resultado); let (tx, rx) = mpsc::channel();
self.resultado_pendente = Some(rx);
std::thread::spawn(move || {
let resultado = expandir_analise(pre, &notas);
let _ = tx.send(ResultadoPendente::Concluido {
resultado,
avisos: None,
notas: None,
});
});
} }
} }
AcaoModal::ConfirmarExclusaoLayout(id) => { AcaoModal::ConfirmarExclusaoLayout(id) => {
@@ -355,6 +393,63 @@ impl App {
} }
} }
/// Processa o resultado recebido do canal de análise em background.
pub fn processar_resultado_pendente(&mut self, resultado: ResultadoPendente) {
match resultado {
ResultadoPendente::Concluido { resultado, avisos, notas } => {
if let Some(n) = notas {
self.notas_importadas = n;
}
if let Some(av) = &avisos {
self.avisos_importacao = if av.tem_avisos() { Some(av.clone()) } else { None };
}
self.estado = EstadoApp::ExibindoResultado(resultado);
if let Some(av) = &self.avisos_importacao.clone() {
if av.tem_avisos() {
let linhas = av.linhas_para_exibir().join("\n");
self.exibir_aviso("Avisos de Importação", linhas);
}
}
}
ResultadoPendente::AguardandoConfirmacao {
pre,
series_excessivas,
avisos,
notas,
} => {
self.notas_importadas = notas;
self.avisos_importacao = if avisos.tem_avisos() { Some(avisos) } else { None };
let msg = series_excessivas
.iter()
.map(|(serie, count)| {
format!("Série {}: intervalo de {} faltantes detectado", serie, count)
})
.collect::<Vec<_>>()
.join("\n");
self.estado = EstadoApp::ConfirmandoIntervalo {
pre,
series_excessivas,
};
self.exibir_confirmacao(
"Intervalo muito grande",
format!(
"As seguintes séries têm um número muito alto de faltantes:\n\n{}\n\nDeseja continuar mesmo assim?",
msg
),
AcaoModal::ConfirmarExpansaoFaltantes,
);
}
ResultadoPendente::Vazio => {
self.estado = EstadoApp::ConfigurandoColunas;
self.exibir_aviso("Aviso", "Nenhuma nota válida encontrada no arquivo.");
}
ResultadoPendente::Erro(e) => {
self.estado = EstadoApp::ConfigurandoColunas;
self.exibir_erro(format!("Erro ao importar arquivo: {}", e));
}
}
}
/// Executa a análise com as notas importadas. /// Executa a análise com as notas importadas.
pub fn executar_analise(&mut self) { pub fn executar_analise(&mut self) {
if self.notas_importadas.is_empty() { if self.notas_importadas.is_empty() {
@@ -399,7 +494,7 @@ impl App {
pub fn renderizar_breadcrumb(&self, ctx: &Context) { pub fn renderizar_breadcrumb(&self, ctx: &Context) {
let passo_ativo: usize = match self.tela_atual() { let passo_ativo: usize = match self.tela_atual() {
0 | 1 => 1, 0 | 1 => 1,
2 | 4 => 2, 2 | 4 | 6 => 2,
3 => 3, 3 => 3,
_ => return, // GerenciandoLayouts: sem breadcrumb _ => return, // GerenciandoLayouts: sem breadcrumb
}; };
@@ -429,6 +524,24 @@ impl App {
impl eframe::App for App { impl eframe::App for App {
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
// Verificar resultado pendente da análise em background
if let Some(rx) = self.resultado_pendente.take() {
match rx.try_recv() {
Ok(resultado) => {
self.processar_resultado_pendente(resultado);
}
Err(mpsc::TryRecvError::Empty) => {
// Ainda processando: devolver receiver e solicitar repaint
self.resultado_pendente = Some(rx);
ctx.request_repaint();
}
Err(mpsc::TryRecvError::Disconnected) => {
self.estado = EstadoApp::ConfigurandoColunas;
self.exibir_erro("Erro interno: a análise foi interrompida inesperadamente.");
}
}
}
// Sempre renderizar modal por cima de tudo // Sempre renderizar modal por cima de tudo
self.renderizar_modal(ctx); self.renderizar_modal(ctx);
@@ -447,6 +560,15 @@ impl eframe::App for App {
3 => crate::ui::screens::resultado::renderizar(ui, ctx, self), 3 => crate::ui::screens::resultado::renderizar(ui, ctx, self),
4 => crate::ui::screens::configuracao_colunas::renderizar(ui, ctx, self), 4 => crate::ui::screens::configuracao_colunas::renderizar(ui, ctx, self),
5 => crate::ui::screens::layouts::renderizar(ui, ctx, self), 5 => crate::ui::screens::layouts::renderizar(ui, ctx, self),
6 => {
ui.centered_and_justified(|ui| {
ui.label(
egui::RichText::new("⏳ Analisando... aguarde.")
.size(22.0)
.strong(),
);
});
}
_ => {} _ => {}
} }
}); });
+52 -37
View File
@@ -1,6 +1,9 @@
use crate::application::usecases::importar_arquivo::{importar_csv, importar_xlsx}; use crate::application::usecases::importar_arquivo::{importar_csv, importar_xlsx};
use crate::application::usecases::executar_analise::{
expandir_analise, pre_analisar, series_com_intervalo_excessivo,
};
use crate::domain::entities::layout::{Layout, TipoArquivo}; use crate::domain::entities::layout::{Layout, TipoArquivo};
use crate::ui::app::{App, EstadoApp}; use crate::ui::app::{App, EstadoApp, ResultadoPendente};
use egui::{Context, Ui}; use egui::{Context, Ui};
/// Renderiza a tela de configuração de colunas. /// Renderiza a tela de configuração de colunas.
@@ -93,7 +96,7 @@ pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) {
ui.add_enabled_ui(valido && app.caminho_arquivo.is_some(), |ui| { ui.add_enabled_ui(valido && app.caminho_arquivo.is_some(), |ui| {
if ui.button("▶ Importar e Analisar").clicked() { if ui.button("▶ Importar e Analisar").clicked() {
executar_importacao(app); executar_importacao(app, ctx);
} }
}); });
@@ -368,49 +371,61 @@ fn verificar_duplicados(indices: &[(String, usize)], erros: &mut Vec<String>) {
} }
} }
fn executar_importacao(app: &mut App) { fn executar_importacao(app: &mut App, ctx: &egui::Context) {
let caminho = match &app.caminho_arquivo { let caminho = match &app.caminho_arquivo {
Some(p) => p.clone(), Some(p) => p.clone(),
None => return, None => return,
}; };
let resultado = match app.tipo_arquivo_atual.clone() { let tipo = app.tipo_arquivo_atual.clone();
TipoArquivo::Csv => { let layout_csv = app.layout_csv_atual.clone();
importar_csv(&caminho, &app.layout_csv_atual).map_err(|e| e.to_string()) let layout_xlsx = app.layout_xlsx_atual.clone();
}
TipoArquivo::Xlsx => { let (tx, rx) = std::sync::mpsc::channel();
importar_xlsx(&caminho, &app.layout_xlsx_atual).map_err(|e| e.to_string()) app.resultado_pendente = Some(rx);
} app.estado = EstadoApp::Analisando;
ctx.request_repaint();
std::thread::spawn(move || {
// 1. Importar arquivo
let res_importacao = match tipo {
TipoArquivo::Csv => importar_csv(&caminho, &layout_csv).map_err(|e| e.to_string()),
TipoArquivo::Xlsx => importar_xlsx(&caminho, &layout_xlsx).map_err(|e| e.to_string()),
}; };
match resultado { let res = match res_importacao {
Ok(res) => { Err(e) => ResultadoPendente::Erro(e),
if res.notas.is_empty() { Ok(importado) => {
app.exibir_aviso("Aviso", "Nenhuma nota válida encontrada no arquivo."); if importado.notas.is_empty() {
return; ResultadoPendente::Vazio
}
let avisos = res.avisos.clone();
app.notas_importadas = res.notas;
app.avisos_importacao = if avisos.tem_avisos() {
Some(avisos.clone())
} else { } else {
None let avisos = importado.avisos.clone();
let notas = importado.notas;
// 2. Pré-análise
let pre = pre_analisar(&notas);
let excessivos = series_com_intervalo_excessivo(&pre);
if !excessivos.is_empty() {
ResultadoPendente::AguardandoConfirmacao {
pre,
series_excessivas: excessivos,
avisos,
notas,
}
} else {
// 3. Expandir faltantes
let resultado = expandir_analise(pre, &notas);
ResultadoPendente::Concluido {
resultado,
avisos: Some(avisos),
notas: Some(notas),
}
}
}
}
}; };
// Executar análise let _ = tx.send(res);
app.executar_analise(); });
// Exibir avisos consolidados após análise
if let Some(av) = &app.avisos_importacao {
if av.tem_avisos() {
let linhas = av.linhas_para_exibir().join("\n");
app.exibir_aviso("Avisos de Importação", linhas);
}
}
}
Err(e) => {
app.exibir_erro(format!("Erro ao importar arquivo: {}", e));
}
}
} }
+3 -2
View File
@@ -57,8 +57,9 @@ pub fn renderizar_tabela_preview(ui: &mut egui::Ui, linhas: &[Vec<String>]) {
for linha in linhas { for linha in linhas {
for col in 0..num_colunas { for col in 0..num_colunas {
let celula = linha.get(col).map(|s| s.as_str()).unwrap_or(""); let celula = linha.get(col).map(|s| s.as_str()).unwrap_or("");
let texto = if celula.len() > 30 { let texto = if celula.chars().count() > 30 {
format!("{}...", &celula[..30]) let truncado: String = celula.chars().take(30).collect();
format!("{}...", truncado)
} else { } else {
celula.to_string() celula.to_string()
}; };