Merge pull request #80 from fsolla/cursor/pool-i21-97442a76-e99b

B85 — Ações rápidas — Demandas
This commit is contained in:
Francisco Solla 2026-08-01 02:41:05 -03:00 committed by GitHub
commit 90d130af33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 234 additions and 2 deletions

View file

@ -6,6 +6,7 @@ import { getPayload } from 'payload'
import { DemandWorkflowCard } from '@/components/campaign/demand/DemandWorkflowCard'
import { CampaignPageShell } from '@/components/campaign/shell/CampaignPageShell'
import { CampaignQuickActionContextSync } from '@/components/campaign/shell/CampaignQuickActionContextSync'
import { Badge } from '@/components/ui/Badge'
import { Button } from '@/components/ui/button'
import {
@ -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,100 @@
import {
CAMPAIGN_WIZARD_ACTION_SLUGS,
wizardActionHref,
type CampaignWizardActionId,
} from '@/lib/campaignActionRoutes'
import { homeActionsForRole } from '@/lib/campaignHomeActions'
import { CAMPAIGN_DEMANDS_HOME } from '@/lib/campaignPaths'
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
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

@ -4,6 +4,12 @@ import { resolveMunicipalityQuickActionsForPath } from '@/lib/campaignMunicipali
import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths'
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
import { isLeaderContactsPath } from '@/lib/campaignQuickActionMount'
import {
isDemandDetailPath,
isDemandsListPath,
resolveDemandDetailQuickActions,
resolveDemandsListQuickActions,
} from '@/lib/campaignQuickActionDemands'
import {
parseActivityQuickActionSurface,
parseOrganizationQuickActionSurface,
@ -40,12 +46,18 @@ export const resolveQuickActionsForPath = (
return resolveOrganizationQuickActions(organizationSurface, role, context)
}
if (isDemandsListPath(pathname)) {
return resolveDemandsListQuickActions(role)
}
if (isDemandDetailPath(pathname)) {
return resolveDemandDetailQuickActions(role, context)
}
const municipalityActions = resolveMunicipalityQuickActionsForPath(pathname, role, context)
if (municipalityActions.length > 0) return municipalityActions
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

@ -61,11 +61,12 @@ describe('searchStaffMunicipalityHits (B60)', () => {
throw new Error('could not allocate two municipalities with distinct word-start queries')
}
}
await fixtures.assignMunicipalityAdvisors(administered.id, [advisor.id])
const otherQuery = distinctWordStartQuery(other.name, administered.name)
const administeredQuery = distinctWordStartQuery(administered.name, other.name)
await fixtures.assignMunicipalityAdvisors(administered.id, [advisor.id])
const advisorResult = await searchStaffMunicipalityHits(payload, advisor, otherQuery)
expect(advisorResult.map((hit) => hit.slug)).toEqual([])

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', () => {