chore(tenants): conteudo de publico/ atualizado
This commit is contained in:
@@ -3756,7 +3756,7 @@ data:
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/publico/painel.js?v=2.88"></script>
|
||||
<script src="/publico/painel.js?v=2.89"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4238,7 +4238,8 @@ data:
|
||||
+ (whatsapp(c.whatsapp) ? '<a class="btn claro" href="' + esc(whatsapp(c.whatsapp)) + '" target="_blank" rel="noopener noreferrer">WhatsApp</a>' : "")
|
||||
+ (c.email ? '<a class="btn claro" href="mailto:' + esc(c.email) + '">E-mail</a>' : "")
|
||||
+ (f.faturaVencida ? '<a class="btn claro" href="' + esc(linkSeguro(f.faturaVencida) || "#") + '" target="_blank" rel="noopener noreferrer">Fatura vencida</a>' : "")
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '">Reenviar licença</button>' : "")
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="reenviar">Reenviar licença</button>'
|
||||
+ '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="trocar">Trocar chave</button>' : "")
|
||||
+ '</div><p id="licencaAviso" class="ajuda" aria-live="polite"></p></header>'
|
||||
+ (alertas ? '<ul class="lista-alertas">' + alertas + "</ul>" : "")
|
||||
|
||||
@@ -4760,20 +4761,31 @@ data:
|
||||
corpo.addEventListener("keydown", function (ev) { if (ev.key === "Enter") aoEscolher(ev); });
|
||||
$("alertas").addEventListener("click", aoEscolher);
|
||||
|
||||
/* ── licenca do app do treinador ──────────────────────────────────────────
|
||||
* Reenviar = gerar uma chave NOVA e mandar ao dono da compra. A anterior
|
||||
* deixa de valer para ATIVAR (quem ja ativou o app continua entrando: o app
|
||||
* guarda a escolinha, nao a chave). Por isso pede confirmacao. */
|
||||
/* ── licenca dos apps (dono, treinador, pais) ─────────────────────────────
|
||||
* Reenviar = mandar a MESMA chave da escola ao dono da compra (captacao 4.32+;
|
||||
* so a chave antiga, guardada so com hash, e trocada uma vez). Nada muda para
|
||||
* ninguem: sem confirmacao.
|
||||
* Trocar = revogar a chave e gerar outra. Desde os apps 1.0.4 (captacao 4.33)
|
||||
* o app pergunta a situacao da chave ao abrir: trocar DERRUBA os apps de todos
|
||||
* da escola (dono, treinadores, pais) em ate 15 min de uso, e cada um precisa
|
||||
* da chave nova. Por isso pede o nome do tenant digitado. */
|
||||
var API_LICENCA = "/api/plataforma/painel/licencas/";
|
||||
|
||||
function reenviarLicenca(tenant, aviso) {
|
||||
function acaoDaLicenca(tenant, acao, aviso) {
|
||||
if (!tokenAtual) return Promise.resolve();
|
||||
if (!window.confirm("Gerar uma chave NOVA para " + tenant + " e mandar por e-mail ao dono da compra?\n\n"
|
||||
+ "A chave atual deixa de valer para ativar o app (quem já ativou continua entrando).")) {
|
||||
return Promise.resolve();
|
||||
if (acao === "trocar") {
|
||||
var digitado = window.prompt("TROCAR a chave de " + tenant + "?\n\n"
|
||||
+ "A chave atual para de valer e os apps de TODOS da escola (dono, treinadores e pais) saem da conta "
|
||||
+ "em até 15 minutos de uso; cada um vai precisar da chave nova, que vai por e-mail ao dono da compra.\n"
|
||||
+ "Use só se a chave vazou.\n\nPara confirmar, digite o nome do tenant (" + tenant + "):");
|
||||
if (digitado === null) return Promise.resolve();
|
||||
if (digitado.trim() !== tenant) {
|
||||
aviso.textContent = "Nada foi trocado: o nome digitado não confere.";
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
aviso.textContent = "Enviando…";
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/reenviar", {
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/" + acao, {
|
||||
method: "POST", headers: { "x-atm-admin-token": tokenAtual, "Content-Type": "application/json" }, body: "{}"
|
||||
}).then(function (r) {
|
||||
if (r.status === 401) throw new Error("O token expirou. Saia e entre de novo no painel.");
|
||||
@@ -4782,7 +4794,8 @@ data:
|
||||
return d;
|
||||
});
|
||||
}).then(function (d) {
|
||||
aviso.textContent = (d.emailEnviado ? "Chave nova enviada para " : "Chave nova criada; o e-mail vai em instantes para ")
|
||||
var oQue = acao === "trocar" ? "Chave NOVA" : "Chave da escola";
|
||||
aviso.textContent = oQue + (d.emailEnviado ? " enviada para " : " pronta; o e-mail vai em instantes para ")
|
||||
+ (d.destino || "o dono da compra") + " (termina em " + d.finalDaChave + ").";
|
||||
}).catch(function (e) {
|
||||
aviso.textContent = e && e.message && e.message !== "Failed to fetch" ? e.message : "Sem conexão com o servidor.";
|
||||
@@ -4793,7 +4806,8 @@ data:
|
||||
var b = ev.target.closest ? ev.target.closest("[data-licenca]") : null;
|
||||
if (!b) return;
|
||||
b.disabled = true;
|
||||
reenviarLicenca(b.getAttribute("data-licenca"), $("licencaAviso")).then(function () { b.disabled = false; });
|
||||
acaoDaLicenca(b.getAttribute("data-licenca"), b.getAttribute("data-acao") || "reenviar", $("licencaAviso"))
|
||||
.then(function () { b.disabled = false; });
|
||||
});
|
||||
|
||||
$("fecharFicha").addEventListener("click", fecharFicha);
|
||||
|
||||
@@ -370,6 +370,332 @@ data:
|
||||
});
|
||||
})();
|
||||
|
||||
convite.css: |
|
||||
/* Convite para competição: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). */
|
||||
main { max-width: 38rem; }
|
||||
.explica { color: var(--muted); }
|
||||
form { display: grid; gap: 1rem; }
|
||||
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
|
||||
legend { font-weight: 700; padding: 0 .4rem; }
|
||||
label { display: grid; gap: .2rem; font-size: .92rem; font-weight: 600; }
|
||||
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
|
||||
input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
|
||||
.linha-jogador { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
|
||||
.linha-jogador button { padding: .4rem .6rem; }
|
||||
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
|
||||
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
|
||||
button:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.erro { color: #b42318; margin: 0; font-weight: 600; }
|
||||
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: 1rem 1.2rem; }
|
||||
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
|
||||
|
||||
convite.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, nofollow">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Convite para competição — Athletic Map</title>
|
||||
<link rel="stylesheet" href="/publico/_estilo.css">
|
||||
<link rel="stylesheet" href="/publico/convite.css?v=1">
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="marca">Athletic <span>Map</span></div></header>
|
||||
<main>
|
||||
<h1 id="titulo">Convite para competição</h1>
|
||||
<p id="situacao" role="status" aria-live="polite">Abrindo o convite…</p>
|
||||
|
||||
<form id="formulario" hidden novalidate>
|
||||
<p class="explica">
|
||||
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
|
||||
A organização confere e aprova antes do time entrar na competição.
|
||||
</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Seu time</legend>
|
||||
<label>Nome do time *<input id="nome" maxlength="120" autocomplete="off" required></label>
|
||||
<label>Escola ou clube<input id="escola" maxlength="160" autocomplete="organization"></label>
|
||||
<label>Cidade<input id="cidade" maxlength="120" autocomplete="address-level2"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Responsável pelo time</legend>
|
||||
<label>Nome<input id="contatoNome" maxlength="120" autocomplete="name"></label>
|
||||
<label>Telefone<input id="contatoTelefone" maxlength="40" autocomplete="tel" inputmode="tel"></label>
|
||||
<label>E-mail<input id="contatoEmail" type="email" maxlength="160" autocomplete="email"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Jogadores</legend>
|
||||
<div id="jogadores"></div>
|
||||
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
|
||||
</fieldset>
|
||||
|
||||
<p id="erro" class="erro" role="alert" hidden></p>
|
||||
<button type="submit" id="enviar">Enviar time</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/publico/desafio.js?v=1"></script>
|
||||
<script src="/publico/convite.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
convite.js: |
|
||||
/* ===========================================================================
|
||||
* Convite para competição — a página que a OUTRA escola abre pelo link.
|
||||
*
|
||||
* Sem login: o token no endereço é o único segredo. A página pergunta ao
|
||||
* servidor a que o convite se refere e, se ele vale, deixa a pessoa mandar o
|
||||
* time com os jogadores (nome e camisa). O time chega PENDENTE: quem organiza
|
||||
* confere e aprova.
|
||||
*
|
||||
* Antes de enviar, a verificação anti-robô (desafio.js): pede um desafio ao
|
||||
* servidor, resolve no navegador ("Verificando…") e manda a prova junto com o
|
||||
* time, em `prova`. Um desafio por tentativa: ele é de uso único.
|
||||
*
|
||||
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
|
||||
* HTML. O script é externo porque a CSP é `script-src 'self'`.
|
||||
* =========================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var global = window;
|
||||
|
||||
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
|
||||
var MAX_JOGADORES = 60;
|
||||
|
||||
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
|
||||
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
|
||||
|
||||
var titulo = document.getElementById("titulo");
|
||||
var situacao = document.getElementById("situacao");
|
||||
var formulario = document.getElementById("formulario");
|
||||
var listaJogadores = document.getElementById("jogadores");
|
||||
var erro = document.getElementById("erro");
|
||||
var enviar = document.getElementById("enviar");
|
||||
var ROTULO_ENVIAR = enviar.textContent;
|
||||
|
||||
function recusar(texto) {
|
||||
situacao.textContent = texto;
|
||||
situacao.className = "invalido";
|
||||
formulario.hidden = true;
|
||||
}
|
||||
|
||||
function mostrarErro(texto) {
|
||||
erro.textContent = texto;
|
||||
erro.hidden = !texto;
|
||||
}
|
||||
|
||||
function linhaDeJogador() {
|
||||
var linha = document.createElement("div");
|
||||
linha.className = "linha-jogador";
|
||||
|
||||
var nome = document.createElement("input");
|
||||
nome.placeholder = "Nome do jogador";
|
||||
nome.maxLength = 120;
|
||||
nome.setAttribute("aria-label", "Nome do jogador");
|
||||
nome.setAttribute("data-campo", "nome");
|
||||
|
||||
var camisa = document.createElement("input");
|
||||
camisa.type = "number";
|
||||
camisa.min = "0";
|
||||
camisa.max = "999";
|
||||
camisa.placeholder = "nº";
|
||||
camisa.setAttribute("aria-label", "Número da camisa");
|
||||
camisa.setAttribute("data-campo", "camisa");
|
||||
|
||||
var tirar = document.createElement("button");
|
||||
tirar.type = "button";
|
||||
tirar.className = "secundario";
|
||||
tirar.textContent = "×";
|
||||
tirar.setAttribute("aria-label", "Remover jogador");
|
||||
tirar.addEventListener("click", function () { linha.remove(); });
|
||||
|
||||
linha.appendChild(nome);
|
||||
linha.appendChild(camisa);
|
||||
linha.appendChild(tirar);
|
||||
listaJogadores.appendChild(linha);
|
||||
return linha;
|
||||
}
|
||||
|
||||
function valor(id) {
|
||||
var v = document.getElementById(id).value.trim();
|
||||
return v === "" ? null : v;
|
||||
}
|
||||
|
||||
function montarCorpo() {
|
||||
var jogadores = [];
|
||||
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
|
||||
for (var i = 0; i < linhas.length; i++) {
|
||||
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
|
||||
if (!nome) continue;
|
||||
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
|
||||
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
|
||||
}
|
||||
return {
|
||||
nome: valor("nome"),
|
||||
escola: valor("escola"),
|
||||
cidade: valor("cidade"),
|
||||
contatoNome: valor("contatoNome"),
|
||||
contatoTelefone: valor("contatoTelefone"),
|
||||
contatoEmail: valor("contatoEmail"),
|
||||
jogadores: jogadores
|
||||
};
|
||||
}
|
||||
|
||||
function lerResposta(r) {
|
||||
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
|
||||
}
|
||||
|
||||
function aoEnviar(ev) {
|
||||
ev.preventDefault();
|
||||
mostrarErro("");
|
||||
var corpo = montarCorpo();
|
||||
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
|
||||
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
|
||||
enviar.disabled = true;
|
||||
enviar.textContent = "Verificando…";
|
||||
enviar.setAttribute("aria-busy", "true");
|
||||
|
||||
function voltar(texto) {
|
||||
enviar.disabled = false;
|
||||
enviar.textContent = ROTULO_ENVIAR;
|
||||
enviar.removeAttribute("aria-busy");
|
||||
mostrarErro(texto);
|
||||
}
|
||||
|
||||
fetch(base + "/desafio").then(lerResposta).then(function (r) {
|
||||
if (r.status !== 200) {
|
||||
var e = new Error("recusado");
|
||||
e.detalhe = (r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.";
|
||||
throw e;
|
||||
}
|
||||
if (!global.AtmDesafio) throw new Error("sem-crypto");
|
||||
return global.AtmDesafio.resolver(r.corpo);
|
||||
}).then(function (prova) {
|
||||
corpo.prova = prova;
|
||||
enviar.textContent = "Enviando…";
|
||||
return fetch(base + "/times", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(corpo)
|
||||
}).then(lerResposta);
|
||||
}).then(function (r) {
|
||||
if (r.status >= 200 && r.status < 300) {
|
||||
formulario.hidden = true;
|
||||
titulo.textContent = "Time enviado";
|
||||
situacao.textContent = "Recebemos o time \"" + corpo.nome + "\". A organização vai conferir os dados e aprovar. Você já pode fechar esta página.";
|
||||
situacao.className = "ok";
|
||||
return;
|
||||
}
|
||||
voltar((r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.");
|
||||
}).catch(function (e) {
|
||||
if (e && e.detalhe) { voltar(e.detalhe); return; }
|
||||
if (e && (e.message === "sem-crypto" || e.message === "desafio-invalido" || e.message === "sem-solucao")) {
|
||||
voltar("Não foi possível fazer a verificação anti-robô neste navegador. Atualize a página ou use outro navegador.");
|
||||
return;
|
||||
}
|
||||
voltar("Sem conexão. Confira a internet e tente de novo.");
|
||||
});
|
||||
}
|
||||
|
||||
function iniciar() {
|
||||
if (!TOKEN.test(token)) { recusar("Este endereço de convite não é válido."); return; }
|
||||
fetch(base).then(lerResposta).then(function (r) {
|
||||
var info = r.corpo || {};
|
||||
if (!info.valido) {
|
||||
recusar(info.motivo ? "Não dá para usar este convite: " + info.motivo + "." : "Este convite não está disponível.");
|
||||
return;
|
||||
}
|
||||
titulo.textContent = "Convite: " + (info.competicao || "competição");
|
||||
situacao.textContent = (info.temporada ? "Temporada " + info.temporada + ". " : "")
|
||||
+ "Restam " + info.vagasRestantes + " vaga(s) neste convite.";
|
||||
situacao.className = "";
|
||||
formulario.hidden = false;
|
||||
for (var i = 0; i < 5; i++) linhaDeJogador();
|
||||
}).catch(function () {
|
||||
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("maisJogador").addEventListener("click", function () {
|
||||
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
|
||||
});
|
||||
formulario.addEventListener("submit", aoEnviar);
|
||||
iniciar();
|
||||
})();
|
||||
|
||||
desafio.js: |
|
||||
/* ===========================================================================
|
||||
* Verificação anti-robô das páginas públicas — prova de trabalho no estilo
|
||||
* ALTCHA, sem serviço de terceiro (nada de quem preenche sai do Athletic Map).
|
||||
*
|
||||
* O servidor manda { algoritmo: "SHA-256", desafio, sal, assinatura, maximo }.
|
||||
* Aqui se procura o número n (0..maximo) tal que sha256(sal + n) == desafio, e
|
||||
* devolve-se { numero, sal, assinatura } para ir junto no envio. Em média são
|
||||
* maximo/2 hashes: ~1–2 s num celular com maximo = 150 000.
|
||||
*
|
||||
* Usa crypto.subtle (só existe em HTTPS — e as páginas públicas são HTTPS).
|
||||
* Os hashes saem em lotes paralelos: um `await` por hash custaria mais que o
|
||||
* próprio hash. Script externo porque a CSP é `script-src 'self'`.
|
||||
*
|
||||
* O teste do servidor é DesafioDoConvite (servico-campeonato).
|
||||
* =========================================================================== */
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var LOTE = 1000;
|
||||
var HEX = /^[0-9a-f]{64}$/;
|
||||
|
||||
function bytesDoHex(h) {
|
||||
var b = new Uint8Array(h.length / 2);
|
||||
for (var i = 0; i < b.length; i++) b[i] = parseInt(h.substr(i * 2, 2), 16);
|
||||
return b;
|
||||
}
|
||||
|
||||
function iguais(buf, alvo) {
|
||||
var a = new Uint8Array(buf);
|
||||
if (a.length !== alvo.length) return false;
|
||||
for (var i = 0; i < a.length; i++) if (a[i] !== alvo[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{algoritmo:string, desafio:string, sal:string, assinatura:string, maximo:number}} d
|
||||
* @returns {Promise<{numero:number, sal:string, assinatura:string}>}
|
||||
* rejeita com Error("sem-crypto"), Error("desafio-invalido") ou Error("sem-solucao")
|
||||
*/
|
||||
function resolver(d) {
|
||||
var subtle = global.crypto && global.crypto.subtle;
|
||||
var Codificador = global.TextEncoder;
|
||||
if (!subtle || !Codificador) return Promise.reject(new Error("sem-crypto"));
|
||||
if (!d || d.algoritmo !== "SHA-256" || !HEX.test(d.desafio || "") || typeof d.sal !== "string"
|
||||
|| typeof d.maximo !== "number" || d.maximo < 0 || d.maximo > 5000000) {
|
||||
return Promise.reject(new Error("desafio-invalido"));
|
||||
}
|
||||
var alvo = bytesDoHex(d.desafio);
|
||||
var cod = new Codificador();
|
||||
|
||||
function lote(inicio) {
|
||||
if (inicio > d.maximo) return Promise.reject(new Error("sem-solucao"));
|
||||
var fim = Math.min(inicio + LOTE - 1, d.maximo);
|
||||
var hashes = [];
|
||||
for (var n = inicio; n <= fim; n++) hashes.push(subtle.digest("SHA-256", cod.encode(d.sal + n)));
|
||||
return Promise.all(hashes).then(function (hs) {
|
||||
for (var i = 0; i < hs.length; i++) {
|
||||
if (iguais(hs[i], alvo)) return { numero: inicio + i, sal: d.sal, assinatura: d.assinatura };
|
||||
}
|
||||
return lote(fim + 1);
|
||||
});
|
||||
}
|
||||
return lote(0);
|
||||
}
|
||||
|
||||
global.AtmDesafio = { resolver: resolver };
|
||||
})(window);
|
||||
|
||||
fontes--fontes.css: |
|
||||
/* ============================================================
|
||||
* As fontes da landing page, servidas POR NOS.
|
||||
@@ -609,9 +935,7 @@ data:
|
||||
|
||||
/* marca */
|
||||
.logo{display:inline-flex; align-items:center; gap:11px; font-weight:700; font-size:1.22rem; letter-spacing:-.02em; color:var(--ink); text-decoration:none;}
|
||||
.mark{display:inline-flex; gap:3px; align-items:flex-end;}
|
||||
.mark i{display:block; width:5px; border-radius:2px; background:var(--laranja); transform:skewX(-14deg);}
|
||||
.mark i:nth-child(1){height:13px} .mark i:nth-child(2){height:19px} .mark i:nth-child(3){height:25px}
|
||||
.logo img{display:block; height:37px; width:auto;}
|
||||
|
||||
.topbar{background:var(--surface); border-bottom:1px solid var(--line);}
|
||||
.topbar .wrap{display:flex; align-items:center; justify-content:space-between; gap:16px; padding-block:14px;}
|
||||
@@ -814,7 +1138,7 @@ data:
|
||||
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="logo" href="#"><span class="mark"><i></i><i></i><i></i></span>Athletic Map</a>
|
||||
<a class="logo" href="#" aria-label="Athletic Map"><img src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37"></a>
|
||||
<a class="zap" id="topZap" href="#form">Falar com o time →</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1698,6 +2022,20 @@ data:
|
||||
.termos-uso-caixa input { margin-right: 6px; }
|
||||
.termos-uso-aceite { margin: 10px 0; }
|
||||
|
||||
/* ── Abas (2026-09-29) — mesmo desenho do painel do dono ───────────────────── */
|
||||
.abas { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin: 4px 0 18px; overflow-x: auto; }
|
||||
.aba {
|
||||
background: none; border: 0; border-bottom: 3px solid transparent; padding: 10px 16px;
|
||||
font: inherit; font-weight: 600; color: var(--muted); cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.aba:hover { color: var(--navy); }
|
||||
.aba.ativa { color: var(--navy); border-bottom-color: var(--orange); }
|
||||
.aba .badge {
|
||||
display: inline-block; min-width: 8px; height: 8px; margin-left: 6px; border-radius: 999px;
|
||||
background: var(--orange); vertical-align: middle;
|
||||
}
|
||||
.aba .badge.num { min-width: 18px; height: auto; padding: 0 6px; font-size: 12px; color: #fff; line-height: 18px; }
|
||||
|
||||
minha-conta.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
@@ -1722,7 +2060,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.88">
|
||||
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.90">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1765,23 +2103,56 @@ 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>
|
||||
<!-- Abas (2026-09-29, pedido do dono): a pagina empilhava visao geral, Termos,
|
||||
escolas ativas e canceladas numa coluna so. Mesmo padrao do painel do dono. -->
|
||||
<nav class="abas" role="tablist" aria-label="Seções da conta">
|
||||
<button type="button" class="aba ativa" data-aba="escolas" role="tab">Minhas escolas</button>
|
||||
<button type="button" class="aba" data-aba="visao" role="tab">Visão geral</button>
|
||||
<button type="button" class="aba" data-aba="termos" role="tab">Termos de Uso <span id="termosBadge" class="badge oculto"></span></button>
|
||||
<button type="button" class="aba oculto" data-aba="canceladas" role="tab" id="abaCanceladas">Canceladas</button>
|
||||
</nav>
|
||||
|
||||
<div id="produtos" class="produtos"></div>
|
||||
<div id="aba-escolas" class="painel-aba">
|
||||
<div id="produtos" class="produtos"></div>
|
||||
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-visao" class="painel-aba 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="visaoVazia" class="cartao">
|
||||
<h2>Visão <span class="hl">geral</span></h2>
|
||||
<p class="ajuda">Os números aparecem aqui quando as suas escolinhas enviarem o primeiro resumo
|
||||
(uma vez por dia, de madrugada).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-termos" class="painel-aba oculto">
|
||||
<div id="termosDeUso"></div>
|
||||
<div id="termosEmDia" class="cartao oculto">
|
||||
<h2>Termos de Uso</h2>
|
||||
<p class="ajuda">Nada pendente: a versão vigente já foi aceita para todas as suas escolinhas.
|
||||
<a href="/termos" target="_blank" rel="noopener">Ler os Termos de Uso</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-canceladas" class="painel-aba oculto">
|
||||
<p class="ajuda">Assinaturas canceladas. Regularize o pagamento para voltar a entrar; os dados ficam
|
||||
guardados por 30 dias depois do cancelamento.</p>
|
||||
<div id="produtosCancelados" class="produtos"></div>
|
||||
</div>
|
||||
|
||||
<p class="rodape">
|
||||
@@ -1801,7 +2172,7 @@ data:
|
||||
|
||||
<footer class="direitos">© 2026 Athletic Map. Todos os direitos reservados.</footer>
|
||||
|
||||
<script src="/publico/minha-conta.js?v=2.88"></script>
|
||||
<script src="/publico/minha-conta.js?v=2.90"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1870,6 +2241,36 @@ data:
|
||||
var erroTexto = document.getElementById("erroTexto");
|
||||
var botaoSair = document.getElementById("sair");
|
||||
|
||||
/* ── ABAS (2026-09-29) ────────────────────────────────────────────────────────
|
||||
* Minhas escolas | Visao geral | Termos de Uso | Canceladas. A ultima aba aberta
|
||||
* fica no localStorage (conveniencia deste navegador; sem ela, "escolas"). */
|
||||
var ABA_CHAVE = "atm.conta.aba";
|
||||
function abrirAba(nome) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao || botao.classList.contains("oculto")) { nome = "escolas"; botao = document.querySelector('.aba[data-aba="escolas"]'); }
|
||||
if (!botao) return;
|
||||
document.querySelectorAll(".aba").forEach(function (b) { b.classList.toggle("ativa", b === botao); });
|
||||
document.querySelectorAll(".painel-aba").forEach(function (painel) {
|
||||
painel.classList.toggle("oculto", painel.id !== "aba-" + nome);
|
||||
});
|
||||
try { localStorage.setItem(ABA_CHAVE, nome); } catch (e) { /* aba privada */ }
|
||||
}
|
||||
function abaLembrada() { try { return localStorage.getItem(ABA_CHAVE) || "escolas"; } catch (e) { return "escolas"; } }
|
||||
/* Marca a aba: `pendente` acende a bolinha; `quantos` escreve o numero. */
|
||||
function marcarAba(nome, pendente, quantos) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao) return;
|
||||
var badge = botao.querySelector(".badge");
|
||||
if (badge) {
|
||||
badge.classList.toggle("oculto", !pendente && !quantos);
|
||||
badge.classList.toggle("num", !!quantos);
|
||||
badge.textContent = quantos ? String(quantos) : "";
|
||||
}
|
||||
}
|
||||
document.querySelectorAll(".aba").forEach(function (b) {
|
||||
b.addEventListener("click", function () { abrirAba(b.getAttribute("data-aba")); });
|
||||
});
|
||||
|
||||
function mostrar(qual) {
|
||||
[porta, carregando, conteudo, caixaErro].forEach(function (s) {
|
||||
if (s) s.classList.add("oculto");
|
||||
@@ -1988,14 +2389,18 @@ data:
|
||||
return n ? n.charAt(0).toUpperCase() + n.slice(1) : "—";
|
||||
}
|
||||
|
||||
/* Tenant de DEMONSTRACAO do dono: o servidor marca (`demonstracao: true`). O
|
||||
cartao diz isso ao lado da situacao, e nao ganha "Trocar de plano" nem
|
||||
"Modulos avulsos" — nao ha cobranca, e o servidor recusaria de qualquer jeito. */
|
||||
function cartao(p) {
|
||||
var s = SITUACAO[p.situacao] || ["neutro", String(p.situacao || "").toLowerCase()];
|
||||
var podeEntrar = p.endereco && p.situacao === "ATIVA";
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + '"'
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + (p.demonstracao ? " demonstracao" : "") + '"'
|
||||
+ (p.tenant ? ' data-tenant="' + escapar(p.tenant) + '"' : "") + ">"
|
||||
+ '<span class="faixa-plano">Plano ' + escapar(nomeDoPlano(p.plano)) + "</span>"
|
||||
+ "<h2>" + escapar(p.escolinha || "Sua escolinha") + "</h2>"
|
||||
+ '<span class="selo ' + s[0] + '">' + s[1] + "</span>"
|
||||
+ (p.demonstracao ? '<span class="selo neutro">demonstração — sem cobrança</span>' : "")
|
||||
+ (podeEntrar
|
||||
? '<a class="btn" href="' + escapar(p.endereco) + '">Entrar →</a>'
|
||||
: '<p class="ajuda">' + (p.situacao === "PREPARANDO"
|
||||
@@ -2014,10 +2419,22 @@ data:
|
||||
.then(function (lista) {
|
||||
enderecos = {};
|
||||
(lista || []).forEach(function (p) { if (p.tenant) enderecos[p.tenant] = p.endereco; });
|
||||
produtos.innerHTML = (lista || []).map(cartao).join("");
|
||||
nenhum.classList.toggle("oculto", (lista || []).length > 0);
|
||||
var ativas = (lista || []).filter(function (p) { return p.situacao !== "CANCELADA"; });
|
||||
var canceladas = (lista || []).filter(function (p) { return p.situacao === "CANCELADA"; });
|
||||
produtos.innerHTML = ativas.map(cartao).join("");
|
||||
var caixaCanceladas = document.getElementById("produtosCancelados");
|
||||
if (caixaCanceladas) caixaCanceladas.innerHTML = canceladas.map(cartao).join("");
|
||||
var abaCanceladas = document.getElementById("abaCanceladas");
|
||||
if (abaCanceladas) {
|
||||
abaCanceladas.classList.toggle("oculto", canceladas.length === 0);
|
||||
abaCanceladas.textContent = "Canceladas" + (canceladas.length ? " (" + canceladas.length + ")" : "");
|
||||
}
|
||||
nenhum.classList.toggle("oculto", ativas.length > 0);
|
||||
mostrar(conteudo);
|
||||
(lista || []).forEach(function (p) { if (p.tenant && p.situacao === "ATIVA") acrescentarBotaoDePlano(p.tenant); });
|
||||
abrirAba(abaLembrada());
|
||||
(lista || []).forEach(function (p) {
|
||||
if (p.tenant && p.situacao === "ATIVA" && !p.demonstracao) acrescentarBotaoDePlano(p.tenant);
|
||||
});
|
||||
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas(), carregarVisao(),
|
||||
carregarTermosDeUso()]);
|
||||
});
|
||||
@@ -2323,8 +2740,10 @@ data:
|
||||
.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; });
|
||||
var emDia = document.getElementById("termosEmDia");
|
||||
var faltam = (d && d.versao && d.escolinhas) ? d.escolinhas.filter(function (e) { return !e.aceito; }) : [];
|
||||
if (emDia) emDia.classList.toggle("oculto", faltam.length > 0);
|
||||
marcarAba("termos", faltam.length > 0);
|
||||
if (!faltam.length) return;
|
||||
var aviso = document.createElement("section");
|
||||
aviso.id = "avisoTermos";
|
||||
@@ -2335,7 +2754,8 @@ data:
|
||||
+ ". 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);
|
||||
var caixaTermos = document.getElementById("termosDeUso");
|
||||
if (caixaTermos) caixaTermos.appendChild(aviso); else produtos.parentNode.insertBefore(aviso, produtos);
|
||||
aviso.querySelector("button").addEventListener("click", function () {
|
||||
aceitarTermosDeUso(aviso, d.versao);
|
||||
});
|
||||
@@ -2588,7 +3008,9 @@ data:
|
||||
.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 vazia = document.getElementById("visaoVazia");
|
||||
if (!tabela) { caixa.classList.add("oculto"); if (vazia) vazia.classList.remove("oculto"); return; }
|
||||
if (vazia) vazia.classList.add("oculto");
|
||||
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.";
|
||||
@@ -3171,7 +3593,7 @@ data:
|
||||
<option value="NASCENDO">Nascendo</option>
|
||||
</select>
|
||||
<label class="filtro"><input type="checkbox" id="soAtencao"> só com alerta</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes e demonstrações</label>
|
||||
</div>
|
||||
<div class="tabela-rolagem">
|
||||
<table class="tabela clientes">
|
||||
@@ -3334,7 +3756,7 @@ data:
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/publico/painel.js?v=2.86"></script>
|
||||
<script src="/publico/painel.js?v=2.89"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3630,9 +4052,13 @@ data:
|
||||
bloco("Recebido desde o início", reaisCurto(i.recebidoTotal),
|
||||
i.clientesSemDadoDoAsaas > 0 ? i.clientesSemDadoDoAsaas + " sem resposta do Asaas" : "extrato do Asaas", "")
|
||||
].join("");
|
||||
// Teste e demonstracao aparecem na lista, mas nao nos numeros — e a tela diz.
|
||||
var foraDosNumeros = [];
|
||||
if (i.contasDeTeste > 0) foraDosNumeros.push(i.contasDeTeste + " de teste");
|
||||
if (i.demonstracoes > 0) foraDosNumeros.push(i.demonstracoes + " de demonstração");
|
||||
$("kpiClientes").innerHTML = [
|
||||
bloco("Clientes ativos", esc(i.clientesAtivos),
|
||||
i.contasDeTeste > 0 ? "+ " + i.contasDeTeste + " de teste (fora dos números)" : "", "ok"),
|
||||
foraDosNumeros.length ? "+ " + foraDosNumeros.join(", ") + " (fora dos números)" : "", "ok"),
|
||||
bloco("Novos no mês", esc(i.novosNoMes), comparacao(i.novosNoMes, i.novosMesAnterior), ""),
|
||||
bloco("Leads no mês", esc(i.leadsNoMes), comparacao(i.leadsNoMes, i.leadsMesAnterior), ""),
|
||||
bloco("Conversão", pct(i.conversao), "de lead para cliente pagante", ""),
|
||||
@@ -3671,7 +4097,7 @@ data:
|
||||
if (plano && c.plano !== plano) return false;
|
||||
if (estado && (c.estado || "NASCENDO") !== estado) return false;
|
||||
if (soAtencao && !(c.alertas || []).length) return false;
|
||||
if (semTeste && c.teste) return false;
|
||||
if (semTeste && (c.teste || c.demonstracao)) return false;
|
||||
if (busca) {
|
||||
var alvo = [c.cliente, c.escolinha, c.email, c.cidade, c.tenant, c.whatsapp, c.documento]
|
||||
.join(" ").toLowerCase();
|
||||
@@ -3701,7 +4127,9 @@ data:
|
||||
}
|
||||
|
||||
var SITUACAO_ASAAS = { ACTIVE: "ativa", INACTIVE: "encerrada", EXPIRED: "expirada" };
|
||||
function assinaturaNoAsaas(a) {
|
||||
/* A demonstracao nao existe no Asaas DE PROPOSITO: nao e "nao existe" em vermelho. */
|
||||
function assinaturaNoAsaas(a, c) {
|
||||
if (c && c.demonstracao) return "demonstração — sem cobrança";
|
||||
if (!a || !a.disponivel) return "indisponível agora";
|
||||
if (!a.existe) return '<b class="vermelho">não existe</b>';
|
||||
var s = SITUACAO_ASAAS[a.situacao] || txt(a.situacao);
|
||||
@@ -3738,10 +4166,11 @@ data:
|
||||
var gravidade = (c.alertas || []).reduce(function (g, a) {
|
||||
return a.gravidade === "critico" ? "critico" : (g === "critico" ? g : (a.gravidade === "alerta" ? "alerta" : g));
|
||||
}, "");
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste ? " teste" : "")
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste || c.demonstracao ? " teste" : "")
|
||||
+ (!c.tenant ? " destaque" : "") + '" data-assinatura="' + esc(c.assinatura) + '" tabindex="0">'
|
||||
+ "<td><b>" + txt(c.cliente) + "</b>"
|
||||
+ (c.teste ? ' <span class="selo neutro">teste</span>' : "")
|
||||
+ (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "")
|
||||
+ '<span class="sub">' + (c.tenant
|
||||
? '<a href="' + esc(c.endereco) + '" target="_blank" rel="noopener noreferrer">' + esc(c.tenant) + "</a>"
|
||||
: "sem tenant")
|
||||
@@ -3750,9 +4179,9 @@ data:
|
||||
+ "<td>" + etiquetaPlano(c) + "</td>"
|
||||
+ "<td>" + selo(c.estado, c.tenant) + '<span class="sub">' + (c.diasDeCliente !== null && c.diasDeCliente !== undefined
|
||||
? "cliente há " + esc(c.diasDeCliente) + " dia(s)" : "") + "</span></td>"
|
||||
+ '<td class="num">' + reais(c.valorMensal) + diferencaDeTabela(c) + "</td>"
|
||||
+ '<td class="num">' + (f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + proximaCobranca(f) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? '<span class="ajuda">sem cobrança</span>' : reais(c.valorMensal) + diferencaDeTabela(c)) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? "—" : f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + (c.demonstracao ? '<span class="ajuda">—</span>' : proximaCobranca(f)) + "</td>"
|
||||
+ "<td>" + usoCurto(c.uso, c.usoDeDentro) + "</td>"
|
||||
+ "<td>" + seloSubconta(c.subconta) + "</td>"
|
||||
+ "</tr>";
|
||||
@@ -3778,7 +4207,9 @@ data:
|
||||
+ esc(a.titulo) + '</b><span class="sub">' + esc(a.detalhe) + "</span></div></li>";
|
||||
}).join("");
|
||||
|
||||
var extrato = f.disponivel
|
||||
var extrato = c.demonstracao
|
||||
? '<p class="ajuda">Tenant de demonstração: não há assinatura nem cobrança no Asaas.</p>'
|
||||
: f.disponivel
|
||||
? ((f.cobrancas || []).length
|
||||
? '<table class="tabela"><thead><tr><th>Vencimento</th><th>Pago em</th><th>Forma</th><th class="num">Valor</th><th class="num">Líquido</th><th>Situação</th><th></th></tr></thead><tbody>'
|
||||
+ f.cobrancas.map(function (cb) {
|
||||
@@ -3799,7 +4230,7 @@ data:
|
||||
return '<header class="ficha-topo ' + classePlano(c.plano) + '">'
|
||||
+ '<span class="etiqueta ' + classePlano(c.plano) + '">Plano ' + esc(nomePlano(c.plano))
|
||||
+ (c.ciclo === "ANUAL" ? " · anual" : " · mensal") + "</span>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + "</h2>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "") + "</h2>"
|
||||
+ '<p class="sub">' + (c.escolinha && c.escolinha !== c.cliente ? esc(c.escolinha) + " · " : "")
|
||||
+ selo(c.estado, c.tenant) + (c.estadoDesde ? " desde " + dataHora(c.estadoDesde) : "") + "</p>"
|
||||
+ '<div class="atalhos">'
|
||||
@@ -3807,7 +4238,9 @@ data:
|
||||
+ (whatsapp(c.whatsapp) ? '<a class="btn claro" href="' + esc(whatsapp(c.whatsapp)) + '" target="_blank" rel="noopener noreferrer">WhatsApp</a>' : "")
|
||||
+ (c.email ? '<a class="btn claro" href="mailto:' + esc(c.email) + '">E-mail</a>' : "")
|
||||
+ (f.faturaVencida ? '<a class="btn claro" href="' + esc(linkSeguro(f.faturaVencida) || "#") + '" target="_blank" rel="noopener noreferrer">Fatura vencida</a>' : "")
|
||||
+ "</div></header>"
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="reenviar">Reenviar licença</button>'
|
||||
+ '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="trocar">Trocar chave</button>' : "")
|
||||
+ '</div><p id="licencaAviso" class="ajuda" aria-live="polite"></p></header>'
|
||||
+ (alertas ? '<ul class="lista-alertas">' + alertas + "</ul>" : "")
|
||||
|
||||
+ '<div class="grade-ficha">'
|
||||
@@ -3821,14 +4254,17 @@ data:
|
||||
+ '<section><h3>Assinatura</h3><dl>'
|
||||
+ item("Plano", esc(nomePlano(c.plano)) + " " + (c.ciclo === "ANUAL" ? "anual" : "mensal"))
|
||||
+ item("Valor do ciclo", reais(c.valorDoCiclo)) + item("Vale por mês", reais(c.valorMensal) + diferencaDeTabela(c))
|
||||
+ item("De onde vem o valor", c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas))
|
||||
+ item("De onde vem o valor", c.demonstracao ? "demonstração — sem cobrança"
|
||||
: c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas, c))
|
||||
+ item("Contratado em", dataHora(c.contratadoEm)) + item("Cliente há", c.diasDeCliente === null || c.diasDeCliente === undefined ? "—" : esc(c.diasDeCliente) + " dia(s)")
|
||||
+ item("Assinatura no Asaas", txt(c.assinatura)) + item("Cliente no Asaas", txt(c.clienteNoProvedor))
|
||||
+ "</dl></section>"
|
||||
|
||||
+ '<section><h3>Dinheiro</h3><dl>'
|
||||
+ (f.disponivel
|
||||
+ (c.demonstracao
|
||||
? item("Cobrança", "nenhuma — tenant de demonstração")
|
||||
: f.disponivel
|
||||
? item("Total pago", reais(f.totalPago)) + item("Líquido (após taxas)", reais(f.totalLiquido))
|
||||
+ item("Taxas pagas", reais(f.taxas))
|
||||
+ item("Último pagamento", f.ultimoPagamento ? data(f.ultimoPagamento.data) + " · " + reais(f.ultimoPagamento.valor) + " · " + txt(f.ultimoPagamento.forma) : "—")
|
||||
@@ -4325,6 +4761,55 @@ data:
|
||||
corpo.addEventListener("keydown", function (ev) { if (ev.key === "Enter") aoEscolher(ev); });
|
||||
$("alertas").addEventListener("click", aoEscolher);
|
||||
|
||||
/* ── licenca dos apps (dono, treinador, pais) ─────────────────────────────
|
||||
* Reenviar = mandar a MESMA chave da escola ao dono da compra (captacao 4.32+;
|
||||
* so a chave antiga, guardada so com hash, e trocada uma vez). Nada muda para
|
||||
* ninguem: sem confirmacao.
|
||||
* Trocar = revogar a chave e gerar outra. Desde os apps 1.0.4 (captacao 4.33)
|
||||
* o app pergunta a situacao da chave ao abrir: trocar DERRUBA os apps de todos
|
||||
* da escola (dono, treinadores, pais) em ate 15 min de uso, e cada um precisa
|
||||
* da chave nova. Por isso pede o nome do tenant digitado. */
|
||||
var API_LICENCA = "/api/plataforma/painel/licencas/";
|
||||
|
||||
function acaoDaLicenca(tenant, acao, aviso) {
|
||||
if (!tokenAtual) return Promise.resolve();
|
||||
if (acao === "trocar") {
|
||||
var digitado = window.prompt("TROCAR a chave de " + tenant + "?\n\n"
|
||||
+ "A chave atual para de valer e os apps de TODOS da escola (dono, treinadores e pais) saem da conta "
|
||||
+ "em até 15 minutos de uso; cada um vai precisar da chave nova, que vai por e-mail ao dono da compra.\n"
|
||||
+ "Use só se a chave vazou.\n\nPara confirmar, digite o nome do tenant (" + tenant + "):");
|
||||
if (digitado === null) return Promise.resolve();
|
||||
if (digitado.trim() !== tenant) {
|
||||
aviso.textContent = "Nada foi trocado: o nome digitado não confere.";
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
aviso.textContent = "Enviando…";
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/" + acao, {
|
||||
method: "POST", headers: { "x-atm-admin-token": tokenAtual, "Content-Type": "application/json" }, body: "{}"
|
||||
}).then(function (r) {
|
||||
if (r.status === 401) throw new Error("O token expirou. Saia e entre de novo no painel.");
|
||||
return r.json().then(function (d) {
|
||||
if (!r.ok) throw new Error((d && d.detail) || ("O servidor recusou (" + r.status + ")."));
|
||||
return d;
|
||||
});
|
||||
}).then(function (d) {
|
||||
var oQue = acao === "trocar" ? "Chave NOVA" : "Chave da escola";
|
||||
aviso.textContent = oQue + (d.emailEnviado ? " enviada para " : " pronta; o e-mail vai em instantes para ")
|
||||
+ (d.destino || "o dono da compra") + " (termina em " + d.finalDaChave + ").";
|
||||
}).catch(function (e) {
|
||||
aviso.textContent = e && e.message && e.message !== "Failed to fetch" ? e.message : "Sem conexão com o servidor.";
|
||||
});
|
||||
}
|
||||
|
||||
$("fichaCorpo").addEventListener("click", function (ev) {
|
||||
var b = ev.target.closest ? ev.target.closest("[data-licenca]") : null;
|
||||
if (!b) return;
|
||||
b.disabled = true;
|
||||
acaoDaLicenca(b.getAttribute("data-licenca"), b.getAttribute("data-acao") || "reenviar", $("licencaAviso"))
|
||||
.then(function () { b.disabled = false; });
|
||||
});
|
||||
|
||||
$("fecharFicha").addEventListener("click", fecharFicha);
|
||||
$("ficha").addEventListener("click", function (ev) { if (ev.target === $("ficha")) fecharFicha(); });
|
||||
document.addEventListener("keydown", function (ev) { if (ev.key === "Escape") fecharFicha(); });
|
||||
|
||||
@@ -370,6 +370,332 @@ data:
|
||||
});
|
||||
})();
|
||||
|
||||
convite.css: |
|
||||
/* Convite para competição: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). */
|
||||
main { max-width: 38rem; }
|
||||
.explica { color: var(--muted); }
|
||||
form { display: grid; gap: 1rem; }
|
||||
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
|
||||
legend { font-weight: 700; padding: 0 .4rem; }
|
||||
label { display: grid; gap: .2rem; font-size: .92rem; font-weight: 600; }
|
||||
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
|
||||
input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
|
||||
.linha-jogador { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
|
||||
.linha-jogador button { padding: .4rem .6rem; }
|
||||
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
|
||||
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
|
||||
button:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.erro { color: #b42318; margin: 0; font-weight: 600; }
|
||||
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: 1rem 1.2rem; }
|
||||
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
|
||||
|
||||
convite.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, nofollow">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Convite para competição — Athletic Map</title>
|
||||
<link rel="stylesheet" href="/publico/_estilo.css">
|
||||
<link rel="stylesheet" href="/publico/convite.css?v=1">
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="marca">Athletic <span>Map</span></div></header>
|
||||
<main>
|
||||
<h1 id="titulo">Convite para competição</h1>
|
||||
<p id="situacao" role="status" aria-live="polite">Abrindo o convite…</p>
|
||||
|
||||
<form id="formulario" hidden novalidate>
|
||||
<p class="explica">
|
||||
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
|
||||
A organização confere e aprova antes do time entrar na competição.
|
||||
</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Seu time</legend>
|
||||
<label>Nome do time *<input id="nome" maxlength="120" autocomplete="off" required></label>
|
||||
<label>Escola ou clube<input id="escola" maxlength="160" autocomplete="organization"></label>
|
||||
<label>Cidade<input id="cidade" maxlength="120" autocomplete="address-level2"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Responsável pelo time</legend>
|
||||
<label>Nome<input id="contatoNome" maxlength="120" autocomplete="name"></label>
|
||||
<label>Telefone<input id="contatoTelefone" maxlength="40" autocomplete="tel" inputmode="tel"></label>
|
||||
<label>E-mail<input id="contatoEmail" type="email" maxlength="160" autocomplete="email"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Jogadores</legend>
|
||||
<div id="jogadores"></div>
|
||||
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
|
||||
</fieldset>
|
||||
|
||||
<p id="erro" class="erro" role="alert" hidden></p>
|
||||
<button type="submit" id="enviar">Enviar time</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/publico/desafio.js?v=1"></script>
|
||||
<script src="/publico/convite.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
convite.js: |
|
||||
/* ===========================================================================
|
||||
* Convite para competição — a página que a OUTRA escola abre pelo link.
|
||||
*
|
||||
* Sem login: o token no endereço é o único segredo. A página pergunta ao
|
||||
* servidor a que o convite se refere e, se ele vale, deixa a pessoa mandar o
|
||||
* time com os jogadores (nome e camisa). O time chega PENDENTE: quem organiza
|
||||
* confere e aprova.
|
||||
*
|
||||
* Antes de enviar, a verificação anti-robô (desafio.js): pede um desafio ao
|
||||
* servidor, resolve no navegador ("Verificando…") e manda a prova junto com o
|
||||
* time, em `prova`. Um desafio por tentativa: ele é de uso único.
|
||||
*
|
||||
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
|
||||
* HTML. O script é externo porque a CSP é `script-src 'self'`.
|
||||
* =========================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var global = window;
|
||||
|
||||
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
|
||||
var MAX_JOGADORES = 60;
|
||||
|
||||
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
|
||||
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
|
||||
|
||||
var titulo = document.getElementById("titulo");
|
||||
var situacao = document.getElementById("situacao");
|
||||
var formulario = document.getElementById("formulario");
|
||||
var listaJogadores = document.getElementById("jogadores");
|
||||
var erro = document.getElementById("erro");
|
||||
var enviar = document.getElementById("enviar");
|
||||
var ROTULO_ENVIAR = enviar.textContent;
|
||||
|
||||
function recusar(texto) {
|
||||
situacao.textContent = texto;
|
||||
situacao.className = "invalido";
|
||||
formulario.hidden = true;
|
||||
}
|
||||
|
||||
function mostrarErro(texto) {
|
||||
erro.textContent = texto;
|
||||
erro.hidden = !texto;
|
||||
}
|
||||
|
||||
function linhaDeJogador() {
|
||||
var linha = document.createElement("div");
|
||||
linha.className = "linha-jogador";
|
||||
|
||||
var nome = document.createElement("input");
|
||||
nome.placeholder = "Nome do jogador";
|
||||
nome.maxLength = 120;
|
||||
nome.setAttribute("aria-label", "Nome do jogador");
|
||||
nome.setAttribute("data-campo", "nome");
|
||||
|
||||
var camisa = document.createElement("input");
|
||||
camisa.type = "number";
|
||||
camisa.min = "0";
|
||||
camisa.max = "999";
|
||||
camisa.placeholder = "nº";
|
||||
camisa.setAttribute("aria-label", "Número da camisa");
|
||||
camisa.setAttribute("data-campo", "camisa");
|
||||
|
||||
var tirar = document.createElement("button");
|
||||
tirar.type = "button";
|
||||
tirar.className = "secundario";
|
||||
tirar.textContent = "×";
|
||||
tirar.setAttribute("aria-label", "Remover jogador");
|
||||
tirar.addEventListener("click", function () { linha.remove(); });
|
||||
|
||||
linha.appendChild(nome);
|
||||
linha.appendChild(camisa);
|
||||
linha.appendChild(tirar);
|
||||
listaJogadores.appendChild(linha);
|
||||
return linha;
|
||||
}
|
||||
|
||||
function valor(id) {
|
||||
var v = document.getElementById(id).value.trim();
|
||||
return v === "" ? null : v;
|
||||
}
|
||||
|
||||
function montarCorpo() {
|
||||
var jogadores = [];
|
||||
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
|
||||
for (var i = 0; i < linhas.length; i++) {
|
||||
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
|
||||
if (!nome) continue;
|
||||
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
|
||||
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
|
||||
}
|
||||
return {
|
||||
nome: valor("nome"),
|
||||
escola: valor("escola"),
|
||||
cidade: valor("cidade"),
|
||||
contatoNome: valor("contatoNome"),
|
||||
contatoTelefone: valor("contatoTelefone"),
|
||||
contatoEmail: valor("contatoEmail"),
|
||||
jogadores: jogadores
|
||||
};
|
||||
}
|
||||
|
||||
function lerResposta(r) {
|
||||
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
|
||||
}
|
||||
|
||||
function aoEnviar(ev) {
|
||||
ev.preventDefault();
|
||||
mostrarErro("");
|
||||
var corpo = montarCorpo();
|
||||
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
|
||||
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
|
||||
enviar.disabled = true;
|
||||
enviar.textContent = "Verificando…";
|
||||
enviar.setAttribute("aria-busy", "true");
|
||||
|
||||
function voltar(texto) {
|
||||
enviar.disabled = false;
|
||||
enviar.textContent = ROTULO_ENVIAR;
|
||||
enviar.removeAttribute("aria-busy");
|
||||
mostrarErro(texto);
|
||||
}
|
||||
|
||||
fetch(base + "/desafio").then(lerResposta).then(function (r) {
|
||||
if (r.status !== 200) {
|
||||
var e = new Error("recusado");
|
||||
e.detalhe = (r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.";
|
||||
throw e;
|
||||
}
|
||||
if (!global.AtmDesafio) throw new Error("sem-crypto");
|
||||
return global.AtmDesafio.resolver(r.corpo);
|
||||
}).then(function (prova) {
|
||||
corpo.prova = prova;
|
||||
enviar.textContent = "Enviando…";
|
||||
return fetch(base + "/times", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(corpo)
|
||||
}).then(lerResposta);
|
||||
}).then(function (r) {
|
||||
if (r.status >= 200 && r.status < 300) {
|
||||
formulario.hidden = true;
|
||||
titulo.textContent = "Time enviado";
|
||||
situacao.textContent = "Recebemos o time \"" + corpo.nome + "\". A organização vai conferir os dados e aprovar. Você já pode fechar esta página.";
|
||||
situacao.className = "ok";
|
||||
return;
|
||||
}
|
||||
voltar((r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.");
|
||||
}).catch(function (e) {
|
||||
if (e && e.detalhe) { voltar(e.detalhe); return; }
|
||||
if (e && (e.message === "sem-crypto" || e.message === "desafio-invalido" || e.message === "sem-solucao")) {
|
||||
voltar("Não foi possível fazer a verificação anti-robô neste navegador. Atualize a página ou use outro navegador.");
|
||||
return;
|
||||
}
|
||||
voltar("Sem conexão. Confira a internet e tente de novo.");
|
||||
});
|
||||
}
|
||||
|
||||
function iniciar() {
|
||||
if (!TOKEN.test(token)) { recusar("Este endereço de convite não é válido."); return; }
|
||||
fetch(base).then(lerResposta).then(function (r) {
|
||||
var info = r.corpo || {};
|
||||
if (!info.valido) {
|
||||
recusar(info.motivo ? "Não dá para usar este convite: " + info.motivo + "." : "Este convite não está disponível.");
|
||||
return;
|
||||
}
|
||||
titulo.textContent = "Convite: " + (info.competicao || "competição");
|
||||
situacao.textContent = (info.temporada ? "Temporada " + info.temporada + ". " : "")
|
||||
+ "Restam " + info.vagasRestantes + " vaga(s) neste convite.";
|
||||
situacao.className = "";
|
||||
formulario.hidden = false;
|
||||
for (var i = 0; i < 5; i++) linhaDeJogador();
|
||||
}).catch(function () {
|
||||
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("maisJogador").addEventListener("click", function () {
|
||||
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
|
||||
});
|
||||
formulario.addEventListener("submit", aoEnviar);
|
||||
iniciar();
|
||||
})();
|
||||
|
||||
desafio.js: |
|
||||
/* ===========================================================================
|
||||
* Verificação anti-robô das páginas públicas — prova de trabalho no estilo
|
||||
* ALTCHA, sem serviço de terceiro (nada de quem preenche sai do Athletic Map).
|
||||
*
|
||||
* O servidor manda { algoritmo: "SHA-256", desafio, sal, assinatura, maximo }.
|
||||
* Aqui se procura o número n (0..maximo) tal que sha256(sal + n) == desafio, e
|
||||
* devolve-se { numero, sal, assinatura } para ir junto no envio. Em média são
|
||||
* maximo/2 hashes: ~1–2 s num celular com maximo = 150 000.
|
||||
*
|
||||
* Usa crypto.subtle (só existe em HTTPS — e as páginas públicas são HTTPS).
|
||||
* Os hashes saem em lotes paralelos: um `await` por hash custaria mais que o
|
||||
* próprio hash. Script externo porque a CSP é `script-src 'self'`.
|
||||
*
|
||||
* O teste do servidor é DesafioDoConvite (servico-campeonato).
|
||||
* =========================================================================== */
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var LOTE = 1000;
|
||||
var HEX = /^[0-9a-f]{64}$/;
|
||||
|
||||
function bytesDoHex(h) {
|
||||
var b = new Uint8Array(h.length / 2);
|
||||
for (var i = 0; i < b.length; i++) b[i] = parseInt(h.substr(i * 2, 2), 16);
|
||||
return b;
|
||||
}
|
||||
|
||||
function iguais(buf, alvo) {
|
||||
var a = new Uint8Array(buf);
|
||||
if (a.length !== alvo.length) return false;
|
||||
for (var i = 0; i < a.length; i++) if (a[i] !== alvo[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{algoritmo:string, desafio:string, sal:string, assinatura:string, maximo:number}} d
|
||||
* @returns {Promise<{numero:number, sal:string, assinatura:string}>}
|
||||
* rejeita com Error("sem-crypto"), Error("desafio-invalido") ou Error("sem-solucao")
|
||||
*/
|
||||
function resolver(d) {
|
||||
var subtle = global.crypto && global.crypto.subtle;
|
||||
var Codificador = global.TextEncoder;
|
||||
if (!subtle || !Codificador) return Promise.reject(new Error("sem-crypto"));
|
||||
if (!d || d.algoritmo !== "SHA-256" || !HEX.test(d.desafio || "") || typeof d.sal !== "string"
|
||||
|| typeof d.maximo !== "number" || d.maximo < 0 || d.maximo > 5000000) {
|
||||
return Promise.reject(new Error("desafio-invalido"));
|
||||
}
|
||||
var alvo = bytesDoHex(d.desafio);
|
||||
var cod = new Codificador();
|
||||
|
||||
function lote(inicio) {
|
||||
if (inicio > d.maximo) return Promise.reject(new Error("sem-solucao"));
|
||||
var fim = Math.min(inicio + LOTE - 1, d.maximo);
|
||||
var hashes = [];
|
||||
for (var n = inicio; n <= fim; n++) hashes.push(subtle.digest("SHA-256", cod.encode(d.sal + n)));
|
||||
return Promise.all(hashes).then(function (hs) {
|
||||
for (var i = 0; i < hs.length; i++) {
|
||||
if (iguais(hs[i], alvo)) return { numero: inicio + i, sal: d.sal, assinatura: d.assinatura };
|
||||
}
|
||||
return lote(fim + 1);
|
||||
});
|
||||
}
|
||||
return lote(0);
|
||||
}
|
||||
|
||||
global.AtmDesafio = { resolver: resolver };
|
||||
})(window);
|
||||
|
||||
fontes--fontes.css: |
|
||||
/* ============================================================
|
||||
* As fontes da landing page, servidas POR NOS.
|
||||
@@ -609,9 +935,7 @@ data:
|
||||
|
||||
/* marca */
|
||||
.logo{display:inline-flex; align-items:center; gap:11px; font-weight:700; font-size:1.22rem; letter-spacing:-.02em; color:var(--ink); text-decoration:none;}
|
||||
.mark{display:inline-flex; gap:3px; align-items:flex-end;}
|
||||
.mark i{display:block; width:5px; border-radius:2px; background:var(--laranja); transform:skewX(-14deg);}
|
||||
.mark i:nth-child(1){height:13px} .mark i:nth-child(2){height:19px} .mark i:nth-child(3){height:25px}
|
||||
.logo img{display:block; height:37px; width:auto;}
|
||||
|
||||
.topbar{background:var(--surface); border-bottom:1px solid var(--line);}
|
||||
.topbar .wrap{display:flex; align-items:center; justify-content:space-between; gap:16px; padding-block:14px;}
|
||||
@@ -814,7 +1138,7 @@ data:
|
||||
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="logo" href="#"><span class="mark"><i></i><i></i><i></i></span>Athletic Map</a>
|
||||
<a class="logo" href="#" aria-label="Athletic Map"><img src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37"></a>
|
||||
<a class="zap" id="topZap" href="#form">Falar com o time →</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1698,6 +2022,20 @@ data:
|
||||
.termos-uso-caixa input { margin-right: 6px; }
|
||||
.termos-uso-aceite { margin: 10px 0; }
|
||||
|
||||
/* ── Abas (2026-09-29) — mesmo desenho do painel do dono ───────────────────── */
|
||||
.abas { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin: 4px 0 18px; overflow-x: auto; }
|
||||
.aba {
|
||||
background: none; border: 0; border-bottom: 3px solid transparent; padding: 10px 16px;
|
||||
font: inherit; font-weight: 600; color: var(--muted); cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.aba:hover { color: var(--navy); }
|
||||
.aba.ativa { color: var(--navy); border-bottom-color: var(--orange); }
|
||||
.aba .badge {
|
||||
display: inline-block; min-width: 8px; height: 8px; margin-left: 6px; border-radius: 999px;
|
||||
background: var(--orange); vertical-align: middle;
|
||||
}
|
||||
.aba .badge.num { min-width: 18px; height: auto; padding: 0 6px; font-size: 12px; color: #fff; line-height: 18px; }
|
||||
|
||||
minha-conta.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
@@ -1722,7 +2060,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.88">
|
||||
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.90">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1765,23 +2103,56 @@ 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>
|
||||
<!-- Abas (2026-09-29, pedido do dono): a pagina empilhava visao geral, Termos,
|
||||
escolas ativas e canceladas numa coluna so. Mesmo padrao do painel do dono. -->
|
||||
<nav class="abas" role="tablist" aria-label="Seções da conta">
|
||||
<button type="button" class="aba ativa" data-aba="escolas" role="tab">Minhas escolas</button>
|
||||
<button type="button" class="aba" data-aba="visao" role="tab">Visão geral</button>
|
||||
<button type="button" class="aba" data-aba="termos" role="tab">Termos de Uso <span id="termosBadge" class="badge oculto"></span></button>
|
||||
<button type="button" class="aba oculto" data-aba="canceladas" role="tab" id="abaCanceladas">Canceladas</button>
|
||||
</nav>
|
||||
|
||||
<div id="produtos" class="produtos"></div>
|
||||
<div id="aba-escolas" class="painel-aba">
|
||||
<div id="produtos" class="produtos"></div>
|
||||
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-visao" class="painel-aba 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="visaoVazia" class="cartao">
|
||||
<h2>Visão <span class="hl">geral</span></h2>
|
||||
<p class="ajuda">Os números aparecem aqui quando as suas escolinhas enviarem o primeiro resumo
|
||||
(uma vez por dia, de madrugada).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-termos" class="painel-aba oculto">
|
||||
<div id="termosDeUso"></div>
|
||||
<div id="termosEmDia" class="cartao oculto">
|
||||
<h2>Termos de Uso</h2>
|
||||
<p class="ajuda">Nada pendente: a versão vigente já foi aceita para todas as suas escolinhas.
|
||||
<a href="/termos" target="_blank" rel="noopener">Ler os Termos de Uso</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-canceladas" class="painel-aba oculto">
|
||||
<p class="ajuda">Assinaturas canceladas. Regularize o pagamento para voltar a entrar; os dados ficam
|
||||
guardados por 30 dias depois do cancelamento.</p>
|
||||
<div id="produtosCancelados" class="produtos"></div>
|
||||
</div>
|
||||
|
||||
<p class="rodape">
|
||||
@@ -1801,7 +2172,7 @@ data:
|
||||
|
||||
<footer class="direitos">© 2026 Athletic Map. Todos os direitos reservados.</footer>
|
||||
|
||||
<script src="/publico/minha-conta.js?v=2.88"></script>
|
||||
<script src="/publico/minha-conta.js?v=2.90"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1870,6 +2241,36 @@ data:
|
||||
var erroTexto = document.getElementById("erroTexto");
|
||||
var botaoSair = document.getElementById("sair");
|
||||
|
||||
/* ── ABAS (2026-09-29) ────────────────────────────────────────────────────────
|
||||
* Minhas escolas | Visao geral | Termos de Uso | Canceladas. A ultima aba aberta
|
||||
* fica no localStorage (conveniencia deste navegador; sem ela, "escolas"). */
|
||||
var ABA_CHAVE = "atm.conta.aba";
|
||||
function abrirAba(nome) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao || botao.classList.contains("oculto")) { nome = "escolas"; botao = document.querySelector('.aba[data-aba="escolas"]'); }
|
||||
if (!botao) return;
|
||||
document.querySelectorAll(".aba").forEach(function (b) { b.classList.toggle("ativa", b === botao); });
|
||||
document.querySelectorAll(".painel-aba").forEach(function (painel) {
|
||||
painel.classList.toggle("oculto", painel.id !== "aba-" + nome);
|
||||
});
|
||||
try { localStorage.setItem(ABA_CHAVE, nome); } catch (e) { /* aba privada */ }
|
||||
}
|
||||
function abaLembrada() { try { return localStorage.getItem(ABA_CHAVE) || "escolas"; } catch (e) { return "escolas"; } }
|
||||
/* Marca a aba: `pendente` acende a bolinha; `quantos` escreve o numero. */
|
||||
function marcarAba(nome, pendente, quantos) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao) return;
|
||||
var badge = botao.querySelector(".badge");
|
||||
if (badge) {
|
||||
badge.classList.toggle("oculto", !pendente && !quantos);
|
||||
badge.classList.toggle("num", !!quantos);
|
||||
badge.textContent = quantos ? String(quantos) : "";
|
||||
}
|
||||
}
|
||||
document.querySelectorAll(".aba").forEach(function (b) {
|
||||
b.addEventListener("click", function () { abrirAba(b.getAttribute("data-aba")); });
|
||||
});
|
||||
|
||||
function mostrar(qual) {
|
||||
[porta, carregando, conteudo, caixaErro].forEach(function (s) {
|
||||
if (s) s.classList.add("oculto");
|
||||
@@ -1988,14 +2389,18 @@ data:
|
||||
return n ? n.charAt(0).toUpperCase() + n.slice(1) : "—";
|
||||
}
|
||||
|
||||
/* Tenant de DEMONSTRACAO do dono: o servidor marca (`demonstracao: true`). O
|
||||
cartao diz isso ao lado da situacao, e nao ganha "Trocar de plano" nem
|
||||
"Modulos avulsos" — nao ha cobranca, e o servidor recusaria de qualquer jeito. */
|
||||
function cartao(p) {
|
||||
var s = SITUACAO[p.situacao] || ["neutro", String(p.situacao || "").toLowerCase()];
|
||||
var podeEntrar = p.endereco && p.situacao === "ATIVA";
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + '"'
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + (p.demonstracao ? " demonstracao" : "") + '"'
|
||||
+ (p.tenant ? ' data-tenant="' + escapar(p.tenant) + '"' : "") + ">"
|
||||
+ '<span class="faixa-plano">Plano ' + escapar(nomeDoPlano(p.plano)) + "</span>"
|
||||
+ "<h2>" + escapar(p.escolinha || "Sua escolinha") + "</h2>"
|
||||
+ '<span class="selo ' + s[0] + '">' + s[1] + "</span>"
|
||||
+ (p.demonstracao ? '<span class="selo neutro">demonstração — sem cobrança</span>' : "")
|
||||
+ (podeEntrar
|
||||
? '<a class="btn" href="' + escapar(p.endereco) + '">Entrar →</a>'
|
||||
: '<p class="ajuda">' + (p.situacao === "PREPARANDO"
|
||||
@@ -2014,10 +2419,22 @@ data:
|
||||
.then(function (lista) {
|
||||
enderecos = {};
|
||||
(lista || []).forEach(function (p) { if (p.tenant) enderecos[p.tenant] = p.endereco; });
|
||||
produtos.innerHTML = (lista || []).map(cartao).join("");
|
||||
nenhum.classList.toggle("oculto", (lista || []).length > 0);
|
||||
var ativas = (lista || []).filter(function (p) { return p.situacao !== "CANCELADA"; });
|
||||
var canceladas = (lista || []).filter(function (p) { return p.situacao === "CANCELADA"; });
|
||||
produtos.innerHTML = ativas.map(cartao).join("");
|
||||
var caixaCanceladas = document.getElementById("produtosCancelados");
|
||||
if (caixaCanceladas) caixaCanceladas.innerHTML = canceladas.map(cartao).join("");
|
||||
var abaCanceladas = document.getElementById("abaCanceladas");
|
||||
if (abaCanceladas) {
|
||||
abaCanceladas.classList.toggle("oculto", canceladas.length === 0);
|
||||
abaCanceladas.textContent = "Canceladas" + (canceladas.length ? " (" + canceladas.length + ")" : "");
|
||||
}
|
||||
nenhum.classList.toggle("oculto", ativas.length > 0);
|
||||
mostrar(conteudo);
|
||||
(lista || []).forEach(function (p) { if (p.tenant && p.situacao === "ATIVA") acrescentarBotaoDePlano(p.tenant); });
|
||||
abrirAba(abaLembrada());
|
||||
(lista || []).forEach(function (p) {
|
||||
if (p.tenant && p.situacao === "ATIVA" && !p.demonstracao) acrescentarBotaoDePlano(p.tenant);
|
||||
});
|
||||
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas(), carregarVisao(),
|
||||
carregarTermosDeUso()]);
|
||||
});
|
||||
@@ -2323,8 +2740,10 @@ data:
|
||||
.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; });
|
||||
var emDia = document.getElementById("termosEmDia");
|
||||
var faltam = (d && d.versao && d.escolinhas) ? d.escolinhas.filter(function (e) { return !e.aceito; }) : [];
|
||||
if (emDia) emDia.classList.toggle("oculto", faltam.length > 0);
|
||||
marcarAba("termos", faltam.length > 0);
|
||||
if (!faltam.length) return;
|
||||
var aviso = document.createElement("section");
|
||||
aviso.id = "avisoTermos";
|
||||
@@ -2335,7 +2754,8 @@ data:
|
||||
+ ". 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);
|
||||
var caixaTermos = document.getElementById("termosDeUso");
|
||||
if (caixaTermos) caixaTermos.appendChild(aviso); else produtos.parentNode.insertBefore(aviso, produtos);
|
||||
aviso.querySelector("button").addEventListener("click", function () {
|
||||
aceitarTermosDeUso(aviso, d.versao);
|
||||
});
|
||||
@@ -2588,7 +3008,9 @@ data:
|
||||
.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 vazia = document.getElementById("visaoVazia");
|
||||
if (!tabela) { caixa.classList.add("oculto"); if (vazia) vazia.classList.remove("oculto"); return; }
|
||||
if (vazia) vazia.classList.add("oculto");
|
||||
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.";
|
||||
@@ -3171,7 +3593,7 @@ data:
|
||||
<option value="NASCENDO">Nascendo</option>
|
||||
</select>
|
||||
<label class="filtro"><input type="checkbox" id="soAtencao"> só com alerta</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes e demonstrações</label>
|
||||
</div>
|
||||
<div class="tabela-rolagem">
|
||||
<table class="tabela clientes">
|
||||
@@ -3334,7 +3756,7 @@ data:
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/publico/painel.js?v=2.86"></script>
|
||||
<script src="/publico/painel.js?v=2.89"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3630,9 +4052,13 @@ data:
|
||||
bloco("Recebido desde o início", reaisCurto(i.recebidoTotal),
|
||||
i.clientesSemDadoDoAsaas > 0 ? i.clientesSemDadoDoAsaas + " sem resposta do Asaas" : "extrato do Asaas", "")
|
||||
].join("");
|
||||
// Teste e demonstracao aparecem na lista, mas nao nos numeros — e a tela diz.
|
||||
var foraDosNumeros = [];
|
||||
if (i.contasDeTeste > 0) foraDosNumeros.push(i.contasDeTeste + " de teste");
|
||||
if (i.demonstracoes > 0) foraDosNumeros.push(i.demonstracoes + " de demonstração");
|
||||
$("kpiClientes").innerHTML = [
|
||||
bloco("Clientes ativos", esc(i.clientesAtivos),
|
||||
i.contasDeTeste > 0 ? "+ " + i.contasDeTeste + " de teste (fora dos números)" : "", "ok"),
|
||||
foraDosNumeros.length ? "+ " + foraDosNumeros.join(", ") + " (fora dos números)" : "", "ok"),
|
||||
bloco("Novos no mês", esc(i.novosNoMes), comparacao(i.novosNoMes, i.novosMesAnterior), ""),
|
||||
bloco("Leads no mês", esc(i.leadsNoMes), comparacao(i.leadsNoMes, i.leadsMesAnterior), ""),
|
||||
bloco("Conversão", pct(i.conversao), "de lead para cliente pagante", ""),
|
||||
@@ -3671,7 +4097,7 @@ data:
|
||||
if (plano && c.plano !== plano) return false;
|
||||
if (estado && (c.estado || "NASCENDO") !== estado) return false;
|
||||
if (soAtencao && !(c.alertas || []).length) return false;
|
||||
if (semTeste && c.teste) return false;
|
||||
if (semTeste && (c.teste || c.demonstracao)) return false;
|
||||
if (busca) {
|
||||
var alvo = [c.cliente, c.escolinha, c.email, c.cidade, c.tenant, c.whatsapp, c.documento]
|
||||
.join(" ").toLowerCase();
|
||||
@@ -3701,7 +4127,9 @@ data:
|
||||
}
|
||||
|
||||
var SITUACAO_ASAAS = { ACTIVE: "ativa", INACTIVE: "encerrada", EXPIRED: "expirada" };
|
||||
function assinaturaNoAsaas(a) {
|
||||
/* A demonstracao nao existe no Asaas DE PROPOSITO: nao e "nao existe" em vermelho. */
|
||||
function assinaturaNoAsaas(a, c) {
|
||||
if (c && c.demonstracao) return "demonstração — sem cobrança";
|
||||
if (!a || !a.disponivel) return "indisponível agora";
|
||||
if (!a.existe) return '<b class="vermelho">não existe</b>';
|
||||
var s = SITUACAO_ASAAS[a.situacao] || txt(a.situacao);
|
||||
@@ -3738,10 +4166,11 @@ data:
|
||||
var gravidade = (c.alertas || []).reduce(function (g, a) {
|
||||
return a.gravidade === "critico" ? "critico" : (g === "critico" ? g : (a.gravidade === "alerta" ? "alerta" : g));
|
||||
}, "");
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste ? " teste" : "")
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste || c.demonstracao ? " teste" : "")
|
||||
+ (!c.tenant ? " destaque" : "") + '" data-assinatura="' + esc(c.assinatura) + '" tabindex="0">'
|
||||
+ "<td><b>" + txt(c.cliente) + "</b>"
|
||||
+ (c.teste ? ' <span class="selo neutro">teste</span>' : "")
|
||||
+ (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "")
|
||||
+ '<span class="sub">' + (c.tenant
|
||||
? '<a href="' + esc(c.endereco) + '" target="_blank" rel="noopener noreferrer">' + esc(c.tenant) + "</a>"
|
||||
: "sem tenant")
|
||||
@@ -3750,9 +4179,9 @@ data:
|
||||
+ "<td>" + etiquetaPlano(c) + "</td>"
|
||||
+ "<td>" + selo(c.estado, c.tenant) + '<span class="sub">' + (c.diasDeCliente !== null && c.diasDeCliente !== undefined
|
||||
? "cliente há " + esc(c.diasDeCliente) + " dia(s)" : "") + "</span></td>"
|
||||
+ '<td class="num">' + reais(c.valorMensal) + diferencaDeTabela(c) + "</td>"
|
||||
+ '<td class="num">' + (f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + proximaCobranca(f) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? '<span class="ajuda">sem cobrança</span>' : reais(c.valorMensal) + diferencaDeTabela(c)) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? "—" : f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + (c.demonstracao ? '<span class="ajuda">—</span>' : proximaCobranca(f)) + "</td>"
|
||||
+ "<td>" + usoCurto(c.uso, c.usoDeDentro) + "</td>"
|
||||
+ "<td>" + seloSubconta(c.subconta) + "</td>"
|
||||
+ "</tr>";
|
||||
@@ -3778,7 +4207,9 @@ data:
|
||||
+ esc(a.titulo) + '</b><span class="sub">' + esc(a.detalhe) + "</span></div></li>";
|
||||
}).join("");
|
||||
|
||||
var extrato = f.disponivel
|
||||
var extrato = c.demonstracao
|
||||
? '<p class="ajuda">Tenant de demonstração: não há assinatura nem cobrança no Asaas.</p>'
|
||||
: f.disponivel
|
||||
? ((f.cobrancas || []).length
|
||||
? '<table class="tabela"><thead><tr><th>Vencimento</th><th>Pago em</th><th>Forma</th><th class="num">Valor</th><th class="num">Líquido</th><th>Situação</th><th></th></tr></thead><tbody>'
|
||||
+ f.cobrancas.map(function (cb) {
|
||||
@@ -3799,7 +4230,7 @@ data:
|
||||
return '<header class="ficha-topo ' + classePlano(c.plano) + '">'
|
||||
+ '<span class="etiqueta ' + classePlano(c.plano) + '">Plano ' + esc(nomePlano(c.plano))
|
||||
+ (c.ciclo === "ANUAL" ? " · anual" : " · mensal") + "</span>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + "</h2>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "") + "</h2>"
|
||||
+ '<p class="sub">' + (c.escolinha && c.escolinha !== c.cliente ? esc(c.escolinha) + " · " : "")
|
||||
+ selo(c.estado, c.tenant) + (c.estadoDesde ? " desde " + dataHora(c.estadoDesde) : "") + "</p>"
|
||||
+ '<div class="atalhos">'
|
||||
@@ -3807,7 +4238,9 @@ data:
|
||||
+ (whatsapp(c.whatsapp) ? '<a class="btn claro" href="' + esc(whatsapp(c.whatsapp)) + '" target="_blank" rel="noopener noreferrer">WhatsApp</a>' : "")
|
||||
+ (c.email ? '<a class="btn claro" href="mailto:' + esc(c.email) + '">E-mail</a>' : "")
|
||||
+ (f.faturaVencida ? '<a class="btn claro" href="' + esc(linkSeguro(f.faturaVencida) || "#") + '" target="_blank" rel="noopener noreferrer">Fatura vencida</a>' : "")
|
||||
+ "</div></header>"
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="reenviar">Reenviar licença</button>'
|
||||
+ '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="trocar">Trocar chave</button>' : "")
|
||||
+ '</div><p id="licencaAviso" class="ajuda" aria-live="polite"></p></header>'
|
||||
+ (alertas ? '<ul class="lista-alertas">' + alertas + "</ul>" : "")
|
||||
|
||||
+ '<div class="grade-ficha">'
|
||||
@@ -3821,14 +4254,17 @@ data:
|
||||
+ '<section><h3>Assinatura</h3><dl>'
|
||||
+ item("Plano", esc(nomePlano(c.plano)) + " " + (c.ciclo === "ANUAL" ? "anual" : "mensal"))
|
||||
+ item("Valor do ciclo", reais(c.valorDoCiclo)) + item("Vale por mês", reais(c.valorMensal) + diferencaDeTabela(c))
|
||||
+ item("De onde vem o valor", c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas))
|
||||
+ item("De onde vem o valor", c.demonstracao ? "demonstração — sem cobrança"
|
||||
: c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas, c))
|
||||
+ item("Contratado em", dataHora(c.contratadoEm)) + item("Cliente há", c.diasDeCliente === null || c.diasDeCliente === undefined ? "—" : esc(c.diasDeCliente) + " dia(s)")
|
||||
+ item("Assinatura no Asaas", txt(c.assinatura)) + item("Cliente no Asaas", txt(c.clienteNoProvedor))
|
||||
+ "</dl></section>"
|
||||
|
||||
+ '<section><h3>Dinheiro</h3><dl>'
|
||||
+ (f.disponivel
|
||||
+ (c.demonstracao
|
||||
? item("Cobrança", "nenhuma — tenant de demonstração")
|
||||
: f.disponivel
|
||||
? item("Total pago", reais(f.totalPago)) + item("Líquido (após taxas)", reais(f.totalLiquido))
|
||||
+ item("Taxas pagas", reais(f.taxas))
|
||||
+ item("Último pagamento", f.ultimoPagamento ? data(f.ultimoPagamento.data) + " · " + reais(f.ultimoPagamento.valor) + " · " + txt(f.ultimoPagamento.forma) : "—")
|
||||
@@ -4325,6 +4761,55 @@ data:
|
||||
corpo.addEventListener("keydown", function (ev) { if (ev.key === "Enter") aoEscolher(ev); });
|
||||
$("alertas").addEventListener("click", aoEscolher);
|
||||
|
||||
/* ── licenca dos apps (dono, treinador, pais) ─────────────────────────────
|
||||
* Reenviar = mandar a MESMA chave da escola ao dono da compra (captacao 4.32+;
|
||||
* so a chave antiga, guardada so com hash, e trocada uma vez). Nada muda para
|
||||
* ninguem: sem confirmacao.
|
||||
* Trocar = revogar a chave e gerar outra. Desde os apps 1.0.4 (captacao 4.33)
|
||||
* o app pergunta a situacao da chave ao abrir: trocar DERRUBA os apps de todos
|
||||
* da escola (dono, treinadores, pais) em ate 15 min de uso, e cada um precisa
|
||||
* da chave nova. Por isso pede o nome do tenant digitado. */
|
||||
var API_LICENCA = "/api/plataforma/painel/licencas/";
|
||||
|
||||
function acaoDaLicenca(tenant, acao, aviso) {
|
||||
if (!tokenAtual) return Promise.resolve();
|
||||
if (acao === "trocar") {
|
||||
var digitado = window.prompt("TROCAR a chave de " + tenant + "?\n\n"
|
||||
+ "A chave atual para de valer e os apps de TODOS da escola (dono, treinadores e pais) saem da conta "
|
||||
+ "em até 15 minutos de uso; cada um vai precisar da chave nova, que vai por e-mail ao dono da compra.\n"
|
||||
+ "Use só se a chave vazou.\n\nPara confirmar, digite o nome do tenant (" + tenant + "):");
|
||||
if (digitado === null) return Promise.resolve();
|
||||
if (digitado.trim() !== tenant) {
|
||||
aviso.textContent = "Nada foi trocado: o nome digitado não confere.";
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
aviso.textContent = "Enviando…";
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/" + acao, {
|
||||
method: "POST", headers: { "x-atm-admin-token": tokenAtual, "Content-Type": "application/json" }, body: "{}"
|
||||
}).then(function (r) {
|
||||
if (r.status === 401) throw new Error("O token expirou. Saia e entre de novo no painel.");
|
||||
return r.json().then(function (d) {
|
||||
if (!r.ok) throw new Error((d && d.detail) || ("O servidor recusou (" + r.status + ")."));
|
||||
return d;
|
||||
});
|
||||
}).then(function (d) {
|
||||
var oQue = acao === "trocar" ? "Chave NOVA" : "Chave da escola";
|
||||
aviso.textContent = oQue + (d.emailEnviado ? " enviada para " : " pronta; o e-mail vai em instantes para ")
|
||||
+ (d.destino || "o dono da compra") + " (termina em " + d.finalDaChave + ").";
|
||||
}).catch(function (e) {
|
||||
aviso.textContent = e && e.message && e.message !== "Failed to fetch" ? e.message : "Sem conexão com o servidor.";
|
||||
});
|
||||
}
|
||||
|
||||
$("fichaCorpo").addEventListener("click", function (ev) {
|
||||
var b = ev.target.closest ? ev.target.closest("[data-licenca]") : null;
|
||||
if (!b) return;
|
||||
b.disabled = true;
|
||||
acaoDaLicenca(b.getAttribute("data-licenca"), b.getAttribute("data-acao") || "reenviar", $("licencaAviso"))
|
||||
.then(function () { b.disabled = false; });
|
||||
});
|
||||
|
||||
$("fecharFicha").addEventListener("click", fecharFicha);
|
||||
$("ficha").addEventListener("click", function (ev) { if (ev.target === $("ficha")) fecharFicha(); });
|
||||
document.addEventListener("keydown", function (ev) { if (ev.key === "Escape") fecharFicha(); });
|
||||
|
||||
@@ -370,6 +370,332 @@ data:
|
||||
});
|
||||
})();
|
||||
|
||||
convite.css: |
|
||||
/* Convite para competição: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). */
|
||||
main { max-width: 38rem; }
|
||||
.explica { color: var(--muted); }
|
||||
form { display: grid; gap: 1rem; }
|
||||
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
|
||||
legend { font-weight: 700; padding: 0 .4rem; }
|
||||
label { display: grid; gap: .2rem; font-size: .92rem; font-weight: 600; }
|
||||
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
|
||||
input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
|
||||
.linha-jogador { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
|
||||
.linha-jogador button { padding: .4rem .6rem; }
|
||||
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
|
||||
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
|
||||
button:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.erro { color: #b42318; margin: 0; font-weight: 600; }
|
||||
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: 1rem 1.2rem; }
|
||||
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
|
||||
|
||||
convite.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, nofollow">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Convite para competição — Athletic Map</title>
|
||||
<link rel="stylesheet" href="/publico/_estilo.css">
|
||||
<link rel="stylesheet" href="/publico/convite.css?v=1">
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="marca">Athletic <span>Map</span></div></header>
|
||||
<main>
|
||||
<h1 id="titulo">Convite para competição</h1>
|
||||
<p id="situacao" role="status" aria-live="polite">Abrindo o convite…</p>
|
||||
|
||||
<form id="formulario" hidden novalidate>
|
||||
<p class="explica">
|
||||
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
|
||||
A organização confere e aprova antes do time entrar na competição.
|
||||
</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Seu time</legend>
|
||||
<label>Nome do time *<input id="nome" maxlength="120" autocomplete="off" required></label>
|
||||
<label>Escola ou clube<input id="escola" maxlength="160" autocomplete="organization"></label>
|
||||
<label>Cidade<input id="cidade" maxlength="120" autocomplete="address-level2"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Responsável pelo time</legend>
|
||||
<label>Nome<input id="contatoNome" maxlength="120" autocomplete="name"></label>
|
||||
<label>Telefone<input id="contatoTelefone" maxlength="40" autocomplete="tel" inputmode="tel"></label>
|
||||
<label>E-mail<input id="contatoEmail" type="email" maxlength="160" autocomplete="email"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Jogadores</legend>
|
||||
<div id="jogadores"></div>
|
||||
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
|
||||
</fieldset>
|
||||
|
||||
<p id="erro" class="erro" role="alert" hidden></p>
|
||||
<button type="submit" id="enviar">Enviar time</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/publico/desafio.js?v=1"></script>
|
||||
<script src="/publico/convite.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
convite.js: |
|
||||
/* ===========================================================================
|
||||
* Convite para competição — a página que a OUTRA escola abre pelo link.
|
||||
*
|
||||
* Sem login: o token no endereço é o único segredo. A página pergunta ao
|
||||
* servidor a que o convite se refere e, se ele vale, deixa a pessoa mandar o
|
||||
* time com os jogadores (nome e camisa). O time chega PENDENTE: quem organiza
|
||||
* confere e aprova.
|
||||
*
|
||||
* Antes de enviar, a verificação anti-robô (desafio.js): pede um desafio ao
|
||||
* servidor, resolve no navegador ("Verificando…") e manda a prova junto com o
|
||||
* time, em `prova`. Um desafio por tentativa: ele é de uso único.
|
||||
*
|
||||
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
|
||||
* HTML. O script é externo porque a CSP é `script-src 'self'`.
|
||||
* =========================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var global = window;
|
||||
|
||||
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
|
||||
var MAX_JOGADORES = 60;
|
||||
|
||||
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
|
||||
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
|
||||
|
||||
var titulo = document.getElementById("titulo");
|
||||
var situacao = document.getElementById("situacao");
|
||||
var formulario = document.getElementById("formulario");
|
||||
var listaJogadores = document.getElementById("jogadores");
|
||||
var erro = document.getElementById("erro");
|
||||
var enviar = document.getElementById("enviar");
|
||||
var ROTULO_ENVIAR = enviar.textContent;
|
||||
|
||||
function recusar(texto) {
|
||||
situacao.textContent = texto;
|
||||
situacao.className = "invalido";
|
||||
formulario.hidden = true;
|
||||
}
|
||||
|
||||
function mostrarErro(texto) {
|
||||
erro.textContent = texto;
|
||||
erro.hidden = !texto;
|
||||
}
|
||||
|
||||
function linhaDeJogador() {
|
||||
var linha = document.createElement("div");
|
||||
linha.className = "linha-jogador";
|
||||
|
||||
var nome = document.createElement("input");
|
||||
nome.placeholder = "Nome do jogador";
|
||||
nome.maxLength = 120;
|
||||
nome.setAttribute("aria-label", "Nome do jogador");
|
||||
nome.setAttribute("data-campo", "nome");
|
||||
|
||||
var camisa = document.createElement("input");
|
||||
camisa.type = "number";
|
||||
camisa.min = "0";
|
||||
camisa.max = "999";
|
||||
camisa.placeholder = "nº";
|
||||
camisa.setAttribute("aria-label", "Número da camisa");
|
||||
camisa.setAttribute("data-campo", "camisa");
|
||||
|
||||
var tirar = document.createElement("button");
|
||||
tirar.type = "button";
|
||||
tirar.className = "secundario";
|
||||
tirar.textContent = "×";
|
||||
tirar.setAttribute("aria-label", "Remover jogador");
|
||||
tirar.addEventListener("click", function () { linha.remove(); });
|
||||
|
||||
linha.appendChild(nome);
|
||||
linha.appendChild(camisa);
|
||||
linha.appendChild(tirar);
|
||||
listaJogadores.appendChild(linha);
|
||||
return linha;
|
||||
}
|
||||
|
||||
function valor(id) {
|
||||
var v = document.getElementById(id).value.trim();
|
||||
return v === "" ? null : v;
|
||||
}
|
||||
|
||||
function montarCorpo() {
|
||||
var jogadores = [];
|
||||
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
|
||||
for (var i = 0; i < linhas.length; i++) {
|
||||
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
|
||||
if (!nome) continue;
|
||||
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
|
||||
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
|
||||
}
|
||||
return {
|
||||
nome: valor("nome"),
|
||||
escola: valor("escola"),
|
||||
cidade: valor("cidade"),
|
||||
contatoNome: valor("contatoNome"),
|
||||
contatoTelefone: valor("contatoTelefone"),
|
||||
contatoEmail: valor("contatoEmail"),
|
||||
jogadores: jogadores
|
||||
};
|
||||
}
|
||||
|
||||
function lerResposta(r) {
|
||||
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
|
||||
}
|
||||
|
||||
function aoEnviar(ev) {
|
||||
ev.preventDefault();
|
||||
mostrarErro("");
|
||||
var corpo = montarCorpo();
|
||||
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
|
||||
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
|
||||
enviar.disabled = true;
|
||||
enviar.textContent = "Verificando…";
|
||||
enviar.setAttribute("aria-busy", "true");
|
||||
|
||||
function voltar(texto) {
|
||||
enviar.disabled = false;
|
||||
enviar.textContent = ROTULO_ENVIAR;
|
||||
enviar.removeAttribute("aria-busy");
|
||||
mostrarErro(texto);
|
||||
}
|
||||
|
||||
fetch(base + "/desafio").then(lerResposta).then(function (r) {
|
||||
if (r.status !== 200) {
|
||||
var e = new Error("recusado");
|
||||
e.detalhe = (r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.";
|
||||
throw e;
|
||||
}
|
||||
if (!global.AtmDesafio) throw new Error("sem-crypto");
|
||||
return global.AtmDesafio.resolver(r.corpo);
|
||||
}).then(function (prova) {
|
||||
corpo.prova = prova;
|
||||
enviar.textContent = "Enviando…";
|
||||
return fetch(base + "/times", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(corpo)
|
||||
}).then(lerResposta);
|
||||
}).then(function (r) {
|
||||
if (r.status >= 200 && r.status < 300) {
|
||||
formulario.hidden = true;
|
||||
titulo.textContent = "Time enviado";
|
||||
situacao.textContent = "Recebemos o time \"" + corpo.nome + "\". A organização vai conferir os dados e aprovar. Você já pode fechar esta página.";
|
||||
situacao.className = "ok";
|
||||
return;
|
||||
}
|
||||
voltar((r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.");
|
||||
}).catch(function (e) {
|
||||
if (e && e.detalhe) { voltar(e.detalhe); return; }
|
||||
if (e && (e.message === "sem-crypto" || e.message === "desafio-invalido" || e.message === "sem-solucao")) {
|
||||
voltar("Não foi possível fazer a verificação anti-robô neste navegador. Atualize a página ou use outro navegador.");
|
||||
return;
|
||||
}
|
||||
voltar("Sem conexão. Confira a internet e tente de novo.");
|
||||
});
|
||||
}
|
||||
|
||||
function iniciar() {
|
||||
if (!TOKEN.test(token)) { recusar("Este endereço de convite não é válido."); return; }
|
||||
fetch(base).then(lerResposta).then(function (r) {
|
||||
var info = r.corpo || {};
|
||||
if (!info.valido) {
|
||||
recusar(info.motivo ? "Não dá para usar este convite: " + info.motivo + "." : "Este convite não está disponível.");
|
||||
return;
|
||||
}
|
||||
titulo.textContent = "Convite: " + (info.competicao || "competição");
|
||||
situacao.textContent = (info.temporada ? "Temporada " + info.temporada + ". " : "")
|
||||
+ "Restam " + info.vagasRestantes + " vaga(s) neste convite.";
|
||||
situacao.className = "";
|
||||
formulario.hidden = false;
|
||||
for (var i = 0; i < 5; i++) linhaDeJogador();
|
||||
}).catch(function () {
|
||||
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("maisJogador").addEventListener("click", function () {
|
||||
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
|
||||
});
|
||||
formulario.addEventListener("submit", aoEnviar);
|
||||
iniciar();
|
||||
})();
|
||||
|
||||
desafio.js: |
|
||||
/* ===========================================================================
|
||||
* Verificação anti-robô das páginas públicas — prova de trabalho no estilo
|
||||
* ALTCHA, sem serviço de terceiro (nada de quem preenche sai do Athletic Map).
|
||||
*
|
||||
* O servidor manda { algoritmo: "SHA-256", desafio, sal, assinatura, maximo }.
|
||||
* Aqui se procura o número n (0..maximo) tal que sha256(sal + n) == desafio, e
|
||||
* devolve-se { numero, sal, assinatura } para ir junto no envio. Em média são
|
||||
* maximo/2 hashes: ~1–2 s num celular com maximo = 150 000.
|
||||
*
|
||||
* Usa crypto.subtle (só existe em HTTPS — e as páginas públicas são HTTPS).
|
||||
* Os hashes saem em lotes paralelos: um `await` por hash custaria mais que o
|
||||
* próprio hash. Script externo porque a CSP é `script-src 'self'`.
|
||||
*
|
||||
* O teste do servidor é DesafioDoConvite (servico-campeonato).
|
||||
* =========================================================================== */
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var LOTE = 1000;
|
||||
var HEX = /^[0-9a-f]{64}$/;
|
||||
|
||||
function bytesDoHex(h) {
|
||||
var b = new Uint8Array(h.length / 2);
|
||||
for (var i = 0; i < b.length; i++) b[i] = parseInt(h.substr(i * 2, 2), 16);
|
||||
return b;
|
||||
}
|
||||
|
||||
function iguais(buf, alvo) {
|
||||
var a = new Uint8Array(buf);
|
||||
if (a.length !== alvo.length) return false;
|
||||
for (var i = 0; i < a.length; i++) if (a[i] !== alvo[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{algoritmo:string, desafio:string, sal:string, assinatura:string, maximo:number}} d
|
||||
* @returns {Promise<{numero:number, sal:string, assinatura:string}>}
|
||||
* rejeita com Error("sem-crypto"), Error("desafio-invalido") ou Error("sem-solucao")
|
||||
*/
|
||||
function resolver(d) {
|
||||
var subtle = global.crypto && global.crypto.subtle;
|
||||
var Codificador = global.TextEncoder;
|
||||
if (!subtle || !Codificador) return Promise.reject(new Error("sem-crypto"));
|
||||
if (!d || d.algoritmo !== "SHA-256" || !HEX.test(d.desafio || "") || typeof d.sal !== "string"
|
||||
|| typeof d.maximo !== "number" || d.maximo < 0 || d.maximo > 5000000) {
|
||||
return Promise.reject(new Error("desafio-invalido"));
|
||||
}
|
||||
var alvo = bytesDoHex(d.desafio);
|
||||
var cod = new Codificador();
|
||||
|
||||
function lote(inicio) {
|
||||
if (inicio > d.maximo) return Promise.reject(new Error("sem-solucao"));
|
||||
var fim = Math.min(inicio + LOTE - 1, d.maximo);
|
||||
var hashes = [];
|
||||
for (var n = inicio; n <= fim; n++) hashes.push(subtle.digest("SHA-256", cod.encode(d.sal + n)));
|
||||
return Promise.all(hashes).then(function (hs) {
|
||||
for (var i = 0; i < hs.length; i++) {
|
||||
if (iguais(hs[i], alvo)) return { numero: inicio + i, sal: d.sal, assinatura: d.assinatura };
|
||||
}
|
||||
return lote(fim + 1);
|
||||
});
|
||||
}
|
||||
return lote(0);
|
||||
}
|
||||
|
||||
global.AtmDesafio = { resolver: resolver };
|
||||
})(window);
|
||||
|
||||
fontes--fontes.css: |
|
||||
/* ============================================================
|
||||
* As fontes da landing page, servidas POR NOS.
|
||||
@@ -609,9 +935,7 @@ data:
|
||||
|
||||
/* marca */
|
||||
.logo{display:inline-flex; align-items:center; gap:11px; font-weight:700; font-size:1.22rem; letter-spacing:-.02em; color:var(--ink); text-decoration:none;}
|
||||
.mark{display:inline-flex; gap:3px; align-items:flex-end;}
|
||||
.mark i{display:block; width:5px; border-radius:2px; background:var(--laranja); transform:skewX(-14deg);}
|
||||
.mark i:nth-child(1){height:13px} .mark i:nth-child(2){height:19px} .mark i:nth-child(3){height:25px}
|
||||
.logo img{display:block; height:37px; width:auto;}
|
||||
|
||||
.topbar{background:var(--surface); border-bottom:1px solid var(--line);}
|
||||
.topbar .wrap{display:flex; align-items:center; justify-content:space-between; gap:16px; padding-block:14px;}
|
||||
@@ -814,7 +1138,7 @@ data:
|
||||
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="logo" href="#"><span class="mark"><i></i><i></i><i></i></span>Athletic Map</a>
|
||||
<a class="logo" href="#" aria-label="Athletic Map"><img src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37"></a>
|
||||
<a class="zap" id="topZap" href="#form">Falar com o time →</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1698,6 +2022,20 @@ data:
|
||||
.termos-uso-caixa input { margin-right: 6px; }
|
||||
.termos-uso-aceite { margin: 10px 0; }
|
||||
|
||||
/* ── Abas (2026-09-29) — mesmo desenho do painel do dono ───────────────────── */
|
||||
.abas { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin: 4px 0 18px; overflow-x: auto; }
|
||||
.aba {
|
||||
background: none; border: 0; border-bottom: 3px solid transparent; padding: 10px 16px;
|
||||
font: inherit; font-weight: 600; color: var(--muted); cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.aba:hover { color: var(--navy); }
|
||||
.aba.ativa { color: var(--navy); border-bottom-color: var(--orange); }
|
||||
.aba .badge {
|
||||
display: inline-block; min-width: 8px; height: 8px; margin-left: 6px; border-radius: 999px;
|
||||
background: var(--orange); vertical-align: middle;
|
||||
}
|
||||
.aba .badge.num { min-width: 18px; height: auto; padding: 0 6px; font-size: 12px; color: #fff; line-height: 18px; }
|
||||
|
||||
minha-conta.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
@@ -1722,7 +2060,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.88">
|
||||
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.90">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1765,23 +2103,56 @@ 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>
|
||||
<!-- Abas (2026-09-29, pedido do dono): a pagina empilhava visao geral, Termos,
|
||||
escolas ativas e canceladas numa coluna so. Mesmo padrao do painel do dono. -->
|
||||
<nav class="abas" role="tablist" aria-label="Seções da conta">
|
||||
<button type="button" class="aba ativa" data-aba="escolas" role="tab">Minhas escolas</button>
|
||||
<button type="button" class="aba" data-aba="visao" role="tab">Visão geral</button>
|
||||
<button type="button" class="aba" data-aba="termos" role="tab">Termos de Uso <span id="termosBadge" class="badge oculto"></span></button>
|
||||
<button type="button" class="aba oculto" data-aba="canceladas" role="tab" id="abaCanceladas">Canceladas</button>
|
||||
</nav>
|
||||
|
||||
<div id="produtos" class="produtos"></div>
|
||||
<div id="aba-escolas" class="painel-aba">
|
||||
<div id="produtos" class="produtos"></div>
|
||||
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-visao" class="painel-aba 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="visaoVazia" class="cartao">
|
||||
<h2>Visão <span class="hl">geral</span></h2>
|
||||
<p class="ajuda">Os números aparecem aqui quando as suas escolinhas enviarem o primeiro resumo
|
||||
(uma vez por dia, de madrugada).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-termos" class="painel-aba oculto">
|
||||
<div id="termosDeUso"></div>
|
||||
<div id="termosEmDia" class="cartao oculto">
|
||||
<h2>Termos de Uso</h2>
|
||||
<p class="ajuda">Nada pendente: a versão vigente já foi aceita para todas as suas escolinhas.
|
||||
<a href="/termos" target="_blank" rel="noopener">Ler os Termos de Uso</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-canceladas" class="painel-aba oculto">
|
||||
<p class="ajuda">Assinaturas canceladas. Regularize o pagamento para voltar a entrar; os dados ficam
|
||||
guardados por 30 dias depois do cancelamento.</p>
|
||||
<div id="produtosCancelados" class="produtos"></div>
|
||||
</div>
|
||||
|
||||
<p class="rodape">
|
||||
@@ -1801,7 +2172,7 @@ data:
|
||||
|
||||
<footer class="direitos">© 2026 Athletic Map. Todos os direitos reservados.</footer>
|
||||
|
||||
<script src="/publico/minha-conta.js?v=2.88"></script>
|
||||
<script src="/publico/minha-conta.js?v=2.90"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1870,6 +2241,36 @@ data:
|
||||
var erroTexto = document.getElementById("erroTexto");
|
||||
var botaoSair = document.getElementById("sair");
|
||||
|
||||
/* ── ABAS (2026-09-29) ────────────────────────────────────────────────────────
|
||||
* Minhas escolas | Visao geral | Termos de Uso | Canceladas. A ultima aba aberta
|
||||
* fica no localStorage (conveniencia deste navegador; sem ela, "escolas"). */
|
||||
var ABA_CHAVE = "atm.conta.aba";
|
||||
function abrirAba(nome) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao || botao.classList.contains("oculto")) { nome = "escolas"; botao = document.querySelector('.aba[data-aba="escolas"]'); }
|
||||
if (!botao) return;
|
||||
document.querySelectorAll(".aba").forEach(function (b) { b.classList.toggle("ativa", b === botao); });
|
||||
document.querySelectorAll(".painel-aba").forEach(function (painel) {
|
||||
painel.classList.toggle("oculto", painel.id !== "aba-" + nome);
|
||||
});
|
||||
try { localStorage.setItem(ABA_CHAVE, nome); } catch (e) { /* aba privada */ }
|
||||
}
|
||||
function abaLembrada() { try { return localStorage.getItem(ABA_CHAVE) || "escolas"; } catch (e) { return "escolas"; } }
|
||||
/* Marca a aba: `pendente` acende a bolinha; `quantos` escreve o numero. */
|
||||
function marcarAba(nome, pendente, quantos) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao) return;
|
||||
var badge = botao.querySelector(".badge");
|
||||
if (badge) {
|
||||
badge.classList.toggle("oculto", !pendente && !quantos);
|
||||
badge.classList.toggle("num", !!quantos);
|
||||
badge.textContent = quantos ? String(quantos) : "";
|
||||
}
|
||||
}
|
||||
document.querySelectorAll(".aba").forEach(function (b) {
|
||||
b.addEventListener("click", function () { abrirAba(b.getAttribute("data-aba")); });
|
||||
});
|
||||
|
||||
function mostrar(qual) {
|
||||
[porta, carregando, conteudo, caixaErro].forEach(function (s) {
|
||||
if (s) s.classList.add("oculto");
|
||||
@@ -1988,14 +2389,18 @@ data:
|
||||
return n ? n.charAt(0).toUpperCase() + n.slice(1) : "—";
|
||||
}
|
||||
|
||||
/* Tenant de DEMONSTRACAO do dono: o servidor marca (`demonstracao: true`). O
|
||||
cartao diz isso ao lado da situacao, e nao ganha "Trocar de plano" nem
|
||||
"Modulos avulsos" — nao ha cobranca, e o servidor recusaria de qualquer jeito. */
|
||||
function cartao(p) {
|
||||
var s = SITUACAO[p.situacao] || ["neutro", String(p.situacao || "").toLowerCase()];
|
||||
var podeEntrar = p.endereco && p.situacao === "ATIVA";
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + '"'
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + (p.demonstracao ? " demonstracao" : "") + '"'
|
||||
+ (p.tenant ? ' data-tenant="' + escapar(p.tenant) + '"' : "") + ">"
|
||||
+ '<span class="faixa-plano">Plano ' + escapar(nomeDoPlano(p.plano)) + "</span>"
|
||||
+ "<h2>" + escapar(p.escolinha || "Sua escolinha") + "</h2>"
|
||||
+ '<span class="selo ' + s[0] + '">' + s[1] + "</span>"
|
||||
+ (p.demonstracao ? '<span class="selo neutro">demonstração — sem cobrança</span>' : "")
|
||||
+ (podeEntrar
|
||||
? '<a class="btn" href="' + escapar(p.endereco) + '">Entrar →</a>'
|
||||
: '<p class="ajuda">' + (p.situacao === "PREPARANDO"
|
||||
@@ -2014,10 +2419,22 @@ data:
|
||||
.then(function (lista) {
|
||||
enderecos = {};
|
||||
(lista || []).forEach(function (p) { if (p.tenant) enderecos[p.tenant] = p.endereco; });
|
||||
produtos.innerHTML = (lista || []).map(cartao).join("");
|
||||
nenhum.classList.toggle("oculto", (lista || []).length > 0);
|
||||
var ativas = (lista || []).filter(function (p) { return p.situacao !== "CANCELADA"; });
|
||||
var canceladas = (lista || []).filter(function (p) { return p.situacao === "CANCELADA"; });
|
||||
produtos.innerHTML = ativas.map(cartao).join("");
|
||||
var caixaCanceladas = document.getElementById("produtosCancelados");
|
||||
if (caixaCanceladas) caixaCanceladas.innerHTML = canceladas.map(cartao).join("");
|
||||
var abaCanceladas = document.getElementById("abaCanceladas");
|
||||
if (abaCanceladas) {
|
||||
abaCanceladas.classList.toggle("oculto", canceladas.length === 0);
|
||||
abaCanceladas.textContent = "Canceladas" + (canceladas.length ? " (" + canceladas.length + ")" : "");
|
||||
}
|
||||
nenhum.classList.toggle("oculto", ativas.length > 0);
|
||||
mostrar(conteudo);
|
||||
(lista || []).forEach(function (p) { if (p.tenant && p.situacao === "ATIVA") acrescentarBotaoDePlano(p.tenant); });
|
||||
abrirAba(abaLembrada());
|
||||
(lista || []).forEach(function (p) {
|
||||
if (p.tenant && p.situacao === "ATIVA" && !p.demonstracao) acrescentarBotaoDePlano(p.tenant);
|
||||
});
|
||||
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas(), carregarVisao(),
|
||||
carregarTermosDeUso()]);
|
||||
});
|
||||
@@ -2323,8 +2740,10 @@ data:
|
||||
.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; });
|
||||
var emDia = document.getElementById("termosEmDia");
|
||||
var faltam = (d && d.versao && d.escolinhas) ? d.escolinhas.filter(function (e) { return !e.aceito; }) : [];
|
||||
if (emDia) emDia.classList.toggle("oculto", faltam.length > 0);
|
||||
marcarAba("termos", faltam.length > 0);
|
||||
if (!faltam.length) return;
|
||||
var aviso = document.createElement("section");
|
||||
aviso.id = "avisoTermos";
|
||||
@@ -2335,7 +2754,8 @@ data:
|
||||
+ ". 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);
|
||||
var caixaTermos = document.getElementById("termosDeUso");
|
||||
if (caixaTermos) caixaTermos.appendChild(aviso); else produtos.parentNode.insertBefore(aviso, produtos);
|
||||
aviso.querySelector("button").addEventListener("click", function () {
|
||||
aceitarTermosDeUso(aviso, d.versao);
|
||||
});
|
||||
@@ -2588,7 +3008,9 @@ data:
|
||||
.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 vazia = document.getElementById("visaoVazia");
|
||||
if (!tabela) { caixa.classList.add("oculto"); if (vazia) vazia.classList.remove("oculto"); return; }
|
||||
if (vazia) vazia.classList.add("oculto");
|
||||
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.";
|
||||
@@ -3171,7 +3593,7 @@ data:
|
||||
<option value="NASCENDO">Nascendo</option>
|
||||
</select>
|
||||
<label class="filtro"><input type="checkbox" id="soAtencao"> só com alerta</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes e demonstrações</label>
|
||||
</div>
|
||||
<div class="tabela-rolagem">
|
||||
<table class="tabela clientes">
|
||||
@@ -3334,7 +3756,7 @@ data:
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/publico/painel.js?v=2.86"></script>
|
||||
<script src="/publico/painel.js?v=2.89"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3630,9 +4052,13 @@ data:
|
||||
bloco("Recebido desde o início", reaisCurto(i.recebidoTotal),
|
||||
i.clientesSemDadoDoAsaas > 0 ? i.clientesSemDadoDoAsaas + " sem resposta do Asaas" : "extrato do Asaas", "")
|
||||
].join("");
|
||||
// Teste e demonstracao aparecem na lista, mas nao nos numeros — e a tela diz.
|
||||
var foraDosNumeros = [];
|
||||
if (i.contasDeTeste > 0) foraDosNumeros.push(i.contasDeTeste + " de teste");
|
||||
if (i.demonstracoes > 0) foraDosNumeros.push(i.demonstracoes + " de demonstração");
|
||||
$("kpiClientes").innerHTML = [
|
||||
bloco("Clientes ativos", esc(i.clientesAtivos),
|
||||
i.contasDeTeste > 0 ? "+ " + i.contasDeTeste + " de teste (fora dos números)" : "", "ok"),
|
||||
foraDosNumeros.length ? "+ " + foraDosNumeros.join(", ") + " (fora dos números)" : "", "ok"),
|
||||
bloco("Novos no mês", esc(i.novosNoMes), comparacao(i.novosNoMes, i.novosMesAnterior), ""),
|
||||
bloco("Leads no mês", esc(i.leadsNoMes), comparacao(i.leadsNoMes, i.leadsMesAnterior), ""),
|
||||
bloco("Conversão", pct(i.conversao), "de lead para cliente pagante", ""),
|
||||
@@ -3671,7 +4097,7 @@ data:
|
||||
if (plano && c.plano !== plano) return false;
|
||||
if (estado && (c.estado || "NASCENDO") !== estado) return false;
|
||||
if (soAtencao && !(c.alertas || []).length) return false;
|
||||
if (semTeste && c.teste) return false;
|
||||
if (semTeste && (c.teste || c.demonstracao)) return false;
|
||||
if (busca) {
|
||||
var alvo = [c.cliente, c.escolinha, c.email, c.cidade, c.tenant, c.whatsapp, c.documento]
|
||||
.join(" ").toLowerCase();
|
||||
@@ -3701,7 +4127,9 @@ data:
|
||||
}
|
||||
|
||||
var SITUACAO_ASAAS = { ACTIVE: "ativa", INACTIVE: "encerrada", EXPIRED: "expirada" };
|
||||
function assinaturaNoAsaas(a) {
|
||||
/* A demonstracao nao existe no Asaas DE PROPOSITO: nao e "nao existe" em vermelho. */
|
||||
function assinaturaNoAsaas(a, c) {
|
||||
if (c && c.demonstracao) return "demonstração — sem cobrança";
|
||||
if (!a || !a.disponivel) return "indisponível agora";
|
||||
if (!a.existe) return '<b class="vermelho">não existe</b>';
|
||||
var s = SITUACAO_ASAAS[a.situacao] || txt(a.situacao);
|
||||
@@ -3738,10 +4166,11 @@ data:
|
||||
var gravidade = (c.alertas || []).reduce(function (g, a) {
|
||||
return a.gravidade === "critico" ? "critico" : (g === "critico" ? g : (a.gravidade === "alerta" ? "alerta" : g));
|
||||
}, "");
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste ? " teste" : "")
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste || c.demonstracao ? " teste" : "")
|
||||
+ (!c.tenant ? " destaque" : "") + '" data-assinatura="' + esc(c.assinatura) + '" tabindex="0">'
|
||||
+ "<td><b>" + txt(c.cliente) + "</b>"
|
||||
+ (c.teste ? ' <span class="selo neutro">teste</span>' : "")
|
||||
+ (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "")
|
||||
+ '<span class="sub">' + (c.tenant
|
||||
? '<a href="' + esc(c.endereco) + '" target="_blank" rel="noopener noreferrer">' + esc(c.tenant) + "</a>"
|
||||
: "sem tenant")
|
||||
@@ -3750,9 +4179,9 @@ data:
|
||||
+ "<td>" + etiquetaPlano(c) + "</td>"
|
||||
+ "<td>" + selo(c.estado, c.tenant) + '<span class="sub">' + (c.diasDeCliente !== null && c.diasDeCliente !== undefined
|
||||
? "cliente há " + esc(c.diasDeCliente) + " dia(s)" : "") + "</span></td>"
|
||||
+ '<td class="num">' + reais(c.valorMensal) + diferencaDeTabela(c) + "</td>"
|
||||
+ '<td class="num">' + (f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + proximaCobranca(f) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? '<span class="ajuda">sem cobrança</span>' : reais(c.valorMensal) + diferencaDeTabela(c)) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? "—" : f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + (c.demonstracao ? '<span class="ajuda">—</span>' : proximaCobranca(f)) + "</td>"
|
||||
+ "<td>" + usoCurto(c.uso, c.usoDeDentro) + "</td>"
|
||||
+ "<td>" + seloSubconta(c.subconta) + "</td>"
|
||||
+ "</tr>";
|
||||
@@ -3778,7 +4207,9 @@ data:
|
||||
+ esc(a.titulo) + '</b><span class="sub">' + esc(a.detalhe) + "</span></div></li>";
|
||||
}).join("");
|
||||
|
||||
var extrato = f.disponivel
|
||||
var extrato = c.demonstracao
|
||||
? '<p class="ajuda">Tenant de demonstração: não há assinatura nem cobrança no Asaas.</p>'
|
||||
: f.disponivel
|
||||
? ((f.cobrancas || []).length
|
||||
? '<table class="tabela"><thead><tr><th>Vencimento</th><th>Pago em</th><th>Forma</th><th class="num">Valor</th><th class="num">Líquido</th><th>Situação</th><th></th></tr></thead><tbody>'
|
||||
+ f.cobrancas.map(function (cb) {
|
||||
@@ -3799,7 +4230,7 @@ data:
|
||||
return '<header class="ficha-topo ' + classePlano(c.plano) + '">'
|
||||
+ '<span class="etiqueta ' + classePlano(c.plano) + '">Plano ' + esc(nomePlano(c.plano))
|
||||
+ (c.ciclo === "ANUAL" ? " · anual" : " · mensal") + "</span>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + "</h2>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "") + "</h2>"
|
||||
+ '<p class="sub">' + (c.escolinha && c.escolinha !== c.cliente ? esc(c.escolinha) + " · " : "")
|
||||
+ selo(c.estado, c.tenant) + (c.estadoDesde ? " desde " + dataHora(c.estadoDesde) : "") + "</p>"
|
||||
+ '<div class="atalhos">'
|
||||
@@ -3807,7 +4238,9 @@ data:
|
||||
+ (whatsapp(c.whatsapp) ? '<a class="btn claro" href="' + esc(whatsapp(c.whatsapp)) + '" target="_blank" rel="noopener noreferrer">WhatsApp</a>' : "")
|
||||
+ (c.email ? '<a class="btn claro" href="mailto:' + esc(c.email) + '">E-mail</a>' : "")
|
||||
+ (f.faturaVencida ? '<a class="btn claro" href="' + esc(linkSeguro(f.faturaVencida) || "#") + '" target="_blank" rel="noopener noreferrer">Fatura vencida</a>' : "")
|
||||
+ "</div></header>"
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="reenviar">Reenviar licença</button>'
|
||||
+ '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="trocar">Trocar chave</button>' : "")
|
||||
+ '</div><p id="licencaAviso" class="ajuda" aria-live="polite"></p></header>'
|
||||
+ (alertas ? '<ul class="lista-alertas">' + alertas + "</ul>" : "")
|
||||
|
||||
+ '<div class="grade-ficha">'
|
||||
@@ -3821,14 +4254,17 @@ data:
|
||||
+ '<section><h3>Assinatura</h3><dl>'
|
||||
+ item("Plano", esc(nomePlano(c.plano)) + " " + (c.ciclo === "ANUAL" ? "anual" : "mensal"))
|
||||
+ item("Valor do ciclo", reais(c.valorDoCiclo)) + item("Vale por mês", reais(c.valorMensal) + diferencaDeTabela(c))
|
||||
+ item("De onde vem o valor", c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas))
|
||||
+ item("De onde vem o valor", c.demonstracao ? "demonstração — sem cobrança"
|
||||
: c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas, c))
|
||||
+ item("Contratado em", dataHora(c.contratadoEm)) + item("Cliente há", c.diasDeCliente === null || c.diasDeCliente === undefined ? "—" : esc(c.diasDeCliente) + " dia(s)")
|
||||
+ item("Assinatura no Asaas", txt(c.assinatura)) + item("Cliente no Asaas", txt(c.clienteNoProvedor))
|
||||
+ "</dl></section>"
|
||||
|
||||
+ '<section><h3>Dinheiro</h3><dl>'
|
||||
+ (f.disponivel
|
||||
+ (c.demonstracao
|
||||
? item("Cobrança", "nenhuma — tenant de demonstração")
|
||||
: f.disponivel
|
||||
? item("Total pago", reais(f.totalPago)) + item("Líquido (após taxas)", reais(f.totalLiquido))
|
||||
+ item("Taxas pagas", reais(f.taxas))
|
||||
+ item("Último pagamento", f.ultimoPagamento ? data(f.ultimoPagamento.data) + " · " + reais(f.ultimoPagamento.valor) + " · " + txt(f.ultimoPagamento.forma) : "—")
|
||||
@@ -4325,6 +4761,55 @@ data:
|
||||
corpo.addEventListener("keydown", function (ev) { if (ev.key === "Enter") aoEscolher(ev); });
|
||||
$("alertas").addEventListener("click", aoEscolher);
|
||||
|
||||
/* ── licenca dos apps (dono, treinador, pais) ─────────────────────────────
|
||||
* Reenviar = mandar a MESMA chave da escola ao dono da compra (captacao 4.32+;
|
||||
* so a chave antiga, guardada so com hash, e trocada uma vez). Nada muda para
|
||||
* ninguem: sem confirmacao.
|
||||
* Trocar = revogar a chave e gerar outra. Desde os apps 1.0.4 (captacao 4.33)
|
||||
* o app pergunta a situacao da chave ao abrir: trocar DERRUBA os apps de todos
|
||||
* da escola (dono, treinadores, pais) em ate 15 min de uso, e cada um precisa
|
||||
* da chave nova. Por isso pede o nome do tenant digitado. */
|
||||
var API_LICENCA = "/api/plataforma/painel/licencas/";
|
||||
|
||||
function acaoDaLicenca(tenant, acao, aviso) {
|
||||
if (!tokenAtual) return Promise.resolve();
|
||||
if (acao === "trocar") {
|
||||
var digitado = window.prompt("TROCAR a chave de " + tenant + "?\n\n"
|
||||
+ "A chave atual para de valer e os apps de TODOS da escola (dono, treinadores e pais) saem da conta "
|
||||
+ "em até 15 minutos de uso; cada um vai precisar da chave nova, que vai por e-mail ao dono da compra.\n"
|
||||
+ "Use só se a chave vazou.\n\nPara confirmar, digite o nome do tenant (" + tenant + "):");
|
||||
if (digitado === null) return Promise.resolve();
|
||||
if (digitado.trim() !== tenant) {
|
||||
aviso.textContent = "Nada foi trocado: o nome digitado não confere.";
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
aviso.textContent = "Enviando…";
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/" + acao, {
|
||||
method: "POST", headers: { "x-atm-admin-token": tokenAtual, "Content-Type": "application/json" }, body: "{}"
|
||||
}).then(function (r) {
|
||||
if (r.status === 401) throw new Error("O token expirou. Saia e entre de novo no painel.");
|
||||
return r.json().then(function (d) {
|
||||
if (!r.ok) throw new Error((d && d.detail) || ("O servidor recusou (" + r.status + ")."));
|
||||
return d;
|
||||
});
|
||||
}).then(function (d) {
|
||||
var oQue = acao === "trocar" ? "Chave NOVA" : "Chave da escola";
|
||||
aviso.textContent = oQue + (d.emailEnviado ? " enviada para " : " pronta; o e-mail vai em instantes para ")
|
||||
+ (d.destino || "o dono da compra") + " (termina em " + d.finalDaChave + ").";
|
||||
}).catch(function (e) {
|
||||
aviso.textContent = e && e.message && e.message !== "Failed to fetch" ? e.message : "Sem conexão com o servidor.";
|
||||
});
|
||||
}
|
||||
|
||||
$("fichaCorpo").addEventListener("click", function (ev) {
|
||||
var b = ev.target.closest ? ev.target.closest("[data-licenca]") : null;
|
||||
if (!b) return;
|
||||
b.disabled = true;
|
||||
acaoDaLicenca(b.getAttribute("data-licenca"), b.getAttribute("data-acao") || "reenviar", $("licencaAviso"))
|
||||
.then(function () { b.disabled = false; });
|
||||
});
|
||||
|
||||
$("fecharFicha").addEventListener("click", fecharFicha);
|
||||
$("ficha").addEventListener("click", function (ev) { if (ev.target === $("ficha")) fecharFicha(); });
|
||||
document.addEventListener("keydown", function (ev) { if (ev.key === "Escape") fecharFicha(); });
|
||||
|
||||
@@ -370,6 +370,332 @@ data:
|
||||
});
|
||||
})();
|
||||
|
||||
convite.css: |
|
||||
/* Convite para competição: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). */
|
||||
main { max-width: 38rem; }
|
||||
.explica { color: var(--muted); }
|
||||
form { display: grid; gap: 1rem; }
|
||||
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
|
||||
legend { font-weight: 700; padding: 0 .4rem; }
|
||||
label { display: grid; gap: .2rem; font-size: .92rem; font-weight: 600; }
|
||||
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
|
||||
input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
|
||||
.linha-jogador { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
|
||||
.linha-jogador button { padding: .4rem .6rem; }
|
||||
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
|
||||
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
|
||||
button:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.erro { color: #b42318; margin: 0; font-weight: 600; }
|
||||
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: 1rem 1.2rem; }
|
||||
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
|
||||
|
||||
convite.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, nofollow">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Convite para competição — Athletic Map</title>
|
||||
<link rel="stylesheet" href="/publico/_estilo.css">
|
||||
<link rel="stylesheet" href="/publico/convite.css?v=1">
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="marca">Athletic <span>Map</span></div></header>
|
||||
<main>
|
||||
<h1 id="titulo">Convite para competição</h1>
|
||||
<p id="situacao" role="status" aria-live="polite">Abrindo o convite…</p>
|
||||
|
||||
<form id="formulario" hidden novalidate>
|
||||
<p class="explica">
|
||||
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
|
||||
A organização confere e aprova antes do time entrar na competição.
|
||||
</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Seu time</legend>
|
||||
<label>Nome do time *<input id="nome" maxlength="120" autocomplete="off" required></label>
|
||||
<label>Escola ou clube<input id="escola" maxlength="160" autocomplete="organization"></label>
|
||||
<label>Cidade<input id="cidade" maxlength="120" autocomplete="address-level2"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Responsável pelo time</legend>
|
||||
<label>Nome<input id="contatoNome" maxlength="120" autocomplete="name"></label>
|
||||
<label>Telefone<input id="contatoTelefone" maxlength="40" autocomplete="tel" inputmode="tel"></label>
|
||||
<label>E-mail<input id="contatoEmail" type="email" maxlength="160" autocomplete="email"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Jogadores</legend>
|
||||
<div id="jogadores"></div>
|
||||
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
|
||||
</fieldset>
|
||||
|
||||
<p id="erro" class="erro" role="alert" hidden></p>
|
||||
<button type="submit" id="enviar">Enviar time</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/publico/desafio.js?v=1"></script>
|
||||
<script src="/publico/convite.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
convite.js: |
|
||||
/* ===========================================================================
|
||||
* Convite para competição — a página que a OUTRA escola abre pelo link.
|
||||
*
|
||||
* Sem login: o token no endereço é o único segredo. A página pergunta ao
|
||||
* servidor a que o convite se refere e, se ele vale, deixa a pessoa mandar o
|
||||
* time com os jogadores (nome e camisa). O time chega PENDENTE: quem organiza
|
||||
* confere e aprova.
|
||||
*
|
||||
* Antes de enviar, a verificação anti-robô (desafio.js): pede um desafio ao
|
||||
* servidor, resolve no navegador ("Verificando…") e manda a prova junto com o
|
||||
* time, em `prova`. Um desafio por tentativa: ele é de uso único.
|
||||
*
|
||||
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
|
||||
* HTML. O script é externo porque a CSP é `script-src 'self'`.
|
||||
* =========================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var global = window;
|
||||
|
||||
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
|
||||
var MAX_JOGADORES = 60;
|
||||
|
||||
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
|
||||
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
|
||||
|
||||
var titulo = document.getElementById("titulo");
|
||||
var situacao = document.getElementById("situacao");
|
||||
var formulario = document.getElementById("formulario");
|
||||
var listaJogadores = document.getElementById("jogadores");
|
||||
var erro = document.getElementById("erro");
|
||||
var enviar = document.getElementById("enviar");
|
||||
var ROTULO_ENVIAR = enviar.textContent;
|
||||
|
||||
function recusar(texto) {
|
||||
situacao.textContent = texto;
|
||||
situacao.className = "invalido";
|
||||
formulario.hidden = true;
|
||||
}
|
||||
|
||||
function mostrarErro(texto) {
|
||||
erro.textContent = texto;
|
||||
erro.hidden = !texto;
|
||||
}
|
||||
|
||||
function linhaDeJogador() {
|
||||
var linha = document.createElement("div");
|
||||
linha.className = "linha-jogador";
|
||||
|
||||
var nome = document.createElement("input");
|
||||
nome.placeholder = "Nome do jogador";
|
||||
nome.maxLength = 120;
|
||||
nome.setAttribute("aria-label", "Nome do jogador");
|
||||
nome.setAttribute("data-campo", "nome");
|
||||
|
||||
var camisa = document.createElement("input");
|
||||
camisa.type = "number";
|
||||
camisa.min = "0";
|
||||
camisa.max = "999";
|
||||
camisa.placeholder = "nº";
|
||||
camisa.setAttribute("aria-label", "Número da camisa");
|
||||
camisa.setAttribute("data-campo", "camisa");
|
||||
|
||||
var tirar = document.createElement("button");
|
||||
tirar.type = "button";
|
||||
tirar.className = "secundario";
|
||||
tirar.textContent = "×";
|
||||
tirar.setAttribute("aria-label", "Remover jogador");
|
||||
tirar.addEventListener("click", function () { linha.remove(); });
|
||||
|
||||
linha.appendChild(nome);
|
||||
linha.appendChild(camisa);
|
||||
linha.appendChild(tirar);
|
||||
listaJogadores.appendChild(linha);
|
||||
return linha;
|
||||
}
|
||||
|
||||
function valor(id) {
|
||||
var v = document.getElementById(id).value.trim();
|
||||
return v === "" ? null : v;
|
||||
}
|
||||
|
||||
function montarCorpo() {
|
||||
var jogadores = [];
|
||||
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
|
||||
for (var i = 0; i < linhas.length; i++) {
|
||||
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
|
||||
if (!nome) continue;
|
||||
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
|
||||
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
|
||||
}
|
||||
return {
|
||||
nome: valor("nome"),
|
||||
escola: valor("escola"),
|
||||
cidade: valor("cidade"),
|
||||
contatoNome: valor("contatoNome"),
|
||||
contatoTelefone: valor("contatoTelefone"),
|
||||
contatoEmail: valor("contatoEmail"),
|
||||
jogadores: jogadores
|
||||
};
|
||||
}
|
||||
|
||||
function lerResposta(r) {
|
||||
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
|
||||
}
|
||||
|
||||
function aoEnviar(ev) {
|
||||
ev.preventDefault();
|
||||
mostrarErro("");
|
||||
var corpo = montarCorpo();
|
||||
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
|
||||
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
|
||||
enviar.disabled = true;
|
||||
enviar.textContent = "Verificando…";
|
||||
enviar.setAttribute("aria-busy", "true");
|
||||
|
||||
function voltar(texto) {
|
||||
enviar.disabled = false;
|
||||
enviar.textContent = ROTULO_ENVIAR;
|
||||
enviar.removeAttribute("aria-busy");
|
||||
mostrarErro(texto);
|
||||
}
|
||||
|
||||
fetch(base + "/desafio").then(lerResposta).then(function (r) {
|
||||
if (r.status !== 200) {
|
||||
var e = new Error("recusado");
|
||||
e.detalhe = (r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.";
|
||||
throw e;
|
||||
}
|
||||
if (!global.AtmDesafio) throw new Error("sem-crypto");
|
||||
return global.AtmDesafio.resolver(r.corpo);
|
||||
}).then(function (prova) {
|
||||
corpo.prova = prova;
|
||||
enviar.textContent = "Enviando…";
|
||||
return fetch(base + "/times", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(corpo)
|
||||
}).then(lerResposta);
|
||||
}).then(function (r) {
|
||||
if (r.status >= 200 && r.status < 300) {
|
||||
formulario.hidden = true;
|
||||
titulo.textContent = "Time enviado";
|
||||
situacao.textContent = "Recebemos o time \"" + corpo.nome + "\". A organização vai conferir os dados e aprovar. Você já pode fechar esta página.";
|
||||
situacao.className = "ok";
|
||||
return;
|
||||
}
|
||||
voltar((r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.");
|
||||
}).catch(function (e) {
|
||||
if (e && e.detalhe) { voltar(e.detalhe); return; }
|
||||
if (e && (e.message === "sem-crypto" || e.message === "desafio-invalido" || e.message === "sem-solucao")) {
|
||||
voltar("Não foi possível fazer a verificação anti-robô neste navegador. Atualize a página ou use outro navegador.");
|
||||
return;
|
||||
}
|
||||
voltar("Sem conexão. Confira a internet e tente de novo.");
|
||||
});
|
||||
}
|
||||
|
||||
function iniciar() {
|
||||
if (!TOKEN.test(token)) { recusar("Este endereço de convite não é válido."); return; }
|
||||
fetch(base).then(lerResposta).then(function (r) {
|
||||
var info = r.corpo || {};
|
||||
if (!info.valido) {
|
||||
recusar(info.motivo ? "Não dá para usar este convite: " + info.motivo + "." : "Este convite não está disponível.");
|
||||
return;
|
||||
}
|
||||
titulo.textContent = "Convite: " + (info.competicao || "competição");
|
||||
situacao.textContent = (info.temporada ? "Temporada " + info.temporada + ". " : "")
|
||||
+ "Restam " + info.vagasRestantes + " vaga(s) neste convite.";
|
||||
situacao.className = "";
|
||||
formulario.hidden = false;
|
||||
for (var i = 0; i < 5; i++) linhaDeJogador();
|
||||
}).catch(function () {
|
||||
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("maisJogador").addEventListener("click", function () {
|
||||
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
|
||||
});
|
||||
formulario.addEventListener("submit", aoEnviar);
|
||||
iniciar();
|
||||
})();
|
||||
|
||||
desafio.js: |
|
||||
/* ===========================================================================
|
||||
* Verificação anti-robô das páginas públicas — prova de trabalho no estilo
|
||||
* ALTCHA, sem serviço de terceiro (nada de quem preenche sai do Athletic Map).
|
||||
*
|
||||
* O servidor manda { algoritmo: "SHA-256", desafio, sal, assinatura, maximo }.
|
||||
* Aqui se procura o número n (0..maximo) tal que sha256(sal + n) == desafio, e
|
||||
* devolve-se { numero, sal, assinatura } para ir junto no envio. Em média são
|
||||
* maximo/2 hashes: ~1–2 s num celular com maximo = 150 000.
|
||||
*
|
||||
* Usa crypto.subtle (só existe em HTTPS — e as páginas públicas são HTTPS).
|
||||
* Os hashes saem em lotes paralelos: um `await` por hash custaria mais que o
|
||||
* próprio hash. Script externo porque a CSP é `script-src 'self'`.
|
||||
*
|
||||
* O teste do servidor é DesafioDoConvite (servico-campeonato).
|
||||
* =========================================================================== */
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var LOTE = 1000;
|
||||
var HEX = /^[0-9a-f]{64}$/;
|
||||
|
||||
function bytesDoHex(h) {
|
||||
var b = new Uint8Array(h.length / 2);
|
||||
for (var i = 0; i < b.length; i++) b[i] = parseInt(h.substr(i * 2, 2), 16);
|
||||
return b;
|
||||
}
|
||||
|
||||
function iguais(buf, alvo) {
|
||||
var a = new Uint8Array(buf);
|
||||
if (a.length !== alvo.length) return false;
|
||||
for (var i = 0; i < a.length; i++) if (a[i] !== alvo[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{algoritmo:string, desafio:string, sal:string, assinatura:string, maximo:number}} d
|
||||
* @returns {Promise<{numero:number, sal:string, assinatura:string}>}
|
||||
* rejeita com Error("sem-crypto"), Error("desafio-invalido") ou Error("sem-solucao")
|
||||
*/
|
||||
function resolver(d) {
|
||||
var subtle = global.crypto && global.crypto.subtle;
|
||||
var Codificador = global.TextEncoder;
|
||||
if (!subtle || !Codificador) return Promise.reject(new Error("sem-crypto"));
|
||||
if (!d || d.algoritmo !== "SHA-256" || !HEX.test(d.desafio || "") || typeof d.sal !== "string"
|
||||
|| typeof d.maximo !== "number" || d.maximo < 0 || d.maximo > 5000000) {
|
||||
return Promise.reject(new Error("desafio-invalido"));
|
||||
}
|
||||
var alvo = bytesDoHex(d.desafio);
|
||||
var cod = new Codificador();
|
||||
|
||||
function lote(inicio) {
|
||||
if (inicio > d.maximo) return Promise.reject(new Error("sem-solucao"));
|
||||
var fim = Math.min(inicio + LOTE - 1, d.maximo);
|
||||
var hashes = [];
|
||||
for (var n = inicio; n <= fim; n++) hashes.push(subtle.digest("SHA-256", cod.encode(d.sal + n)));
|
||||
return Promise.all(hashes).then(function (hs) {
|
||||
for (var i = 0; i < hs.length; i++) {
|
||||
if (iguais(hs[i], alvo)) return { numero: inicio + i, sal: d.sal, assinatura: d.assinatura };
|
||||
}
|
||||
return lote(fim + 1);
|
||||
});
|
||||
}
|
||||
return lote(0);
|
||||
}
|
||||
|
||||
global.AtmDesafio = { resolver: resolver };
|
||||
})(window);
|
||||
|
||||
fontes--fontes.css: |
|
||||
/* ============================================================
|
||||
* As fontes da landing page, servidas POR NOS.
|
||||
@@ -609,9 +935,7 @@ data:
|
||||
|
||||
/* marca */
|
||||
.logo{display:inline-flex; align-items:center; gap:11px; font-weight:700; font-size:1.22rem; letter-spacing:-.02em; color:var(--ink); text-decoration:none;}
|
||||
.mark{display:inline-flex; gap:3px; align-items:flex-end;}
|
||||
.mark i{display:block; width:5px; border-radius:2px; background:var(--laranja); transform:skewX(-14deg);}
|
||||
.mark i:nth-child(1){height:13px} .mark i:nth-child(2){height:19px} .mark i:nth-child(3){height:25px}
|
||||
.logo img{display:block; height:37px; width:auto;}
|
||||
|
||||
.topbar{background:var(--surface); border-bottom:1px solid var(--line);}
|
||||
.topbar .wrap{display:flex; align-items:center; justify-content:space-between; gap:16px; padding-block:14px;}
|
||||
@@ -814,7 +1138,7 @@ data:
|
||||
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="logo" href="#"><span class="mark"><i></i><i></i><i></i></span>Athletic Map</a>
|
||||
<a class="logo" href="#" aria-label="Athletic Map"><img src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37"></a>
|
||||
<a class="zap" id="topZap" href="#form">Falar com o time →</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1698,6 +2022,20 @@ data:
|
||||
.termos-uso-caixa input { margin-right: 6px; }
|
||||
.termos-uso-aceite { margin: 10px 0; }
|
||||
|
||||
/* ── Abas (2026-09-29) — mesmo desenho do painel do dono ───────────────────── */
|
||||
.abas { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin: 4px 0 18px; overflow-x: auto; }
|
||||
.aba {
|
||||
background: none; border: 0; border-bottom: 3px solid transparent; padding: 10px 16px;
|
||||
font: inherit; font-weight: 600; color: var(--muted); cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.aba:hover { color: var(--navy); }
|
||||
.aba.ativa { color: var(--navy); border-bottom-color: var(--orange); }
|
||||
.aba .badge {
|
||||
display: inline-block; min-width: 8px; height: 8px; margin-left: 6px; border-radius: 999px;
|
||||
background: var(--orange); vertical-align: middle;
|
||||
}
|
||||
.aba .badge.num { min-width: 18px; height: auto; padding: 0 6px; font-size: 12px; color: #fff; line-height: 18px; }
|
||||
|
||||
minha-conta.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
@@ -1722,7 +2060,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.88">
|
||||
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.90">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1765,23 +2103,56 @@ 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>
|
||||
<!-- Abas (2026-09-29, pedido do dono): a pagina empilhava visao geral, Termos,
|
||||
escolas ativas e canceladas numa coluna so. Mesmo padrao do painel do dono. -->
|
||||
<nav class="abas" role="tablist" aria-label="Seções da conta">
|
||||
<button type="button" class="aba ativa" data-aba="escolas" role="tab">Minhas escolas</button>
|
||||
<button type="button" class="aba" data-aba="visao" role="tab">Visão geral</button>
|
||||
<button type="button" class="aba" data-aba="termos" role="tab">Termos de Uso <span id="termosBadge" class="badge oculto"></span></button>
|
||||
<button type="button" class="aba oculto" data-aba="canceladas" role="tab" id="abaCanceladas">Canceladas</button>
|
||||
</nav>
|
||||
|
||||
<div id="produtos" class="produtos"></div>
|
||||
<div id="aba-escolas" class="painel-aba">
|
||||
<div id="produtos" class="produtos"></div>
|
||||
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-visao" class="painel-aba 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="visaoVazia" class="cartao">
|
||||
<h2>Visão <span class="hl">geral</span></h2>
|
||||
<p class="ajuda">Os números aparecem aqui quando as suas escolinhas enviarem o primeiro resumo
|
||||
(uma vez por dia, de madrugada).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-termos" class="painel-aba oculto">
|
||||
<div id="termosDeUso"></div>
|
||||
<div id="termosEmDia" class="cartao oculto">
|
||||
<h2>Termos de Uso</h2>
|
||||
<p class="ajuda">Nada pendente: a versão vigente já foi aceita para todas as suas escolinhas.
|
||||
<a href="/termos" target="_blank" rel="noopener">Ler os Termos de Uso</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-canceladas" class="painel-aba oculto">
|
||||
<p class="ajuda">Assinaturas canceladas. Regularize o pagamento para voltar a entrar; os dados ficam
|
||||
guardados por 30 dias depois do cancelamento.</p>
|
||||
<div id="produtosCancelados" class="produtos"></div>
|
||||
</div>
|
||||
|
||||
<p class="rodape">
|
||||
@@ -1801,7 +2172,7 @@ data:
|
||||
|
||||
<footer class="direitos">© 2026 Athletic Map. Todos os direitos reservados.</footer>
|
||||
|
||||
<script src="/publico/minha-conta.js?v=2.88"></script>
|
||||
<script src="/publico/minha-conta.js?v=2.90"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1870,6 +2241,36 @@ data:
|
||||
var erroTexto = document.getElementById("erroTexto");
|
||||
var botaoSair = document.getElementById("sair");
|
||||
|
||||
/* ── ABAS (2026-09-29) ────────────────────────────────────────────────────────
|
||||
* Minhas escolas | Visao geral | Termos de Uso | Canceladas. A ultima aba aberta
|
||||
* fica no localStorage (conveniencia deste navegador; sem ela, "escolas"). */
|
||||
var ABA_CHAVE = "atm.conta.aba";
|
||||
function abrirAba(nome) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao || botao.classList.contains("oculto")) { nome = "escolas"; botao = document.querySelector('.aba[data-aba="escolas"]'); }
|
||||
if (!botao) return;
|
||||
document.querySelectorAll(".aba").forEach(function (b) { b.classList.toggle("ativa", b === botao); });
|
||||
document.querySelectorAll(".painel-aba").forEach(function (painel) {
|
||||
painel.classList.toggle("oculto", painel.id !== "aba-" + nome);
|
||||
});
|
||||
try { localStorage.setItem(ABA_CHAVE, nome); } catch (e) { /* aba privada */ }
|
||||
}
|
||||
function abaLembrada() { try { return localStorage.getItem(ABA_CHAVE) || "escolas"; } catch (e) { return "escolas"; } }
|
||||
/* Marca a aba: `pendente` acende a bolinha; `quantos` escreve o numero. */
|
||||
function marcarAba(nome, pendente, quantos) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao) return;
|
||||
var badge = botao.querySelector(".badge");
|
||||
if (badge) {
|
||||
badge.classList.toggle("oculto", !pendente && !quantos);
|
||||
badge.classList.toggle("num", !!quantos);
|
||||
badge.textContent = quantos ? String(quantos) : "";
|
||||
}
|
||||
}
|
||||
document.querySelectorAll(".aba").forEach(function (b) {
|
||||
b.addEventListener("click", function () { abrirAba(b.getAttribute("data-aba")); });
|
||||
});
|
||||
|
||||
function mostrar(qual) {
|
||||
[porta, carregando, conteudo, caixaErro].forEach(function (s) {
|
||||
if (s) s.classList.add("oculto");
|
||||
@@ -1988,14 +2389,18 @@ data:
|
||||
return n ? n.charAt(0).toUpperCase() + n.slice(1) : "—";
|
||||
}
|
||||
|
||||
/* Tenant de DEMONSTRACAO do dono: o servidor marca (`demonstracao: true`). O
|
||||
cartao diz isso ao lado da situacao, e nao ganha "Trocar de plano" nem
|
||||
"Modulos avulsos" — nao ha cobranca, e o servidor recusaria de qualquer jeito. */
|
||||
function cartao(p) {
|
||||
var s = SITUACAO[p.situacao] || ["neutro", String(p.situacao || "").toLowerCase()];
|
||||
var podeEntrar = p.endereco && p.situacao === "ATIVA";
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + '"'
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + (p.demonstracao ? " demonstracao" : "") + '"'
|
||||
+ (p.tenant ? ' data-tenant="' + escapar(p.tenant) + '"' : "") + ">"
|
||||
+ '<span class="faixa-plano">Plano ' + escapar(nomeDoPlano(p.plano)) + "</span>"
|
||||
+ "<h2>" + escapar(p.escolinha || "Sua escolinha") + "</h2>"
|
||||
+ '<span class="selo ' + s[0] + '">' + s[1] + "</span>"
|
||||
+ (p.demonstracao ? '<span class="selo neutro">demonstração — sem cobrança</span>' : "")
|
||||
+ (podeEntrar
|
||||
? '<a class="btn" href="' + escapar(p.endereco) + '">Entrar →</a>'
|
||||
: '<p class="ajuda">' + (p.situacao === "PREPARANDO"
|
||||
@@ -2014,10 +2419,22 @@ data:
|
||||
.then(function (lista) {
|
||||
enderecos = {};
|
||||
(lista || []).forEach(function (p) { if (p.tenant) enderecos[p.tenant] = p.endereco; });
|
||||
produtos.innerHTML = (lista || []).map(cartao).join("");
|
||||
nenhum.classList.toggle("oculto", (lista || []).length > 0);
|
||||
var ativas = (lista || []).filter(function (p) { return p.situacao !== "CANCELADA"; });
|
||||
var canceladas = (lista || []).filter(function (p) { return p.situacao === "CANCELADA"; });
|
||||
produtos.innerHTML = ativas.map(cartao).join("");
|
||||
var caixaCanceladas = document.getElementById("produtosCancelados");
|
||||
if (caixaCanceladas) caixaCanceladas.innerHTML = canceladas.map(cartao).join("");
|
||||
var abaCanceladas = document.getElementById("abaCanceladas");
|
||||
if (abaCanceladas) {
|
||||
abaCanceladas.classList.toggle("oculto", canceladas.length === 0);
|
||||
abaCanceladas.textContent = "Canceladas" + (canceladas.length ? " (" + canceladas.length + ")" : "");
|
||||
}
|
||||
nenhum.classList.toggle("oculto", ativas.length > 0);
|
||||
mostrar(conteudo);
|
||||
(lista || []).forEach(function (p) { if (p.tenant && p.situacao === "ATIVA") acrescentarBotaoDePlano(p.tenant); });
|
||||
abrirAba(abaLembrada());
|
||||
(lista || []).forEach(function (p) {
|
||||
if (p.tenant && p.situacao === "ATIVA" && !p.demonstracao) acrescentarBotaoDePlano(p.tenant);
|
||||
});
|
||||
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas(), carregarVisao(),
|
||||
carregarTermosDeUso()]);
|
||||
});
|
||||
@@ -2323,8 +2740,10 @@ data:
|
||||
.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; });
|
||||
var emDia = document.getElementById("termosEmDia");
|
||||
var faltam = (d && d.versao && d.escolinhas) ? d.escolinhas.filter(function (e) { return !e.aceito; }) : [];
|
||||
if (emDia) emDia.classList.toggle("oculto", faltam.length > 0);
|
||||
marcarAba("termos", faltam.length > 0);
|
||||
if (!faltam.length) return;
|
||||
var aviso = document.createElement("section");
|
||||
aviso.id = "avisoTermos";
|
||||
@@ -2335,7 +2754,8 @@ data:
|
||||
+ ". 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);
|
||||
var caixaTermos = document.getElementById("termosDeUso");
|
||||
if (caixaTermos) caixaTermos.appendChild(aviso); else produtos.parentNode.insertBefore(aviso, produtos);
|
||||
aviso.querySelector("button").addEventListener("click", function () {
|
||||
aceitarTermosDeUso(aviso, d.versao);
|
||||
});
|
||||
@@ -2588,7 +3008,9 @@ data:
|
||||
.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 vazia = document.getElementById("visaoVazia");
|
||||
if (!tabela) { caixa.classList.add("oculto"); if (vazia) vazia.classList.remove("oculto"); return; }
|
||||
if (vazia) vazia.classList.add("oculto");
|
||||
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.";
|
||||
@@ -3171,7 +3593,7 @@ data:
|
||||
<option value="NASCENDO">Nascendo</option>
|
||||
</select>
|
||||
<label class="filtro"><input type="checkbox" id="soAtencao"> só com alerta</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes e demonstrações</label>
|
||||
</div>
|
||||
<div class="tabela-rolagem">
|
||||
<table class="tabela clientes">
|
||||
@@ -3334,7 +3756,7 @@ data:
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/publico/painel.js?v=2.86"></script>
|
||||
<script src="/publico/painel.js?v=2.89"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3630,9 +4052,13 @@ data:
|
||||
bloco("Recebido desde o início", reaisCurto(i.recebidoTotal),
|
||||
i.clientesSemDadoDoAsaas > 0 ? i.clientesSemDadoDoAsaas + " sem resposta do Asaas" : "extrato do Asaas", "")
|
||||
].join("");
|
||||
// Teste e demonstracao aparecem na lista, mas nao nos numeros — e a tela diz.
|
||||
var foraDosNumeros = [];
|
||||
if (i.contasDeTeste > 0) foraDosNumeros.push(i.contasDeTeste + " de teste");
|
||||
if (i.demonstracoes > 0) foraDosNumeros.push(i.demonstracoes + " de demonstração");
|
||||
$("kpiClientes").innerHTML = [
|
||||
bloco("Clientes ativos", esc(i.clientesAtivos),
|
||||
i.contasDeTeste > 0 ? "+ " + i.contasDeTeste + " de teste (fora dos números)" : "", "ok"),
|
||||
foraDosNumeros.length ? "+ " + foraDosNumeros.join(", ") + " (fora dos números)" : "", "ok"),
|
||||
bloco("Novos no mês", esc(i.novosNoMes), comparacao(i.novosNoMes, i.novosMesAnterior), ""),
|
||||
bloco("Leads no mês", esc(i.leadsNoMes), comparacao(i.leadsNoMes, i.leadsMesAnterior), ""),
|
||||
bloco("Conversão", pct(i.conversao), "de lead para cliente pagante", ""),
|
||||
@@ -3671,7 +4097,7 @@ data:
|
||||
if (plano && c.plano !== plano) return false;
|
||||
if (estado && (c.estado || "NASCENDO") !== estado) return false;
|
||||
if (soAtencao && !(c.alertas || []).length) return false;
|
||||
if (semTeste && c.teste) return false;
|
||||
if (semTeste && (c.teste || c.demonstracao)) return false;
|
||||
if (busca) {
|
||||
var alvo = [c.cliente, c.escolinha, c.email, c.cidade, c.tenant, c.whatsapp, c.documento]
|
||||
.join(" ").toLowerCase();
|
||||
@@ -3701,7 +4127,9 @@ data:
|
||||
}
|
||||
|
||||
var SITUACAO_ASAAS = { ACTIVE: "ativa", INACTIVE: "encerrada", EXPIRED: "expirada" };
|
||||
function assinaturaNoAsaas(a) {
|
||||
/* A demonstracao nao existe no Asaas DE PROPOSITO: nao e "nao existe" em vermelho. */
|
||||
function assinaturaNoAsaas(a, c) {
|
||||
if (c && c.demonstracao) return "demonstração — sem cobrança";
|
||||
if (!a || !a.disponivel) return "indisponível agora";
|
||||
if (!a.existe) return '<b class="vermelho">não existe</b>';
|
||||
var s = SITUACAO_ASAAS[a.situacao] || txt(a.situacao);
|
||||
@@ -3738,10 +4166,11 @@ data:
|
||||
var gravidade = (c.alertas || []).reduce(function (g, a) {
|
||||
return a.gravidade === "critico" ? "critico" : (g === "critico" ? g : (a.gravidade === "alerta" ? "alerta" : g));
|
||||
}, "");
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste ? " teste" : "")
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste || c.demonstracao ? " teste" : "")
|
||||
+ (!c.tenant ? " destaque" : "") + '" data-assinatura="' + esc(c.assinatura) + '" tabindex="0">'
|
||||
+ "<td><b>" + txt(c.cliente) + "</b>"
|
||||
+ (c.teste ? ' <span class="selo neutro">teste</span>' : "")
|
||||
+ (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "")
|
||||
+ '<span class="sub">' + (c.tenant
|
||||
? '<a href="' + esc(c.endereco) + '" target="_blank" rel="noopener noreferrer">' + esc(c.tenant) + "</a>"
|
||||
: "sem tenant")
|
||||
@@ -3750,9 +4179,9 @@ data:
|
||||
+ "<td>" + etiquetaPlano(c) + "</td>"
|
||||
+ "<td>" + selo(c.estado, c.tenant) + '<span class="sub">' + (c.diasDeCliente !== null && c.diasDeCliente !== undefined
|
||||
? "cliente há " + esc(c.diasDeCliente) + " dia(s)" : "") + "</span></td>"
|
||||
+ '<td class="num">' + reais(c.valorMensal) + diferencaDeTabela(c) + "</td>"
|
||||
+ '<td class="num">' + (f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + proximaCobranca(f) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? '<span class="ajuda">sem cobrança</span>' : reais(c.valorMensal) + diferencaDeTabela(c)) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? "—" : f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + (c.demonstracao ? '<span class="ajuda">—</span>' : proximaCobranca(f)) + "</td>"
|
||||
+ "<td>" + usoCurto(c.uso, c.usoDeDentro) + "</td>"
|
||||
+ "<td>" + seloSubconta(c.subconta) + "</td>"
|
||||
+ "</tr>";
|
||||
@@ -3778,7 +4207,9 @@ data:
|
||||
+ esc(a.titulo) + '</b><span class="sub">' + esc(a.detalhe) + "</span></div></li>";
|
||||
}).join("");
|
||||
|
||||
var extrato = f.disponivel
|
||||
var extrato = c.demonstracao
|
||||
? '<p class="ajuda">Tenant de demonstração: não há assinatura nem cobrança no Asaas.</p>'
|
||||
: f.disponivel
|
||||
? ((f.cobrancas || []).length
|
||||
? '<table class="tabela"><thead><tr><th>Vencimento</th><th>Pago em</th><th>Forma</th><th class="num">Valor</th><th class="num">Líquido</th><th>Situação</th><th></th></tr></thead><tbody>'
|
||||
+ f.cobrancas.map(function (cb) {
|
||||
@@ -3799,7 +4230,7 @@ data:
|
||||
return '<header class="ficha-topo ' + classePlano(c.plano) + '">'
|
||||
+ '<span class="etiqueta ' + classePlano(c.plano) + '">Plano ' + esc(nomePlano(c.plano))
|
||||
+ (c.ciclo === "ANUAL" ? " · anual" : " · mensal") + "</span>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + "</h2>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "") + "</h2>"
|
||||
+ '<p class="sub">' + (c.escolinha && c.escolinha !== c.cliente ? esc(c.escolinha) + " · " : "")
|
||||
+ selo(c.estado, c.tenant) + (c.estadoDesde ? " desde " + dataHora(c.estadoDesde) : "") + "</p>"
|
||||
+ '<div class="atalhos">'
|
||||
@@ -3807,7 +4238,9 @@ data:
|
||||
+ (whatsapp(c.whatsapp) ? '<a class="btn claro" href="' + esc(whatsapp(c.whatsapp)) + '" target="_blank" rel="noopener noreferrer">WhatsApp</a>' : "")
|
||||
+ (c.email ? '<a class="btn claro" href="mailto:' + esc(c.email) + '">E-mail</a>' : "")
|
||||
+ (f.faturaVencida ? '<a class="btn claro" href="' + esc(linkSeguro(f.faturaVencida) || "#") + '" target="_blank" rel="noopener noreferrer">Fatura vencida</a>' : "")
|
||||
+ "</div></header>"
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="reenviar">Reenviar licença</button>'
|
||||
+ '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="trocar">Trocar chave</button>' : "")
|
||||
+ '</div><p id="licencaAviso" class="ajuda" aria-live="polite"></p></header>'
|
||||
+ (alertas ? '<ul class="lista-alertas">' + alertas + "</ul>" : "")
|
||||
|
||||
+ '<div class="grade-ficha">'
|
||||
@@ -3821,14 +4254,17 @@ data:
|
||||
+ '<section><h3>Assinatura</h3><dl>'
|
||||
+ item("Plano", esc(nomePlano(c.plano)) + " " + (c.ciclo === "ANUAL" ? "anual" : "mensal"))
|
||||
+ item("Valor do ciclo", reais(c.valorDoCiclo)) + item("Vale por mês", reais(c.valorMensal) + diferencaDeTabela(c))
|
||||
+ item("De onde vem o valor", c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas))
|
||||
+ item("De onde vem o valor", c.demonstracao ? "demonstração — sem cobrança"
|
||||
: c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas, c))
|
||||
+ item("Contratado em", dataHora(c.contratadoEm)) + item("Cliente há", c.diasDeCliente === null || c.diasDeCliente === undefined ? "—" : esc(c.diasDeCliente) + " dia(s)")
|
||||
+ item("Assinatura no Asaas", txt(c.assinatura)) + item("Cliente no Asaas", txt(c.clienteNoProvedor))
|
||||
+ "</dl></section>"
|
||||
|
||||
+ '<section><h3>Dinheiro</h3><dl>'
|
||||
+ (f.disponivel
|
||||
+ (c.demonstracao
|
||||
? item("Cobrança", "nenhuma — tenant de demonstração")
|
||||
: f.disponivel
|
||||
? item("Total pago", reais(f.totalPago)) + item("Líquido (após taxas)", reais(f.totalLiquido))
|
||||
+ item("Taxas pagas", reais(f.taxas))
|
||||
+ item("Último pagamento", f.ultimoPagamento ? data(f.ultimoPagamento.data) + " · " + reais(f.ultimoPagamento.valor) + " · " + txt(f.ultimoPagamento.forma) : "—")
|
||||
@@ -4325,6 +4761,55 @@ data:
|
||||
corpo.addEventListener("keydown", function (ev) { if (ev.key === "Enter") aoEscolher(ev); });
|
||||
$("alertas").addEventListener("click", aoEscolher);
|
||||
|
||||
/* ── licenca dos apps (dono, treinador, pais) ─────────────────────────────
|
||||
* Reenviar = mandar a MESMA chave da escola ao dono da compra (captacao 4.32+;
|
||||
* so a chave antiga, guardada so com hash, e trocada uma vez). Nada muda para
|
||||
* ninguem: sem confirmacao.
|
||||
* Trocar = revogar a chave e gerar outra. Desde os apps 1.0.4 (captacao 4.33)
|
||||
* o app pergunta a situacao da chave ao abrir: trocar DERRUBA os apps de todos
|
||||
* da escola (dono, treinadores, pais) em ate 15 min de uso, e cada um precisa
|
||||
* da chave nova. Por isso pede o nome do tenant digitado. */
|
||||
var API_LICENCA = "/api/plataforma/painel/licencas/";
|
||||
|
||||
function acaoDaLicenca(tenant, acao, aviso) {
|
||||
if (!tokenAtual) return Promise.resolve();
|
||||
if (acao === "trocar") {
|
||||
var digitado = window.prompt("TROCAR a chave de " + tenant + "?\n\n"
|
||||
+ "A chave atual para de valer e os apps de TODOS da escola (dono, treinadores e pais) saem da conta "
|
||||
+ "em até 15 minutos de uso; cada um vai precisar da chave nova, que vai por e-mail ao dono da compra.\n"
|
||||
+ "Use só se a chave vazou.\n\nPara confirmar, digite o nome do tenant (" + tenant + "):");
|
||||
if (digitado === null) return Promise.resolve();
|
||||
if (digitado.trim() !== tenant) {
|
||||
aviso.textContent = "Nada foi trocado: o nome digitado não confere.";
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
aviso.textContent = "Enviando…";
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/" + acao, {
|
||||
method: "POST", headers: { "x-atm-admin-token": tokenAtual, "Content-Type": "application/json" }, body: "{}"
|
||||
}).then(function (r) {
|
||||
if (r.status === 401) throw new Error("O token expirou. Saia e entre de novo no painel.");
|
||||
return r.json().then(function (d) {
|
||||
if (!r.ok) throw new Error((d && d.detail) || ("O servidor recusou (" + r.status + ")."));
|
||||
return d;
|
||||
});
|
||||
}).then(function (d) {
|
||||
var oQue = acao === "trocar" ? "Chave NOVA" : "Chave da escola";
|
||||
aviso.textContent = oQue + (d.emailEnviado ? " enviada para " : " pronta; o e-mail vai em instantes para ")
|
||||
+ (d.destino || "o dono da compra") + " (termina em " + d.finalDaChave + ").";
|
||||
}).catch(function (e) {
|
||||
aviso.textContent = e && e.message && e.message !== "Failed to fetch" ? e.message : "Sem conexão com o servidor.";
|
||||
});
|
||||
}
|
||||
|
||||
$("fichaCorpo").addEventListener("click", function (ev) {
|
||||
var b = ev.target.closest ? ev.target.closest("[data-licenca]") : null;
|
||||
if (!b) return;
|
||||
b.disabled = true;
|
||||
acaoDaLicenca(b.getAttribute("data-licenca"), b.getAttribute("data-acao") || "reenviar", $("licencaAviso"))
|
||||
.then(function () { b.disabled = false; });
|
||||
});
|
||||
|
||||
$("fecharFicha").addEventListener("click", fecharFicha);
|
||||
$("ficha").addEventListener("click", function (ev) { if (ev.target === $("ficha")) fecharFicha(); });
|
||||
document.addEventListener("keydown", function (ev) { if (ev.key === "Escape") fecharFicha(); });
|
||||
|
||||
@@ -370,6 +370,332 @@ data:
|
||||
});
|
||||
})();
|
||||
|
||||
convite.css: |
|
||||
/* Convite para competição: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). */
|
||||
main { max-width: 38rem; }
|
||||
.explica { color: var(--muted); }
|
||||
form { display: grid; gap: 1rem; }
|
||||
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
|
||||
legend { font-weight: 700; padding: 0 .4rem; }
|
||||
label { display: grid; gap: .2rem; font-size: .92rem; font-weight: 600; }
|
||||
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
|
||||
input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
|
||||
.linha-jogador { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
|
||||
.linha-jogador button { padding: .4rem .6rem; }
|
||||
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
|
||||
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
|
||||
button:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.erro { color: #b42318; margin: 0; font-weight: 600; }
|
||||
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: 1rem 1.2rem; }
|
||||
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
|
||||
|
||||
convite.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, nofollow">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Convite para competição — Athletic Map</title>
|
||||
<link rel="stylesheet" href="/publico/_estilo.css">
|
||||
<link rel="stylesheet" href="/publico/convite.css?v=1">
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="marca">Athletic <span>Map</span></div></header>
|
||||
<main>
|
||||
<h1 id="titulo">Convite para competição</h1>
|
||||
<p id="situacao" role="status" aria-live="polite">Abrindo o convite…</p>
|
||||
|
||||
<form id="formulario" hidden novalidate>
|
||||
<p class="explica">
|
||||
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
|
||||
A organização confere e aprova antes do time entrar na competição.
|
||||
</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Seu time</legend>
|
||||
<label>Nome do time *<input id="nome" maxlength="120" autocomplete="off" required></label>
|
||||
<label>Escola ou clube<input id="escola" maxlength="160" autocomplete="organization"></label>
|
||||
<label>Cidade<input id="cidade" maxlength="120" autocomplete="address-level2"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Responsável pelo time</legend>
|
||||
<label>Nome<input id="contatoNome" maxlength="120" autocomplete="name"></label>
|
||||
<label>Telefone<input id="contatoTelefone" maxlength="40" autocomplete="tel" inputmode="tel"></label>
|
||||
<label>E-mail<input id="contatoEmail" type="email" maxlength="160" autocomplete="email"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Jogadores</legend>
|
||||
<div id="jogadores"></div>
|
||||
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
|
||||
</fieldset>
|
||||
|
||||
<p id="erro" class="erro" role="alert" hidden></p>
|
||||
<button type="submit" id="enviar">Enviar time</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/publico/desafio.js?v=1"></script>
|
||||
<script src="/publico/convite.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
convite.js: |
|
||||
/* ===========================================================================
|
||||
* Convite para competição — a página que a OUTRA escola abre pelo link.
|
||||
*
|
||||
* Sem login: o token no endereço é o único segredo. A página pergunta ao
|
||||
* servidor a que o convite se refere e, se ele vale, deixa a pessoa mandar o
|
||||
* time com os jogadores (nome e camisa). O time chega PENDENTE: quem organiza
|
||||
* confere e aprova.
|
||||
*
|
||||
* Antes de enviar, a verificação anti-robô (desafio.js): pede um desafio ao
|
||||
* servidor, resolve no navegador ("Verificando…") e manda a prova junto com o
|
||||
* time, em `prova`. Um desafio por tentativa: ele é de uso único.
|
||||
*
|
||||
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
|
||||
* HTML. O script é externo porque a CSP é `script-src 'self'`.
|
||||
* =========================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var global = window;
|
||||
|
||||
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
|
||||
var MAX_JOGADORES = 60;
|
||||
|
||||
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
|
||||
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
|
||||
|
||||
var titulo = document.getElementById("titulo");
|
||||
var situacao = document.getElementById("situacao");
|
||||
var formulario = document.getElementById("formulario");
|
||||
var listaJogadores = document.getElementById("jogadores");
|
||||
var erro = document.getElementById("erro");
|
||||
var enviar = document.getElementById("enviar");
|
||||
var ROTULO_ENVIAR = enviar.textContent;
|
||||
|
||||
function recusar(texto) {
|
||||
situacao.textContent = texto;
|
||||
situacao.className = "invalido";
|
||||
formulario.hidden = true;
|
||||
}
|
||||
|
||||
function mostrarErro(texto) {
|
||||
erro.textContent = texto;
|
||||
erro.hidden = !texto;
|
||||
}
|
||||
|
||||
function linhaDeJogador() {
|
||||
var linha = document.createElement("div");
|
||||
linha.className = "linha-jogador";
|
||||
|
||||
var nome = document.createElement("input");
|
||||
nome.placeholder = "Nome do jogador";
|
||||
nome.maxLength = 120;
|
||||
nome.setAttribute("aria-label", "Nome do jogador");
|
||||
nome.setAttribute("data-campo", "nome");
|
||||
|
||||
var camisa = document.createElement("input");
|
||||
camisa.type = "number";
|
||||
camisa.min = "0";
|
||||
camisa.max = "999";
|
||||
camisa.placeholder = "nº";
|
||||
camisa.setAttribute("aria-label", "Número da camisa");
|
||||
camisa.setAttribute("data-campo", "camisa");
|
||||
|
||||
var tirar = document.createElement("button");
|
||||
tirar.type = "button";
|
||||
tirar.className = "secundario";
|
||||
tirar.textContent = "×";
|
||||
tirar.setAttribute("aria-label", "Remover jogador");
|
||||
tirar.addEventListener("click", function () { linha.remove(); });
|
||||
|
||||
linha.appendChild(nome);
|
||||
linha.appendChild(camisa);
|
||||
linha.appendChild(tirar);
|
||||
listaJogadores.appendChild(linha);
|
||||
return linha;
|
||||
}
|
||||
|
||||
function valor(id) {
|
||||
var v = document.getElementById(id).value.trim();
|
||||
return v === "" ? null : v;
|
||||
}
|
||||
|
||||
function montarCorpo() {
|
||||
var jogadores = [];
|
||||
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
|
||||
for (var i = 0; i < linhas.length; i++) {
|
||||
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
|
||||
if (!nome) continue;
|
||||
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
|
||||
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
|
||||
}
|
||||
return {
|
||||
nome: valor("nome"),
|
||||
escola: valor("escola"),
|
||||
cidade: valor("cidade"),
|
||||
contatoNome: valor("contatoNome"),
|
||||
contatoTelefone: valor("contatoTelefone"),
|
||||
contatoEmail: valor("contatoEmail"),
|
||||
jogadores: jogadores
|
||||
};
|
||||
}
|
||||
|
||||
function lerResposta(r) {
|
||||
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
|
||||
}
|
||||
|
||||
function aoEnviar(ev) {
|
||||
ev.preventDefault();
|
||||
mostrarErro("");
|
||||
var corpo = montarCorpo();
|
||||
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
|
||||
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
|
||||
enviar.disabled = true;
|
||||
enviar.textContent = "Verificando…";
|
||||
enviar.setAttribute("aria-busy", "true");
|
||||
|
||||
function voltar(texto) {
|
||||
enviar.disabled = false;
|
||||
enviar.textContent = ROTULO_ENVIAR;
|
||||
enviar.removeAttribute("aria-busy");
|
||||
mostrarErro(texto);
|
||||
}
|
||||
|
||||
fetch(base + "/desafio").then(lerResposta).then(function (r) {
|
||||
if (r.status !== 200) {
|
||||
var e = new Error("recusado");
|
||||
e.detalhe = (r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.";
|
||||
throw e;
|
||||
}
|
||||
if (!global.AtmDesafio) throw new Error("sem-crypto");
|
||||
return global.AtmDesafio.resolver(r.corpo);
|
||||
}).then(function (prova) {
|
||||
corpo.prova = prova;
|
||||
enviar.textContent = "Enviando…";
|
||||
return fetch(base + "/times", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(corpo)
|
||||
}).then(lerResposta);
|
||||
}).then(function (r) {
|
||||
if (r.status >= 200 && r.status < 300) {
|
||||
formulario.hidden = true;
|
||||
titulo.textContent = "Time enviado";
|
||||
situacao.textContent = "Recebemos o time \"" + corpo.nome + "\". A organização vai conferir os dados e aprovar. Você já pode fechar esta página.";
|
||||
situacao.className = "ok";
|
||||
return;
|
||||
}
|
||||
voltar((r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.");
|
||||
}).catch(function (e) {
|
||||
if (e && e.detalhe) { voltar(e.detalhe); return; }
|
||||
if (e && (e.message === "sem-crypto" || e.message === "desafio-invalido" || e.message === "sem-solucao")) {
|
||||
voltar("Não foi possível fazer a verificação anti-robô neste navegador. Atualize a página ou use outro navegador.");
|
||||
return;
|
||||
}
|
||||
voltar("Sem conexão. Confira a internet e tente de novo.");
|
||||
});
|
||||
}
|
||||
|
||||
function iniciar() {
|
||||
if (!TOKEN.test(token)) { recusar("Este endereço de convite não é válido."); return; }
|
||||
fetch(base).then(lerResposta).then(function (r) {
|
||||
var info = r.corpo || {};
|
||||
if (!info.valido) {
|
||||
recusar(info.motivo ? "Não dá para usar este convite: " + info.motivo + "." : "Este convite não está disponível.");
|
||||
return;
|
||||
}
|
||||
titulo.textContent = "Convite: " + (info.competicao || "competição");
|
||||
situacao.textContent = (info.temporada ? "Temporada " + info.temporada + ". " : "")
|
||||
+ "Restam " + info.vagasRestantes + " vaga(s) neste convite.";
|
||||
situacao.className = "";
|
||||
formulario.hidden = false;
|
||||
for (var i = 0; i < 5; i++) linhaDeJogador();
|
||||
}).catch(function () {
|
||||
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("maisJogador").addEventListener("click", function () {
|
||||
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
|
||||
});
|
||||
formulario.addEventListener("submit", aoEnviar);
|
||||
iniciar();
|
||||
})();
|
||||
|
||||
desafio.js: |
|
||||
/* ===========================================================================
|
||||
* Verificação anti-robô das páginas públicas — prova de trabalho no estilo
|
||||
* ALTCHA, sem serviço de terceiro (nada de quem preenche sai do Athletic Map).
|
||||
*
|
||||
* O servidor manda { algoritmo: "SHA-256", desafio, sal, assinatura, maximo }.
|
||||
* Aqui se procura o número n (0..maximo) tal que sha256(sal + n) == desafio, e
|
||||
* devolve-se { numero, sal, assinatura } para ir junto no envio. Em média são
|
||||
* maximo/2 hashes: ~1–2 s num celular com maximo = 150 000.
|
||||
*
|
||||
* Usa crypto.subtle (só existe em HTTPS — e as páginas públicas são HTTPS).
|
||||
* Os hashes saem em lotes paralelos: um `await` por hash custaria mais que o
|
||||
* próprio hash. Script externo porque a CSP é `script-src 'self'`.
|
||||
*
|
||||
* O teste do servidor é DesafioDoConvite (servico-campeonato).
|
||||
* =========================================================================== */
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var LOTE = 1000;
|
||||
var HEX = /^[0-9a-f]{64}$/;
|
||||
|
||||
function bytesDoHex(h) {
|
||||
var b = new Uint8Array(h.length / 2);
|
||||
for (var i = 0; i < b.length; i++) b[i] = parseInt(h.substr(i * 2, 2), 16);
|
||||
return b;
|
||||
}
|
||||
|
||||
function iguais(buf, alvo) {
|
||||
var a = new Uint8Array(buf);
|
||||
if (a.length !== alvo.length) return false;
|
||||
for (var i = 0; i < a.length; i++) if (a[i] !== alvo[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{algoritmo:string, desafio:string, sal:string, assinatura:string, maximo:number}} d
|
||||
* @returns {Promise<{numero:number, sal:string, assinatura:string}>}
|
||||
* rejeita com Error("sem-crypto"), Error("desafio-invalido") ou Error("sem-solucao")
|
||||
*/
|
||||
function resolver(d) {
|
||||
var subtle = global.crypto && global.crypto.subtle;
|
||||
var Codificador = global.TextEncoder;
|
||||
if (!subtle || !Codificador) return Promise.reject(new Error("sem-crypto"));
|
||||
if (!d || d.algoritmo !== "SHA-256" || !HEX.test(d.desafio || "") || typeof d.sal !== "string"
|
||||
|| typeof d.maximo !== "number" || d.maximo < 0 || d.maximo > 5000000) {
|
||||
return Promise.reject(new Error("desafio-invalido"));
|
||||
}
|
||||
var alvo = bytesDoHex(d.desafio);
|
||||
var cod = new Codificador();
|
||||
|
||||
function lote(inicio) {
|
||||
if (inicio > d.maximo) return Promise.reject(new Error("sem-solucao"));
|
||||
var fim = Math.min(inicio + LOTE - 1, d.maximo);
|
||||
var hashes = [];
|
||||
for (var n = inicio; n <= fim; n++) hashes.push(subtle.digest("SHA-256", cod.encode(d.sal + n)));
|
||||
return Promise.all(hashes).then(function (hs) {
|
||||
for (var i = 0; i < hs.length; i++) {
|
||||
if (iguais(hs[i], alvo)) return { numero: inicio + i, sal: d.sal, assinatura: d.assinatura };
|
||||
}
|
||||
return lote(fim + 1);
|
||||
});
|
||||
}
|
||||
return lote(0);
|
||||
}
|
||||
|
||||
global.AtmDesafio = { resolver: resolver };
|
||||
})(window);
|
||||
|
||||
fontes--fontes.css: |
|
||||
/* ============================================================
|
||||
* As fontes da landing page, servidas POR NOS.
|
||||
@@ -609,9 +935,7 @@ data:
|
||||
|
||||
/* marca */
|
||||
.logo{display:inline-flex; align-items:center; gap:11px; font-weight:700; font-size:1.22rem; letter-spacing:-.02em; color:var(--ink); text-decoration:none;}
|
||||
.mark{display:inline-flex; gap:3px; align-items:flex-end;}
|
||||
.mark i{display:block; width:5px; border-radius:2px; background:var(--laranja); transform:skewX(-14deg);}
|
||||
.mark i:nth-child(1){height:13px} .mark i:nth-child(2){height:19px} .mark i:nth-child(3){height:25px}
|
||||
.logo img{display:block; height:37px; width:auto;}
|
||||
|
||||
.topbar{background:var(--surface); border-bottom:1px solid var(--line);}
|
||||
.topbar .wrap{display:flex; align-items:center; justify-content:space-between; gap:16px; padding-block:14px;}
|
||||
@@ -814,7 +1138,7 @@ data:
|
||||
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="logo" href="#"><span class="mark"><i></i><i></i><i></i></span>Athletic Map</a>
|
||||
<a class="logo" href="#" aria-label="Athletic Map"><img src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37"></a>
|
||||
<a class="zap" id="topZap" href="#form">Falar com o time →</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1698,6 +2022,20 @@ data:
|
||||
.termos-uso-caixa input { margin-right: 6px; }
|
||||
.termos-uso-aceite { margin: 10px 0; }
|
||||
|
||||
/* ── Abas (2026-09-29) — mesmo desenho do painel do dono ───────────────────── */
|
||||
.abas { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin: 4px 0 18px; overflow-x: auto; }
|
||||
.aba {
|
||||
background: none; border: 0; border-bottom: 3px solid transparent; padding: 10px 16px;
|
||||
font: inherit; font-weight: 600; color: var(--muted); cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.aba:hover { color: var(--navy); }
|
||||
.aba.ativa { color: var(--navy); border-bottom-color: var(--orange); }
|
||||
.aba .badge {
|
||||
display: inline-block; min-width: 8px; height: 8px; margin-left: 6px; border-radius: 999px;
|
||||
background: var(--orange); vertical-align: middle;
|
||||
}
|
||||
.aba .badge.num { min-width: 18px; height: auto; padding: 0 6px; font-size: 12px; color: #fff; line-height: 18px; }
|
||||
|
||||
minha-conta.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
@@ -1722,7 +2060,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.88">
|
||||
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.90">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1765,23 +2103,56 @@ 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>
|
||||
<!-- Abas (2026-09-29, pedido do dono): a pagina empilhava visao geral, Termos,
|
||||
escolas ativas e canceladas numa coluna so. Mesmo padrao do painel do dono. -->
|
||||
<nav class="abas" role="tablist" aria-label="Seções da conta">
|
||||
<button type="button" class="aba ativa" data-aba="escolas" role="tab">Minhas escolas</button>
|
||||
<button type="button" class="aba" data-aba="visao" role="tab">Visão geral</button>
|
||||
<button type="button" class="aba" data-aba="termos" role="tab">Termos de Uso <span id="termosBadge" class="badge oculto"></span></button>
|
||||
<button type="button" class="aba oculto" data-aba="canceladas" role="tab" id="abaCanceladas">Canceladas</button>
|
||||
</nav>
|
||||
|
||||
<div id="produtos" class="produtos"></div>
|
||||
<div id="aba-escolas" class="painel-aba">
|
||||
<div id="produtos" class="produtos"></div>
|
||||
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-visao" class="painel-aba 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="visaoVazia" class="cartao">
|
||||
<h2>Visão <span class="hl">geral</span></h2>
|
||||
<p class="ajuda">Os números aparecem aqui quando as suas escolinhas enviarem o primeiro resumo
|
||||
(uma vez por dia, de madrugada).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-termos" class="painel-aba oculto">
|
||||
<div id="termosDeUso"></div>
|
||||
<div id="termosEmDia" class="cartao oculto">
|
||||
<h2>Termos de Uso</h2>
|
||||
<p class="ajuda">Nada pendente: a versão vigente já foi aceita para todas as suas escolinhas.
|
||||
<a href="/termos" target="_blank" rel="noopener">Ler os Termos de Uso</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-canceladas" class="painel-aba oculto">
|
||||
<p class="ajuda">Assinaturas canceladas. Regularize o pagamento para voltar a entrar; os dados ficam
|
||||
guardados por 30 dias depois do cancelamento.</p>
|
||||
<div id="produtosCancelados" class="produtos"></div>
|
||||
</div>
|
||||
|
||||
<p class="rodape">
|
||||
@@ -1801,7 +2172,7 @@ data:
|
||||
|
||||
<footer class="direitos">© 2026 Athletic Map. Todos os direitos reservados.</footer>
|
||||
|
||||
<script src="/publico/minha-conta.js?v=2.88"></script>
|
||||
<script src="/publico/minha-conta.js?v=2.90"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1870,6 +2241,36 @@ data:
|
||||
var erroTexto = document.getElementById("erroTexto");
|
||||
var botaoSair = document.getElementById("sair");
|
||||
|
||||
/* ── ABAS (2026-09-29) ────────────────────────────────────────────────────────
|
||||
* Minhas escolas | Visao geral | Termos de Uso | Canceladas. A ultima aba aberta
|
||||
* fica no localStorage (conveniencia deste navegador; sem ela, "escolas"). */
|
||||
var ABA_CHAVE = "atm.conta.aba";
|
||||
function abrirAba(nome) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao || botao.classList.contains("oculto")) { nome = "escolas"; botao = document.querySelector('.aba[data-aba="escolas"]'); }
|
||||
if (!botao) return;
|
||||
document.querySelectorAll(".aba").forEach(function (b) { b.classList.toggle("ativa", b === botao); });
|
||||
document.querySelectorAll(".painel-aba").forEach(function (painel) {
|
||||
painel.classList.toggle("oculto", painel.id !== "aba-" + nome);
|
||||
});
|
||||
try { localStorage.setItem(ABA_CHAVE, nome); } catch (e) { /* aba privada */ }
|
||||
}
|
||||
function abaLembrada() { try { return localStorage.getItem(ABA_CHAVE) || "escolas"; } catch (e) { return "escolas"; } }
|
||||
/* Marca a aba: `pendente` acende a bolinha; `quantos` escreve o numero. */
|
||||
function marcarAba(nome, pendente, quantos) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao) return;
|
||||
var badge = botao.querySelector(".badge");
|
||||
if (badge) {
|
||||
badge.classList.toggle("oculto", !pendente && !quantos);
|
||||
badge.classList.toggle("num", !!quantos);
|
||||
badge.textContent = quantos ? String(quantos) : "";
|
||||
}
|
||||
}
|
||||
document.querySelectorAll(".aba").forEach(function (b) {
|
||||
b.addEventListener("click", function () { abrirAba(b.getAttribute("data-aba")); });
|
||||
});
|
||||
|
||||
function mostrar(qual) {
|
||||
[porta, carregando, conteudo, caixaErro].forEach(function (s) {
|
||||
if (s) s.classList.add("oculto");
|
||||
@@ -1988,14 +2389,18 @@ data:
|
||||
return n ? n.charAt(0).toUpperCase() + n.slice(1) : "—";
|
||||
}
|
||||
|
||||
/* Tenant de DEMONSTRACAO do dono: o servidor marca (`demonstracao: true`). O
|
||||
cartao diz isso ao lado da situacao, e nao ganha "Trocar de plano" nem
|
||||
"Modulos avulsos" — nao ha cobranca, e o servidor recusaria de qualquer jeito. */
|
||||
function cartao(p) {
|
||||
var s = SITUACAO[p.situacao] || ["neutro", String(p.situacao || "").toLowerCase()];
|
||||
var podeEntrar = p.endereco && p.situacao === "ATIVA";
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + '"'
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + (p.demonstracao ? " demonstracao" : "") + '"'
|
||||
+ (p.tenant ? ' data-tenant="' + escapar(p.tenant) + '"' : "") + ">"
|
||||
+ '<span class="faixa-plano">Plano ' + escapar(nomeDoPlano(p.plano)) + "</span>"
|
||||
+ "<h2>" + escapar(p.escolinha || "Sua escolinha") + "</h2>"
|
||||
+ '<span class="selo ' + s[0] + '">' + s[1] + "</span>"
|
||||
+ (p.demonstracao ? '<span class="selo neutro">demonstração — sem cobrança</span>' : "")
|
||||
+ (podeEntrar
|
||||
? '<a class="btn" href="' + escapar(p.endereco) + '">Entrar →</a>'
|
||||
: '<p class="ajuda">' + (p.situacao === "PREPARANDO"
|
||||
@@ -2014,10 +2419,22 @@ data:
|
||||
.then(function (lista) {
|
||||
enderecos = {};
|
||||
(lista || []).forEach(function (p) { if (p.tenant) enderecos[p.tenant] = p.endereco; });
|
||||
produtos.innerHTML = (lista || []).map(cartao).join("");
|
||||
nenhum.classList.toggle("oculto", (lista || []).length > 0);
|
||||
var ativas = (lista || []).filter(function (p) { return p.situacao !== "CANCELADA"; });
|
||||
var canceladas = (lista || []).filter(function (p) { return p.situacao === "CANCELADA"; });
|
||||
produtos.innerHTML = ativas.map(cartao).join("");
|
||||
var caixaCanceladas = document.getElementById("produtosCancelados");
|
||||
if (caixaCanceladas) caixaCanceladas.innerHTML = canceladas.map(cartao).join("");
|
||||
var abaCanceladas = document.getElementById("abaCanceladas");
|
||||
if (abaCanceladas) {
|
||||
abaCanceladas.classList.toggle("oculto", canceladas.length === 0);
|
||||
abaCanceladas.textContent = "Canceladas" + (canceladas.length ? " (" + canceladas.length + ")" : "");
|
||||
}
|
||||
nenhum.classList.toggle("oculto", ativas.length > 0);
|
||||
mostrar(conteudo);
|
||||
(lista || []).forEach(function (p) { if (p.tenant && p.situacao === "ATIVA") acrescentarBotaoDePlano(p.tenant); });
|
||||
abrirAba(abaLembrada());
|
||||
(lista || []).forEach(function (p) {
|
||||
if (p.tenant && p.situacao === "ATIVA" && !p.demonstracao) acrescentarBotaoDePlano(p.tenant);
|
||||
});
|
||||
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas(), carregarVisao(),
|
||||
carregarTermosDeUso()]);
|
||||
});
|
||||
@@ -2323,8 +2740,10 @@ data:
|
||||
.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; });
|
||||
var emDia = document.getElementById("termosEmDia");
|
||||
var faltam = (d && d.versao && d.escolinhas) ? d.escolinhas.filter(function (e) { return !e.aceito; }) : [];
|
||||
if (emDia) emDia.classList.toggle("oculto", faltam.length > 0);
|
||||
marcarAba("termos", faltam.length > 0);
|
||||
if (!faltam.length) return;
|
||||
var aviso = document.createElement("section");
|
||||
aviso.id = "avisoTermos";
|
||||
@@ -2335,7 +2754,8 @@ data:
|
||||
+ ". 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);
|
||||
var caixaTermos = document.getElementById("termosDeUso");
|
||||
if (caixaTermos) caixaTermos.appendChild(aviso); else produtos.parentNode.insertBefore(aviso, produtos);
|
||||
aviso.querySelector("button").addEventListener("click", function () {
|
||||
aceitarTermosDeUso(aviso, d.versao);
|
||||
});
|
||||
@@ -2588,7 +3008,9 @@ data:
|
||||
.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 vazia = document.getElementById("visaoVazia");
|
||||
if (!tabela) { caixa.classList.add("oculto"); if (vazia) vazia.classList.remove("oculto"); return; }
|
||||
if (vazia) vazia.classList.add("oculto");
|
||||
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.";
|
||||
@@ -3171,7 +3593,7 @@ data:
|
||||
<option value="NASCENDO">Nascendo</option>
|
||||
</select>
|
||||
<label class="filtro"><input type="checkbox" id="soAtencao"> só com alerta</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes e demonstrações</label>
|
||||
</div>
|
||||
<div class="tabela-rolagem">
|
||||
<table class="tabela clientes">
|
||||
@@ -3334,7 +3756,7 @@ data:
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/publico/painel.js?v=2.86"></script>
|
||||
<script src="/publico/painel.js?v=2.89"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3630,9 +4052,13 @@ data:
|
||||
bloco("Recebido desde o início", reaisCurto(i.recebidoTotal),
|
||||
i.clientesSemDadoDoAsaas > 0 ? i.clientesSemDadoDoAsaas + " sem resposta do Asaas" : "extrato do Asaas", "")
|
||||
].join("");
|
||||
// Teste e demonstracao aparecem na lista, mas nao nos numeros — e a tela diz.
|
||||
var foraDosNumeros = [];
|
||||
if (i.contasDeTeste > 0) foraDosNumeros.push(i.contasDeTeste + " de teste");
|
||||
if (i.demonstracoes > 0) foraDosNumeros.push(i.demonstracoes + " de demonstração");
|
||||
$("kpiClientes").innerHTML = [
|
||||
bloco("Clientes ativos", esc(i.clientesAtivos),
|
||||
i.contasDeTeste > 0 ? "+ " + i.contasDeTeste + " de teste (fora dos números)" : "", "ok"),
|
||||
foraDosNumeros.length ? "+ " + foraDosNumeros.join(", ") + " (fora dos números)" : "", "ok"),
|
||||
bloco("Novos no mês", esc(i.novosNoMes), comparacao(i.novosNoMes, i.novosMesAnterior), ""),
|
||||
bloco("Leads no mês", esc(i.leadsNoMes), comparacao(i.leadsNoMes, i.leadsMesAnterior), ""),
|
||||
bloco("Conversão", pct(i.conversao), "de lead para cliente pagante", ""),
|
||||
@@ -3671,7 +4097,7 @@ data:
|
||||
if (plano && c.plano !== plano) return false;
|
||||
if (estado && (c.estado || "NASCENDO") !== estado) return false;
|
||||
if (soAtencao && !(c.alertas || []).length) return false;
|
||||
if (semTeste && c.teste) return false;
|
||||
if (semTeste && (c.teste || c.demonstracao)) return false;
|
||||
if (busca) {
|
||||
var alvo = [c.cliente, c.escolinha, c.email, c.cidade, c.tenant, c.whatsapp, c.documento]
|
||||
.join(" ").toLowerCase();
|
||||
@@ -3701,7 +4127,9 @@ data:
|
||||
}
|
||||
|
||||
var SITUACAO_ASAAS = { ACTIVE: "ativa", INACTIVE: "encerrada", EXPIRED: "expirada" };
|
||||
function assinaturaNoAsaas(a) {
|
||||
/* A demonstracao nao existe no Asaas DE PROPOSITO: nao e "nao existe" em vermelho. */
|
||||
function assinaturaNoAsaas(a, c) {
|
||||
if (c && c.demonstracao) return "demonstração — sem cobrança";
|
||||
if (!a || !a.disponivel) return "indisponível agora";
|
||||
if (!a.existe) return '<b class="vermelho">não existe</b>';
|
||||
var s = SITUACAO_ASAAS[a.situacao] || txt(a.situacao);
|
||||
@@ -3738,10 +4166,11 @@ data:
|
||||
var gravidade = (c.alertas || []).reduce(function (g, a) {
|
||||
return a.gravidade === "critico" ? "critico" : (g === "critico" ? g : (a.gravidade === "alerta" ? "alerta" : g));
|
||||
}, "");
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste ? " teste" : "")
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste || c.demonstracao ? " teste" : "")
|
||||
+ (!c.tenant ? " destaque" : "") + '" data-assinatura="' + esc(c.assinatura) + '" tabindex="0">'
|
||||
+ "<td><b>" + txt(c.cliente) + "</b>"
|
||||
+ (c.teste ? ' <span class="selo neutro">teste</span>' : "")
|
||||
+ (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "")
|
||||
+ '<span class="sub">' + (c.tenant
|
||||
? '<a href="' + esc(c.endereco) + '" target="_blank" rel="noopener noreferrer">' + esc(c.tenant) + "</a>"
|
||||
: "sem tenant")
|
||||
@@ -3750,9 +4179,9 @@ data:
|
||||
+ "<td>" + etiquetaPlano(c) + "</td>"
|
||||
+ "<td>" + selo(c.estado, c.tenant) + '<span class="sub">' + (c.diasDeCliente !== null && c.diasDeCliente !== undefined
|
||||
? "cliente há " + esc(c.diasDeCliente) + " dia(s)" : "") + "</span></td>"
|
||||
+ '<td class="num">' + reais(c.valorMensal) + diferencaDeTabela(c) + "</td>"
|
||||
+ '<td class="num">' + (f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + proximaCobranca(f) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? '<span class="ajuda">sem cobrança</span>' : reais(c.valorMensal) + diferencaDeTabela(c)) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? "—" : f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + (c.demonstracao ? '<span class="ajuda">—</span>' : proximaCobranca(f)) + "</td>"
|
||||
+ "<td>" + usoCurto(c.uso, c.usoDeDentro) + "</td>"
|
||||
+ "<td>" + seloSubconta(c.subconta) + "</td>"
|
||||
+ "</tr>";
|
||||
@@ -3778,7 +4207,9 @@ data:
|
||||
+ esc(a.titulo) + '</b><span class="sub">' + esc(a.detalhe) + "</span></div></li>";
|
||||
}).join("");
|
||||
|
||||
var extrato = f.disponivel
|
||||
var extrato = c.demonstracao
|
||||
? '<p class="ajuda">Tenant de demonstração: não há assinatura nem cobrança no Asaas.</p>'
|
||||
: f.disponivel
|
||||
? ((f.cobrancas || []).length
|
||||
? '<table class="tabela"><thead><tr><th>Vencimento</th><th>Pago em</th><th>Forma</th><th class="num">Valor</th><th class="num">Líquido</th><th>Situação</th><th></th></tr></thead><tbody>'
|
||||
+ f.cobrancas.map(function (cb) {
|
||||
@@ -3799,7 +4230,7 @@ data:
|
||||
return '<header class="ficha-topo ' + classePlano(c.plano) + '">'
|
||||
+ '<span class="etiqueta ' + classePlano(c.plano) + '">Plano ' + esc(nomePlano(c.plano))
|
||||
+ (c.ciclo === "ANUAL" ? " · anual" : " · mensal") + "</span>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + "</h2>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "") + "</h2>"
|
||||
+ '<p class="sub">' + (c.escolinha && c.escolinha !== c.cliente ? esc(c.escolinha) + " · " : "")
|
||||
+ selo(c.estado, c.tenant) + (c.estadoDesde ? " desde " + dataHora(c.estadoDesde) : "") + "</p>"
|
||||
+ '<div class="atalhos">'
|
||||
@@ -3807,7 +4238,9 @@ data:
|
||||
+ (whatsapp(c.whatsapp) ? '<a class="btn claro" href="' + esc(whatsapp(c.whatsapp)) + '" target="_blank" rel="noopener noreferrer">WhatsApp</a>' : "")
|
||||
+ (c.email ? '<a class="btn claro" href="mailto:' + esc(c.email) + '">E-mail</a>' : "")
|
||||
+ (f.faturaVencida ? '<a class="btn claro" href="' + esc(linkSeguro(f.faturaVencida) || "#") + '" target="_blank" rel="noopener noreferrer">Fatura vencida</a>' : "")
|
||||
+ "</div></header>"
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="reenviar">Reenviar licença</button>'
|
||||
+ '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="trocar">Trocar chave</button>' : "")
|
||||
+ '</div><p id="licencaAviso" class="ajuda" aria-live="polite"></p></header>'
|
||||
+ (alertas ? '<ul class="lista-alertas">' + alertas + "</ul>" : "")
|
||||
|
||||
+ '<div class="grade-ficha">'
|
||||
@@ -3821,14 +4254,17 @@ data:
|
||||
+ '<section><h3>Assinatura</h3><dl>'
|
||||
+ item("Plano", esc(nomePlano(c.plano)) + " " + (c.ciclo === "ANUAL" ? "anual" : "mensal"))
|
||||
+ item("Valor do ciclo", reais(c.valorDoCiclo)) + item("Vale por mês", reais(c.valorMensal) + diferencaDeTabela(c))
|
||||
+ item("De onde vem o valor", c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas))
|
||||
+ item("De onde vem o valor", c.demonstracao ? "demonstração — sem cobrança"
|
||||
: c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas, c))
|
||||
+ item("Contratado em", dataHora(c.contratadoEm)) + item("Cliente há", c.diasDeCliente === null || c.diasDeCliente === undefined ? "—" : esc(c.diasDeCliente) + " dia(s)")
|
||||
+ item("Assinatura no Asaas", txt(c.assinatura)) + item("Cliente no Asaas", txt(c.clienteNoProvedor))
|
||||
+ "</dl></section>"
|
||||
|
||||
+ '<section><h3>Dinheiro</h3><dl>'
|
||||
+ (f.disponivel
|
||||
+ (c.demonstracao
|
||||
? item("Cobrança", "nenhuma — tenant de demonstração")
|
||||
: f.disponivel
|
||||
? item("Total pago", reais(f.totalPago)) + item("Líquido (após taxas)", reais(f.totalLiquido))
|
||||
+ item("Taxas pagas", reais(f.taxas))
|
||||
+ item("Último pagamento", f.ultimoPagamento ? data(f.ultimoPagamento.data) + " · " + reais(f.ultimoPagamento.valor) + " · " + txt(f.ultimoPagamento.forma) : "—")
|
||||
@@ -4325,6 +4761,55 @@ data:
|
||||
corpo.addEventListener("keydown", function (ev) { if (ev.key === "Enter") aoEscolher(ev); });
|
||||
$("alertas").addEventListener("click", aoEscolher);
|
||||
|
||||
/* ── licenca dos apps (dono, treinador, pais) ─────────────────────────────
|
||||
* Reenviar = mandar a MESMA chave da escola ao dono da compra (captacao 4.32+;
|
||||
* so a chave antiga, guardada so com hash, e trocada uma vez). Nada muda para
|
||||
* ninguem: sem confirmacao.
|
||||
* Trocar = revogar a chave e gerar outra. Desde os apps 1.0.4 (captacao 4.33)
|
||||
* o app pergunta a situacao da chave ao abrir: trocar DERRUBA os apps de todos
|
||||
* da escola (dono, treinadores, pais) em ate 15 min de uso, e cada um precisa
|
||||
* da chave nova. Por isso pede o nome do tenant digitado. */
|
||||
var API_LICENCA = "/api/plataforma/painel/licencas/";
|
||||
|
||||
function acaoDaLicenca(tenant, acao, aviso) {
|
||||
if (!tokenAtual) return Promise.resolve();
|
||||
if (acao === "trocar") {
|
||||
var digitado = window.prompt("TROCAR a chave de " + tenant + "?\n\n"
|
||||
+ "A chave atual para de valer e os apps de TODOS da escola (dono, treinadores e pais) saem da conta "
|
||||
+ "em até 15 minutos de uso; cada um vai precisar da chave nova, que vai por e-mail ao dono da compra.\n"
|
||||
+ "Use só se a chave vazou.\n\nPara confirmar, digite o nome do tenant (" + tenant + "):");
|
||||
if (digitado === null) return Promise.resolve();
|
||||
if (digitado.trim() !== tenant) {
|
||||
aviso.textContent = "Nada foi trocado: o nome digitado não confere.";
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
aviso.textContent = "Enviando…";
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/" + acao, {
|
||||
method: "POST", headers: { "x-atm-admin-token": tokenAtual, "Content-Type": "application/json" }, body: "{}"
|
||||
}).then(function (r) {
|
||||
if (r.status === 401) throw new Error("O token expirou. Saia e entre de novo no painel.");
|
||||
return r.json().then(function (d) {
|
||||
if (!r.ok) throw new Error((d && d.detail) || ("O servidor recusou (" + r.status + ")."));
|
||||
return d;
|
||||
});
|
||||
}).then(function (d) {
|
||||
var oQue = acao === "trocar" ? "Chave NOVA" : "Chave da escola";
|
||||
aviso.textContent = oQue + (d.emailEnviado ? " enviada para " : " pronta; o e-mail vai em instantes para ")
|
||||
+ (d.destino || "o dono da compra") + " (termina em " + d.finalDaChave + ").";
|
||||
}).catch(function (e) {
|
||||
aviso.textContent = e && e.message && e.message !== "Failed to fetch" ? e.message : "Sem conexão com o servidor.";
|
||||
});
|
||||
}
|
||||
|
||||
$("fichaCorpo").addEventListener("click", function (ev) {
|
||||
var b = ev.target.closest ? ev.target.closest("[data-licenca]") : null;
|
||||
if (!b) return;
|
||||
b.disabled = true;
|
||||
acaoDaLicenca(b.getAttribute("data-licenca"), b.getAttribute("data-acao") || "reenviar", $("licencaAviso"))
|
||||
.then(function () { b.disabled = false; });
|
||||
});
|
||||
|
||||
$("fecharFicha").addEventListener("click", fecharFicha);
|
||||
$("ficha").addEventListener("click", function (ev) { if (ev.target === $("ficha")) fecharFicha(); });
|
||||
document.addEventListener("keydown", function (ev) { if (ev.key === "Escape") fecharFicha(); });
|
||||
|
||||
@@ -370,6 +370,332 @@ data:
|
||||
});
|
||||
})();
|
||||
|
||||
convite.css: |
|
||||
/* Convite para competição: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). */
|
||||
main { max-width: 38rem; }
|
||||
.explica { color: var(--muted); }
|
||||
form { display: grid; gap: 1rem; }
|
||||
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
|
||||
legend { font-weight: 700; padding: 0 .4rem; }
|
||||
label { display: grid; gap: .2rem; font-size: .92rem; font-weight: 600; }
|
||||
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
|
||||
input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
|
||||
.linha-jogador { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
|
||||
.linha-jogador button { padding: .4rem .6rem; }
|
||||
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
|
||||
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
|
||||
button:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.erro { color: #b42318; margin: 0; font-weight: 600; }
|
||||
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: 1rem 1.2rem; }
|
||||
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
|
||||
|
||||
convite.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, nofollow">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Convite para competição — Athletic Map</title>
|
||||
<link rel="stylesheet" href="/publico/_estilo.css">
|
||||
<link rel="stylesheet" href="/publico/convite.css?v=1">
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="marca">Athletic <span>Map</span></div></header>
|
||||
<main>
|
||||
<h1 id="titulo">Convite para competição</h1>
|
||||
<p id="situacao" role="status" aria-live="polite">Abrindo o convite…</p>
|
||||
|
||||
<form id="formulario" hidden novalidate>
|
||||
<p class="explica">
|
||||
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
|
||||
A organização confere e aprova antes do time entrar na competição.
|
||||
</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Seu time</legend>
|
||||
<label>Nome do time *<input id="nome" maxlength="120" autocomplete="off" required></label>
|
||||
<label>Escola ou clube<input id="escola" maxlength="160" autocomplete="organization"></label>
|
||||
<label>Cidade<input id="cidade" maxlength="120" autocomplete="address-level2"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Responsável pelo time</legend>
|
||||
<label>Nome<input id="contatoNome" maxlength="120" autocomplete="name"></label>
|
||||
<label>Telefone<input id="contatoTelefone" maxlength="40" autocomplete="tel" inputmode="tel"></label>
|
||||
<label>E-mail<input id="contatoEmail" type="email" maxlength="160" autocomplete="email"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Jogadores</legend>
|
||||
<div id="jogadores"></div>
|
||||
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
|
||||
</fieldset>
|
||||
|
||||
<p id="erro" class="erro" role="alert" hidden></p>
|
||||
<button type="submit" id="enviar">Enviar time</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/publico/desafio.js?v=1"></script>
|
||||
<script src="/publico/convite.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
convite.js: |
|
||||
/* ===========================================================================
|
||||
* Convite para competição — a página que a OUTRA escola abre pelo link.
|
||||
*
|
||||
* Sem login: o token no endereço é o único segredo. A página pergunta ao
|
||||
* servidor a que o convite se refere e, se ele vale, deixa a pessoa mandar o
|
||||
* time com os jogadores (nome e camisa). O time chega PENDENTE: quem organiza
|
||||
* confere e aprova.
|
||||
*
|
||||
* Antes de enviar, a verificação anti-robô (desafio.js): pede um desafio ao
|
||||
* servidor, resolve no navegador ("Verificando…") e manda a prova junto com o
|
||||
* time, em `prova`. Um desafio por tentativa: ele é de uso único.
|
||||
*
|
||||
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
|
||||
* HTML. O script é externo porque a CSP é `script-src 'self'`.
|
||||
* =========================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var global = window;
|
||||
|
||||
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
|
||||
var MAX_JOGADORES = 60;
|
||||
|
||||
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
|
||||
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
|
||||
|
||||
var titulo = document.getElementById("titulo");
|
||||
var situacao = document.getElementById("situacao");
|
||||
var formulario = document.getElementById("formulario");
|
||||
var listaJogadores = document.getElementById("jogadores");
|
||||
var erro = document.getElementById("erro");
|
||||
var enviar = document.getElementById("enviar");
|
||||
var ROTULO_ENVIAR = enviar.textContent;
|
||||
|
||||
function recusar(texto) {
|
||||
situacao.textContent = texto;
|
||||
situacao.className = "invalido";
|
||||
formulario.hidden = true;
|
||||
}
|
||||
|
||||
function mostrarErro(texto) {
|
||||
erro.textContent = texto;
|
||||
erro.hidden = !texto;
|
||||
}
|
||||
|
||||
function linhaDeJogador() {
|
||||
var linha = document.createElement("div");
|
||||
linha.className = "linha-jogador";
|
||||
|
||||
var nome = document.createElement("input");
|
||||
nome.placeholder = "Nome do jogador";
|
||||
nome.maxLength = 120;
|
||||
nome.setAttribute("aria-label", "Nome do jogador");
|
||||
nome.setAttribute("data-campo", "nome");
|
||||
|
||||
var camisa = document.createElement("input");
|
||||
camisa.type = "number";
|
||||
camisa.min = "0";
|
||||
camisa.max = "999";
|
||||
camisa.placeholder = "nº";
|
||||
camisa.setAttribute("aria-label", "Número da camisa");
|
||||
camisa.setAttribute("data-campo", "camisa");
|
||||
|
||||
var tirar = document.createElement("button");
|
||||
tirar.type = "button";
|
||||
tirar.className = "secundario";
|
||||
tirar.textContent = "×";
|
||||
tirar.setAttribute("aria-label", "Remover jogador");
|
||||
tirar.addEventListener("click", function () { linha.remove(); });
|
||||
|
||||
linha.appendChild(nome);
|
||||
linha.appendChild(camisa);
|
||||
linha.appendChild(tirar);
|
||||
listaJogadores.appendChild(linha);
|
||||
return linha;
|
||||
}
|
||||
|
||||
function valor(id) {
|
||||
var v = document.getElementById(id).value.trim();
|
||||
return v === "" ? null : v;
|
||||
}
|
||||
|
||||
function montarCorpo() {
|
||||
var jogadores = [];
|
||||
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
|
||||
for (var i = 0; i < linhas.length; i++) {
|
||||
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
|
||||
if (!nome) continue;
|
||||
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
|
||||
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
|
||||
}
|
||||
return {
|
||||
nome: valor("nome"),
|
||||
escola: valor("escola"),
|
||||
cidade: valor("cidade"),
|
||||
contatoNome: valor("contatoNome"),
|
||||
contatoTelefone: valor("contatoTelefone"),
|
||||
contatoEmail: valor("contatoEmail"),
|
||||
jogadores: jogadores
|
||||
};
|
||||
}
|
||||
|
||||
function lerResposta(r) {
|
||||
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
|
||||
}
|
||||
|
||||
function aoEnviar(ev) {
|
||||
ev.preventDefault();
|
||||
mostrarErro("");
|
||||
var corpo = montarCorpo();
|
||||
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
|
||||
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
|
||||
enviar.disabled = true;
|
||||
enviar.textContent = "Verificando…";
|
||||
enviar.setAttribute("aria-busy", "true");
|
||||
|
||||
function voltar(texto) {
|
||||
enviar.disabled = false;
|
||||
enviar.textContent = ROTULO_ENVIAR;
|
||||
enviar.removeAttribute("aria-busy");
|
||||
mostrarErro(texto);
|
||||
}
|
||||
|
||||
fetch(base + "/desafio").then(lerResposta).then(function (r) {
|
||||
if (r.status !== 200) {
|
||||
var e = new Error("recusado");
|
||||
e.detalhe = (r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.";
|
||||
throw e;
|
||||
}
|
||||
if (!global.AtmDesafio) throw new Error("sem-crypto");
|
||||
return global.AtmDesafio.resolver(r.corpo);
|
||||
}).then(function (prova) {
|
||||
corpo.prova = prova;
|
||||
enviar.textContent = "Enviando…";
|
||||
return fetch(base + "/times", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(corpo)
|
||||
}).then(lerResposta);
|
||||
}).then(function (r) {
|
||||
if (r.status >= 200 && r.status < 300) {
|
||||
formulario.hidden = true;
|
||||
titulo.textContent = "Time enviado";
|
||||
situacao.textContent = "Recebemos o time \"" + corpo.nome + "\". A organização vai conferir os dados e aprovar. Você já pode fechar esta página.";
|
||||
situacao.className = "ok";
|
||||
return;
|
||||
}
|
||||
voltar((r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.");
|
||||
}).catch(function (e) {
|
||||
if (e && e.detalhe) { voltar(e.detalhe); return; }
|
||||
if (e && (e.message === "sem-crypto" || e.message === "desafio-invalido" || e.message === "sem-solucao")) {
|
||||
voltar("Não foi possível fazer a verificação anti-robô neste navegador. Atualize a página ou use outro navegador.");
|
||||
return;
|
||||
}
|
||||
voltar("Sem conexão. Confira a internet e tente de novo.");
|
||||
});
|
||||
}
|
||||
|
||||
function iniciar() {
|
||||
if (!TOKEN.test(token)) { recusar("Este endereço de convite não é válido."); return; }
|
||||
fetch(base).then(lerResposta).then(function (r) {
|
||||
var info = r.corpo || {};
|
||||
if (!info.valido) {
|
||||
recusar(info.motivo ? "Não dá para usar este convite: " + info.motivo + "." : "Este convite não está disponível.");
|
||||
return;
|
||||
}
|
||||
titulo.textContent = "Convite: " + (info.competicao || "competição");
|
||||
situacao.textContent = (info.temporada ? "Temporada " + info.temporada + ". " : "")
|
||||
+ "Restam " + info.vagasRestantes + " vaga(s) neste convite.";
|
||||
situacao.className = "";
|
||||
formulario.hidden = false;
|
||||
for (var i = 0; i < 5; i++) linhaDeJogador();
|
||||
}).catch(function () {
|
||||
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("maisJogador").addEventListener("click", function () {
|
||||
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
|
||||
});
|
||||
formulario.addEventListener("submit", aoEnviar);
|
||||
iniciar();
|
||||
})();
|
||||
|
||||
desafio.js: |
|
||||
/* ===========================================================================
|
||||
* Verificação anti-robô das páginas públicas — prova de trabalho no estilo
|
||||
* ALTCHA, sem serviço de terceiro (nada de quem preenche sai do Athletic Map).
|
||||
*
|
||||
* O servidor manda { algoritmo: "SHA-256", desafio, sal, assinatura, maximo }.
|
||||
* Aqui se procura o número n (0..maximo) tal que sha256(sal + n) == desafio, e
|
||||
* devolve-se { numero, sal, assinatura } para ir junto no envio. Em média são
|
||||
* maximo/2 hashes: ~1–2 s num celular com maximo = 150 000.
|
||||
*
|
||||
* Usa crypto.subtle (só existe em HTTPS — e as páginas públicas são HTTPS).
|
||||
* Os hashes saem em lotes paralelos: um `await` por hash custaria mais que o
|
||||
* próprio hash. Script externo porque a CSP é `script-src 'self'`.
|
||||
*
|
||||
* O teste do servidor é DesafioDoConvite (servico-campeonato).
|
||||
* =========================================================================== */
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var LOTE = 1000;
|
||||
var HEX = /^[0-9a-f]{64}$/;
|
||||
|
||||
function bytesDoHex(h) {
|
||||
var b = new Uint8Array(h.length / 2);
|
||||
for (var i = 0; i < b.length; i++) b[i] = parseInt(h.substr(i * 2, 2), 16);
|
||||
return b;
|
||||
}
|
||||
|
||||
function iguais(buf, alvo) {
|
||||
var a = new Uint8Array(buf);
|
||||
if (a.length !== alvo.length) return false;
|
||||
for (var i = 0; i < a.length; i++) if (a[i] !== alvo[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{algoritmo:string, desafio:string, sal:string, assinatura:string, maximo:number}} d
|
||||
* @returns {Promise<{numero:number, sal:string, assinatura:string}>}
|
||||
* rejeita com Error("sem-crypto"), Error("desafio-invalido") ou Error("sem-solucao")
|
||||
*/
|
||||
function resolver(d) {
|
||||
var subtle = global.crypto && global.crypto.subtle;
|
||||
var Codificador = global.TextEncoder;
|
||||
if (!subtle || !Codificador) return Promise.reject(new Error("sem-crypto"));
|
||||
if (!d || d.algoritmo !== "SHA-256" || !HEX.test(d.desafio || "") || typeof d.sal !== "string"
|
||||
|| typeof d.maximo !== "number" || d.maximo < 0 || d.maximo > 5000000) {
|
||||
return Promise.reject(new Error("desafio-invalido"));
|
||||
}
|
||||
var alvo = bytesDoHex(d.desafio);
|
||||
var cod = new Codificador();
|
||||
|
||||
function lote(inicio) {
|
||||
if (inicio > d.maximo) return Promise.reject(new Error("sem-solucao"));
|
||||
var fim = Math.min(inicio + LOTE - 1, d.maximo);
|
||||
var hashes = [];
|
||||
for (var n = inicio; n <= fim; n++) hashes.push(subtle.digest("SHA-256", cod.encode(d.sal + n)));
|
||||
return Promise.all(hashes).then(function (hs) {
|
||||
for (var i = 0; i < hs.length; i++) {
|
||||
if (iguais(hs[i], alvo)) return { numero: inicio + i, sal: d.sal, assinatura: d.assinatura };
|
||||
}
|
||||
return lote(fim + 1);
|
||||
});
|
||||
}
|
||||
return lote(0);
|
||||
}
|
||||
|
||||
global.AtmDesafio = { resolver: resolver };
|
||||
})(window);
|
||||
|
||||
fontes--fontes.css: |
|
||||
/* ============================================================
|
||||
* As fontes da landing page, servidas POR NOS.
|
||||
@@ -609,9 +935,7 @@ data:
|
||||
|
||||
/* marca */
|
||||
.logo{display:inline-flex; align-items:center; gap:11px; font-weight:700; font-size:1.22rem; letter-spacing:-.02em; color:var(--ink); text-decoration:none;}
|
||||
.mark{display:inline-flex; gap:3px; align-items:flex-end;}
|
||||
.mark i{display:block; width:5px; border-radius:2px; background:var(--laranja); transform:skewX(-14deg);}
|
||||
.mark i:nth-child(1){height:13px} .mark i:nth-child(2){height:19px} .mark i:nth-child(3){height:25px}
|
||||
.logo img{display:block; height:37px; width:auto;}
|
||||
|
||||
.topbar{background:var(--surface); border-bottom:1px solid var(--line);}
|
||||
.topbar .wrap{display:flex; align-items:center; justify-content:space-between; gap:16px; padding-block:14px;}
|
||||
@@ -814,7 +1138,7 @@ data:
|
||||
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="logo" href="#"><span class="mark"><i></i><i></i><i></i></span>Athletic Map</a>
|
||||
<a class="logo" href="#" aria-label="Athletic Map"><img src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37"></a>
|
||||
<a class="zap" id="topZap" href="#form">Falar com o time →</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1698,6 +2022,20 @@ data:
|
||||
.termos-uso-caixa input { margin-right: 6px; }
|
||||
.termos-uso-aceite { margin: 10px 0; }
|
||||
|
||||
/* ── Abas (2026-09-29) — mesmo desenho do painel do dono ───────────────────── */
|
||||
.abas { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin: 4px 0 18px; overflow-x: auto; }
|
||||
.aba {
|
||||
background: none; border: 0; border-bottom: 3px solid transparent; padding: 10px 16px;
|
||||
font: inherit; font-weight: 600; color: var(--muted); cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.aba:hover { color: var(--navy); }
|
||||
.aba.ativa { color: var(--navy); border-bottom-color: var(--orange); }
|
||||
.aba .badge {
|
||||
display: inline-block; min-width: 8px; height: 8px; margin-left: 6px; border-radius: 999px;
|
||||
background: var(--orange); vertical-align: middle;
|
||||
}
|
||||
.aba .badge.num { min-width: 18px; height: auto; padding: 0 6px; font-size: 12px; color: #fff; line-height: 18px; }
|
||||
|
||||
minha-conta.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
@@ -1722,7 +2060,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.88">
|
||||
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.90">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1765,23 +2103,56 @@ 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>
|
||||
<!-- Abas (2026-09-29, pedido do dono): a pagina empilhava visao geral, Termos,
|
||||
escolas ativas e canceladas numa coluna so. Mesmo padrao do painel do dono. -->
|
||||
<nav class="abas" role="tablist" aria-label="Seções da conta">
|
||||
<button type="button" class="aba ativa" data-aba="escolas" role="tab">Minhas escolas</button>
|
||||
<button type="button" class="aba" data-aba="visao" role="tab">Visão geral</button>
|
||||
<button type="button" class="aba" data-aba="termos" role="tab">Termos de Uso <span id="termosBadge" class="badge oculto"></span></button>
|
||||
<button type="button" class="aba oculto" data-aba="canceladas" role="tab" id="abaCanceladas">Canceladas</button>
|
||||
</nav>
|
||||
|
||||
<div id="produtos" class="produtos"></div>
|
||||
<div id="aba-escolas" class="painel-aba">
|
||||
<div id="produtos" class="produtos"></div>
|
||||
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-visao" class="painel-aba 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="visaoVazia" class="cartao">
|
||||
<h2>Visão <span class="hl">geral</span></h2>
|
||||
<p class="ajuda">Os números aparecem aqui quando as suas escolinhas enviarem o primeiro resumo
|
||||
(uma vez por dia, de madrugada).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-termos" class="painel-aba oculto">
|
||||
<div id="termosDeUso"></div>
|
||||
<div id="termosEmDia" class="cartao oculto">
|
||||
<h2>Termos de Uso</h2>
|
||||
<p class="ajuda">Nada pendente: a versão vigente já foi aceita para todas as suas escolinhas.
|
||||
<a href="/termos" target="_blank" rel="noopener">Ler os Termos de Uso</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-canceladas" class="painel-aba oculto">
|
||||
<p class="ajuda">Assinaturas canceladas. Regularize o pagamento para voltar a entrar; os dados ficam
|
||||
guardados por 30 dias depois do cancelamento.</p>
|
||||
<div id="produtosCancelados" class="produtos"></div>
|
||||
</div>
|
||||
|
||||
<p class="rodape">
|
||||
@@ -1801,7 +2172,7 @@ data:
|
||||
|
||||
<footer class="direitos">© 2026 Athletic Map. Todos os direitos reservados.</footer>
|
||||
|
||||
<script src="/publico/minha-conta.js?v=2.88"></script>
|
||||
<script src="/publico/minha-conta.js?v=2.90"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1870,6 +2241,36 @@ data:
|
||||
var erroTexto = document.getElementById("erroTexto");
|
||||
var botaoSair = document.getElementById("sair");
|
||||
|
||||
/* ── ABAS (2026-09-29) ────────────────────────────────────────────────────────
|
||||
* Minhas escolas | Visao geral | Termos de Uso | Canceladas. A ultima aba aberta
|
||||
* fica no localStorage (conveniencia deste navegador; sem ela, "escolas"). */
|
||||
var ABA_CHAVE = "atm.conta.aba";
|
||||
function abrirAba(nome) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao || botao.classList.contains("oculto")) { nome = "escolas"; botao = document.querySelector('.aba[data-aba="escolas"]'); }
|
||||
if (!botao) return;
|
||||
document.querySelectorAll(".aba").forEach(function (b) { b.classList.toggle("ativa", b === botao); });
|
||||
document.querySelectorAll(".painel-aba").forEach(function (painel) {
|
||||
painel.classList.toggle("oculto", painel.id !== "aba-" + nome);
|
||||
});
|
||||
try { localStorage.setItem(ABA_CHAVE, nome); } catch (e) { /* aba privada */ }
|
||||
}
|
||||
function abaLembrada() { try { return localStorage.getItem(ABA_CHAVE) || "escolas"; } catch (e) { return "escolas"; } }
|
||||
/* Marca a aba: `pendente` acende a bolinha; `quantos` escreve o numero. */
|
||||
function marcarAba(nome, pendente, quantos) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao) return;
|
||||
var badge = botao.querySelector(".badge");
|
||||
if (badge) {
|
||||
badge.classList.toggle("oculto", !pendente && !quantos);
|
||||
badge.classList.toggle("num", !!quantos);
|
||||
badge.textContent = quantos ? String(quantos) : "";
|
||||
}
|
||||
}
|
||||
document.querySelectorAll(".aba").forEach(function (b) {
|
||||
b.addEventListener("click", function () { abrirAba(b.getAttribute("data-aba")); });
|
||||
});
|
||||
|
||||
function mostrar(qual) {
|
||||
[porta, carregando, conteudo, caixaErro].forEach(function (s) {
|
||||
if (s) s.classList.add("oculto");
|
||||
@@ -1988,14 +2389,18 @@ data:
|
||||
return n ? n.charAt(0).toUpperCase() + n.slice(1) : "—";
|
||||
}
|
||||
|
||||
/* Tenant de DEMONSTRACAO do dono: o servidor marca (`demonstracao: true`). O
|
||||
cartao diz isso ao lado da situacao, e nao ganha "Trocar de plano" nem
|
||||
"Modulos avulsos" — nao ha cobranca, e o servidor recusaria de qualquer jeito. */
|
||||
function cartao(p) {
|
||||
var s = SITUACAO[p.situacao] || ["neutro", String(p.situacao || "").toLowerCase()];
|
||||
var podeEntrar = p.endereco && p.situacao === "ATIVA";
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + '"'
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + (p.demonstracao ? " demonstracao" : "") + '"'
|
||||
+ (p.tenant ? ' data-tenant="' + escapar(p.tenant) + '"' : "") + ">"
|
||||
+ '<span class="faixa-plano">Plano ' + escapar(nomeDoPlano(p.plano)) + "</span>"
|
||||
+ "<h2>" + escapar(p.escolinha || "Sua escolinha") + "</h2>"
|
||||
+ '<span class="selo ' + s[0] + '">' + s[1] + "</span>"
|
||||
+ (p.demonstracao ? '<span class="selo neutro">demonstração — sem cobrança</span>' : "")
|
||||
+ (podeEntrar
|
||||
? '<a class="btn" href="' + escapar(p.endereco) + '">Entrar →</a>'
|
||||
: '<p class="ajuda">' + (p.situacao === "PREPARANDO"
|
||||
@@ -2014,10 +2419,22 @@ data:
|
||||
.then(function (lista) {
|
||||
enderecos = {};
|
||||
(lista || []).forEach(function (p) { if (p.tenant) enderecos[p.tenant] = p.endereco; });
|
||||
produtos.innerHTML = (lista || []).map(cartao).join("");
|
||||
nenhum.classList.toggle("oculto", (lista || []).length > 0);
|
||||
var ativas = (lista || []).filter(function (p) { return p.situacao !== "CANCELADA"; });
|
||||
var canceladas = (lista || []).filter(function (p) { return p.situacao === "CANCELADA"; });
|
||||
produtos.innerHTML = ativas.map(cartao).join("");
|
||||
var caixaCanceladas = document.getElementById("produtosCancelados");
|
||||
if (caixaCanceladas) caixaCanceladas.innerHTML = canceladas.map(cartao).join("");
|
||||
var abaCanceladas = document.getElementById("abaCanceladas");
|
||||
if (abaCanceladas) {
|
||||
abaCanceladas.classList.toggle("oculto", canceladas.length === 0);
|
||||
abaCanceladas.textContent = "Canceladas" + (canceladas.length ? " (" + canceladas.length + ")" : "");
|
||||
}
|
||||
nenhum.classList.toggle("oculto", ativas.length > 0);
|
||||
mostrar(conteudo);
|
||||
(lista || []).forEach(function (p) { if (p.tenant && p.situacao === "ATIVA") acrescentarBotaoDePlano(p.tenant); });
|
||||
abrirAba(abaLembrada());
|
||||
(lista || []).forEach(function (p) {
|
||||
if (p.tenant && p.situacao === "ATIVA" && !p.demonstracao) acrescentarBotaoDePlano(p.tenant);
|
||||
});
|
||||
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas(), carregarVisao(),
|
||||
carregarTermosDeUso()]);
|
||||
});
|
||||
@@ -2323,8 +2740,10 @@ data:
|
||||
.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; });
|
||||
var emDia = document.getElementById("termosEmDia");
|
||||
var faltam = (d && d.versao && d.escolinhas) ? d.escolinhas.filter(function (e) { return !e.aceito; }) : [];
|
||||
if (emDia) emDia.classList.toggle("oculto", faltam.length > 0);
|
||||
marcarAba("termos", faltam.length > 0);
|
||||
if (!faltam.length) return;
|
||||
var aviso = document.createElement("section");
|
||||
aviso.id = "avisoTermos";
|
||||
@@ -2335,7 +2754,8 @@ data:
|
||||
+ ". 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);
|
||||
var caixaTermos = document.getElementById("termosDeUso");
|
||||
if (caixaTermos) caixaTermos.appendChild(aviso); else produtos.parentNode.insertBefore(aviso, produtos);
|
||||
aviso.querySelector("button").addEventListener("click", function () {
|
||||
aceitarTermosDeUso(aviso, d.versao);
|
||||
});
|
||||
@@ -2588,7 +3008,9 @@ data:
|
||||
.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 vazia = document.getElementById("visaoVazia");
|
||||
if (!tabela) { caixa.classList.add("oculto"); if (vazia) vazia.classList.remove("oculto"); return; }
|
||||
if (vazia) vazia.classList.add("oculto");
|
||||
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.";
|
||||
@@ -3171,7 +3593,7 @@ data:
|
||||
<option value="NASCENDO">Nascendo</option>
|
||||
</select>
|
||||
<label class="filtro"><input type="checkbox" id="soAtencao"> só com alerta</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes e demonstrações</label>
|
||||
</div>
|
||||
<div class="tabela-rolagem">
|
||||
<table class="tabela clientes">
|
||||
@@ -3334,7 +3756,7 @@ data:
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/publico/painel.js?v=2.86"></script>
|
||||
<script src="/publico/painel.js?v=2.89"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3630,9 +4052,13 @@ data:
|
||||
bloco("Recebido desde o início", reaisCurto(i.recebidoTotal),
|
||||
i.clientesSemDadoDoAsaas > 0 ? i.clientesSemDadoDoAsaas + " sem resposta do Asaas" : "extrato do Asaas", "")
|
||||
].join("");
|
||||
// Teste e demonstracao aparecem na lista, mas nao nos numeros — e a tela diz.
|
||||
var foraDosNumeros = [];
|
||||
if (i.contasDeTeste > 0) foraDosNumeros.push(i.contasDeTeste + " de teste");
|
||||
if (i.demonstracoes > 0) foraDosNumeros.push(i.demonstracoes + " de demonstração");
|
||||
$("kpiClientes").innerHTML = [
|
||||
bloco("Clientes ativos", esc(i.clientesAtivos),
|
||||
i.contasDeTeste > 0 ? "+ " + i.contasDeTeste + " de teste (fora dos números)" : "", "ok"),
|
||||
foraDosNumeros.length ? "+ " + foraDosNumeros.join(", ") + " (fora dos números)" : "", "ok"),
|
||||
bloco("Novos no mês", esc(i.novosNoMes), comparacao(i.novosNoMes, i.novosMesAnterior), ""),
|
||||
bloco("Leads no mês", esc(i.leadsNoMes), comparacao(i.leadsNoMes, i.leadsMesAnterior), ""),
|
||||
bloco("Conversão", pct(i.conversao), "de lead para cliente pagante", ""),
|
||||
@@ -3671,7 +4097,7 @@ data:
|
||||
if (plano && c.plano !== plano) return false;
|
||||
if (estado && (c.estado || "NASCENDO") !== estado) return false;
|
||||
if (soAtencao && !(c.alertas || []).length) return false;
|
||||
if (semTeste && c.teste) return false;
|
||||
if (semTeste && (c.teste || c.demonstracao)) return false;
|
||||
if (busca) {
|
||||
var alvo = [c.cliente, c.escolinha, c.email, c.cidade, c.tenant, c.whatsapp, c.documento]
|
||||
.join(" ").toLowerCase();
|
||||
@@ -3701,7 +4127,9 @@ data:
|
||||
}
|
||||
|
||||
var SITUACAO_ASAAS = { ACTIVE: "ativa", INACTIVE: "encerrada", EXPIRED: "expirada" };
|
||||
function assinaturaNoAsaas(a) {
|
||||
/* A demonstracao nao existe no Asaas DE PROPOSITO: nao e "nao existe" em vermelho. */
|
||||
function assinaturaNoAsaas(a, c) {
|
||||
if (c && c.demonstracao) return "demonstração — sem cobrança";
|
||||
if (!a || !a.disponivel) return "indisponível agora";
|
||||
if (!a.existe) return '<b class="vermelho">não existe</b>';
|
||||
var s = SITUACAO_ASAAS[a.situacao] || txt(a.situacao);
|
||||
@@ -3738,10 +4166,11 @@ data:
|
||||
var gravidade = (c.alertas || []).reduce(function (g, a) {
|
||||
return a.gravidade === "critico" ? "critico" : (g === "critico" ? g : (a.gravidade === "alerta" ? "alerta" : g));
|
||||
}, "");
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste ? " teste" : "")
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste || c.demonstracao ? " teste" : "")
|
||||
+ (!c.tenant ? " destaque" : "") + '" data-assinatura="' + esc(c.assinatura) + '" tabindex="0">'
|
||||
+ "<td><b>" + txt(c.cliente) + "</b>"
|
||||
+ (c.teste ? ' <span class="selo neutro">teste</span>' : "")
|
||||
+ (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "")
|
||||
+ '<span class="sub">' + (c.tenant
|
||||
? '<a href="' + esc(c.endereco) + '" target="_blank" rel="noopener noreferrer">' + esc(c.tenant) + "</a>"
|
||||
: "sem tenant")
|
||||
@@ -3750,9 +4179,9 @@ data:
|
||||
+ "<td>" + etiquetaPlano(c) + "</td>"
|
||||
+ "<td>" + selo(c.estado, c.tenant) + '<span class="sub">' + (c.diasDeCliente !== null && c.diasDeCliente !== undefined
|
||||
? "cliente há " + esc(c.diasDeCliente) + " dia(s)" : "") + "</span></td>"
|
||||
+ '<td class="num">' + reais(c.valorMensal) + diferencaDeTabela(c) + "</td>"
|
||||
+ '<td class="num">' + (f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + proximaCobranca(f) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? '<span class="ajuda">sem cobrança</span>' : reais(c.valorMensal) + diferencaDeTabela(c)) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? "—" : f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + (c.demonstracao ? '<span class="ajuda">—</span>' : proximaCobranca(f)) + "</td>"
|
||||
+ "<td>" + usoCurto(c.uso, c.usoDeDentro) + "</td>"
|
||||
+ "<td>" + seloSubconta(c.subconta) + "</td>"
|
||||
+ "</tr>";
|
||||
@@ -3778,7 +4207,9 @@ data:
|
||||
+ esc(a.titulo) + '</b><span class="sub">' + esc(a.detalhe) + "</span></div></li>";
|
||||
}).join("");
|
||||
|
||||
var extrato = f.disponivel
|
||||
var extrato = c.demonstracao
|
||||
? '<p class="ajuda">Tenant de demonstração: não há assinatura nem cobrança no Asaas.</p>'
|
||||
: f.disponivel
|
||||
? ((f.cobrancas || []).length
|
||||
? '<table class="tabela"><thead><tr><th>Vencimento</th><th>Pago em</th><th>Forma</th><th class="num">Valor</th><th class="num">Líquido</th><th>Situação</th><th></th></tr></thead><tbody>'
|
||||
+ f.cobrancas.map(function (cb) {
|
||||
@@ -3799,7 +4230,7 @@ data:
|
||||
return '<header class="ficha-topo ' + classePlano(c.plano) + '">'
|
||||
+ '<span class="etiqueta ' + classePlano(c.plano) + '">Plano ' + esc(nomePlano(c.plano))
|
||||
+ (c.ciclo === "ANUAL" ? " · anual" : " · mensal") + "</span>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + "</h2>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "") + "</h2>"
|
||||
+ '<p class="sub">' + (c.escolinha && c.escolinha !== c.cliente ? esc(c.escolinha) + " · " : "")
|
||||
+ selo(c.estado, c.tenant) + (c.estadoDesde ? " desde " + dataHora(c.estadoDesde) : "") + "</p>"
|
||||
+ '<div class="atalhos">'
|
||||
@@ -3807,7 +4238,9 @@ data:
|
||||
+ (whatsapp(c.whatsapp) ? '<a class="btn claro" href="' + esc(whatsapp(c.whatsapp)) + '" target="_blank" rel="noopener noreferrer">WhatsApp</a>' : "")
|
||||
+ (c.email ? '<a class="btn claro" href="mailto:' + esc(c.email) + '">E-mail</a>' : "")
|
||||
+ (f.faturaVencida ? '<a class="btn claro" href="' + esc(linkSeguro(f.faturaVencida) || "#") + '" target="_blank" rel="noopener noreferrer">Fatura vencida</a>' : "")
|
||||
+ "</div></header>"
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="reenviar">Reenviar licença</button>'
|
||||
+ '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="trocar">Trocar chave</button>' : "")
|
||||
+ '</div><p id="licencaAviso" class="ajuda" aria-live="polite"></p></header>'
|
||||
+ (alertas ? '<ul class="lista-alertas">' + alertas + "</ul>" : "")
|
||||
|
||||
+ '<div class="grade-ficha">'
|
||||
@@ -3821,14 +4254,17 @@ data:
|
||||
+ '<section><h3>Assinatura</h3><dl>'
|
||||
+ item("Plano", esc(nomePlano(c.plano)) + " " + (c.ciclo === "ANUAL" ? "anual" : "mensal"))
|
||||
+ item("Valor do ciclo", reais(c.valorDoCiclo)) + item("Vale por mês", reais(c.valorMensal) + diferencaDeTabela(c))
|
||||
+ item("De onde vem o valor", c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas))
|
||||
+ item("De onde vem o valor", c.demonstracao ? "demonstração — sem cobrança"
|
||||
: c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas, c))
|
||||
+ item("Contratado em", dataHora(c.contratadoEm)) + item("Cliente há", c.diasDeCliente === null || c.diasDeCliente === undefined ? "—" : esc(c.diasDeCliente) + " dia(s)")
|
||||
+ item("Assinatura no Asaas", txt(c.assinatura)) + item("Cliente no Asaas", txt(c.clienteNoProvedor))
|
||||
+ "</dl></section>"
|
||||
|
||||
+ '<section><h3>Dinheiro</h3><dl>'
|
||||
+ (f.disponivel
|
||||
+ (c.demonstracao
|
||||
? item("Cobrança", "nenhuma — tenant de demonstração")
|
||||
: f.disponivel
|
||||
? item("Total pago", reais(f.totalPago)) + item("Líquido (após taxas)", reais(f.totalLiquido))
|
||||
+ item("Taxas pagas", reais(f.taxas))
|
||||
+ item("Último pagamento", f.ultimoPagamento ? data(f.ultimoPagamento.data) + " · " + reais(f.ultimoPagamento.valor) + " · " + txt(f.ultimoPagamento.forma) : "—")
|
||||
@@ -4325,6 +4761,55 @@ data:
|
||||
corpo.addEventListener("keydown", function (ev) { if (ev.key === "Enter") aoEscolher(ev); });
|
||||
$("alertas").addEventListener("click", aoEscolher);
|
||||
|
||||
/* ── licenca dos apps (dono, treinador, pais) ─────────────────────────────
|
||||
* Reenviar = mandar a MESMA chave da escola ao dono da compra (captacao 4.32+;
|
||||
* so a chave antiga, guardada so com hash, e trocada uma vez). Nada muda para
|
||||
* ninguem: sem confirmacao.
|
||||
* Trocar = revogar a chave e gerar outra. Desde os apps 1.0.4 (captacao 4.33)
|
||||
* o app pergunta a situacao da chave ao abrir: trocar DERRUBA os apps de todos
|
||||
* da escola (dono, treinadores, pais) em ate 15 min de uso, e cada um precisa
|
||||
* da chave nova. Por isso pede o nome do tenant digitado. */
|
||||
var API_LICENCA = "/api/plataforma/painel/licencas/";
|
||||
|
||||
function acaoDaLicenca(tenant, acao, aviso) {
|
||||
if (!tokenAtual) return Promise.resolve();
|
||||
if (acao === "trocar") {
|
||||
var digitado = window.prompt("TROCAR a chave de " + tenant + "?\n\n"
|
||||
+ "A chave atual para de valer e os apps de TODOS da escola (dono, treinadores e pais) saem da conta "
|
||||
+ "em até 15 minutos de uso; cada um vai precisar da chave nova, que vai por e-mail ao dono da compra.\n"
|
||||
+ "Use só se a chave vazou.\n\nPara confirmar, digite o nome do tenant (" + tenant + "):");
|
||||
if (digitado === null) return Promise.resolve();
|
||||
if (digitado.trim() !== tenant) {
|
||||
aviso.textContent = "Nada foi trocado: o nome digitado não confere.";
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
aviso.textContent = "Enviando…";
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/" + acao, {
|
||||
method: "POST", headers: { "x-atm-admin-token": tokenAtual, "Content-Type": "application/json" }, body: "{}"
|
||||
}).then(function (r) {
|
||||
if (r.status === 401) throw new Error("O token expirou. Saia e entre de novo no painel.");
|
||||
return r.json().then(function (d) {
|
||||
if (!r.ok) throw new Error((d && d.detail) || ("O servidor recusou (" + r.status + ")."));
|
||||
return d;
|
||||
});
|
||||
}).then(function (d) {
|
||||
var oQue = acao === "trocar" ? "Chave NOVA" : "Chave da escola";
|
||||
aviso.textContent = oQue + (d.emailEnviado ? " enviada para " : " pronta; o e-mail vai em instantes para ")
|
||||
+ (d.destino || "o dono da compra") + " (termina em " + d.finalDaChave + ").";
|
||||
}).catch(function (e) {
|
||||
aviso.textContent = e && e.message && e.message !== "Failed to fetch" ? e.message : "Sem conexão com o servidor.";
|
||||
});
|
||||
}
|
||||
|
||||
$("fichaCorpo").addEventListener("click", function (ev) {
|
||||
var b = ev.target.closest ? ev.target.closest("[data-licenca]") : null;
|
||||
if (!b) return;
|
||||
b.disabled = true;
|
||||
acaoDaLicenca(b.getAttribute("data-licenca"), b.getAttribute("data-acao") || "reenviar", $("licencaAviso"))
|
||||
.then(function () { b.disabled = false; });
|
||||
});
|
||||
|
||||
$("fecharFicha").addEventListener("click", fecharFicha);
|
||||
$("ficha").addEventListener("click", function (ev) { if (ev.target === $("ficha")) fecharFicha(); });
|
||||
document.addEventListener("keydown", function (ev) { if (ev.key === "Escape") fecharFicha(); });
|
||||
|
||||
@@ -3756,7 +3756,7 @@ data:
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/publico/painel.js?v=2.88"></script>
|
||||
<script src="/publico/painel.js?v=2.89"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4238,7 +4238,8 @@ data:
|
||||
+ (whatsapp(c.whatsapp) ? '<a class="btn claro" href="' + esc(whatsapp(c.whatsapp)) + '" target="_blank" rel="noopener noreferrer">WhatsApp</a>' : "")
|
||||
+ (c.email ? '<a class="btn claro" href="mailto:' + esc(c.email) + '">E-mail</a>' : "")
|
||||
+ (f.faturaVencida ? '<a class="btn claro" href="' + esc(linkSeguro(f.faturaVencida) || "#") + '" target="_blank" rel="noopener noreferrer">Fatura vencida</a>' : "")
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '">Reenviar licença</button>' : "")
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="reenviar">Reenviar licença</button>'
|
||||
+ '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="trocar">Trocar chave</button>' : "")
|
||||
+ '</div><p id="licencaAviso" class="ajuda" aria-live="polite"></p></header>'
|
||||
+ (alertas ? '<ul class="lista-alertas">' + alertas + "</ul>" : "")
|
||||
|
||||
@@ -4760,20 +4761,31 @@ data:
|
||||
corpo.addEventListener("keydown", function (ev) { if (ev.key === "Enter") aoEscolher(ev); });
|
||||
$("alertas").addEventListener("click", aoEscolher);
|
||||
|
||||
/* ── licenca do app do treinador ──────────────────────────────────────────
|
||||
* Reenviar = gerar uma chave NOVA e mandar ao dono da compra. A anterior
|
||||
* deixa de valer para ATIVAR (quem ja ativou o app continua entrando: o app
|
||||
* guarda a escolinha, nao a chave). Por isso pede confirmacao. */
|
||||
/* ── licenca dos apps (dono, treinador, pais) ─────────────────────────────
|
||||
* Reenviar = mandar a MESMA chave da escola ao dono da compra (captacao 4.32+;
|
||||
* so a chave antiga, guardada so com hash, e trocada uma vez). Nada muda para
|
||||
* ninguem: sem confirmacao.
|
||||
* Trocar = revogar a chave e gerar outra. Desde os apps 1.0.4 (captacao 4.33)
|
||||
* o app pergunta a situacao da chave ao abrir: trocar DERRUBA os apps de todos
|
||||
* da escola (dono, treinadores, pais) em ate 15 min de uso, e cada um precisa
|
||||
* da chave nova. Por isso pede o nome do tenant digitado. */
|
||||
var API_LICENCA = "/api/plataforma/painel/licencas/";
|
||||
|
||||
function reenviarLicenca(tenant, aviso) {
|
||||
function acaoDaLicenca(tenant, acao, aviso) {
|
||||
if (!tokenAtual) return Promise.resolve();
|
||||
if (!window.confirm("Gerar uma chave NOVA para " + tenant + " e mandar por e-mail ao dono da compra?\n\n"
|
||||
+ "A chave atual deixa de valer para ativar o app (quem já ativou continua entrando).")) {
|
||||
return Promise.resolve();
|
||||
if (acao === "trocar") {
|
||||
var digitado = window.prompt("TROCAR a chave de " + tenant + "?\n\n"
|
||||
+ "A chave atual para de valer e os apps de TODOS da escola (dono, treinadores e pais) saem da conta "
|
||||
+ "em até 15 minutos de uso; cada um vai precisar da chave nova, que vai por e-mail ao dono da compra.\n"
|
||||
+ "Use só se a chave vazou.\n\nPara confirmar, digite o nome do tenant (" + tenant + "):");
|
||||
if (digitado === null) return Promise.resolve();
|
||||
if (digitado.trim() !== tenant) {
|
||||
aviso.textContent = "Nada foi trocado: o nome digitado não confere.";
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
aviso.textContent = "Enviando…";
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/reenviar", {
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/" + acao, {
|
||||
method: "POST", headers: { "x-atm-admin-token": tokenAtual, "Content-Type": "application/json" }, body: "{}"
|
||||
}).then(function (r) {
|
||||
if (r.status === 401) throw new Error("O token expirou. Saia e entre de novo no painel.");
|
||||
@@ -4782,7 +4794,8 @@ data:
|
||||
return d;
|
||||
});
|
||||
}).then(function (d) {
|
||||
aviso.textContent = (d.emailEnviado ? "Chave nova enviada para " : "Chave nova criada; o e-mail vai em instantes para ")
|
||||
var oQue = acao === "trocar" ? "Chave NOVA" : "Chave da escola";
|
||||
aviso.textContent = oQue + (d.emailEnviado ? " enviada para " : " pronta; o e-mail vai em instantes para ")
|
||||
+ (d.destino || "o dono da compra") + " (termina em " + d.finalDaChave + ").";
|
||||
}).catch(function (e) {
|
||||
aviso.textContent = e && e.message && e.message !== "Failed to fetch" ? e.message : "Sem conexão com o servidor.";
|
||||
@@ -4793,7 +4806,8 @@ data:
|
||||
var b = ev.target.closest ? ev.target.closest("[data-licenca]") : null;
|
||||
if (!b) return;
|
||||
b.disabled = true;
|
||||
reenviarLicenca(b.getAttribute("data-licenca"), $("licencaAviso")).then(function () { b.disabled = false; });
|
||||
acaoDaLicenca(b.getAttribute("data-licenca"), b.getAttribute("data-acao") || "reenviar", $("licencaAviso"))
|
||||
.then(function () { b.disabled = false; });
|
||||
});
|
||||
|
||||
$("fecharFicha").addEventListener("click", fecharFicha);
|
||||
|
||||
@@ -370,6 +370,332 @@ data:
|
||||
});
|
||||
})();
|
||||
|
||||
convite.css: |
|
||||
/* Convite para competição: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). */
|
||||
main { max-width: 38rem; }
|
||||
.explica { color: var(--muted); }
|
||||
form { display: grid; gap: 1rem; }
|
||||
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
|
||||
legend { font-weight: 700; padding: 0 .4rem; }
|
||||
label { display: grid; gap: .2rem; font-size: .92rem; font-weight: 600; }
|
||||
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
|
||||
input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
|
||||
.linha-jogador { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
|
||||
.linha-jogador button { padding: .4rem .6rem; }
|
||||
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
|
||||
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
|
||||
button:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.erro { color: #b42318; margin: 0; font-weight: 600; }
|
||||
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: 1rem 1.2rem; }
|
||||
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
|
||||
|
||||
convite.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, nofollow">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Convite para competição — Athletic Map</title>
|
||||
<link rel="stylesheet" href="/publico/_estilo.css">
|
||||
<link rel="stylesheet" href="/publico/convite.css?v=1">
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="marca">Athletic <span>Map</span></div></header>
|
||||
<main>
|
||||
<h1 id="titulo">Convite para competição</h1>
|
||||
<p id="situacao" role="status" aria-live="polite">Abrindo o convite…</p>
|
||||
|
||||
<form id="formulario" hidden novalidate>
|
||||
<p class="explica">
|
||||
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
|
||||
A organização confere e aprova antes do time entrar na competição.
|
||||
</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Seu time</legend>
|
||||
<label>Nome do time *<input id="nome" maxlength="120" autocomplete="off" required></label>
|
||||
<label>Escola ou clube<input id="escola" maxlength="160" autocomplete="organization"></label>
|
||||
<label>Cidade<input id="cidade" maxlength="120" autocomplete="address-level2"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Responsável pelo time</legend>
|
||||
<label>Nome<input id="contatoNome" maxlength="120" autocomplete="name"></label>
|
||||
<label>Telefone<input id="contatoTelefone" maxlength="40" autocomplete="tel" inputmode="tel"></label>
|
||||
<label>E-mail<input id="contatoEmail" type="email" maxlength="160" autocomplete="email"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Jogadores</legend>
|
||||
<div id="jogadores"></div>
|
||||
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
|
||||
</fieldset>
|
||||
|
||||
<p id="erro" class="erro" role="alert" hidden></p>
|
||||
<button type="submit" id="enviar">Enviar time</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/publico/desafio.js?v=1"></script>
|
||||
<script src="/publico/convite.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
convite.js: |
|
||||
/* ===========================================================================
|
||||
* Convite para competição — a página que a OUTRA escola abre pelo link.
|
||||
*
|
||||
* Sem login: o token no endereço é o único segredo. A página pergunta ao
|
||||
* servidor a que o convite se refere e, se ele vale, deixa a pessoa mandar o
|
||||
* time com os jogadores (nome e camisa). O time chega PENDENTE: quem organiza
|
||||
* confere e aprova.
|
||||
*
|
||||
* Antes de enviar, a verificação anti-robô (desafio.js): pede um desafio ao
|
||||
* servidor, resolve no navegador ("Verificando…") e manda a prova junto com o
|
||||
* time, em `prova`. Um desafio por tentativa: ele é de uso único.
|
||||
*
|
||||
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
|
||||
* HTML. O script é externo porque a CSP é `script-src 'self'`.
|
||||
* =========================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var global = window;
|
||||
|
||||
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
|
||||
var MAX_JOGADORES = 60;
|
||||
|
||||
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
|
||||
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
|
||||
|
||||
var titulo = document.getElementById("titulo");
|
||||
var situacao = document.getElementById("situacao");
|
||||
var formulario = document.getElementById("formulario");
|
||||
var listaJogadores = document.getElementById("jogadores");
|
||||
var erro = document.getElementById("erro");
|
||||
var enviar = document.getElementById("enviar");
|
||||
var ROTULO_ENVIAR = enviar.textContent;
|
||||
|
||||
function recusar(texto) {
|
||||
situacao.textContent = texto;
|
||||
situacao.className = "invalido";
|
||||
formulario.hidden = true;
|
||||
}
|
||||
|
||||
function mostrarErro(texto) {
|
||||
erro.textContent = texto;
|
||||
erro.hidden = !texto;
|
||||
}
|
||||
|
||||
function linhaDeJogador() {
|
||||
var linha = document.createElement("div");
|
||||
linha.className = "linha-jogador";
|
||||
|
||||
var nome = document.createElement("input");
|
||||
nome.placeholder = "Nome do jogador";
|
||||
nome.maxLength = 120;
|
||||
nome.setAttribute("aria-label", "Nome do jogador");
|
||||
nome.setAttribute("data-campo", "nome");
|
||||
|
||||
var camisa = document.createElement("input");
|
||||
camisa.type = "number";
|
||||
camisa.min = "0";
|
||||
camisa.max = "999";
|
||||
camisa.placeholder = "nº";
|
||||
camisa.setAttribute("aria-label", "Número da camisa");
|
||||
camisa.setAttribute("data-campo", "camisa");
|
||||
|
||||
var tirar = document.createElement("button");
|
||||
tirar.type = "button";
|
||||
tirar.className = "secundario";
|
||||
tirar.textContent = "×";
|
||||
tirar.setAttribute("aria-label", "Remover jogador");
|
||||
tirar.addEventListener("click", function () { linha.remove(); });
|
||||
|
||||
linha.appendChild(nome);
|
||||
linha.appendChild(camisa);
|
||||
linha.appendChild(tirar);
|
||||
listaJogadores.appendChild(linha);
|
||||
return linha;
|
||||
}
|
||||
|
||||
function valor(id) {
|
||||
var v = document.getElementById(id).value.trim();
|
||||
return v === "" ? null : v;
|
||||
}
|
||||
|
||||
function montarCorpo() {
|
||||
var jogadores = [];
|
||||
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
|
||||
for (var i = 0; i < linhas.length; i++) {
|
||||
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
|
||||
if (!nome) continue;
|
||||
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
|
||||
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
|
||||
}
|
||||
return {
|
||||
nome: valor("nome"),
|
||||
escola: valor("escola"),
|
||||
cidade: valor("cidade"),
|
||||
contatoNome: valor("contatoNome"),
|
||||
contatoTelefone: valor("contatoTelefone"),
|
||||
contatoEmail: valor("contatoEmail"),
|
||||
jogadores: jogadores
|
||||
};
|
||||
}
|
||||
|
||||
function lerResposta(r) {
|
||||
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
|
||||
}
|
||||
|
||||
function aoEnviar(ev) {
|
||||
ev.preventDefault();
|
||||
mostrarErro("");
|
||||
var corpo = montarCorpo();
|
||||
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
|
||||
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
|
||||
enviar.disabled = true;
|
||||
enviar.textContent = "Verificando…";
|
||||
enviar.setAttribute("aria-busy", "true");
|
||||
|
||||
function voltar(texto) {
|
||||
enviar.disabled = false;
|
||||
enviar.textContent = ROTULO_ENVIAR;
|
||||
enviar.removeAttribute("aria-busy");
|
||||
mostrarErro(texto);
|
||||
}
|
||||
|
||||
fetch(base + "/desafio").then(lerResposta).then(function (r) {
|
||||
if (r.status !== 200) {
|
||||
var e = new Error("recusado");
|
||||
e.detalhe = (r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.";
|
||||
throw e;
|
||||
}
|
||||
if (!global.AtmDesafio) throw new Error("sem-crypto");
|
||||
return global.AtmDesafio.resolver(r.corpo);
|
||||
}).then(function (prova) {
|
||||
corpo.prova = prova;
|
||||
enviar.textContent = "Enviando…";
|
||||
return fetch(base + "/times", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(corpo)
|
||||
}).then(lerResposta);
|
||||
}).then(function (r) {
|
||||
if (r.status >= 200 && r.status < 300) {
|
||||
formulario.hidden = true;
|
||||
titulo.textContent = "Time enviado";
|
||||
situacao.textContent = "Recebemos o time \"" + corpo.nome + "\". A organização vai conferir os dados e aprovar. Você já pode fechar esta página.";
|
||||
situacao.className = "ok";
|
||||
return;
|
||||
}
|
||||
voltar((r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.");
|
||||
}).catch(function (e) {
|
||||
if (e && e.detalhe) { voltar(e.detalhe); return; }
|
||||
if (e && (e.message === "sem-crypto" || e.message === "desafio-invalido" || e.message === "sem-solucao")) {
|
||||
voltar("Não foi possível fazer a verificação anti-robô neste navegador. Atualize a página ou use outro navegador.");
|
||||
return;
|
||||
}
|
||||
voltar("Sem conexão. Confira a internet e tente de novo.");
|
||||
});
|
||||
}
|
||||
|
||||
function iniciar() {
|
||||
if (!TOKEN.test(token)) { recusar("Este endereço de convite não é válido."); return; }
|
||||
fetch(base).then(lerResposta).then(function (r) {
|
||||
var info = r.corpo || {};
|
||||
if (!info.valido) {
|
||||
recusar(info.motivo ? "Não dá para usar este convite: " + info.motivo + "." : "Este convite não está disponível.");
|
||||
return;
|
||||
}
|
||||
titulo.textContent = "Convite: " + (info.competicao || "competição");
|
||||
situacao.textContent = (info.temporada ? "Temporada " + info.temporada + ". " : "")
|
||||
+ "Restam " + info.vagasRestantes + " vaga(s) neste convite.";
|
||||
situacao.className = "";
|
||||
formulario.hidden = false;
|
||||
for (var i = 0; i < 5; i++) linhaDeJogador();
|
||||
}).catch(function () {
|
||||
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("maisJogador").addEventListener("click", function () {
|
||||
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
|
||||
});
|
||||
formulario.addEventListener("submit", aoEnviar);
|
||||
iniciar();
|
||||
})();
|
||||
|
||||
desafio.js: |
|
||||
/* ===========================================================================
|
||||
* Verificação anti-robô das páginas públicas — prova de trabalho no estilo
|
||||
* ALTCHA, sem serviço de terceiro (nada de quem preenche sai do Athletic Map).
|
||||
*
|
||||
* O servidor manda { algoritmo: "SHA-256", desafio, sal, assinatura, maximo }.
|
||||
* Aqui se procura o número n (0..maximo) tal que sha256(sal + n) == desafio, e
|
||||
* devolve-se { numero, sal, assinatura } para ir junto no envio. Em média são
|
||||
* maximo/2 hashes: ~1–2 s num celular com maximo = 150 000.
|
||||
*
|
||||
* Usa crypto.subtle (só existe em HTTPS — e as páginas públicas são HTTPS).
|
||||
* Os hashes saem em lotes paralelos: um `await` por hash custaria mais que o
|
||||
* próprio hash. Script externo porque a CSP é `script-src 'self'`.
|
||||
*
|
||||
* O teste do servidor é DesafioDoConvite (servico-campeonato).
|
||||
* =========================================================================== */
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var LOTE = 1000;
|
||||
var HEX = /^[0-9a-f]{64}$/;
|
||||
|
||||
function bytesDoHex(h) {
|
||||
var b = new Uint8Array(h.length / 2);
|
||||
for (var i = 0; i < b.length; i++) b[i] = parseInt(h.substr(i * 2, 2), 16);
|
||||
return b;
|
||||
}
|
||||
|
||||
function iguais(buf, alvo) {
|
||||
var a = new Uint8Array(buf);
|
||||
if (a.length !== alvo.length) return false;
|
||||
for (var i = 0; i < a.length; i++) if (a[i] !== alvo[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{algoritmo:string, desafio:string, sal:string, assinatura:string, maximo:number}} d
|
||||
* @returns {Promise<{numero:number, sal:string, assinatura:string}>}
|
||||
* rejeita com Error("sem-crypto"), Error("desafio-invalido") ou Error("sem-solucao")
|
||||
*/
|
||||
function resolver(d) {
|
||||
var subtle = global.crypto && global.crypto.subtle;
|
||||
var Codificador = global.TextEncoder;
|
||||
if (!subtle || !Codificador) return Promise.reject(new Error("sem-crypto"));
|
||||
if (!d || d.algoritmo !== "SHA-256" || !HEX.test(d.desafio || "") || typeof d.sal !== "string"
|
||||
|| typeof d.maximo !== "number" || d.maximo < 0 || d.maximo > 5000000) {
|
||||
return Promise.reject(new Error("desafio-invalido"));
|
||||
}
|
||||
var alvo = bytesDoHex(d.desafio);
|
||||
var cod = new Codificador();
|
||||
|
||||
function lote(inicio) {
|
||||
if (inicio > d.maximo) return Promise.reject(new Error("sem-solucao"));
|
||||
var fim = Math.min(inicio + LOTE - 1, d.maximo);
|
||||
var hashes = [];
|
||||
for (var n = inicio; n <= fim; n++) hashes.push(subtle.digest("SHA-256", cod.encode(d.sal + n)));
|
||||
return Promise.all(hashes).then(function (hs) {
|
||||
for (var i = 0; i < hs.length; i++) {
|
||||
if (iguais(hs[i], alvo)) return { numero: inicio + i, sal: d.sal, assinatura: d.assinatura };
|
||||
}
|
||||
return lote(fim + 1);
|
||||
});
|
||||
}
|
||||
return lote(0);
|
||||
}
|
||||
|
||||
global.AtmDesafio = { resolver: resolver };
|
||||
})(window);
|
||||
|
||||
fontes--fontes.css: |
|
||||
/* ============================================================
|
||||
* As fontes da landing page, servidas POR NOS.
|
||||
@@ -609,9 +935,7 @@ data:
|
||||
|
||||
/* marca */
|
||||
.logo{display:inline-flex; align-items:center; gap:11px; font-weight:700; font-size:1.22rem; letter-spacing:-.02em; color:var(--ink); text-decoration:none;}
|
||||
.mark{display:inline-flex; gap:3px; align-items:flex-end;}
|
||||
.mark i{display:block; width:5px; border-radius:2px; background:var(--laranja); transform:skewX(-14deg);}
|
||||
.mark i:nth-child(1){height:13px} .mark i:nth-child(2){height:19px} .mark i:nth-child(3){height:25px}
|
||||
.logo img{display:block; height:37px; width:auto;}
|
||||
|
||||
.topbar{background:var(--surface); border-bottom:1px solid var(--line);}
|
||||
.topbar .wrap{display:flex; align-items:center; justify-content:space-between; gap:16px; padding-block:14px;}
|
||||
@@ -814,7 +1138,7 @@ data:
|
||||
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="logo" href="#"><span class="mark"><i></i><i></i><i></i></span>Athletic Map</a>
|
||||
<a class="logo" href="#" aria-label="Athletic Map"><img src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37"></a>
|
||||
<a class="zap" id="topZap" href="#form">Falar com o time →</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1698,6 +2022,20 @@ data:
|
||||
.termos-uso-caixa input { margin-right: 6px; }
|
||||
.termos-uso-aceite { margin: 10px 0; }
|
||||
|
||||
/* ── Abas (2026-09-29) — mesmo desenho do painel do dono ───────────────────── */
|
||||
.abas { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin: 4px 0 18px; overflow-x: auto; }
|
||||
.aba {
|
||||
background: none; border: 0; border-bottom: 3px solid transparent; padding: 10px 16px;
|
||||
font: inherit; font-weight: 600; color: var(--muted); cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.aba:hover { color: var(--navy); }
|
||||
.aba.ativa { color: var(--navy); border-bottom-color: var(--orange); }
|
||||
.aba .badge {
|
||||
display: inline-block; min-width: 8px; height: 8px; margin-left: 6px; border-radius: 999px;
|
||||
background: var(--orange); vertical-align: middle;
|
||||
}
|
||||
.aba .badge.num { min-width: 18px; height: auto; padding: 0 6px; font-size: 12px; color: #fff; line-height: 18px; }
|
||||
|
||||
minha-conta.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
@@ -1722,7 +2060,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.88">
|
||||
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.90">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1765,23 +2103,56 @@ 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>
|
||||
<!-- Abas (2026-09-29, pedido do dono): a pagina empilhava visao geral, Termos,
|
||||
escolas ativas e canceladas numa coluna so. Mesmo padrao do painel do dono. -->
|
||||
<nav class="abas" role="tablist" aria-label="Seções da conta">
|
||||
<button type="button" class="aba ativa" data-aba="escolas" role="tab">Minhas escolas</button>
|
||||
<button type="button" class="aba" data-aba="visao" role="tab">Visão geral</button>
|
||||
<button type="button" class="aba" data-aba="termos" role="tab">Termos de Uso <span id="termosBadge" class="badge oculto"></span></button>
|
||||
<button type="button" class="aba oculto" data-aba="canceladas" role="tab" id="abaCanceladas">Canceladas</button>
|
||||
</nav>
|
||||
|
||||
<div id="produtos" class="produtos"></div>
|
||||
<div id="aba-escolas" class="painel-aba">
|
||||
<div id="produtos" class="produtos"></div>
|
||||
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-visao" class="painel-aba 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="visaoVazia" class="cartao">
|
||||
<h2>Visão <span class="hl">geral</span></h2>
|
||||
<p class="ajuda">Os números aparecem aqui quando as suas escolinhas enviarem o primeiro resumo
|
||||
(uma vez por dia, de madrugada).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-termos" class="painel-aba oculto">
|
||||
<div id="termosDeUso"></div>
|
||||
<div id="termosEmDia" class="cartao oculto">
|
||||
<h2>Termos de Uso</h2>
|
||||
<p class="ajuda">Nada pendente: a versão vigente já foi aceita para todas as suas escolinhas.
|
||||
<a href="/termos" target="_blank" rel="noopener">Ler os Termos de Uso</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-canceladas" class="painel-aba oculto">
|
||||
<p class="ajuda">Assinaturas canceladas. Regularize o pagamento para voltar a entrar; os dados ficam
|
||||
guardados por 30 dias depois do cancelamento.</p>
|
||||
<div id="produtosCancelados" class="produtos"></div>
|
||||
</div>
|
||||
|
||||
<p class="rodape">
|
||||
@@ -1801,7 +2172,7 @@ data:
|
||||
|
||||
<footer class="direitos">© 2026 Athletic Map. Todos os direitos reservados.</footer>
|
||||
|
||||
<script src="/publico/minha-conta.js?v=2.88"></script>
|
||||
<script src="/publico/minha-conta.js?v=2.90"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1870,6 +2241,36 @@ data:
|
||||
var erroTexto = document.getElementById("erroTexto");
|
||||
var botaoSair = document.getElementById("sair");
|
||||
|
||||
/* ── ABAS (2026-09-29) ────────────────────────────────────────────────────────
|
||||
* Minhas escolas | Visao geral | Termos de Uso | Canceladas. A ultima aba aberta
|
||||
* fica no localStorage (conveniencia deste navegador; sem ela, "escolas"). */
|
||||
var ABA_CHAVE = "atm.conta.aba";
|
||||
function abrirAba(nome) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao || botao.classList.contains("oculto")) { nome = "escolas"; botao = document.querySelector('.aba[data-aba="escolas"]'); }
|
||||
if (!botao) return;
|
||||
document.querySelectorAll(".aba").forEach(function (b) { b.classList.toggle("ativa", b === botao); });
|
||||
document.querySelectorAll(".painel-aba").forEach(function (painel) {
|
||||
painel.classList.toggle("oculto", painel.id !== "aba-" + nome);
|
||||
});
|
||||
try { localStorage.setItem(ABA_CHAVE, nome); } catch (e) { /* aba privada */ }
|
||||
}
|
||||
function abaLembrada() { try { return localStorage.getItem(ABA_CHAVE) || "escolas"; } catch (e) { return "escolas"; } }
|
||||
/* Marca a aba: `pendente` acende a bolinha; `quantos` escreve o numero. */
|
||||
function marcarAba(nome, pendente, quantos) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao) return;
|
||||
var badge = botao.querySelector(".badge");
|
||||
if (badge) {
|
||||
badge.classList.toggle("oculto", !pendente && !quantos);
|
||||
badge.classList.toggle("num", !!quantos);
|
||||
badge.textContent = quantos ? String(quantos) : "";
|
||||
}
|
||||
}
|
||||
document.querySelectorAll(".aba").forEach(function (b) {
|
||||
b.addEventListener("click", function () { abrirAba(b.getAttribute("data-aba")); });
|
||||
});
|
||||
|
||||
function mostrar(qual) {
|
||||
[porta, carregando, conteudo, caixaErro].forEach(function (s) {
|
||||
if (s) s.classList.add("oculto");
|
||||
@@ -1988,14 +2389,18 @@ data:
|
||||
return n ? n.charAt(0).toUpperCase() + n.slice(1) : "—";
|
||||
}
|
||||
|
||||
/* Tenant de DEMONSTRACAO do dono: o servidor marca (`demonstracao: true`). O
|
||||
cartao diz isso ao lado da situacao, e nao ganha "Trocar de plano" nem
|
||||
"Modulos avulsos" — nao ha cobranca, e o servidor recusaria de qualquer jeito. */
|
||||
function cartao(p) {
|
||||
var s = SITUACAO[p.situacao] || ["neutro", String(p.situacao || "").toLowerCase()];
|
||||
var podeEntrar = p.endereco && p.situacao === "ATIVA";
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + '"'
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + (p.demonstracao ? " demonstracao" : "") + '"'
|
||||
+ (p.tenant ? ' data-tenant="' + escapar(p.tenant) + '"' : "") + ">"
|
||||
+ '<span class="faixa-plano">Plano ' + escapar(nomeDoPlano(p.plano)) + "</span>"
|
||||
+ "<h2>" + escapar(p.escolinha || "Sua escolinha") + "</h2>"
|
||||
+ '<span class="selo ' + s[0] + '">' + s[1] + "</span>"
|
||||
+ (p.demonstracao ? '<span class="selo neutro">demonstração — sem cobrança</span>' : "")
|
||||
+ (podeEntrar
|
||||
? '<a class="btn" href="' + escapar(p.endereco) + '">Entrar →</a>'
|
||||
: '<p class="ajuda">' + (p.situacao === "PREPARANDO"
|
||||
@@ -2014,10 +2419,22 @@ data:
|
||||
.then(function (lista) {
|
||||
enderecos = {};
|
||||
(lista || []).forEach(function (p) { if (p.tenant) enderecos[p.tenant] = p.endereco; });
|
||||
produtos.innerHTML = (lista || []).map(cartao).join("");
|
||||
nenhum.classList.toggle("oculto", (lista || []).length > 0);
|
||||
var ativas = (lista || []).filter(function (p) { return p.situacao !== "CANCELADA"; });
|
||||
var canceladas = (lista || []).filter(function (p) { return p.situacao === "CANCELADA"; });
|
||||
produtos.innerHTML = ativas.map(cartao).join("");
|
||||
var caixaCanceladas = document.getElementById("produtosCancelados");
|
||||
if (caixaCanceladas) caixaCanceladas.innerHTML = canceladas.map(cartao).join("");
|
||||
var abaCanceladas = document.getElementById("abaCanceladas");
|
||||
if (abaCanceladas) {
|
||||
abaCanceladas.classList.toggle("oculto", canceladas.length === 0);
|
||||
abaCanceladas.textContent = "Canceladas" + (canceladas.length ? " (" + canceladas.length + ")" : "");
|
||||
}
|
||||
nenhum.classList.toggle("oculto", ativas.length > 0);
|
||||
mostrar(conteudo);
|
||||
(lista || []).forEach(function (p) { if (p.tenant && p.situacao === "ATIVA") acrescentarBotaoDePlano(p.tenant); });
|
||||
abrirAba(abaLembrada());
|
||||
(lista || []).forEach(function (p) {
|
||||
if (p.tenant && p.situacao === "ATIVA" && !p.demonstracao) acrescentarBotaoDePlano(p.tenant);
|
||||
});
|
||||
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas(), carregarVisao(),
|
||||
carregarTermosDeUso()]);
|
||||
});
|
||||
@@ -2323,8 +2740,10 @@ data:
|
||||
.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; });
|
||||
var emDia = document.getElementById("termosEmDia");
|
||||
var faltam = (d && d.versao && d.escolinhas) ? d.escolinhas.filter(function (e) { return !e.aceito; }) : [];
|
||||
if (emDia) emDia.classList.toggle("oculto", faltam.length > 0);
|
||||
marcarAba("termos", faltam.length > 0);
|
||||
if (!faltam.length) return;
|
||||
var aviso = document.createElement("section");
|
||||
aviso.id = "avisoTermos";
|
||||
@@ -2335,7 +2754,8 @@ data:
|
||||
+ ". 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);
|
||||
var caixaTermos = document.getElementById("termosDeUso");
|
||||
if (caixaTermos) caixaTermos.appendChild(aviso); else produtos.parentNode.insertBefore(aviso, produtos);
|
||||
aviso.querySelector("button").addEventListener("click", function () {
|
||||
aceitarTermosDeUso(aviso, d.versao);
|
||||
});
|
||||
@@ -2588,7 +3008,9 @@ data:
|
||||
.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 vazia = document.getElementById("visaoVazia");
|
||||
if (!tabela) { caixa.classList.add("oculto"); if (vazia) vazia.classList.remove("oculto"); return; }
|
||||
if (vazia) vazia.classList.add("oculto");
|
||||
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.";
|
||||
@@ -3171,7 +3593,7 @@ data:
|
||||
<option value="NASCENDO">Nascendo</option>
|
||||
</select>
|
||||
<label class="filtro"><input type="checkbox" id="soAtencao"> só com alerta</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes e demonstrações</label>
|
||||
</div>
|
||||
<div class="tabela-rolagem">
|
||||
<table class="tabela clientes">
|
||||
@@ -3334,7 +3756,7 @@ data:
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/publico/painel.js?v=2.86"></script>
|
||||
<script src="/publico/painel.js?v=2.89"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3630,9 +4052,13 @@ data:
|
||||
bloco("Recebido desde o início", reaisCurto(i.recebidoTotal),
|
||||
i.clientesSemDadoDoAsaas > 0 ? i.clientesSemDadoDoAsaas + " sem resposta do Asaas" : "extrato do Asaas", "")
|
||||
].join("");
|
||||
// Teste e demonstracao aparecem na lista, mas nao nos numeros — e a tela diz.
|
||||
var foraDosNumeros = [];
|
||||
if (i.contasDeTeste > 0) foraDosNumeros.push(i.contasDeTeste + " de teste");
|
||||
if (i.demonstracoes > 0) foraDosNumeros.push(i.demonstracoes + " de demonstração");
|
||||
$("kpiClientes").innerHTML = [
|
||||
bloco("Clientes ativos", esc(i.clientesAtivos),
|
||||
i.contasDeTeste > 0 ? "+ " + i.contasDeTeste + " de teste (fora dos números)" : "", "ok"),
|
||||
foraDosNumeros.length ? "+ " + foraDosNumeros.join(", ") + " (fora dos números)" : "", "ok"),
|
||||
bloco("Novos no mês", esc(i.novosNoMes), comparacao(i.novosNoMes, i.novosMesAnterior), ""),
|
||||
bloco("Leads no mês", esc(i.leadsNoMes), comparacao(i.leadsNoMes, i.leadsMesAnterior), ""),
|
||||
bloco("Conversão", pct(i.conversao), "de lead para cliente pagante", ""),
|
||||
@@ -3671,7 +4097,7 @@ data:
|
||||
if (plano && c.plano !== plano) return false;
|
||||
if (estado && (c.estado || "NASCENDO") !== estado) return false;
|
||||
if (soAtencao && !(c.alertas || []).length) return false;
|
||||
if (semTeste && c.teste) return false;
|
||||
if (semTeste && (c.teste || c.demonstracao)) return false;
|
||||
if (busca) {
|
||||
var alvo = [c.cliente, c.escolinha, c.email, c.cidade, c.tenant, c.whatsapp, c.documento]
|
||||
.join(" ").toLowerCase();
|
||||
@@ -3701,7 +4127,9 @@ data:
|
||||
}
|
||||
|
||||
var SITUACAO_ASAAS = { ACTIVE: "ativa", INACTIVE: "encerrada", EXPIRED: "expirada" };
|
||||
function assinaturaNoAsaas(a) {
|
||||
/* A demonstracao nao existe no Asaas DE PROPOSITO: nao e "nao existe" em vermelho. */
|
||||
function assinaturaNoAsaas(a, c) {
|
||||
if (c && c.demonstracao) return "demonstração — sem cobrança";
|
||||
if (!a || !a.disponivel) return "indisponível agora";
|
||||
if (!a.existe) return '<b class="vermelho">não existe</b>';
|
||||
var s = SITUACAO_ASAAS[a.situacao] || txt(a.situacao);
|
||||
@@ -3738,10 +4166,11 @@ data:
|
||||
var gravidade = (c.alertas || []).reduce(function (g, a) {
|
||||
return a.gravidade === "critico" ? "critico" : (g === "critico" ? g : (a.gravidade === "alerta" ? "alerta" : g));
|
||||
}, "");
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste ? " teste" : "")
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste || c.demonstracao ? " teste" : "")
|
||||
+ (!c.tenant ? " destaque" : "") + '" data-assinatura="' + esc(c.assinatura) + '" tabindex="0">'
|
||||
+ "<td><b>" + txt(c.cliente) + "</b>"
|
||||
+ (c.teste ? ' <span class="selo neutro">teste</span>' : "")
|
||||
+ (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "")
|
||||
+ '<span class="sub">' + (c.tenant
|
||||
? '<a href="' + esc(c.endereco) + '" target="_blank" rel="noopener noreferrer">' + esc(c.tenant) + "</a>"
|
||||
: "sem tenant")
|
||||
@@ -3750,9 +4179,9 @@ data:
|
||||
+ "<td>" + etiquetaPlano(c) + "</td>"
|
||||
+ "<td>" + selo(c.estado, c.tenant) + '<span class="sub">' + (c.diasDeCliente !== null && c.diasDeCliente !== undefined
|
||||
? "cliente há " + esc(c.diasDeCliente) + " dia(s)" : "") + "</span></td>"
|
||||
+ '<td class="num">' + reais(c.valorMensal) + diferencaDeTabela(c) + "</td>"
|
||||
+ '<td class="num">' + (f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + proximaCobranca(f) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? '<span class="ajuda">sem cobrança</span>' : reais(c.valorMensal) + diferencaDeTabela(c)) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? "—" : f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + (c.demonstracao ? '<span class="ajuda">—</span>' : proximaCobranca(f)) + "</td>"
|
||||
+ "<td>" + usoCurto(c.uso, c.usoDeDentro) + "</td>"
|
||||
+ "<td>" + seloSubconta(c.subconta) + "</td>"
|
||||
+ "</tr>";
|
||||
@@ -3778,7 +4207,9 @@ data:
|
||||
+ esc(a.titulo) + '</b><span class="sub">' + esc(a.detalhe) + "</span></div></li>";
|
||||
}).join("");
|
||||
|
||||
var extrato = f.disponivel
|
||||
var extrato = c.demonstracao
|
||||
? '<p class="ajuda">Tenant de demonstração: não há assinatura nem cobrança no Asaas.</p>'
|
||||
: f.disponivel
|
||||
? ((f.cobrancas || []).length
|
||||
? '<table class="tabela"><thead><tr><th>Vencimento</th><th>Pago em</th><th>Forma</th><th class="num">Valor</th><th class="num">Líquido</th><th>Situação</th><th></th></tr></thead><tbody>'
|
||||
+ f.cobrancas.map(function (cb) {
|
||||
@@ -3799,7 +4230,7 @@ data:
|
||||
return '<header class="ficha-topo ' + classePlano(c.plano) + '">'
|
||||
+ '<span class="etiqueta ' + classePlano(c.plano) + '">Plano ' + esc(nomePlano(c.plano))
|
||||
+ (c.ciclo === "ANUAL" ? " · anual" : " · mensal") + "</span>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + "</h2>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "") + "</h2>"
|
||||
+ '<p class="sub">' + (c.escolinha && c.escolinha !== c.cliente ? esc(c.escolinha) + " · " : "")
|
||||
+ selo(c.estado, c.tenant) + (c.estadoDesde ? " desde " + dataHora(c.estadoDesde) : "") + "</p>"
|
||||
+ '<div class="atalhos">'
|
||||
@@ -3807,7 +4238,9 @@ data:
|
||||
+ (whatsapp(c.whatsapp) ? '<a class="btn claro" href="' + esc(whatsapp(c.whatsapp)) + '" target="_blank" rel="noopener noreferrer">WhatsApp</a>' : "")
|
||||
+ (c.email ? '<a class="btn claro" href="mailto:' + esc(c.email) + '">E-mail</a>' : "")
|
||||
+ (f.faturaVencida ? '<a class="btn claro" href="' + esc(linkSeguro(f.faturaVencida) || "#") + '" target="_blank" rel="noopener noreferrer">Fatura vencida</a>' : "")
|
||||
+ "</div></header>"
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="reenviar">Reenviar licença</button>'
|
||||
+ '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="trocar">Trocar chave</button>' : "")
|
||||
+ '</div><p id="licencaAviso" class="ajuda" aria-live="polite"></p></header>'
|
||||
+ (alertas ? '<ul class="lista-alertas">' + alertas + "</ul>" : "")
|
||||
|
||||
+ '<div class="grade-ficha">'
|
||||
@@ -3821,14 +4254,17 @@ data:
|
||||
+ '<section><h3>Assinatura</h3><dl>'
|
||||
+ item("Plano", esc(nomePlano(c.plano)) + " " + (c.ciclo === "ANUAL" ? "anual" : "mensal"))
|
||||
+ item("Valor do ciclo", reais(c.valorDoCiclo)) + item("Vale por mês", reais(c.valorMensal) + diferencaDeTabela(c))
|
||||
+ item("De onde vem o valor", c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas))
|
||||
+ item("De onde vem o valor", c.demonstracao ? "demonstração — sem cobrança"
|
||||
: c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas, c))
|
||||
+ item("Contratado em", dataHora(c.contratadoEm)) + item("Cliente há", c.diasDeCliente === null || c.diasDeCliente === undefined ? "—" : esc(c.diasDeCliente) + " dia(s)")
|
||||
+ item("Assinatura no Asaas", txt(c.assinatura)) + item("Cliente no Asaas", txt(c.clienteNoProvedor))
|
||||
+ "</dl></section>"
|
||||
|
||||
+ '<section><h3>Dinheiro</h3><dl>'
|
||||
+ (f.disponivel
|
||||
+ (c.demonstracao
|
||||
? item("Cobrança", "nenhuma — tenant de demonstração")
|
||||
: f.disponivel
|
||||
? item("Total pago", reais(f.totalPago)) + item("Líquido (após taxas)", reais(f.totalLiquido))
|
||||
+ item("Taxas pagas", reais(f.taxas))
|
||||
+ item("Último pagamento", f.ultimoPagamento ? data(f.ultimoPagamento.data) + " · " + reais(f.ultimoPagamento.valor) + " · " + txt(f.ultimoPagamento.forma) : "—")
|
||||
@@ -4325,6 +4761,55 @@ data:
|
||||
corpo.addEventListener("keydown", function (ev) { if (ev.key === "Enter") aoEscolher(ev); });
|
||||
$("alertas").addEventListener("click", aoEscolher);
|
||||
|
||||
/* ── licenca dos apps (dono, treinador, pais) ─────────────────────────────
|
||||
* Reenviar = mandar a MESMA chave da escola ao dono da compra (captacao 4.32+;
|
||||
* so a chave antiga, guardada so com hash, e trocada uma vez). Nada muda para
|
||||
* ninguem: sem confirmacao.
|
||||
* Trocar = revogar a chave e gerar outra. Desde os apps 1.0.4 (captacao 4.33)
|
||||
* o app pergunta a situacao da chave ao abrir: trocar DERRUBA os apps de todos
|
||||
* da escola (dono, treinadores, pais) em ate 15 min de uso, e cada um precisa
|
||||
* da chave nova. Por isso pede o nome do tenant digitado. */
|
||||
var API_LICENCA = "/api/plataforma/painel/licencas/";
|
||||
|
||||
function acaoDaLicenca(tenant, acao, aviso) {
|
||||
if (!tokenAtual) return Promise.resolve();
|
||||
if (acao === "trocar") {
|
||||
var digitado = window.prompt("TROCAR a chave de " + tenant + "?\n\n"
|
||||
+ "A chave atual para de valer e os apps de TODOS da escola (dono, treinadores e pais) saem da conta "
|
||||
+ "em até 15 minutos de uso; cada um vai precisar da chave nova, que vai por e-mail ao dono da compra.\n"
|
||||
+ "Use só se a chave vazou.\n\nPara confirmar, digite o nome do tenant (" + tenant + "):");
|
||||
if (digitado === null) return Promise.resolve();
|
||||
if (digitado.trim() !== tenant) {
|
||||
aviso.textContent = "Nada foi trocado: o nome digitado não confere.";
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
aviso.textContent = "Enviando…";
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/" + acao, {
|
||||
method: "POST", headers: { "x-atm-admin-token": tokenAtual, "Content-Type": "application/json" }, body: "{}"
|
||||
}).then(function (r) {
|
||||
if (r.status === 401) throw new Error("O token expirou. Saia e entre de novo no painel.");
|
||||
return r.json().then(function (d) {
|
||||
if (!r.ok) throw new Error((d && d.detail) || ("O servidor recusou (" + r.status + ")."));
|
||||
return d;
|
||||
});
|
||||
}).then(function (d) {
|
||||
var oQue = acao === "trocar" ? "Chave NOVA" : "Chave da escola";
|
||||
aviso.textContent = oQue + (d.emailEnviado ? " enviada para " : " pronta; o e-mail vai em instantes para ")
|
||||
+ (d.destino || "o dono da compra") + " (termina em " + d.finalDaChave + ").";
|
||||
}).catch(function (e) {
|
||||
aviso.textContent = e && e.message && e.message !== "Failed to fetch" ? e.message : "Sem conexão com o servidor.";
|
||||
});
|
||||
}
|
||||
|
||||
$("fichaCorpo").addEventListener("click", function (ev) {
|
||||
var b = ev.target.closest ? ev.target.closest("[data-licenca]") : null;
|
||||
if (!b) return;
|
||||
b.disabled = true;
|
||||
acaoDaLicenca(b.getAttribute("data-licenca"), b.getAttribute("data-acao") || "reenviar", $("licencaAviso"))
|
||||
.then(function () { b.disabled = false; });
|
||||
});
|
||||
|
||||
$("fecharFicha").addEventListener("click", fecharFicha);
|
||||
$("ficha").addEventListener("click", function (ev) { if (ev.target === $("ficha")) fecharFicha(); });
|
||||
document.addEventListener("keydown", function (ev) { if (ev.key === "Escape") fecharFicha(); });
|
||||
|
||||
@@ -370,6 +370,332 @@ data:
|
||||
});
|
||||
})();
|
||||
|
||||
convite.css: |
|
||||
/* Convite para competição: página pública, sem fonte nem script de fora (a CSP só deixa 'self'). */
|
||||
main { max-width: 38rem; }
|
||||
.explica { color: var(--muted); }
|
||||
form { display: grid; gap: 1rem; }
|
||||
fieldset { border: 1px solid var(--line); border-radius: 8px; background: #fff; padding: .75rem 1rem 1rem; display: grid; gap: .6rem; }
|
||||
legend { font-weight: 700; padding: 0 .4rem; }
|
||||
label { display: grid; gap: .2rem; font-size: .92rem; font-weight: 600; }
|
||||
input { font: inherit; padding: .5rem .6rem; border: 1px solid var(--line); border-radius: 6px; width: 100%; }
|
||||
input:focus-visible { outline: 3px solid var(--orange); outline-offset: 1px; }
|
||||
.linha-jogador { display: grid; grid-template-columns: 1fr 5.5rem auto; gap: .4rem; align-items: center; }
|
||||
.linha-jogador button { padding: .4rem .6rem; }
|
||||
button { font: inherit; font-weight: 700; padding: .65rem 1.1rem; border: 0; border-radius: 8px; background: var(--orange); color: #fff; cursor: pointer; }
|
||||
button.secundario { background: #fff; color: var(--navy); border: 1px solid var(--line); justify-self: start; }
|
||||
button:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.erro { color: #b42318; margin: 0; font-weight: 600; }
|
||||
.ok { background: #e7f6ec; border-left: 4px solid #067647; padding: 1rem 1.2rem; }
|
||||
#situacao.invalido { background: #fff1e3; border-left: 4px solid var(--orange); padding: 1rem 1.2rem; }
|
||||
|
||||
convite.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, nofollow">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<title>Convite para competição — Athletic Map</title>
|
||||
<link rel="stylesheet" href="/publico/_estilo.css">
|
||||
<link rel="stylesheet" href="/publico/convite.css?v=1">
|
||||
</head>
|
||||
<body>
|
||||
<header><div class="marca">Athletic <span>Map</span></div></header>
|
||||
<main>
|
||||
<h1 id="titulo">Convite para competição</h1>
|
||||
<p id="situacao" role="status" aria-live="polite">Abrindo o convite…</p>
|
||||
|
||||
<form id="formulario" hidden novalidate>
|
||||
<p class="explica">
|
||||
Preencha os dados do seu time e dos jogadores (só o nome e o número da camisa).
|
||||
A organização confere e aprova antes do time entrar na competição.
|
||||
</p>
|
||||
|
||||
<fieldset>
|
||||
<legend>Seu time</legend>
|
||||
<label>Nome do time *<input id="nome" maxlength="120" autocomplete="off" required></label>
|
||||
<label>Escola ou clube<input id="escola" maxlength="160" autocomplete="organization"></label>
|
||||
<label>Cidade<input id="cidade" maxlength="120" autocomplete="address-level2"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Responsável pelo time</legend>
|
||||
<label>Nome<input id="contatoNome" maxlength="120" autocomplete="name"></label>
|
||||
<label>Telefone<input id="contatoTelefone" maxlength="40" autocomplete="tel" inputmode="tel"></label>
|
||||
<label>E-mail<input id="contatoEmail" type="email" maxlength="160" autocomplete="email"></label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Jogadores</legend>
|
||||
<div id="jogadores"></div>
|
||||
<button type="button" id="maisJogador" class="secundario">+ Jogador</button>
|
||||
</fieldset>
|
||||
|
||||
<p id="erro" class="erro" role="alert" hidden></p>
|
||||
<button type="submit" id="enviar">Enviar time</button>
|
||||
</form>
|
||||
</main>
|
||||
<script src="/publico/desafio.js?v=1"></script>
|
||||
<script src="/publico/convite.js?v=2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
convite.js: |
|
||||
/* ===========================================================================
|
||||
* Convite para competição — a página que a OUTRA escola abre pelo link.
|
||||
*
|
||||
* Sem login: o token no endereço é o único segredo. A página pergunta ao
|
||||
* servidor a que o convite se refere e, se ele vale, deixa a pessoa mandar o
|
||||
* time com os jogadores (nome e camisa). O time chega PENDENTE: quem organiza
|
||||
* confere e aprova.
|
||||
*
|
||||
* Antes de enviar, a verificação anti-robô (desafio.js): pede um desafio ao
|
||||
* servidor, resolve no navegador ("Verificando…") e manda a prova junto com o
|
||||
* time, em `prova`. Um desafio por tentativa: ele é de uso único.
|
||||
*
|
||||
* Tudo o que vem do servidor entra na página com `textContent` — nunca como
|
||||
* HTML. O script é externo porque a CSP é `script-src 'self'`.
|
||||
* =========================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var global = window;
|
||||
|
||||
var TOKEN = /^[A-Za-z0-9_-]{20,64}$/;
|
||||
var MAX_JOGADORES = 60;
|
||||
|
||||
var token = decodeURIComponent((location.pathname.split("/").filter(Boolean).pop() || ""));
|
||||
var base = "/api/public/competicoes/convites/" + encodeURIComponent(token);
|
||||
|
||||
var titulo = document.getElementById("titulo");
|
||||
var situacao = document.getElementById("situacao");
|
||||
var formulario = document.getElementById("formulario");
|
||||
var listaJogadores = document.getElementById("jogadores");
|
||||
var erro = document.getElementById("erro");
|
||||
var enviar = document.getElementById("enviar");
|
||||
var ROTULO_ENVIAR = enviar.textContent;
|
||||
|
||||
function recusar(texto) {
|
||||
situacao.textContent = texto;
|
||||
situacao.className = "invalido";
|
||||
formulario.hidden = true;
|
||||
}
|
||||
|
||||
function mostrarErro(texto) {
|
||||
erro.textContent = texto;
|
||||
erro.hidden = !texto;
|
||||
}
|
||||
|
||||
function linhaDeJogador() {
|
||||
var linha = document.createElement("div");
|
||||
linha.className = "linha-jogador";
|
||||
|
||||
var nome = document.createElement("input");
|
||||
nome.placeholder = "Nome do jogador";
|
||||
nome.maxLength = 120;
|
||||
nome.setAttribute("aria-label", "Nome do jogador");
|
||||
nome.setAttribute("data-campo", "nome");
|
||||
|
||||
var camisa = document.createElement("input");
|
||||
camisa.type = "number";
|
||||
camisa.min = "0";
|
||||
camisa.max = "999";
|
||||
camisa.placeholder = "nº";
|
||||
camisa.setAttribute("aria-label", "Número da camisa");
|
||||
camisa.setAttribute("data-campo", "camisa");
|
||||
|
||||
var tirar = document.createElement("button");
|
||||
tirar.type = "button";
|
||||
tirar.className = "secundario";
|
||||
tirar.textContent = "×";
|
||||
tirar.setAttribute("aria-label", "Remover jogador");
|
||||
tirar.addEventListener("click", function () { linha.remove(); });
|
||||
|
||||
linha.appendChild(nome);
|
||||
linha.appendChild(camisa);
|
||||
linha.appendChild(tirar);
|
||||
listaJogadores.appendChild(linha);
|
||||
return linha;
|
||||
}
|
||||
|
||||
function valor(id) {
|
||||
var v = document.getElementById(id).value.trim();
|
||||
return v === "" ? null : v;
|
||||
}
|
||||
|
||||
function montarCorpo() {
|
||||
var jogadores = [];
|
||||
var linhas = listaJogadores.querySelectorAll(".linha-jogador");
|
||||
for (var i = 0; i < linhas.length; i++) {
|
||||
var nome = linhas[i].querySelector('[data-campo="nome"]').value.trim();
|
||||
if (!nome) continue;
|
||||
var c = linhas[i].querySelector('[data-campo="camisa"]').value;
|
||||
jogadores.push({ nome: nome, numeroCamisa: c === "" ? null : parseInt(c, 10) });
|
||||
}
|
||||
return {
|
||||
nome: valor("nome"),
|
||||
escola: valor("escola"),
|
||||
cidade: valor("cidade"),
|
||||
contatoNome: valor("contatoNome"),
|
||||
contatoTelefone: valor("contatoTelefone"),
|
||||
contatoEmail: valor("contatoEmail"),
|
||||
jogadores: jogadores
|
||||
};
|
||||
}
|
||||
|
||||
function lerResposta(r) {
|
||||
return r.json().catch(function () { return {}; }).then(function (corpo) { return { status: r.status, corpo: corpo }; });
|
||||
}
|
||||
|
||||
function aoEnviar(ev) {
|
||||
ev.preventDefault();
|
||||
mostrarErro("");
|
||||
var corpo = montarCorpo();
|
||||
if (!corpo.nome) { mostrarErro("Informe o nome do time."); return; }
|
||||
if (corpo.jogadores.length > MAX_JOGADORES) { mostrarErro("No máximo " + MAX_JOGADORES + " jogadores."); return; }
|
||||
enviar.disabled = true;
|
||||
enviar.textContent = "Verificando…";
|
||||
enviar.setAttribute("aria-busy", "true");
|
||||
|
||||
function voltar(texto) {
|
||||
enviar.disabled = false;
|
||||
enviar.textContent = ROTULO_ENVIAR;
|
||||
enviar.removeAttribute("aria-busy");
|
||||
mostrarErro(texto);
|
||||
}
|
||||
|
||||
fetch(base + "/desafio").then(lerResposta).then(function (r) {
|
||||
if (r.status !== 200) {
|
||||
var e = new Error("recusado");
|
||||
e.detalhe = (r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.";
|
||||
throw e;
|
||||
}
|
||||
if (!global.AtmDesafio) throw new Error("sem-crypto");
|
||||
return global.AtmDesafio.resolver(r.corpo);
|
||||
}).then(function (prova) {
|
||||
corpo.prova = prova;
|
||||
enviar.textContent = "Enviando…";
|
||||
return fetch(base + "/times", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(corpo)
|
||||
}).then(lerResposta);
|
||||
}).then(function (r) {
|
||||
if (r.status >= 200 && r.status < 300) {
|
||||
formulario.hidden = true;
|
||||
titulo.textContent = "Time enviado";
|
||||
situacao.textContent = "Recebemos o time \"" + corpo.nome + "\". A organização vai conferir os dados e aprovar. Você já pode fechar esta página.";
|
||||
situacao.className = "ok";
|
||||
return;
|
||||
}
|
||||
voltar((r.corpo && r.corpo.detail) || "Não foi possível enviar agora. Tente de novo em instantes.");
|
||||
}).catch(function (e) {
|
||||
if (e && e.detalhe) { voltar(e.detalhe); return; }
|
||||
if (e && (e.message === "sem-crypto" || e.message === "desafio-invalido" || e.message === "sem-solucao")) {
|
||||
voltar("Não foi possível fazer a verificação anti-robô neste navegador. Atualize a página ou use outro navegador.");
|
||||
return;
|
||||
}
|
||||
voltar("Sem conexão. Confira a internet e tente de novo.");
|
||||
});
|
||||
}
|
||||
|
||||
function iniciar() {
|
||||
if (!TOKEN.test(token)) { recusar("Este endereço de convite não é válido."); return; }
|
||||
fetch(base).then(lerResposta).then(function (r) {
|
||||
var info = r.corpo || {};
|
||||
if (!info.valido) {
|
||||
recusar(info.motivo ? "Não dá para usar este convite: " + info.motivo + "." : "Este convite não está disponível.");
|
||||
return;
|
||||
}
|
||||
titulo.textContent = "Convite: " + (info.competicao || "competição");
|
||||
situacao.textContent = (info.temporada ? "Temporada " + info.temporada + ". " : "")
|
||||
+ "Restam " + info.vagasRestantes + " vaga(s) neste convite.";
|
||||
situacao.className = "";
|
||||
formulario.hidden = false;
|
||||
for (var i = 0; i < 5; i++) linhaDeJogador();
|
||||
}).catch(function () {
|
||||
recusar("Não foi possível abrir o convite agora. Tente de novo em instantes.");
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("maisJogador").addEventListener("click", function () {
|
||||
if (listaJogadores.querySelectorAll(".linha-jogador").length < MAX_JOGADORES) linhaDeJogador().querySelector("input").focus();
|
||||
});
|
||||
formulario.addEventListener("submit", aoEnviar);
|
||||
iniciar();
|
||||
})();
|
||||
|
||||
desafio.js: |
|
||||
/* ===========================================================================
|
||||
* Verificação anti-robô das páginas públicas — prova de trabalho no estilo
|
||||
* ALTCHA, sem serviço de terceiro (nada de quem preenche sai do Athletic Map).
|
||||
*
|
||||
* O servidor manda { algoritmo: "SHA-256", desafio, sal, assinatura, maximo }.
|
||||
* Aqui se procura o número n (0..maximo) tal que sha256(sal + n) == desafio, e
|
||||
* devolve-se { numero, sal, assinatura } para ir junto no envio. Em média são
|
||||
* maximo/2 hashes: ~1–2 s num celular com maximo = 150 000.
|
||||
*
|
||||
* Usa crypto.subtle (só existe em HTTPS — e as páginas públicas são HTTPS).
|
||||
* Os hashes saem em lotes paralelos: um `await` por hash custaria mais que o
|
||||
* próprio hash. Script externo porque a CSP é `script-src 'self'`.
|
||||
*
|
||||
* O teste do servidor é DesafioDoConvite (servico-campeonato).
|
||||
* =========================================================================== */
|
||||
(function (global) {
|
||||
"use strict";
|
||||
|
||||
var LOTE = 1000;
|
||||
var HEX = /^[0-9a-f]{64}$/;
|
||||
|
||||
function bytesDoHex(h) {
|
||||
var b = new Uint8Array(h.length / 2);
|
||||
for (var i = 0; i < b.length; i++) b[i] = parseInt(h.substr(i * 2, 2), 16);
|
||||
return b;
|
||||
}
|
||||
|
||||
function iguais(buf, alvo) {
|
||||
var a = new Uint8Array(buf);
|
||||
if (a.length !== alvo.length) return false;
|
||||
for (var i = 0; i < a.length; i++) if (a[i] !== alvo[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{algoritmo:string, desafio:string, sal:string, assinatura:string, maximo:number}} d
|
||||
* @returns {Promise<{numero:number, sal:string, assinatura:string}>}
|
||||
* rejeita com Error("sem-crypto"), Error("desafio-invalido") ou Error("sem-solucao")
|
||||
*/
|
||||
function resolver(d) {
|
||||
var subtle = global.crypto && global.crypto.subtle;
|
||||
var Codificador = global.TextEncoder;
|
||||
if (!subtle || !Codificador) return Promise.reject(new Error("sem-crypto"));
|
||||
if (!d || d.algoritmo !== "SHA-256" || !HEX.test(d.desafio || "") || typeof d.sal !== "string"
|
||||
|| typeof d.maximo !== "number" || d.maximo < 0 || d.maximo > 5000000) {
|
||||
return Promise.reject(new Error("desafio-invalido"));
|
||||
}
|
||||
var alvo = bytesDoHex(d.desafio);
|
||||
var cod = new Codificador();
|
||||
|
||||
function lote(inicio) {
|
||||
if (inicio > d.maximo) return Promise.reject(new Error("sem-solucao"));
|
||||
var fim = Math.min(inicio + LOTE - 1, d.maximo);
|
||||
var hashes = [];
|
||||
for (var n = inicio; n <= fim; n++) hashes.push(subtle.digest("SHA-256", cod.encode(d.sal + n)));
|
||||
return Promise.all(hashes).then(function (hs) {
|
||||
for (var i = 0; i < hs.length; i++) {
|
||||
if (iguais(hs[i], alvo)) return { numero: inicio + i, sal: d.sal, assinatura: d.assinatura };
|
||||
}
|
||||
return lote(fim + 1);
|
||||
});
|
||||
}
|
||||
return lote(0);
|
||||
}
|
||||
|
||||
global.AtmDesafio = { resolver: resolver };
|
||||
})(window);
|
||||
|
||||
fontes--fontes.css: |
|
||||
/* ============================================================
|
||||
* As fontes da landing page, servidas POR NOS.
|
||||
@@ -609,9 +935,7 @@ data:
|
||||
|
||||
/* marca */
|
||||
.logo{display:inline-flex; align-items:center; gap:11px; font-weight:700; font-size:1.22rem; letter-spacing:-.02em; color:var(--ink); text-decoration:none;}
|
||||
.mark{display:inline-flex; gap:3px; align-items:flex-end;}
|
||||
.mark i{display:block; width:5px; border-radius:2px; background:var(--laranja); transform:skewX(-14deg);}
|
||||
.mark i:nth-child(1){height:13px} .mark i:nth-child(2){height:19px} .mark i:nth-child(3){height:25px}
|
||||
.logo img{display:block; height:37px; width:auto;}
|
||||
|
||||
.topbar{background:var(--surface); border-bottom:1px solid var(--line);}
|
||||
.topbar .wrap{display:flex; align-items:center; justify-content:space-between; gap:16px; padding-block:14px;}
|
||||
@@ -814,7 +1138,7 @@ data:
|
||||
|
||||
<header class="topbar">
|
||||
<div class="wrap">
|
||||
<a class="logo" href="#"><span class="mark"><i></i><i></i><i></i></span>Athletic Map</a>
|
||||
<a class="logo" href="#" aria-label="Athletic Map"><img src="/publico/img/logo-athletic-map.png" alt="Athletic Map" width="150" height="37"></a>
|
||||
<a class="zap" id="topZap" href="#form">Falar com o time →</a>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1698,6 +2022,20 @@ data:
|
||||
.termos-uso-caixa input { margin-right: 6px; }
|
||||
.termos-uso-aceite { margin: 10px 0; }
|
||||
|
||||
/* ── Abas (2026-09-29) — mesmo desenho do painel do dono ───────────────────── */
|
||||
.abas { display: flex; gap: 4px; border-bottom: 1px solid var(--line); margin: 4px 0 18px; overflow-x: auto; }
|
||||
.aba {
|
||||
background: none; border: 0; border-bottom: 3px solid transparent; padding: 10px 16px;
|
||||
font: inherit; font-weight: 600; color: var(--muted); cursor: pointer; white-space: nowrap;
|
||||
}
|
||||
.aba:hover { color: var(--navy); }
|
||||
.aba.ativa { color: var(--navy); border-bottom-color: var(--orange); }
|
||||
.aba .badge {
|
||||
display: inline-block; min-width: 8px; height: 8px; margin-left: 6px; border-radius: 999px;
|
||||
background: var(--orange); vertical-align: middle;
|
||||
}
|
||||
.aba .badge.num { min-width: 18px; height: auto; padding: 0 6px; font-size: 12px; color: #fff; line-height: 18px; }
|
||||
|
||||
minha-conta.html: |
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
@@ -1722,7 +2060,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.88">
|
||||
<link rel="stylesheet" href="/publico/minha-conta.css?v=2.90">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1765,23 +2103,56 @@ 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>
|
||||
<!-- Abas (2026-09-29, pedido do dono): a pagina empilhava visao geral, Termos,
|
||||
escolas ativas e canceladas numa coluna so. Mesmo padrao do painel do dono. -->
|
||||
<nav class="abas" role="tablist" aria-label="Seções da conta">
|
||||
<button type="button" class="aba ativa" data-aba="escolas" role="tab">Minhas escolas</button>
|
||||
<button type="button" class="aba" data-aba="visao" role="tab">Visão geral</button>
|
||||
<button type="button" class="aba" data-aba="termos" role="tab">Termos de Uso <span id="termosBadge" class="badge oculto"></span></button>
|
||||
<button type="button" class="aba oculto" data-aba="canceladas" role="tab" id="abaCanceladas">Canceladas</button>
|
||||
</nav>
|
||||
|
||||
<div id="produtos" class="produtos"></div>
|
||||
<div id="aba-escolas" class="painel-aba">
|
||||
<div id="produtos" class="produtos"></div>
|
||||
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
<div id="nenhum" class="cartao oculto">
|
||||
<h2>Nenhum produto ainda</h2>
|
||||
<p class="ajuda">
|
||||
Se você acabou de pagar, a conta pode estar sendo criada — isso leva
|
||||
alguns minutos. Se já faz mais tempo, fale com a gente.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-visao" class="painel-aba 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="visaoVazia" class="cartao">
|
||||
<h2>Visão <span class="hl">geral</span></h2>
|
||||
<p class="ajuda">Os números aparecem aqui quando as suas escolinhas enviarem o primeiro resumo
|
||||
(uma vez por dia, de madrugada).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-termos" class="painel-aba oculto">
|
||||
<div id="termosDeUso"></div>
|
||||
<div id="termosEmDia" class="cartao oculto">
|
||||
<h2>Termos de Uso</h2>
|
||||
<p class="ajuda">Nada pendente: a versão vigente já foi aceita para todas as suas escolinhas.
|
||||
<a href="/termos" target="_blank" rel="noopener">Ler os Termos de Uso</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="aba-canceladas" class="painel-aba oculto">
|
||||
<p class="ajuda">Assinaturas canceladas. Regularize o pagamento para voltar a entrar; os dados ficam
|
||||
guardados por 30 dias depois do cancelamento.</p>
|
||||
<div id="produtosCancelados" class="produtos"></div>
|
||||
</div>
|
||||
|
||||
<p class="rodape">
|
||||
@@ -1801,7 +2172,7 @@ data:
|
||||
|
||||
<footer class="direitos">© 2026 Athletic Map. Todos os direitos reservados.</footer>
|
||||
|
||||
<script src="/publico/minha-conta.js?v=2.88"></script>
|
||||
<script src="/publico/minha-conta.js?v=2.90"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1870,6 +2241,36 @@ data:
|
||||
var erroTexto = document.getElementById("erroTexto");
|
||||
var botaoSair = document.getElementById("sair");
|
||||
|
||||
/* ── ABAS (2026-09-29) ────────────────────────────────────────────────────────
|
||||
* Minhas escolas | Visao geral | Termos de Uso | Canceladas. A ultima aba aberta
|
||||
* fica no localStorage (conveniencia deste navegador; sem ela, "escolas"). */
|
||||
var ABA_CHAVE = "atm.conta.aba";
|
||||
function abrirAba(nome) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao || botao.classList.contains("oculto")) { nome = "escolas"; botao = document.querySelector('.aba[data-aba="escolas"]'); }
|
||||
if (!botao) return;
|
||||
document.querySelectorAll(".aba").forEach(function (b) { b.classList.toggle("ativa", b === botao); });
|
||||
document.querySelectorAll(".painel-aba").forEach(function (painel) {
|
||||
painel.classList.toggle("oculto", painel.id !== "aba-" + nome);
|
||||
});
|
||||
try { localStorage.setItem(ABA_CHAVE, nome); } catch (e) { /* aba privada */ }
|
||||
}
|
||||
function abaLembrada() { try { return localStorage.getItem(ABA_CHAVE) || "escolas"; } catch (e) { return "escolas"; } }
|
||||
/* Marca a aba: `pendente` acende a bolinha; `quantos` escreve o numero. */
|
||||
function marcarAba(nome, pendente, quantos) {
|
||||
var botao = document.querySelector('.aba[data-aba="' + nome + '"]');
|
||||
if (!botao) return;
|
||||
var badge = botao.querySelector(".badge");
|
||||
if (badge) {
|
||||
badge.classList.toggle("oculto", !pendente && !quantos);
|
||||
badge.classList.toggle("num", !!quantos);
|
||||
badge.textContent = quantos ? String(quantos) : "";
|
||||
}
|
||||
}
|
||||
document.querySelectorAll(".aba").forEach(function (b) {
|
||||
b.addEventListener("click", function () { abrirAba(b.getAttribute("data-aba")); });
|
||||
});
|
||||
|
||||
function mostrar(qual) {
|
||||
[porta, carregando, conteudo, caixaErro].forEach(function (s) {
|
||||
if (s) s.classList.add("oculto");
|
||||
@@ -1988,14 +2389,18 @@ data:
|
||||
return n ? n.charAt(0).toUpperCase() + n.slice(1) : "—";
|
||||
}
|
||||
|
||||
/* Tenant de DEMONSTRACAO do dono: o servidor marca (`demonstracao: true`). O
|
||||
cartao diz isso ao lado da situacao, e nao ganha "Trocar de plano" nem
|
||||
"Modulos avulsos" — nao ha cobranca, e o servidor recusaria de qualquer jeito. */
|
||||
function cartao(p) {
|
||||
var s = SITUACAO[p.situacao] || ["neutro", String(p.situacao || "").toLowerCase()];
|
||||
var podeEntrar = p.endereco && p.situacao === "ATIVA";
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + '"'
|
||||
return '<article class="produto ' + classeDoPlano(p.plano) + (p.demonstracao ? " demonstracao" : "") + '"'
|
||||
+ (p.tenant ? ' data-tenant="' + escapar(p.tenant) + '"' : "") + ">"
|
||||
+ '<span class="faixa-plano">Plano ' + escapar(nomeDoPlano(p.plano)) + "</span>"
|
||||
+ "<h2>" + escapar(p.escolinha || "Sua escolinha") + "</h2>"
|
||||
+ '<span class="selo ' + s[0] + '">' + s[1] + "</span>"
|
||||
+ (p.demonstracao ? '<span class="selo neutro">demonstração — sem cobrança</span>' : "")
|
||||
+ (podeEntrar
|
||||
? '<a class="btn" href="' + escapar(p.endereco) + '">Entrar →</a>'
|
||||
: '<p class="ajuda">' + (p.situacao === "PREPARANDO"
|
||||
@@ -2014,10 +2419,22 @@ data:
|
||||
.then(function (lista) {
|
||||
enderecos = {};
|
||||
(lista || []).forEach(function (p) { if (p.tenant) enderecos[p.tenant] = p.endereco; });
|
||||
produtos.innerHTML = (lista || []).map(cartao).join("");
|
||||
nenhum.classList.toggle("oculto", (lista || []).length > 0);
|
||||
var ativas = (lista || []).filter(function (p) { return p.situacao !== "CANCELADA"; });
|
||||
var canceladas = (lista || []).filter(function (p) { return p.situacao === "CANCELADA"; });
|
||||
produtos.innerHTML = ativas.map(cartao).join("");
|
||||
var caixaCanceladas = document.getElementById("produtosCancelados");
|
||||
if (caixaCanceladas) caixaCanceladas.innerHTML = canceladas.map(cartao).join("");
|
||||
var abaCanceladas = document.getElementById("abaCanceladas");
|
||||
if (abaCanceladas) {
|
||||
abaCanceladas.classList.toggle("oculto", canceladas.length === 0);
|
||||
abaCanceladas.textContent = "Canceladas" + (canceladas.length ? " (" + canceladas.length + ")" : "");
|
||||
}
|
||||
nenhum.classList.toggle("oculto", ativas.length > 0);
|
||||
mostrar(conteudo);
|
||||
(lista || []).forEach(function (p) { if (p.tenant && p.situacao === "ATIVA") acrescentarBotaoDePlano(p.tenant); });
|
||||
abrirAba(abaLembrada());
|
||||
(lista || []).forEach(function (p) {
|
||||
if (p.tenant && p.situacao === "ATIVA" && !p.demonstracao) acrescentarBotaoDePlano(p.tenant);
|
||||
});
|
||||
return Promise.all([carregarCobranca(), carregarTermo(), carregarFaturas(), carregarVisao(),
|
||||
carregarTermosDeUso()]);
|
||||
});
|
||||
@@ -2323,8 +2740,10 @@ data:
|
||||
.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; });
|
||||
var emDia = document.getElementById("termosEmDia");
|
||||
var faltam = (d && d.versao && d.escolinhas) ? d.escolinhas.filter(function (e) { return !e.aceito; }) : [];
|
||||
if (emDia) emDia.classList.toggle("oculto", faltam.length > 0);
|
||||
marcarAba("termos", faltam.length > 0);
|
||||
if (!faltam.length) return;
|
||||
var aviso = document.createElement("section");
|
||||
aviso.id = "avisoTermos";
|
||||
@@ -2335,7 +2754,8 @@ data:
|
||||
+ ". 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);
|
||||
var caixaTermos = document.getElementById("termosDeUso");
|
||||
if (caixaTermos) caixaTermos.appendChild(aviso); else produtos.parentNode.insertBefore(aviso, produtos);
|
||||
aviso.querySelector("button").addEventListener("click", function () {
|
||||
aceitarTermosDeUso(aviso, d.versao);
|
||||
});
|
||||
@@ -2588,7 +3008,9 @@ data:
|
||||
.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 vazia = document.getElementById("visaoVazia");
|
||||
if (!tabela) { caixa.classList.add("oculto"); if (vazia) vazia.classList.remove("oculto"); return; }
|
||||
if (vazia) vazia.classList.add("oculto");
|
||||
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.";
|
||||
@@ -3171,7 +3593,7 @@ data:
|
||||
<option value="NASCENDO">Nascendo</option>
|
||||
</select>
|
||||
<label class="filtro"><input type="checkbox" id="soAtencao"> só com alerta</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes</label>
|
||||
<label class="filtro"><input type="checkbox" id="semTeste"> esconder testes e demonstrações</label>
|
||||
</div>
|
||||
<div class="tabela-rolagem">
|
||||
<table class="tabela clientes">
|
||||
@@ -3334,7 +3756,7 @@ data:
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/publico/painel.js?v=2.86"></script>
|
||||
<script src="/publico/painel.js?v=2.89"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3630,9 +4052,13 @@ data:
|
||||
bloco("Recebido desde o início", reaisCurto(i.recebidoTotal),
|
||||
i.clientesSemDadoDoAsaas > 0 ? i.clientesSemDadoDoAsaas + " sem resposta do Asaas" : "extrato do Asaas", "")
|
||||
].join("");
|
||||
// Teste e demonstracao aparecem na lista, mas nao nos numeros — e a tela diz.
|
||||
var foraDosNumeros = [];
|
||||
if (i.contasDeTeste > 0) foraDosNumeros.push(i.contasDeTeste + " de teste");
|
||||
if (i.demonstracoes > 0) foraDosNumeros.push(i.demonstracoes + " de demonstração");
|
||||
$("kpiClientes").innerHTML = [
|
||||
bloco("Clientes ativos", esc(i.clientesAtivos),
|
||||
i.contasDeTeste > 0 ? "+ " + i.contasDeTeste + " de teste (fora dos números)" : "", "ok"),
|
||||
foraDosNumeros.length ? "+ " + foraDosNumeros.join(", ") + " (fora dos números)" : "", "ok"),
|
||||
bloco("Novos no mês", esc(i.novosNoMes), comparacao(i.novosNoMes, i.novosMesAnterior), ""),
|
||||
bloco("Leads no mês", esc(i.leadsNoMes), comparacao(i.leadsNoMes, i.leadsMesAnterior), ""),
|
||||
bloco("Conversão", pct(i.conversao), "de lead para cliente pagante", ""),
|
||||
@@ -3671,7 +4097,7 @@ data:
|
||||
if (plano && c.plano !== plano) return false;
|
||||
if (estado && (c.estado || "NASCENDO") !== estado) return false;
|
||||
if (soAtencao && !(c.alertas || []).length) return false;
|
||||
if (semTeste && c.teste) return false;
|
||||
if (semTeste && (c.teste || c.demonstracao)) return false;
|
||||
if (busca) {
|
||||
var alvo = [c.cliente, c.escolinha, c.email, c.cidade, c.tenant, c.whatsapp, c.documento]
|
||||
.join(" ").toLowerCase();
|
||||
@@ -3701,7 +4127,9 @@ data:
|
||||
}
|
||||
|
||||
var SITUACAO_ASAAS = { ACTIVE: "ativa", INACTIVE: "encerrada", EXPIRED: "expirada" };
|
||||
function assinaturaNoAsaas(a) {
|
||||
/* A demonstracao nao existe no Asaas DE PROPOSITO: nao e "nao existe" em vermelho. */
|
||||
function assinaturaNoAsaas(a, c) {
|
||||
if (c && c.demonstracao) return "demonstração — sem cobrança";
|
||||
if (!a || !a.disponivel) return "indisponível agora";
|
||||
if (!a.existe) return '<b class="vermelho">não existe</b>';
|
||||
var s = SITUACAO_ASAAS[a.situacao] || txt(a.situacao);
|
||||
@@ -3738,10 +4166,11 @@ data:
|
||||
var gravidade = (c.alertas || []).reduce(function (g, a) {
|
||||
return a.gravidade === "critico" ? "critico" : (g === "critico" ? g : (a.gravidade === "alerta" ? "alerta" : g));
|
||||
}, "");
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste ? " teste" : "")
|
||||
return '<tr class="clicavel' + (gravidade ? " linha-" + gravidade : "") + (c.teste || c.demonstracao ? " teste" : "")
|
||||
+ (!c.tenant ? " destaque" : "") + '" data-assinatura="' + esc(c.assinatura) + '" tabindex="0">'
|
||||
+ "<td><b>" + txt(c.cliente) + "</b>"
|
||||
+ (c.teste ? ' <span class="selo neutro">teste</span>' : "")
|
||||
+ (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "")
|
||||
+ '<span class="sub">' + (c.tenant
|
||||
? '<a href="' + esc(c.endereco) + '" target="_blank" rel="noopener noreferrer">' + esc(c.tenant) + "</a>"
|
||||
: "sem tenant")
|
||||
@@ -3750,9 +4179,9 @@ data:
|
||||
+ "<td>" + etiquetaPlano(c) + "</td>"
|
||||
+ "<td>" + selo(c.estado, c.tenant) + '<span class="sub">' + (c.diasDeCliente !== null && c.diasDeCliente !== undefined
|
||||
? "cliente há " + esc(c.diasDeCliente) + " dia(s)" : "") + "</span></td>"
|
||||
+ '<td class="num">' + reais(c.valorMensal) + diferencaDeTabela(c) + "</td>"
|
||||
+ '<td class="num">' + (f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + proximaCobranca(f) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? '<span class="ajuda">sem cobrança</span>' : reais(c.valorMensal) + diferencaDeTabela(c)) + "</td>"
|
||||
+ '<td class="num">' + (c.demonstracao ? "—" : f.disponivel ? reais(f.totalPago) + '<span class="sub">' + esc(f.cobrancasPagas) + " cobrança(s)</span>" : "—") + "</td>"
|
||||
+ "<td>" + (c.demonstracao ? '<span class="ajuda">—</span>' : proximaCobranca(f)) + "</td>"
|
||||
+ "<td>" + usoCurto(c.uso, c.usoDeDentro) + "</td>"
|
||||
+ "<td>" + seloSubconta(c.subconta) + "</td>"
|
||||
+ "</tr>";
|
||||
@@ -3778,7 +4207,9 @@ data:
|
||||
+ esc(a.titulo) + '</b><span class="sub">' + esc(a.detalhe) + "</span></div></li>";
|
||||
}).join("");
|
||||
|
||||
var extrato = f.disponivel
|
||||
var extrato = c.demonstracao
|
||||
? '<p class="ajuda">Tenant de demonstração: não há assinatura nem cobrança no Asaas.</p>'
|
||||
: f.disponivel
|
||||
? ((f.cobrancas || []).length
|
||||
? '<table class="tabela"><thead><tr><th>Vencimento</th><th>Pago em</th><th>Forma</th><th class="num">Valor</th><th class="num">Líquido</th><th>Situação</th><th></th></tr></thead><tbody>'
|
||||
+ f.cobrancas.map(function (cb) {
|
||||
@@ -3799,7 +4230,7 @@ data:
|
||||
return '<header class="ficha-topo ' + classePlano(c.plano) + '">'
|
||||
+ '<span class="etiqueta ' + classePlano(c.plano) + '">Plano ' + esc(nomePlano(c.plano))
|
||||
+ (c.ciclo === "ANUAL" ? " · anual" : " · mensal") + "</span>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + "</h2>"
|
||||
+ '<h2 id="fichaTitulo">' + txt(c.cliente) + (c.demonstracao ? ' <span class="selo neutro">demonstração</span>' : "") + "</h2>"
|
||||
+ '<p class="sub">' + (c.escolinha && c.escolinha !== c.cliente ? esc(c.escolinha) + " · " : "")
|
||||
+ selo(c.estado, c.tenant) + (c.estadoDesde ? " desde " + dataHora(c.estadoDesde) : "") + "</p>"
|
||||
+ '<div class="atalhos">'
|
||||
@@ -3807,7 +4238,9 @@ data:
|
||||
+ (whatsapp(c.whatsapp) ? '<a class="btn claro" href="' + esc(whatsapp(c.whatsapp)) + '" target="_blank" rel="noopener noreferrer">WhatsApp</a>' : "")
|
||||
+ (c.email ? '<a class="btn claro" href="mailto:' + esc(c.email) + '">E-mail</a>' : "")
|
||||
+ (f.faturaVencida ? '<a class="btn claro" href="' + esc(linkSeguro(f.faturaVencida) || "#") + '" target="_blank" rel="noopener noreferrer">Fatura vencida</a>' : "")
|
||||
+ "</div></header>"
|
||||
+ (c.tenant ? '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="reenviar">Reenviar licença</button>'
|
||||
+ '<button type="button" class="btn claro" data-licenca="' + esc(c.tenant) + '" data-acao="trocar">Trocar chave</button>' : "")
|
||||
+ '</div><p id="licencaAviso" class="ajuda" aria-live="polite"></p></header>'
|
||||
+ (alertas ? '<ul class="lista-alertas">' + alertas + "</ul>" : "")
|
||||
|
||||
+ '<div class="grade-ficha">'
|
||||
@@ -3821,14 +4254,17 @@ data:
|
||||
+ '<section><h3>Assinatura</h3><dl>'
|
||||
+ item("Plano", esc(nomePlano(c.plano)) + " " + (c.ciclo === "ANUAL" ? "anual" : "mensal"))
|
||||
+ item("Valor do ciclo", reais(c.valorDoCiclo)) + item("Vale por mês", reais(c.valorMensal) + diferencaDeTabela(c))
|
||||
+ item("De onde vem o valor", c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas))
|
||||
+ item("De onde vem o valor", c.demonstracao ? "demonstração — sem cobrança"
|
||||
: c.valorDaFonte === "asaas" ? "a assinatura no Asaas" : "a tabela de preços (Asaas não respondeu)")
|
||||
+ item("Situação no Asaas", assinaturaNoAsaas(c.assinaturaNoAsaas, c))
|
||||
+ item("Contratado em", dataHora(c.contratadoEm)) + item("Cliente há", c.diasDeCliente === null || c.diasDeCliente === undefined ? "—" : esc(c.diasDeCliente) + " dia(s)")
|
||||
+ item("Assinatura no Asaas", txt(c.assinatura)) + item("Cliente no Asaas", txt(c.clienteNoProvedor))
|
||||
+ "</dl></section>"
|
||||
|
||||
+ '<section><h3>Dinheiro</h3><dl>'
|
||||
+ (f.disponivel
|
||||
+ (c.demonstracao
|
||||
? item("Cobrança", "nenhuma — tenant de demonstração")
|
||||
: f.disponivel
|
||||
? item("Total pago", reais(f.totalPago)) + item("Líquido (após taxas)", reais(f.totalLiquido))
|
||||
+ item("Taxas pagas", reais(f.taxas))
|
||||
+ item("Último pagamento", f.ultimoPagamento ? data(f.ultimoPagamento.data) + " · " + reais(f.ultimoPagamento.valor) + " · " + txt(f.ultimoPagamento.forma) : "—")
|
||||
@@ -4325,6 +4761,55 @@ data:
|
||||
corpo.addEventListener("keydown", function (ev) { if (ev.key === "Enter") aoEscolher(ev); });
|
||||
$("alertas").addEventListener("click", aoEscolher);
|
||||
|
||||
/* ── licenca dos apps (dono, treinador, pais) ─────────────────────────────
|
||||
* Reenviar = mandar a MESMA chave da escola ao dono da compra (captacao 4.32+;
|
||||
* so a chave antiga, guardada so com hash, e trocada uma vez). Nada muda para
|
||||
* ninguem: sem confirmacao.
|
||||
* Trocar = revogar a chave e gerar outra. Desde os apps 1.0.4 (captacao 4.33)
|
||||
* o app pergunta a situacao da chave ao abrir: trocar DERRUBA os apps de todos
|
||||
* da escola (dono, treinadores, pais) em ate 15 min de uso, e cada um precisa
|
||||
* da chave nova. Por isso pede o nome do tenant digitado. */
|
||||
var API_LICENCA = "/api/plataforma/painel/licencas/";
|
||||
|
||||
function acaoDaLicenca(tenant, acao, aviso) {
|
||||
if (!tokenAtual) return Promise.resolve();
|
||||
if (acao === "trocar") {
|
||||
var digitado = window.prompt("TROCAR a chave de " + tenant + "?\n\n"
|
||||
+ "A chave atual para de valer e os apps de TODOS da escola (dono, treinadores e pais) saem da conta "
|
||||
+ "em até 15 minutos de uso; cada um vai precisar da chave nova, que vai por e-mail ao dono da compra.\n"
|
||||
+ "Use só se a chave vazou.\n\nPara confirmar, digite o nome do tenant (" + tenant + "):");
|
||||
if (digitado === null) return Promise.resolve();
|
||||
if (digitado.trim() !== tenant) {
|
||||
aviso.textContent = "Nada foi trocado: o nome digitado não confere.";
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
aviso.textContent = "Enviando…";
|
||||
return fetch(API_LICENCA + encodeURIComponent(tenant) + "/" + acao, {
|
||||
method: "POST", headers: { "x-atm-admin-token": tokenAtual, "Content-Type": "application/json" }, body: "{}"
|
||||
}).then(function (r) {
|
||||
if (r.status === 401) throw new Error("O token expirou. Saia e entre de novo no painel.");
|
||||
return r.json().then(function (d) {
|
||||
if (!r.ok) throw new Error((d && d.detail) || ("O servidor recusou (" + r.status + ")."));
|
||||
return d;
|
||||
});
|
||||
}).then(function (d) {
|
||||
var oQue = acao === "trocar" ? "Chave NOVA" : "Chave da escola";
|
||||
aviso.textContent = oQue + (d.emailEnviado ? " enviada para " : " pronta; o e-mail vai em instantes para ")
|
||||
+ (d.destino || "o dono da compra") + " (termina em " + d.finalDaChave + ").";
|
||||
}).catch(function (e) {
|
||||
aviso.textContent = e && e.message && e.message !== "Failed to fetch" ? e.message : "Sem conexão com o servidor.";
|
||||
});
|
||||
}
|
||||
|
||||
$("fichaCorpo").addEventListener("click", function (ev) {
|
||||
var b = ev.target.closest ? ev.target.closest("[data-licenca]") : null;
|
||||
if (!b) return;
|
||||
b.disabled = true;
|
||||
acaoDaLicenca(b.getAttribute("data-licenca"), b.getAttribute("data-acao") || "reenviar", $("licencaAviso"))
|
||||
.then(function () { b.disabled = false; });
|
||||
});
|
||||
|
||||
$("fecharFicha").addEventListener("click", fecharFicha);
|
||||
$("ficha").addEventListener("click", function (ev) { if (ev.target === $("ficha")) fecharFicha(); });
|
||||
document.addEventListener("keydown", function (ev) { if (ev.key === "Escape") fecharFicha(); });
|
||||
|
||||
Reference in New Issue
Block a user