chore(tenants): conteudo de publico/ atualizado

This commit is contained in:
deploy
2026-09-26 22:18:45 +00:00
parent d801718c35
commit f3de600db9
2 changed files with 478 additions and 20 deletions
+239 -10
View File
@@ -1410,6 +1410,24 @@ data:
.form-cobranca .btn:disabled { opacity: .6; cursor: wait; }
.erro-form { color: var(--perigo); font-size: 14px; margin: 0; }
/* ── o termo de dados, dentro do cartao ─────────────────────────────────── */
.termo {
align-self: stretch; margin-top: 20px; padding-top: 16px;
border-top: 1px dashed var(--line);
display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
}
.termo .selo { margin-bottom: 0; }
.produto .termo .btn { margin-top: 6px; }
.termo .btn:disabled { opacity: .6; cursor: wait; }
.btn.secundario { background: transparent; color: var(--navy); border: 1px solid var(--line); }
.btn.secundario:hover { background: var(--line); }
.termo details { align-self: stretch; font-size: 14px; }
.termo summary { cursor: pointer; font-weight: 600; }
.termo-texto {
white-space: pre-wrap; margin-top: 8px; padding: 12px; max-height: 280px; overflow-y: auto;
border: 1px solid var(--line); border-radius: 10px; background: var(--branco); line-height: 1.5;
}
/* Empilhado, a foto sai: no celular ela so empurraria os produtos para baixo. */
@media (max-width: 760px) {
.topo { padding: 12px 16px; }
@@ -1445,7 +1463,7 @@ data:
numero a cada publicacao que mexa nesses arquivos.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.82">
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.83">
</head>
<body>
@@ -1515,7 +1533,7 @@ data:
<footer class="direitos">&copy; 2026 Athletic Map. Todos os direitos reservados.</footer>
<script src="/publico/minha-conta.js?v=2.82"></script>
<script src="/publico/minha-conta.js?v=2.83"></script>
</body>
</html>
@@ -1553,7 +1571,8 @@ data:
CLIENTE: "painel-do-cliente",
RETORNO: location.origin + "/minha-conta",
API: "/api/cliente/meus-produtos",
COBRANCA: "/api/cliente/cobranca"
COBRANCA: "/api/cliente/cobranca",
TERMO: "/api/cliente/termo-de-dados"
};
var CHAVE_VERIFIER = "atm.conta.verifier";
@@ -1725,7 +1744,7 @@ data:
produtos.innerHTML = (lista || []).map(cartao).join("");
nenhum.classList.toggle("oculto", (lista || []).length > 0);
mostrar(conteudo);
return carregarCobranca();
return Promise.all([carregarCobranca(), carregarTermo()]);
});
}
@@ -1835,7 +1854,10 @@ data:
if (!alvo) return;
var velho = alvo.querySelector(".cobranca");
if (velho) velho.remove();
alvo.insertAdjacentHTML("beforeend", alertaDeCobranca(c));
// Antes do bloco do termo, se ele ja estiver la: a cobranca e mais urgente.
var termo = alvo.querySelector(".termo");
if (termo) termo.insertAdjacentHTML("beforebegin", alertaDeCobranca(c));
else alvo.insertAdjacentHTML("beforeend", alertaDeCobranca(c));
});
})
.catch(function (e) { if (window.console) console.warn("cobranca:", e); });
@@ -1872,6 +1894,83 @@ data:
});
}
/* ── O TERMO DE DADOS ────────────────────────────────────────────────────────
*
* A autorizacao para a Athletic Map ver o USO do sistema (ultimo acesso,
* alunos ativos, treinos). Sem termo publicado, o bloco nem aparece — o
* servidor devolve `termo: null` enquanto o texto e rascunho. Como a
* cobranca, falhar aqui nao derruba a tela. */
function dataCurta(iso) {
var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || ""));
return m ? m[3] + "/" + m[2] + "/" + m[1] : "";
}
function blocoDoTermo(termo, e) {
var titulo = '<span class="cobranca-titulo">Acompanhamento de uso</span>';
if (e.aceito) {
return '<div class="termo">' + titulo
+ '<span class="selo ok">autorizado' + (e.aceitoEm ? " em " + dataCurta(e.aceitoEm) : "") + "</span>"
+ '<p class="ajuda">Vemos s&oacute; a data do &uacute;ltimo acesso e contagens de alunos e treinos.</p>'
+ '<button type="button" class="btn secundario" data-termo="revogar" data-tenant="'
+ escapar(e.tenant) + '">Revogar autoriza&ccedil;&atilde;o</button>'
+ '<p class="erro-form oculto"></p></div>';
}
return '<div class="termo">' + titulo
+ '<span class="selo neutro">n&atilde;o autorizado</span>'
+ '<p class="ajuda">Autorize a Athletic Map a acompanhar o uso do sistema para dar suporte.</p>'
+ '<details><summary>Ler o termo (vers&atilde;o ' + escapar(termo.versao) + ")</summary>"
+ '<div class="termo-texto">' + escapar(termo.texto) + "</div></details>"
+ '<button type="button" class="btn" data-termo="aceitar" data-tenant="'
+ escapar(e.tenant) + '">Li e autorizo</button>'
+ '<p class="erro-form oculto"></p></div>';
}
function carregarTermo() {
return fetch(CONFIG.TERMO, { headers: { Authorization: "Bearer " + token } })
.then(function (r) { return r.ok ? r.json() : { termo: null, escolinhas: [] }; })
.then(function (d) {
produtos.querySelectorAll(".termo").forEach(function (el) { el.remove(); });
if (!d || !d.termo) return;
(d.escolinhas || []).forEach(function (e) {
var alvo = null;
produtos.querySelectorAll("[data-tenant]").forEach(function (el) {
if (el.tagName === "ARTICLE" && el.getAttribute("data-tenant") === e.tenant) alvo = el;
});
if (alvo) alvo.insertAdjacentHTML("beforeend", blocoDoTermo(d.termo, e));
});
})
.catch(function (e) { if (window.console) console.warn("termo:", e); });
}
function agirNoTermo(botao) {
var acao = botao.getAttribute("data-termo");
var erro = botao.parentNode.querySelector(".erro-form");
if (acao === "revogar" && typeof window.confirm === "function"
&& !window.confirm("Revogar? Paramos de ver o uso e apagamos os registros de acesso guardados.")) {
return Promise.resolve();
}
botao.disabled = true;
return fetch(CONFIG.TERMO + "/" + encodeURIComponent(botao.getAttribute("data-tenant")), {
method: acao === "aceitar" ? "POST" : "DELETE",
headers: { Authorization: "Bearer " + token }
}).then(function (r) {
if (r.ok) return carregarTermo();
return r.json().catch(function () { return {}; }).then(function (d) {
throw new Error(d.erro || "Nao consegui registrar agora. Tente de novo em instantes.");
});
}).catch(function (e) {
erro.textContent = e.message;
erro.classList.remove("oculto");
botao.disabled = false;
});
}
if (produtos) produtos.addEventListener("click", function (ev) {
var b = ev.target && ev.target.closest ? ev.target.closest("[data-termo]") : null;
if (b) agirNoTermo(b);
});
/* Delegacao, e nao `onsubmit=`: a CSP recusa manipulador embutido, e os
formularios nascem depois deste codigo. */
if (produtos) produtos.addEventListener("submit", function (ev) {
@@ -2234,6 +2333,20 @@ data:
.grade-ficha dl > div { grid-template-columns: 110px 1fr; }
}
/* ── termo de dados ──────────────────────────────────────────────────────── */
.termo-versao { border: 1px solid var(--line); border-radius: 12px; padding: 16px; margin: 14px 0; }
.termo-versao h3 { margin: 0 0 6px; font-size: 15px; display: flex; gap: 10px; align-items: center; }
.termo-versao textarea {
width: 100%; min-height: 320px; font: inherit; font-size: 14px; line-height: 1.5;
padding: 12px; border: 1px solid var(--line); border-radius: 10px; box-sizing: border-box;
}
.termo-versao .texto-publicado {
white-space: pre-wrap; font-size: 14px; line-height: 1.5; max-height: 260px; overflow-y: auto;
background: var(--paper); border-radius: 10px; padding: 12px;
}
.termo-acoes { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 10px; }
.erro-termo { color: var(--perigo); font-size: 13px; }
painel.html: |
<!DOCTYPE html>
<html lang="pt-BR">
@@ -2258,7 +2371,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
<link rel="stylesheet" href="/publico/painel.css?v=2.84">
</head>
<body>
@@ -2317,6 +2430,7 @@ data:
<button type="button" class="aba" data-aba="vendas" role="tab">Vendas e leads</button>
<button type="button" class="aba" data-aba="receita" role="tab">Receita e carteira</button>
<button type="button" class="aba" data-aba="atividade" role="tab">Atividade</button>
<button type="button" class="aba" data-aba="termo" role="tab">Termo de dados</button>
</nav>
<!-- CLIENTES -->
@@ -2420,6 +2534,21 @@ data:
</div>
</section>
<!-- TERMO DE DADOS: o que autoriza ler o uso de dentro de cada sistema -->
<section id="aba-termo" class="painel-aba oculto">
<div class="cartao">
<h2>Termo de dados</h2>
<p class="ajuda">
Autoriza ver o &uacute;ltimo acesso, os alunos ativos e os treinos dos &uacute;ltimos 30
dias de cada cliente. S&oacute; a vers&atilde;o <b>publicada</b> aparece para o cliente
aceitar; rascunho n&atilde;o vale. Publicar &eacute; definitivo &mdash; mudar o texto
depois &eacute; uma vers&atilde;o nova, que pede aceite novo.
</p>
<div id="termoVersoes"><p class="ajuda">Abra esta aba para carregar.</p></div>
<button type="button" id="termoNovaVersao" class="btn claro oculto">Nova vers&atilde;o a partir da &uacute;ltima</button>
</div>
</section>
</section>
<!-- A FICHA de um cliente. Abre por cima, e fecha com Esc ou no X. -->
@@ -2432,7 +2561,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.83"></script>
<script src="/publico/painel.js?v=2.84"></script>
</body>
</html>
@@ -2467,6 +2596,7 @@ data:
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var API_TERMO = "/api/plataforma/painel/termo-de-dados";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
@@ -2803,14 +2933,28 @@ data:
return (a.ativa ? s : '<b class="vermelho">' + s + "</b>") + (a.forma ? " · " + esc(a.forma) : "");
}
function usoCurto(u) {
function usoCurto(u, d) {
if (!u) return '<span class="ajuda">—</span>';
if (!u.disponivel) return '<span class="ajuda">indisponível</span>';
return esc(u.usuarios) + " usuário(s)" + '<span class="sub">'
+ (u.sessoesAtivas > 0 ? '<span class="ponto-on"></span>' + u.sessoesAtivas + " logado(s)" : "ninguém logado")
+ "</span>";
+ "</span>" + '<span class="sub">' + ultimoAcessoCurto(d) + "</span>";
}
/* O uso de DENTRO do sistema so existe com o termo de dados aceito — sem ele
o servidor nem pergunta, e a tela diz por que nao ha numero. */
var ROTULO_TERMO = {
SEM_TERMO_PUBLICADO: "termo não publicado",
AGUARDANDO_ACEITE: "sem aceite do termo",
INDISPONIVEL: "termo indisponível"
};
function ultimoAcessoCurto(d) {
if (!d) return "";
if (d.termo !== "ACEITO") return esc(ROTULO_TERMO[d.termo] || "");
return d.ultimoAcesso ? "último acesso " + esc(ha(d.ultimoAcesso)) : "sem acesso registrado";
}
function numeroOuTraco(v) { return v === null || v === undefined ? "—" : esc(v); }
function pintarLinhas() {
var lista = filtrados();
vazio.classList.toggle("oculto", lista.length > 0);
@@ -2834,7 +2978,7 @@ data:
+ '<td class="num">' + reais(c.valorMensal) + diferencaDeTabela(c) + "</td>"
+ '<td class="num">' + (f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
+ "<td>" + proximaCobranca(f) + "</td>"
+ "<td>" + usoCurto(c.uso) + "</td>"
+ "<td>" + usoCurto(c.uso, c.usoDeDentro) + "</td>"
+ "<td>" + seloSubconta(c.subconta) + "</td>"
+ "</tr>";
}).join("");
@@ -2923,6 +3067,7 @@ data:
? item("Pessoas com conta", esc(u.usuarios)) + item("Logadas agora", esc(u.sessoesAtivas))
+ item("Login", u.realmLigado ? "ligado" : '<b class="vermelho">desligado</b>')
: item("Login", u ? "indisponível agora" : "ainda sem realm"))
+ usoDeDentroNaFicha(c.usoDeDentro)
+ item("Realm", txt(c.realm))
+ item("Cobrança dos alunos", seloSubconta(c.subconta) + (c.subconta && c.subconta.desde ? " desde " + data(c.subconta.desde) : ""))
+ "</dl></section>"
@@ -3051,6 +3196,89 @@ data:
}).join("") || '<li class="ajuda">Nada ainda.</li>';
}
function usoDeDentroNaFicha(d) {
if (!d) return "";
if (d.termo !== "ACEITO") return item("Uso de dentro", esc(ROTULO_TERMO[d.termo] || "—"));
return item("Último acesso", d.ultimoAcesso
? dataHora(d.ultimoAcesso) + " (" + esc(ha(d.ultimoAcesso)) + ")" : "nenhum desde o aceite")
+ item("Alunos ativos", numeroOuTraco(d.alunosAtivos))
+ item("Treinos (30 dias)", numeroOuTraco(d.treinos30Dias));
}
/* ── termo de dados ─────────────────────────────────────────────────────── */
function pedirTermo(metodo, caminho, corpo) {
var h = { "x-atm-admin-token": tokenAtual };
if (corpo) h["Content-Type"] = "application/json";
return fetch(API_TERMO + (caminho || ""), { method: metodo, headers: h,
body: corpo ? JSON.stringify(corpo) : undefined })
.then(function (r) {
if (r.ok) return r.json();
return r.json().catch(function () { return {}; }).then(function (d) {
throw new Error(d.erro || "HTTP " + r.status);
});
});
}
function versaoDoTermo(v) {
var aceites = (v.aceites || []).map(function (a) { return esc(a.tenant) + " (" + data(a.aceitoEm) + ")"; });
var cab = "<h3>Versão " + esc(v.versao) + " "
+ (v.publicadoEm ? '<span class="selo ok">publicada em ' + data(v.publicadoEm) + "</span>"
: '<span class="selo alerta">rascunho</span>') + "</h3>";
if (v.publicadoEm) {
return '<div class="termo-versao">' + cab
+ '<p class="ajuda">Aceites vivos: ' + (aceites.length ? aceites.join(", ") : "nenhum") + "</p>"
+ '<div class="texto-publicado">' + esc(v.texto) + "</div></div>";
}
return '<div class="termo-versao" data-versao="' + esc(v.versao) + '">' + cab
+ '<textarea aria-label="Texto da versão ' + esc(v.versao) + '">' + esc(v.texto) + "</textarea>"
+ '<div class="termo-acoes">'
+ '<button type="button" class="btn claro" data-termo-acao="salvar">Salvar rascunho</button>'
+ '<button type="button" class="btn" data-termo-acao="publicar">Publicar</button>'
+ '<span class="erro-termo"></span></div></div>';
}
function carregarTermo() {
if (!tokenAtual) return Promise.resolve();
return pedirTermo("GET").then(function (lista) {
$("termoVersoes").innerHTML = (lista || []).map(versaoDoTermo).join("")
|| '<p class="ajuda">Nenhuma versão.</p>';
var temRascunho = (lista || []).some(function (v) { return !v.publicadoEm; });
$("termoNovaVersao").classList.toggle("oculto", temRascunho);
}).catch(function (e) {
$("termoVersoes").innerHTML = '<p class="ajuda">Não consegui carregar: ' + esc(e.message) + "</p>";
});
}
function agirNoTermo(botao) {
var caixa = botao.closest(".termo-versao");
var versao = caixa.getAttribute("data-versao");
var erro = caixa.querySelector(".erro-termo");
var texto = caixa.querySelector("textarea").value;
var acao = botao.getAttribute("data-termo-acao");
if (acao === "publicar" && !window.confirm("Publicar a versão " + versao + "? O texto não pode mais "
+ "ser mudado, e passa a aparecer para os clientes aceitarem.")) return Promise.resolve();
erro.textContent = "";
botao.disabled = true;
// Publicar salva antes: o que vai para o cliente e o que esta na caixa.
return pedirTermo("PUT", "/" + encodeURIComponent(versao), { texto: texto })
.then(function () {
return acao === "publicar" ? pedirTermo("POST", "/" + encodeURIComponent(versao) + "/publicar") : null;
})
.then(carregarTermo)
.catch(function (e) { erro.textContent = e.message; botao.disabled = false; });
}
$("termoVersoes").addEventListener("click", function (ev) {
var b = ev.target.closest ? ev.target.closest("[data-termo-acao]") : null;
if (b) agirNoTermo(b);
});
$("termoNovaVersao").addEventListener("click", function () {
pedirTermo("POST").then(carregarTermo).catch(function (e) {
$("termoVersoes").insertAdjacentHTML("afterbegin", '<p class="erro-termo">' + esc(e.message) + "</p>");
});
});
/* ── montagem ───────────────────────────────────────────────────────────── */
function pintar() {
@@ -3149,6 +3377,7 @@ data:
document.querySelectorAll(".painel-aba").forEach(function (p) {
p.classList.toggle("oculto", p.id !== "aba-" + b.getAttribute("data-aba"));
});
if (b.getAttribute("data-aba") === "termo") carregarTermo();
});
});
+239 -10
View File
@@ -1410,6 +1410,24 @@ data:
.form-cobranca .btn:disabled { opacity: .6; cursor: wait; }
.erro-form { color: var(--perigo); font-size: 14px; margin: 0; }
/* ── o termo de dados, dentro do cartao ─────────────────────────────────── */
.termo {
align-self: stretch; margin-top: 20px; padding-top: 16px;
border-top: 1px dashed var(--line);
display: flex; flex-direction: column; align-items: flex-start; gap: 8px;
}
.termo .selo { margin-bottom: 0; }
.produto .termo .btn { margin-top: 6px; }
.termo .btn:disabled { opacity: .6; cursor: wait; }
.btn.secundario { background: transparent; color: var(--navy); border: 1px solid var(--line); }
.btn.secundario:hover { background: var(--line); }
.termo details { align-self: stretch; font-size: 14px; }
.termo summary { cursor: pointer; font-weight: 600; }
.termo-texto {
white-space: pre-wrap; margin-top: 8px; padding: 12px; max-height: 280px; overflow-y: auto;
border: 1px solid var(--line); border-radius: 10px; background: var(--branco); line-height: 1.5;
}
/* Empilhado, a foto sai: no celular ela so empurraria os produtos para baixo. */
@media (max-width: 760px) {
.topo { padding: 12px 16px; }
@@ -1445,7 +1463,7 @@ data:
numero a cada publicacao que mexa nesses arquivos.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.82">
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.83">
</head>
<body>
@@ -1515,7 +1533,7 @@ data:
<footer class="direitos">&copy; 2026 Athletic Map. Todos os direitos reservados.</footer>
<script src="/publico/minha-conta.js?v=2.82"></script>
<script src="/publico/minha-conta.js?v=2.83"></script>
</body>
</html>
@@ -1553,7 +1571,8 @@ data:
CLIENTE: "painel-do-cliente",
RETORNO: location.origin + "/minha-conta",
API: "/api/cliente/meus-produtos",
COBRANCA: "/api/cliente/cobranca"
COBRANCA: "/api/cliente/cobranca",
TERMO: "/api/cliente/termo-de-dados"
};
var CHAVE_VERIFIER = "atm.conta.verifier";
@@ -1725,7 +1744,7 @@ data:
produtos.innerHTML = (lista || []).map(cartao).join("");
nenhum.classList.toggle("oculto", (lista || []).length > 0);
mostrar(conteudo);
return carregarCobranca();
return Promise.all([carregarCobranca(), carregarTermo()]);
});
}
@@ -1835,7 +1854,10 @@ data:
if (!alvo) return;
var velho = alvo.querySelector(".cobranca");
if (velho) velho.remove();
alvo.insertAdjacentHTML("beforeend", alertaDeCobranca(c));
// Antes do bloco do termo, se ele ja estiver la: a cobranca e mais urgente.
var termo = alvo.querySelector(".termo");
if (termo) termo.insertAdjacentHTML("beforebegin", alertaDeCobranca(c));
else alvo.insertAdjacentHTML("beforeend", alertaDeCobranca(c));
});
})
.catch(function (e) { if (window.console) console.warn("cobranca:", e); });
@@ -1872,6 +1894,83 @@ data:
});
}
/* ── O TERMO DE DADOS ────────────────────────────────────────────────────────
*
* A autorizacao para a Athletic Map ver o USO do sistema (ultimo acesso,
* alunos ativos, treinos). Sem termo publicado, o bloco nem aparece — o
* servidor devolve `termo: null` enquanto o texto e rascunho. Como a
* cobranca, falhar aqui nao derruba a tela. */
function dataCurta(iso) {
var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || ""));
return m ? m[3] + "/" + m[2] + "/" + m[1] : "";
}
function blocoDoTermo(termo, e) {
var titulo = '<span class="cobranca-titulo">Acompanhamento de uso</span>';
if (e.aceito) {
return '<div class="termo">' + titulo
+ '<span class="selo ok">autorizado' + (e.aceitoEm ? " em " + dataCurta(e.aceitoEm) : "") + "</span>"
+ '<p class="ajuda">Vemos s&oacute; a data do &uacute;ltimo acesso e contagens de alunos e treinos.</p>'
+ '<button type="button" class="btn secundario" data-termo="revogar" data-tenant="'
+ escapar(e.tenant) + '">Revogar autoriza&ccedil;&atilde;o</button>'
+ '<p class="erro-form oculto"></p></div>';
}
return '<div class="termo">' + titulo
+ '<span class="selo neutro">n&atilde;o autorizado</span>'
+ '<p class="ajuda">Autorize a Athletic Map a acompanhar o uso do sistema para dar suporte.</p>'
+ '<details><summary>Ler o termo (vers&atilde;o ' + escapar(termo.versao) + ")</summary>"
+ '<div class="termo-texto">' + escapar(termo.texto) + "</div></details>"
+ '<button type="button" class="btn" data-termo="aceitar" data-tenant="'
+ escapar(e.tenant) + '">Li e autorizo</button>'
+ '<p class="erro-form oculto"></p></div>';
}
function carregarTermo() {
return fetch(CONFIG.TERMO, { headers: { Authorization: "Bearer " + token } })
.then(function (r) { return r.ok ? r.json() : { termo: null, escolinhas: [] }; })
.then(function (d) {
produtos.querySelectorAll(".termo").forEach(function (el) { el.remove(); });
if (!d || !d.termo) return;
(d.escolinhas || []).forEach(function (e) {
var alvo = null;
produtos.querySelectorAll("[data-tenant]").forEach(function (el) {
if (el.tagName === "ARTICLE" && el.getAttribute("data-tenant") === e.tenant) alvo = el;
});
if (alvo) alvo.insertAdjacentHTML("beforeend", blocoDoTermo(d.termo, e));
});
})
.catch(function (e) { if (window.console) console.warn("termo:", e); });
}
function agirNoTermo(botao) {
var acao = botao.getAttribute("data-termo");
var erro = botao.parentNode.querySelector(".erro-form");
if (acao === "revogar" && typeof window.confirm === "function"
&& !window.confirm("Revogar? Paramos de ver o uso e apagamos os registros de acesso guardados.")) {
return Promise.resolve();
}
botao.disabled = true;
return fetch(CONFIG.TERMO + "/" + encodeURIComponent(botao.getAttribute("data-tenant")), {
method: acao === "aceitar" ? "POST" : "DELETE",
headers: { Authorization: "Bearer " + token }
}).then(function (r) {
if (r.ok) return carregarTermo();
return r.json().catch(function () { return {}; }).then(function (d) {
throw new Error(d.erro || "Nao consegui registrar agora. Tente de novo em instantes.");
});
}).catch(function (e) {
erro.textContent = e.message;
erro.classList.remove("oculto");
botao.disabled = false;
});
}
if (produtos) produtos.addEventListener("click", function (ev) {
var b = ev.target && ev.target.closest ? ev.target.closest("[data-termo]") : null;
if (b) agirNoTermo(b);
});
/* Delegacao, e nao `onsubmit=`: a CSP recusa manipulador embutido, e os
formularios nascem depois deste codigo. */
if (produtos) produtos.addEventListener("submit", function (ev) {
@@ -2234,6 +2333,20 @@ data:
.grade-ficha dl > div { grid-template-columns: 110px 1fr; }
}
/* ── termo de dados ──────────────────────────────────────────────────────── */
.termo-versao { border: 1px solid var(--line); border-radius: 12px; padding: 16px; margin: 14px 0; }
.termo-versao h3 { margin: 0 0 6px; font-size: 15px; display: flex; gap: 10px; align-items: center; }
.termo-versao textarea {
width: 100%; min-height: 320px; font: inherit; font-size: 14px; line-height: 1.5;
padding: 12px; border: 1px solid var(--line); border-radius: 10px; box-sizing: border-box;
}
.termo-versao .texto-publicado {
white-space: pre-wrap; font-size: 14px; line-height: 1.5; max-height: 260px; overflow-y: auto;
background: var(--paper); border-radius: 10px; padding: 12px;
}
.termo-acoes { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 10px; }
.erro-termo { color: var(--perigo); font-size: 13px; }
painel.html: |
<!DOCTYPE html>
<html lang="pt-BR">
@@ -2258,7 +2371,7 @@ data:
`Cache-Control: immutable` por um ano. Suba o numero a cada publicacao.
-->
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/painel.css?v=2.83">
<link rel="stylesheet" href="/publico/painel.css?v=2.84">
</head>
<body>
@@ -2317,6 +2430,7 @@ data:
<button type="button" class="aba" data-aba="vendas" role="tab">Vendas e leads</button>
<button type="button" class="aba" data-aba="receita" role="tab">Receita e carteira</button>
<button type="button" class="aba" data-aba="atividade" role="tab">Atividade</button>
<button type="button" class="aba" data-aba="termo" role="tab">Termo de dados</button>
</nav>
<!-- CLIENTES -->
@@ -2420,6 +2534,21 @@ data:
</div>
</section>
<!-- TERMO DE DADOS: o que autoriza ler o uso de dentro de cada sistema -->
<section id="aba-termo" class="painel-aba oculto">
<div class="cartao">
<h2>Termo de dados</h2>
<p class="ajuda">
Autoriza ver o &uacute;ltimo acesso, os alunos ativos e os treinos dos &uacute;ltimos 30
dias de cada cliente. S&oacute; a vers&atilde;o <b>publicada</b> aparece para o cliente
aceitar; rascunho n&atilde;o vale. Publicar &eacute; definitivo &mdash; mudar o texto
depois &eacute; uma vers&atilde;o nova, que pede aceite novo.
</p>
<div id="termoVersoes"><p class="ajuda">Abra esta aba para carregar.</p></div>
<button type="button" id="termoNovaVersao" class="btn claro oculto">Nova vers&atilde;o a partir da &uacute;ltima</button>
</div>
</section>
</section>
<!-- A FICHA de um cliente. Abre por cima, e fecha com Esc ou no X. -->
@@ -2432,7 +2561,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.83"></script>
<script src="/publico/painel.js?v=2.84"></script>
</body>
</html>
@@ -2467,6 +2596,7 @@ data:
var API = "/api/plataforma/painel/visao";
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var API_TERMO = "/api/plataforma/painel/termo-de-dados";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
@@ -2803,14 +2933,28 @@ data:
return (a.ativa ? s : '<b class="vermelho">' + s + "</b>") + (a.forma ? " · " + esc(a.forma) : "");
}
function usoCurto(u) {
function usoCurto(u, d) {
if (!u) return '<span class="ajuda">—</span>';
if (!u.disponivel) return '<span class="ajuda">indisponível</span>';
return esc(u.usuarios) + " usuário(s)" + '<span class="sub">'
+ (u.sessoesAtivas > 0 ? '<span class="ponto-on"></span>' + u.sessoesAtivas + " logado(s)" : "ninguém logado")
+ "</span>";
+ "</span>" + '<span class="sub">' + ultimoAcessoCurto(d) + "</span>";
}
/* O uso de DENTRO do sistema so existe com o termo de dados aceito — sem ele
o servidor nem pergunta, e a tela diz por que nao ha numero. */
var ROTULO_TERMO = {
SEM_TERMO_PUBLICADO: "termo não publicado",
AGUARDANDO_ACEITE: "sem aceite do termo",
INDISPONIVEL: "termo indisponível"
};
function ultimoAcessoCurto(d) {
if (!d) return "";
if (d.termo !== "ACEITO") return esc(ROTULO_TERMO[d.termo] || "");
return d.ultimoAcesso ? "último acesso " + esc(ha(d.ultimoAcesso)) : "sem acesso registrado";
}
function numeroOuTraco(v) { return v === null || v === undefined ? "—" : esc(v); }
function pintarLinhas() {
var lista = filtrados();
vazio.classList.toggle("oculto", lista.length > 0);
@@ -2834,7 +2978,7 @@ data:
+ '<td class="num">' + reais(c.valorMensal) + diferencaDeTabela(c) + "</td>"
+ '<td class="num">' + (f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
+ "<td>" + proximaCobranca(f) + "</td>"
+ "<td>" + usoCurto(c.uso) + "</td>"
+ "<td>" + usoCurto(c.uso, c.usoDeDentro) + "</td>"
+ "<td>" + seloSubconta(c.subconta) + "</td>"
+ "</tr>";
}).join("");
@@ -2923,6 +3067,7 @@ data:
? item("Pessoas com conta", esc(u.usuarios)) + item("Logadas agora", esc(u.sessoesAtivas))
+ item("Login", u.realmLigado ? "ligado" : '<b class="vermelho">desligado</b>')
: item("Login", u ? "indisponível agora" : "ainda sem realm"))
+ usoDeDentroNaFicha(c.usoDeDentro)
+ item("Realm", txt(c.realm))
+ item("Cobrança dos alunos", seloSubconta(c.subconta) + (c.subconta && c.subconta.desde ? " desde " + data(c.subconta.desde) : ""))
+ "</dl></section>"
@@ -3051,6 +3196,89 @@ data:
}).join("") || '<li class="ajuda">Nada ainda.</li>';
}
function usoDeDentroNaFicha(d) {
if (!d) return "";
if (d.termo !== "ACEITO") return item("Uso de dentro", esc(ROTULO_TERMO[d.termo] || "—"));
return item("Último acesso", d.ultimoAcesso
? dataHora(d.ultimoAcesso) + " (" + esc(ha(d.ultimoAcesso)) + ")" : "nenhum desde o aceite")
+ item("Alunos ativos", numeroOuTraco(d.alunosAtivos))
+ item("Treinos (30 dias)", numeroOuTraco(d.treinos30Dias));
}
/* ── termo de dados ─────────────────────────────────────────────────────── */
function pedirTermo(metodo, caminho, corpo) {
var h = { "x-atm-admin-token": tokenAtual };
if (corpo) h["Content-Type"] = "application/json";
return fetch(API_TERMO + (caminho || ""), { method: metodo, headers: h,
body: corpo ? JSON.stringify(corpo) : undefined })
.then(function (r) {
if (r.ok) return r.json();
return r.json().catch(function () { return {}; }).then(function (d) {
throw new Error(d.erro || "HTTP " + r.status);
});
});
}
function versaoDoTermo(v) {
var aceites = (v.aceites || []).map(function (a) { return esc(a.tenant) + " (" + data(a.aceitoEm) + ")"; });
var cab = "<h3>Versão " + esc(v.versao) + " "
+ (v.publicadoEm ? '<span class="selo ok">publicada em ' + data(v.publicadoEm) + "</span>"
: '<span class="selo alerta">rascunho</span>') + "</h3>";
if (v.publicadoEm) {
return '<div class="termo-versao">' + cab
+ '<p class="ajuda">Aceites vivos: ' + (aceites.length ? aceites.join(", ") : "nenhum") + "</p>"
+ '<div class="texto-publicado">' + esc(v.texto) + "</div></div>";
}
return '<div class="termo-versao" data-versao="' + esc(v.versao) + '">' + cab
+ '<textarea aria-label="Texto da versão ' + esc(v.versao) + '">' + esc(v.texto) + "</textarea>"
+ '<div class="termo-acoes">'
+ '<button type="button" class="btn claro" data-termo-acao="salvar">Salvar rascunho</button>'
+ '<button type="button" class="btn" data-termo-acao="publicar">Publicar</button>'
+ '<span class="erro-termo"></span></div></div>';
}
function carregarTermo() {
if (!tokenAtual) return Promise.resolve();
return pedirTermo("GET").then(function (lista) {
$("termoVersoes").innerHTML = (lista || []).map(versaoDoTermo).join("")
|| '<p class="ajuda">Nenhuma versão.</p>';
var temRascunho = (lista || []).some(function (v) { return !v.publicadoEm; });
$("termoNovaVersao").classList.toggle("oculto", temRascunho);
}).catch(function (e) {
$("termoVersoes").innerHTML = '<p class="ajuda">Não consegui carregar: ' + esc(e.message) + "</p>";
});
}
function agirNoTermo(botao) {
var caixa = botao.closest(".termo-versao");
var versao = caixa.getAttribute("data-versao");
var erro = caixa.querySelector(".erro-termo");
var texto = caixa.querySelector("textarea").value;
var acao = botao.getAttribute("data-termo-acao");
if (acao === "publicar" && !window.confirm("Publicar a versão " + versao + "? O texto não pode mais "
+ "ser mudado, e passa a aparecer para os clientes aceitarem.")) return Promise.resolve();
erro.textContent = "";
botao.disabled = true;
// Publicar salva antes: o que vai para o cliente e o que esta na caixa.
return pedirTermo("PUT", "/" + encodeURIComponent(versao), { texto: texto })
.then(function () {
return acao === "publicar" ? pedirTermo("POST", "/" + encodeURIComponent(versao) + "/publicar") : null;
})
.then(carregarTermo)
.catch(function (e) { erro.textContent = e.message; botao.disabled = false; });
}
$("termoVersoes").addEventListener("click", function (ev) {
var b = ev.target.closest ? ev.target.closest("[data-termo-acao]") : null;
if (b) agirNoTermo(b);
});
$("termoNovaVersao").addEventListener("click", function () {
pedirTermo("POST").then(carregarTermo).catch(function (e) {
$("termoVersoes").insertAdjacentHTML("afterbegin", '<p class="erro-termo">' + esc(e.message) + "</p>");
});
});
/* ── montagem ───────────────────────────────────────────────────────────── */
function pintar() {
@@ -3149,6 +3377,7 @@ data:
document.querySelectorAll(".painel-aba").forEach(function (p) {
p.classList.toggle("oculto", p.id !== "aba-" + b.getAttribute("data-aba"));
});
if (b.getAttribute("data-aba") === "termo") carregarTermo();
});
});