feat: adiciona pré-visualização de arquivos CSV e XLSX na interface

This commit is contained in:
FelipeCN
2026-03-03 09:25:12 -03:00
parent 0f728b9684
commit 41c75e5ed1
6 changed files with 185 additions and 1 deletions
+36
View File
@@ -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<Vec<Vec<String>>, 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)
}
+47
View File
@@ -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<Vec<Vec<String>>, ErroArquivo> {
verificar_tamanho(caminho)?;
let mut workbook = open_workbook_auto(caminho)
.map_err(|e| ErroArquivo::Corrompido(e.to_string()))?;
let range: calamine::Range<calamine::Data> = 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.
+5
View File
@@ -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<Vec<Vec<String>>>,
}
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 => {
+22
View File
@@ -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
+21 -1
View File
@@ -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;
+54
View File
@@ -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<String>]) {
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();
}
});
});
}