Files
comparador-notas/src/ui/app.rs
T

382 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String>, 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<AcaoModal>,
}
#[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<Connection>,
pub banco_foi_recriado: bool,
// Dados em memória do arquivo atual
pub notas_importadas: Vec<Nota>,
pub caminho_arquivo: Option<PathBuf>,
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<String>,
// Layouts salvos no banco
pub layouts_salvos: Vec<Layout>,
// Modal
pub modal: Modal,
// Avisos da última importação
pub avisos_importacao: Option<ResumoAvisos>,
// 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<String>) {
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<String>, msg: impl Into<String>) {
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<String>,
msg: impl Into<String>,
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::<Vec<_>>()
.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),
_ => {}
}
});
}
}