teqo/src/lib/schemas/advisor.ts
Francisco Ignácio Fontoura Solla 812e3a0bee B34: editable município chips on the leaderships list.
The "Municípios" column was dead text; it is now chips that navigate to the
município, with a territory chip when a leadership covers a whole TI, and it is
editable in place. What picks inline vs. Drawer is the `pointer:` media query,
not viewport width: on a fine pointer the editor is always mounted (the "×" sits
at the chip's corner, revealed on hover/focus) and on a coarse pointer a tap
anywhere in the cell opens a Drawer with 44 px targets.

`AdvisorMunicipalityCell` is promoted to `shared/MunicipalityPortfolioCell` and
/campanha/assessores migrates onto it in the same change, so the second call site
is born with the first. One batch action only; the single-id advisor twin is
deleted with its int tests migrated. The anti-goal on always-mounted list inputs
is amended for chip-based relations on fine pointers, not reopened.

Also fixes a latent bug: `CampaignTableHead`'s `description` seam broke any RSC
caller, because its two helpers lived in a 'use client' module.
2026-07-27 22:35:19 -03:00

78 lines
2.9 KiB
TypeScript

import { z } from 'zod'
import { MAX_ADVISORS_PER_MUNICIPALITY } from '@/lib/schemas/municipality'
import {
brazilianMobile,
optionalPersistedEmail,
positiveRelationshipId,
trimmedOptionalText,
} from '@/lib/schemas/primitives'
const PLANILHA_INVALID_EMAIL_SUFFIX = '@planilha.invalid'
export const isPlanilhaPlaceholderEmail = (email: string | null | undefined): boolean =>
typeof email === 'string' && email.trim().toLowerCase().endsWith(PLANILHA_INVALID_EMAIL_SUFFIX)
/** Stable seed-style placeholder used when the e-mail field is cleared in the list UI. */
export const planilhaPlaceholderEmailForAdvisor = (advisorId: number): string =>
`assessor-${advisorId}${PLANILHA_INVALID_EMAIL_SUFFIX}`
const advisorPhoneInput = z.union([brazilianMobile, z.literal(''), z.null()]).optional()
const optionalAdvisorPhone = advisorPhoneInput.transform((value) =>
value === '' || value === null || value === undefined ? undefined : value,
)
const nullableAdvisorPhone = advisorPhoneInput.transform((value) =>
value === '' || value === undefined ? null : value,
)
export const PLACEHOLDER_RESET_MESSAGE =
'Troque o e-mail placeholder da planilha por um e-mail real antes de enviar o link de senha.'
export const ADVISOR_UNRESTRICTED_MESSAGE =
'Somente a coordenação geral ou o candidato gerencia assessores.'
export const ADVISOR_EMAIL_CONFLICT_MESSAGE = 'Já existe uma conta com este e-mail.'
export const ADVISOR_ACTION_SAFE_MESSAGES = [
ADVISOR_UNRESTRICTED_MESSAGE,
ADVISOR_EMAIL_CONFLICT_MESSAGE,
PLACEHOLDER_RESET_MESSAGE,
'Só é possível gerenciar contas com papel de Assessor nesta tela.',
'Use Meu perfil para alterar a própria conta.',
`Cada município aceita no máximo ${MAX_ADVISORS_PER_MUNICIPALITY} assessores.`,
] as const
export const advisorCreateSchema = z.object({
name: z.string().trim().min(2).max(160),
email: z.email('E-mail inválido.'),
phone: optionalAdvisorPhone,
})
export const advisorProfileUpdateSchema = z
.object({
id: positiveRelationshipId,
name: trimmedOptionalText(160),
email: optionalPersistedEmail,
phone: nullableAdvisorPhone,
})
.refine(
(value) => value.name !== undefined || value.email !== undefined || value.phone !== undefined,
{ message: 'Informe ao menos um campo para atualizar.' },
)
export const advisorMunicipalitiesBatchSchema = z.object({
advisorId: positiveRelationshipId,
municipalityIds: z.array(positiveRelationshipId).min(1).max(435),
assigned: z.boolean(),
})
export const advisorPasswordResetSchema = z.object({
advisorId: positiveRelationshipId,
})
export type AdvisorCreateInput = z.input<typeof advisorCreateSchema>
export type AdvisorProfileUpdateInput = z.input<typeof advisorProfileUpdateSchema>
export type AdvisorMunicipalitiesBatchInput = z.input<typeof advisorMunicipalitiesBatchSchema>
export type AdvisorPasswordResetInput = z.input<typeof advisorPasswordResetSchema>