- 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
70 lines
2.1 KiB
TypeScript
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'))
|
|
}
|