Merge pull request #82 from fsolla/cursor/pool-i23-0fce1b59-4cb2

B87 — Ações rápidas — Assessores (unrestricted)
This commit is contained in:
Francisco Solla 2026-08-01 03:03:49 -03:00 committed by GitHub
commit 5c6ed8cf31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 180 additions and 5 deletions

View file

@ -1,6 +1,8 @@
import { redirect } from 'next/navigation'
/** Inline create lives on the assessors list — keep old bookmarks working. */
import { advisorQuickCreateHref } from '@/lib/campaignAdvisorQuickActions'
/** Inline create lives on the assessores list — keep old bookmarks working. */
export default function AdvisorNewRedirectPage() {
redirect('/campanha/assessores')
redirect(advisorQuickCreateHref)
}

View file

@ -1,3 +1,5 @@
import { ADVISOR_QUICK_CREATE_PARAM } from '@/lib/campaignAdvisorQuickActions'
import { CAMPAIGN_ADVISORS_HOME } from '@/lib/campaignPaths'
import config from '@payload-config'
import { getPayload } from 'payload'
@ -14,6 +16,7 @@ import {
loadAdvisorListPageData,
parseAdvisorListParams,
} from '@/utilities/advisorData'
import { firstValue } from '@/utilities/campaignListUrl'
import { requireCampaignPageActor } from '@/utilities/campaignPageActor'
import { loadMunicipalityPortfolioIndex } from '@/utilities/municipality/municipalityPortfolioIndex'
import {
@ -35,6 +38,7 @@ export default async function AdvisorsPage({ searchParams }: AdvisorsPageProps)
])
const state = parseAdvisorListParams(rawSearchParams)
const autoCreateDraft = firstValue(rawSearchParams[ADVISOR_QUICK_CREATE_PARAM]) === '1'
const [{ rows, totalDocs, totalPages }, municipalityIndex] = await Promise.all([
loadAdvisorListPageData(payload, state),
loadMunicipalityPortfolioIndex(),
@ -55,7 +59,7 @@ export default async function AdvisorsPage({ searchParams }: AdvisorsPageProps)
ariaLabel="Buscar assessor por nome ou e-mail"
placeholder="Buscar por nome ou e-mail…"
initialQuery={state.q ?? ''}
basePath="/campanha/assessores"
basePath={CAMPAIGN_ADVISORS_HOME}
/>
<CampaignListResults>
@ -67,6 +71,7 @@ export default async function AdvisorsPage({ searchParams }: AdvisorsPageProps)
municipalitiesAction={setAdvisorMunicipalitiesFormAction}
createAction={createAdvisorFormAction}
passwordResetAction={sendAdvisorPasswordResetFormAction}
autoCreateDraft={autoCreateDraft}
/>
{totalDocs > 0 ? (

View file

@ -11,7 +11,7 @@ import {
} from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useMemo, useState, useTransition } from 'react'
import { useEffect, useMemo, useState, useTransition } from 'react'
import { toast } from 'sonner'
import { AdvisorDebouncedTextCell } from '@/components/campaign/advisor/AdvisorDebouncedTextCell'
@ -73,6 +73,7 @@ type AdvisorsTableProps = {
state: CampaignFormActionState,
formData: FormData,
) => Promise<CampaignFormActionState>
autoCreateDraft?: boolean
}
const emptyDraft = (): DraftAdvisor => ({
@ -95,6 +96,7 @@ export const AdvisorsTable = ({
municipalitiesAction,
createAction,
passwordResetAction,
autoCreateDraft = false,
}: AdvisorsTableProps) => {
const router = useRouter()
const [editing, setEditing] = useState(false)
@ -115,6 +117,10 @@ export const AdvisorsTable = ({
setDraft(emptyDraft())
}
useEffect(() => {
if (autoCreateDraft) setDraft(emptyDraft())
}, [autoCreateDraft])
const cancelDraft = () => setDraft(null)
const leaveEditing = () => {

View file

@ -0,0 +1,67 @@
import { UserPlus } from 'lucide-react'
import { CAMPAIGN_ADVISORS_HOME } from '@/lib/campaignPaths'
import type { CampaignQuickActionContext } from '@/lib/campaignQuickActionContext'
import type { CampaignQuickAction } from '@/lib/campaignQuickActionTypes'
import { isUnrestrictedCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
export const ADVISOR_QUICK_CREATE_PARAM = 'criar' as const
export const advisorQuickCreateHref =
`${CAMPAIGN_ADVISORS_HOME}?${ADVISOR_QUICK_CREATE_PARAM}=1` as const
const advisorDetailPathPattern = /^\/campanha\/assessores\/(\d+)(?:\/|$)/
export const isAdvisorsListPath = (pathname: string): boolean =>
pathname === CAMPAIGN_ADVISORS_HOME || pathname === `${CAMPAIGN_ADVISORS_HOME}/`
export const isAdvisorsPath = (pathname: string): boolean =>
isAdvisorsListPath(pathname) || parseAdvisorDetailId(pathname) !== undefined
export const parseAdvisorDetailId = (pathname: string): number | undefined => {
const match = pathname.match(advisorDetailPathPattern)
if (!match) return undefined
const id = Number(match[1])
return Number.isInteger(id) && id > 0 ? id : undefined
}
const newAdvisorAction: CampaignQuickAction = {
id: 'new-advisor',
label: 'Novo assessor',
icon: UserPlus,
description: 'Criar conta de assessor na tabela',
href: advisorQuickCreateHref,
}
const advisorQuickActionsByRole = new Map<CampaignRole, readonly CampaignQuickAction[]>()
const advisorQuickActionsForRole = (role: CampaignRole): readonly CampaignQuickAction[] => {
if (!isUnrestrictedCampaignRole(role)) return []
const cached = advisorQuickActionsByRole.get(role)
if (cached) return cached
const actions = [newAdvisorAction] as const
advisorQuickActionsByRole.set(role, actions)
return actions
}
export const resolveAdvisorListQuickActions = (
role: CampaignRole,
): readonly CampaignQuickAction[] => advisorQuickActionsForRole(role)
export const resolveAdvisorDetailQuickActions = (
role: CampaignRole,
): readonly CampaignQuickAction[] => advisorQuickActionsForRole(role)
export const resolveAdvisorQuickActionsForPath = (
pathname: string,
role: CampaignRole,
_context: CampaignQuickActionContext,
): readonly CampaignQuickAction[] => {
if (!isUnrestrictedCampaignRole(role)) return []
if (isAdvisorsListPath(pathname) || parseAdvisorDetailId(pathname) !== undefined) {
return advisorQuickActionsForRole(role)
}
return []
}

View file

@ -5,4 +5,6 @@ export const CAMPAIGN_TERRITORIES_HOME = '/campanha/territorios' as const
export const CAMPAIGN_DEMANDS_HOME = '/campanha/demandas' as const
export const CAMPAIGN_ADVISORS_HOME = '/campanha/assessores' as const
export const LEADER_CONTACTS_HOME = '/campanha/contatos' as const

View file

@ -1,7 +1,12 @@
import { CAMPAIGN_ACTIONS_HOME } from '@/lib/campaignActionRoutes'
import { isAdvisorsPath } from '@/lib/campaignAdvisorQuickActions'
import { CAMPAIGN_HOME, LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
import { isActivityTourComposerPath } from '@/lib/campaignQuickActionPaths'
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
import {
isStaffCampaignRole,
isUnrestrictedCampaignRole,
type CampaignRole,
} from '@/lib/campaignRoles'
/** Início matches only exactly — same rule as sidebar nav. */
export const isCampaignHomePath = (pathname: string): boolean =>
@ -23,5 +28,6 @@ export const shouldMountQuickActionsDrawer = (pathname: string, role: CampaignRo
// B84: E13 compositor already exposes primary CTAs — skip competing drawer chrome.
if (isActivityTourComposerPath(pathname)) return false
if (role === 'leader') return isLeaderContactsPath(pathname)
if (isAdvisorsPath(pathname)) return isUnrestrictedCampaignRole(role)
return isStaffCampaignRole(role)
}

View file

@ -1,4 +1,5 @@
import { resolveActivityQuickActions } from '@/lib/activityQuickActions'
import { resolveAdvisorQuickActionsForPath } from '@/lib/campaignAdvisorQuickActions'
import { resolveStaffHomeQuickActions } from '@/lib/campaignHomeActions'
import { resolveMunicipalityQuickActionsForPath } from '@/lib/campaignMunicipalityQuickActions'
import { CAMPAIGN_TERRITORIES_HOME } from '@/lib/campaignPaths'
@ -56,6 +57,9 @@ export const resolveQuickActionsForPath = (
const municipalityActions = resolveMunicipalityQuickActionsForPath(pathname, role, context)
if (municipalityActions.length > 0) return municipalityActions
const advisorActions = resolveAdvisorQuickActionsForPath(pathname, role, context)
if (advisorActions.length > 0) return advisorActions
if (isTerritoriesListPath(pathname)) {
return resolveStaffHomeQuickActions(role)
}

View file

@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest'
import {
advisorQuickCreateHref,
isAdvisorsListPath,
isAdvisorsPath,
parseAdvisorDetailId,
resolveAdvisorDetailQuickActions,
resolveAdvisorListQuickActions,
resolveAdvisorQuickActionsForPath,
} from '@/lib/campaignAdvisorQuickActions'
describe('campaignAdvisorQuickActions paths', () => {
it('matches assessores list exactly', () => {
expect(isAdvisorsListPath('/campanha/assessores')).toBe(true)
expect(isAdvisorsListPath('/campanha/assessores/')).toBe(true)
expect(isAdvisorsListPath('/campanha/assessores/novo')).toBe(false)
})
it('matches assessores detail ids', () => {
expect(parseAdvisorDetailId('/campanha/assessores/42')).toBe(42)
expect(parseAdvisorDetailId('/campanha/assessores/42/')).toBe(42)
expect(parseAdvisorDetailId('/campanha/assessores/novo')).toBeUndefined()
expect(isAdvisorsPath('/campanha/assessores/42')).toBe(true)
})
})
describe('resolveAdvisorListQuickActions', () => {
it('returns new-advisor for unrestricted roles', () => {
const actions = resolveAdvisorListQuickActions('coordinator')
expect(actions).toHaveLength(1)
expect(actions[0]?.id).toBe('new-advisor')
expect(actions[0]?.href).toBe(advisorQuickCreateHref)
})
it('returns empty catalog for advisor lockdown', () => {
expect(resolveAdvisorListQuickActions('advisor')).toEqual([])
})
})
describe('resolveAdvisorDetailQuickActions', () => {
it('returns new-advisor for unrestricted roles', () => {
const actions = resolveAdvisorDetailQuickActions('candidate')
expect(actions.map((action) => action.id)).toEqual(['new-advisor'])
expect(actions[0]?.href).toBe(advisorQuickCreateHref)
})
})
describe('resolveAdvisorQuickActionsForPath', () => {
it('delegates list and detail catalogs (B87)', () => {
const list = resolveAdvisorQuickActionsForPath('/campanha/assessores', 'coordinator', {})
expect(list.map((action) => action.id)).toEqual(['new-advisor'])
const detail = resolveAdvisorQuickActionsForPath('/campanha/assessores/7', 'coordinator', {})
expect(detail.map((action) => action.id)).toEqual(['new-advisor'])
})
it('returns empty catalog for advisor role even on assessores paths', () => {
expect(resolveAdvisorQuickActionsForPath('/campanha/assessores', 'advisor', {})).toEqual([])
})
})

View file

@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import { resolveActivityQuickActions } from '@/lib/activityQuickActions'
import { CAMPAIGN_WIZARD_ACTION_SLUGS } from '@/lib/campaignActionRoutes'
import { advisorQuickCreateHref } from '@/lib/campaignAdvisorQuickActions'
import { UNCOVERED_MUNICIPALITIES_LIST_HREF, homeActionsForRole } from '@/lib/campaignHomeActions'
import {
CAMPAIGN_DEMANDS_CREATE_HREF,
@ -72,6 +73,13 @@ describe('campaignQuickActionMount', () => {
expect(isActivityTourComposerPath(ACTIVITY_TOUR_COMPOSER_PATH)).toBe(true)
expect(shouldMountQuickActionsDrawer(ACTIVITY_TOUR_COMPOSER_PATH, 'coordinator')).toBe(false)
})
it('mounts assessores only for unrestricted roles (B87)', () => {
expect(shouldMountQuickActionsDrawer('/campanha/assessores', 'coordinator')).toBe(true)
expect(shouldMountQuickActionsDrawer('/campanha/assessores/12', 'candidate')).toBe(true)
expect(shouldMountQuickActionsDrawer('/campanha/assessores', 'advisor')).toBe(false)
expect(shouldMountQuickActionsDrawer('/campanha/assessores/12', 'advisor')).toBe(false)
})
})
describe('campaignQuickActionPaths (activities)', () => {
@ -279,6 +287,20 @@ describe('campaignQuickActionRegistry', () => {
resolveQuickActionsForPath('/campanha/demandas/pedido-cairu', 'coordinator', {}),
).toEqual([])
})
it('delegates assessores list and detail catalogs (B87)', () => {
const list = resolveQuickActionsForPath('/campanha/assessores', 'coordinator', {})
expect(list.map((action) => action.id)).toEqual(['new-advisor'])
expect(list[0]?.href).toBe(advisorQuickCreateHref)
const detail = resolveQuickActionsForPath('/campanha/assessores/9', 'candidate', {})
expect(detail.map((action) => action.id)).toEqual(['new-advisor'])
expect(detail[0]?.href).toBe(advisorQuickCreateHref)
})
it('returns empty catalog on assessores for advisor lockdown', () => {
expect(resolveQuickActionsForPath('/campanha/assessores', 'advisor', {})).toEqual([])
})
})
describe('campaignQuickActionDemands paths', () => {