B88 — Ações rápidas em Organizações (lista + detalhe)

Registra /campanha/organizacoes no registry B79 com launcher
new-organization na lista; detalhe vazio até existir filtro de
lideranças por org ou município óbvio único.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-08-01 05:11:31 +00:00
parent d7bef57933
commit 9b76f19b85
No known key found for this signature in database
4 changed files with 126 additions and 4 deletions

View file

@ -1,4 +1,4 @@
/** Client-safe `/campanha/atividades` path helpers for the B84 quick-action registry. */
/** Client-safe quick-action path helpers for the B84+ vertical registries. */
export const ACTIVITY_LIST_PATH = '/campanha/atividades' as const
@ -6,20 +6,35 @@ export const ACTIVITY_NEW_PATH = '/campanha/atividades/nova' as const
export const ACTIVITY_TOUR_COMPOSER_PATH = '/campanha/atividades/giros' as const
export const ORGANIZATIONS_LIST_PATH = '/campanha/organizacoes' as const
export const ORGANIZATION_NEW_PATH = '/campanha/organizacoes/nova' as const
export type ActivityQuickActionSurface = { kind: 'list' } | { kind: 'detail'; activitySlug: string }
export type OrganizationQuickActionSurface =
| { kind: 'list' }
| { kind: 'detail'; organizationSlug: 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}/`
const listDetailSlug = (
pathname: string,
listPath: string,
excludedSegments: readonly string[],
): string | null => {
const prefix = `${listPath}/`
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
if (excludedSegments.includes(rest)) return null
return rest
}
const activityDetailSlug = (pathname: string): string | null =>
listDetailSlug(pathname, ACTIVITY_LIST_PATH, ['nova', 'giros'])
export const parseActivityQuickActionSurface = (
pathname: string,
): ActivityQuickActionSurface | null => {
@ -29,5 +44,14 @@ export const parseActivityQuickActionSurface = (
return slug ? { kind: 'detail', activitySlug: slug } : null
}
export const parseOrganizationQuickActionSurface = (
pathname: string,
): OrganizationQuickActionSurface | null => {
const normalized = normalizePathname(pathname)
if (normalized === ORGANIZATIONS_LIST_PATH) return { kind: 'list' }
const slug = listDetailSlug(normalized, ORGANIZATIONS_LIST_PATH, ['nova'])
return slug ? { kind: 'detail', organizationSlug: slug } : null
}
export const isActivityTourComposerPath = (pathname: string): boolean =>
normalizePathname(pathname) === ACTIVITY_TOUR_COMPOSER_PATH

View file

@ -1,6 +1,8 @@
import { resolveActivityQuickActions } from '@/lib/activityQuickActions'
import { resolveStaffHomeQuickActions } from '@/lib/campaignHomeActions'
import { resolveMunicipalityQuickActionsForPath } from '@/lib/campaignMunicipalityQuickActions'
import { parseOrganizationQuickActionSurface } from '@/lib/campaignQuickActionPaths'
import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions'
import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths'
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
import { parseActivityQuickActionSurface } from '@/lib/campaignQuickActionPaths'
@ -25,6 +27,11 @@ export const resolveQuickActionsForPath = (
return resolveActivityQuickActions(activitySurface, role, context)
}
const organizationSurface = parseOrganizationQuickActionSurface(pathname)
if (organizationSurface) {
return resolveOrganizationQuickActions(organizationSurface, role, context)
}
const municipalityActions = resolveMunicipalityQuickActionsForPath(pathname, role, context)
if (municipalityActions.length > 0) return municipalityActions

View file

@ -0,0 +1,35 @@
import { Plus } from 'lucide-react'
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
import {
ORGANIZATION_NEW_PATH,
type OrganizationQuickActionSurface,
} from '@/lib/campaignQuickActionPaths'
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
const listQuickActions = (): readonly CampaignQuickAction[] => [
{
id: 'new-organization',
label: 'Nova organização',
icon: Plus,
description: 'Cadastrar sindicato, associação ou movimento de apoio',
href: ORGANIZATION_NEW_PATH,
},
]
export const resolveOrganizationQuickActions = (
surface: OrganizationQuickActionSurface,
role: CampaignRole,
_context: CampaignQuickActionContext,
): readonly CampaignQuickAction[] => {
if (!isStaffCampaignRole(role)) return []
if (surface.kind === 'list') {
return listQuickActions()
}
// Detail: leadership list has no organization filter URL yet (B88 as-built);
// orgs rarely have a single obvious municipality for A1–A5 prefills.
return []
}

View file

@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import { resolveActivityQuickActions } from '@/lib/activityQuickActions'
import { resolveOrganizationQuickActions } from '@/lib/organizationQuickActions'
import { CAMPAIGN_WIZARD_ACTION_SLUGS } from '@/lib/campaignActionRoutes'
import { UNCOVERED_MUNICIPALITIES_LIST_HREF, homeActionsForRole } from '@/lib/campaignHomeActions'
import {
@ -13,8 +14,11 @@ import {
ACTIVITY_LIST_PATH,
ACTIVITY_NEW_PATH,
ACTIVITY_TOUR_COMPOSER_PATH,
ORGANIZATION_NEW_PATH,
ORGANIZATIONS_LIST_PATH,
isActivityTourComposerPath,
parseActivityQuickActionSurface,
parseOrganizationQuickActionSurface,
} from '@/lib/campaignQuickActionPaths'
import { resolveQuickActionsForPath } from '@/lib/campaignQuickActionRegistry'
import {
@ -77,6 +81,44 @@ describe('campaignQuickActionPaths (activities)', () => {
})
})
describe('organizationQuickActions (B88)', () => {
it('parses list and detail surfaces', () => {
expect(parseOrganizationQuickActionSurface(ORGANIZATIONS_LIST_PATH)).toEqual({ kind: 'list' })
expect(parseOrganizationQuickActionSurface(`${ORGANIZATIONS_LIST_PATH}/`)).toEqual({
kind: 'list',
})
expect(parseOrganizationQuickActionSurface('/campanha/organizacoes/sindmed')).toEqual({
kind: 'detail',
organizationSlug: 'sindmed',
})
})
it('ignores nova and nested routes', () => {
expect(parseOrganizationQuickActionSurface(ORGANIZATION_NEW_PATH)).toBeNull()
expect(parseOrganizationQuickActionSurface('/campanha/organizacoes/foo/editar')).toBeNull()
})
it('lists new-organization on the catalog page', () => {
const actions = resolveOrganizationQuickActions({ kind: 'list' }, 'coordinator', {})
expect(actions.map((action) => action.id)).toEqual(['new-organization'])
expect(actions[0]?.href).toBe(ORGANIZATION_NEW_PATH)
})
it('returns empty detail catalog without leadership filter URL', () => {
expect(
resolveOrganizationQuickActions(
{ kind: 'detail', organizationSlug: 'sindmed' },
'coordinator',
{ organizationSlug: 'sindmed' },
),
).toEqual([])
})
it('returns empty catalog for leader lockdown', () => {
expect(resolveOrganizationQuickActions({ kind: 'list' }, 'leader', {})).toEqual([])
})
})
describe('activityQuickActions (B84)', () => {
it('lists vertical verbs on the activity list', () => {
const actions = resolveActivityQuickActions({ kind: 'list' }, 'coordinator', {})
@ -125,6 +167,20 @@ describe('activityQuickActions (B84)', () => {
describe('campaignQuickActionRegistry', () => {
const staffActionIds = homeActionsForRole('coordinator').map((action) => action.id)
it('delegates organization routes to the B88 catalog', () => {
const actions = resolveQuickActionsForPath(ORGANIZATIONS_LIST_PATH, 'coordinator', {})
expect(actions.map((action) => action.id)).toEqual(['new-organization'])
expect(actions[0]?.href).toBe(ORGANIZATION_NEW_PATH)
})
it('returns empty organization detail catalog in the registry', () => {
expect(
resolveQuickActionsForPath('/campanha/organizacoes/sindmed', 'advisor', {
organizationSlug: 'sindmed',
}),
).toEqual([])
})
it('returns empty catalog for unregistered paths', () => {
expect(resolveQuickActionsForPath('/campanha/apoiadores', 'coordinator', {})).toEqual([])
})