From 8c9eeaf273bdfb25258a399f2c49713acd8ecb74 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 04:06:22 +0000 Subject: [PATCH 1/3] =?UTF-8?q?B81=20=E2=80=94=20register=20staff=20In?= =?UTF-8?q?=C3=ADcio=20quick=20actions=20on=20/campanha/territorios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Solla --- src/lib/campaignHomeActions.ts | 12 +++++++++ src/lib/campaignQuickActionRegistry.ts | 19 ++++++++++++--- tests/unit/campaignQuickAction.unit.spec.ts | 27 ++++++++++++++++++++- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/lib/campaignHomeActions.ts b/src/lib/campaignHomeActions.ts index 40da5fc4..cd04fe35 100644 --- a/src/lib/campaignHomeActions.ts +++ b/src/lib/campaignHomeActions.ts @@ -124,6 +124,18 @@ export type ResolvedCampaignHomeAction = CampaignHomeAction & { href?: string } +/** Mirrors Início `buildMunicipalityListHref({ coverage: 'sem_assessor', sort: 'votos' }, 1)`. */ +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 const resolveStaffHomeQuickActions = ( + role: CampaignRole, +): readonly ResolvedCampaignHomeAction[] => { + if (!isStaffCampaignRole(role)) return [] + return toHomeActionButtonProps(homeActionsForRole(role), UNCOVERED_MUNICIPALITIES_LIST_HREF) +} + export const toHomeActionButtonProps = ( actions: readonly CampaignHomeAction[], uncoveredMunicipalitiesHref?: string, diff --git a/src/lib/campaignQuickActionRegistry.ts b/src/lib/campaignQuickActionRegistry.ts index ff2f3eec..d3960cd9 100644 --- a/src/lib/campaignQuickActionRegistry.ts +++ b/src/lib/campaignQuickActionRegistry.ts @@ -1,14 +1,25 @@ -import type { ResolvedCampaignHomeAction } from '@/lib/campaignHomeActions' +import { + resolveStaffHomeQuickActions, + type ResolvedCampaignHomeAction, +} from '@/lib/campaignHomeActions' import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext' import type { CampaignRole } from '@/lib/campaignRoles' +const isTerritoriesListPath = (pathname: string): boolean => + pathname === '/campanha/territorios' || pathname.startsWith('/campanha/territorios/') + /** * 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). */ export const resolveQuickActionsForPath = ( - _pathname: string, - _role: CampaignRole, + pathname: string, + role: CampaignRole, _context: CampaignQuickActionContext, -): readonly ResolvedCampaignHomeAction[] => [] +): readonly ResolvedCampaignHomeAction[] => { + if (isTerritoriesListPath(pathname)) { + return resolveStaffHomeQuickActions(role) + } + return [] +} diff --git a/tests/unit/campaignQuickAction.unit.spec.ts b/tests/unit/campaignQuickAction.unit.spec.ts index 9447fcaa..5322d5e2 100644 --- a/tests/unit/campaignQuickAction.unit.spec.ts +++ b/tests/unit/campaignQuickAction.unit.spec.ts @@ -12,6 +12,10 @@ import { QUICK_ACTIONS_SNAP_EXPANDED, quickActionsSnapIsExpanded, } from '@/lib/campaignQuickActionSnap' +import { + UNCOVERED_MUNICIPALITIES_LIST_HREF, + homeActionsForRole, +} from '@/lib/campaignHomeActions' describe('campaignQuickActionMount', () => { it('treats Início as exact match only', () => { @@ -34,6 +38,7 @@ describe('campaignQuickActionMount', () => { it('mounts for staff outside Início and acoes', () => { expect(shouldMountQuickActionsDrawer('/campanha/municipios', 'coordinator')).toBe(true) + expect(shouldMountQuickActionsDrawer('/campanha/territorios', 'coordinator')).toBe(true) expect(shouldMountQuickActionsDrawer('/campanha', 'coordinator')).toBe(false) expect(shouldMountQuickActionsDrawer('/campanha/acoes/registrar-sinal', 'advisor')).toBe(false) }) @@ -45,13 +50,33 @@ describe('campaignQuickActionMount', () => { }) describe('campaignQuickActionRegistry', () => { - it('returns empty catalog until B80+ register providers', () => { + const staffActionIds = homeActionsForRole('coordinator').map((action) => action.id) + + it('returns empty catalog for unregistered paths', () => { expect( resolveQuickActionsForPath('/campanha/municipios/foo', 'coordinator', { municipalitySlug: 'foo', }), ).toEqual([]) }) + + it('returns staff Início catalog on territorios without municipality prefill (B81)', () => { + const actions = resolveQuickActionsForPath('/campanha/territorios', 'coordinator', {}) + expect(actions.map((action) => action.id)).toEqual(staffActionIds) + 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, + ) + for (const action of actions) { + expect(action.href).not.toContain('municipio=') + } + }) + + it('returns empty catalog on territorios for leader lockdown', () => { + expect(resolveQuickActionsForPath('/campanha/territorios', 'leader', {})).toEqual([]) + }) }) describe('campaignQuickActionSnap', () => { From 7f2ac2936814ac8a476ea9773859b41f8fa914df Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 04:09:35 +0000 Subject: [PATCH 2/3] =?UTF-8?q?B81=20=E2=80=94=20simplify:=20path=20consta?= =?UTF-8?q?nt,=20role=20cache,=20href=20parity=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Francisco Solla --- src/lib/campaignHomeActions.ts | 17 ++++++++++++--- src/lib/campaignPaths.ts | 2 ++ src/lib/campaignQuickActionRegistry.ts | 4 +++- tests/unit/campaignHomeActions.unit.spec.ts | 23 +++++++++++++++++---- 4 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/lib/campaignHomeActions.ts b/src/lib/campaignHomeActions.ts index cd04fe35..f1255cfa 100644 --- a/src/lib/campaignHomeActions.ts +++ b/src/lib/campaignHomeActions.ts @@ -16,8 +16,7 @@ import { type CampaignWizardActionId, } from '@/lib/campaignActionRoutes' import { LEADER_CONTACTS_HOME } from '@/lib/campaignPaths' -import type { CampaignRole } from '@/lib/campaignRoles' -import { isStaffCampaignRole } from '@/lib/campaignRoles' +import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles' type CampaignHomeActionId = | 'update-votes' @@ -129,11 +128,23 @@ 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+). */ +const staffHomeQuickActionsByRole = new Map< + CampaignRole, + readonly ResolvedCampaignHomeAction[] +>() + export const resolveStaffHomeQuickActions = ( role: CampaignRole, ): readonly ResolvedCampaignHomeAction[] => { if (!isStaffCampaignRole(role)) return [] - return toHomeActionButtonProps(homeActionsForRole(role), UNCOVERED_MUNICIPALITIES_LIST_HREF) + const cached = staffHomeQuickActionsByRole.get(role) + if (cached) return cached + const actions = toHomeActionButtonProps( + homeActionsForRole(role), + UNCOVERED_MUNICIPALITIES_LIST_HREF, + ) + staffHomeQuickActionsByRole.set(role, actions) + return actions } export const toHomeActionButtonProps = ( diff --git a/src/lib/campaignPaths.ts b/src/lib/campaignPaths.ts index cec8b080..03d540f5 100644 --- a/src/lib/campaignPaths.ts +++ b/src/lib/campaignPaths.ts @@ -1,4 +1,6 @@ /** Client-safe `/campanha` route paths shared by nav, gates, and home actions. */ export const CAMPAIGN_HOME = '/campanha' as const +export const CAMPAIGN_TERRITORIES_HOME = '/campanha/territorios' as const + export const LEADER_CONTACTS_HOME = '/campanha/contatos' as const diff --git a/src/lib/campaignQuickActionRegistry.ts b/src/lib/campaignQuickActionRegistry.ts index d3960cd9..cbfe95f1 100644 --- a/src/lib/campaignQuickActionRegistry.ts +++ b/src/lib/campaignQuickActionRegistry.ts @@ -3,10 +3,12 @@ import { type ResolvedCampaignHomeAction, } from '@/lib/campaignHomeActions' import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext' +import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths' import type { CampaignRole } from '@/lib/campaignRoles' const isTerritoriesListPath = (pathname: string): boolean => - pathname === '/campanha/territorios' || pathname.startsWith('/campanha/territorios/') + pathname === CAMPAIGN_TERRITORIES_HOME || + pathname.startsWith(`${CAMPAIGN_TERRITORIES_HOME}/`) /** * Pathname + role + page context → contextual quick actions. diff --git a/tests/unit/campaignHomeActions.unit.spec.ts b/tests/unit/campaignHomeActions.unit.spec.ts index 627b1bb1..0caae0a1 100644 --- a/tests/unit/campaignHomeActions.unit.spec.ts +++ b/tests/unit/campaignHomeActions.unit.spec.ts @@ -1,7 +1,12 @@ import { describe, expect, it } from 'vitest' -import { homeActionsForRole, toHomeActionButtonProps } from '@/lib/campaignHomeActions' +import { + homeActionsForRole, + toHomeActionButtonProps, + UNCOVERED_MUNICIPALITIES_LIST_HREF, +} from '@/lib/campaignHomeActions' import { LEADER_CONTACTS_HOME } from '@/lib/campaignPaths' +import { buildMunicipalityListHref } from '@/utilities/municipality/municipalityListUrl' const staffActionIds = [ 'update-votes', @@ -48,10 +53,12 @@ describe('toHomeActionButtonProps', () => { }) it('wires uncovered-municipalities when href is provided', () => { - const href = '/campanha/municipios?coverage=sem_assessor&sort=votos' - const props = toHomeActionButtonProps(homeActionsForRole('coordinator'), href) + const props = toHomeActionButtonProps( + homeActionsForRole('coordinator'), + UNCOVERED_MUNICIPALITIES_LIST_HREF, + ) const uncovered = props.find((action) => action.id === 'uncovered-municipalities') - expect(uncovered?.href).toBe(href) + expect(uncovered?.href).toBe(UNCOVERED_MUNICIPALITIES_LIST_HREF) const updateVotes = props.find((action) => action.id === 'update-votes') expect(updateVotes?.href).toBe('/campanha/acoes/atualizar-votos') const registerSignal = props.find((action) => action.id === 'register-signal') @@ -63,3 +70,11 @@ describe('toHomeActionButtonProps', () => { expect(props.find((action) => action.id === 'uncovered-municipalities')?.href).toBeUndefined() }) }) + +describe('UNCOVERED_MUNICIPALITIES_LIST_HREF', () => { + it('matches canonical municipality list builder (client/server parity)', () => { + expect( + buildMunicipalityListHref({ page: 1, coverage: 'sem_assessor', sort: 'votos' }, 1), + ).toBe(UNCOVERED_MUNICIPALITIES_LIST_HREF) + }) +}) From 09e76ec8cbee00f86ff2dfa63717ac5b6d8c6b02 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 04:10:16 +0000 Subject: [PATCH 3/3] =?UTF-8?q?B81=20=E2=80=94=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/campaignHomeActions.ts | 5 +---- src/lib/campaignQuickActionRegistry.ts | 5 ++--- tests/unit/campaignHomeActions.unit.spec.ts | 6 +++--- tests/unit/campaignQuickAction.unit.spec.ts | 5 +---- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/lib/campaignHomeActions.ts b/src/lib/campaignHomeActions.ts index f1255cfa..6be195c9 100644 --- a/src/lib/campaignHomeActions.ts +++ b/src/lib/campaignHomeActions.ts @@ -128,10 +128,7 @@ 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+). */ -const staffHomeQuickActionsByRole = new Map< - CampaignRole, - readonly ResolvedCampaignHomeAction[] ->() +const staffHomeQuickActionsByRole = new Map() export const resolveStaffHomeQuickActions = ( role: CampaignRole, diff --git a/src/lib/campaignQuickActionRegistry.ts b/src/lib/campaignQuickActionRegistry.ts index cbfe95f1..dd8f9f9d 100644 --- a/src/lib/campaignQuickActionRegistry.ts +++ b/src/lib/campaignQuickActionRegistry.ts @@ -2,13 +2,12 @@ import { resolveStaffHomeQuickActions, type ResolvedCampaignHomeAction, } from '@/lib/campaignHomeActions' -import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext' import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths' +import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext' import type { CampaignRole } from '@/lib/campaignRoles' const isTerritoriesListPath = (pathname: string): boolean => - pathname === CAMPAIGN_TERRITORIES_HOME || - pathname.startsWith(`${CAMPAIGN_TERRITORIES_HOME}/`) + pathname === CAMPAIGN_TERRITORIES_HOME || pathname.startsWith(`${CAMPAIGN_TERRITORIES_HOME}/`) /** * Pathname + role + page context → contextual quick actions. diff --git a/tests/unit/campaignHomeActions.unit.spec.ts b/tests/unit/campaignHomeActions.unit.spec.ts index 0caae0a1..23b9847f 100644 --- a/tests/unit/campaignHomeActions.unit.spec.ts +++ b/tests/unit/campaignHomeActions.unit.spec.ts @@ -73,8 +73,8 @@ describe('toHomeActionButtonProps', () => { describe('UNCOVERED_MUNICIPALITIES_LIST_HREF', () => { it('matches canonical municipality list builder (client/server parity)', () => { - expect( - buildMunicipalityListHref({ page: 1, coverage: 'sem_assessor', sort: 'votos' }, 1), - ).toBe(UNCOVERED_MUNICIPALITIES_LIST_HREF) + expect(buildMunicipalityListHref({ page: 1, coverage: 'sem_assessor', sort: 'votos' }, 1)).toBe( + UNCOVERED_MUNICIPALITIES_LIST_HREF, + ) }) }) diff --git a/tests/unit/campaignQuickAction.unit.spec.ts b/tests/unit/campaignQuickAction.unit.spec.ts index 5322d5e2..0d59a4e6 100644 --- a/tests/unit/campaignQuickAction.unit.spec.ts +++ b/tests/unit/campaignQuickAction.unit.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest' +import { UNCOVERED_MUNICIPALITIES_LIST_HREF, homeActionsForRole } from '@/lib/campaignHomeActions' import { isCampaignActionsPath, isCampaignHomePath, @@ -12,10 +13,6 @@ import { QUICK_ACTIONS_SNAP_EXPANDED, quickActionsSnapIsExpanded, } from '@/lib/campaignQuickActionSnap' -import { - UNCOVERED_MUNICIPALITIES_LIST_HREF, - homeActionsForRole, -} from '@/lib/campaignHomeActions' describe('campaignQuickActionMount', () => { it('treats Início as exact match only', () => {