From c854086e697ca2ac72b7951dfb982a9031ed1dae Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 31 Mar 2026 18:14:52 -0300 Subject: [PATCH 01/16] FEATURE: Adicionado novo modelo CI/CD --- .env.example | 17 +++++++ MakeFile | 2 + README.md | 92 +++++++----------------------------- database/Dockerfile | 0 database/init/001.schema.sql | 0 docker-compose.yml | 31 ++++++++++++ 6 files changed, 66 insertions(+), 76 deletions(-) create mode 100644 .env.example create mode 100644 MakeFile create mode 100644 database/Dockerfile create mode 100644 database/init/001.schema.sql diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9eb68fb --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +# Deploy (docker-compose) environment variables + +# Postgres (used by postgres service) +POSTGRES_USER=omnichannel +POSTGRES_PASSWORD=change-me +POSTGRES_DB=omnichannel + +# App database connection (used by backend) +DB_HOST=postgres +DB_PORT=5432 +DB_USER=omnichannel +DB_PASSWORD=change-me +DB_NAME=omnichannel + +# Ports (optional, if you want to reference in compose later) +FRONTEND_PORT=3000 +BACKEND_PORT=3001 diff --git a/MakeFile b/MakeFile new file mode 100644 index 0000000..01b58f6 --- /dev/null +++ b/MakeFile @@ -0,0 +1,2 @@ +deploy-dev: + ssh desenvolvimento@10.0.120.75 -p 60000 "/home/desenvolvimento/scripts/deploy-omnichannel-dev.sh" \ No newline at end of file diff --git a/README.md b/README.md index 83d625b..7417b7e 100644 --- a/README.md +++ b/README.md @@ -1,89 +1,29 @@ -# Omnichannel Sothis +# Omnichannel Sothis - Deploy -Protótipo visual do frontend MVP do sistema Omnichannel da Sothis. +Este repositorio e o **deploy** do ecossistema. Ele **nao** contem o codigo do frontend nem do backend em producao. Esses dois sao repositorios separados e devem ser clonados ao lado deste deploy para subir o ambiente completo. -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. +## Estrutura esperada (3 repositorios) -## O que existe hoje +- `deploy/` (este repo): `docker-compose.yml`, `database/`, `.gitignore`, `README.md` +- `frontend/`: interface do produto +- `backend/`: API e regras de negocio -- 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 +## Como subir tudo localmente -## Estrutura esperada do ecossistema +Passo a passo (na mesma pasta raiz): -Hoje este repositório cobre o frontend e um `docker-compose.yml` local para desenvolvimento/apresentação. - -Para rodar o ambiente completo no futuro, a separação esperada é: - -- `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 - -## Como rodar somente o frontend - -### Opção 1: com Docker - -Na raiz deste projeto: +1. Clonar o repo de deploy na raiz (.) +2. Clonar o repo de frontend na pasta `frontend` +3. Clonar o repo de backend na pasta `backend` +4. Subir tudo: ```bash docker compose up -d --build ``` -Depois acesse: +Isso sobe `frontend`, `backend` e `database` em uma unica operacao. -```text -http://localhost:3000 -``` +## Observacoes -### Opção 2: com Node local - -Entre na pasta do frontend: - -```bash -cd frontend -npm install -npm run dev -``` - -Depois acesse: - -```text -http://localhost:3000 -``` - -## Como gerar build do frontend - -```bash -cd frontend -npm run build -``` - -## Para rodar o ambiente completo - -Quando a solução estiver separada em múltiplos repositórios, o fluxo esperado será: - -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. - -## Documentação - -A documentação funcional do frontend está em [`frontend/docs`](./frontend/docs): - -- visão geral do projeto -- documentação por módulo/tela -- documentação em formato narrativo/RPG para explicar os casos de uso +- O `docker-compose.yml` deste repo espera `frontend/` e `backend/` presentes na mesma raiz. +- Em producao, o fluxo pode mudar para imagens pre-buildadas, mas para desenvolvimento local esta estrutura funciona bem. diff --git a/database/Dockerfile b/database/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/database/init/001.schema.sql b/database/init/001.schema.sql new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose.yml b/docker-compose.yml index 41329fb..05e123a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,3 +5,34 @@ services: dockerfile: Dockerfile ports: - "3000:3000" + + backend: + build: + context: ./backend + dockerfile: Dockerfile + ports: + - "3001:3001" + env_file: + - .env.development + depends_on: + postgres: + condition: service_healthy + + postgres: + image: postgres:16-alpine + restart: always + env_file: + - .env.development + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + - ./database/init:/docker-entrypoint-initdb.d + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + pgdata: -- 2.43.0 From 310ab6b8f2dc7d9c684adbed953379664119854b Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Wed, 1 Apr 2026 10:38:08 -0300 Subject: [PATCH 02/16] FEAT: Alterado metodo de deploy para uso do .bat --- MakeFile | 2 -- deploy-dev.bat | 1 + docker-compose.yml | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) delete mode 100644 MakeFile create mode 100644 deploy-dev.bat diff --git a/MakeFile b/MakeFile deleted file mode 100644 index 01b58f6..0000000 --- a/MakeFile +++ /dev/null @@ -1,2 +0,0 @@ -deploy-dev: - ssh desenvolvimento@10.0.120.75 -p 60000 "/home/desenvolvimento/scripts/deploy-omnichannel-dev.sh" \ No newline at end of file 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 05e123a..bfda2ee 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,14 +4,14 @@ services: context: ./frontend dockerfile: Dockerfile ports: - - "3000:3000" + - "3001:3000" backend: build: context: ./backend dockerfile: Dockerfile ports: - - "3001:3001" + - "3002:3001" env_file: - .env.development depends_on: -- 2.43.0 From b4c6bb4ba10e11afdd258ed87edf1c5da202e956 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Wed, 1 Apr 2026 10:41:55 -0300 Subject: [PATCH 03/16] FIX: Ajustado portas para build --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index bfda2ee..52bfe12 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,14 +4,14 @@ services: context: ./frontend dockerfile: Dockerfile ports: - - "3001:3000" + - "4000:3000" backend: build: context: ./backend dockerfile: Dockerfile ports: - - "3002:3001" + - "4001:3001" env_file: - .env.development depends_on: @@ -24,7 +24,7 @@ services: env_file: - .env.development ports: - - "5432:5432" + - "5434:5432" volumes: - pgdata:/var/lib/postgresql/data - ./database/init:/docker-entrypoint-initdb.d -- 2.43.0 From c5d23c876b0f7cdf8d60e4d50d2e6f7e3b7f01aa Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Fri, 8 May 2026 16:29:29 -0300 Subject: [PATCH 04/16] FEAT : Atualizado env para login com Microsoft e LDAP/AD --- .env.example | 29 +++++++++++++++++++++++++++++ .gitignore | 2 ++ 2 files changed, 31 insertions(+) diff --git a/.env.example b/.env.example index 9eb68fb..5f7d33b 100644 --- a/.env.example +++ b/.env.example @@ -15,3 +15,32 @@ DB_NAME=omnichannel # Ports (optional, if you want to reference in compose later) FRONTEND_PORT=3000 BACKEND_PORT=3001 + +# Backend HTTP/JWT +PORT=3001 +FRONTEND_URL=http://localhost:3000 +JWT_SECRET=change-this-long-random-secret +JWT_EXPIRES_IN=8h + +# Auth providers: ldap,microsoft or only one of them +AUTH_PROVIDERS=ldap,microsoft + +# LDAP / Active Directory +LDAP_ENABLED=true +LDAP_URL=ldaps://kratos.sothistelecom.com:636 +LDAP_DOMAIN=sothis.com.br +# Alternative when the bind user is not username@domain: +LDAP_USER_DN_TEMPLATE={{username}}@sothis.com.br +# Optional search to enrich the authenticated profile: +LDAP_SEARCH_BASE=DC=sothistelecom,DC=com +LDAP_SEARCH_FILTER=(&(objectClass=user)(sAMAccountName={{username}})) +# LDAP_BIND_DN=CN=ldap-reader,OU=Users,DC=example,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:3001/auth/oauth/microsoft/callback +MICROSOFT_SUCCESS_REDIRECT_URL=http://localhost:3000/login diff --git a/.gitignore b/.gitignore index 7066758..c0e58f5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ dist frontend/node_modules frontend/dist .DS_Store +.env.development +.env.production \ No newline at end of file -- 2.43.0 From bf95c883cefdd13446026aa3489f7af7d72cb4e2 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Thu, 14 May 2026 17:38:13 -0300 Subject: [PATCH 05/16] =?UTF-8?q?FEAT:=20Adicionado=20perfis,=20atuentica?= =?UTF-8?q?=C3=A7=C3=A3o,=20areas=20e=20acessos,=20dados=20para=20demo=20f?= =?UTF-8?q?oram=20adicionados=20tamb=C3=A9m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- database/init/001.schema.sql | 0 database/migrations/001_auth.sql | 112 ++++++++++++++++++++++++ database/migrations/002_area.sql | 63 +++++++++++++ database/migrations/003_demo_access.sql | 62 +++++++++++++ 4 files changed, 237 insertions(+) delete mode 100644 database/init/001.schema.sql create mode 100644 database/migrations/001_auth.sql create mode 100644 database/migrations/002_area.sql create mode 100644 database/migrations/003_demo_access.sql diff --git a/database/init/001.schema.sql b/database/init/001.schema.sql deleted file mode 100644 index e69de29..0000000 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(); -- 2.43.0 From 12e79cc6d69170ed6fabccbc2a1368ff4178d5f6 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Mon, 18 May 2026 11:15:29 -0300 Subject: [PATCH 06/16] =?UTF-8?q?FEAT:=20Implementa=20chat=20whatsapp=20e?= =?UTF-8?q?=20atribui=C3=A7=C3=B5es=20de=20conversas=20-=20Atualiza=C3=A7?= =?UTF-8?q?=C3=A3o=20e=20polimento=20dos=20arquivos=20'.gitignore'=20(raiz?= =?UTF-8?q?,=20frontend,=20backend)=20para=20ignorar=20sess=C3=B5es=20loca?= =?UTF-8?q?is=20do=20Puppeteer,=20bancos=20de=20persist=C3=AAncia=20JSON?= =?UTF-8?q?=20locais,=20dumps=20tempor=C3=A1rios=20de=20testes=20e=20logs?= =?UTF-8?q?=20de=20desenvolvimento.=20-=20Adi=C3=A7=C3=A3o=20da=20migratio?= =?UTF-8?q?n=20com=20atribui=C3=A7=C3=B5es=20aos=20chats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 18 ++++++++++++++---- database/migrations/004_whatsapp.sql | 8 ++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 database/migrations/004_whatsapp.sql diff --git a/.gitignore b/.gitignore index c0e58f5..bc13c1b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,17 @@ node_modules dist -frontend/node_modules -frontend/dist +.env* +*.log .DS_Store -.env.development -.env.production \ No newline at end of file + +# 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/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) +); -- 2.43.0 From 036d539fe13be65a7ccecee4593a580f958a4d16 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Mon, 18 May 2026 13:28:37 -0300 Subject: [PATCH 07/16] =?UTF-8?q?FEAT:=20Tabela=20templates=20para=20mensa?= =?UTF-8?q?gens=20pr=C3=A9=20aprovadas=20pela=20meta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- database/migrations/005_templates.sql | 14 +++++ docs/chat-whatsapp.md | 78 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 database/migrations/005_templates.sql create mode 100644 docs/chat-whatsapp.md 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/docs/chat-whatsapp.md b/docs/chat-whatsapp.md new file mode 100644 index 0000000..be60961 --- /dev/null +++ b/docs/chat-whatsapp.md @@ -0,0 +1,78 @@ +# Arquitetura e Integracao do Modulo WhatsApp (Omnichannel) + +## Visao Geral do Sistema + +Este documento descreve a arquitetura de alto nivel do modulo de **WhatsApp** integrado ao ecossistema **Sothis Omnichannel**. A solucao une uma interface web moderna de atendimento, uma API NestJS robusta e o controle nativo do WhatsApp Web via automacao e WebSockets em tempo real. + +--- + +## Diagrama de Fluxo e Integracao + +O fluxo de comunicacao entre os diferentes componentes do ecossistema e estruturado conforme o diagrama a seguir: + +```mermaid +graph TD + %% Componentes + FE[Frontend - React/Vite] + BE[Backend - NestJS] + DB[(Banco PostgreSQL)] + PERSIST[(Persistencia JSON Local)] + PUPP[Puppeteer Client - WhatsApp Web] + WPP[Servidores do WhatsApp] + + %% Relacionamentos do Frontend + FE <-->|WebSockets: Socket.io| BE + FE -->|HTTP: Enviar Mensagem / Atribuir / Liberar| BE + + %% Relacionamentos do Backend + BE <-->|Transacoes SQL| DB + BE <-->|Leitura/Escrita Cache| PERSIST + BE <-->|Automacao de Headless Chrome| PUPP + + %% WhatsApp Web + PUPP <-->|Sincronizacao Nativa| WPP +``` + +--- + +## Divisao de Responsabilidades + +### 1. Frontend (Interface Operacional) +* **Visualizacao**: Renderiza o historico de bolhas de mensagens e arquivos de midia (imagens, audios, anexos). +* **Posse de Chat (Ownership)**: Permite ao operador assumir chats livres (`takeChat`) ou libera-los (`releaseChat`), aplicando travas visuais se a conversa estiver sob a posse de outro colaborador. +* **UX Ininterrupta**: Aplica de-duplicação temporal em milissegundos e cria bolhas locais com ID temporario antes do disparo de rede para garantir experiencia livre de lag. + +### 2. Backend (Orquestracao e Integracao) +* **Automacao (whatsapp-web.js)**: Carrega o headless Chrome (via Puppeteer), autentica a sessao por QR Code, inicializa o cliente e gerencia reconexoes. +* **Transmissao em Tempo Real**: Escuta eventos de mensagem (`message_create`) do Puppeteer, formata o payload (com suporte a midias baixadas e resolucao inteligente de nomes) e distribui via Socket.io Gateway para a interface do atendente. +* **Processamento Pesado**: Aceita payloads de midia em Base64 de ate 50MB no canal de entrada para evitar falhas de upload. + +### 3. Banco de Dados PostgreSQL (Persistencia Transacional) +* **Controle de Atribuicoes**: A tabela `whatsapp_chat_atribuicoes` atua como fonte unica da verdade (*Single Source of Truth*) para posse de atendimento. +* **Chaves Estrangeiras**: Vincula transacionalmente o ID do chat (`chat_id`) com chaves numericas de usuarios (`user_id`) e setores operacionais (`area_id`). + +### 4. Cache JSON Local (whatsapp-chats-persist.json) +* **Performance de Listagem**: Evita multiplas consultas a servidores de rede locais ou API do WhatsApp na listagem inicial de chats operacionais. +* **Historico e previews**: Armazena carimbos de tempo (timestamps), contadores de nao lidas e previews de midia de forma otimizada. + +--- + +## Casos de Uso Principais + +### A. Assumir Chat Automaticamente ao Enviar Mensagem +1. O operador envia uma mensagem em uma conversa livre. +2. O frontend verifica se o chat esta sem dono e faz um POST para `/whatsapp/assign`. +3. O backend insere na tabela `whatsapp_chat_atribuicoes` o ID do atendente e o ID numerico de sua respectiva area de trabalho. +4. A transacao e confirmada. O frontend atualiza a UI marcando o operador como dono e libera o canal de conversacao. + +### B. Envio e Recebimento de Midias +1. O cliente envia uma imagem pelo WhatsApp. +2. O Puppeteer no backend detecta a mensagem com midia e baixa seus bytes em tempo real. +3. O backend emite via socket os metadados e os bytes brutos (`mimetype`, `data` em base64, `filename`). +4. O frontend identifica a presenca da midia e renderiza o player de audio, imagem ou link de download de forma nativa e estetica. + +--- + +## Politicas de Segurança e Versionamento +* As pastas de sessoes (`/whatsapp-session`), arquivos locais de persistencia JSON e logs de desenvolvimento estao estritamente declarados nos arquivos `.gitignore` para nao serem expostos nos repositorios Git. +* Credenciais de banco residem exclusivamente em arquivos `.env.*` mantidos fora do controle de versao. -- 2.43.0 From ba88d9cf0ba658319080f598b8cc1f55e230f431 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 19 May 2026 15:26:59 -0300 Subject: [PATCH 08/16] FEAT: Adicionado bot de triagem e filas --- backend | 1 + .../006_whatsapp_assignment_queue.sql | 41 +++++++++++++++++++ .../migrations/007_whatsapp_triage_state.sql | 12 ++++++ frontend | 1 + 4 files changed, 55 insertions(+) create mode 160000 backend create mode 100644 database/migrations/006_whatsapp_assignment_queue.sql create mode 100644 database/migrations/007_whatsapp_triage_state.sql create mode 160000 frontend 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/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/frontend b/frontend new file mode 160000 index 0000000..7dc07c2 --- /dev/null +++ b/frontend @@ -0,0 +1 @@ +Subproject commit 7dc07c2a806d6352d2a84c333f09974d997918b0 -- 2.43.0 From cfde2a23c8da0abbee26b8ab5707ddef6399cccd Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 19 May 2026 17:50:08 -0300 Subject: [PATCH 09/16] FEAT: Tabelas de contatos e notas dos agentes --- database/migrations/008_agent_notes.sql | 14 ++++++++++++++ database/migrations/009_customer_contacts.sql | 18 ++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 database/migrations/008_agent_notes.sql create mode 100644 database/migrations/009_customer_contacts.sql 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); -- 2.43.0 From a0e9fc120f48ad29e89b750dada1fded97a33818 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Wed, 20 May 2026 13:57:33 -0300 Subject: [PATCH 10/16] FEAT: Adiciona tabela agenda_contatos e templates de abertura do WhatsApp --- database/migrations/010_agenda_contatos.sql | 36 +++++++++++++++++++ .../011_whatsapp_opening_templates.sql | 19 ++++++++++ .../012_whatsapp_awaiting_customer_reply.sql | 10 ++++++ 3 files changed, 65 insertions(+) create mode 100644 database/migrations/010_agenda_contatos.sql create mode 100644 database/migrations/011_whatsapp_opening_templates.sql create mode 100644 database/migrations/012_whatsapp_awaiting_customer_reply.sql 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); -- 2.43.0 From cfa99a4881577def461973973ec069002ff697ce Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Fri, 22 May 2026 10:44:27 -0300 Subject: [PATCH 11/16] FEAT: Incrementa templates whatsapp --- .../014_whatsapp_template_workflow.sql | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 database/migrations/014_whatsapp_template_workflow.sql 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); -- 2.43.0 From 08eea51d5ae3275dffeb2fea615e5149b293ec8d Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Mon, 25 May 2026 14:27:57 -0300 Subject: [PATCH 12/16] DOCS: Atualizado Readme --- README.md | 547 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 531 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 7417b7e..50103bf 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,544 @@ -# Omnichannel Sothis - Deploy +# Omnichannel Sothis -Este repositorio e o **deploy** do ecossistema. Ele **nao** contem o codigo do frontend nem do backend em producao. Esses dois sao repositorios separados e devem ser clonados ao lado deste deploy para subir o ambiente completo. +Plataforma omnichannel para atendimento com foco inicial em WhatsApp, autenticação corporativa, controle de acesso por perfil, filas por especialidade, agenda de contatos, templates de mensagens e painéis operacionais para agente, supervisor e administrador. -## Estrutura esperada (3 repositorios) +Este repositório é a raiz do ambiente local/deploy. Ele contém o `docker-compose.yml`, as migrations de banco em `database/` e espera os projetos `frontend/` e `backend/` clonados ou presentes na mesma pasta. -- `deploy/` (este repo): `docker-compose.yml`, `database/`, `.gitignore`, `README.md` -- `frontend/`: interface do produto -- `backend/`: API e regras de negocio +## Estado Atual do Produto -## Como subir tudo localmente +O produto hoje permite: -Passo a passo (na mesma pasta raiz): +- Login via Active Directory/LDAP e Microsoft OAuth. +- Redirecionamento da Home conforme perfil do usuário. +- Atendimento WhatsApp em tempo real com socket. +- Triagem automática inicial pelo Omnino. +- Fila de atendimento por especialidade. +- Assumir, liberar e transferir atendimentos. +- Controle da janela de 24 horas do WhatsApp. +- Envio e recebimento de texto, imagem, vídeo, áudio e documentos. +- Lazy loading de mídias históricas. +- Agenda geral de contatos. +- Novo atendimento ativo via templates aprovados. +- Notas pessoais do atendente. +- Administração de usuários, perfis e especialidades. +- Workflow de criação, aprovação, reprovação e exclusão de templates. +- Painel mensal do admin e painel operacional do supervisor. -1. Clonar o repo de deploy na raiz (.) -2. Clonar o repo de frontend na pasta `frontend` -3. Clonar o repo de backend na pasta `backend` -4. Subir tudo: +## Perfis e Acesso + +### Usuário sem perfil + +Quando um usuário autentica pela primeira vez e ainda não tem perfil/especialidade definida, ele cai em uma tela vazia informando que não está atribuído a uma área/perfil. + +A liberação desse usuário é feita no painel do admin em `Usuários & Acessos`. + +### Agente + +O agente: + +- Vê somente atendimentos das especialidades em que está vinculado. +- Pode ver chamados da especialidade enquanto estão na fila. +- Só pode responder depois de assumir o atendimento. +- Pode liberar um atendimento para voltar para a fila. +- Pode transferir atendimento somente depois de assumir. +- Pode transferir para outra especialidade ou para outro usuário elegível. +- Pode editar/salvar contato na agenda. +- Pode criar notas pessoais. +- Pode abrir novo atendimento usando template aprovado. + +### Supervisor + +O supervisor: + +- Acessa a Home operacional da supervisão. +- Vê indicadores e filas das especialidades que supervisiona. +- Pode usar templates, base de conhecimento, auditoria, atendimento, abrir atendimento, disparo em massa e contatos. +- Pode solicitar novos templates, mas eles entram primeiro em aprovação do admin. + +### Admin + +O admin: + +- Não precisa pertencer a uma especialidade. +- Vê todas as filas e todos os atendimentos já roteados/classificados. +- Não vê conversas ainda em triagem automática do Omnino (`bot_triage`). +- Cria e gerencia especialidades. +- Define perfis de usuários. +- Pode tornar outro usuário admin. +- Pode definir se um usuário atua como agente ou supervisor em especialidades. +- Pode aprovar, reprovar e excluir templates. +- Acessa a visão mensal administrativa. +- Também pode atender, usando o menu administrativo. + +## Regras de Atendimento WhatsApp + +### Triagem automática pelo Omnino + +Toda primeira mensagem recebida no WhatsApp passa pela triagem automática do Omnino quando ainda não existe atendimento classificado. + +Regras atuais: + +- Mensagens vazias são registradas, mas não disparam triagem. +- Mídia sem legenda é registrada, mas não aciona o Omnino automaticamente. +- A triagem é serializada por conversa para evitar respostas duplicadas quando o WhatsApp dispara eventos quase simultâneos. +- O backend guarda `last_routed_message_id` para evitar processar a mesma mensagem mais de uma vez. + +Fluxo de decisão: + +1. Se a mensagem já contém uma intenção clara, o Omnino roteia direto para a especialidade. +2. Se a primeira mensagem é genérica, o Omnino envia a saudação completa e mantém a conversa em `bot_triage`. +3. Se a segunda mensagem ainda não identifica intenção, o Omnino pede explicitamente: suporte, financeiro ou comercial. +4. Se a terceira mensagem ainda for inválida, a conversa cai automaticamente em Suporte. + +Exemplos de intenção: + +- Suporte: suporte, bug, problema, técnico. +- Financeiro: boleto, dinheiro, cartão, atraso, fatura. +- Comercial: produto, novo, contratar, proposta. + +### Estados do atendimento + +A tabela `whatsapp_chat_atribuicoes` representa o estado do atendimento: + +- `bot_triage`: conversa ainda está com o Omnino. +- `queued`: conversa está na fila da especialidade, sem atendente. +- `assigned`: conversa foi assumida ou atribuída diretamente a um atendente. + +Regras: + +- Conversas em `bot_triage` não aparecem para agentes. +- Conversas em `queued` aparecem para usuários da especialidade. +- Conversas em `assigned` continuam visíveis para a especialidade, mas só o responsável responde. +- Admin vê todas as conversas roteadas, exceto `bot_triage`. + +### Assumir, liberar e transferir + +Para responder um chamado, o usuário precisa assumir o atendimento. + +- Ao assumir, `user_id` passa a ser o usuário atual. +- Ao liberar, o atendimento volta para fila da especialidade. +- Transferência só fica disponível depois que o usuário assume. +- Transferência para a mesma especialidade pode apontar para outro usuário. +- Transferência para outra especialidade cai na fila da nova especialidade. +- A observação da transferência fica visível para o próximo atendente e é limpa depois que o atendimento é respondido. + +### Janela de 24 horas + +O produto respeita a regra operacional do WhatsApp: + +- Conversas têm uma janela de 24 horas representada por `conversation_started_at` e `expires_at`. +- Ao abrir atendimento ativo para um cliente, o atendente deve usar template aprovado. +- Depois do primeiro contato ativo, o atendimento fica bloqueado para novas mensagens livres até o cliente responder. +- Quando o cliente responde, o bloqueio `awaiting_customer_reply` é removido e o atendente pode seguir a conversa. + +## Mensagens e Mídias + +### Envio de mensagens + +Mensagens enviadas por atendente são formatadas com identificação: + +```text +Atendente: Nome do Usuário + +Mensagem +``` + +No WhatsApp, essa identificação é enviada usando negrito: + +```text +*Atendente: Nome do Usuário* + +Mensagem +``` + +Se uma mídia for enviada sem texto, o backend não envia cabeçalho solto. A mídia segue sem legenda. + +### Recebimento de mídias + +Tipos suportados: + +- Imagens: PNG, JPG, JPEG, WEBP. +- Vídeos: MP4, WEBM. +- Áudios: MP3, OGG, WAV. +- Documentos: exibidos como card de arquivo. + +Para preservar performance, o histórico de mensagens não carrega Base64 de mídia diretamente. O frontend busca a mídia sob demanda pelo endpoint: + +```http +GET /whatsapp/media/:chatId/:messageId +``` + +### Limites de upload + +O backend aceita JSON até `25mb` por padrão, configurável via: + +```env +REQUEST_BODY_LIMIT=25mb +``` + +O frontend bloqueia anexos acima de 15 MB para evitar falhas de payload e degradação da experiência. + +## Chat + +A tela `/chat` exibe somente conversas reais vindas do backend/WhatsApp. Não existe mais fallback de conversas mockadas. + +Recursos atuais: + +- Lista de conversas ativas com scroll. +- Chat com altura delimitada e scroll interno. +- Separador de datas nas mensagens. +- Horário por mensagem. +- Deduplicação visual de mensagens enviadas localmente e confirmadas por socket. +- Ocultação de mensagens vazias. +- Botão para assumir atendimento. +- Botão para sair do atendimento. +- Transferência de atendimento. +- Painel de contato do cliente. +- Envio de mídia. + +Indicadores na lista: + +- Bolinha amarela: chamado está na fila da especialidade e ainda não foi atribuído. +- Bolinha azul: chamado está atribuído ao usuário atual. +- Bolinha vermelha: chamado está atribuído a outra pessoa. +- Badge de especialidade: mostra para qual fila o chamado foi roteado. +- Ícone de contato: indica que o contato está salvo na agenda. + +## Home do Agente + +A Home do agente usa dados reais do chat quando o WhatsApp está conectado. + +Recursos atuais: + +- Últimos atendimentos. +- Preview do chat com scroll automático para a última mensagem. +- Respostas sugeridas baseadas no contexto. +- Envio da resposta sugerida direto para o chat correto. +- Comunicados e notas. +- Notas pessoais persistidas por usuário. +- Relógio/data em tempo real. +- Atalhos para scripts, relatórios pessoais, disparo em massa e base de conhecimento. + +## Novo Atendimento + +A tela `/new-attendance` permite iniciar contato ativo. + +Regras: + +- WhatsApp é o canal funcional. +- Email e SMS aparecem bloqueados como funcionalidades em construção. +- O atendente pode buscar contatos da agenda. +- Também pode digitar novo número, nome, empresa e observação. +- O telefone tem máscara e seletor de país. +- Países disponíveis atualmente: Brasil, EUA, Argentina, Chile e México. +- O primeiro contato ativo exige seleção de template aprovado. +- Ao iniciar atendimento, o sistema salva/atualiza o contato na agenda e abre o chat daquela conversa. + +## Agenda de Contatos + +A agenda é geral, não vinculada a uma especialidade específica. + +Tabela principal: + +```text +agenda_contatos +``` + +Campos funcionais: + +- Nome. +- Empresa. +- Telefone. +- Observação. +- Chat ID. +- Usuário que criou/atualizou. + +Na conversa, o telefone não é editável quando vem do WhatsApp, mas nome, empresa e observação podem ser atualizados. + +## Templates WhatsApp + +Templates ficam na tabela: + +```text +whatsapp_templates +``` + +Campos relevantes: + +- `name` +- `content` +- `area_id` +- `status` +- `requested_by_role` +- `admin_approved_at` +- `meta_submitted_at` +- `meta_approved_at` + +Status atuais: + +- `approved`: template aprovado e disponível para uso. +- `meta_review`: enviado para aprovação da Meta. +- `admin_review`: aguardando aprovação do admin. +- `rejected`: reprovado pelo admin. + +Regras: + +- Admin cria template e envia para aprovação. +- Supervisor cria template, mas ele entra em aprovação do admin. +- Admin pode aprovar e enviar para Meta. +- Admin pode reprovar. +- Admin pode excluir. +- A aprovação da Meta é simulada: templates em análise são aprovados automaticamente depois de um intervalo configurado no código. +- A listagem possui filtro por especialidade e scroll para não ocupar a página inteira. + +## Painel Admin + +A Home do admin é uma visão mensal. + +Recursos atuais: + +- Filtro por especialidade no topo. +- KPIs mensais. +- Total de atendimentos. +- Tempo médio. +- TME. +- TMR. +- Satisfação. +- Atendentes ativos. +- Gráfico de atendimentos por dia. +- Donut de distribuição por canal. +- Ranking de atendentes. +- Painel de avisos. +- Menu administrativo lateral. + +Menu atual: + +- Home. +- Operação. +- Usuários & Acessos. +- Templates. +- Base de conhecimento IA. +- Auditoria. +- Canais. +- Atendimento. +- Abrir Atendimento. +- Disparo em Massa. +- Contatos. +- Configurações. +- Sair. + +## Usuários & Acessos + +Tela administrativa para usuários, perfis e especialidades. + +Regras atuais: + +- Usuários vêm do banco/autenticação, não de mock. +- Admin Demo, Supervisor Demo e Atendente Demo foram removidos por migration. +- Admin pode editar um usuário em modal. +- Se o usuário for admin, não há seleção de especialidades. +- Se não for admin, é possível vincular múltiplas especialidades. +- Para cada especialidade, o usuário pode atuar como agente ou supervisor. +- A criação/edição de especialidades é feita pelo admin. +- A definição de supervisores agora ocorre no vínculo do usuário, não dentro da especialidade. + +## Painel Supervisor / Operação + +O painel de supervisor também é usado como visão de operação no menu do admin. + +Recursos atuais: + +- Filtro por especialidade. +- KPIs do dia. +- Atendimentos finalizados. +- Atendimentos em aberto. +- Conversas na fila. +- Tempo médio do dia. +- Atendentes online simulados. +- Painel do time. +- Fila de espera. +- Atribuição mockada via modal. +- Gráfico do dia por hora. + +Sempre que há dado real disponível, a tendência do produto é substituir mock por dado real. Indicadores sem origem real consolidada ainda usam dados simulados. + +## Autenticação + +Endpoints principais: + +```http +GET /auth/config +POST /auth/login +GET /auth/oauth/microsoft/start +GET /auth/oauth/microsoft/callback +``` + +Providers suportados: + +- LDAP/Active Directory. +- Microsoft OAuth. + +Variáveis relevantes: + +```env +AUTH_PROVIDERS=ldap,microsoft +LDAP_ENABLED=true +LDAP_URL=ldaps://... +LDAP_DOMAIN=... +MICROSOFT_ENABLED=false +MICROSOFT_TENANT_ID=common +MICROSOFT_CLIENT_ID= +MICROSOFT_CLIENT_SECRET= +``` + +## Endpoints Principais + +### WhatsApp + +```http +GET /whatsapp/status +GET /whatsapp/chats +GET /whatsapp/messages/:chatId +GET /whatsapp/media/:chatId/:messageId +POST /whatsapp/send +POST /whatsapp/start-attendance +POST /whatsapp/assign +POST /whatsapp/transfer +DELETE /whatsapp/release/:chatId +GET /whatsapp/assignment/:chatId +``` + +### Templates + +```http +GET /whatsapp/templates +POST /whatsapp/templates +POST /whatsapp/templates/update/:id +POST /whatsapp/templates/approve-admin/:id +POST /whatsapp/templates/reject-admin/:id +DELETE /whatsapp/templates/:id +``` + +### Admin / Acessos + +```http +GET /admin/access/options +GET /admin/access/overview +GET /admin/access/areas +POST /admin/access/areas +PUT /admin/access/areas/:id +GET /admin/access/users +PUT /admin/access/users/:id +``` + +## Banco de Dados + +Migrations principais: + +- `005_templates.sql`: criação inicial de templates. +- `006_whatsapp_assignment_queue.sql`: fila e atribuição de WhatsApp. +- `007_whatsapp_triage_state.sql`: estado de triagem automática. +- `008_agent_notes.sql`: notas pessoais do atendente. +- `009_customer_contacts.sql`: primeira versão de contatos. +- `010_agenda_contatos.sql`: agenda geral consolidada. +- `011_whatsapp_opening_templates.sql`: templates de abertura ativa. +- `012_whatsapp_awaiting_customer_reply.sql`: bloqueio até resposta do cliente. +- `013_remove_demo_access_users.sql`: remoção de usuários demo. +- `014_whatsapp_template_workflow.sql`: workflow de aprovação de templates. + +## Estrutura do Repositório + +```text +omnichannel/ +├── backend/ # API NestJS e regras de negócio +├── frontend/ # Interface React/Vite +├── database/migrations/ # Migrations SQL +├── docker-compose.yml # Orquestração local +├── .env.example # Exemplo de variáveis +└── README.md +``` + +## Como Subir Localmente + +Pré-requisitos: + +- Node.js compatível com os projetos. +- Docker e Docker Compose. +- PostgreSQL via `docker-compose`. + +Na raiz: ```bash docker compose up -d --build ``` -Isso sobe `frontend`, `backend` e `database` em uma unica operacao. +Também é possível rodar frontend e backend em modo desenvolvimento: -## Observacoes +```bash +cd backend +npm install +npm run dev +``` + +```bash +cd frontend +npm install +npm run dev +``` + +URLs locais comuns: + +- Frontend: `http://localhost:5173` +- Backend: `http://localhost:3001` +- Status WhatsApp: `http://localhost:3001/whatsapp/status` + +## Variáveis de Ambiente + +Principais variáveis da raiz: + +```env +POSTGRES_USER=omnichannel +POSTGRES_PASSWORD=change-me +POSTGRES_DB=omnichannel + +DB_HOST=postgres +DB_PORT=5432 +DB_USER=omnichannel +DB_PASSWORD=change-me +DB_NAME=omnichannel + +PORT=3001 +FRONTEND_URL=http://localhost:3000 +JWT_SECRET=change-this-long-random-secret +JWT_EXPIRES_IN=8h + +REQUEST_BODY_LIMIT=25mb +``` + +No frontend, a URL da API deve vir de: + +```env +VITE_API_URL=http://localhost:3001 +``` + +## Limitações Conhecidas + +- O WhatsApp Web carrega conversas por lazy loading. Nem sempre todo histórico aparece imediatamente após conectar o QR Code. +- O status real de presença do contato foi removido por limitação prática da biblioteca/WhatsApp Web. +- O painel de supervisor ainda possui indicadores simulados onde não há métrica real consolidada. +- Email, SMS e ligação não estão operacionais como canais de atendimento. +- A aprovação da Meta para templates é simulada. +- Mídias ainda trafegam via Base64 no JSON; para produção, o ideal é evoluir para upload/armazenamento próprio e envio por referência. + +## Regras Importantes Para Desenvolvimento + +- Não reintroduzir mock de conversas no chat. O `/chat` deve exibir apenas conversas reais vindas do backend/WhatsApp. +- Toda alteração de banco deve virar nova migration numerada. +- Não permitir resposta sem atendimento assumido. +- Não permitir transferência sem atendimento assumido. +- Conversas em `bot_triage` não devem aparecer para agentes. +- Admin pode ver todas as filas e atendimentos roteados. +- Novo atendimento ativo deve usar template aprovado. +- Depois de contato ativo, bloquear novas mensagens livres até resposta do cliente. +- Mídia sem texto não deve enviar cabeçalho solto de atendente. -- O `docker-compose.yml` deste repo espera `frontend/` e `backend/` presentes na mesma raiz. -- Em producao, o fluxo pode mudar para imagens pre-buildadas, mas para desenvolvimento local esta estrutura funciona bem. -- 2.43.0 From a6a09aa740a1ffcb50410edfed0c1d966f600d26 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Mon, 25 May 2026 14:29:45 -0300 Subject: [PATCH 13/16] FEAT: Adicionado tabela para validar atividade do agente --- .../migrations/015_agent_presence_pause.sql | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 database/migrations/015_agent_presence_pause.sql 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'; -- 2.43.0 From e9e214195af733498617371aa5cffbe2f0142d1e Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 26 May 2026 09:08:53 -0300 Subject: [PATCH 14/16] FEAT: documenta regras do produto e adiciona migrations de suporte MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - documenta estado atual, regras de negócio e próximos passos do Omnichannel Sothis - adiciona migrations para triagem RH, flow builder, auditoria, conteúdos da IA e categoria de templates - prepara estrutura de banco para fluxo configurável do bot, encerramento automático e templates categorizados --- README.md | 29 ++-- .../016_hr_decision_tree_keywords.sql | 99 ++++++++++++ .../017_configurable_triage_flow.sql | 145 ++++++++++++++++++ .../migrations/018_triage_resolution_step.sql | 56 +++++++ database/migrations/019_bot_flow_builder.sql | 127 +++++++++++++++ ...20_bot_flow_close_node_and_area_delete.sql | 12 ++ .../021_admin_audit_ai_contents.sql | 41 +++++ .../022_whatsapp_template_category.sql | 14 ++ 8 files changed, 509 insertions(+), 14 deletions(-) create mode 100644 database/migrations/016_hr_decision_tree_keywords.sql create mode 100644 database/migrations/017_configurable_triage_flow.sql create mode 100644 database/migrations/018_triage_resolution_step.sql create mode 100644 database/migrations/019_bot_flow_builder.sql create mode 100644 database/migrations/020_bot_flow_close_node_and_area_delete.sql create mode 100644 database/migrations/021_admin_audit_ai_contents.sql create mode 100644 database/migrations/022_whatsapp_template_category.sql diff --git a/README.md b/README.md index 50103bf..74d016d 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ O produto hoje permite: - Login via Active Directory/LDAP e Microsoft OAuth. - Redirecionamento da Home conforme perfil do usuário. - Atendimento WhatsApp em tempo real com socket. -- Triagem automática inicial pelo Omnino. +- Triagem automática inicial pelo Agente Virtual Sothis. - Fila de atendimento por especialidade. - Assumir, liberar e transferir atendimentos. - Controle da janela de 24 horas do WhatsApp. @@ -61,7 +61,7 @@ O admin: - Não precisa pertencer a uma especialidade. - Vê todas as filas e todos os atendimentos já roteados/classificados. -- Não vê conversas ainda em triagem automática do Omnino (`bot_triage`). +- Não vê conversas ainda em triagem automática do Agente Virtual Sothis (`bot_triage`). - Cria e gerencia especialidades. - Define perfis de usuários. - Pode tornar outro usuário admin. @@ -72,22 +72,22 @@ O admin: ## Regras de Atendimento WhatsApp -### Triagem automática pelo Omnino +### Triagem automática pelo Agente Virtual Sothis -Toda primeira mensagem recebida no WhatsApp passa pela triagem automática do Omnino quando ainda não existe atendimento classificado. +Toda primeira mensagem recebida no WhatsApp passa pela triagem automática do Agente Virtual Sothis quando ainda não existe atendimento classificado. Regras atuais: - Mensagens vazias são registradas, mas não disparam triagem. -- Mídia sem legenda é registrada, mas não aciona o Omnino automaticamente. +- Mídia sem legenda é registrada, mas não aciona o Agente Virtual Sothis automaticamente. - A triagem é serializada por conversa para evitar respostas duplicadas quando o WhatsApp dispara eventos quase simultâneos. - O backend guarda `last_routed_message_id` para evitar processar a mesma mensagem mais de uma vez. Fluxo de decisão: -1. Se a mensagem já contém uma intenção clara, o Omnino roteia direto para a especialidade. -2. Se a primeira mensagem é genérica, o Omnino envia a saudação completa e mantém a conversa em `bot_triage`. -3. Se a segunda mensagem ainda não identifica intenção, o Omnino pede explicitamente: suporte, financeiro ou comercial. +1. Se a mensagem já contém uma intenção clara, o Agente Virtual Sothis roteia direto para a especialidade. +2. Se a primeira mensagem é genérica, o Agente Virtual Sothis envia a saudação completa e mantém a conversa em `bot_triage`. +3. Se a segunda mensagem ainda não identifica intenção, o Agente Virtual Sothis pede explicitamente: suporte, financeiro ou comercial. 4. Se a terceira mensagem ainda for inválida, a conversa cai automaticamente em Suporte. Exemplos de intenção: @@ -100,7 +100,7 @@ Exemplos de intenção: A tabela `whatsapp_chat_atribuicoes` representa o estado do atendimento: -- `bot_triage`: conversa ainda está com o Omnino. +- `bot_triage`: conversa ainda está com o Agente Virtual Sothis. - `queued`: conversa está na fila da especialidade, sem atendente. - `assigned`: conversa foi assumida ou atribuída diretamente a um atendente. @@ -180,7 +180,7 @@ O frontend bloqueia anexos acima de 15 MB para evitar falhas de payload e degrad ## Chat -A tela `/chat` exibe somente conversas reais vindas do backend/WhatsApp. Não existe mais fallback de conversas mockadas. +A tela `/chat` exibe somente conversas reais vindas do backend/WhatsApp. Não existe fallback local de conversas. Recursos atuais: @@ -333,7 +333,7 @@ Tela administrativa para usuários, perfis e especialidades. Regras atuais: -- Usuários vêm do banco/autenticação, não de mock. +- Usuários vêm do banco/autenticação. - Admin Demo, Supervisor Demo e Atendente Demo foram removidos por migration. - Admin pode editar um usuário em modal. - Se o usuário for admin, não há seleção de especialidades. @@ -357,10 +357,10 @@ Recursos atuais: - Atendentes online simulados. - Painel do time. - Fila de espera. -- Atribuição mockada via modal. +- Atribuição via modal. - Gráfico do dia por hora. -Sempre que há dado real disponível, a tendência do produto é substituir mock por dado real. Indicadores sem origem real consolidada ainda usam dados simulados. +Sempre que há dado operacional disponível, os painéis usam a origem real consolidada. Indicadores sem origem consolidada ainda usam dados de apoio para compor a visão. ## Autenticação @@ -532,7 +532,7 @@ VITE_API_URL=http://localhost:3001 ## Regras Importantes Para Desenvolvimento -- Não reintroduzir mock de conversas no chat. O `/chat` deve exibir apenas conversas reais vindas do backend/WhatsApp. +- Não reintroduzir fallback local de conversas no chat. O `/chat` deve exibir apenas conversas reais vindas do backend/WhatsApp. - Toda alteração de banco deve virar nova migration numerada. - Não permitir resposta sem atendimento assumido. - Não permitir transferência sem atendimento assumido. @@ -542,3 +542,4 @@ VITE_API_URL=http://localhost:3001 - Depois de contato ativo, bloquear novas mensagens livres até resposta do cliente. - Mídia sem texto não deve enviar cabeçalho solto de atendente. + 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); -- 2.43.0 From 85627152f62e5947acb172c7016cd2b90267ae19 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Tue, 26 May 2026 11:34:29 -0300 Subject: [PATCH 15/16] FEAT: Adicionado migration para canais da agenda de contatos --- database/migrations/023_agenda_contact_channels.sql | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 database/migrations/023_agenda_contact_channels.sql 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); -- 2.43.0 From f55e823e61a79d828254632be25160877282cbfc Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Wed, 27 May 2026 15:28:06 -0300 Subject: [PATCH 16/16] =?UTF-8?q?DOCS:=20Atualizado=20a=20documenta=C3=A7?= =?UTF-8?q?=C3=A3o=20do=20Omnichannel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 38 ++- README.md | 598 ++++++++---------------------------------- docker-compose.yml | 22 -- docs/chat-whatsapp.md | 78 ------ 4 files changed, 130 insertions(+), 606 deletions(-) delete mode 100644 docs/chat-whatsapp.md diff --git a/.env.example b/.env.example index 5f7d33b..41e33ea 100644 --- a/.env.example +++ b/.env.example @@ -1,40 +1,38 @@ # Deploy (docker-compose) environment variables - -# Postgres (used by postgres service) -POSTGRES_USER=omnichannel -POSTGRES_PASSWORD=change-me -POSTGRES_DB=omnichannel +# +# 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=postgres +DB_HOST=db.empresa.local DB_PORT=5432 DB_USER=omnichannel DB_PASSWORD=change-me DB_NAME=omnichannel -# Ports (optional, if you want to reference in compose later) -FRONTEND_PORT=3000 -BACKEND_PORT=3001 - # Backend HTTP/JWT +NODE_ENV=development PORT=3001 -FRONTEND_URL=http://localhost:3000 +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://kratos.sothistelecom.com:636 -LDAP_DOMAIN=sothis.com.br -# Alternative when the bind user is not username@domain: -LDAP_USER_DN_TEMPLATE={{username}}@sothis.com.br -# Optional search to enrich the authenticated profile: -LDAP_SEARCH_BASE=DC=sothistelecom,DC=com +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_BIND_DN=CN=ldap-reader,OU=Users,DC=example,DC=com +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 @@ -42,5 +40,5 @@ MICROSOFT_ENABLED=false MICROSOFT_TENANT_ID=common MICROSOFT_CLIENT_ID= MICROSOFT_CLIENT_SECRET= -MICROSOFT_REDIRECT_URI=http://localhost:3001/auth/oauth/microsoft/callback -MICROSOFT_SUCCESS_REDIRECT_URL=http://localhost:3000/login +MICROSOFT_REDIRECT_URI=http://localhost:4001/auth/oauth/microsoft/callback +MICROSOFT_SUCCESS_REDIRECT_URL=http://localhost:4000/login diff --git a/README.md b/README.md index 74d016d..bf52214 100644 --- a/README.md +++ b/README.md @@ -1,478 +1,99 @@ # Omnichannel Sothis -Plataforma omnichannel para atendimento com foco inicial em WhatsApp, autenticação corporativa, controle de acesso por perfil, filas por especialidade, agenda de contatos, templates de mensagens e painéis operacionais para agente, supervisor e administrador. +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. -Este repositório é a raiz do ambiente local/deploy. Ele contém o `docker-compose.yml`, as migrations de banco em `database/` e espera os projetos `frontend/` e `backend/` clonados ou presentes na mesma pasta. +O projeto foi construído para validar e evoluir um MVP de atendimento corporativo, com perfis de agente, supervisor e administrador. -## Estado Atual do Produto +## Principais Recursos -O produto hoje permite: +- 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. -- Login via Active Directory/LDAP e Microsoft OAuth. -- Redirecionamento da Home conforme perfil do usuário. -- Atendimento WhatsApp em tempo real com socket. -- Triagem automática inicial pelo Agente Virtual Sothis. -- Fila de atendimento por especialidade. -- Assumir, liberar e transferir atendimentos. -- Controle da janela de 24 horas do WhatsApp. -- Envio e recebimento de texto, imagem, vídeo, áudio e documentos. -- Lazy loading de mídias históricas. -- Agenda geral de contatos. -- Novo atendimento ativo via templates aprovados. -- Notas pessoais do atendente. -- Administração de usuários, perfis e especialidades. -- Workflow de criação, aprovação, reprovação e exclusão de templates. -- Painel mensal do admin e painel operacional do supervisor. +## Stack Técnica -## Perfis e Acesso +### Backend -### Usuário sem perfil +- 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`. -Quando um usuário autentica pela primeira vez e ainda não tem perfil/especialidade definida, ele cai em uma tela vazia informando que não está atribuído a uma área/perfil. +### Frontend -A liberação desse usuário é feita no painel do admin em `Usuários & Acessos`. +- React `^18.3`. +- Vite `^5.4`. +- React Router `^6.30`. +- Socket.IO Client `^4.8`. -### Agente +### Banco -O agente: +- PostgreSQL 16 recomendado. +- O banco não é gerenciado pelo `docker-compose.yml` deste repositório. +- As migrations ficam em `database/migrations`. -- Vê somente atendimentos das especialidades em que está vinculado. -- Pode ver chamados da especialidade enquanto estão na fila. -- Só pode responder depois de assumir o atendimento. -- Pode liberar um atendimento para voltar para a fila. -- Pode transferir atendimento somente depois de assumir. -- Pode transferir para outra especialidade ou para outro usuário elegível. -- Pode editar/salvar contato na agenda. -- Pode criar notas pessoais. -- Pode abrir novo atendimento usando template aprovado. +### Docker -### Supervisor +O Docker Compose da raiz sobe somente: -O supervisor: +- `backend` +- `frontend` -- Acessa a Home operacional da supervisão. -- Vê indicadores e filas das especialidades que supervisiona. -- Pode usar templates, base de conhecimento, auditoria, atendimento, abrir atendimento, disparo em massa e contatos. -- Pode solicitar novos templates, mas eles entram primeiro em aprovação do admin. - -### Admin - -O admin: - -- Não precisa pertencer a uma especialidade. -- Vê todas as filas e todos os atendimentos já roteados/classificados. -- Não vê conversas ainda em triagem automática do Agente Virtual Sothis (`bot_triage`). -- Cria e gerencia especialidades. -- Define perfis de usuários. -- Pode tornar outro usuário admin. -- Pode definir se um usuário atua como agente ou supervisor em especialidades. -- Pode aprovar, reprovar e excluir templates. -- Acessa a visão mensal administrativa. -- Também pode atender, usando o menu administrativo. - -## Regras de Atendimento WhatsApp - -### Triagem automática pelo Agente Virtual Sothis - -Toda primeira mensagem recebida no WhatsApp passa pela triagem automática do Agente Virtual Sothis quando ainda não existe atendimento classificado. - -Regras atuais: - -- Mensagens vazias são registradas, mas não disparam triagem. -- Mídia sem legenda é registrada, mas não aciona o Agente Virtual Sothis automaticamente. -- A triagem é serializada por conversa para evitar respostas duplicadas quando o WhatsApp dispara eventos quase simultâneos. -- O backend guarda `last_routed_message_id` para evitar processar a mesma mensagem mais de uma vez. - -Fluxo de decisão: - -1. Se a mensagem já contém uma intenção clara, o Agente Virtual Sothis roteia direto para a especialidade. -2. Se a primeira mensagem é genérica, o Agente Virtual Sothis envia a saudação completa e mantém a conversa em `bot_triage`. -3. Se a segunda mensagem ainda não identifica intenção, o Agente Virtual Sothis pede explicitamente: suporte, financeiro ou comercial. -4. Se a terceira mensagem ainda for inválida, a conversa cai automaticamente em Suporte. - -Exemplos de intenção: - -- Suporte: suporte, bug, problema, técnico. -- Financeiro: boleto, dinheiro, cartão, atraso, fatura. -- Comercial: produto, novo, contratar, proposta. - -### Estados do atendimento - -A tabela `whatsapp_chat_atribuicoes` representa o estado do atendimento: - -- `bot_triage`: conversa ainda está com o Agente Virtual Sothis. -- `queued`: conversa está na fila da especialidade, sem atendente. -- `assigned`: conversa foi assumida ou atribuída diretamente a um atendente. - -Regras: - -- Conversas em `bot_triage` não aparecem para agentes. -- Conversas em `queued` aparecem para usuários da especialidade. -- Conversas em `assigned` continuam visíveis para a especialidade, mas só o responsável responde. -- Admin vê todas as conversas roteadas, exceto `bot_triage`. - -### Assumir, liberar e transferir - -Para responder um chamado, o usuário precisa assumir o atendimento. - -- Ao assumir, `user_id` passa a ser o usuário atual. -- Ao liberar, o atendimento volta para fila da especialidade. -- Transferência só fica disponível depois que o usuário assume. -- Transferência para a mesma especialidade pode apontar para outro usuário. -- Transferência para outra especialidade cai na fila da nova especialidade. -- A observação da transferência fica visível para o próximo atendente e é limpa depois que o atendimento é respondido. - -### Janela de 24 horas - -O produto respeita a regra operacional do WhatsApp: - -- Conversas têm uma janela de 24 horas representada por `conversation_started_at` e `expires_at`. -- Ao abrir atendimento ativo para um cliente, o atendente deve usar template aprovado. -- Depois do primeiro contato ativo, o atendimento fica bloqueado para novas mensagens livres até o cliente responder. -- Quando o cliente responde, o bloqueio `awaiting_customer_reply` é removido e o atendente pode seguir a conversa. - -## Mensagens e Mídias - -### Envio de mensagens - -Mensagens enviadas por atendente são formatadas com identificação: - -```text -Atendente: Nome do Usuário - -Mensagem -``` - -No WhatsApp, essa identificação é enviada usando negrito: - -```text -*Atendente: Nome do Usuário* - -Mensagem -``` - -Se uma mídia for enviada sem texto, o backend não envia cabeçalho solto. A mídia segue sem legenda. - -### Recebimento de mídias - -Tipos suportados: - -- Imagens: PNG, JPG, JPEG, WEBP. -- Vídeos: MP4, WEBM. -- Áudios: MP3, OGG, WAV. -- Documentos: exibidos como card de arquivo. - -Para preservar performance, o histórico de mensagens não carrega Base64 de mídia diretamente. O frontend busca a mídia sob demanda pelo endpoint: - -```http -GET /whatsapp/media/:chatId/:messageId -``` - -### Limites de upload - -O backend aceita JSON até `25mb` por padrão, configurável via: - -```env -REQUEST_BODY_LIMIT=25mb -``` - -O frontend bloqueia anexos acima de 15 MB para evitar falhas de payload e degradação da experiência. - -## Chat - -A tela `/chat` exibe somente conversas reais vindas do backend/WhatsApp. Não existe fallback local de conversas. - -Recursos atuais: - -- Lista de conversas ativas com scroll. -- Chat com altura delimitada e scroll interno. -- Separador de datas nas mensagens. -- Horário por mensagem. -- Deduplicação visual de mensagens enviadas localmente e confirmadas por socket. -- Ocultação de mensagens vazias. -- Botão para assumir atendimento. -- Botão para sair do atendimento. -- Transferência de atendimento. -- Painel de contato do cliente. -- Envio de mídia. - -Indicadores na lista: - -- Bolinha amarela: chamado está na fila da especialidade e ainda não foi atribuído. -- Bolinha azul: chamado está atribuído ao usuário atual. -- Bolinha vermelha: chamado está atribuído a outra pessoa. -- Badge de especialidade: mostra para qual fila o chamado foi roteado. -- Ícone de contato: indica que o contato está salvo na agenda. - -## Home do Agente - -A Home do agente usa dados reais do chat quando o WhatsApp está conectado. - -Recursos atuais: - -- Últimos atendimentos. -- Preview do chat com scroll automático para a última mensagem. -- Respostas sugeridas baseadas no contexto. -- Envio da resposta sugerida direto para o chat correto. -- Comunicados e notas. -- Notas pessoais persistidas por usuário. -- Relógio/data em tempo real. -- Atalhos para scripts, relatórios pessoais, disparo em massa e base de conhecimento. - -## Novo Atendimento - -A tela `/new-attendance` permite iniciar contato ativo. - -Regras: - -- WhatsApp é o canal funcional. -- Email e SMS aparecem bloqueados como funcionalidades em construção. -- O atendente pode buscar contatos da agenda. -- Também pode digitar novo número, nome, empresa e observação. -- O telefone tem máscara e seletor de país. -- Países disponíveis atualmente: Brasil, EUA, Argentina, Chile e México. -- O primeiro contato ativo exige seleção de template aprovado. -- Ao iniciar atendimento, o sistema salva/atualiza o contato na agenda e abre o chat daquela conversa. - -## Agenda de Contatos - -A agenda é geral, não vinculada a uma especialidade específica. - -Tabela principal: - -```text -agenda_contatos -``` - -Campos funcionais: - -- Nome. -- Empresa. -- Telefone. -- Observação. -- Chat ID. -- Usuário que criou/atualizou. - -Na conversa, o telefone não é editável quando vem do WhatsApp, mas nome, empresa e observação podem ser atualizados. - -## Templates WhatsApp - -Templates ficam na tabela: - -```text -whatsapp_templates -``` - -Campos relevantes: - -- `name` -- `content` -- `area_id` -- `status` -- `requested_by_role` -- `admin_approved_at` -- `meta_submitted_at` -- `meta_approved_at` - -Status atuais: - -- `approved`: template aprovado e disponível para uso. -- `meta_review`: enviado para aprovação da Meta. -- `admin_review`: aguardando aprovação do admin. -- `rejected`: reprovado pelo admin. - -Regras: - -- Admin cria template e envia para aprovação. -- Supervisor cria template, mas ele entra em aprovação do admin. -- Admin pode aprovar e enviar para Meta. -- Admin pode reprovar. -- Admin pode excluir. -- A aprovação da Meta é simulada: templates em análise são aprovados automaticamente depois de um intervalo configurado no código. -- A listagem possui filtro por especialidade e scroll para não ocupar a página inteira. - -## Painel Admin - -A Home do admin é uma visão mensal. - -Recursos atuais: - -- Filtro por especialidade no topo. -- KPIs mensais. -- Total de atendimentos. -- Tempo médio. -- TME. -- TMR. -- Satisfação. -- Atendentes ativos. -- Gráfico de atendimentos por dia. -- Donut de distribuição por canal. -- Ranking de atendentes. -- Painel de avisos. -- Menu administrativo lateral. - -Menu atual: - -- Home. -- Operação. -- Usuários & Acessos. -- Templates. -- Base de conhecimento IA. -- Auditoria. -- Canais. -- Atendimento. -- Abrir Atendimento. -- Disparo em Massa. -- Contatos. -- Configurações. -- Sair. - -## Usuários & Acessos - -Tela administrativa para usuários, perfis e especialidades. - -Regras atuais: - -- Usuários vêm do banco/autenticação. -- Admin Demo, Supervisor Demo e Atendente Demo foram removidos por migration. -- Admin pode editar um usuário em modal. -- Se o usuário for admin, não há seleção de especialidades. -- Se não for admin, é possível vincular múltiplas especialidades. -- Para cada especialidade, o usuário pode atuar como agente ou supervisor. -- A criação/edição de especialidades é feita pelo admin. -- A definição de supervisores agora ocorre no vínculo do usuário, não dentro da especialidade. - -## Painel Supervisor / Operação - -O painel de supervisor também é usado como visão de operação no menu do admin. - -Recursos atuais: - -- Filtro por especialidade. -- KPIs do dia. -- Atendimentos finalizados. -- Atendimentos em aberto. -- Conversas na fila. -- Tempo médio do dia. -- Atendentes online simulados. -- Painel do time. -- Fila de espera. -- Atribuição via modal. -- Gráfico do dia por hora. - -Sempre que há dado operacional disponível, os painéis usam a origem real consolidada. Indicadores sem origem consolidada ainda usam dados de apoio para compor a visão. - -## Autenticação - -Endpoints principais: - -```http -GET /auth/config -POST /auth/login -GET /auth/oauth/microsoft/start -GET /auth/oauth/microsoft/callback -``` - -Providers suportados: - -- LDAP/Active Directory. -- Microsoft OAuth. - -Variáveis relevantes: - -```env -AUTH_PROVIDERS=ldap,microsoft -LDAP_ENABLED=true -LDAP_URL=ldaps://... -LDAP_DOMAIN=... -MICROSOFT_ENABLED=false -MICROSOFT_TENANT_ID=common -MICROSOFT_CLIENT_ID= -MICROSOFT_CLIENT_SECRET= -``` - -## Endpoints Principais - -### WhatsApp - -```http -GET /whatsapp/status -GET /whatsapp/chats -GET /whatsapp/messages/:chatId -GET /whatsapp/media/:chatId/:messageId -POST /whatsapp/send -POST /whatsapp/start-attendance -POST /whatsapp/assign -POST /whatsapp/transfer -DELETE /whatsapp/release/:chatId -GET /whatsapp/assignment/:chatId -``` - -### Templates - -```http -GET /whatsapp/templates -POST /whatsapp/templates -POST /whatsapp/templates/update/:id -POST /whatsapp/templates/approve-admin/:id -POST /whatsapp/templates/reject-admin/:id -DELETE /whatsapp/templates/:id -``` - -### Admin / Acessos - -```http -GET /admin/access/options -GET /admin/access/overview -GET /admin/access/areas -POST /admin/access/areas -PUT /admin/access/areas/:id -GET /admin/access/users -PUT /admin/access/users/:id -``` - -## Banco de Dados - -Migrations principais: - -- `005_templates.sql`: criação inicial de templates. -- `006_whatsapp_assignment_queue.sql`: fila e atribuição de WhatsApp. -- `007_whatsapp_triage_state.sql`: estado de triagem automática. -- `008_agent_notes.sql`: notas pessoais do atendente. -- `009_customer_contacts.sql`: primeira versão de contatos. -- `010_agenda_contatos.sql`: agenda geral consolidada. -- `011_whatsapp_opening_templates.sql`: templates de abertura ativa. -- `012_whatsapp_awaiting_customer_reply.sql`: bloqueio até resposta do cliente. -- `013_remove_demo_access_users.sql`: remoção de usuários demo. -- `014_whatsapp_template_workflow.sql`: workflow de aprovação de templates. +O banco deve ser externo ao compose: VM, banco corporativo, RDS, container separado ou PostgreSQL local gerenciado fora deste projeto. ## Estrutura do Repositório -```text +```txt omnichannel/ ├── backend/ # API NestJS e regras de negócio ├── frontend/ # Interface React/Vite ├── database/migrations/ # Migrations SQL -├── docker-compose.yml # Orquestração local -├── .env.example # Exemplo de variáveis +├── docs/ # Wiki operacional e arquitetura +├── docker-compose.yml # Sobe backend e frontend └── README.md ``` -## Como Subir Localmente +## Como Subir com Docker Compose -Pré-requisitos: - -- Node.js compatível com os projetos. -- Docker e Docker Compose. -- PostgreSQL via `docker-compose`. - -Na raiz: +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 ``` -Também é possível rodar frontend e backend em modo desenvolvimento: +URLs padrão: + +- Frontend: `http://localhost:4000` +- Backend: `http://localhost:4001` + +Health: + +```bash +curl http://localhost:4001/health +``` + +## Como Rodar em Desenvolvimento + +Backend: ```bash cd backend @@ -480,66 +101,71 @@ npm install npm run dev ``` +Frontend: + ```bash cd frontend npm install npm run dev ``` -URLs locais comuns: +URLs comuns: -- Frontend: `http://localhost:5173` +- Frontend Vite: `http://localhost:5173` - Backend: `http://localhost:3001` -- Status WhatsApp: `http://localhost:3001/whatsapp/status` -## Variáveis de Ambiente +## Banco e Migrations -Principais variáveis da raiz: +As migrations SQL estão em: -```env -POSTGRES_USER=omnichannel -POSTGRES_PASSWORD=change-me -POSTGRES_DB=omnichannel - -DB_HOST=postgres -DB_PORT=5432 -DB_USER=omnichannel -DB_PASSWORD=change-me -DB_NAME=omnichannel - -PORT=3001 -FRONTEND_URL=http://localhost:3000 -JWT_SECRET=change-this-long-random-secret -JWT_EXPIRES_IN=8h - -REQUEST_BODY_LIMIT=25mb +```txt +database/migrations ``` -No frontend, a URL da API deve vir de: +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. -```env -VITE_API_URL=http://localhost:3001 -``` +Para ambiente novo, antes de subir backend para uso real: -## Limitações Conhecidas +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. -- O WhatsApp Web carrega conversas por lazy loading. Nem sempre todo histórico aparece imediatamente após conectar o QR Code. -- O status real de presença do contato foi removido por limitação prática da biblioteca/WhatsApp Web. -- O painel de supervisor ainda possui indicadores simulados onde não há métrica real consolidada. -- Email, SMS e ligação não estão operacionais como canais de atendimento. -- A aprovação da Meta para templates é simulada. -- Mídias ainda trafegam via Base64 no JSON; para produção, o ideal é evoluir para upload/armazenamento próprio e envio por referência. +Detalhes em: -## Regras Importantes Para Desenvolvimento +- [Deploy e operação](./docs/deploy.md) +- [Database](./backend/docs/database.md) -- Não reintroduzir fallback local de conversas no chat. O `/chat` deve exibir apenas conversas reais vindas do backend/WhatsApp. -- Toda alteração de banco deve virar nova migration numerada. -- Não permitir resposta sem atendimento assumido. -- Não permitir transferência sem atendimento assumido. -- Conversas em `bot_triage` não devem aparecer para agentes. -- Admin pode ver todas as filas e atendimentos roteados. -- Novo atendimento ativo deve usar template aprovado. -- Depois de contato ativo, bloquear novas mensagens livres até resposta do cliente. -- Mídia sem texto não deve enviar cabeçalho solto de atendente. +## Documentação +Wiki raiz: +- [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/docker-compose.yml b/docker-compose.yml index 52bfe12..79b36fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,25 +14,3 @@ services: - "4001:3001" env_file: - .env.development - depends_on: - postgres: - condition: service_healthy - - postgres: - image: postgres:16-alpine - restart: always - env_file: - - .env.development - ports: - - "5434:5432" - volumes: - - pgdata:/var/lib/postgresql/data - - ./database/init:/docker-entrypoint-initdb.d - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"] - interval: 5s - timeout: 5s - retries: 5 - -volumes: - pgdata: diff --git a/docs/chat-whatsapp.md b/docs/chat-whatsapp.md deleted file mode 100644 index be60961..0000000 --- a/docs/chat-whatsapp.md +++ /dev/null @@ -1,78 +0,0 @@ -# Arquitetura e Integracao do Modulo WhatsApp (Omnichannel) - -## Visao Geral do Sistema - -Este documento descreve a arquitetura de alto nivel do modulo de **WhatsApp** integrado ao ecossistema **Sothis Omnichannel**. A solucao une uma interface web moderna de atendimento, uma API NestJS robusta e o controle nativo do WhatsApp Web via automacao e WebSockets em tempo real. - ---- - -## Diagrama de Fluxo e Integracao - -O fluxo de comunicacao entre os diferentes componentes do ecossistema e estruturado conforme o diagrama a seguir: - -```mermaid -graph TD - %% Componentes - FE[Frontend - React/Vite] - BE[Backend - NestJS] - DB[(Banco PostgreSQL)] - PERSIST[(Persistencia JSON Local)] - PUPP[Puppeteer Client - WhatsApp Web] - WPP[Servidores do WhatsApp] - - %% Relacionamentos do Frontend - FE <-->|WebSockets: Socket.io| BE - FE -->|HTTP: Enviar Mensagem / Atribuir / Liberar| BE - - %% Relacionamentos do Backend - BE <-->|Transacoes SQL| DB - BE <-->|Leitura/Escrita Cache| PERSIST - BE <-->|Automacao de Headless Chrome| PUPP - - %% WhatsApp Web - PUPP <-->|Sincronizacao Nativa| WPP -``` - ---- - -## Divisao de Responsabilidades - -### 1. Frontend (Interface Operacional) -* **Visualizacao**: Renderiza o historico de bolhas de mensagens e arquivos de midia (imagens, audios, anexos). -* **Posse de Chat (Ownership)**: Permite ao operador assumir chats livres (`takeChat`) ou libera-los (`releaseChat`), aplicando travas visuais se a conversa estiver sob a posse de outro colaborador. -* **UX Ininterrupta**: Aplica de-duplicação temporal em milissegundos e cria bolhas locais com ID temporario antes do disparo de rede para garantir experiencia livre de lag. - -### 2. Backend (Orquestracao e Integracao) -* **Automacao (whatsapp-web.js)**: Carrega o headless Chrome (via Puppeteer), autentica a sessao por QR Code, inicializa o cliente e gerencia reconexoes. -* **Transmissao em Tempo Real**: Escuta eventos de mensagem (`message_create`) do Puppeteer, formata o payload (com suporte a midias baixadas e resolucao inteligente de nomes) e distribui via Socket.io Gateway para a interface do atendente. -* **Processamento Pesado**: Aceita payloads de midia em Base64 de ate 50MB no canal de entrada para evitar falhas de upload. - -### 3. Banco de Dados PostgreSQL (Persistencia Transacional) -* **Controle de Atribuicoes**: A tabela `whatsapp_chat_atribuicoes` atua como fonte unica da verdade (*Single Source of Truth*) para posse de atendimento. -* **Chaves Estrangeiras**: Vincula transacionalmente o ID do chat (`chat_id`) com chaves numericas de usuarios (`user_id`) e setores operacionais (`area_id`). - -### 4. Cache JSON Local (whatsapp-chats-persist.json) -* **Performance de Listagem**: Evita multiplas consultas a servidores de rede locais ou API do WhatsApp na listagem inicial de chats operacionais. -* **Historico e previews**: Armazena carimbos de tempo (timestamps), contadores de nao lidas e previews de midia de forma otimizada. - ---- - -## Casos de Uso Principais - -### A. Assumir Chat Automaticamente ao Enviar Mensagem -1. O operador envia uma mensagem em uma conversa livre. -2. O frontend verifica se o chat esta sem dono e faz um POST para `/whatsapp/assign`. -3. O backend insere na tabela `whatsapp_chat_atribuicoes` o ID do atendente e o ID numerico de sua respectiva area de trabalho. -4. A transacao e confirmada. O frontend atualiza a UI marcando o operador como dono e libera o canal de conversacao. - -### B. Envio e Recebimento de Midias -1. O cliente envia uma imagem pelo WhatsApp. -2. O Puppeteer no backend detecta a mensagem com midia e baixa seus bytes em tempo real. -3. O backend emite via socket os metadados e os bytes brutos (`mimetype`, `data` em base64, `filename`). -4. O frontend identifica a presenca da midia e renderiza o player de audio, imagem ou link de download de forma nativa e estetica. - ---- - -## Politicas de Segurança e Versionamento -* As pastas de sessoes (`/whatsapp-session`), arquivos locais de persistencia JSON e logs de desenvolvimento estao estritamente declarados nos arquivos `.gitignore` para nao serem expostos nos repositorios Git. -* Credenciais de banco residem exclusivamente em arquivos `.env.*` mantidos fora do controle de versao. -- 2.43.0