12 KiB
Escala e DRY pós-C3 (planos de ação)
Status: entregue (2026-07-19) Atualizado em: 2026-07-19 Item do roadmap: docs/roadmap.md (Trilha C, item C7) Responsável: —
Contexto
O C3 (eventos-agenda-mobilizacao.md) entregou a vertical /campanha/planos (actionPlan, lista/detalhe/forms, blocos "Próximos eventos"). A passagem /simplify de 2026-07-18 aplicou limpezas pontuais (alias de access, labels a partir do schema, paginação via getNucleusPaginationPages, snapshot de campos staff para liderança, loaders/overview em paralelo), mas deixou de fora débitos que os revisores (quality / reuse / performance) marcaram como importantes e maiores que cleanup:
- Compor território:
NucleusTerritoryAndZonesFieldsforcou um fork quase completo emNucleusTerritoryFields(~319 linhas). - Unificar o picker assíncrono de contato (
ContactComboboxvsPrimaryContactCombobox). - Extrair
validateTerritoryFormData/ parse de território FormData (hoje clone núcleo ↔ plano). - Consumir (ou espelhar) helpers de URL de lista — C6 Fase 2 já prevê
campaignListUrl;actionPlanUiainda clona o maquinário denucleusUi. - Leituras caras: detalhe/edit sempre carregam
tasks+updatesemdepth: 1; lista hidrata todas as tasks só para progresso; form prefetcha até 200 lideranças. - Escritas amplificadas: toggle de tarefa / append de update fazem read-modify-write do documento inteiro.
- Índice composto ausente para a query quente
status IN (…) AND startAt >= now ORDER BY startAt.
Este plano é o registro canônico desses follow-ups. Sem ele, a agenda funciona em volume baixo, mas o DRY com núcleos e o custo por plano crescem com o feed de atualizações e a lista de "Próximos".
Entregue (2026-07-19): Fases 1–5 completas — composição territorial (useCampaignTerritoryFieldsState + CampaignTerritoryCoreFields), shell AsyncSearchCombobox (Contact + Primary + liderança), helpers FormData/URL, leituras por aba + taskProgress no detalhe + typeahead de liderança, short-circuit de hooks em toggle/append. Feed O(n) / collection actionPlanUpdate permanece condicional (fora de escopo).
Primeira fatia (2026-07-19): FormData território, contadores lista + migration 20260719_014906_action_plan_list_perf, locks advisory, contactSearchQuery, rename CampaignTerritoryFields.
Objetivos
- Território Bahia (sem ZE) é um único componente/core; o formulário de núcleo só adiciona
TseZoneInput+ chips de ZE. - Contatos remotos usam um shell assíncrono;
PrimaryContactComboboxadapta a API legada{ current, options }. - Validação/parse de território FormData e (quando C6 F2 existir) URL de lista deixam de ser triplicados entre núcleo / apoiador / plano.
- Detalhe de plano carrega arrays por aba; lista não faz join de todas as tasks para contar progresso; liderança no form é typeahead (não prefetch 200×
depth: 1). - Toggle/append não reescrevem o documento inteiro; query "Próximos" tem índice adequado.
- Guardrails:
overrideAccess: falsecomuser; escritas multi-step continuam emwithPayloadTransaction; liderança continua restrita atasks.done+ appendupdates; sem novoConsent.
Decisões travadas
- Um item C7, cinco fases ordenadas. Mesmo racional do C6: um ID de roadmap, PRs por fase. Ordem: DRY barato → DRY helpers → leituras → escritas/índice.
- Dependência dura de C3. Não reabre o escopo de v1 do
actionPlan(território próprio, arrays embutidos no MVP, access porcoordinators/leadership). - Dependência suave de C6 Fase 2 para
campaignListUrl/ pagination shell. Se C6 F2 ainda não existir, C7 Fase 3 extrai só o necessário paraactionPlanUiou espera o helper do C6 — não inventar um terceiro*ListUrl. - Cortável se a agenda permanecer pequena. Fases 4–5 (leituras pesadas, RMW, índice) podem escorregar se houver poucos planos/updates; Fases 1–2 (composição de UI) continuam baratas e reduzem risco de drift com A2.
- Arrays embutidos no MVP; collection
actionPlanUpdatesó se a Fase 5 medir necessidade. Preferir patch de linha emaction_plan_tasks/action_plan_updates(drizzle ou Local API alvo) antes de nova collection. - Contadores de progresso na lista: preferir campos derivados
taskDoneCount/taskTotalmantidos nobeforeChange(migration pequena) a hidratartasks: { done: true }em toda página. - i18n e naming (AGENTS.md): identificadores em inglês (
CampaignTerritoryFields,AsyncContactCombobox,validateCampaignTerritoryFormData,taskDoneCount,searchActionPlanLeadershipOptions), strings visíveis em pt-BR.
Questões em aberto
- Renomear
NucleusTerritoryFields→CampaignTerritoryFieldsneste item? Decisão (2026-07-19): feito; alias deprecated removido no simplify por ausência de importadores. - Patch de array via drizzle vs Local API? Recomendação: locks +
selectmínimo já entregues; próximo passo é short-circuit de hooks (context.mutationKind: 'taskToggle' | 'appendUpdate') antes de SQL direto. - Índice
(status, start_at)na mesma migration dos contadores? Decisão (2026-07-19): feito —20260719_014906_action_plan_list_perfcom índice parcialaction_plan_upcoming_start_at_idx. - Typeahead de liderança reusa
ContactComboboxouNativeSelect+ search? Recomendação: Server ActionsearchActionPlanLeadershipOptions(query)+ combobox (mesmo shell da Fase 2),depth: 0, label a partir do contact id/nome já resolvido. - Migrar timestamps de núcleos para
formatBahiaDateTimeLabel? Recomendação: fill-in separado (não bloqueia C7); o helper date-only foi removido no simplify e só volta se a UI precisar.
Abordagem proposta
flowchart TD
F1["Fase 1 — DRY território UI<br/>core + AndZones compõe"]
F2["Fase 2 — DRY contact picker<br/>shell assíncrono único"]
F3["Fase 3 — DRY form/URL território<br/>+ consumir campaignListUrl"]
F4["Fase 4 — Leituras<br/>tabs · contadores · typeahead"]
F5["Fase 5 — Escritas + índice<br/>RMW mínimo · status+startAt"]
C3["C3 actionPlan ✓"] --> F1
F1 --> F2
F2 --> F3
F3 --> F4
F4 --> F5
C6["C6 F2 campaignListUrl"] -.suave.-> F3
Fase 1 — Território UI composto (entregue)
- Extrair o bloco regions/cities/neighborhoods/locality/notes (+ chips) para um core compartilhado (hoje duplicado em
NucleusTerritoryFields.tsx≈NucleusTerritoryAndZonesFields.tsx). NucleusTerritoryAndZonesFields: compõe o core +TseZoneInput+ sugestões de ZE (buildTerritorySuggestions/outsideZones).ActionPlanForm: continua importando só o core (sem ZE).- Entregue: rename
CampaignTerritoryFields+ consumo no form de plano; coreuseCampaignTerritoryFieldsState+CampaignTerritoryCoreFields.
Fase 2 — Contact picker único (entregue)
- Shell assíncrono (debounce,
requestId, CommandDialog, loading/erro) a partir deContactCombobox.tsx. PrimaryContactCombobox: adapta{ current, options }/ search de inteligência de núcleo para o shell (sem segundo Dialog).- Estado controlado (
value+onChange); eliminar espelho pai/filho que o simplify apontou. - Entregue:
AsyncSearchCombobox+ adaptadores; helpercontactSearchQuery(mín. 2 chars).
Fase 3 — Helpers FormData / URL (entregue)
validateCampaignTerritoryFormData/parseCampaignTerritoryFields: ✅ extraído paracampaignTerritoryFormData.ts;nucleusFormData.tseactionPlanFormData.tsconsomem.- URL de lista: ✅
actionPlanUi.tsjá consomecampaignListUrl.ts(C6 F2). - Opcional:
actionPlanDetailTabUiparametrizado com o helper genérico de tabs do detalhe de núcleo (só se o PR já tocar URL).
Fase 4 — Leituras (entregue)
[slug]/page.tsx: selects por aba viagetActionPlanDetailSelect+getActionPlanDetailPageData(overview sem arrays; tarefas comdepth: 1; atualizações comdepth: 0+ batch de autores).- Lista: ✅
taskDoneCount/taskTotal+actionPlanListSelectsemtasks. getActionPlanLeadershipOptions→ search: ✅searchActionPlanLeadershipOptions+AsyncSearchComboboxno form.ContactCombobox: ✅ não fetch com query < 2 chars (contactSearchQuery.ts).- Detalhe: ✅
taskProgressno view model da overview (contadores, não filtro inline detasks).
Fase 5 — Escritas + índice (entregue; feed O(n) condicional)
toggleActionPlanTaskRecord/appendActionPlanUpdateRecord: ✅ locks advisory +selectmínimo nofindByID.- Short-circuit de hooks: ✅
context.mutationKind: 'taskToggle' | 'appendUpdate'nas actions; hooksbeforeValidateretornam cedo. - Migration
action_plan_list_perf: ✅ contadores + índice parcialaction_plan_upcoming_start_at_idx(20260719_014906_action_plan_list_perf). - Feed O(n): monitorar tamanho de
updates; só então collectionactionPlanUpdateappend-only. (condicional — não implementado)
Migration: Fases 1–3 sem schema. Migration 20260719_014906_action_plan_list_perf ✅ entregue (contadores + índice parcial). Sem Consent novo.
Dependências
- Dura: C3 Planos de Ação (código em
actionPlan*//campanha/planos). - Suave: C6 Fase 2 (
campaignListUrl) — evita terceiro clone de URL helpers. - Reusa:
NucleusTerritoryAndZonesFields.tsx,NucleusTerritoryFields.tsx,PrimaryContactCombobox.tsx,ContactCombobox.tsx,nucleusFormData.ts,actionPlanFormData.ts,actionPlanUi.ts,actionPlanPageData.ts,actionPlanViewModels.ts,campanha/actions/actionPlan.ts, padrão de loaders por aba do detalhe de núcleo, C6 plano escala-dry-pos-c2.md.
Não escopo
- Reabrir decisões de v1 do C3 (
startAtsó obrigatório fora de rascunho, território sem link a núcleo, updates embutidos no MVP) — eventos-agenda-mobilizacao.md. - Escala/DRY de apoiadores (import bulk, KPI SQL, preview token) — C6 / escala-dry-pos-c2.md.
- Demandas / relação opcional
actionPlan— C4. - Notificações de agenda — D2 / notifications.md.
- Calendário visual / mapa de eventos / recorrência — fora do horizonte pré-16/08.
- Migrar todos os
Intl.DateTimeFormatde núcleos para Bahia — fill-in. - Unificar
validateCampaignTerritoryFormDatacomcreateCampaignTerritoryValidationHooknum core único — tipos de erro diferentes (FormData vs Payload); refactor transversal; reavaliar após C7 Fases 1–2. - Índice
pg_trgmemcontact— C8 / escala-dry-pos-c6.md.
Referências
docs/roadmap.md— Trilha C, item C7; sequência Janela 2- eventos-agenda-mobilizacao.md — C3 v1 entregue
- escala-dry-pos-c2.md — precedente C6 +
campaignListUrl - Review
/simplify2026-07-18 (quality / reuse / performance) — origem das fases - Review
/simplify2026-07-19 — débitos registrados nas fases pendentes acima - Review
/simplify2026-07-19 (pós-entrega C7) — débitos maiores → C11 / escala-dry-pos-c7.md src/lib/contactSearchQuery.ts— guard compartilhado client/server (entregue)src/migrations/20260719_014906_action_plan_list_perf.ts— contadores + índice parcial (entregue)src/components/campaign/AsyncSearchCombobox.tsx— shell assíncrono compartilhado (entregue)src/components/campaign/useCampaignTerritoryFieldsState.ts/CampaignTerritoryCoreFields.tsx— core territorial (entregue)src/utilities/actionPlanFormData.ts/nucleusFormData.ts/actionPlanUi.tssrc/utilities/actionPlanPageData.ts/actionPlanViewModels.ts/actionPlanLeadershipOptions.tssrc/app/(campaign)/campanha/actions/actionPlan.ts— toggle/append RMWsrc/migrations/20260718_222832_add_action_plan.ts— índices atuaisstatus/start_atseparados- AGENTS.md —
overrideAccess: false, transações, naming, território Bahia
Simplify (2026-07-19)
Passagens /simplify sobre o diff do C7 (incl. pós-rebase com C8–C9/Onda 0) aplicaram limpezas pontuais (fieldError, LeadershipCombobox, searchRef, depth overview, exports mortos, skip buildTerritorySuggestions com override). Débitos maiores registrados no item C11 do roadmap — escala-dry-pos-c7.md.