diff --git a/src/infrastructure/csv_reader.rs b/src/infrastructure/csv_reader.rs index ea90fa4..25fb5a6 100644 --- a/src/infrastructure/csv_reader.rs +++ b/src/infrastructure/csv_reader.rs @@ -88,3 +88,39 @@ pub fn ler_csv( avisos, }) } + +/// Retorna as primeiras `n` linhas brutas do CSV (sem pular cabeçalho). +/// Usado exclusivamente para pré-visualização na UI. +pub fn preview_csv( + caminho: &Path, + delimitador: u8, + encoding: &str, + n: usize, +) -> Result>, ErroArquivo> { + let bytes = std::fs::read(caminho) + .map_err(|e| ErroArquivo::ErroLeitura(e.to_string()))?; + + let conteudo = match encoding.to_lowercase().as_str() { + "windows-1252" | "latin-1" | "iso-8859-1" => { + let (decoded, _, _) = WINDOWS_1252.decode(&bytes); + decoded.into_owned() + } + _ => String::from_utf8(bytes) + .map_err(|e| ErroArquivo::ErroLeitura(format!("Encoding inválido: {}", e)))?, + }; + + let mut reader = csv::ReaderBuilder::new() + .delimiter(delimitador) + .has_headers(false) + .flexible(true) + .from_reader(conteudo.as_bytes()); + + let linhas = reader + .records() + .take(n) + .filter_map(|r| r.ok()) + .map(|r| r.iter().map(|s| s.to_string()).collect()) + .collect(); + + Ok(linhas) +} diff --git a/src/infrastructure/xlsx_reader.rs b/src/infrastructure/xlsx_reader.rs index d7ab4cf..eeb039f 100644 --- a/src/infrastructure/xlsx_reader.rs +++ b/src/infrastructure/xlsx_reader.rs @@ -97,6 +97,53 @@ pub fn ler_xlsx( Ok(ResultadoXlsx { linhas, avisos }) } +/// Retorna as primeiras 5 linhas de uma aba XLSX, a partir da linha 1. +/// Usado exclusivamente para pré-visualização na UI. +pub fn preview_xlsx( + caminho: &Path, + nome_aba: &str, +) -> Result>, ErroArquivo> { + verificar_tamanho(caminho)?; + + let mut workbook = open_workbook_auto(caminho) + .map_err(|e| ErroArquivo::Corrompido(e.to_string()))?; + + let range: calamine::Range = workbook + .worksheet_range(nome_aba) + .map_err(|e| ErroArquivo::Corrompido(e.to_string()))?; + + let linhas = range + .rows() + .take(5) + .map(|row| { + row.iter() + .map(|cell| { + use calamine::Data; + match cell { + Data::Empty => String::new(), + Data::String(s) => s.clone(), + Data::Float(f) => { + if f.fract() == 0.0 { + format!("{}", *f as i64) + } else { + format!("{}", f) + } + } + Data::Int(i) => i.to_string(), + Data::Bool(b) => b.to_string(), + Data::DateTime(dt) => dt.to_string(), + Data::DateTimeIso(s) => s.clone(), + Data::DurationIso(s) => s.clone(), + Data::Error(_) => String::new(), + } + }) + .collect() + }) + .collect(); + + Ok(linhas) +} + /// Converte uma notação LetraLinha (ex: "B3") para (coluna_base0, linha_base1). /// /// Retorna `None` se a notação for inválida. diff --git a/src/ui/app.rs b/src/ui/app.rs index fbbde75..cd63064 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -108,6 +108,9 @@ pub struct App { pub pagina_faltantes: usize, pub pagina_duplicatas: usize, pub itens_por_pagina: usize, + + // Pré-visualização das primeiras linhas do arquivo + pub preview_arquivo: Option>>, } impl Default for App { @@ -130,6 +133,7 @@ impl Default for App { pagina_faltantes: 0, pagina_duplicatas: 0, itens_por_pagina: 100, + preview_arquivo: None, } } } @@ -311,6 +315,7 @@ impl App { AcaoModal::SobrescreverLayout => {} // Handled inline in layouts screen AcaoModal::ConfirmarNovaAnalise => { self.notas_importadas.clear(); + self.preview_arquivo = None; self.estado = EstadoApp::Importando; } AcaoModal::SalvarLayoutConfig => { diff --git a/src/ui/screens/configuracao_colunas.rs b/src/ui/screens/configuracao_colunas.rs index 9af8e31..f36b192 100644 --- a/src/ui/screens/configuracao_colunas.rs +++ b/src/ui/screens/configuracao_colunas.rs @@ -64,6 +64,14 @@ pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) { TipoArquivo::Xlsx => renderizar_xlsx(ui, app), } + // Pré-visualização do arquivo + if let Some(preview) = &app.preview_arquivo.clone() { + ui.add_space(8.0); + ui.separator(); + ui.add_space(4.0); + crate::ui::screens::renderizar_tabela_preview(ui, preview); + } + ui.add_space(16.0); ui.separator(); ui.add_space(8.0); @@ -109,6 +117,7 @@ fn renderizar_csv(ui: &mut Ui, app: &mut App) { '\t' => "Tabulação (Tab)", _ => "Outro", }; + let mut delim_mudou = false; egui::ComboBox::from_id_salt("combo_delimitador") .selected_text(delim_str) .show_ui(ui, |ui| { @@ -117,6 +126,7 @@ fn renderizar_csv(ui: &mut Ui, app: &mut App) { .clicked() { app.layout_csv_atual.delimitador = ','; + delim_mudou = true; } if ui .selectable_label( @@ -126,6 +136,7 @@ fn renderizar_csv(ui: &mut Ui, app: &mut App) { .clicked() { app.layout_csv_atual.delimitador = ';'; + delim_mudou = true; } if ui .selectable_label( @@ -135,8 +146,19 @@ fn renderizar_csv(ui: &mut Ui, app: &mut App) { .clicked() { app.layout_csv_atual.delimitador = '\t'; + delim_mudou = true; } }); + if delim_mudou { + if let Some(caminho) = &app.caminho_arquivo.clone() { + app.preview_arquivo = crate::infrastructure::csv_reader::preview_csv( + caminho, + app.layout_csv_atual.delimitador as u8, + &app.layout_csv_atual.encoding.clone(), + 5, + ).ok(); + } + } }); // Encoding diff --git a/src/ui/screens/import.rs b/src/ui/screens/import.rs index b65b40a..09f36e5 100644 --- a/src/ui/screens/import.rs +++ b/src/ui/screens/import.rs @@ -102,6 +102,19 @@ pub fn renderizar_selecao_aba(ui: &mut Ui, _ctx: &Context, app: &mut App) { for aba in &abas { if ui.selectable_label(aba_atual == *aba, aba).clicked() { app.layout_xlsx_atual.aba = aba.clone(); + // Gerar pré-visualização da aba selecionada + app.preview_arquivo = crate::infrastructure::xlsx_reader::preview_xlsx( + &caminho, + aba, + ).ok(); + } + } + + // Pré-visualização da aba selecionada + if !app.layout_xlsx_atual.aba.is_empty() { + if let Some(preview) = &app.preview_arquivo { + ui.add_space(8.0); + crate::ui::screens::renderizar_tabela_preview(ui, preview); } } @@ -137,8 +150,15 @@ fn on_arquivo_selecionado(app: &mut App, caminho: PathBuf) { match extensao.as_str() { "csv" => { app.tipo_arquivo_atual = TipoArquivo::Csv; - app.caminho_arquivo = Some(caminho); + app.caminho_arquivo = Some(caminho.clone()); app.notas_importadas.clear(); + // Gerar pré-visualização com o delimitador atual + app.preview_arquivo = crate::infrastructure::csv_reader::preview_csv( + &caminho, + app.layout_csv_atual.delimitador as u8, + &app.layout_csv_atual.encoding.clone(), + 5, + ).ok(); } "xlsx" | "xls" => { app.tipo_arquivo_atual = TipoArquivo::Xlsx; diff --git a/src/ui/screens/mod.rs b/src/ui/screens/mod.rs index 0cbff0b..3d97c09 100644 --- a/src/ui/screens/mod.rs +++ b/src/ui/screens/mod.rs @@ -2,3 +2,57 @@ pub mod configuracao_colunas; pub mod import; pub mod layouts; pub mod resultado; + +/// Renderiza uma tabela simples de pré-visualização do arquivo. +/// Exibe uma linha de cabeçalho com os índices das colunas (0, 1, 2, ...) +/// seguida pelas linhas de dados. +pub fn renderizar_tabela_preview(ui: &mut egui::Ui, linhas: &[Vec]) { + let num_colunas = linhas.iter().map(|l| l.len()).max().unwrap_or(0); + if num_colunas == 0 { + return; + } + + ui.label( + egui::RichText::new(format!( + "Pré-visualização ({} linha(s))", + linhas.len() + )) + .small() + .weak(), + ); + ui.add_space(2.0); + + egui::ScrollArea::horizontal() + .id_salt("scroll_preview") + .max_height(160.0) + .show(ui, |ui| { + egui::Grid::new("tabela_preview") + .striped(true) + .spacing([8.0, 2.0]) + .show(ui, |ui| { + // Linha de cabeçalho: índices 0, 1, 2, ... + for i in 0..num_colunas { + ui.label( + egui::RichText::new(format!("[{}]", i)) + .strong() + .monospace(), + ); + } + ui.end_row(); + + // Linhas de dados + for linha in linhas { + for col in 0..num_colunas { + let celula = linha.get(col).map(|s| s.as_str()).unwrap_or(""); + let texto = if celula.len() > 30 { + format!("{}...", &celula[..30]) + } else { + celula.to_string() + }; + ui.label(egui::RichText::new(texto).monospace().small()); + } + ui.end_row(); + } + }); + }); +} \ No newline at end of file