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
+295
View File
@@ -0,0 +1,295 @@
use crate::application::usecases::importar_arquivo::{importar_csv, importar_xlsx};
use crate::domain::entities::layout::TipoArquivo;
use crate::ui::app::{App, EstadoApp};
use egui::{Context, Ui};
/// Renderiza a tela de configuração de colunas.
pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) {
ui.heading("Configuração de Colunas");
ui.add_space(8.0);
if let Some(caminho) = &app.caminho_arquivo.clone() {
ui.label(format!("Arquivo: {}", caminho.display()));
}
ui.add_space(12.0);
match app.tipo_arquivo_atual.clone() {
TipoArquivo::Csv => renderizar_csv(ui, app),
TipoArquivo::Xlsx => renderizar_xlsx(ui, app),
}
ui.add_space(16.0);
ui.separator();
ui.add_space(8.0);
// Validação e botões de ação
let (valido, erros) = validar_config(app);
if !erros.is_empty() {
for erro in &erros {
ui.colored_label(egui::Color32::RED, format!("{}", erro));
}
ui.add_space(8.0);
}
ui.horizontal(|ui| {
if ui.button("← Voltar").clicked() {
app.estado = EstadoApp::Importando;
}
ui.add_enabled_ui(valido && app.caminho_arquivo.is_some(), |ui| {
if ui.button("▶ Importar e Analisar").clicked() {
executar_importacao(app);
}
});
});
}
fn renderizar_csv(ui: &mut Ui, app: &mut App) {
ui.group(|ui| {
ui.label("Configurações CSV");
ui.add_space(4.0);
// Delimitador
ui.horizontal(|ui| {
ui.label("Delimitador:");
let delim_str = match app.layout_csv_atual.delimitador {
',' => "Vírgula (,)",
';' => "Ponto e vírgula (;)",
'\t' => "Tabulação (Tab)",
_ => "Outro",
};
egui::ComboBox::from_id_salt("combo_delimitador")
.selected_text(delim_str)
.show_ui(ui, |ui| {
if ui.selectable_label(app.layout_csv_atual.delimitador == ',', "Vírgula (,)").clicked() {
app.layout_csv_atual.delimitador = ',';
}
if ui.selectable_label(app.layout_csv_atual.delimitador == ';', "Ponto e vírgula (;)").clicked() {
app.layout_csv_atual.delimitador = ';';
}
if ui.selectable_label(app.layout_csv_atual.delimitador == '\t', "Tabulação (Tab)").clicked() {
app.layout_csv_atual.delimitador = '\t';
}
});
});
// Encoding
ui.horizontal(|ui| {
ui.label("Encoding:");
egui::ComboBox::from_id_salt("combo_encoding")
.selected_text(&app.layout_csv_atual.encoding)
.show_ui(ui, |ui| {
if ui.selectable_label(app.layout_csv_atual.encoding == "utf-8", "UTF-8").clicked() {
app.layout_csv_atual.encoding = "utf-8".to_string();
}
if ui.selectable_label(app.layout_csv_atual.encoding == "windows-1252", "Windows-1252 (Latin-1)").clicked() {
app.layout_csv_atual.encoding = "windows-1252".to_string();
}
});
});
// Linha cabeçalho
ui.horizontal(|ui| {
ui.label("Linha do cabeçalho (0 = sem cabeçalho):");
let mut val = app.layout_csv_atual.linha_cabecalho;
ui.add(egui::DragValue::new(&mut val).range(0..=100));
app.layout_csv_atual.linha_cabecalho = val;
});
});
ui.add_space(8.0);
ui.group(|ui| {
ui.label("Mapeamento de Colunas (índice base 0)");
ui.add_space(4.0);
campo_indice(ui, "Número (obrigatório):", &mut app.layout_csv_atual.indice_numero);
campo_indice(ui, "Série (obrigatório):", &mut app.layout_csv_atual.indice_serie);
campo_indice_opcional(ui, "Valor (opcional):", &mut app.layout_csv_atual.indice_valor);
campo_indice_opcional(ui, "Data (opcional):", &mut app.layout_csv_atual.indice_data);
});
}
fn renderizar_xlsx(ui: &mut Ui, app: &mut App) {
ui.group(|ui| {
ui.label("Configurações XLSX");
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("Aba:");
if app.abas_xlsx.is_empty() {
ui.text_edit_singleline(&mut app.layout_xlsx_atual.aba);
} else {
let aba_atual = app.layout_xlsx_atual.aba.clone();
egui::ComboBox::from_id_salt("combo_aba")
.selected_text(&aba_atual)
.show_ui(ui, |ui| {
for aba in &app.abas_xlsx.clone() {
if ui.selectable_label(aba_atual == *aba, aba).clicked() {
app.layout_xlsx_atual.aba = aba.clone();
}
}
});
}
});
});
ui.add_space(8.0);
ui.group(|ui| {
ui.label("Mapeamento de Colunas (formato LetraLinha, ex: B3)");
ui.add_space(4.0);
campo_letra_linha(ui, "Número (obrigatório):", &mut app.layout_xlsx_atual.pos_numero);
campo_letra_linha(ui, "Série (obrigatório):", &mut app.layout_xlsx_atual.pos_serie);
campo_letra_linha_opcional(ui, "Valor (opcional):", &mut app.layout_xlsx_atual.pos_valor);
campo_letra_linha_opcional(ui, "Data (opcional):", &mut app.layout_xlsx_atual.pos_data);
});
}
fn campo_indice(ui: &mut Ui, label: &str, valor: &mut usize) {
ui.horizontal(|ui| {
ui.label(label);
ui.add(egui::DragValue::new(valor).range(0..=999usize));
});
}
fn campo_indice_opcional(ui: &mut Ui, label: &str, valor: &mut Option<usize>) {
ui.horizontal(|ui| {
let mut ativo = valor.is_some();
if ui.checkbox(&mut ativo, label).changed() {
*valor = if ativo { Some(0) } else { None };
}
if let Some(v) = valor {
ui.add(egui::DragValue::new(v).range(0..=999usize));
}
});
}
fn campo_letra_linha(ui: &mut Ui, label: &str, valor: &mut String) {
ui.horizontal(|ui| {
ui.label(label);
ui.text_edit_singleline(valor);
});
}
fn campo_letra_linha_opcional(ui: &mut Ui, label: &str, valor: &mut Option<String>) {
ui.horizontal(|ui| {
let mut ativo = valor.is_some();
if ui.checkbox(&mut ativo, label).changed() {
*valor = if ativo { Some(String::new()) } else { None };
}
if let Some(v) = valor {
ui.text_edit_singleline(v);
}
});
}
/// Valida a configuração atual. Retorna (é_válido, lista_de_erros).
fn validar_config(app: &App) -> (bool, Vec<String>) {
let mut erros = Vec::new();
match &app.tipo_arquivo_atual {
TipoArquivo::Csv => {
let c = &app.layout_csv_atual;
// Verificar índices duplicados
let mut indices: Vec<(String, usize)> = vec![
("Numero".to_string(), c.indice_numero),
("Serie".to_string(), c.indice_serie),
];
if let Some(v) = c.indice_valor {
indices.push(("Valor".to_string(), v));
}
if let Some(d) = c.indice_data {
indices.push(("Data".to_string(), d));
}
verificar_duplicados(&indices, &mut erros);
}
TipoArquivo::Xlsx => {
let c = &app.layout_xlsx_atual;
if c.aba.trim().is_empty() {
erros.push("Selecione uma aba".to_string());
}
if c.pos_numero.trim().is_empty() {
erros.push("Posição do campo Numero é obrigatória".to_string());
} else if crate::infrastructure::xlsx_reader::parsear_letra_linha(&c.pos_numero)
.is_none()
{
erros.push(format!("Posição Numero inválida: '{}'", c.pos_numero));
}
if c.pos_serie.trim().is_empty() {
erros.push("Posição do campo Serie é obrigatória".to_string());
} else if crate::infrastructure::xlsx_reader::parsear_letra_linha(&c.pos_serie)
.is_none()
{
erros.push(format!("Posição Serie inválida: '{}'", c.pos_serie));
}
}
}
let valido = erros.is_empty();
(valido, erros)
}
fn verificar_duplicados(indices: &[(String, usize)], erros: &mut Vec<String>) {
for i in 0..indices.len() {
for j in (i + 1)..indices.len() {
if indices[i].1 == indices[j].1 {
erros.push(format!(
"Campos '{}' e '{}' mapeados para o mesmo índice {}",
indices[i].0, indices[j].0, indices[i].1
));
}
}
}
}
fn executar_importacao(app: &mut App) {
let caminho = match &app.caminho_arquivo {
Some(p) => p.clone(),
None => return,
};
let resultado = match app.tipo_arquivo_atual.clone() {
TipoArquivo::Csv => {
importar_csv(&caminho, &app.layout_csv_atual)
.map_err(|e| e.to_string())
}
TipoArquivo::Xlsx => {
importar_xlsx(&caminho, &app.layout_xlsx_atual)
.map_err(|e| e.to_string())
}
};
match resultado {
Ok(res) => {
if res.notas.is_empty() {
app.exibir_aviso("Aviso", "Nenhuma nota válida encontrada no arquivo.");
return;
}
let avisos = res.avisos.clone();
app.notas_importadas = res.notas;
app.avisos_importacao = if avisos.tem_avisos() {
Some(avisos.clone())
} else {
None
};
// Executar análise
app.executar_analise();
// Exibir avisos consolidados após análise
if let Some(av) = &app.avisos_importacao {
if av.tem_avisos() {
let linhas = av.linhas_para_exibir().join("\n");
app.exibir_aviso("Avisos de Importação", linhas);
}
}
}
Err(e) => {
app.exibir_erro(format!("Erro ao importar arquivo: {}", e));
}
}
}
+169
View File
@@ -0,0 +1,169 @@
use crate::application::usecases::importar_arquivo::{importar_csv, importar_xlsx, listar_abas_xlsx};
use crate::domain::entities::layout::TipoArquivo;
use crate::ui::app::{App, EstadoApp};
use egui::{Context, Ui};
use std::path::PathBuf;
/// Renderiza a tela de importação de arquivos.
pub fn renderizar(ui: &mut Ui, _ctx: &Context, app: &mut App) {
ui.heading("Comparador de Notas — Importar Arquivo");
ui.add_space(16.0);
// --- Seleção de arquivo ---
ui.group(|ui| {
ui.label("Arquivo:");
ui.horizontal(|ui| {
let nome = if app.nome_arquivo.is_empty() {
"Nenhum arquivo selecionado".to_string()
} else {
app.nome_arquivo.clone()
};
ui.label(nome);
if ui.button("📂 Selecionar arquivo...").clicked() {
if let Some(caminho) = rfd::FileDialog::new()
.add_filter("Planilhas", &["csv", "xlsx"])
.pick_file()
{
on_arquivo_selecionado(app, caminho);
}
}
});
});
ui.add_space(8.0);
// Coletar infos dos layouts antecipadamente para evitar borrow duplo
let tipo_atual = app.tipo_arquivo_atual.clone();
let opcoes_layout: Vec<(i64, String)> = app
.layouts_salvos
.iter()
.filter(|l| l.tipo() == tipo_atual)
.filter_map(|l| l.id().map(|id| (id, l.nome().to_string())))
.collect();
let nome_layout_atual = app.nome_layout_atual.clone();
ui.horizontal(|ui| {
ui.label("Layout:");
egui::ComboBox::from_id_salt("combo_layouts_import")
.selected_text(if nome_layout_atual.is_empty() {
"— Selecionar layout —"
} else {
&nome_layout_atual
})
.show_ui(ui, |ui| {
for (id, nome) in &opcoes_layout {
if ui.selectable_label(nome_layout_atual == *nome, nome.as_str()).clicked() {
app.nome_layout_atual = nome.clone();
if let Some(layout) = app.layouts_salvos.iter().find(|l| l.id() == Some(*id)) {
let layout = layout.clone();
aplicar_layout(app, &layout);
}
}
}
});
if ui.button("⚙ Gerenciar Layouts").clicked() {
app.estado = EstadoApp::GerenciandoLayouts;
}
});
ui.add_space(16.0);
if app.caminho_arquivo.is_some() {
if ui.button("▶ Configurar Colunas").clicked() {
app.estado = EstadoApp::ConfigurandoColunas;
}
}
}
/// Renderiza a tela de seleção de aba (XLSX).
pub fn renderizar_selecao_aba(ui: &mut Ui, _ctx: &Context, app: &mut App) {
ui.heading("Selecionar Aba da Planilha");
ui.add_space(16.0);
let (abas, caminho) = match &app.estado {
EstadoApp::SelecionandoAba { abas, caminho } => (abas.clone(), caminho.clone()),
_ => return,
};
ui.label(format!("Arquivo: {}", caminho.display()));
ui.add_space(8.0);
ui.label("Selecione a aba a processar:");
let aba_atual = app.layout_xlsx_atual.aba.clone();
for aba in &abas {
if ui.selectable_label(aba_atual == *aba, aba).clicked() {
app.layout_xlsx_atual.aba = aba.clone();
}
}
ui.add_space(12.0);
if !app.layout_xlsx_atual.aba.is_empty() {
if ui.button("▶ Configurar Colunas").clicked() {
app.nome_arquivo = caminho
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
app.caminho_arquivo = Some(caminho);
app.estado = EstadoApp::ConfigurandoColunas;
}
}
if ui.button("← Voltar").clicked() {
app.estado = EstadoApp::Importando;
}
}
fn on_arquivo_selecionado(app: &mut App, caminho: PathBuf) {
let extensao = caminho
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
app.nome_arquivo = caminho
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
match extensao.as_str() {
"csv" => {
app.tipo_arquivo_atual = TipoArquivo::Csv;
app.caminho_arquivo = Some(caminho);
app.notas_importadas.clear();
}
"xlsx" => {
app.tipo_arquivo_atual = TipoArquivo::Xlsx;
match listar_abas_xlsx(&caminho) {
Ok(info) => {
app.abas_xlsx = info.abas.clone();
app.estado = EstadoApp::SelecionandoAba {
abas: info.abas,
caminho: caminho.clone(),
};
app.notas_importadas.clear();
}
Err(e) => {
app.exibir_erro(format!("Erro ao ler abas do arquivo: {}", e));
}
}
}
_ => {
app.exibir_erro("Formato de arquivo não suportado. Use CSV ou XLSX.");
}
}
}
fn aplicar_layout(app: &mut App, layout: &crate::domain::entities::layout::Layout) {
match layout {
crate::domain::entities::layout::Layout::Csv { config, .. } => {
app.layout_csv_atual = config.clone();
app.tipo_arquivo_atual = TipoArquivo::Csv;
}
crate::domain::entities::layout::Layout::Xlsx { config, .. } => {
app.layout_xlsx_atual = config.clone();
app.tipo_arquivo_atual = TipoArquivo::Xlsx;
}
}
}
+213
View File
@@ -0,0 +1,213 @@
use crate::application::usecases::layouts::{
exportar_layout_json, importar_layout_json, salvar_layout,
};
use crate::domain::entities::layout::{Layout, LayoutCsv, LayoutXlsx, TipoArquivo};
use crate::domain::errors::ErroLayout;
use crate::ui::app::{AcaoModal, App, EstadoApp};
use egui::{Context, Ui};
/// Renderiza a tela de gerenciamento de layouts.
pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) {
ui.heading("Gerenciar Layouts");
ui.add_space(8.0);
ui.horizontal(|ui| {
if ui.button("← Voltar").clicked() {
app.estado = EstadoApp::Importando;
}
});
ui.add_space(12.0);
ui.separator();
// Salvar layout atual
ui.add_space(8.0);
ui.group(|ui| {
ui.label("Salvar Layout Atual");
ui.horizontal(|ui| {
ui.label("Nome:");
ui.text_edit_singleline(&mut app.nome_layout_atual);
if ui.button("💾 Salvar").clicked() {
salvar_layout_atual(app);
}
});
});
ui.add_space(12.0);
ui.separator();
ui.add_space(8.0);
// Layouts CSV
let layouts_csv: Vec<_> = app
.layouts_salvos
.iter()
.filter(|l| l.tipo() == TipoArquivo::Csv)
.cloned()
.collect();
let layouts_xlsx: Vec<_> = app
.layouts_salvos
.iter()
.filter(|l| l.tipo() == TipoArquivo::Xlsx)
.cloned()
.collect();
egui::ScrollArea::vertical().show(ui, |ui| {
renderizar_secao_layouts(ui, ctx, app, "Layouts CSV", &layouts_csv);
ui.add_space(12.0);
renderizar_secao_layouts(ui, ctx, app, "Layouts XLSX", &layouts_xlsx);
ui.add_space(16.0);
ui.separator();
ui.add_space(8.0);
// Importar de JSON
ui.horizontal(|ui| {
ui.label("Importar layout de arquivo JSON:");
if ui.button("📥 Importar JSON").clicked() {
if let Some(caminho) = rfd::FileDialog::new()
.add_filter("JSON", &["json"])
.pick_file()
{
match std::fs::read_to_string(&caminho) {
Ok(conteudo) => importar_json(app, &conteudo),
Err(e) => {
app.exibir_erro(format!("Erro ao ler arquivo JSON: {}", e));
}
}
}
}
});
});
}
fn renderizar_secao_layouts(
ui: &mut Ui,
ctx: &Context,
app: &mut App,
titulo: &str,
layouts: &[Layout],
) {
ui.label(egui::RichText::new(titulo).strong());
ui.add_space(4.0);
if layouts.is_empty() {
ui.label("(nenhum layout salvo)");
return;
}
for layout in layouts {
ui.horizontal(|ui| {
ui.label(layout.nome());
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
// Excluir
if let Some(id) = layout.id() {
if ui.button("🗑 Excluir").clicked() {
app.exibir_confirmacao(
"Confirmar exclusão",
format!("Deseja excluir o layout '{}'?", layout.nome()),
AcaoModal::ConfirmarExclusaoLayout(id),
);
}
// Exportar
if ui.button("📤 Exportar JSON").clicked() {
match exportar_layout_json(layout) {
Ok((conteudo, nome_sugerido)) => {
if let Some(caminho) = rfd::FileDialog::new()
.set_file_name(&nome_sugerido)
.add_filter("JSON", &["json"])
.save_file()
{
if let Err(e) = std::fs::write(&caminho, &conteudo) {
app.exibir_erro(format!("Erro ao salvar JSON: {}", e));
}
}
}
Err(e) => {
app.exibir_erro(format!("Erro ao exportar layout: {}", e));
}
}
}
// Carregar
if ui.button("📂 Carregar").clicked() {
aplicar_layout(app, layout);
app.nome_layout_atual = layout.nome().to_string();
app.estado = EstadoApp::ConfigurandoColunas;
}
}
});
});
}
}
fn salvar_layout_atual(app: &mut App) {
if app.nome_layout_atual.trim().is_empty() {
app.exibir_aviso("Nome inválido", "Informe um nome para o layout.");
return;
}
let layout = match app.tipo_arquivo_atual.clone() {
TipoArquivo::Csv => Layout::Csv {
id: None,
nome: app.nome_layout_atual.trim().to_string(),
config: app.layout_csv_atual.clone(),
},
TipoArquivo::Xlsx => Layout::Xlsx {
id: None,
nome: app.nome_layout_atual.trim().to_string(),
config: app.layout_xlsx_atual.clone(),
},
};
if let Some(conn) = &app.conn {
match salvar_layout(conn, &layout) {
Ok(_) => {
app.recarregar_layouts();
app.exibir_aviso("Sucesso", "Layout salvo com sucesso.");
}
Err(e) => {
app.exibir_erro(format!("Erro ao salvar layout: {}", e));
}
}
}
}
fn importar_json(app: &mut App, conteudo: &str) {
if let Some(conn) = &app.conn {
match importar_layout_json(conn, conteudo, false, None) {
Ok(_) => {
app.recarregar_layouts();
app.exibir_aviso("Sucesso", "Layout importado com sucesso.");
}
Err(ErroLayout::NomeConflitante(nome)) => {
// Exibir opções: sobrescrever ou cancelar
app.exibir_aviso(
"Conflito de nome",
format!(
"Já existe um layout com o nome '{}'. Use 'Salvar com novo nome' ou cancele a importação.",
nome
),
);
// TODO: implementar fluxo completo de sobrescrever com entrada de novo nome
}
Err(e) => {
app.exibir_erro(format!("Erro ao importar layout: {}", e));
}
}
}
}
fn aplicar_layout(app: &mut App, layout: &Layout) {
match layout {
Layout::Csv { config, .. } => {
app.layout_csv_atual = config.clone();
app.tipo_arquivo_atual = TipoArquivo::Csv;
}
Layout::Xlsx { config, .. } => {
app.layout_xlsx_atual = config.clone();
app.tipo_arquivo_atual = TipoArquivo::Xlsx;
}
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod configuracao_colunas;
pub mod import;
pub mod layouts;
pub mod resultado;
+259
View File
@@ -0,0 +1,259 @@
use crate::application::usecases::exportar_pdf::exportar_pdf;
use crate::domain::{
entities::resultado_analise::ResultadoAnalise,
services::parser_monetario::formatar_valor_br,
};
use crate::infrastructure::pdf_generator::GenpdfGenerator;
use crate::ui::app::{App, EstadoApp};
use egui::{Context, Ui};
const OPCOES_PAGINA: &[usize] = &[50, 100, 200, 1000];
/// Renderiza a tela de resultados.
pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) {
// Extrair resultado do estado (sem mover)
let resultado = match &app.estado {
EstadoApp::ExibindoResultado(r) => r.clone(),
_ => return,
};
ui.heading("Resultado da Análise");
ui.add_space(8.0);
ui.horizontal(|ui| {
if ui.button("← Nova Análise").clicked() {
app.estado = EstadoApp::Importando;
app.notas_importadas.clear();
return;
}
if ui.button("⚙ Reconfigurar Colunas").clicked() {
app.estado = EstadoApp::ConfigurandoColunas;
return;
}
if ui.button("📄 Exportar PDF").clicked() {
exportar_para_pdf(app, &resultado);
}
});
ui.add_space(8.0);
// Controle de itens por página
ui.horizontal(|ui| {
ui.label("Itens por página:");
for &opcao in OPCOES_PAGINA {
if ui
.selectable_label(app.itens_por_pagina == opcao, opcao.to_string())
.clicked()
{
app.itens_por_pagina = opcao;
app.pagina_faltantes = 0;
app.pagina_duplicatas = 0;
}
}
});
ui.add_space(12.0);
ui.separator();
egui::ScrollArea::vertical().show(ui, |ui| {
// Totais
renderizar_totais(ui, &resultado);
ui.add_space(12.0);
ui.separator();
// Faltantes
renderizar_faltantes(ui, app, &resultado);
ui.add_space(12.0);
ui.separator();
// Duplicatas
renderizar_duplicatas(ui, app, &resultado);
});
}
fn renderizar_totais(ui: &mut Ui, resultado: &ResultadoAnalise) {
ui.label(egui::RichText::new("Totais").heading().strong());
ui.add_space(4.0);
ui.label(format!(
"Total Geral: R$ {}",
formatar_valor_br(&resultado.soma_total)
));
let mut series: Vec<&String> = resultado.soma_por_serie.keys().collect();
series.sort();
for serie in series {
let soma = &resultado.soma_por_serie[serie];
let total_notas = resultado.total_por_serie.get(serie).copied().unwrap_or(0);
ui.label(format!(
" Série {}: {} nota(s) — R$ {}",
serie,
total_notas,
formatar_valor_br(soma)
));
}
}
fn renderizar_faltantes(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalise) {
let total_faltantes = resultado.total_faltantes();
ui.label(
egui::RichText::new(format!(
"Notas Faltantes ({} total)",
total_faltantes
))
.heading()
.strong(),
);
ui.add_space(4.0);
if total_faltantes == 0 {
ui.label("✔ Nenhuma nota faltante.");
return;
}
let mut series: Vec<&String> = resultado.faltantes_por_serie.keys().collect();
series.sort();
for serie in series {
let faltantes = &resultado.faltantes_por_serie[serie];
if faltantes.is_empty() {
continue;
}
ui.label(format!(
"Série {}{} faltante(s):",
serie,
faltantes.len()
));
// Paginação
let total_paginas = (faltantes.len() + app.itens_por_pagina - 1) / app.itens_por_pagina;
if app.pagina_faltantes >= total_paginas {
app.pagina_faltantes = 0;
}
let inicio = app.pagina_faltantes * app.itens_por_pagina;
let fim = (inicio + app.itens_por_pagina).min(faltantes.len());
for numero in &faltantes[inicio..fim] {
ui.label(format!("{}", numero));
}
if total_paginas > 1 {
ui.horizontal(|ui| {
if ui.button("").clicked() && app.pagina_faltantes > 0 {
app.pagina_faltantes -= 1;
}
ui.label(format!(
"Página {} / {}",
app.pagina_faltantes + 1,
total_paginas
));
if ui.button("").clicked() && app.pagina_faltantes + 1 < total_paginas {
app.pagina_faltantes += 1;
}
});
}
}
}
fn renderizar_duplicatas(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalise) {
let total_dup = resultado.total_duplicatas();
ui.label(
egui::RichText::new(format!(
"Notas Duplicadas ({} grupo(s))",
total_dup
))
.heading()
.strong(),
);
ui.add_space(4.0);
if total_dup == 0 {
ui.label("✔ Nenhuma nota duplicada.");
return;
}
let mut series: Vec<&String> = resultado.duplicadas_por_serie.keys().collect();
series.sort();
for serie in series {
let duplicatas = &resultado.duplicadas_por_serie[serie];
if duplicatas.is_empty() {
continue;
}
ui.label(format!(
"Série {}{} grupo(s) duplicado(s):",
serie,
duplicatas.len()
));
let total_paginas =
(duplicatas.len() + app.itens_por_pagina - 1) / app.itens_por_pagina;
if app.pagina_duplicatas >= total_paginas {
app.pagina_duplicatas = 0;
}
let inicio = app.pagina_duplicatas * app.itens_por_pagina;
let fim = (inicio + app.itens_por_pagina).min(duplicatas.len());
for (numero, count) in &duplicatas[inicio..fim] {
ui.label(format!(
" • NF {} / Série {}{} ocorrências",
numero, serie, count
));
}
if total_paginas > 1 {
ui.horizontal(|ui| {
if ui.button("").clicked() && app.pagina_duplicatas > 0 {
app.pagina_duplicatas -= 1;
}
ui.label(format!(
"Página {} / {}",
app.pagina_duplicatas + 1,
total_paginas
));
if ui.button("").clicked() && app.pagina_duplicatas + 1 < total_paginas {
app.pagina_duplicatas += 1;
}
});
}
}
}
fn exportar_para_pdf(app: &mut App, resultado: &ResultadoAnalise) {
if let Some(caminho) = rfd::FileDialog::new()
.set_file_name("relatorio.pdf")
.add_filter("PDF", &["pdf"])
.save_file()
{
let gerador = GenpdfGenerator;
let nome_layout = if app.nome_layout_atual.is_empty() {
None
} else {
Some(app.nome_layout_atual.as_str())
};
match exportar_pdf(
&gerador,
resultado,
&app.nome_arquivo,
nome_layout,
&caminho,
) {
Ok(_) => {
app.exibir_aviso("Sucesso", format!("PDF exportado para: {}", caminho.display()));
}
Err(e) => {
app.exibir_erro(format!("Erro ao exportar PDF: {}", e));
}
}
}
}