Compare commits
4 Commits
a0e9fc120f
...
e9e214195a
| Author | SHA1 | Date | |
|---|---|---|---|
| e9e214195a | |||
| a6a09aa740 | |||
| 08eea51d5a | |||
| cfa99a4881 |
548
README.md
548
README.md
@ -1,29 +1,545 @@
|
|||||||
# Omnichannel Sothis - Deploy
|
# Omnichannel Sothis
|
||||||
|
|
||||||
Este repositorio e o **deploy** do ecossistema. Ele **nao** contem o codigo do frontend nem do backend em producao. Esses dois sao repositorios separados e devem ser clonados ao lado deste deploy para subir o ambiente completo.
|
Plataforma omnichannel para atendimento com foco inicial em WhatsApp, autenticação corporativa, controle de acesso por perfil, filas por especialidade, agenda de contatos, templates de mensagens e painéis operacionais para agente, supervisor e administrador.
|
||||||
|
|
||||||
## Estrutura esperada (3 repositorios)
|
Este repositório é a raiz do ambiente local/deploy. Ele contém o `docker-compose.yml`, as migrations de banco em `database/` e espera os projetos `frontend/` e `backend/` clonados ou presentes na mesma pasta.
|
||||||
|
|
||||||
- `deploy/` (este repo): `docker-compose.yml`, `database/`, `.gitignore`, `README.md`
|
## Estado Atual do Produto
|
||||||
- `frontend/`: interface do produto
|
|
||||||
- `backend/`: API e regras de negocio
|
|
||||||
|
|
||||||
## Como subir tudo localmente
|
O produto hoje permite:
|
||||||
|
|
||||||
Passo a passo (na mesma pasta raiz):
|
- Login via Active Directory/LDAP e Microsoft OAuth.
|
||||||
|
- Redirecionamento da Home conforme perfil do usuário.
|
||||||
|
- Atendimento WhatsApp em tempo real com socket.
|
||||||
|
- Triagem automática inicial pelo Agente Virtual Sothis.
|
||||||
|
- Fila de atendimento por especialidade.
|
||||||
|
- Assumir, liberar e transferir atendimentos.
|
||||||
|
- Controle da janela de 24 horas do WhatsApp.
|
||||||
|
- Envio e recebimento de texto, imagem, vídeo, áudio e documentos.
|
||||||
|
- Lazy loading de mídias históricas.
|
||||||
|
- Agenda geral de contatos.
|
||||||
|
- Novo atendimento ativo via templates aprovados.
|
||||||
|
- Notas pessoais do atendente.
|
||||||
|
- Administração de usuários, perfis e especialidades.
|
||||||
|
- Workflow de criação, aprovação, reprovação e exclusão de templates.
|
||||||
|
- Painel mensal do admin e painel operacional do supervisor.
|
||||||
|
|
||||||
1. Clonar o repo de deploy na raiz (.)
|
## Perfis e Acesso
|
||||||
2. Clonar o repo de frontend na pasta `frontend`
|
|
||||||
3. Clonar o repo de backend na pasta `backend`
|
### Usuário sem perfil
|
||||||
4. Subir tudo:
|
|
||||||
|
Quando um usuário autentica pela primeira vez e ainda não tem perfil/especialidade definida, ele cai em uma tela vazia informando que não está atribuído a uma área/perfil.
|
||||||
|
|
||||||
|
A liberação desse usuário é feita no painel do admin em `Usuários & Acessos`.
|
||||||
|
|
||||||
|
### Agente
|
||||||
|
|
||||||
|
O agente:
|
||||||
|
|
||||||
|
- Vê somente atendimentos das especialidades em que está vinculado.
|
||||||
|
- Pode ver chamados da especialidade enquanto estão na fila.
|
||||||
|
- Só pode responder depois de assumir o atendimento.
|
||||||
|
- Pode liberar um atendimento para voltar para a fila.
|
||||||
|
- Pode transferir atendimento somente depois de assumir.
|
||||||
|
- Pode transferir para outra especialidade ou para outro usuário elegível.
|
||||||
|
- Pode editar/salvar contato na agenda.
|
||||||
|
- Pode criar notas pessoais.
|
||||||
|
- Pode abrir novo atendimento usando template aprovado.
|
||||||
|
|
||||||
|
### Supervisor
|
||||||
|
|
||||||
|
O supervisor:
|
||||||
|
|
||||||
|
- Acessa a Home operacional da supervisão.
|
||||||
|
- Vê indicadores e filas das especialidades que supervisiona.
|
||||||
|
- Pode usar templates, base de conhecimento, auditoria, atendimento, abrir atendimento, disparo em massa e contatos.
|
||||||
|
- Pode solicitar novos templates, mas eles entram primeiro em aprovação do admin.
|
||||||
|
|
||||||
|
### Admin
|
||||||
|
|
||||||
|
O admin:
|
||||||
|
|
||||||
|
- Não precisa pertencer a uma especialidade.
|
||||||
|
- Vê todas as filas e todos os atendimentos já roteados/classificados.
|
||||||
|
- Não vê conversas ainda em triagem automática do Agente Virtual Sothis (`bot_triage`).
|
||||||
|
- Cria e gerencia especialidades.
|
||||||
|
- Define perfis de usuários.
|
||||||
|
- Pode tornar outro usuário admin.
|
||||||
|
- Pode definir se um usuário atua como agente ou supervisor em especialidades.
|
||||||
|
- Pode aprovar, reprovar e excluir templates.
|
||||||
|
- Acessa a visão mensal administrativa.
|
||||||
|
- Também pode atender, usando o menu administrativo.
|
||||||
|
|
||||||
|
## Regras de Atendimento WhatsApp
|
||||||
|
|
||||||
|
### Triagem automática pelo Agente Virtual Sothis
|
||||||
|
|
||||||
|
Toda primeira mensagem recebida no WhatsApp passa pela triagem automática do Agente Virtual Sothis quando ainda não existe atendimento classificado.
|
||||||
|
|
||||||
|
Regras atuais:
|
||||||
|
|
||||||
|
- Mensagens vazias são registradas, mas não disparam triagem.
|
||||||
|
- Mídia sem legenda é registrada, mas não aciona o Agente Virtual Sothis automaticamente.
|
||||||
|
- A triagem é serializada por conversa para evitar respostas duplicadas quando o WhatsApp dispara eventos quase simultâneos.
|
||||||
|
- O backend guarda `last_routed_message_id` para evitar processar a mesma mensagem mais de uma vez.
|
||||||
|
|
||||||
|
Fluxo de decisão:
|
||||||
|
|
||||||
|
1. Se a mensagem já contém uma intenção clara, o Agente Virtual Sothis roteia direto para a especialidade.
|
||||||
|
2. Se a primeira mensagem é genérica, o Agente Virtual Sothis envia a saudação completa e mantém a conversa em `bot_triage`.
|
||||||
|
3. Se a segunda mensagem ainda não identifica intenção, o Agente Virtual Sothis pede explicitamente: suporte, financeiro ou comercial.
|
||||||
|
4. Se a terceira mensagem ainda for inválida, a conversa cai automaticamente em Suporte.
|
||||||
|
|
||||||
|
Exemplos de intenção:
|
||||||
|
|
||||||
|
- Suporte: suporte, bug, problema, técnico.
|
||||||
|
- Financeiro: boleto, dinheiro, cartão, atraso, fatura.
|
||||||
|
- Comercial: produto, novo, contratar, proposta.
|
||||||
|
|
||||||
|
### Estados do atendimento
|
||||||
|
|
||||||
|
A tabela `whatsapp_chat_atribuicoes` representa o estado do atendimento:
|
||||||
|
|
||||||
|
- `bot_triage`: conversa ainda está com o Agente Virtual Sothis.
|
||||||
|
- `queued`: conversa está na fila da especialidade, sem atendente.
|
||||||
|
- `assigned`: conversa foi assumida ou atribuída diretamente a um atendente.
|
||||||
|
|
||||||
|
Regras:
|
||||||
|
|
||||||
|
- Conversas em `bot_triage` não aparecem para agentes.
|
||||||
|
- Conversas em `queued` aparecem para usuários da especialidade.
|
||||||
|
- Conversas em `assigned` continuam visíveis para a especialidade, mas só o responsável responde.
|
||||||
|
- Admin vê todas as conversas roteadas, exceto `bot_triage`.
|
||||||
|
|
||||||
|
### Assumir, liberar e transferir
|
||||||
|
|
||||||
|
Para responder um chamado, o usuário precisa assumir o atendimento.
|
||||||
|
|
||||||
|
- Ao assumir, `user_id` passa a ser o usuário atual.
|
||||||
|
- Ao liberar, o atendimento volta para fila da especialidade.
|
||||||
|
- Transferência só fica disponível depois que o usuário assume.
|
||||||
|
- Transferência para a mesma especialidade pode apontar para outro usuário.
|
||||||
|
- Transferência para outra especialidade cai na fila da nova especialidade.
|
||||||
|
- A observação da transferência fica visível para o próximo atendente e é limpa depois que o atendimento é respondido.
|
||||||
|
|
||||||
|
### Janela de 24 horas
|
||||||
|
|
||||||
|
O produto respeita a regra operacional do WhatsApp:
|
||||||
|
|
||||||
|
- Conversas têm uma janela de 24 horas representada por `conversation_started_at` e `expires_at`.
|
||||||
|
- Ao abrir atendimento ativo para um cliente, o atendente deve usar template aprovado.
|
||||||
|
- Depois do primeiro contato ativo, o atendimento fica bloqueado para novas mensagens livres até o cliente responder.
|
||||||
|
- Quando o cliente responde, o bloqueio `awaiting_customer_reply` é removido e o atendente pode seguir a conversa.
|
||||||
|
|
||||||
|
## Mensagens e Mídias
|
||||||
|
|
||||||
|
### Envio de mensagens
|
||||||
|
|
||||||
|
Mensagens enviadas por atendente são formatadas com identificação:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Atendente: Nome do Usuário
|
||||||
|
|
||||||
|
Mensagem
|
||||||
|
```
|
||||||
|
|
||||||
|
No WhatsApp, essa identificação é enviada usando negrito:
|
||||||
|
|
||||||
|
```text
|
||||||
|
*Atendente: Nome do Usuário*
|
||||||
|
|
||||||
|
Mensagem
|
||||||
|
```
|
||||||
|
|
||||||
|
Se uma mídia for enviada sem texto, o backend não envia cabeçalho solto. A mídia segue sem legenda.
|
||||||
|
|
||||||
|
### Recebimento de mídias
|
||||||
|
|
||||||
|
Tipos suportados:
|
||||||
|
|
||||||
|
- Imagens: PNG, JPG, JPEG, WEBP.
|
||||||
|
- Vídeos: MP4, WEBM.
|
||||||
|
- Áudios: MP3, OGG, WAV.
|
||||||
|
- Documentos: exibidos como card de arquivo.
|
||||||
|
|
||||||
|
Para preservar performance, o histórico de mensagens não carrega Base64 de mídia diretamente. O frontend busca a mídia sob demanda pelo endpoint:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /whatsapp/media/:chatId/:messageId
|
||||||
|
```
|
||||||
|
|
||||||
|
### Limites de upload
|
||||||
|
|
||||||
|
O backend aceita JSON até `25mb` por padrão, configurável via:
|
||||||
|
|
||||||
|
```env
|
||||||
|
REQUEST_BODY_LIMIT=25mb
|
||||||
|
```
|
||||||
|
|
||||||
|
O frontend bloqueia anexos acima de 15 MB para evitar falhas de payload e degradação da experiência.
|
||||||
|
|
||||||
|
## Chat
|
||||||
|
|
||||||
|
A tela `/chat` exibe somente conversas reais vindas do backend/WhatsApp. Não existe fallback local de conversas.
|
||||||
|
|
||||||
|
Recursos atuais:
|
||||||
|
|
||||||
|
- Lista de conversas ativas com scroll.
|
||||||
|
- Chat com altura delimitada e scroll interno.
|
||||||
|
- Separador de datas nas mensagens.
|
||||||
|
- Horário por mensagem.
|
||||||
|
- Deduplicação visual de mensagens enviadas localmente e confirmadas por socket.
|
||||||
|
- Ocultação de mensagens vazias.
|
||||||
|
- Botão para assumir atendimento.
|
||||||
|
- Botão para sair do atendimento.
|
||||||
|
- Transferência de atendimento.
|
||||||
|
- Painel de contato do cliente.
|
||||||
|
- Envio de mídia.
|
||||||
|
|
||||||
|
Indicadores na lista:
|
||||||
|
|
||||||
|
- Bolinha amarela: chamado está na fila da especialidade e ainda não foi atribuído.
|
||||||
|
- Bolinha azul: chamado está atribuído ao usuário atual.
|
||||||
|
- Bolinha vermelha: chamado está atribuído a outra pessoa.
|
||||||
|
- Badge de especialidade: mostra para qual fila o chamado foi roteado.
|
||||||
|
- Ícone de contato: indica que o contato está salvo na agenda.
|
||||||
|
|
||||||
|
## Home do Agente
|
||||||
|
|
||||||
|
A Home do agente usa dados reais do chat quando o WhatsApp está conectado.
|
||||||
|
|
||||||
|
Recursos atuais:
|
||||||
|
|
||||||
|
- Últimos atendimentos.
|
||||||
|
- Preview do chat com scroll automático para a última mensagem.
|
||||||
|
- Respostas sugeridas baseadas no contexto.
|
||||||
|
- Envio da resposta sugerida direto para o chat correto.
|
||||||
|
- Comunicados e notas.
|
||||||
|
- Notas pessoais persistidas por usuário.
|
||||||
|
- Relógio/data em tempo real.
|
||||||
|
- Atalhos para scripts, relatórios pessoais, disparo em massa e base de conhecimento.
|
||||||
|
|
||||||
|
## Novo Atendimento
|
||||||
|
|
||||||
|
A tela `/new-attendance` permite iniciar contato ativo.
|
||||||
|
|
||||||
|
Regras:
|
||||||
|
|
||||||
|
- WhatsApp é o canal funcional.
|
||||||
|
- Email e SMS aparecem bloqueados como funcionalidades em construção.
|
||||||
|
- O atendente pode buscar contatos da agenda.
|
||||||
|
- Também pode digitar novo número, nome, empresa e observação.
|
||||||
|
- O telefone tem máscara e seletor de país.
|
||||||
|
- Países disponíveis atualmente: Brasil, EUA, Argentina, Chile e México.
|
||||||
|
- O primeiro contato ativo exige seleção de template aprovado.
|
||||||
|
- Ao iniciar atendimento, o sistema salva/atualiza o contato na agenda e abre o chat daquela conversa.
|
||||||
|
|
||||||
|
## Agenda de Contatos
|
||||||
|
|
||||||
|
A agenda é geral, não vinculada a uma especialidade específica.
|
||||||
|
|
||||||
|
Tabela principal:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agenda_contatos
|
||||||
|
```
|
||||||
|
|
||||||
|
Campos funcionais:
|
||||||
|
|
||||||
|
- Nome.
|
||||||
|
- Empresa.
|
||||||
|
- Telefone.
|
||||||
|
- Observação.
|
||||||
|
- Chat ID.
|
||||||
|
- Usuário que criou/atualizou.
|
||||||
|
|
||||||
|
Na conversa, o telefone não é editável quando vem do WhatsApp, mas nome, empresa e observação podem ser atualizados.
|
||||||
|
|
||||||
|
## Templates WhatsApp
|
||||||
|
|
||||||
|
Templates ficam na tabela:
|
||||||
|
|
||||||
|
```text
|
||||||
|
whatsapp_templates
|
||||||
|
```
|
||||||
|
|
||||||
|
Campos relevantes:
|
||||||
|
|
||||||
|
- `name`
|
||||||
|
- `content`
|
||||||
|
- `area_id`
|
||||||
|
- `status`
|
||||||
|
- `requested_by_role`
|
||||||
|
- `admin_approved_at`
|
||||||
|
- `meta_submitted_at`
|
||||||
|
- `meta_approved_at`
|
||||||
|
|
||||||
|
Status atuais:
|
||||||
|
|
||||||
|
- `approved`: template aprovado e disponível para uso.
|
||||||
|
- `meta_review`: enviado para aprovação da Meta.
|
||||||
|
- `admin_review`: aguardando aprovação do admin.
|
||||||
|
- `rejected`: reprovado pelo admin.
|
||||||
|
|
||||||
|
Regras:
|
||||||
|
|
||||||
|
- Admin cria template e envia para aprovação.
|
||||||
|
- Supervisor cria template, mas ele entra em aprovação do admin.
|
||||||
|
- Admin pode aprovar e enviar para Meta.
|
||||||
|
- Admin pode reprovar.
|
||||||
|
- Admin pode excluir.
|
||||||
|
- A aprovação da Meta é simulada: templates em análise são aprovados automaticamente depois de um intervalo configurado no código.
|
||||||
|
- A listagem possui filtro por especialidade e scroll para não ocupar a página inteira.
|
||||||
|
|
||||||
|
## Painel Admin
|
||||||
|
|
||||||
|
A Home do admin é uma visão mensal.
|
||||||
|
|
||||||
|
Recursos atuais:
|
||||||
|
|
||||||
|
- Filtro por especialidade no topo.
|
||||||
|
- KPIs mensais.
|
||||||
|
- Total de atendimentos.
|
||||||
|
- Tempo médio.
|
||||||
|
- TME.
|
||||||
|
- TMR.
|
||||||
|
- Satisfação.
|
||||||
|
- Atendentes ativos.
|
||||||
|
- Gráfico de atendimentos por dia.
|
||||||
|
- Donut de distribuição por canal.
|
||||||
|
- Ranking de atendentes.
|
||||||
|
- Painel de avisos.
|
||||||
|
- Menu administrativo lateral.
|
||||||
|
|
||||||
|
Menu atual:
|
||||||
|
|
||||||
|
- Home.
|
||||||
|
- Operação.
|
||||||
|
- Usuários & Acessos.
|
||||||
|
- Templates.
|
||||||
|
- Base de conhecimento IA.
|
||||||
|
- Auditoria.
|
||||||
|
- Canais.
|
||||||
|
- Atendimento.
|
||||||
|
- Abrir Atendimento.
|
||||||
|
- Disparo em Massa.
|
||||||
|
- Contatos.
|
||||||
|
- Configurações.
|
||||||
|
- Sair.
|
||||||
|
|
||||||
|
## Usuários & Acessos
|
||||||
|
|
||||||
|
Tela administrativa para usuários, perfis e especialidades.
|
||||||
|
|
||||||
|
Regras atuais:
|
||||||
|
|
||||||
|
- Usuários vêm do banco/autenticação.
|
||||||
|
- Admin Demo, Supervisor Demo e Atendente Demo foram removidos por migration.
|
||||||
|
- Admin pode editar um usuário em modal.
|
||||||
|
- Se o usuário for admin, não há seleção de especialidades.
|
||||||
|
- Se não for admin, é possível vincular múltiplas especialidades.
|
||||||
|
- Para cada especialidade, o usuário pode atuar como agente ou supervisor.
|
||||||
|
- A criação/edição de especialidades é feita pelo admin.
|
||||||
|
- A definição de supervisores agora ocorre no vínculo do usuário, não dentro da especialidade.
|
||||||
|
|
||||||
|
## Painel Supervisor / Operação
|
||||||
|
|
||||||
|
O painel de supervisor também é usado como visão de operação no menu do admin.
|
||||||
|
|
||||||
|
Recursos atuais:
|
||||||
|
|
||||||
|
- Filtro por especialidade.
|
||||||
|
- KPIs do dia.
|
||||||
|
- Atendimentos finalizados.
|
||||||
|
- Atendimentos em aberto.
|
||||||
|
- Conversas na fila.
|
||||||
|
- Tempo médio do dia.
|
||||||
|
- Atendentes online simulados.
|
||||||
|
- Painel do time.
|
||||||
|
- Fila de espera.
|
||||||
|
- Atribuição via modal.
|
||||||
|
- Gráfico do dia por hora.
|
||||||
|
|
||||||
|
Sempre que há dado operacional disponível, os painéis usam a origem real consolidada. Indicadores sem origem consolidada ainda usam dados de apoio para compor a visão.
|
||||||
|
|
||||||
|
## Autenticação
|
||||||
|
|
||||||
|
Endpoints principais:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /auth/config
|
||||||
|
POST /auth/login
|
||||||
|
GET /auth/oauth/microsoft/start
|
||||||
|
GET /auth/oauth/microsoft/callback
|
||||||
|
```
|
||||||
|
|
||||||
|
Providers suportados:
|
||||||
|
|
||||||
|
- LDAP/Active Directory.
|
||||||
|
- Microsoft OAuth.
|
||||||
|
|
||||||
|
Variáveis relevantes:
|
||||||
|
|
||||||
|
```env
|
||||||
|
AUTH_PROVIDERS=ldap,microsoft
|
||||||
|
LDAP_ENABLED=true
|
||||||
|
LDAP_URL=ldaps://...
|
||||||
|
LDAP_DOMAIN=...
|
||||||
|
MICROSOFT_ENABLED=false
|
||||||
|
MICROSOFT_TENANT_ID=common
|
||||||
|
MICROSOFT_CLIENT_ID=
|
||||||
|
MICROSOFT_CLIENT_SECRET=
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints Principais
|
||||||
|
|
||||||
|
### WhatsApp
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /whatsapp/status
|
||||||
|
GET /whatsapp/chats
|
||||||
|
GET /whatsapp/messages/:chatId
|
||||||
|
GET /whatsapp/media/:chatId/:messageId
|
||||||
|
POST /whatsapp/send
|
||||||
|
POST /whatsapp/start-attendance
|
||||||
|
POST /whatsapp/assign
|
||||||
|
POST /whatsapp/transfer
|
||||||
|
DELETE /whatsapp/release/:chatId
|
||||||
|
GET /whatsapp/assignment/:chatId
|
||||||
|
```
|
||||||
|
|
||||||
|
### Templates
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /whatsapp/templates
|
||||||
|
POST /whatsapp/templates
|
||||||
|
POST /whatsapp/templates/update/:id
|
||||||
|
POST /whatsapp/templates/approve-admin/:id
|
||||||
|
POST /whatsapp/templates/reject-admin/:id
|
||||||
|
DELETE /whatsapp/templates/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Admin / Acessos
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /admin/access/options
|
||||||
|
GET /admin/access/overview
|
||||||
|
GET /admin/access/areas
|
||||||
|
POST /admin/access/areas
|
||||||
|
PUT /admin/access/areas/:id
|
||||||
|
GET /admin/access/users
|
||||||
|
PUT /admin/access/users/:id
|
||||||
|
```
|
||||||
|
|
||||||
|
## Banco de Dados
|
||||||
|
|
||||||
|
Migrations principais:
|
||||||
|
|
||||||
|
- `005_templates.sql`: criação inicial de templates.
|
||||||
|
- `006_whatsapp_assignment_queue.sql`: fila e atribuição de WhatsApp.
|
||||||
|
- `007_whatsapp_triage_state.sql`: estado de triagem automática.
|
||||||
|
- `008_agent_notes.sql`: notas pessoais do atendente.
|
||||||
|
- `009_customer_contacts.sql`: primeira versão de contatos.
|
||||||
|
- `010_agenda_contatos.sql`: agenda geral consolidada.
|
||||||
|
- `011_whatsapp_opening_templates.sql`: templates de abertura ativa.
|
||||||
|
- `012_whatsapp_awaiting_customer_reply.sql`: bloqueio até resposta do cliente.
|
||||||
|
- `013_remove_demo_access_users.sql`: remoção de usuários demo.
|
||||||
|
- `014_whatsapp_template_workflow.sql`: workflow de aprovação de templates.
|
||||||
|
|
||||||
|
## Estrutura do Repositório
|
||||||
|
|
||||||
|
```text
|
||||||
|
omnichannel/
|
||||||
|
├── backend/ # API NestJS e regras de negócio
|
||||||
|
├── frontend/ # Interface React/Vite
|
||||||
|
├── database/migrations/ # Migrations SQL
|
||||||
|
├── docker-compose.yml # Orquestração local
|
||||||
|
├── .env.example # Exemplo de variáveis
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Como Subir Localmente
|
||||||
|
|
||||||
|
Pré-requisitos:
|
||||||
|
|
||||||
|
- Node.js compatível com os projetos.
|
||||||
|
- Docker e Docker Compose.
|
||||||
|
- PostgreSQL via `docker-compose`.
|
||||||
|
|
||||||
|
Na raiz:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d --build
|
docker compose up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Isso sobe `frontend`, `backend` e `database` em uma unica operacao.
|
Também é possível rodar frontend e backend em modo desenvolvimento:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
URLs locais comuns:
|
||||||
|
|
||||||
|
- Frontend: `http://localhost:5173`
|
||||||
|
- Backend: `http://localhost:3001`
|
||||||
|
- Status WhatsApp: `http://localhost:3001/whatsapp/status`
|
||||||
|
|
||||||
|
## Variáveis de Ambiente
|
||||||
|
|
||||||
|
Principais variáveis da raiz:
|
||||||
|
|
||||||
|
```env
|
||||||
|
POSTGRES_USER=omnichannel
|
||||||
|
POSTGRES_PASSWORD=change-me
|
||||||
|
POSTGRES_DB=omnichannel
|
||||||
|
|
||||||
|
DB_HOST=postgres
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USER=omnichannel
|
||||||
|
DB_PASSWORD=change-me
|
||||||
|
DB_NAME=omnichannel
|
||||||
|
|
||||||
|
PORT=3001
|
||||||
|
FRONTEND_URL=http://localhost:3000
|
||||||
|
JWT_SECRET=change-this-long-random-secret
|
||||||
|
JWT_EXPIRES_IN=8h
|
||||||
|
|
||||||
|
REQUEST_BODY_LIMIT=25mb
|
||||||
|
```
|
||||||
|
|
||||||
|
No frontend, a URL da API deve vir de:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_URL=http://localhost:3001
|
||||||
|
```
|
||||||
|
|
||||||
|
## Limitações Conhecidas
|
||||||
|
|
||||||
|
- O WhatsApp Web carrega conversas por lazy loading. Nem sempre todo histórico aparece imediatamente após conectar o QR Code.
|
||||||
|
- O status real de presença do contato foi removido por limitação prática da biblioteca/WhatsApp Web.
|
||||||
|
- O painel de supervisor ainda possui indicadores simulados onde não há métrica real consolidada.
|
||||||
|
- Email, SMS e ligação não estão operacionais como canais de atendimento.
|
||||||
|
- A aprovação da Meta para templates é simulada.
|
||||||
|
- Mídias ainda trafegam via Base64 no JSON; para produção, o ideal é evoluir para upload/armazenamento próprio e envio por referência.
|
||||||
|
|
||||||
|
## Regras Importantes Para Desenvolvimento
|
||||||
|
|
||||||
|
- Não reintroduzir fallback local de conversas no chat. O `/chat` deve exibir apenas conversas reais vindas do backend/WhatsApp.
|
||||||
|
- Toda alteração de banco deve virar nova migration numerada.
|
||||||
|
- Não permitir resposta sem atendimento assumido.
|
||||||
|
- Não permitir transferência sem atendimento assumido.
|
||||||
|
- Conversas em `bot_triage` não devem aparecer para agentes.
|
||||||
|
- Admin pode ver todas as filas e atendimentos roteados.
|
||||||
|
- Novo atendimento ativo deve usar template aprovado.
|
||||||
|
- Depois de contato ativo, bloquear novas mensagens livres até resposta do cliente.
|
||||||
|
- Mídia sem texto não deve enviar cabeçalho solto de atendente.
|
||||||
|
|
||||||
## Observacoes
|
|
||||||
|
|
||||||
- O `docker-compose.yml` deste repo espera `frontend/` e `backend/` presentes na mesma raiz.
|
|
||||||
- Em producao, o fluxo pode mudar para imagens pre-buildadas, mas para desenvolvimento local esta estrutura funciona bem.
|
|
||||||
|
|||||||
21
database/migrations/014_whatsapp_template_workflow.sql
Normal file
21
database/migrations/014_whatsapp_template_workflow.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Migration 014: Workflow de aprovação de templates WhatsApp
|
||||||
|
-- Tabela: whatsapp_templates
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE whatsapp_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS area_id INTEGER REFERENCES areas (id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS status VARCHAR(40) NOT NULL DEFAULT 'approved',
|
||||||
|
ADD COLUMN IF NOT EXISTS requested_by_role VARCHAR(40),
|
||||||
|
ADD COLUMN IF NOT EXISTS admin_approved_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
ADD COLUMN IF NOT EXISTS meta_submitted_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
ADD COLUMN IF NOT EXISTS meta_approved_at TIMESTAMP WITH TIME ZONE;
|
||||||
|
|
||||||
|
UPDATE whatsapp_templates
|
||||||
|
SET
|
||||||
|
status = COALESCE(status, 'approved'),
|
||||||
|
meta_approved_at = COALESCE(meta_approved_at, updated_at, created_at, CURRENT_TIMESTAMP)
|
||||||
|
WHERE status = 'approved';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_whatsapp_templates_area ON whatsapp_templates (area_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_whatsapp_templates_status ON whatsapp_templates (status);
|
||||||
52
database/migrations/015_agent_presence_pause.sql
Normal file
52
database/migrations/015_agent_presence_pause.sql
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Migration 015: Presenca do agente e reserva de chamados em pausa
|
||||||
|
-- Tabelas:
|
||||||
|
-- agent_presence
|
||||||
|
-- whatsapp_chat_atribuicoes
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS agent_presence (
|
||||||
|
user_id INTEGER PRIMARY KEY REFERENCES usuarios(id) ON DELETE CASCADE,
|
||||||
|
status VARCHAR(40) NOT NULL DEFAULT 'offline',
|
||||||
|
paused_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'chk_agent_presence_status'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE agent_presence
|
||||||
|
ADD CONSTRAINT chk_agent_presence_status
|
||||||
|
CHECK (status IN ('available', 'paused', 'offline'));
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
INSERT INTO agent_presence (user_id, status, last_seen_at, updated_at)
|
||||||
|
SELECT id, 'available', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||||
|
FROM usuarios
|
||||||
|
ON CONFLICT (user_id) DO NOTHING;
|
||||||
|
|
||||||
|
ALTER TABLE whatsapp_chat_atribuicoes
|
||||||
|
ADD COLUMN IF NOT EXISTS reserved_user_id INTEGER REFERENCES usuarios(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS reserved_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
ADD COLUMN IF NOT EXISTS pause_released_at TIMESTAMP WITH TIME ZONE;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_agent_presence_status
|
||||||
|
ON agent_presence (status);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_agent_presence_paused_at
|
||||||
|
ON agent_presence (paused_at)
|
||||||
|
WHERE status = 'paused';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_reserved_user
|
||||||
|
ON whatsapp_chat_atribuicoes (reserved_user_id, status)
|
||||||
|
WHERE reserved_user_id IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_reserved_queue
|
||||||
|
ON whatsapp_chat_atribuicoes (area_id, status, reserved_user_id)
|
||||||
|
WHERE status = 'queued';
|
||||||
99
database/migrations/016_hr_decision_tree_keywords.sql
Normal file
99
database/migrations/016_hr_decision_tree_keywords.sql
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Migration 016: Arvore de decisao por especialidade para RH
|
||||||
|
-- Tabelas:
|
||||||
|
-- area_routing_keywords
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS area_routing_keywords (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
area_id INTEGER NOT NULL REFERENCES areas(id) ON DELETE CASCADE,
|
||||||
|
keyword VARCHAR(160) NOT NULL,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT uq_area_routing_keyword UNIQUE (area_id, keyword)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_area_routing_keywords_area
|
||||||
|
ON area_routing_keywords (area_id, active);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_area_routing_keywords_keyword
|
||||||
|
ON area_routing_keywords (keyword);
|
||||||
|
|
||||||
|
INSERT INTO areas (nome, descricao) VALUES
|
||||||
|
('Benefícios', 'Duvidas de RH sobre beneficios, convenios, vale transporte e vale refeicao'),
|
||||||
|
('Ponto', 'Ajustes de ponto, banco de horas, atrasos e jornada'),
|
||||||
|
('Holerite', 'Holerite, folha de pagamento, descontos e demonstrativos'),
|
||||||
|
('Férias', 'Ferias, abono, programacao e saldo de descanso'),
|
||||||
|
('Recrutamento', 'Vagas internas, candidatura, entrevista e processo seletivo')
|
||||||
|
ON CONFLICT (nome) DO UPDATE SET
|
||||||
|
descricao = EXCLUDED.descricao,
|
||||||
|
ativo = TRUE,
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
INSERT INTO area_routing_keywords (area_id, keyword)
|
||||||
|
SELECT a.id, keyword
|
||||||
|
FROM areas a
|
||||||
|
JOIN (
|
||||||
|
VALUES
|
||||||
|
('Benefícios', 'beneficio'),
|
||||||
|
('Benefícios', 'beneficios'),
|
||||||
|
('Benefícios', 'vale refeicao'),
|
||||||
|
('Benefícios', 'vale alimentacao'),
|
||||||
|
('Benefícios', 'vale transporte'),
|
||||||
|
('Benefícios', 'convenio'),
|
||||||
|
('Benefícios', 'plano de saude'),
|
||||||
|
('Benefícios', 'odonto'),
|
||||||
|
('Ponto', 'ponto'),
|
||||||
|
('Ponto', 'espelho de ponto'),
|
||||||
|
('Ponto', 'banco de horas'),
|
||||||
|
('Ponto', 'atraso'),
|
||||||
|
('Ponto', 'jornada'),
|
||||||
|
('Ponto', 'batida'),
|
||||||
|
('Ponto', 'marcacao'),
|
||||||
|
('Holerite', 'holerite'),
|
||||||
|
('Holerite', 'folha'),
|
||||||
|
('Holerite', 'pagamento'),
|
||||||
|
('Holerite', 'salario'),
|
||||||
|
('Holerite', 'desconto'),
|
||||||
|
('Holerite', 'demonstrativo'),
|
||||||
|
('Férias', 'ferias'),
|
||||||
|
('Férias', 'abono'),
|
||||||
|
('Férias', 'descanso'),
|
||||||
|
('Férias', 'saldo de ferias'),
|
||||||
|
('Férias', 'programar ferias'),
|
||||||
|
('Recrutamento', 'vaga'),
|
||||||
|
('Recrutamento', 'vagas'),
|
||||||
|
('Recrutamento', 'processo seletivo'),
|
||||||
|
('Recrutamento', 'entrevista'),
|
||||||
|
('Recrutamento', 'curriculo'),
|
||||||
|
('Recrutamento', 'candidatura')
|
||||||
|
) AS seed(area_nome, keyword) ON seed.area_nome = a.nome
|
||||||
|
ON CONFLICT (area_id, keyword) DO UPDATE SET
|
||||||
|
active = TRUE,
|
||||||
|
updated_at = CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
INSERT INTO whatsapp_templates (name, content, area_id, status, requested_by_role, admin_approved_at, meta_submitted_at, meta_approved_at, updated_at)
|
||||||
|
SELECT template.name, template.content, a.id, 'approved', 'admin', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||||
|
FROM areas a
|
||||||
|
JOIN (
|
||||||
|
VALUES
|
||||||
|
('rh_abertura_beneficios', 'Ola, {nome}. Aqui e o Atendimento Sothis RH. Recebemos sua solicitacao sobre beneficios e vamos te apoiar por aqui.'),
|
||||||
|
('rh_abertura_ponto', 'Ola, {nome}. Aqui e o Atendimento Sothis RH. Vamos te ajudar com sua solicitacao sobre ponto, jornada ou banco de horas.'),
|
||||||
|
('rh_abertura_holerite', 'Ola, {nome}. Aqui e o Atendimento Sothis RH. Vamos te ajudar com holerite, folha ou demonstrativo de pagamento.'),
|
||||||
|
('rh_abertura_ferias', 'Ola, {nome}. Aqui e o Atendimento Sothis RH. Vamos te apoiar com sua solicitacao sobre ferias.'),
|
||||||
|
('rh_abertura_recrutamento', 'Ola, {nome}. Aqui e o Atendimento Sothis RH. Vamos te orientar sobre vagas, candidatura ou processo seletivo.')
|
||||||
|
) AS template(name, content) ON template.name LIKE
|
||||||
|
CASE a.nome
|
||||||
|
WHEN 'Benefícios' THEN '%beneficios'
|
||||||
|
WHEN 'Ponto' THEN '%ponto'
|
||||||
|
WHEN 'Holerite' THEN '%holerite'
|
||||||
|
WHEN 'Férias' THEN '%ferias'
|
||||||
|
WHEN 'Recrutamento' THEN '%recrutamento'
|
||||||
|
END
|
||||||
|
ON CONFLICT (name) DO UPDATE SET
|
||||||
|
content = EXCLUDED.content,
|
||||||
|
area_id = EXCLUDED.area_id,
|
||||||
|
status = 'approved',
|
||||||
|
meta_approved_at = CURRENT_TIMESTAMP,
|
||||||
|
updated_at = CURRENT_TIMESTAMP;
|
||||||
145
database/migrations/017_configurable_triage_flow.sql
Normal file
145
database/migrations/017_configurable_triage_flow.sql
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Migration 017: Fluxo configuravel de triagem do Agente Virtual Sothis
|
||||||
|
-- Tabelas:
|
||||||
|
-- bot_triage_flows
|
||||||
|
-- bot_triage_audiences
|
||||||
|
-- bot_triage_intents
|
||||||
|
-- whatsapp_chat_atribuicoes
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS bot_triage_flows (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(160) NOT NULL UNIQUE,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
greeting_message TEXT NOT NULL,
|
||||||
|
audience_question TEXT NOT NULL,
|
||||||
|
intent_question_template TEXT NOT NULL,
|
||||||
|
fallback_message TEXT NOT NULL,
|
||||||
|
fallback_area_id INTEGER REFERENCES areas(id) ON DELETE SET NULL,
|
||||||
|
max_attempts INTEGER NOT NULL DEFAULT 2,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS bot_triage_audiences (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
flow_id INTEGER NOT NULL REFERENCES bot_triage_flows(id) ON DELETE CASCADE,
|
||||||
|
label VARCHAR(160) NOT NULL,
|
||||||
|
keywords TEXT,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 1,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS bot_triage_intents (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
audience_id INTEGER NOT NULL REFERENCES bot_triage_audiences(id) ON DELETE CASCADE,
|
||||||
|
label VARCHAR(160) NOT NULL,
|
||||||
|
area_id INTEGER NOT NULL REFERENCES areas(id) ON DELETE CASCADE,
|
||||||
|
keywords TEXT,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 1,
|
||||||
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE whatsapp_chat_atribuicoes
|
||||||
|
ADD COLUMN IF NOT EXISTS triage_flow_id INTEGER REFERENCES bot_triage_flows(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS triage_audience_id INTEGER REFERENCES bot_triage_audiences(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS triage_step VARCHAR(40);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bot_triage_flows_active
|
||||||
|
ON bot_triage_flows (active);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bot_triage_audiences_flow
|
||||||
|
ON bot_triage_audiences (flow_id, active, sort_order);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bot_triage_intents_audience
|
||||||
|
ON bot_triage_intents (audience_id, active, sort_order);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_triage_flow
|
||||||
|
ON whatsapp_chat_atribuicoes (triage_flow_id, triage_audience_id, triage_step);
|
||||||
|
|
||||||
|
INSERT INTO areas (nome, descricao) VALUES
|
||||||
|
('Documentos RH', 'Documentos para ex-colaboradores, informe de rendimentos e comprovantes'),
|
||||||
|
('Rescisao', 'Rescisao, FGTS, verbas rescisorias e encerramento de contrato')
|
||||||
|
ON CONFLICT (nome) DO UPDATE SET
|
||||||
|
descricao = EXCLUDED.descricao,
|
||||||
|
ativo = TRUE,
|
||||||
|
updated_at = NOW();
|
||||||
|
|
||||||
|
INSERT INTO bot_triage_flows (
|
||||||
|
name,
|
||||||
|
active,
|
||||||
|
greeting_message,
|
||||||
|
audience_question,
|
||||||
|
intent_question_template,
|
||||||
|
fallback_message,
|
||||||
|
fallback_area_id,
|
||||||
|
max_attempts,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'RH CAOA - Atendimento Sothis',
|
||||||
|
TRUE,
|
||||||
|
'Ola! Sou o Agente Virtual Sothis. Vou te direcionar para o atendimento correto de RH.',
|
||||||
|
'Digite o numero da opcao que melhor descreve voce:',
|
||||||
|
'Perfeito. Agora digite o numero do assunto que voce precisa:',
|
||||||
|
'Nao consegui identificar com seguranca. Vou encaminhar seu atendimento para o suporte de RH.',
|
||||||
|
a.id,
|
||||||
|
2,
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
FROM areas a
|
||||||
|
WHERE a.nome = 'Suporte'
|
||||||
|
ON CONFLICT (name) DO UPDATE SET
|
||||||
|
active = TRUE,
|
||||||
|
greeting_message = EXCLUDED.greeting_message,
|
||||||
|
audience_question = EXCLUDED.audience_question,
|
||||||
|
intent_question_template = EXCLUDED.intent_question_template,
|
||||||
|
fallback_message = EXCLUDED.fallback_message,
|
||||||
|
fallback_area_id = EXCLUDED.fallback_area_id,
|
||||||
|
max_attempts = EXCLUDED.max_attempts,
|
||||||
|
updated_at = CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
WITH flow AS (
|
||||||
|
SELECT id FROM bot_triage_flows WHERE name = 'RH CAOA - Atendimento Sothis'
|
||||||
|
)
|
||||||
|
INSERT INTO bot_triage_audiences (flow_id, label, keywords, sort_order, active, updated_at)
|
||||||
|
SELECT flow.id, seed.label, seed.keywords, seed.sort_order, TRUE, CURRENT_TIMESTAMP
|
||||||
|
FROM flow
|
||||||
|
JOIN (
|
||||||
|
VALUES
|
||||||
|
('Sou colaborador ativo', 'colaborador, funcionario, matricula, holerite, ferias, ponto, beneficios', 1),
|
||||||
|
('Sou ex-colaborador', 'ex colaborador, ex-colaborador, rescisao, fgts, informe de rendimentos, desligamento', 2),
|
||||||
|
('Sou candidato a uma vaga', 'candidato, vaga, curriculo, processo seletivo, entrevista, candidatura', 3)
|
||||||
|
) AS seed(label, keywords, sort_order) ON TRUE
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
WITH audiences AS (
|
||||||
|
SELECT bta.id, bta.label
|
||||||
|
FROM bot_triage_audiences bta
|
||||||
|
INNER JOIN bot_triage_flows btf ON btf.id = bta.flow_id
|
||||||
|
WHERE btf.name = 'RH CAOA - Atendimento Sothis'
|
||||||
|
),
|
||||||
|
seed AS (
|
||||||
|
SELECT * FROM (
|
||||||
|
VALUES
|
||||||
|
('Sou colaborador ativo', 'Beneficios', 'beneficios, vale refeicao, vale transporte, convenio, plano de saude', 'Benefícios', 1),
|
||||||
|
('Sou colaborador ativo', 'Holerite', 'holerite, folha, salario, pagamento, desconto', 'Holerite', 2),
|
||||||
|
('Sou colaborador ativo', 'Ferias', 'ferias, abono, descanso, saldo de ferias', 'Férias', 3),
|
||||||
|
('Sou colaborador ativo', 'Ponto', 'ponto, espelho de ponto, banco de horas, jornada, batida', 'Ponto', 4),
|
||||||
|
('Sou ex-colaborador', 'Documentos', 'documento, documentos, comprovante, informe de rendimentos', 'Documentos RH', 1),
|
||||||
|
('Sou ex-colaborador', 'FGTS e rescisao', 'fgts, rescisao, verbas rescisorias, desligamento', 'Rescisao', 2),
|
||||||
|
('Sou ex-colaborador', 'Informe de rendimentos', 'informe, rendimentos, imposto de renda, ir', 'Documentos RH', 3),
|
||||||
|
('Sou candidato a uma vaga', 'Status da vaga', 'status, vaga, retorno, resultado', 'Recrutamento', 1),
|
||||||
|
('Sou candidato a uma vaga', 'Reagendar entrevista', 'reagendar, entrevista, agenda, horario', 'Recrutamento', 2),
|
||||||
|
('Sou candidato a uma vaga', 'Nova candidatura', 'nova candidatura, curriculo, candidatar, oportunidade', 'Recrutamento', 3)
|
||||||
|
) AS rows(audience_label, label, keywords, area_nome, sort_order)
|
||||||
|
)
|
||||||
|
INSERT INTO bot_triage_intents (audience_id, label, area_id, keywords, sort_order, active, updated_at)
|
||||||
|
SELECT audiences.id, seed.label, areas.id, seed.keywords, seed.sort_order, TRUE, CURRENT_TIMESTAMP
|
||||||
|
FROM seed
|
||||||
|
INNER JOIN audiences ON audiences.label = seed.audience_label
|
||||||
|
INNER JOIN areas ON areas.nome = seed.area_nome
|
||||||
|
ON CONFLICT DO NOTHING;
|
||||||
56
database/migrations/018_triage_resolution_step.sql
Normal file
56
database/migrations/018_triage_resolution_step.sql
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Migration 018: Etapa de resposta antes da escalacao para fila
|
||||||
|
-- Tabelas:
|
||||||
|
-- bot_triage_flows
|
||||||
|
-- bot_triage_intents
|
||||||
|
-- whatsapp_chat_atribuicoes
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE bot_triage_flows
|
||||||
|
ADD COLUMN IF NOT EXISTS resolution_question TEXT NOT NULL DEFAULT 'Essa informacao resolveu sua duvida? Responda 1 para encerrar ou 2 para falar com um especialista.';
|
||||||
|
|
||||||
|
ALTER TABLE bot_triage_intents
|
||||||
|
ADD COLUMN IF NOT EXISTS response_message TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS resolution_question TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS escalation_message TEXT NOT NULL DEFAULT 'Certo, vou encaminhar seu atendimento para um especialista no assunto.';
|
||||||
|
|
||||||
|
ALTER TABLE whatsapp_chat_atribuicoes
|
||||||
|
ADD COLUMN IF NOT EXISTS triage_intent_id INTEGER REFERENCES bot_triage_intents(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_triage_intent
|
||||||
|
ON whatsapp_chat_atribuicoes (triage_intent_id, triage_step);
|
||||||
|
|
||||||
|
UPDATE bot_triage_intents
|
||||||
|
SET response_message = CASE label
|
||||||
|
WHEN 'Beneficios' THEN 'Para consultar beneficios, verifique no portal de RH a area Beneficios. La voce encontra vale refeicao, vale transporte, convenio medico, odontologico e regras de elegibilidade.'
|
||||||
|
WHEN 'Holerite' THEN 'Seu holerite fica disponivel no portal de RH na area Folha de pagamento. Normalmente ele pode ser consultado por competencia, com detalhamento de salario, descontos e beneficios.'
|
||||||
|
WHEN 'Ferias' THEN 'Para ferias, consulte saldo e periodos disponiveis no portal de RH. A solicitacao precisa respeitar a politica interna e o fluxo de aprovacao do gestor.'
|
||||||
|
WHEN 'Ponto' THEN 'Para ponto, confira o espelho de ponto e solicite correcao de batida quando necessario. Ajustes de jornada e banco de horas seguem aprovacao do gestor.'
|
||||||
|
WHEN 'Documentos' THEN 'Documentos de ex-colaborador podem ser solicitados pelo canal de RH. Informe o documento desejado e mantenha seus dados de contato atualizados.'
|
||||||
|
WHEN 'FGTS e rescisao' THEN 'Para FGTS e rescisao, o time de RH valida o status do desligamento, prazos legais e documentos pendentes antes de retornar.'
|
||||||
|
WHEN 'Informe de rendimentos' THEN 'O informe de rendimentos e disponibilizado conforme calendario fiscal. Caso nao encontre, o RH pode apoiar com a segunda via.'
|
||||||
|
WHEN 'Status da vaga' THEN 'Para status de vaga, acompanhe o processo seletivo pelo canal informado na candidatura. O RH pode consultar a etapa atual quando houver identificacao do candidato.'
|
||||||
|
WHEN 'Reagendar entrevista' THEN 'Para reagendar entrevista, informe nome completo, vaga e melhor disponibilidade. O time de recrutamento avalia a agenda.'
|
||||||
|
WHEN 'Nova candidatura' THEN 'Para nova candidatura, acesse o portal de vagas e mantenha seu curriculo atualizado. O RH pode orientar sobre oportunidades abertas.'
|
||||||
|
ELSE COALESCE(response_message, 'Tenho uma orientacao inicial para esse assunto. Se ainda precisar de ajuda, posso encaminhar para um especialista.')
|
||||||
|
END
|
||||||
|
WHERE response_message IS NULL;
|
||||||
|
|
||||||
|
UPDATE bot_triage_intents
|
||||||
|
SET resolution_question = 'Isso resolveu sua duvida? Responda 1 para encerrar ou 2 para falar com um especialista de RH.'
|
||||||
|
WHERE resolution_question IS NULL;
|
||||||
|
|
||||||
|
UPDATE bot_triage_intents
|
||||||
|
SET escalation_message = CASE label
|
||||||
|
WHEN 'Beneficios' THEN 'Certo, vou te encaminhar para um especialista em beneficios.'
|
||||||
|
WHEN 'Holerite' THEN 'Certo, vou te encaminhar para um especialista em holerite e folha.'
|
||||||
|
WHEN 'Ferias' THEN 'Certo, vou te encaminhar para um especialista em ferias.'
|
||||||
|
WHEN 'Ponto' THEN 'Certo, vou te encaminhar para um especialista em ponto.'
|
||||||
|
WHEN 'Documentos' THEN 'Certo, vou te encaminhar para um especialista em documentos de RH.'
|
||||||
|
WHEN 'FGTS e rescisao' THEN 'Certo, vou te encaminhar para um especialista em rescisao.'
|
||||||
|
WHEN 'Informe de rendimentos' THEN 'Certo, vou te encaminhar para um especialista em documentos de RH.'
|
||||||
|
WHEN 'Status da vaga' THEN 'Certo, vou te encaminhar para o time de recrutamento.'
|
||||||
|
WHEN 'Reagendar entrevista' THEN 'Certo, vou te encaminhar para o time de recrutamento.'
|
||||||
|
WHEN 'Nova candidatura' THEN 'Certo, vou te encaminhar para o time de recrutamento.'
|
||||||
|
ELSE escalation_message
|
||||||
|
END;
|
||||||
127
database/migrations/019_bot_flow_builder.sql
Normal file
127
database/migrations/019_bot_flow_builder.sql
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Migration 019: Flow Builder visual do bot
|
||||||
|
-- Tabelas:
|
||||||
|
-- bot_flow_versions
|
||||||
|
-- bot_flow_nodes
|
||||||
|
-- whatsapp_chat_atribuicoes
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS bot_flow_versions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(160) NOT NULL DEFAULT 'Fluxo RH Sothis',
|
||||||
|
status VARCHAR(30) NOT NULL DEFAULT 'draft',
|
||||||
|
version_number INTEGER NOT NULL DEFAULT 0,
|
||||||
|
root_node_id INTEGER,
|
||||||
|
published_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
snapshot JSONB,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS bot_flow_nodes (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
version_id INTEGER NOT NULL REFERENCES bot_flow_versions(id) ON DELETE CASCADE,
|
||||||
|
parent_id INTEGER REFERENCES bot_flow_nodes(id) ON DELETE CASCADE,
|
||||||
|
node_type VARCHAR(30) NOT NULL CHECK (node_type IN ('greeting', 'question', 'agent', 'close')),
|
||||||
|
title VARCHAR(160) NOT NULL,
|
||||||
|
message_text TEXT,
|
||||||
|
keywords TEXT,
|
||||||
|
fallback_message TEXT,
|
||||||
|
fallback_attempts INTEGER NOT NULL DEFAULT 2,
|
||||||
|
fallback_area_id INTEGER REFERENCES areas(id) ON DELETE SET NULL,
|
||||||
|
area_id INTEGER REFERENCES areas(id) ON DELETE SET NULL,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 1,
|
||||||
|
position_x INTEGER NOT NULL DEFAULT 0,
|
||||||
|
position_y INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'fk_bot_flow_versions_root'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE bot_flow_versions
|
||||||
|
ADD CONSTRAINT fk_bot_flow_versions_root
|
||||||
|
FOREIGN KEY (root_node_id) REFERENCES bot_flow_nodes(id) ON DELETE SET NULL;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE whatsapp_chat_atribuicoes
|
||||||
|
ADD COLUMN IF NOT EXISTS triage_builder_version_id INTEGER REFERENCES bot_flow_versions(id) ON DELETE SET NULL,
|
||||||
|
ADD COLUMN IF NOT EXISTS triage_builder_node_id INTEGER REFERENCES bot_flow_nodes(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bot_flow_versions_status
|
||||||
|
ON bot_flow_versions (status, published_at DESC, id DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_bot_flow_nodes_version_parent
|
||||||
|
ON bot_flow_nodes (version_id, parent_id, sort_order, id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_builder_node
|
||||||
|
ON whatsapp_chat_atribuicoes (triage_builder_version_id, triage_builder_node_id);
|
||||||
|
|
||||||
|
WITH inserted_version AS (
|
||||||
|
INSERT INTO bot_flow_versions (name, status, version_number, updated_at)
|
||||||
|
SELECT 'Fluxo RH Sothis', 'draft', 0, CURRENT_TIMESTAMP
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM bot_flow_versions WHERE status = 'draft')
|
||||||
|
RETURNING id
|
||||||
|
),
|
||||||
|
draft_version AS (
|
||||||
|
SELECT id FROM inserted_version
|
||||||
|
UNION ALL
|
||||||
|
SELECT id
|
||||||
|
FROM (
|
||||||
|
SELECT id
|
||||||
|
FROM bot_flow_versions
|
||||||
|
WHERE status = 'draft'
|
||||||
|
AND NOT EXISTS (SELECT 1 FROM inserted_version)
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1
|
||||||
|
) existing_draft
|
||||||
|
),
|
||||||
|
inserted_root AS (
|
||||||
|
INSERT INTO bot_flow_nodes (
|
||||||
|
version_id,
|
||||||
|
parent_id,
|
||||||
|
node_type,
|
||||||
|
title,
|
||||||
|
message_text,
|
||||||
|
keywords,
|
||||||
|
fallback_message,
|
||||||
|
fallback_attempts,
|
||||||
|
sort_order,
|
||||||
|
position_x,
|
||||||
|
position_y,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
draft_version.id,
|
||||||
|
NULL,
|
||||||
|
'greeting',
|
||||||
|
'Saudacao inicial',
|
||||||
|
'Ola! Sou o Agente Virtual Sothis. Vou te direcionar para o atendimento correto de RH. Digite o numero da opcao que melhor descreve voce:\n\n1 - Sou colaborador ativo\n2 - Sou ex-colaborador\n3 - Sou candidato a uma vaga',
|
||||||
|
NULL,
|
||||||
|
'Nao consegui identificar seu perfil. Digite 1 para colaborador ativo, 2 para ex-colaborador ou 3 para candidato.',
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
FROM draft_version
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM bot_flow_nodes node
|
||||||
|
WHERE node.version_id = draft_version.id
|
||||||
|
AND node.node_type = 'greeting'
|
||||||
|
)
|
||||||
|
RETURNING id, version_id
|
||||||
|
)
|
||||||
|
UPDATE bot_flow_versions version
|
||||||
|
SET root_node_id = inserted_root.id,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
FROM inserted_root
|
||||||
|
WHERE version.id = inserted_root.version_id
|
||||||
|
AND version.root_node_id IS NULL;
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Migration 020: No terminal de encerramento pelo bot
|
||||||
|
-- Tabelas:
|
||||||
|
-- bot_flow_nodes
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE bot_flow_nodes
|
||||||
|
DROP CONSTRAINT IF EXISTS bot_flow_nodes_node_type_check;
|
||||||
|
|
||||||
|
ALTER TABLE bot_flow_nodes
|
||||||
|
ADD CONSTRAINT bot_flow_nodes_node_type_check
|
||||||
|
CHECK (node_type IN ('greeting', 'question', 'agent', 'close'));
|
||||||
41
database/migrations/021_admin_audit_ai_contents.sql
Normal file
41
database/migrations/021_admin_audit_ai_contents.sql
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Migration 021: Auditoria e conteúdos da IA
|
||||||
|
-- Tabelas:
|
||||||
|
-- admin_audit_logs
|
||||||
|
-- ai_knowledge_contents
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS admin_audit_logs (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
actor_user_id INTEGER REFERENCES usuarios(id) ON DELETE SET NULL,
|
||||||
|
actor_name VARCHAR(180),
|
||||||
|
action VARCHAR(120) NOT NULL,
|
||||||
|
target_type VARCHAR(80),
|
||||||
|
target_id VARCHAR(120),
|
||||||
|
details TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_admin_audit_logs_created_at
|
||||||
|
ON admin_audit_logs (created_at DESC, id DESC);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ai_knowledge_contents (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(220) NOT NULL,
|
||||||
|
area_id INTEGER REFERENCES areas(id) ON DELETE SET NULL,
|
||||||
|
filename VARCHAR(260),
|
||||||
|
mimetype VARCHAR(160),
|
||||||
|
file_size INTEGER,
|
||||||
|
content_base64 TEXT,
|
||||||
|
status VARCHAR(40) NOT NULL DEFAULT 'available',
|
||||||
|
notes TEXT,
|
||||||
|
created_by_user_id INTEGER REFERENCES usuarios(id) ON DELETE SET NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ai_knowledge_contents_area
|
||||||
|
ON ai_knowledge_contents (area_id, status);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_ai_knowledge_contents_created_at
|
||||||
|
ON ai_knowledge_contents (created_at DESC, id DESC);
|
||||||
14
database/migrations/022_whatsapp_template_category.sql
Normal file
14
database/migrations/022_whatsapp_template_category.sql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- Migration 022: Categoria de templates WhatsApp
|
||||||
|
-- Tabela: whatsapp_templates
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE whatsapp_templates
|
||||||
|
ADD COLUMN IF NOT EXISTS category VARCHAR(40) NOT NULL DEFAULT 'UTILITY';
|
||||||
|
|
||||||
|
UPDATE whatsapp_templates
|
||||||
|
SET category = 'UTILITY'
|
||||||
|
WHERE category IS NULL OR category = '';
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_whatsapp_templates_category
|
||||||
|
ON whatsapp_templates (category);
|
||||||
Loading…
Reference in New Issue
Block a user