chore(tenants): conteudo de publico/ atualizado
This commit is contained in:
@@ -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 →";
|
||||
// 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 →";
|
||||
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ã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">© 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ã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ão ' + escapar(d.versao) + " dos Termos de Uso ainda não foi aceita para "
|
||||
+ faltam.map(function (e) { return "<b>" + escapar(e.escolinha || e.tenant) + "</b>"; }).join(", ")
|
||||
+ ". Você continua usando o sistema, vendo faturas e dados normalmente; só trocar de plano"
|
||||
+ " e comprar mó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ç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ç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ê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ência nos treinos", function (c) {
|
||||
var p = pct(c.frequencia);
|
||||
return p ? [p, inteiro(c.presencas) + " presenças"] : null;
|
||||
}],
|
||||
["Adesões (novos atletas)", function (c) { return [inteiro(c.adesoes)]; }],
|
||||
["Matrículas novas", function (c) { var n = inteiro(c.matriculasNovas); return n === null ? null : [n]; }],
|
||||
["Evasão (matrículas canceladas)", function (c) { var n = inteiro(c.evasoes); return n === null ? null : [n]; }],
|
||||
["Atividade do mê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 + '">—</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úmeros de " + nomeDoMes(d.competencia)
|
||||
+ ". Só totais — 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ão enviou"
|
||||
: " escolinhas ainda não enviaram") + " o resumo do mês.</p>" : "")
|
||||
+ '<p class="ajuda nota">— = não medido (por exemplo, sem o mó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ç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ão vigente: <b id="termosUsoVigente">—</b>. Uma linha por contratação,
|
||||
com o aceite que vale (o da versão vigente, se houver). Quem está numa versão
|
||||
antiga vê o pedido de aceite no /minha-conta, e só troca de plano ou compra
|
||||
módulo depois de aceitar.
|
||||
</p>
|
||||
<div class="tabela-rolagem">
|
||||
<table class="tabela"><thead><tr><th>Escolinha</th><th>E-mail</th><th>Versã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>Últimos aceites</h3>
|
||||
<div class="tabela-rolagem">
|
||||
<table class="tabela"><thead><tr><th>Quando</th><th>Versão</th><th>Por onde</th><th>Açã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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user