Fix /campanha/liderancas 500: RSC-safe phone display

CampaignCopyableCell is a Client Component; passing formatDisplay as a
function from the leaderships RSC page broke serialization in production.
Use a preformatted displayValue string at the call sites instead.
This commit is contained in:
Francisco Ignácio Fontoura Solla 2026-07-26 20:16:01 -03:00
parent 3d2cd4e163
commit 013f167d47
4 changed files with 14 additions and 8 deletions

View file

@ -9,6 +9,8 @@ Responsável: —
**Nota de revisão (2026-07-26):** entregue como especificado — `CampaignCopyableCell` nasceu compartilhado (`shared/`) já no 2º call site (`AdvisorsTable.tsx` migrado no mesmo PR), `whatsAppHrefForPhone` promovido a `lib/phone.ts`, `LeadershipRowViewModel.email` populado sem query nova, e `loadLeadershipDetail` deduplicado (reusa `row.email` de `toLeadershipRows` em vez de reparsear `doc.contact`). Ordem de colunas final: Nome → E-mail → Celular → Status → Municípios → Organizações → Acesso ao app → Ações (WhatsApp), exatamente como travado abaixo. Gate completo (tsc/lint/format/knip/cycles/test/build) verde; `pnpm exec knip` mantém o erro pré-existente ao carregar `payload.config.ts` (P3, não relacionado); `pnpm test:e2e` reproduziu apenas o flake pré-existente já documentado em `campaignMunicipalities.e2e.spec.ts` (checkbox de consentimento do `LeaderContactsPanel` antes da hidratação) — nenhuma falha nas rotas tocadas por este item.
**Hotfix produção (2026-07-26 noite):** `/campanha/liderancas` 500 em prod — RSC não pode passar `formatDisplay` (função) para o Client Component `CampaignCopyableCell`. API trocada para `displayValue?: string` (formatação no caller); Vercel runtime error: "Functions cannot be passed directly to Client Components". Assessores (`AdvisorsTable`, já `'use client'`) não quebrava, mas foi alinhado.
## Design (Impeccable)
Âncoras: `PRODUCT.md` (princípios 2 clareza sob pressão, 8 Feel the action; anti spreadsheet) / `DESIGN.md` (register `product`; Field Desk) · tema `data-theme='campaign'` · precedente de interação [`AdvisorsTable.tsx`](../../src/components/campaign/advisor/AdvisorsTable.tsx) (B19 ✓) · shells `CampaignTable` / `CampaignPageShell`.

View file

@ -71,7 +71,7 @@ const leadershipColumns = (
<CampaignCopyableCell
value={row.phone}
label="Celular"
formatDisplay={formatBrazilianPhoneInput}
displayValue={row.phone ? formatBrazilianPhoneInput(row.phone) : undefined}
className="tabular-nums"
/>
),

View file

@ -357,7 +357,9 @@ export const AdvisorsTable = ({
<CampaignCopyableCell
value={row.phone}
label="Celular"
formatDisplay={formatBrazilianPhoneInput}
displayValue={
row.phone ? formatBrazilianPhoneInput(row.phone) : undefined
}
className="tabular-nums"
/>
)}

View file

@ -22,8 +22,12 @@ type CampaignCopyableCellProps = {
value: string | null
/** Used in the toast ("`label` copiado.") and the button's `aria-label`. */
label: string
/** Transforms `value` for display only — the clipboard still gets the raw value (e.g. phone masking). */
formatDisplay?: (value: string) => string
/**
* Preformatted label (e.g. masked phone). Clipboard still gets raw `value`.
* String, not a formatter — Client Component; RSC cannot pass functions
* (broke `/campanha/liderancas` after B28).
*/
displayValue?: string
emptyDisplay?: string
className?: string
}
@ -37,7 +41,7 @@ type CampaignCopyableCellProps = {
export const CampaignCopyableCell = ({
value,
label,
formatDisplay,
displayValue,
emptyDisplay = '—',
className,
}: CampaignCopyableCellProps) => {
@ -49,8 +53,6 @@ export const CampaignCopyableCell = ({
)
}
const displayValue = formatDisplay ? formatDisplay(value) : value
return (
<button
type="button"
@ -58,7 +60,7 @@ export const CampaignCopyableCell = ({
aria-label={`Copiar ${label.toLowerCase()}`}
onClick={() => void copyText(label, value)}
>
<span className="truncate">{displayValue}</span>
<span className="truncate">{displayValue ?? value}</span>
</button>
)
}