use crate::application::usecases::{ executar_analise::{expandir_analise, pre_analisar, series_com_intervalo_excessivo}, importar_arquivo::{importar_csv, importar_xlsx, listar_abas_xlsx, ResultadoImportacao}, layouts::{ carregar_layout, excluir_layout, exportar_layout_json, importar_layout_json, listar_layouts, salvar_layout, }, }; use crate::domain::{ entities::{ layout::{Layout, LayoutCsv, LayoutXlsx, TipoArquivo}, nota::Nota, resultado_analise::{ResultadoAnalise, ResultadoPreAnalise}, }, errors::ResumoAvisos, }; use crate::infrastructure::{ pdf_generator::GenpdfGenerator, sqlite::{connection::abrir_banco, migrations::aplicar_migrations}, }; use egui::Context; use rusqlite::Connection; use std::path::PathBuf; /// Estado global da aplicação. pub enum EstadoApp { /// Tela inicial: importar arquivo. Importando, /// Aguardando seleção de aba de arquivo XLSX. SelecionandoAba { abas: Vec, caminho: PathBuf }, /// Configuração de colunas após importar arquivo. ConfigurandoColunas, /// Resultado pronto para exibição. ExibindoResultado(ResultadoAnalise), /// Aguardando confirmação do usuário para expandir faltantes. ConfirmandoIntervalo { pre: ResultadoPreAnalise, series_excessivas: Vec<(String, u64)>, }, /// Gerenciamento de layouts. GerenciandoLayouts, } /// Modal bloqueante a ser exibido sobre qualquer tela. #[derive(Default)] pub struct Modal { pub visivel: bool, pub titulo: String, pub mensagem: String, pub tipo: TipoModal, /// Para modal de confirmação, a ação ao confirmar. pub acao_confirmacao: Option, } #[derive(Default, Clone, PartialEq)] pub enum TipoModal { #[default] Informacao, Aviso, Erro, Confirmacao, } #[derive(Clone)] pub enum AcaoModal { ConfirmarExpansaoFaltantes, ConfirmarExclusaoLayout(i64), SobrescreverLayout, ConfirmarNovaAnalise, } /// Struct principal da aplicação egui. pub struct App { pub estado: EstadoApp, pub conn: Option, pub banco_foi_recriado: bool, // Dados em memória do arquivo atual pub notas_importadas: Vec, pub caminho_arquivo: Option, pub nome_arquivo: String, // Configuração de layout atual pub tipo_arquivo_atual: TipoArquivo, pub layout_csv_atual: LayoutCsv, pub layout_xlsx_atual: LayoutXlsx, pub nome_layout_atual: String, // Abas XLSX disponíveis (após seleção do arquivo) pub abas_xlsx: Vec, // Layouts salvos no banco pub layouts_salvos: Vec, // Modal pub modal: Modal, // Avisos da última importação pub avisos_importacao: Option, // Paginação (para tela de resultado) pub pagina_faltantes: usize, pub pagina_duplicatas: usize, pub itens_por_pagina: usize, } impl Default for App { fn default() -> Self { Self { estado: EstadoApp::Importando, conn: None, banco_foi_recriado: false, notas_importadas: Vec::new(), caminho_arquivo: None, nome_arquivo: String::new(), tipo_arquivo_atual: TipoArquivo::Csv, layout_csv_atual: LayoutCsv::default(), layout_xlsx_atual: LayoutXlsx::default(), nome_layout_atual: String::new(), abas_xlsx: Vec::new(), layouts_salvos: Vec::new(), modal: Modal::default(), avisos_importacao: None, pagina_faltantes: 0, pagina_duplicatas: 0, itens_por_pagina: 100, } } } impl App { /// Retorna um id numérico da tela atual para evitar borrow conflicts. pub fn tela_atual(&self) -> u8 { match &self.estado { EstadoApp::Importando => 0, EstadoApp::SelecionandoAba { .. } => 1, EstadoApp::ConfigurandoColunas => 2, EstadoApp::ExibindoResultado(_) => 3, EstadoApp::ConfirmandoIntervalo { .. } => 4, EstadoApp::GerenciandoLayouts => 5, } } /// Inicializa o banco de dados e carrega os layouts salvos. pub fn inicializar(&mut self) { match abrir_banco() { Ok((conn, recriado)) => { self.banco_foi_recriado = recriado; if let Err(e) = aplicar_migrations(&conn) { self.exibir_erro(format!("Erro ao inicializar banco: {}", e)); return; } match listar_layouts(&conn) { Ok(layouts) => self.layouts_salvos = layouts, Err(e) => self.exibir_erro(format!("Erro ao carregar layouts: {}", e)), } self.conn = Some(conn); if recriado { self.exibir_aviso( "Banco de dados corrompido", "O banco de dados estava corrompido e foi recriado. Os layouts anteriores foram arquivados em config.db.bak.", ); } } Err(e) => { self.exibir_erro(format!("Erro crítico ao abrir banco de dados: {}", e)); } } } pub fn exibir_erro(&mut self, msg: impl Into) { self.modal = Modal { visivel: true, titulo: "Erro".to_string(), mensagem: msg.into(), tipo: TipoModal::Erro, acao_confirmacao: None, }; } pub fn exibir_aviso(&mut self, titulo: impl Into, msg: impl Into) { self.modal = Modal { visivel: true, titulo: titulo.into(), mensagem: msg.into(), tipo: TipoModal::Aviso, acao_confirmacao: None, }; } pub fn exibir_confirmacao( &mut self, titulo: impl Into, msg: impl Into, acao: AcaoModal, ) { self.modal = Modal { visivel: true, titulo: titulo.into(), mensagem: msg.into(), tipo: TipoModal::Confirmacao, acao_confirmacao: Some(acao), }; } /// Recarrega a lista de layouts do banco. pub fn recarregar_layouts(&mut self) { if let Some(conn) = &self.conn { match listar_layouts(conn) { Ok(layouts) => self.layouts_salvos = layouts, Err(e) => self.exibir_erro(format!("Erro ao carregar layouts: {}", e)), } } } /// Renderiza o modal bloqueante, se visível. pub fn renderizar_modal(&mut self, ctx: &Context) { if !self.modal.visivel { return; } let titulo = self.modal.titulo.clone(); let mensagem = self.modal.mensagem.clone(); let tipo = self.modal.tipo.clone(); let acao = self.modal.acao_confirmacao.clone(); egui::Window::new(&titulo) .collapsible(false) .resizable(false) .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) .show(ctx, |ui| { ui.add_space(8.0); ui.label(&mensagem); ui.add_space(12.0); ui.horizontal(|ui| { if tipo == TipoModal::Confirmacao { if ui.button("✔ Confirmar").clicked() { self.modal.visivel = false; if let Some(acao) = acao.clone() { self.executar_acao_modal(acao); } } if ui.button("✖ Cancelar").clicked() { self.modal.visivel = false; } } else { if ui.button("OK").clicked() { self.modal.visivel = false; } } }); }); } 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) { let resultado = expandir_analise(pre, &self.notas_importadas); self.estado = EstadoApp::ExibindoResultado(resultado); } } AcaoModal::ConfirmarExclusaoLayout(id) => { if let Some(conn) = &self.conn { if let Err(e) = excluir_layout(conn, id) { self.exibir_erro(e); } else { self.recarregar_layouts(); } } } AcaoModal::SobrescreverLayout => {} // Handled inline in layouts screen AcaoModal::ConfirmarNovaAnalise => { self.notas_importadas.clear(); self.estado = EstadoApp::Importando; } } } /// Executa a análise com as notas importadas. pub fn executar_analise(&mut self) { if self.notas_importadas.is_empty() { self.exibir_aviso("Aviso", "Nenhuma nota importada para analisar."); return; } let pre = pre_analisar(&self.notas_importadas); let excessivos = series_com_intervalo_excessivo(&pre); if !excessivos.is_empty() { let msg = excessivos .iter() .map(|(serie, count)| { format!("Série {}: intervalo de {} faltantes detectado", serie, count) }) .collect::>() .join("\n"); let acao = AcaoModal::ConfirmarExpansaoFaltantes; self.estado = EstadoApp::ConfirmandoIntervalo { pre, series_excessivas: excessivos, }; 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 ), acao, ); } else { let resultado = expandir_analise(pre, &self.notas_importadas); self.estado = EstadoApp::ExibindoResultado(resultado); } } } impl App { /// Renderiza o breadcrumb de etapas no topo (exceto na tela de layouts). pub fn renderizar_breadcrumb(&self, ctx: &Context) { let passo_ativo: usize = match self.tela_atual() { 0 | 1 => 1, 2 | 4 => 2, 3 => 3, _ => return, // GerenciandoLayouts: sem breadcrumb }; egui::TopBottomPanel::top("breadcrumb").show(ctx, |ui| { ui.add_space(4.0); ui.horizontal(|ui| { for (i, label) in ["① Arquivo", "② Colunas", "③ Resultado"].iter().enumerate() { let n = i + 1; let texto = egui::RichText::new(*label); if n == passo_ativo { ui.label(texto.strong()); } else if n < passo_ativo { ui.label(texto); } else { ui.label(texto.weak()); } if n < 3 { ui.label(egui::RichText::new(" ›").weak()); } } }); ui.add_space(4.0); }); } } impl eframe::App for App { fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { // Sempre renderizar modal por cima de tudo self.renderizar_modal(ctx); // Breadcrumb de etapas no topo self.renderizar_breadcrumb(ctx); // Determinar qual tela exibir sem borrar self.estado let tela_id = self.tela_atual(); // Renderizar a tela atual egui::CentralPanel::default().show(ctx, |ui| { match tela_id { 0 => crate::ui::screens::import::renderizar(ui, ctx, self), 1 => crate::ui::screens::import::renderizar_selecao_aba(ui, ctx, self), 2 => crate::ui::screens::configuracao_colunas::renderizar(ui, ctx, self), 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), _ => {} } }); } }