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:
+340
@@ -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),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user