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,
|
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 })
|
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).
|
/// Converte uma notação LetraLinha (ex: "B3") para (coluna_base0, linha_base1).
|
||||||
///
|
///
|
||||||
/// Retorna `None` se a notação for inválida.
|
/// Retorna `None` se a notação for inválida.
|
||||||
|
|||||||
@@ -108,6 +108,9 @@ pub struct App {
|
|||||||
pub pagina_faltantes: usize,
|
pub pagina_faltantes: usize,
|
||||||
pub pagina_duplicatas: usize,
|
pub pagina_duplicatas: usize,
|
||||||
pub itens_por_pagina: 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 {
|
impl Default for App {
|
||||||
@@ -130,6 +133,7 @@ impl Default for App {
|
|||||||
pagina_faltantes: 0,
|
pagina_faltantes: 0,
|
||||||
pagina_duplicatas: 0,
|
pagina_duplicatas: 0,
|
||||||
itens_por_pagina: 100,
|
itens_por_pagina: 100,
|
||||||
|
preview_arquivo: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,6 +315,7 @@ impl App {
|
|||||||
AcaoModal::SobrescreverLayout => {} // Handled inline in layouts screen
|
AcaoModal::SobrescreverLayout => {} // Handled inline in layouts screen
|
||||||
AcaoModal::ConfirmarNovaAnalise => {
|
AcaoModal::ConfirmarNovaAnalise => {
|
||||||
self.notas_importadas.clear();
|
self.notas_importadas.clear();
|
||||||
|
self.preview_arquivo = None;
|
||||||
self.estado = EstadoApp::Importando;
|
self.estado = EstadoApp::Importando;
|
||||||
}
|
}
|
||||||
AcaoModal::SalvarLayoutConfig => {
|
AcaoModal::SalvarLayoutConfig => {
|
||||||
|
|||||||
@@ -64,6 +64,14 @@ pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) {
|
|||||||
TipoArquivo::Xlsx => renderizar_xlsx(ui, 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.add_space(16.0);
|
||||||
ui.separator();
|
ui.separator();
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
@@ -109,6 +117,7 @@ fn renderizar_csv(ui: &mut Ui, app: &mut App) {
|
|||||||
'\t' => "Tabulação (Tab)",
|
'\t' => "Tabulação (Tab)",
|
||||||
_ => "Outro",
|
_ => "Outro",
|
||||||
};
|
};
|
||||||
|
let mut delim_mudou = false;
|
||||||
egui::ComboBox::from_id_salt("combo_delimitador")
|
egui::ComboBox::from_id_salt("combo_delimitador")
|
||||||
.selected_text(delim_str)
|
.selected_text(delim_str)
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
@@ -117,6 +126,7 @@ fn renderizar_csv(ui: &mut Ui, app: &mut App) {
|
|||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.layout_csv_atual.delimitador = ',';
|
app.layout_csv_atual.delimitador = ',';
|
||||||
|
delim_mudou = true;
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.selectable_label(
|
.selectable_label(
|
||||||
@@ -126,6 +136,7 @@ fn renderizar_csv(ui: &mut Ui, app: &mut App) {
|
|||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.layout_csv_atual.delimitador = ';';
|
app.layout_csv_atual.delimitador = ';';
|
||||||
|
delim_mudou = true;
|
||||||
}
|
}
|
||||||
if ui
|
if ui
|
||||||
.selectable_label(
|
.selectable_label(
|
||||||
@@ -135,8 +146,19 @@ fn renderizar_csv(ui: &mut Ui, app: &mut App) {
|
|||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.layout_csv_atual.delimitador = '\t';
|
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
|
// Encoding
|
||||||
|
|||||||
@@ -102,6 +102,19 @@ pub fn renderizar_selecao_aba(ui: &mut Ui, _ctx: &Context, app: &mut App) {
|
|||||||
for aba in &abas {
|
for aba in &abas {
|
||||||
if ui.selectable_label(aba_atual == *aba, aba).clicked() {
|
if ui.selectable_label(aba_atual == *aba, aba).clicked() {
|
||||||
app.layout_xlsx_atual.aba = aba.clone();
|
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() {
|
match extensao.as_str() {
|
||||||
"csv" => {
|
"csv" => {
|
||||||
app.tipo_arquivo_atual = TipoArquivo::Csv;
|
app.tipo_arquivo_atual = TipoArquivo::Csv;
|
||||||
app.caminho_arquivo = Some(caminho);
|
app.caminho_arquivo = Some(caminho.clone());
|
||||||
app.notas_importadas.clear();
|
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" => {
|
"xlsx" | "xls" => {
|
||||||
app.tipo_arquivo_atual = TipoArquivo::Xlsx;
|
app.tipo_arquivo_atual = TipoArquivo::Xlsx;
|
||||||
|
|||||||
@@ -2,3 +2,57 @@ pub mod configuracao_colunas;
|
|||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod layouts;
|
pub mod layouts;
|
||||||
pub mod resultado;
|
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