diff --git a/src/components/campaign/shell/CampaignQuickActionContextBridge.tsx b/src/components/campaign/shell/CampaignQuickActionContextBridge.tsx
new file mode 100644
index 00000000..3102fa82
--- /dev/null
+++ b/src/components/campaign/shell/CampaignQuickActionContextBridge.tsx
@@ -0,0 +1,46 @@
+'use client'
+
+import { useEffect, useMemo } from 'react'
+
+import { useCampaignQuickActionContext } from '@/components/campaign/shell/CampaignQuickActionContext'
+import {
+ emptyCampaignQuickActionContext,
+ type CampaignQuickActionContext,
+} from '@/lib/campaignQuickActionContext'
+
+type CampaignQuickActionContextBridgeProps = {
+ municipalitySlug?: string
+ activitySlug?: string
+ leadershipId?: number
+ organizationSlug?: string
+ demandSlug?: string
+}
+
+/** Syncs page-local quick-action context into the B79 drawer host. */
+export const CampaignQuickActionContextBridge = ({
+ municipalitySlug,
+ activitySlug,
+ leadershipId,
+ organizationSlug,
+ demandSlug,
+}: CampaignQuickActionContextBridgeProps) => {
+ const { setContext } = useCampaignQuickActionContext()
+ const context = useMemo((): CampaignQuickActionContext => {
+ const next: CampaignQuickActionContext = {}
+ if (municipalitySlug) next.municipalitySlug = municipalitySlug
+ if (activitySlug) next.activitySlug = activitySlug
+ if (leadershipId !== undefined) next.leadershipId = leadershipId
+ if (organizationSlug) next.organizationSlug = organizationSlug
+ if (demandSlug) next.demandSlug = demandSlug
+ return next
+ }, [activitySlug, demandSlug, leadershipId, municipalitySlug, organizationSlug])
+
+ useEffect(() => {
+ setContext(context)
+ return () => {
+ setContext(emptyCampaignQuickActionContext())
+ }
+ }, [context, setContext])
+
+ return null
+}
diff --git a/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx b/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx
index 537c95a3..ef039786 100644
--- a/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx
+++ b/src/components/campaign/shell/CampaignQuickActionsDrawer.tsx
@@ -4,7 +4,7 @@ import { useCallback, useState } from 'react'
import { CampaignHomeActionStrip } from '@/components/campaign/dashboard/CampaignHomeActionStrip'
import { Drawer, DrawerContent, DrawerTitle } from '@/components/ui/Drawer'
-import type { ResolvedCampaignHomeAction } from '@/lib/campaignHomeActions'
+import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
import {
QUICK_ACTIONS_SNAP_COLLAPSED,
QUICK_ACTIONS_SNAP_EXPANDED,
@@ -19,7 +19,7 @@ const SNAP_POINTS = [...QUICK_ACTIONS_SNAP_POINTS]
export const CampaignQuickActionsDrawer = ({
actions,
}: {
- actions: readonly ResolvedCampaignHomeAction[]
+ actions: readonly CampaignQuickAction[]
}) => {
const [snapPoint, setSnapPoint] = useState(
QUICK_ACTIONS_SNAP_COLLAPSED,
diff --git a/src/lib/activityQuickActions.ts b/src/lib/activityQuickActions.ts
new file mode 100644
index 00000000..6c68821e
--- /dev/null
+++ b/src/lib/activityQuickActions.ts
@@ -0,0 +1,113 @@
+import { ClipboardList, ListChecks, MapPinned, Pencil, Plus } from 'lucide-react'
+
+import {
+ CAMPAIGN_WIZARD_ACTION_SLUGS,
+ wizardActionHref,
+ type CampaignWizardActionId,
+} from '@/lib/campaignActionRoutes'
+import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
+import {
+ ACTIVITY_NEW_PATH,
+ ACTIVITY_TOUR_COMPOSER_PATH,
+ type ActivityQuickActionSurface,
+} from '@/lib/campaignQuickActionPaths'
+import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
+import { homeActionsForRole, toHomeActionButtonProps } from '@/lib/campaignHomeActions'
+import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
+
+const WIZARD_ACTION_IDS: readonly CampaignWizardActionId[] = [
+ 'update-votes',
+ 'register-signal',
+ 'change-trend',
+ 'update-leadership',
+ 'register-demand',
+]
+
+const listQuickActions = (): readonly CampaignQuickAction[] => [
+ {
+ id: 'new-activity',
+ label: 'Nova atividade',
+ icon: Plus,
+ description: 'Criar caminhada, comício, panfletagem ou outra ação de campanha',
+ href: ACTIVITY_NEW_PATH,
+ },
+ {
+ id: 'plan-tour',
+ label: 'Planejar giro',
+ icon: MapPinned,
+ description: 'Compor um giro por território e gerar rascunhos de atividade',
+ href: ACTIVITY_TOUR_COMPOSER_PATH,
+ },
+]
+
+const wizardActionsWithMunicipalityPrefill = (
+ role: CampaignRole,
+ municipalitySlug: string,
+): readonly CampaignQuickAction[] => {
+ const homeActions = homeActionsForRole(role).filter((action) =>
+ WIZARD_ACTION_IDS.includes(action.id as CampaignWizardActionId),
+ )
+
+ return toHomeActionButtonProps(homeActions).map((action) => {
+ const wizardId = action.id as CampaignWizardActionId
+ return {
+ id: action.id,
+ label: action.label,
+ icon: action.icon,
+ description: action.description,
+ href: wizardActionHref(CAMPAIGN_WIZARD_ACTION_SLUGS[wizardId], municipalitySlug),
+ }
+ })
+}
+
+const detailVerticalQuickActions = (activitySlug: string): readonly CampaignQuickAction[] => {
+ const base = `/campanha/atividades/${activitySlug}`
+ const actions: CampaignQuickAction[] = [
+ {
+ id: 'edit-activity',
+ label: 'Editar',
+ icon: Pencil,
+ description: 'Abrir o formulário completo da atividade',
+ href: `${base}/editar`,
+ },
+ {
+ id: 'activity-tasks',
+ label: 'Tarefas',
+ icon: ListChecks,
+ description: 'Ver e marcar o checklist desta atividade',
+ href: `${base}?tab=tasks`,
+ },
+ {
+ id: 'activity-updates',
+ label: 'Atualizações',
+ icon: ClipboardList,
+ description: 'Registrar ou revisar o feed de atualizações',
+ href: `${base}?tab=updates`,
+ },
+ ]
+
+ return actions
+}
+
+export const resolveActivityQuickActions = (
+ surface: ActivityQuickActionSurface,
+ role: CampaignRole,
+ context: CampaignQuickActionContext,
+): readonly CampaignQuickAction[] => {
+ if (!isStaffCampaignRole(role)) return []
+
+ if (surface.kind === 'list') {
+ return listQuickActions()
+ }
+
+ const activitySlug = context.activitySlug ?? surface.activitySlug
+ const municipalitySlug = context.municipalitySlug
+
+ const detailActions = detailVerticalQuickActions(activitySlug)
+
+ if (!municipalitySlug) {
+ return detailActions
+ }
+
+ return [...wizardActionsWithMunicipalityPrefill(role, municipalitySlug), ...detailActions]
+}
\ No newline at end of file
diff --git a/src/lib/campaignQuickActionMount.ts b/src/lib/campaignQuickActionMount.ts
index 814bde7b..7cd0b029 100644
--- a/src/lib/campaignQuickActionMount.ts
+++ b/src/lib/campaignQuickActionMount.ts
@@ -1,4 +1,5 @@
import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes'
+import { isActivityTourComposerPath } from '@/lib/campaignQuickActionPaths'
import { CAMPAIGN_HOME, LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
@@ -19,6 +20,8 @@ export const isLeaderContactsPath = (pathname: string): boolean =>
export const shouldMountQuickActionsDrawer = (pathname: string, role: CampaignRole): boolean => {
if (isCampaignHomePath(pathname)) return false
if (isCampaignActionsPath(pathname)) return false
+ // B84: E13 compositor already exposes primary CTAs — skip competing drawer chrome.
+ if (isActivityTourComposerPath(pathname)) return false
if (role === 'leader') return isLeaderContactsPath(pathname)
return isStaffCampaignRole(role)
}
diff --git a/src/lib/campaignQuickActionPaths.ts b/src/lib/campaignQuickActionPaths.ts
new file mode 100644
index 00000000..ba9ce220
--- /dev/null
+++ b/src/lib/campaignQuickActionPaths.ts
@@ -0,0 +1,35 @@
+/** Client-safe `/campanha/atividades` path helpers for the B84 quick-action registry. */
+
+export const ACTIVITY_LIST_PATH = '/campanha/atividades' as const
+
+export const ACTIVITY_NEW_PATH = '/campanha/atividades/nova' as const
+
+export const ACTIVITY_TOUR_COMPOSER_PATH = '/campanha/atividades/giros' as const
+
+export type ActivityQuickActionSurface =
+ | { kind: 'list' }
+ | { kind: 'detail'; activitySlug: string }
+
+const normalizePathname = (pathname: string): string =>
+ pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname
+
+const activityDetailSlug = (pathname: string): string | null => {
+ const prefix = `${ACTIVITY_LIST_PATH}/`
+ if (!pathname.startsWith(prefix)) return null
+ const rest = pathname.slice(prefix.length)
+ if (!rest || rest.includes('/')) return null
+ if (rest === 'nova' || rest === 'giros') return null
+ return rest
+}
+
+export const parseActivityQuickActionSurface = (
+ pathname: string,
+): ActivityQuickActionSurface | null => {
+ const normalized = normalizePathname(pathname)
+ if (normalized === ACTIVITY_LIST_PATH) return { kind: 'list' }
+ const slug = activityDetailSlug(normalized)
+ return slug ? { kind: 'detail', activitySlug: slug } : null
+}
+
+export const isActivityTourComposerPath = (pathname: string): boolean =>
+ normalizePathname(pathname) === ACTIVITY_TOUR_COMPOSER_PATH
diff --git a/src/lib/campaignQuickActionRegistry.ts b/src/lib/campaignQuickActionRegistry.ts
index dd8f9f9d..edcc0819 100644
--- a/src/lib/campaignQuickActionRegistry.ts
+++ b/src/lib/campaignQuickActionRegistry.ts
@@ -1,9 +1,15 @@
+<<<<<<< HEAD
import {
resolveStaffHomeQuickActions,
type ResolvedCampaignHomeAction,
} from '@/lib/campaignHomeActions'
import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths'
+=======
+import { resolveActivityQuickActions } from '@/lib/activityQuickActions'
+>>>>>>> ccc5faf (B84 — Ações rápidas na vertical Atividades (lista + detalhe))
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
+import { parseActivityQuickActionSurface } from '@/lib/campaignQuickActionPaths'
+import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
import type { CampaignRole } from '@/lib/campaignRoles'
const isTerritoriesListPath = (pathname: string): boolean =>
@@ -12,15 +18,25 @@ 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,
+<<<<<<< HEAD
_context: CampaignQuickActionContext,
): readonly ResolvedCampaignHomeAction[] => {
if (isTerritoriesListPath(pathname)) {
return resolveStaffHomeQuickActions(role)
}
+=======
+ context: CampaignQuickActionContext,
+): readonly CampaignQuickAction[] => {
+ const activitySurface = parseActivityQuickActionSurface(pathname)
+ if (activitySurface) {
+ return resolveActivityQuickActions(activitySurface, role, context)
+ }
+
+>>>>>>> ccc5faf (B84 — Ações rápidas na vertical Atividades (lista + detalhe))
return []
}
diff --git a/src/lib/campaignQuickActionTypes.ts b/src/lib/campaignQuickActionTypes.ts
new file mode 100644
index 00000000..10cd88be
--- /dev/null
+++ b/src/lib/campaignQuickActionTypes.ts
@@ -0,0 +1,10 @@
+import type { LucideIcon } from 'lucide-react'
+
+/** Client-safe quick-action row rendered by the B79 drawer strip. */
+export type CampaignQuickAction = {
+ id: string
+ label: string
+ icon: LucideIcon
+ description: string
+ href?: string
+}
diff --git a/tests/unit/campaignQuickAction.unit.spec.ts b/tests/unit/campaignQuickAction.unit.spec.ts
index 0d59a4e6..f97c4fa9 100644
--- a/tests/unit/campaignQuickAction.unit.spec.ts
+++ b/tests/unit/campaignQuickAction.unit.spec.ts
@@ -1,6 +1,17 @@
import { describe, expect, it } from 'vitest'
+<<<<<<< HEAD
import { UNCOVERED_MUNICIPALITIES_LIST_HREF, homeActionsForRole } from '@/lib/campaignHomeActions'
+=======
+import { resolveActivityQuickActions } from '@/lib/activityQuickActions'
+import {
+ ACTIVITY_LIST_PATH,
+ ACTIVITY_NEW_PATH,
+ ACTIVITY_TOUR_COMPOSER_PATH,
+ isActivityTourComposerPath,
+ parseActivityQuickActionSurface,
+} from '@/lib/campaignQuickActionPaths'
+>>>>>>> ccc5faf (B84 — Ações rápidas na vertical Atividades (lista + detalhe))
import {
isCampaignActionsPath,
isCampaignHomePath,
@@ -13,6 +24,7 @@ import {
QUICK_ACTIONS_SNAP_EXPANDED,
quickActionsSnapIsExpanded,
} from '@/lib/campaignQuickActionSnap'
+import { CAMPAIGN_WIZARD_ACTION_SLUGS } from '@/lib/campaignActionRoutes'
describe('campaignQuickActionMount', () => {
it('treats Início as exact match only', () => {
@@ -44,9 +56,75 @@ describe('campaignQuickActionMount', () => {
expect(shouldMountQuickActionsDrawer('/campanha/contatos', 'leader')).toBe(true)
expect(shouldMountQuickActionsDrawer('/campanha/municipios', 'leader')).toBe(false)
})
+
+ it('skips the E13 tour composer (B84)', () => {
+ expect(isActivityTourComposerPath(ACTIVITY_TOUR_COMPOSER_PATH)).toBe(true)
+ expect(shouldMountQuickActionsDrawer(ACTIVITY_TOUR_COMPOSER_PATH, 'coordinator')).toBe(false)
+ })
+})
+
+describe('campaignQuickActionPaths (activities)', () => {
+ it('parses list and detail surfaces', () => {
+ expect(parseActivityQuickActionSurface(ACTIVITY_LIST_PATH)).toEqual({ kind: 'list' })
+ expect(parseActivityQuickActionSurface(`${ACTIVITY_LIST_PATH}/`)).toEqual({ kind: 'list' })
+ expect(parseActivityQuickActionSurface('/campanha/atividades/caminhada-centro')).toEqual({
+ kind: 'detail',
+ activitySlug: 'caminhada-centro',
+ })
+ })
+
+ it('ignores nova, giros and nested edit routes', () => {
+ expect(parseActivityQuickActionSurface(ACTIVITY_NEW_PATH)).toBeNull()
+ expect(parseActivityQuickActionSurface(ACTIVITY_TOUR_COMPOSER_PATH)).toBeNull()
+ expect(parseActivityQuickActionSurface('/campanha/atividades/foo/editar')).toBeNull()
+ })
+})
+
+describe('activityQuickActions (B84)', () => {
+ it('lists vertical verbs on the activity list', () => {
+ const actions = resolveActivityQuickActions({ kind: 'list' }, 'coordinator', {})
+ expect(actions.map((action) => action.id)).toEqual(['new-activity', 'plan-tour'])
+ expect(actions[0]?.href).toBe(ACTIVITY_NEW_PATH)
+ expect(actions[1]?.href).toBe(ACTIVITY_TOUR_COMPOSER_PATH)
+ })
+
+ it('prefills wizards and detail shortcuts on activity detail', () => {
+ const actions = resolveActivityQuickActions(
+ { kind: 'detail', activitySlug: 'comicio-feira' },
+ 'coordinator',
+ { activitySlug: 'comicio-feira', municipalitySlug: 'feira-de-santana' },
+ )
+
+ expect(actions.some((action) => action.id === 'update-votes')).toBe(true)
+ expect(
+ actions.find((action) => action.id === 'update-votes')?.href?.includes('municipio=feira-de-santana'),
+ ).toBe(true)
+ expect(actions.find((action) => action.id === 'edit-activity')?.href).toBe(
+ '/campanha/atividades/comicio-feira/editar',
+ )
+ expect(actions.find((action) => action.id === 'activity-tasks')?.href).toBe(
+ '/campanha/atividades/comicio-feira?tab=tasks',
+ )
+ })
+
+ it('omits wizard prefills without municipality context', () => {
+ const actions = resolveActivityQuickActions(
+ { kind: 'detail', activitySlug: 'panfletagem' },
+ 'advisor',
+ { activitySlug: 'panfletagem' },
+ )
+
+ expect(actions.some((action) => action.id === 'register-demand')).toBe(false)
+ expect(actions.map((action) => action.id)).toEqual([
+ 'edit-activity',
+ 'activity-tasks',
+ 'activity-updates',
+ ])
+ })
})
describe('campaignQuickActionRegistry', () => {
+<<<<<<< HEAD
const staffActionIds = homeActionsForRole('coordinator').map((action) => action.id)
it('returns empty catalog for unregistered paths', () => {
@@ -55,6 +133,27 @@ describe('campaignQuickActionRegistry', () => {
municipalitySlug: 'foo',
}),
).toEqual([])
+=======
+ it('returns empty catalog on unrelated staff routes', () => {
+ expect(resolveQuickActionsForPath('/campanha/municipios/foo', 'coordinator', {})).toEqual([])
+ })
+
+ it('delegates activity routes to the B84 catalog', () => {
+ const actions = resolveQuickActionsForPath(ACTIVITY_LIST_PATH, 'coordinator', {})
+ expect(actions.map((action) => action.id)).toEqual(['new-activity', 'plan-tour'])
+ })
+
+ it('builds wizard hrefs with municipality slug from context', () => {
+ const actions = resolveQuickActionsForPath(
+ '/campanha/atividades/evento-zona-1',
+ 'coordinator',
+ { municipalitySlug: 'salvador-ze-01' },
+ )
+ const registerSignal = actions.find((action) => action.id === 'register-signal')
+ expect(registerSignal?.href).toBe(
+ `/campanha/acoes/${CAMPAIGN_WIZARD_ACTION_SLUGS['register-signal']}?municipio=salvador-ze-01`,
+ )
+>>>>>>> ccc5faf (B84 — Ações rápidas na vertical Atividades (lista + detalhe))
})
it('returns staff Início catalog on territorios without municipality prefill (B81)', () => {