This commit is contained in:
FelipeCN
2026-03-03 13:51:26 -03:00
parent a5605de6a3
commit 2c96e99ccc
7 changed files with 166 additions and 27 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(cargo build)"
]
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"sqltools.connections": [
{
"previewLimit": 50,
"driver": "SQLite",
"database": "C:\\Users\\felip\\AppData\\Roaming\\comparador-notas\\config.db",
"name": "comparador-notas"
}
]
}
+13 -4
View File
@@ -7,17 +7,26 @@ use rusqlite::Connection;
/// Salva um layout no banco de dados. /// Salva um layout no banco de dados.
/// Se o layout já tem um id, atualiza. Caso contrário, insere. /// Se o layout já tem um id, atualiza. Caso contrário, insere.
pub fn salvar_layout(conn: &Connection, layout: &Layout) -> Result<i64, String> { /// Retorna `ErroLayout::NomeConflitante` se já existir um layout com o mesmo nome.
pub fn salvar_layout(conn: &Connection, layout: &Layout) -> Result<i64, ErroLayout> {
// Validar campos obrigatórios // Validar campos obrigatórios
if layout.nome().trim().is_empty() { if layout.nome().trim().is_empty() {
return Err("Nome do layout não pode ser vazio".to_string()); return Err(ErroLayout::CampoObrigatorioAusente("nome".to_string()));
} }
if let Some(id) = layout.id() { if let Some(id) = layout.id() {
layout_repository::atualizar(conn, layout).map_err(|e| e.to_string())?; layout_repository::atualizar(conn, layout)
.map_err(|e| ErroLayout::JsonMalformado(e.to_string()))?;
Ok(id) Ok(id)
} else { } else {
layout_repository::salvar(conn, layout).map_err(|e| e.to_string()) let nome = layout.nome().to_string();
let existe = layout_repository::existe_nome(conn, &nome)
.map_err(|e| ErroLayout::JsonMalformado(e.to_string()))?;
if existe {
return Err(ErroLayout::NomeConflitante(nome));
}
layout_repository::salvar(conn, layout)
.map_err(|e| ErroLayout::JsonMalformado(e.to_string()))
} }
} }
+26 -7
View File
@@ -1,7 +1,7 @@
use rusqlite::{Connection, Result}; use rusqlite::{Connection, Result};
/// Versão atual do schema do banco de dados. /// Versão atual do schema do banco de dados.
const VERSAO_SCHEMA_ATUAL: i64 = 1; const VERSAO_SCHEMA_ATUAL: i64 = 2;
/// Aplica todas as migrations necessárias para atualizar o banco /// Aplica todas as migrations necessárias para atualizar o banco
/// para a versão mais recente. /// para a versão mais recente.
@@ -21,15 +21,34 @@ pub fn aplicar_migrations(conn: &Connection) -> Result<()> {
) )
.unwrap_or(0); .unwrap_or(0);
if versao_atual < VERSAO_SCHEMA_ATUAL { if versao_atual < 1 {
migration_v1(conn)?; migration_v1(conn)?;
if versao_atual == 0 { }
conn.execute("INSERT INTO schema_version (versao) VALUES (?1);", [VERSAO_SCHEMA_ATUAL])?; if versao_atual < 2 {
} else { migration_v2(conn)?;
conn.execute("UPDATE schema_version SET versao = ?1;", [VERSAO_SCHEMA_ATUAL])?;
}
} }
if versao_atual == 0 {
conn.execute("INSERT INTO schema_version (versao) VALUES (?1);", [VERSAO_SCHEMA_ATUAL])?;
} else if versao_atual < VERSAO_SCHEMA_ATUAL {
conn.execute("UPDATE schema_version SET versao = ?1;", [VERSAO_SCHEMA_ATUAL])?;
}
Ok(())
}
/// Migration v2: adicionar índice único em layouts.nome.
/// Renomeia duplicatas (sufixo com id) antes de criar o índice para não falhar
/// em bancos que já possuem nomes repetidos.
fn migration_v2(conn: &Connection) -> Result<()> {
conn.execute_batch(
"UPDATE layouts
SET nome = nome || ' (' || id || ')'
WHERE id NOT IN (
SELECT MIN(id) FROM layouts GROUP BY nome
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_layouts_nome ON layouts (nome);",
)?;
Ok(()) Ok(())
} }
+1 -1
View File
@@ -13,7 +13,7 @@ fn main() -> eframe::Result {
.with_min_inner_size([800.0, 600.0]), .with_min_inner_size([800.0, 600.0]),
..Default::default() ..Default::default()
}; };
eframe::run_native( eframe::run_native(
"Comparador de Notas", "Comparador de Notas",
native_options, native_options,
+45 -5
View File
@@ -8,7 +8,7 @@ use crate::domain::{
nota::Nota, nota::Nota,
resultado_analise::{ResultadoAnalise, ResultadoPreAnalise}, resultado_analise::{ResultadoAnalise, ResultadoPreAnalise},
}, },
errors::ResumoAvisos, errors::{ErroLayout, ResumoAvisos},
}; };
use crate::infrastructure::sqlite::{connection::abrir_banco, migrations::aplicar_migrations}; use crate::infrastructure::sqlite::{connection::abrir_banco, migrations::aplicar_migrations};
use egui::Context; use egui::Context;
@@ -62,8 +62,8 @@ pub enum TipoModal {
pub enum AcaoModal { pub enum AcaoModal {
ConfirmarExpansaoFaltantes, ConfirmarExpansaoFaltantes,
ConfirmarExclusaoLayout(i64), ConfirmarExclusaoLayout(i64),
#[allow(dead_code)] /// Sobrescrever layout existente — layout já carrega o id correto.
SobrescreverLayout, SobrescreverLayout(Layout),
ConfirmarNovaAnalise, ConfirmarNovaAnalise,
/// Salvar a configuração atual como novo layout, usando modal.input_texto como nome. /// Salvar a configuração atual como novo layout, usando modal.input_texto como nome.
SalvarLayoutConfig, SalvarLayoutConfig,
@@ -341,7 +341,18 @@ impl App {
} }
} }
} }
AcaoModal::SobrescreverLayout => {} // Handled inline in layouts screen AcaoModal::SobrescreverLayout(layout) => {
if let Some(conn) = &self.conn {
match salvar_layout(conn, &layout) {
Ok(_) => {
self.recarregar_layouts();
}
Err(e) => {
self.exibir_erro(e.to_string());
}
}
}
}
AcaoModal::ConfirmarNovaAnalise => { AcaoModal::ConfirmarNovaAnalise => {
self.notas_importadas.clear(); self.notas_importadas.clear();
self.preview_arquivo = None; self.preview_arquivo = None;
@@ -377,7 +388,36 @@ impl App {
self.recarregar_layouts(); self.recarregar_layouts();
self.exibir_aviso("Layout salvo", "Layout salvo com sucesso."); self.exibir_aviso("Layout salvo", "Layout salvo com sucesso.");
} }
Some(Err(e)) => self.exibir_erro(e), Some(Err(ErroLayout::NomeConflitante(nome_conflito))) => {
let id_existente = self
.layouts_salvos
.iter()
.find(|l| l.nome() == nome_conflito)
.and_then(|l| l.id());
if let Some(id) = id_existente {
let layout_com_id = match &self.tipo_arquivo_atual {
TipoArquivo::Csv => Layout::Csv {
id: Some(id),
nome: nome_conflito.clone(),
config: self.layout_csv_atual.clone(),
},
TipoArquivo::Xlsx => Layout::Xlsx {
id: Some(id),
nome: nome_conflito.clone(),
config: self.layout_xlsx_atual.clone(),
},
};
self.exibir_confirmacao(
"Conflito de nome",
format!(
"Já existe um layout com o nome '{}'. Deseja sobrescrever?",
nome_conflito
),
AcaoModal::SobrescreverLayout(layout_com_id),
);
}
}
Some(Err(e)) => self.exibir_erro(e.to_string()),
None => {} None => {}
} }
} }
+64 -10
View File
@@ -1,7 +1,7 @@
use crate::application::usecases::layouts::{ use crate::application::usecases::layouts::{
exportar_layout_json, importar_layout_json, salvar_layout, exportar_layout_json, importar_layout_json, salvar_layout,
}; };
use crate::domain::entities::layout::{Layout, TipoArquivo}; use crate::domain::entities::layout::{Layout, LayoutJson, TipoArquivo};
use crate::domain::errors::ErroLayout; use crate::domain::errors::ErroLayout;
use crate::ui::app::{AcaoModal, App, EstadoApp}; use crate::ui::app::{AcaoModal, App, EstadoApp};
use egui::{Context, Ui}; use egui::{Context, Ui};
@@ -167,6 +167,35 @@ fn salvar_layout_atual(app: &mut App) {
app.recarregar_layouts(); app.recarregar_layouts();
app.exibir_aviso("Sucesso", "Layout salvo com sucesso."); app.exibir_aviso("Sucesso", "Layout salvo com sucesso.");
} }
Err(ErroLayout::NomeConflitante(nome)) => {
let id_existente = app
.layouts_salvos
.iter()
.find(|l| l.nome() == nome)
.and_then(|l| l.id());
if let Some(id) = id_existente {
let layout_com_id = match app.tipo_arquivo_atual.clone() {
TipoArquivo::Csv => Layout::Csv {
id: Some(id),
nome: nome.clone(),
config: app.layout_csv_atual.clone(),
},
TipoArquivo::Xlsx => Layout::Xlsx {
id: Some(id),
nome: nome.clone(),
config: app.layout_xlsx_atual.clone(),
},
};
app.exibir_confirmacao(
"Conflito de nome",
format!(
"Já existe um layout com o nome '{}'. Deseja sobrescrever?",
nome
),
AcaoModal::SobrescreverLayout(layout_com_id),
);
}
}
Err(e) => { Err(e) => {
app.exibir_erro(format!("Erro ao salvar layout: {}", e)); app.exibir_erro(format!("Erro ao salvar layout: {}", e));
} }
@@ -182,15 +211,40 @@ fn importar_json(app: &mut App, conteudo: &str) {
app.exibir_aviso("Sucesso", "Layout importado com sucesso."); app.exibir_aviso("Sucesso", "Layout importado com sucesso.");
} }
Err(ErroLayout::NomeConflitante(nome)) => { Err(ErroLayout::NomeConflitante(nome)) => {
// Exibir opções: sobrescrever ou cancelar // Recriar o layout parseado para passá-lo no modal de confirmação.
app.exibir_aviso( // O JSON já foi validado pela chamada acima, então o parse aqui não falha.
"Conflito de nome", let parsed = serde_json::from_str::<LayoutJson>(conteudo)
format!( .ok()
"Já existe um layout com o nome '{}'. Use 'Salvar com novo nome' ou cancele a importação.", .and_then(|json_repr| Layout::try_from(json_repr).ok());
nome
), let id_existente = app
); .layouts_salvos
// TODO: implementar fluxo completo de sobrescrever com entrada de novo nome .iter()
.find(|l| l.nome() == nome)
.and_then(|l| l.id());
match (parsed, id_existente) {
(Some(mut layout), Some(id)) => {
match &mut layout {
Layout::Csv { id: i, .. } => *i = Some(id),
Layout::Xlsx { id: i, .. } => *i = Some(id),
}
app.exibir_confirmacao(
"Conflito de nome",
format!(
"Já existe um layout com o nome '{}'. Deseja sobrescrever?",
nome
),
AcaoModal::SobrescreverLayout(layout),
);
}
_ => {
app.exibir_erro(format!(
"Conflito de nome: layout '{}' já existe.",
nome
));
}
}
} }
Err(e) => { Err(e) => {
app.exibir_erro(format!("Erro ao importar layout: {}", e)); app.exibir_erro(format!("Erro ao importar layout: {}", e));