Commit graph

76 commits

Author SHA1 Message Date
Francisco Solla
d7acb5aa48 feat(C129): nome de legenda da dobradinha — campo ballotName + display discreto sob o nome (Closes #696) 2026-08-12 12:52:35 -03:00
Francisco Solla
2f247d40a6 fix(C115): revisão /simplify — relógio com baseline de espelho, reopen sem re-cancelamento, hardening do webhook
- lastMirroredChangeAt (activity, migration): clock rule compara contra a
  última mudança espelhada, não updatedAt (task toggle não silencia edição
  pendente do Google)
- snapshot exclui ids que o motor deletou (removedEventIds separado do
  remoteEventIds): reopen pós-cancelamento não vira re-cancelamento fantasma
- cancelamento do Google respeita o relógio: cancel mais novo cancela; Teqo
  mais novo (reopen) limpa o trashed e recria
- escrita reversa sem req.user: autoria do registro correta + sem gate 403
  do deputado; hook de sync pula a própria escrita reversa (sem passada aninhada)
- canal: segredo rotaciona por criação; novo canal persistido antes do stop
  do antigo; identity fields read:false; create admin-only; leitura prefere
  linha configurada; pre-filter do webhook antes do bootstrap do Payload
- testes: all-day ida-e-volta, task-toggle, reopen, slug congelado, e2e dos
  caminhos 200, render tests do dialog/feed; asserções herméticas a
  atividades ambiente dos arquivos irmãos (DB compartilhado)
2026-08-11 22:42:05 -03:00
Francisco Solla
7629a505e0 feat(C115): edição pelo Google Calendar volta para o Teqo (motor bidirecional + webhook push)
- runSyncPass bidirecional: regra do relógio decide direção (Google mais novo
  aplica título/horário; Teqo re-asserta), cancelamento via showDeleted +
  snapshot lastSeenEventIds, registro no updates feed
- webhook público /campanha/agenda/google-webhook/[secret] fail-closed
  (secret + X-Goog headers constant-time), canal lazy sem cron (ensure em
  toda passada, renovação com id novo + stop do antigo)
- migration hand-written (campos de canal/snapshot em googleCalendarSync);
  mutationKind dedicado libera título via Google sem quebrar slug canônico
- UI: seção 'Edições pelo Google' no dialog + autor 'Google Calendar' no feed
- testes: unit (reverse mapping, validação webhook), int (motor bidirecional
  com stub, 10 cenários), e2e (webhook 404 fail-closed)
2026-08-11 22:38:53 -03:00
Francisco Solla
94c89a57a1 feat(C124): tarefa de atividade só com texto, responsável e concluída (sem prazo) 2026-08-11 16:05:07 -03:00
Francisco Solla
5fa4e22c9d feat(C119): status de apoio 'Lembrança' em lideranças — enum, badge e exclusão de pendências 2026-08-11 14:52:49 -03:00
Francisco Solla
70070db8dc feat(C114): agenda da campanha espelhada no Google Calendar com notificação (push Teqo → Google)
Espelho oficial da agenda em calendário Google compartilhado via API
(service account, jose + fetch): reconciliação por passada com id
determinístico teqo+base32hex (upsert idempotente, sem estado por evento),
hooks afterChange/afterDelete na activity (só campos espelhados, nunca
lança), auto-resync quando calendarId muda, retry na view da agenda,
pill de estado no header + FAB + diálogo (4 estados). Cancelado no Teqo
deleta o evento no Google. Sem participantes no v1 (decisão de produto).
Sem migration nova de tabela (hand-written). Closes #635
2026-08-11 13:53:51 -03:00
Francisco Solla
6de8fad8f6 feat(C112): múltiplos telefones por pessoa — Contact.phones array (ordem = prioridade)
- Contact.phone (text único) → phones (array field Payload, subcampo value):
  fonte única; principal = phones[0]; beforeValidate normaliza, filtra vazios,
  rejeita inválido e duplicado DENTRO da ficha (C111 entre fichas preservado)
- migration add_contact_phones_array: backfill phone → phones[0] (gen_random_uuid)
  antes do drop da coluna + índice em value (hand-edit documentado; snapshot .json
  gerado mantém as colunas C99/C100 já existentes)
- primaryPhoneOf / phoneValuesOf / reorderWithPrimaryPhone em src/lib/phone.ts
  (ordem = prioridade; inline edit = seta o principal preservando o resto)
- findOrCreateContactByPhone aceita phones[] (dedupe pelo primário; reuse faz
  append dos faltantes; locks em todos os números digitados)
- sync conta→ficha (CampaignUser): phone da conta faz APPEND quando a ficha não
  tem (nunca reordena nem remove); tombstone LGPD substitui TODOS os telefones
- convite redemption: telefone digitado vira principal, demais preservados
- import CSV: preview sinaliza compartilhado também por número secundário;
  bulk insere contact_phones (JOIN nas leituras, uuid no insert)
- busca por telefone (apoiadores, pickers, combobox) casa qualquer número
- UI: PhonesFieldEditor compartilhado (repetível mascarado, remover, ↑/↓,
  modo inline com Salvar) nas 5 superfícies — liderança (form/ficha/lista),
  apoiador (form/ficha), dobradinha (ficha), assessor (ficha), wizard
- UI pública (petição/WhatsApp) intocada: zod e forms ficam com phone;
  as ações mapeiam phone → phones[0]
- testes: spec novo campaignMultiPhones (aceite completo), specs C111/C99
  atualizados, e2e wizard/apoiador ajustados
2026-08-11 10:47:02 -03:00
Francisco Solla
162a518416 feat(C111): telefone não único — duas pessoas podem compartilhar o mesmo celular
Contact.phone deixa de ser único em enforcement (nunca houve constraint DB):
- remove o hook enforceUniqueContactPhone e os asserts assertContactPhone*
  (conflicts/ambiguity) — os 5 fluxos de escrita de pessoa salvam com número
  compartilhado; rename contactPhoneInvariant.ts -> contactPhoneLocks.ts
  (locks passam a significar serialização, não unicidade)
- findOrCreateContactByPhone: 0 fichas cria, 1 reusa (dedupe C6/C99),
  2+ cria ficha nova (a identidade da pessoa em criação é a ficha nova)
- convite autofill/login: ficha ancorada pode compartilhar o phone; o lado
  conta permanece fail-closed (username DB-unique; check de 2+ contas)
- import CSV: preview sinaliza telefone_compartilhado (resolução manual);
  confirm/bulk abortam fail-closed se um phone ganhar 2+ fichas (race)
- fix de achado de execução: supporter_import_batch.actor_id NOT NULL com
  FK ON DELETE set null derrubava o delete de campaignUser com batch
  órfão (23502) — CampaignUser.beforeDelete limpa batches + fixture
  cleanup descobre/deleta batches dos accounts owned
2026-08-10 23:03:05 -03:00
Francisco Solla
712d40f83f feat(B195): finalizar wizard Registrar pedido — campo único + título derivado por IA + edição no detalhe 2026-08-10 20:15:05 -03:00
Francisco Solla
ea700e3113 feat(C104): compromisso 'Todo o dia' na criação rápida da agenda (só datas, multi-dia) 2026-08-10 01:13:12 -03:00
Francisco Solla
27daa1339e feat(C99): toda conta campaignUser aponta para uma ficha Contact; lideranças ganham assessores responsáveis 2026-08-09 19:12:35 -03:00
Francisco Solla
29ed733190 fix(C92): calendar feed creation stripped by admin-only system fields
createCalendarFeedLinkRecord failed with `overrideAccess:false` +
`user: campaignUser` because Payload stripped the required
`secretSlug`/`createdBy` fields (create access was admin-only) -> 400
behind the generic message; revokeCalendarFeedRecord silently no-opped
(revokedAt update access also admin-only).

- canCreateCalendarFeed now enforces advisor filterMunicipality scope
  on data (canCreateSupporter precedent) + canSetCalendarFeedSecret /
  canSetCalendarFeedRevocation permit staff to mint the secret on create
  and mark a feed revoked; secret read stays `() => false` and update
  admin-only.
- CalendarFeed stamps createdBy via stampCampaignCreatedBy; secret
  create + revokedAt update use the new staff field accesses.
- Action refactored to `*Record` (payload, actor) cores + thin wrappers;
  assertFilterMunicipalityInScope (tour precedent); console.error keeps
  the real error on the server while the friendly message stays.
- 10 int tests for create/scope/revoke/leader/secret; fixture tracks
  calendarFeed cleanup.

Closes #436
2026-08-08 23:13:30 -03:00
Francisco Solla
63642483a4 feat(C90): remodela responsáveis da Atividade em campo único polimórfico
Um campo responsible (staff campaignUser · leadership · stateDeputy) multi-valor
substitui responsible/Contact + advisors + leadership; assessor mantém acesso via
notação-objeto equals no access; nova migração esquema+dados (advisors/leadership
→ responsible); busca q da lista título-only; notificação notifica staff do campo.
Closes #426
2026-08-08 17:10:48 -03:00
Francisco Solla
25773d0296
feat(C16): add calendar feed sync by filter with Google Calendar import link (#419)
- Add calendarFeed collection with secret slug credential
- Add iCal feed endpoint at /campanha/agenda/ical/[secret] (unauthenticated)
- Add server actions for creating and revoking calendar feeds
- Add CalendarFeedButton UI component in agenda filters bar
- Add unit tests for iCal generation
- Fail-closed access: feed re-evaluates creator's municipality scope on each read
- Leader lockdown preserved; no PII in feed output
2026-08-08 14:47:17 +00:00
Francisco Solla
c5001b5005
Merge pull request #410 from fsolla/C87-atualizacao-unificada-texto-polaridade-urgente
feat(campaign): C87 — atualização unificada (texto + polaridade + urgente)
2026-08-07 07:20:28 -03:00
Francisco Solla
212a1f2423 feat(campaign): C87 — atualização unificada (texto + polaridade + urgente)
Substitui o modelo de MunicipalityUpdate (kind/semanal/urgente/nota/sinal + signalType)
por um modelo unificado: body obrigatório + polarity (boa/neutra/ruim) + urgent + adversarySignal.

- Schema: polarity enum obrigatório, urgent boolean default false, adversarySignal boolean
- Wizard: novo WizardUpdateBodyStep com campo de texto + polaridade + urgente
- Lista: controle unificado de atualização com polaridade inline
- Feed: exibe polaridade como badge colorido + indicador de urgência
- V2 status strip: atualização rápida de polaridade
- E11: queries atualizadas para polarity + adversarySignal
- Migração: 20260807_030000_unify_municipality_update (backfill polarity=neutra, urgent de kind, adversarySignal de kind+signalType)
- Testes: 18/18 integração C87, 593/593 total, 1399/1399 unitários
- Fix: testes de state deputy usavam nomes fixos que colidiam entre execuções
2026-08-07 05:04:41 -03:00
Francisco Solla
59983b33f7 feat(campanha): C15 — FullCalendar em /campanha/agenda
Issue #390 — entrada operacional staff com FullCalendar v7 route-local
(semana/mês/lista), filtros compartilháveis por município/deputado/tag,
criação pelo slot, detalhe por URL e remarcação drag/resize sob RBAC,
transação e lock por atividade.

- /campanha/atividades e giros permanecem como lista/fluxos legados
- Sem migration/collection nova; reutiliza activity de C14
- Fuso Bahia, range exclusivo, erro/revert, loading/empty testados
- Leader negado, advisor limitado pelo access atual
- E2E cobre filtros, drag, mobile 390x844 e slot prefill
- Int specs cobrem RBAC, concorrência e defense-in-depth

Impl: docs/plans/fullcalendar-agenda-campanha-impl.md
2026-08-07 04:37:36 -03:00
Francisco Ignácio Fontoura Solla
033c270a89 feat(campanha): B163 — editar contatos das dobradinhas 2026-08-06 16:52:55 -03:00
Francisco Ignácio Fontoura Solla
adc29c0379 feat(campaign): C14 remodel activity for agenda paradigm (#389)
- Replace kind enum with free-form tags (text[] hasMany)
- Remove origin and deadline fields
- Status: remove rascunho/planejado, keep confirmado/realizado/cancelado
- startAt required for user-facing creates (optional for tour drafts)
- Add deputyPresent time gate (only coordinator/candidate can reschedule)
- UI: calendar-like form with tag input, no origin/deadline, no draft status
- E13 giros: tourStopRoleActivityKind → tourStopRoleActivityTags
- Migration: kind→tags with label mapping, status remap, enum recreation
- Activity texts table creation (was missing from migration history)

Closes #389
2026-08-06 05:20:05 -03:00
Francisco Ignácio Fontoura Solla
6b8864debb B156 — assessores por dobradinha (campo, coluna e seção; RelationOptionCell extraído) 2026-08-04 04:57:15 -03:00
Francisco Ignácio Fontoura Solla
086a9c8ee0 B157 — coluna de dobradinhas na lista de municípios (+ criação inline) 2026-08-04 02:53:43 -03:00
Cursor Agent
a2553e1f03
merge main into B147; keep optional sinal body for v2 status
Align with B134 engagement write path; preserve B147 sinal empty-motivo
(collection + zod) required by the status-strip confirm rite.
2026-08-02 23:48:29 +00:00
Cursor Agent
6b8aa8b6cf
fix(campanha): B147 simplify — sinal empty body + cold select
Allow empty motivo on sinal through collection beforeValidate; distinguish
warm-typeless vs cold in the v2 signal select; reuse list signal form action.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:33:46 +00:00
Cursor Agent
f40957f101
B134 — allow empty rationale on movimento allocation decisions
- Relax AllocationDecision.rationale required flag; validate in hook
- Fix empty-motivo int test to use first movement from null

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-02 23:08:52 +00:00
Cursor Agent
af6c180211
B57 — delta 7 dias do total estimado no Início
- Collection campaignVoteSummarySnapshot (snapshot diário statewide)
- Gravação idempotente on-read no loadCampaignHomeSummary + retenção 30d
- UI: seta + magnitude ao lado do total B56 (aria-label completo)
- Reusa rollup statewide para CG/candidato; assessor vê Δ global (opção A)

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 06:33:39 +00:00
Cursor Agent
de67acf41b D2 — Push + sino in-app para /campanha
- Collections Notification e PushSubscription com migration
- Sino no header (mobile + desktop) com drawer de avisos lidos/não lidos
- Hooks em municipalityUpdate, supporter, activity e convite aceito
- Push Web via web-push + handlers no service worker /campanha/sw.js
- Opt-in LGPD em /campanha/perfil (chave campanha-notificacoes-push)
- Testes unit/int de contrato e access

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-07-31 01:51:41 +00: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
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
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
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
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
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
bb5ac362ed Add users.roles so Payload /admin can separate editors from admins.
Editors publish post/tag/media without PII or campaign access; existing users backfill as admin.
2026-07-27 01:54:29 -03:00
Francisco Ignácio Fontoura Solla
2bbb630af6 simplify(B27): fix optimistic-state clobber, unify JSON route error shell, dedupe advisor id resolution
- MunicipalityListAdvisorsControl: stop advancing lastPropsIDsRef from a
  successful delta response — it exists only to detect a genuine external
  prop change, and writing it there made a later stale re-render of the
  pre-edit content look external and clobber optimistic state back to it.
  Also reconcile with the latest confirmed server set on the error path
  once every in-flight delta settles (previously only the success path did),
  and consolidate chip/tooltip name resolution onto the shared
  advisorEntriesFromIds helper instead of a bespoke lookup with an
  '#id' placeholder.
- Extract campaignJsonMutationErrorResponse/parseCampaignJsonRequestBody
  (src/utilities/campaignJsonMutationRoute.ts) and adopt them in both
  municipios/advisors/route.ts and municipios/expected-votes/route.ts,
  removing the duplicated body-parse + error-mapping ladder.
- Municipality.ts's validateMunicipalityAdvisors now uses
  uniqueRelationshipIds instead of re-inlining the Set+map+filter dance
  the helper was extracted for.

Verified: tsc --noEmit, lint, format:check, check:cycles all clean;
full unit suite (450 tests) and the municipality/advisor int suites pass.
knip's pre-existing payload.config.ts load error is unrelated (P3).
Playwright's spawned webServer failed to boot in this sandbox (unrelated
module-resolution environment issue, reproduced even before this diff);
manual pnpm dev boots fine and the change is covered by unit/int tests.
2026-07-26 03:51:32 -03:00
Francisco Ignácio Fontoura Solla
619666dd51 C13 pos-simplify: fecha os residuos da varredura e estreita os guards
O rename mecanico deixou um defeito silencioso e alguns fosseis: o fixture de
recentVisits perdeu o ${index} (todas as entradas com o mesmo label, invisivel
porque o label nao era asserido) e access/shared.ts guardava "action plan" em
prosa, forma que o guard de vocabulario nao cobria. O padrao agora tolera espaco
e a allowlist encolheu: o comentario de campaignMigrationReconciliation cita
activity e o arquivo saiu da excecao.

Na migration, a entrada action_plan_municipality_idx era inalcancavel (nenhuma
migration jamais criou esse nome) e saiu, deixando up() e down() com os mesmos
48 pares; as duas buscas de FK passam a usar to_regclass, que retorna NULL em vez
de lancar -- a unica consulta que contradizia a invariante do proprio cabecalho.

O detalhe de organizacao para de derivar o rotulo de status a mao e reusa
ActivityStatusBadge, com status tipado como ActivityStatus no loader.

Debitos do /simplify registrados: C8 F4b (o guard de formActions so inspeciona
arquivos chamados exatamente formActions.ts) no plano do C8; defers e descartes
com gatilho no plano do C13.
2026-07-25 21:29:32 -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
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
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
11d3fcd24c Pass 2 W4e: tipos de fonte unica
- triplo de cenarios: municipalityMapData usa VoteEstimateScenarioFields (2 grafias inline eliminadas)
- MAX_LEADERSHIP_MUNICIPALITIES: Leadership.ts importa de lib/schemas/leadership (const local removida)
- campaignUserInitials: copia divergente do AvatarStack removida; fonte unica em campaignUserProfile (com fallback defensivo)
- tendencia politica: options do select da collection derivam de politicalTrendStatuses + politicalTrendLabels; ProjectionTrendStatus vira alias de PoliticalTrendStatusValue
- MunicipalityStrategyViewModel nomeado e exportado (consumidores param de indexar NonNullable<...['strategy']>); nota de por que NAO deriva do zod
- DynamicFind documentado como a unica grafia aprovada do cast
- stub<T> nos doubles de teste onde a checagem por propriedade e real (campaignUserPhone, campaignAuth, campaignConsent); deep fakes mantem o cast localizado
2026-07-25 15:05:03 -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
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
fe90cb7450 update 2026-07-24 16:13:13 -03:00
Francisco Ignácio Fontoura Solla
0407f02ce2 Import projection-sheet strategy columns into municipalities (E4R fase 2)
- pnpm db:seed:projecao now also imports SITUAÇÃO -> politicalTrend,
  DOBRADINHAS -> stateDeputy + stateDeputies + dobradinhaNotes,
  LIDERANÇAS -> name-only contact + leadership (statewide slug dedupe),
  ASSESSOR RESPONSÁVEL -> campaignUser + advisors (Edizio=coordinator,
  Solla=candidate, placeholder @planilha.invalid emails),
  ENCAMINHAMENTOS -> nextSteps, OBSERVAÇÃO -> strengths/risks (curated).
  PRIORITÁRIAS overrides MAPA GERAL when non-empty; Salvador stays skipped;
  relationships are unions; re-run is idempotent.
- parseSituationCell + splitNameCell (noise/collective/action filters,
  honorific protection, parenthetical extraction) with unit tests.
- Contact.phone optional (migration 20260724_175500_contact_phone_optional);
  phone invariant hooks skip phone-less contacts; invite flows guard.
- candidate role now eligible for municipality/action-plan advisors
  (eligibleCampaignStaffWhere) per product decision.
2026-07-24 15:58:06 -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
473c82d09d Lock down CMS/PII collection access and leader routes
- users/signature/subscription/consent: all operations Payload-admin-only
  (Payload's default allowed ANY authenticated user, incl. campaign JWTs
  via /api — privilege escalation + PII exposure)
- post/tag/media/petition: public read kept, writes admin-only,
  readVersions admin-only on drafted collections
- leader lockdown: staff gates on /campanha/demandas* and /campanha/planos*;
  removed the dead leader branch of the demand detail page
- fix createLeaderSupporter: scope now asserted via the leader's engaged
  leadership municipalities (canReadMunicipality always denied leaders)
- candidate alignment: supporter area, invite creation, and escalated
  demand decisions now treat candidate as unrestricted staff consistently
- tests: collectionAccessLockdown.int.spec pins the lockdown; leased-consent
  helpers generalized (supporter keys); fixtures purge deletes residue
  invites before leaderships; stale leader-phone tests updated to the
  lockdown model; 'sem Praça' copy renamed to 'sem município'
2026-07-23 22:25:18 -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