feat(cliente): exibir conexoes WhatsApp em cards com icones de acao e popups animados
This commit is contained in:
@@ -460,10 +460,66 @@ public final class ClienteHandlers implements HttpHandler {
|
||||
private void conectarEvolution(HttpExchange ex,long empresaId)throws Exception{Map<String,String>b=body(ex);String url=b.getOrDefault("instance_url","");String numero=EvolutionApiClient.onlyDigits(req(b,"numero_whatsapp"));if(numero.length()<10){HttpResponses.json(ex,400,Map.of("erro","numero_whatsapp_invalido","mensagem","Informe o número com DDD e código do país."));return;}try(Connection m=masterDs.getMasterConnection()){liberarCadastrosEvolutionOrfaos(m,numero);Map<String,Object> existente=one(m,"SELECT id,empresa_id,status FROM evolution_instancias WHERE numero_whatsapp=? ORDER BY id DESC LIMIT 1",Arrays.asList(numero));if(existente!=null){HttpResponses.json(ex,409,Map.of("erro","numero_whatsapp_ja_cadastrado","mensagem","Este número de WhatsApp já está cadastrado em uma conexão."));return;}}String instance="empresa"+empresaId+"_"+System.currentTimeMillis();String token=UUID.randomUUID().toString().replace("-","");var criada=evolutionApiClient.criarInstancia(url,instance,token,numero);try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("INSERT INTO evolution_instancias (empresa_id,instance_url,instance_token_enc,numero_whatsapp,status) VALUES (?,?,?,?, 'pendente') RETURNING id")){ps.setLong(1,empresaId);ps.setString(2,url);ps.setString(3,criada.instanceName()+":"+criada.token());ps.setString(4,numero);try(ResultSet rs=ps.executeQuery()){rs.next();Map<String,Object> resp=new LinkedHashMap<>();resp.put("id",rs.getLong(1));resp.put("instance_name",criada.instanceName());resp.put("status","pendente");resp.put("qrcode_base64",criada.qrcodeBase64());resp.put("mock",criada.mock());HttpResponses.json(ex,201,resp);}}}
|
||||
private void liberarCadastrosEvolutionOrfaos(Connection m,String numero)throws Exception{List<Map<String,Object>> existentes=rows(m,"SELECT id,instance_url,instance_token_enc FROM evolution_instancias WHERE numero_whatsapp=? ORDER BY id",Arrays.asList(numero));for(Map<String,Object> inst:existentes){String token=str(inst.get("instance_token_enc"));String instanceName=token.split(":",2)[0];if(instanceName.isBlank())continue;try{evolutionApiClient.estado(str(inst.get("instance_url")),instanceName);}catch(Exception err){if(!evolutionNaoEncontrada(err))throw err;try(PreparedStatement ps=m.prepareStatement("DELETE FROM evolution_instancias WHERE id=?")){ps.setLong(1,((Number)inst.get("id")).longValue());ps.executeUpdate();}}}}
|
||||
private boolean evolutionNaoEncontrada(Exception err){String msg=err.getMessage()==null?"":err.getMessage().toLowerCase(Locale.ROOT);return msg.contains("404")||msg.contains("not found")||msg.contains("instance not found")||msg.contains("nao encontrada")||msg.contains("não encontrada");}
|
||||
private void statusEvolution(HttpExchange ex,long empresaId)throws Exception{Map<String,Object> inst=evolutionInstancia(empresaId);if(inst==null){HttpResponses.json(ex,200,Map.of("status","nao_configurada"));return;}Map<String,Object> resp=new LinkedHashMap<>();resp.put("instancia",inst);try{String[] parts=String.valueOf(inst.get("instance_token_enc")).split(":",2);resp.put("evolution",evolutionApiClient.estado(str(inst.get("instance_url")),parts[0]));}catch(Exception ignored){}HttpResponses.json(ex,200,resp);}
|
||||
private void qrcodeEvolution(HttpExchange ex,long empresaId)throws Exception{Map<String,Object> inst=evolutionInstancia(empresaId);if(inst==null){HttpResponses.json(ex,404,Map.of("erro","instancia_nao_configurada"));return;}String[] parts=String.valueOf(inst.get("instance_token_enc")).split(":",2);Map<String,Object> qr=evolutionApiClient.conectar(str(inst.get("instance_url")),parts[0],str(inst.get("numero_whatsapp")));HttpResponses.json(ex,200,qr);}
|
||||
private void desconectarEvolution(HttpExchange ex,long empresaId)throws Exception{Map<String,Object> inst=evolutionInstancia(empresaId);if(inst==null){HttpResponses.json(ex,404,Map.of("erro","instancia_nao_configurada"));return;}String[] parts=String.valueOf(inst.get("instance_token_enc")).split(":",2);HttpResponses.json(ex,200,evolutionApiClient.logout(str(inst.get("instance_url")),parts[0]));}
|
||||
private void reconectarEvolution(HttpExchange ex,long empresaId)throws Exception{Map<String,Object> inst=evolutionInstancia(empresaId);if(inst==null){HttpResponses.json(ex,404,Map.of("erro","instancia_nao_configurada"));return;}String[] parts=String.valueOf(inst.get("instance_token_enc")).split(":",2);HttpResponses.json(ex,200,evolutionApiClient.reiniciar(str(inst.get("instance_url")),parts[0]));}
|
||||
private void statusEvolution(HttpExchange ex,long empresaId)throws Exception{
|
||||
List<Map<String,Object>> insts;
|
||||
try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT id,empresa_id,instance_url,instance_token_enc,numero_whatsapp,status,solicitado_em,aprovado_em FROM evolution_instancias WHERE empresa_id=? ORDER BY solicitado_em DESC,id DESC")){
|
||||
ps.setLong(1,empresaId);
|
||||
insts=new ArrayList<>();
|
||||
try(ResultSet rs=ps.executeQuery()){while(rs.next())insts.add(resultMap(rs));}
|
||||
}
|
||||
if(insts.isEmpty()){
|
||||
HttpResponses.json(ex,200,Map.of("status","nao_configurada","instancias",List.of()));
|
||||
return;
|
||||
}
|
||||
for(Map<String,Object> i:insts){
|
||||
String token=str(i.get("instance_token_enc"));
|
||||
if(token!=null&&!token.isBlank()){
|
||||
String instanceName=token.split(":",2)[0];
|
||||
i.put("instance_name",instanceName);
|
||||
try{
|
||||
i.put("evolution",evolutionApiClient.estado(str(i.get("instance_url")),instanceName));
|
||||
}catch(Exception ignored){
|
||||
i.put("evolution",Map.of("state","desconhecido"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Map<String,Object> resp=new LinkedHashMap<>();
|
||||
resp.put("instancia",insts.get(0));
|
||||
resp.put("instancias",insts);
|
||||
resp.put("evolution",insts.get(0).get("evolution"));
|
||||
HttpResponses.json(ex,200,resp);
|
||||
}
|
||||
private void qrcodeEvolution(HttpExchange ex,long empresaId)throws Exception{
|
||||
Map<String,String> b=body(ex);
|
||||
Long id=optLong(b.get("id"));
|
||||
Map<String,Object> inst=id!=null?evolutionInstanciaPorId(empresaId,id):evolutionInstancia(empresaId);
|
||||
if(inst==null){HttpResponses.json(ex,404,Map.of("erro","instancia_nao_configurada"));return;}
|
||||
String[] parts=String.valueOf(inst.get("instance_token_enc")).split(":",2);
|
||||
Map<String,Object> qr=evolutionApiClient.conectar(str(inst.get("instance_url")),parts[0],str(inst.get("numero_whatsapp")));
|
||||
HttpResponses.json(ex,200,qr);
|
||||
}
|
||||
private void desconectarEvolution(HttpExchange ex,long empresaId)throws Exception{
|
||||
Map<String,String> b=body(ex);
|
||||
Long id=optLong(b.get("id"));
|
||||
Map<String,Object> inst=id!=null?evolutionInstanciaPorId(empresaId,id):evolutionInstancia(empresaId);
|
||||
if(inst==null){HttpResponses.json(ex,404,Map.of("erro","instancia_nao_configurada"));return;}
|
||||
String[] parts=String.valueOf(inst.get("instance_token_enc")).split(":",2);
|
||||
HttpResponses.json(ex,200,evolutionApiClient.logout(str(inst.get("instance_url")),parts[0]));
|
||||
}
|
||||
private void reconectarEvolution(HttpExchange ex,long empresaId)throws Exception{
|
||||
Map<String,String> b=body(ex);
|
||||
Long id=optLong(b.get("id"));
|
||||
Map<String,Object> inst=id!=null?evolutionInstanciaPorId(empresaId,id):evolutionInstancia(empresaId);
|
||||
if(inst==null){HttpResponses.json(ex,404,Map.of("erro","instancia_nao_configurada"));return;}
|
||||
String[] parts=String.valueOf(inst.get("instance_token_enc")).split(":",2);
|
||||
HttpResponses.json(ex,200,evolutionApiClient.reiniciar(str(inst.get("instance_url")),parts[0]));
|
||||
}
|
||||
private Map<String,Object> evolutionInstanciaPorId(long empresaId,long id)throws Exception{
|
||||
try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT id,empresa_id,instance_url,instance_token_enc,numero_whatsapp,status,solicitado_em,aprovado_em FROM evolution_instancias WHERE id=? AND empresa_id=?")){
|
||||
ps.setLong(1,id);ps.setLong(2,empresaId);
|
||||
try(ResultSet rs=ps.executeQuery()){return rs.next()?resultMap(rs):null;}
|
||||
}
|
||||
}
|
||||
private Map<String,Object> evolutionInstancia(long empresaId)throws Exception{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT id,empresa_id,instance_url,instance_token_enc,numero_whatsapp,status,solicitado_em,aprovado_em FROM evolution_instancias WHERE empresa_id=? ORDER BY solicitado_em DESC,id DESC LIMIT 1")){ps.setLong(1,empresaId);try(ResultSet rs=ps.executeQuery()){return rs.next()?resultMap(rs):null;}}}
|
||||
|
||||
private void listarAssembleiasCalendario(HttpExchange ex,Connection c,long emp,String cargo,long fid)throws Exception{
|
||||
|
||||
@@ -1 +1,139 @@
|
||||
<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>WhatsApp</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>WhatsApp</h1></div><section class="panel"><h2>WhatsApp</h2><form id="wa-form" class="row"><div class="field"><label>Número WhatsApp</label><input name="numero_whatsapp" required placeholder="5511999999999"></div><button class="btn">Cadastrar</button></form></section><section class="panel"><h2>Status</h2><div id="status"></div><div class="row"><button id="btn-qr" class="btn secondary" type="button">Conectar via QR Code</button><button id="btn-reconnect" class="btn secondary" type="button">Reconectar</button><button id="btn-disconnect" class="btn danger" type="button">Desconectar</button></div><div id="qr-box" class="hidden"><h2>QR Code</h2><img id="qr" class="qr hidden" alt="QR Code Evolution"><div class="field"><label>Código de pareamento</label><input id="pairing" readonly></div></div></section></main><script>Layout.mount('empresa','configuracoes');</script><script src="/cliente/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>WhatsApp</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>WhatsApp</h1>
|
||||
</div>
|
||||
|
||||
<!-- Seção de Cadastro de Novo Número -->
|
||||
<section class="panel">
|
||||
<h2>Cadastrar Novo WhatsApp</h2>
|
||||
<form id="wa-form" class="row" style="align-items: flex-end; gap: 14px; max-width: 580px;">
|
||||
<div class="field" style="flex: 1;">
|
||||
<label>Número WhatsApp (com DDD)</label>
|
||||
<input name="numero_whatsapp" required placeholder="5511999999999" autocomplete="tel">
|
||||
</div>
|
||||
<button class="btn" type="submit" style="height: 42px; display: inline-flex; align-items: center; gap: 8px;">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||
Cadastrar
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Seção de Cards de Conexões -->
|
||||
<section class="panel">
|
||||
<div class="row" style="align-items: center; justify-content: space-between; margin-bottom: 8px;">
|
||||
<h2>Minhas Conexões WhatsApp</h2>
|
||||
<button class="btn secondary" type="button" onclick="load()" style="height: 34px; padding: 0 12px; font-size: 13px;">Atualizar</button>
|
||||
</div>
|
||||
<div id="cards-container" class="wa-cards-grid">
|
||||
<div class="wa-empty-card">Carregando conexões...</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Modal / Popup com QR Code -->
|
||||
<div id="qr-modal" class="modal animated-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">Conectar 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: 14px; font-size: 14px; color: var(--muted-strong); line-height: 1.4;">
|
||||
Abra o WhatsApp no celular, vá em <strong>Aparelhos conectados</strong> > <strong>Conectar um aparelho</strong> e aponte para a imagem:
|
||||
</p>
|
||||
<div id="qr-loading" style="padding: 24px;">
|
||||
<div class="action-loading-spinner"></div>
|
||||
<p style="font-size: 14px; color: var(--muted-strong);">Gerando QR Code na Evolution...</p>
|
||||
</div>
|
||||
<img id="qr-modal-img" class="qr hidden" alt="QR Code WhatsApp" style="margin: 0 auto; max-width: 260px; display: block; border-radius: 12px; box-shadow: 0 4px 14px rgba(0,0,0,0.08); background: #fff; padding: 10px;">
|
||||
<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: 18px; font-weight: bold; text-align: center; letter-spacing: 3px;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions" style="justify-content: center; gap: 10px;">
|
||||
<button class="btn secondary" type="button" onclick="fecharModalQr()">Fechar</button>
|
||||
<button id="btn-atualizar-qr" class="btn" type="button" onclick="requisitarQrCode()">Atualizar QR Code</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Modal / Popup com Animação para Reconectar -->
|
||||
<div id="reconectar-modal" class="modal animated-modal hidden">
|
||||
<div class="modal-backdrop" onclick="fecharModalReconectar()"></div>
|
||||
<section class="modal-panel" style="max-width: 420px; text-align: center;">
|
||||
<div class="modal-head">
|
||||
<h2>Reconectar Sessão</h2>
|
||||
<button class="btn secondary" type="button" onclick="fecharModalReconectar()">✕</button>
|
||||
</div>
|
||||
<div id="reconectar-step-confirm" style="padding: 16px 0;">
|
||||
<div class="action-icon-circle brand">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>
|
||||
</div>
|
||||
<p style="font-size: 15px; color: var(--text); font-weight: 700; margin-bottom: 6px;">
|
||||
Restabelecer conexão do WhatsApp?
|
||||
</p>
|
||||
<p style="font-size: 13px; color: var(--muted-strong); line-height: 1.4;">
|
||||
A Evolution tentará reativar e sincronizar a sessão existente deste número imediatamente.
|
||||
</p>
|
||||
<div class="modal-actions" style="justify-content: center; margin-top: 20px;">
|
||||
<button class="btn secondary" type="button" onclick="fecharModalReconectar()">Cancelar</button>
|
||||
<button class="btn" type="button" onclick="executarReconectar()">Reconectar Agora</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="reconectar-step-loading" class="hidden" style="padding: 24px 0;">
|
||||
<div class="action-loading-spinner"></div>
|
||||
<p style="font-size: 14px; font-weight: 700; color: var(--text);">Reconectando...</p>
|
||||
<p style="font-size: 12px; color: var(--muted-strong); margin-top: 4px;">Aguarde a resposta da Evolution API.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Modal / Popup com Animação para Desconectar -->
|
||||
<div id="desconectar-modal" class="modal animated-modal hidden">
|
||||
<div class="modal-backdrop" onclick="fecharModalDesconectar()"></div>
|
||||
<section class="modal-panel" style="max-width: 420px; text-align: center;">
|
||||
<div class="modal-head">
|
||||
<h2>Desconectar WhatsApp</h2>
|
||||
<button class="btn secondary" type="button" onclick="fecharModalDesconectar()">✕</button>
|
||||
</div>
|
||||
<div id="desconectar-step-confirm" style="padding: 16px 0;">
|
||||
<div class="action-icon-circle danger">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line></svg>
|
||||
</div>
|
||||
<p style="font-size: 15px; color: var(--text); font-weight: 700; margin-bottom: 6px;">
|
||||
Desconectar esta sessão do WhatsApp?
|
||||
</p>
|
||||
<p style="font-size: 13px; color: var(--muted-strong); line-height: 1.4;">
|
||||
O envio de avisos ficará pausado até que o aparelho seja reconectado via QR Code.
|
||||
</p>
|
||||
<div class="modal-actions" style="justify-content: center; margin-top: 20px;">
|
||||
<button class="btn secondary" type="button" onclick="fecharModalDesconectar()">Cancelar</button>
|
||||
<button class="btn danger" type="button" onclick="executarDesconectar()">Desconectar</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="desconectar-step-loading" class="hidden" style="padding: 24px 0;">
|
||||
<div class="action-loading-spinner" style="border-top-color: var(--danger);"></div>
|
||||
<p style="font-size: 14px; font-weight: 700; color: var(--text);">Desconectando...</p>
|
||||
<p style="font-size: 12px; color: var(--muted-strong); margin-top: 4px;">Finalizando a sessão com a Evolution API.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<script>Layout.mount('empresa','configuracoes');</script>
|
||||
<script src="/cliente/configuracoes.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1 +1,323 @@
|
||||
const form=document.querySelector('#wa-form'),statusEl=document.querySelector('#status'),qr=document.querySelector('#qr'),qrBox=document.querySelector('#qr-box'),pairing=document.querySelector('#pairing'),btnQr=document.querySelector('#btn-qr'),btnReconnect=document.querySelector('#btn-reconnect'),btnDisconnect=document.querySelector('#btn-disconnect');async function load(){try{const d=await Api.get('/api/config/evolution-instancia/status');const i=d.instancia;const evo=d.evolution||{};statusEl.innerHTML=i?`<p>Status de aprovação: ${Util.badge(i.status)}</p><p>Número: ${i.numero_whatsapp||'-'}</p><p>Conexão: ${Util.badge(evo.state||'-')}</p>`:`<p>Status: ${Util.badge(d.status)}</p>`;const has=i!=null;btnQr.disabled=!has;btnReconnect.disabled=!has;btnDisconnect.disabled=!has}catch(e){Util.toast(e.message,true)}}function showQr(d){qrBox.classList.remove('hidden');const base64=d.qrcode_base64||d.qr_code_base64;if(base64){qr.src=base64.startsWith('data:image/')?base64:'data:image/png;base64,'+base64;qr.classList.remove('hidden')}else{qr.classList.add('hidden');Util.toast('Evolution não retornou QR Code nesta tentativa',true)}pairing.value=d.pairing_code||''}form.onsubmit=async e=>{e.preventDefault();try{const d=await Api.post('/api/config/evolution-instancia/conectar',Util.formData(form));statusEl.innerHTML=`<p class="success-message">WhatsApp cadastrado com sucesso. Aguarde a liberação no painel Master.</p><p>Status de aprovação: ${Util.badge(d.status)}</p><p>Instância: ${d.instance_name}</p>`;showQr(d);Util.toast('WhatsApp cadastrado com sucesso');setTimeout(load,1600)}catch(err){Util.toast(err.message,true)}};btnQr.onclick=async()=>{try{const d=await Api.post('/api/config/evolution-instancia/qrcode',{});showQr(d);Util.toast('QR Code atualizado')}catch(e){Util.toast(e.message,true)}};btnReconnect.onclick=async()=>{try{await Api.post('/api/config/evolution-instancia/reconectar',{});Util.toast('Reconexão solicitada');setTimeout(load,1200)}catch(e){Util.toast(e.message,true)}};btnDisconnect.onclick=async()=>{if(!await Util.confirm('Desconectar esta sessão do WhatsApp?','Desconectar WhatsApp'))return;try{await Api.post('/api/config/evolution-instancia/desconectar',{});Util.toast('Sessão desconectada');setTimeout(load,1200)}catch(e){Util.toast(e.message,true)}};load();
|
||||
const form = document.querySelector('#wa-form');
|
||||
const cardsContainer = document.querySelector('#cards-container');
|
||||
|
||||
// QR Modal Elements
|
||||
const qrModal = document.querySelector('#qr-modal');
|
||||
const qrModalTitle = document.querySelector('#qr-modal-title');
|
||||
const qrModalImg = document.querySelector('#qr-modal-img');
|
||||
const qrLoading = document.querySelector('#qr-loading');
|
||||
const qrPairingBox = document.querySelector('#qr-pairing-box');
|
||||
const qrModalPairing = document.querySelector('#qr-modal-pairing');
|
||||
|
||||
// Reconnect Modal Elements
|
||||
const reconectarModal = document.querySelector('#reconectar-modal');
|
||||
const reconectarStepConfirm = document.querySelector('#reconectar-step-confirm');
|
||||
const reconectarStepLoading = document.querySelector('#reconectar-step-loading');
|
||||
|
||||
// Disconnect Modal Elements
|
||||
const desconectarModal = document.querySelector('#desconectar-modal');
|
||||
const desconectarStepConfirm = document.querySelector('#desconectar-step-confirm');
|
||||
const desconectarStepLoading = document.querySelector('#desconectar-step-loading');
|
||||
|
||||
let conexoes = [];
|
||||
let selectedInstanciaId = null;
|
||||
|
||||
const Icons = {
|
||||
qrCode: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect><rect x="14" y="14" width="3" height="3"></rect><rect x="18" y="18" width="3" height="3"></rect><rect x="18" y="14" width="3" height="3"></rect><rect x="14" y="18" width="3" height="3"></rect></svg>',
|
||||
restart: '<svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg>',
|
||||
power: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18.36 6.64a9 9 0 1 1-12.73 0"></path><line x1="12" y1="2" x2="12" y2="12"></line></svg>',
|
||||
whatsapp: '<svg viewBox="0 0 24 24" aria-hidden="true" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path></svg>'
|
||||
};
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function formatPhonePlain(raw) {
|
||||
if (!raw) return '';
|
||||
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;
|
||||
}
|
||||
|
||||
function formatPhone(raw) {
|
||||
const formatted = formatPhonePlain(raw);
|
||||
return formatted || '<span class="badge warn">Não informado</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>';
|
||||
}
|
||||
return `<span class="badge">${escapeHtml(state || 'Desconhecido')}</span>`;
|
||||
}
|
||||
|
||||
function renderCards(list) {
|
||||
if (!list || list.length === 0) {
|
||||
cardsContainer.innerHTML = `
|
||||
<div class="wa-empty-card">
|
||||
<p style="font-size: 15px; font-weight: 700; margin-bottom: 6px; color: var(--text);">Nenhuma conexão WhatsApp configurada</p>
|
||||
<p style="font-size: 13px; color: var(--muted-strong);">Cadastre um número acima para habilitar o envio de avisos e notificações.</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
cardsContainer.innerHTML = list.map(inst => {
|
||||
const evo = inst.evolution || {};
|
||||
const evoState = evo.state || 'close';
|
||||
const instName = inst.instance_name || ('instancia_' + inst.id);
|
||||
const phoneFormatted = formatPhone(inst.numero_whatsapp);
|
||||
|
||||
return `
|
||||
<article class="wa-card" data-id="${inst.id}">
|
||||
<header class="wa-card-header">
|
||||
<div class="wa-card-title-group">
|
||||
<div class="wa-icon-bubble">
|
||||
${Icons.whatsapp}
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="wa-card-number">${phoneFormatted}</h3>
|
||||
<span class="wa-card-inst">${escapeHtml(instName)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wa-card-badges">
|
||||
${connectionBadge(evoState)}
|
||||
${Util.badge(inst.status)}
|
||||
</div>
|
||||
</header>
|
||||
<div class="wa-card-body">
|
||||
<div class="wa-info-row">
|
||||
<span class="wa-info-label">Status da Conexão</span>
|
||||
<span class="wa-info-value">${connectionBadge(evoState)}</span>
|
||||
</div>
|
||||
<div class="wa-info-row">
|
||||
<span class="wa-info-label">Aprovação do Sistema</span>
|
||||
<span class="wa-info-value">${Util.badge(inst.status)}</span>
|
||||
</div>
|
||||
<div class="wa-info-row">
|
||||
<span class="wa-info-label">Cadastrado em</span>
|
||||
<span class="wa-info-value">${Util.date(inst.solicitado_em)}</span>
|
||||
</div>
|
||||
<div class="wa-info-row">
|
||||
<span class="wa-info-label">Aprovado em</span>
|
||||
<span class="wa-info-value">${inst.aprovado_em ? Util.date(inst.aprovado_em) : '<span class="badge warn">Pendente</span>'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="wa-card-footer">
|
||||
<span style="font-size: 12px; font-weight: 600; color: var(--muted-strong);">Ações:</span>
|
||||
<div class="table-actions">
|
||||
<button class="table-icon-btn" type="button" title="Conectar via QR Code" aria-label="Conectar via QR Code" onclick="abrirModalQr(${inst.id})">
|
||||
${Icons.qrCode}
|
||||
</button>
|
||||
<button class="table-icon-btn" type="button" title="Reconectar WhatsApp" aria-label="Reconectar WhatsApp" onclick="abrirModalReconectar(${inst.id})">
|
||||
${Icons.restart}
|
||||
</button>
|
||||
<button class="table-icon-btn danger" type="button" title="Desconectar WhatsApp" aria-label="Desconectar WhatsApp" onclick="abrirModalDesconectar(${inst.id})">
|
||||
${Icons.power}
|
||||
</button>
|
||||
</div>
|
||||
</footer>
|
||||
</article>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const d = await Api.get('/api/config/evolution-instancia/status');
|
||||
if (Array.isArray(d.instancias)) {
|
||||
conexoes = d.instancias;
|
||||
} else if (d.instancia) {
|
||||
const inst = d.instancia;
|
||||
inst.evolution = d.evolution || {};
|
||||
conexoes = [inst];
|
||||
} else {
|
||||
conexoes = [];
|
||||
}
|
||||
renderCards(conexoes);
|
||||
} catch (e) {
|
||||
cardsContainer.innerHTML = `<div class="wa-empty-card" style="color: var(--danger);">Erro ao carregar conexões: ${escapeHtml(e.message)}</div>`;
|
||||
Util.toast(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function exibirQrCode(d) {
|
||||
const base64 = d.qrcode_base64 || d.qr_code_base64;
|
||||
qrLoading.classList.add('hidden');
|
||||
if (base64) {
|
||||
qrModalImg.src = base64.startsWith('data:image/') ? base64 : 'data:image/png;base64,' + base64;
|
||||
qrModalImg.classList.remove('hidden');
|
||||
} else {
|
||||
qrModalImg.classList.add('hidden');
|
||||
Util.toast('Evolution não retornou imagem de QR Code nesta tentativa', true);
|
||||
}
|
||||
if (d.pairing_code) {
|
||||
qrModalPairing.value = d.pairing_code;
|
||||
qrPairingBox.classList.remove('hidden');
|
||||
} else {
|
||||
qrPairingBox.classList.add('hidden');
|
||||
qrModalPairing.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function requisitarQrCode() {
|
||||
qrLoading.classList.remove('hidden');
|
||||
qrModalImg.classList.add('hidden');
|
||||
qrPairingBox.classList.add('hidden');
|
||||
try {
|
||||
const payload = selectedInstanciaId ? { id: selectedInstanciaId } : {};
|
||||
const d = await Api.post('/api/config/evolution-instancia/qrcode', payload);
|
||||
exibirQrCode(d);
|
||||
} catch (e) {
|
||||
qrLoading.classList.add('hidden');
|
||||
Util.toast(e.message || 'Erro ao obter QR Code', true);
|
||||
}
|
||||
}
|
||||
|
||||
function abrirModalQr(id, initialData = null) {
|
||||
selectedInstanciaId = id;
|
||||
const inst = conexoes.find(c => c.id === id);
|
||||
if (inst && inst.numero_whatsapp) {
|
||||
qrModalTitle.textContent = `Conectar ${formatPhonePlain(inst.numero_whatsapp)}`;
|
||||
} else {
|
||||
qrModalTitle.textContent = 'Conectar WhatsApp';
|
||||
}
|
||||
qrModal.classList.remove('hidden');
|
||||
|
||||
if (initialData && (initialData.qrcode_base64 || initialData.qr_code_base64)) {
|
||||
exibirQrCode(initialData);
|
||||
} else {
|
||||
requisitarQrCode();
|
||||
}
|
||||
}
|
||||
|
||||
function fecharModalQr() {
|
||||
qrModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
function abrirModalReconectar(id) {
|
||||
selectedInstanciaId = id;
|
||||
reconectarStepConfirm.classList.remove('hidden');
|
||||
reconectarStepLoading.classList.add('hidden');
|
||||
reconectarModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function fecharModalReconectar() {
|
||||
reconectarModal.classList.add('hidden');
|
||||
reconectarStepLoading.classList.add('hidden');
|
||||
reconectarStepConfirm.classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function executarReconectar() {
|
||||
reconectarStepConfirm.classList.add('hidden');
|
||||
reconectarStepLoading.classList.remove('hidden');
|
||||
try {
|
||||
const payload = selectedInstanciaId ? { id: selectedInstanciaId } : {};
|
||||
await Api.post('/api/config/evolution-instancia/reconectar', payload);
|
||||
Util.toast('Reconexão solicitada com sucesso!');
|
||||
setTimeout(() => {
|
||||
fecharModalReconectar();
|
||||
load();
|
||||
}, 1200);
|
||||
} catch (e) {
|
||||
reconectarStepLoading.classList.add('hidden');
|
||||
reconectarStepConfirm.classList.remove('hidden');
|
||||
Util.toast(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function abrirModalDesconectar(id) {
|
||||
selectedInstanciaId = id;
|
||||
desconectarStepConfirm.classList.remove('hidden');
|
||||
desconectarStepLoading.classList.add('hidden');
|
||||
desconectarModal.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function fecharModalDesconectar() {
|
||||
desconectarModal.classList.add('hidden');
|
||||
desconectarStepLoading.classList.add('hidden');
|
||||
desconectarStepConfirm.classList.remove('hidden');
|
||||
}
|
||||
|
||||
async function executarDesconectar() {
|
||||
desconectarStepConfirm.classList.add('hidden');
|
||||
desconectarStepLoading.classList.remove('hidden');
|
||||
try {
|
||||
const payload = selectedInstanciaId ? { id: selectedInstanciaId } : {};
|
||||
await Api.post('/api/config/evolution-instancia/desconectar', payload);
|
||||
Util.toast('Sessão desconectada com sucesso!');
|
||||
setTimeout(() => {
|
||||
fecharModalDesconectar();
|
||||
load();
|
||||
}, 1200);
|
||||
} catch (e) {
|
||||
desconectarStepLoading.classList.add('hidden');
|
||||
desconectarStepConfirm.classList.remove('hidden');
|
||||
Util.toast(e.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const submitBtn = form.querySelector('button[type="submit"]');
|
||||
try {
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
const body = Util.formData(form);
|
||||
const d = await Api.post('/api/config/evolution-instancia/conectar', body);
|
||||
form.reset();
|
||||
Util.toast('WhatsApp cadastrado com sucesso!');
|
||||
await load();
|
||||
if (d.id) {
|
||||
abrirModalQr(d.id, d);
|
||||
}
|
||||
} catch (err) {
|
||||
Util.toast(err.message, true);
|
||||
} finally {
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
fecharModalQr();
|
||||
fecharModalReconectar();
|
||||
fecharModalDesconectar();
|
||||
}
|
||||
});
|
||||
|
||||
// Expor funções globais para handlers inline
|
||||
window.load = load;
|
||||
window.abrirModalQr = abrirModalQr;
|
||||
window.fecharModalQr = fecharModalQr;
|
||||
window.requisitarQrCode = requisitarQrCode;
|
||||
window.abrirModalReconectar = abrirModalReconectar;
|
||||
window.fecharModalReconectar = fecharModalReconectar;
|
||||
window.executarReconectar = executarReconectar;
|
||||
window.abrirModalDesconectar = abrirModalDesconectar;
|
||||
window.fecharModalDesconectar = fecharModalDesconectar;
|
||||
window.executarDesconectar = executarDesconectar;
|
||||
|
||||
load();
|
||||
|
||||
@@ -1415,3 +1415,195 @@ th {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* WhatsApp Cards & Animated Popups */
|
||||
.wa-cards-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.wa-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--line, #e2e8f0);
|
||||
border-radius: var(--radius-lg, 16px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wa-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.05);
|
||||
border-color: var(--brand-light, #cbd5e1);
|
||||
}
|
||||
|
||||
.wa-card-header {
|
||||
padding: 18px 20px;
|
||||
border-bottom: 1px solid var(--line, #e2e8f0);
|
||||
background: #fbfcfd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wa-card-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wa-icon-bubble {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
background: var(--ok-soft, #e6f7ec);
|
||||
color: var(--ok, #10b981);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.wa-card-number {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
color: var(--text, #1e293b);
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.wa-card-inst {
|
||||
font-size: 12px;
|
||||
color: var(--muted-strong, #64748b);
|
||||
font-family: monospace;
|
||||
margin-top: 2px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.wa-card-badges {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.wa-card-body {
|
||||
padding: 18px 20px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wa-info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.wa-info-label {
|
||||
color: var(--muted-strong, #64748b);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wa-info-value {
|
||||
font-weight: 700;
|
||||
color: var(--text, #1e293b);
|
||||
}
|
||||
|
||||
.wa-card-footer {
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--line, #e2e8f0);
|
||||
background: #fbfcfd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.wa-card-footer .table-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.wa-empty-card {
|
||||
padding: 36px 20px;
|
||||
text-align: center;
|
||||
background: #ffffff;
|
||||
border: 1px dashed var(--line, #e2e8f0);
|
||||
border-radius: var(--radius-lg, 16px);
|
||||
color: var(--muted-strong, #64748b);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* Animations for Popups */
|
||||
@keyframes modalPopIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.92) translateY(12px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spinnerRotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes pulseIcon {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.1); opacity: 0.8; }
|
||||
}
|
||||
|
||||
.animated-modal .modal-panel {
|
||||
animation: modalPopIn 0.25s cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
}
|
||||
|
||||
.action-loading-spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid var(--line, #e2e8f0);
|
||||
border-top-color: var(--brand, #0f766e);
|
||||
border-radius: 50%;
|
||||
animation: spinnerRotate 0.8s linear infinite;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.action-icon-circle {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.action-icon-circle.danger {
|
||||
background: var(--danger-soft, #fee2e2);
|
||||
color: var(--danger, #ef4444);
|
||||
animation: pulseIcon 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.action-icon-circle.brand {
|
||||
background: var(--brand-soft, #ccfbf1);
|
||||
color: var(--brand, #0f766e);
|
||||
animation: pulseIcon 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.action-icon-circle.ok {
|
||||
background: var(--ok-soft, #d1fae5);
|
||||
color: var(--ok, #10b981);
|
||||
}
|
||||
|
||||
.action-icon-circle svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
stroke-width: 2.5;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user