feat: adiciona pré-visualização de arquivos CSV e XLSX na interface
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user