diff --git a/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx index 14e712a6..5e3c6082 100644 --- a/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx +++ b/src/app/(campaign)/campanha/(app)/acoes/[slug]/page.tsx @@ -67,7 +67,6 @@ export default async function CampaignActionWizardPage({ actionSlug={slug} municipalityId={municipality.id} municipalityName={municipality.name} - municipalitySlug={municipality.slug} initialExpectedVotes={toVoteEstimateScenarioViewModel(municipality.expectedVotes)} /> ) diff --git a/src/app/(campaign)/campanha/(app)/municipios/expected-votes/types.ts b/src/app/(campaign)/campanha/(app)/municipios/expected-votes/types.ts index 56111a76..f227246e 100644 --- a/src/app/(campaign)/campanha/(app)/municipios/expected-votes/types.ts +++ b/src/app/(campaign)/campanha/(app)/municipios/expected-votes/types.ts @@ -2,6 +2,9 @@ import type { VoteEstimateScenarioViewModel } from '@/lib/voteEstimate' export const MUNICIPALITY_EXPECTED_VOTES_ENDPOINT = '/campanha/municipios/expected-votes' as const +export const MUNICIPALITY_EXPECTED_VOTES_SAVE_ERROR_MESSAGE = + 'Não foi possível salvar os votos estimados. Tente novamente.' as const + export type MunicipalityListExpectedVotesResponse = | { status: 'success' diff --git a/src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx b/src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx index 0a60b630..4b409678 100644 --- a/src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx +++ b/src/components/campaign/municipality/MunicipalityListExpectedVotesControl.tsx @@ -2,6 +2,7 @@ import { MUNICIPALITY_EXPECTED_VOTES_ENDPOINT, + MUNICIPALITY_EXPECTED_VOTES_SAVE_ERROR_MESSAGE, type MunicipalityListExpectedVotesResponse, } from '@/app/(campaign)/campanha/(app)/municipios/expected-votes/types' import { useMunicipalityEstimateScenarioOptional } from '@/components/campaign/municipality/MunicipalityEstimateScenarioContext' @@ -21,7 +22,6 @@ import { import type { MunicipalityPledgeCoverageView } from '@/utilities/votePledgeViews' const AUTOSAVE_MS = 600 -const SAVE_ERROR_MESSAGE = 'Não foi possível salvar os votos estimados. Tente novamente.' /** * `municipalityName` is the Drawer's subject line and part of the trigger's @@ -57,7 +57,7 @@ export const MunicipalityListExpectedVotesControl = ({ endpoint: MUNICIPALITY_EXPECTED_VOTES_ENDPOINT, buildBody: (values) => ({ municipalityId: municipalityID, expectedVotes: values }), readSaved: (payload) => payload.savedExpectedVotes, - errorMessage: SAVE_ERROR_MESSAGE, + errorMessage: MUNICIPALITY_EXPECTED_VOTES_SAVE_ERROR_MESSAGE, pendingMessage: 'Salvando votos estimados.', }) diff --git a/src/components/campaign/shared/WizardExpectedVotesStep.tsx b/src/components/campaign/shared/WizardExpectedVotesStep.tsx index 06213f01..165da108 100644 --- a/src/components/campaign/shared/WizardExpectedVotesStep.tsx +++ b/src/components/campaign/shared/WizardExpectedVotesStep.tsx @@ -5,6 +5,7 @@ import { useState, useTransition } from 'react' import { MUNICIPALITY_EXPECTED_VOTES_ENDPOINT, + MUNICIPALITY_EXPECTED_VOTES_SAVE_ERROR_MESSAGE, type MunicipalityListExpectedVotesResponse, } from '@/app/(campaign)/campanha/(app)/municipios/expected-votes/types' import { CampaignWizardShell } from '@/components/campaign/shared/CampaignWizardShell' @@ -16,28 +17,26 @@ import { wizardActionHref } from '@/lib/campaignActionRoutes' import { postCampaignJson } from '@/lib/campaignJsonRequest' import { CAMPAIGN_HOME } from '@/lib/campaignPaths' import { - WIZARD_VOTES_NEXT_FLOW_PLACEHOLDER, - WIZARD_VOTES_SAVE_ERROR_MESSAGE, + WIZARD_VOTES_FINAL_CTA_LABEL, WIZARD_VOTES_SAVED_MESSAGE, wizardFlowTitleForSlug, + wizardNextStepPlaceholder, wizardNextStepTitle, } from '@/lib/campaignWizardCopy' import { type VoteEstimateScenario, type VoteEstimateScenarioViewModel } from '@/lib/voteEstimate' import { applyVoteShortcut, getWizardVoteViolation, - getWizardVoteViolationHighlights, VOTE_SHORTCUTS, voteShortcutLabels, - wizardVoteFinalCtaLabel, type VoteShortcut, + type WizardVoteViolation, } from '@/lib/wizardVoteEstimate' type WizardExpectedVotesStepProps = { actionSlug: string municipalityId: number municipalityName: string - municipalitySlug: string initialExpectedVotes: VoteEstimateScenarioViewModel } @@ -45,22 +44,17 @@ export const WizardExpectedVotesStep = ({ actionSlug, municipalityId, municipalityName, - municipalitySlug: _municipalitySlug, initialExpectedVotes, }: WizardExpectedVotesStepProps) => { const [isPending, startTransition] = useTransition() const [estimates, setEstimates] = useState(initialExpectedVotes) const [focusedScenario, setFocusedScenario] = useState('central') - const [violationMessage, setViolationMessage] = useState(null) - const [highlightScenarios, setHighlightScenarios] = useState>( - new Set(), - ) + const [violation, setViolation] = useState(null) const [saveError, setSaveError] = useState(null) const [phase, setPhase] = useState<'editing' | 'saved'>('editing') const clearViolationState = () => { - setViolationMessage(null) - setHighlightScenarios(new Set()) + if (violation) setViolation(null) } const handleValuesChange = (next: VoteEstimateScenarioViewModel) => { @@ -77,10 +71,9 @@ export const WizardExpectedVotesStep = ({ const handleConfirm = () => { if (isPending) return - const violation = getWizardVoteViolation(estimates) - if (violation) { - setViolationMessage(violation.message) - setHighlightScenarios(new Set(getWizardVoteViolationHighlights(estimates))) + const nextViolation = getWizardVoteViolation(estimates) + if (nextViolation) { + setViolation(nextViolation) setSaveError(null) return } @@ -97,7 +90,11 @@ export const WizardExpectedVotesStep = ({ ) if (!ok || payload.status !== 'success') { - setSaveError(payload.status === 'error' ? payload.message : WIZARD_VOTES_SAVE_ERROR_MESSAGE) + setSaveError( + payload.status === 'error' + ? payload.message + : MUNICIPALITY_EXPECTED_VOTES_SAVE_ERROR_MESSAGE, + ) return } @@ -118,7 +115,7 @@ export const WizardExpectedVotesStep = ({ >

{WIZARD_VOTES_SAVED_MESSAGE}

-

{WIZARD_VOTES_NEXT_FLOW_PLACEHOLDER}

+

{wizardNextStepPlaceholder(actionSlug)}

@@ -149,7 +146,7 @@ export const WizardExpectedVotesStep = ({ autoFocusScenario="central" activeScenario={focusedScenario} onFocusScenario={setFocusedScenario} - errorScenarios={highlightScenarios} + errorScenarios={violation ? new Set(violation.highlightScenarios) : undefined} disabled={isPending} onValuesChange={handleValuesChange} /> @@ -169,9 +166,9 @@ export const WizardExpectedVotesStep = ({ ))}
- {violationMessage ? ( + {violation ? ( - {violationMessage} + {violation.message} ) : null} @@ -197,7 +194,7 @@ export const WizardExpectedVotesStep = ({ Salvando… ) : ( - wizardVoteFinalCtaLabel + WIZARD_VOTES_FINAL_CTA_LABEL )} diff --git a/src/lib/campaignActionRoutes.ts b/src/lib/campaignActionRoutes.ts index 1c296c9b..a5bf003c 100644 --- a/src/lib/campaignActionRoutes.ts +++ b/src/lib/campaignActionRoutes.ts @@ -59,7 +59,5 @@ export const parseWizardMunicipioParam = ( return trimmed ? trimmed : undefined } -export const hasWizardScenarioParam = (value: string | string[] | undefined): boolean => { - const raw = Array.isArray(value) ? value[0] : value - return raw?.trim() ? true : false -} +export const hasWizardScenarioParam = (value: string | string[] | undefined): boolean => + parseWizardMunicipioParam(value) !== undefined diff --git a/src/lib/campaignWizardCopy.ts b/src/lib/campaignWizardCopy.ts index c2d8c003..77680c04 100644 --- a/src/lib/campaignWizardCopy.ts +++ b/src/lib/campaignWizardCopy.ts @@ -31,8 +31,7 @@ export const WIZARD_VOTES_SAVED_MESSAGE = 'Votos estimados atualizados.' as cons export const WIZARD_VOTES_NEXT_FLOW_PLACEHOLDER = 'Próximo passo deste fluxo (sinal, tendência ou resumo) em breve.' as const -export const WIZARD_VOTES_SAVE_ERROR_MESSAGE = - 'Não foi possível salvar os votos estimados. Tente novamente.' as const +export const WIZARD_VOTES_FINAL_CTA_LABEL = 'Salvar estimativas →' as const const WIZARD_NEXT_STEP_GENERIC_PLACEHOLDER = 'Próximo passo deste fluxo em breve.' as const diff --git a/src/lib/wizardVoteEstimate.ts b/src/lib/wizardVoteEstimate.ts index 50bcd5bd..d0a8e12c 100644 --- a/src/lib/wizardVoteEstimate.ts +++ b/src/lib/wizardVoteEstimate.ts @@ -38,21 +38,10 @@ export const applyVoteShortcut = (value: number | null, shortcut: VoteShortcut): return Math.max(0, Math.min(base + delta, MAX_VOTE_COUNT)) } -export const parseWizardVoteDraft = (raw: string): number | null => { - const trimmed = raw.trim() - if (!trimmed) return null - const digitsOnly = trimmed.replace(/\D/g, '') - if (!digitsOnly) return null - const parsed = Number(digitsOnly) - if (!Number.isFinite(parsed)) return null - return Math.min(Math.trunc(parsed), MAX_VOTE_COUNT) -} - -export const wizardVoteFinalCtaLabel = 'Salvar estimativas →' as const - export type WizardVoteViolation = { violatingScenario: VoteEstimateScenario message: string + highlightScenarios: VoteEstimateScenario[] } export const getWizardVoteViolation = ( @@ -64,16 +53,14 @@ export const getWizardVoteViolation = ( return { violatingScenario, message: buildWizardVoteViolationMessage(estimates, violatingScenario), + highlightScenarios: resolveWizardVoteHighlights(estimates, violatingScenario), } } -export const getWizardVoteViolationHighlights = ( +const resolveWizardVoteHighlights = ( estimates: VoteEstimateScenarioViewModel, + violatingScenario: VoteEstimateScenario, ): VoteEstimateScenario[] => { - const violation = getWizardVoteViolation(estimates) - if (!violation) return [] - - const { violatingScenario } = violation const { pessimistic, central, optimistic } = estimates if ( diff --git a/tests/unit/wizardExpectedVotesStep.unit.spec.tsx b/tests/unit/wizardExpectedVotesStep.unit.spec.tsx index 1094e078..7d1a1a9c 100644 --- a/tests/unit/wizardExpectedVotesStep.unit.spec.tsx +++ b/tests/unit/wizardExpectedVotesStep.unit.spec.tsx @@ -21,7 +21,6 @@ const defaultProps = { actionSlug: 'atualizar-votos', municipalityId: 1, municipalityName: 'Cairu', - municipalitySlug: 'cairu', initialExpectedVotes: { pessimistic: 100, central: 200, optimistic: 300 }, } diff --git a/tests/unit/wizardVoteEstimate.unit.spec.ts b/tests/unit/wizardVoteEstimate.unit.spec.ts index 6bbc98cb..885365b0 100644 --- a/tests/unit/wizardVoteEstimate.unit.spec.ts +++ b/tests/unit/wizardVoteEstimate.unit.spec.ts @@ -1,12 +1,7 @@ import { describe, expect, it } from 'vitest' import { MAX_VOTE_COUNT } from '@/lib/schemas/primitives' -import { - applyVoteShortcut, - getWizardVoteViolation, - getWizardVoteViolationHighlights, - parseWizardVoteDraft, -} from '@/lib/wizardVoteEstimate' +import { applyVoteShortcut, getWizardVoteViolation } from '@/lib/wizardVoteEstimate' describe('applyVoteShortcut', () => { it('doubles the current value', () => { @@ -31,49 +26,23 @@ describe('applyVoteShortcut', () => { }) }) -describe('parseWizardVoteDraft', () => { - it('parses digits and empty as null', () => { - expect(parseWizardVoteDraft('')).toBeNull() - expect(parseWizardVoteDraft(' 1.200 ')).toBe(1200) - expect(parseWizardVoteDraft('abc')).toBeNull() - }) -}) - describe('getWizardVoteViolation', () => { it('returns null for ordered triples', () => { expect(getWizardVoteViolation({ pessimistic: 100, central: 200, optimistic: 300 })).toBeNull() }) - it('flags pessimista above média with readable message', () => { + it('flags pessimista above média with readable message and highlights', () => { const violation = getWizardVoteViolation({ pessimistic: 300, central: 200, optimistic: 400 }) expect(violation?.violatingScenario).toBe('central') expect(violation?.message).toContain('Pessimista') expect(violation?.message).toContain('média') + expect(violation?.highlightScenarios).toEqual(['pessimistic', 'central']) }) - it('flags otimista below média', () => { + it('flags otimista below média with highlights', () => { const violation = getWizardVoteViolation({ pessimistic: 100, central: 500, optimistic: 400 }) expect(violation?.violatingScenario).toBe('optimistic') expect(violation?.message).toContain('Otimista') - }) -}) - -describe('getWizardVoteViolationHighlights', () => { - it('highlights pessimista and média when pessimista is above média', () => { - expect( - getWizardVoteViolationHighlights({ pessimistic: 300, central: 200, optimistic: 400 }), - ).toEqual(['pessimistic', 'central']) - }) - - it('highlights otimista and média when otimista is below média', () => { - expect( - getWizardVoteViolationHighlights({ pessimistic: 100, central: 500, optimistic: 400 }), - ).toEqual(['optimistic', 'central']) - }) - - it('returns empty when estimates are ordered', () => { - expect( - getWizardVoteViolationHighlights({ pessimistic: 100, central: 200, optimistic: 300 }), - ).toEqual([]) + expect(violation?.highlightScenarios).toEqual(['optimistic', 'central']) }) })