update.
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(cargo build)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"sqltools.connections": [
|
||||||
|
{
|
||||||
|
"previewLimit": 50,
|
||||||
|
"driver": "SQLite",
|
||||||
|
"database": "C:\\Users\\felip\\AppData\\Roaming\\comparador-notas\\config.db",
|
||||||
|
"name": "comparador-notas"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
-5
@@ -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
@@ -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));
|
||||||
|
|||||||
Reference in New Issue
Block a user