feat: implement main application structure with UI and layout management

- Added main application logic in `src/ui/app.rs` to handle state and modal dialogs.
- Created module structure for UI components in `src/ui/mod.rs`.
- Implemented column configuration screen in `src/ui/screens/configuracao_colunas.rs`.
- Developed file import screen in `src/ui/screens/import.rs` for CSV and XLSX files.
- Added layout management screen in `src/ui/screens/layouts.rs` for saving and importing layouts.
- Created result display screen in `src/ui/screens/resultado.rs` to show analysis results.
- Introduced modular organization for screens in `src/ui/screens/mod.rs`.
This commit is contained in:
2026-03-02 22:14:02 -03:00
parent 31cd94907a
commit e64988a139
36 changed files with 9166 additions and 2 deletions
+340
View File
@@ -0,0 +1,340 @@
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,
}
/// 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
}
}
/// 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 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);
// 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),
_ => {}
}
});
}
}