chore(tenants): conteudo de publico/ atualizado

This commit is contained in:
deploy
2026-09-29 16:51:27 +00:00
parent ad92deb861
commit 3b0c2fffaf
10 changed files with 10582 additions and 202 deletions
+559 -25
View File
@@ -196,6 +196,180 @@ data:
};
})(typeof window !== 'undefined' ? window : this);
contratar.css: |
/* A pagina /contratar. Arquivo proprio, e nao <style> embutido: a CSP do
servidor nao libera estilo inline. */
body { display: flex; min-height: 100vh; align-items: center; justify-content: center;
margin: 0; padding: 16px; box-sizing: border-box;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
main { max-width: 32rem; width: 100%; }
h1 { font-size: 1.5rem; margin: 0 0 .75rem; }
p { line-height: 1.55; margin: 0 0 .75rem; }
.resumo { display: grid; grid-template-columns: max-content 1fr; gap: .4rem 1rem; margin: 1rem 0 1.25rem; }
.resumo dt { opacity: .7; }
.resumo dd { margin: 0; font-weight: 600; }
.aceite { display: flex; gap: .6rem; align-items: flex-start; margin: 0 0 1rem; line-height: 1.5; }
.aceite input { margin-top: .3rem; width: 1.1rem; height: 1.1rem; flex: none; }
.erro { color: #d93025; font-weight: 600; }
#seguir { padding: .8rem 1.4rem; border: 0; border-radius: 8px; background: #28c76f; color: #06210f;
font-weight: 600; font-size: 1rem; cursor: pointer; }
#seguir:disabled { opacity: .6; cursor: default; }
.nota { margin-top: 1.25rem; font-size: .85rem; opacity: .65; }
contratar.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<meta name="referrer" content="no-referrer">
<title>Confirmar contratação — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/contratar.css?v=1">
</head>
<body data-situacao="carregando">
<!-- O link de pagamento aberto por quem fechou a venda (endpoint administrativo)
chega AQUI, e nao direto no Asaas: quem paga ve o plano, o preco e aceita os
Termos de Uso antes do checkout. Ver AbrirAssinaturaDoLead (V024). -->
<main>
<h1 id="titulo">Confirmar contratação</h1>
<p id="mensagem" aria-live="polite">Carregando…</p>
<form id="formulario" hidden novalidate>
<dl class="resumo">
<dt>Escolinha</dt><dd id="escolinha">—</dd>
<dt>Plano</dt><dd id="plano">—</dd>
<dt>Valor</dt><dd id="valor">—</dd>
</dl>
<div class="aceite">
<input id="termos" type="checkbox">
<label for="termos">Li e aceito os <a href="/termos" target="_blank" rel="noopener">Termos de Uso</a>
(versão <span id="versao">—</span>) para contratar a Athletic Map.</label>
</div>
<p id="erro" class="erro" role="alert" hidden></p>
<button id="seguir" type="submit">Aceitar e ir para o pagamento</button>
<p class="nota">O pagamento é feito no Asaas, nosso parceiro de cobrança.</p>
</form>
</main>
<script src="/publico/contratar.js?v=1"></script>
</body>
</html>
contratar.js: |
/* ===========================================================================
* A pagina /contratar — o aceite dos Termos de Uso ANTES do checkout.
*
* Quem fecha a venda abre o link pelo endpoint administrativo, e o que o
* comprador recebe e ESTE endereco (`/contratar?c=TOKEN`), e nao o do Asaas. A
* pagina mostra o plano e o preco, pede a caixa dos Termos e so entao recebe do
* servidor o endereco do checkout — que nao existe em nenhum outro lugar fora do
* banco. Ver AbrirAssinaturaDoLead e V024.
*
* Script em arquivo, e nao inline: a CSP do servidor diz `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var API = "/api/public/contratacoes/confirmacao/";
var token = new URLSearchParams(location.search).get("c");
var titulo = document.getElementById("titulo");
var mensagem = document.getElementById("mensagem");
var formulario = document.getElementById("formulario");
var caixa = document.getElementById("termos");
var erro = document.getElementById("erro");
var botao = document.getElementById("seguir");
var versao = null;
/* A unica saida da pagina. Costura para o teste ver PARA ONDE ela manda — o
jsdom nao deixa trocar `location.assign`. */
function navegar(url) {
if (typeof window.__atmNavegar === "function") { window.__atmNavegar(url); return; }
location.assign(url);
}
function reais(v) {
var n = Number(v);
return isFinite(n) ? n.toLocaleString("pt-BR", { style: "currency", currency: "BRL" }) : "—";
}
function nomeDoPlano(p) {
var n = String(p || "");
return n ? n.charAt(0).toUpperCase() + n.slice(1) : "—";
}
function falhar(texto) {
document.body.setAttribute("data-situacao", "falhou");
titulo.textContent = "Não foi possível abrir esta contratação";
mensagem.textContent = texto;
formulario.hidden = true;
}
function mostrarErro(texto) {
erro.textContent = texto;
erro.hidden = false;
}
if (!token) {
falhar("O endereço está incompleto. Peça um link novo a quem o enviou.");
return;
}
fetch(API + encodeURIComponent(token), { headers: { "Accept": "application/json" } })
.then(function (r) {
if (r.status === 404) throw new Error("Este link de contratação não existe. Peça um novo a quem o enviou.");
if (!r.ok) throw new Error("Não conseguimos carregar agora. Tente de novo em instantes.");
return r.json();
})
.then(function (d) {
versao = d.versao;
// textContent em tudo: o nome da escolinha foi digitado por alguem.
document.getElementById("escolinha").textContent = d.escolinha || "—";
document.getElementById("plano").textContent = nomeDoPlano(d.plano)
+ (d.ciclo === "ANUAL" ? " (anual)" : " (mensal)");
document.getElementById("valor").textContent = reais(d.valor) + (d.ciclo === "ANUAL" ? "/ano" : "/mês");
document.getElementById("versao").textContent = d.versao || "—";
mensagem.textContent = "Confira o que você está contratando e aceite os Termos de Uso para seguir ao pagamento.";
document.body.setAttribute("data-situacao", "pronta");
formulario.hidden = false;
})
.catch(function (e) { falhar(e.message); });
formulario.addEventListener("submit", function (ev) {
ev.preventDefault();
erro.hidden = true;
if (!caixa.checked) {
mostrarErro("Para contratar, leia e aceite os Termos de Uso.");
return;
}
botao.disabled = true;
fetch(API + encodeURIComponent(token), {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json" },
body: JSON.stringify({ termos_aceitos: true, termos_versao: versao })
}).then(function (r) {
return r.json().catch(function () { return {}; }).then(function (d) {
if (r.ok && typeof d.link === "string" && /^https:\/\//.test(d.link)) {
navegar(d.link);
return;
}
if (d.codigo === "termos_desatualizados") {
// O texto mudou enquanto a pagina estava aberta: mostra a versao nova
// e pede a caixa de novo — o aceite antigo nao vale para ela.
versao = d.versao;
document.getElementById("versao").textContent = d.versao || "—";
caixa.checked = false;
}
throw new Error(d.erro || d.mensagem || "Não conseguimos seguir agora. Tente de novo em instantes.");
});
}).catch(function (e) {
mostrarErro(e.message);
botao.disabled = false;
});
});
})();
fontes--fontes.css: |
/* ============================================================
* As fontes da landing page, servidas POR NOS.
@@ -471,6 +645,9 @@ data:
.consent{display:grid; grid-template-columns:18px 1fr; gap:10px; align-items:start;}
.consent input{width:18px; height:18px; margin-top:3px; accent-color:var(--laranja);}
.consent label{font-family:"Barlow",sans-serif; font-size:.81rem; font-weight:400; color:var(--muted); line-height:1.45; margin:0;}
/* O aceite dos Termos de Uso so existe na contratacao (plano escolhido). */
.consent.termos{display:none;}
.consent.termos.visivel{display:grid;}
.btn{display:inline-flex; align-items:center; justify-content:center; gap:9px; width:100%; border:none; border-radius:var(--r-pill); background:linear-gradient(100deg,var(--laranja) 0%,#FF9E42 100%); color:#fff; font-size:1.05rem; font-weight:600; padding:15px 22px; cursor:pointer; text-decoration:none; box-shadow:0 6px 16px -8px rgba(245,130,31,.9);}
.btn:hover{filter:brightness(1.05)}
.form-foot{font-size:.79rem; color:var(--muted); text-align:center; margin:13px 0 0;}
@@ -619,7 +796,7 @@ data:
<meta property="og:type" content="website">
<title>Athletic Map para Escolinhas</title>
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/lp-escolinhas.css">
<link rel="stylesheet" href="/publico/lp-escolinhas.css?v=2.86">
</head>
<body>
@@ -695,6 +872,15 @@ data:
<input id="consent" name="consent" type="checkbox" required>
<label for="consent">Autorizo a Athletic Map a entrar em contato e a tratar meus dados para esta finalidade, conforme a <a href="/privacidade" target="_blank" rel="noopener">Política de Privacidade</a>.</label>
</div>
<!-- So aparece com um plano escolhido (lp-escolinhas.js, `escolher`). Sem
este aceite a contratacao e recusada no servidor (400). O
`data-versao` TEM de ser a versao do termos.html e a
`atm.captacao.termos-de-uso-versao` do servico-captacao: mudou o
texto dos Termos, suba as tres juntas. -->
<div class="consent termos" id="aceiteTermos">
<input id="termos" type="checkbox" data-versao="1.0">
<label for="termos">Li e aceito os <a href="/termos" target="_blank" rel="noopener">Termos de Uso</a> (versão 1.0) para contratar a Athletic Map.</label>
</div>
<button class="btn" type="submit">Quero ver funcionando <span aria-hidden="true">→</span></button>
</div>
</form>
@@ -833,7 +1019,7 @@ data:
<!-- O portao de consentimento. Tem de vir ANTES do script da pagina: e ele
que decide se Meta e Google carregam. Ver publico/consentimento.js. -->
<script src="/publico/consentimento.js"></script>
<script src="/publico/lp-escolinhas.js?v=2.85"></script>
<script src="/publico/lp-escolinhas.js?v=2.86"></script>
</body>
</html>
@@ -1006,6 +1192,19 @@ data:
});
}
/* O aceite dos Termos de Uso. So aparece — e so e exigido — com um plano
escolhido: e contrato, e quem so pede contato nao contrata nada. O servidor
recusa a contratacao sem ele (400), na versao que esta no `data-versao`. */
var termos = document.getElementById("termos");
var caixaTermos = document.getElementById("aceiteTermos");
function mostrarTermos(sim) {
if (caixaTermos) caixaTermos.classList.toggle("visivel", sim);
if (termos) {
termos.required = sim;
if (!sim) termos.checked = false;
}
}
function escolher(plano) {
escolha = { plano: plano, ciclo: cicloAtual };
var aviso = document.getElementById("planoEscolhido");
@@ -1017,6 +1216,8 @@ data:
}
var b = form.querySelector("button[type=submit]");
if (b) b.innerHTML = "Ir para o pagamento &rarr;";
// Contratar exige o aceite dos Termos de Uso; pedir contato, nao.
mostrarTermos(true);
track("InitiateCheckout", { content_name: plano, content_category: cicloAtual });
document.getElementById("form").scrollIntoView({ behavior: "smooth", block: "start" });
}
@@ -1027,6 +1228,7 @@ data:
if (aviso) aviso.classList.remove("visivel");
var b = form.querySelector("button[type=submit]");
if (b) b.innerHTML = "Quero ver funcionando &rarr;";
mostrarTermos(false);
}
Array.prototype.forEach.call(document.querySelectorAll(".ciclo"), function (b) {
@@ -1114,6 +1316,17 @@ data:
if (!form.reportValidity()) return;
var data = Object.fromEntries(new FormData(form).entries());
data.consent = !!document.getElementById("consent").checked;
// A decisao do banner de medicao viaja com o lead. E ela que o servidor
// consulta antes de mandar o `Purchase` ao Meta e ao Google: quem recusou
// aqui nao e medido la. Sem decisao vai null — e null, la, e nao.
var decisao = AtmConsentimento.estado();
data.consentimento_medicao =
(decisao === AtmConsentimento.ACEITO || decisao === AtmConsentimento.RECUSADO) ? decisao : null;
if (escolha && !(termos && termos.checked)) {
alert("Para contratar, leia e aceite os Termos de Uso.");
if (termos && termos.focus) termos.focus();
return;
}
Object.assign(data, tracking);
data.pagina = location.pathname;
data.enviado_em = new Date().toISOString();
@@ -1181,7 +1394,9 @@ data:
if (escolha) {
fetch(CONFIG.CONTRATACAO_ENDPOINT, {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({lead: data, plano: escolha.plano, ciclo: escolha.ciclo})
body: JSON.stringify({lead: data, plano: escolha.plano, ciclo: escolha.ciclo,
termos_aceitos: true,
termos_versao: termos.getAttribute("data-versao")})
}).then(function(r){
return r.json().catch(function(){ return null; })
.then(function(corpo){ r.ok ? contratado(corpo) : recusado(corpo); });
@@ -1460,6 +1675,29 @@ data:
.lista-planos .selo { margin: 0; }
.plano-troca .link { background: none; border: 0; color: var(--orange-600); font-weight: 600; cursor: pointer; padding: 0; font: inherit; }
/* ── N16: a visao geral das escolinhas ──────────────────────────────────────
Uma tabela: metricas nas linhas, escolinhas nas colunas, o somado por
ultimo. Rola de lado DENTRO do cartao no celular — a pagina nao. */
.visao .hl { color: var(--orange); }
.visao-tabela { margin-top: 14px; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.visao-tabela table { border-collapse: collapse; width: 100%; font-size: 14px; min-width: 420px; }
.visao-tabela th, .visao-tabela td { padding: 9px 12px; text-align: right; border-top: 1px solid var(--line); white-space: nowrap; }
.visao-tabela thead th { border-top: 0; vertical-align: bottom; font-weight: 700; color: var(--navy); }
.visao-tabela thead th small { display: block; font-weight: 400; color: var(--muted); font-size: 12px; }
.visao-tabela th[scope="row"] { text-align: left; font-weight: 500; color: var(--muted); }
.visao-tabela .grupo th { text-align: left; padding-top: 16px; font-family: 'IBM Plex Mono', monospace;
font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--orange-600); border-top: 0; }
.visao-tabela .somado { background: #FFF6EC; font-weight: 700; }
.visao-tabela td.vazio { color: var(--muted); }
.visao-tabela td small { display: block; color: var(--muted); font-size: 12px; font-weight: 400; }
.visao .nota { font-size: 13px; margin-top: 10px; }
/* Os Termos de Uso (V024): o aviso de versao nova e a caixa dentro de uma acao. */
.aviso-termos { border-left: 4px solid var(--orange-600); }
.termos-uso-caixa { display: block; margin: 8px 0; line-height: 1.5; }
.termos-uso-caixa input { margin-right: 6px; }
.termos-uso-aceite { margin: 10px 0; }
minha-conta.html: |
<!DOCTYPE html>
<html lang="pt-BR">
@@ -1484,7 +1722,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.85">
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.88">
</head>
<body>
@@ -1527,6 +1765,15 @@ data:
</section>
<section id="conteudo" class="oculto">
<!-- N16: a visao geral das escolinhas DESTE comprador, lado a lado e somada.
So numeros agregados, que cada escolinha envia uma vez por dia.
Fica oculta enquanto nenhuma escolinha tiver mandado o resumo. -->
<section id="visao" class="cartao visao oculto" aria-labelledby="visaoTitulo">
<h2 id="visaoTitulo">Vis&atilde;o <span class="hl">geral</span></h2>
<p id="visaoSub" class="ajuda"></p>
<div id="visaoTabela" class="visao-tabela"></div>
</section>
<div id="produtos" class="produtos"></div>
<div id="nenhum" class="cartao oculto">
@@ -1554,7 +1801,7 @@ data:
<footer class="direitos">&copy; 2026 Athletic Map. Todos os direitos reservados.</footer>
<script src="/publico/minha-conta.js?v=2.86"></script>
<script src="/publico/minha-conta.js?v=2.88"></script>
</body>
</html>
@@ -1596,7 +1843,9 @@ data:
TERMO: "/api/cliente/termo-de-dados",
FATURAS: "/api/cliente/faturas",
PLANO: "/api/cliente/plano",
MODULOS: "/api/cliente/modulos"
MODULOS: "/api/cliente/modulos",
TERMOS_DE_USO: "/api/cliente/termos-de-uso",
VISAO: "/api/cliente/visao-geral"
};
var CHAVE_VERIFIER = "atm.conta.verifier";
@@ -1769,7 +2018,8 @@ data:
nenhum.classList.toggle("oculto", (lista || []).length > 0);
mostrar(conteudo);
(lista || []).forEach(function (p) { if (p.tenant && p.situacao === "ATIVA") acrescentarBotaoDePlano(p.tenant); });
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas()]);
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas(), carregarVisao(),
carregarTermosDeUso()]);
});
}
@@ -1948,12 +2198,7 @@ data:
if (corpo) h["Content-Type"] = "application/json";
return fetch(CONFIG.PLANO + "/" + encodeURIComponent(tenant), {
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 || "Não consegui agora. Tente de novo em instantes.");
});
});
}).then(respostaDaAcao);
}
function blocoDePlano(d) {
@@ -2000,7 +2245,13 @@ data:
var plano = botao.getAttribute("data-plano");
if (typeof window.confirm === "function" && !window.confirm("Mudar para " + (NOME_DO_PLANO[plano] || plano)
+ "? " + botao.getAttribute("data-quando") + ".")) { botao.disabled = false; return Promise.resolve(); }
return pedirPlano("POST", tenant, { plano: plano }).then(function (d) { mostrarPlano(tenant, d); }).catch(erro);
var trocar = function (aceite) {
var corpo = { plano: plano };
if (aceite) { corpo.termos_aceitos = aceite.termos_aceitos; corpo.termos_versao = aceite.termos_versao; }
return pedirPlano("POST", tenant, corpo).then(function (d) { mostrarPlano(tenant, d); })
.catch(function (e) { return e.termos ? pedirAceite(caixa, e.termos, "mudar de plano", trocar, botao) : erro(e); });
};
return trocar(null);
}
if (acao === "cancelar") {
return pedirPlano("DELETE", tenant).then(function (d) { mostrarPlano(tenant, d); }).catch(erro);
@@ -2013,21 +2264,124 @@ data:
if (b) agirNoPlano(b);
});
/* ── OS TERMOS DE USO (V024) ─────────────────────────────────────────────────
*
* Trocar de plano e comprar modulo MUDAM o contrato: se ele ainda nao aceitou a
* versao vigente dos Termos de Uso, o servidor responde 428 com a versao, e a
* caixa aparece ali mesmo — marcada, a acao e repetida levando o aceite. Se ja
* aceitou, nada e perguntado.
*
* Versao nova dos Termos: um aviso no topo pede o aceite, SEM bloquear nada —
* faturas, dados e o proprio sistema continuam. So as acoes que mudam o
* contrato pedem. Cancelar (troca agendada ou modulo) nunca pede. */
function respostaDaAcao(r) {
if (r.ok) return r.json();
return r.json().catch(function () { return {}; }).then(function (d) {
var e = new Error(d.erro || "Não consegui agora. Tente de novo em instantes.");
if (r.status === 428 && d.versao) e.termos = d;
throw e;
});
}
function caixaDeAceite(versao, rotulo) {
return '<label class="termos-uso-caixa"><input type="checkbox"> Li e aceito os '
+ '<a href="/termos" target="_blank" rel="noopener">Termos de Uso</a> (vers&atilde;o '
+ escapar(versao) + ")</label> "
+ '<button type="button" class="btn">' + rotulo + "</button>"
+ '<p class="erro-form oculto"></p>';
}
/* A caixa dentro do cartao da acao. `repetir` refaz o pedido com o aceite. */
function pedirAceite(caixa, t, oQue, repetir, botao) {
caixa.querySelectorAll(".termos-uso-aceite").forEach(function (el) { el.remove(); });
var div = document.createElement("div");
div.className = "termos-uso-aceite";
div.innerHTML = '<p class="ajuda">Para ' + escapar(oQue) + ", aceite os Termos de Uso vigentes.</p>"
+ caixaDeAceite(t.versao, "Aceitar e continuar");
caixa.insertBefore(div, caixa.querySelector(".erro-form"));
div.querySelector("button").addEventListener("click", function () {
var aviso = div.querySelector(".erro-form");
if (!div.querySelector("input").checked) {
aviso.textContent = "Marque a caixa para aceitar os Termos de Uso.";
aviso.classList.remove("oculto");
return;
}
div.querySelector("button").disabled = true;
repetir({ termos_aceitos: true, termos_versao: t.versao }).then(function () {
if (div.parentNode) div.parentNode.removeChild(div);
carregarTermosDeUso();
});
});
if (botao) botao.disabled = false;
return Promise.resolve();
}
function carregarTermosDeUso() {
return fetch(CONFIG.TERMOS_DE_USO, { headers: { Authorization: "Bearer " + token } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
var velho = document.getElementById("avisoTermos");
if (velho) velho.parentNode.removeChild(velho);
if (!d || !d.versao || !d.escolinhas) return;
var faltam = d.escolinhas.filter(function (e) { return !e.aceito; });
if (!faltam.length) return;
var aviso = document.createElement("section");
aviso.id = "avisoTermos";
aviso.className = "cartao aviso-termos";
aviso.innerHTML = "<h2>Termos de Uso</h2>"
+ '<p class="ajuda">A vers&atilde;o ' + escapar(d.versao) + " dos Termos de Uso ainda n&atilde;o foi aceita para "
+ faltam.map(function (e) { return "<b>" + escapar(e.escolinha || e.tenant) + "</b>"; }).join(", ")
+ ". Voc&ecirc; continua usando o sistema, vendo faturas e dados normalmente; s&oacute; trocar de plano"
+ " e comprar m&oacute;dulos pedem o aceite.</p>"
+ caixaDeAceite(d.versao, "Aceitar");
produtos.parentNode.insertBefore(aviso, produtos);
aviso.querySelector("button").addEventListener("click", function () {
aceitarTermosDeUso(aviso, d.versao);
});
})
.catch(function (e) { if (window.console) console.warn("termos de uso:", e); });
}
function aceitarTermosDeUso(aviso, versao) {
var erro = aviso.querySelector(".erro-form");
var botao = aviso.querySelector("button");
if (!aviso.querySelector("input").checked) {
erro.textContent = "Marque a caixa para aceitar os Termos de Uso.";
erro.classList.remove("oculto");
return Promise.resolve();
}
botao.disabled = true;
return fetch(CONFIG.TERMOS_DE_USO, {
method: "POST",
headers: { Authorization: "Bearer " + token, "Content-Type": "application/json" },
body: JSON.stringify({ termos_aceitos: true, termos_versao: versao })
}).then(function (r) {
if (r.ok) return carregarTermosDeUso();
return r.json().catch(function () { return {}; }).then(function (d) {
// Versao trocou com a pagina aberta: o aviso volta com a nova.
if (d.codigo === "termos_desatualizados") return carregarTermosDeUso();
throw new Error(d.erro || "Não consegui registrar agora. Tente de novo em instantes.");
});
}).catch(function (e) {
erro.textContent = e.message;
erro.classList.remove("oculto");
botao.disabled = false;
});
}
/* ── A LOJA DE MODULOS ───────────────────────────────────────────────────────
*
* Mesmas regras da troca de plano: comprar vale na hora e o valor entra na
* proxima cobranca; cancelar vale no fim do periodo pago. O que o plano ja
* inclui aparece como incluido, sem botao. */
function pedirModulo(metodo, tenant, modulo) {
function pedirModulo(metodo, tenant, modulo, corpo) {
var h = { Authorization: "Bearer " + token };
if (corpo) h["Content-Type"] = "application/json";
return fetch(CONFIG.MODULOS + "/" + encodeURIComponent(tenant) + (modulo ? "/" + encodeURIComponent(modulo) : ""), {
method: metodo, headers: { Authorization: "Bearer " + token }
}).then(function (r) {
if (r.ok) return r.json();
return r.json().catch(function () { return {}; }).then(function (d) {
throw new Error(d.erro || "Não consegui agora. Tente de novo em instantes.");
});
});
method: metodo, headers: h, body: corpo ? JSON.stringify(corpo) : undefined
}).then(respostaDaAcao);
}
function blocoDaLoja(d) {
@@ -2070,7 +2424,11 @@ data:
if (acao === "comprar") {
if (typeof window.confirm === "function" && !window.confirm("Comprar " + botao.getAttribute("data-nome")
+ "? Libera na hora; o valor entra na próxima cobrança.")) { botao.disabled = false; return Promise.resolve(); }
return pedirModulo("POST", tenant, modulo).then(mostrar).catch(erro);
var comprar = function (aceite) {
return pedirModulo("POST", tenant, modulo, aceite).then(mostrar)
.catch(function (e) { return e.termos ? pedirAceite(caixa, e.termos, "comprar o módulo", comprar, botao) : erro(e); });
};
return comprar(null);
}
if (acao === "cancelar") {
if (typeof window.confirm === "function" && !window.confirm("Cancelar este módulo? Ele continua até o fim do período pago."))
@@ -2140,6 +2498,109 @@ data:
.catch(function (e) { if (window.console) console.warn("faturas:", e); });
}
/* ── A VISAO GERAL (N16) ─────────────────────────────────────────────────────
*
* As escolinhas DESTE comprador lado a lado, e o somado quando ha mais de uma.
* Os numeros sao AGREGADOS que cada escolinha envia uma vez por dia — nenhum
* dado de aluno sai de la. Sem nenhuma escolinha com resumo, o bloco nem
* aparece. Como os outros complementos, falhar aqui nao derruba a tela. */
var MESES = ["janeiro", "fevereiro", "mar&ccedil;o", "abril", "maio", "junho", "julho",
"agosto", "setembro", "outubro", "novembro", "dezembro"];
function nomeDoMes(competencia) {
var m = /^(\d{4})-(\d{2})$/.exec(String(competencia || ""));
return m ? MESES[Number(m[2]) - 1] + " de " + m[1] : "";
}
function inteiro(v) {
var n = Number(v);
return v === null || v === undefined || isNaN(n) ? null : String(Math.round(n));
}
function pct(v) {
var n = Number(v);
return v === null || v === undefined || isNaN(n) ? null : n.toFixed(1).replace(".", ",") + "%";
}
/* Cada linha: rotulo, e como tirar o texto (e a observacao, se houver) de uma
coluna. `null` vira "—": o numero nao existe para aquela escolinha (sem o
modulo Financeiro, por exemplo), o que e diferente de zero. */
var LINHAS_DA_VISAO = [
["Finan&ccedil;as"],
["Recebido", function (c) { return c.financeiro ? [reais(c.financeiro.recebido)] : null; }],
["A receber", function (c) { return c.financeiro ? [reais(c.financeiro.aReceber)] : null; }],
["Inadimpl&ecirc;ncia", function (c) {
if (!c.financeiro) return null;
var p = pct(c.financeiro.percentualInadimplencia);
return [reais(c.financeiro.vencido), p ? p + " do previsto" : null];
}],
["Alunos"],
["Alunos ativos", function (c) { return [inteiro(c.alunosAtivos)]; }],
["Frequ&ecirc;ncia nos treinos", function (c) {
var p = pct(c.frequencia);
return p ? [p, inteiro(c.presencas) + " presen&ccedil;as"] : null;
}],
["Ades&otilde;es (novos atletas)", function (c) { return [inteiro(c.adesoes)]; }],
["Matr&iacute;culas novas", function (c) { var n = inteiro(c.matriculasNovas); return n === null ? null : [n]; }],
["Evas&atilde;o (matr&iacute;culas canceladas)", function (c) { var n = inteiro(c.evasoes); return n === null ? null : [n]; }],
["Atividade do m&ecirc;s"],
["Treinos", function (c) { return [inteiro(c.treinos)]; }],
["Jogos", function (c) { return [inteiro(c.jogos)]; }]
];
function celula(par, somado) {
var classe = somado ? "somado" : "";
if (!par || par[0] === null || par[0] === undefined) {
return '<td class="vazio ' + classe + '">&mdash;</td>';
}
return '<td class="' + classe + '">' + escapar(par[0])
+ (par[1] ? "<small>" + par[1] + "</small>" : "") + "</td>";
}
function blocoDaVisao(d) {
var escolas = (d.escolas || []).filter(function (e) { return e.temDados; });
if (!escolas.length) return null;
var colunas = escolas.slice();
if (d.total) colunas.push(Object.assign({ somado: true }, d.total));
var cabeca = "<thead><tr><th></th>" + colunas.map(function (c) {
if (c.somado) return '<th class="somado" scope="col">Somado<small>' + escolas.length + " escolinhas</small></th>";
return '<th scope="col">' + escapar(c.escolinha || c.tenant || "Escolinha")
+ (c.atualizadoEm ? "<small>atualizado em " + escapar(dataCurta(c.atualizadoEm)) + "</small>" : "")
+ "</th>";
}).join("") + "</tr></thead>";
var corpo = "<tbody>" + LINHAS_DA_VISAO.map(function (l) {
if (!l[1]) return '<tr class="grupo"><th colspan="' + (colunas.length + 1) + '">' + l[0] + "</th></tr>";
return '<tr><th scope="row">' + l[0] + "</th>" + colunas.map(function (c) {
return celula(l[1](c), c.somado);
}).join("") + "</tr>";
}).join("") + "</tbody>";
return "<table>" + cabeca + corpo + "</table>";
}
function carregarVisao() {
var caixa = document.getElementById("visao");
if (!caixa) return Promise.resolve();
return fetch(CONFIG.VISAO, { headers: { Authorization: "Bearer " + token } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
var tabela = d ? blocoDaVisao(d) : null;
if (!tabela) { caixa.classList.add("oculto"); return; }
var semDados = (d.escolas || []).filter(function (e) { return !e.temDados; }).length;
document.getElementById("visaoSub").innerHTML = "N&uacute;meros de " + nomeDoMes(d.competencia)
+ ". S&oacute; totais &mdash; nenhum dado de aluno sai da escolinha. Atualizados uma vez por dia.";
document.getElementById("visaoTabela").innerHTML = tabela
+ (semDados ? '<p class="ajuda nota">' + semDados + (semDados === 1 ? " escolinha ainda n&atilde;o enviou"
: " escolinhas ainda n&atilde;o enviaram") + " o resumo do m&ecirc;s.</p>" : "")
+ '<p class="ajuda nota">&mdash; = n&atilde;o medido (por exemplo, sem o m&oacute;dulo Financeiro no plano).</p>';
caixa.classList.remove("oculto");
})
.catch(function (e) { if (window.console) console.warn("visao:", e); });
}
/* ── O TERMO DE DADOS ────────────────────────────────────────────────────────
*
* A autorizacao para a Athletic Map ver o USO do sistema (ultimo acesso,
@@ -2631,7 +3092,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.85">
<link rel="stylesheet" href="/publico/painel.css?v=2.86">
</head>
<body>
@@ -2691,6 +3152,7 @@ data:
<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>
<button type="button" class="aba" data-aba="termosdeuso" role="tab">Termos de Uso</button>
<button type="button" class="aba" data-aba="catalogo" role="tab">Produtos e pre&ccedil;os</button>
</nav>
@@ -2821,6 +3283,30 @@ data:
</div>
</section>
<!-- TERMOS DE USO: quem aceitou qual versao (V024) -->
<section id="aba-termosdeuso" class="painel-aba oculto">
<div class="cartao">
<h2>Termos de Uso</h2>
<p class="ajuda">
Vers&atilde;o vigente: <b id="termosUsoVigente">&mdash;</b>. Uma linha por contrata&ccedil;&atilde;o,
com o aceite que vale (o da vers&atilde;o vigente, se houver). Quem est&aacute; numa vers&atilde;o
antiga v&ecirc; o pedido de aceite no /minha-conta, e s&oacute; troca de plano ou compra
m&oacute;dulo depois de aceitar.
</p>
<div class="tabela-rolagem">
<table class="tabela"><thead><tr><th>Escolinha</th><th>E-mail</th><th>Vers&atilde;o aceita</th><th>Quando</th><th>Por onde</th></tr></thead>
<tbody id="termosUsoContratos"><tr><td colspan="5" class="ajuda">Abra esta aba para carregar.</td></tr></tbody>
</table>
</div>
<h3>&Uacute;ltimos aceites</h3>
<div class="tabela-rolagem">
<table class="tabela"><thead><tr><th>Quando</th><th>Vers&atilde;o</th><th>Por onde</th><th>A&ccedil;&atilde;o</th><th>Quem</th></tr></thead>
<tbody id="termosUsoHistorico"></tbody>
</table>
</div>
</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">
@@ -2848,7 +3334,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.85"></script>
<script src="/publico/painel.js?v=2.86"></script>
</body>
</html>
@@ -2885,6 +3371,7 @@ data:
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var API_TERMO = "/api/plataforma/painel/termo-de-dados";
var API_CATALOGO = "/api/plataforma/painel/catalogo";
var API_TERMOS_DE_USO = "/api/plataforma/painel/termos-de-uso";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
@@ -3679,6 +4166,52 @@ data:
});
});
/* ── termos de uso: quem aceitou qual versao (V024) ─────────────────────── */
var ORIGEM_DO_ACEITE = { LANDING: "landing page", LINK_ADMIN: "link enviado", MINHA_CONTA: "minha conta" };
var ACAO_DO_ACEITE = {
CONTRATACAO: "contratação", TROCA_DE_PLANO: "troca de plano",
COMPRA_DE_MODULO: "compra de módulo", ATUALIZACAO_DOS_TERMOS: "versão nova"
};
function linhaDeContrato(c, vigente) {
var versao = c.versao
? '<span class="selo ' + (c.emDia ? "ok" : "alerta") + '">' + esc(c.versao) + "</span>"
: '<span class="selo alerta">sem aceite</span>';
return "<tr><td>" + txt(c.escolinha) + (c.tenant ? ' <span class="ajuda">' + esc(c.tenant) + "</span>" : "")
+ "</td><td>" + txt(c.email) + "</td><td>" + versao
+ (c.versao && !c.emDia ? ' <span class="ajuda">(vigente: ' + esc(vigente) + ")</span>" : "")
+ "</td><td>" + (c.aceitoEm ? esc(dataHora(c.aceitoEm)) : "—")
+ "</td><td>" + txt(ORIGEM_DO_ACEITE[c.origem] || c.origem) + "</td></tr>";
}
function linhaDoHistorico(a) {
return "<tr><td>" + esc(dataHora(a.aceitoEm)) + "</td><td>" + txt(a.versao)
+ "</td><td>" + txt(ORIGEM_DO_ACEITE[a.origem] || a.origem)
+ "</td><td>" + txt(ACAO_DO_ACEITE[a.acao] || a.acao)
+ "</td><td>" + txt(a.email) + (a.tenant ? ' <span class="ajuda">' + esc(a.tenant) + "</span>" : "")
+ "</td></tr>";
}
function carregarTermosDeUso() {
if (!tokenAtual) return Promise.resolve();
return fetch(API_TERMOS_DE_USO, { headers: { "x-atm-admin-token": tokenAtual } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.then(function (d) {
d = d || {};
$("termosUsoVigente").textContent = d.versaoVigente || "—";
$("termosUsoContratos").innerHTML = (d.contratos || []).map(function (c) {
return linhaDeContrato(c, d.versaoVigente);
}).join("") || '<tr><td colspan="5" class="ajuda">Nenhuma contratação ainda.</td></tr>';
$("termosUsoHistorico").innerHTML = (d.aceites || []).map(linhaDoHistorico).join("")
|| '<tr><td colspan="5" class="ajuda">Nenhum aceite registrado.</td></tr>';
})
.catch(function (e) {
$("termosUsoContratos").innerHTML = '<tr><td colspan="5" class="ajuda">Não consegui carregar: '
+ esc(e.message) + "</td></tr>";
});
}
/* ── montagem ───────────────────────────────────────────────────────────── */
function pintar() {
@@ -3778,6 +4311,7 @@ data:
p.classList.toggle("oculto", p.id !== "aba-" + b.getAttribute("data-aba"));
});
if (b.getAttribute("data-aba") === "termo") carregarTermo();
if (b.getAttribute("data-aba") === "termosdeuso") carregarTermosDeUso();
if (b.getAttribute("data-aba") === "catalogo") carregarCatalogo();
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+559 -25
View File
@@ -196,6 +196,180 @@ data:
};
})(typeof window !== 'undefined' ? window : this);
contratar.css: |
/* A pagina /contratar. Arquivo proprio, e nao <style> embutido: a CSP do
servidor nao libera estilo inline. */
body { display: flex; min-height: 100vh; align-items: center; justify-content: center;
margin: 0; padding: 16px; box-sizing: border-box;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif; }
main { max-width: 32rem; width: 100%; }
h1 { font-size: 1.5rem; margin: 0 0 .75rem; }
p { line-height: 1.55; margin: 0 0 .75rem; }
.resumo { display: grid; grid-template-columns: max-content 1fr; gap: .4rem 1rem; margin: 1rem 0 1.25rem; }
.resumo dt { opacity: .7; }
.resumo dd { margin: 0; font-weight: 600; }
.aceite { display: flex; gap: .6rem; align-items: flex-start; margin: 0 0 1rem; line-height: 1.5; }
.aceite input { margin-top: .3rem; width: 1.1rem; height: 1.1rem; flex: none; }
.erro { color: #d93025; font-weight: 600; }
#seguir { padding: .8rem 1.4rem; border: 0; border-radius: 8px; background: #28c76f; color: #06210f;
font-weight: 600; font-size: 1rem; cursor: pointer; }
#seguir:disabled { opacity: .6; cursor: default; }
.nota { margin-top: 1.25rem; font-size: .85rem; opacity: .65; }
contratar.html: |
<!doctype html>
<html lang="pt-BR">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<meta name="referrer" content="no-referrer">
<title>Confirmar contratação — Athletic Map</title>
<link rel="stylesheet" href="/publico/_estilo.css">
<link rel="stylesheet" href="/publico/contratar.css?v=1">
</head>
<body data-situacao="carregando">
<!-- O link de pagamento aberto por quem fechou a venda (endpoint administrativo)
chega AQUI, e nao direto no Asaas: quem paga ve o plano, o preco e aceita os
Termos de Uso antes do checkout. Ver AbrirAssinaturaDoLead (V024). -->
<main>
<h1 id="titulo">Confirmar contratação</h1>
<p id="mensagem" aria-live="polite">Carregando…</p>
<form id="formulario" hidden novalidate>
<dl class="resumo">
<dt>Escolinha</dt><dd id="escolinha">—</dd>
<dt>Plano</dt><dd id="plano">—</dd>
<dt>Valor</dt><dd id="valor">—</dd>
</dl>
<div class="aceite">
<input id="termos" type="checkbox">
<label for="termos">Li e aceito os <a href="/termos" target="_blank" rel="noopener">Termos de Uso</a>
(versão <span id="versao">—</span>) para contratar a Athletic Map.</label>
</div>
<p id="erro" class="erro" role="alert" hidden></p>
<button id="seguir" type="submit">Aceitar e ir para o pagamento</button>
<p class="nota">O pagamento é feito no Asaas, nosso parceiro de cobrança.</p>
</form>
</main>
<script src="/publico/contratar.js?v=1"></script>
</body>
</html>
contratar.js: |
/* ===========================================================================
* A pagina /contratar — o aceite dos Termos de Uso ANTES do checkout.
*
* Quem fecha a venda abre o link pelo endpoint administrativo, e o que o
* comprador recebe e ESTE endereco (`/contratar?c=TOKEN`), e nao o do Asaas. A
* pagina mostra o plano e o preco, pede a caixa dos Termos e so entao recebe do
* servidor o endereco do checkout — que nao existe em nenhum outro lugar fora do
* banco. Ver AbrirAssinaturaDoLead e V024.
*
* Script em arquivo, e nao inline: a CSP do servidor diz `script-src 'self'`.
* =========================================================================== */
(function () {
"use strict";
var API = "/api/public/contratacoes/confirmacao/";
var token = new URLSearchParams(location.search).get("c");
var titulo = document.getElementById("titulo");
var mensagem = document.getElementById("mensagem");
var formulario = document.getElementById("formulario");
var caixa = document.getElementById("termos");
var erro = document.getElementById("erro");
var botao = document.getElementById("seguir");
var versao = null;
/* A unica saida da pagina. Costura para o teste ver PARA ONDE ela manda — o
jsdom nao deixa trocar `location.assign`. */
function navegar(url) {
if (typeof window.__atmNavegar === "function") { window.__atmNavegar(url); return; }
location.assign(url);
}
function reais(v) {
var n = Number(v);
return isFinite(n) ? n.toLocaleString("pt-BR", { style: "currency", currency: "BRL" }) : "—";
}
function nomeDoPlano(p) {
var n = String(p || "");
return n ? n.charAt(0).toUpperCase() + n.slice(1) : "—";
}
function falhar(texto) {
document.body.setAttribute("data-situacao", "falhou");
titulo.textContent = "Não foi possível abrir esta contratação";
mensagem.textContent = texto;
formulario.hidden = true;
}
function mostrarErro(texto) {
erro.textContent = texto;
erro.hidden = false;
}
if (!token) {
falhar("O endereço está incompleto. Peça um link novo a quem o enviou.");
return;
}
fetch(API + encodeURIComponent(token), { headers: { "Accept": "application/json" } })
.then(function (r) {
if (r.status === 404) throw new Error("Este link de contratação não existe. Peça um novo a quem o enviou.");
if (!r.ok) throw new Error("Não conseguimos carregar agora. Tente de novo em instantes.");
return r.json();
})
.then(function (d) {
versao = d.versao;
// textContent em tudo: o nome da escolinha foi digitado por alguem.
document.getElementById("escolinha").textContent = d.escolinha || "—";
document.getElementById("plano").textContent = nomeDoPlano(d.plano)
+ (d.ciclo === "ANUAL" ? " (anual)" : " (mensal)");
document.getElementById("valor").textContent = reais(d.valor) + (d.ciclo === "ANUAL" ? "/ano" : "/mês");
document.getElementById("versao").textContent = d.versao || "—";
mensagem.textContent = "Confira o que você está contratando e aceite os Termos de Uso para seguir ao pagamento.";
document.body.setAttribute("data-situacao", "pronta");
formulario.hidden = false;
})
.catch(function (e) { falhar(e.message); });
formulario.addEventListener("submit", function (ev) {
ev.preventDefault();
erro.hidden = true;
if (!caixa.checked) {
mostrarErro("Para contratar, leia e aceite os Termos de Uso.");
return;
}
botao.disabled = true;
fetch(API + encodeURIComponent(token), {
method: "POST",
headers: { "Content-Type": "application/json", "Accept": "application/json" },
body: JSON.stringify({ termos_aceitos: true, termos_versao: versao })
}).then(function (r) {
return r.json().catch(function () { return {}; }).then(function (d) {
if (r.ok && typeof d.link === "string" && /^https:\/\//.test(d.link)) {
navegar(d.link);
return;
}
if (d.codigo === "termos_desatualizados") {
// O texto mudou enquanto a pagina estava aberta: mostra a versao nova
// e pede a caixa de novo — o aceite antigo nao vale para ela.
versao = d.versao;
document.getElementById("versao").textContent = d.versao || "—";
caixa.checked = false;
}
throw new Error(d.erro || d.mensagem || "Não conseguimos seguir agora. Tente de novo em instantes.");
});
}).catch(function (e) {
mostrarErro(e.message);
botao.disabled = false;
});
});
})();
fontes--fontes.css: |
/* ============================================================
* As fontes da landing page, servidas POR NOS.
@@ -471,6 +645,9 @@ data:
.consent{display:grid; grid-template-columns:18px 1fr; gap:10px; align-items:start;}
.consent input{width:18px; height:18px; margin-top:3px; accent-color:var(--laranja);}
.consent label{font-family:"Barlow",sans-serif; font-size:.81rem; font-weight:400; color:var(--muted); line-height:1.45; margin:0;}
/* O aceite dos Termos de Uso so existe na contratacao (plano escolhido). */
.consent.termos{display:none;}
.consent.termos.visivel{display:grid;}
.btn{display:inline-flex; align-items:center; justify-content:center; gap:9px; width:100%; border:none; border-radius:var(--r-pill); background:linear-gradient(100deg,var(--laranja) 0%,#FF9E42 100%); color:#fff; font-size:1.05rem; font-weight:600; padding:15px 22px; cursor:pointer; text-decoration:none; box-shadow:0 6px 16px -8px rgba(245,130,31,.9);}
.btn:hover{filter:brightness(1.05)}
.form-foot{font-size:.79rem; color:var(--muted); text-align:center; margin:13px 0 0;}
@@ -619,7 +796,7 @@ data:
<meta property="og:type" content="website">
<title>Athletic Map para Escolinhas</title>
<link rel="stylesheet" href="/publico/fontes/fontes.css">
<link rel="stylesheet" href="/publico/lp-escolinhas.css">
<link rel="stylesheet" href="/publico/lp-escolinhas.css?v=2.86">
</head>
<body>
@@ -695,6 +872,15 @@ data:
<input id="consent" name="consent" type="checkbox" required>
<label for="consent">Autorizo a Athletic Map a entrar em contato e a tratar meus dados para esta finalidade, conforme a <a href="/privacidade" target="_blank" rel="noopener">Política de Privacidade</a>.</label>
</div>
<!-- So aparece com um plano escolhido (lp-escolinhas.js, `escolher`). Sem
este aceite a contratacao e recusada no servidor (400). O
`data-versao` TEM de ser a versao do termos.html e a
`atm.captacao.termos-de-uso-versao` do servico-captacao: mudou o
texto dos Termos, suba as tres juntas. -->
<div class="consent termos" id="aceiteTermos">
<input id="termos" type="checkbox" data-versao="1.0">
<label for="termos">Li e aceito os <a href="/termos" target="_blank" rel="noopener">Termos de Uso</a> (versão 1.0) para contratar a Athletic Map.</label>
</div>
<button class="btn" type="submit">Quero ver funcionando <span aria-hidden="true">→</span></button>
</div>
</form>
@@ -833,7 +1019,7 @@ data:
<!-- O portao de consentimento. Tem de vir ANTES do script da pagina: e ele
que decide se Meta e Google carregam. Ver publico/consentimento.js. -->
<script src="/publico/consentimento.js"></script>
<script src="/publico/lp-escolinhas.js?v=2.85"></script>
<script src="/publico/lp-escolinhas.js?v=2.86"></script>
</body>
</html>
@@ -1006,6 +1192,19 @@ data:
});
}
/* O aceite dos Termos de Uso. So aparece — e so e exigido — com um plano
escolhido: e contrato, e quem so pede contato nao contrata nada. O servidor
recusa a contratacao sem ele (400), na versao que esta no `data-versao`. */
var termos = document.getElementById("termos");
var caixaTermos = document.getElementById("aceiteTermos");
function mostrarTermos(sim) {
if (caixaTermos) caixaTermos.classList.toggle("visivel", sim);
if (termos) {
termos.required = sim;
if (!sim) termos.checked = false;
}
}
function escolher(plano) {
escolha = { plano: plano, ciclo: cicloAtual };
var aviso = document.getElementById("planoEscolhido");
@@ -1017,6 +1216,8 @@ data:
}
var b = form.querySelector("button[type=submit]");
if (b) b.innerHTML = "Ir para o pagamento &rarr;";
// Contratar exige o aceite dos Termos de Uso; pedir contato, nao.
mostrarTermos(true);
track("InitiateCheckout", { content_name: plano, content_category: cicloAtual });
document.getElementById("form").scrollIntoView({ behavior: "smooth", block: "start" });
}
@@ -1027,6 +1228,7 @@ data:
if (aviso) aviso.classList.remove("visivel");
var b = form.querySelector("button[type=submit]");
if (b) b.innerHTML = "Quero ver funcionando &rarr;";
mostrarTermos(false);
}
Array.prototype.forEach.call(document.querySelectorAll(".ciclo"), function (b) {
@@ -1114,6 +1316,17 @@ data:
if (!form.reportValidity()) return;
var data = Object.fromEntries(new FormData(form).entries());
data.consent = !!document.getElementById("consent").checked;
// A decisao do banner de medicao viaja com o lead. E ela que o servidor
// consulta antes de mandar o `Purchase` ao Meta e ao Google: quem recusou
// aqui nao e medido la. Sem decisao vai null — e null, la, e nao.
var decisao = AtmConsentimento.estado();
data.consentimento_medicao =
(decisao === AtmConsentimento.ACEITO || decisao === AtmConsentimento.RECUSADO) ? decisao : null;
if (escolha && !(termos && termos.checked)) {
alert("Para contratar, leia e aceite os Termos de Uso.");
if (termos && termos.focus) termos.focus();
return;
}
Object.assign(data, tracking);
data.pagina = location.pathname;
data.enviado_em = new Date().toISOString();
@@ -1181,7 +1394,9 @@ data:
if (escolha) {
fetch(CONFIG.CONTRATACAO_ENDPOINT, {
method: "POST", headers: {"Content-Type": "application/json"},
body: JSON.stringify({lead: data, plano: escolha.plano, ciclo: escolha.ciclo})
body: JSON.stringify({lead: data, plano: escolha.plano, ciclo: escolha.ciclo,
termos_aceitos: true,
termos_versao: termos.getAttribute("data-versao")})
}).then(function(r){
return r.json().catch(function(){ return null; })
.then(function(corpo){ r.ok ? contratado(corpo) : recusado(corpo); });
@@ -1460,6 +1675,29 @@ data:
.lista-planos .selo { margin: 0; }
.plano-troca .link { background: none; border: 0; color: var(--orange-600); font-weight: 600; cursor: pointer; padding: 0; font: inherit; }
/* ── N16: a visao geral das escolinhas ──────────────────────────────────────
Uma tabela: metricas nas linhas, escolinhas nas colunas, o somado por
ultimo. Rola de lado DENTRO do cartao no celular — a pagina nao. */
.visao .hl { color: var(--orange); }
.visao-tabela { margin-top: 14px; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.visao-tabela table { border-collapse: collapse; width: 100%; font-size: 14px; min-width: 420px; }
.visao-tabela th, .visao-tabela td { padding: 9px 12px; text-align: right; border-top: 1px solid var(--line); white-space: nowrap; }
.visao-tabela thead th { border-top: 0; vertical-align: bottom; font-weight: 700; color: var(--navy); }
.visao-tabela thead th small { display: block; font-weight: 400; color: var(--muted); font-size: 12px; }
.visao-tabela th[scope="row"] { text-align: left; font-weight: 500; color: var(--muted); }
.visao-tabela .grupo th { text-align: left; padding-top: 16px; font-family: 'IBM Plex Mono', monospace;
font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--orange-600); border-top: 0; }
.visao-tabela .somado { background: #FFF6EC; font-weight: 700; }
.visao-tabela td.vazio { color: var(--muted); }
.visao-tabela td small { display: block; color: var(--muted); font-size: 12px; font-weight: 400; }
.visao .nota { font-size: 13px; margin-top: 10px; }
/* Os Termos de Uso (V024): o aviso de versao nova e a caixa dentro de uma acao. */
.aviso-termos { border-left: 4px solid var(--orange-600); }
.termos-uso-caixa { display: block; margin: 8px 0; line-height: 1.5; }
.termos-uso-caixa input { margin-right: 6px; }
.termos-uso-aceite { margin: 10px 0; }
minha-conta.html: |
<!DOCTYPE html>
<html lang="pt-BR">
@@ -1484,7 +1722,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.85">
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.88">
</head>
<body>
@@ -1527,6 +1765,15 @@ data:
</section>
<section id="conteudo" class="oculto">
<!-- N16: a visao geral das escolinhas DESTE comprador, lado a lado e somada.
So numeros agregados, que cada escolinha envia uma vez por dia.
Fica oculta enquanto nenhuma escolinha tiver mandado o resumo. -->
<section id="visao" class="cartao visao oculto" aria-labelledby="visaoTitulo">
<h2 id="visaoTitulo">Vis&atilde;o <span class="hl">geral</span></h2>
<p id="visaoSub" class="ajuda"></p>
<div id="visaoTabela" class="visao-tabela"></div>
</section>
<div id="produtos" class="produtos"></div>
<div id="nenhum" class="cartao oculto">
@@ -1554,7 +1801,7 @@ data:
<footer class="direitos">&copy; 2026 Athletic Map. Todos os direitos reservados.</footer>
<script src="/publico/minha-conta.js?v=2.86"></script>
<script src="/publico/minha-conta.js?v=2.88"></script>
</body>
</html>
@@ -1596,7 +1843,9 @@ data:
TERMO: "/api/cliente/termo-de-dados",
FATURAS: "/api/cliente/faturas",
PLANO: "/api/cliente/plano",
MODULOS: "/api/cliente/modulos"
MODULOS: "/api/cliente/modulos",
TERMOS_DE_USO: "/api/cliente/termos-de-uso",
VISAO: "/api/cliente/visao-geral"
};
var CHAVE_VERIFIER = "atm.conta.verifier";
@@ -1769,7 +2018,8 @@ data:
nenhum.classList.toggle("oculto", (lista || []).length > 0);
mostrar(conteudo);
(lista || []).forEach(function (p) { if (p.tenant && p.situacao === "ATIVA") acrescentarBotaoDePlano(p.tenant); });
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas()]);
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas(), carregarVisao(),
carregarTermosDeUso()]);
});
}
@@ -1948,12 +2198,7 @@ data:
if (corpo) h["Content-Type"] = "application/json";
return fetch(CONFIG.PLANO + "/" + encodeURIComponent(tenant), {
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 || "Não consegui agora. Tente de novo em instantes.");
});
});
}).then(respostaDaAcao);
}
function blocoDePlano(d) {
@@ -2000,7 +2245,13 @@ data:
var plano = botao.getAttribute("data-plano");
if (typeof window.confirm === "function" && !window.confirm("Mudar para " + (NOME_DO_PLANO[plano] || plano)
+ "? " + botao.getAttribute("data-quando") + ".")) { botao.disabled = false; return Promise.resolve(); }
return pedirPlano("POST", tenant, { plano: plano }).then(function (d) { mostrarPlano(tenant, d); }).catch(erro);
var trocar = function (aceite) {
var corpo = { plano: plano };
if (aceite) { corpo.termos_aceitos = aceite.termos_aceitos; corpo.termos_versao = aceite.termos_versao; }
return pedirPlano("POST", tenant, corpo).then(function (d) { mostrarPlano(tenant, d); })
.catch(function (e) { return e.termos ? pedirAceite(caixa, e.termos, "mudar de plano", trocar, botao) : erro(e); });
};
return trocar(null);
}
if (acao === "cancelar") {
return pedirPlano("DELETE", tenant).then(function (d) { mostrarPlano(tenant, d); }).catch(erro);
@@ -2013,21 +2264,124 @@ data:
if (b) agirNoPlano(b);
});
/* ── OS TERMOS DE USO (V024) ─────────────────────────────────────────────────
*
* Trocar de plano e comprar modulo MUDAM o contrato: se ele ainda nao aceitou a
* versao vigente dos Termos de Uso, o servidor responde 428 com a versao, e a
* caixa aparece ali mesmo — marcada, a acao e repetida levando o aceite. Se ja
* aceitou, nada e perguntado.
*
* Versao nova dos Termos: um aviso no topo pede o aceite, SEM bloquear nada —
* faturas, dados e o proprio sistema continuam. So as acoes que mudam o
* contrato pedem. Cancelar (troca agendada ou modulo) nunca pede. */
function respostaDaAcao(r) {
if (r.ok) return r.json();
return r.json().catch(function () { return {}; }).then(function (d) {
var e = new Error(d.erro || "Não consegui agora. Tente de novo em instantes.");
if (r.status === 428 && d.versao) e.termos = d;
throw e;
});
}
function caixaDeAceite(versao, rotulo) {
return '<label class="termos-uso-caixa"><input type="checkbox"> Li e aceito os '
+ '<a href="/termos" target="_blank" rel="noopener">Termos de Uso</a> (vers&atilde;o '
+ escapar(versao) + ")</label> "
+ '<button type="button" class="btn">' + rotulo + "</button>"
+ '<p class="erro-form oculto"></p>';
}
/* A caixa dentro do cartao da acao. `repetir` refaz o pedido com o aceite. */
function pedirAceite(caixa, t, oQue, repetir, botao) {
caixa.querySelectorAll(".termos-uso-aceite").forEach(function (el) { el.remove(); });
var div = document.createElement("div");
div.className = "termos-uso-aceite";
div.innerHTML = '<p class="ajuda">Para ' + escapar(oQue) + ", aceite os Termos de Uso vigentes.</p>"
+ caixaDeAceite(t.versao, "Aceitar e continuar");
caixa.insertBefore(div, caixa.querySelector(".erro-form"));
div.querySelector("button").addEventListener("click", function () {
var aviso = div.querySelector(".erro-form");
if (!div.querySelector("input").checked) {
aviso.textContent = "Marque a caixa para aceitar os Termos de Uso.";
aviso.classList.remove("oculto");
return;
}
div.querySelector("button").disabled = true;
repetir({ termos_aceitos: true, termos_versao: t.versao }).then(function () {
if (div.parentNode) div.parentNode.removeChild(div);
carregarTermosDeUso();
});
});
if (botao) botao.disabled = false;
return Promise.resolve();
}
function carregarTermosDeUso() {
return fetch(CONFIG.TERMOS_DE_USO, { headers: { Authorization: "Bearer " + token } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
var velho = document.getElementById("avisoTermos");
if (velho) velho.parentNode.removeChild(velho);
if (!d || !d.versao || !d.escolinhas) return;
var faltam = d.escolinhas.filter(function (e) { return !e.aceito; });
if (!faltam.length) return;
var aviso = document.createElement("section");
aviso.id = "avisoTermos";
aviso.className = "cartao aviso-termos";
aviso.innerHTML = "<h2>Termos de Uso</h2>"
+ '<p class="ajuda">A vers&atilde;o ' + escapar(d.versao) + " dos Termos de Uso ainda n&atilde;o foi aceita para "
+ faltam.map(function (e) { return "<b>" + escapar(e.escolinha || e.tenant) + "</b>"; }).join(", ")
+ ". Voc&ecirc; continua usando o sistema, vendo faturas e dados normalmente; s&oacute; trocar de plano"
+ " e comprar m&oacute;dulos pedem o aceite.</p>"
+ caixaDeAceite(d.versao, "Aceitar");
produtos.parentNode.insertBefore(aviso, produtos);
aviso.querySelector("button").addEventListener("click", function () {
aceitarTermosDeUso(aviso, d.versao);
});
})
.catch(function (e) { if (window.console) console.warn("termos de uso:", e); });
}
function aceitarTermosDeUso(aviso, versao) {
var erro = aviso.querySelector(".erro-form");
var botao = aviso.querySelector("button");
if (!aviso.querySelector("input").checked) {
erro.textContent = "Marque a caixa para aceitar os Termos de Uso.";
erro.classList.remove("oculto");
return Promise.resolve();
}
botao.disabled = true;
return fetch(CONFIG.TERMOS_DE_USO, {
method: "POST",
headers: { Authorization: "Bearer " + token, "Content-Type": "application/json" },
body: JSON.stringify({ termos_aceitos: true, termos_versao: versao })
}).then(function (r) {
if (r.ok) return carregarTermosDeUso();
return r.json().catch(function () { return {}; }).then(function (d) {
// Versao trocou com a pagina aberta: o aviso volta com a nova.
if (d.codigo === "termos_desatualizados") return carregarTermosDeUso();
throw new Error(d.erro || "Não consegui registrar agora. Tente de novo em instantes.");
});
}).catch(function (e) {
erro.textContent = e.message;
erro.classList.remove("oculto");
botao.disabled = false;
});
}
/* ── A LOJA DE MODULOS ───────────────────────────────────────────────────────
*
* Mesmas regras da troca de plano: comprar vale na hora e o valor entra na
* proxima cobranca; cancelar vale no fim do periodo pago. O que o plano ja
* inclui aparece como incluido, sem botao. */
function pedirModulo(metodo, tenant, modulo) {
function pedirModulo(metodo, tenant, modulo, corpo) {
var h = { Authorization: "Bearer " + token };
if (corpo) h["Content-Type"] = "application/json";
return fetch(CONFIG.MODULOS + "/" + encodeURIComponent(tenant) + (modulo ? "/" + encodeURIComponent(modulo) : ""), {
method: metodo, headers: { Authorization: "Bearer " + token }
}).then(function (r) {
if (r.ok) return r.json();
return r.json().catch(function () { return {}; }).then(function (d) {
throw new Error(d.erro || "Não consegui agora. Tente de novo em instantes.");
});
});
method: metodo, headers: h, body: corpo ? JSON.stringify(corpo) : undefined
}).then(respostaDaAcao);
}
function blocoDaLoja(d) {
@@ -2070,7 +2424,11 @@ data:
if (acao === "comprar") {
if (typeof window.confirm === "function" && !window.confirm("Comprar " + botao.getAttribute("data-nome")
+ "? Libera na hora; o valor entra na próxima cobrança.")) { botao.disabled = false; return Promise.resolve(); }
return pedirModulo("POST", tenant, modulo).then(mostrar).catch(erro);
var comprar = function (aceite) {
return pedirModulo("POST", tenant, modulo, aceite).then(mostrar)
.catch(function (e) { return e.termos ? pedirAceite(caixa, e.termos, "comprar o módulo", comprar, botao) : erro(e); });
};
return comprar(null);
}
if (acao === "cancelar") {
if (typeof window.confirm === "function" && !window.confirm("Cancelar este módulo? Ele continua até o fim do período pago."))
@@ -2140,6 +2498,109 @@ data:
.catch(function (e) { if (window.console) console.warn("faturas:", e); });
}
/* ── A VISAO GERAL (N16) ─────────────────────────────────────────────────────
*
* As escolinhas DESTE comprador lado a lado, e o somado quando ha mais de uma.
* Os numeros sao AGREGADOS que cada escolinha envia uma vez por dia — nenhum
* dado de aluno sai de la. Sem nenhuma escolinha com resumo, o bloco nem
* aparece. Como os outros complementos, falhar aqui nao derruba a tela. */
var MESES = ["janeiro", "fevereiro", "mar&ccedil;o", "abril", "maio", "junho", "julho",
"agosto", "setembro", "outubro", "novembro", "dezembro"];
function nomeDoMes(competencia) {
var m = /^(\d{4})-(\d{2})$/.exec(String(competencia || ""));
return m ? MESES[Number(m[2]) - 1] + " de " + m[1] : "";
}
function inteiro(v) {
var n = Number(v);
return v === null || v === undefined || isNaN(n) ? null : String(Math.round(n));
}
function pct(v) {
var n = Number(v);
return v === null || v === undefined || isNaN(n) ? null : n.toFixed(1).replace(".", ",") + "%";
}
/* Cada linha: rotulo, e como tirar o texto (e a observacao, se houver) de uma
coluna. `null` vira "—": o numero nao existe para aquela escolinha (sem o
modulo Financeiro, por exemplo), o que e diferente de zero. */
var LINHAS_DA_VISAO = [
["Finan&ccedil;as"],
["Recebido", function (c) { return c.financeiro ? [reais(c.financeiro.recebido)] : null; }],
["A receber", function (c) { return c.financeiro ? [reais(c.financeiro.aReceber)] : null; }],
["Inadimpl&ecirc;ncia", function (c) {
if (!c.financeiro) return null;
var p = pct(c.financeiro.percentualInadimplencia);
return [reais(c.financeiro.vencido), p ? p + " do previsto" : null];
}],
["Alunos"],
["Alunos ativos", function (c) { return [inteiro(c.alunosAtivos)]; }],
["Frequ&ecirc;ncia nos treinos", function (c) {
var p = pct(c.frequencia);
return p ? [p, inteiro(c.presencas) + " presen&ccedil;as"] : null;
}],
["Ades&otilde;es (novos atletas)", function (c) { return [inteiro(c.adesoes)]; }],
["Matr&iacute;culas novas", function (c) { var n = inteiro(c.matriculasNovas); return n === null ? null : [n]; }],
["Evas&atilde;o (matr&iacute;culas canceladas)", function (c) { var n = inteiro(c.evasoes); return n === null ? null : [n]; }],
["Atividade do m&ecirc;s"],
["Treinos", function (c) { return [inteiro(c.treinos)]; }],
["Jogos", function (c) { return [inteiro(c.jogos)]; }]
];
function celula(par, somado) {
var classe = somado ? "somado" : "";
if (!par || par[0] === null || par[0] === undefined) {
return '<td class="vazio ' + classe + '">&mdash;</td>';
}
return '<td class="' + classe + '">' + escapar(par[0])
+ (par[1] ? "<small>" + par[1] + "</small>" : "") + "</td>";
}
function blocoDaVisao(d) {
var escolas = (d.escolas || []).filter(function (e) { return e.temDados; });
if (!escolas.length) return null;
var colunas = escolas.slice();
if (d.total) colunas.push(Object.assign({ somado: true }, d.total));
var cabeca = "<thead><tr><th></th>" + colunas.map(function (c) {
if (c.somado) return '<th class="somado" scope="col">Somado<small>' + escolas.length + " escolinhas</small></th>";
return '<th scope="col">' + escapar(c.escolinha || c.tenant || "Escolinha")
+ (c.atualizadoEm ? "<small>atualizado em " + escapar(dataCurta(c.atualizadoEm)) + "</small>" : "")
+ "</th>";
}).join("") + "</tr></thead>";
var corpo = "<tbody>" + LINHAS_DA_VISAO.map(function (l) {
if (!l[1]) return '<tr class="grupo"><th colspan="' + (colunas.length + 1) + '">' + l[0] + "</th></tr>";
return '<tr><th scope="row">' + l[0] + "</th>" + colunas.map(function (c) {
return celula(l[1](c), c.somado);
}).join("") + "</tr>";
}).join("") + "</tbody>";
return "<table>" + cabeca + corpo + "</table>";
}
function carregarVisao() {
var caixa = document.getElementById("visao");
if (!caixa) return Promise.resolve();
return fetch(CONFIG.VISAO, { headers: { Authorization: "Bearer " + token } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (d) {
var tabela = d ? blocoDaVisao(d) : null;
if (!tabela) { caixa.classList.add("oculto"); return; }
var semDados = (d.escolas || []).filter(function (e) { return !e.temDados; }).length;
document.getElementById("visaoSub").innerHTML = "N&uacute;meros de " + nomeDoMes(d.competencia)
+ ". S&oacute; totais &mdash; nenhum dado de aluno sai da escolinha. Atualizados uma vez por dia.";
document.getElementById("visaoTabela").innerHTML = tabela
+ (semDados ? '<p class="ajuda nota">' + semDados + (semDados === 1 ? " escolinha ainda n&atilde;o enviou"
: " escolinhas ainda n&atilde;o enviaram") + " o resumo do m&ecirc;s.</p>" : "")
+ '<p class="ajuda nota">&mdash; = n&atilde;o medido (por exemplo, sem o m&oacute;dulo Financeiro no plano).</p>';
caixa.classList.remove("oculto");
})
.catch(function (e) { if (window.console) console.warn("visao:", e); });
}
/* ── O TERMO DE DADOS ────────────────────────────────────────────────────────
*
* A autorizacao para a Athletic Map ver o USO do sistema (ultimo acesso,
@@ -2631,7 +3092,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.85">
<link rel="stylesheet" href="/publico/painel.css?v=2.86">
</head>
<body>
@@ -2691,6 +3152,7 @@ data:
<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>
<button type="button" class="aba" data-aba="termosdeuso" role="tab">Termos de Uso</button>
<button type="button" class="aba" data-aba="catalogo" role="tab">Produtos e pre&ccedil;os</button>
</nav>
@@ -2821,6 +3283,30 @@ data:
</div>
</section>
<!-- TERMOS DE USO: quem aceitou qual versao (V024) -->
<section id="aba-termosdeuso" class="painel-aba oculto">
<div class="cartao">
<h2>Termos de Uso</h2>
<p class="ajuda">
Vers&atilde;o vigente: <b id="termosUsoVigente">&mdash;</b>. Uma linha por contrata&ccedil;&atilde;o,
com o aceite que vale (o da vers&atilde;o vigente, se houver). Quem est&aacute; numa vers&atilde;o
antiga v&ecirc; o pedido de aceite no /minha-conta, e s&oacute; troca de plano ou compra
m&oacute;dulo depois de aceitar.
</p>
<div class="tabela-rolagem">
<table class="tabela"><thead><tr><th>Escolinha</th><th>E-mail</th><th>Vers&atilde;o aceita</th><th>Quando</th><th>Por onde</th></tr></thead>
<tbody id="termosUsoContratos"><tr><td colspan="5" class="ajuda">Abra esta aba para carregar.</td></tr></tbody>
</table>
</div>
<h3>&Uacute;ltimos aceites</h3>
<div class="tabela-rolagem">
<table class="tabela"><thead><tr><th>Quando</th><th>Vers&atilde;o</th><th>Por onde</th><th>A&ccedil;&atilde;o</th><th>Quem</th></tr></thead>
<tbody id="termosUsoHistorico"></tbody>
</table>
</div>
</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">
@@ -2848,7 +3334,7 @@ data:
</main>
<script src="/publico/painel.js?v=2.85"></script>
<script src="/publico/painel.js?v=2.86"></script>
</body>
</html>
@@ -2885,6 +3371,7 @@ data:
var API_ALERTAS = "/api/plataforma/painel/alertas-infra";
var API_TERMO = "/api/plataforma/painel/termo-de-dados";
var API_CATALOGO = "/api/plataforma/painel/catalogo";
var API_TERMOS_DE_USO = "/api/plataforma/painel/termos-de-uso";
var CHAVE = "atm.painel.token";
var INTERVALO_SINO_MS = 60000;
@@ -3679,6 +4166,52 @@ data:
});
});
/* ── termos de uso: quem aceitou qual versao (V024) ─────────────────────── */
var ORIGEM_DO_ACEITE = { LANDING: "landing page", LINK_ADMIN: "link enviado", MINHA_CONTA: "minha conta" };
var ACAO_DO_ACEITE = {
CONTRATACAO: "contratação", TROCA_DE_PLANO: "troca de plano",
COMPRA_DE_MODULO: "compra de módulo", ATUALIZACAO_DOS_TERMOS: "versão nova"
};
function linhaDeContrato(c, vigente) {
var versao = c.versao
? '<span class="selo ' + (c.emDia ? "ok" : "alerta") + '">' + esc(c.versao) + "</span>"
: '<span class="selo alerta">sem aceite</span>';
return "<tr><td>" + txt(c.escolinha) + (c.tenant ? ' <span class="ajuda">' + esc(c.tenant) + "</span>" : "")
+ "</td><td>" + txt(c.email) + "</td><td>" + versao
+ (c.versao && !c.emDia ? ' <span class="ajuda">(vigente: ' + esc(vigente) + ")</span>" : "")
+ "</td><td>" + (c.aceitoEm ? esc(dataHora(c.aceitoEm)) : "—")
+ "</td><td>" + txt(ORIGEM_DO_ACEITE[c.origem] || c.origem) + "</td></tr>";
}
function linhaDoHistorico(a) {
return "<tr><td>" + esc(dataHora(a.aceitoEm)) + "</td><td>" + txt(a.versao)
+ "</td><td>" + txt(ORIGEM_DO_ACEITE[a.origem] || a.origem)
+ "</td><td>" + txt(ACAO_DO_ACEITE[a.acao] || a.acao)
+ "</td><td>" + txt(a.email) + (a.tenant ? ' <span class="ajuda">' + esc(a.tenant) + "</span>" : "")
+ "</td></tr>";
}
function carregarTermosDeUso() {
if (!tokenAtual) return Promise.resolve();
return fetch(API_TERMOS_DE_USO, { headers: { "x-atm-admin-token": tokenAtual } })
.then(function (r) { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })
.then(function (d) {
d = d || {};
$("termosUsoVigente").textContent = d.versaoVigente || "—";
$("termosUsoContratos").innerHTML = (d.contratos || []).map(function (c) {
return linhaDeContrato(c, d.versaoVigente);
}).join("") || '<tr><td colspan="5" class="ajuda">Nenhuma contratação ainda.</td></tr>';
$("termosUsoHistorico").innerHTML = (d.aceites || []).map(linhaDoHistorico).join("")
|| '<tr><td colspan="5" class="ajuda">Nenhum aceite registrado.</td></tr>';
})
.catch(function (e) {
$("termosUsoContratos").innerHTML = '<tr><td colspan="5" class="ajuda">Não consegui carregar: '
+ esc(e.message) + "</td></tr>";
});
}
/* ── montagem ───────────────────────────────────────────────────────────── */
function pintar() {
@@ -3778,6 +4311,7 @@ data:
p.classList.toggle("oculto", p.id !== "aba-" + b.getAttribute("data-aba"));
});
if (b.getAttribute("data-aba") === "termo") carregarTermo();
if (b.getAttribute("data-aba") === "termosdeuso") carregarTermosDeUso();
if (b.getAttribute("data-aba") === "catalogo") carregarCatalogo();
});
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff