From a58ada2b349e5d0c0d24443841fac54e6f2e6708 Mon Sep 17 00:00:00 2001 From: Tiago Date: Tue, 13 Oct 2026 09:22:01 -0300 Subject: [PATCH] feat(master): listar todas as conexoes da Evolution, com opcoes de reativar para cliente e excluir --- .../integracao/EvolutionApiClient.java | 117 +++++++ .../master/http/MasterHandlers.java | 319 ++++++++++++++++++ .../condocontab/util/SimpleJson.java | 163 ++++++++- src/main/webapp/css/app.css | 5 + src/main/webapp/master/configuracoes.html | 136 +++++++- src/main/webapp/master/configuracoes.js | 316 ++++++++++++++++- 6 files changed, 1053 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/seconectai/condocontab/integracao/EvolutionApiClient.java b/src/main/java/com/seconectai/condocontab/integracao/EvolutionApiClient.java index c73f1b7..ef832c7 100644 --- a/src/main/java/com/seconectai/condocontab/integracao/EvolutionApiClient.java +++ b/src/main/java/com/seconectai/condocontab/integracao/EvolutionApiClient.java @@ -6,6 +6,8 @@ import com.google.zxing.BarcodeFormat; import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.qrcode.QRCodeWriter; +import com.seconectai.condocontab.util.SimpleJson; + import java.io.IOException; import java.io.ByteArrayOutputStream; import java.net.URI; @@ -14,8 +16,11 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.ArrayList; import java.util.Base64; import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; import java.util.Map; public final class EvolutionApiClient { @@ -29,6 +34,118 @@ public final class EvolutionApiClient { this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build(); } + public String defaultBaseUrl() { + return defaultBaseUrl; + } + + public List> listarInstancias(String baseUrl) throws IOException, InterruptedException { + if (baseUrl == null || baseUrl.isBlank()) { + baseUrl = defaultBaseUrl; + } + if (mockMode(baseUrl)) { + return List.of(); + } + HttpResponse resp; + try { + resp = sendNoBody("GET", clean(baseUrl) + "/instance/fetchInstances", globalApiKey); + } catch (IOException e) { + String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase(Locale.ROOT); + if (msg.contains("404")) { + resp = sendNoBody("GET", clean(baseUrl) + "/instance/all", globalApiKey); + } else { + throw e; + } + } + if (resp.statusCode() < 200 || resp.statusCode() >= 300) { + throw new IOException("Evolution API retornou HTTP " + resp.statusCode() + ": " + resp.body()); + } + return parseInstancesResponse(resp.body(), baseUrl); + } + + private List> parseInstancesResponse(String body, String baseUrl) { + if (body == null || body.isBlank()) { + return List.of(); + } + Object parsed = SimpleJson.parse(body); + List> out = new ArrayList<>(); + if (parsed instanceof List list) { + for (Object item : list) { + Map info = extrairInstanciaInfo(item, baseUrl); + if (info != null) { + out.add(info); + } + } + } else if (parsed instanceof Map map) { + Object inner = map.get("instances"); + if (inner == null) inner = map.get("data"); + if (inner instanceof List list) { + for (Object item : list) { + Map info = extrairInstanciaInfo(item, baseUrl); + if (info != null) { + out.add(info); + } + } + } else { + Map info = extrairInstanciaInfo(map, baseUrl); + if (info != null) { + out.add(info); + } + } + } + return out; + } + + private Map extrairInstanciaInfo(Object rawItem, String baseUrl) { + if (!(rawItem instanceof Map map)) { + return null; + } + Map data = map; + if (map.get("instance") instanceof Map inner) { + data = inner; + } + + String name = firstNonBlank(str(data.get("instanceName")), firstNonBlank(str(data.get("name")), str(map.get("instanceName")))); + if (name == null || name.isBlank()) { + name = str(map.get("name")); + } + if (name == null || name.isBlank()) { + return null; + } + + String state = firstNonBlank(str(data.get("connectionStatus")), firstNonBlank(str(data.get("status")), str(map.get("connectionStatus")))); + if (state == null || state.isBlank()) { + state = firstNonBlank(str(map.get("status")), "close"); + } + + String ownerRaw = firstNonBlank(str(data.get("owner")), firstNonBlank(str(data.get("ownerJid")), firstNonBlank(str(data.get("number")), str(map.get("owner"))))); + if (ownerRaw == null || ownerRaw.isBlank()) { + ownerRaw = str(map.get("ownerJid")); + } + String numero = null; + if (ownerRaw != null && !ownerRaw.isBlank()) { + String digits = onlyDigits(ownerRaw); + if (!digits.isBlank()) { + numero = digits; + } + } + + String profileName = firstNonBlank(str(data.get("profileName")), str(map.get("profileName"))); + String profilePictureUrl = firstNonBlank(str(data.get("profilePictureUrl")), str(map.get("profilePictureUrl"))); + + Map out = new LinkedHashMap<>(); + out.put("instance_name", name); + out.put("connection_status", state); + out.put("numero_whatsapp", numero); + out.put("profile_name", profileName); + out.put("profile_picture_url", profilePictureUrl); + out.put("instance_url", baseUrl); + return out; + } + + private String str(Object o) { + return o == null ? null : String.valueOf(o).trim(); + } + public CriacaoInstancia criarInstancia(String baseUrl, String instanceName, String token, String numero) throws IOException, InterruptedException { if (baseUrl == null || baseUrl.isBlank()) { baseUrl = defaultBaseUrl; diff --git a/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java b/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java index 5b7b6d0..8ef94a2 100644 --- a/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java +++ b/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java @@ -119,6 +119,26 @@ public final class MasterHandlers implements HttpHandler { excluirEvolutionInstancia(ex, parseId(parts[1]), parseId(parts[3])); return; } + if (parts.length == 1 && "evolution-instancias".equals(parts[0]) && "GET".equals(method)) { + listarTodasEvolutionInstancias(ex); + return; + } + if (parts.length == 2 && "evolution-instancias".equals(parts[0]) && "reativar".equals(parts[1]) && "POST".equals(method)) { + reativarEvolutionInstancia(ex); + return; + } + if (parts.length == 2 && "evolution-instancias".equals(parts[0]) && "DELETE".equals(method)) { + excluirEvolutionInstanciaPorNome(ex, parts[1]); + return; + } + if (parts.length == 3 && "evolution-instancias".equals(parts[0]) && "qrcode".equals(parts[2]) && "POST".equals(method)) { + obterQrCodeEvolutionPorNome(ex, parts[1]); + return; + } + if (parts.length == 3 && "evolution-instancias".equals(parts[0]) && "reiniciar".equals(parts[2]) && "POST".equals(method)) { + reiniciarEvolutionInstanciaPorNome(ex, parts[1]); + return; + } if (parts.length == 1 && "configuracoes".equals(parts[0])) { if ("GET".equals(method)) listarConfiguracoes(ex); else if ("PUT".equals(method)) salvarConfiguracoes(ex); @@ -340,6 +360,305 @@ public final class MasterHandlers implements HttpHandler { } } + private void listarTodasEvolutionInstancias(HttpExchange ex) throws Exception { + Map> empresasMap = new LinkedHashMap<>(); + List> empresasList = new ArrayList<>(); + try (Connection conn = dataSourceProvider.getMasterConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT id, nome, slug, status FROM empresas WHERE status <> 'excluida' ORDER BY nome")) { + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + Map emp = resultMap(rs); + empresasMap.put(rs.getLong("id"), emp); + empresasList.add(emp); + } + } + } + + List> dbRows = new ArrayList<>(); + try (Connection conn = dataSourceProvider.getMasterConnection(); + PreparedStatement ps = conn.prepareStatement( + "SELECT ei.id, ei.empresa_id, ei.instance_url, ei.instance_token_enc, ei.numero_whatsapp, ei.status, ei.solicitado_em, ei.aprovado_em, ei.aprovado_por, e.nome AS empresa_nome " + + "FROM evolution_instancias ei " + + "LEFT JOIN empresas e ON e.id = ei.empresa_id " + + "ORDER BY ei.solicitado_em DESC, ei.id DESC")) { + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + dbRows.add(resultMap(rs)); + } + } + } + + List> evoInstances = new ArrayList<>(); + String evolutionErro = null; + try { + evoInstances = evolutionApiClient.listarInstancias(null); + } catch (Exception err) { + evolutionErro = err.getMessage(); + } + + Map> combined = new LinkedHashMap<>(); + Set matchedDbIds = new HashSet<>(); + + for (Map evo : evoInstances) { + String instName = str(evo.get("instance_name")); + if (instName == null || instName.isBlank()) continue; + + Map item = new LinkedHashMap<>(evo); + item.put("origem_evolution", true); + + Map matchedDb = null; + for (Map db : dbRows) { + String tokenEnc = str(db.get("instance_token_enc")); + if (tokenEnc != null) { + String nameInDb = tokenEnc.split(":", 2)[0]; + if (instName.equalsIgnoreCase(nameInDb)) { + matchedDb = db; + break; + } + } + } + + if (matchedDb != null) { + matchedDbIds.add(((Number) matchedDb.get("id")).longValue()); + item.put("db_id", matchedDb.get("id")); + item.put("empresa_id", matchedDb.get("empresa_id")); + item.put("empresa_nome", matchedDb.get("empresa_nome")); + item.put("status", matchedDb.get("status")); + item.put("solicitado_em", matchedDb.get("solicitado_em")); + item.put("aprovado_em", matchedDb.get("aprovado_em")); + if ((item.get("numero_whatsapp") == null || str(item.get("numero_whatsapp")).isBlank()) && matchedDb.get("numero_whatsapp") != null) { + item.put("numero_whatsapp", matchedDb.get("numero_whatsapp")); + } + } else { + Long deducedEmpresaId = deduzirEmpresaIdPorNome(instName); + if (deducedEmpresaId != null && empresasMap.containsKey(deducedEmpresaId)) { + item.put("empresa_id", deducedEmpresaId); + item.put("empresa_nome", empresasMap.get(deducedEmpresaId).get("nome")); + item.put("status", "sem_vinculo"); + } else { + item.put("empresa_id", null); + item.put("empresa_nome", "Não vinculado"); + item.put("status", "sem_vinculo"); + } + } + + combined.put(instName.toLowerCase(Locale.ROOT), item); + } + + for (Map db : dbRows) { + long dbId = ((Number) db.get("id")).longValue(); + if (matchedDbIds.contains(dbId)) continue; + + String tokenEnc = str(db.get("instance_token_enc")); + String instName = (tokenEnc != null && !tokenEnc.isBlank()) ? tokenEnc.split(":", 2)[0] : ("instancia_" + dbId); + String key = instName.toLowerCase(Locale.ROOT); + + if (!combined.containsKey(key)) { + Map item = new LinkedHashMap<>(); + item.put("instance_name", instName); + item.put("connection_status", (evolutionErro != null) ? "evolution_indisponivel" : "nao_encontrada_na_evolution"); + item.put("numero_whatsapp", db.get("numero_whatsapp")); + item.put("profile_name", null); + item.put("profile_picture_url", null); + item.put("instance_url", db.get("instance_url")); + item.put("db_id", db.get("id")); + item.put("empresa_id", db.get("empresa_id")); + item.put("empresa_nome", db.get("empresa_nome")); + item.put("status", db.get("status")); + item.put("solicitado_em", db.get("solicitado_em")); + item.put("aprovado_em", db.get("aprovado_em")); + item.put("origem_evolution", false); + combined.put(key, item); + } + } + + Map response = new LinkedHashMap<>(); + response.put("instancias", new ArrayList<>(combined.values())); + response.put("empresas", empresasList); + response.put("evolution_status", (evolutionErro == null) ? "online" : "erro"); + if (evolutionErro != null) { + response.put("evolution_erro", evolutionErro); + } + HttpResponses.json(ex, 200, response); + } + + private void reativarEvolutionInstancia(HttpExchange ex) throws Exception { + Map b = body(ex); + String instanceName = b.get("instance_name"); + if (instanceName == null || instanceName.isBlank()) { + HttpResponses.json(ex, 400, Map.of("erro", "nome_instancia_obrigatorio")); + return; + } + long empresaId; + try { + empresaId = Long.parseLong(req(b, "empresa_id")); + } catch (Exception e) { + HttpResponses.json(ex, 400, Map.of("erro", "empresa_id_invalida")); + return; + } + + String empresaNome; + try (Connection conn = dataSourceProvider.getMasterConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT nome FROM empresas WHERE id=? AND status<>'excluida'")) { + ps.setLong(1, empresaId); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + HttpResponses.json(ex, 404, Map.of("erro", "empresa_nao_encontrada")); + return; + } + empresaNome = rs.getString("nome"); + } + } + + String numero = EvolutionApiClient.onlyDigits(b.get("numero_whatsapp")); + String defaultUrl = evolutionApiClient.defaultBaseUrl(); + + try (Connection conn = dataSourceProvider.getMasterConnection()) { + conn.setAutoCommit(false); + try { + if (numero != null && !numero.isBlank()) { + try (PreparedStatement ps = conn.prepareStatement( + "DELETE FROM evolution_instancias WHERE numero_whatsapp=? AND (instance_token_enc NOT LIKE ? OR instance_token_enc IS NULL)")) { + ps.setString(1, numero); + ps.setString(2, instanceName + "%"); + ps.executeUpdate(); + } + } + + Long existingId = null; + try (PreparedStatement ps = conn.prepareStatement( + "SELECT id, numero_whatsapp FROM evolution_instancias WHERE instance_token_enc LIKE ? OR instance_token_enc=? ORDER BY id DESC LIMIT 1")) { + ps.setString(1, instanceName + ":%"); + ps.setString(2, instanceName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + existingId = rs.getLong("id"); + if ((numero == null || numero.isBlank()) && rs.getString("numero_whatsapp") != null) { + numero = rs.getString("numero_whatsapp"); + } + } + } + } + + if (existingId != null) { + try (PreparedStatement ps = conn.prepareStatement( + "UPDATE evolution_instancias SET empresa_id=?, status='aprovada', aprovado_em=now(), aprovado_por=NULL, numero_whatsapp=COALESCE(NULLIF(?, ''), numero_whatsapp) WHERE id=?")) { + ps.setLong(1, empresaId); + ps.setString(2, numero == null ? "" : numero); + ps.setLong(3, existingId); + ps.executeUpdate(); + } + } else { + String tokenEnc = instanceName + ":" + UUID.randomUUID().toString().replace("-", ""); + try (PreparedStatement ps = conn.prepareStatement( + "INSERT INTO evolution_instancias (empresa_id, instance_url, instance_token_enc, numero_whatsapp, status, solicitado_em, aprovado_em) VALUES (?, ?, ?, NULLIF(?, ''), 'aprovada', now(), now())")) { + ps.setLong(1, empresaId); + ps.setString(2, defaultUrl); + ps.setString(3, tokenEnc); + ps.setString(4, numero == null ? "" : numero); + ps.executeUpdate(); + } + } + + ativarFuncionalidade(conn, empresaId, "whatsapp_envio"); + conn.commit(); + } catch (Exception err) { + conn.rollback(); + throw err; + } finally { + conn.setAutoCommit(true); + } + } + + try { + evolutionApiClient.reiniciar(defaultUrl, instanceName); + } catch (Exception ignored) {} + + HttpResponses.json(ex, 200, Map.of("ok", true, "mensagem", "Conexão de WhatsApp reativada com sucesso para " + empresaNome)); + } + + private void excluirEvolutionInstanciaPorNome(HttpExchange ex, String instanceName) throws Exception { + if (instanceName == null || instanceName.isBlank()) { + HttpResponses.json(ex, 400, Map.of("erro", "nome_instancia_obrigatorio")); + return; + } + String defaultUrl = evolutionApiClient.defaultBaseUrl(); + String evolutionErro = null; + try { + evolutionApiClient.excluirInstancia(defaultUrl, instanceName); + } catch (Exception err) { + evolutionErro = err.getMessage(); + } + + try (Connection conn = dataSourceProvider.getMasterConnection(); + PreparedStatement ps = conn.prepareStatement("DELETE FROM evolution_instancias WHERE instance_token_enc LIKE ? OR instance_token_enc=?")) { + ps.setString(1, instanceName + ":%"); + ps.setString(2, instanceName); + ps.executeUpdate(); + } + + Map resp = new LinkedHashMap<>(); + resp.put("ok", true); + resp.put("mensagem", "Instância '" + instanceName + "' excluída com sucesso."); + if (evolutionErro != null) { + resp.put("aviso_evolution", evolutionErro); + } + HttpResponses.json(ex, 200, resp); + } + + private void obterQrCodeEvolutionPorNome(HttpExchange ex, String instanceName) throws Exception { + if (instanceName == null || instanceName.isBlank()) { + HttpResponses.json(ex, 400, Map.of("erro", "nome_instancia_obrigatorio")); + return; + } + String numero = ""; + try (Connection conn = dataSourceProvider.getMasterConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT numero_whatsapp FROM evolution_instancias WHERE instance_token_enc LIKE ? OR instance_token_enc=? ORDER BY id DESC LIMIT 1")) { + ps.setString(1, instanceName + ":%"); + ps.setString(2, instanceName); + try (ResultSet rs = ps.executeQuery()) { + if (rs.next() && rs.getString("numero_whatsapp") != null) { + numero = rs.getString("numero_whatsapp"); + } + } + } + String defaultUrl = evolutionApiClient.defaultBaseUrl(); + Map qr = evolutionApiClient.conectar(defaultUrl, instanceName, numero); + HttpResponses.json(ex, 200, qr); + } + + private void reiniciarEvolutionInstanciaPorNome(HttpExchange ex, String instanceName) throws Exception { + if (instanceName == null || instanceName.isBlank()) { + HttpResponses.json(ex, 400, Map.of("erro", "nome_instancia_obrigatorio")); + return; + } + String defaultUrl = evolutionApiClient.defaultBaseUrl(); + Map res = evolutionApiClient.reiniciar(defaultUrl, instanceName); + HttpResponses.json(ex, 200, res); + } + + private Long deduzirEmpresaIdPorNome(String name) { + if (name == null) return null; + if (name.startsWith("empresa")) { + int underscore = name.indexOf('_'); + String idPart = (underscore > 7) ? name.substring(7, underscore) : name.substring(7); + try { + return Long.parseLong(idPart); + } catch (NumberFormatException ignored) {} + } + return null; + } + + private String req(Map m, String k) { + String v = m.get(k); + if (v == null || v.isBlank()) throw new IllegalArgumentException("Campo obrigatório: " + k); + return v; + } + + private String str(Object o) { + return o == null ? null : String.valueOf(o).trim(); + } + private void ativarFuncionalidade(Connection conn, long empresaId, String chave) throws SQLException { String sql = """ INSERT INTO empresa_funcionalidades (empresa_id, funcionalidade_id, ativa, liberado_em, liberado_por) diff --git a/src/main/java/com/seconectai/condocontab/util/SimpleJson.java b/src/main/java/com/seconectai/condocontab/util/SimpleJson.java index 247e528..c503c1c 100644 --- a/src/main/java/com/seconectai/condocontab/util/SimpleJson.java +++ b/src/main/java/com/seconectai/condocontab/util/SimpleJson.java @@ -1,6 +1,8 @@ package com.seconectai.condocontab.util; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; public final class SimpleJson { @@ -12,6 +14,14 @@ public final class SimpleJson { return parser.parseObject(); } + public static Object parse(String json) { + if (json == null || json.isBlank()) { + return null; + } + Parser parser = new Parser(json); + return parser.parseValue(); + } + private static final class Parser { private final String text; private int pos; @@ -62,6 +72,145 @@ public final class SimpleJson { return text.substring(start, pos).trim(); } + private Object parseValue() { + skipWs(); + char c = peek(); + if (c == '{') { + return parseGenericMap(); + } + if (c == '[') { + return parseGenericList(); + } + if (c == '"') { + return parseString(); + } + if (c == 't' || c == 'T') { + expectWord("true"); + return Boolean.TRUE; + } + if (c == 'f' || c == 'F') { + expectWord("false"); + return Boolean.FALSE; + } + if (c == 'n' || c == 'N') { + expectWord("null"); + return null; + } + if (c == '-' || (c >= '0' && c <= '9')) { + return parseNumber(); + } + return parseBareWord(); + } + + private Map parseGenericMap() { + Map map = new LinkedHashMap<>(); + expect('{'); + skipWs(); + if (peek() == '}') { + pos++; + return map; + } + while (true) { + skipWs(); + String key = parseString(); + skipWs(); + expect(':'); + skipWs(); + map.put(key, parseValue()); + skipWs(); + char c = peek(); + if (c == ',') { + pos++; + continue; + } + if (c == '}') { + pos++; + return map; + } + throw new IllegalArgumentException("JSON invalido na posicao " + pos); + } + } + + private List parseGenericList() { + List list = new ArrayList<>(); + expect('['); + skipWs(); + if (peek() == ']') { + pos++; + return list; + } + while (true) { + skipWs(); + list.add(parseValue()); + skipWs(); + char c = peek(); + if (c == ',') { + pos++; + continue; + } + if (c == ']') { + pos++; + return list; + } + throw new IllegalArgumentException("JSON invalido na posicao " + pos); + } + } + + private Object parseNumber() { + int start = pos; + if (peek() == '-') pos++; + while (pos < text.length() && Character.isDigit(text.charAt(pos))) { + pos++; + } + boolean isDecimal = false; + if (pos < text.length() && text.charAt(pos) == '.') { + isDecimal = true; + pos++; + while (pos < text.length() && Character.isDigit(text.charAt(pos))) { + pos++; + } + } + if (pos < text.length() && (text.charAt(pos) == 'e' || text.charAt(pos) == 'E')) { + isDecimal = true; + pos++; + if (pos < text.length() && (text.charAt(pos) == '+' || text.charAt(pos) == '-')) { + pos++; + } + while (pos < text.length() && Character.isDigit(text.charAt(pos))) { + pos++; + } + } + String raw = text.substring(start, pos); + if (isDecimal) { + try { + return Double.parseDouble(raw); + } catch (NumberFormatException ignored) {} + } else { + try { + return Long.parseLong(raw); + } catch (NumberFormatException ignored) {} + } + return raw; + } + + private String parseBareWord() { + int start = pos; + while (pos < text.length() && ",}]".indexOf(text.charAt(pos)) < 0) { + pos++; + } + return text.substring(start, pos).trim(); + } + + private void expectWord(String word) { + for (int i = 0; i < word.length(); i++) { + if (pos < text.length() && Character.toLowerCase(text.charAt(pos)) == Character.toLowerCase(word.charAt(i))) { + pos++; + } else { + return; + } + } + } + private String parseString() { expect('"'); StringBuilder out = new StringBuilder(); @@ -84,7 +233,19 @@ public final class SimpleJson { case 'n' -> out.append('\n'); case 'r' -> out.append('\r'); case 't' -> out.append('\t'); - default -> throw new IllegalArgumentException("Escape JSON nao suportado: " + escaped); + case 'u' -> { + if (pos + 4 > text.length()) { + throw new IllegalArgumentException("Unicode escape incompleto"); + } + String hex = text.substring(pos, pos + 4); + pos += 4; + try { + out.append((char) Integer.parseInt(hex, 16)); + } catch (NumberFormatException e) { + out.append("\\u").append(hex); + } + } + default -> out.append(escaped); } } else { out.append(c); diff --git a/src/main/webapp/css/app.css b/src/main/webapp/css/app.css index 228bf12..9a99392 100644 --- a/src/main/webapp/css/app.css +++ b/src/main/webapp/css/app.css @@ -628,6 +628,11 @@ th { color: var(--warn); } +.badge.danger { + background: var(--danger-soft); + color: var(--danger); +} + .toast, .finance-banner { position: fixed; diff --git a/src/main/webapp/master/configuracoes.html b/src/main/webapp/master/configuracoes.html index 8f3b396..11d7c1b 100644 --- a/src/main/webapp/master/configuracoes.html +++ b/src/main/webapp/master/configuracoes.html @@ -1 +1,135 @@ -Configurações

Configurações

E-mail

AbacatePay

Acessos WhatsApp

EmpresaNúmeroStatusSolicitado emAções
\ No newline at end of file + + + + + + Configurações + + + + + + + +
+
+

Configurações

+
+ +
+

E-mail

+
+
+
+
+
+
+
+ +
+ +
+
+
+ +
+

AbacatePay

+
+
+
+ +
+
+
+ +
+
+

Acessos WhatsApp (Evolution API)

+
+ + +
+
+
+ + + + + + + + + + + + +
InstânciaEmpresa / ClienteNúmeroStatus ConexãoStatus PlataformaAções
+
+
+ + + + + + +
+ + + + \ No newline at end of file diff --git a/src/main/webapp/master/configuracoes.js b/src/main/webapp/master/configuracoes.js index efe4ac4..62c2774 100644 --- a/src/main/webapp/master/configuracoes.js +++ b/src/main/webapp/master/configuracoes.js @@ -1 +1,315 @@ -const emailForm=document.querySelector('#email-form'),abacateForm=document.querySelector('#abacate-form'),emailRows=document.querySelector('#email-rows'),abacateRows=document.querySelector('#abacate-rows'),evolutionRows=document.querySelector('#evolution-rows');let empresas=[];const emailKeys=['smtp_from','smtp_host','smtp_password','smtp_port','smtp_starttls','smtp_user'];const abacateKeys=['abacatepay_api_key','abacatepay_webhook_secret'];function renderConfig(target,cfg,keys,labels){target.innerHTML=keys.map(k=>`

${labels[k]||k}: ${cfg[k]||''}

`).join('')}async function load(){try{const d=await Api.get('/api/master/configuracoes');const cfg=d.configuracoes||{};for(const [k,v] of Object.entries(cfg)){if(emailForm.elements[k])emailForm.elements[k].value=v;if(abacateForm.elements[k])abacateForm.elements[k].value=v}renderConfig(emailRows,cfg,emailKeys,{smtp_from:'Remetente',smtp_host:'Host',smtp_password:'Senha',smtp_port:'Porta',smtp_starttls:'StartTLS',smtp_user:'Usuário'});renderConfig(abacateRows,cfg,abacateKeys,{abacatepay_api_key:'API Key',abacatepay_webhook_secret:'Webhook Secret'});await loadEvolution()}catch(e){Util.toast(e.message,true)}}async function loadEvolution(){const d=await Api.get('/api/master/empresas');empresas=d.empresas||[];const grupos=await Promise.all(empresas.map(async e=>{try{const r=await Api.get(`/api/master/empresas/${e.id}/evolution-instancias`);return (r.instancias||[]).map(i=>({...i,empresa_nome:e.nome}))}catch(_){return[]}}));const instancias=grupos.flat();evolutionRows.innerHTML=instancias.length?instancias.map(i=>`${i.empresa_nome}${i.numero_whatsapp||'-'}${Util.badge(i.status)}${Util.date(i.solicitado_em)} `).join(''):'Nenhuma solicitação encontrada.'}async function aprovar(emp,id){try{await Api.post(`/api/master/empresas/${emp}/evolution-instancias/${id}/aprovar`,{});Util.toast('Acesso liberado');loadEvolution()}catch(e){Util.toast(e.message,true)}}async function rejeitar(emp,id){try{await Api.post(`/api/master/empresas/${emp}/evolution-instancias/${id}/rejeitar`,{});Util.toast('Acesso rejeitado');loadEvolution()}catch(e){Util.toast(e.message,true)}}async function excluir(emp,id){if(!await Util.confirm('Excluir esta conexão de WhatsApp?','Excluir conexão'))return;try{await Api.request(`/api/master/empresas/${emp}/evolution-instancias/${id}`,{method:'DELETE'});Util.toast('Conexão excluída');loadEvolution()}catch(e){Util.toast(e.message,true)}}emailForm.onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/master/configuracoes',Util.formData(emailForm));Util.toast('Configurações de e-mail salvas');load()}catch(err){Util.toast(err.message,true)}};abacateForm.onsubmit=async e=>{e.preventDefault();try{await Api.put('/api/master/configuracoes',Util.formData(abacateForm));Util.toast('Configurações da AbacatePay salvas');load()}catch(err){Util.toast(err.message,true)}};load(); +const emailForm = document.querySelector('#email-form'), + abacateForm = document.querySelector('#abacate-form'), + emailRows = document.querySelector('#email-rows'), + abacateRows = document.querySelector('#abacate-rows'), + evolutionRows = document.querySelector('#evolution-rows'), + evolutionStatusBadge = document.querySelector('#evolution-status-badge'), + reativarModal = document.querySelector('#reativar-modal'), + reativarForm = document.querySelector('#reativar-form'), + reativarEmpresaSelect = document.querySelector('#reativar-empresa-id'), + qrModal = document.querySelector('#qr-modal'), + qrModalImg = document.querySelector('#qr-modal-img'), + qrLoading = document.querySelector('#qr-loading'), + qrPairingBox = document.querySelector('#qr-pairing-box'), + qrModalPairing = document.querySelector('#qr-modal-pairing'), + qrModalTitle = document.querySelector('#qr-modal-title'); + +let empresas = []; + +const emailKeys = ['smtp_from', 'smtp_host', 'smtp_password', 'smtp_port', 'smtp_starttls', 'smtp_user']; +const abacateKeys = ['abacatepay_api_key', 'abacatepay_webhook_secret']; + +function renderConfig(target, cfg, keys, labels) { + target.innerHTML = keys.map(k => `

${labels[k] || k}: ${cfg[k] || ''}

`).join(''); +} + +function formatPhone(raw) { + if (!raw) return 'Não conectado'; + const d = String(raw).replace(/\D/g, ''); + if (d.length === 13 && d.startsWith('55')) { + return `+55 (${d.slice(2, 4)}) ${d.slice(4, 9)}-${d.slice(9)}`; + } + if (d.length === 12 && d.startsWith('55')) { + return `+55 (${d.slice(2, 4)}) ${d.slice(4, 8)}-${d.slice(8)}`; + } + if (d.length === 11) { + return `(${d.slice(0, 2)}) ${d.slice(2, 7)}-${d.slice(7)}`; + } + if (d.length === 10) { + return `(${d.slice(0, 2)}) ${d.slice(2, 6)}-${d.slice(6)}`; + } + return d || 'Não conectado'; +} + +function connectionBadge(state) { + const s = String(state || '').toLowerCase(); + if (s === 'open' || s === 'conectado') { + return 'Conectado (open)'; + } + if (s === 'close' || s === 'desconectado') { + return 'Desconectado (close)'; + } + if (s === 'connecting' || s === 'conectando') { + return 'Conectando'; + } + if (s === 'nao_encontrada_na_evolution') { + return 'Inexistente na Evolution'; + } + if (s === 'evolution_indisponivel') { + return 'Evolution Indisponível'; + } + return `${state || '-'}`; +} + +function platformBadge(status) { + const s = String(status || '').toLowerCase(); + if (s === 'aprovada' || s === 'ativa') { + return 'Aprovada'; + } + if (s === 'pendente') { + return 'Pendente'; + } + if (s === 'rejeitada') { + return 'Rejeitada'; + } + if (s === 'sem_vinculo') { + return 'Sem vínculo'; + } + return Util.badge(status); +} + +async function load() { + try { + const d = await Api.get('/api/master/configuracoes'); + const cfg = d.configuracoes || {}; + for (const [k, v] of Object.entries(cfg)) { + if (emailForm.elements[k]) emailForm.elements[k].value = v; + if (abacateForm.elements[k]) abacateForm.elements[k].value = v; + } + renderConfig(emailRows, cfg, emailKeys, { + smtp_from: 'Remetente', + smtp_host: 'Host', + smtp_password: 'Senha', + smtp_port: 'Porta', + smtp_starttls: 'StartTLS', + smtp_user: 'Usuário' + }); + renderConfig(abacateRows, cfg, abacateKeys, { + abacatepay_api_key: 'API Key', + abacatepay_webhook_secret: 'Webhook Secret' + }); + await loadEvolution(); + } catch (e) { + Util.toast(e.message, true); + } +} + +async function loadEvolution() { + evolutionRows.innerHTML = 'Carregando conexões da Evolution...'; + try { + const d = await Api.get('/api/master/evolution-instancias'); + empresas = d.empresas || []; + const instancias = d.instancias || []; + + if (evolutionStatusBadge) { + if (d.evolution_status === 'online') { + evolutionStatusBadge.innerHTML = 'Evolution Conectada'; + } else { + evolutionStatusBadge.innerHTML = `Evolution Offline/Aviso`; + } + } + + if (!instancias.length) { + evolutionRows.innerHTML = 'Nenhuma conexão de WhatsApp encontrada na Evolution ou no sistema.'; + return; + } + + evolutionRows.innerHTML = instancias.map(i => { + const instName = i.instance_name || '-'; + const safeName = String(instName).replace(/'/g, "\\'"); + const empresaNome = i.empresa_nome ? `${i.empresa_nome}` : 'Não vinculado'; + const numFmt = formatPhone(i.numero_whatsapp); + const connBadge = connectionBadge(i.connection_status); + const platBadge = platformBadge(i.status); + + let btns = []; + + // Botão Reativar para Cliente (sempre disponível) + btns.push(``); + + // Se pendente com empresa vinculada, botão aprovar rápido + if (i.status === 'pendente' && i.empresa_id && i.db_id) { + btns.push(``); + } + + // Se não conectado na Evolution, botões de QR Code e Reiniciar + if (String(i.connection_status).toLowerCase() !== 'open') { + btns.push(``); + btns.push(``); + } + + // Botão Excluir da Evolution (sempre disponível) + btns.push(``); + + return ` + ${instName} + ${empresaNome} + ${numFmt} + ${connBadge} + ${platBadge} + ${btns.join(' ')} + `; + }).join(''); + } catch (err) { + if (evolutionStatusBadge) { + evolutionStatusBadge.innerHTML = 'Falha ao carregar'; + } + evolutionRows.innerHTML = `Erro ao carregar conexões da Evolution: ${err.message}`; + Util.toast(err.message, true); + } +} + +function abrirModalReativar(instanceName, empresaId, numero) { + document.querySelector('#reativar-instance-name').value = instanceName; + document.querySelector('#reativar-numero').value = numero || ''; + + reativarEmpresaSelect.innerHTML = '' + + empresas.map(e => ``).join(''); + + reativarModal.classList.remove('hidden'); +} + +function fecharModalReativar() { + reativarModal.classList.add('hidden'); +} + +async function salvarReativacao(e) { + e.preventDefault(); + const form = e.target; + const instanceName = form.instance_name.value; + const empresaId = form.empresa_id.value; + const numero = form.numero_whatsapp.value; + + if (!empresaId) { + Util.toast('Selecione um cliente para vincular a conexão', true); + return; + } + + try { + const res = await Api.post('/api/master/evolution-instancias/reativar', { + instance_name: instanceName, + empresa_id: empresaId, + numero_whatsapp: numero + }); + Util.toast(res.mensagem || 'Conexão reativada com sucesso'); + fecharModalReativar(); + loadEvolution(); + } catch (err) { + Util.toast(err.message, true); + } +} + +async function excluirEvolution(instanceName) { + const ok = await Util.confirm( + `Tem certeza que deseja excluir a conexão "${instanceName}" da Evolution API? Esta ação removerá a instância definitivamente.`, + 'Excluir da Evolution' + ); + if (!ok) return; + + try { + const res = await Api.request(`/api/master/evolution-instancias/${encodeURIComponent(instanceName)}`, { + method: 'DELETE' + }); + Util.toast(res.mensagem || 'Instância excluída com sucesso'); + loadEvolution(); + } catch (err) { + Util.toast(err.message, true); + } +} + +async function reiniciarEvolution(instanceName) { + try { + await Api.post(`/api/master/evolution-instancias/${encodeURIComponent(instanceName)}/reiniciar`, {}); + Util.toast('Instância reiniciada na Evolution'); + setTimeout(loadEvolution, 1500); + } catch (err) { + Util.toast(err.message, true); + } +} + +async function verQrCode(instanceName) { + qrModalTitle.textContent = `QR Code: ${instanceName}`; + qrModalImg.classList.add('hidden'); + qrPairingBox.classList.add('hidden'); + qrLoading.classList.remove('hidden'); + qrLoading.textContent = 'Solicitando QR Code à Evolution...'; + qrModal.classList.remove('hidden'); + + try { + const d = await Api.post(`/api/master/evolution-instancias/${encodeURIComponent(instanceName)}/qrcode`, {}); + qrLoading.classList.add('hidden'); + const base64 = d.qrcode_base64 || d.qr_code_base64; + if (base64) { + qrModalImg.src = base64.startsWith('data:image/') ? base64 : ('data:image/png;base64,' + base64); + qrModalImg.classList.remove('hidden'); + } else { + qrLoading.textContent = 'Evolution não gerou imagem de QR Code nesta tentativa. Tente clicar em Reiniciar.'; + qrLoading.classList.remove('hidden'); + } + if (d.pairing_code) { + qrModalPairing.value = d.pairing_code; + qrPairingBox.classList.remove('hidden'); + } + } catch (err) { + qrLoading.textContent = `Erro ao obter QR Code: ${err.message}`; + Util.toast(err.message, true); + } +} + +function fecharModalQr() { + qrModal.classList.add('hidden'); +} + +async function aprovar(emp, id) { + try { + await Api.post(`/api/master/empresas/${emp}/evolution-instancias/${id}/aprovar`, {}); + Util.toast('Acesso liberado'); + loadEvolution(); + } catch (e) { + Util.toast(e.message, true); + } +} + +async function rejeitar(emp, id) { + try { + await Api.post(`/api/master/empresas/${emp}/evolution-instancias/${id}/rejeitar`, {}); + Util.toast('Acesso rejeitado'); + loadEvolution(); + } catch (e) { + Util.toast(e.message, true); + } +} + +emailForm.onsubmit = async e => { + e.preventDefault(); + try { + await Api.put('/api/master/configuracoes', Util.formData(emailForm)); + Util.toast('Configurações de e-mail salvas'); + load(); + } catch (err) { + Util.toast(err.message, true); + } +}; + +abacateForm.onsubmit = async e => { + e.preventDefault(); + try { + await Api.put('/api/master/configuracoes', Util.formData(abacateForm)); + Util.toast('Configurações da AbacatePay salvas'); + load(); + } catch (err) { + Util.toast(err.message, true); + } +}; + +load();