Expand the compiled next-steps doc back into the full delivery history, cross-reference plan statuses, and sync AGENTS.md, PRODUCT.md, and the Núcleos project notebook.
16 KiB
Overview na lista de núcleos
Status: implementado (2026-07-18) Atualizado em: 2026-07-18 Item do roadmap: docs/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:
liderancavê o overview — sim, comselectreduzido (semproposedVoteEstimate) 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 · 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 — 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 temadata-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, semConsent.
Decisões travadas
- Mesmos filtros da página. O overview é computado a partir do mesmo
NucleusListStatejá parseado emloadNucleusListPageData(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: falseselecionando só os campos de agregação. Mesmo padrão já usado porgetCampaignDashboardPageData(que carrega todos os núcleos ativos compagination: 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:geralvê tudo,coordenadorvê só os seus núcleos,liderancavê só os núcleos com liderança engajada — exatamente como a lista já faz. Para "Últimas atualizações",canReadNucleusUpdatejá restringeliderancaao próprio autor; o preview deliderancamostra 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 oEmptyde "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
liderancavê o overview? A lista de núcleos é visível para os três papéis, e o roadmap não excluilideranca. 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 (
getNucleusUpdatesPageDatapreview). 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
geralsem 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 comcountpor cláusula depois. - Cache/ISR. A página já é dinâmica (lê
searchParams). O overview é computado por request com dados frescos — semunstable_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/statuse respeita os mesmos filtros de território. - Demandas: idem. Mesma recomendação — arquitetura aberta, decisão no plano de Demandas.
Abordagem proposta
flowchart LR
Page["NucleiPage (server)<br/>loadNucleusListPageData<br/>+ loadNucleusListOverviewData"]
State["NucleusListState<br/>(mesmos filtros)"]
Where["buildNucleusListWhere(state)"]
AggNuclei["find electoralNucleus<br/>pagination:false<br/>overrideAccess:false<br/>select agregados"]
Updates["find nucleusUpdate<br/>nucleus in ids<br/>sort -createdAt limit N<br/>overrideAccess:false"]
VM["buildNucleusListOverviewViewModel<br/>(estimativa + cobertura<br/>+ preview atualizações)"]
Comp["NucleusListOverview (server)<br/>grid de blocos"]
Events["Bloco Eventos<br/>(futuro)"]
Demandas["Bloco Demandas<br/>(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(emsrc/utilities/nucleusListOverviewPageData.ts): recebepayload,user,state: NucleusListState. Roda em paralelo comloadNucleusListPageData(mesmastate):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, retornanull(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. Paralideranca, o access control já filtra só próprios; parageral/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 degetNucleusUpdatesPageData). - Retorna
{ nuclei: overviewNuclei, recentUpdates }(tipoNucleusListOverviewInput).
buildNucleusListOverviewViewModel(emsrc/utilities/nucleusListOverviewViewModels.ts): recebe o input +nowe produzNucleusListOverviewViewModel:estimate: { confirmedTotal: number, confirmedPercent: number, pendingCount: number }— soma deconfirmedVoteEstimate, % de núcleos comconfirmedVoteEstimate != null, contagem deproposedVoteEstimate != null(comconfirmedVoteEstimate == nullou não — definir; recomendação: pendente =proposedVoteEstimate != null, independente de confirmada, espelhando o dashboardpendingEstimate).coverage: { coordinatedCount, total, percent }— % comcoordinators.length > 0(opcional, ver questões em aberto).recentUpdates: Array<{ id, nucleusSlug, nucleusName, authorName, kind, body, createdAt }>— já truncado pelolimit.- Reusa
percentagee helpers de formatação no estilo decampaignDashboardViewModels.ts.
NucleusListOverview(server component, emsrc/components/campaign/): grid responsivo de blocos (Card/CardHeader/CardContent, reusandosrc/components/ui/card):- Bloco Estimativa de votos — total confirmado (
Intl.NumberFormat('pt-BR')+ "votos"),% com estimativa confirmadacom<Progress>, e badge "N sugestões pendentes". - Bloco Últimas atualizações — lista compacta (autor, tipo, data relativa, link para
/campanha/nucleos/{slug}?tab=updates). Reusa orelativeFormatterdoCampaignDashboard. Vazio: "Nenhuma atualização recente". - Espaço reservado (sem UI) para Próximos eventos e Demandas — comentário no componente marcando onde plugar.
- Bloco Estimativa de votos — total confirmado (
- Integração na página (
src/app/(campaign)/campanha/(app)/nucleos/page.tsx): chamarloadNucleusListPageDataeloadNucleusListOverviewDataem paralelo (ambas usam a mesmastatejá resolvida porresolveNucleusListUrl). Renderizar<NucleusFilters>e, seoverviewnão fornulleresult.docs.length > 0,<NucleusListOverview view={overview} now={now} />antes de<NucleusList>. - Sem migration, sem collection, sem server action. Todo o fluxo é leitura no server.
Dependências
- Nenhuma de outro plano. Reusa
NucleusListState,buildNucleusListWhere,parseNucleusListParams(src/utilities/nucleusUi.ts),getBahiaWeekRange(src/utilities/campaignTime.ts) se precisar de janela semanal, access control existente (src/utilities/campaignAccess.ts) e UICard/Progress/Badge(src/components/ui/). - Eventos e Demandas dependem dos respectivos domínios serem modelados (itens separados do roadmap). Este plano entrega só a fundação + os dois blocos ativos.
Não escopo
- Criar as collections Eventos ou Demandas — domínios separados no roadmap.
- Alterar os filtros existentes ou a paginação da lista — o overview é aditivo e reage ao estado já existente.
- Substituir o dashboard
/campanha— o dashboard continua o recorte global (todos os ativos, sem filtros de lista); o overview é o recorte filtrado da lista. - Estimativa estatística/previsão de votos (roadmap linha 60) — aqui só somamos
confirmedVoteEstimate. - Novo access control ou novo
Consent— herda o que existe. - Compartilhar núcleo a partir da lista (
NucleusCardactions) — escopo do planocompartilhar-pagina.md(só o detalhe neste ciclo).
Referências
docs/roadmap.md(linha 50, e linhas 54–55 para Eventos/Demandas)src/app/(campaign)/campanha/(app)/nucleos/page.tsx— página onde o overview entrasrc/utilities/nucleusPageData.ts—loadNucleusListPageData(mesmastate)src/utilities/nucleusUi.ts—NucleusListState,parseNucleusListParams,buildNucleusListWheresrc/utilities/campaignDashboardPageData.tsesrc/utilities/campaignDashboardViewModels.ts— padrão de agregação e KPIs a espelharsrc/utilities/nucleusUpdatePageData.ts— padrão do preview de atualizações (limit 3, resolve autores)src/components/campaign/CampaignDashboard.tsx—KpiCard,Progress,relativeFormatter, layout de gridsrc/utilities/campaignAccess.ts—canReadElectoralNucleus,canReadNucleusUpdate(escopo por papel)- AGENTS.md — Campaign auth, naming conventions, padrão de leitura com
overrideAccess: false
