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