From 6030d42e2f022c7464cce4b6d4f27b00062aa226 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 04:07:07 +0000 Subject: [PATCH] =?UTF-8?q?B80=20=E2=80=94=20A=C3=A7=C3=B5es=20r=C3=A1pida?= =?UTF-8?q?s=20em=20Munic=C3=ADpios=20(lista=20+=20detalhe=20A1=E2=80=93A5?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/(campaign)/campanha/(app)/page.tsx | 6 +- src/lib/campaignHomeActions.ts | 32 ++++--- src/lib/campaignMunicipalityQuickActions.ts | 54 ++++++++++++ src/lib/campaignQuickActionRegistry.ts | 11 ++- tests/unit/campaignHomeActions.unit.spec.ts | 10 +++ ...paignMunicipalityQuickActions.unit.spec.ts | 88 +++++++++++++++++++ tests/unit/campaignQuickAction.unit.spec.ts | 19 ++-- 7 files changed, 198 insertions(+), 22 deletions(-) create mode 100644 src/lib/campaignMunicipalityQuickActions.ts create mode 100644 tests/unit/campaignMunicipalityQuickActions.unit.spec.ts diff --git a/src/app/(campaign)/campanha/(app)/page.tsx b/src/app/(campaign)/campanha/(app)/page.tsx index 31d65982..59836e38 100644 --- a/src/app/(campaign)/campanha/(app)/page.tsx +++ b/src/app/(campaign)/campanha/(app)/page.tsx @@ -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 = ( () 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 } : {}), } }) +} diff --git a/src/lib/campaignMunicipalityQuickActions.ts b/src/lib/campaignMunicipalityQuickActions.ts new file mode 100644 index 00000000..194b9366 --- /dev/null +++ b/src/lib/campaignMunicipalityQuickActions.ts @@ -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 [] +} diff --git a/src/lib/campaignQuickActionRegistry.ts b/src/lib/campaignQuickActionRegistry.ts index dd8f9f9d..332212d0 100644 --- a/src/lib/campaignQuickActionRegistry.ts +++ b/src/lib/campaignQuickActionRegistry.ts @@ -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 [] } diff --git a/tests/unit/campaignHomeActions.unit.spec.ts b/tests/unit/campaignHomeActions.unit.spec.ts index 23b9847f..5bfbfef5 100644 --- a/tests/unit/campaignHomeActions.unit.spec.ts +++ b/tests/unit/campaignHomeActions.unit.spec.ts @@ -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', () => { diff --git a/tests/unit/campaignMunicipalityQuickActions.unit.spec.ts b/tests/unit/campaignMunicipalityQuickActions.unit.spec.ts new file mode 100644 index 00000000..21862091 --- /dev/null +++ b/tests/unit/campaignMunicipalityQuickActions.unit.spec.ts @@ -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') + }) +}) diff --git a/tests/unit/campaignQuickAction.unit.spec.ts b/tests/unit/campaignQuickAction.unit.spec.ts index 0d59a4e6..01933a8e 100644 --- a/tests/unit/campaignQuickAction.unit.spec.ts +++ b/tests/unit/campaignQuickAction.unit.spec.ts @@ -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)', () => {