feat(cliente): exibir conexoes WhatsApp em cards com icones de acao e popups animados

This commit is contained in:
Tiago
2026-10-13 09:55:31 -03:00
parent 12d03940cf
commit 83d5b68459
4 changed files with 714 additions and 6 deletions
@@ -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{
+139 -1
View File
@@ -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> &gt; <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>
+323 -1
View File
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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();
+192
View File
@@ -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;
}