teqo/src/utilities/activityPageData.ts
Francisco Solla 59983b33f7 feat(campanha): C15 — FullCalendar em /campanha/agenda
Issue #390 — entrada operacional staff com FullCalendar v7 route-local
(semana/mês/lista), filtros compartilháveis por município/deputado/tag,
criação pelo slot, detalhe por URL e remarcação drag/resize sob RBAC,
transação e lock por atividade.

- /campanha/atividades e giros permanecem como lista/fluxos legados
- Sem migration/collection nova; reutiliza activity de C14
- Fuso Bahia, range exclusivo, erro/revert, loading/empty testados
- Leader negado, advisor limitado pelo access atual
- E2E cobre filtros, drag, mobile 390x844 e slot prefill
- Int specs cobrem RBAC, concorrência e defense-in-depth

Impl: docs/plans/fullcalendar-agenda-campanha-impl.md
2026-08-07 04:37:36 -03:00

149 lines
3.9 KiB
TypeScript

import 'server-only'
import type { Payload } from 'payload'
import type { ActivityAgendaRequestData } from '@/lib/schemas/activity'
import type { Activity, CampaignUser } from '@/payload-types'
import type { ActivityDetailTab } from '@/utilities/activityDetailTabUi'
import {
activityPageSize,
buildActivityAgendaWhere,
buildActivityListWhere,
parseActivityListParams,
} from '@/utilities/activityUi'
import {
activityAgendaSelect,
activityFormSelect,
activityListSelect,
getActivityDetailSelect,
toActivityAgendaEvent,
toActivityFormViewModel,
} from '@/utilities/activityViewModels'
import { createEntityNotFoundError } from '@/utilities/entityNotFound'
type ActivityListSearchParams = Record<string, string | string[] | undefined>
export const ActivityNotFoundError = createEntityNotFoundError(
'Activity',
'Atividade não encontrada.',
)
export type AccessibleActivityContext = {
id: number
slug: string
document: Activity
}
export const loadActivityListPageData = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
searchParams: Promise<ActivityListSearchParams> | ActivityListSearchParams,
now = new Date(),
) => {
const state = parseActivityListParams(await searchParams)
const result = await payload.find({
collection: 'activity',
depth: 1,
limit: activityPageSize,
page: state.page,
sort: 'startAt',
where: buildActivityListWhere(state, now),
select: activityListSelect,
user,
overrideAccess: false,
})
return { result, state }
}
export const loadActivityAgendaEventsData = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
request: ActivityAgendaRequestData,
) => {
const result = await payload.find({
collection: 'activity',
depth: 1,
limit: 0,
pagination: false,
sort: 'startAt',
where: buildActivityAgendaWhere(request, request.rangeStart, request.rangeEnd),
select: activityAgendaSelect,
user,
overrideAccess: false,
})
return result.docs.map((activity) => toActivityAgendaEvent(activity as Activity, user))
}
export const loadAccessibleActivityTags = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
): Promise<string[]> => {
const result = await payload.find({
collection: 'activity',
depth: 0,
limit: 0,
pagination: false,
select: { tags: true },
user,
overrideAccess: false,
})
const tags = new Set(result.docs.flatMap((activity) => activity.tags ?? []))
return [...tags].sort((left, right) => left.localeCompare(right, 'pt-BR'))
}
const getActivityDetailQueryDepth = (activeTab: ActivityDetailTab): number =>
activeTab === 'updates' ? 0 : 1
const loadAccessibleActivityBySlug = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
activitySlug: string,
select: ReturnType<typeof getActivityDetailSelect> | typeof activityFormSelect,
depth: number,
): Promise<Activity> => {
const result = await payload.find({
collection: 'activity',
where: { slug: { equals: activitySlug } },
depth,
limit: 1,
pagination: false,
select,
user,
overrideAccess: false,
})
const activity = result.docs[0]
if (!activity) throw new ActivityNotFoundError()
return activity as Activity
}
export const resolveAccessibleActivityContext = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
activitySlug: string,
activeTab: ActivityDetailTab = 'overview',
): Promise<AccessibleActivityContext> => {
const document = await loadAccessibleActivityBySlug(
payload,
user,
activitySlug,
getActivityDetailSelect(activeTab),
getActivityDetailQueryDepth(activeTab),
)
return {
id: document.id,
slug: document.slug,
document,
}
}
export const getActivityEditPageData = async (
payload: Payload,
user: CampaignUser,
activitySlug: string,
) =>
toActivityFormViewModel(
await loadAccessibleActivityBySlug(payload, user, activitySlug, activityFormSelect, 1),
)