417 lines
14 KiB
Rust
417 lines
14 KiB
Rust
use crate::application::usecases::importar_arquivo::{importar_csv, importar_xlsx};
|
|
use crate::domain::entities::layout::{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(8.0);
|
|
|
|
// Seletor de layout
|
|
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_config")
|
|
.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();
|
|
match &layout {
|
|
Layout::Csv { config, .. } => {
|
|
app.layout_csv_atual = config.clone();
|
|
}
|
|
Layout::Xlsx { config, .. } => {
|
|
app.layout_xlsx_atual = config.clone();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
ui.add_space(12.0);
|
|
|
|
match app.tipo_arquivo_atual.clone() {
|
|
TipoArquivo::Csv => renderizar_csv(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.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);
|
|
}
|
|
});
|
|
|
|
if ui.button("💾 Salvar como layout...").clicked() {
|
|
app.exibir_modal_salvar_layout();
|
|
}
|
|
});
|
|
}
|
|
|
|
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",
|
|
};
|
|
let mut delim_mudou = false;
|
|
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 = ',';
|
|
delim_mudou = true;
|
|
}
|
|
if ui
|
|
.selectable_label(
|
|
app.layout_csv_atual.delimitador == ';',
|
|
"Ponto e vírgula (;)",
|
|
)
|
|
.clicked()
|
|
{
|
|
app.layout_csv_atual.delimitador = ';';
|
|
delim_mudou = true;
|
|
}
|
|
if ui
|
|
.selectable_label(
|
|
app.layout_csv_atual.delimitador == '\t',
|
|
"Tabulação (Tab)",
|
|
)
|
|
.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
|
|
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));
|
|
}
|
|
}
|
|
}
|