B80 — Ações rápidas em Municípios (lista + detalhe A1–A5)

Registra catálogo staff no drawer B79 para /campanha/municipios e
/campanha/municipios/[slug]: lista espelha Início (sem prefill); detalhe
prefill ?municipio= nos wizards A1–A5 e omite Ver esquecidos.

Closes #16

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-01 04:07:07 +00:00
parent a638561ce9
commit 6030d42e2f
No known key found for this signature in database
7 changed files with 198 additions and 22 deletions

View file

@ -7,10 +7,10 @@ import { CampaignHomeLayout } from '@/components/campaign/dashboard/CampaignHome
import { CampaignHomeStaffChrome } from '@/components/campaign/dashboard/CampaignHomeStaffChrome'
import { CampaignHomeSummary } from '@/components/campaign/dashboard/CampaignHomeSummary'
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
import { UNCOVERED_MUNICIPALITIES_LIST_HREF } from '@/lib/campaignHomeActions'
import { isStaffCampaignRole } from '@/lib/campaignRoles'
import { loadCampaignHomeSummary } from '@/utilities/campaignDashboardData'
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
import { buildMunicipalityListHref } from '@/utilities/municipality/municipalityListUrl'
export const dynamic = 'force-dynamic'
@ -19,9 +19,7 @@ export default async function CampaignHomePage() {
const staff = isStaffCampaignRole(user.role)
const uncoveredMunicipalitiesHref = staff
? buildMunicipalityListHref({ page: 1, coverage: 'sem_assessor', sort: 'votos' }, 1)
: undefined
const uncoveredMunicipalitiesHref = staff ? UNCOVERED_MUNICIPALITIES_LIST_HREF : undefined
const actions = (
<CampaignHomeActions

View file

@ -11,8 +11,10 @@ import {
} from 'lucide-react'
import {
CAMPAIGN_WIZARD_ACTION_SLUGS,
campaignActionEntryHref,
isCampaignWizardActionId,
wizardActionHref,
type CampaignWizardActionId,
} from '@/lib/campaignActionRoutes'
import { LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
@ -127,7 +129,12 @@ export type ResolvedCampaignHomeAction = CampaignHomeAction & {
export const UNCOVERED_MUNICIPALITIES_LIST_HREF =
'/campanha/municipios?coverage=sem_assessor&sort=votos' as const
/** Staff Início catalog with hrefs — shared by quick-action registries (B81+). */
export type HomeActionButtonPropsOptions = {
uncoveredMunicipalitiesHref?: string
municipalitySlug?: string
}
/** Staff Início catalog with hrefs — shared by quick-action registries (B80+). */
const staffHomeQuickActionsByRole = new Map<CampaignRole, readonly ResolvedCampaignHomeAction[]>()
export const resolveStaffHomeQuickActions = (
@ -136,29 +143,34 @@ export const resolveStaffHomeQuickActions = (
if (!isStaffCampaignRole(role)) return []
const cached = staffHomeQuickActionsByRole.get(role)
if (cached) return cached
const actions = toHomeActionButtonProps(
homeActionsForRole(role),
UNCOVERED_MUNICIPALITIES_LIST_HREF,
)
const actions = toHomeActionButtonProps(homeActionsForRole(role), {
uncoveredMunicipalitiesHref: UNCOVERED_MUNICIPALITIES_LIST_HREF,
})
staffHomeQuickActionsByRole.set(role, actions)
return actions
}
export const toHomeActionButtonProps = (
actions: readonly CampaignHomeAction[],
uncoveredMunicipalitiesHref?: string,
): ResolvedCampaignHomeAction[] =>
actions.map((action) => {
options?: string | HomeActionButtonPropsOptions,
): ResolvedCampaignHomeAction[] => {
const resolved =
typeof options === 'string' ? { uncoveredMunicipalitiesHref: options } : (options ?? {})
return actions.map((action) => {
let href: string | undefined
if (action.id === 'my-contacts') {
href = LEADER_CONTACTS_HOME
} else if (action.id === 'uncovered-municipalities') {
href = uncoveredMunicipalitiesHref
href = resolved.uncoveredMunicipalitiesHref
} else if (isCampaignWizardActionId(action.id)) {
href = campaignActionEntryHref(action.id)
href = resolved.municipalitySlug
? wizardActionHref(CAMPAIGN_WIZARD_ACTION_SLUGS[action.id], resolved.municipalitySlug)
: campaignActionEntryHref(action.id)
}
return {
...action,
...(href ? { href } : {}),
}
})
}

View file

@ -0,0 +1,54 @@
import {
homeActionsForRole,
resolveStaffHomeQuickActions,
toHomeActionButtonProps,
type ResolvedCampaignHomeAction,
} from '@/lib/campaignHomeActions'
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
import type { CampaignRole } from '@/lib/campaignRoles'
import { isStaffCampaignRole } from '@/lib/campaignRoles'
const MUNICIPALITIES_LIST_PATH = '/campanha/municipios' as const
const municipalityDetailPathPattern = /^\/campanha\/municipios\/([^/]+)(?:\/|$)/
export const isMunicipalitiesListPath = (pathname: string): boolean =>
pathname === MUNICIPALITIES_LIST_PATH || pathname === `${MUNICIPALITIES_LIST_PATH}/`
export const parseMunicipalityDetailSlug = (pathname: string): string | undefined => {
const match = pathname.match(municipalityDetailPathPattern)
return match?.[1]
}
export const resolveMunicipalityListQuickActions = (
role: CampaignRole,
): readonly ResolvedCampaignHomeAction[] => resolveStaffHomeQuickActions(role)
export const resolveMunicipalityDetailQuickActions = (
role: CampaignRole,
municipalitySlug: string,
): readonly ResolvedCampaignHomeAction[] => {
if (!isStaffCampaignRole(role)) return []
const actions = homeActionsForRole(role).filter(
(action) => action.id !== 'uncovered-municipalities',
)
return toHomeActionButtonProps(actions, { municipalitySlug })
}
export const resolveMunicipalityQuickActionsForPath = (
pathname: string,
role: CampaignRole,
context: CampaignQuickActionContext,
): readonly ResolvedCampaignHomeAction[] => {
if (isMunicipalitiesListPath(pathname)) {
return resolveMunicipalityListQuickActions(role)
}
const slug = context.municipalitySlug ?? parseMunicipalityDetailSlug(pathname)
if (slug) {
return resolveMunicipalityDetailQuickActions(role, slug)
}
return []
}

View file

@ -2,8 +2,9 @@ import {
resolveStaffHomeQuickActions,
type ResolvedCampaignHomeAction,
} from '@/lib/campaignHomeActions'
import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths'
import { resolveMunicipalityQuickActionsForPath } from '@/lib/campaignMunicipalityQuickActions'
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths'
import type { CampaignRole } from '@/lib/campaignRoles'
const isTerritoriesListPath = (pathname: string): boolean =>
@ -12,15 +13,19 @@ const isTerritoriesListPath = (pathname: string): boolean =>
/**
* Pathname + role + page context → contextual quick actions.
* Catalogs per vertical land in B80–B90; the B79 chassis renders whatever
* this helper returns (empty until those items register providers).
* this helper returns.
*/
export const resolveQuickActionsForPath = (
pathname: string,
role: CampaignRole,
_context: CampaignQuickActionContext,
context: CampaignQuickActionContext,
): readonly ResolvedCampaignHomeAction[] => {
const municipalityActions = resolveMunicipalityQuickActionsForPath(pathname, role, context)
if (municipalityActions.length > 0) return municipalityActions
if (isTerritoriesListPath(pathname)) {
return resolveStaffHomeQuickActions(role)
}
return []
}

View file

@ -69,6 +69,16 @@ describe('toHomeActionButtonProps', () => {
const props = toHomeActionButtonProps(homeActionsForRole('coordinator'))
expect(props.find((action) => action.id === 'uncovered-municipalities')?.href).toBeUndefined()
})
it('prefills wizard hrefs when municipalitySlug is provided', () => {
const props = toHomeActionButtonProps(homeActionsForRole('coordinator'), {
municipalitySlug: 'cairu',
})
expect(props.find((action) => action.id === 'update-votes')?.href).toBe(
'/campanha/acoes/atualizar-votos?municipio=cairu',
)
expect(props.find((action) => action.id === 'uncovered-municipalities')?.href).toBeUndefined()
})
})
describe('UNCOVERED_MUNICIPALITIES_LIST_HREF', () => {

View file

@ -0,0 +1,88 @@
import { describe, expect, it } from 'vitest'
import { CAMPAIGN_WIZARD_ACTION_SLUGS } from '@/lib/campaignActionRoutes'
import {
isMunicipalitiesListPath,
parseMunicipalityDetailSlug,
resolveMunicipalityDetailQuickActions,
resolveMunicipalityListQuickActions,
resolveMunicipalityQuickActionsForPath,
} from '@/lib/campaignMunicipalityQuickActions'
import { UNCOVERED_MUNICIPALITIES_LIST_HREF } from '@/lib/campaignHomeActions'
const wizardActionIds = [
'update-votes',
'register-signal',
'change-trend',
'update-leadership',
'register-demand',
] as const
describe('campaignMunicipalityQuickActions paths', () => {
it('detects the municipality list path', () => {
expect(isMunicipalitiesListPath('/campanha/municipios')).toBe(true)
expect(isMunicipalitiesListPath('/campanha/municipios/')).toBe(true)
expect(isMunicipalitiesListPath('/campanha/municipios/cairu')).toBe(false)
})
it('parses municipality detail slugs', () => {
expect(parseMunicipalityDetailSlug('/campanha/municipios/cairu')).toBe('cairu')
expect(parseMunicipalityDetailSlug('/campanha/municipios/cairu/editar')).toBe('cairu')
expect(parseMunicipalityDetailSlug('/campanha/municipios')).toBeUndefined()
})
})
describe('resolveMunicipalityListQuickActions', () => {
it('mirrors the Início staff catalog without municipality prefill', () => {
const actions = resolveMunicipalityListQuickActions('coordinator')
expect(actions.map((action) => action.id)).toEqual([
...wizardActionIds,
'uncovered-municipalities',
])
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 nothing for leader', () => {
expect(resolveMunicipalityListQuickActions('leader')).toEqual([])
})
})
describe('resolveMunicipalityDetailQuickActions', () => {
it('prefills wizard hrefs and omits uncovered municipalities', () => {
const actions = resolveMunicipalityDetailQuickActions('coordinator', 'cairu')
expect(actions.map((action) => action.id)).toEqual([...wizardActionIds])
expect(actions.find((action) => action.id === 'update-votes')?.href).toBe(
`/campanha/acoes/${CAMPAIGN_WIZARD_ACTION_SLUGS['update-votes']}?municipio=cairu`,
)
expect(actions.some((action) => action.id === 'uncovered-municipalities')).toBe(false)
})
})
describe('resolveMunicipalityQuickActionsForPath', () => {
it('routes list and detail paths', () => {
const list = resolveMunicipalityQuickActionsForPath('/campanha/municipios', 'advisor', {})
expect(list).toHaveLength(6)
const detail = resolveMunicipalityQuickActionsForPath(
'/campanha/municipios/feira-de-santana',
'candidate',
{},
)
expect(detail).toHaveLength(5)
expect(detail[0]?.href).toContain('municipio=feira-de-santana')
})
it('prefers context slug over pathname', () => {
const actions = resolveMunicipalityQuickActionsForPath(
'/campanha/municipios/feira-de-santana',
'coordinator',
{ municipalitySlug: 'cairu' },
)
expect(actions.find((action) => action.id === 'update-votes')?.href).toContain('municipio=cairu')
})
})

View file

@ -50,11 +50,20 @@ describe('campaignQuickActionRegistry', () => {
const staffActionIds = homeActionsForRole('coordinator').map((action) => action.id)
it('returns empty catalog for unregistered paths', () => {
expect(
resolveQuickActionsForPath('/campanha/municipios/foo', 'coordinator', {
municipalitySlug: 'foo',
}),
).toEqual([])
expect(resolveQuickActionsForPath('/campanha/apoiadores', 'coordinator', {})).toEqual([])
})
it('delegates municipality list and detail catalogs (B80)', () => {
const list = resolveQuickActionsForPath('/campanha/municipios', 'coordinator', {})
expect(list).toHaveLength(6)
const detail = resolveQuickActionsForPath('/campanha/municipios/foo', 'coordinator', {
municipalitySlug: 'foo',
})
expect(detail).toHaveLength(5)
expect(detail.find((action) => action.id === 'update-votes')?.href).toBe(
'/campanha/acoes/atualizar-votos?municipio=foo',
)
})
it('returns staff Início catalog on territorios without municipality prefill (B81)', () => {