Módulo 2 - Comunicação Omnichannel & Chat (communication)
Módulo 2: Comunicação Omnichannel & Chat (communication)
Section titled “Módulo 2: Comunicação Omnichannel & Chat (communication)”O seletor de rota só apresenta destino confirmado após success=true e opções em formato de lista. Respostas recusadas ou incompletas mostram falha e retry, evitando confirmação indevida de Management.
Falha e carregamento do destino WhatsApp permanecem visíveis em telas pequenas e são anunciados por alert/status. O botão de nova tentativa mantém seu tamanho quando a explicação quebra em linhas.
Datas do encaminhamento usam Europe/London tanto no relógio quanto em Today/Tomorrow/Yesterday. Fora desses dias relativos, incluem o ano para distinguir histórico e agenda futura. A comparação usa dias civis, incluindo transições de horário de verão, independentemente do fuso do dispositivo.
ChatPermissionBanner.test.tsx monta React em jsdom e valida cliques simultâneos, estado ocupado, retry após rejeição, resposta tardia após remontagem e separação web/nativa. A ponte do sistema é simulada; abertura real continua pertencendo à homologação em aparelho.
O banner serializa ações de permissão/configurações, mostra processamento durante toda a operação e ignora conclusões após desmontagem. Rejeições inesperadas oferecem erro recuperável.
Feedback de permissão nativa no hook compartilhado usa instruções de configurações do dispositivo, válidas para Android e iOS, e não depende do rótulo Subscribe de uma tela específica.
Quando a permissão está bloqueada, a ação do banner abre as configurações de notificações via adaptador Android/iOS. Na web, o botão informa que verifica a permissão novamente. Falha ao abrir configurações mostra orientação manual.
O aviso de permissão bloqueada distingue o runtime: Android/iOS orientam às configurações do aplicativo no celular; navegador/PWA mantêm a orientação de permissões do site.
A filtragem de templates é importada sob demanda após a resposta válida do catálogo. A limpeza síncrona do cache da sessão não depende da execução desse módulo.
O editor de templates usa a chave composta usuário/canal do composer. Trocar qualquer uma dessas identidades remonta o editor, removendo imediatamente rascunho, seleção e estado de envio anteriores.
O catálogo exige sucesso explícito da API antes de consumir templates. Respostas recusadas não são armazenadas nem apresentadas como lista vazia confirmada; o composer oferece nova tentativa.
A limpeza de conteúdo privado da sessão invalida sincronamente o catálogo de templates antes de aguardar IndexedDB. Logout e revogação descartam sua memória; respostas em voo anteriores não a restauram.
A limpeza do catálogo de templates invalida sua geração: consultas anteriores podem concluir para seus chamadores, mas não repovoam o cache nem removem a consulta compartilhada mais recente.
Falha de carregamento do catálogo oferece “Retry loading templates” na conversa. A tentativa reutiliza a deduplicação do catálogo compartilhado e invalida a conclusão da leitura anterior, sem exigir fechar ou recarregar o aplicativo.
Durante o envio de template, variáveis e retorno à lista ficam desabilitados até a resposta. Falhas usam alertas acessíveis, carregamento usa status e o envio informa aria-busy, preservando feedback em VoiceOver/TalkBack e desktop.
Tarefas elegíveis com data inválida aparecem em “Schedule unavailable” no encaminhamento, preservando o destino sem inventar um horário. A classificação interpreta cada data uma vez e conserva os pedidos originais.
A seleção de tarefa para encaminhamento reclassifica próximas/passadas pelo relógio compartilhado de foreground. A lista acompanha a passagem do horário e o retorno do aplicativo sem exigir alteração dos pedidos ou recarregamento manual.
Encaminhar para tarefa e mover para Management possuem locks síncronos e cancelamento HTTP por usuário/conversa. A navegação ao destino revalida o contexto após atualizar os canais; trocar de conversa durante a operação impede navegação e feedback tardios. A mensagem movida exibe processamento e aria-busy. Abortar o request não desfaz uma transferência já aceita pelo servidor.
O seletor de rota distingue uma escolha vigente do modo automático: sem sessão válida, mostra Automatic · conversation context, pois o contexto humano recente pode levar ao JOB. Uma pergunta ainda sem resposta aparece como Awaiting client choice · private; Management continua selecionável para fixar explicitamente o destino privado em ambos os casos. O selo Active só aparece para uma tarefa com escolha vigente. Na retomada web/nativa, o banner oculta opções antigas até confirmar a rota no servidor; uma gravação em curso faz essa leitura ao concluir. Trocar de conversa, inclusive do mesmo cliente, invalida operações e feedback anteriores.
O módulo Communication centraliza a mensageria da empresa, unificando conversas internas com o WhatsApp oficial via Meta Cloud API. A coexistência também importa respostas feitas no aplicativo WhatsApp Business para que telefone, web, PWA, Android, iOS e desktop compartilhem o mesmo histórico.
O breakpoint md é uma decisão de renderização, não apenas de CSS. ChatInbox e ClientChatView montam exclusivamente a árvore desktop ou mobile usando o mesmo useMediaQuery fornecido ao controlador. Isso inclui a conversa, o perfil de cliente/equipe e o painel lateral da tarefa: a árvore escondida não mantém uma segunda instância, polling/WebSocket, marcação de leitura, composer, seletor de rota WhatsApp nem efeitos do dashboard ativos. Ao cruzar 768 px, o React desmonta o lifecycle anterior e monta a superfície apropriada ao novo runtime/tamanho.
O menu desktop de troca de conversa limita sua lista com dvh, acompanhando redimensionamento, split-screen e barras do navegador. A ação selecionada continua visível e rolável sem depender de uma altura vh capturada antes da mudança do viewport.
O banner de destino WhatsApp trata leitura e mudança como operações pertencentes ao cliente/conversa atual. Cada transporte recebe AbortSignal; trocar owner ou desmontar cancela fetch e retries. A mutação possui lock antes do primeiro await, promove 4xx funcional a falha e só atualiza, recarrega ou emite toast se seu epoch ainda estiver ativo.
Falhas inesperadas dos endpoints de canais usam um contrato único em channelError.ts. Listagem/criação de canais, mensagens, participantes, reações, leitura, digitação, agendamento, tradução, encaminhamento e movimentação nunca devolvem error.message do D1, Durable Objects, Gemini ou Evolution ao cliente. O aplicativo recebe uma mensagem estável com possibilidade de nova tentativa; o diagnóstico completo passa pelo sanitizador central e é gravado em system_logs com ação e tipo de entidade para investigação administrativa. Erros funcionais 400/401/403/404/423 permanecem específicos para orientar a interface igualmente no desktop, PWA, Android e iOS.
Preferências locais do módulo não expõem IDs de canais ou clientes em texto claro. Conversas fixadas usam marcadores opacos com namespace próprio e migram automaticamente chat_pinned_channels_v1 para v2; o dismiss do convite WhatsApp converte a chave antiga baseada em client.id e remove o legado. Esses marcadores servem somente à interface por dispositivo — mensagens e autorização WhatsApp continuam tendo D1 como fonte de verdade.
A velocidade das notas de voz é compartilhada entre o player da conversa e a prévia do compositor, ciclando 1x → 1.5x → 2x. A preferência usa uiPreferenceStorage: persiste normalmente por dispositivo e mantém fallback em memória quando Safari privado, sandbox ou WebView bloqueia localStorage, de modo que players montados depois na mesma sessão continuam na velocidade escolhida.
O alerta WhatsApp disparado quando um cliente escreve pelo app é um side effect posterior ao envio autoritativo do chat. Ele usa transporte autenticado com teto de 15 segundos, AbortSignal por sessão e contenção explícita da rejeição; não atrasa nem reclassifica a mensagem principal. Troca de identidade antes ou durante o relay invalida o epoch e aborta controllers ativos, impedindo trabalho tardio pertencente à conta anterior.
Anexos de imagem abrem no lightbox compartilhado com pinch, duplo toque e pan. A superfície usa o mesmo contrato dos bottom sheets: diálogo modal nomeado, scroll bloqueado, foco contido/restaurado, Escape apenas no topo da pilha e integração com back nativo. A ação de abrir/entregar o arquivo externamente bloqueia repetição e anuncia progresso.
Permissões de push também usam um contrato único. notificationPermissionProvider consulta, solicita e observa autorização; a Permissions API fica restrita ao navegador/PWA com listener removível, enquanto Android/iOS revalidam no resume e no registro APNs/FCM. O perfil recebe o mesmo estado sem manter uma implementação paralela por plataforma. notificationDeliveryProvider separa autorização do sistema e prontidão para entrega. No gate operacional, a concessão só conclui a etapa depois que a assinatura Web Push foi persistida com sucesso ou o token nativo persistido confirma um destino utilizável; falha ou timeout usa registration_failed, mostra que a permissão já está ativa e oferece nova tentativa sem classificar o problema como recusa do usuário. O perfil nativo comunica o mesmo timeout depois de devolver o controle ao botão. Ao restaurar a sessão, a checagem é somente leitura e o evento de registro nativo atualiza a etapa sem exigir reinício.
A lista My devices possui prontidão independente do seu snapshot. Antes da primeira resposta mostra verificação em andamento; somente uma resposta válida pode produzir No devices subscribed yet. Falha inicial ou de atualização mantém alerta persistente e retry, preserva qualquer lista anterior e marca o diagnóstico de registro backend como não verificável. Owner, lifecycle e passe final do single-flight continuam impedindo que uma resposta de outra sessão altere a tela em desktop, PWA, Android ou iOS.
As categorias de What sends a push usam o banco como fonte de verdade; a cópia local serve apenas para a primeira pintura. Login, retorno ao foreground e retry confirmam o snapshot com owner e sequência de request. Enquanto a leitura está pendente ou falhou, os toggles server-side permanecem bloqueados e a tela mostra status/alerta, sem confundir cache com configuração aplicada em outros dispositivos. Uma alteração otimista possui lock no mesmo frame; se o endpoint recusar, apenas disabledPushCategories volta ao último valor confirmado, o cache local acompanha o rollback e as preferências exclusivamente locais, como som e duração, são preservadas.
As APIs compartilhadas de preferências, inscrições/dispositivos, histórico/ações de notificação e banner global usam dez saídas atomicServerError e um registro específico de configuração VAPID. Falhas inesperadas de D1 ou criptografia ficam sanitizadas no painel; o cliente recebe retry estável. Estados funcionais continuam próprios: sessão/autorização, dispositivo já vinculado, VAPID ausente, nenhum dispositivo, rejeição do push e orientação de re-inscrição. A tela de diagnóstico nunca interpola a exceção que tornou o par VAPID inválido.
Na configuração do bot, os três modos de resposta são um grupo nomeado e cada cartão publica seu estado pressionado. Assim a seleção “Options menu / Keywords only / Every message” permanece compreensível por teclado, VoiceOver e TalkBack, sem criar uma interface alternativa ao toque.
Notas de voz e o checklist compacto usam o indicador quantitativo compartilhado: playback anuncia percentagem reproduzida; checklist anuncia itens concluídos sobre o total. A animação visual continua leve e decorativa, enquanto leitores de tela recebem valores estáveis.
Gravações de voz também obedecem ao lifecycle compartilhado. NativeBridge publica primecrown:app-pause e primecrown:app-resume; inclusive uma leitura inicial que encontre o shell já em segundo plano publica a pausa, sem fabricar uma retomada quando ele nasce ativo. O compositor encerra uma captura ativa ao receber a pausa nativa ou visibilitychange oculto, liberando o microfone e preservando o trecho como prévia não enviada. Os dois sinais podem ocorrer juntos sem chamar stop() duas vezes, e seus listeners são removidos no teardown. Assim, Android/iOS não deixam timer ou microfone ativos no background, enquanto PWA/desktop aplicam a mesma proteção ao ocultar a página.
Configurações do Chat usam CFInput nos editores de instruções do bot e mensagem de ausência e Button nas ações de salvar. Um lock síncrono precede a mutação e toda a superfície editável fica inert/aria-busy durante a tentativa. Instruções e mensagem são aparadas, e horário comercial ativo exige resposta não vazia. saveSettings precisa aceitar o snapshot antes de os stores compartilhados e o háptico de sucesso serem atualizados; falha mantém a edição local visível para retry sem fingir que o próximo boot preservará a mudança.
Uploads de foto, documento e voz no chat pertencem à combinação usuário+canal. O cliente multipart aceita AbortSignal; troca de conversa, conta ou desmontagem aborta o request e invalida envio, agendamento, toast e estado de loading tardios. Até uma callback de câmera mantida pela renderização anterior precisa provar que seu proprietário coincide com o epoch atual antes de iniciar o upload. A listagem de mensagens agendadas usa a mesma propriedade, impedindo dados do canal anterior após navegação rápida.
O compositor também pertence a usuário+canal. Texto digitado é mantido como rascunho separado por conversa, como em mensageiros nativos, sem reaparecer no destinatário seguinte. Trocar de canal limpa reply, menus, sugestões e prévia de voz; invalida câmera e rascunho de IA; e encerra somente a instância de gravador da tentativa anterior. Um rec.start() atrasado não fecha uma nova gravação nem publica áudio no canal seguinte, e dois starts simultâneos são bloqueados antes da resposta da permissão.
O sheet de sugestões inteligentes possui lock síncrono e token por geração. Toque duplo não cria requests concorrentes, e fechamento/troca de canal invalida a resposta antes de escrever sugestões ou loading na conversa seguinte. Quando a IA falha, retorna vazia ou ainda não há histórico, os chips prontos continuam disponíveis, mas o cabeçalho e uma região de status os identificam como Ready-made replies, sem atribuí-los falsamente à IA. A preferência pt-BR continua enviada ao endpoint para o conteúdo gerado; o fallback fixo segue o contrato de cópia inglesa da interface.
O envio textual possui lock síncrono no próprio compositor. Dois toques/cliques antes de o draft limpo voltar pelo React produzem uma única mensagem, mesmo que a API gere a chave de idempotência mais abaixo. Upload e abertura da câmera publicam aria-busy e desabilitam campo e envio; o botão tem nome explícito para VoiceOver/TalkBack. A textarea especializada permanece intencional: capitalização, correção e teclado textual são nativos, Return cria parágrafo no touch e Enter envia no desktop sem interceptar composição IME.
Mensagens aceitas sem conectividade mudam de Sending para Queued offline depois que o IndexedDB confirma a gravação. A fila guarda também o instante original e recompõe os balões ao reabrir PWA, Android, iOS ou desktop ainda offline. Gatilhos simultâneos do navegador e do plugin Network entram numa única drenagem serial; troca de conta aguarda a drenagem anterior e revalida o epoch. Falha online vira Failed com botão Retry acessível, que reaproveita o externalId original e só remove o item local após confirmação do servidor.
O painel Inbox operations trata fila e alertas WhatsApp com locks síncronos por item. Retry, dismiss, send now, cancel e decisão de reabertura não aceitam dois toques antes do render; ações em itens diferentes preservam seus próprios spinners, e o sheet não fecha no meio de uma mutação. O conteúdo publica aria-busy e emite feedback háptico somente após aceite. No ACK continua deliberadamente não reenviável — o destinatário pode ainda receber a tentativa original — e o estado vazio diz apenas que não há alertas pendentes, sem afirmar entrega que não foi comprovada.
No Worker, send_now e cancel aceitam exclusivamente registros PENDING. Um item PROCESSING já foi reivindicado atomicamente pelo runner e aparece como In flight, sem ações: recolocá-lo ou apagá-lo nesse intervalo poderia duplicar o envio ou remover um balão que chegou ao WhatsApp. A condição é repetida no SELECT e na mutação; se o runner vencer a corrida, a API responde 409 e o painel recarrega. Falha de D1 ao listar fila/alertas retorna 500 estável e mantém o último estado visível, nunca um falso success: true com lista vazia.
Retry e resolução de alertas também reconciliam a conversa aberta pelo Durable Object. Reenfileirar transmite pending; descartar uma falha cuja mensagem foi mantida limpa o estado da fila; reconhecer No ACK persiste e transmite deliveryAlertDismissedAt. Esse marcador integra o tipo compartilhado, o mapper HTTP, o merge/payload WebSocket e isUnconfirmedWhatsAppSend, portanto o alerta desaparece imediatamente e continua ausente após reload, sem exigir fechar a conversa nem sugerir reenvio.
Falha total ou parcial ao carregar Inbox operations não é apresentada como lista vazia. O último snapshot permanece visível com alerta persistente, o ícone fechado recebe marcador e nome “data may be out of date”, e um retry explícito anuncia loading sem competir com o polling serializado. Estados Nothing waiting / Queue is empty / No delivery failures só aparecem quando a leitura atual foi confiável; o polling continua silencioso em toast para não interromper o operador repetidamente.
Cada montagem de Inbox operations também delimita um ciclo de vida. Logout, troca de identidade ou desmontagem invalidam a leitura ativa e todos os resultados de ações ainda pendentes antes que possam alterar listas, busy state, toast, háptico ou callbacks do inbox anterior. O lock da leitura usa um token por request, não apenas um booleano: a remontagem de desenvolvimento do React Strict Mode pode iniciar uma leitura nova sem a resposta antiga limpar ou sobrescrever seu loading.
O pedido de reabertura do funcionário segue o mesmo ownership. Um lock por ref fecha a janela de toque duplo antes do render; Waiting for a manager, háptico e sucesso só aparecem depois de success=true. Recusa e exceção preservam a ação para retry. ownerKey=channelId remonta a notice na troca de conversa, invalidando a conclusão antes de estado, toast ou callback e impedindo que askedNow atravesse canais; somente um aceite solicita refresh da lista.
O save das configurações do chat comunica seu estado ao shell. Enquanto a persistência atômica está pendente, edição, Back interno, tabs do sidebar, troca de seção, backdrop, Escape e gestos de histórico permanecem bloqueados pelo mesmo isChatSettingsSaving. Uma desmontagem forçada, como logout, incrementa o lifecycle epoch e impede que o snapshot, toast ou háptico da identidade anterior seja publicado; a tela só destrava no finally pertencente à execução atual.
O diagnóstico Delivery Health não infere saúde da ausência de dados. Antes da primeira resposta, Risk/Evolution/Queue/Failures mostram Checking; após falha sem snapshot, mostram Unknown, nunca LOW, 0 pending ou 0 failed. Se já existe leitura confirmada, ela permanece visível com alerta de possível desatualização. Entrada automática e botão Refresh reutilizam o mesmo token/lock; saída da seção ou sessão invalida a resposta, e o botão canônico anuncia loading e bloqueia toque concorrente nas quatro plataformas.
Busca por mensagem citada, cancelamento agendado e exclusão usam o mesmo epoch. Os dois requestAnimationFrame necessários para aguardar o prepend do histórico são registrados e cancelados no teardown; cada frame revalida o canal antes de rolar ou destacar. Rollback da agenda e toasts de exclusão também são ignorados depois da troca, evitando foco e feedback pertencentes à conversa anterior.
O dashboard contextual de cliente/funcionário também pertence à identidade selecionada. A identidade tipo:id e os valores atuais de nome, e-mail e telefone invalidam save/timer anteriores e repõem o formulário, portanto uma sincronização externa da mesma pessoa não deixa o editor com dados antigos. O save usa lock síncrono, captura cliente/funcionário antes do await, aguarda a persistência com rollback da store e só então apresenta háptico/estado Saved; teardown limpa o indicador temporário. A lista de adiantamentos ordena uma cópia, de modo que abrir a aba Payroll nunca muda silenciosamente a coleção compartilhada da store.
💬 1. Arquitetura do Chat e Tipos de Canais
Section titled “💬 1. Arquitetura do Chat e Tipos de Canais”graph TD
subgraph UI ["Interface do Chat (src/components/chat/)"]
Inbox["ChatInbox.tsx (Painel Geral)"]
Sidebar["ChatSidebar.tsx (Lista de Clientes & Grupos)"]
MessageList["ServiceMessageList.tsx (Visualizador)"]
end
subgraph Channels ["Tipos de Canais no D1"]
Direct["Channel DIRECT: Suporte Direto Cliente ↔ Gerente"]
Job["Channel JOB: Grupo Operacional do Serviço (Cliente + Equipe)"]
end
subgraph Infrastructure ["Infraestrutura de Mensageria"]
DO["Durable Objects (ChatChannelDO) - Realtime WebSockets"]
EvoAPI["Evolution API - Integração WhatsApp Webhook"]
end
Inbox --> Direct
Inbox --> Job
Direct --> DO
Job --> DO
DO <--> EvoAPI
⏱️ 2. Controle de SLA de Resposta (ResponseSlaBadge)
Section titled “⏱️ 2. Controle de SLA de Resposta (ResponseSlaBadge)”O sistema monitora o tempo decorrido desde a última mensagem enviada pelo cliente sem resposta do suporte:
- 🟢 SLA Ok (< 15 min): Atendimento dentro do tempo ideal.
- 🟡 Atendimento Prioritário (15 - 30 min): Sinalização amarela no painel.
- 🔴 SLA Crítico (> 30 min): Destaque vermelho e notificação de alta prioridade na inbox do gerente.
🛡️ 3. Política Anti-Ban do WhatsApp (antiBanPolicy.ts)
Section titled “🛡️ 3. Política Anti-Ban do WhatsApp (antiBanPolicy.ts)”Para reduzir o risco de bloqueio e manter um comportamento coerente com uma equipa real, todos os envios passam pela mesma política:
- Jitter de envio: intervalo aleatório de 5 a 15 segundos, com máximo de 4 mensagens por minuto.
- Limites de volume: 150 mensagens por dia, 12 por destinatário/hora, 30 por destinatário/dia e 20 mensagens de marketing/dia.
- Janela operacional: mensagens não transacionais ficam em fila fora de 08:00–20:00.
- Warm-up: números novos ou reconectados usam uma rampa de volume durante sete dias antes de voltar ao limite normal.
- Fila e estabilidade: falhas usam backoff progressivo; o envio pausa quando a Evolution desconecta ou a fila fica instável.
- Comportamento visível: o indicador de digitação varia conforme o tamanho da mensagem; presença e confirmação de leitura só acompanham atividade real.
- Variação segura: apenas a saudação de mensagens de marketing pode variar. Mensagens transacionais e de suporte permanecem exatas, sem caracteres invisíveis.
- Baseline da Evolution:
alwaysOnline, leitura automática de mensagens e leitura automática de status permanecem desligados; mensagens de grupos são ignoradas.
🛠️ 4. Arquivos Principais do Módulo
Section titled “🛠️ 4. Arquivos Principais do Módulo”- Inbox Principal:
src/pages/ChatInbox.tsx - Hook de Mensagens:
src/features/communication/hooks/useMessages.ts— WebSocket via Durable Object; poll HTTP só como fallback - Hook de Canais:
src/features/communication/hooks/useChannels.ts— lista da inbox: load inicial + refresh ao focar a aba + reconcile lento (60s) só com tab visível (sem poll a 8s) - Tradução automática: só bolhas visíveis (IntersectionObserver + margem); probes lookup vão em batch (
messageIds) — no máximo uma tentativa Gemini por mensagem/idioma - Webhook de WhatsApp:
functions/api/whatsapp/evolution-webhook.ts - Durable Object (WebSockets):
chat-do/src/index.js— relay de mensagens + cron*/5para dispatch/auto-lock/fila WA. Um segundo trigger0 9 * * 1executa a rotina semanal do Google Business Profile; o handler comparaevent.cronexatamente para que o tick de manutenção das 09:00/09:05 não duplique posts ou sincronizações de avaliações.