teqo-backup/src/utilities/activityDetailPageData.ts
Cursor Agent 8372cec91d
C11: picker remoto de atividades na demanda + paginação de vínculos
- Substitui loadActivityOptions (pagination: false) por AsyncSearchCombobox
  com busca server-backed filtrada por município (searchActivityRelationOptions).
- Pagina card Demandas vinculadas no detalhe da atividade (10/página) com
  soma de custo em query separada e link Ver todas na lista filtrada.
- Adiciona filtro ?activity= na lista /campanha/demandas.

Co-authored-by: Francisco Solla <fsolla@users.noreply.github.com>
2026-08-01 06:37:53 +00:00

179 lines
5.1 KiB
TypeScript

import 'server-only'
import type { Payload } from 'payload'
import { relationshipId } from '@/lib/relationship'
import type { Activity, CampaignDemand, CampaignUser } from '@/payload-types'
import type { ActivityDetailTab } from '@/utilities/activityDetailTabUi'
import type { AccessibleActivityContext } from '@/utilities/activityPageData'
import {
activityMunicipalitySummary,
toActivityDetailViewModel,
type ActivityDetailViewModel,
type ActivityMunicipalitySummary,
} from '@/utilities/activityViewModels'
export const ACTIVITY_LINKED_DEMANDS_PAGE_SIZE = 10
type ActivityDemandSummary = {
id: number
title: string
slug: string
kind: CampaignDemand['kind']
status: CampaignDemand['status']
cost: number | null
}
export type ActivityLinkedDemandsPage = {
demands: ActivityDemandSummary[]
page: number
totalPages: number
totalDocs: number
demandCostTotal: number
}
const emptyLinkedDemandsPage = (): ActivityLinkedDemandsPage => ({
demands: [],
page: 1,
totalPages: 1,
totalDocs: 0,
demandCostTotal: 0,
})
const loadActivityUpdateAuthorNames = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
updates: NonNullable<Activity['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 an activity's municipality when the document was loaded at
* depth 0 (updates tab). The actor already passed row access on the activity
* itself, so reading the municipality's name/slug privileged avoids a second
* per-role access round-trip (established display-name pattern).
*/
const loadActivityMunicipalitySummaryById = async (
payload: Pick<Payload, 'find'>,
municipalityId: number,
): Promise<ActivityMunicipalitySummary | 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
}
const loadLinkedDemandsPage = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
activityId: number,
page: number,
): Promise<ActivityLinkedDemandsPage> => {
const where = { activity: { equals: activityId } }
const [pageResult, costRows] = await Promise.all([
payload.find({
collection: 'campaignDemand',
where,
depth: 0,
limit: ACTIVITY_LINKED_DEMANDS_PAGE_SIZE,
page,
sort: '-createdAt',
select: { title: true, slug: true, kind: true, status: true, cost: true },
user,
overrideAccess: false,
}),
payload.find({
collection: 'campaignDemand',
where,
depth: 0,
limit: 0,
pagination: false,
select: { cost: true },
user,
overrideAccess: false,
}),
])
const demands = pageResult.docs.map((demand) => ({
id: demand.id,
title: demand.title,
slug: demand.slug,
kind: demand.kind,
status: demand.status,
cost: demand.cost ?? null,
}))
return {
demands,
page: pageResult.page ?? page,
totalPages: pageResult.totalPages,
totalDocs: pageResult.totalDocs,
demandCostTotal: costRows.docs.reduce((total, demand) => total + (demand.cost ?? 0), 0),
}
}
export const getActivityDetailPageData = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
context: AccessibleActivityContext,
activeTab: ActivityDetailTab,
linkedDemandsPage = 1,
): Promise<ActivityDetailViewModel & ActivityLinkedDemandsPage> => {
const municipalityId = relationshipId(context.document.municipality)
const populatedMunicipalitySummary =
activityMunicipalitySummary(context.document.municipality) ?? null
const municipalitySummaryPromise = populatedMunicipalitySummary
? Promise.resolve(populatedMunicipalitySummary)
: municipalityId
? loadActivityMunicipalitySummaryById(payload, municipalityId)
: Promise.resolve(null)
const authorNamesPromise =
activeTab === 'updates' && context.document.updates?.length
? loadActivityUpdateAuthorNames(payload, user, context.document.updates)
: Promise.resolve(new Map<number, string>())
const linkedDemandsPromise =
activeTab === 'overview'
? loadLinkedDemandsPage(payload, user, context.id, linkedDemandsPage)
: Promise.resolve(emptyLinkedDemandsPage())
const [municipalitySummary, authorNames, linkedDemands] = await Promise.all([
municipalitySummaryPromise,
authorNamesPromise,
linkedDemandsPromise,
])
return {
...toActivityDetailViewModel(context.document, activeTab, authorNames, municipalitySummary),
...linkedDemands,
}
}