feat: implement main application structure with UI and layout management

- Added main application logic in `src/ui/app.rs` to handle state and modal dialogs.
- Created module structure for UI components in `src/ui/mod.rs`.
- Implemented column configuration screen in `src/ui/screens/configuracao_colunas.rs`.
- Developed file import screen in `src/ui/screens/import.rs` for CSV and XLSX files.
- Added layout management screen in `src/ui/screens/layouts.rs` for saving and importing layouts.
- Created result display screen in `src/ui/screens/resultado.rs` to show analysis results.
- Introduced modular organization for screens in `src/ui/screens/mod.rs`.
This commit is contained in:
2026-03-02 22:14:02 -03:00
parent 31cd94907a
commit e64988a139
36 changed files with 9166 additions and 2 deletions
+188
View File
@@ -0,0 +1,188 @@
use crate::domain::errors::{ErroArquivo, ResumoAvisos};
use calamine::{open_workbook, Reader, Xlsx};
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
const LIMITE_BYTES: u64 = 50 * 1024 * 1024;
/// Representa uma coordenada de célula (coluna base-0, linha base-0).
#[derive(Debug, Clone, Copy)]
pub struct Coordenada {
pub coluna: u32,
pub linha: u32,
}
/// Resultado da leitura de um arquivo XLSX.
pub struct ResultadoXlsx {
/// Linhas de dados, a partir da posição de início.
/// Cada linha é um vetor de strings (indexado pela posição da coluna de início).
pub linhas: Vec<Vec<String>>,
/// Avisos coletados durante a leitura.
pub avisos: ResumoAvisos,
}
/// Lista as abas disponíveis em um arquivo XLSX.
pub fn listar_abas(caminho: &Path) -> Result<Vec<String>, ErroArquivo> {
verificar_tamanho(caminho)?;
let workbook: Xlsx<BufReader<File>> = open_workbook::<Xlsx<_>, _>(caminho)
.map_err(|e| ErroArquivo::Corrompido(e.to_string()))?;
Ok(workbook.sheet_names().to_vec())
}
/// Lê os dados de uma aba XLSX a partir de uma linha de início.
///
/// # Parâmetros
/// - `caminho`: caminho do arquivo
/// - `nome_aba`: nome da aba a ser lida
/// - `linha_inicio`: número da linha de início dos dados (base 1, ex: 3 para "B3")
pub fn ler_xlsx(
caminho: &Path,
nome_aba: &str,
linha_inicio: u32,
) -> Result<ResultadoXlsx, ErroArquivo> {
verificar_tamanho(caminho)?;
let mut workbook: Xlsx<BufReader<File>> = open_workbook::<Xlsx<_>, _>(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 mut avisos = ResumoAvisos::default();
let mut linhas: Vec<Vec<String>> = Vec::new();
let linha_inicio_base0 = (linha_inicio.saturating_sub(1)) as usize;
for (row_idx, row) in range.rows().enumerate() {
if row_idx < linha_inicio_base0 {
continue;
}
let campos: Vec<String> = 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();
// Ignorar linhas completamente em branco
if campos.iter().all(|s: &String| s.trim().is_empty()) {
continue;
}
linhas.push(campos);
}
Ok(ResultadoXlsx { linhas, avisos })
}
/// Converte uma notação LetraLinha (ex: "B3") para (coluna_base0, linha_base1).
///
/// Retorna `None` se a notação for inválida.
pub fn parsear_letra_linha(s: &str) -> Option<Coordenada> {
let s = s.trim().to_uppercase();
if s.is_empty() {
return None;
}
let pos_numero = s.find(|c: char| c.is_ascii_digit())?;
let (letras, numeros) = s.split_at(pos_numero);
if letras.is_empty() || numeros.is_empty() {
return None;
}
// Converter letras para índice de coluna (base 0)
// A=0, B=1, ..., Z=25, AA=26, ...
let coluna = letras
.chars()
.try_fold(0u32, |acc, c| {
if c.is_ascii_uppercase() {
Some(acc * 26 + (c as u32 - 'A' as u32 + 1))
} else {
None
}
})?
.checked_sub(1)?;
let linha: u32 = numeros.parse().ok()?;
if linha == 0 {
return None;
}
Some(Coordenada { coluna, linha })
}
fn verificar_tamanho(caminho: &Path) -> Result<(), ErroArquivo> {
let metadata = std::fs::metadata(caminho)
.map_err(|e| ErroArquivo::ErroLeitura(e.to_string()))?;
if metadata.len() > LIMITE_BYTES {
return Err(ErroArquivo::TamanhoExcedido(metadata.len()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parsear_b3() {
let coord = parsear_letra_linha("B3").unwrap();
assert_eq!(coord.coluna, 1); // B = coluna 1 (base 0)
assert_eq!(coord.linha, 3);
}
#[test]
fn parsear_a1() {
let coord = parsear_letra_linha("A1").unwrap();
assert_eq!(coord.coluna, 0);
assert_eq!(coord.linha, 1);
}
#[test]
fn parsear_z1() {
let coord = parsear_letra_linha("Z1").unwrap();
assert_eq!(coord.coluna, 25);
}
#[test]
fn parsear_aa1() {
let coord = parsear_letra_linha("AA1").unwrap();
assert_eq!(coord.coluna, 26);
}
#[test]
fn parsear_invalido() {
assert!(parsear_letra_linha("").is_none());
assert!(parsear_letra_linha("3B").is_none());
assert!(parsear_letra_linha("123").is_none());
}
#[test]
fn parsear_minusculo() {
let coord = parsear_letra_linha("b3").unwrap();
assert_eq!(coord.coluna, 1);
}
}