Expand the compiled next-steps doc back into the full delivery history, cross-reference plan statuses, and sync AGENTS.md, PRODUCT.md, and the Núcleos project notebook.
14 KiB
Compartilhar página do núcleo
Status: implementado (2026-07-18) Atualizado em: 2026-07-18 Item do roadmap: docs/roadmap.md (Trilha C → C1) Responsável: —
Como foi implementado (2026-07-18)
Entregue com ShareNucleusDialogShell + ShareNucleusDialog (src/components/campaign/), getNucleusShareRecipients (src/utilities/nucleusShareRecipients.ts) e a Server Action loadNucleusShareRecipients (src/app/(campaign)/campanha/(app)/nucleos/[slug]/shareRecipientsActions.ts). Botão no header do detalhe, template de mensagem e wa.me + copiar link conforme o plano. Desvios e refinamentos em relação ao texto original:
- Carregamento sob demanda, não no
Promise.allda página. Os destinatários são buscados por uma Server Action de leitura só quando o diálogo abre (shell lazy no padrão dos demais diálogos), evitando serializar telefones no HTML da página. - Leituras privilegiadas atrás do gate do núcleo. Em vez de depender do field access por linha (
overrideAccess: false, que causaria N+1 checagens de telefone), o loader confirma primeiro o acesso do ator ao núcleo (overrideAccess: false) e só então lê as três seções comoverrideAccess: true, retornando apenas{ id, name, phone }. - Seção "Com liderança" restrita a
supportStatus = 'engajado'e omitida para o papellideranca(o loader retorna lista vazia; o componente oculta seções vazias). canReadCampaignUserPhoneampliado: telefone de usuáriosgeralé legível por qualquer usuário de campanha autenticado (para contato), mantendo a regra de escopo por núcleo para os demais.- UI: Dialog central único (não Sheet/Drawer responsivo); destinatário sem telefone não aparece; estados de loading/erro/retry e "copiado" implementados.
Testes: tests/int/campaignNucleusShareRecipients.int.spec.ts, tests/int/campaignUserPhone.int.spec.ts, tests/unit/campaignNucleusShare.unit.spec.ts. O restante do documento é o plano original, mantido como registro.
Referência visual (UX Pilot)
Design: Compartilhar-Nucleo.png · Compartilhar-Nucleo.html
Como usar:
- Adotar a estrutura: bottom sheet no mobile com título "Compartilhar núcleo", subtítulo reforçando "envia apenas o link, não concede acesso" (copy que materializa a decisão travada), três seções de destinatários ("Com coordenação geral", "Com coordenador", "Com liderança" — nome + papel + botão verde "Abrir" com ícone WhatsApp) e a seção "Copiar link" com a URL canônica truncada + botão "Copiar".
- Ajuste em relação ao plano: o plano especifica
Dialogcentral (comoLeadershipInviteDialog); o design mostra bottom sheet. Adotar o padrão responsivo já usado no painel de lideranças (Sheet/Drawer no mobile, Dialog no desktop) — a estrutura interna do design vale para ambos. - Lembrar das regras que o design não mostra: ocultar a seção "Com liderança" para o papel
lideranca; ocultar destinatários sem telefone; ocultar seções vazias; estado "copiado" após o clique. - Ajustar cores: paleta antiga no HTML/PNG (header vermelho escuro, navy). Implementar com os tokens do tema
campaigne componentessrc/components/ui; avatares com foto viram iniciais.
Contexto
Hoje o detalhe do núcleo (/campanha/nucleos/[slug]) só tem ações de escrita (Editar, Nova liderança, Nova atualização, Arquivar). Não existe um caminho rápido para um usuário da campanha mandar o link daquele núcleo a outra pessoa do mesmo núcleo. A decisão de produto (2026-07-17) é adicionar um botão Compartilhar que oferece quatro destinos: coordenação geral, coordenador, liderança ou copiar link. Para os três primeiros, abre o WhatsApp via wa.me com o telefone do destinatário — o mesmo padrão já usado pelos convites, sem WhatsApp Business API.
Importante: compartilhar não concede acesso. O link aponta para /campanha/nucleos/[slug], rota autenticada que já filtra por escopo (canReadElectoralNucleus em src/utilities/campaignAccess.ts). O destinatário só vê o núcleo se tiver sessão de campanha válida e o núcleo no seu escopo. O compartilhamento é só conveniência — envia o link, não cria convite, não gera token, não toca em Consent.
Objetivos
- Botão de ação "Compartilhar" no header do detalhe do núcleo, ao lado das ações existentes.
- Quatro destinos: coordenação geral, coordenador, liderança, copiar link.
- Para os três primeiros, abrir
wa.mecom o telefone do destinatário e uma mensagem pronta (remetente + nome do núcleo + link). - Reusar o padrão
wa.medos convites (buildWhatsAppUrlemsrc/utilities/phone.tsebuildCampaignInviteWhatsAppLinkemsrc/utilities/campaignInvite.tscomo referência de texto). - Manter access control existente: nenhum dado sensível (telefone) é exposto a quem já não pudesse lê-lo. Sem server action de escrita, sem collection nova, sem
Consent.
Decisões travadas
- Escopo: só o detalhe do núcleo. O botão vive no header de
src/app/(campaign)/campanha/(app)/nucleos/[slug]/page.tsx. A listagem de núcleos (NucleusCard) fica fora deste ciclo (ver "Não escopo"). - Compartilhar não é convite. Não cria
campaignInvite, não gera token, não expira, não revoga. Reusa o padrãowa.meapenas como canal (mesma decisão do notebook: "Convidar pelo WhatsApp" cria convite; aqui não). - Sem WhatsApp Business API. Mesma justificativa do roadmap linha 57: o remetente envia pelo seu próprio WhatsApp, abrindo
wa.meem nova aba comnoopener,noreferrer(igual aoLeadershipInviteDialog). - Sem server action. A URL
wa.meé montada no cliente a partir do telefone do destinatário (já carregado no server) + mensagem com remetente/nome do núcleo/link. Nenhuma escrita, nenhuma transação, nenhumreq. - Access control por
overrideAccess: false. Os telefones dos destinatários vêm de queries comuser+overrideAccess: false, então o field access existente (canReadCampaignUserPhone,canReadContacts) filtra naturalmente quem pode ver qual telefone. O plano não inventa regra nova. - Link compartilhado = URL canônica do núcleo.
${NEXT_PUBLIC_SITE_URL}/campanha/nucleos/${slug}— reusagetCampaignInviteBaseURL()desrc/utilities/campaignInvite.tspara derivar a origem (mesmo guard de produção/localhost). - i18n e naming seguem o AGENTS.md: identificadores em inglês (
ShareNucleusDialog,getNucleusShareRecipients), strings visíveis em pt-BR. liderancacompartilha, mas sem "Com liderança".liderancavê "Com coordenação geral", "Com coordenador" e "Copiar link". A seção "Com liderança" fica oculta para esse papel: o escopo decanReadLeadershipparaliderancaéuser = self, então só retornaria a própria liderança, o que é inútil.liderancalendo telefone de coordenador/geral.canReadCampaignUserPhonejá aprova leitura quando o alvo é coordenador de um núcleo acessível ao usuário, eliderancatem núcleos acessíveis via liderança engajada. Mantém-se a política atual do app; se produto quiser bloquear, é mudarcanReadCampaignUserPhone, não este plano.geralé destinatário sim. Além de coordenador e liderança, a coordenação geral (campaignUser.role = 'geral') também é destinatária — própria seção "Com coordenação geral", listando todos os usuáriosgeral(name+telefone,overrideAccess: false). Qualquergeralé destinatário válido de qualquer núcleo porque vê todos.- Loader dedicado.
getNucleusShareRecipientsseleciona só nome+telefone degeral, coordenadores e lideranças do núcleo,overrideAccess: false, sem paginação (volume por núcleo é baixo). Evita acoplar ao estado de filtro/paginação da aba de lideranças e evita vazar telefone de quem o ator não pode ver. - Destinatário sem telefone é ocultado. Sem telefone não há
wa.me; o destinatário simplesmente não aparece na lista (em vez de aparecer desabilitado). - Núcleo arquivado também compartilha. O link funciona para quem tem acesso; o botão permanece visível mesmo com
status = 'arquivado'. - UI: Dialog. Dialog central (como
LeadershipInviteDialog), reusandosrc/components/ui/dialoge o padrão visual doLeadershipInviteDialog— dá espaço para listar destinatários em mobile. - Texto da mensagem (pt-BR). Template fixo:
Oi {destinatário}, aqui é {remetente} da campanha do Solla. Veja o núcleo {nome}: {link}.
Abordagem proposta
flowchart LR
Page["NucleusDetailPage (server)<br/>getNucleusShareRecipients<br/>overrideAccess:false"]
Header["Header de ações<br/>botão Compartilhar"]
Dialog["ShareNucleusDialog (client)<br/>4 destinos"]
Geral["Com coordenação geral<br/>lista {name,phone}<br/>buildWhatsAppUrl"]
Coord["Com coordenador<br/>lista {name,phone}<br/>buildWhatsAppUrl"]
Lider["Com liderança<br/>lista {name,phone}<br/>buildWhatsAppUrl"]
Copy["Copiar link<br/>navigator.clipboard"]
Wa["wa.me<br/>window.open noopener,noreferrer"]
Recipient["Destinatário<br/>abre /campanha/nucleos/slug<br/>auth + escopo existente"]
Page --> Header
Header --> Dialog
Dialog --> Geral
Dialog --> Coord
Dialog --> Lider
Dialog --> Copy
Geral --> Wa
Coord --> Wa
Lider --> Wa
Wa --> Recipient
Componentes:
getNucleusShareRecipients(emsrc/utilities/nucleusShareRecipients.ts): recebepayload,user,nucleusSlug. Carrega o núcleo por slug (overrideAccess: false,depth: 0,select: { coordinators: true }) para confirmar acesso e obter IDs dos coordenadores. Depois:- Coordenação geral:
payload.find({ collection: 'campaignUser', where: { role: { equals: 'geral' } }, select: { name: true, phone: true }, overrideAccess: false, user }). - Coordenadores:
payload.find({ collection: 'campaignUser', where: { and: [{ id: { in: coordinatorIds } }, { role: { equals: 'coordenador' } }] }, select: { name: true, phone: true }, overrideAccess: false, user })(filtracoordenadorpara não duplicargeraljá listado;geraleventualmente presente emcoordinatorsaparece só na seção de coordenação geral). - Lideranças:
payload.find({ collection: 'leadership', where: { nucleus: { equals: nucleusId } }, depth: 1, select: { contact: true }, overrideAccess: false, user })→ extrai{ id, name, phone }docontactpopulado. - Retorna
{ general: Recipient[], coordinators: Recipient[], leaderships: Recipient[], nucleusUrl }, ondeRecipient = { id, name, phone }. Telefones que o ator não pode ler chegam comonull/ausente e são filtrados pelo componente. Paralideranca, a query de lideranças já retorna só a própria (escopo do access), e o componente oculta a seção.
- Coordenação geral:
ShareNucleusDialog(client, emsrc/components/campaign/): botão "Compartilhar" (ShareIcon) que abreDialog. Dentro, quatro seções:- Com coordenação geral — lista de usuários
geralcom botão "Abrir WhatsApp" cada. - Com coordenador — lista de coordenadores do núcleo. Oculta quando vazia.
- Com liderança — lista de lideranças do núcleo. Oculta para
lideranca(e quando vazia). - Copiar link —
navigator.clipboard.writeText(nucleusUrl)com estado "copiado" (reusar o padrão decopieddoLeadershipInviteDialog). - Cada ação de WhatsApp monta
buildWhatsAppUrl(phone, message)commessage = "Oi {name}, aqui é {senderName} da campanha do Solla. Veja o núcleo {nucleusName}: {nucleusUrl}"e abre comwindow.open(url, '_blank', 'noopener,noreferrer'). - Reusa
<Dialog>/<DialogContent>/<Button>/<Alert>e íconeslucide-react(ShareIcon,MessageCircleIcon,CopyIcon,CheckIcon,ExternalLinkIcon), igual aoLeadershipInviteDialog.
- Com coordenação geral — lista de usuários
- Integração na página (
src/app/(campaign)/campanha/(app)/nucleos/[slug]/page.tsx): chamargetNucleusShareRecipientsnoPromise.alljá existente (junto com as demais*PageData), e renderizar<ShareNucleusDialog>no bloco de botões do header (ao lado de Editar/Nova liderança/Nova atualização). Passarrecipients,nucleusUrl,nucleusName,senderName={user.name},role={user.role}. - Sem migration, sem collection, sem server action. Todo o fluxo é leitura no server + montagem de URL no cliente.
Dependências
- Nenhuma de outro plano. Reusa
buildWhatsAppUrl(src/utilities/phone.ts),getCampaignInviteBaseURL(src/utilities/campaignInvite.ts), access control existente (src/utilities/campaignAccess.ts) e UIDialog/Alert(src/components/ui/).
Não escopo
- Compartilhar a partir da listagem de núcleos (
NucleusCardactions) — só o detalhe neste ciclo. - Compartilhar outras páginas da campanha (dashboard, lideranças) — o padrão é extensível, mas fica para depois.
- WhatsApp Business API (item separado, roadmap linha 57).
- Criar convite/acesso — compartilhar não concede nada; o destinatário já precisa ter acesso.
- Compartilhar por canal que não WhatsApp (e-mail, SMS, push) — só
wa.me+ copiar link.
Referências
docs/roadmap.md(linha 51)src/app/(campaign)/campanha/(app)/nucleos/[slug]/page.tsx— header onde o botão entrasrc/utilities/phone.ts—buildWhatsAppUrl(wa.me, normalização 11 dígitos)src/utilities/campaignInvite.ts—buildCampaignInviteWhatsAppLink(referência de texto),getCampaignInviteBaseURLsrc/components/campaign/LeadershipInviteDialog.tsx— padrão visual de Dialog +window.open+ copiar linksrc/utilities/campaignAccess.ts—canReadCampaignUserPhone,canReadContacts,canReadElectoralNucleus(escopo que protege o link)src/utilities/leadershipViewModels.ts,src/utilities/nucleusCoordinatorAssignmentPageData.ts— formato dos destinatários- AGENTS.md — Campaign auth, naming conventions, padrão
wa.medos convites
