'use client'
import {
MessageCircleIcon,
PencilIcon,
PlusIcon,
SaveIcon,
SearchXIcon,
UserCogIcon,
XIcon,
} from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useEffect, useMemo, useState, useTransition } from 'react'
import { toast } from 'sonner'
import { AdvisorDebouncedTextCell } from '@/components/campaign/advisor/AdvisorDebouncedTextCell'
import { AdvisorPasswordResetButton } from '@/components/campaign/advisor/AdvisorPasswordResetButton'
import {
CampaignCopyableCell,
campaignReadCellClassName,
} from '@/components/campaign/shared/CampaignCopyableCell'
import { CampaignListEmptyState } from '@/components/campaign/shared/CampaignListEmptyState'
import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost'
import { MunicipalityPortfolioCell } from '@/components/campaign/shared/MunicipalityPortfolioCell'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Spinner } from '@/components/ui/Spinner'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/Table'
import {
resolveVisibleColumns,
toCampaignColumnPickerColumns,
type CampaignColumnVisibility,
} from '@/lib/campaignColumnVisibility'
import { campaignInlineInputClassName } from '@/lib/campaignInlineInput'
import type { MunicipalityPortfolioIndexEntry } from '@/lib/municipalityPortfolio'
import {
formatBrazilianPhoneInput,
sanitizeBrazilianPhoneInput,
whatsAppHrefForPhone,
} from '@/lib/phone'
import { isPlanilhaPlaceholderEmail } from '@/lib/schemas/advisor'
import { cn } from '@/lib/utils'
import type { AdvisorRowViewModel } from '@/utilities/advisorData'
import type { CampaignFormActionState } from '@/utilities/campaignFormActionError'
type DraftAdvisor = {
name: string
email: string
phone: string
municipalityIds: number[]
}
// B197+ — single source for the `
`s and the picker menu: a column added
// here appears in both, and `resolveVisibleColumns` keeps the cookie filtering
// honest (a `mandatory` column is never dropped). Only what the body renders
// conditionally is hideable: the cells are hand-wired per column id (draft row
// AND data row), so `email` is the sole non-mandatory column — the others must
// stay mandatory or a hidden id would drop the header under a 5-cell body.
type AdvisorTableColumn = {
id: string
label: string
mandatory?: boolean
headerClassName?: string
/** Renders the label visually-hidden — the icon-only actions column. */
visuallyHiddenLabel?: boolean
}
const advisorTableColumns: readonly AdvisorTableColumn[] = [
{ id: 'name', label: 'Nome', mandatory: true, headerClassName: 'w-[20%]' },
{ id: 'email', label: 'E-mail', headerClassName: 'w-[24%]' },
{ id: 'phone', label: 'Celular', mandatory: true, headerClassName: 'w-[14%]' },
{ id: 'municipalities', label: 'Municípios', mandatory: true },
{
id: 'actions',
label: 'Ações',
mandatory: true,
headerClassName: 'w-28 text-right',
visuallyHiddenLabel: true,
},
]
export const advisorPickerColumns = toCampaignColumnPickerColumns(advisorTableColumns)
type AdvisorsTableProps = {
rows: AdvisorRowViewModel[]
municipalityIndex: MunicipalityPortfolioIndexEntry[]
hasQuery: boolean
/** B197 — hidden column ids from the `campaign_columns` cookie (server truth). */
columnVisibility: CampaignColumnVisibility
updateProfileAction: (
state: CampaignFormActionState,
formData: FormData,
) => Promise
municipalitiesAction: (
state: CampaignFormActionState,
formData: FormData,
) => Promise
createAction: (
state: CampaignFormActionState,
formData: FormData,
) => Promise
passwordResetAction: (
state: CampaignFormActionState,
formData: FormData,
) => Promise
autoCreateDraft?: boolean
}
const emptyDraft = (): DraftAdvisor => ({
name: '',
email: '',
phone: '',
municipalityIds: [],
})
const displayEmail = (email: string | null): string | null => {
if (!email || isPlanilhaPlaceholderEmail(email)) return null
return email
}
export const AdvisorsTable = ({
rows,
municipalityIndex,
hasQuery,
columnVisibility,
updateProfileAction,
municipalitiesAction,
createAction,
passwordResetAction,
autoCreateDraft = false,
}: AdvisorsTableProps) => {
const router = useRouter()
const [editing, setEditing] = useState(false)
const [draft, setDraft] = useState(null)
const [isCreating, startCreate] = useTransition()
// B197 — email starts hidden on this list (cookie default); the picker
// writes the cookie and the server re-renders with a fresh value. B197+:
// the visible set is what the `
`s render, so this exception below can
// never disagree with the header. The create-draft row is the ONE exception:
// email is required to create an advisor, so its input renders always.
const visibleColumns = resolveVisibleColumns(
advisorTableColumns,
columnVisibility.hiddenColumnIds,
)
const emailHidden = !visibleColumns.some((column) => column.id === 'email')
const refresh = () => router.refresh()
// One array per row per render of `rows`, not per render of this table: the
// cell reconciles its optimistic state against this prop by identity.
const municipalityIdsByAdvisor = useMemo(
() => new Map(rows.map((row) => [row.id, row.municipalityIDs])),
[rows],
)
const startDraft = () => {
setEditing(true)
setDraft(emptyDraft())
}
useEffect(() => {
if (autoCreateDraft) setDraft(emptyDraft())
}, [autoCreateDraft])
const cancelDraft = () => setDraft(null)
const leaveEditing = () => {
setDraft(null)
setEditing(false)
}
const saveDraft = () => {
if (!draft) return
if (draft.name.trim().length < 2) {
toast.error('Informe o nome do assessor.')
return
}
if (!draft.email.trim()) {
toast.error('Informe o e-mail do assessor.')
return
}
const formData = new FormData()
formData.set('name', draft.name.trim())
formData.set('email', draft.email.trim())
if (draft.phone.trim()) formData.set('phone', draft.phone.trim())
for (const municipalityId of draft.municipalityIds) {
formData.append('municipalityIds', String(municipalityId))
}
startCreate(async () => {
const result = await createAction({}, formData)
if (result.status === 'success') {
toast.success(result.message ?? 'Assessor criado.')
setDraft(null)
refresh()
return
}
toast.error(result.message ?? 'Não foi possível criar o assessor.')
})
}
const showEmpty = rows.length === 0 && !draft
return (
// One shared Drawer for the portfolio chip cells on coarse pointers
// (miss #52 — never a Drawer root per opened cell).
{editing ? (
) : (
)}
{showEmpty ? (
{!hasQuery ? (
) : null}
) : (
{visibleColumns.map((column) => (
{column.visuallyHiddenLabel ? (
{column.label}
) : (
column.label
)}
))}
{draft ? (
{
const name = event.currentTarget.value
setDraft((current) => (current ? { ...current, name } : current))
}}
placeholder="Nome"
aria-label="Nome do novo assessor"
className={campaignInlineInputClassName}
autoFocus
/>
{
const email = event.currentTarget.value
setDraft((current) => (current ? { ...current, email } : current))
}}
placeholder="E-mail"
aria-label="E-mail do novo assessor"
className={campaignInlineInputClassName}
/>
{
const phone = formatBrazilianPhoneInput(
sanitizeBrazilianPhoneInput(event.currentTarget.value),
)
setDraft((current) => (current ? { ...current, phone } : current))
}}
placeholder="Celular"
aria-label="Celular do novo assessor"
className={campaignInlineInputClassName}
/>
setDraft((current) =>
current ? { ...current, municipalityIds } : current,
)
}
commitAction={municipalitiesAction}
drawerTitle="Carteira do assessor"
updateErrorMessage="Não foi possível atualizar a carteira."
/>