Compare commits
2
Commits
2c08f237e9
...
a5605de6a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5605de6a3 | ||
|
|
0354ece935 |
@@ -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 |
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
use crate::domain::{
|
use crate::domain::{
|
||||||
entities::{
|
entities::{
|
||||||
nota::Nota,
|
nota::Nota,
|
||||||
resultado_analise::{IntervaloSerie, ResultadoAnalise, ResultadoPreAnalise},
|
resultado_analise::{ResultadoAnalise, ResultadoPreAnalise},
|
||||||
},
|
},
|
||||||
services::{
|
services::{
|
||||||
detector_duplicidade::duplicidades_por_serie,
|
detector_duplicidade::duplicidades_por_serie,
|
||||||
detector_sequencia::{calcular_intervalo, detectar_faltantes, LIMITE_FALTANTES},
|
detector_sequencia::{LIMITE_FALTANTES, calcular_intervalo, detectar_faltantes},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
@@ -55,9 +55,7 @@ pub fn pre_analisar(notas: &[Nota]) -> ResultadoPreAnalise {
|
|||||||
|
|
||||||
/// Verifica se alguma série excede o limite de faltantes.
|
/// Verifica se alguma série excede o limite de faltantes.
|
||||||
/// Retorna lista de séries que precisam de confirmação.
|
/// Retorna lista de séries que precisam de confirmação.
|
||||||
pub fn series_com_intervalo_excessivo(
|
pub fn series_com_intervalo_excessivo(pre: &ResultadoPreAnalise) -> Vec<(String, u64)> {
|
||||||
pre: &ResultadoPreAnalise,
|
|
||||||
) -> Vec<(String, u64)> {
|
|
||||||
pre.intervalos_por_serie
|
pre.intervalos_por_serie
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, iv)| iv.excede_limite(LIMITE_FALTANTES))
|
.filter(|(_, iv)| iv.excede_limite(LIMITE_FALTANTES))
|
||||||
@@ -89,19 +87,18 @@ pub fn expandir_analise(pre: ResultadoPreAnalise, notas: &[Nota]) -> ResultadoAn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executa análise completa sem verificar limites (use case simplificado).
|
|
||||||
/// Útil quando o caller já confirmou ou sabe que não há intervalos excessivos.
|
|
||||||
pub fn executar_analise(notas: &[Nota]) -> ResultadoAnalise {
|
|
||||||
let pre = pre_analisar(notas);
|
|
||||||
expandir_analise(pre, notas)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::domain::entities::nota::Nota;
|
use crate::domain::entities::nota::Nota;
|
||||||
use rust_decimal_macros::dec;
|
use rust_decimal_macros::dec;
|
||||||
|
|
||||||
|
/// Executa análise completa sem verificar limites (atalho para testes).
|
||||||
|
fn executar_analise(notas: &[Nota]) -> ResultadoAnalise {
|
||||||
|
let pre = pre_analisar(notas);
|
||||||
|
expandir_analise(pre, notas)
|
||||||
|
}
|
||||||
|
|
||||||
fn nota(numero: u64, serie: &str, valor: Option<rust_decimal::Decimal>) -> Nota {
|
fn nota(numero: u64, serie: &str, valor: Option<rust_decimal::Decimal>) -> Nota {
|
||||||
Nota::new(numero, serie.to_string(), valor, None)
|
Nota::new(numero, serie.to_string(), valor, None)
|
||||||
}
|
}
|
||||||
@@ -133,10 +130,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pre_analise_detecta_intervalo_excessivo() {
|
fn pre_analise_detecta_intervalo_excessivo() {
|
||||||
let notas = vec![
|
let notas = vec![nota(1, "001", None), nota(20_000, "001", None)];
|
||||||
nota(1, "001", None),
|
|
||||||
nota(20_000, "001", None),
|
|
||||||
];
|
|
||||||
let pre = pre_analisar(¬as);
|
let pre = pre_analisar(¬as);
|
||||||
let excessivos = series_com_intervalo_excessivo(&pre);
|
let excessivos = series_com_intervalo_excessivo(&pre);
|
||||||
assert_eq!(excessivos.len(), 1);
|
assert_eq!(excessivos.len(), 1);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::domain::entities::layout::{Layout, LayoutCsv, LayoutXlsx};
|
use crate::domain::entities::layout::{LayoutCsv, LayoutXlsx};
|
||||||
use crate::domain::entities::nota::Nota;
|
use crate::domain::entities::nota::Nota;
|
||||||
use crate::domain::entities::serie::validar_serie;
|
use crate::domain::entities::serie::validar_serie;
|
||||||
use crate::domain::errors::{ErroArquivo, ResumoAvisos};
|
use crate::domain::errors::{ErroArquivo, ResumoAvisos};
|
||||||
@@ -24,7 +24,10 @@ pub fn listar_abas_xlsx(caminho: &Path) -> Result<InfoXlsx, ErroArquivo> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Importa um arquivo CSV e retorna as notas válidas + avisos consolidados.
|
/// Importa um arquivo CSV e retorna as notas válidas + avisos consolidados.
|
||||||
pub fn importar_csv(caminho: &Path, config: &LayoutCsv) -> Result<ResultadoImportacao, ErroArquivo> {
|
pub fn importar_csv(
|
||||||
|
caminho: &Path,
|
||||||
|
config: &LayoutCsv,
|
||||||
|
) -> Result<ResultadoImportacao, ErroArquivo> {
|
||||||
let resultado = csv_reader::ler_csv(
|
let resultado = csv_reader::ler_csv(
|
||||||
caminho,
|
caminho,
|
||||||
config.delimitador as u8,
|
config.delimitador as u8,
|
||||||
@@ -46,17 +49,21 @@ pub fn importar_csv(caminho: &Path, config: &LayoutCsv) -> Result<ResultadoImpor
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Importa um arquivo XLSX e retorna as notas válidas + avisos consolidados.
|
/// Importa um arquivo XLSX e retorna as notas válidas + avisos consolidados.
|
||||||
pub fn importar_xlsx(caminho: &Path, config: &LayoutXlsx) -> Result<ResultadoImportacao, ErroArquivo> {
|
pub fn importar_xlsx(
|
||||||
|
caminho: &Path,
|
||||||
|
config: &LayoutXlsx,
|
||||||
|
) -> Result<ResultadoImportacao, ErroArquivo> {
|
||||||
// Determinar linha de início a partir de qualquer campo mapeado
|
// Determinar linha de início a partir de qualquer campo mapeado
|
||||||
let linha_inicio = {
|
let linha_inicio = {
|
||||||
let coord_num = xlsx_reader::parsear_letra_linha(&config.pos_numero)
|
let coord_num = xlsx_reader::parsear_letra_linha(&config.pos_numero).ok_or_else(|| {
|
||||||
.ok_or_else(|| ErroArquivo::ErroLeitura(format!(
|
ErroArquivo::ErroLeitura(format!(
|
||||||
"Posição de Numero inválida: '{}'", config.pos_numero
|
"Posição de Numero inválida: '{}'",
|
||||||
)))?;
|
config.pos_numero
|
||||||
let coord_ser = xlsx_reader::parsear_letra_linha(&config.pos_serie)
|
))
|
||||||
.ok_or_else(|| ErroArquivo::ErroLeitura(format!(
|
})?;
|
||||||
"Posição de Serie inválida: '{}'", config.pos_serie
|
let coord_ser = xlsx_reader::parsear_letra_linha(&config.pos_serie).ok_or_else(|| {
|
||||||
)))?;
|
ErroArquivo::ErroLeitura(format!("Posição de Serie inválida: '{}'", config.pos_serie))
|
||||||
|
})?;
|
||||||
coord_num.linha.min(coord_ser.linha)
|
coord_num.linha.min(coord_ser.linha)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -113,7 +120,9 @@ fn mapear_linhas_para_notas(
|
|||||||
Some(s) if !s.trim().is_empty() => s.trim().to_string(),
|
Some(s) if !s.trim().is_empty() => s.trim().to_string(),
|
||||||
_ => {
|
_ => {
|
||||||
avisos.numeros_invalidos += 1;
|
avisos.numeros_invalidos += 1;
|
||||||
avisos.detalhes.push(format!("Linha {}: campo Numero ausente", linha_num));
|
avisos
|
||||||
|
.detalhes
|
||||||
|
.push(format!("Linha {}: campo Numero ausente", linha_num));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -123,7 +132,9 @@ fn mapear_linhas_para_notas(
|
|||||||
Ok(n) => n,
|
Ok(n) => n,
|
||||||
Err(msg) => {
|
Err(msg) => {
|
||||||
avisos.numeros_invalidos += 1;
|
avisos.numeros_invalidos += 1;
|
||||||
avisos.detalhes.push(format!("Linha {}: {}", linha_num, msg));
|
avisos
|
||||||
|
.detalhes
|
||||||
|
.push(format!("Linha {}: {}", linha_num, msg));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -133,7 +144,9 @@ fn mapear_linhas_para_notas(
|
|||||||
Some(s) => s.trim().to_string(),
|
Some(s) => s.trim().to_string(),
|
||||||
None => {
|
None => {
|
||||||
avisos.series_invalidas += 1;
|
avisos.series_invalidas += 1;
|
||||||
avisos.detalhes.push(format!("Linha {}: campo Serie ausente", linha_num));
|
avisos
|
||||||
|
.detalhes
|
||||||
|
.push(format!("Linha {}: campo Serie ausente", linha_num));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use crate::domain::{
|
|||||||
entities::layout::{Layout, LayoutJson},
|
entities::layout::{Layout, LayoutJson},
|
||||||
errors::ErroLayout,
|
errors::ErroLayout,
|
||||||
};
|
};
|
||||||
use crate::infrastructure::sqlite::{layout_repository, migrations};
|
use crate::infrastructure::sqlite::layout_repository;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
|
|
||||||
/// Salva um layout no banco de dados.
|
/// Salva um layout no banco de dados.
|
||||||
@@ -14,12 +14,10 @@ pub fn salvar_layout(conn: &Connection, layout: &Layout) -> Result<i64, String>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(id) = layout.id() {
|
if let Some(id) = layout.id() {
|
||||||
layout_repository::atualizar(conn, layout)
|
layout_repository::atualizar(conn, layout).map_err(|e| e.to_string())?;
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(id)
|
Ok(id)
|
||||||
} else {
|
} else {
|
||||||
layout_repository::salvar(conn, layout)
|
layout_repository::salvar(conn, layout).map_err(|e| e.to_string())
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,11 +26,6 @@ pub fn listar_layouts(conn: &Connection) -> Result<Vec<Layout>, String> {
|
|||||||
layout_repository::listar(conn).map_err(|e| e.to_string())
|
layout_repository::listar(conn).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Carrega um layout pelo id.
|
|
||||||
pub fn carregar_layout(conn: &Connection, id: i64) -> Result<Option<Layout>, String> {
|
|
||||||
layout_repository::buscar_por_id(conn, id).map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Exclui um layout pelo id.
|
/// Exclui um layout pelo id.
|
||||||
pub fn excluir_layout(conn: &Connection, id: i64) -> Result<(), String> {
|
pub fn excluir_layout(conn: &Connection, id: i64) -> Result<(), String> {
|
||||||
layout_repository::excluir(conn, id).map_err(|e| e.to_string())
|
layout_repository::excluir(conn, id).map_err(|e| e.to_string())
|
||||||
@@ -61,8 +54,8 @@ pub fn importar_layout_json(
|
|||||||
novo_nome: Option<&str>,
|
novo_nome: Option<&str>,
|
||||||
) -> Result<i64, ErroLayout> {
|
) -> Result<i64, ErroLayout> {
|
||||||
// Deserializar
|
// Deserializar
|
||||||
let json_repr: LayoutJson = serde_json::from_str(json)
|
let json_repr: LayoutJson =
|
||||||
.map_err(|e| ErroLayout::JsonMalformado(e.to_string()))?;
|
serde_json::from_str(json).map_err(|e| ErroLayout::JsonMalformado(e.to_string()))?;
|
||||||
|
|
||||||
let mut layout = Layout::try_from(json_repr)?;
|
let mut layout = Layout::try_from(json_repr)?;
|
||||||
|
|
||||||
@@ -105,6 +98,5 @@ pub fn importar_layout_json(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inserir novo
|
// Inserir novo
|
||||||
layout_repository::salvar(conn, &layout)
|
layout_repository::salvar(conn, &layout).map_err(|e| ErroLayout::JsonMalformado(e.to_string()))
|
||||||
.map_err(|e| ErroLayout::JsonMalformado(e.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error, Clone)]
|
|
||||||
pub enum ErroNumero {
|
|
||||||
#[error("Número zero não é válido para uma nota fiscal")]
|
|
||||||
Zero,
|
|
||||||
#[error("Valor não numérico: '{0}'")]
|
|
||||||
NaoNumerico(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error, Clone)]
|
#[derive(Debug, Error, Clone)]
|
||||||
pub enum ErroSerie {
|
pub enum ErroSerie {
|
||||||
#[error("Série inválida: '{0}' (deve conter de 1 a 3 dígitos numéricos)")]
|
#[error("Série inválida: '{0}' (deve conter de 1 a 3 dígitos numéricos)")]
|
||||||
@@ -40,52 +32,10 @@ pub enum ErroArquivo {
|
|||||||
TamanhoExcedido(u64),
|
TamanhoExcedido(u64),
|
||||||
#[error("Arquivo corrompido ou ilegível: {0}")]
|
#[error("Arquivo corrompido ou ilegível: {0}")]
|
||||||
Corrompido(String),
|
Corrompido(String),
|
||||||
#[error("Formato não suportado: {0}")]
|
|
||||||
FormatoNaoSuportado(String),
|
|
||||||
#[error("Erro de leitura: {0}")]
|
#[error("Erro de leitura: {0}")]
|
||||||
ErroLeitura(String),
|
ErroLeitura(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum ErroAnalise {
|
|
||||||
#[error("Intervalo de faltantes muito grande para a série '{serie}': {intervalo} registros")]
|
|
||||||
IntervaloMuitoGrande { serie: String, intervalo: u64 },
|
|
||||||
#[error("Campo obrigatório não mapeado: {0}")]
|
|
||||||
CampoObrigatorioNaoMapeado(String),
|
|
||||||
#[error("Índice de coluna inválido: campo '{campo}', índice {indice}")]
|
|
||||||
IndiceInvalido { campo: String, indice: usize },
|
|
||||||
#[error("Dois campos mapeados para o mesmo índice: {0}")]
|
|
||||||
IndicesDuplicados(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Aviso coletado durante a importação, para ser exibido de forma consolidada ao usuário.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct AvisoImportacao {
|
|
||||||
pub categoria: CategoriaAviso,
|
|
||||||
pub mensagem: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum CategoriaAviso {
|
|
||||||
LinhaMalformada,
|
|
||||||
NumeroInvalido,
|
|
||||||
SerieInvalida,
|
|
||||||
ValorInvalido,
|
|
||||||
RegistroDescartado,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for CategoriaAviso {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
CategoriaAviso::LinhaMalformada => write!(f, "Linhas malformadas"),
|
|
||||||
CategoriaAviso::NumeroInvalido => write!(f, "Valores de Numero inválidos"),
|
|
||||||
CategoriaAviso::SerieInvalida => write!(f, "Registros com Série inválida"),
|
|
||||||
CategoriaAviso::ValorInvalido => write!(f, "Valores monetários inválidos"),
|
|
||||||
CategoriaAviso::RegistroDescartado => write!(f, "Registros descartados"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resumo consolidado de avisos para exibição em um único modal.
|
/// Resumo consolidado de avisos para exibição em um único modal.
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct ResumoAvisos {
|
pub struct ResumoAvisos {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::domain::services::parser_monetario::formatar_valor_br;
|
|||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use genpdf::{
|
use genpdf::{
|
||||||
elements::{Break, Paragraph},
|
elements::{Break, Paragraph},
|
||||||
fonts, style, Document, Element, SimplePageDecorator,
|
fonts, style, Document, SimplePageDecorator,
|
||||||
};
|
};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
const LIMITE_50MB: u64 = 50 * 1024 * 1024;
|
|
||||||
|
|
||||||
/// Determina o caminho do banco de dados conforme o sistema operacional.
|
/// Determina o caminho do banco de dados conforme o sistema operacional.
|
||||||
pub fn caminho_banco() -> PathBuf {
|
pub fn caminho_banco() -> PathBuf {
|
||||||
let config_dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
|
let config_dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
|
||||||
@@ -33,7 +31,7 @@ pub fn abrir_banco_no_caminho(caminho: &Path) -> Result<(Connection, bool), Stri
|
|||||||
// Testar se o banco funciona com uma query simples
|
// Testar se o banco funciona com uma query simples
|
||||||
match conn.execute_batch("SELECT 1;") {
|
match conn.execute_batch("SELECT 1;") {
|
||||||
Ok(_) => return Ok((conn, false)),
|
Ok(_) => return Ok((conn, false)),
|
||||||
Err(e) => {
|
Err(_e) => {
|
||||||
// Banco corrompido
|
// Banco corrompido
|
||||||
drop(conn);
|
drop(conn);
|
||||||
let bak = caminho.with_extension("db.bak");
|
let bak = caminho.with_extension("db.bak");
|
||||||
|
|||||||
@@ -145,55 +145,6 @@ pub fn listar(conn: &Connection) -> Result<Vec<Layout>> {
|
|||||||
layouts
|
layouts
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Busca um layout pelo id.
|
|
||||||
pub fn buscar_por_id(conn: &Connection, id: i64) -> Result<Option<Layout>> {
|
|
||||||
let mut stmt = conn.prepare(
|
|
||||||
"SELECT id, nome, tipo,
|
|
||||||
delimitador, encoding, linha_cabecalho,
|
|
||||||
indice_numero, indice_serie, indice_valor, indice_data,
|
|
||||||
aba, pos_numero, pos_serie, pos_valor, pos_data
|
|
||||||
FROM layouts WHERE id = ?1",
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut results = stmt.query_map([id], |row| {
|
|
||||||
let id: i64 = row.get(0)?;
|
|
||||||
let nome: String = row.get(1)?;
|
|
||||||
let tipo: String = row.get(2)?;
|
|
||||||
|
|
||||||
if tipo == "csv" {
|
|
||||||
let delim_str: String = row.get(3)?;
|
|
||||||
let delimitador = delim_str.chars().next().unwrap_or(';');
|
|
||||||
Ok(Layout::Csv {
|
|
||||||
id: Some(id),
|
|
||||||
nome,
|
|
||||||
config: LayoutCsv {
|
|
||||||
delimitador,
|
|
||||||
encoding: row.get(4)?,
|
|
||||||
linha_cabecalho: row.get::<_, i64>(5)? as usize,
|
|
||||||
indice_numero: row.get::<_, i64>(6)? as usize,
|
|
||||||
indice_serie: row.get::<_, i64>(7)? as usize,
|
|
||||||
indice_valor: row.get::<_, Option<i64>>(8)?.map(|v| v as usize),
|
|
||||||
indice_data: row.get::<_, Option<i64>>(9)?.map(|v| v as usize),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Ok(Layout::Xlsx {
|
|
||||||
id: Some(id),
|
|
||||||
nome,
|
|
||||||
config: LayoutXlsx {
|
|
||||||
aba: row.get(10)?,
|
|
||||||
pos_numero: row.get(11)?,
|
|
||||||
pos_serie: row.get(12)?,
|
|
||||||
pos_valor: row.get(13)?,
|
|
||||||
pos_data: row.get(14)?,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
|
|
||||||
results.next().transpose()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove um layout pelo id.
|
/// Remove um layout pelo id.
|
||||||
pub fn excluir(conn: &Connection, id: i64) -> Result<()> {
|
pub fn excluir(conn: &Connection, id: i64) -> Result<()> {
|
||||||
conn.execute("DELETE FROM layouts WHERE id = ?1", [id])?;
|
conn.execute("DELETE FROM layouts WHERE id = ?1", [id])?;
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ pub fn aplicar_migrations(conn: &Connection) -> Result<()> {
|
|||||||
)
|
)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
if versao_atual < 1 {
|
if versao_atual < VERSAO_SCHEMA_ATUAL {
|
||||||
migration_v1(conn)?;
|
migration_v1(conn)?;
|
||||||
if versao_atual == 0 {
|
if versao_atual == 0 {
|
||||||
conn.execute("INSERT INTO schema_version (versao) VALUES (?1);", [1])?;
|
conn.execute("INSERT INTO schema_version (versao) VALUES (?1);", [VERSAO_SCHEMA_ATUAL])?;
|
||||||
} else {
|
} else {
|
||||||
conn.execute("UPDATE schema_version SET versao = ?1;", [1])?;
|
conn.execute("UPDATE schema_version SET versao = ?1;", [VERSAO_SCHEMA_ATUAL])?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ pub fn ler_xlsx(
|
|||||||
.worksheet_range(nome_aba)
|
.worksheet_range(nome_aba)
|
||||||
.map_err(|e| ErroArquivo::Corrompido(e.to_string()))?;
|
.map_err(|e| ErroArquivo::Corrompido(e.to_string()))?;
|
||||||
|
|
||||||
let mut avisos = ResumoAvisos::default();
|
let avisos = ResumoAvisos::default();
|
||||||
let mut linhas: Vec<Vec<String>> = Vec::new();
|
let mut linhas: Vec<Vec<String>> = Vec::new();
|
||||||
|
|
||||||
let linha_inicio_base0 = (linha_inicio.saturating_sub(1)) as usize;
|
let linha_inicio_base0 = (linha_inicio.saturating_sub(1)) as usize;
|
||||||
|
|||||||
+32
-30
@@ -1,10 +1,6 @@
|
|||||||
use crate::application::usecases::{
|
use crate::application::usecases::{
|
||||||
executar_analise::{expandir_analise, pre_analisar, series_com_intervalo_excessivo},
|
executar_analise::{expandir_analise, pre_analisar, series_com_intervalo_excessivo},
|
||||||
importar_arquivo::{importar_csv, importar_xlsx, listar_abas_xlsx, ResultadoImportacao},
|
layouts::{excluir_layout, listar_layouts, salvar_layout},
|
||||||
layouts::{
|
|
||||||
carregar_layout, excluir_layout, exportar_layout_json, importar_layout_json,
|
|
||||||
listar_layouts, salvar_layout,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use crate::domain::{
|
use crate::domain::{
|
||||||
entities::{
|
entities::{
|
||||||
@@ -14,10 +10,7 @@ use crate::domain::{
|
|||||||
},
|
},
|
||||||
errors::ResumoAvisos,
|
errors::ResumoAvisos,
|
||||||
};
|
};
|
||||||
use crate::infrastructure::{
|
use crate::infrastructure::sqlite::{connection::abrir_banco, migrations::aplicar_migrations};
|
||||||
pdf_generator::GenpdfGenerator,
|
|
||||||
sqlite::{connection::abrir_banco, migrations::aplicar_migrations},
|
|
||||||
};
|
|
||||||
use egui::Context;
|
use egui::Context;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -34,10 +27,7 @@ pub enum EstadoApp {
|
|||||||
/// Resultado pronto para exibição.
|
/// Resultado pronto para exibição.
|
||||||
ExibindoResultado(ResultadoAnalise),
|
ExibindoResultado(ResultadoAnalise),
|
||||||
/// Aguardando confirmação do usuário para expandir faltantes.
|
/// Aguardando confirmação do usuário para expandir faltantes.
|
||||||
ConfirmandoIntervalo {
|
ConfirmandoIntervalo { pre: ResultadoPreAnalise },
|
||||||
pre: ResultadoPreAnalise,
|
|
||||||
series_excessivas: Vec<(String, u64)>,
|
|
||||||
},
|
|
||||||
/// Gerenciamento de layouts.
|
/// Gerenciamento de layouts.
|
||||||
GerenciandoLayouts,
|
GerenciandoLayouts,
|
||||||
/// Análise em execução em background (thread separada).
|
/// Análise em execução em background (thread separada).
|
||||||
@@ -72,6 +62,7 @@ pub enum TipoModal {
|
|||||||
pub enum AcaoModal {
|
pub enum AcaoModal {
|
||||||
ConfirmarExpansaoFaltantes,
|
ConfirmarExpansaoFaltantes,
|
||||||
ConfirmarExclusaoLayout(i64),
|
ConfirmarExclusaoLayout(i64),
|
||||||
|
#[allow(dead_code)]
|
||||||
SobrescreverLayout,
|
SobrescreverLayout,
|
||||||
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.
|
||||||
@@ -396,12 +387,20 @@ impl App {
|
|||||||
/// Processa o resultado recebido do canal de análise em background.
|
/// Processa o resultado recebido do canal de análise em background.
|
||||||
pub fn processar_resultado_pendente(&mut self, resultado: ResultadoPendente) {
|
pub fn processar_resultado_pendente(&mut self, resultado: ResultadoPendente) {
|
||||||
match resultado {
|
match resultado {
|
||||||
ResultadoPendente::Concluido { resultado, avisos, notas } => {
|
ResultadoPendente::Concluido {
|
||||||
|
resultado,
|
||||||
|
avisos,
|
||||||
|
notas,
|
||||||
|
} => {
|
||||||
if let Some(n) = notas {
|
if let Some(n) = notas {
|
||||||
self.notas_importadas = n;
|
self.notas_importadas = n;
|
||||||
}
|
}
|
||||||
if let Some(av) = &avisos {
|
if let Some(av) = &avisos {
|
||||||
self.avisos_importacao = if av.tem_avisos() { Some(av.clone()) } else { None };
|
self.avisos_importacao = if av.tem_avisos() {
|
||||||
|
Some(av.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
}
|
}
|
||||||
self.estado = EstadoApp::ExibindoResultado(resultado);
|
self.estado = EstadoApp::ExibindoResultado(resultado);
|
||||||
if let Some(av) = &self.avisos_importacao.clone() {
|
if let Some(av) = &self.avisos_importacao.clone() {
|
||||||
@@ -418,18 +417,22 @@ impl App {
|
|||||||
notas,
|
notas,
|
||||||
} => {
|
} => {
|
||||||
self.notas_importadas = notas;
|
self.notas_importadas = notas;
|
||||||
self.avisos_importacao = if avisos.tem_avisos() { Some(avisos) } else { None };
|
self.avisos_importacao = if avisos.tem_avisos() {
|
||||||
|
Some(avisos)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let msg = series_excessivas
|
let msg = series_excessivas
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(serie, count)| {
|
.map(|(serie, count)| {
|
||||||
format!("Série {}: intervalo de {} faltantes detectado", serie, count)
|
format!(
|
||||||
|
"Série {}: intervalo de {} faltantes detectado",
|
||||||
|
serie, count
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
self.estado = EstadoApp::ConfirmandoIntervalo {
|
self.estado = EstadoApp::ConfirmandoIntervalo { pre };
|
||||||
pre,
|
|
||||||
series_excessivas,
|
|
||||||
};
|
|
||||||
self.exibir_confirmacao(
|
self.exibir_confirmacao(
|
||||||
"Intervalo muito grande",
|
"Intervalo muito grande",
|
||||||
format!(
|
format!(
|
||||||
@@ -464,16 +467,16 @@ impl App {
|
|||||||
let msg = excessivos
|
let msg = excessivos
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(serie, count)| {
|
.map(|(serie, count)| {
|
||||||
format!("Série {}: intervalo de {} faltantes detectado", serie, count)
|
format!(
|
||||||
|
"Série {}: intervalo de {} faltantes detectado",
|
||||||
|
serie, count
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
let acao = AcaoModal::ConfirmarExpansaoFaltantes;
|
let acao = AcaoModal::ConfirmarExpansaoFaltantes;
|
||||||
self.estado = EstadoApp::ConfirmandoIntervalo {
|
self.estado = EstadoApp::ConfirmandoIntervalo { pre };
|
||||||
pre,
|
|
||||||
series_excessivas: excessivos,
|
|
||||||
};
|
|
||||||
self.exibir_confirmacao(
|
self.exibir_confirmacao(
|
||||||
"Intervalo muito grande",
|
"Intervalo muito grande",
|
||||||
format!(
|
format!(
|
||||||
@@ -502,7 +505,8 @@ impl App {
|
|||||||
egui::TopBottomPanel::top("breadcrumb").show(ctx, |ui| {
|
egui::TopBottomPanel::top("breadcrumb").show(ctx, |ui| {
|
||||||
ui.add_space(4.0);
|
ui.add_space(4.0);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
for (i, label) in ["① Arquivo", "② Colunas", "③ Resultado"].iter().enumerate() {
|
for (i, label) in ["① Arquivo", "② Colunas", "③ Resultado"].iter().enumerate()
|
||||||
|
{
|
||||||
let n = i + 1;
|
let n = i + 1;
|
||||||
let texto = egui::RichText::new(*label);
|
let texto = egui::RichText::new(*label);
|
||||||
if n == passo_ativo {
|
if n == passo_ativo {
|
||||||
@@ -552,8 +556,7 @@ impl eframe::App for App {
|
|||||||
let tela_id = self.tela_atual();
|
let tela_id = self.tela_atual();
|
||||||
|
|
||||||
// Renderizar a tela atual
|
// Renderizar a tela atual
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| match tela_id {
|
||||||
match tela_id {
|
|
||||||
0 => crate::ui::screens::import::renderizar(ui, ctx, self),
|
0 => crate::ui::screens::import::renderizar(ui, ctx, self),
|
||||||
1 => crate::ui::screens::import::renderizar_selecao_aba(ui, ctx, self),
|
1 => crate::ui::screens::import::renderizar_selecao_aba(ui, ctx, self),
|
||||||
2 => crate::ui::screens::configuracao_colunas::renderizar(ui, ctx, self),
|
2 => crate::ui::screens::configuracao_colunas::renderizar(ui, ctx, self),
|
||||||
@@ -570,7 +573,6 @@ impl eframe::App for App {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,6 +100,16 @@ pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if valido && !app.notas_importadas.is_empty() {
|
||||||
|
if ui
|
||||||
|
.button("🔄 Reanalisar")
|
||||||
|
.on_hover_text("Reanalisa as notas já importadas sem reimportar o arquivo")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
app.executar_analise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ui.button("💾 Salvar como layout...").clicked() {
|
if ui.button("💾 Salvar como layout...").clicked() {
|
||||||
app.exibir_modal_salvar_layout();
|
app.exibir_modal_salvar_layout();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use crate::application::usecases::importar_arquivo::{
|
use crate::application::usecases::importar_arquivo::listar_abas_xlsx;
|
||||||
importar_csv, importar_xlsx, listar_abas_xlsx,
|
|
||||||
};
|
|
||||||
use crate::domain::entities::layout::TipoArquivo;
|
use crate::domain::entities::layout::TipoArquivo;
|
||||||
use crate::ui::app::{App, EstadoApp};
|
use crate::ui::app::{App, EstadoApp};
|
||||||
use egui::{Context, Ui};
|
use egui::{Context, Ui};
|
||||||
|
|||||||
@@ -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, LayoutCsv, LayoutXlsx, TipoArquivo};
|
use crate::domain::entities::layout::{Layout, 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};
|
||||||
@@ -83,7 +83,7 @@ pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) {
|
|||||||
|
|
||||||
fn renderizar_secao_layouts(
|
fn renderizar_secao_layouts(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
ctx: &Context,
|
_ctx: &Context,
|
||||||
app: &mut App,
|
app: &mut App,
|
||||||
titulo: &str,
|
titulo: &str,
|
||||||
layouts: &[Layout],
|
layouts: &[Layout],
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use egui::{Context, Ui};
|
|||||||
const OPCOES_PAGINA: &[usize] = &[50, 100, 200, 1000];
|
const OPCOES_PAGINA: &[usize] = &[50, 100, 200, 1000];
|
||||||
|
|
||||||
/// Renderiza a tela de resultados.
|
/// Renderiza a tela de resultados.
|
||||||
pub fn renderizar(ui: &mut Ui, ctx: &Context, app: &mut App) {
|
pub fn renderizar(ui: &mut Ui, _ctx: &Context, app: &mut App) {
|
||||||
// Extrair resultado do estado (sem mover)
|
// Extrair resultado do estado (sem mover)
|
||||||
let resultado = match &app.estado {
|
let resultado = match &app.estado {
|
||||||
EstadoApp::ExibindoResultado(r) => r.clone(),
|
EstadoApp::ExibindoResultado(r) => r.clone(),
|
||||||
|
|||||||
Reference in New Issue
Block a user