feat(master): listar todas as conexoes da Evolution, com opcoes de reativar para cliente e excluir
This commit is contained in:
@@ -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<Map<String, Object>> listarInstancias(String baseUrl) throws IOException, InterruptedException {
|
||||
if (baseUrl == null || baseUrl.isBlank()) {
|
||||
baseUrl = defaultBaseUrl;
|
||||
}
|
||||
if (mockMode(baseUrl)) {
|
||||
return List.of();
|
||||
}
|
||||
HttpResponse<String> 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<Map<String, Object>> parseInstancesResponse(String body, String baseUrl) {
|
||||
if (body == null || body.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
Object parsed = SimpleJson.parse(body);
|
||||
List<Map<String, Object>> out = new ArrayList<>();
|
||||
if (parsed instanceof List<?> list) {
|
||||
for (Object item : list) {
|
||||
Map<String, Object> 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<String, Object> info = extrairInstanciaInfo(item, baseUrl);
|
||||
if (info != null) {
|
||||
out.add(info);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Map<String, Object> info = extrairInstanciaInfo(map, baseUrl);
|
||||
if (info != null) {
|
||||
out.add(info);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private Map<String, Object> 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<String, Object> 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;
|
||||
|
||||
@@ -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<Long, Map<String, Object>> empresasMap = new LinkedHashMap<>();
|
||||
List<Map<String, Object>> 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<String, Object> emp = resultMap(rs);
|
||||
empresasMap.put(rs.getLong("id"), emp);
|
||||
empresasList.add(emp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, Object>> 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<Map<String, Object>> evoInstances = new ArrayList<>();
|
||||
String evolutionErro = null;
|
||||
try {
|
||||
evoInstances = evolutionApiClient.listarInstancias(null);
|
||||
} catch (Exception err) {
|
||||
evolutionErro = err.getMessage();
|
||||
}
|
||||
|
||||
Map<String, Map<String, Object>> combined = new LinkedHashMap<>();
|
||||
Set<Long> matchedDbIds = new HashSet<>();
|
||||
|
||||
for (Map<String, Object> evo : evoInstances) {
|
||||
String instName = str(evo.get("instance_name"));
|
||||
if (instName == null || instName.isBlank()) continue;
|
||||
|
||||
Map<String, Object> item = new LinkedHashMap<>(evo);
|
||||
item.put("origem_evolution", true);
|
||||
|
||||
Map<String, Object> matchedDb = null;
|
||||
for (Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, String> 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)
|
||||
|
||||
@@ -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<String, Object> parseGenericMap() {
|
||||
Map<String, Object> 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<Object> parseGenericList() {
|
||||
List<Object> 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);
|
||||
|
||||
@@ -628,6 +628,11 @@ th {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.badge.danger {
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.toast,
|
||||
.finance-banner {
|
||||
position: fixed;
|
||||
|
||||
@@ -1 +1,135 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Configurações</title><link rel="stylesheet" href="/css/app.css"><script src="/js/auth.js"></script><script src="/js/util.js"></script><script src="/js/api.js"></script><script src="/js/layout.js"></script></head><body><main><div class="topbar"><h1>Configurações</h1></div><section class="panel"><h2>E-mail</h2><form id="email-form" class="grid cols"><div class="field"><label>Host</label><input name="smtp_host"></div><div class="field"><label>Porta</label><input name="smtp_port" type="number"></div><div class="field"><label>Usuário</label><input name="smtp_user"></div><div class="field"><label>Senha</label><input name="smtp_password" type="password"></div><div class="field"><label>Remetente</label><input name="smtp_from"></div><div class="field"><label>StartTLS</label><select name="smtp_starttls"><option value="true">Sim</option><option value="false">Não</option></select></div><button class="btn">Salvar e-mail</button></form><div id="email-rows"></div></section><section class="panel"><h2>AbacatePay</h2><form id="abacate-form" class="grid cols"><div class="field"><label>API Key</label><input name="abacatepay_api_key" type="password" placeholder="Cole a chave da AbacatePay"></div><div class="field"><label>Webhook Secret</label><input name="abacatepay_webhook_secret" type="password" placeholder="Segredo do webhook"></div><button class="btn">Salvar AbacatePay</button></form><div id="abacate-rows"></div></section><section class="panel"><h2>Acessos WhatsApp</h2><div class="table-wrap"><table><thead><tr><th>Empresa</th><th>Número</th><th>Status</th><th>Solicitado em</th><th>Ações</th></tr></thead><tbody id="evolution-rows"></tbody></table></div></section></main><script>Layout.mount('master','configuracoes');</script><script src="/master/configuracoes.js"></script></body></html>
|
||||
<!doctype html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Configurações</title>
|
||||
<link rel="stylesheet" href="/css/app.css">
|
||||
<script src="/js/auth.js"></script>
|
||||
<script src="/js/util.js"></script>
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/layout.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<div class="topbar">
|
||||
<h1>Configurações</h1>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<h2>E-mail</h2>
|
||||
<form id="email-form" class="grid cols">
|
||||
<div class="field"><label>Host</label><input name="smtp_host"></div>
|
||||
<div class="field"><label>Porta</label><input name="smtp_port" type="number"></div>
|
||||
<div class="field"><label>Usuário</label><input name="smtp_user"></div>
|
||||
<div class="field"><label>Senha</label><input name="smtp_password" type="password"></div>
|
||||
<div class="field"><label>Remetente</label><input name="smtp_from"></div>
|
||||
<div class="field"><label>StartTLS</label>
|
||||
<select name="smtp_starttls">
|
||||
<option value="true">Sim</option>
|
||||
<option value="false">Não</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn">Salvar e-mail</button>
|
||||
</form>
|
||||
<div id="email-rows"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>AbacatePay</h2>
|
||||
<form id="abacate-form" class="grid cols">
|
||||
<div class="field"><label>API Key</label><input name="abacatepay_api_key" type="password" placeholder="Cole a chave da AbacatePay"></div>
|
||||
<div class="field"><label>Webhook Secret</label><input name="abacatepay_webhook_secret" type="password" placeholder="Segredo do webhook"></div>
|
||||
<button class="btn">Salvar AbacatePay</button>
|
||||
</form>
|
||||
<div id="abacate-rows"></div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<div class="row" style="align-items: center; justify-content: space-between; margin-bottom: 16px;">
|
||||
<h2>Acessos WhatsApp (Evolution API)</h2>
|
||||
<div class="row" style="align-items: center; gap: 8px;">
|
||||
<span id="evolution-status-badge"></span>
|
||||
<button class="btn secondary" type="button" onclick="loadEvolution()">Atualizar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Instância</th>
|
||||
<th>Empresa / Cliente</th>
|
||||
<th>Número</th>
|
||||
<th>Status Conexão</th>
|
||||
<th>Status Plataforma</th>
|
||||
<th>Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="evolution-rows"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Modal Reativar para Cliente -->
|
||||
<div id="reativar-modal" class="modal hidden">
|
||||
<div class="modal-backdrop" onclick="fecharModalReativar()"></div>
|
||||
<section class="modal-panel" style="max-width: 520px;">
|
||||
<div class="modal-head">
|
||||
<h2>Reativar Conexão WhatsApp</h2>
|
||||
<button class="btn secondary" type="button" onclick="fecharModalReativar()">✕</button>
|
||||
</div>
|
||||
<form id="reativar-form" onsubmit="salvarReativacao(event)">
|
||||
<div class="field" style="margin-bottom: 14px;">
|
||||
<label>Instância da Evolution</label>
|
||||
<input id="reativar-instance-name" name="instance_name" readonly style="background: var(--bg-soft, #f4f6f8); font-weight: 700;">
|
||||
</div>
|
||||
<div class="field" style="margin-bottom: 14px;">
|
||||
<label>Cliente / Condomínio Destino</label>
|
||||
<select id="reativar-empresa-id" name="empresa_id" required>
|
||||
<option value="">Selecione um cliente...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" style="margin-bottom: 16px;">
|
||||
<label>Número WhatsApp (opcional - com DDD)</label>
|
||||
<input id="reativar-numero" name="numero_whatsapp" placeholder="Ex: 5511999999999">
|
||||
<small style="color: var(--muted-strong); font-size: 12px; margin-top: 4px; display: block;">
|
||||
Deixe em branco para manter o número já detectado na instância ou caso o cliente vá conectar via QR Code.
|
||||
</small>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn secondary" type="button" onclick="fecharModalReativar()">Cancelar</button>
|
||||
<button class="btn" type="submit">Reativar para este Cliente</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Modal QR Code -->
|
||||
<div id="qr-modal" class="modal hidden">
|
||||
<div class="modal-backdrop" onclick="fecharModalQr()"></div>
|
||||
<section class="modal-panel" style="max-width: 440px; text-align: center;">
|
||||
<div class="modal-head">
|
||||
<h2 id="qr-modal-title">QR Code WhatsApp</h2>
|
||||
<button class="btn secondary" type="button" onclick="fecharModalQr()">✕</button>
|
||||
</div>
|
||||
<div style="padding: 16px 0;">
|
||||
<p id="qr-modal-desc" style="margin-bottom: 12px; font-size: 14px; color: var(--muted-strong);">
|
||||
Escaneie com o WhatsApp no celular para conectar:
|
||||
</p>
|
||||
<div id="qr-loading" style="padding: 24px; color: var(--muted-strong);">Carregando QR Code...</div>
|
||||
<img id="qr-modal-img" class="qr hidden" alt="QR Code" style="margin: 0 auto; max-width: 260px; display: block; border-radius: 8px;">
|
||||
<div id="qr-pairing-box" class="field hidden" style="margin-top: 16px; text-align: left;">
|
||||
<label>Código de pareamento</label>
|
||||
<input id="qr-modal-pairing" readonly style="font-size: 16px; font-weight: bold; text-align: center; letter-spacing: 2px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions" style="justify-content: center;">
|
||||
<button class="btn secondary" type="button" onclick="fecharModalQr()">Fechar</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<script>Layout.mount('master','configuracoes');</script>
|
||||
<script src="/master/configuracoes.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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=>`<p><strong>${labels[k]||k}</strong>: ${cfg[k]||''}</p>`).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=>`<tr><td>${i.empresa_nome}</td><td>${i.numero_whatsapp||'-'}</td><td>${Util.badge(i.status)}</td><td>${Util.date(i.solicitado_em)}</td><td><button class="btn secondary" onclick="aprovar(${i.empresa_id},${i.id})">Aprovar</button> <button class="btn danger" onclick="rejeitar(${i.empresa_id},${i.id})">Rejeitar</button> <button class="btn danger" onclick="excluir(${i.empresa_id},${i.id})">Excluir</button></td></tr>`).join(''):'<tr><td colspan="5">Nenhuma solicitação encontrada.</td></tr>'}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 => `<p><strong>${labels[k] || k}</strong>: ${cfg[k] || ''}</p>`).join('');
|
||||
}
|
||||
|
||||
function formatPhone(raw) {
|
||||
if (!raw) return '<span class="badge warn">Não conectado</span>';
|
||||
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 || '<span class="badge warn">Não conectado</span>';
|
||||
}
|
||||
|
||||
function connectionBadge(state) {
|
||||
const s = String(state || '').toLowerCase();
|
||||
if (s === 'open' || s === 'conectado') {
|
||||
return '<span class="badge ok">Conectado (open)</span>';
|
||||
}
|
||||
if (s === 'close' || s === 'desconectado') {
|
||||
return '<span class="badge warn">Desconectado (close)</span>';
|
||||
}
|
||||
if (s === 'connecting' || s === 'conectando') {
|
||||
return '<span class="badge warn">Conectando</span>';
|
||||
}
|
||||
if (s === 'nao_encontrada_na_evolution') {
|
||||
return '<span class="badge">Inexistente na Evolution</span>';
|
||||
}
|
||||
if (s === 'evolution_indisponivel') {
|
||||
return '<span class="badge warn">Evolution Indisponível</span>';
|
||||
}
|
||||
return `<span class="badge">${state || '-'}</span>`;
|
||||
}
|
||||
|
||||
function platformBadge(status) {
|
||||
const s = String(status || '').toLowerCase();
|
||||
if (s === 'aprovada' || s === 'ativa') {
|
||||
return '<span class="badge ok">Aprovada</span>';
|
||||
}
|
||||
if (s === 'pendente') {
|
||||
return '<span class="badge warn">Pendente</span>';
|
||||
}
|
||||
if (s === 'rejeitada') {
|
||||
return '<span class="badge">Rejeitada</span>';
|
||||
}
|
||||
if (s === 'sem_vinculo') {
|
||||
return '<span class="badge">Sem vínculo</span>';
|
||||
}
|
||||
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 = '<tr><td colspan="6" style="text-align:center; padding: 20px;">Carregando conexões da Evolution...</td></tr>';
|
||||
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 = '<span class="badge ok">Evolution Conectada</span>';
|
||||
} else {
|
||||
evolutionStatusBadge.innerHTML = `<span class="badge warn" title="${d.evolution_erro || ''}">Evolution Offline/Aviso</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!instancias.length) {
|
||||
evolutionRows.innerHTML = '<tr><td colspan="6">Nenhuma conexão de WhatsApp encontrada na Evolution ou no sistema.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
evolutionRows.innerHTML = instancias.map(i => {
|
||||
const instName = i.instance_name || '-';
|
||||
const safeName = String(instName).replace(/'/g, "\\'");
|
||||
const empresaNome = i.empresa_nome ? `<strong>${i.empresa_nome}</strong>` : '<span class="badge">Não vinculado</span>';
|
||||
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(`<button class="btn secondary" type="button" onclick="abrirModalReativar('${safeName}', ${i.empresa_id || 'null'}, '${i.numero_whatsapp || ''}')">Reativar</button>`);
|
||||
|
||||
// Se pendente com empresa vinculada, botão aprovar rápido
|
||||
if (i.status === 'pendente' && i.empresa_id && i.db_id) {
|
||||
btns.push(`<button class="btn secondary" type="button" onclick="aprovar(${i.empresa_id}, ${i.db_id})">Aprovar</button>`);
|
||||
}
|
||||
|
||||
// Se não conectado na Evolution, botões de QR Code e Reiniciar
|
||||
if (String(i.connection_status).toLowerCase() !== 'open') {
|
||||
btns.push(`<button class="btn secondary" type="button" onclick="verQrCode('${safeName}')">QR Code</button>`);
|
||||
btns.push(`<button class="btn secondary" type="button" onclick="reiniciarEvolution('${safeName}')">Reiniciar</button>`);
|
||||
}
|
||||
|
||||
// Botão Excluir da Evolution (sempre disponível)
|
||||
btns.push(`<button class="btn danger" type="button" onclick="excluirEvolution('${safeName}')">Excluir da Evolution</button>`);
|
||||
|
||||
return `<tr>
|
||||
<td><code>${instName}</code></td>
|
||||
<td>${empresaNome}</td>
|
||||
<td>${numFmt}</td>
|
||||
<td>${connBadge}</td>
|
||||
<td>${platBadge}</td>
|
||||
<td style="white-space: nowrap;">${btns.join(' ')}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
} catch (err) {
|
||||
if (evolutionStatusBadge) {
|
||||
evolutionStatusBadge.innerHTML = '<span class="badge warn">Falha ao carregar</span>';
|
||||
}
|
||||
evolutionRows.innerHTML = `<tr><td colspan="6" style="color:var(--danger)">Erro ao carregar conexões da Evolution: ${err.message}</td></tr>`;
|
||||
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 = '<option value="">Selecione um cliente...</option>' +
|
||||
empresas.map(e => `<option value="${e.id}" ${empresaId == e.id ? 'selected' : ''}>${e.nome}</option>`).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();
|
||||
|
||||
Reference in New Issue
Block a user