teqo/src/utilities/activityLeadershipOptions.ts
Francisco Solla 6de8fad8f6 feat(C112): múltiplos telefones por pessoa — Contact.phones array (ordem = prioridade)
- Contact.phone (text único) → phones (array field Payload, subcampo value):
  fonte única; principal = phones[0]; beforeValidate normaliza, filtra vazios,
  rejeita inválido e duplicado DENTRO da ficha (C111 entre fichas preservado)
- migration add_contact_phones_array: backfill phone → phones[0] (gen_random_uuid)
  antes do drop da coluna + índice em value (hand-edit documentado; snapshot .json
  gerado mantém as colunas C99/C100 já existentes)
- primaryPhoneOf / phoneValuesOf / reorderWithPrimaryPhone em src/lib/phone.ts
  (ordem = prioridade; inline edit = seta o principal preservando o resto)
- findOrCreateContactByPhone aceita phones[] (dedupe pelo primário; reuse faz
  append dos faltantes; locks em todos os números digitados)
- sync conta→ficha (CampaignUser): phone da conta faz APPEND quando a ficha não
  tem (nunca reordena nem remove); tombstone LGPD substitui TODOS os telefones
- convite redemption: telefone digitado vira principal, demais preservados
- import CSV: preview sinaliza compartilhado também por número secundário;
  bulk insere contact_phones (JOIN nas leituras, uuid no insert)
- busca por telefone (apoiadores, pickers, combobox) casa qualquer número
- UI: PhonesFieldEditor compartilhado (repetível mascarado, remover, ↑/↓,
  modo inline com Salvar) nas 5 superfícies — liderança (form/ficha/lista),
  apoiador (form/ficha), dobradinha (ficha), assessor (ficha), wizard
- UI pública (petição/WhatsApp) intocada: zod e forms ficam com phone;
  as ações mapeiam phone → phones[0]
- testes: spec novo campaignMultiPhones (aceite completo), specs C111/C99
  atualizados, e2e wizard/apoiador ajustados
2026-08-11 10:47:02 -03:00

70 lines
2.1 KiB
TypeScript

import 'server-only'
import type { Payload } from 'payload'
import { isContactSearchQueryReady, normalizeContactSearchQuery } from '@/lib/contactSearchQuery'
import { isPopulatedRelationship, relationshipId } from '@/lib/relationship'
import type { CampaignUser, Contact, Leadership } from '@/payload-types'
export type ActivityLeadershipOption = {
id: number
label: string
}
const LEADERSHIP_SEARCH_LIMIT = 20
const leadershipLabel = (leadership: Leadership): string | null => {
const id = relationshipId(leadership.id)
if (!id) return null
const contact = leadership.contact
return isPopulatedRelationship<Contact>(contact) ? contact.name : `Liderança #${id}`
}
/**
* Engaged leaderships visible to the actor (for linking an activity to a leadership).
* Uses overrideAccess: false so Payload row-level access applies.
*/
export const searchActivityLeadershipOptions = async (
payload: Pick<Payload, 'find'>,
user: CampaignUser,
query: string,
): Promise<ActivityLeadershipOption[]> => {
const normalizedQuery = typeof query === 'string' ? query : ''
if (!isContactSearchQueryReady(normalizedQuery)) return []
const { trimmed, digits } = normalizeContactSearchQuery(normalizedQuery)
const result = await payload.find({
collection: 'leadership',
where: {
and: [
{ supportStatus: { equals: 'engajado' } },
{
or: [
{ 'contact.name': { contains: trimmed } },
{ 'contact.phones.value': { contains: digits || trimmed } },
],
},
],
},
depth: 1,
limit: LEADERSHIP_SEARCH_LIMIT,
select: {
contact: true,
supportStatus: true,
},
user,
overrideAccess: false,
})
return result.docs
.map((doc) => {
const leadership = doc as Leadership
const id = relationshipId(leadership.id)
const label = leadershipLabel(leadership)
if (!id || !label) return null
return { id, label }
})
.filter((option): option is ActivityLeadershipOption => option !== null)
.sort((left, right) => left.label.localeCompare(right.label, 'pt-BR'))
}