Implementado (3 features)

**Agrupamento de faltantes contíguos** (`detector_sequencia.rs`)
- Nova função pública `agrupar_contiguos(&[u64]) -> Vec<(u64, u64)>` com
  4 testes
- Na tela de resultado, faltantes agora aparecem como `• 100–104 (5
  notas)` em vez de 5 linhas separadas

**Estatísticas de completude** (`resultado.rs`)
- Cada série exibe: `Série 001 — 10 faltante(s) — 990/1000 notas (99.0%
  completo):`

**Copiar para clipboard** (`resultado.rs`)
- Botão `📋 Copiar` ao lado de cada série nos faltantes — copia todos os
  números (não só a página atual)
- Botão `📋 Copiar` nas duplicatas — copia no formato `1234 (3x), 5678
  (2x)`

Total: **42 → 46 testes**, todos passando.
This commit is contained in:
FelipeCN
2026-03-03 16:41:32 -03:00
parent 2c96e99ccc
commit b9052e073f
21 changed files with 558 additions and 778 deletions
+62 -36
View File
@@ -1,7 +1,7 @@
use crate::application::usecases::exportar_pdf::exportar_pdf;
use crate::domain::{
entities::resultado_analise::ResultadoAnalise,
services::parser_monetario::formatar_valor_br,
entities::{chave_serie::ChaveSerie, resultado_analise::ResultadoAnalise},
services::{detector_sequencia::agrupar_contiguos, parser_monetario::formatar_valor_br},
};
use crate::infrastructure::pdf_generator::GenpdfGenerator;
use crate::ui::app::{AcaoModal, App, EstadoApp};
@@ -86,15 +86,15 @@ fn renderizar_totais(ui: &mut Ui, resultado: &ResultadoAnalise) {
formatar_valor_br(&resultado.soma_total)
));
let mut series: Vec<&String> = resultado.soma_por_serie.keys().collect();
series.sort();
let mut chaves: Vec<&ChaveSerie> = resultado.soma_por_serie.keys().collect();
chaves.sort();
for serie in series {
let soma = &resultado.soma_por_serie[serie];
let total_notas = resultado.total_por_serie.get(serie).copied().unwrap_or(0);
for chave in chaves {
let soma = &resultado.soma_por_serie[chave];
let total_notas = resultado.total_por_serie.get(chave).copied().unwrap_or(0);
ui.label(format!(
" Série {}: {} nota(s) — R$ {}",
serie,
chave.label(),
total_notas,
formatar_valor_br(soma)
));
@@ -104,12 +104,9 @@ fn renderizar_totais(ui: &mut Ui, resultado: &ResultadoAnalise) {
fn renderizar_faltantes(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalise) {
let total_faltantes = resultado.total_faltantes();
ui.label(
egui::RichText::new(format!(
"Notas Faltantes ({} total)",
total_faltantes
))
.heading()
.strong(),
egui::RichText::new(format!("Notas Faltantes ({} total)", total_faltantes))
.heading()
.strong(),
);
ui.add_space(4.0);
@@ -118,22 +115,36 @@ fn renderizar_faltantes(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalise
return;
}
let mut series: Vec<&String> = resultado.faltantes_por_serie.keys().collect();
series.sort();
let mut chaves: Vec<&ChaveSerie> = resultado.faltantes_por_serie.keys().collect();
chaves.sort();
for serie in series {
let faltantes = &resultado.faltantes_por_serie[serie];
for chave in chaves {
let faltantes = &resultado.faltantes_por_serie[chave];
if faltantes.is_empty() {
continue;
}
ui.label(format!(
"Série {}{} faltante(s):",
serie,
faltantes.len()
));
// Estatística de completude por série
let total_notas = resultado.total_por_serie.get(chave).copied().unwrap_or(0);
let total_esperado = total_notas + faltantes.len();
let percentual = total_notas as f64 / total_esperado as f64 * 100.0;
// Paginação
ui.horizontal(|ui| {
ui.label(format!(
"Série {}{} faltante(s) — {}/{} notas ({:.1}% completo):",
chave.label(),
faltantes.len(),
total_notas,
total_esperado,
percentual,
));
if ui.button("📋 Copiar").on_hover_text("Copiar todos os números faltantes").clicked() {
let texto = faltantes.iter().map(|n| n.to_string()).collect::<Vec<_>>().join(", ");
ui.ctx().copy_text(texto);
}
});
// Paginação (por faltante individual)
let total_paginas = (faltantes.len() + app.itens_por_pagina - 1) / app.itens_por_pagina;
if app.pagina_faltantes >= total_paginas {
app.pagina_faltantes = 0;
@@ -142,8 +153,13 @@ fn renderizar_faltantes(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalise
let inicio = app.pagina_faltantes * app.itens_por_pagina;
let fim = (inicio + app.itens_por_pagina).min(faltantes.len());
for numero in &faltantes[inicio..fim] {
ui.label(format!("{}", numero));
// Exibir grupos contíguos da página atual
for (a, b) in agrupar_contiguos(&faltantes[inicio..fim]) {
if a == b {
ui.label(format!("{}", a));
} else {
ui.label(format!("{}{} ({} notas)", a, b, b - a + 1));
}
}
if total_paginas > 1 {
@@ -181,20 +197,30 @@ fn renderizar_duplicatas(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalis
return;
}
let mut series: Vec<&String> = resultado.duplicadas_por_serie.keys().collect();
series.sort();
let mut chaves: Vec<&ChaveSerie> = resultado.duplicadas_por_serie.keys().collect();
chaves.sort();
for serie in series {
let duplicatas = &resultado.duplicadas_por_serie[serie];
for chave in chaves {
let duplicatas = &resultado.duplicadas_por_serie[chave];
if duplicatas.is_empty() {
continue;
}
ui.label(format!(
"Série {}{} grupo(s) duplicado(s):",
serie,
duplicatas.len()
));
ui.horizontal(|ui| {
ui.label(format!(
"Série {}{} grupo(s) duplicado(s):",
chave.label(),
duplicatas.len()
));
if ui.button("📋 Copiar").on_hover_text("Copiar números duplicados").clicked() {
let texto = duplicatas
.iter()
.map(|(n, c)| format!("{} ({}x)", n, c))
.collect::<Vec<_>>()
.join(", ");
ui.ctx().copy_text(texto);
}
});
let total_paginas =
(duplicatas.len() + app.itens_por_pagina - 1) / app.itens_por_pagina;
@@ -208,7 +234,7 @@ fn renderizar_duplicatas(ui: &mut Ui, app: &mut App, resultado: &ResultadoAnalis
for (numero, count) in &duplicatas[inicio..fim] {
ui.label(format!(
" • NF {} / Série {}{} ocorrências",
numero, serie, count
numero, chave.label(), count
));
}