Compare commits

..

No commits in common. "e9e214195af733498617371aa5cffbe2f0142d1e" and "a0e9fc120f48ad29e89b750dada1fded97a33818" have entirely different histories.

10 changed files with 16 additions and 1099 deletions

548
README.md
View File

@ -1,545 +1,29 @@
# Omnichannel Sothis
# Omnichannel Sothis - Deploy
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.
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.
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.
## Estrutura esperada (3 repositorios)
## Estado Atual do Produto
- `deploy/` (este repo): `docker-compose.yml`, `database/`, `.gitignore`, `README.md`
- `frontend/`: interface do produto
- `backend/`: API e regras de negocio
O produto hoje permite:
## Como subir tudo localmente
- 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.
Passo a passo (na mesma pasta raiz):
## Perfis e Acesso
### Usuário sem perfil
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:
1. Clonar o repo de deploy na raiz (.)
2. Clonar o repo de frontend na pasta `frontend`
3. Clonar o repo de backend na pasta `backend`
4. Subir tudo:
```bash
docker compose up -d --build
```
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.
Isso sobe `frontend`, `backend` e `database` em uma unica operacao.
## 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.

View File

@ -1,21 +0,0 @@
-- ============================================================
-- 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);

View File

@ -1,52 +0,0 @@
-- ============================================================
-- 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';

View File

@ -1,99 +0,0 @@
-- ============================================================
-- 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;

View File

@ -1,145 +0,0 @@
-- ============================================================
-- 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;

View File

@ -1,56 +0,0 @@
-- ============================================================
-- 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;

View File

@ -1,127 +0,0 @@
-- ============================================================
-- 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;

View File

@ -1,12 +0,0 @@
-- ============================================================
-- 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'));

View File

@ -1,41 +0,0 @@
-- ============================================================
-- 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);

View File

@ -1,14 +0,0 @@
-- ============================================================
-- 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);