'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." />
) : null} {rows.map((row) => { const whatsAppHref = whatsAppHrefForPhone(row.phone) const emailShown = displayEmail(row.email) return ( {editing ? ( ) : ( {row.name} )} {!emailHidden ? ( {editing ? ( ) : ( )} ) : null} {editing ? ( ) : ( )}
{whatsAppHref ? ( ) : ( )}
) })}
)}
) }