diff --git a/docs/plans/motivo-opcional-tendencia-e-nivel-impl.md b/docs/plans/motivo-opcional-tendencia-e-nivel-impl.md new file mode 100644 index 00000000..48e5a9db --- /dev/null +++ b/docs/plans/motivo-opcional-tendencia-e-nivel-impl.md @@ -0,0 +1,64 @@ +# Impl: Motivo opcional — tendência (wizard) + nível; remover voltar atrás do DB (B134) + +Status: aprovado +Atualizado em: 2026-08-02 +Issue: #288 +Intenção: docs/plans/motivo-opcional-tendencia-e-nivel.md +Appetite restante: herdado (~1 dia eng) + +## Leitura da intenção + +- **Outcome:** staff grava tendência ou nível sem texto obrigatório; Motivo permanece opcional nos dois ritos; `reversalSignals` some da UI, write path e snapshots persistidos. +- **O que NÃO negociar:** histerese/override/choque triangulado intactos; `leader` lockdown; sem auto-save no nível; motivo vazio grava `null` (não preserva anterior); dados históricos de `reversalSignals` apagados do DB. +- **O que reavaliar:** não há coluna dedicada — `reversalSignals` vive só no JSON `allocation_decision.snapshot`; migration é data-only, sem alterar `AllocationDecision` config. + +## Abordagem recomendada + +```mermaid +flowchart LR + UI[Popover / wizard] --> schema[municipalityEngagementLevelSchema] + schema --> action[setMunicipalityEngagementLevelRecord] + action --> mun[municipality.levelNote nullable] + action --> ad[allocationDecision snapshot sem reversalSignals] + mig[migration SQL] --> ad +``` + +**Opções consideradas:** A remover só da UI/schema | B migration JSON strip + schema/UI/action +**Recomendação:** B — aceite exige apagar do banco, não só esconder. +**Rejeitadas:** A (órfãos no snapshot); tornar `rationale` opcional na collection (escopo desnecessário — string vazia basta quando motivo ausente). + +### Componentes / mudanças + +- **`municipalityEngagementLevelSchema`** (`src/lib/schemas/municipality.ts`): `note` → `trimmedNullableText`; remover `reversalSignals`. +- **`setMunicipalityEngagementLevelRecord`** (`actions/municipality.ts`): snapshot sem `reversalSignals`; `rationale: note ?? ''`. +- **`MunicipalityListLevelControl`**: um campo Motivo (opcional); `canSubmit` só exige movimento + override quando violações. +- **`WizardTrendNoteStep`**: remover `required`; label com “(opcional)”. +- **Migration:** `20260802_*_strip_engagement_reversal_signals` — `UPDATE allocation_decision SET snapshot = snapshot - 'reversalSignals' WHERE …`. +- **Access / Consent:** inalterado (`canManageMunicipalityEngagementLevel`). + +### Dados → forma + +- Tendência: schema já usa `trimmedNullableText` — só UI wizard tinha `required` HTML. +- Nível: `levelNote` nullable no município; `allocationDecision.rationale` string (vazia se sem motivo). + +## Fases verificáveis + +1. **Schema+server** — schema, action, migration, int test com motivo vazio + snapshot sem reversalSignals. +2. **UI** — labels “(opcional)”, remover segundo textarea. +3. **Gates** — `pnpm gate:fast`; push via `pnpm push`. + +## Rabbit holes / Não escopo (engenharia) + +- Alterar copy do glossário `campaignIntelligenceConcepts` (prosa conceitual, não o campo). +- `rationale` required na collection — string vazia, não relaxar schema Payload. + +## Riscos e mitigação + +- **Rationale vazio rejeitado pelo Payload:** int test com `note` omitido; se falhar, usar `''` explicitamente (textarea required aceita vazio). +- **Migration idempotente:** operador JSONB `-` só onde chave existe; log de row count. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto +- [x] Invariantes AGENTS/engineering-standards +- [x] Testes de domínio previstos (int engagement level + migration unit) diff --git a/docs/plans/sidebar-organizacoes-impl.md b/docs/plans/sidebar-organizacoes-impl.md new file mode 100644 index 00000000..54837e0a --- /dev/null +++ b/docs/plans/sidebar-organizacoes-impl.md @@ -0,0 +1,61 @@ +# Impl: Sidebar — entrada para Organizações + +Status: aprovado +Atualizado em: 2026-08-02 +Issue: #313 +Intenção: docs/plans/sidebar-organizacoes.md +Appetite restante: ~0,25 dia eng (um item de nav; sem migration) + +## Leitura da intenção + +- **Outcome:** Staff (coordenador, assessor, candidato) descobre `/campanha/organizacoes` pela sidebar desktop e Sheet mobile; liderança não vê o item; destino ativo em lista, detalhe e `/nova`. +- **O que NÃO negociar:** lockdown de `leader`; não redesenhar sidebar; não duplicar lista; href canônico `/campanha/organizacoes`. +- **O que reavaliar:** hipótese de importar `ORGANIZATIONS_LIST_PATH` — `nav.ts` usa literais (precedente Conceitos) para não puxar módulos ao client bundle; manter literal. + +## Abordagem recomendada + +```mermaid +flowchart LR + staffNav["staffNav + Organizações"] --> getCampaignNav + getCampaignNav --> CampaignSidebar + getCampaignNav --> MobileSheet["Sheet mobile"] + isCampaignNavActive --> Active["lista / slug / nova"] +``` + +**Opções consideradas:** A) item após Lideranças | B) após Dobradinhas | C) antes de Apoiadores +**Recomendação:** **A** — afinidade domínio orgs ↔ lideranças; alinhado à recomendação da intenção. +**Rejeitadas:** B/C — sem ganho de descoberta; A é o modelo mental acordado. + +### Componentes / mudanças + +- **`staffNav`** (`src/components/campaign/shell/nav.ts`): inserir `{ title: 'Organizações', href: '/campanha/organizacoes', icon: LandmarkIcon }` logo após Lideranças; import `LandmarkIcon` de lucide-react (instituições — distinto de `HandshakeIcon` e `Building2` usado em município no dashboard). +- **Migration:** sem migration +- **Access / Consent:** nenhum — `getCampaignNav` já exclui `leader`; staff vê por default (mesmo que Lideranças/Demandas) +- **UI:** Impeccable B — encaixe no shell existente; `CampaignSidebar` e Sheet já consomem `getCampaignNav`; `isCampaignNavActive` já cobre paths aninhados (`/nova`, `[slug]`) + +### Dados → forma + +N/A — só navegação. + +## Fases verificáveis + +1. **Nav** — uma linha em `staffNav` + ícone +2. **Testes** — unit: href presente para staff, ausente para leader; `isCampaignNavActive` em lista/detalhe/nova +3. **Gates** — `pnpm gate:fast`; entrega `pnpm push` + +## Rabbit holes / Não escopo (engenharia) + +- Reordenar demais itens além do encaixe após Lideranças +- Exportar constante de href (só `MUNICIPALITY_NAV_HREF` tem consumidor especial B18) +- `staffSecondaryNav` — destino de trabalho, não referência + +## Riscos e mitigação + +- **Leader vê link:** mitigado — `getCampaignNav('leader')` retorna `leaderNav` separado +- **Active state em `/nova`:** mitigado — `isCampaignNavActive` usa `pathname.startsWith(\`${href}/\`)` + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto +- [x] Invariantes AGENTS/engineering-standards +- [x] Testes de domínio previstos (unit) para nav + active state diff --git a/src/app/(campaign)/campanha/actions/municipality.ts b/src/app/(campaign)/campanha/actions/municipality.ts index c356148c..79e8c596 100644 --- a/src/app/(campaign)/campanha/actions/municipality.ts +++ b/src/app/(campaign)/campanha/actions/municipality.ts @@ -151,10 +151,6 @@ export const setMunicipalityEngagementLevelRecord = async ( ) => { const { municipality, level, note, triangulatedShock, override } = municipalityEngagementLevelSchema.parse(input) - const levelNote = note ?? null - // allocationDecision.rationale is required; empty Motivo still leaves an - // auditable row without inventing coordinator prose. - const rationale = levelNote ?? 'Sem motivo registrado.' return withPayloadTransaction( payload, @@ -194,7 +190,7 @@ export const setMunicipalityEngagementLevelRecord = async ( const updated = await payload.update({ collection: 'municipality', id: municipality, - data: { engagementLevel: level, levelNote }, + data: { engagementLevel: level, levelNote: note }, depth: 0, user: currentActor, overrideAccess: false, @@ -207,7 +203,7 @@ export const setMunicipalityEngagementLevelRecord = async ( municipality, patternId: ENGAGEMENT_LEVEL_PATTERN_ID, outcome: 'movimento', - rationale, + rationale: note ?? '', // Only the values the decision was taken on — no document dumps // (the collection caps the serialized snapshot at 16 KB). snapshot: { diff --git a/src/collections/AllocationDecision.ts b/src/collections/AllocationDecision.ts index ea83bbf0..9cec5aac 100644 --- a/src/collections/AllocationDecision.ts +++ b/src/collections/AllocationDecision.ts @@ -32,6 +32,13 @@ const validateAllocationDecision: CollectionBeforeValidateHook = ({ throw new APIError('Informe a leitura alternativa ao descartar a sugestão.', 400) } + if ( + nextData.outcome !== 'movimento' && + (typeof nextData.rationale !== 'string' || !nextData.rationale.trim()) + ) { + throw new APIError('Informe a justificativa.', 400) + } + try { const serializedSnapshot = JSON.stringify(nextData.snapshot) if ( @@ -116,7 +123,8 @@ export const AllocationDecision: CollectionConfig = { name: 'rationale', type: 'textarea', label: 'Justificativa', - required: true, + // E14 movimento may omit motivo (B134); triage outcomes validate in beforeValidate. + required: false, maxLength: 2000, }, { diff --git a/src/components/campaign/municipality/MunicipalityListLevelControl.tsx b/src/components/campaign/municipality/MunicipalityListLevelControl.tsx index 6c56b1d4..e1fa9abc 100644 --- a/src/components/campaign/municipality/MunicipalityListLevelControl.tsx +++ b/src/components/campaign/municipality/MunicipalityListLevelControl.tsx @@ -45,8 +45,9 @@ type MunicipalityListLevelControlProps = { /** * E14 — unlike the trend and estimate cells next to it, this one submits - * explicitly: a movement may need an override / triangulated shock. Motivo is - * optional (B134/B147). Auto-saving would skip the confirmation rite. + * explicitly: a movement may need an override, and the motivo is optional. + * Auto-saving half of that would file an incomplete decision under the + * coordinator's name. */ export const MunicipalityListLevelControl = ({ municipalityID, @@ -170,7 +171,7 @@ export const MunicipalityListLevelControl = ({ { municipalityId: municipalityID, level: draftLevel, - note: note.trim() ? note : null, + note, triangulatedShock, override, }, diff --git a/src/components/campaign/municipality/WizardTrendNoteStep.tsx b/src/components/campaign/municipality/WizardTrendNoteStep.tsx index f3d0e969..9b675b0e 100644 --- a/src/components/campaign/municipality/WizardTrendNoteStep.tsx +++ b/src/components/campaign/municipality/WizardTrendNoteStep.tsx @@ -94,7 +94,7 @@ export const WizardTrendNoteStep = ({