Commit graph

53 commits

Author SHA1 Message Date
Cursor Agent
1ed65d0d81
B92 — wizard idle suggest: rank + loader + UI
Add wizard-municipality-suggest mode to home-search with cold-first
ranking (signal → E14 → trend → E10 class → 2022 votes). Wizard
municipality step now fetches suggestions when query is idle.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 07:01:19 +00:00
Cursor Agent
5f5b513336
style: prettier 2026-08-01 06:09:33 +00:00
Cursor Agent
104d6d4589
fix(RS+): depth:0 sem select parcial; form schema de convite separado
Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 06:08:53 +00:00
Cursor Agent
7484aab28a
RS+ Fase 1+3: auth read leve, password fields e login state
- authenticateCampaignToken usa depth:0 + select mínimo (sem join media)
- getCampaignUserWithAvatar para layout/perfil; hasCampaignSession em rotas públicas
- CampaignPasswordFields reutilizado em reset, perfil e convite
- LoginForm migra para CampaignFormActionState + mapCampaignFormActionError
- invite login schema unifica refineMatchingPasswords

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 06:07:28 +00:00
Francisco Ignácio Fontoura Solla
dbd9d0fa3e feat(campanha): ship B70 leadership update wizard (grid + short form).
Replaces the post-municipality stub with a full wizard: tile grid, contact+leadership writes, entryAction skip, and Continuar after save.
2026-07-30 03:51:25 -03:00
Francisco Ignácio Fontoura Solla
37a7062428 B69: simplify the leadership model to exclusive + drop sector/consentNote.
Organization stays by product decision (political branching, e.g. SindMed);
the leadership gains `exclusive` (default true) with list badge and form
checkbox, and sector/sectorNotes/consentNote leave the collection, zod,
forms, list filters/sort, invite autofill and redemption.
2026-07-30 02:32:56 -03:00
Francisco Ignácio Fontoura Solla
120ed8d9b5 B62: simplify the signal model to type + text (drop fonte/triangulado).
Drops signalSource/triangulated from municipalityUpdate (migration
simplify_municipality_signal_fields), rewrites signal-type labels for the
field team, and treats any adversary signal as confirmed in E11 (P1
triage 1). Also fixes the stale post-B43 leader-home e2e and lowers a
drifted bypass pin.
2026-07-29 23:24:00 -03:00
Francisco Ignácio Fontoura Solla
f6b73b3e63 B60: wizard municipality search step with shared staff loader.
Staff wizards pick a municipality via word-start search, auto-advance with ?municipio=, and reuse searchStaffMunicipalityHits extracted from B48.
2026-07-29 23:10:06 -03:00
Francisco Ignácio Fontoura Solla
e733efbe42 B68: curated municipality suggestions on empty home search focus.
Staff see role-ranked shortcuts (advisor portfolio by signal age; CG/candidate by alta priority deficit) via POST mode suggest, without treating empty query as search.
2026-07-29 22:06:02 -03:00
Francisco Ignácio Fontoura Solla
05072547cd B48: home search results for municipalities and identity territories.
Staff Início posts to /campanha/home-search with scoped word-start matches, shared readout component, and simplify hardening (messages, fetch busy, malformed response guard).
2026-07-29 16:05:56 -03:00
Francisco Ignácio Fontoura Solla
4806c417c8 Engineering Pass 3: consolidate patterns and lock them with guards.
Fold duplicated campanha access, loaders, list/filter/form shells, and scripts into shared modules; reorganize utilities by domain; stabilize e2e; and document prevention-first audit workflow so the same smells cannot creep back in.
2026-07-29 06:11:29 -03:00
Francisco Ignácio Fontoura Solla
8cae4adae9 E11 post-rebase simplify: clamp the composed rationale, extract firstFormActionMessage at its 3rd call site, keep metrics server-side. 2026-07-28 14:03:29 -03:00
Francisco Ignácio Fontoura Solla
189ad7a349 E11: suggestion engine v1 — the human-in-the-loop decision queue. 2026-07-28 14:03:29 -03:00
Francisco Ignácio Fontoura Solla
4d56a32ed2 Adopt B32+'s JSON route shell and fetch helper in the WebAuthn paths.
The rebase brought a convention guard that refuses a route.ts exporting POST
outside campaignJsonMutationRoute, and it named all four WebAuthn routes. They
now come from the shell, so the same-origin check is structural rather than
copied, and postJson delegates to postCampaignJson — keeping only what the
ceremonies add on top: the envelope's discriminant decides instead of the
status, and a non-JSON response reads as one sentence while a network failure
still propagates untouched. The two *-options routes have no body, hence
campaignWebAuthnNoBodySchema.
2026-07-28 11:33:22 -03:00
Francisco Ignácio Fontoura Solla
763f64080a Deliver B40: biometric login for /campanha.
WebAuthn platform passkeys: opt-in enrollment after a password login, a
password-less sign-in that mints the same 14-day campaign-token, and
revocation from the profile. The session is minted through Payload's public
pieces (checkLoginPermission, addSessionToUser, getFieldsToSign, jwtSign)
inside a transaction with an advisory lock, so a passkey login neither
bypasses a lockout nor evicts the account's other sessions. The rpID is
derived from NEXT_PUBLIC_SITE_URL and resolves to null when the request host
disagrees, so previews never offer a ceremony the browser would refuse.
2026-07-28 10:59:23 -03:00
Francisco Ignácio Fontoura Solla
0a63621d4b B37 simplify: one membership delta, cap messages named once.
nextStateDeputyIdsAfterMunicipalityMembership was the third byte-for-byte copy
of the same single-id delta — nextAdvisorIdsAfterMembership and
nextStateDeputyIdsAfterMembership differ from it only in which cap they read —
so the algorithm now lives once in nextIdsAfterMembership with the cap as data,
and the three named wrappers just bind their own. The three cloned spec files
became one describe.each suite: coverage went up rather than down, since every
relation now gets the "can go to zero" and no-mutation cases only B37's clone
had, plus an explicit assertion that each wrapper refuses at ITS cap with ITS
message.

That pairing matters because mapCampaignFormActionError matches thrown messages
against safeMessages by exact string, and two of those cap messages were bare
literals in three src places each (thrown, plus two independent allowlists):
rewording any one would have silently collapsed a real refusal into "tente
novamente". Both are constants now, next to their caps, following the
MUNICIPALITY_STATE_DEPUTIES_CAP_MESSAGE precedent already in those same files.
The dobradinha staff message had the same exposure in four places — and
actions/stateDeputy.ts already imported the constant while restating it as a
literal one line below — so it is STATE_DEPUTY_STAFF_MESSAGE (renamed: it
guards create/update too, not just the batch), and the conflict message it
shares with the create route is named too.

The batch action stopped resolving the dobradinha slug before its loop: the
slug exists only to target the revalidate, which the caller already skips when
nothing changed, so on a no-op batch that read was fetched and thrown away —
the guard setLeadershipMunicipalitiesMembershipRecord already had. Its
redundant second dedup is gone (the schema's transform already deduped; the
sort stays, for deadlock-free lock ordering), and the bare 435 shared with the
advisor batch is MAX_MUNICIPALITIES_PER_BATCH.

Two memoization gaps closed, both restoring invariants the codebase had already
established elsewhere. An advisor's scoped .filter() returned a fresh array per
row, defeating the WeakMap that exists precisely to share derivations across
rows, so scopedPortfolioIndex memoizes on (index, addableIds) and every row
gets one array. And LeadershipStateDeputyRelationCell called matchesAtWordStart
per candidate, paying an NFD pass plus three Unicode regexes on both sides per
keystroke over the unpaginated leadership catalog — the cost wordStartFilter's
own doc comment warns about; it now normalizes the query once against labels
cached on options, like municipalityPortfolio.

Rejected with reason: maxItems on the dobradinhas cell. A dobradinha has no
product ceiling on municípios (that is why the bound is the catalog), so a
"generous 30" would invent a rule that refuses a legitimate statewide
dobradinha, and the honest 435 is unreachable when ids are unique.

Also dropped the dead addPlaceholder knob no caller ever set.
2026-07-28 10:25:47 -03:00
Francisco Ignácio Fontoura Solla
da4ca2c713 Deliver B37: editable município chips on the dobradinhas list.
The "Municípios" column of /campanha/dobradinhas reuses
MunicipalityPortfolioCell unchanged, writing the inverted side of the
relation (municipality.stateDeputies) through a single transactional
batch action (setStateDeputyMunicipalitiesBatch): per-município advisory
lock, all-or-nothing on a partial-scope batch instead of a silent partial
apply, no floor (a dobradinha can hold zero municípios). stateDeputiesArraySchema's
cap moved 20 → 435 to fit a whole território batch from the município side.

The post-implementation critique found two P1s, and both were fixed instead
of deferred. First: MunicipalityPortfolioCell and LeadershipStateDeputyRelationCell
had grown the same chip-editing machine twice — optimistic delta, undo, ARIA
1.2 combobox, floor/ceiling, pointer-fine/coarse split — with incompatible
contracts. This was the 3rd call site B34+ was waiting for, so RelationChipCell
is now the single engine and both cells shrank to thin domain wrappers (chip/
search building, FormData, copy — nothing interaction-specific left in either).
LeadershipStateDeputyRelationCell also gained an ownerName prop and its first
dedicated test suite, which it shipped without before this.

Second: an advisor could see, and attempt to remove, a município chip outside
the portfolio they administer, failing silently at the document level.
MunicipalityPortfolioCell now filters the visible chips by addableIds instead
of only gating what can be added, so an advisor sees and can only touch
municípios they administer.

No migration, collection, or Consent. Docs updated: plan as-built, roadmap,
AGENTS.md, project notebook.
2026-07-28 09:34:39 -03:00
Francisco Ignácio Fontoura Solla
3536eb02d3 Deliver B29: header sort and filters on the leaderships list.
Bring B15/B16-style URL sort/filter to /campanha/liderancas on the B33 mold, with shared CampaignTableHead filter slot and B18 exhaustive enum canonicalize from /simplify.
2026-07-28 01:43:44 -03:00
Francisco Ignácio Fontoura Solla
812e3a0bee B34: editable município chips on the leaderships list.
The "Municípios" column was dead text; it is now chips that navigate to the
município, with a territory chip when a leadership covers a whole TI, and it is
editable in place. What picks inline vs. Drawer is the `pointer:` media query,
not viewport width: on a fine pointer the editor is always mounted (the "×" sits
at the chip's corner, revealed on hover/focus) and on a coarse pointer a tap
anywhere in the cell opens a Drawer with 44 px targets.

`AdvisorMunicipalityCell` is promoted to `shared/MunicipalityPortfolioCell` and
/campanha/assessores migrates onto it in the same change, so the second call site
is born with the first. One batch action only; the single-id advisor twin is
deleted with its int tests migrated. The anti-goal on always-mounted list inputs
is amended for chip-based relations on fine pointers, not reopened.

Also fixes a latent bug: `CampaignTableHead`'s `description` seam broke any RSC
caller, because its two helpers lived in a 'use client' module.
2026-07-27 22:35:19 -03:00
Francisco Ignácio Fontoura Solla
c7001b75f0 E14: engagement levels N0-N4 and the first allocationDecision writer.
`municipality.engagementLevel` (staff-only, no default) records what the
campaign decided to invest in a município, next to the E8/E10 metrics that
describe what it knows. Moving it is a reallocation decision, so only
unrestricted staff may, every move carries a rationale plus the signals that
would reverse it, and the write files an `allocationDecision` in the same
transaction under an advisory lock taken before the current level is read —
otherwise the snapshot's `from` is a guess. The enum gains `movimento`: a level
move is neither `aceita` nor `descarta`.

No backfill. Null is "Sem nível", and filtering by it is the triage queue.
That is also why the list sorts `nivel` in memory rather than in the query:
Postgres puts NULLs first on DESC, so a native sort would open "N4 primeiro"
on a page of municípios nobody has decided about yet. The filter stays native.

The hysteresis rules from research §6.8 are pure and client-safe, so the
Popover explains a block before submitting and the server re-runs them anyway;
a blocked move answers 409 with typed violations and the override is recorded
with exactly the rules it contradicted.
2026-07-27 21:19:33 -03:00
Francisco Ignácio Fontoura Solla
d5b4af2d98 E13: visit eligibility checklist and the tour composer.
Five conditions per município (volume, headroom, rede, janela, encaixe) with
their reason always visible, plus a staff-only composer at
/campanha/atividades/giros that turns an âncora + satélites + semente into draft
activities in one transaction. No `visita` kind and no `tour` entity:
`deputyPresent` already marks the candidate's own agenda.
2026-07-27 21:04:04 -03:00
Francisco Ignácio Fontoura Solla
3892c2e0a4 Add remembered campaign sessions
Keep campaign JWT and cookie lifetimes aligned while revoking the active server session on logout.
2026-07-27 08:57:26 -03:00
Francisco Ignácio Fontoura Solla
ef3d5b6c22 B31: coluna de dobradinhas na lista de lideranças
Coluna "Dobradinhas" em /campanha/liderancas com chips clicáveis
para /campanha/dobradinhas/<slug> e edição via Popover+Command
(padrão B27, escrita via server action delta + lock, como B19).

- LeadershipRowViewModel.stateDeputies (StateDeputySummary[]) via
  loadStateDeputySummaries, substituindo stateDeputyNames.
- setLeadershipStateDeputyMembership(Record): delta com
  withPayloadTransaction + acquireTextAdvisoryLocks + revalidatePath.
- nextStateDeputyIdsAfterMembership (lib/leadershipStateDeputyMembership.ts),
  espelha nextAdvisorIdsAfterMembership.
- loadStateDeputyOptions estendido com plainName/party/slug.
- Catálogo entregue uma vez por tabela via leadershipColumns(options)
  (columns factory, mesmo precedente de municipalityListColumns).

/simplify (1ª rodada): corrige revert otimista por delta (não pelo
snapshot inteiro), remove fallback silencioso na adição, extrai
sameIdSet compartilhado com MunicipalityListAdvisorsControl (B27), e
evita leitura+revalidação supérfluas no caminho no-op da action.

/simplify (2ª rodada, pós-rebase): troca o StateDeputyOptionsProvider
(Context) inicial pela columns factory acima — a 1ª rodada tinha
validado o Context por falta da evidência de que MunicipalityList.tsx
já resolve o mesmo problema sem Context; corrige revalidatePath para
o id concreto em vez do template dinâmico '/campanha/liderancas/[id]'
(que invalidava toda ficha a cada toggle); remove non-null assertion
movendo a guarda para dentro do setCurrent updater.

Gate verde: tsc, lint --max-warnings=0, format:check, unit 530/530,
int 406/406 (incl. os 6 de campaignLeadershipStateDeputyMembership),
build. Aikido: 0 achados nos arquivos novos/editados.
2026-07-26 19:50:15 -03:00
Francisco Ignácio Fontoura Solla
7c50e29aa7 B27: combobox + chips + auto-save no popover de Assessores da lista de municípios
Replaces the checkbox-list + "Salvar assessores" button in the
Assessors popover (/campanha/municipios) with removable chips + a
Command combobox, delta-based auto-save (one advisor add/remove per
request, never the whole array), and optimistic UI with request
sequencing to resolve out-of-order responses correctly.

- New POST /campanha/municipios/advisors JSON route (mirrors
  expected-votes/), backed by setMunicipalityAdvisorMembership(Record)
  in actions/municipality.ts (text advisory lock + transaction).
- nextAdvisorIdsAfterMembership moved to
  src/lib/municipalityAdvisorMembership.ts with unit tests.
- Shared isSameOriginRequest (src/utilities/sameOriginRequest.ts) and
  uniqueRelationshipIds (src/utilities/relationship.ts) helpers,
  de-duplicating the same-origin CSRF check and the
  Set+map(relationshipId)+filter pattern across advisor call sites.
- MunicipalityListAdvisorsControl.tsx rewritten: chips, Command
  combobox, per-item pending affordance, rollback on error, and a
  request-sequence guard so only the most recently sent delta's
  response is adopted when concurrent optimistic updates settle.
- e2e coverage for the combobox auto-save flow; setup prewarms the new
  POST-only route to avoid Playwright dev-server HMR aborting the
  first request.
- Plan and roadmap marked as delivered (B27).
2026-07-26 03:31:51 -03:00
Francisco Ignácio Fontoura Solla
7b93c232ff C13: renomeia "Plano de Acao" para "Atividade" em toda a vertical /campanha
Collection actionPlan -> activity (labels "Atividade"/"Atividades"), campo
campaignDemand.actionPlan -> activity, rota /campanha/planos ->
/campanha/atividades (criacao em atividades/nova), sidebar "Atividades" e
rotulos alinhados a entidade ("Tipo de atividade", "Origem da atividade",
"Resultado da atividade"). Valores de enum kind/status/origin inalterados.

Schema movido por migration data-preserving escrita a mao
(20260725_213000_rename_action_plan_to_activity): 4 tabelas, 3 enums, 2
colunas, 39 indices (4 pkeys inclusos), 14 FKs e 2 sequences, cada statement
guardado na existencia do objeto de origem, com down() simetrico. O gerador do
Payload emitiria DROP+CREATE e nao ve o indice parcial escrito a mao
action_plan_upcoming_start_at_idx, entao a lista veio de
pg_indexes/pg_constraint/pg_type/pg_sequences e o snapshot .json foi derivado
do anterior por transformacao textual -- validado por migrate:create __probe
sem mudanca de schema. A mesma migration alinha os 8 indices fosseis %plaza%
que a remodelagem de municipios deixou (renomeou as colunas plaza_id, nao os
indices).

Guard: o describe "banned campaign terminology" de
codebaseConventions.unit.spec.ts virou tabela de termos varrendo src+tests+
scripts (migrations fora -- historia congelada), banindo os compostos
actionPlan, a copy "plano de acao" e a rota antiga, mas nao plan/plano isolado
(colidiria com os planos de assinatura da Fase 2). Allowlist: o proprio spec,
scripts/generate-remodel-municipalities-migration.mjs e o comentario historico
em campaignMigrationReconciliation.int.spec.ts.

Plano: docs/plans/renomear-plano-acao-para-atividade.md
2026-07-25 20:50:38 -03:00
Francisco Ignácio Fontoura Solla
228eb36e90 B26: registrar sinal tipado a partir da célula Último sinal na lista de municípios.
Fecha o laço da fila E9 — Popover/Drawer cria municipalityUpdate kind sinal sem sair da varredura; submit explícito multi-campo; revalidate da lista.
2026-07-25 20:42:01 -03:00
Francisco Ignácio Fontoura Solla
e6f7c2e70f Tooling: prettier como autoridade de formatacao (organize-imports deterministico, scripts format/format:check, normalizacao mecanica de 312 arquivos; artefatos gerados e migrations excluidos via .prettierignore) 2026-07-25 17:22:20 -03:00
Francisco Ignácio Fontoura Solla
c01467547d Pass 2 W4b: zero exports mortos; knip exports/types/enumMembers em ERROR no CI
- ~190 findings zerados: knip --fix (grafo corrigido: tests como entries, migrations como consumidores) + delecao guardada dos orfaos (git grep -w antes de deletar) + reparos manuais dos corpos arrow pendurados
- pecas nao usadas do kit ui deletadas (PaginationLink/Next/Previous, familias Sidebar/Combobox/Sheet/Drawer/Dialog etc.) - shadcn add recoloca se precisar
- acoes mortas removidas (updateCampaignDemandDetails*, *Record orfaos), schemas/tipos mortos (campaignDemandDetailsUpdateSchema, Contact do zod), helpers mortos
- tipos de eleicao derivam direto das *_OPTIONS (consts intermediarias eliminadas)
- knip.json: rules exports/types/enumMembers=error, duplicates=off (unicos hits: 6 aliases deliberados canUpdateX=canReadX); hints aplicados (payload.config entry redundante, ignores de tsx/sharp/playwright/payload-types desnecessarios)
- ledger: linha P2 dos unused exports fechada
2026-07-25 15:51:52 -03:00
Francisco Ignácio Fontoura Solla
e357c3cc19 Pass 2 W4f: varredura de copy Praca -> Municipio
- ~70 strings visiveis + labels de admin (headers de coluna, APIErrors, empty states, dashboard, mapa, formularios, filtros) com concordancia de genero corrigida
- apoiadores desambiguado: cidade do contato vira 'Cidade'; a unidade operacional assume 'Municipio' (filtro, coluna, ficha)
- comentarios de codigo atualizados; payload-types regenerado
- fora por decisao D6: textos provisorios de consent do Onda 0 (lote juridico) e migrations congeladas
- e2e do plano de acao acompanha o label ('Municipio *')
2026-07-25 15:12:07 -03:00
Francisco Ignácio Fontoura Solla
cf6ae6c47c Pass 2 W2: correcoes de fronteira lib/utilities e client boundary
- helpers puros movidos utilities->lib: slug, phone, wordStartFilter, voteEstimate (9 inversoes lib->utilities zeradas)
- chaves de consent em src/lib/campaignConsentKeys.ts (client-safe); campaignInvite/campaignConsent re-exportam; onda0ConsentTexts referencia as constantes
- 'server-only' em 21 loaders Payload/cache (CLI segue com --conditions=react-server)
- ciclo de tipos supporterListFilters <-> supporterUi quebrado (SupporterListFilterInput estrutural; sqlFilters idem)
- votePledgeData dividido: votePledgeViews (puro, client-safe) + votePledgeData (loaders, server-only) - padrao contract-module
- predicados de role client-safe em src/lib/campaignRoles.ts; access/shared deriva deles (fonte unica); nav.ts para de importar o barrel campaignAccess (casts 'as CampaignUser' eliminados)
2026-07-25 14:38:41 -03:00
Francisco Ignácio Fontoura Solla
01e52834e2 B19: Gerenciar assessores (/campanha/assessores)
Lista+detalhe única de edição para coordinator/candidate: nome, e-mail e
celular com auto-save debounced (edit-where-you-see-it); carteira de
municípios por chips com agregação de território de identidade,
adição/remoção com UI otimista; criação inline no topo da tabela; ação de
WhatsApp por assessor; reset de senha via e-mail (bloqueado para
placeholders @planilha.invalid). Alinha access de update/phone/atribuição
de município a isCampaignUnrestricted (coordinator + candidate).

Sem migration. Testes int (campaignAdvisorManagement) + unit (schema e
portfólio de municípios) cobrindo access, conflito de e-mail único, cap de
assessores por município e agregação por território/ZE.
2026-07-25 02:40:47 -03:00
Francisco Ignácio Fontoura Solla
2682078fe4 E16: Dossiê do município (pré-agenda) with print view and budgetNotes.
Composes existing loaders into a staff-only pre-visit tab, adds G11 manual emendas notes, and unlocks CSS print for 1–2 A4 pages.
2026-07-25 01:28:03 -03:00
Francisco Ignácio Fontoura Solla
63910014ad Implement C12 campaign foundation records
Preserve pledge history, type field signals, link plan demands, and record immutable ex-ante allocation decisions.
2026-07-24 19:27:15 -03:00
Francisco Ignácio Fontoura Solla
c07969f4a7 Remove duplicated voteGoals; seed projection sheet into expectedVotes.
Meta Bom/Regular/Mínimo duplicated the estimate scenarios — keep a single municipality series (pessimista/média/otimista), backfill then drop the old columns, and map the spreadsheet onto expectedVotes.
2026-07-24 14:04:44 -03:00
Francisco Ignácio Fontoura Solla
f23c2582ee Break up the god modules into deep domain modules
- campaignAccess.ts (990 lines, ~80 exports) is now a 130-line explicit
  re-export surface over 14 domain modules in src/utilities/access/
  (shared actor predicates + per-collection access); public surface
  AST-verified identical, import sites untouched
- actions/supporter.ts split: single-supporter lifecycle (407) vs the CSV
  import pipeline in supporterImport.ts (357); 'use server' re-export
  constraint honored with thin async delegates
- organization/stateDeputy clone twins deduped by POLICY:
  runStaffEntityMutation (fresh staff assert + unique-conflict translation)
  keeps each caller's concretely-typed Payload mutation
- runCampaignRedirectFormAction removes the copy-pasted try/map/redirect
  ladder from 4 create formActions (planos kept its genuinely custom flow)
- Plaza→Municipality identifier rename finished (context keys, list-label
  and leadership constants, fixture sequence, test constants)
- RefreshRouteOnSave (React component) moved out of utilities/ to components/
2026-07-23 23:44:45 -03:00
Francisco Ignácio Fontoura Solla
e2724c10da Remodel the campaign vertical from Praça to Município
Rename the operational unit "Praça"/"Núcleo" to "Município" (code identifier
`municipality`) and land the 2026-07-23 course changes:

- New `candidate` role with unrestricted visibility (`isCampaignStaff` now
  includes it) alongside coordinator/advisor; leader is locked down to the
  supporter contact tool only.
- Demandas become staff-only; dobradinhas modeled via a `stateDeputy` entity.
- Camaçari consolidated to a single município (ZE 170/171 aggregated);
  Salvador keeps its 19 zone municípios.
- Map moves to the staff dashboard; `/campanha/municipios` is list/overview.
- Vote pledges keep the declared vs estimated (scenario) asymmetry; leaders
  never see estimates.

Destructive migration for the campaign vertical (drops nucleus/plaza-era
tables, wipes campaign rows, seeds 435 municípios). Public-site data untouched.

Includes the post-delivery /simplify cleanups: shared reloadStaffActor /
reloadCoordinatorActor helpers, req.context cache for getFreshCampaignUser,
parallelized dashboard loads, candidate dashboard-copy fix, and dead
leader-dashboard / roleKind code removal.
2026-07-23 21:36:20 -03:00
Francisco Ignácio Fontoura Solla
aa12e173d8 Ship A10 vote estimate scenarios (pessimistic/central/optimistic) for Praças.
Staff surfaces share one Cenário control; leaders keep a single declaredVotes value. Unlocks E8.
2026-07-23 19:12:22 -03:00
Francisco Ignácio Fontoura Solla
0dcef662e7 Deliver A9+ shared Praças list loader and scoped revalidation.
Deduplicate pledge aggregation via loadPlazaListPageBundle, add plazaRevalidation with slug-scoped detail paths after inline saves, and document the fill-in as shipped.
2026-07-21 13:22:12 -03:00
Francisco Ignácio Fontoura Solla
b333609726 Deliver A9 plaza expectedVotes and register A9+ follow-up.
Add staff-only expectedVotes with migration, edit flow, and expectedVotes ?? pledge fallback across map, overview, and dashboard; document delivery and the A9+ loader dedup fill-in on the roadmap.
2026-07-21 11:02:11 -03:00
Francisco Ignácio Fontoura Solla
cf13c441aa Remodel /campanha around predefined Praças replacing electoral nuclei.
436 seeded Praças (municipality-based; per-TSE-zone in Salvador/Camaçari) become
the operational unit; roles renamed to coordinator/advisor/leader with advisors
scoped to administered Praças. Leadership is one record per person across many
Praças/organizations; vote pledges split leader-declared vs staff-estimated
(estimates never serialized to leaders). Adds organizations, demand workflow
with staff-only cost/receipts, action-plan results with deputy presence, manual
political trend, multi-candidate comparison and the diverging red/white/blue
map mode. Migration 20260721_020109_remodel_plazas deliberately resets the
campaign vertical in production (public site untouched); test suite ported to
the new model (unit 187 / int 306 / e2e 9 green).
2026-07-21 02:28:00 -03:00
Francisco Ignácio Fontoura Solla
3b463173da Add campaign password reset, profile photo, and RS+ follow-up plan.
Deliver self-service forgot/reset via Resend, logged-in password change, and optional avatar on /campanha/perfil; register post-simplify engineering debts as RS+.
2026-07-19 05:26:56 -03:00
Francisco Ignácio Fontoura Solla
e5e051d175 Deliver E1 vote goals and E3 strategy fields on nuclei.
Wire schema, UI, aggregates, and tests for scenario goals, priority filter, staff-only dobradinha notes and next steps; register E6 scale follow-ups from simplify.
2026-07-19 04:38:07 -03:00
Francisco Ignácio Fontoura Solla
9fdbae05d7 Add C6 scale/DRY for supporters: bulk import, KPI aggregate, shared shells.
Phase 1 UX: StrictCombobox municipality in SupporterForm, AlertDialog in
RemoveSupporterDataButton. Phase 2 DRY: campaignListUrl + CampaignListPagination
+ campaignFormFields + mapCampaignFormActionError shared across nuclei,
supporters and action plans. Phase 3: single SQL aggregate for supporter list
KPIs via drizzle in the Payload txn. Phase 4: bulk drizzle insert for CSV import
inside the existing Payload txn with ON CONFLICT DO NOTHING, plus a fail-closed
skipContactPhoneInvariant context flag. Phase 5: HMAC-signed single-use import
token backed by a new supporterImportBatch staging collection so the full ok-row
set never crosses the Server Action boundary twice.
2026-07-18 22:17:31 -03:00
Francisco Ignácio Fontoura Solla
e995aefbd4 Add action-plan agenda vertical for /campanha/planos.
Ship C3 mobilization events with the actionPlan collection, list/detail/forms UI, upcoming-event blocks, access control, and migration.
2026-07-18 20:56:20 -03:00
Francisco Ignácio Fontoura Solla
a18e2f15f5 Simplify C2 supporter paths and register C6 scale follow-ups.
Tighten import/create view models after review, and capture the deferred DRY/import-scale work as roadmap item C6.
2026-07-18 20:33:50 -03:00
Francisco Ignácio Fontoura Solla
b079daa9bb Add C2 nominal supporter registry for /campanha.
Ship the supporter join collection, consent-keyed create/import/remove actions, and apoiadores UI so staff can build a phone-required nominal base before mass field ops.
2026-07-18 20:32:54 -03:00
Francisco Ignácio Fontoura Solla
ac7f2775cd Add opt-in TSE zone and territory suggestion chips for nuclei.
Ship A2 with a static Bahia municipality↔zone map, pure suggestion engine, and form chips that never auto-apply geography.
2026-07-18 20:04:04 -03:00
Francisco Ignácio Fontoura Solla
b035cc4a2f Support multi-municipality and multi-neighborhood nucleus territory.
Replace scalar region/city/neighborhood with hasMany arrays so nuclei can span multiple Bahia municipalities while keeping neighborhoods scoped to a single city.
2026-07-18 18:31:51 -03:00
Francisco Ignácio Fontoura Solla
bcac665cb5 Ship /campanha nuclei MVP with RBAC, invites, and vote estimates.
Adds electoral nuclei, leaderships, updates, and WhatsApp invite flows behind campaign auth, plus the consolidated schema migration that Vercel applies on deploy.
2026-07-18 15:01:56 -03:00
Francisco Ignácio Fontoura Solla
c4866fabc0 Add /campanha internal area with isolated Payload auth
Introduce the internal campaign-management area gated by its own
authentication, kept isolated from the Payload admin so a campaign
session and an admin session can coexist in one browser.

- Add separate `campaignUser` auth collection (not `users` + roles)
- Authenticate via an isolated httpOnly `campaign-token` cookie scoped
  to /campanha, distinct from the default `payload-token`
- Add loginCampaign/logoutCampaign server actions and getCampaignUser
  session verification helper
- Add public login route and a protected (app) route group whose
  layout enforces the auth barrier
- Add migration 20260716_010420_add_campaign_user and document the
  auth foundation in AGENTS.md
2026-07-15 22:33:26 -03:00