From 2c08f237e9c62909bc5ca791f2e27776b143d4af Mon Sep 17 00:00:00 2001 From: FelipeCN <57776624+felipecaninnovaes@users.noreply.github.com> Date: Tue, 3 Mar 2026 10:16:50 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20adiciona=20suporte=20para=20an=C3=A1lis?= =?UTF-8?q?e=20em=20background=20e=20processamento=20de=20resultados=20pen?= =?UTF-8?q?dentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/app.rs | 132 ++++++++++++++++++++++++- src/ui/screens/configuracao_colunas.rs | 87 +++++++++------- src/ui/screens/mod.rs | 5 +- 3 files changed, 181 insertions(+), 43 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index cd63064..20f3e26 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -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, + /// Notas importadas (None quando o App já as possui). + notas: Option>, + }, + /// 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, + }, + /// 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>>, + // Canal para receber resultado da análise em background + pub resultado_pendente: Option>, } 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, ¬as); + 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::>() + .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(), + ); + }); + } _ => {} } }); diff --git a/src/ui/screens/configuracao_colunas.rs b/src/ui/screens/configuracao_colunas.rs index f36b192..d56a8e8 100644 --- a/src/ui/screens/configuracao_colunas.rs +++ b/src/ui/screens/configuracao_colunas.rs @@ -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) { } } -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(); - match resultado { - Ok(res) => { - if res.notas.is_empty() { - app.exibir_aviso("Aviso", "Nenhuma nota válida encontrada no arquivo."); - return; - } + let (tx, rx) = std::sync::mpsc::channel(); + app.resultado_pendente = Some(rx); + app.estado = EstadoApp::Analisando; + ctx.request_repaint(); - let avisos = res.avisos.clone(); - app.notas_importadas = res.notas; - app.avisos_importacao = if avisos.tem_avisos() { - Some(avisos.clone()) - } else { - None - }; + 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()), + }; - // Executar análise - app.executar_analise(); + let res = match res_importacao { + Err(e) => ResultadoPendente::Erro(e), + Ok(importado) => { + if importado.notas.is_empty() { + ResultadoPendente::Vazio + } else { + let avisos = importado.avisos.clone(); + let notas = importado.notas; - // 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); + // 2. Pré-análise + let pre = pre_analisar(¬as); + 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, ¬as); + ResultadoPendente::Concluido { + resultado, + avisos: Some(avisos), + notas: Some(notas), + } + } } } - } - Err(e) => { - app.exibir_erro(format!("Erro ao importar arquivo: {}", e)); - } - } + }; + + let _ = tx.send(res); + }); } diff --git a/src/ui/screens/mod.rs b/src/ui/screens/mod.rs index 8811818..b292dc8 100644 --- a/src/ui/screens/mod.rs +++ b/src/ui/screens/mod.rs @@ -57,8 +57,9 @@ pub fn renderizar_tabela_preview(ui: &mut egui::Ui, linhas: &[Vec]) { 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() };