teqo/src/utilities/plazaViewModels.ts
Francisco Ignácio Fontoura Solla cf13c441aa Remodel /campanha around predefined Praças replacing electoral nuclei.
436 seeded Praças (municipality-based; per-TSE-zone in Salvador/Camaçari) become
the operational unit; roles renamed to coordinator/advisor/leader with advisors
scoped to administered Praças. Leadership is one record per person across many
Praças/organizations; vote pledges split leader-declared vs staff-estimated
(estimates never serialized to leaders). Adds organizations, demand workflow
with staff-only cost/receipts, action-plan results with deputy presence, manual
political trend, multi-candidate comparison and the diverging red/white/blue
map mode. Migration 20260721_020109_remodel_plazas deliberately resets the
campaign vertical in production (public site untouched); test suite ported to
the new model (unit 187 / int 306 / e2e 9 green).
2026-07-21 02:28:00 -03:00

185 lines
4.9 KiB
TypeScript

import type { Payload } from 'payload'
import type { CampaignUser, Plaza } from '@/payload-types'
import { eligibleCampaignStaffWhere } from '@/utilities/campaignAccess'
import { relationshipId } from '@/utilities/relationship'
import { toVoteGoalsViewModel, type VoteGoalsViewModel } from '@/utilities/voteGoals'
import type { PlazaPledgeAggregate } from '@/utilities/votePledgeData'
import { emptyPlazaPledgeAggregate } from '@/utilities/votePledgeData'
export const plazaListSelect = {
name: true,
slug: true,
kind: true,
city: true,
region: true,
ibgeCode: true,
zoneNumber: true,
advisors: true,
priority: true,
lastUpdateAt: true,
} as const
export type PlazaListViewModel = {
id: number
name: string
slug: string
kind: Plaza['kind']
city: string
region: string
ibgeCode: string
zoneNumber: number | null
advisorIDs: number[]
priority: 'alta' | 'normal'
lastUpdateAt: string | null
pledges: PlazaPledgeAggregate
}
export const toPlazaListViewModel = (
plaza: Plaza,
pledges?: PlazaPledgeAggregate,
): PlazaListViewModel => ({
id: plaza.id,
name: plaza.name,
slug: plaza.slug,
kind: plaza.kind,
city: plaza.city,
region: plaza.region,
ibgeCode: plaza.ibgeCode,
zoneNumber: plaza.zoneNumber ?? null,
advisorIDs: (plaza.advisors ?? []).map(relationshipId).filter((id): id is number => id !== null),
priority: plaza.priority === 'alta' ? 'alta' : 'normal',
lastUpdateAt: plaza.lastUpdateAt ?? null,
pledges: pledges ?? { ...emptyPlazaPledgeAggregate },
})
export type PlazaPoliticalTrendViewModel = {
status: 'favoravel' | 'neutra' | 'desfavoravel' | null
note: string | null
recordedByName: string | null
recordedAt: string | null
}
export type PlazaAdvisorSummary = {
id: number
name: string
phone: string | null
}
export type PlazaDetailViewModel = {
id: number
name: string
slug: string
kind: Plaza['kind']
city: string
region: string
ibgeCode: string
tseCityCode: string
zoneNumber: number | null
tseZones: number[]
advisorIDs: number[]
lastUpdateAt: string | null
/** Staff-only block — null for the leader view model. */
strategy: {
priority: 'alta' | 'normal'
voteGoals: VoteGoalsViewModel
politicalTrend: PlazaPoliticalTrendViewModel
strengths: string[]
risks: string[]
dobradinhaNotes: string | null
nextSteps: string | null
} | null
}
export const toPlazaDetailViewModel = (
plaza: Plaza,
role: CampaignUser['role'],
tseZones: number[],
trendRecordedByName: string | null = null,
): PlazaDetailViewModel => ({
id: plaza.id,
name: plaza.name,
slug: plaza.slug,
kind: plaza.kind,
city: plaza.city,
region: plaza.region,
ibgeCode: plaza.ibgeCode,
tseCityCode: plaza.tseCityCode,
zoneNumber: plaza.zoneNumber ?? null,
tseZones,
advisorIDs: (plaza.advisors ?? []).map(relationshipId).filter((id): id is number => id !== null),
lastUpdateAt: plaza.lastUpdateAt ?? null,
strategy:
role === 'leader'
? null
: {
priority: plaza.priority === 'alta' ? 'alta' : 'normal',
voteGoals: toVoteGoalsViewModel(plaza.voteGoals),
politicalTrend: {
status: plaza.politicalTrend?.status ?? null,
note: plaza.politicalTrend?.note ?? null,
recordedByName: trendRecordedByName,
recordedAt: plaza.politicalTrend?.recordedAt ?? null,
},
strengths: (plaza.strengths ?? []).map((item) => item.text),
risks: (plaza.risks ?? []).map((item) => item.text),
dobradinhaNotes: plaza.dobradinhaNotes ?? null,
nextSteps: plaza.nextSteps ?? null,
},
})
export const loadAdvisorSummaries = async (
payload: Payload,
user: CampaignUser,
advisorIDs: number[],
): Promise<PlazaAdvisorSummary[]> => {
if (advisorIDs.length === 0) return []
const result = await payload.find({
collection: 'campaignUser',
where: {
and: [{ id: { in: advisorIDs } }, eligibleCampaignStaffWhere],
},
depth: 0,
pagination: false,
select: { name: true, phone: true },
user,
overrideAccess: false,
})
const advisorById = new Map(
result.docs.map(({ id, name, phone }) => [id, { id, name, phone: phone ?? null }]),
)
return advisorIDs.flatMap((id) => {
const advisor = advisorById.get(id)
return advisor ? [advisor] : []
})
}
export type EligibleAdvisorOption = Pick<CampaignUser, 'id' | 'name'> & {
isCurrent: boolean
}
export const getEligibleAdvisorOptions = async (
payload: Payload,
user: CampaignUser,
): Promise<EligibleAdvisorOption[]> => {
const result = await payload.find({
collection: 'campaignUser',
depth: 0,
pagination: false,
sort: 'name',
where: eligibleCampaignStaffWhere,
select: { name: true },
user,
overrideAccess: false,
})
return result.docs
.map(({ id, name }) => ({
id,
name,
isCurrent: id === user.id,
}))
.sort((left, right) => Number(right.isCurrent) - Number(left.isCurrent))
}