From 3af14ab9574bbd41330607db5c237f1435385d60 Mon Sep 17 00:00:00 2001 From: Felipe Canin Novaes Date: Wed, 4 Mar 2026 18:18:11 -0300 Subject: [PATCH] Create AGENTS.md --- MIGRATION_ICED.md | 504 ------------------ ROTEIRO.md | 217 -------- UI_REDESIGN.md | 291 ---------- .../FEATURES_BACKLOG.md | 0 .../FIX_SALVAR_LAYOUT.md | 0 src/application/AGENTS.md | 205 +++++++ src/domain/AGENTS.md | 304 +++++++++++ src/infrastructure/AGENTS.md | 207 +++++++ src/infrastructure/sqlite/AGENTS.md | 202 +++++++ 9 files changed, 918 insertions(+), 1012 deletions(-) delete mode 100644 MIGRATION_ICED.md delete mode 100644 ROTEIRO.md delete mode 100644 UI_REDESIGN.md rename FEATURES_BACKLOG.md => docs/FEATURES_BACKLOG.md (100%) rename FIX_SALVAR_LAYOUT.md => docs/FIX_SALVAR_LAYOUT.md (100%) create mode 100644 src/application/AGENTS.md create mode 100644 src/domain/AGENTS.md create mode 100644 src/infrastructure/AGENTS.md create mode 100644 src/infrastructure/sqlite/AGENTS.md diff --git a/MIGRATION_ICED.md b/MIGRATION_ICED.md deleted file mode 100644 index 8867620..0000000 --- a/MIGRATION_ICED.md +++ /dev/null @@ -1,504 +0,0 @@ -# Migração egui → iced: Estado e Referência - -**Branch:** `change-ui` -**Versão iced:** 0.13.1 -**Última atualização:** 04/03/2026 -**Status:** ✅ **COMPILANDO** — `cargo build` passa sem erros (5 warnings não-bloqueantes, todos em camadas intocáveis) - ---- - -## Estado Atual - -### O que foi feito (100% completo) - -| Arquivo | Status | Descrição | -|---|---|---| -| `Cargo.toml` | ✅ | `eframe`/`egui`/`image` removidos; `iced 0.13` + `tokio` adicionados | -| `src/main.rs` | ✅ | Reescrito com `iced::application(...)` | -| `src/ui/mod.rs` | ✅ | `pub mod app; pub mod components; pub mod message; pub mod screens;` | -| `src/ui/message.rs` | ✅ | Enum `Message` completo + enum `ResultadoPendente` | -| `src/ui/app.rs` | ✅ | Reescrito para iced (Elm architecture) — `App`, `update()`, `view()` | -| `src/ui/screens/mod.rs` | ✅ | Re-exports + `pub fn indice_para_letra(idx: usize) -> String` | -| `src/ui/screens/import.rs` | ✅ | Tela de importação — pick_list de layout usa `LayoutSelecionado` | -| `src/ui/screens/selecionar_aba.rs` | ✅ | Tela de seleção de aba XLSX | -| `src/ui/screens/configuracao_colunas.rs` | ✅ | Tela de configuração — pick_list de layout usa `LayoutSelecionado` | -| `src/ui/screens/resultado.rs` | ✅ | Tela de resultado — botão "Nova Análise" usa `Message::NovaAnalise` | -| `src/ui/screens/layouts.rs` | ✅ | Tela de gerenciamento de layouts | -| `src/ui/components/mod.rs` | ✅ | `pub mod modal; pub mod paginacao; pub mod tabela_preview;` | -| `src/ui/components/modal.rs` | ✅ | Overlay real com `stack!` + `mouse_area` | -| `src/ui/components/tabela_preview.rs` | ✅ | Tabela com scroll horizontal e cabeçalho estilo Excel | -| `src/ui/components/paginacao.rs` | ✅ | Controles reutilizáveis de paginação | - -### O que NÃO foi alterado (intocado por design) - -- `src/domain/` — entidades, serviços, erros -- `src/application/` — casos de uso -- `src/infrastructure/` — leitores CSV/XLSX, gerador PDF, SQLite - ---- - -## Arquitetura da UI - -### Padrão Elm (iced) - -``` -App (estado) ──→ view(&self) ──→ Element (widgets renderizados) - ↑ ↓ - │ usuário interage - │ ↓ - └── update(&mut self, msg) ←── Message (enum) - retorna Task -``` - -- `view()` é **pura e somente leitura** — nunca muta estado -- Toda mutação acontece **exclusivamente** em `update()` -- Background tasks retornam via `Task::perform(async { ... }, Message::Variante)` - -### Estrutura de arquivos - -``` -src/ -├── main.rs -└── ui/ - ├── mod.rs - ├── app.rs (struct App + update + view) - ├── message.rs (enum Message + ResultadoPendente) - ├── screens/ - │ ├── mod.rs (re-exports + indice_para_letra) - │ ├── import.rs - │ ├── selecionar_aba.rs - │ ├── configuracao_colunas.rs - │ ├── resultado.rs - │ └── layouts.rs - └── components/ - ├── mod.rs - ├── modal.rs - ├── tabela_preview.rs - └── paginacao.rs -``` - ---- - -## Diferenças Críticas da API — iced 0.13 - -Estas diferenças causaram erros de compilação e **devem ser lembradas** em qualquer adição futura: - -### 1. `Command` foi renomeado para `Task` - -```rust -// ❌ NÃO EXISTE em iced 0.13 -use iced::Command; -Command::none() - -// ✅ CORRETO -use iced::Task; -Task::none() -Task::perform(future, mapper) -``` - -Todos os retornos de `update()` são `Task`. - -### 2. `.align_items()` foi dividido - -```rust -// ❌ NÃO EXISTE em iced 0.13 -row![...].align_items(Alignment::Center) - -// ✅ CORRETO — Row usa align_y, Column usa align_x -row![...].align_y(Alignment::Center) -column![...].align_x(Alignment::Center) -``` - -### 3. `.center_x()` / `.center_y()` exigem argumento `Length` - -```rust -// ❌ NÃO COMPILA -container(...).center_x().center_y() - -// ✅ CORRETO -container(...).center_x(Length::Fill).center_y(Length::Fill) -``` - -### 4. `iced::clipboard::write` retorna `Task` - -```rust -// Pode ser retornado diretamente de update(): -return iced::clipboard::write(texto); -``` - -### 5. Entry point usa API funcional, não trait `Application` - -```rust -// main.rs -fn main() -> iced::Result { - iced::application("Título", App::update, App::view) - .window(iced::window::Settings { ... }) - .run_with(App::new) -} - -// App::new retorna (Self, Task) -pub fn new() -> (Self, Task) { ... } -``` - -### 6. Lifetimes em funções que retornam `Element` - -Sempre usar `Element<'_, Message>` (não `Element`) em funções que recebem referências: - -```rust -// ❌ Gera warning mismatched_lifetime_syntaxes -pub fn view(app: &App) -> Element - -// ✅ CORRETO -pub fn view(app: &App) -> Element<'_, Message> -``` - -**Armadilha com Vec local:** criar `Vec` local e passar `&vec` para função que retorna `Element<'a>` causa E0515. Solução: filtrar diretamente de dados que vivem no `&app`. - -### 7. Borrow checker com `Arc>` - -```rust -// ❌ Mantém borrow imutável de &self enquanto tenta &mut self -if let Some(conn_arc) = &self.conn { - let conn = conn_arc.lock().unwrap(); - self.exibir_erro(...); // ERRO: borrow ativo -} - -// ✅ Clonar o Arc primeiro (O(1), não clona a Connection) -let conn_arc = self.conn.as_ref().map(Arc::clone); -if let Some(conn_arc) = conn_arc { - let conn = conn_arc.lock().unwrap(); - drop(conn); // liberar antes de &mut self - self.exibir_erro(...); -} -``` - ---- - -## Componentes Implementados - -### `App` (src/ui/app.rs) - -**Estado:** -```rust -pub struct App { - pub estado: EstadoApp, - pub conn: Option>>, - pub banco_foi_recriado: bool, - pub notas_importadas: Vec, - pub caminho_arquivo: Option, // ← populado para CSV e XLSX - pub nome_arquivo: String, - pub tipo_arquivo_atual: TipoArquivo, - pub layout_csv_atual: LayoutCsv, - pub layout_xlsx_atual: LayoutXlsx, - pub nome_layout_atual: String, - pub abas_xlsx: Vec, - pub layouts_salvos: Vec, - pub modal: Option, - pub avisos_importacao: Option, - pub pagina_faltantes: usize, - pub pagina_duplicatas: usize, - pub itens_por_pagina: usize, - pub preview_arquivo: Option>>, - pub resultado_anterior: Option, -} -``` - -**Máquina de estados (`EstadoApp`):** -``` -Importando - → ArquivoSelecionado (CSV) → ConfigurandoColunas - → ArquivoSelecionado (XLSX) → SelecionandoAba → ConfigurandoColunas -ConfigurandoColunas - → ExecutarImportacao → Analisando -Analisando - → AnaliseCompleta (ok, sem excessivos) → ExibindoResultado - → AnaliseCompleta (excessivos) → ConfirmandoIntervalo (+ modal) -ConfirmandoIntervalo - → ModalConfirmado (ConfirmarExpansaoFaltantes) → Analisando → ExibindoResultado - → ModalCancelado → ConfigurandoColunas -ExibindoResultado - → ReanalisarArquivo → Analisando - → IrParaConfiguracaoColunas → ConfigurandoColunas - → NovaAnalise (modal) → Importando (após confirmação) -GerenciandoLayouts - → IrParaImportacao → Importando -``` - -**Modal (`EstadoModal` / `AcaoModal`):** -```rust -pub enum EstadoModal { - Informacao { titulo: String, mensagem: String }, // definido, não usado ainda - Aviso { titulo: String, mensagem: String }, - Erro { titulo: String, mensagem: String }, - Confirmacao { titulo: String, mensagem: String, acao: AcaoModal }, - InputTexto { titulo: String, mensagem: String, texto: String, acao: AcaoModal }, -} - -pub enum AcaoModal { - ConfirmarExpansaoFaltantes, - ConfirmarExclusaoLayout(i64), - SobrescreverLayout(Layout), - ConfirmarNovaAnalise, - SalvarLayoutConfig, -} -``` - -**Comportamento do `ModalCancelado`:** -- Fecha o modal (`self.modal = None`) -- Se o estado for `ConfirmandoIntervalo`, **também** reseta para `ConfigurandoColunas` - -### `Message` (src/ui/message.rs) — enum completo - -```rust -pub enum Message { - // Inicialização - BancoInicializado(Result<(Arc>, bool, Vec), String>), - LayoutsRecarregados(Vec), - - // Navegação - IrParaImportacao, IrParaConfiguracaoColunas, IrParaLayouts, Voltar, - - // Arquivo - SelecionarArquivo, - ArquivoSelecionado(PathBuf), - AbaSelecionada(String), - AbaxlsxCarregadas { caminho: PathBuf, abas: Vec, layout_xlsx: LayoutXlsx, nome_layout: String }, - XlsxErroAoCarregar(String), - - // Background - AnaliseCompleta(ResultadoPendente), - - // Config CSV - DelimitadorAlterado(char), EncodingAlterado(String), LinhaCabecalhoAlterada(usize), - IndiceNumeroAlterado(usize), IndiceSerieAlterado(usize), - IndiceValorToggle(bool), IndiceValorAlterado(usize), - IndiceDataToggle(bool), IndiceDataAlterado(usize), - IndiceDocTipoToggle(bool), IndiceDocTipoAlterado(usize), - - // Config XLSX - AbaXlsxAlterada(String), - PosNumeroAlterada(String), PosSerieAlterada(String), - PosValorToggle(bool), PosValorAlterada(String), - PosDataToggle(bool), PosDataAlterada(String), - PosDocTipoToggle(bool), PosDocTipoAlterada(String), - - // Análise - ExecutarImportacao, ReanalisarArquivo, - ConfirmarExpansaoFaltantes, - CancelarExpansao, // arm existe em update() mas nenhum widget o dispara diretamente - NovaAnalise, // abre modal de confirmação (AcaoModal::ConfirmarNovaAnalise) - - // Resultado - PaginaFaltantesAlterada(usize), PaginaDuplicatasAlterada(usize), - ItensPorPaginaAlterado(usize), - CopiarFaltantes(ChaveSerie), CopiarDuplicatas(ChaveSerie), - ExportarPdf, PdfExportado(Result), - - // Layouts - LayoutSelecionado(i64), // aplica layout (chama aplicar_layout) + atualiza nome - SalvarLayout, // abre modal InputTexto - NomeLayoutAlterado(String), - ExcluirLayout(i64), ExclusaoConfirmada(i64), - ExportarLayoutJson(i64), ImportarLayoutJson, LayoutJsonImportado(String), - SobrescreverLayout(Layout), - - // Modal - ModalTextoAlterado(String), ModalConfirmado, ModalCancelado, - - Noop, -} -``` - -### `modal.rs` (src/ui/components/modal.rs) - -Overlay real: `stack![conteudo, overlay_escuro]`. O `mouse_area` captura cliques fora da caixa e dispara `ModalCancelado`. - -```rust -pub fn view_com_modal<'a>( - conteudo: Element<'a, Message>, - modal: &'a EstadoModal, -) -> Element<'a, Message> -``` - -Botões da caixa modal: -- Sempre: **"Fechar"** → `ModalCancelado` -- Se `com_confirmar`: **"Confirmar"** → `ModalConfirmado` - -### `tabela_preview.rs` (src/ui/components/tabela_preview.rs) - -Cabeçalho com letras estilo Excel (A, B, C...) + índice numérico. Scroll horizontal. Trunca células em 30 caracteres. Altura fixa de 160px. - -### `paginacao.rs` (src/ui/components/paginacao.rs) - -```rust -pub fn controles_paginacao( - pagina_atual: usize, - total_paginas: usize, - msg_anterior: Message, - msg_proximo: Message, -) -> Element<'static, Message> -``` - -Botões ◀ / ▶ desabilitados automaticamente na primeira/última página via `on_press_maybe`. - ---- - -## Regras de Seleção de Layout (pick_list) - -**Em todas as telas** que exibem um `pick_list` de layouts, o handler **deve** usar `LayoutSelecionado(id)`, não `NomeLayoutAlterado`: - -```rust -pick_list(opcoes_layout, nome_sel, { - let layouts = app.layouts_salvos.clone(); - move |nome: String| { - if let Some(id) = layouts.iter().find(|l| l.nome() == nome).and_then(|l| l.id()) { - Message::LayoutSelecionado(id) - } else { - Message::NomeLayoutAlterado(nome) // fallback (nunca deve ocorrer com layouts do banco) - } - } -}) -``` - -Isso vale para: `import.rs`, `configuracao_colunas.rs`. A diferença: -- `NomeLayoutAlterado` — só atualiza `self.nome_layout_atual` (string), **não preenche o formulário** -- `LayoutSelecionado(id)` — chama `aplicar_layout()`, que preenche `layout_csv_atual` ou `layout_xlsx_atual` - ---- - -## Fluxo XLSX — Detalhe Crítico - -### Onde `caminho_arquivo` é populado - -Para **CSV**: em `processar_arquivo_selecionado()`, logo após detectar a extensão. - -Para **XLSX**: o caminho fica dentro do estado `SelecionandoAba { caminho }` e é copiado para `self.caminho_arquivo` **somente quando o usuário seleciona uma aba** (handler `AbaSelecionada`). - -``` -ArquivoSelecionado(xlsx) - → AbaxlsxCarregadas - → aba já definida no layout e existe no arquivo? - Sim → caminho_arquivo setado aqui → disparar_importacao() - Não → estado = SelecionandoAba { caminho } ← caminho_arquivo ainda None aqui - → AbaSelecionada(aba) - → caminho_arquivo = Some(caminho) ← só aqui é setado - → estado permanece SelecionandoAba - → IrParaConfiguracaoColunas - → estado = ConfigurandoColunas - → tem_arquivo = true ✓ -``` - -**Consequência:** nunca verificar `app.caminho_arquivo.is_some()` como condição de habilitação antes do usuário ter selecionado uma aba no fluxo XLSX. - ---- - -## Background Tasks - -Todas as operações pesadas usam `Task::perform`: - -```rust -// Padrão para operações síncronas em background: -Task::perform( - async move { - tokio::task::spawn_blocking(move || { - // código síncrono (rusqlite, calamine, etc.) - }) - .await - .unwrap_or_else(|e| ResultadoPendente::Erro(e.to_string())) - }, - Message::AnaliseCompleta, -) -``` - -| Operação | Message de retorno | -|---|---| -| Abrir banco + migrations + listar layouts | `BancoInicializado` | -| Listar layouts após salvar/excluir | `LayoutsRecarregados` | -| Listar abas de arquivo XLSX | `AbaxlsxCarregadas` / `XlsxErroAoCarregar` | -| Importar CSV/XLSX + analisar | `AnaliseCompleta` | -| Expandir análise com faltantes | `AnaliseCompleta` | -| Exportar PDF | `PdfExportado` | -| Exportar layout JSON | `Noop` (salva diretamente) | -| Importar layout JSON | `LayoutJsonImportado` | - -**Diálogos de arquivo** usam `rfd::AsyncFileDialog` (não bloqueia): -```rust -Task::perform( - async { - rfd::AsyncFileDialog::new() - .add_filter("Planilhas", &["csv", "xlsx", "xls"]) - .pick_file() - .await - .map(|h| h.path().to_path_buf()) - }, - |r| r.map(Message::ArquivoSelecionado).unwrap_or(Message::Noop), -) -``` - ---- - -## Warnings Existentes (não-bloqueantes) - -| Warning | Local | Situação | -|---|---|---| -| `field data is never read` | `domain/nota.rs` | Domínio intocável — ignorar | -| `fields minimo/maximo never read` | `domain/resultado_analise.rs` | Domínio intocável — ignorar | -| `method sem_inconsistencias never used` | `domain/resultado_analise.rs` | Domínio intocável — ignorar | -| `variant Informacao never constructed` | `ui/app.rs` | Mantido para uso futuro (modal informativo) | -| `variant CancelarExpansao never constructed` | `ui/message.rs` | O arm existe em `update()`, mas nenhum widget o dispara; o cancelamento do intervalo ocorre via `ModalCancelado` | - ---- - -## O Que Falta Implementar - -### Bugs/lacunas conhecidos - -1. **`selecionar_aba.rs` — botões sem diferenciação visual entre aba selecionada e não selecionada** — ambos os branches do `if selecionada` são idênticos (sem estilo diferente). A aba ativa deveria ter destaque visual. - -2. **`selecionar_aba.rs` — layout não é aplicável nessa tela** — se o usuário chegou aqui sem ter selecionado um layout previamente, não há como selecionar um layout antes de avançar para ConfigurandoColunas. Considerar adicionar um `pick_list` de layouts na tela `SelecionandoAba`. - -3. **Sem tema dark/light** — usa o tema padrão do sistema. Para adicionar: - ```rust - // Em main.rs: encadear .theme(|app, _| app.tema.clone()) - // Em App: campo pub tema: iced::Theme - // Mensagem: Message::TemaAlterado(iced::Theme) - ``` - -4. **Sem drag-and-drop de arquivos** — importação tem apenas o botão de seleção. Verificar suporte nativo em iced 0.13 antes de implementar. - -5. **Sem filtro/busca nos resultados** — sem `text_input` para filtrar `faltantes_por_serie`. - -6. **`EstadoModal::Informacao` nunca é usado** — definido mas sem chamadas. Remover ou usar. - -### Funcionalidades futuras (Fase 6 do plano original) - -- Histórico de análises (nova tela + schema SQLite v4) -- Gráficos de completude por série -- Exportação para Excel/CSV - ---- - -## Como Rodar - -```bash -# Verificar sem compilar binário (rápido) -cargo check - -# Compilar (primeira vez ~1 min por baixar iced + wgpu) -cargo build - -# Executar em modo debug -cargo run - -# Build de release (otimizado, sem console no Windows) -cargo build --release -``` - ---- - -## Referências - -- [iced 0.13 changelog](https://github.com/iced-rs/iced/blob/master/CHANGELOG.md) -- [iced exemplos oficiais](https://github.com/iced-rs/iced/tree/master/examples) -- [rfd AsyncFileDialog](https://docs.rs/rfd/latest/rfd/struct.AsyncFileDialog.html) -- [iced::clipboard::write](https://docs.rs/iced/0.13.1/iced/clipboard/fn.write.html) diff --git a/ROTEIRO.md b/ROTEIRO.md deleted file mode 100644 index 0c3265d..0000000 --- a/ROTEIRO.md +++ /dev/null @@ -1,217 +0,0 @@ -# Roteiro de Desenvolvimento — Comparador de Notas - -**Gerado em:** 04/03/2026 -**Baseado em:** PRD v1.7 + análise estática do código atual - ---- - -## 1. Warnings do Compilador - -São 7 warnings ativos (`cargo build`). Nenhum é crítico, mas todos devem ser eliminados para manter o código limpo. - -### W-01 — `Nota.data` nunca lida - -**Arquivo:** `src/domain/entities/nota.rs:17` -**Causa:** O campo `data: Option` é armazenado na struct `Nota`, mas nenhum código de negócio ou de UI o consome atualmente. -**Contexto PRD:** O campo Data é definido no modelo de dados (seção 5) como opcional. O PRD diz que é "exibida como informação adicional no relatório PDF. Não participa de nenhuma regra de validação ou cálculo." -**Solução:** Usar o campo `data` na geração do PDF (`pdf_generator.rs`), exibindo a data da nota nas seções de faltantes ou duplicatas quando disponível. Isso resolve o warning e implementa o requisito do PRD. - ---- - -### W-02 — `IntervaloSerie.minimo` e `IntervaloSerie.maximo` nunca lidos - -**Arquivo:** `src/domain/entities/resultado_analise.rs:23-24` -**Causa:** Os campos `minimo: u64` e `maximo: u64` existem na struct `IntervaloSerie`, mas nenhum código os consome após o cálculo. -**Contexto PRD:** RF04 define que o intervalo de faltantes é avaliado entre o menor e o maior número encontrado. O aviso de confirmação (RF04 — Proteção contra intervalos anormalmente grandes) exibe o intervalo calculado ao usuário. -**Solução:** Exibir `minimo` e `maximo` na mensagem de confirmação em `app.rs` quando o intervalo exceder 10.000. Exemplo: `"Série 001 / NFE: intervalo de 999.996 faltantes detectado (de 1 a 1.000.000)"`. O PRD especifica exatamente esse formato de aviso. - ---- - -### W-03 — `ResultadoAnalise::sem_inconsistencias` nunca usada - -**Arquivo:** `src/domain/entities/resultado_analise.rs:51` -**Causa:** O método `pub fn sem_inconsistencias()` está definido mas nunca chamado. -**Contexto PRD:** Nenhuma funcionalidade específica é mapeada diretamente para este método. -**Solução (opção A — remover):** Remover o método se não houver uso planejado próximo. É código morto. -**Solução (opção B — usar):** Usar o método na tela de resultado (`resultado.rs`) para exibir um badge "Sem inconsistências" ou mensagem de sucesso no topo quando `sem_inconsistencias()` for `true`. Melhora a UX e elimina o warning. - ---- - -### W-04 — `EstadoModal::Informacao` nunca construída - -**Arquivo:** `src/ui/app.rs:39` -**Causa:** A variante `Informacao { titulo, mensagem }` existe no enum `EstadoModal` mas nenhum código a instancia. Existe `Aviso` e `Erro` para as demais situações. -**Solução:** Remover a variante `Informacao` do enum se não houver distinção visual planejada entre "Informação" e "Aviso". Alternativamente, usá-la onde hoje se usa `Aviso` para casos puramente informativos (sem cor amarela de alerta). A variante mais simples é a remoção. - ---- - -### W-05 — `Message::CancelarExpansao` nunca construída - -**Arquivo:** `src/ui/message.rs:76` -**Causa:** A variante `CancelarExpansao` existe no enum `Message` e é tratada em `update()` (muda o estado para `ConfigurandoColunas`), mas nenhuma tela emite essa mensagem. O cancelamento do intervalo excessivo é feito via `Message::ModalCancelado`. -**Solução:** Remover `Message::CancelarExpansao` do enum. O comportamento de cancelar a expansão já está implementado em `Message::ModalCancelado` (`app.rs:629-636`). - ---- - -### W-06 — `theme::cabecalho_tabela` nunca usada - -**Arquivo:** `src/ui/theme.rs:140` -**Causa:** A função de estilo `cabecalho_tabela` está definida mas nenhuma tela a usa. -**Contexto:** O componente `tabela_preview.rs` usa estilo inline para o cabeçalho em vez desta função. -**Solução:** Aplicar `t::cabecalho_tabela` no cabeçalho da tabela de preview em `tabela_preview.rs`, substituindo o estilo inline atual. Isso centraliza o estilo e elimina o warning. - ---- - -### W-07 — `theme::badge_sucesso` nunca usada - -**Arquivo:** `src/ui/theme.rs:149` -**Causa:** A função de estilo `badge_sucesso` está definida mas nenhuma tela a usa. Os badges de sucesso são aplicados via `t::badge_aviso` (amarelo) ou texto colorido com `t::SUCCESS`. -**Contexto PRD:** A tela de resultado exibe indicador de completude por série. Quando 100% completo (sem faltantes), poderia exibir um badge verde. -**Solução (opção A — usar):** Exibir badge `t::badge_sucesso` na tela de resultado quando uma série não tem faltantes, em vez de apenas texto verde. Melhora a distinção visual. -**Solução (opção B — remover):** Remover se não houver uso planejado. - ---- - -## 2. Funcionalidades Faltantes (vs. PRD v1.7) - -### F-01 — Campo `data` no PDF (RF07.1) - -**Status:** Não implementado -**PRD:** Seção 5 e RF07.1 — "Data de emissão da nota. Exibida como informação adicional no relatório PDF." -**Situação atual:** O campo `data` é lido do arquivo (`csv_reader.rs`, `xlsx_reader.rs`) e armazenado em `Nota.data`, mas `pdf_generator.rs` não o utiliza em nenhuma seção. -**O que falta:** Exibir a data de cada nota nas seções de faltantes e/ou duplicatas do PDF quando disponível. Por exemplo, no detalhamento de duplicatas: `"NF 42 / Série 001 — 3 ocorrências (última: 15/01/2025)"`. - ---- - -### F-02 — Mensagem de confirmação com intervalo exato (RF04) - -**Status:** Parcialmente implementado -**PRD:** RF04 — "Exibir aviso informando o intervalo calculado (ex: 'Série 001 / NFE: intervalo de 999.996 faltantes detectado')" -**Situação atual:** O aviso de confirmação em `app.rs:930-948` exibe apenas `"Série X: intervalo de N faltantes detectado"`, mas `minimo` e `maximo` de `IntervaloSerie` não são incluídos na mensagem. Os campos existem mas não são usados (W-02 acima). -**O que falta:** Incluir `minimo` e `maximo` na mensagem de confirmação para que o usuário veja o intervalo completo. - ---- - -### F-03 — Limite de tamanho de arquivo 50 MB (RF01.3) - -**Status:** Não implementado -**PRD:** RF01.3 — "O sistema deve recusar arquivos maiores que 50 MB e exibir mensagem de erro ao usuário." -**Situação atual:** Nenhuma verificação de tamanho de arquivo existe nos readers (`csv_reader.rs`, `xlsx_reader.rs`) nem em `processar_arquivo_selecionado` (`app.rs`). -**O que falta:** Verificar `std::fs::metadata(caminho)?.len()` antes de processar. Se > 50 MB, retornar `ErroArquivo::TamanhoExcedido` (o tipo já está definido em `domain/errors.rs`) e exibir modal de erro. - ---- - -### F-04 — Arquivo XLSX corrompido: limpar estado (RF01.2) - -**Status:** Parcialmente implementado -**PRD:** RF01.2 — "Se o arquivo não puder ser lido, exibir mensagem de erro em modal e limpar o arquivo carregado; o estado anterior é descartado." -**Situação atual:** `Message::XlsxErroAoCarregar` exibe o erro em modal, mas não limpa `self.caminho_arquivo` nem `self.nome_arquivo` nem `self.preview_arquivo`. O estado anterior do arquivo permanece em memória. -**O que falta:** No handler de `Message::XlsxErroAoCarregar`, zerar `self.caminho_arquivo = None`, `self.nome_arquivo = String::new()`, `self.preview_arquivo = None` e `self.notas_importadas.clear()` antes de exibir o erro. - ---- - -### F-05 — Relatório de linhas malformadas ao usuário (RF01.1 / RF01.2) - -**Status:** Parcialmente implementado -**PRD:** RF01.1/RF01.2 — "o sistema deve reportar ao usuário quais linhas foram descartadas, sem interromper a importação" -**Situação atual:** `ResumoAvisos` agrega contagens, e o modal de avisos exibe resumos do tipo "32 linhas descartadas por malformação". Porém, **os números de linha específicos** (ex: "linhas 15, 42, 103 descartadas") não são rastreados nem exibidos. -**O que falta:** Avaliar se o PRD exige listagem de números de linha individuais (o texto diz "quais linhas foram descartadas"). A interpretação atual (contagens por categoria) pode ser suficiente, mas merece revisão explícita com o product owner. Se linhas individuais forem necessárias, `ResumoAvisos` precisa armazenar `Vec` por categoria. - ---- - -### F-06 — Validação de campos duplicados no mapeamento (RF02.3) - -**Status:** Não implementado -**PRD:** RF02 — Tratamento de Erros de Configuração: "Dois campos mapeados para o mesmo índice/posição → Bloquear e exibir erro de validação imediatamente, antes de executar a análise" -**Situação atual:** `executar_importacao_sync` delega a validação ao reader, mas não há verificação explícita de índices/posições duplicados antes do disparo da análise. Se o usuário mapear, por exemplo, Numero e Serie para o mesmo índice CSV, os dados serão importados incorretamente sem aviso. -**O que falta:** Adicionar validação em `disparar_importacao` (ou no use case `importar_arquivo`) que verifique se algum índice (CSV) ou posição (XLSX) aparece em mais de um campo mapeado e retorne erro descritivo antes de iniciar o processamento. - ---- - -### F-07 — Índice/posição inválido: identificar qual campo (RF02) - -**Status:** Parcialmente implementado -**PRD:** RF02 — "Índice/posição configurado não existe no arquivo importado → Exibir erro ao usuário identificando qual campo está inválido" -**Situação atual:** Os readers retornam erros quando um índice não existe, mas a mensagem de erro pode não identificar claramente qual campo (Numero, Serie, Valor etc.) causou o problema. -**O que falta:** Garantir que as mensagens de erro de importação identifiquem o campo problemático pelo nome lógico (ex: "Campo 'Numero': índice 5 não existe — o arquivo tem 4 colunas"). - ---- - -### F-08 — Spinner/indicador visual durante análise (RNF03 / RNF04) - -**Status:** Minimamente implementado -**PRD:** RNF03 — "A análise é executada em uma thread separada para não bloquear a interface gráfica" -**Situação atual:** O estado `EstadoApp::Analisando` exibe apenas dois textos estáticos ("Analisando..." e "Aguarde...") sem nenhum indicador de progresso animado. -**O que falta:** Implementar um spinner ou progress bar indeterminado na tela de análise para dar feedback visual ao usuário de que o processamento está ativo. O iced suporta animações via `Subscription` com `time::every`. - ---- - -### F-09 — Paginação independente por série (RF07) - -**Status:** Não implementado (limitação funcional) -**PRD:** RF07 — paginação para listas longas -**Situação atual:** `app.pagina_faltantes` e `app.pagina_duplicatas` são contadores globais únicos, compartilhados entre todas as séries. Ao navegar para a página 2 de faltantes da Série 001, a paginação também afeta a Série 002 se ambas aparecerem na mesma tela. -**O que falta:** Tornar a paginação independente por `ChaveSerie`, usando um `HashMap` no estado da aplicação em vez de um único `usize` global. Isso requer mudanças em `App`, em `Message` e na tela `resultado.rs`. - ---- - -### F-10 — Exportação do resultado em CSV (seção 4.2 / seção 14) - -**Status:** Não incluído no MVP (seção 4.2) -**PRD:** Listado como "Não Incluído (MVP)" na seção 4.2 e como evolução futura na seção 14. -**Nota:** Não é um requisito do MVP. Documentado aqui apenas para rastreabilidade. - ---- - -## 3. Resumo por Prioridade - -| ID | Tipo | Arquivo(s) afetado(s) | Esforço estimado | -|----|------|-----------------------|-----------------| -| W-01 | Warning → usar campo `data` | `pdf_generator.rs` | Baixo | -| W-02 | Warning → usar `minimo`/`maximo` | `app.rs` | Baixo | -| W-03 | Warning → remover ou usar | `resultado_analise.rs`, `resultado.rs` | Baixo | -| W-04 | Warning → remover variante | `app.rs` | Baixo | -| W-05 | Warning → remover variante | `message.rs`, `app.rs` | Baixo | -| W-06 | Warning → aplicar estilo | `tabela_preview.rs` | Baixo | -| W-07 | Warning → usar ou remover | `resultado.rs` ou `theme.rs` | Baixo | -| F-01 | Funcionalidade faltante | `pdf_generator.rs` | Baixo | -| F-02 | Funcionalidade incompleta | `app.rs` | Baixo | -| F-03 | Funcionalidade faltante | `csv_reader.rs`, `xlsx_reader.rs`, `app.rs` | Médio | -| F-04 | Funcionalidade incompleta | `app.rs` | Baixo | -| F-05 | Revisão de requisito | `domain/errors.rs`, readers | Médio | -| F-06 | Funcionalidade faltante | `app.rs` ou `importar_arquivo.rs` | Médio | -| F-07 | Funcionalidade incompleta | `csv_reader.rs`, `xlsx_reader.rs` | Médio | -| F-08 | UX faltante | `app.rs` | Médio | -| F-09 | Limitação funcional | `app.rs`, `message.rs`, `resultado.rs` | Alto | -| F-10 | Fora do MVP | — | — | - ---- - -## 4. Ordem de Execução Sugerida - -### Fase 1 — Warnings (todos baixo esforço, fazem parte da limpeza) - -1. W-05: Remover `Message::CancelarExpansao` -2. W-04: Remover `EstadoModal::Informacao` -3. W-03: Decidir entre remover `sem_inconsistencias` ou usá-la em `resultado.rs` -4. W-07: Decidir entre remover `badge_sucesso` ou usá-la em `resultado.rs` -5. W-06: Aplicar `t::cabecalho_tabela` em `tabela_preview.rs` -6. W-02 + F-02: Usar `minimo`/`maximo` na mensagem de confirmação de intervalo -7. W-01 + F-01: Usar `data` no PDF - -### Fase 2 — Bugs e requisitos críticos - -8. F-04: Limpar estado ao falhar leitura XLSX -9. F-03: Validar limite de 50 MB -10. F-06: Validar campos duplicados no mapeamento -11. F-07: Mensagens de erro com nome do campo - -### Fase 3 — Melhorias de UX - -12. F-08: Spinner animado na tela de análise -13. F-09: Paginação independente por série -14. F-05: Revisar requisito de rastreamento de linhas individuais - ---- - -*Fim do roteiro.* diff --git a/UI_REDESIGN.md b/UI_REDESIGN.md deleted file mode 100644 index 130d228..0000000 --- a/UI_REDESIGN.md +++ /dev/null @@ -1,291 +0,0 @@ -# UI Redesign — Comparador de Notas - -**Data:** 04/03/2026 -**Branch:** `change-ui` -**Base:** iced 0.13.1 (Elm architecture) -**Referência visual:** `ui-ideia/mockup.html` + `ui-ideia/design_tokens.json` - ---- - -## 1. Objetivo - -Aplicar o visual do mockup (tema dark navy, cards, badges coloridos, progress bars) a todas as telas da aplicação, mantendo a lógica de negócio e a arquitetura Elm intocadas. - ---- - -## 2. Design Tokens (mapeados para Rust) - -### Paleta de cores - -| Token | Hex | Uso | -|------------------------|-----------|---------------------------------------| -| `BG` | `#0F172A` | Fundo geral da janela | -| `SURFACE` | `#1E293B` | Cards / containers primários | -| `SURFACE_2` | `#334155` | Cards secundários, cabeçalho de tabela| -| `BORDER` | `#334155` | Bordas de inputs e cards | -| `TEXT` | `#F1F5F9` | Texto principal | -| `TEXT_SECONDARY` | `#94A3B8` | Labels, placeholders, texto muted | -| `TEXT_MUTED` | `#64748B` | Texto desabilitado | -| `PRIMARY` | `#3B82F6` | Botões primários, links, step ativo | -| `PRIMARY_HOVER` | `#2563EB` | Hover em botões primários | -| `SUCCESS` | `#22C55E` | Badge OK, progress bar ≥ 90% | -| `WARNING` | `#F59E0B` | Badge Faltante, progress bar 60–89% | -| `ERROR` | `#EF4444` | Badge Duplicada, progress bar < 60% | -| `OVERLAY` | rgba(0,0,0,0.6) | Fundo do modal | - -### Espaçamentos - -| Token | px | -|-------|----| -| `XS` | 4 | -| `SM` | 8 | -| `MD` | 12 | -| `LG` | 16 | -| `XL` | 24 | -| `XXL` | 32 | - -### Border radius - -| Token | px | -|--------|----| -| `SM` | 4 | -| `MD` | 6 | -| `LG` | 8 | - ---- - -## 3. Arquivo de tema: `src/ui/theme.rs` - -Módulo responsável por expor: - -- `PALETA`: constantes `Color` para todas as cores acima -- Funções de estilo para `container::Style`, `button::Style`, `text_input::Style`, `progress_bar::Style` -- Nenhuma lógica de negócio — apenas aparência - -### Estratégia de tema iced - -```rust -// main.rs — encadear .theme() -iced::application(...) - .theme(|_app, _| tema_dark()) - .run_with(App::new) - -// theme.rs -pub fn tema_dark() -> iced::Theme { - iced::Theme::custom("dark".to_string(), iced::theme::Palette { - background: hex("#0F172A"), - text: hex("#F1F5F9"), - primary: hex("#3B82F6"), - success: hex("#22C55E"), - danger: hex("#EF4444"), - }) -} -``` - -Widgets que precisam de aparência customizada além da paleta (cards, badges) recebem closure `.style(|theme| ...)` inline ou via função helper em `theme.rs`. - ---- - -## 4. Componentes visuais novos / modificados - -### 4.1 Card container - -Container com background `SURFACE`, borda `BORDER` 1px, radius `LG` (8px), padding `LG` (16px). - -```rust -// theme.rs -pub fn card(theme: &iced::Theme) -> container::Style { ... } -pub fn card_secondary(theme: &iced::Theme) -> container::Style { ... } -``` - -### 4.2 Botão primary - -Background `PRIMARY`, texto `TEXT`, radius `MD` (6px), sem borda. -Hover: background `PRIMARY_HOVER`. - -### 4.3 Botão secondary / ghost - -Background `SURFACE_2`, texto `TEXT`, radius `MD`. - -### 4.4 Botão danger - -Background semi-transparente `ERROR` (20% alpha), texto `ERROR`, radius `MD`. - -### 4.5 Badge de status (inline) - -Container com padding `[2, 8]`, radius `SM` (4px), background 20% alpha da cor semântica. -Implementado como `container(text(...).size(12))` com style closure. - -| Status | Cor texto | Background alpha | -|------------|------------|-----------------| -| OK | `SUCCESS` | 20% | -| Faltante | `WARNING` | 20% | -| Duplicada | `ERROR` | 20% | - -### 4.6 Progress bar por série - -Componente `progress_bar` nativo do iced com style closure que escolhe cor baseada no percentual: -- ≥ 90% → `SUCCESS` -- 60–89% → `WARNING` -- < 60% → `ERROR` - -Background da trilha: `#111827` (mais escuro que SURFACE). - -### 4.7 Stat cards (tela de resultado) - -Row de 3 cards com número grande colorido + label. Componente reutilizável `stat_card(valor, label, cor)`. - -### 4.8 Breadcrumb - -Row no topo da janela (exceto Layouts e Analisando). Steps separados por `›`. Background `SURFACE`, padding `[8, 16]`, borda inferior 1px `BORDER`. - -| Estado do step | Cor | Tamanho | -|---------------|-------------|---------| -| Ativo | `PRIMARY` | 14px | -| Concluído | `TEXT_SECONDARY` | 13px | -| Futuro | `TEXT_MUTED` | 13px | - ---- - -## 5. Telas — mudanças por arquivo - -### 5.1 `screens/import.rs` - -**Atual:** Coluna plana com título, row de arquivo e row de layout. - -**Novo:** -- Card central (max-width 600px) centrado na tela -- Área de drop zone estilizada com borda tracejada `BORDER`, radius `LG`, padding `XL` -- Ícone `📂` grande + texto instrucional -- Nome do arquivo selecionado com truncamento -- Seção de layout com separador visual -- Botão primary "▶ Configurar Colunas" ocupando largura do card - -### 5.2 `screens/selecionar_aba.rs` - -**Atual:** Lista de botões idênticos (bug de highlight). - -**Novo:** -- Card com lista de abas scrollável -- Aba selecionada: background `PRIMARY` (20% alpha), texto `PRIMARY`, borda `PRIMARY` -- Aba não selecionada: background `SURFACE_2`, texto `TEXT` -- Preview abaixo da lista em card separado - -### 5.3 `screens/configuracao_colunas.rs` - -**Atual:** Coluna plana de inputs. - -**Novo:** -- Seção de configuração em card `SURFACE` -- Labels com `TEXT_SECONDARY`, inputs com fundo `BG`, borda `BORDER` -- Campos opcionais: checkbox com estilo consistente + input inline -- Erros em card com borda `ERROR` (20% alpha) -- Botões na barra inferior fixada: "Voltar" (ghost), "Analisar" (primary), "Reanalisar" (secondary), "Salvar" (ghost) - -### 5.4 `screens/resultado.rs` - -**Atual:** Coluna de texto puro. - -**Novo (alinhado ao mockup):** - -1. **Header row:** título + botões de ação à direita -2. **Stat cards row:** 3 cards — "Notas Faltantes" (azul), "Duplicadas" (vermelho), "Total R$" (texto branco) -3. **Card de completude por série:** para cada série, label + progress bar colorida por threshold -4. **Controle de itens/página:** botões com highlight no ativo -5. **Seções faltantes/duplicatas:** cabeçalho de série em row com badge de contagem + botão Copiar; itens em lista - -### 5.5 `screens/layouts.rs` - -**Atual:** Coluna plana. - -**Novo:** -- Seções CSV e XLSX em cards separados -- Cada layout numa row com hover highlight -- Botões de ação menores (ícone + texto compacto) -- Linha de importar JSON no rodapé do card - -### 5.6 `components/modal.rs` - -**Atual:** Box com estilo do tema padrão. - -**Novo:** -- Background `SURFACE`, borda `BORDER`, radius `LG` -- Título `TEXT` 18px, mensagem `TEXT_SECONDARY` 14px -- Separador entre conteúdo e botões -- Botão "Fechar" ghost, "Confirmar" primary -- Tipos Erro/Aviso com ícone + cor no título - -### 5.7 `components/tabela_preview.rs` - -**Atual:** Monospace puro. - -**Novo:** -- Cabeçalho com background `SURFACE_2`, texto `TEXT_SECONDARY` -- Células com background `SURFACE`, texto `TEXT`, fonte monospace -- Borda inferior `BORDER` nas células - -### 5.8 `components/paginacao.rs` - -**Atual:** Row simples de botões. - -**Novo:** -- Botões ◀/▶ com estilo ghost -- "Página X / Y" em `TEXT_SECONDARY` - ---- - -## 6. Arquivos a criar/modificar - -| Arquivo | Ação | -|---------|------| -| `src/main.rs` | Adicionar `.theme(...)` | -| `src/ui/mod.rs` | Adicionar `pub mod theme;` | -| `src/ui/theme.rs` | **Criar** — paleta + helpers de estilo | -| `src/ui/app.rs` | Breadcrumb novo estilo + tela Analisando centralizada | -| `src/ui/screens/import.rs` | Reescrever | -| `src/ui/screens/selecionar_aba.rs` | Reescrever (corrigir bug highlight) | -| `src/ui/screens/configuracao_colunas.rs` | Reescrever | -| `src/ui/screens/resultado.rs` | Reescrever | -| `src/ui/screens/layouts.rs` | Reescrever | -| `src/ui/components/modal.rs` | Reescrever | -| `src/ui/components/tabela_preview.rs` | Reescrever | -| `src/ui/components/paginacao.rs` | Reescrever | - ---- - -## 7. Limitações do iced 0.13 e workarounds - -| Limitação CSS | Workaround iced | -|---------------|----------------| -| `box-shadow` | Cor de borda ou sem sombra (aceitar diferença) | -| `rgba(r,g,b,0.2)` | `Color { r, g, b, a: 0.2 }` com valores 0.0–1.0 | -| Font Inter | Usa fonte do sistema (system-ui) — sem mudança necessária | -| `display: flex; gap` | `row![...].spacing(N)` | -| `border-bottom` nas células | `container` com border bottom via `border.width` fracional não suportado — usar separador visual alternativo | - ---- - -## 8. Ordem de implementação - -1. `theme.rs` — paleta e helpers (base para tudo) -2. `main.rs` — ativar tema -3. `modal.rs` — usado por todas as telas -4. `resultado.rs` — tela principal do mockup -5. `import.rs` -6. `selecionar_aba.rs` -7. `configuracao_colunas.rs` -8. `layouts.rs` -9. `app.rs` — breadcrumb + Analisando -10. `tabela_preview.rs` + `paginacao.rs` -11. Compilar e corrigir - ---- - -## 9. Notas de compatibilidade iced 0.13 - -- `button::Style` inclui `background`, `text_color`, `border: Border { color, width, radius }`, `shadow` -- `container::Style` inclui `background`, `text_color`, `border`, `shadow` -- `progress_bar::Style` inclui `background` e `bar` -- `text_input::Style` inclui `background`, `border`, `icon`, `placeholder`, `value`, `selection` -- Closures de estilo recebem `&Theme` e retornam o `Style` concreto do widget -- `iced::Border` aceita `radius: iced::border::Radius` — usar `N.into()` para uniform radius diff --git a/FEATURES_BACKLOG.md b/docs/FEATURES_BACKLOG.md similarity index 100% rename from FEATURES_BACKLOG.md rename to docs/FEATURES_BACKLOG.md diff --git a/FIX_SALVAR_LAYOUT.md b/docs/FIX_SALVAR_LAYOUT.md similarity index 100% rename from FIX_SALVAR_LAYOUT.md rename to docs/FIX_SALVAR_LAYOUT.md diff --git a/src/application/AGENTS.md b/src/application/AGENTS.md new file mode 100644 index 0000000..9517c72 --- /dev/null +++ b/src/application/AGENTS.md @@ -0,0 +1,205 @@ +# Application — AGENTS.md + +Camada de aplicação do projeto `comparador-notas`. Orquestra os serviços de domínio +e a infraestrutura para expor casos de uso coesos à camada de UI (Tauri/frontend). +Não contém regras de negócio próprias; delega ao `domain` e ao `infrastructure`. + +--- + +## Estrutura dos arquivos + +``` +src/application/ +├── mod.rs +└── usecases/ + ├── mod.rs + ├── executar_analise.rs # Análise de sequência e duplicatas + ├── exportar_pdf.rs # Exportação de relatório PDF + ├── importar_arquivo.rs # Importação de CSV e XLSX → Vec + └── layouts.rs # CRUD e import/export JSON de layouts +``` + +--- + +## usecases/mod.rs + +Re-exporta os quatro módulos de casos de uso: + +```rust +pub mod executar_analise; +pub mod exportar_pdf; +pub mod importar_arquivo; +pub mod layouts; +``` + +--- + +## executar_analise.rs + +Orquestra a análise em **duas etapas** para lidar com grandes intervalos de +faltantes sem travar a UI (regra RF04). + +### Etapa 1 — `pre_analisar(notas) -> ResultadoPreAnalise` + +1. Agrupa as notas por `ChaveSerie` (`serie` + `documento_tipo`). +2. Para cada grupo: + - Acumula soma total e por série. + - Chama `calcular_intervalo` (sem materializar faltantes). +3. Detecta duplicidades via `duplicidades_por_serie`. +4. Retorna `ResultadoPreAnalise` com intervalos, duplicadas e somas. + +### Verificação intermediária — `series_com_intervalo_excessivo(pre) -> Vec<(ChaveSerie, IntervaloSerie)>` + +Filtra as séries cujo `contagem_faltantes > LIMITE_FALTANTES` (10.000). +O caller (UI) deve exibir confirmação ao usuário se a lista não for vazia. + +### Etapa 2 — `expandir_analise(pre, notas) -> ResultadoAnalise` + +Materializa a lista completa de faltantes via `detectar_faltantes` e combina +com os dados já calculados na pré-análise (duplicadas, somas, totais). + +### Fluxo de uso + +``` +pre_analisar(notas) + └─ ResultadoPreAnalise + +series_com_intervalo_excessivo(&pre) + ├─ [] → chamar expandir_analise diretamente + └─ [...] → exibir diálogo de confirmação na UI + └─ confirmado → expandir_analise(pre, notas) + +expandir_analise(pre, notas) + └─ ResultadoAnalise (com faltantes materializados) +``` + +--- + +## exportar_pdf.rs + +### `exportar_pdf(gerador, resultado, notas, nome_arquivo, nome_layout, caminho_saida) -> Result<(), String>` + +Caso de uso simples que: + +1. Constrói `MetadadosRelatorio` com `nome_arquivo`, `nome_layout` e timestamp + `Local::now()`. +2. Delega a geração para `gerador.gerar(...)` via a trait abstrata `PdfGenerator`. + +A dependência em `&dyn PdfGenerator` (e não em `GenpdfGenerator` diretamente) +mantém o use case desacoplado da implementação concreta e facilita testes. + +--- + +## importar_arquivo.rs + +Converte arquivos brutos (CSV ou XLSX) em `Vec` prontas para análise, +acumulando avisos não-fatais em `ResumoAvisos`. + +### Tipos de saída + +```rust +pub struct ResultadoImportacao { + pub notas: Vec, + pub avisos: ResumoAvisos, +} + +pub struct InfoXlsx { + pub abas: Vec, +} +``` + +### `listar_abas_xlsx(caminho) -> Result` + +Delega para `xlsx_reader::listar_abas`. Retorna `InfoXlsx` com os nomes das +abas para que a UI permita ao usuário selecionar a aba correta. + +### `importar_csv(caminho, config: &LayoutCsv) -> Result` + +1. Chama `csv_reader::ler_csv` com os parâmetros do layout. +2. Passa as linhas brutas para `mapear_linhas_para_notas`. + +### `importar_xlsx(caminho, config: &LayoutXlsx) -> Result` + +1. Converte `pos_numero` e `pos_serie` (LetraLinha) para coordenadas. +2. Determina `linha_inicio` como o mínimo entre as linhas das duas posições. +3. Chama `xlsx_reader::ler_xlsx`. +4. Converte posições de todos os campos mapeados para índices de coluna (base 0). +5. Passa as linhas brutas para `mapear_linhas_para_notas`. + +### `mapear_linhas_para_notas(...)` (privada) + +Função central de mapeamento. Para cada linha: + +1. **Validação de índices** (na primeira linha disponível): + - Campos obrigatórios (`Numero`, `Serie`): retorna `Err` se o índice não existe. + - Campos opcionais (`Valor`, `Data`, `Tipo de Documento`): retorna `Err` se + configurado com índice fora dos limites. +2. **Numero**: tenta `parse::` direto; se falhar, extrai apenas dígitos. + Rejeita zero. Incrementa `avisos.numeros_invalidos` e pula a linha se inválido. +3. **Serie**: valida via `domain::entities::serie::validar_serie`. Pula linha se inválida. +4. **Valor** (opcional): faz `parse_valor`; se inválido, registra em + `avisos.valores_invalidos` e usa `None` (não descarta a linha). +5. **Data** (opcional): tenta `dd/mm/aaaa`, `aaaa-mm-dd` e `dd-mm-aaaa`. `None` + se nenhum formato casar (não gera aviso). +6. **Tipo de Documento** (opcional): qualquer string não vazia. + +### `parse_numero(s) -> Result` (privada) + +- Tenta `s.parse::()` diretamente. +- Se falhar, extrai apenas dígitos ASCII e tenta novamente. +- Rejeita zero em ambos os casos. + +### `parse_data(s) -> Option` (privada) + +Tenta os formatos `%d/%m/%Y`, `%Y-%m-%d` e `%d-%m-%Y` nessa ordem. + +--- + +## layouts.rs + +CRUD de layouts sobre o banco SQLite e import/export em JSON. + +### `salvar_layout(conn, layout) -> Result` + +- Valida que `nome` não está vazio. +- Se `layout.id()` é `Some` → chama `layout_repository::atualizar` e retorna o id. +- Se `None` → verifica conflito de nome via `existe_nome`; se existir, retorna + `ErroLayout::NomeConflitante`; caso contrário, insere e retorna o novo id. + +### `listar_layouts(conn) -> Result, String>` + +Delega para `layout_repository::listar`. Retorna todos os layouts ordenados +por nome. + +### `excluir_layout(conn, id) -> Result<(), String>` + +Delega para `layout_repository::excluir`. + +### `exportar_layout_json(layout) -> Result<(String, String), String>` + +1. Converte `Layout` para `LayoutJson` via `From<&Layout>`. +2. Serializa com `serde_json::to_string_pretty`. +3. Retorna `(conteúdo_json, nome_arquivo_sugerido)` onde o nome é `"{nome}.json"`. + +### `importar_layout_json(conn, json, sobrescrever_se_existir, novo_nome) -> Result` + +1. Desserializa `json` para `LayoutJson`. +2. Converte para `Layout` via `TryFrom` (valida campos obrigatórios). +3. Aplica `novo_nome` se fornecido (mutação direta no enum). +4. Verifica conflito de nome: + - Se existe e `sobrescrever_se_existir == true`: busca o id existente, + injeta no layout e chama `atualizar`. + - Se existe e `false`: retorna `ErroLayout::NomeConflitante`. +5. Se não existe: chama `layout_repository::salvar`. + +--- + +## Dependencias de outros módulos + +``` +application::usecases + ├─ domain::entities::{chave_serie, nota, resultado_analise, layout, serie} + ├─ domain::services::{detector_duplicidade, detector_sequencia, parser_monetario} + ├─ domain::errors::{ErroArquivo, ErroLayout, ResumoAvisos} + └─ infrastructure::{csv_reader, xlsx_reader, pdf_generator, sqlite::layout_repository} +``` diff --git a/src/domain/AGENTS.md b/src/domain/AGENTS.md new file mode 100644 index 0000000..837ac84 --- /dev/null +++ b/src/domain/AGENTS.md @@ -0,0 +1,304 @@ +# Domain — AGENTS.md + +Camada de domínio do projeto `comparador-notas`. Contém as entidades, serviços +de negócio e erros tipados. Não possui dependências de infraestrutura — todo +acesso a banco, arquivos ou UI é responsabilidade das camadas superiores. + +--- + +## Estrutura dos arquivos + +``` +src/domain/ +├── mod.rs +├── errors.rs # Enums de erro tipados +├── entities/ +│ ├── mod.rs +│ ├── chave_serie.rs # Chave composta (serie + documento_tipo) +│ ├── layout.rs # Entidades de configuração de layout CSV/XLSX +│ ├── nota.rs # Entidade Nota Fiscal +│ ├── resultado_analise.rs # Resultados de análise (pré e completo) +│ └── serie.rs # Validação de série +└── services/ + ├── mod.rs + ├── detector_duplicidade.rs # Detecção de notas duplicadas + ├── detector_sequencia.rs # Detecção de notas faltantes na sequência + └── parser_monetario.rs # Parsing e formatação de valores monetários +``` + +--- + +## errors.rs + +Erros tipados com a crate `thiserror`. Cada domínio tem seu próprio enum. + +### `ErroSerie` + +| Variante | Mensagem | +|---|---| +| `Invalida(String)` | Série inválida: deve conter de 1 a 3 dígitos numéricos | +| `Vazia` | Série vazia | + +### `ErroValor` + +| Variante | Mensagem | +|---|---| +| `Negativo(String)` | Valor negativo não é permitido | +| `NaoNumerico(String)` | Valor não numérico | + +### `ErroLayout` + +| Variante | Mensagem | +|---|---| +| `CampoObrigatorioAusente(String)` | Campo obrigatório ausente | +| `JsonMalformado(String)` | JSON malformado | +| `NomeConflitante(String)` | Layout com esse nome já existe | + +### `ErroArquivo` + +| Variante | Mensagem | +|---|---| +| `TamanhoExcedido(u64)` | Arquivo > 50 MB | +| `Corrompido(String)` | Arquivo corrompido ou ilegível | +| `ErroLeitura(String)` | Erro genérico de leitura | + +### `ResumoAvisos` + +Estrutura de avisos não-fatais acumulados durante a importação de um arquivo. + +```rust +pub struct ResumoAvisos { + pub linhas_malformadas: usize, + pub numeros_invalidos: usize, + pub series_invalidas: usize, + pub valores_invalidos: usize, + pub detalhes: Vec, // mensagens individuais por linha +} +``` + +- `tem_avisos()` → `true` se qualquer contador > 0 +- `linhas_para_exibir()` → `Vec` com resumo para exibição em modal + +--- + +## entities/ + +### `chave_serie.rs` — `ChaveSerie` + +Chave composta que identifica um grupo de notas fiscais. Combina série com +tipo de documento (`NFE`, `NFCE`, etc.). + +```rust +pub struct ChaveSerie { + pub serie: String, + pub documento_tipo: Option, +} +``` + +- Deriva `Hash`, `Eq`, `Ord` — usada como chave em `HashMap` e para ordenação. +- `new(serie, documento_tipo)` — construtor. +- `label()` — formata para exibição: + - `Some(tipo)` → `"001 / NFE"` + - `None` → `"001"` + +### `nota.rs` — `Nota` + +Entidade central. `(numero, serie, documento_tipo)` é o identificador único. + +```rust +pub struct Nota { + pub numero: u64, + pub serie: String, + pub documento_tipo: Option, // None quando não mapeado + pub valor: Option, + pub data: Option, // usado no PDF, não em regras +} +``` + +### `serie.rs` — `validar_serie` + +```rust +pub fn validar_serie(s: &str) -> Result +``` + +Valida via regex `^[0-9]{1,3}$` (1 a 3 dígitos numéricos). Faz trim antes +de validar. Usa `OnceLock` para compilar o regex uma única vez. + +### `resultado_analise.rs` + +Dois tipos de resultado que modelam o fluxo de análise em duas etapas: + +#### `ResultadoPreAnalise` + +Resultado intermediário — calculado sem expandir a lista completa de faltantes. +Usado para verificar se algum intervalo excede 10.000 registros (RF04) antes +de pedir confirmação ao usuário. + +```rust +pub struct ResultadoPreAnalise { + pub intervalos_por_serie: HashMap, + pub duplicadas_por_serie: HashMap>, + pub soma_total: Decimal, + pub soma_por_serie: HashMap, + pub total_por_serie: HashMap, +} +``` + +#### `IntervaloSerie` + +```rust +pub struct IntervaloSerie { + pub minimo: u64, + pub maximo: u64, + pub contagem_faltantes: u64, +} +``` + +- `excede_limite(limite)` → `contagem_faltantes > limite` + +#### `ResultadoAnalise` + +Resultado completo com a lista materializada de faltantes. + +```rust +pub struct ResultadoAnalise { + pub faltantes_por_serie: HashMap>, // ordenados crescentemente + pub duplicadas_por_serie: HashMap>, + pub soma_total: Decimal, + pub soma_por_serie: HashMap, + pub total_por_serie: HashMap, +} +``` + +Métodos auxiliares: +- `sem_inconsistencias()` → `true` se não há faltantes nem duplicatas +- `total_faltantes()` → soma do tamanho de todas as listas de faltantes +- `total_duplicatas()` → soma do número de grupos de duplicatas + +### `layout.rs` + +Entidades de configuração de layout de arquivo. + +#### `TipoArquivo` + +```rust +pub enum TipoArquivo { Csv, Xlsx } +``` + +#### `LayoutCsv` + +| Campo | Tipo | Descrição | +|---|---|---| +| `delimitador` | `char` | `','`, `';'` ou `'\t'` | +| `encoding` | `String` | `"utf-8"` ou `"windows-1252"` | +| `linha_cabecalho` | `usize` | Linha do cabeçalho (base 1). `0` = sem cabeçalho | +| `indice_numero` | `usize` | Índice da coluna Numero (base 0) | +| `indice_serie` | `usize` | Índice da coluna Serie (base 0) | +| `indice_valor` | `Option` | Índice da coluna Valor (base 0), opcional | +| `indice_data` | `Option` | Índice da coluna Data (base 0), opcional | +| `indice_documento_tipo` | `Option` | Índice da coluna Tipo Documento (base 0), opcional | + +#### `LayoutXlsx` + +| Campo | Tipo | Descrição | +|---|---|---| +| `aba` | `String` | Nome da aba a processar | +| `pos_numero` | `String` | Posição LetraLinha (ex: `"D3"`) | +| `pos_serie` | `String` | Posição LetraLinha (ex: `"B3"`) | +| `pos_valor` | `Option` | Posição LetraLinha, opcional | +| `pos_data` | `Option` | Posição LetraLinha, opcional | +| `pos_documento_tipo` | `Option` | Posição LetraLinha, opcional | + +#### `Layout` (enum) + +```rust +pub enum Layout { + Csv { id: Option, nome: String, config: LayoutCsv }, + Xlsx { id: Option, nome: String, config: LayoutXlsx }, +} +``` + +Métodos: `id()`, `nome()`, `tipo()`. + +#### `LayoutJson` (serialização) + +Representação `serde` com tag `"tipo"` para importação/exportação em JSON. +Implementa `TryFrom for Layout` (valida campos obrigatórios) e +`From<&Layout> for LayoutJson`. + +--- + +## services/ + +### `detector_duplicidade.rs` + +#### `detectar_duplicidades(notas) -> HashMap<(u64, String, Option), usize>` + +Conta ocorrências de cada `(numero, serie, documento_tipo)`. Retém apenas +grupos com mais de uma ocorrência. + +#### `duplicidades_por_serie(notas) -> HashMap>` + +Agrupa o resultado de `detectar_duplicidades` por `ChaveSerie`. Cada vetor +é ordenado por `numero` crescente. + +Regras: +- O mesmo número em séries diferentes **não** é duplicata. +- O mesmo número com `documento_tipo` diferente **não** é duplicata. +- O mesmo número com mesma série e mesmo `documento_tipo` **é** duplicata. + +--- + +### `detector_sequencia.rs` + +#### Constante + +```rust +pub const LIMITE_FALTANTES: u64 = 10_000; +``` + +#### `calcular_intervalo(notas) -> Option` + +Calcula `minimo`, `maximo` e `contagem_faltantes` de forma incremental +(usando `windows(2)`) **sem materializar** a lista de faltantes. Deduplica +números antes do cálculo para não contar duplicatas como faltantes. + +#### `detectar_faltantes(notas) -> Vec` + +Materializa a lista completa de faltantes em ordem crescente. Deve ser +chamado apenas após confirmação do usuário quando `calcular_intervalo` +indica que o intervalo excede `LIMITE_FALTANTES`. + +#### `agrupar_contiguos(faltantes) -> Vec<(u64, u64)>` + +Recebe uma lista **já ordenada** de números faltantes e retorna intervalos +contíguos como pares `(inicio, fim)`. Números isolados têm `inicio == fim`. + +Exemplo: `[1, 2, 3, 5, 8, 9]` → `[(1, 3), (5, 5), (8, 9)]` + +--- + +### `parser_monetario.rs` + +#### `parse_valor(input) -> Result` + +Faz o parsing de uma string monetária suportando formatos brasileiro e americano. +Rejeita valores negativos. + +Algoritmo (RF06): + +| Condição | Regra | +|---|---| +| Contém ponto **e** vírgula | Último separador é o decimal | +| Apenas ponto, 3 dígitos após | Separador de milhar (`1.234` → `1234`) | +| Apenas ponto, outros casos | Decimal (`1000.00`) | +| Apenas vírgula, 3 dígitos após | Separador de milhar (`1,234` → `1234`) | +| Apenas vírgula, outros casos | Decimal (`1000,00` → `1000.00`) | +| Sem separador | Número inteiro | + +Aceita prefixo `R$` (case-insensitive). + +#### `formatar_valor_br(valor) -> String` + +Formata `Decimal` para exibição brasileira com 2 casas decimais e pontos de +milhar. Ex: `1234567.89` → `"1.234.567,89"`. diff --git a/src/infrastructure/AGENTS.md b/src/infrastructure/AGENTS.md new file mode 100644 index 0000000..6d7c4a6 --- /dev/null +++ b/src/infrastructure/AGENTS.md @@ -0,0 +1,207 @@ +# Infrastructure — AGENTS.md + +Camada de infraestrutura do projeto `comparador-notas`. Responsável por toda I/O +concreta: leitura de arquivos (CSV e XLSX), geração de PDF e persistência SQLite. +Não contém regras de negócio; depende do `domain` para tipos e erros. + +--- + +## Estrutura dos arquivos + +``` +src/infrastructure/ +├── mod.rs # Re-exporta os submódulos públicos +├── csv_reader.rs # Leitura e preview de arquivos CSV +├── xlsx_reader.rs # Leitura e preview de arquivos XLSX/XLS +├── pdf_generator.rs # Trait abstrata + implementação concreta de geração de PDF +└── sqlite/ # Submódulo de persistência (ver sqlite/AGENTS.md) +``` + +--- + +## mod.rs + +Re-exporta os quatro submódulos: + +```rust +pub mod csv_reader; +pub mod pdf_generator; +pub mod sqlite; +pub mod xlsx_reader; +``` + +--- + +## csv_reader.rs + +Leitura de arquivos CSV com suporte a múltiplos encodings e delimitadores. + +### Constante + +```rust +const LIMITE_BYTES: u64 = 50 * 1024 * 1024; // 50 MB +``` + +### `ResultadoCsv` + +```rust +pub struct ResultadoCsv { + pub linhas: Vec>, // dados sem o cabeçalho + pub avisos: ResumoAvisos, +} +``` + +### `ler_csv(caminho, delimitador, encoding, linha_cabecalho) -> Result` + +Fluxo: + +1. Verifica tamanho do arquivo — retorna `ErroArquivo::TamanhoExcedido` se > 50 MB. +2. Lê os bytes brutos com `std::fs::read`. +3. Decodifica o conteúdo: + - `"windows-1252"`, `"latin-1"`, `"iso-8859-1"` → `encoding_rs::WINDOWS_1252` + - qualquer outro → `String::from_utf8` (UTF-8) +4. Constrói um `csv::ReaderBuilder` com `flexible(true)` e `has_headers(false)`. +5. Itera sobre todos os registros: + - Pula linhas até e incluindo `linha_cabecalho` (quando > 0). + - Ignora linhas completamente em branco. + - Registra erros de parse em `avisos.linhas_malformadas`. +6. Retorna `ResultadoCsv` com as linhas de dados e os avisos. + +### `preview_csv(caminho, delimitador, encoding, n) -> Result>, ErroArquivo>` + +Retorna as primeiras `n` linhas brutas (sem pular cabeçalho). Usado exclusivamente +para pré-visualização na UI. Não verifica tamanho do arquivo. + +--- + +## xlsx_reader.rs + +Leitura de arquivos XLSX (e XLS por magic bytes) com suporte a coordenadas +no formato `LetraLinha` (ex: `"B3"`). + +### Constante + +```rust +const LIMITE_BYTES: u64 = 50 * 1024 * 1024; // 50 MB +``` + +### Tipos auxiliares + +```rust +pub struct Coordenada { + pub coluna: u32, // base 0 + pub linha: u32, // base 1 +} +``` + +### `listar_abas(caminho) -> Result, ErroArquivo>` + +Abre o workbook via `calamine::open_workbook_auto` (detecção por magic bytes) +e retorna os nomes das abas. + +### `ler_xlsx(caminho, nome_aba, linha_inicio) -> Result` + +1. Verifica tamanho (50 MB). +2. Abre workbook e seleciona a aba pelo nome. +3. Itera sobre as linhas a partir de `linha_inicio - 1` (base 0 internamente). +4. Converte cada célula para `String` via `celula_para_string` (ver abaixo). +5. Ignora linhas completamente em branco. +6. Retorna `ResultadoXlsx { linhas, avisos }`. + +### `preview_xlsx(caminho, nome_aba) -> Result>, ErroArquivo>` + +Retorna as primeiras 5 linhas brutas da aba, a partir da linha 1. Usado para +pré-visualização na UI. + +### `parsear_letra_linha(s) -> Option` + +Converte notação Excel (`"B3"`) para `Coordenada { coluna: 1, linha: 3 }`: + +- Normaliza para maiúsculas e faz trim. +- Divide entre letras e dígitos. +- Converte letras para índice de coluna base 0: + `A=0, B=1, ..., Z=25, AA=26, ...` +- Retorna `None` para notações inválidas (vazio, só números, zero, etc.). + +### `celula_para_string(cell) -> String` (privada) + +| Tipo calamine | Conversão | +|---|---| +| `Empty` | `""` | +| `String(s)` | `s.clone()` | +| `Float(f)` | sem `.0` quando `f.fract() == 0.0` | +| `Int(i)` | `i.to_string()` | +| `Bool(b)` | `b.to_string()` | +| `DateTime` / `DateTimeIso` / `DurationIso` | representação string | +| `Error(_)` | `""` | + +--- + +## pdf_generator.rs + +Geração de relatórios PDF com fontes embutidas no binário. + +### Fontes embutidas + +```rust +const FONT_REGULAR: &[u8] = include_bytes!("../../assets/fonts/LiberationSans-Regular.ttf"); +const FONT_BOLD: &[u8] = include_bytes!("../../assets/fonts/LiberationSans-Bold.ttf"); +``` + +Liberation Sans (~402 KB/variante) é usada em vez de Arial do sistema (~993 KB), +eliminando dependência externa e reduzindo o tamanho dos PDFs. + +### `PdfGenerator` (trait pública) + +```rust +pub trait PdfGenerator { + fn gerar( + &self, + resultado: &ResultadoAnalise, + notas: &[Nota], + meta: &MetadadosRelatorio, + caminho_saida: &Path, + ) -> Result<(), String>; +} +``` + +Permite que o use case `exportar_pdf` dependa da abstração, não da crate `genpdf`. + +### `GenpdfGenerator` (implementação concreta) + +Implementa `PdfGenerator` usando a crate `genpdf`. Estrutura do PDF gerado: + +1. **Título** — "Relatório de Análise de Notas Fiscais" (bold, 16pt) +2. **Metadados** — nome do arquivo, layout (se presente) e data/hora de geração +3. **Totais** — soma total e por série (`ChaveSerie.label()`) +4. **Notas Faltantes por Série** — lista agrupada em intervalos contíguos + (ex: `100–104 (5 notas)` em vez de `100, 101, 102, 103, 104`) +5. **Duplicatas por Série** — grupo por número com contagem e data da última ocorrência + +### `MetadadosRelatorio` + +```rust +pub struct MetadadosRelatorio { + pub nome_arquivo: String, + pub nome_layout: Option, + pub gerado_em: DateTime, +} +``` + +### `carregar_fonte_familia()` (privada) + +Constrói `fonts::FontFamily` com os 4 slots exigidos por `genpdf`. Como o +relatório não usa itálico, `italic` e `bold_italic` reutilizam os dados de +`regular` e `bold` respectivamente. + +--- + +## Dependencias externas relevantes + +| Crate | Uso | +|-------|-----| +| `csv` | Parser de arquivos CSV com suporte a delimitadores e modo flexível | +| `encoding_rs` | Decodificação Windows-1252 / Latin-1 | +| `calamine` | Leitura de XLSX/XLS com detecção automática por magic bytes | +| `genpdf` | Geração de PDF com layout de parágrafos e decorador de página | +| `chrono` | `DateTime` para timestamp do relatório | diff --git a/src/infrastructure/sqlite/AGENTS.md b/src/infrastructure/sqlite/AGENTS.md new file mode 100644 index 0000000..d7f5163 --- /dev/null +++ b/src/infrastructure/sqlite/AGENTS.md @@ -0,0 +1,202 @@ +# SQLite Infrastructure — AGENTS.md + +Visão geral da camada de persistência SQLite do projeto `comparador-notas`. + +--- + +## Estrutura dos arquivos + +``` +src/infrastructure/sqlite/ +├── mod.rs # Re-exporta os módulos públicos +├── connection.rs # Abertura e validação da conexão +├── migrations.rs # Controle de versão do schema +└── layout_repository.rs # CRUD da entidade Layout +``` + +--- + +## mod.rs + +Ponto de entrada do módulo. Apenas re-exporta os três submódulos: + +```rust +pub mod connection; +pub mod layout_repository; +pub mod migrations; +``` + +--- + +## connection.rs + +Responsável por localizar, abrir e validar o arquivo SQLite. + +### Caminho do banco + +`caminho_banco()` resolve o diretório de configuração do sistema operacional via +`dirs::config_dir()` e retorna: + +``` +/comparador-notas/config.db +``` + +Exemplos por SO: +- **Linux**: `~/.config/comparador-notas/config.db` +- **macOS**: `~/Library/Application Support/comparador-notas/config.db` +- **Windows**: `%APPDATA%\comparador-notas\config.db` + +### Abertura da conexão — `abrir_banco()` + +Delega para `abrir_banco_no_caminho()` com o caminho padrão. Retorna +`Result<(Connection, bool), String>`, onde o `bool` indica se o banco foi +**recriado** (era corrompido). + +### Lógica de recuperação de corrupção — `abrir_banco_no_caminho(path)` + +1. Cria o diretório pai caso não exista (`create_dir_all`). +2. Se o arquivo já existe, tenta abri-lo com `Connection::open`. +3. Executa `SELECT 1;` como teste de sanidade. + - Sucesso → retorna a conexão com flag `false` (não recriado). + - Falha (corrupção ou erro de abertura) → renomeia o arquivo para + `config.db.bak` e segue para a criação de um banco novo. +4. Cria um banco vazio e retorna com flag `true` (banco foi recriado). + +--- + +## migrations.rs + +Controla a evolução incremental do schema via uma tabela interna de versão. + +### Tabela de controle + +```sql +CREATE TABLE IF NOT EXISTS schema_version ( + versao INTEGER NOT NULL +); +``` + +Armazena apenas uma linha com a versão atual do schema. + +### `aplicar_migrations(conn)` + +Fluxo: + +1. Garante que `schema_version` existe. +2. Lê a versão atual (padrão `0` caso a tabela esteja vazia). +3. Executa sequencialmente as migrations pendentes: + - `versao_atual < 1` → `migration_v1` + - `versao_atual < 2` → `migration_v2` + - `versao_atual < 3` → `migration_v3` +4. Persiste a nova versão (`INSERT` se era `0`, `UPDATE` caso contrário). + +### Histórico de migrations + +| Versão | Descrição | +|--------|-----------| +| **v1** | Cria a tabela `layouts` com campos para CSV e XLSX. | +| **v2** | Renomeia layouts com nomes duplicados (sufixo `(id)`) e cria índice único `idx_layouts_nome` em `layouts.nome`. | +| **v3** | Adiciona colunas `indice_documento_tipo` (INTEGER) e `pos_documento_tipo` (TEXT) na tabela `layouts`. | + +### Schema final da tabela `layouts` + +```sql +CREATE TABLE layouts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nome TEXT NOT NULL, + tipo TEXT NOT NULL CHECK(tipo IN ('csv', 'xlsx')), + + -- Campos CSV + delimitador TEXT, + encoding TEXT, + linha_cabecalho INTEGER, + indice_numero INTEGER, + indice_serie INTEGER, + indice_valor INTEGER, + indice_data INTEGER, + indice_documento_tipo INTEGER, -- adicionado em v3 + + -- Campos XLSX + aba TEXT, + pos_numero TEXT, + pos_serie TEXT, + pos_valor TEXT, + pos_data TEXT, + pos_documento_tipo TEXT -- adicionado em v3 +); + +CREATE UNIQUE INDEX idx_layouts_nome ON layouts (nome); -- adicionado em v2 +``` + +--- + +## layout_repository.rs + +Implementa as operações CRUD sobre a entidade `Layout` (enum com variantes +`Layout::Csv` e `Layout::Xlsx`). + +### Funções públicas + +#### `salvar(conn, layout) -> Result` + +Insere um novo layout e retorna o `rowid` gerado. + +- `Layout::Csv` → preenche colunas CSV; colunas XLSX ficam `NULL`. +- `Layout::Xlsx` → preenche colunas XLSX; colunas CSV ficam `NULL`. + +#### `atualizar(conn, layout) -> Result<()>` + +Atualiza um layout existente pelo `id` embutido na variante. Retorna erro se +`id` for `None`. + +- `Layout::Csv` → atualiza apenas as colunas CSV. +- `Layout::Xlsx` → atualiza apenas as colunas XLSX. + +#### `listar(conn) -> Result>` + +Seleciona todos os layouts ordenados por `nome ASC`. Para cada linha: + +- `tipo == "csv"` → constrói `Layout::Csv` mapeando as colunas de índice. +- `tipo == "xlsx"` → constrói `Layout::Xlsx` mapeando as colunas de posição. + +Campos opcionais (`Option`) são lidos como `Option` e convertidos. + +#### `excluir(conn, id) -> Result<()>` + +Remove o registro com o `id` informado via `DELETE`. + +#### `existe_nome(conn, nome) -> Result` + +Conta registros com o nome fornecido; retorna `true` se `COUNT(*) > 0`. +Usado para validar unicidade antes de salvar. + +--- + +## Fluxo de inicialização + +``` +abrir_banco() + └─> abrir_banco_no_caminho(caminho) + ├─ cria diretório se necessário + ├─ testa banco existente (SELECT 1) + │ ├─ OK → retorna (conn, false) + │ └─ ERR → renomeia para .bak, cria banco novo → (conn, true) + └─ banco novo → retorna (conn, true) + +aplicar_migrations(conn) + ├─ cria schema_version se necessário + ├─ lê versão atual + ├─ executa migrations pendentes (v1 → v2 → v3) + └─ grava versão final +``` + +Após esse fluxo, a conexão está pronta para uso pelo `layout_repository`. + +--- + +## Dependencias externas relevantes + +| Crate | Uso | +|-------|-----| +| `rusqlite` | Driver SQLite embutido (sem servidor externo) | +| `dirs` | Resolve `config_dir()` conforme o SO |