B85 — Ações rápidas no drawer de Demandas (lista + detalhe)

Registra catálogo contextual no chassis B79:
- Lista: um launcher register-demand → /demandas/nova (wizard A5 ainda stub)
- Detalhe: A1–A5 com prefill de município via context sync
- Expõe municipalityId no view model para prefill do form de criação

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-01 04:23:39 +00:00
parent 374e27f7eb
commit c05b04236e
No known key found for this signature in database
8 changed files with 233 additions and 2 deletions

View file

@ -5,6 +5,7 @@ import { notFound } from 'next/navigation'
import { getPayload } from 'payload'
import { DemandWorkflowCard } from '@/components/campaign/demand/DemandWorkflowCard'
import { CampaignQuickActionContextSync } from '@/components/campaign/shell/CampaignQuickActionContextSync'
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
import { Badge } from '@/components/ui/Badge'
import { Button } from '@/components/ui/button'
@ -59,6 +60,11 @@ export default async function DemandDetailPage({ params }: DemandDetailPageProps
return (
<CampaignPageShell>
<CampaignQuickActionContextSync
municipalitySlug={demand.municipalitySlug}
municipalityId={demand.municipalityId > 0 ? demand.municipalityId : undefined}
demandSlug={demand.slug}
/>
<header className="flex flex-col gap-2">
<Button asChild variant="ghost" className="min-h-11 self-start">
<Link href="/campanha/demandas">

View file

@ -0,0 +1,43 @@
'use client'
import { useEffect } from 'react'
import { useCampaignQuickActionContext } from '@/components/campaign/shell/CampaignQuickActionContext'
import {
emptyCampaignQuickActionContext,
type CampaignQuickActionContext,
} from '@/lib/campaignQuickActionContext'
/** RSC pages push route context into the B79 drawer host (B80+). */
export const CampaignQuickActionContextSync = ({
municipalitySlug,
municipalityId,
leadershipId,
organizationSlug,
activitySlug,
demandSlug,
}: CampaignQuickActionContext) => {
const { setContext } = useCampaignQuickActionContext()
useEffect(() => {
setContext({
municipalitySlug,
municipalityId,
leadershipId,
organizationSlug,
activitySlug,
demandSlug,
})
return () => setContext(emptyCampaignQuickActionContext())
}, [
setContext,
municipalitySlug,
municipalityId,
leadershipId,
organizationSlug,
activitySlug,
demandSlug,
])
return null
}

View file

@ -3,4 +3,6 @@ export const CAMPAIGN_HOME = '/campanha' as const
export const CAMPAIGN_TERRITORIES_HOME = '/campanha/territorios' as const
export const CAMPAIGN_DEMANDS_HOME = '/campanha/demandas' as const
export const LEADER_CONTACTS_HOME = '/campanha/contatos' as const

View file

@ -4,6 +4,7 @@
*/
export type CampaignQuickActionContext = {
municipalitySlug?: string
municipalityId?: number
leadershipId?: number
organizationSlug?: string
activitySlug?: string

View file

@ -0,0 +1,99 @@
import {
CAMPAIGN_WIZARD_ACTION_SLUGS,
wizardActionHref,
type CampaignWizardActionId,
} from '@/lib/campaignActionRoutes'
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
import { CAMPAIGN_DEMANDS_HOME } from '@/lib/campaignPaths'
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
export const CAMPAIGN_DEMANDS_CREATE_HREF = `${CAMPAIGN_DEMANDS_HOME}/nova` as const
const DETAIL_WIZARD_ACTION_IDS: readonly CampaignWizardActionId[] = [
'update-votes',
'register-signal',
'change-trend',
'update-leadership',
'register-demand',
]
export const demandCreateHref = (municipalityId?: number): string => {
if (municipalityId === undefined) {
return CAMPAIGN_DEMANDS_CREATE_HREF
}
return `${CAMPAIGN_DEMANDS_CREATE_HREF}?municipality=${municipalityId}`
}
export const isDemandsListPath = (pathname: string): boolean =>
pathname === CAMPAIGN_DEMANDS_HOME || pathname === `${CAMPAIGN_DEMANDS_HOME}/`
export const isDemandDetailPath = (pathname: string): boolean => {
if (!pathname.startsWith(`${CAMPAIGN_DEMANDS_HOME}/`)) {
return false
}
const rest = pathname.slice(`${CAMPAIGN_DEMANDS_HOME}/`.length)
if (!rest || rest.includes('/')) {
return false
}
return rest !== 'nova'
}
const registerDemandListAction = (role: CampaignRole): CampaignQuickAction | null => {
const source = homeActionsForRole(role).find((action) => action.id === 'register-demand')
if (!source) {
return null
}
return {
...source,
href: CAMPAIGN_DEMANDS_CREATE_HREF,
}
}
/** B85 list: single "Registrar pedido" launcher (form route — wizard A5 is still a stub). */
export const resolveDemandsListQuickActions = (
role: CampaignRole,
): readonly CampaignQuickAction[] => {
if (!isStaffCampaignRole(role)) {
return []
}
const action = registerDemandListAction(role)
return action ? [action] : []
}
/** B85 detail: staff Início A1–A5 with municipality prefill when linked. */
export const resolveDemandDetailQuickActions = (
role: CampaignRole,
context: CampaignQuickActionContext,
): readonly CampaignQuickAction[] => {
if (!isStaffCampaignRole(role)) {
return []
}
const { municipalitySlug, municipalityId } = context
if (!municipalitySlug) {
return []
}
const byId = new Map(homeActionsForRole(role).map((action) => [action.id, action]))
return DETAIL_WIZARD_ACTION_IDS.flatMap((id) => {
const action = byId.get(id)
if (!action) {
return []
}
if (id === 'register-demand') {
return [
{
...action,
href: demandCreateHref(municipalityId),
},
]
}
return [
{
...action,
href: wizardActionHref(CAMPAIGN_WIZARD_ACTION_SLUGS[id], municipalitySlug),
},
]
})
}

View file

@ -1,13 +1,19 @@
import { resolveActivityQuickActions } from '@/lib/activityQuickActions'
import { resolveStaffHomeQuickActions } from '@/lib/campaignHomeActions'
import { resolveMunicipalityQuickActionsForPath } from '@/lib/campaignMunicipalityQuickActions'
import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths'
import {
isDemandDetailPath,
isDemandsListPath,
resolveDemandDetailQuickActions,
resolveDemandsListQuickActions,
} from '@/lib/campaignQuickActionDemands'
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
import {
parseActivityQuickActionSurface,
parseOrganizationQuickActionSurface,
} from '@/lib/campaignQuickActionPaths'
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths'
import type { CampaignRole } from '@/lib/campaignRoles'
import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions'
@ -29,9 +35,17 @@ export const resolveQuickActionsForPath = (
return resolveActivityQuickActions(activitySurface, role, context)
}
<<<<<<< HEAD
const organizationSurface = parseOrganizationQuickActionSurface(pathname)
if (organizationSurface) {
return resolveOrganizationQuickActions(organizationSurface, role, context)
=======
if (isDemandsListPath(pathname)) {
return resolveDemandsListQuickActions(role)
}
if (isDemandDetailPath(pathname)) {
return resolveDemandDetailQuickActions(role, context)
>>>>>>> 2417ed3 (B85 — Ações rápidas no drawer de Demandas (lista + detalhe))
}
const municipalityActions = resolveMunicipalityQuickActionsForPath(pathname, role, context)
@ -40,6 +54,5 @@ export const resolveQuickActionsForPath = (
if (isTerritoriesListPath(pathname)) {
return resolveStaffHomeQuickActions(role)
}
return []
}

View file

@ -27,6 +27,7 @@ export type DemandRowViewModel = {
slug: string
kind: CampaignDemandKind
status: CampaignDemandStatus
municipalityId: number
municipalityName: string
municipalitySlug: string
requesterName: string | null
@ -107,6 +108,7 @@ const toDemandRow = (
requesterBy: Map<number, string>,
): DemandRowViewModel => {
const municipality = municipalityBy.get(relationshipId(demand.municipality) ?? -1)
const municipalityID = relationshipId(demand.municipality)
const leadershipID = relationshipId(demand.leadership)
return {
id: demand.id,
@ -114,6 +116,7 @@ const toDemandRow = (
slug: demand.slug,
kind: demand.kind as CampaignDemandKind,
status: demand.status as CampaignDemandStatus,
municipalityId: municipalityID ?? 0,
municipalityName: municipality?.name ?? 'Município',
municipalitySlug: municipality?.slug ?? '',
requesterName: leadershipID ? (requesterBy.get(leadershipID) ?? null) : null,

View file

@ -3,6 +3,14 @@ import { describe, expect, it } from 'vitest'
import { resolveActivityQuickActions } from '@/lib/activityQuickActions'
import { CAMPAIGN_WIZARD_ACTION_SLUGS } from '@/lib/campaignActionRoutes'
import { UNCOVERED_MUNICIPALITIES_LIST_HREF, homeActionsForRole } from '@/lib/campaignHomeActions'
import {
CAMPAIGN_DEMANDS_CREATE_HREF,
demandCreateHref,
isDemandDetailPath,
isDemandsListPath,
resolveDemandDetailQuickActions,
resolveDemandsListQuickActions,
} from '@/lib/campaignQuickActionDemands'
import {
isCampaignActionsPath,
isCampaignHomePath,
@ -49,6 +57,8 @@ 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/demandas', 'coordinator')).toBe(true)
expect(shouldMountQuickActionsDrawer('/campanha/demandas/foo', 'coordinator')).toBe(true)
expect(shouldMountQuickActionsDrawer('/campanha', 'coordinator')).toBe(false)
expect(shouldMountQuickActionsDrawer('/campanha/acoes/registrar-sinal', 'advisor')).toBe(false)
})
@ -234,6 +244,60 @@ describe('campaignQuickActionRegistry', () => {
it('returns empty catalog on territorios for leader lockdown', () => {
expect(resolveQuickActionsForPath('/campanha/territorios', 'leader', {})).toEqual([])
})
it('returns single register-demand launcher on demandas list (B85)', () => {
const actions = resolveQuickActionsForPath('/campanha/demandas', 'coordinator', {})
expect(actions.map((action) => action.id)).toEqual(['register-demand'])
expect(actions[0]?.href).toBe(CAMPAIGN_DEMANDS_CREATE_HREF)
expect(actions[0]?.href).not.toContain('municipio=')
})
it('returns A1–A5 prefilled on demand detail when municipality context is set (B85)', () => {
const actions = resolveQuickActionsForPath('/campanha/demandas/pedido-cairu', 'coordinator', {
municipalitySlug: 'cairu',
municipalityId: 42,
demandSlug: 'pedido-cairu',
})
expect(actions.map((action) => action.id)).toEqual([
'update-votes',
'register-signal',
'change-trend',
'update-leadership',
'register-demand',
])
expect(actions.find((action) => action.id === 'update-votes')?.href).toBe(
'/campanha/acoes/atualizar-votos?municipio=cairu',
)
expect(actions.find((action) => action.id === 'register-demand')?.href).toBe(
demandCreateHref(42),
)
expect(actions.some((action) => action.id === 'uncovered-municipalities')).toBe(false)
})
it('returns empty catalog on demand detail without municipality context', () => {
expect(resolveQuickActionsForPath('/campanha/demandas/pedido-cairu', 'coordinator', {})).toEqual(
[],
)
})
})
describe('campaignQuickActionDemands paths', () => {
it('matches list and detail paths but not nova', () => {
expect(isDemandsListPath('/campanha/demandas')).toBe(true)
expect(isDemandsListPath('/campanha/demandas/')).toBe(true)
expect(isDemandDetailPath('/campanha/demandas/pedido-cairu')).toBe(true)
expect(isDemandDetailPath('/campanha/demandas/nova')).toBe(false)
expect(isDemandDetailPath('/campanha/demandas')).toBe(false)
})
})
describe('campaignQuickActionDemands resolvers', () => {
it('returns empty catalog for leader lockdown', () => {
expect(resolveDemandsListQuickActions('leader')).toEqual([])
expect(
resolveDemandDetailQuickActions('leader', { municipalitySlug: 'cairu', municipalityId: 1 }),
).toEqual([])
})
})
describe('campaignQuickActionSnap', () => {