Merge pull request #77 from fsolla/cursor/pool-i19-c4629a49-6cdb

B83 — Ações rápidas — Dobradinhas
This commit is contained in:
Francisco Solla 2026-08-01 03:22:15 -03:00 committed by GitHub
commit 14c8f79ff9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 123 additions and 2 deletions

View file

@ -1,7 +1,7 @@
# Ações rápidas — Dobradinhas
Status: rascunho
Atualizado em: 2026-07-30
Status: entregue (B83 — #19)
Atualizado em: 2026-08-01
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item **B83**)
Impeccable: B — drawer B79 em `/dobradinhas` (+ detalhe se houver)
Appetite: ~0,25–0,5 dia eng; sem migration
@ -63,3 +63,9 @@ Nenhum neste item.
## Referências
`dobradinhas/**` · B79 · `campaignHomeActions.ts`
## As-built (#19)
- Registry: `campaignQuickActionDobradinhas.ts` — lista (`Nova dobradinha` + 6 ações Início sem prefill), detalhe/nova (só Início).
- Atalho “Municípios desta dobradinha” **omitido** — `municipalityListUrl` não expõe filtro por `stateDeputy`.
- WA share (B55) **omitido** no drawer (recomendação A do plano).

View file

@ -0,0 +1,52 @@
import { Plus } from 'lucide-react'
import { resolveStaffHomeQuickActions } from '@/lib/campaignHomeActions'
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
const STATE_DEPUTY_LIST_PATH = '/campanha/dobradinhas' as const
const STATE_DEPUTY_CREATE_PATH = '/campanha/dobradinhas/nova' as const
const STATE_DEPUTY_PATH_PREFIX = `${STATE_DEPUTY_LIST_PATH}/` as const
const isStateDeputyListPath = (pathname: string): boolean =>
pathname === STATE_DEPUTY_LIST_PATH || pathname === `${STATE_DEPUTY_LIST_PATH}/`
const isStateDeputyCreatePath = (pathname: string): boolean => pathname === STATE_DEPUTY_CREATE_PATH
export const parseStateDeputyDetailSlug = (pathname: string): string | undefined => {
if (!pathname.startsWith(STATE_DEPUTY_PATH_PREFIX)) return undefined
const slug = pathname.slice(STATE_DEPUTY_PATH_PREFIX.length)
if (!slug || slug.includes('/') || slug === 'nova') return undefined
return slug
}
export const matchesDobradinhasQuickActionSurface = (pathname: string): boolean =>
isStateDeputyListPath(pathname) ||
isStateDeputyCreatePath(pathname) ||
parseStateDeputyDetailSlug(pathname) !== undefined
const newStateDeputyAction = (): CampaignQuickAction => ({
id: 'new-state-deputy',
label: 'Nova dobradinha',
icon: Plus,
description: 'Cadastrar um deputado estadual parceiro da campanha',
href: STATE_DEPUTY_CREATE_PATH,
})
/** B83 — `/dobradinhas` list, detail and create surfaces. */
export const resolveDobradinhasQuickActions = (
pathname: string,
role: CampaignRole,
): readonly CampaignQuickAction[] => {
if (!matchesDobradinhasQuickActionSurface(pathname) || !isStaffCampaignRole(role)) {
return []
}
const staffActions = resolveStaffHomeQuickActions(role)
if (isStateDeputyListPath(pathname)) {
return [newStateDeputyAction(), ...staffActions]
}
return staffActions
}

View file

@ -10,6 +10,10 @@ import {
resolveDemandDetailQuickActions,
resolveDemandsListQuickActions,
} from '@/lib/campaignQuickActionDemands'
import {
matchesDobradinhasQuickActionSurface,
resolveDobradinhasQuickActions,
} from '@/lib/campaignQuickActionDobradinhas'
import { isLeaderContactsPath } from '@/lib/campaignQuickActionMount'
import {
parseActivityQuickActionSurface,
@ -57,6 +61,10 @@ export const resolveQuickActionsForPath = (
const municipalityActions = resolveMunicipalityQuickActionsForPath(pathname, role, context)
if (municipalityActions.length > 0) return municipalityActions
if (matchesDobradinhasQuickActionSurface(pathname)) {
return resolveDobradinhasQuickActions(pathname, role)
}
const advisorActions = resolveAdvisorQuickActionsForPath(pathname, role, context)
if (advisorActions.length > 0) return advisorActions

View file

@ -12,6 +12,11 @@ import {
resolveDemandDetailQuickActions,
resolveDemandsListQuickActions,
} from '@/lib/campaignQuickActionDemands'
import {
matchesDobradinhasQuickActionSurface,
parseStateDeputyDetailSlug,
resolveDobradinhasQuickActions,
} from '@/lib/campaignQuickActionDobradinhas'
import {
isCampaignActionsPath,
isCampaignHomePath,
@ -80,6 +85,11 @@ describe('campaignQuickActionMount', () => {
expect(shouldMountQuickActionsDrawer('/campanha/assessores', 'advisor')).toBe(false)
expect(shouldMountQuickActionsDrawer('/campanha/assessores/12', 'advisor')).toBe(false)
})
it('mounts on dobradinhas for staff (B83)', () => {
expect(shouldMountQuickActionsDrawer('/campanha/dobradinhas', 'coordinator')).toBe(true)
expect(shouldMountQuickActionsDrawer('/campanha/dobradinhas/foo', 'advisor')).toBe(true)
})
})
describe('campaignQuickActionPaths (activities)', () => {
@ -182,6 +192,46 @@ describe('activityQuickActions (B84)', () => {
})
})
describe('campaignQuickActionDobradinhas (B83)', () => {
const staffActionIds = homeActionsForRole('coordinator').map((action) => action.id)
it('recognizes list, create and detail surfaces', () => {
expect(matchesDobradinhasQuickActionSurface('/campanha/dobradinhas')).toBe(true)
expect(matchesDobradinhasQuickActionSurface('/campanha/dobradinhas/nova')).toBe(true)
expect(matchesDobradinhasQuickActionSurface('/campanha/dobradinhas/eduardo-alves')).toBe(true)
expect(matchesDobradinhasQuickActionSurface('/campanha/municipios')).toBe(false)
})
it('parses detail slug and rejects nova', () => {
expect(parseStateDeputyDetailSlug('/campanha/dobradinhas/eduardo-alves')).toBe('eduardo-alves')
expect(parseStateDeputyDetailSlug('/campanha/dobradinhas/nova')).toBeUndefined()
})
it('returns list catalog with Nova dobradinha plus staff Início actions without prefill', () => {
const actions = resolveDobradinhasQuickActions('/campanha/dobradinhas', 'coordinator')
expect(actions.map((action) => action.id)).toEqual(['new-state-deputy', ...staffActionIds])
expect(actions[0]?.href).toBe('/campanha/dobradinhas/nova')
expect(actions.find((action) => action.id === 'update-votes')?.href).toBe(
'/campanha/acoes/atualizar-votos',
)
expect(actions.find((action) => action.id === 'uncovered-municipalities')?.href).toBe(
UNCOVERED_MUNICIPALITIES_LIST_HREF,
)
})
it('returns staff actions without Nova on detail and create', () => {
for (const pathname of ['/campanha/dobradinhas/eduardo-alves', '/campanha/dobradinhas/nova']) {
const actions = resolveDobradinhasQuickActions(pathname, 'advisor')
expect(actions.map((action) => action.id)).toEqual(staffActionIds)
expect(actions.some((action) => action.id === 'new-state-deputy')).toBe(false)
}
})
it('returns empty for leader lockdown', () => {
expect(resolveDobradinhasQuickActions('/campanha/dobradinhas', 'leader')).toEqual([])
})
})
describe('campaignQuickActionRegistry', () => {
const staffActionIds = homeActionsForRole('coordinator').map((action) => action.id)
@ -221,6 +271,11 @@ describe('campaignQuickActionRegistry', () => {
expect(actions.map((action) => action.id)).toEqual(['new-activity', 'plan-tour'])
})
it('delegates dobradinhas paths to the B83 catalog', () => {
const actions = resolveQuickActionsForPath('/campanha/dobradinhas', 'coordinator', {})
expect(actions.some((action) => action.id === 'new-state-deputy')).toBe(true)
})
it('builds wizard hrefs with municipality slug from context', () => {
const actions = resolveQuickActionsForPath(
'/campanha/atividades/evento-zona-1',