Compare commits
1
Commits
857fcd6a82
...
change-ui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c4f573cd1 |
@@ -2,40 +2,6 @@
|
|||||||
|
|
||||||
> Features avaliadas em 03/03/2026. Organizadas por categoria e esforço estimado.
|
> Features avaliadas em 03/03/2026. Organizadas por categoria e esforço estimado.
|
||||||
|
|
||||||
## Alta Prioridade
|
|
||||||
|
|
||||||
### F-01 — Exportar Resultado em CSV.
|
|
||||||
|
|
||||||
**Problema:** O único formato de exportação é PDF. Para processar os resultados em ferramentas externas (Excel, Power BI, sistemas ERP), o usuário precisa redigitar dados do PDF.
|
|
||||||
|
|
||||||
**Solução:** Botão "Exportar CSV" na tela de resultado, gerando dois arquivos (ou um com duas seções):
|
|
||||||
- `faltantes.csv`: `serie,documento_tipo,numero`
|
|
||||||
- `duplicatas.csv`: `serie,documento_tipo,numero,ocorrencias`
|
|
||||||
|
|
||||||
**Escopo técnico:**
|
|
||||||
- `use case` `exportar_csv(resultado: &ResultadoAnalise, caminho: &Path)`
|
|
||||||
- Trait `CsvExporter` análoga à `PdfGenerator` (opcional, para testabilidade)
|
|
||||||
- Botão na barra de ações da tela `resultado.rs`
|
|
||||||
- Dependência `csv` já está no `Cargo.toml`
|
|
||||||
|
|
||||||
**Esforço estimado:** Médio (2–3h)
|
|
||||||
**Impacto:** Alto — elimina retrabalho manual
|
|
||||||
|
|
||||||
**Observações:**
|
|
||||||
Se não for muito custoso para implementar, pode ser uma boa ideia adicionar uma essa função.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### F-02 — Validação de Tamanho de Arquivo (PRD RF01.3) (Implementado)
|
|
||||||
|
|
||||||
**Status:** Já implementado. Ambos `csv_reader.rs` e `xlsx_reader.rs` verificam o tamanho do arquivo
|
|
||||||
antes de qualquer leitura via constante `LIMITE_BYTES = 50 MB`, retornando `ErroArquivo::TamanhoExcedido`
|
|
||||||
se excedido — exibido como modal de erro pela UI.
|
|
||||||
|
|
||||||
**Esforço estimado:** Baixo (30min) — *concluído*
|
|
||||||
**Impacto:** Médio — evita travamentos inesperados
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Média Prioridade
|
## Média Prioridade
|
||||||
|
|
||||||
@@ -59,43 +25,6 @@ Verificar se já existe no codigo, pois na tela de configuração do Layout ele
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### F-04 — Busca por Número na Tela de Resultado (Util)
|
|
||||||
|
|
||||||
**Problema:** Com listas longas, o usuário precisa navegar páginas para verificar se um número específico está faltante ou duplicado.
|
|
||||||
|
|
||||||
**Solução:** Campo de busca no topo da tela de resultado. Ao digitar `1234`, destaca se a nota:
|
|
||||||
- está **faltante** (aparece na lista de faltantes)
|
|
||||||
- está **duplicada** (aparece na lista de duplicatas)
|
|
||||||
- está **presente** (está nas notas importadas)
|
|
||||||
- **não encontrada** (fora do intervalo conhecido)
|
|
||||||
|
|
||||||
**Escopo técnico:**
|
|
||||||
- Campo `filtro_numero: String` no `App`
|
|
||||||
- Busca em `resultado.faltantes_por_serie` e `resultado.duplicadas_por_serie`
|
|
||||||
- Para "presente": busca em `app.notas_importadas` (já mantido em memória)
|
|
||||||
- Banner de resultado no topo da tela
|
|
||||||
|
|
||||||
**Esforço estimado:** Médio (2–3h)
|
|
||||||
**Impacto:** Médio — uso diário em auditorias pontuais
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### F-05 — Recarregar Arquivo Sem Reconfigurar (Implementado)
|
|
||||||
|
|
||||||
**Problema:** Quando o usuário corrige o arquivo fonte e quer re-verificar, precisa navegar todo o fluxo novamente (selecionar arquivo → configurar colunas → analisar).
|
|
||||||
|
|
||||||
**Solução:** Botão "🔄 Reanalisar Arquivo" na tela de resultado que reimporta o mesmo caminho com o mesmo layout atual, sem nenhuma interação adicional.
|
|
||||||
|
|
||||||
**Escopo técnico:**
|
|
||||||
- Persistir `caminho_arquivo_atual: Option<PathBuf>` no `App` (já existe parcialmente como `nome_arquivo`)
|
|
||||||
- Reaproveitar o fluxo de `executar_importacao()` com os parâmetros atuais
|
|
||||||
- Botão na barra de ações da tela `resultado.rs`
|
|
||||||
|
|
||||||
**Esforço estimado:** Baixo (1h)
|
|
||||||
**Impacto:** Médio — elimina atrito no ciclo corrigir → verificar
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### F-06 — Auto-detecção de Delimitador CSV (Util)
|
### F-06 — Auto-detecção de Delimitador CSV (Util)
|
||||||
|
|
||||||
**Problema:** O usuário precisa saber antecipadamente qual delimitador o arquivo usa (`,`, `;`, `\t`). Arquivos gerados por diferentes sistemas variam.
|
**Problema:** O usuário precisa saber antecipadamente qual delimitador o arquivo usa (`,`, `;`, `\t`). Arquivos gerados por diferentes sistemas variam.
|
||||||
@@ -152,11 +81,6 @@ Verificar se já existe no codigo, pois na tela de configuração do Layout ele
|
|||||||
|
|
||||||
| ID | Feature | Esforço | Impacto | Prioridade | Status |
|
| ID | Feature | Esforço | Impacto | Prioridade | Status |
|
||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| F-01 | Exportar resultado em CSV | Médio | Alto | Alta |
|
|
||||||
| F-02 | Validação de tamanho de arquivo | Baixo | Médio | Alta |
|
|
||||||
| F-03 | Preview de colunas antes da análise | Alto | Alto | Média |
|
| F-03 | Preview de colunas antes da análise | Alto | Alto | Média |
|
||||||
| F-04 | Busca por número no resultado | Médio | Médio | Média |
|
|
||||||
| F-06 | Recarregar arquivo sem reconfigurar | Baixo | Médio | Média |
|
|
||||||
| F-07 | Auto-detecção de delimitador CSV | Médio | Baixo-Médio | Média |
|
| F-07 | Auto-detecção de delimitador CSV | Médio | Baixo-Médio | Média |
|
||||||
| F-08 | Auto-detecção de encoding CSV | Médio | Baixo | Baixa |
|
| F-08 | Auto-detecção de encoding CSV | Médio | Baixo | Baixa |
|
||||||
| F-09 | Agrupamento de faltantes no PDF | Baixo | Baixo-Médio | Baixa | ✅ Implementado |
|
|
||||||
|
|||||||
+17
-3
@@ -81,11 +81,19 @@ literais fora de `theme.rs`.**
|
|||||||
|
|
||||||
**Botões:**
|
**Botões:**
|
||||||
- `t::btn_primary` — azul sólido, ação principal
|
- `t::btn_primary` — azul sólido, ação principal
|
||||||
- `t::btn_secondary` — SURFACE_2, ação secundária
|
- `t::btn_secondary` — SURFACE_2 com borda `TEXT_SECONDARY` 55%, ação secundária
|
||||||
- `t::btn_ghost` — transparente com borda, ação terciária
|
- `t::btn_ghost` — transparente com borda `TEXT_SECONDARY` 55%, ação terciária
|
||||||
- `t::btn_danger` — vermelho semitransparente, exclusão
|
- `t::btn_danger` — vermelho semitransparente, exclusão
|
||||||
- `t::btn_aba_ativa` / `t::btn_aba_inativa` — seleção de aba XLSX
|
- `t::btn_aba_ativa` / `t::btn_aba_inativa` — seleção de aba XLSX
|
||||||
- `t::btn_pagina_ativo` / `t::btn_pagina_inativo` — paginação
|
- `t::btn_pagina_ativo` / `t::btn_pagina_inativo` — paginação
|
||||||
|
- `t::btn_breadcrumb_visitado` — botão sem fundo para passos anteriores do breadcrumb
|
||||||
|
|
||||||
|
### Contraste de estados de botão
|
||||||
|
|
||||||
|
`btn_secondary` e `btn_ghost` usam borda visível (`TEXT_SECONDARY` a 55% de opacidade)
|
||||||
|
no estado normal para se destacar de containers com fundo `SURFACE_2`. O estado
|
||||||
|
`Disabled` reduz a borda para 20% de opacidade, criando contraste claro entre ativo e
|
||||||
|
inativo. **Nunca remova a borda desses estilos** — sem ela os botões se perdem no fundo.
|
||||||
|
|
||||||
**Inputs:**
|
**Inputs:**
|
||||||
- `t::input_dark` — text_input com fundo BG, borda BORDER, focus PRIMARY
|
- `t::input_dark` — text_input com fundo BG, borda BORDER, focus PRIMARY
|
||||||
@@ -214,7 +222,7 @@ Chamado em `app.rs` quando `self.modal.is_some()`.
|
|||||||
Renderiza as primeiras N linhas do arquivo com cabeçalho estilo Excel (A, B, C...).
|
Renderiza as primeiras N linhas do arquivo com cabeçalho estilo Excel (A, B, C...).
|
||||||
|
|
||||||
- Células com `Fixed(100.0)` — tamanho fixo mínimo com scroll horizontal.
|
- Células com `Fixed(100.0)` — tamanho fixo mínimo com scroll horizontal.
|
||||||
- A altura da área de scroll está fixada em `Fixed(160.0)` — intencional.
|
- A altura da área de scroll está fixada em `Fixed(200.0)` — acomoda 5 linhas + cabeçalho + scrollbar horizontal.
|
||||||
- Scroll horizontal via `scrollable::Direction::Horizontal`.
|
- Scroll horizontal via `scrollable::Direction::Horizontal`.
|
||||||
|
|
||||||
### `paginacao::controles_paginacao(pagina, total, msg_anterior, msg_proxima)`
|
### `paginacao::controles_paginacao(pagina, total, msg_anterior, msg_proxima)`
|
||||||
@@ -244,6 +252,12 @@ diretamente de dentro de uma tela.**
|
|||||||
O breadcrumb é renderizado automaticamente por `app.rs` para todos os estados
|
O breadcrumb é renderizado automaticamente por `app.rs` para todos os estados
|
||||||
exceto `GerenciandoLayouts` e `Analisando`.
|
exceto `GerenciandoLayouts` e `Analisando`.
|
||||||
|
|
||||||
|
### Comportamento de navegação do breadcrumb
|
||||||
|
|
||||||
|
- **Passo ativo** — destaque azul com fundo semitransparente (`PRIMARY` a 15%).
|
||||||
|
- **Passos anteriores** — `button` com `t::btn_breadcrumb_visitado` e `on_press` com a mensagem de navegação correspondente (`Message::IrParaImportacao` para passo 1, `Message::IrParaConfiguracaoColunas` para passo 2). O hover revela fundo sutil + borda, sinalizando interatividade.
|
||||||
|
- **Passos futuros** — `container` com texto `TEXT_MUTED`, sem interação (não é possível pular à frente).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Adicionando uma nova tela
|
## 7. Adicionando uma nova tela
|
||||||
|
|||||||
+19
-3
@@ -14,7 +14,7 @@ use crate::domain::{
|
|||||||
use crate::infrastructure::sqlite::{connection::abrir_banco, migrations::aplicar_migrations};
|
use crate::infrastructure::sqlite::{connection::abrir_banco, migrations::aplicar_migrations};
|
||||||
use crate::ui::message::{Message, ResultadoPendente};
|
use crate::ui::message::{Message, ResultadoPendente};
|
||||||
use crate::ui::theme as t;
|
use crate::ui::theme as t;
|
||||||
use iced::widget::{column, container, text, Row};
|
use iced::widget::{button, column, container, text, Row};
|
||||||
use iced::{Alignment, Element, Length, Task};
|
use iced::{Alignment, Element, Length, Task};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -788,6 +788,12 @@ impl App {
|
|||||||
let mut items: Vec<Element<'_, Message>> = Vec::new();
|
let mut items: Vec<Element<'_, Message>> = Vec::new();
|
||||||
|
|
||||||
for (i, (label, n)) in passos.iter().enumerate() {
|
for (i, (label, n)) in passos.iter().enumerate() {
|
||||||
|
let msg_nav = match n {
|
||||||
|
1 => Some(Message::IrParaImportacao),
|
||||||
|
2 => Some(Message::IrParaConfiguracaoColunas),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
let elem: Element<'_, Message> = if *n == passo_ativo {
|
let elem: Element<'_, Message> = if *n == passo_ativo {
|
||||||
container(text(*label).size(13).color(t::PRIMARY))
|
container(text(*label).size(13).color(t::PRIMARY))
|
||||||
.padding([4, 10])
|
.padding([4, 10])
|
||||||
@@ -798,9 +804,19 @@ impl App {
|
|||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
} else if *n < passo_ativo {
|
} else if *n < passo_ativo {
|
||||||
text(*label).size(13).color(t::TEXT_SECONDARY).into()
|
// Passo já visitado — botão clicável
|
||||||
|
let mut btn = button(text(*label).size(13))
|
||||||
|
.padding([4, 10])
|
||||||
|
.style(t::btn_breadcrumb_visitado);
|
||||||
|
if let Some(msg) = msg_nav {
|
||||||
|
btn = btn.on_press(msg);
|
||||||
|
}
|
||||||
|
btn.into()
|
||||||
} else {
|
} else {
|
||||||
text(*label).size(13).color(t::TEXT_MUTED).into()
|
// Passo futuro — ainda não disponível
|
||||||
|
container(text(*label).size(13).color(t::TEXT_MUTED))
|
||||||
|
.padding([4, 10])
|
||||||
|
.into()
|
||||||
};
|
};
|
||||||
items.push(elem);
|
items.push(elem);
|
||||||
if i < passos.len() - 1 {
|
if i < passos.len() - 1 {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ pub fn tabela_preview(linhas: &[Vec<String>]) -> Element<'_, Message> {
|
|||||||
.direction(scrollable::Direction::Horizontal(
|
.direction(scrollable::Direction::Horizontal(
|
||||||
scrollable::Scrollbar::default(),
|
scrollable::Scrollbar::default(),
|
||||||
))
|
))
|
||||||
.height(Length::Fixed(160.0)),
|
.height(Length::Fixed(200.0)),
|
||||||
)
|
)
|
||||||
.style(|_theme| iced::widget::container::Style {
|
.style(|_theme| iced::widget::container::Style {
|
||||||
background: Some(t::SURFACE.into()),
|
background: Some(t::SURFACE.into()),
|
||||||
|
|||||||
+51
-5
@@ -288,8 +288,12 @@ pub fn btn_secondary(theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
background: Some(SURFACE_2.into()),
|
background: Some(SURFACE_2.into()),
|
||||||
text_color: TEXT,
|
text_color: TEXT,
|
||||||
border: Border {
|
border: Border {
|
||||||
|
color: Color {
|
||||||
|
a: 0.55,
|
||||||
|
..TEXT_SECONDARY
|
||||||
|
},
|
||||||
|
width: 1.0,
|
||||||
radius: 6.0.into(),
|
radius: 6.0.into(),
|
||||||
..Default::default()
|
|
||||||
},
|
},
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -304,6 +308,10 @@ pub fn btn_secondary(theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
|
border: Border {
|
||||||
|
color: TEXT_SECONDARY,
|
||||||
|
..base.border
|
||||||
|
},
|
||||||
..base
|
..base
|
||||||
},
|
},
|
||||||
button::Status::Disabled => button::Style {
|
button::Status::Disabled => button::Style {
|
||||||
@@ -315,6 +323,10 @@ pub fn btn_secondary(theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
text_color: Color { a: 0.4, ..TEXT },
|
text_color: Color { a: 0.4, ..TEXT },
|
||||||
|
border: Border {
|
||||||
|
color: Color { a: 0.2, ..BORDER },
|
||||||
|
..base.border
|
||||||
|
},
|
||||||
..base
|
..base
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
@@ -324,13 +336,16 @@ pub fn btn_secondary(theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Botão ghost (sem fundo, borda sutil)
|
/// Botão ghost (sem fundo, borda visível)
|
||||||
pub fn btn_ghost(theme: &Theme, status: button::Status) -> button::Style {
|
pub fn btn_ghost(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let base = button::Style {
|
let base = button::Style {
|
||||||
background: Some(Color::TRANSPARENT.into()),
|
background: Some(Color::TRANSPARENT.into()),
|
||||||
text_color: TEXT_SECONDARY,
|
text_color: TEXT,
|
||||||
border: Border {
|
border: Border {
|
||||||
color: BORDER,
|
color: Color {
|
||||||
|
a: 0.55,
|
||||||
|
..TEXT_SECONDARY
|
||||||
|
},
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: 6.0.into(),
|
radius: 6.0.into(),
|
||||||
},
|
},
|
||||||
@@ -340,12 +355,16 @@ pub fn btn_ghost(theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
button::Status::Hovered => button::Style {
|
button::Status::Hovered => button::Style {
|
||||||
background: Some(Color { a: 0.08, ..PRIMARY }.into()),
|
background: Some(Color { a: 0.08, ..PRIMARY }.into()),
|
||||||
text_color: TEXT,
|
text_color: TEXT,
|
||||||
|
border: Border {
|
||||||
|
color: PRIMARY,
|
||||||
|
..base.border
|
||||||
|
},
|
||||||
..base
|
..base
|
||||||
},
|
},
|
||||||
button::Status::Disabled => button::Style {
|
button::Status::Disabled => button::Style {
|
||||||
text_color: TEXT_MUTED,
|
text_color: TEXT_MUTED,
|
||||||
border: Border {
|
border: Border {
|
||||||
color: Color { a: 0.3, ..BORDER },
|
color: Color { a: 0.2, ..BORDER },
|
||||||
..base.border
|
..base.border
|
||||||
},
|
},
|
||||||
..base
|
..base
|
||||||
@@ -381,6 +400,33 @@ pub fn btn_danger(theme: &Theme, status: button::Status) -> button::Style {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Botão de passo anterior no breadcrumb (clicável, mas sem destaque)
|
||||||
|
pub fn btn_breadcrumb_visitado(_theme: &Theme, status: button::Status) -> button::Style {
|
||||||
|
let base = button::Style {
|
||||||
|
background: None,
|
||||||
|
text_color: TEXT_SECONDARY,
|
||||||
|
border: Border {
|
||||||
|
color: Color::TRANSPARENT,
|
||||||
|
width: 0.0,
|
||||||
|
radius: 4.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
match status {
|
||||||
|
button::Status::Hovered => button::Style {
|
||||||
|
background: Some(Color { a: 0.08, ..TEXT }.into()),
|
||||||
|
text_color: TEXT,
|
||||||
|
border: Border {
|
||||||
|
color: Color { a: 0.15, ..TEXT },
|
||||||
|
width: 1.0,
|
||||||
|
radius: 4.0.into(),
|
||||||
|
},
|
||||||
|
..base
|
||||||
|
},
|
||||||
|
_ => base,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Botão de aba selecionada (tela selecionar_aba)
|
/// Botão de aba selecionada (tela selecionar_aba)
|
||||||
pub fn btn_aba_ativa(theme: &Theme, status: button::Status) -> button::Style {
|
pub fn btn_aba_ativa(theme: &Theme, status: button::Status) -> button::Style {
|
||||||
let base = button::Style {
|
let base = button::Style {
|
||||||
|
|||||||
Reference in New Issue
Block a user