Merge pull request 'FEAT: Agora é possível enviar media utilizando a meta API' (#1) from feat/send-media into dev
All checks were successful
Deploy Dev — Frontend / Deploy para VM Dev (push) Successful in 8s
All checks were successful
Deploy Dev — Frontend / Deploy para VM Dev (push) Successful in 8s
Reviewed-on: #1
This commit is contained in:
commit
5cdfc47193
@ -1,13 +0,0 @@
|
|||||||
# Documentação do Frontend
|
|
||||||
|
|
||||||
Esta pasta reúne a documentação funcional e conceitual do frontend MVP Omnichannel.
|
|
||||||
|
|
||||||
## Índice
|
|
||||||
|
|
||||||
- [Visão Geral](./visao-geral.md)
|
|
||||||
- [Módulo Auth / Login](./modulo-auth.md)
|
|
||||||
- [Módulo Home / Dashboard](./modulo-home.md)
|
|
||||||
- [Módulo Chat](./modulo-chat.md)
|
|
||||||
- [Módulo Call](./modulo-call.md)
|
|
||||||
- [Módulo Attendance / Novo Atendimento](./modulo-attendance.md)
|
|
||||||
- [Casos de Uso em Formato RPG](./casos-de-uso-rpg.md)
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
# Casos de Uso em Formato RPG
|
|
||||||
|
|
||||||
## Introdução
|
|
||||||
|
|
||||||
Imagine o reino de **Sharvus**, onde toda vila, fortaleza e guilda depende de mensagens rápidas para manter ordem, comércio e confiança com seus cidadãos.
|
|
||||||
|
|
||||||
No centro desse reino existe a fortaleza da **Ordem de Sothis**, onde trabalham os guerreiros do suporte, os mensageiros do comercial e os guardiões do financeiro.
|
|
||||||
|
|
||||||
Cada atendimento é uma missão.
|
|
||||||
|
|
||||||
Cada cliente é um personagem importante.
|
|
||||||
|
|
||||||
Cada tela do sistema é uma parte da jornada.
|
|
||||||
|
|
||||||
## O Herói
|
|
||||||
|
|
||||||
Nosso herói é **Aren**, um guerreiro de suporte da Ordem de Sothis.
|
|
||||||
|
|
||||||
Sua missão não é derrotar monstros, mas resolver problemas antes que eles virem caos no reino.
|
|
||||||
|
|
||||||
Para isso, ele usa o grande portal chamado **Omnichannel**.
|
|
||||||
|
|
||||||
## Capítulo 1: O Portal de Entrada
|
|
||||||
|
|
||||||
Aren chega ao salão principal e encontra o **Portal de Login**.
|
|
||||||
|
|
||||||
Ali ele:
|
|
||||||
|
|
||||||
- informa suas credenciais
|
|
||||||
- entra no sistema
|
|
||||||
- acessa o centro de comando
|
|
||||||
|
|
||||||
Na prática, este é o caso de uso de autenticação visual do módulo `auth`.
|
|
||||||
|
|
||||||
## Capítulo 2: O Mapa da Operação
|
|
||||||
|
|
||||||
Ao entrar, Aren vê o grande mapa do reino: a **Home / Dashboard**.
|
|
||||||
|
|
||||||
Nesse mapa ele consegue:
|
|
||||||
|
|
||||||
- ver conversas ativas
|
|
||||||
- trocar entre mensagens e ligações
|
|
||||||
- buscar contatos
|
|
||||||
- iniciar novas missões
|
|
||||||
|
|
||||||
Na prática, este é o caso de uso central do módulo `home`.
|
|
||||||
|
|
||||||
## Capítulo 3: A Mensagem do Cidadão
|
|
||||||
|
|
||||||
Uma cidadã chamada **Maria Souza** envia um pedido urgente por WhatsApp.
|
|
||||||
|
|
||||||
Aren abre a conversa no módulo `chat` e pode:
|
|
||||||
|
|
||||||
- ler o histórico
|
|
||||||
- responder rapidamente
|
|
||||||
- acompanhar novas mensagens
|
|
||||||
- transferir o caso para outra guilda, como Financeiro ou Comercial
|
|
||||||
|
|
||||||
Na prática, este módulo representa o caso de uso de atendimento textual em tempo real.
|
|
||||||
|
|
||||||
## Capítulo 4: O Chamado por Voz
|
|
||||||
|
|
||||||
Nem toda missão pode ser resolvida por pergaminhos e mensagens.
|
|
||||||
|
|
||||||
Às vezes, o cidadão precisa ouvir a voz de alguém da Ordem.
|
|
||||||
|
|
||||||
Então Aren inicia uma ligação no módulo `call`, onde ele:
|
|
||||||
|
|
||||||
- visualiza quem está na chamada
|
|
||||||
- acompanha o tempo da conversa
|
|
||||||
- usa controles de chamada
|
|
||||||
- encerra o contato quando a missão termina
|
|
||||||
|
|
||||||
Na prática, este módulo representa o caso de uso de atendimento por voz.
|
|
||||||
|
|
||||||
## Capítulo 5: A Missão Começa Aqui
|
|
||||||
|
|
||||||
Antes de qualquer conversa, Aren pode abrir o módulo `attendance` para iniciar uma nova missão.
|
|
||||||
|
|
||||||
Ele escolhe:
|
|
||||||
|
|
||||||
- quem será atendido
|
|
||||||
- qual canal usar
|
|
||||||
- para qual área o caso deve ir
|
|
||||||
|
|
||||||
Depois disso:
|
|
||||||
|
|
||||||
- se for mensagem, ele segue para o chat
|
|
||||||
- se for voz, ele segue para a chamada
|
|
||||||
|
|
||||||
Na prática, este módulo representa o caso de uso de abertura rápida de atendimento.
|
|
||||||
|
|
||||||
## Moral da História
|
|
||||||
|
|
||||||
O Omnichannel é a mesa de guerra de Sharvus.
|
|
||||||
|
|
||||||
Ele permite que um único guerreiro:
|
|
||||||
|
|
||||||
- veja o cenário
|
|
||||||
- escolha o canal
|
|
||||||
- converse com o cidadão
|
|
||||||
- transfira a missão
|
|
||||||
- resolva o problema com agilidade
|
|
||||||
|
|
||||||
Em linguagem de produto, o sistema mostra como centralizar operação, comunicação e contexto em uma experiência única.
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
# Modulo de Chat WhatsApp (Frontend)
|
|
||||||
|
|
||||||
## Visao geral
|
|
||||||
|
|
||||||
O modulo de Chat no frontend integra as conversas em tempo real do WhatsApp diretamente na tela de atendimento do operador.
|
|
||||||
|
|
||||||
A interface e altamente responsiva, provendo feedback instantaneo de envio (zero latencia) e sincronizando com o backend via WebSockets (Socket.io) para atualizar estados de de-duplicacao, novas mensagens, midias e controle de posse do atendimento.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Componentes Principais
|
|
||||||
|
|
||||||
### 1. Hook de Negocio (`useChat.js`)
|
|
||||||
Centraliza todo o estado das conversas, conexao WebSocket e operacoes de rede:
|
|
||||||
* **`contacts`**: Lista de chats ativos sincronizados. Cada contato possui um objeto `assignment` (atribuicao) normalizado.
|
|
||||||
* **`messagesByContact`**: Map de historico de mensagens por JID/contato.
|
|
||||||
* **`takeChat()`**: Dispara a requisicao de rede `/whatsapp/assign` enviando o ID do atendente e o ID numerico da area do usuario logado (convertido com seguranca para inteiro).
|
|
||||||
* **`sendMessage()`**: Trata a de-duplicacao de mensagens em milissegundos e gerencia a concorrência (race condition).
|
|
||||||
|
|
||||||
### 2. Painel de Atendimento (`ChatWindow.jsx`)
|
|
||||||
O container principal da conversa selecionada. Ele renderiza:
|
|
||||||
* **Header**: Mostra o nome resolvido do cliente, canal (WhatsApp) e o indicador de quem esta atendendo.
|
|
||||||
* **Historico**: Area de scroll contendo as bolhas de mensagens do atendente (`agent`) e do cliente (`customer`), incluindo visualizadores para imagens, audios e links de arquivos.
|
|
||||||
* **Footer de Input**: Caixa de texto com suporte a tecla Enter e icone de anexo de midia (com validacao automatica de tamanho).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Mecanismos de UX e Estabilidade
|
|
||||||
|
|
||||||
### 1. Insercao Instantanea (UX Zero-Latency)
|
|
||||||
Para evitar que o atendente perceba qualquer latencia de rede, o envio e dividido em duas etapas:
|
|
||||||
1. **Fase Local**: A bolha de mensagem e inserida na tela imediatamente com um ID temporario (`temp-` + timestamp) e o texto digitado. O input de texto e arquivos e limpo na mesma hora.
|
|
||||||
2. **Fase de Disparo**: A requisicao HTTP POST e disparada para o backend em segundo plano.
|
|
||||||
|
|
||||||
### 2. De-duplicacao de Mensagens (Prevecao de Race Condition)
|
|
||||||
Como o backend envia a mensagem recebida via WebSocket assim que o Puppeteer a dispara, a bolha poderia aparecer duplicada na tela se a requisição de envio original ainda estivesse processando.
|
|
||||||
* **A Solucao**: O hook de WebSocket compara as mensagens recebidas em tempo real. Se o texto bater e a diferenca temporal de timestamp for inferior a 4 segundos, ele identifica a bolha `temp-...` local, remove o prefixo temporario e atualiza-a com o ID oficial do WhatsApp gerado no servidor. **Zero duplicacoes, zero flashes na tela.**
|
|
||||||
|
|
||||||
### 3. Validação de Posse (Type-Safe User IDs)
|
|
||||||
Para evitar conflitos na exibicao do banner *"⚠️ Atendido por outro colaborador"*, realizamos casting explicito dos IDs dos usuarios envolvidos:
|
|
||||||
```javascript
|
|
||||||
const isAssignedToMe = activeContact?.assignment?.userId && String(activeContact.assignment.userId) === String(currentUser.id);
|
|
||||||
const isAssignedToOthers = activeContact?.assignment && String(activeContact.assignment.userId) !== String(currentUser.id);
|
|
||||||
```
|
|
||||||
Isso impede que comparacoes como `4 === "4"` (inteiro vindo do banco relacional vs string vindo do localStorage/JWT) avaliem incorretamente como falso, mantendo a tela bloqueada ou liberada com precisao.
|
|
||||||
|
|
||||||
### 4. Layout e Rolagem Estrita (680px Scroll)
|
|
||||||
A interface de mensagens possui limitacoes verticais restritas para evitar que a tela se alongue infinitamente para baixo.
|
|
||||||
* A bolha de historico e fixada com altura proporcional (`height: 680px` ou `calc`) e controle de transbordo `overflow-y: auto`.
|
|
||||||
* O hook de chat escuta mudancas na lista de mensagens e realiza rolagem automatica suave (`smooth`) para o fim da tela sempre que uma nova bolha e adicionada.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Novos Fluxos Homologados (WhatsApp / Meta)
|
|
||||||
|
|
||||||
### 1. Novo Atendimento Inteligente (`NewAttendancePage.jsx`)
|
|
||||||
* **Remoção do Seletor de Área**: O seletor manual foi removido da tela para simplificar a operação. O sistema resolve a área dinamicamente a partir do atendente logado (`currentUser.areaPrincipal` ou `areas[0]`).
|
|
||||||
* **Bloqueio de Campo**: Ao escolher um contato dos recentes ou da busca lateral, o input do telefone e do nome do cliente ficam bloqueados para escrita.
|
|
||||||
* **Modo "Novo Número"**: Ao clicar no botão, o operador habilita os inputs de nome e telefone. Caso inicie o chat sem digitar um nome personalizado, o sistema aplica um fallback limpo no formato `Contato Novo (+55...)`.
|
|
||||||
|
|
||||||
### 2. Bloqueio e Envio de Templates Meta (`ChatWindow.jsx`)
|
|
||||||
Como a API oficial do WhatsApp/Meta exige uma mensagem pré-aprovada para iniciar conversas ativas (sem histórico prévio), a interface aplica travas estritas:
|
|
||||||
* **Travamento do Input**: Se a conversa selecionada possuir histórico de envio vazio (`!hasAgentMessages`), a caixa de texto principal e o botão "Enviar" ficam bloqueados.
|
|
||||||
* **Painel de Templates**: Logo acima do rodapé de digitação, renderiza-se um seletor horizontal com os templates oficiais Meta ativos no banco (buscados de `GET /whatsapp/templates`).
|
|
||||||
* **Substituição Dinâmica**: Ao clicar em um template, as variáveis `|NOME|`, `|DATA|` ou `|PROTOCOLO|` são interpoladas em tempo real com os dados do cliente, populando o input principal e liberando o fluxo de envio da primeira mensagem.
|
|
||||||
|
|
||||||
### 3. Gerenciamento de Templates para Supervisores (`SupervisorPage.jsx`)
|
|
||||||
Supervisores possuem controle administrativo total sobre as mensagens homologadas:
|
|
||||||
* **CRUD de Modelos**: Exibe todos os templates de WhatsApp em formato de cards visuais.
|
|
||||||
* **Painel de Edição**: Permite criar novos templates ou editar identificadores/conteúdos de templates existentes. As alterações persistem imediatamente no banco PostgreSQL por meio dos endpoints `/whatsapp/templates`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Como Integrar e Rodar
|
|
||||||
|
|
||||||
### Variaveis de Ambiente
|
|
||||||
O frontend conecta no WebSocket e na API do backend usando a porta padrao do NestJS:
|
|
||||||
```env
|
|
||||||
VITE_API_URL=http://localhost:3001
|
|
||||||
VITE_WS_URL=http://localhost:3001
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compilando e Rodando localmente
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
Ao selecionar uma conversa de canal "WhatsApp" que esteja livre, basta digitar uma mensagem e pressionar Enter. O chat sera automaticamente assumido por voce em tempo real, gravando no PostgreSQL e desbloqueando a janela de chat de forma instantanea.
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
# Módulo Attendance / Novo Atendimento
|
|
||||||
|
|
||||||
## Objetivo
|
|
||||||
|
|
||||||
Permitir que um operador inicie rapidamente um novo atendimento.
|
|
||||||
|
|
||||||
## Tela principal
|
|
||||||
|
|
||||||
- `NewAttendancePage.jsx`
|
|
||||||
|
|
||||||
## Componentes e lógica
|
|
||||||
|
|
||||||
- `RecentContactsList.jsx`: contatos recentes e seleção rápida
|
|
||||||
- `attendanceMocks.js`: canais, áreas e contatos mockados
|
|
||||||
|
|
||||||
## Funcionalidades simuladas
|
|
||||||
|
|
||||||
- buscar contato
|
|
||||||
- escolher um contato recente
|
|
||||||
- informar novo número
|
|
||||||
- escolher canal:
|
|
||||||
- WhatsApp
|
|
||||||
- SMS
|
|
||||||
- Ligação
|
|
||||||
- selecionar área opcional
|
|
||||||
- iniciar atendimento
|
|
||||||
|
|
||||||
## Regras de navegação
|
|
||||||
|
|
||||||
- se o canal escolhido for `WhatsApp` ou `SMS`, a navegação vai para `/chat`
|
|
||||||
- se o canal escolhido for `Ligação`, a navegação vai para `/call`
|
|
||||||
|
|
||||||
## Papel na apresentação
|
|
||||||
|
|
||||||
Esse módulo deixa muito claro o ganho operacional do produto: o atendente consegue iniciar fluxos rapidamente sem sair da mesma plataforma.
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
# Módulo Auth / Login
|
|
||||||
|
|
||||||
## Objetivo
|
|
||||||
|
|
||||||
Apresentar uma entrada elegante e moderna para o produto, simulando autenticação corporativa sem backend real.
|
|
||||||
|
|
||||||
## Tela principal
|
|
||||||
|
|
||||||
- `LoginPage.jsx`
|
|
||||||
|
|
||||||
## Componentes e lógica
|
|
||||||
|
|
||||||
- `LoginForm.jsx`: formulário visual de acesso
|
|
||||||
- `useLogin.js`: controla o envio e redirecionamento
|
|
||||||
- `authService.js`: mock de autenticação
|
|
||||||
|
|
||||||
## Fluxo
|
|
||||||
|
|
||||||
1. O usuário informa usuário e senha
|
|
||||||
2. Clica em `Entrar`
|
|
||||||
3. O login mock é executado
|
|
||||||
4. O usuário é redirecionado para `/home`
|
|
||||||
|
|
||||||
## Elementos importantes
|
|
||||||
|
|
||||||
- branding Sothis
|
|
||||||
- botão `Entrar com Microsoft`
|
|
||||||
- link `Esqueci minha senha`
|
|
||||||
- visual de produto SaaS
|
|
||||||
|
|
||||||
## Papel na apresentação
|
|
||||||
|
|
||||||
A tela de login serve como entrada institucional do MVP e ajuda a criar percepção de maturidade do produto logo no primeiro contato.
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
# Módulo Call
|
|
||||||
|
|
||||||
## Objetivo
|
|
||||||
|
|
||||||
Simular uma ligação ativa em modo softphone, com visual de operação em tempo real.
|
|
||||||
|
|
||||||
## Tela principal
|
|
||||||
|
|
||||||
- `CallPage.jsx`
|
|
||||||
|
|
||||||
## Componentes e lógica
|
|
||||||
|
|
||||||
- `CallHeader.jsx`: contexto superior e retorno para home
|
|
||||||
- `CallControls.jsx`: controles visuais da chamada
|
|
||||||
- `useCallTimer.js`: timer automático
|
|
||||||
- `callMocks.js`: dados do cliente e controles
|
|
||||||
|
|
||||||
## Funcionalidades simuladas
|
|
||||||
|
|
||||||
- contagem automática do tempo de chamada
|
|
||||||
- exibição do cliente ativo
|
|
||||||
- avatar e fila de atendimento
|
|
||||||
- controles:
|
|
||||||
- mudo
|
|
||||||
- teclado
|
|
||||||
- alto-falante
|
|
||||||
- transferir
|
|
||||||
- encerrar chamada
|
|
||||||
|
|
||||||
## Papel na apresentação
|
|
||||||
|
|
||||||
Este módulo mostra que o produto não é apenas mensageria, mas também cobre voz dentro da mesma proposta omnichannel.
|
|
||||||
@ -1,37 +0,0 @@
|
|||||||
# Módulo Chat
|
|
||||||
|
|
||||||
## Objetivo
|
|
||||||
|
|
||||||
Simular um atendimento em tempo real com aparência próxima de um produto de operação real.
|
|
||||||
|
|
||||||
## Tela principal
|
|
||||||
|
|
||||||
- `ChatPage.jsx`
|
|
||||||
|
|
||||||
## Componentes e lógica
|
|
||||||
|
|
||||||
- `ChatConversationList.jsx`: lista de contatos e canais
|
|
||||||
- `ChatWindow.jsx`: header, mensagens e input
|
|
||||||
- `ChatTransferPanel.jsx`: fluxo visual de transferência
|
|
||||||
- `useChat.js`: estado do chat, envio e resposta simulada
|
|
||||||
- `chatMocks.js`: contatos, áreas, atendentes e mensagens iniciais
|
|
||||||
|
|
||||||
## Funcionalidades simuladas
|
|
||||||
|
|
||||||
- alternar entre conversas
|
|
||||||
- enviar mensagem
|
|
||||||
- receber resposta mock automática
|
|
||||||
- rolagem automática
|
|
||||||
- transferência para outra área
|
|
||||||
- escolha de atendente de destino
|
|
||||||
- observação opcional na transferência
|
|
||||||
|
|
||||||
## Canais representados
|
|
||||||
|
|
||||||
- WhatsApp
|
|
||||||
- SMS
|
|
||||||
- Email
|
|
||||||
|
|
||||||
## Papel na apresentação
|
|
||||||
|
|
||||||
O módulo de chat demonstra como a plataforma concentra diferentes canais em uma única experiência operacional.
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
# Módulo Home / Dashboard
|
|
||||||
|
|
||||||
## Objetivo
|
|
||||||
|
|
||||||
Ser o hub central do atendente após o login.
|
|
||||||
|
|
||||||
## Tela principal
|
|
||||||
|
|
||||||
- `HomePage.jsx`
|
|
||||||
|
|
||||||
## Componentes e lógica
|
|
||||||
|
|
||||||
- `HomeTopbar.jsx`: busca, toggle de abas, avatar e contexto superior
|
|
||||||
- `HomeSidebar.jsx`: navegação lateral e botão de novo atendimento
|
|
||||||
- `MessagesWorkspace.jsx`: lista de conversas, chat resumido e painel de ações
|
|
||||||
- `CallsWorkspace.jsx`: visão resumida das ligações
|
|
||||||
- `homeMocks.js`: dados mockados do dashboard
|
|
||||||
|
|
||||||
## Funcionalidades simuladas
|
|
||||||
|
|
||||||
- trocar entre `Mensagens` e `Ligações`
|
|
||||||
- buscar conversas mockadas
|
|
||||||
- abrir rota de chat
|
|
||||||
- abrir rota de call
|
|
||||||
- abrir rota de novo atendimento
|
|
||||||
|
|
||||||
## Papel na apresentação
|
|
||||||
|
|
||||||
É a tela que melhor comunica o conceito do produto:
|
|
||||||
|
|
||||||
- múltiplos canais
|
|
||||||
- visão operacional única
|
|
||||||
- agilidade no atendimento
|
|
||||||
- sensação de plataforma pronta para uso
|
|
||||||
|
|
||||||
## Pontos de UX
|
|
||||||
|
|
||||||
- responsividade adaptada para mobile, tablet, desktop e desktop largo
|
|
||||||
- sidebar separada do conteúdo para leitura mais clara
|
|
||||||
- cards de contexto e indicadores operacionais
|
|
||||||
@ -1,83 +0,0 @@
|
|||||||
# Visão Geral do Projeto
|
|
||||||
|
|
||||||
## Objetivo
|
|
||||||
|
|
||||||
O projeto representa o frontend MVP do Omnichannel Sothis.
|
|
||||||
|
|
||||||
O objetivo principal é demonstrar, de forma visual e convincente, como um atendente pode:
|
|
||||||
|
|
||||||
- entrar na plataforma
|
|
||||||
- visualizar atendimentos e conversas
|
|
||||||
- iniciar um novo atendimento
|
|
||||||
- conversar com clientes em canais diferentes
|
|
||||||
- simular uma ligação ativa
|
|
||||||
|
|
||||||
## Diretriz do MVP
|
|
||||||
|
|
||||||
Este MVP prioriza percepção de produto acabado.
|
|
||||||
|
|
||||||
Ou seja:
|
|
||||||
|
|
||||||
- os fluxos parecem reais
|
|
||||||
- os dados são mockados
|
|
||||||
- a navegação existe
|
|
||||||
- a experiência é pensada para demonstração, validação e apresentação
|
|
||||||
|
|
||||||
## Stack
|
|
||||||
|
|
||||||
- React
|
|
||||||
- Vite
|
|
||||||
- JavaScript
|
|
||||||
- React Router
|
|
||||||
- CSS via estilos modernos em componentes
|
|
||||||
|
|
||||||
## Estrutura
|
|
||||||
|
|
||||||
O frontend foi organizado por módulos, seguindo uma abordagem feature-based:
|
|
||||||
|
|
||||||
- `auth`
|
|
||||||
- `home`
|
|
||||||
- `chat`
|
|
||||||
- `call`
|
|
||||||
- `attendance`
|
|
||||||
|
|
||||||
Cada módulo concentra suas páginas, componentes, hooks e services mockados.
|
|
||||||
|
|
||||||
## Rotas atuais
|
|
||||||
|
|
||||||
- `/login`
|
|
||||||
- `/home`
|
|
||||||
- `/chat`
|
|
||||||
- `/call`
|
|
||||||
- `/new-attendance`
|
|
||||||
|
|
||||||
## Módulos
|
|
||||||
|
|
||||||
### Auth
|
|
||||||
|
|
||||||
Simula autenticação e entrada no sistema.
|
|
||||||
|
|
||||||
### Home
|
|
||||||
|
|
||||||
É a central do operador, com dashboard, conversas, atalhos e navegação fake para os fluxos principais.
|
|
||||||
|
|
||||||
### Chat
|
|
||||||
|
|
||||||
Simula atendimento em tempo real com mensagens, transferência e respostas automáticas mockadas.
|
|
||||||
|
|
||||||
### Call
|
|
||||||
|
|
||||||
Simula uma ligação ativa com timer automático e controles visuais de softphone.
|
|
||||||
|
|
||||||
### Attendance
|
|
||||||
|
|
||||||
Permite iniciar rapidamente um novo atendimento, escolhendo contato, canal e área.
|
|
||||||
|
|
||||||
## Público da documentação
|
|
||||||
|
|
||||||
Esta documentação serve para:
|
|
||||||
|
|
||||||
- apresentação de produto
|
|
||||||
- onboarding técnico
|
|
||||||
- alinhamento entre frontend, backend e deploy
|
|
||||||
- futura evolução para integração real
|
|
||||||
@ -188,7 +188,7 @@ function MediaRenderer({ message, isAgent }) {
|
|||||||
|
|
||||||
function AttachmentPreview({ file, onRemove }) {
|
function AttachmentPreview({ file, onRemove }) {
|
||||||
if (!file) return null;
|
if (!file) return null;
|
||||||
const mediaUrl = getMediaUrl({ data: file.data, mimetype: file.type });
|
const mediaUrl = file.previewUrl || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -203,7 +203,7 @@ function AttachmentPreview({ file, onRemove }) {
|
|||||||
background: '#fff',
|
background: '#fff',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{file.type?.startsWith('image/') ? (
|
{file.type?.startsWith('image/') && mediaUrl ? (
|
||||||
<img
|
<img
|
||||||
src={mediaUrl}
|
src={mediaUrl}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
|
|||||||
@ -16,11 +16,12 @@ import {
|
|||||||
listWhatsappChats,
|
listWhatsappChats,
|
||||||
listWhatsappMessages,
|
listWhatsappMessages,
|
||||||
releaseWhatsappChat,
|
releaseWhatsappChat,
|
||||||
|
sendWhatsappMedia,
|
||||||
sendWhatsappMessage,
|
sendWhatsappMessage,
|
||||||
transferWhatsappChat,
|
transferWhatsappChat,
|
||||||
} from '../services/whatsappChatService';
|
} from '../services/whatsappChatService';
|
||||||
|
|
||||||
const MAX_ATTACHMENT_SIZE_BYTES = 15 * 1024 * 1024;
|
const MAX_ATTACHMENT_SIZE_BYTES = 100 * 1024 * 1024;
|
||||||
|
|
||||||
function getPresenceByUserId(presenceList, userId) {
|
function getPresenceByUserId(presenceList, userId) {
|
||||||
return presenceList.find((presence) => Number(presence.user_id) === Number(userId)) || null;
|
return presenceList.find((presence) => Number(presence.user_id) === Number(userId)) || null;
|
||||||
@ -165,17 +166,6 @@ function dedupeMessages(messages) {
|
|||||||
return messages.reduce((acc, message) => mergeMessageList(acc, message), []);
|
return messages.reduce((acc, message) => mergeMessageList(acc, message), []);
|
||||||
}
|
}
|
||||||
|
|
||||||
function fileToBase64(file) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
const result = String(reader.result || '');
|
|
||||||
resolve(result.includes(',') ? result.split(',')[1] : result);
|
|
||||||
};
|
|
||||||
reader.onerror = reject;
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeComparableContact(contact) {
|
function normalizeComparableContact(contact) {
|
||||||
return {
|
return {
|
||||||
@ -581,23 +571,23 @@ export function useChat() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function attachFile(file) {
|
function attachFile(file) {
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
if (file.size > MAX_ATTACHMENT_SIZE_BYTES) {
|
if (file.size > MAX_ATTACHMENT_SIZE_BYTES) {
|
||||||
setApiError('Arquivo muito grande. Envie uma mídia de até 15 MB.');
|
setApiError('Arquivo muito grande. O limite máximo é 100 MB.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await fileToBase64(file);
|
|
||||||
setAttachedFile({
|
setAttachedFile({
|
||||||
name: file.name,
|
name: file.name,
|
||||||
type: file.type || 'application/octet-stream',
|
type: file.type || 'application/octet-stream',
|
||||||
data,
|
previewUrl: URL.createObjectURL(file),
|
||||||
|
rawFile: file,
|
||||||
});
|
});
|
||||||
setApiError(null);
|
setApiError(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAttachedFile() {
|
function removeAttachedFile() {
|
||||||
|
if (attachedFile?.previewUrl) URL.revokeObjectURL(attachedFile.previewUrl);
|
||||||
setAttachedFile(null);
|
setAttachedFile(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -763,28 +753,23 @@ export function useChat() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const media = attachedFile
|
const fileToSend = attachedFile || null;
|
||||||
? {
|
|
||||||
data: attachedFile.data,
|
|
||||||
mimetype: attachedFile.type,
|
|
||||||
filename: attachedFile.name,
|
|
||||||
}
|
|
||||||
: null;
|
|
||||||
const newMessage = {
|
const newMessage = {
|
||||||
id: `temp-${Date.now()}`,
|
id: `temp-${Date.now()}`,
|
||||||
chatId: contactId,
|
chatId: contactId,
|
||||||
sender: 'agent',
|
sender: 'agent',
|
||||||
text: trimmed,
|
text: trimmed,
|
||||||
timestamp: Math.floor(Date.now() / 1000),
|
timestamp: Math.floor(Date.now() / 1000),
|
||||||
hasMedia: Boolean(media),
|
hasMedia: Boolean(fileToSend),
|
||||||
media,
|
mediaUrl: fileToSend?.previewUrl || null,
|
||||||
|
mediaMimeType: fileToSend?.type || null,
|
||||||
};
|
};
|
||||||
|
|
||||||
setMessagesByContact((current) => ({
|
setMessagesByContact((current) => ({
|
||||||
...current,
|
...current,
|
||||||
[contactId]: mergeMessageList(current[contactId] || [], newMessage),
|
[contactId]: mergeMessageList(current[contactId] || [], newMessage),
|
||||||
}));
|
}));
|
||||||
updateContactPreview(contactId, trimmed || '[Mídia]', media);
|
updateContactPreview(contactId, trimmed || '[Mídia]', fileToSend ? { filename: fileToSend.name } : null);
|
||||||
if (contactId === activeContactId) {
|
if (contactId === activeContactId) {
|
||||||
setDraft('');
|
setDraft('');
|
||||||
}
|
}
|
||||||
@ -793,12 +778,20 @@ export function useChat() {
|
|||||||
if (!contactId) return;
|
if (!contactId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await sendWhatsappMessage({
|
if (fileToSend) {
|
||||||
to: contactId,
|
const form = new FormData();
|
||||||
message: trimmed,
|
form.append('file', fileToSend.rawFile, fileToSend.name);
|
||||||
senderName: getUserDisplayName(currentUser),
|
form.append('to', contactId);
|
||||||
media,
|
form.append('senderName', getUserDisplayName(currentUser));
|
||||||
});
|
if (trimmed) form.append('caption', trimmed);
|
||||||
|
await sendWhatsappMedia(form);
|
||||||
|
} else {
|
||||||
|
await sendWhatsappMessage({
|
||||||
|
to: contactId,
|
||||||
|
message: trimmed,
|
||||||
|
senderName: getUserDisplayName(currentUser),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setApiError(null);
|
setApiError(null);
|
||||||
updateContact(contactId, (contact) => ({
|
updateContact(contactId, (contact) => ({
|
||||||
@ -810,7 +803,7 @@ export function useChat() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
setApiError(
|
setApiError(
|
||||||
error.status === 413
|
error.status === 413
|
||||||
? 'Arquivo muito grande para envio. Tente uma midia menor.'
|
? 'Arquivo muito grande para envio. Tente uma mídia menor.'
|
||||||
: error.message,
|
: error.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,3 +56,11 @@ export function transferWhatsappChat(payload) {
|
|||||||
fallbackMessage: 'Nao foi possivel transferir o atendimento.',
|
fallbackMessage: 'Nao foi possivel transferir o atendimento.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sendWhatsappMedia(formData) {
|
||||||
|
return apiRequest('/whatsapp/send-media', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
fallbackMessage: 'Não foi possível enviar a mídia.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user