teqo-backup/src/lib/campaignHomeActions.ts
Francisco Solla 0d483a08db feat(campaign): C142 UI respects advisor permission profile somente_leitura
- Pure helpers: advisorEditingScope, rowEditingAllowed in campaignAdvisorProfile.ts
- gate: 'writable' in requireCampaignPageActor for route-level enforcement
- Shared cells accept readOnly prop: CampaignInlineEditableCell, MunicipalityRelationEditor, PhonesFieldEditor, DemandDescriptionEditor, etc.
- Municipality pages compute editingScope via resolveMunicipalityWriteScope and thread it to list/detail/edit surfaces
- Home actions, FAB mount rule, quick actions registry accept editingScope parameter
- Demands, activities, supporters, organizations create buttons gated
- E2E spec: campaignPermissionProfile.e2e.spec.ts covering 3 profiles
- Unit tests: campaignAdvisorProfile, accessShared, campaignComponents updated
- gate:fast passed (tsc, lint, format, knip, cycles, unit)
2026-08-21 23:01:35 -03:00

206 lines
6 KiB
TypeScript

import type { LucideIcon } from 'lucide-react'
import {
BarChart3,
Handshake,
Inbox,
Megaphone,
TrendingUp,
UserPlus,
UserRoundSearch,
Users,
} from 'lucide-react'
import {
appendWizardReturnPath,
CAMPAIGN_WIZARD_ACTION_SLUGS,
campaignActionEntryHref,
isCampaignWizardActionId,
wizardActionHref,
type CampaignWizardActionId,
} from '@/lib/campaignActionRoutes'
import type { AdvisorEditingScope } from '@/lib/campaignAdvisorProfile'
import { LEADER_CONTACTS_HOME } from '@/lib/campaignPaths'
import { isStaffCampaignRole, type CampaignRole } from '@/lib/campaignRoles'
type CampaignHomeActionId =
| 'update-votes'
| 'register-update'
| 'change-trend'
| 'update-leadership'
| 'register-demand'
| 'uncovered-municipalities'
| 'register-supporter'
| 'my-contacts'
export type CampaignHomeAction = {
id: CampaignHomeActionId
label: string
icon: LucideIcon
description: string
}
const staffHomeActionsCoordinator: readonly CampaignHomeAction[] = [
{
id: 'update-votes',
label: 'Ajustar votos',
icon: BarChart3,
description: 'Atualizar a projeção (média, pessimista, otimista) de um município',
},
{
id: 'register-update',
label: 'Registrar atualização',
icon: Megaphone,
description: 'Anotar o que aconteceu no município — texto, polaridade e urgência.',
},
{
id: 'change-trend',
label: 'Mudar tendência',
icon: TrendingUp,
description: 'Favorável, neutra ou desfavorável — com o porquê',
},
{
id: 'update-leadership',
label: 'Atualizar liderança',
icon: Handshake,
description: 'Trocar quem coordena, status de apoio ou votos que a pessoa declara',
},
{
id: 'register-demand',
label: 'Registrar pedido',
icon: Inbox,
description: 'Demanda de material, transporte, diária ou outro pedido do município',
},
{
id: 'uncovered-municipalities',
label: 'Ver esquecidos',
icon: UserRoundSearch,
description: 'Abrir a lista de municípios sem assessor (atalho — não é wizard)',
},
]
const portfolioScopeSuffix = ' nos municípios da sua carteira'
const staffHomeActionsAdvisor: readonly CampaignHomeAction[] = staffHomeActionsCoordinator.map(
(action) => {
if (action.id === 'uncovered-municipalities') {
return {
...action,
description:
'Abrir a lista de municípios sem assessor da sua carteira (atalho — não é wizard)',
}
}
return {
...action,
description: `${action.description}${portfolioScopeSuffix}`,
}
},
)
const leaderHomeActions: readonly CampaignHomeAction[] = [
{
id: 'register-supporter',
label: 'Cadastrar apoiador',
icon: UserPlus,
description: 'Registrar alguém da sua rede que apoia o Solla',
},
{
id: 'my-contacts',
label: 'Ver meus contatos',
icon: Users,
description: 'Abrir a lista dos apoiadores que você cadastrou',
},
]
export const wizardFlowTitleForActionId = (id: CampaignWizardActionId): string => {
const action = staffHomeActionsCoordinator.find((entry) => entry.id === id)
return action?.label ?? 'Continuar'
}
export const homeActionsForRole = (
role: CampaignRole,
editingScope: AdvisorEditingScope = 'tudo',
): readonly CampaignHomeAction[] => {
if (role === 'leader') return leaderHomeActions
if (!isStaffCampaignRole(role)) return []
// C142 — advisors with `somente_leitura` see only navigation actions
// (uncovered-municipalities); the 4 wizards are write destinations.
if (role === 'advisor') {
if (editingScope === 'none') {
return staffHomeActionsAdvisor.filter((action) => action.id === 'uncovered-municipalities')
}
return staffHomeActionsAdvisor
}
return staffHomeActionsCoordinator
}
export type ResolvedCampaignHomeAction = CampaignHomeAction & {
href?: string
}
/** Mirrors Início `buildMunicipalityListHref({ coverage: 'sem_assessor', sort: 'votos' }, 1)`. */
export const UNCOVERED_MUNICIPALITIES_LIST_HREF =
'/campanha/municipios?coverage=sem_assessor&sort=votos' as const
export type HomeActionButtonPropsOptions = {
uncoveredMunicipalitiesHref?: string
municipalitySlug?: string
returnPath?: string
}
/** Staff Início catalog with hrefs — shared by quick-action registries (B80+). */
const staffHomeQuickActionsByRole = new Map<CampaignRole, readonly ResolvedCampaignHomeAction[]>()
export const resolveStaffHomeQuickActions = (
role: CampaignRole,
editingScope: AdvisorEditingScope = 'tudo',
returnPath?: string,
): readonly ResolvedCampaignHomeAction[] => {
if (!isStaffCampaignRole(role)) return []
if (!returnPath) {
const cached = staffHomeQuickActionsByRole.get(role)
if (cached && editingScope === 'tudo') return cached
}
const actions = toHomeActionButtonProps(homeActionsForRole(role, editingScope), {
uncoveredMunicipalitiesHref: UNCOVERED_MUNICIPALITIES_LIST_HREF,
returnPath,
})
if (!returnPath && editingScope === 'tudo') {
staffHomeQuickActionsByRole.set(role, actions)
}
return actions
}
export const toHomeActionButtonProps = (
actions: readonly CampaignHomeAction[],
options?: string | HomeActionButtonPropsOptions,
): ResolvedCampaignHomeAction[] => {
const resolved =
typeof options === 'string' ? { uncoveredMunicipalitiesHref: options } : (options ?? {})
return actions.map((action) => {
let href: string | undefined
if (action.id === 'my-contacts') {
href = LEADER_CONTACTS_HOME
} else if (action.id === 'uncovered-municipalities') {
href = resolved.uncoveredMunicipalitiesHref
} else if (isCampaignWizardActionId(action.id)) {
if (resolved.municipalitySlug) {
href = wizardActionHref(
CAMPAIGN_WIZARD_ACTION_SLUGS[action.id],
resolved.municipalitySlug,
{
returnPath: resolved.returnPath,
},
)
} else {
href = appendWizardReturnPath(campaignActionEntryHref(action.id), resolved.returnPath)
}
}
return {
...action,
...(href ? { href } : {}),
}
})
}