import 'server-only' import type { Payload } from 'payload' import type { RelationOption } from '@/components/campaign/shared/RelationMultiSelect' import { populatedContactName } from '@/lib/relationship' import type { CampaignUser } from '@/payload-types' import { eligibleCampaignStaffWhere, getWritableMunicipalityIds } from '@/utilities/campaignAccess' /** Municipalities the actor may operate on (coordinator: all 435; advisor: administered). */ export const loadMunicipalityOptions = async ( payload: Payload, user: CampaignUser, ): Promise => { const result = await payload.find({ collection: 'municipality', depth: 0, limit: 0, pagination: false, sort: 'name', select: { name: true }, where: {}, user, overrideAccess: false, }) return result.docs.map((municipality) => ({ id: municipality.id, name: municipality.name })) } /** * C144 — WRITE-scoped variant for the write forms: only municipalities the * actor may write under the Edição axis (`getWritableMunicipalityIds`). * `null` (unrestricted staff or `editing === 'tudo'`) delegates to the read * loader (same query — the whole catalog); `[]` (`somente_leitura`) returns no * options without a fetch; a carteira resolves to exactly the portfolio ids. * Read surfaces keep `loadMunicipalityOptions`. */ export const loadWritableMunicipalityOptions = async ( payload: Payload, user: CampaignUser, ): Promise => { const writableIds = await getWritableMunicipalityIds(payload, user) if (writableIds === null) return loadMunicipalityOptions(payload, user) if (writableIds.length === 0) return [] const result = await payload.find({ collection: 'municipality', depth: 0, limit: 0, pagination: false, sort: 'name', select: { name: true }, where: { id: { in: writableIds } }, user, overrideAccess: false, }) return result.docs.map((municipality) => ({ id: municipality.id, name: municipality.name })) } export const loadOrganizationOptions = async ( payload: Payload, user: CampaignUser, ): Promise => { const result = await payload.find({ collection: 'organization', depth: 0, limit: 0, pagination: false, sort: 'name', select: { name: true }, where: {}, user, overrideAccess: false, }) return result.docs.map((organization) => ({ id: organization.id, name: organization.name })) } /** * `name` folds in the party for select/typeahead disambiguation (e.g. * `RelationMultiSelect`); `plainName`/`party`/`slug` ride along so cell-level * consumers (B31's "Dobradinhas" column) can build a `StateDeputySummary` * chip for an optimistic add and link to `/campanha/dobradinhas/` * without a second catalog fetch. */ export type StateDeputyRelationOption = RelationOption & { plainName: string party: string | null slug: string } export const loadStateDeputyOptions = async ( payload: Payload, user: CampaignUser, ): Promise => { const result = await payload.find({ collection: 'stateDeputy', depth: 1, limit: 0, pagination: false, sort: 'contact.name', select: { contact: true, party: true, slug: true }, where: {}, user, overrideAccess: false, }) return result.docs.map((stateDeputy) => { const plainName = populatedContactName(stateDeputy.contact) return { id: stateDeputy.id, name: stateDeputy.party ? `${plainName} (${stateDeputy.party})` : plainName, plainName, party: stateDeputy.party ?? null, slug: stateDeputy.slug, } }) } /** * Leadership catalog for typeahead cells (B36). Access of `canReadLeadership` * already scopes an advisor to leaderships of administered municipalities — * no manual intersection with `getAccessibleLeadershipIds`. */ export const loadLeadershipOptions = async ( payload: Payload, user: CampaignUser, ): Promise => { const result = await payload.find({ collection: 'leadership', depth: 1, limit: 0, pagination: false, select: { contact: true }, where: {}, user, overrideAccess: false, }) return result.docs .map((leadership) => ({ id: leadership.id, name: populatedContactName(leadership.contact), })) .sort((left, right) => left.name.localeCompare(right.name, 'pt-BR')) } export type CampaignUserSummary = { id: number name: string } /** * Names for a set of assigned advisor ids (B156) — the same catalog read the * municipios list does with `loadAdvisorSummaries`, but without the phone and * in the shared relation-options module (the dobradinhas cells need id+name * only). Honours `canReadCampaignUsers` and the eligibility filter, so a * stale id that fell out of staff is dropped — same fail-closed contract as * `loadAdvisorSummaries`. */ export const loadCampaignUserSummaries = async ( payload: Payload, user: CampaignUser, ids: number[], ): Promise => { if (ids.length === 0) return [] const result = await payload.find({ collection: 'campaignUser', where: { and: [{ id: { in: ids } }, eligibleCampaignStaffWhere], }, depth: 0, pagination: false, select: { name: true }, user, overrideAccess: false, }) const byId = new Map(result.docs.map((doc) => [doc.id, { id: doc.id, name: doc.name }])) return ids.flatMap((id) => { const summary = byId.get(id) return summary ? [summary] : [] }) } /** * The addable staff catalog for an advisor-relation cell (B156): every * eligible account (coordinator/advisor/candidate), sorted by name. The cell * resolves optimistic adds through it, same role as `getEligibleAdvisorOptions` * on the municipios list. */ export const loadEligibleAdvisorOptions = async ( payload: Payload, user: CampaignUser, ): Promise => { const result = await payload.find({ collection: 'campaignUser', depth: 0, pagination: false, sort: 'name', where: eligibleCampaignStaffWhere, select: { name: true }, user, overrideAccess: false, }) return result.docs.map(({ id, name }) => ({ id, name })) }