Merge pull request 'Adição de migrations' (#1) from dev into master

Reviewed-on: https://chaleiradev.sothistelecom.com/Sothis/omnichannel-deploy/pulls/1
This commit is contained in:
Rafael Alves Lopes 2026-05-27 16:06:43 -03:00
commit f4e6dc9fb0
30 changed files with 1190 additions and 53 deletions

44
.env.example Normal file
View File

@ -0,0 +1,44 @@
# Deploy (docker-compose) environment variables
#
# Docker Compose sobe somente frontend e backend.
# O PostgreSQL deve existir fora do compose, em uma instancia local, VM, RDS,
# container separado ou banco corporativo.
# App database connection (used by backend)
DB_HOST=db.empresa.local
DB_PORT=5432
DB_USER=omnichannel
DB_PASSWORD=change-me
DB_NAME=omnichannel
# Backend HTTP/JWT
NODE_ENV=development
PORT=3001
FRONTEND_URL=http://localhost:4000
JWT_SECRET=change-this-long-random-secret
JWT_EXPIRES_IN=8h
REQUEST_BODY_LIMIT=25mb
# Auth providers: ldap,microsoft or only one of them
AUTH_PROVIDERS=ldap,microsoft
# LDAP / Active Directory
LDAP_ENABLED=true
LDAP_URL=ldaps://servidor-ad:636
LDAP_DOMAIN=empresa.com.br
LDAP_USER_DN_TEMPLATE={{username}}@empresa.com.br
LDAP_SEARCH_BASE=DC=empresa,DC=com
LDAP_SEARCH_FILTER=(&(objectClass=user)(sAMAccountName={{username}}))
LDAP_TIMEOUT_MS=5000
# Optional LDAP bind account when search requires service credentials
# LDAP_BIND_DN=CN=ldap-reader,OU=Users,DC=empresa,DC=com
# LDAP_BIND_PASSWORD=change-me
# Microsoft Entra ID OAuth
MICROSOFT_ENABLED=false
MICROSOFT_TENANT_ID=common
MICROSOFT_CLIENT_ID=
MICROSOFT_CLIENT_SECRET=
MICROSOFT_REDIRECT_URI=http://localhost:4001/auth/oauth/microsoft/callback
MICROSOFT_SUCCESS_REDIRECT_URL=http://localhost:4000/login

16
.gitignore vendored
View File

@ -1,5 +1,17 @@
node_modules
dist
frontend/node_modules
frontend/dist
.env*
*.log
.DS_Store
# Backend Specific Ignore
backend/node_modules
backend/dist
backend/whatsapp-session
backend/whatsapp-chats-persist.json
backend/all-chats-dump.json
backend/test-api-out.json
# Frontend Specific Ignore
frontend/node_modules
frontend/dist

182
README.md
View File

@ -1,49 +1,107 @@
# Omnichannel Sothis
Protótipo visual do frontend MVP do sistema Omnichannel da Sothis.
Plataforma omnichannel para atendimento com foco inicial em WhatsApp. O sistema combina atendimento em tempo real, Agente Virtual para triagem, filas por especialidade, abertura ativa por template, agenda de contatos, painéis operacionais e administração de usuários/perfis.
O foco desta versão é apresentação de produto: a aplicação simula fluxos reais de atendimento com dados mockados, UX moderna e navegação entre telas principais.
O projeto foi construído para validar e evoluir um MVP de atendimento corporativo, com perfis de agente, supervisor e administrador.
## O que existe hoje
## Principais Recursos
- Frontend em React + Vite dentro de `frontend/`
- Docker Compose na raiz para subir o frontend desta apresentação
- Telas implementadas:
- Login
- Home / Dashboard
- Chat
- Call / Softphone mock
- Novo Atendimento
- Login corporativo via LDAP/Active Directory.
- Estrutura para Microsoft OAuth / Entra ID.
- JWT próprio da aplicação com perfis e especialidades.
- Atendimento WhatsApp em tempo real via `whatsapp-web.js`.
- Socket.IO para atualização de chats/mensagens.
- Agente Virtual Sothis para triagem e roteamento.
- Fila por especialidade.
- Assumir, liberar, transferir e fechar atendimento.
- Abertura ativa com templates aprovados.
- Agenda de contatos com WhatsApp, telefone/SMS, email, etiqueta e observação.
- Painel do agente.
- Painel operacional do supervisor.
- Painel administrativo com usuários, acessos, templates, IA, canais e configurações.
- Conteúdos da IA e regras/travas.
- Migrations SQL versionadas.
## Estrutura esperada do ecossistema
## Stack Técnica
Hoje este repositório cobre o frontend e um `docker-compose.yml` local para desenvolvimento/apresentação.
### Backend
Para rodar o ambiente completo no futuro, a separação esperada é:
- Node.js 20+ recomendado.
- NestJS `^11.1`.
- TypeScript `^6.0`.
- PostgreSQL via `pg`.
- Socket.IO `^4.8`.
- `whatsapp-web.js` `^1.34`.
- LDAP via `ldapts`.
- JWT via `jsonwebtoken`.
- Logs com `winston`.
- `frontend`: interface do produto
- `backend`: API e regras de negócio
- `deploy`: repositório raiz de infraestrutura/orquestração, onde ficará o `docker-compose` final com frontend, backend, banco e demais serviços
### Frontend
## Como rodar somente o frontend
- React `^18.3`.
- Vite `^5.4`.
- React Router `^6.30`.
- Socket.IO Client `^4.8`.
### Opção 1: com Docker
### Banco
Na raiz deste projeto:
- PostgreSQL 16 recomendado.
- O banco não é gerenciado pelo `docker-compose.yml` deste repositório.
- As migrations ficam em `database/migrations`.
### Docker
O Docker Compose da raiz sobe somente:
- `backend`
- `frontend`
O banco deve ser externo ao compose: VM, banco corporativo, RDS, container separado ou PostgreSQL local gerenciado fora deste projeto.
## Estrutura do Repositório
```txt
omnichannel/
├── backend/ # API NestJS e regras de negócio
├── frontend/ # Interface React/Vite
├── database/migrations/ # Migrations SQL
├── docs/ # Wiki operacional e arquitetura
├── docker-compose.yml # Sobe backend e frontend
└── README.md
```
## Como Subir com Docker Compose
1. Configure `.env.development` na raiz com os dados do banco externo.
2. Garanta que o PostgreSQL externo esteja acessível a partir do container backend.
3. Suba backend e frontend:
```bash
docker compose up -d --build
```
Depois acesse:
URLs padrão:
```text
http://localhost:3000
- Frontend: `http://localhost:4000`
- Backend: `http://localhost:4001`
Health:
```bash
curl http://localhost:4001/health
```
### Opção 2: com Node local
## Como Rodar em Desenvolvimento
Entre na pasta do frontend:
Backend:
```bash
cd backend
npm install
npm run dev
```
Frontend:
```bash
cd frontend
@ -51,39 +109,63 @@ npm install
npm run dev
```
Depois acesse:
URLs comuns:
```text
http://localhost:3000
- Frontend Vite: `http://localhost:5173`
- Backend: `http://localhost:3001`
## Banco e Migrations
As migrations SQL estão em:
```txt
database/migrations
```
## Como gerar build do frontend
Elas representam a intenção de schema final/evolutivo do produto, mas o projeto ainda precisa de um runner formal para aplicar tudo em ordem em ambientes novos.
```bash
cd frontend
npm run build
```
Para ambiente novo, antes de subir backend para uso real:
## Para rodar o ambiente completo
1. criar o banco PostgreSQL;
2. aplicar as migrations em ordem;
3. validar tabelas principais;
4. criar/atribuir usuário admin;
5. subir backend e frontend.
Quando a solução estiver separada em múltiplos repositórios, o fluxo esperado será:
Detalhes em:
1. Fazer `pull` do repositório `frontend`
2. Fazer `pull` do repositório `backend`
3. Fazer `pull` do repositório `deploy`
4. Entrar no repositório `deploy` (raiz de infraestrutura)
5. Subir tudo com:
```bash
docker compose up -d --build
```
Em outras palavras: o `docker compose` definitivo do ambiente completo deve ser executado a partir do repositório `deploy`, que será a raiz de orquestração.
- [Deploy e operação](./docs/deploy.md)
- [Database](./backend/docs/database.md)
## Documentação
A documentação funcional do frontend está em [`frontend/docs`](./frontend/docs):
Wiki raiz:
- visão geral do projeto
- documentação por módulo/tela
- documentação em formato narrativo/RPG para explicar os casos de uso
- [docs/README.md](./docs/README.md)
- [Deploy e operação](./docs/deploy.md)
- [Arquitetura geral](./docs/arquitetura.md)
- [Fluxos end-to-end](./docs/fluxos-end-to-end.md)
- [Regras de negócio](./docs/regras-negocio.md)
- [ADRs](./docs/adrs.md)
- [Ambientes](./docs/ambientes.md)
- [Runbook](./docs/runbook.md)
Backend:
- [backend/docs/README.md](./backend/docs/README.md)
- [Auth](./backend/docs/auth.md)
- [WhatsApp](./backend/docs/whatsapp.md)
- [Admin](./backend/docs/admin.md)
- [Swagger/OpenAPI](./backend/docs/swagger.md)
## Estado Atual e Próximos Passos
O produto já foi validado em demo com cliente final. Antes de produção real, os principais fechamentos são:
- implementar guards JWT no backend;
- validar autorização por perfil no backend;
- formalizar runner de migrations;
- configurar backup/restore do banco externo;
- persistir sessão WhatsApp em volume;
- criar Swagger com DTOs;
- adicionar testes nos fluxos críticos.

1
backend Submodule

@ -0,0 +1 @@
Subproject commit 8790ce70d05d0256ded89ea8fb9335afad41bfa8

0
database/Dockerfile Normal file
View File

View File

@ -0,0 +1,112 @@
-- ============================================================
-- Migration 001: Módulo de Autenticação
-- Tabelas: usuários, provedores, perfis de acesso e auditoria
-- ============================================================
-- ------------------------------------------------------------
-- Tabela: usuarios
-- Representa qualquer pessoa que acessa o sistema,
-- independente de como ela se autenticou
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS usuarios (
id SERIAL PRIMARY KEY,
nome VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE, -- pode ser nulo em contas só com LDAP sem email
ativo BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_usuarios_email ON usuarios (email);
-- ------------------------------------------------------------
-- Tabela: usuarios_provedores
-- Vincula um usuário a um ou mais provedores de autenticação
-- Um mesmo usuário pode logar via LDAP e via Microsoft
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS usuarios_provedores (
id SERIAL PRIMARY KEY,
-- FK para o usuário correspondente
usuario_id INTEGER NOT NULL REFERENCES usuarios (id) ON DELETE CASCADE,
-- Provedor de autenticação: 'ldap' | 'microsoft' | 'google' | etc.
provedor VARCHAR(50) NOT NULL,
-- ID do usuário dentro do provedor (azure_id, username do AD, sub do Google...)
provedor_user_id VARCHAR(255) NOT NULL,
-- Evita duplicidade do mesmo provedor pro mesmo usuário
CONSTRAINT uq_provedor_user UNIQUE (provedor, provedor_user_id),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_usuarios_provedores_usuario ON usuarios_provedores (usuario_id);
CREATE INDEX IF NOT EXISTS idx_usuarios_provedores_lookup ON usuarios_provedores (provedor, provedor_user_id);
-- ------------------------------------------------------------
-- Tabela: perfis_acesso
-- Define os papéis disponíveis no sistema
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS perfis_acesso (
id SERIAL PRIMARY KEY,
nome VARCHAR(100) NOT NULL UNIQUE,
descricao TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- ------------------------------------------------------------
-- Tabela: usuarios_perfis
-- Relacionamento entre usuários e perfis (muitos-para-muitos)
-- Um usuário pode ter mais de um perfil se necessário
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS usuarios_perfis (
id SERIAL PRIMARY KEY,
usuario_id INTEGER NOT NULL REFERENCES usuarios (id) ON DELETE CASCADE,
perfil_id INTEGER NOT NULL REFERENCES perfis_acesso (id) ON DELETE CASCADE,
CONSTRAINT uq_usuario_perfil UNIQUE (usuario_id, perfil_id),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_usuarios_perfis_usuario ON usuarios_perfis (usuario_id);
CREATE INDEX IF NOT EXISTS idx_usuarios_perfis_perfil ON usuarios_perfis (perfil_id);
-- ------------------------------------------------------------
-- Tabela: logs_auditoria
-- Registra ações relevantes feitas por usuários ou pelo sistema
-- usuario_id NULL = ação do sistema (ex: tentativa de login falha)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS logs_auditoria (
id SERIAL PRIMARY KEY,
usuario_id INTEGER REFERENCES usuarios (id) ON DELETE SET NULL,
-- Ação realizada — ex: 'LOGIN_LDAP', 'LOGIN_MICROSOFT', 'LOGIN_FALHOU', 'USUARIO_CRIADO'
acao VARCHAR(100) NOT NULL,
-- Dados extras livres — ex: { "ip": "...", "provedor": "microsoft", "motivo": "..." }
detalhes JSONB,
ip_origem VARCHAR(45),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_logs_usuario ON logs_auditoria (usuario_id);
CREATE INDEX IF NOT EXISTS idx_logs_acao ON logs_auditoria (acao);
CREATE INDEX IF NOT EXISTS idx_logs_created_at ON logs_auditoria (created_at);
-- ------------------------------------------------------------
-- Dados iniciais: perfis de acesso
-- ON CONFLICT garante que pode rodar mais de uma vez sem erro
-- ------------------------------------------------------------
INSERT INTO perfis_acesso (nome, descricao) VALUES
('Agente', 'Atendente responsável por responder e encaminhar chamados'),
('Supervisor', 'Gestor com visibilidade de filas e relatórios'),
('Admin', 'Administrador com acesso total ao sistema')
ON CONFLICT (nome) DO NOTHING;

View File

@ -0,0 +1,63 @@
-- ============================================================
-- Migration 002: Modulo de Areas
-- Tabelas: areas e relacionamento usuarios_areas
-- ============================================================
-- ------------------------------------------------------------
-- Tabela: areas
-- Representa as areas operacionais do atendimento
-- Ex: Suporte, Financeiro, Comercial
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS areas (
id SERIAL PRIMARY KEY,
nome VARCHAR(120) NOT NULL UNIQUE,
descricao TEXT,
responsavel_usuario_id INTEGER REFERENCES usuarios (id) ON DELETE SET NULL,
ativo BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_areas_nome ON areas (nome);
CREATE INDEX IF NOT EXISTS idx_areas_responsavel ON areas (responsavel_usuario_id);
CREATE INDEX IF NOT EXISTS idx_areas_ativo ON areas (ativo);
-- ------------------------------------------------------------
-- Tabela: usuarios_areas
-- Relacionamento muitos-para-muitos entre usuarios e areas
-- Um usuario pode atuar em mais de uma area e uma area pode ter
-- varios usuarios.
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS usuarios_areas (
id SERIAL PRIMARY KEY,
usuario_id INTEGER NOT NULL REFERENCES usuarios (id) ON DELETE CASCADE,
area_id INTEGER NOT NULL REFERENCES areas (id) ON DELETE CASCADE,
funcao VARCHAR(80),
principal BOOLEAN NOT NULL DEFAULT FALSE,
ativo BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT uq_usuario_area UNIQUE (usuario_id, area_id)
);
CREATE INDEX IF NOT EXISTS idx_usuarios_areas_usuario ON usuarios_areas (usuario_id);
CREATE INDEX IF NOT EXISTS idx_usuarios_areas_area ON usuarios_areas (area_id);
CREATE INDEX IF NOT EXISTS idx_usuarios_areas_ativo ON usuarios_areas (ativo);
-- Garante que cada usuario tenha no maximo uma area principal.
CREATE UNIQUE INDEX IF NOT EXISTS uq_usuario_area_principal
ON usuarios_areas (usuario_id)
WHERE principal = TRUE;
-- ------------------------------------------------------------
-- Dados iniciais: areas padrao para o MVP
-- ------------------------------------------------------------
INSERT INTO areas (nome, descricao) VALUES
('Suporte', 'Atendimento operacional e resolucao de duvidas tecnicas'),
('Financeiro', 'Atendimento relacionado a cobrancas, pagamentos e notas'),
('Comercial', 'Atendimento de vendas, propostas e relacionamento comercial')
ON CONFLICT (nome) DO NOTHING;

View File

@ -0,0 +1,62 @@
-- ============================================================
-- Migration 003: Usuarios de demonstracao e acessos iniciais
-- Perfis: Admin, Supervisor e Agente
-- Areas: Suporte, Financeiro e Comercial
-- ============================================================
INSERT INTO usuarios (nome, email, ativo) VALUES
('Admin Demo', 'admin@sothis.com.br', TRUE),
('Supervisor Demo', 'supervisor@sothis.com.br', TRUE),
('Atendente Demo', 'atendente@sothis.com.br', TRUE)
ON CONFLICT (email) DO UPDATE SET
nome = EXCLUDED.nome,
ativo = TRUE,
updated_at = NOW();
INSERT INTO usuarios_provedores (usuario_id, provedor, provedor_user_id)
SELECT u.id, provider.provedor, provider.provedor_user_id
FROM usuarios u
JOIN (
VALUES
('admin@sothis.com.br', 'ldap', 'admin'),
('admin@sothis.com.br', 'microsoft', 'admin@sothis.com.br'),
('supervisor@sothis.com.br', 'ldap', 'supervisor'),
('supervisor@sothis.com.br', 'microsoft', 'supervisor@sothis.com.br'),
('atendente@sothis.com.br', 'ldap', 'atendente'),
('atendente@sothis.com.br', 'microsoft', 'atendente@sothis.com.br')
) AS provider(email, provedor, provedor_user_id) ON provider.email = u.email
ON CONFLICT (provedor, provedor_user_id)
DO UPDATE SET usuario_id = EXCLUDED.usuario_id;
INSERT INTO usuarios_perfis (usuario_id, perfil_id)
SELECT u.id, p.id
FROM usuarios u
JOIN (
VALUES
('admin@sothis.com.br', 'Admin'),
('supervisor@sothis.com.br', 'Supervisor'),
('atendente@sothis.com.br', 'Agente')
) AS access(email, perfil) ON access.email = u.email
JOIN perfis_acesso p ON p.nome = access.perfil
ON CONFLICT (usuario_id, perfil_id) DO NOTHING;
INSERT INTO usuarios_areas (usuario_id, area_id, funcao, principal, ativo)
SELECT u.id, a.id, access.funcao, TRUE, TRUE
FROM usuarios u
JOIN (
VALUES
('admin@sothis.com.br', 'Suporte', 'Administrador'),
('supervisor@sothis.com.br', 'Suporte', 'Supervisor'),
('atendente@sothis.com.br', 'Suporte', 'Atendente')
) AS access(email, area, funcao) ON access.email = u.email
JOIN areas a ON a.nome = access.area
ON CONFLICT (usuario_id, area_id)
DO UPDATE SET
funcao = EXCLUDED.funcao,
principal = TRUE,
ativo = TRUE,
updated_at = NOW();

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS whatsapp_chat_atribuicoes (
id SERIAL PRIMARY KEY,
chat_id VARCHAR(255) NOT NULL,
user_id SERIAL REFERENCES usuarios(id) ON DELETE CASCADE,
area_id SERIAL REFERENCES areas(id) ON DELETE SET NULL,
assigned_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(chat_id)
);

View File

@ -0,0 +1,14 @@
CREATE TABLE IF NOT EXISTS whatsapp_templates (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL UNIQUE,
content TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO whatsapp_templates (name, content) VALUES
('aviso_fatura', 'Olá, {nome}. Estamos entrando em contato para lembrá-lo que a sua fatura está programada para {data}.'),
('boas_vindas', 'Olá, {nome}! Obrigado por entrar em contato conosco. Como podemos te ajudar hoje?'),
('lembrete_consulta', 'Olá, {nome}. Gostaríamos de confirmar o seu agendamento para {data}. Está confirmado?'),
('suporte_tecnico', 'Olá, {nome}. Sou o atendente e irei te auxiliar no seu suporte sob protocolo {protocolo}.')
ON CONFLICT (name) DO NOTHING;

View File

@ -0,0 +1,41 @@
-- ============================================================
-- Migration 006: Fila e controle de atribuicao do WhatsApp
-- Tabela: whatsapp_chat_atribuicoes
-- ============================================================
-- A atribuicao passa a representar dois estados principais:
-- 1. queued: conversa esta na fila de uma area, sem atendente definido
-- 2. assigned: conversa foi assumida ou transferida diretamente para um atendente
--
-- A janela de atendimento e controlada por expires_at. Ao expirar, a aplicacao
-- trata a proxima mensagem como um novo ciclo de conversa.
ALTER TABLE whatsapp_chat_atribuicoes
ALTER COLUMN user_id DROP NOT NULL,
ALTER COLUMN area_id DROP NOT NULL;
ALTER TABLE whatsapp_chat_atribuicoes
ADD COLUMN IF NOT EXISTS status VARCHAR(40) NOT NULL DEFAULT 'assigned',
ADD COLUMN IF NOT EXISTS conversation_started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN IF NOT EXISTS expires_at TIMESTAMP WITH TIME ZONE DEFAULT (CURRENT_TIMESTAMP + INTERVAL '24 hours'),
ADD COLUMN IF NOT EXISTS transfer_note TEXT,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP;
UPDATE whatsapp_chat_atribuicoes
SET
status = CASE
WHEN user_id IS NULL THEN 'queued'
ELSE 'assigned'
END,
conversation_started_at = COALESCE(conversation_started_at, assigned_at, CURRENT_TIMESTAMP),
expires_at = COALESCE(expires_at, assigned_at + INTERVAL '24 hours', CURRENT_TIMESTAMP + INTERVAL '24 hours'),
updated_at = COALESCE(updated_at, assigned_at, CURRENT_TIMESTAMP);
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_area_status
ON whatsapp_chat_atribuicoes (area_id, status);
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_user_status
ON whatsapp_chat_atribuicoes (user_id, status);
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_expires_at
ON whatsapp_chat_atribuicoes (expires_at);

View File

@ -0,0 +1,12 @@
-- ============================================================
-- Migration 007: Estado de triagem automatica do Omnino
-- Tabela: whatsapp_chat_atribuicoes
-- ============================================================
ALTER TABLE whatsapp_chat_atribuicoes
ADD COLUMN IF NOT EXISTS routing_attempts INTEGER NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS last_routed_message_id VARCHAR(255),
ADD COLUMN IF NOT EXISTS last_bot_sent_at TIMESTAMP WITH TIME ZONE;
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_triage
ON whatsapp_chat_atribuicoes (status, routing_attempts);

View File

@ -0,0 +1,14 @@
-- ============================================================
-- Migration 008: Notas pessoais dos atendentes
-- Tabela: agent_notes
-- ============================================================
CREATE TABLE IF NOT EXISTS agent_notes (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES usuarios(id) ON DELETE CASCADE,
text TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_agent_notes_user_created_at
ON agent_notes (user_id, created_at DESC);

View File

@ -0,0 +1,18 @@
-- ============================================================
-- Migration 009: Agenda de contatos dos clientes
-- Tabela: customer_contacts
-- ============================================================
CREATE TABLE IF NOT EXISTS customer_contacts (
chat_id VARCHAR(255) PRIMARY KEY,
phone VARCHAR(80) NOT NULL,
name VARCHAR(255),
company VARCHAR(255),
note TEXT,
updated_by_user_id INTEGER REFERENCES usuarios(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_customer_contacts_updated_at
ON customer_contacts (updated_at DESC);

View File

@ -0,0 +1,36 @@
-- ============================================================
-- Migration 010: Agenda geral de contatos
-- Tabela: agenda_contatos
-- ============================================================
CREATE TABLE IF NOT EXISTS agenda_contatos (
chat_id VARCHAR(255) PRIMARY KEY,
phone VARCHAR(80) NOT NULL,
name VARCHAR(255),
company VARCHAR(255),
note TEXT,
updated_by_user_id INTEGER REFERENCES usuarios(id) ON DELETE SET NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
DO $$
BEGIN
IF to_regclass('public.customer_contacts') IS NOT NULL THEN
INSERT INTO agenda_contatos (
chat_id, phone, name, company, note, updated_by_user_id, created_at, updated_at
)
SELECT chat_id, phone, name, company, note, updated_by_user_id, created_at, updated_at
FROM customer_contacts
ON CONFLICT (chat_id) DO UPDATE SET
phone = EXCLUDED.phone,
name = EXCLUDED.name,
company = EXCLUDED.company,
note = EXCLUDED.note,
updated_by_user_id = EXCLUDED.updated_by_user_id,
updated_at = EXCLUDED.updated_at;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_agenda_contatos_updated_at
ON agenda_contatos (updated_at DESC);

View File

@ -0,0 +1,19 @@
-- ============================================================
-- Migration 011: Templates de abertura ativa do WhatsApp
-- Tabela: whatsapp_templates
-- ============================================================
INSERT INTO whatsapp_templates (name, content) VALUES
('abertura_atendimento_padrao', 'Ola, {nome}. Tudo bem? Estamos entrando em contato pelo atendimento. Podemos seguir por aqui?'),
('abertura_retorno_contato', 'Ola, {nome}. Estamos retornando seu contato para dar continuidade ao seu atendimento.'),
('abertura_suporte', 'Ola, {nome}. Aqui e do suporte. Estamos entrando em contato para te ajudar com sua solicitacao.'),
('abertura_financeiro', 'Ola, {nome}. Aqui e do financeiro. Estamos entrando em contato para tratar de uma informacao importante sobre seu atendimento.'),
('abertura_comercial', 'Ola, {nome}. Aqui e do comercial. Estamos entrando em contato para conversar sobre sua solicitacao.'),
('abertura_confirmacao_dados', 'Ola, {nome}. Precisamos confirmar alguns dados para seguir com seu atendimento.'),
('abertura_contato_agendado', 'Ola, {nome}. Este contato foi combinado anteriormente e estamos disponiveis para seguir.'),
('abertura_pos_atendimento', 'Ola, {nome}. Estamos fazendo um acompanhamento sobre seu atendimento recente.'),
('abertura_aviso_importante', 'Ola, {nome}. Temos uma informacao importante para compartilhar com voce.'),
('abertura_contato_inicial', 'Ola, {nome}. Vamos iniciar seu atendimento por este canal.')
ON CONFLICT (name) DO UPDATE SET
content = EXCLUDED.content,
updated_at = CURRENT_TIMESTAMP;

View File

@ -0,0 +1,10 @@
-- ============================================================
-- Migration 012: Bloqueio apos abertura ativa do WhatsApp
-- Tabela: whatsapp_chat_atribuicoes
-- ============================================================
ALTER TABLE whatsapp_chat_atribuicoes
ADD COLUMN IF NOT EXISTS awaiting_customer_reply BOOLEAN NOT NULL DEFAULT FALSE;
CREATE INDEX IF NOT EXISTS idx_whatsapp_atribuicoes_awaiting_customer_reply
ON whatsapp_chat_atribuicoes (awaiting_customer_reply, status);

View 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);

View 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';

View 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;

View 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;

View 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;

View 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;

View File

@ -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'));

View 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);

View 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);

View File

@ -0,0 +1,11 @@
-- ============================================================
-- Migration 023: Canais adicionais da agenda de contatos
-- Tabela: agenda_contatos
-- ============================================================
ALTER TABLE agenda_contatos
ADD COLUMN IF NOT EXISTS call_sms_phone VARCHAR(80),
ADD COLUMN IF NOT EXISTS email VARCHAR(255);
CREATE INDEX IF NOT EXISTS idx_agenda_contatos_email
ON agenda_contatos (email);

1
deploy-dev.bat Normal file
View File

@ -0,0 +1 @@
ssh desenvolvimento@10.0.120.75 -p 60000 "/home/desenvolvimento/scripts/deploy-omnichannel-dev.sh"

View File

@ -4,4 +4,13 @@ services:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
- "4000:3000"
backend:
build:
context: ./backend
dockerfile: Dockerfile
ports:
- "4001:3001"
env_file:
- .env.development

1
frontend Submodule

@ -0,0 +1 @@
Subproject commit 7dc07c2a806d6352d2a84c333f09974d997918b0