# Overview na lista de núcleos
Status: **implementado** (2026-07-18)
Atualizado em: 2026-07-18
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B → B1)
Responsável: —
## Como foi implementado (2026-07-18)
Entregue conforme a abordagem proposta: `loadNucleusListOverviewData` (`src/utilities/nucleusListOverviewPageData.ts`) + `buildNucleusListOverviewViewModel` (`src/utilities/nucleusListOverviewViewModels.ts`) + componente server `NucleusListOverview` (`src/components/campaign/NucleusListOverview.tsx`), integrado em `src/app/(campaign)/campanha/(app)/nucleos/page.tsx` entre filtros e lista. Sem collection, migration ou server action; todas as queries com `user` + `overrideAccess: false`. Questões em aberto resolvidas:
- **`lideranca` vê o overview** — sim, com `select` reduzido (sem `proposedVoteEstimate`) e sem o badge de sugestões pendentes; as atualizações da prévia são só as próprias (escopo do access control) e o autor é resolvido localmente sem query extra.
- **Preview de atualizações: 3** (`nucleusListOverviewPreviewLimit`), com autor, badge de tipo, data relativa e link para `?tab=updates`.
- **Pendente = `proposedVoteEstimate != null`**, espelhando o dashboard; o total soma apenas confirmadas.
- **Bloco Cobertura incluído** (% de núcleos com coordenador no conjunto filtrado).
- **Overview oculto** quando o conjunto filtrado é vazio (loader retorna `null`).
- **Eventos/Demandas**: apenas o ponto de extensão comentado no componente, sem UI órfã.
Testes: `tests/int/campaignNucleusListOverview.int.spec.ts`, `tests/unit/campaignNucleusListOverview.unit.spec.ts`. O restante do documento é o plano original, mantido como registro.
## Referência visual (UX Pilot)
Design: [`Lista-Nucleos-Overview.png`](../design-refs/latest/Lista-Nucleos-Overview.png) · [`Lista-Nucleos-Overview.html`](../design-refs/latest/Lista-Nucleos-Overview.html)

Como usar:
- **Adotar a estrutura:** header → busca → chips de filtro ativos → caption "Mostrando agregados de 14 núcleos filtrados" → blocos de overview → lista. O bloco "Estimativa de votos" (total grande + barra de "% com estimativa confirmada" + badge "3 sugestões pendentes") e os blocos compactos "Cobertura" e "Últimas atualizações" (autor, badge de tipo, data relativa, núcleo, "Ver todas") correspondem exatamente aos objetivos deste plano.
- **Fora deste plano:** o bloco "Baseline 2022" que aparece no design é a Fase 3 de [baseline-eleitoral-tse.md](baseline-eleitoral-tse.md) — só ganha UI quando aquele plano for implementado (mantém-se a decisão "sem UI órfã"). O bottom nav com "Apoiadores" e "Agenda" antecipa os domínios C2/C3; as entradas só entram quando existirem.
- **Ajustar cores e código:** o HTML/PNG usa a paleta antiga (vermelho escuro `#8E0E23`, navy `#1B2B4B`, dourado `#C8874B`) e Tailwind via CDN. Implementar com os componentes shadcn existentes (`src/components/ui`) e os tokens do tema `data-theme='campaign'` (`src/app/(frontend)/styles.css`): fundo branco, primário `#C51414`, superfícies neutras claras. O header navy do design vira o shell claro atual; avatares com foto viram avatares de iniciais (padrão do app).
## Contexto
Hoje `/campanha/nucleos` é só cabeçalho + filtros + lista paginada (25/página). Não há agregados: o usuário precisa varrer a lista (ou ir ao dashboard `/campanha`) para sentir quanto daquela seleção tem estimativa confirmada, quantos votos somam, ou quais foram os últimos reportes. O dashboard geral já calcula esses agregados, mas para **todos** os núcleos ativos — não respeita os filtros de território/cobertura/estimativa da lista.
A decisão de produto (2026-07-17) é adicionar um **painel de overview acima da lista**, com blocos como "Estimativa de votos" e "Últimas atualizações", e espaço reservado para "Próximos eventos" e "Demandas" (que só ganham UI quando os domínios correspondentes existirem). O ponto-chave: **o overview usa os mesmos filtros da página** — se o usuário filtra por um território, os agregados são só daquele território. O overview é, portanto, um recorte do dashboard reagindo aos filtros da lista.
## Objetivos
- Painel de overview renderizado entre os filtros e a lista em `src/app/(campaign)/campanha/(app)/nucleos/page.tsx`.
- Bloco **Estimativa de votos**: soma de `confirmedVoteEstimate`, % com estimativa confirmada e contagem de sugestões pendentes — todos calculados sobre o conjunto **filtrado** (não só a página atual).
- Bloco **Últimas atualizações**: preview das N atualizações mais recentes (`nucleusUpdate`) entre os núcleos filtrados, com link para o detalhe do núcleo.
- Blocos **Próximos eventos** e **Demandas**: arquitetura extensível, mas sem UI órfã — só aparecem quando os domínios existirem.
- Reusar `NucleusListState` / `buildNucleusListWhere(state)` para que o overview sempre concorde com a lista visível.
- Manter access control existente: todas as queries com `user` + `overrideAccess: false`. Sem collection nova, sem migration, sem server action, sem `Consent`.
## Decisões travadas
- **Mesmos filtros da página.** O overview é computado a partir do mesmo `NucleusListState` já parseado em `loadNucleusListPageData` (`parseNucleusListParams` + `buildNucleusListWhere`). Não existe filtro "do overview" separado do filtro "da lista". (Decisão de produto 2026-07-17; roadmap linha 50.)
- **Agregados sobre o conjunto inteiro filtrado, não só a página atual.** A lista é paginada em 25, mas o overview precisa de totais — logo exige uma query adicional com `pagination: false` selecionando só os campos de agregação. Mesmo padrão já usado por `getCampaignDashboardPageData` (que carrega todos os núcleos ativos com `pagination: false`).
- **Sem collection/migration/server action.** Tudo é leitura no server component. Nenhuma escrita, nenhuma transação, nenhum `Consent`.
- **Access control por `overrideAccess: false`.** O overview herda naturalmente o escopo de papel: `geral` vê tudo, `coordenador` vê só os seus núcleos, `lideranca` vê só os núcleos com liderança engajada — exatamente como a lista já faz. Para "Últimas atualizações", `canReadNucleusUpdate` já restringe `lideranca` ao próprio autor; o preview de `lideranca` mostra só os próprios reportes nos núcleos filtrados. O plano não inventa regra nova.
- **Ordem na página: header → filtros → overview → lista.** O overview fica **abaixo** dos filtros (para refletir a seleção que o usuário acabou de fazer) e **acima** da lista. "Acima da lista" (roadmap) é respeitado; colocar abaixo dos filtros é deliberado para que os agregados correspondam à seleção visível.
- **Overview oculto quando não há resultados.** Se `result.totalDocs === 0`, o overview não renderiza — a página já mostra o `Empty` de "Nenhum núcleo encontrado". Evita agregados zerados sem sentido.
- **Eventos e Demandas não ganham UI placeholder.** "Quando o domínio existir" significa: o bloco só é implementado quando a collection existir. Não renderizar cards "em breve" — UI órfã confunde mais do que informa. A arquitetura (view model + componente) fica pronta para plugar.
- **i18n e naming** seguem o AGENTS.md: identificadores em inglês (`NucleusListOverview`, `loadNucleusListOverviewData`, `buildNucleusListOverviewViewModel`), strings visíveis em pt-BR.
## Questões em aberto
- **`lideranca` vê o overview?** A lista de núcleos é visível para os três papéis, e o roadmap não exclui `lideranca`. **Recomendação:** sim, mostrar — os agregados de estimativa e o preview das próprias atualizações são úteis e já respeitam o escopo. Se produto quiser esconder, é só não montar o componente para esse papel.
- **Quantas atualizações no preview?** O detalhe do núcleo usa 3 (`getNucleusUpdatesPageData` preview). **Recomendação:** 3 ou 4 no overview, suficiente para dar um "pulso" sem virar feed. Definir com produto.
- **"Estimativa de votos" mostra também `proposedVoteEstimate`?** Hoje a lista marca "Sugestão pendente". **Recomendação:** o bloco mostra soma das confirmadas + contagem de pendentes (não soma das propostas — proposta não é voto). Soma de propostas pode confundir com confirmado.
- **Agregados por território/cobertura extras?** Ex.: % com coordenador no conjunto filtrado (já existe no dashboard geral). **Recomendação:** incluir como KPI secundário no bloco de estimativa ou num terceiro bloco "Cobertura" — alinhado com o dashboard, mas reagindo a filtros.
- **Custo da segunda query.** Para `geral` sem filtros, o overview carrega todos os núcleos ativos (`pagination: false`) só para agregar — duplica o trabalho da listagem paginada. **Recomendação:** aceitar (o dashboard já faz isso) e selecionar só `{ id, slug, name, coordinators, confirmedVoteEstimate, proposedVoteEstimate, lastUpdateAt }`. Se virar gargalo, otimizar com `count` por cláusula depois.
- **Cache/ISR.** A página já é dinâmica (lê `searchParams`). O overview é computado por request com dados frescos — sem `unstable_cache`, sem tag nova. Confirmar se produto aceita custo por request.
- **Eventos: gatilho de ativação.** Quando o domínio de eventos existir, qual collection o bloco lê? **Recomendação:** deixar a decisão para o plano de Eventos; aqui só fixamos que o bloco consome `nucleus` + `startAt`/`status` e respeita os mesmos filtros de território.
- **Demandas: idem.** Mesma recomendação — arquitetura aberta, decisão no plano de Demandas.
## Abordagem proposta
```mermaid
flowchart LR
Page["NucleiPage (server)
loadNucleusListPageData
+ loadNucleusListOverviewData"]
State["NucleusListState
(mesmos filtros)"]
Where["buildNucleusListWhere(state)"]
AggNuclei["find electoralNucleus
pagination:false
overrideAccess:false
select agregados"]
Updates["find nucleusUpdate
nucleus in ids
sort -createdAt limit N
overrideAccess:false"]
VM["buildNucleusListOverviewViewModel
(estimativa + cobertura
+ preview atualizações)"]
Comp["NucleusListOverview (server)
grid de blocos"]
Events["Bloco Eventos
(futuro)"]
Demandas["Bloco Demandas
(futuro)"]
Page --> State
State --> Where
Where --> AggNuclei
AggNuclei --> Updates
AggNuclei --> VM
Updates --> VM
VM --> Comp
Comp -.extensível.-> Events
Comp -.extensível.-> Demandas
```
Componentes:
- **`loadNucleusListOverviewData`** (em `src/utilities/nucleusListOverviewPageData.ts`): recebe `payload`, `user`, `state: NucleusListState`. Roda em paralelo com `loadNucleusListPageData` (mesma `state`):
- `payload.find({ collection: 'electoralNucleus', where: buildNucleusListWhere(state), depth: 0, pagination: false, sort: 'name', select: { id, slug, name, coordinators, confirmedVoteEstimate, proposedVoteEstimate, lastUpdateAt }, user, overrideAccess: false })` → `overviewNuclei`.
- Se `overviewNuclei.length === 0`, retorna `null` (oculta o overview).
- `payload.find({ collection: 'nucleusUpdate', where: { nucleus: { in: overviewNuclei.map(n => n.id) } }, depth: 0, limit: previewLimit, page: 1, sort: '-createdAt', select: { author: true, kind: true, body: true, createdAt: true, nucleus: true }, user, overrideAccess: false })` → `recentUpdates`. Para `lideranca`, o access control já filtra só próprios; para `geral`/`coordenador`, vem o escopo de núcleos acessíveis.
- Resolve nomes de autores e de núcleos via um `payload.find({ collection: 'campaignUser', where: { id: { in: authorIds } }, select: { name: true, role: true }, overrideAccess: false, user })` (mesmo padrão de `getNucleusUpdatesPageData`).
- Retorna `{ nuclei: overviewNuclei, recentUpdates }` (tipo `NucleusListOverviewInput`).
- **`buildNucleusListOverviewViewModel`** (em `src/utilities/nucleusListOverviewViewModels.ts`): recebe o input + `now` e produz `NucleusListOverviewViewModel`:
- `estimate: { confirmedTotal: number, confirmedPercent: number, pendingCount: number }` — soma de `confirmedVoteEstimate`, % de núcleos com `confirmedVoteEstimate != null`, contagem de `proposedVoteEstimate != null` (com `confirmedVoteEstimate == null` ou não — definir; recomendação: pendente = `proposedVoteEstimate != null`, independente de confirmada, espelhando o dashboard `pendingEstimate`).
- `coverage: { coordinatedCount, total, percent }` — % com `coordinators.length > 0` (opcional, ver questões em aberto).
- `recentUpdates: Array<{ id, nucleusSlug, nucleusName, authorName, kind, body, createdAt }>` — já truncado pelo `limit`.
- Reusa `percentage` e helpers de formatação no estilo de `campaignDashboardViewModels.ts`.
- **`NucleusListOverview`** (server component, em `src/components/campaign/`): grid responsivo de blocos (`Card`/`CardHeader`/`CardContent`, reusando `src/components/ui/card`):
- Bloco **Estimativa de votos** — total confirmado (`Intl.NumberFormat('pt-BR')` + "votos"), `% com estimativa confirmada` com `