mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Fase 3 (server) do plano aprovado:
- schemas/contact.ts: contactCreateSchema (nome único via invariante, state
obrigatório, phones opcionais C112); contactFieldUpdateSchema ganha variantes
city e postalCode ('' → clear); mensagens das actions client-safe aqui.
- schemas/personGenders.ts: enum de gênero em módulo folha (ciclo de imports
contact ↔ leadership detectado e quebrado).
- actions/contact.ts: createContact (gate staff + bypass justificado, lock
contact-name:{normalized}) e updateContactField (escopo 'edita o que vê'
via re-check user-threaded para advisor; reorder de telefone primário).
Apagar reusa as actions C100 existentes.
- collections Contact e Supporter: label de gênero 'Outro' → 'Não binário'
(sem migration; nenhum teste pina a label).
- 11 testes unit novos; lint, tsc e prettier limpos.
228 lines
6 KiB
TypeScript
228 lines
6 KiB
TypeScript
import type { CollectionConfig } from 'payload'
|
|
import { APIError } from 'payload'
|
|
|
|
import { relationshipId } from '@/lib/relationship'
|
|
import {
|
|
canCreateSupporter,
|
|
canDeleteSupporter,
|
|
canManageCampaignStaffField,
|
|
canManageSupporter,
|
|
canReadCampaignStaffField,
|
|
canReadSupporter,
|
|
canSetAdministrativeLeadershipField,
|
|
} from '@/utilities/campaignAccess'
|
|
import { systemStampedActorField } from '@/utilities/campaignAuditFields'
|
|
|
|
export const Supporter: CollectionConfig = {
|
|
slug: 'supporter',
|
|
labels: {
|
|
singular: 'Apoiador',
|
|
plural: 'Apoiadores',
|
|
},
|
|
admin: {
|
|
group: 'Campanha',
|
|
useAsTitle: 'contact',
|
|
defaultColumns: ['contact', 'municipality', 'voteIntention', 'source', 'updatedAt'],
|
|
},
|
|
access: {
|
|
create: canCreateSupporter,
|
|
read: canReadSupporter,
|
|
update: canManageSupporter,
|
|
delete: canDeleteSupporter,
|
|
},
|
|
hooks: {
|
|
beforeChange: [
|
|
async ({ data, operation, originalDoc, req }) => {
|
|
if (operation === 'create' && req.user?.collection === 'campaignUser') {
|
|
data.createdBy = req.user.id
|
|
}
|
|
|
|
const contactID = relationshipId(data.contact ?? originalDoc?.contact)
|
|
const municipalityID = relationshipId(data.municipality ?? originalDoc?.municipality)
|
|
if (!contactID || !municipalityID) return data
|
|
|
|
const existingLeadership = await req.payload.find({
|
|
collection: 'leadership',
|
|
where: {
|
|
and: [{ contact: { equals: contactID } }, { municipalities: { in: [municipalityID] } }],
|
|
},
|
|
depth: 0,
|
|
limit: 1,
|
|
pagination: false,
|
|
overrideAccess: true,
|
|
req,
|
|
})
|
|
|
|
if (existingLeadership.totalDocs > 0) {
|
|
throw new APIError(
|
|
'Este contato já é liderança neste município e não pode ser cadastrado como apoiador.',
|
|
409,
|
|
)
|
|
}
|
|
|
|
return data
|
|
},
|
|
],
|
|
afterChange: [
|
|
async ({ doc, operation, req }) => {
|
|
if (operation !== 'create') return doc
|
|
const { notifySupporterCreated } =
|
|
await import('@/utilities/notification/notificationEvents')
|
|
await notifySupporterCreated(req, doc)
|
|
return doc
|
|
},
|
|
],
|
|
},
|
|
fields: [
|
|
{
|
|
name: 'contact',
|
|
type: 'relationship',
|
|
relationTo: 'contact',
|
|
label: 'Contato',
|
|
required: true,
|
|
index: true,
|
|
access: {
|
|
update: canSetAdministrativeLeadershipField,
|
|
},
|
|
},
|
|
{
|
|
name: 'municipality',
|
|
type: 'relationship',
|
|
relationTo: 'municipality',
|
|
label: 'Município',
|
|
index: true,
|
|
access: {
|
|
update: canManageCampaignStaffField,
|
|
},
|
|
},
|
|
{
|
|
name: 'voteIntention',
|
|
type: 'select',
|
|
label: 'Intenção de voto',
|
|
index: true,
|
|
access: {
|
|
create: canManageCampaignStaffField,
|
|
read: canReadCampaignStaffField,
|
|
update: canManageCampaignStaffField,
|
|
},
|
|
options: [
|
|
{ label: 'Certo', value: 'certo' },
|
|
{ label: 'Tende a certo', value: 'tende_a_certo' },
|
|
{ label: 'Indeciso', value: 'indeciso' },
|
|
{ label: 'Não binário', value: 'outro' },
|
|
],
|
|
},
|
|
{
|
|
name: 'consent',
|
|
type: 'relationship',
|
|
relationTo: 'consent',
|
|
label: 'Consentimento de cadastro',
|
|
access: {
|
|
create: canSetAdministrativeLeadershipField,
|
|
update: canSetAdministrativeLeadershipField,
|
|
},
|
|
},
|
|
{
|
|
name: 'consentContentHash',
|
|
type: 'text',
|
|
label: 'Hash do consentimento de cadastro',
|
|
admin: {
|
|
readOnly: true,
|
|
},
|
|
access: {
|
|
create: canSetAdministrativeLeadershipField,
|
|
update: canSetAdministrativeLeadershipField,
|
|
},
|
|
},
|
|
{
|
|
name: 'consentedAt',
|
|
type: 'date',
|
|
label: 'Consentimento de cadastro em',
|
|
admin: {
|
|
readOnly: true,
|
|
},
|
|
access: {
|
|
create: canSetAdministrativeLeadershipField,
|
|
update: canSetAdministrativeLeadershipField,
|
|
},
|
|
},
|
|
{
|
|
name: 'voteIntentionConsent',
|
|
type: 'relationship',
|
|
relationTo: 'consent',
|
|
label: 'Consentimento de intenção de voto',
|
|
access: {
|
|
create: canSetAdministrativeLeadershipField,
|
|
update: canSetAdministrativeLeadershipField,
|
|
},
|
|
},
|
|
{
|
|
name: 'voteIntentionConsentContentHash',
|
|
type: 'text',
|
|
label: 'Hash do consentimento de intenção',
|
|
admin: {
|
|
readOnly: true,
|
|
},
|
|
access: {
|
|
create: canSetAdministrativeLeadershipField,
|
|
update: canSetAdministrativeLeadershipField,
|
|
},
|
|
},
|
|
{
|
|
name: 'voteIntentionConsentedAt',
|
|
type: 'date',
|
|
label: 'Consentimento de intenção em',
|
|
admin: {
|
|
readOnly: true,
|
|
},
|
|
access: {
|
|
create: canSetAdministrativeLeadershipField,
|
|
update: canSetAdministrativeLeadershipField,
|
|
},
|
|
},
|
|
{
|
|
name: 'source',
|
|
type: 'select',
|
|
label: 'Origem',
|
|
required: true,
|
|
defaultValue: 'manual',
|
|
index: true,
|
|
options: [
|
|
{ label: 'Importação CSV', value: 'import_csv' },
|
|
{ label: 'Cadastro manual', value: 'manual' },
|
|
{ label: 'Liderança', value: 'lideranca' },
|
|
{ label: 'Convite', value: 'convite' },
|
|
{ label: 'Evento', value: 'evento' },
|
|
],
|
|
access: {
|
|
update: canSetAdministrativeLeadershipField,
|
|
},
|
|
},
|
|
{
|
|
name: 'consentNote',
|
|
type: 'textarea',
|
|
label: 'Registro de consentimento externo',
|
|
maxLength: 2000,
|
|
access: {
|
|
create: canManageCampaignStaffField,
|
|
read: canReadCampaignStaffField,
|
|
update: canManageCampaignStaffField,
|
|
},
|
|
},
|
|
{
|
|
name: 'notes',
|
|
type: 'textarea',
|
|
label: 'Observações internas',
|
|
maxLength: 3000,
|
|
access: {
|
|
create: canManageCampaignStaffField,
|
|
read: canReadCampaignStaffField,
|
|
update: canManageCampaignStaffField,
|
|
},
|
|
},
|
|
systemStampedActorField({
|
|
readAccess: canReadCampaignStaffField,
|
|
setAccess: canSetAdministrativeLeadershipField,
|
|
}),
|
|
],
|
|
}
|