teqo/src/utilities/municipalityDossierData.ts
Francisco Ignácio Fontoura Solla 619666dd51 C13 pos-simplify: fecha os residuos da varredura e estreita os guards
O rename mecanico deixou um defeito silencioso e alguns fosseis: o fixture de
recentVisits perdeu o ${index} (todas as entradas com o mesmo label, invisivel
porque o label nao era asserido) e access/shared.ts guardava "action plan" em
prosa, forma que o guard de vocabulario nao cobria. O padrao agora tolera espaco
e a allowlist encolheu: o comentario de campaignMigrationReconciliation cita
activity e o arquivo saiu da excecao.

Na migration, a entrada action_plan_municipality_idx era inalcancavel (nenhuma
migration jamais criou esse nome) e saiu, deixando up() e down() com os mesmos
48 pares; as duas buscas de FK passam a usar to_regclass, que retorna NULL em vez
de lancar -- a unica consulta que contradizia a invariante do proprio cabecalho.

O detalhe de organizacao para de derivar o rotulo de status a mao e reusa
ActivityStatusBadge, com status tipado como ActivityStatus no loader.

Debitos do /simplify registrados: C8 F4b (o guard de formActions so inspeciona
arquivos chamados exatamente formActions.ts) no plano do C8; defers e descartes
com gatilho no plano do C13.
2026-07-25 21:29:32 -03:00

130 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'server-only'
import type { Payload } from 'payload'
import {
demographicsForCode,
type MunicipalityDemographics,
} from '@/lib/bahiaMunicipalityDemographics'
import type { Activity, CampaignUser } from '@/payload-types'
import { buildActivityListWhere } from '@/utilities/activityUi'
import {
activityListSelect,
toActivityListViewModel,
type ActivityListViewModel,
} from '@/utilities/activityViewModels'
import {
loadFreshestMunicipalityLeaderships,
type LeadershipRowViewModel,
} from '@/utilities/leadershipData'
import { municipalityElectionGeographyForSlug } from '@/utilities/municipalityElectionGeography'
import {
loadMunicipalityElectoralBaseline,
type MunicipalityElectoralBaseline,
} from '@/utilities/municipalityElectoralBaseline'
import {
loadMunicipalityGoalAccount,
type MunicipalityGoalAccount,
} from '@/utilities/municipalityGoalAccount'
import {
loadMunicipalityUpdatesFeed,
type MunicipalityUpdateViewModel,
} from '@/utilities/municipalityUpdatePageData'
import type { MunicipalityDetailViewModel } from '@/utilities/municipalityViewModels'
import { loadMunicipalityPledges } from '@/utilities/votePledgeData'
import {
aggregateMunicipalityPledgesFromRows,
type MunicipalityPledgeAggregate,
} from '@/utilities/votePledgeViews'
/**
* E16 dossiê — section caps. The dossier is a 1–2 page pre-visit read, not a
* report: each section shows at most this many rows and links to its full tab.
*/
export const DOSSIER_LEADERSHIP_LIMIT = 8
export const DOSSIER_SIGNAL_LIMIT = 5
const DOSSIER_UPCOMING_ACTIVITY_LIMIT = 3
const DOSSIER_RECENT_ACTIVITY_LIMIT = 2
export type MunicipalityDossierData = {
baseline: MunicipalityElectoralBaseline | null
goalAccount: MunicipalityGoalAccount | null
pledgeAggregate: MunicipalityPledgeAggregate
leaderships: { rows: LeadershipRowViewModel[]; totalCount: number }
signals: { rows: MunicipalityUpdateViewModel[]; totalCount: number }
upcomingActivities: ActivityListViewModel[]
recentActivities: ActivityListViewModel[]
demographics: MunicipalityDemographics | null
}
const loadDossierActivities = async (
payload: Payload,
user: CampaignUser,
municipalityID: number,
): Promise<{ upcoming: ActivityListViewModel[]; recent: ActivityListViewModel[] }> => {
const now = new Date()
const findActivities = (tab: 'proximos' | 'realizados', limit: number, sort: string) =>
payload.find({
collection: 'activity',
where: buildActivityListWhere({ page: 1, tab, municipality: municipalityID }, now),
depth: 0,
limit,
pagination: false,
sort,
select: activityListSelect,
user,
overrideAccess: false,
})
const [upcoming, recent] = await Promise.all([
findActivities('proximos', DOSSIER_UPCOMING_ACTIVITY_LIMIT, 'startAt'),
findActivities('realizados', DOSSIER_RECENT_ACTIVITY_LIMIT, '-startAt'),
])
return {
upcoming: upcoming.docs.map((activity) => toActivityListViewModel(activity as Activity)),
recent: recent.docs.map((activity) => toActivityListViewModel(activity as Activity)),
}
}
/**
* Composes the pre-visit dossier from the same loaders the detail tabs use —
* one pass, no second source of truth (decisão travada do plano E16).
*/
export const loadMunicipalityDossierData = async (
payload: Payload,
user: CampaignUser,
view: MunicipalityDetailViewModel,
): Promise<MunicipalityDossierData> => {
const geography = municipalityElectionGeographyForSlug(view.slug)
const [baseline, pledgesAndGoal, leaderships, feed, activities] = await Promise.all([
geography ? loadMunicipalityElectoralBaseline(user, geography) : null,
loadMunicipalityPledges(payload, user, view.id).then(async (pledges) => {
const pledgeAggregate = aggregateMunicipalityPledgesFromRows(pledges)
const goalAccount = view.strategy
? await loadMunicipalityGoalAccount(
payload,
user,
{ slug: view.slug, expectedVotes: view.strategy.expectedVotes },
pledgeAggregate,
)
: null
return { pledgeAggregate, goalAccount }
}),
loadFreshestMunicipalityLeaderships(payload, user, view.id, DOSSIER_LEADERSHIP_LIMIT),
loadMunicipalityUpdatesFeed(payload, user, view.id, { page: 1 }),
loadDossierActivities(payload, user, view.id),
])
return {
baseline,
goalAccount: pledgesAndGoal.goalAccount,
pledgeAggregate: pledgesAndGoal.pledgeAggregate,
leaderships,
signals: { rows: feed.updates.slice(0, DOSSIER_SIGNAL_LIMIT), totalCount: feed.totalDocs },
upcomingActivities: activities.upcoming,
recentActivities: activities.recent,
demographics: demographicsForCode(view.ibgeCode) ?? null,
}
}