mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:25:18 +00:00
B84 — Ações rápidas na vertical Atividades (lista + detalhe)
Registra catálogo B84 no registry B79: Nova atividade e Planejar giro na lista; wizards A1–A5 com ?municipio= no detalhe quando há município, mais atalhos Editar/Tarefas/Atualizações. Context bridge no detalhe; drawer omitido em /atividades/giros (compositor E13 já tem CTAs). Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
parent
82ca33c3d3
commit
eea2d9afa1
9 changed files with 330 additions and 3 deletions
|
|
@ -13,6 +13,7 @@ import { ActivityTabNav } from '@/components/campaign/activity/ActivityTabNav'
|
|||
import { ActivityTaskChecklist } from '@/components/campaign/activity/ActivityTaskChecklist'
|
||||
import { ActivityUpdateFeed } from '@/components/campaign/activity/ActivityUpdateFeed'
|
||||
import { ActivityUpdateForm } from '@/components/campaign/activity/ActivityUpdateForm'
|
||||
import { CampaignQuickActionContextBridge } from '@/components/campaign/shell/CampaignQuickActionContextBridge'
|
||||
import { Badge } from '@/components/ui/Badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
|
@ -65,6 +66,10 @@ export default async function ActivityDetailPage({
|
|||
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6">
|
||||
<CampaignQuickActionContextBridge
|
||||
activitySlug={view.slug}
|
||||
municipalitySlug={view.municipality?.slug}
|
||||
/>
|
||||
<header className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div className="flex min-w-0 flex-col gap-2">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<QuickActionsSnapPoint | null>(
|
||||
QUICK_ACTIONS_SNAP_COLLAPSED,
|
||||
|
|
|
|||
113
src/lib/activityQuickActions.ts
Normal file
113
src/lib/activityQuickActions.ts
Normal file
|
|
@ -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]
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
35
src/lib/campaignQuickActionPaths.ts
Normal file
35
src/lib/campaignQuickActionPaths.ts
Normal file
|
|
@ -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
|
||||
|
|
@ -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 []
|
||||
}
|
||||
|
|
|
|||
10
src/lib/campaignQuickActionTypes.ts
Normal file
10
src/lib/campaignQuickActionTypes.ts
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue