feat: adiciona análise e proposta de correção para duplicidade de layouts ao salvar
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
# Análise — Duplicidade de Layouts ao Salvar
|
||||
|
||||
**Data:** 03/03/2026
|
||||
**Status:** Análise / Proposta de Correção
|
||||
|
||||
---
|
||||
|
||||
## 1. Problema Identificado
|
||||
|
||||
Ao clicar em **"💾 Salvar"** na tela de gerenciamento de layouts, o sistema permite salvar um layout com um nome já existente, criando entradas duplicadas no banco sem qualquer aviso ou confirmação ao usuário.
|
||||
|
||||
O PRD (RF03) exige que, ao existir conflito de nome, o sistema pergunte ao usuário se deseja **sobrescrever** o existente ou **salvar com novo nome**. Esse comportamento está **implementado apenas no fluxo de importação JSON**, mas está **ausente no fluxo de salvar layout atual**.
|
||||
|
||||
---
|
||||
|
||||
## 2. Raiz do Problema — Análise por Camada
|
||||
|
||||
### 2.1 Camada Infrastructure — `sqlite/migrations.rs`
|
||||
|
||||
A tabela `layouts` foi criada na `migration_v1` **sem nenhuma constraint `UNIQUE` no campo `nome`**:
|
||||
|
||||
```sql
|
||||
-- migration_v1 (src/infrastructure/sqlite/migrations.rs)
|
||||
CREATE TABLE IF NOT EXISTS layouts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
nome TEXT NOT NULL, -- ← sem UNIQUE
|
||||
tipo TEXT NOT NULL CHECK(tipo IN ('csv', 'xlsx')),
|
||||
...
|
||||
);
|
||||
```
|
||||
|
||||
O banco de dados não impede, por si só, a inserção de registros com o mesmo nome.
|
||||
|
||||
---
|
||||
|
||||
### 2.2 Camada Infrastructure — `sqlite/layout_repository.rs`
|
||||
|
||||
A função `salvar` executa um `INSERT` direto, sem qualquer verificação prévia:
|
||||
|
||||
```rust
|
||||
// src/infrastructure/sqlite/layout_repository.rs — fn salvar()
|
||||
conn.execute(
|
||||
"INSERT INTO layouts (nome, tipo, ...) VALUES (?1, 'csv', ...)",
|
||||
params![nome, ...],
|
||||
)?;
|
||||
```
|
||||
|
||||
Existe a função `existe_nome` no mesmo arquivo, que verifica duplicidade por consulta, mas ela **não é chamada** pelo fluxo de salvar layout atual — apenas pelo fluxo de importação JSON.
|
||||
|
||||
---
|
||||
|
||||
### 2.3 Camada Application — `usecases/layouts.rs`
|
||||
|
||||
A função `salvar_layout` só verifica se o `id` está presente:
|
||||
|
||||
```rust
|
||||
// src/application/usecases/layouts.rs — fn salvar_layout()
|
||||
if let Some(id) = layout.id() {
|
||||
layout_repository::atualizar(conn, layout) // atualiza se tem id
|
||||
} else {
|
||||
layout_repository::salvar(conn, layout) // insere direto sem checar nome
|
||||
}
|
||||
```
|
||||
|
||||
A verificação de nome duplicado (`existe_nome`) **está implementada em `importar_layout_json`**, mas **não foi replicada** em `salvar_layout`.
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Camada UI — `ui/screens/layouts.rs`
|
||||
|
||||
A função `salvar_layout_atual` monta o layout sempre com `id: None` e chama `salvar_layout` sem tratar o caso de conflito de nome:
|
||||
|
||||
```rust
|
||||
// src/ui/screens/layouts.rs — fn salvar_layout_atual()
|
||||
let layout = match app.tipo_arquivo_atual.clone() {
|
||||
TipoArquivo::Csv => Layout::Csv {
|
||||
id: None, // ← sempre None, nunca verifica se nome já existe
|
||||
nome: app.nome_layout_atual.trim().to_string(),
|
||||
config: app.layout_csv_atual.clone(),
|
||||
},
|
||||
...
|
||||
};
|
||||
|
||||
match salvar_layout(conn, &layout) {
|
||||
Ok(_) => { ... } // ← não há tratamento para conflito de nome
|
||||
Err(e) => { ... }
|
||||
}
|
||||
```
|
||||
|
||||
O fluxo de resolução de conflito (sobrescrever / salvar com novo nome) **existe apenas em `importar_json`**, que trata `ErroLayout::NomeConflitante`, mas mesmo nesse caso o tratamento está incompleto (há um `TODO` no código).
|
||||
|
||||
---
|
||||
|
||||
## 3. Resumo dos Pontos Falhos
|
||||
|
||||
| # | Localização | Problema |
|
||||
|---|-------------|----------|
|
||||
| 1 | `sqlite/migrations.rs` — `migration_v1` | Nenhum `UNIQUE` constraint em `nome` na tabela `layouts` |
|
||||
| 2 | `usecases/layouts.rs` — `salvar_layout` | Não consulta `existe_nome` antes de inserir |
|
||||
| 3 | `domain/errors.rs` | Falta variante de erro para duplicidade no fluxo de salvar (apenas existe em importar JSON) |
|
||||
| 4 | `ui/screens/layouts.rs` — `salvar_layout_atual` | Não trata conflito de nome; não exibe opção de sobrescrever ou renomear |
|
||||
| 5 | `ui/screens/layouts.rs` — `importar_json` | Tratamento de `NomeConflitante` incompleto (TODO pendente) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Proposta de Correção
|
||||
|
||||
### 4.1 Migration v2 — Adicionar `UNIQUE` em `nome`
|
||||
|
||||
**Arquivo:** `src/infrastructure/sqlite/migrations.rs`
|
||||
|
||||
Adicionar uma segunda migration que cria uma `UNIQUE` constraint na coluna `nome`.
|
||||
Atualizar `VERSAO_SCHEMA_ATUAL` de `1` para `2` e chamar `migration_v2` no bloco condicional de aplicação de migrations.
|
||||
|
||||
```
|
||||
-- migration_v2
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_layouts_nome ON layouts (nome);
|
||||
```
|
||||
|
||||
> Usar um índice único em vez de recriar a tabela é mais seguro, pois preserva dados já existentes. Bancos que já possuem duplicatas precisariam de tratamento antes da migração (ex.: renomear automaticamente os conflitantes com sufixo numérico).
|
||||
|
||||
---
|
||||
|
||||
### 4.2 Retornar Erro Tipado de Conflito em `salvar_layout`
|
||||
|
||||
**Arquivo:** `src/application/usecases/layouts.rs` — função `salvar_layout`
|
||||
|
||||
Antes da inserção, verificar se já existe layout com o mesmo nome usando `layout_repository::existe_nome`. Retornar um erro tipado (ex.: `ErroLayout::NomeConflitante`) em vez de uma `String` quando o nome já existir e nenhum `id` foi fornecido.
|
||||
|
||||
Fluxo esperado:
|
||||
|
||||
```
|
||||
1. layout.id() é None (novo layout)
|
||||
2. Consultar existe_nome(conn, layout.nome())
|
||||
3. Se existir → retornar Err(ErroLayout::NomeConflitante(nome))
|
||||
4. Se não existir → prosseguir com INSERT
|
||||
```
|
||||
|
||||
A assinatura da função deverá mudar de `Result<i64, String>` para `Result<i64, ErroLayout>` para que o chamador consiga distinguir o tipo de erro.
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Tratar Conflito na UI — Fluxo de Sobrescrever ou Renomear
|
||||
|
||||
**Arquivo:** `src/ui/screens/layouts.rs` — função `salvar_layout_atual`
|
||||
|
||||
Ao receber `Err(ErroLayout::NomeConflitante(nome))`, a UI deve exibir um modal de confirmação com duas opções:
|
||||
|
||||
- **Sobrescrever** — buscar o `id` do layout existente pelo nome, montar o layout com esse `id` e chamar `salvar_layout` novamente (o use case irá para o branch `atualizar`)
|
||||
- **Cancelar** — descartar a operação sem salvar
|
||||
|
||||
> A opção "Salvar com novo nome" pode ser considerada, porém aumenta a complexidade da tela. Para a correção mínima, sobrescrever ou cancelar já atende ao PRD.
|
||||
|
||||
O estado da `App` pode precisar de um campo intermediário para guardar o layout pendente de confirmação entre frames do egui, ou se o sistema já possui um mecanismo de `AcaoModal`, criar uma variante `AcaoModal::ConfirmarSobrescritaLayout(Layout)` e processar no handler de ações do modal.
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Completar o `TODO` em `importar_json`
|
||||
|
||||
**Arquivo:** `src/ui/screens/layouts.rs` — função `importar_json`
|
||||
|
||||
O bloco `Err(ErroLayout::NomeConflitante(nome))` contém um `TODO` que não implementa o fluxo completo. Após a correção do item 4.3, a mesma lógica de modal de confirmação deve ser reutilizada aqui, chamando `importar_layout_json` com `sobrescrever_se_existir: true` quando o usuário confirmar.
|
||||
|
||||
---
|
||||
|
||||
## 5. Ordem de Implementação Sugerida
|
||||
|
||||
```
|
||||
1. migration_v2 (constraint UNIQUE) ............ infrastructure/sqlite/migrations.rs
|
||||
2. Erro tipado em salvar_layout ................. application/usecases/layouts.rs
|
||||
3. Modal de conflito na UI (salvar atual) ....... ui/screens/layouts.rs
|
||||
4. Modal de conflito na UI (importar JSON) ...... ui/screens/layouts.rs (completar TODO)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Impacto da Correção
|
||||
|
||||
| Aspecto | Antes | Depois |
|
||||
|---------|-------|--------|
|
||||
| Layouts duplicados no banco | Possível | Bloqueado pelo banco (UNIQUE) |
|
||||
| Feedback ao usuário | Nenhum — salva silenciosamente | Modal com escolha: sobrescrever ou cancelar |
|
||||
| Conformidade com PRD (RF03) | Parcial (apenas importação JSON) | Completa em todos os fluxos |
|
||||
| Importação JSON com conflito | TODO incompleto | Resolvido com mesma lógica |
|
||||
Reference in New Issue
Block a user