feat: implement reimport and analysis functionality for XLSX files

- Added `reimportar_e_analisar` method to `App` struct to handle reimporting and analyzing files.
- Preserved previous analysis results to restore in case of errors or empty imports.
- Updated UI to include a button for reanalyzing the current file.
- Enhanced file selection logic to allow direct processing if a compatible preset is selected.
- Refactored import logic to streamline the analysis process for both CSV and XLSX files.
- Improved user feedback with appropriate error and success messages during file operations.
This commit is contained in:
2026-03-03 23:28:55 -03:00
parent e7d72c7e01
commit 2a21138bf8
5 changed files with 558 additions and 310 deletions
+270 -135
View File
@@ -1,8 +1,8 @@
# PRD — Comparador de Notas
**Versão:** 1.6
**Data:** 02/03/2026
**Status:** Planejamento
**Versão:** 1.7
**Data:** 03/03/2026
**Status:** Implementado (MVP)
---
@@ -53,7 +53,7 @@ Permitir que o usuário importe uma planilha e obtenha automaticamente:
* Notas faltantes
* Notas duplicadas
* Soma total dos valores
* Agrupamento por série
* Agrupamento por série e tipo de documento
## 3.2 Objetivos Secundários
@@ -75,15 +75,21 @@ Permitir que o usuário importe uma planilha e obtenha automaticamente:
* Detecção de quebras de sequência
* Detecção de duplicidades
* Soma de valores
* Agrupamento por série
* Agrupamento por série e tipo de documento
* Pré-visualização das primeiras linhas do arquivo na tela de configuração
* Relatório visual com paginação
* Exportação de relatório para PDF
* Exibição de faltantes agrupados em intervalos contíguos (ex: `1050 (41 notas)`)
* Indicador de completude por série (ex: `48/50 notas — 96,0% completo`)
* Botão de cópia rápida de listas de faltantes/duplicatas para área de transferência
* Exportação de relatório para PDF (fontes Liberation Sans embutidas no binário)
* Salvar layouts personalizados (exclusivos por tipo de arquivo)
* Carregar layouts salvos
* Selecionar layout por menu dropdown
* Excluir layouts
* Exportar layout para JSON
* Importar layout de JSON
* Reanalisar arquivo sem reconfiguração (reimporta o mesmo arquivo com o layout atual)
* Análise em background thread (UI não bloqueia durante importação e análise)
## 4.2 Não Incluído (MVP)
@@ -91,6 +97,7 @@ Permitir que o usuário importe uma planilha e obtenha automaticamente:
* Integração com banco de dados externo
* Multiusuário
* Acesso remoto
* Exportação de resultado em CSV
---
@@ -98,16 +105,17 @@ Permitir que o usuário importe uma planilha e obtenha automaticamente:
O sistema trabalhará com os seguintes campos lógicos:
| Campo | Obrigatório | Descrição |
| ------ | ----------- | ------------------------------------------------------------------------- |
| Numero | Sim | Número incremental da nota. Junto com a Série forma o identificador único. Armazenado internamente como `u64`. |
| Serie | Sim | Série da nota. Deve conter de 1 a 3 dígitos numéricos (regex: `[0-9]{1,3}`, ex: `1`, `01`, `001`). Separa lotes de emissão. |
| Valor | Não | Valor monetário da nota. Armazenado internamente como decimal fixo (`rust_decimal`). |
| Data | Não | Data de emissão da nota. Quando mapeado, exibido como informação adicional no relatório PDF. Não participa de nenhuma regra de validação ou cálculo. |
| Campo | Obrigatório | Descrição |
| --------------- | ----------- | ------------------------------------------------------------------------- |
| Numero | Sim | Número incremental da nota. Armazenado internamente como `u64`. |
| Serie | Sim | Série da nota. Deve conter de 1 a 3 dígitos numéricos (regex: `^[0-9]{1,3}$`, ex: `1`, `01`, `001`). Separa lotes de emissão. |
| Valor | Não | Valor monetário da nota. Armazenado internamente como decimal fixo (`rust_decimal`). |
| Data | Não | Data de emissão da nota. Exibida como informação adicional no relatório PDF. Não participa de nenhuma regra de validação ou cálculo. Formatos aceitos: `dd/mm/aaaa`, `aaaa-mm-dd` e `dd-mm-aaaa`. |
| TipoDocumento | Não | Tipo do documento (ex: `NFE`, `NFCE`). Quando mapeado, compõe a chave de agrupamento junto com a Série. Qualquer string não vazia é aceita. |
Os campos serão mapeados pelo usuário conforme o tipo de arquivo: via **índice numérico** (base 0) para CSV, e via **notação LetraLinha** (ex: `B3`) para XLSX.
> **Identificador único de uma nota:** combinação de `Numero + Serie`. Duplicidade e sequência são sempre avaliadas dentro da mesma série.
> **Identificador único de uma nota:** combinação de `Numero + Serie + TipoDocumento`. Duplicidade e sequência são sempre avaliadas dentro do mesmo grupo `(Serie, TipoDocumento)`. Quando `TipoDocumento` não é mapeado, o agrupamento é feito somente por `Serie` (retrocompatível).
---
@@ -126,7 +134,7 @@ O sistema deve permitir importar arquivos:
| ----------------- | ----------------------------------------------------------------------------- |
| Delimitador | Configurável pelo usuário: vírgula, ponto e vírgula ou tabulação |
| Encoding | Suportados: UTF-8 e Windows-1252 (Latin-1). Configurável pelo usuário |
| Linha do cabeçalho| Configurável pelo usuário (pode estar na linha 1, 4, etc.) |
| Linha do cabeçalho| Configurável pelo usuário (pode estar na linha 1, 4, etc.). 0 = sem cabeçalho |
| Linhas em branco | Devem ser ignoradas silenciosamente |
| Linhas malformadas| Devem ser ignoradas; o sistema deve reportar ao usuário quais linhas foram descartadas, sem interromper a importação |
@@ -160,16 +168,21 @@ O usuário define, via **índice numérico** (posição da coluna, base 0), qual
* Qual índice representa a série (obrigatório)
* Qual índice representa o valor (opcional)
* Qual índice representa a data (opcional)
* Qual índice representa o tipo de documento (opcional)
### RF02.2 — Mapeamento XLSX (letra + linha)
O usuário define, no formato `LetraLinha`, a posição inicial de cada campo na planilha:
* Ex: `B3` indica coluna B a partir da linha 3
* Os campos mapeáveis são os mesmos: Numero (obrigatório), Serie (obrigatório), Valor (opcional) e Data (opcional)
* Os campos mapeáveis são os mesmos: Numero (obrigatório), Serie (obrigatório), Valor (opcional), Data (opcional) e TipoDocumento (opcional)
> A linha informada é a **linha de início dos dados** (não o cabeçalho). O cabeçalho, se existir, é a linha imediatamente anterior.
### RF02.3 — Pré-visualização do Arquivo
A tela de configuração de colunas exibe as primeiras 5 linhas do arquivo com cabeçalho em notação de letras (A, B, C, ... com índice base-0 entre parênteses). A pré-visualização é atualizada automaticamente ao mudar o delimitador ou a aba selecionada.
### Comportamento de Memória
Após a importação, o arquivo permanece em memória e o usuário pode alterar o mapeamento de colunas e reprocessar sem selecionar o arquivo novamente. Ao importar um novo arquivo, os dados do arquivo anterior são descartados da memória.
@@ -207,7 +220,7 @@ Cada layout é exclusivo de um tipo de arquivo: **CSV** ou **XLSX**. Um layout C
Se o usuário importar um arquivo JSON com o nome de um layout já existente no banco, o sistema deve perguntar ao usuário o que fazer, oferecendo as opções:
* Sobrescrever o layout existente
* Salvar com novo nome
* Cancelar a importação
### Erros na Importação de Layout JSON
@@ -227,14 +240,14 @@ Não há limite no número de layouts que podem ser armazenados.
O sistema deve:
* Ordenar os registros por número dentro de cada série
* Ordenar os registros por número dentro de cada grupo `(Serie, TipoDocumento)`
* Detectar números faltantes na sequência
### Regra de Detecção
A sequência é avaliada entre o **menor** e o **maior** número encontrado dentro de cada série. Qualquer número ausente nesse intervalo é considerado faltante.
A sequência é avaliada entre o **menor** e o **maior** número encontrado dentro de cada grupo. Qualquer número ausente nesse intervalo é considerado faltante.
Exemplo: série 001 contém os números `0001, 0002, 0003, 0005``0004` está faltando.
Exemplo: série 001 / NFE contém os números `0001, 0002, 0003, 0005``0004` está faltando.
### Tratamento de Valores Não Numéricos
@@ -251,26 +264,30 @@ Registros com o campo Numero igual a `0` devem ser descartados e reportados ao u
### Escopo da Detecção
Faltantes são detectados **por série**. Cada série possui sua própria sequência independente.
Faltantes são detectados **por grupo `(Serie, TipoDocumento)`**. Cada grupo possui sua própria sequência independente.
### Série com Apenas um Registro
Se uma série contiver apenas um registro, o intervalo de sequência é `numero..numero`. Não há faltantes nesse caso. A série é processada e exibida normalmente.
Se um grupo contiver apenas um registro, o intervalo de sequência é `numero..numero`. Não há faltantes nesse caso. O grupo é processado e exibido normalmente.
### Ordenação
A ordenação dos registros dentro de cada série é sempre **numérica crescente**, independentemente do formato original do campo Numero no arquivo de entrada.
A ordenação dos registros dentro de cada grupo é sempre **numérica crescente**, independentemente do formato original do campo Numero no arquivo de entrada.
### Exibição do Campo Numero
O campo Numero é armazenado e processado como inteiro. Na exibição (listas de faltantes, duplicatas e relatório PDF), o número é exibido **sem zeros à esquerda** (ex: `0001` é exibido como `1`). O formato de exibição não altera a lógica de detecção ou ordenação.
### Agrupamento de Faltantes Contíguos
Na tela de resultado, faltantes consecutivos são exibidos agrupados em intervalos (ex: `1050 (41 notas)`) para facilitar a leitura. Faltantes isolados são exibidos individualmente (ex: `• 75`).
### Proteção contra Intervalos Anormalmente Grandes
Se o intervalo de faltantes de qualquer série exceder **10.000 registros**, o sistema deve:
Se o intervalo de faltantes de qualquer grupo exceder **10.000 registros**, o sistema deve:
1. Interromper o processamento dessa série
2. Exibir aviso informando o intervalo calculado (ex: "Série 001: intervalo de 999.996 faltantes detectado")
1. Interromper o processamento desse grupo
2. Exibir aviso informando o intervalo calculado (ex: "Série 001 / NFE: intervalo de 999.996 faltantes detectado")
3. Solicitar confirmação do usuário antes de continuar
Se o usuário confirmar, o sistema deve listar todos os faltantes normalmente, com paginação. Não há truncamento da lista após a confirmação.
@@ -279,7 +296,7 @@ Esse comportamento protege contra mapeamentos incorretos de colunas que gerariam
### Tratamento de Série com Valor Inválido
A Série é válida se e somente se corresponder à regex `[0-9]{1,3}` após remoção de espaços.
A Série é válida se e somente se corresponder à regex `^[0-9]{1,3}$` após remoção de espaços.
Se o campo Série de um registro estiver vazio, não corresponder à regex ou contiver valor não utilizável:
@@ -294,11 +311,11 @@ Exemplos de valores inválidos: `ABC`, `1A`, `1234` (4 dígitos), string vazia.
O sistema deve identificar registros duplicados.
Um registro é considerado duplicado quando existe mais de uma ocorrência da mesma combinação **Numero + Serie** no arquivo importado.
Um registro é considerado duplicado quando existe mais de uma ocorrência da mesma combinação **Numero + Serie + TipoDocumento** no arquivo importado.
### Exibição das Duplicatas
A lista de duplicatas exibe o identificador (Numero + Serie) e a contagem de ocorrências por grupo. Exemplo: `NF 0004 / Série 001 — 3 ocorrências`. As ocorrências individuais não são listadas separadamente.
A lista de duplicatas exibe o identificador (`Numero + Serie + TipoDocumento`) e a contagem de ocorrências por grupo. Exemplo: `NF 0004 / Série 001 / NFE — 3 ocorrências`. As ocorrências individuais não são listadas separadamente.
### Impacto na Soma de Valores
@@ -311,12 +328,14 @@ Todas as ocorrências de registros duplicados são incluídas na soma de valores
O sistema deve calcular:
* Soma total
* Soma por série
* Soma por grupo `(Serie, TipoDocumento)`
### Formato de Valor Aceito
O sistema deve aceitar valores numéricos em formato brasileiro ou americano, detectando o formato automaticamente por registro seguindo o algoritmo abaixo.
O parser remove prefixos `R$` (maiúsculo ou minúsculo) e espaços antes do processamento.
#### Algoritmo de Parsing Monetário
**Regra 1 — Contém ambos ponto e vírgula:**
@@ -348,7 +367,7 @@ Valores negativos (precedidos de `-`) devem ser rejeitados e reportados ao usuá
Valores que não puderem ser interpretados como número devem ser descartados e reportados ao usuário.
> **Nota de implementação:** O valor deve ser parseado e armazenado internamente como `rust_decimal::Decimal`, nunca como `f64`. Aritmética de ponto flutuante introduz erros de representação em valores monetários (ex: `0.1 + 0.2 ≠ 0.3` em IEEE 754). A soma total e as somas por série devem ser calculadas inteiramente em `Decimal`.
> **Nota de implementação:** O valor deve ser parseado e armazenado internamente como `rust_decimal::Decimal`, nunca como `f64`. Aritmética de ponto flutuante introduz erros de representação em valores monetários (ex: `0.1 + 0.2 ≠ 0.3` em IEEE 754). A soma total e as somas por grupo devem ser calculadas inteiramente em `Decimal`.
### Formato de Exibição de Valores
@@ -360,15 +379,17 @@ Todos os valores monetários são exibidos com **2 casas decimais fixas** no for
O sistema deve exibir:
* Lista de notas faltantes agrupadas por série
* Lista de duplicadas agrupadas por série
* Totais (soma total e soma por série)
* Lista de notas faltantes agrupadas por `(Serie, TipoDocumento)`, com faltantes contíguos agrupados em intervalos
* Indicador de completude por grupo (ex: `48/50 notas — 96,0% completo`)
* Lista de duplicadas agrupadas por `(Serie, TipoDocumento)`
* Totais (soma total e soma por grupo, com contagem de notas por grupo)
* Botão de cópia rápida de listas para a área de transferência
### Organização dos Resultados
| Aspecto | Comportamento |
| ------------- | ----------------------------------------------------- |
| Agrupamento | Resultados sempre agrupados por série |
| Agrupamento | Resultados sempre agrupados por `(Serie, TipoDocumento)`. Quando TipoDocumento não está mapeado, o label do grupo exibe apenas a Série |
| Listas longas | Paginação — o usuário navega entre páginas de resultados |
| Itens por página | Selecionável via dropdown com as opções: 50, 100, 200, 1000 |
@@ -376,15 +397,21 @@ O sistema deve exibir:
O sistema deve permitir exportar o relatório de resultados para **PDF** utilizando a biblioteca `genpdf`.
As fontes do PDF (Liberation Sans) são embutidas no binário em tempo de compilação, eliminando dependência de fontes instaladas no sistema operacional.
O PDF deve conter:
* Notas faltantes por série
* Duplicatas por série
* Totais por série e total geral
* Notas faltantes por grupo `(Serie, TipoDocumento)`
* Duplicatas por grupo
* Totais por grupo e total geral
**Metadados do relatório:**
* Nome do arquivo importado
* Data e hora da geração
* Nome do layout utilizado
* Nome do layout utilizado (se houver)
### RF07.2 — Reanalisar Arquivo
O sistema deve permitir reimportar o mesmo arquivo do disco com o layout atual e executar a análise novamente, sem nenhuma interação adicional. A operação é executada em background thread para não bloquear a interface.
---
@@ -424,6 +451,8 @@ O sistema deve suportar no mínimo:
100.000 registros por arquivo
A análise é executada em uma thread separada (background) para não bloquear a interface gráfica durante o processamento.
> **Nota de implementação:** A detecção de faltantes deve ser implementada de forma **incremental** — ordenar a lista e percorrer comparando elementos consecutivos — evitando a geração de listas intermediárias completas antes da confirmação do usuário. Isso garante consumo de memória proporcional aos dados reais, não ao intervalo.
---
@@ -448,9 +477,10 @@ Todas as mensagens de erro e aviso devem ser exibidas em **modal/popup bloqueant
Quando múltiplos avisos forem gerados durante uma mesma operação (ex: múltiplas linhas malformadas, múltiplos registros descartados), esses avisos devem ser **consolidados em um único modal**, exibindo um resumo ao final da operação. Exemplo de conteúdo consolidado:
* "32 linhas descartadas por mal formação"
* "32 linhas descartadas por malformação"
* "12 valores de Numero inválidos convertidos ou descartados"
* "5 registros com Série inválida descartados"
* "3 valores monetários inválidos descartados"
Cada categoria de problema deve ser exibida como um item separado dentro do mesmo modal. Nunca devem ser abertos múltiplos modais sequenciais para a mesma operação de importação.
@@ -462,9 +492,16 @@ Fluxo principal:
1. Usuário abre o sistema
2. Usuário importa planilha
3. Usuário configura colunas
4. Usuário executa análise
5. Sistema exibe resultado
3. (Para XLSX) Usuário seleciona aba
4. Usuário configura colunas (com pré-visualização das primeiras 5 linhas)
5. Usuário executa análise (processamento em background)
6. Sistema exibe resultado
Fluxo alternativo — layout salvo:
1. Usuário abre o sistema
2. Usuário seleciona layout no dropdown
3. Usuário importa planilha → análise é disparada automaticamente
---
@@ -485,6 +522,7 @@ src/
│ ├─ mod.rs
│ ├─ app.rs
│ ├─ screens/
│ │ ├─ mod.rs (renderizar_tabela_preview, indice_para_letra)
│ │ ├─ import.rs
│ │ ├─ configuracao_colunas.rs
│ │ ├─ layouts.rs
@@ -493,20 +531,25 @@ src/
├─ application/
│ ├─ mod.rs
│ ├─ usecases/
│ │ ├─ mod.rs
│ │ ├─ importar_arquivo.rs
│ │ ├─ executar_analise.rs
│ │ ├─ exportar_pdf.rs
│ │ ├─ layouts.rs
├─ domain/
│ ├─ mod.rs
│ ├─ errors.rs
│ ├─ entities/
│ │ ├─ mod.rs
│ │ ├─ nota.rs
│ │ ├─ serie.rs
│ │ ├─ chave_serie.rs
│ │ ├─ layout.rs
│ │ ├─ resultado_analise.rs
│ │
│ ├─ services/
│ │ ├─ mod.rs
│ │ ├─ detector_sequencia.rs
│ │ ├─ detector_duplicidade.rs
│ │ ├─ parser_monetario.rs
@@ -533,47 +576,77 @@ Contém toda a lógica de negócio real.
**Não pode depender de:**
* egui
* egui / eframe
* rusqlite
* calamine
* csv
* genpdf
Apenas Rust puro + crates matemáticas (`rust_decimal`, `chrono`).
Apenas Rust puro + crates matemáticas (`rust_decimal`, `chrono`) e utilitários de erros (`thiserror`, `regex`).
#### Entidades
**`Nota`**
| Campo | Tipo |
| ------ | ----------------- |
| numero | `u64` |
| serie | `String` |
| valor | `Option<Decimal>` |
| data | `Option<NaiveDate>` |
| Campo | Tipo |
| -------------- | ------------------- |
| numero | `u64` |
| serie | `String` |
| documento_tipo | `Option<String>` |
| valor | `Option<Decimal>` |
| data | `Option<NaiveDate>` |
**`ChaveSerie`**
Chave composta que identifica um grupo de notas. Combina `serie` e `documento_tipo`. Quando `documento_tipo` é `None`, o comportamento é idêntico ao agrupamento somente por série (retrocompatível). Implementa `Hash`, `Eq`, `Ord` para uso como chave de `HashMap` e chave de ordenação.
| Campo | Tipo |
| -------------- | ---------------- |
| serie | `String` |
| documento_tipo | `Option<String>` |
O método `label()` formata para exibição: `"001 / NFE"` quando tipo presente, `"001"` quando ausente.
**`ResultadoPreAnalise`**
Resultado intermediário, antes de materializar os faltantes.
| Campo | Tipo |
| -------------------- | ---------------------------------------- |
| intervalos_por_serie | `HashMap<ChaveSerie, IntervaloSerie>` |
| duplicadas_por_serie | `HashMap<ChaveSerie, Vec<(u64, usize)>>` |
| soma_total | `Decimal` |
| soma_por_serie | `HashMap<ChaveSerie, Decimal>` |
| total_por_serie | `HashMap<ChaveSerie, usize>` |
**`ResultadoAnalise`**
| Campo | Tipo |
| -------------------- | -------------------------------------- |
| faltantes_por_serie | `HashMap<String, Vec<u64>>` |
| duplicadas_por_serie | `HashMap<String, Vec<(u64, usize)>>` |
| soma_total | `Decimal` |
| soma_por_serie | `HashMap<String, Decimal>` |
| Campo | Tipo |
| -------------------- | ---------------------------------------- |
| faltantes_por_serie | `HashMap<ChaveSerie, Vec<u64>>` |
| duplicadas_por_serie | `HashMap<ChaveSerie, Vec<(u64, usize)>>` |
| soma_total | `Decimal` |
| soma_por_serie | `HashMap<ChaveSerie, Decimal>` |
| total_por_serie | `HashMap<ChaveSerie, usize>` |
> **Importante:** a geração dos faltantes não deve ser eager. O use case `executar_analise` deve primeiro calcular os intervalos por série e retornar um resultado intermediário (`ResultadoPreAnalise`) contendo o intervalo calculado. Somente após confirmação do usuário — quando algum intervalo exceder 10.000 registros (RF04) — o sistema expande e materializa a lista completa de faltantes. Isso evita alocar memória para intervalos gerados por mapeamento incorreto de colunas.
#### Services
`detector_sequencia` — recebe `Vec<Nota>` agrupadas por rie, retorna faltantes.
`detector_sequencia` — recebe `Vec<&Nota>` agrupadas por `ChaveSerie`, retorna faltantes. Funções:
- `calcular_intervalo` — retorna `IntervaloSerie` sem materializar a lista completa
- `detectar_faltantes` — materializa a lista completa após confirmação
- `agrupar_contiguos` — agrupa uma lista ordenada de faltantes em pares `(inicio, fim)` para exibição compacta
`detector_duplicidade` — retorna mapa de contagem por `(numero, serie)`.
`detector_duplicidade` — retorna mapa de contagem por `(numero, serie, documento_tipo)`.
`parser_monetario` — implementa exatamente o algoritmo definido no RF06.
`parser_monetario` — implementa exatamente o algoritmo definido no RF06. Também expõe `formatar_valor_br` para exibição no formato `1.234,56`.
#### Erros
`domain/errors.rs` define os erros do domínio de forma tipada (ex: `ErroSerie::Invalida`, `ErroNumero::Zero`, `ErroValor::Negativo`). Nenhuma camada deve propagar `String` livre como erro de domínio.
`domain/errors.rs` define os erros do domínio de forma tipada (ex: `ErroSerie::Invalida`, `ErroNumero::Zero`, `ErroValor::Negativo`, `ErroLayout::NomeConflitante`, `ErroArquivo::TamanhoExcedido`). Nenhuma camada deve propagar `String` livre como erro de domínio.
`ResumoAvisos` consolida contagens de linhas malformadas, números inválidos, séries inválidas e valores inválidos para exibição em um único modal ao final da importação.
---
@@ -585,15 +658,25 @@ Conhece o domain. O domain não conhece o application.
**`executar_analise.rs`**
1. Recebe dados crus
2. Chama `parser_monetario`
3. Chama `detector_sequencia`
4. Chama `detector_duplicidade`
5. Monta `ResultadoAnalise`
Expõe três funções:
1. `pre_analisar(notas)` — calcula intervalos, duplicatas e somas sem expandir faltantes
2. `series_com_intervalo_excessivo(pre)` — retorna grupos com contagem acima de `LIMITE_FALTANTES` (10.000)
3. `expandir_analise(pre, notas)` — materializa a lista completa de faltantes após confirmação
**`exportar_pdf.rs`**
Depende de uma trait abstrata (`PdfGenerator`) definida no próprio módulo application. A implementação concreta fica em `infrastructure/pdf_generator.rs`. Isso evita que o application dependa diretamente de `genpdf`.
Depende da trait abstrata `PdfGenerator` definida em `infrastructure/pdf_generator.rs`. Isso evita que o application dependa diretamente de `genpdf`.
**`importar_arquivo.rs`**
Expõe:
- `importar_csv(caminho, config)` — lê CSV e mapeia para notas
- `importar_xlsx(caminho, config)` — lê XLSX e mapeia para notas
- `listar_abas_xlsx(caminho)` — lista abas antes de configurar
**`layouts.rs`**
Expõe operações de CRUD e import/export de layouts: `salvar_layout`, `listar_layouts`, `excluir_layout`, `exportar_layout_json`, `importar_layout_json`.
---
@@ -603,19 +686,15 @@ Implementa leitores, persistência e geração de arquivos.
| Arquivo | Responsabilidade |
| -------------------------------- | ----------------------------------------- |
| `csv_reader.rs` | Leitura de arquivos CSV via `csv` |
| `xlsx_reader.rs` | Leitura de arquivos XLSX via `calamine` |
| `pdf_generator.rs` | Geração de PDF via `genpdf` |
| `sqlite/connection.rs` | Abertura e inicialização da conexão SQLite |
| `sqlite/migrations.rs` | Aplicação de migrations de schema |
| `sqlite/layout_repository.rs` | CRUD de layouts via `rusqlite` |
| `csv_reader.rs` | Leitura de arquivos CSV via `csv`; `preview_csv` |
| `xlsx_reader.rs` | Leitura de arquivos XLSX via `calamine`; `preview_xlsx`, `parsear_letra_linha`, `listar_abas` |
| `pdf_generator.rs` | Trait `PdfGenerator` + implementação `GenpdfGenerator` via `genpdf`; fontes Liberation Sans embutidas no binário |
| `sqlite/connection.rs` | Abertura e inicialização da conexão SQLite; tratamento de banco corrompido |
| `sqlite/migrations.rs` | Aplicação de migrations de schema (versão atual: 3) |
| `sqlite/layout_repository.rs` | CRUD de layouts via `rusqlite` |
Nada de infrastructure sobe para domain.
#### Por que `layout_repository.rs` dentro de `sqlite/`
Manter o repositório dentro de `sqlite/` concentra todos os artefatos SQLite em um único módulo. Se futuramente o sistema armazenar histórico de análises ou configurações do usuário (RF14/10.8), novos repositórios são adicionados no mesmo lugar sem dispersão.
---
### UI (interface)
@@ -624,16 +703,31 @@ Apenas coleta input, chama use cases e renderiza resultado.
Nenhuma regra de sequência ou parsing monetário deve estar na camada de UI.
#### App (estado global)
`app.rs` contém o estado global da aplicação (`App`), o enum `EstadoApp`, os tipos `Modal`/`TipoModal`/`AcaoModal`, e a lógica de processamento de resultados assíncronos via `mpsc::channel`.
**Estados da aplicação:**
| Estado | Descrição |
| ----------------------- | --------- |
| `Importando` | Tela inicial: seleção de arquivo e layout |
| `SelecionandoAba` | Aguardando seleção de aba XLSX |
| `ConfigurandoColunas` | Mapeamento de colunas com pré-visualização |
| `Analisando` | Análise em execução em background thread |
| `ConfirmandoIntervalo` | Aguardando confirmação do usuário para expandir faltantes |
| `ExibindoResultado` | Resultado pronto para exibição |
| `GerenciandoLayouts` | Gerenciamento de layouts salvos |
#### Screens
| Arquivo | Responsabilidade |
| ------------------------- | ----------------------------------------------------- |
| `import.rs` | Seleção de arquivo e configurações de importação |
| `configuracao_colunas.rs` | Mapeamento de colunas (RF02) |
| `layouts.rs` | Gerenciamento de layouts: salvar, carregar, excluir (RF08) |
| `resultado.rs` | Exibição de resultados com paginação (RF07) |
> `configuracao_colunas.rs` e `layouts.rs` são mantidos separados porque tratam de responsabilidades distintas do RF02 e RF08, evitando que uma única screen acumule lógica de mapeamento de colunas e gerenciamento de persistência.
| `mod.rs` | `renderizar_tabela_preview` e `indice_para_letra` |
| `import.rs` | Seleção de arquivo, dropdown de layout, seleção de aba XLSX |
| `configuracao_colunas.rs` | Mapeamento de colunas com pré-visualização (RF02) |
| `layouts.rs` | Gerenciamento de layouts: salvar, carregar, excluir, exportar/importar JSON (RF08) |
| `resultado.rs` | Exibição de resultados com paginação, intervalos contíguos, indicador de completude, botões copiar/reanalisar/exportar PDF (RF07) |
---
@@ -646,26 +740,34 @@ Infrastructure entra apenas quando necessário.
Exemplo real:
1. UI chama `importar_arquivo`
1. UI chama `executar_importacao` (thread separada)
2. Infrastructure lê CSV/XLSX
3. Application transforma registros em entidades `Nota`
4. Domain executa análise
5. Application retorna `ResultadoAnalise`
6. UI renderiza
4. Domain executa pré-análise (`pre_analisar`)
5. Se intervalo excessivo: UI solicita confirmação → Domain expande faltantes (`expandir_analise`)
6. Application retorna `ResultadoAnalise` via `mpsc::channel`
7. UI renderiza
---
## 9.4 Stack Tecnológica
| Camada | Tecnologia | Status |
| ---------------- | ------------------ | -------------- |
| Linguagem | Rust | Definido |
| Framework de UI | egui | Definido |
| SQLite | rusqlite | Definido |
| Leitura de CSV | csv | Definido |
| Leitura de XLSX | calamine | Definido |
| Decimal fixo | rust_decimal | Definido |
| Geração de PDF | genpdf | Definido |
| Camada | Tecnologia | Versão |
| ---------------- | ------------------ | ------- |
| Linguagem | Rust | edition 2024 |
| Framework de UI | egui + eframe | 0.31 |
| Diálogos nativos | rfd | 0.15 |
| SQLite | rusqlite (bundled) | 0.32 |
| Leitura de CSV | csv | 1.3 |
| Leitura de XLSX | calamine | 0.26 |
| Decimal fixo | rust_decimal | 1.36 |
| Geração de PDF | genpdf | 0.2 |
| Datas | chrono | 0.4 |
| Encoding | encoding_rs | 0.8 |
| Caminhos de dados| dirs | 5 |
| Erros tipados | thiserror | 2 |
| Regex | regex | 1 |
| Serialização | serde + serde_json | 1 |
---
@@ -723,6 +825,14 @@ Os layouts salvos anteriormente serão perdidos neste cenário. O arquivo `confi
O banco de dados deve conter uma tabela de controle de versão (`schema_version`) com o número da versão atual do schema. A cada inicialização, o sistema deve verificar a versão e aplicar migrations automáticas quando necessário, garantindo compatibilidade com versões anteriores do banco.
**Versão atual do schema: 3**
| Versão | Alteração |
| ------ | --------- |
| 1 | Criação da tabela `layouts` |
| 2 | Índice único em `layouts.nome`; renomeia duplicatas com sufixo `(id)` |
| 3 | Adição das colunas `indice_documento_tipo` (CSV) e `pos_documento_tipo` (XLSX) |
------------------------------------------------------------------------
## 10.3 Dados Armazenados
@@ -741,31 +851,33 @@ Cada layout é exclusivo de um tipo de arquivo (`csv` ou `xlsx`). As configuraç
| Campo | Tipo | Descrição |
| ---------- | ------- | -------------------------------------- |
| id | inteiro | Identificador único |
| nome | texto | Nome do layout |
| id | inteiro | Identificador único (auto-incremento) |
| nome | texto | Nome do layout (único no banco) |
| tipo | texto | Tipo do arquivo: `csv` ou `xlsx` |
**Campos exclusivos de layouts CSV:**
| Campo | Tipo | Descrição |
| ------------------ | ------ | -------------------------------------------------- |
| delimitador | texto | Caractere delimitador (`,`, `;`, `\t`) |
| encoding | texto | Encoding do arquivo (`utf-8` ou `windows-1252`) |
| linha_cabecalho | inteiro | Número da linha do cabeçalho (base 1) |
| indice_numero | inteiro | Índice da coluna Numero (base 0) |
| indice_serie | inteiro | Índice da coluna Serie (base 0) |
| indice_valor | inteiro | Índice da coluna Valor (base 0, nulo se ausente) |
| indice_data | inteiro | Índice da coluna Data (base 0, nulo se ausente) |
| Campo | Tipo | Descrição |
| ---------------------- | ------- | ---------------------------------------------------------- |
| delimitador | texto | Caractere delimitador (`,`, `;`, `\t`) |
| encoding | texto | Encoding do arquivo (`utf-8` ou `windows-1252`) |
| linha_cabecalho | inteiro | Número da linha do cabeçalho (base 1). 0 = sem cabeçalho |
| indice_numero | inteiro | Índice da coluna Numero (base 0) |
| indice_serie | inteiro | Índice da coluna Serie (base 0) |
| indice_valor | inteiro | Índice da coluna Valor (base 0, nulo se ausente) |
| indice_data | inteiro | Índice da coluna Data (base 0, nulo se ausente) |
| indice_documento_tipo | inteiro | Índice da coluna TipoDocumento (base 0, nulo se ausente) |
**Campos exclusivos de layouts XLSX:**
| Campo | Tipo | Descrição |
| -------------- | ------ | ---------------------------------------------------------------- |
| aba | texto | Nome ou índice da aba a ser processada |
| pos_numero | texto | Posição inicial da coluna Numero no formato `LetraLinha` (ex: `D3`) |
| pos_serie | texto | Posição inicial da coluna Serie no formato `LetraLinha` (ex: `B3`) |
| pos_valor | texto | Posição inicial da coluna Valor (nulo se ausente) |
| pos_data | texto | Posição inicial da coluna Data (nulo se ausente) |
| Campo | Tipo | Descrição |
| ------------------ | ------ | ----------------------------------------------------------------- |
| aba | texto | Nome da aba a ser processada |
| pos_numero | texto | Posição inicial da coluna Numero no formato `LetraLinha` (ex: `D3`) |
| pos_serie | texto | Posição inicial da coluna Serie no formato `LetraLinha` (ex: `B3`) |
| pos_valor | texto | Posição inicial da coluna Valor (nulo se ausente) |
| pos_data | texto | Posição inicial da coluna Data (nulo se ausente) |
| pos_documento_tipo | texto | Posição inicial da coluna TipoDocumento (nulo se ausente) |
------------------------------------------------------------------------
@@ -785,15 +897,19 @@ Excluir layout
## 10.6 Interface do Usuário
Os layouts salvos devem ser exibidos em um menu dropdown.
Os layouts salvos devem ser exibidos em um menu dropdown, filtrado pelo tipo de arquivo atual (CSV ou XLSX).
O usuário deve poder:
Selecionar layout existente
Criar novo layout
Criar novo layout (via modal com campo de texto ou via tela de gerenciamento)
Excluir layout
Excluir layout (com confirmação)
Exportar layout para JSON
Importar layout de JSON (com tratamento de conflito de nome)
------------------------------------------------------------------------
@@ -835,15 +951,16 @@ O campo `tipo` define qual conjunto de configurações está presente no arquivo
```json
{
"nome": "Layout Padrão CSV",
"tipo": "csv",
"nome": "Layout Padrão CSV",
"delimitador": ";",
"encoding": "utf-8",
"linha_cabecalho": 1,
"indice_numero": 3,
"indice_serie": 1,
"indice_valor": 5,
"indice_data": null
"indice_data": null,
"indice_documento_tipo": null
}
```
@@ -851,16 +968,19 @@ O campo `tipo` define qual conjunto de configurações está presente no arquivo
```json
{
"nome": "Layout Padrão XLSX",
"tipo": "xlsx",
"nome": "Layout Padrão XLSX",
"aba": "Plan1",
"pos_numero": "D3",
"pos_serie": "B3",
"pos_valor": "F3",
"pos_data": null
"pos_data": null,
"pos_documento_tipo": null
}
```
> **Nota:** o campo `tipo` é usado como tag de discriminante pelo `serde` (`#[serde(tag = "tipo")]`). O campo `indice_documento_tipo` / `pos_documento_tipo` usa `#[serde(default)]` para retrocompatibilidade com arquivos JSON exportados antes da versão 1.7.
---
# 12. Critérios de Aceite
@@ -869,14 +989,17 @@ O sistema será considerado funcional quando:
* Importar planilha CSV com configurações de delimitador, encoding e linha de cabeçalho
* Importar planilha XLSX com seleção de aba e posicionamento por `LetraLinha`
* Detectar notas faltantes por série corretamente
* Detectar duplicatas por série corretamente
* Calcular soma total e soma por série corretamente
* Exibir resultados agrupados por série com paginação
* Detectar notas faltantes por grupo `(Serie, TipoDocumento)` corretamente
* Detectar duplicatas por grupo corretamente
* Calcular soma total e soma por grupo corretamente
* Exibir resultados agrupados por grupo com paginação e intervalos contíguos
* Exibir indicador de completude por grupo
* Exportar relatório de resultados para PDF
* Salvar, carregar, selecionar e excluir layouts
* Exportar e importar layouts via JSON
* Exibir aviso de confirmação quando intervalo de faltantes exceder 10.000 por série
* Exibir aviso de confirmação quando intervalo de faltantes exceder 10.000 por grupo
* Executar análise em background sem bloquear a interface
* Exibir pré-visualização das primeiras 5 linhas do arquivo na tela de configuração
---
@@ -885,25 +1008,29 @@ O sistema será considerado funcional quando:
O MVP inclui o escopo completo descrito neste PRD:
* Importar CSV e XLSX
* Mapear colunas Numero, Serie, Valor e Data
* Detectar notas faltantes por série
* Detectar duplicatas por série
* Calcular soma total e por série
* Exibir resultados agrupados por série com paginação
* Mapear colunas Numero, Serie, Valor, Data e TipoDocumento
* Detectar notas faltantes por grupo `(Serie, TipoDocumento)`
* Detectar duplicatas por grupo
* Calcular soma total e por grupo
* Exibir resultados agrupados por grupo com paginação e intervalos contíguos
* Exportar relatório para PDF
* Gerenciar layouts (salvar, carregar, excluir)
* Exportar e importar layouts via JSON
* Gerenciar layouts (salvar, carregar, excluir, exportar/importar JSON)
* Reanalisar arquivo sem reconfiguração
---
# 14. Evoluções Futuras
Possíveis melhorias:
Possíveis melhorias (ver `FEATURES_BACKLOG.md` para detalhes):
* Exportação de resultado em CSV (`faltantes.csv`, `duplicatas.csv`)
* Busca por número na tela de resultado
* Auto-detecção de delimitador CSV
* Auto-detecção de encoding CSV
* Agrupamento de faltantes como intervalos no PDF
* Integração com ERP
* Histórico de análises
* Automação de importação (monitorar pasta)
* Exportação para CSV
* Multiusuário
---
@@ -918,8 +1045,16 @@ Série
Agrupador independente de sequência.
TipoDocumento
Subtipo de documento dentro de uma série (ex: NFE, NFCE). Quando mapeado, compõe a chave de agrupamento junto com a Série.
ChaveSerie
Chave composta `(Serie, TipoDocumento)` que identifica um grupo de notas para fins de detecção de sequência, duplicidade e cálculo de somas.
Sequência
Ordem numérica crescente sem lacunas.
Ordem numérica crescente sem lacunas dentro de um mesmo grupo `(Serie, TipoDocumento)`.
---