diff --git a/docs/plans/acoes-rapidas-dobradinhas.md b/docs/plans/acoes-rapidas-dobradinhas.md index 9829a7ac..bda4b7a8 100644 --- a/docs/plans/acoes-rapidas-dobradinhas.md +++ b/docs/plans/acoes-rapidas-dobradinhas.md @@ -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). diff --git a/src/lib/campaignQuickActionDobradinhas.ts b/src/lib/campaignQuickActionDobradinhas.ts new file mode 100644 index 00000000..6b84396c --- /dev/null +++ b/src/lib/campaignQuickActionDobradinhas.ts @@ -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 +} diff --git a/src/lib/campaignQuickActionRegistry.ts b/src/lib/campaignQuickActionRegistry.ts index 06643fff..0e35e495 100644 --- a/src/lib/campaignQuickActionRegistry.ts +++ b/src/lib/campaignQuickActionRegistry.ts @@ -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 diff --git a/tests/unit/campaignQuickAction.unit.spec.ts b/tests/unit/campaignQuickAction.unit.spec.ts index 4c906c9b..30ef56bf 100644 --- a/tests/unit/campaignQuickAction.unit.spec.ts +++ b/tests/unit/campaignQuickAction.unit.spec.ts @@ -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',