mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-15 02:23:07 +00:00
B77+: simplify vote wizard helpers after single-screen delivery.
Unify violation highlights with getWizardVoteViolation, share the expected-votes save error constant, and drop dead parseWizardVoteDraft plus unused municipalitySlug prop.
This commit is contained in:
parent
f2bdb1ce3c
commit
625f25c984
9 changed files with 36 additions and 85 deletions
|
|
@ -67,7 +67,6 @@ export default async function CampaignActionWizardPage({
|
|||
actionSlug={slug}
|
||||
municipalityId={municipality.id}
|
||||
municipalityName={municipality.name}
|
||||
municipalitySlug={municipality.slug}
|
||||
initialExpectedVotes={toVoteEstimateScenarioViewModel(municipality.expectedVotes)}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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<VoteEstimateScenario>('central')
|
||||
const [violationMessage, setViolationMessage] = useState<string | null>(null)
|
||||
const [highlightScenarios, setHighlightScenarios] = useState<ReadonlySet<VoteEstimateScenario>>(
|
||||
new Set(),
|
||||
)
|
||||
const [violation, setViolation] = useState<WizardVoteViolation | null>(null)
|
||||
const [saveError, setSaveError] = useState<string | null>(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 = ({
|
|||
>
|
||||
<div className="flex flex-col gap-4" role="status" aria-live="polite">
|
||||
<p className="text-sm text-muted-foreground">{WIZARD_VOTES_SAVED_MESSAGE}</p>
|
||||
<p className="text-sm text-muted-foreground">{WIZARD_VOTES_NEXT_FLOW_PLACEHOLDER}</p>
|
||||
<p className="text-sm text-muted-foreground">{wizardNextStepPlaceholder(actionSlug)}</p>
|
||||
<Button asChild className="min-h-11 w-full sm:w-auto">
|
||||
<Link href={CAMPAIGN_HOME}>Voltar ao Início</Link>
|
||||
</Button>
|
||||
|
|
@ -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 = ({
|
|||
))}
|
||||
</div>
|
||||
|
||||
{violationMessage ? (
|
||||
{violation ? (
|
||||
<Alert variant="pending" className="border-estimate-pending">
|
||||
<AlertDescription className="text-sm">{violationMessage}</AlertDescription>
|
||||
<AlertDescription className="text-sm">{violation.message}</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
|
|
@ -197,7 +194,7 @@ export const WizardExpectedVotesStep = ({
|
|||
Salvando…
|
||||
</>
|
||||
) : (
|
||||
wizardVoteFinalCtaLabel
|
||||
WIZARD_VOTES_FINAL_CTA_LABEL
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ const defaultProps = {
|
|||
actionSlug: 'atualizar-votos',
|
||||
municipalityId: 1,
|
||||
municipalityName: 'Cairu',
|
||||
municipalitySlug: 'cairu',
|
||||
initialExpectedVotes: { pessimistic: 100, central: 200, optimistic: 300 },
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue