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 rusqlite::Connection;
use std::path::PathBuf;
use std::sync::mpsc;
/// Estado global da aplicação.
pub enum EstadoApp {
@@ -39,6 +40,8 @@ pub enum EstadoApp {
},
/// Gerenciamento de layouts.
GerenciandoLayouts,
/// Análise em execução em background (thread separada).
Analisando,
}
/// Modal bloqueante a ser exibido sobre qualquer tela.
@@ -75,6 +78,29 @@ pub enum AcaoModal {
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.
pub struct App {
pub estado: EstadoApp,
@@ -111,6 +137,8 @@ pub struct App {
// Pré-visualização das primeiras linhas do arquivo
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 {
@@ -134,6 +162,7 @@ impl Default for App {
pagina_duplicatas: 0,
itens_por_pagina: 100,
preview_arquivo: None,
resultado_pendente: None,
}
}
}
@@ -148,6 +177,7 @@ impl App {
EstadoApp::ExibindoResultado(_) => 3,
EstadoApp::ConfirmandoIntervalo { .. } => 4,
EstadoApp::GerenciandoLayouts => 5,
EstadoApp::Analisando => 6,
}
}
@@ -295,12 +325,20 @@ impl App {
fn executar_acao_modal(&mut self, acao: AcaoModal) {
match acao {
AcaoModal::ConfirmarExpansaoFaltantes => {
// Retirar pre-análise do estado e expandir
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);
self.estado = EstadoApp::ExibindoResultado(resultado);
let notas = self.notas_importadas.clone();
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) => {
@@ -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.
pub fn executar_analise(&mut self) {
if self.notas_importadas.is_empty() {
@@ -399,7 +494,7 @@ impl App {
pub fn renderizar_breadcrumb(&self, ctx: &Context) {
let passo_ativo: usize = match self.tela_atual() {
0 | 1 => 1,
2 | 4 => 2,
2 | 4 | 6 => 2,
3 => 3,
_ => return, // GerenciandoLayouts: sem breadcrumb
};
@@ -429,6 +524,24 @@ impl App {
impl eframe::App for App {
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
self.renderizar_modal(ctx);
@@ -447,6 +560,15 @@ impl eframe::App for App {
3 => crate::ui::screens::resultado::renderizar(ui, ctx, self),
4 => crate::ui::screens::configuracao_colunas::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::executar_analise::{
expandir_analise, pre_analisar, series_com_intervalo_excessivo,
};
use crate::domain::entities::layout::{Layout, TipoArquivo};
use crate::ui::app::{App, EstadoApp};
use crate::ui::app::{App, EstadoApp, ResultadoPendente};
use egui::{Context, Ui};
/// 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| {
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 {
Some(p) => p.clone(),
None => return,
};
let resultado = match app.tipo_arquivo_atual.clone() {
TipoArquivo::Csv => {
importar_csv(&caminho, &app.layout_csv_atual).map_err(|e| e.to_string())
}
TipoArquivo::Xlsx => {
importar_xlsx(&caminho, &app.layout_xlsx_atual).map_err(|e| e.to_string())
}
let tipo = app.tipo_arquivo_atual.clone();
let layout_csv = app.layout_csv_atual.clone();
let layout_xlsx = app.layout_xlsx_atual.clone();
let (tx, rx) = std::sync::mpsc::channel();
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 {
Ok(res) => {
if res.notas.is_empty() {
app.exibir_aviso("Aviso", "Nenhuma nota válida encontrada no arquivo.");
return;
}
let avisos = res.avisos.clone();
app.notas_importadas = res.notas;
app.avisos_importacao = if avisos.tem_avisos() {
Some(avisos.clone())
let res = match res_importacao {
Err(e) => ResultadoPendente::Erro(e),
Ok(importado) => {
if importado.notas.is_empty() {
ResultadoPendente::Vazio
} 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
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));
}
}
let _ = tx.send(res);
});
}
+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 col in 0..num_colunas {
let celula = linha.get(col).map(|s| s.as_str()).unwrap_or("");
let texto = if celula.len() > 30 {
format!("{}...", &celula[..30])
let texto = if celula.chars().count() > 30 {
let truncado: String = celula.chars().take(30).collect();
format!("{}...", truncado)
} else {
celula.to_string()
};