teqo-backup/src/utilities/actionPlanDetailPageData.ts
Francisco Ignácio Fontoura Solla cf6ae6c47c Pass 2 W2: correcoes de fronteira lib/utilities e client boundary
- helpers puros movidos utilities->lib: slug, phone, wordStartFilter, voteEstimate (9 inversoes lib->utilities zeradas)
- chaves de consent em src/lib/campaignConsentKeys.ts (client-safe); campaignInvite/campaignConsent re-exportam; onda0ConsentTexts referencia as constantes
- 'server-only' em 21 loaders Payload/cache (CLI segue com --conditions=react-server)
- ciclo de tipos supporterListFilters <-> supporterUi quebrado (SupporterListFilterInput estrutural; sqlFilters idem)
- votePledgeData dividido: votePledgeViews (puro, client-safe) + votePledgeData (loaders, server-only) - padrao contract-module
- predicados de role client-safe em src/lib/campaignRoles.ts; access/shared deriva deles (fonte unica); nav.ts para de importar o barrel campaignAccess (casts 'as CampaignUser' eliminados)
2026-07-25 14:38:41 -03:00

136 lines
4.3 KiB
TypeScript

import 'server-only'
import type { Payload } from 'payload'
import type { ActionPlan, CampaignDemand, CampaignUser } from '@/payload-types'
import type { ActionPlanDetailTab } from '@/utilities/actionPlanDetailTabUi'
import {
actionPlanMunicipalitySummary,
toActionPlanDetailViewModel,
type ActionPlanDetailViewModel,
type ActionPlanMunicipalitySummary,
} from '@/utilities/actionPlanViewModels'
import { relationshipId } from '@/utilities/relationship'
import type { AccessibleActionPlanContext } from '@/utilities/actionPlanPageData'
type ActionPlanDemandSummary = {
id: number
title: string
slug: string
kind: CampaignDemand['kind']
status: CampaignDemand['status']
cost: number | null
}
const loadActionPlanUpdateAuthorNames = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
updates: NonNullable<ActionPlan['updates']>,
): Promise<Map<number, string>> => {
const authorIds = [
...new Set(
updates
.map((update) => relationshipId(update.author))
.filter((id): id is number => id !== null),
),
]
if (authorIds.length === 0) return new Map()
const result = await payload.find({
collection: 'campaignUser',
where: { id: { in: authorIds } },
depth: 0,
pagination: false,
select: { name: true },
user,
overrideAccess: false,
})
return new Map(result.docs.map((author) => [author.id, author.name]))
}
/**
* Display-name lookup for a plan's municipality when the document was loaded at
* depth 0 (updates tab). The actor already passed row access on the plan
* itself, so reading the municipality's name/slug privileged avoids a second
* per-role access round-trip (established display-name pattern).
*/
const loadActionPlanMunicipalitySummaryById = async (
payload: Pick<Payload, 'find'>,
municipalityId: number,
): Promise<ActionPlanMunicipalitySummary | null> => {
const result = await payload.find({
collection: 'municipality',
where: { id: { equals: municipalityId } },
depth: 0,
limit: 1,
pagination: false,
select: { name: true, slug: true },
overrideAccess: true,
})
const municipality = result.docs[0]
return municipality
? { id: municipality.id, name: municipality.name, slug: municipality.slug }
: null
}
export const getActionPlanDetailPageData = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
context: AccessibleActionPlanContext,
activeTab: ActionPlanDetailTab,
): Promise<
ActionPlanDetailViewModel & {
demands: ActionPlanDemandSummary[]
demandCostTotal: number
}
> => {
const municipalityId = relationshipId(context.document.municipality)
const populatedMunicipalitySummary =
actionPlanMunicipalitySummary(context.document.municipality) ?? null
const municipalitySummaryPromise = populatedMunicipalitySummary
? Promise.resolve(populatedMunicipalitySummary)
: municipalityId
? loadActionPlanMunicipalitySummaryById(payload, municipalityId)
: Promise.resolve(null)
const authorNamesPromise =
activeTab === 'updates' && context.document.updates?.length
? loadActionPlanUpdateAuthorNames(payload, user, context.document.updates)
: Promise.resolve(new Map<number, string>())
const demandSummariesPromise: Promise<ActionPlanDemandSummary[]> =
activeTab === 'overview'
? payload
.find({
collection: 'campaignDemand',
where: { actionPlan: { equals: context.id } },
depth: 0,
pagination: false,
sort: 'createdAt',
select: { title: true, slug: true, kind: true, status: true, cost: true },
user,
overrideAccess: false,
})
.then(({ docs }) =>
docs.map((demand) => ({
id: demand.id,
title: demand.title,
slug: demand.slug,
kind: demand.kind,
status: demand.status,
cost: demand.cost ?? null,
})),
)
: Promise.resolve([])
const [municipalitySummary, authorNames, demands] = await Promise.all([
municipalitySummaryPromise,
authorNamesPromise,
demandSummariesPromise,
])
return {
...toActionPlanDetailViewModel(context.document, activeTab, authorNames, municipalitySummary),
demands,
demandCostTotal: demands.reduce((total, demand) => total + (demand.cost ?? 0), 0),
}
}