feat: adiciona suporte para análise em background e processamento de resultados pendentes
This commit is contained in:
+127
-5
@@ -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, ¬as);
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 => {
|
|
||||||
importar_xlsx(&caminho, &app.layout_xlsx_atual).map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match resultado {
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
Ok(res) => {
|
app.resultado_pendente = Some(rx);
|
||||||
if res.notas.is_empty() {
|
app.estado = EstadoApp::Analisando;
|
||||||
app.exibir_aviso("Aviso", "Nenhuma nota válida encontrada no arquivo.");
|
ctx.request_repaint();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let avisos = res.avisos.clone();
|
std::thread::spawn(move || {
|
||||||
app.notas_importadas = res.notas;
|
// 1. Importar arquivo
|
||||||
app.avisos_importacao = if avisos.tem_avisos() {
|
let res_importacao = match tipo {
|
||||||
Some(avisos.clone())
|
TipoArquivo::Csv => importar_csv(&caminho, &layout_csv).map_err(|e| e.to_string()),
|
||||||
} else {
|
TipoArquivo::Xlsx => importar_xlsx(&caminho, &layout_xlsx).map_err(|e| e.to_string()),
|
||||||
None
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// Executar análise
|
let res = match res_importacao {
|
||||||
app.executar_analise();
|
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
|
// 2. Pré-análise
|
||||||
if let Some(av) = &app.avisos_importacao {
|
let pre = pre_analisar(¬as);
|
||||||
if av.tem_avisos() {
|
let excessivos = series_com_intervalo_excessivo(&pre);
|
||||||
let linhas = av.linhas_para_exibir().join("\n");
|
|
||||||
app.exibir_aviso("Avisos de Importação", linhas);
|
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);
|
||||||
}
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user