diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..41e33ea --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore index 7066758..bc13c1b 100644 --- a/.gitignore +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/README.md b/README.md index 83d625b..bf52214 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/backend b/backend new file mode 160000 index 0000000..8790ce7 --- /dev/null +++ b/backend @@ -0,0 +1 @@ +Subproject commit 8790ce70d05d0256ded89ea8fb9335afad41bfa8 diff --git a/database/Dockerfile b/database/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/database/migrations/001_auth.sql b/database/migrations/001_auth.sql new file mode 100644 index 0000000..c0ea1cc --- /dev/null +++ b/database/migrations/001_auth.sql @@ -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; \ No newline at end of file diff --git a/database/migrations/002_area.sql b/database/migrations/002_area.sql new file mode 100644 index 0000000..1153a8b --- /dev/null +++ b/database/migrations/002_area.sql @@ -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; diff --git a/database/migrations/003_demo_access.sql b/database/migrations/003_demo_access.sql new file mode 100644 index 0000000..83e7164 --- /dev/null +++ b/database/migrations/003_demo_access.sql @@ -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(); diff --git a/database/migrations/004_whatsapp.sql b/database/migrations/004_whatsapp.sql new file mode 100644 index 0000000..1633d32 --- /dev/null +++ b/database/migrations/004_whatsapp.sql @@ -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) +); diff --git a/database/migrations/005_templates.sql b/database/migrations/005_templates.sql new file mode 100644 index 0000000..59e84d4 --- /dev/null +++ b/database/migrations/005_templates.sql @@ -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; diff --git a/database/migrations/006_whatsapp_assignment_queue.sql b/database/migrations/006_whatsapp_assignment_queue.sql new file mode 100644 index 0000000..1f5b734 --- /dev/null +++ b/database/migrations/006_whatsapp_assignment_queue.sql @@ -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); diff --git a/database/migrations/007_whatsapp_triage_state.sql b/database/migrations/007_whatsapp_triage_state.sql new file mode 100644 index 0000000..f9fcc7b --- /dev/null +++ b/database/migrations/007_whatsapp_triage_state.sql @@ -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); diff --git a/database/migrations/008_agent_notes.sql b/database/migrations/008_agent_notes.sql new file mode 100644 index 0000000..32e6201 --- /dev/null +++ b/database/migrations/008_agent_notes.sql @@ -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); diff --git a/database/migrations/009_customer_contacts.sql b/database/migrations/009_customer_contacts.sql new file mode 100644 index 0000000..6b1222f --- /dev/null +++ b/database/migrations/009_customer_contacts.sql @@ -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); diff --git a/database/migrations/010_agenda_contatos.sql b/database/migrations/010_agenda_contatos.sql new file mode 100644 index 0000000..597985e --- /dev/null +++ b/database/migrations/010_agenda_contatos.sql @@ -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); diff --git a/database/migrations/011_whatsapp_opening_templates.sql b/database/migrations/011_whatsapp_opening_templates.sql new file mode 100644 index 0000000..7fc3bc6 --- /dev/null +++ b/database/migrations/011_whatsapp_opening_templates.sql @@ -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; diff --git a/database/migrations/012_whatsapp_awaiting_customer_reply.sql b/database/migrations/012_whatsapp_awaiting_customer_reply.sql new file mode 100644 index 0000000..3859212 --- /dev/null +++ b/database/migrations/012_whatsapp_awaiting_customer_reply.sql @@ -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); diff --git a/database/migrations/014_whatsapp_template_workflow.sql b/database/migrations/014_whatsapp_template_workflow.sql new file mode 100644 index 0000000..cce05d3 --- /dev/null +++ b/database/migrations/014_whatsapp_template_workflow.sql @@ -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); diff --git a/database/migrations/015_agent_presence_pause.sql b/database/migrations/015_agent_presence_pause.sql new file mode 100644 index 0000000..4934dad --- /dev/null +++ b/database/migrations/015_agent_presence_pause.sql @@ -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'; diff --git a/database/migrations/016_hr_decision_tree_keywords.sql b/database/migrations/016_hr_decision_tree_keywords.sql new file mode 100644 index 0000000..a575d34 --- /dev/null +++ b/database/migrations/016_hr_decision_tree_keywords.sql @@ -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; diff --git a/database/migrations/017_configurable_triage_flow.sql b/database/migrations/017_configurable_triage_flow.sql new file mode 100644 index 0000000..6f974fb --- /dev/null +++ b/database/migrations/017_configurable_triage_flow.sql @@ -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; diff --git a/database/migrations/018_triage_resolution_step.sql b/database/migrations/018_triage_resolution_step.sql new file mode 100644 index 0000000..24e13f0 --- /dev/null +++ b/database/migrations/018_triage_resolution_step.sql @@ -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; diff --git a/database/migrations/019_bot_flow_builder.sql b/database/migrations/019_bot_flow_builder.sql new file mode 100644 index 0000000..efaaa1f --- /dev/null +++ b/database/migrations/019_bot_flow_builder.sql @@ -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; diff --git a/database/migrations/020_bot_flow_close_node_and_area_delete.sql b/database/migrations/020_bot_flow_close_node_and_area_delete.sql new file mode 100644 index 0000000..0e61915 --- /dev/null +++ b/database/migrations/020_bot_flow_close_node_and_area_delete.sql @@ -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')); diff --git a/database/migrations/021_admin_audit_ai_contents.sql b/database/migrations/021_admin_audit_ai_contents.sql new file mode 100644 index 0000000..c5d5fd5 --- /dev/null +++ b/database/migrations/021_admin_audit_ai_contents.sql @@ -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); diff --git a/database/migrations/022_whatsapp_template_category.sql b/database/migrations/022_whatsapp_template_category.sql new file mode 100644 index 0000000..f1d364b --- /dev/null +++ b/database/migrations/022_whatsapp_template_category.sql @@ -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); diff --git a/database/migrations/023_agenda_contact_channels.sql b/database/migrations/023_agenda_contact_channels.sql new file mode 100644 index 0000000..e88c73b --- /dev/null +++ b/database/migrations/023_agenda_contact_channels.sql @@ -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); diff --git a/deploy-dev.bat b/deploy-dev.bat new file mode 100644 index 0000000..2c56623 --- /dev/null +++ b/deploy-dev.bat @@ -0,0 +1 @@ +ssh desenvolvimento@10.0.120.75 -p 60000 "/home/desenvolvimento/scripts/deploy-omnichannel-dev.sh" diff --git a/docker-compose.yml b/docker-compose.yml index 41329fb..79b36fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/frontend b/frontend new file mode 160000 index 0000000..7dc07c2 --- /dev/null +++ b/frontend @@ -0,0 +1 @@ +Subproject commit 7dc07c2a806d6352d2a84c333f09974d997918b0