382 lines
12 KiB
Rust
382 lines
12 KiB
Rust
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),
|
||
_ => {}
|
||
}
|
||
});
|
||
}
|
||
}
|