teqo/src/utilities/supporterViewModels.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

97 lines
3.2 KiB
TypeScript

import { territoryForCity } from '@/lib/bahiaTerritories'
import type { SupporterVoteIntention } from '@/lib/schemas/supporter'
import type { CampaignUser, Contact, Plaza, Supporter } from '@/payload-types'
import { formatBrazilianPhoneInput } from '@/utilities/phone'
import { isPopulatedRelationship } from '@/utilities/relationship'
import { supporterSourceLabels } from '@/utilities/supporterUi'
export type SupporterListItemViewModel = {
id: number
name: string
phone: string
city: string | null
plazaName: string | null
plazaSlug: string | null
voteIntention: SupporterVoteIntention | null
}
export type SupporterListOverviewViewModel = {
total: number
certoAndTende: number
indeciso: number
}
export type SupporterDetailViewModel = {
id: number
name: string
phone: string
phoneDisplay: string
email: string | null
city: string | null
territory: string | null
plazaName: string | null
plazaSlug: string | null
voteIntention: SupporterVoteIntention | null
sourceLabel: string
consentedAt: string | null
createdByName: string | null
hasVoteIntentionConsent: boolean
createdAt: string
}
const contactFromSupporter = (supporter: { contact: Supporter['contact'] }): Contact | null =>
isPopulatedRelationship<Contact>(supporter.contact) ? supporter.contact : null
const plazaFromSupporter = (supporter: { plaza?: Supporter['plaza'] }): Plaza | null =>
isPopulatedRelationship<Plaza>(supporter.plaza) ? supporter.plaza : null
export const toSupporterListItemViewModel = (
supporter: Pick<Supporter, 'id' | 'voteIntention' | 'contact' | 'plaza'>,
): SupporterListItemViewModel => {
const contact = contactFromSupporter(supporter)
const plaza = plazaFromSupporter(supporter)
return {
id: supporter.id,
name: contact?.name ?? 'Contato sem nome',
phone: contact?.phone ?? '',
city: contact?.city ?? null,
plazaName: plaza?.name ?? null,
plazaSlug: plaza?.slug ?? null,
voteIntention: supporter.voteIntention ?? null,
}
}
export const toSupporterDetailViewModel = (supporter: Supporter): SupporterDetailViewModel => {
const contact = contactFromSupporter(supporter)
const plaza = plazaFromSupporter(supporter)
const creator = isPopulatedRelationship<CampaignUser>(supporter.createdBy)
? supporter.createdBy
: null
const city = contact?.city ?? null
const phone = contact?.phone ?? ''
return {
id: supporter.id,
name: contact?.name ?? 'Contato sem nome',
phone,
phoneDisplay: phone ? formatBrazilianPhoneInput(phone) : '',
email: contact?.email ?? null,
city,
territory: city ? (territoryForCity(city) ?? null) : null,
plazaName: plaza?.name ?? null,
plazaSlug: plaza?.slug ?? null,
voteIntention: supporter.voteIntention ?? null,
sourceLabel: supporterSourceLabels[supporter.source],
consentedAt: supporter.consentedAt ?? null,
createdByName: creator?.name ?? null,
hasVoteIntentionConsent: Boolean(supporter.voteIntentionConsentedAt),
createdAt: supporter.createdAt,
}
}
export const parseSupporterId = (value: string): number | null => {
if (!/^[1-9]\d*$/.test(value)) return null
const id = Number(value)
return Number.isSafeInteger(id) ? id : null
}