teqo-backup/src/utilities/nucleusListOverviewViewModels.ts
Francisco Ignácio Fontoura Solla 94f0bd51f8 Deliver A5 ticket leverage and flip insights with simplify cleanup.
Surface majoritarian base conversion and proportional flip opportunity on nucleus detail and list overview, precompute ticketFlip on the baseline VM, and register post-simplify scale debts in A7 F5 and E7.
2026-07-19 17:34:06 -03:00

134 lines
4.4 KiB
TypeScript

import type { NucleusPriority } from '@/lib/schemas/nucleus'
import type { TicketFlipOverviewAggregate, TicketLeverageOverviewAggregate } from '@/lib/electionInsights'
import type { CampaignUser, NucleusUpdate } from '@/payload-types'
import type { ActionPlanUpcomingPreviewRecord } from '@/utilities/actionPlanUpcomingPreview'
import type { NucleusBaseline2022OverviewAggregate, NucleusConversionOverviewAggregate, NucleusTrendOverviewAggregate } from '@/utilities/nucleusElectoralBaseline'
import type { NucleusChoroplethBundle } from '@/utilities/nucleusChoroplethTypes'
import {
aggregateVoteGoals,
type VoteGoalsSumViewModel,
type VoteGoalsViewModel,
} from '@/utilities/voteGoals'
export const nucleusListOverviewPreviewLimit = 3
export type NucleusListOverviewNucleusRecord = {
id: number
slug: string
name: string
coordinators: Array<number | { id: number }>
cities: string[]
regions: string[]
tseZones: number[]
confirmedVoteEstimate: number | null
proposedVoteEstimate: number | null
voteGoals: VoteGoalsViewModel
priority: NucleusPriority
}
export type NucleusListOverviewUpdateRecord = {
id: number
nucleusSlug: string
nucleusName: string
authorName: string
kind: NucleusUpdate['kind']
createdAt: string
}
export type NucleusListOverviewViewModel = {
totalFiltered: number
estimate: {
confirmedTotal: number
confirmedCount: number
confirmedPercent: number
unconfirmedCount: number
pendingSuggestionsCount?: number
}
coverage: {
coordinatedCount: number
percent: number
}
/** Null when no nucleus in the filtered set has resolvable TSE geography. */
baseline2022: NucleusBaseline2022OverviewAggregate | null
voteGoals: VoteGoalsSumViewModel
highPriorityCount: number
/** Null when no nucleus in the filtered set has resolvable TSE geography. */
trend: NucleusTrendOverviewAggregate | null
/** Null when no comparable nucleus has aptos + confirmed estimate. */
conversion: NucleusConversionOverviewAggregate | null
/** Null when no comparable nucleus has ticket votes + confirmed estimate. */
leverage: TicketLeverageOverviewAggregate | null
/** Null when no nucleus in the filtered set has resolvable TSE geography. */
flipOpportunity: TicketFlipOverviewAggregate | null
recentUpdates: NucleusListOverviewUpdateRecord[]
upcomingActionPlans: ActionPlanUpcomingPreviewRecord[]
choropleth: NucleusChoroplethBundle
}
const percentage = (part: number, total: number): number =>
total === 0 ? 0 : Math.round((part / total) * 100)
export const buildNucleusListOverviewViewModel = ({
nuclei,
recentUpdates,
role,
upcomingActionPlans,
baseline2022 = null,
trend = null,
conversion = null,
leverage = null,
flipOpportunity = null,
choropleth,
}: {
nuclei: NucleusListOverviewNucleusRecord[]
recentUpdates: NucleusListOverviewUpdateRecord[]
role: CampaignUser['role']
upcomingActionPlans: ActionPlanUpcomingPreviewRecord[]
baseline2022?: NucleusBaseline2022OverviewAggregate | null
trend?: NucleusTrendOverviewAggregate | null
conversion?: NucleusConversionOverviewAggregate | null
leverage?: TicketLeverageOverviewAggregate | null
flipOpportunity?: TicketFlipOverviewAggregate | null
choropleth: NucleusChoroplethBundle
}): NucleusListOverviewViewModel => {
const totalFiltered = nuclei.length
const confirmedCount = nuclei.filter(
({ confirmedVoteEstimate }) => confirmedVoteEstimate !== null,
).length
const coordinatedCount = nuclei.filter(({ coordinators }) => coordinators.length > 0).length
const { highPriorityCount, ...voteGoals } = aggregateVoteGoals(nuclei)
return {
totalFiltered,
estimate: {
confirmedTotal: nuclei.reduce(
(sum, nucleus) => sum + (nucleus.confirmedVoteEstimate ?? 0),
0,
),
confirmedCount,
confirmedPercent: percentage(confirmedCount, totalFiltered),
unconfirmedCount: totalFiltered - confirmedCount,
...(role === 'lideranca'
? {}
: {
pendingSuggestionsCount: nuclei.filter(
({ proposedVoteEstimate }) => proposedVoteEstimate !== null,
).length,
}),
},
coverage: {
coordinatedCount,
percent: percentage(coordinatedCount, totalFiltered),
},
baseline2022,
voteGoals,
highPriorityCount,
trend,
conversion,
leverage,
flipOpportunity,
recentUpdates,
upcomingActionPlans,
choropleth,
}
}