teqo-backup/docs/plans/dossie-municipio.md
Francisco Ignácio Fontoura Solla 31fcc20ffd E9 /simplify: consolidate duplicated helpers, dead passthrough, comment drift
Cross-checked by parallel code-quality/performance/reuse reviewers against
the E9 commit. Behavior-preserving:

- laterTimestamp/resolveMunicipalityLastSignalAt were the same "latest of
  two ISO timestamps" written twice; both now delegate to a new
  latestIsoTimestamp in campaignTime.ts, which documents the fixed-width
  UTC invariant that makes the string compare sound.
- Hoisted the frescor comparator's Date.now() out of the per-comparison
  accessor so the sort key stops drifting mid-sort.
- Deleted the unread baseTotal/growthFactor/belowBase passthrough from the
  statewide goal entry point and fixed a comment citing a symbol
  (goalDecompositionBelowBase) that never existed; renamed
  computeStatewideGoalDecomposition -> computeStatewideSuggestedGoals now
  that nothing is proportionally decomposed.
- Collapsed the advisor badge's two copies (desktop table vs mobile card)
  into one AdvisorStatusBadge; they had quietly diverged on which icon
  renders for the "no advisor" state.
- Renamed the sort-menu's stale "Cobertura" label for advisor coverage to
  "Assessoria" (E8 already renamed the list column for the same reason);
  derived the sort-key set from the label record instead of a third
  parallel list.
- Consolidated three zeroed-scenario-record literals into one
  zeroByVoteEstimateScenario() factory in voteEstimate.ts.
- Routed three separate votesByYear[2022] reads through one votesInYear
  helper in municipalityPotential.ts.
- Gated the goal card's empty-state copy on !usesMesaEstimate so it stops
  telling a município with an existing mesa estimate to enter one.
- Swapped a test assertion pinned to a raw Tailwind token for a stable
  data-signal="cold" hook, and tightened the "desert" test's bound to
  check against the actual growth factor (the old bound was true for
  every slug regardless of the formula).

Left alone (roadmap decision, not cleanup): sanityCheckSuggestedGoalsByTerritory
has no production caller yet (deferred to E11/E12); comment shortened
instead of deleting the dead-but-documented code.

Verified: tsc --noEmit, pnpm lint (0 warnings), 337 unit + 366 integration
tests, pnpm build all green.
2026-07-25 02:24:18 -03:00

10 KiB
Raw Permalink Blame History

E16 — Dossiê do município (pré-agenda)

Status: entregue em código (2026-07-25) Atualizado em: 2026-07-25 (as-built: aba ?tab=dossie staff-only com MunicipalityDossier.tsx + municipalityDossierData.ts compondo os loaders existentes — baseline TSE/rank A11, conta da cadeira E8, rede com frescor updatedAt, agenda actionPlan, sinais, perfil A8 com caveat para zonas; caps 8/5/3+2 com "ver tudo"; municipality.budgetNotes via migration 20260725_022155_add_municipality_budget_notes — o .json também sara a cadeia de snapshots pós-campaign_goals; print CSS: chrome print:hidden, shells print:h-auto print:overflow-visible, tipografia A4; int test com mock do unstable_cache do baseline. Revisão pré-implementação 2026-07-24: budgetNotes ganhou migration própria — o veículo C12 caducou; registrado o estado zero-consumidor de A8, resolvido pela seção "Perfil") Item do roadmap: docs/roadmap.md (Inteligência de campanha, E16; plano-mestre inteligencia-campanha.md, gap G11) Impeccable: B — visão nova dentro do detalhe do município existente (tab/rota dossie + print), sem sistema novo Appetite: ~1 dia eng; uma migration pequena (budgetNotes — ver questão resolvida abaixo); o resto compõe dados existentes (fonte estruturada de emendas segue G11 adiado) Responsável: —

Design (Impeccable)

Âncoras: PRODUCT.md (princípios 2 "clarity under pressure" e 6) / DESIGN.md (register product; Flat desk, print sóbrio) · detalhe do município existente (tabs, cards).

Na implementação: craft compacto → critique → polish.

  • Persona / contexto: CG/assessor a caminho de uma agenda: "chego no território sem dossiê" (O6 — prefeito aliado?, economia, disputa local, emendas aportadas; caso Nazaré das Farinhas). Hoje esses dados existem espalhados em tabs.
  • Job principal: uma tela (e um print/PDF de 1–2 páginas) com tudo que se lê antes de entrar na cidade.
  • Estratégia de cor: Restrained; dossiê é leitura, não dashboard — tipografia e hierarquia, zero heat.
  • Anti-goals: formulário novo para o assessor preencher (anti-goal E1 do relatório: extração sem devolução); duplicar os cards das tabs em vez de compô-los; virar relatório de 10 páginas.

Contexto

A sessão real de 2026-07-23 (CUSTOMER.md) trouxe O6 como o único pedido explícito de produto: dossiê de território pré-agenda com conjuntura local (prefeito aliado/oposição, disputa local "Jacu × Beija-Flor"), economia e emendas aportadas por município. O produto já tem quase tudo: série/captura (MunicipalityBaselineCard + artefato), pledges/estimativas (MunicipalityPledgesPanel, A10), lideranças (MunicipalityLeadershipsPanel), estratégia/conjuntura (MunicipalityStrategyCard: politicalTrend, strengths/risks, dobradinhaNotes, nextSteps, dobradinhas M4), sinais/atualizações (municipalityUpdate), demografia IBGE (A8 — bahiaMunicipalityDemographics.ts: população, faixas etárias). O que falta é a composição em visão única imprimível — e o único dado realmente novo é emendas (gap G11 do plano-mestre: manual-first; fonte estruturada Câmara/Transparência adiada com gatilho).

Objetivos

  • Tab/rota "Dossiê" no detalhe do município (staff-only): seções na ordem de leitura de campo — capa (nome, TI, prioridade, nível quando existir), conta local (série 2014/2018/2022, % da própria votação via A11, captura/cobertura quando E8 existir), rede (lideranças ativas + responsável + frescor), conjuntura (politicalTrend + forças/riscos + disputa local), dobradinhas (M4 + notas), agenda/planos recentes e sinais recentes (últimos municipalityUpdate), perfil (demografia A8), emendas (quando houver dado — v1 mostra a seção só se preenchida).
  • Visão print: print:-friendly (CSS print da mesma rota; sem lib de PDF) — 1–2 páginas A4, cabeçalho com data de geração ("dossiê de " — dado envelhece).
  • Emendas manual-first (G11): campo budgetNotes staff-only novo em municipality (migration pequena própria — decisão registrada na questão resolvida abaixo), editado no formulário de estratégia; a seção "Emendas aportadas" do dossiê só aparece quando preenchido.
  • Access: tudo staff (coordinator/advisor/candidate); leader não acessa (lockdown); campos staff-only seguem a redaction dos view models.

Decisões travadas

  • Dossiê compõe, não duplica. As seções renderizam os mesmos loaders/componentes das tabs (ou variantes compactas), nunca uma segunda fonte. Rejeitado: collection dossier ou snapshot persistido (o dossiê é leitura derivada; snapshot ex-ante é papel do allocationDecision/C12).
  • Print via CSS, não gerador de PDF. 1 rota, @media print, sem dependência. Rejeitado: lib de PDF server-side (peso e manutenção por um ganho nulo — o uso real é imprimir/compartilhar do navegador).
  • Emendas entram manual-first (G11). Fonte estruturada (Câmara/Portal da Transparência) só com gatilho (E16 em uso + pedido de dado sistemático). Rejeitado: import automático de emendas na v1 (pipeline novo por uma seção que começa vazia).
  • i18n e naming: rota/tab dossie (valor de URL em pt, como os demais), MunicipalityDossier*, municipalityDossierData.ts; labels pt-BR ("Dossiê", "Conjuntura", "Emendas aportadas").

Questões em aberto

  • Onde mora o dado de emendas na v1? Resolvida (2026-07-24): campo staff-only novo budgetNotes em municipality, com migration própria pequena (add_municipality_budget_notes). A opção "carona na migration de C12" caducou — C12 foi entregue com 20260724_180000_add_campaign_foundation_records antes de E16 começar. Rejeitada: convenção dentro de nextSteps (polui a semântica do campo e quebra a regra "seção Emendas só aparece se preenchida").
  • Dossiê mostra nível N0–N4 (E14)? Recomendação: sim quando existir, staff-only — mas nunca no print destinado a circulação além da coordenação (vocabulário duplo). v1: print omite nível por default.

Abordagem proposta

flowchart LR
    Data["municipalityDossierData.ts<br/>(compõe loaders existentes)"]
    Baseline["baseline/A11<br/>(série, rank, share)"]
    Rede["lideranças + frescor"]
    Conj["politicalTrend + forças/riscos<br/>+ dobradinhas + emendas (G11)"]
    Sinais["municipalityUpdate recentes"]
    Perfil["demografia A8"]
    View["tab Dossiê + @media print"]
    Baseline --> Data
    Rede --> Data
    Conj --> Data
    Sinais --> Data
    Perfil --> Data
    Data --> View

Componentes: src/utilities/municipalityDossierData.ts (novo — orquestra loaders existentes em uma passada), src/components/campaign/MunicipalityDossier.tsx (+ seções compactas reusando cards atuais), entrada na MunicipalityTabNav (dossie), CSS print na própria rota.

Dependências

  • Nenhuma dura — compõe o que existe (baseline, pledges, lideranças, estratégia, updates, A8). Melhora com: A11 (rank/share), E8 (captura/cobertura/meta na "conta local"), C12 (sinais tipados na seção de sinais; migration compartilhada para budgetNotes), E14 (nível), A6 (dobradinha 2026 real).
  • Alimenta: E13 (o compositor de giros linka o dossiê de cada município do giro — preparação de visita).

Não escopo

  • Import estruturado de emendas (G11 adiado com gatilho); dossiê por TI (leitura regional é E17/E12); snapshot histórico de dossiê (C12/E15 cobrem o ex-ante); dossiê para leader (lockdown).

Rabbit holes

  • Virar relatório infinito. 1–2 páginas A4; cada seção tem teto de itens (ex.: 5 sinais recentes, 8 lideranças) com link "ver tudo" na tab correspondente.
  • Economia local virar pesquisa. v1 usa o que A8 tem (demografia); "economia" além disso é nota de conjuntura manual — não construir base socioeconômica nova.
  • Print pixel-perfect. CSS print razoável basta; não é material de campanha, é documento de trabalho.

Já resolvido no simplify (não reabrir)

  • Dados mortos votePosition2022 / pledgeCoverage removidos de MunicipalityDossierData (rank já vem do baseline card).
  • Agenda reusa buildActionPlanListWhere + actionPlanListSelect + toActionPlanListViewModel + pagination: false.
  • loadFreshestMunicipalityLeaderships (sort -updatedAt + limit no Postgres) em vez de fetch-all-and-slice.
  • Conta da cadeira encadeada no Promise.all (pledges → goal), concorrente com baseline/rede/sinais/agenda.
  • Datas/números via formatBahiaDateTimeLabel / formatElectionNumber; Prettier em migrations/index.ts e fontes novas.

Explicitamente fora (triage pós-simplify 2026-07-25)

  • Memoização em module-scope de computeStatewideSuggestedGoals — pré-existente E8, fora do diff do dossiê (revisitar no plano conta-da-cadeira.md se o hot path do dashboard doer).
  • Encolher o page size do feed de sinais de 10 → 5 — o loader compartilhado precisa de totalDocs; micro-otimização sem evidência.
  • Critique do MunicipalityGoalAccountCard (tooltips touch) — sessão E8, já hardenido no follow-up #3 daquele plano; não é dívida do E16.

Adiado com gatilho

  • G11 fonte estruturada de emendas (Câmara/Portal da Transparência → valor por município/ano). Gatilho: E16 em uso real + coordenação pedindo o dado sistemático.
  • Compartilhamento do dossiê via WhatsApp (kit). Gatilho: pedido de campo; hoje o print resolve.
  • Helper compartilhado da proveniência da meta (usesMesaEstimate / rótulo "estimativa da mesa" vs "meta sugerida"). Hoje 2 call sites (MunicipalityGoalAccountCard + dossiê) abaixo do limiar de abstração do repo. Gatilho: 3º consumidor da mesma derivação/cópia.

Referências

  • docs/roadmap.md (E16) · plano-mestre (G11) · planejador-de-giros.md (E13 — consome)
  • docs/CUSTOMER.md — O6 (pedido explícito; Nazaré das Farinhas), restrição "perna" (dossiê barateia a preparação da agenda)
  • src/app/(campaign)/campanha/(app)/municipios/[slug]/page.tsx (tabs), src/components/campaign/MunicipalityBaselineCard.tsx, MunicipalityStrategyCard.tsx, MunicipalityLeadershipsPanel.tsx, MunicipalityPledgesPanel.tsx
  • src/lib/bahiaMunicipalityDemographics.ts (A8), src/lib/bahiaElectionAggregates.ts, ranking-votos-municipio.md (A11)
  • PRODUCT.md/DESIGN.md — clareza sob pressão; print sóbrio; AGENTS.md — access, naming