From f3de600db976e5854246e02f2696fb69c2d17b5d Mon Sep 17 00:00:00 2001 From: deploy Date: Sat, 26 Sep 2026 22:18:45 +0000 Subject: [PATCH] chore(tenants): conteudo de publico/ atualizado --- moldes/consolidado/96-publico-configmap.yaml | 249 ++++++++++++++++++- tenants/plataforma/96-publico-configmap.yaml | 249 ++++++++++++++++++- 2 files changed, 478 insertions(+), 20 deletions(-) diff --git a/moldes/consolidado/96-publico-configmap.yaml b/moldes/consolidado/96-publico-configmap.yaml index a2053ce..4a284ba 100644 --- a/moldes/consolidado/96-publico-configmap.yaml +++ b/moldes/consolidado/96-publico-configmap.yaml @@ -1410,6 +1410,24 @@ data: .form-cobranca .btn:disabled { opacity: .6; cursor: wait; } .erro-form { color: var(--perigo); font-size: 14px; margin: 0; } + /* ── o termo de dados, dentro do cartao ─────────────────────────────────── */ + .termo { + align-self: stretch; margin-top: 20px; padding-top: 16px; + border-top: 1px dashed var(--line); + display: flex; flex-direction: column; align-items: flex-start; gap: 8px; + } + .termo .selo { margin-bottom: 0; } + .produto .termo .btn { margin-top: 6px; } + .termo .btn:disabled { opacity: .6; cursor: wait; } + .btn.secundario { background: transparent; color: var(--navy); border: 1px solid var(--line); } + .btn.secundario:hover { background: var(--line); } + .termo details { align-self: stretch; font-size: 14px; } + .termo summary { cursor: pointer; font-weight: 600; } + .termo-texto { + white-space: pre-wrap; margin-top: 8px; padding: 12px; max-height: 280px; overflow-y: auto; + border: 1px solid var(--line); border-radius: 10px; background: var(--branco); line-height: 1.5; + } + /* Empilhado, a foto sai: no celular ela so empurraria os produtos para baixo. */ @media (max-width: 760px) { .topo { padding: 12px 16px; } @@ -1445,7 +1463,7 @@ data: numero a cada publicacao que mexa nesses arquivos. --> - + @@ -1515,7 +1533,7 @@ data: - + @@ -1553,7 +1571,8 @@ data: CLIENTE: "painel-do-cliente", RETORNO: location.origin + "/minha-conta", API: "/api/cliente/meus-produtos", - COBRANCA: "/api/cliente/cobranca" + COBRANCA: "/api/cliente/cobranca", + TERMO: "/api/cliente/termo-de-dados" }; var CHAVE_VERIFIER = "atm.conta.verifier"; @@ -1725,7 +1744,7 @@ data: produtos.innerHTML = (lista || []).map(cartao).join(""); nenhum.classList.toggle("oculto", (lista || []).length > 0); mostrar(conteudo); - return carregarCobranca(); + return Promise.all([carregarCobranca(), carregarTermo()]); }); } @@ -1835,7 +1854,10 @@ data: if (!alvo) return; var velho = alvo.querySelector(".cobranca"); if (velho) velho.remove(); - alvo.insertAdjacentHTML("beforeend", alertaDeCobranca(c)); + // Antes do bloco do termo, se ele ja estiver la: a cobranca e mais urgente. + var termo = alvo.querySelector(".termo"); + if (termo) termo.insertAdjacentHTML("beforebegin", alertaDeCobranca(c)); + else alvo.insertAdjacentHTML("beforeend", alertaDeCobranca(c)); }); }) .catch(function (e) { if (window.console) console.warn("cobranca:", e); }); @@ -1872,6 +1894,83 @@ data: }); } + /* ── O TERMO DE DADOS ──────────────────────────────────────────────────────── + * + * A autorizacao para a Athletic Map ver o USO do sistema (ultimo acesso, + * alunos ativos, treinos). Sem termo publicado, o bloco nem aparece — o + * servidor devolve `termo: null` enquanto o texto e rascunho. Como a + * cobranca, falhar aqui nao derruba a tela. */ + + function dataCurta(iso) { + var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || "")); + return m ? m[3] + "/" + m[2] + "/" + m[1] : ""; + } + + function blocoDoTermo(termo, e) { + var titulo = 'Acompanhamento de uso'; + if (e.aceito) { + return '
' + titulo + + 'autorizado' + (e.aceitoEm ? " em " + dataCurta(e.aceitoEm) : "") + "" + + '

Vemos só a data do último acesso e contagens de alunos e treinos.

' + + '' + + '

'; + } + return '
' + titulo + + 'não autorizado' + + '

Autorize a Athletic Map a acompanhar o uso do sistema para dar suporte.

' + + '
Ler o termo (versão ' + escapar(termo.versao) + ")" + + '
' + escapar(termo.texto) + "
" + + '' + + '

'; + } + + function carregarTermo() { + return fetch(CONFIG.TERMO, { headers: { Authorization: "Bearer " + token } }) + .then(function (r) { return r.ok ? r.json() : { termo: null, escolinhas: [] }; }) + .then(function (d) { + produtos.querySelectorAll(".termo").forEach(function (el) { el.remove(); }); + if (!d || !d.termo) return; + (d.escolinhas || []).forEach(function (e) { + var alvo = null; + produtos.querySelectorAll("[data-tenant]").forEach(function (el) { + if (el.tagName === "ARTICLE" && el.getAttribute("data-tenant") === e.tenant) alvo = el; + }); + if (alvo) alvo.insertAdjacentHTML("beforeend", blocoDoTermo(d.termo, e)); + }); + }) + .catch(function (e) { if (window.console) console.warn("termo:", e); }); + } + + function agirNoTermo(botao) { + var acao = botao.getAttribute("data-termo"); + var erro = botao.parentNode.querySelector(".erro-form"); + if (acao === "revogar" && typeof window.confirm === "function" + && !window.confirm("Revogar? Paramos de ver o uso e apagamos os registros de acesso guardados.")) { + return Promise.resolve(); + } + botao.disabled = true; + return fetch(CONFIG.TERMO + "/" + encodeURIComponent(botao.getAttribute("data-tenant")), { + method: acao === "aceitar" ? "POST" : "DELETE", + headers: { Authorization: "Bearer " + token } + }).then(function (r) { + if (r.ok) return carregarTermo(); + return r.json().catch(function () { return {}; }).then(function (d) { + throw new Error(d.erro || "Nao consegui registrar agora. Tente de novo em instantes."); + }); + }).catch(function (e) { + erro.textContent = e.message; + erro.classList.remove("oculto"); + botao.disabled = false; + }); + } + + if (produtos) produtos.addEventListener("click", function (ev) { + var b = ev.target && ev.target.closest ? ev.target.closest("[data-termo]") : null; + if (b) agirNoTermo(b); + }); + /* Delegacao, e nao `onsubmit=`: a CSP recusa manipulador embutido, e os formularios nascem depois deste codigo. */ if (produtos) produtos.addEventListener("submit", function (ev) { @@ -2234,6 +2333,20 @@ data: .grade-ficha dl > div { grid-template-columns: 110px 1fr; } } + /* ── termo de dados ──────────────────────────────────────────────────────── */ + .termo-versao { border: 1px solid var(--line); border-radius: 12px; padding: 16px; margin: 14px 0; } + .termo-versao h3 { margin: 0 0 6px; font-size: 15px; display: flex; gap: 10px; align-items: center; } + .termo-versao textarea { + width: 100%; min-height: 320px; font: inherit; font-size: 14px; line-height: 1.5; + padding: 12px; border: 1px solid var(--line); border-radius: 10px; box-sizing: border-box; + } + .termo-versao .texto-publicado { + white-space: pre-wrap; font-size: 14px; line-height: 1.5; max-height: 260px; overflow-y: auto; + background: var(--paper); border-radius: 10px; padding: 12px; + } + .termo-acoes { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 10px; } + .erro-termo { color: var(--perigo); font-size: 13px; } + painel.html: | @@ -2258,7 +2371,7 @@ data: `Cache-Control: immutable` por um ano. Suba o numero a cada publicacao. --> - + @@ -2317,6 +2430,7 @@ data: + @@ -2420,6 +2534,21 @@ data: + +
+
+

Termo de dados

+

+ Autoriza ver o último acesso, os alunos ativos e os treinos dos últimos 30 + dias de cada cliente. Só a versão publicada aparece para o cliente + aceitar; rascunho não vale. Publicar é definitivo — mudar o texto + depois é uma versão nova, que pede aceite novo. +

+

Abra esta aba para carregar.

+ +
+
+ @@ -2432,7 +2561,7 @@ data: - + @@ -2467,6 +2596,7 @@ data: var API = "/api/plataforma/painel/visao"; var API_ALERTAS = "/api/plataforma/painel/alertas-infra"; + var API_TERMO = "/api/plataforma/painel/termo-de-dados"; var CHAVE = "atm.painel.token"; var INTERVALO_SINO_MS = 60000; @@ -2803,14 +2933,28 @@ data: return (a.ativa ? s : '' + s + "") + (a.forma ? " · " + esc(a.forma) : ""); } - function usoCurto(u) { + function usoCurto(u, d) { if (!u) return '—'; if (!u.disponivel) return 'indisponível'; return esc(u.usuarios) + " usuário(s)" + '' + (u.sessoesAtivas > 0 ? '' + u.sessoesAtivas + " logado(s)" : "ninguém logado") - + ""; + + "" + '' + ultimoAcessoCurto(d) + ""; } + /* O uso de DENTRO do sistema so existe com o termo de dados aceito — sem ele + o servidor nem pergunta, e a tela diz por que nao ha numero. */ + var ROTULO_TERMO = { + SEM_TERMO_PUBLICADO: "termo não publicado", + AGUARDANDO_ACEITE: "sem aceite do termo", + INDISPONIVEL: "termo indisponível" + }; + function ultimoAcessoCurto(d) { + if (!d) return ""; + if (d.termo !== "ACEITO") return esc(ROTULO_TERMO[d.termo] || ""); + return d.ultimoAcesso ? "último acesso " + esc(ha(d.ultimoAcesso)) : "sem acesso registrado"; + } + function numeroOuTraco(v) { return v === null || v === undefined ? "—" : esc(v); } + function pintarLinhas() { var lista = filtrados(); vazio.classList.toggle("oculto", lista.length > 0); @@ -2834,7 +2978,7 @@ data: + '' + reais(c.valorMensal) + diferencaDeTabela(c) + "" + '' + (f.disponivel ? reais(f.totalPago) + '' + esc(f.cobrancasPagas) + " cobrança(s)" : "—") + "" + "" + proximaCobranca(f) + "" - + "" + usoCurto(c.uso) + "" + + "" + usoCurto(c.uso, c.usoDeDentro) + "" + "" + seloSubconta(c.subconta) + "" + ""; }).join(""); @@ -2923,6 +3067,7 @@ data: ? item("Pessoas com conta", esc(u.usuarios)) + item("Logadas agora", esc(u.sessoesAtivas)) + item("Login", u.realmLigado ? "ligado" : 'desligado') : item("Login", u ? "indisponível agora" : "ainda sem realm")) + + usoDeDentroNaFicha(c.usoDeDentro) + item("Realm", txt(c.realm)) + item("Cobrança dos alunos", seloSubconta(c.subconta) + (c.subconta && c.subconta.desde ? " desde " + data(c.subconta.desde) : "")) + "" @@ -3051,6 +3196,89 @@ data: }).join("") || '
  • Nada ainda.
  • '; } + function usoDeDentroNaFicha(d) { + if (!d) return ""; + if (d.termo !== "ACEITO") return item("Uso de dentro", esc(ROTULO_TERMO[d.termo] || "—")); + return item("Último acesso", d.ultimoAcesso + ? dataHora(d.ultimoAcesso) + " (" + esc(ha(d.ultimoAcesso)) + ")" : "nenhum desde o aceite") + + item("Alunos ativos", numeroOuTraco(d.alunosAtivos)) + + item("Treinos (30 dias)", numeroOuTraco(d.treinos30Dias)); + } + + /* ── termo de dados ─────────────────────────────────────────────────────── */ + + function pedirTermo(metodo, caminho, corpo) { + var h = { "x-atm-admin-token": tokenAtual }; + if (corpo) h["Content-Type"] = "application/json"; + return fetch(API_TERMO + (caminho || ""), { method: metodo, headers: h, + body: corpo ? JSON.stringify(corpo) : undefined }) + .then(function (r) { + if (r.ok) return r.json(); + return r.json().catch(function () { return {}; }).then(function (d) { + throw new Error(d.erro || "HTTP " + r.status); + }); + }); + } + + function versaoDoTermo(v) { + var aceites = (v.aceites || []).map(function (a) { return esc(a.tenant) + " (" + data(a.aceitoEm) + ")"; }); + var cab = "

    Versão " + esc(v.versao) + " " + + (v.publicadoEm ? 'publicada em ' + data(v.publicadoEm) + "" + : 'rascunho') + "

    "; + if (v.publicadoEm) { + return '
    ' + cab + + '

    Aceites vivos: ' + (aceites.length ? aceites.join(", ") : "nenhum") + "

    " + + '
    ' + esc(v.texto) + "
    "; + } + return '
    ' + cab + + '" + + '
    ' + + '' + + '' + + '
    '; + } + + function carregarTermo() { + if (!tokenAtual) return Promise.resolve(); + return pedirTermo("GET").then(function (lista) { + $("termoVersoes").innerHTML = (lista || []).map(versaoDoTermo).join("") + || '

    Nenhuma versão.

    '; + var temRascunho = (lista || []).some(function (v) { return !v.publicadoEm; }); + $("termoNovaVersao").classList.toggle("oculto", temRascunho); + }).catch(function (e) { + $("termoVersoes").innerHTML = '

    Não consegui carregar: ' + esc(e.message) + "

    "; + }); + } + + function agirNoTermo(botao) { + var caixa = botao.closest(".termo-versao"); + var versao = caixa.getAttribute("data-versao"); + var erro = caixa.querySelector(".erro-termo"); + var texto = caixa.querySelector("textarea").value; + var acao = botao.getAttribute("data-termo-acao"); + if (acao === "publicar" && !window.confirm("Publicar a versão " + versao + "? O texto não pode mais " + + "ser mudado, e passa a aparecer para os clientes aceitarem.")) return Promise.resolve(); + erro.textContent = ""; + botao.disabled = true; + // Publicar salva antes: o que vai para o cliente e o que esta na caixa. + return pedirTermo("PUT", "/" + encodeURIComponent(versao), { texto: texto }) + .then(function () { + return acao === "publicar" ? pedirTermo("POST", "/" + encodeURIComponent(versao) + "/publicar") : null; + }) + .then(carregarTermo) + .catch(function (e) { erro.textContent = e.message; botao.disabled = false; }); + } + + $("termoVersoes").addEventListener("click", function (ev) { + var b = ev.target.closest ? ev.target.closest("[data-termo-acao]") : null; + if (b) agirNoTermo(b); + }); + $("termoNovaVersao").addEventListener("click", function () { + pedirTermo("POST").then(carregarTermo).catch(function (e) { + $("termoVersoes").insertAdjacentHTML("afterbegin", '

    ' + esc(e.message) + "

    "); + }); + }); + /* ── montagem ───────────────────────────────────────────────────────────── */ function pintar() { @@ -3149,6 +3377,7 @@ data: document.querySelectorAll(".painel-aba").forEach(function (p) { p.classList.toggle("oculto", p.id !== "aba-" + b.getAttribute("data-aba")); }); + if (b.getAttribute("data-aba") === "termo") carregarTermo(); }); }); diff --git a/tenants/plataforma/96-publico-configmap.yaml b/tenants/plataforma/96-publico-configmap.yaml index 87663df..a9d1031 100644 --- a/tenants/plataforma/96-publico-configmap.yaml +++ b/tenants/plataforma/96-publico-configmap.yaml @@ -1410,6 +1410,24 @@ data: .form-cobranca .btn:disabled { opacity: .6; cursor: wait; } .erro-form { color: var(--perigo); font-size: 14px; margin: 0; } + /* ── o termo de dados, dentro do cartao ─────────────────────────────────── */ + .termo { + align-self: stretch; margin-top: 20px; padding-top: 16px; + border-top: 1px dashed var(--line); + display: flex; flex-direction: column; align-items: flex-start; gap: 8px; + } + .termo .selo { margin-bottom: 0; } + .produto .termo .btn { margin-top: 6px; } + .termo .btn:disabled { opacity: .6; cursor: wait; } + .btn.secundario { background: transparent; color: var(--navy); border: 1px solid var(--line); } + .btn.secundario:hover { background: var(--line); } + .termo details { align-self: stretch; font-size: 14px; } + .termo summary { cursor: pointer; font-weight: 600; } + .termo-texto { + white-space: pre-wrap; margin-top: 8px; padding: 12px; max-height: 280px; overflow-y: auto; + border: 1px solid var(--line); border-radius: 10px; background: var(--branco); line-height: 1.5; + } + /* Empilhado, a foto sai: no celular ela so empurraria os produtos para baixo. */ @media (max-width: 760px) { .topo { padding: 12px 16px; } @@ -1445,7 +1463,7 @@ data: numero a cada publicacao que mexa nesses arquivos. --> - + @@ -1515,7 +1533,7 @@ data: - + @@ -1553,7 +1571,8 @@ data: CLIENTE: "painel-do-cliente", RETORNO: location.origin + "/minha-conta", API: "/api/cliente/meus-produtos", - COBRANCA: "/api/cliente/cobranca" + COBRANCA: "/api/cliente/cobranca", + TERMO: "/api/cliente/termo-de-dados" }; var CHAVE_VERIFIER = "atm.conta.verifier"; @@ -1725,7 +1744,7 @@ data: produtos.innerHTML = (lista || []).map(cartao).join(""); nenhum.classList.toggle("oculto", (lista || []).length > 0); mostrar(conteudo); - return carregarCobranca(); + return Promise.all([carregarCobranca(), carregarTermo()]); }); } @@ -1835,7 +1854,10 @@ data: if (!alvo) return; var velho = alvo.querySelector(".cobranca"); if (velho) velho.remove(); - alvo.insertAdjacentHTML("beforeend", alertaDeCobranca(c)); + // Antes do bloco do termo, se ele ja estiver la: a cobranca e mais urgente. + var termo = alvo.querySelector(".termo"); + if (termo) termo.insertAdjacentHTML("beforebegin", alertaDeCobranca(c)); + else alvo.insertAdjacentHTML("beforeend", alertaDeCobranca(c)); }); }) .catch(function (e) { if (window.console) console.warn("cobranca:", e); }); @@ -1872,6 +1894,83 @@ data: }); } + /* ── O TERMO DE DADOS ──────────────────────────────────────────────────────── + * + * A autorizacao para a Athletic Map ver o USO do sistema (ultimo acesso, + * alunos ativos, treinos). Sem termo publicado, o bloco nem aparece — o + * servidor devolve `termo: null` enquanto o texto e rascunho. Como a + * cobranca, falhar aqui nao derruba a tela. */ + + function dataCurta(iso) { + var m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(iso || "")); + return m ? m[3] + "/" + m[2] + "/" + m[1] : ""; + } + + function blocoDoTermo(termo, e) { + var titulo = 'Acompanhamento de uso'; + if (e.aceito) { + return '
    ' + titulo + + 'autorizado' + (e.aceitoEm ? " em " + dataCurta(e.aceitoEm) : "") + "" + + '

    Vemos só a data do último acesso e contagens de alunos e treinos.

    ' + + '' + + '

    '; + } + return '
    ' + titulo + + 'não autorizado' + + '

    Autorize a Athletic Map a acompanhar o uso do sistema para dar suporte.

    ' + + '
    Ler o termo (versão ' + escapar(termo.versao) + ")" + + '
    ' + escapar(termo.texto) + "
    " + + '' + + '

    '; + } + + function carregarTermo() { + return fetch(CONFIG.TERMO, { headers: { Authorization: "Bearer " + token } }) + .then(function (r) { return r.ok ? r.json() : { termo: null, escolinhas: [] }; }) + .then(function (d) { + produtos.querySelectorAll(".termo").forEach(function (el) { el.remove(); }); + if (!d || !d.termo) return; + (d.escolinhas || []).forEach(function (e) { + var alvo = null; + produtos.querySelectorAll("[data-tenant]").forEach(function (el) { + if (el.tagName === "ARTICLE" && el.getAttribute("data-tenant") === e.tenant) alvo = el; + }); + if (alvo) alvo.insertAdjacentHTML("beforeend", blocoDoTermo(d.termo, e)); + }); + }) + .catch(function (e) { if (window.console) console.warn("termo:", e); }); + } + + function agirNoTermo(botao) { + var acao = botao.getAttribute("data-termo"); + var erro = botao.parentNode.querySelector(".erro-form"); + if (acao === "revogar" && typeof window.confirm === "function" + && !window.confirm("Revogar? Paramos de ver o uso e apagamos os registros de acesso guardados.")) { + return Promise.resolve(); + } + botao.disabled = true; + return fetch(CONFIG.TERMO + "/" + encodeURIComponent(botao.getAttribute("data-tenant")), { + method: acao === "aceitar" ? "POST" : "DELETE", + headers: { Authorization: "Bearer " + token } + }).then(function (r) { + if (r.ok) return carregarTermo(); + return r.json().catch(function () { return {}; }).then(function (d) { + throw new Error(d.erro || "Nao consegui registrar agora. Tente de novo em instantes."); + }); + }).catch(function (e) { + erro.textContent = e.message; + erro.classList.remove("oculto"); + botao.disabled = false; + }); + } + + if (produtos) produtos.addEventListener("click", function (ev) { + var b = ev.target && ev.target.closest ? ev.target.closest("[data-termo]") : null; + if (b) agirNoTermo(b); + }); + /* Delegacao, e nao `onsubmit=`: a CSP recusa manipulador embutido, e os formularios nascem depois deste codigo. */ if (produtos) produtos.addEventListener("submit", function (ev) { @@ -2234,6 +2333,20 @@ data: .grade-ficha dl > div { grid-template-columns: 110px 1fr; } } + /* ── termo de dados ──────────────────────────────────────────────────────── */ + .termo-versao { border: 1px solid var(--line); border-radius: 12px; padding: 16px; margin: 14px 0; } + .termo-versao h3 { margin: 0 0 6px; font-size: 15px; display: flex; gap: 10px; align-items: center; } + .termo-versao textarea { + width: 100%; min-height: 320px; font: inherit; font-size: 14px; line-height: 1.5; + padding: 12px; border: 1px solid var(--line); border-radius: 10px; box-sizing: border-box; + } + .termo-versao .texto-publicado { + white-space: pre-wrap; font-size: 14px; line-height: 1.5; max-height: 260px; overflow-y: auto; + background: var(--paper); border-radius: 10px; padding: 12px; + } + .termo-acoes { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-top: 10px; } + .erro-termo { color: var(--perigo); font-size: 13px; } + painel.html: | @@ -2258,7 +2371,7 @@ data: `Cache-Control: immutable` por um ano. Suba o numero a cada publicacao. --> - + @@ -2317,6 +2430,7 @@ data: + @@ -2420,6 +2534,21 @@ data: + +
    +
    +

    Termo de dados

    +

    + Autoriza ver o último acesso, os alunos ativos e os treinos dos últimos 30 + dias de cada cliente. Só a versão publicada aparece para o cliente + aceitar; rascunho não vale. Publicar é definitivo — mudar o texto + depois é uma versão nova, que pede aceite novo. +

    +

    Abra esta aba para carregar.

    + +
    +
    + @@ -2432,7 +2561,7 @@ data: - + @@ -2467,6 +2596,7 @@ data: var API = "/api/plataforma/painel/visao"; var API_ALERTAS = "/api/plataforma/painel/alertas-infra"; + var API_TERMO = "/api/plataforma/painel/termo-de-dados"; var CHAVE = "atm.painel.token"; var INTERVALO_SINO_MS = 60000; @@ -2803,14 +2933,28 @@ data: return (a.ativa ? s : '' + s + "") + (a.forma ? " · " + esc(a.forma) : ""); } - function usoCurto(u) { + function usoCurto(u, d) { if (!u) return '—'; if (!u.disponivel) return 'indisponível'; return esc(u.usuarios) + " usuário(s)" + '' + (u.sessoesAtivas > 0 ? '' + u.sessoesAtivas + " logado(s)" : "ninguém logado") - + ""; + + "" + '' + ultimoAcessoCurto(d) + ""; } + /* O uso de DENTRO do sistema so existe com o termo de dados aceito — sem ele + o servidor nem pergunta, e a tela diz por que nao ha numero. */ + var ROTULO_TERMO = { + SEM_TERMO_PUBLICADO: "termo não publicado", + AGUARDANDO_ACEITE: "sem aceite do termo", + INDISPONIVEL: "termo indisponível" + }; + function ultimoAcessoCurto(d) { + if (!d) return ""; + if (d.termo !== "ACEITO") return esc(ROTULO_TERMO[d.termo] || ""); + return d.ultimoAcesso ? "último acesso " + esc(ha(d.ultimoAcesso)) : "sem acesso registrado"; + } + function numeroOuTraco(v) { return v === null || v === undefined ? "—" : esc(v); } + function pintarLinhas() { var lista = filtrados(); vazio.classList.toggle("oculto", lista.length > 0); @@ -2834,7 +2978,7 @@ data: + '' + reais(c.valorMensal) + diferencaDeTabela(c) + "" + '' + (f.disponivel ? reais(f.totalPago) + '' + esc(f.cobrancasPagas) + " cobrança(s)" : "—") + "" + "" + proximaCobranca(f) + "" - + "" + usoCurto(c.uso) + "" + + "" + usoCurto(c.uso, c.usoDeDentro) + "" + "" + seloSubconta(c.subconta) + "" + ""; }).join(""); @@ -2923,6 +3067,7 @@ data: ? item("Pessoas com conta", esc(u.usuarios)) + item("Logadas agora", esc(u.sessoesAtivas)) + item("Login", u.realmLigado ? "ligado" : 'desligado') : item("Login", u ? "indisponível agora" : "ainda sem realm")) + + usoDeDentroNaFicha(c.usoDeDentro) + item("Realm", txt(c.realm)) + item("Cobrança dos alunos", seloSubconta(c.subconta) + (c.subconta && c.subconta.desde ? " desde " + data(c.subconta.desde) : "")) + "" @@ -3051,6 +3196,89 @@ data: }).join("") || '
  • Nada ainda.
  • '; } + function usoDeDentroNaFicha(d) { + if (!d) return ""; + if (d.termo !== "ACEITO") return item("Uso de dentro", esc(ROTULO_TERMO[d.termo] || "—")); + return item("Último acesso", d.ultimoAcesso + ? dataHora(d.ultimoAcesso) + " (" + esc(ha(d.ultimoAcesso)) + ")" : "nenhum desde o aceite") + + item("Alunos ativos", numeroOuTraco(d.alunosAtivos)) + + item("Treinos (30 dias)", numeroOuTraco(d.treinos30Dias)); + } + + /* ── termo de dados ─────────────────────────────────────────────────────── */ + + function pedirTermo(metodo, caminho, corpo) { + var h = { "x-atm-admin-token": tokenAtual }; + if (corpo) h["Content-Type"] = "application/json"; + return fetch(API_TERMO + (caminho || ""), { method: metodo, headers: h, + body: corpo ? JSON.stringify(corpo) : undefined }) + .then(function (r) { + if (r.ok) return r.json(); + return r.json().catch(function () { return {}; }).then(function (d) { + throw new Error(d.erro || "HTTP " + r.status); + }); + }); + } + + function versaoDoTermo(v) { + var aceites = (v.aceites || []).map(function (a) { return esc(a.tenant) + " (" + data(a.aceitoEm) + ")"; }); + var cab = "

    Versão " + esc(v.versao) + " " + + (v.publicadoEm ? 'publicada em ' + data(v.publicadoEm) + "" + : 'rascunho') + "

    "; + if (v.publicadoEm) { + return '
    ' + cab + + '

    Aceites vivos: ' + (aceites.length ? aceites.join(", ") : "nenhum") + "

    " + + '
    ' + esc(v.texto) + "
    "; + } + return '
    ' + cab + + '" + + '
    ' + + '' + + '' + + '
    '; + } + + function carregarTermo() { + if (!tokenAtual) return Promise.resolve(); + return pedirTermo("GET").then(function (lista) { + $("termoVersoes").innerHTML = (lista || []).map(versaoDoTermo).join("") + || '

    Nenhuma versão.

    '; + var temRascunho = (lista || []).some(function (v) { return !v.publicadoEm; }); + $("termoNovaVersao").classList.toggle("oculto", temRascunho); + }).catch(function (e) { + $("termoVersoes").innerHTML = '

    Não consegui carregar: ' + esc(e.message) + "

    "; + }); + } + + function agirNoTermo(botao) { + var caixa = botao.closest(".termo-versao"); + var versao = caixa.getAttribute("data-versao"); + var erro = caixa.querySelector(".erro-termo"); + var texto = caixa.querySelector("textarea").value; + var acao = botao.getAttribute("data-termo-acao"); + if (acao === "publicar" && !window.confirm("Publicar a versão " + versao + "? O texto não pode mais " + + "ser mudado, e passa a aparecer para os clientes aceitarem.")) return Promise.resolve(); + erro.textContent = ""; + botao.disabled = true; + // Publicar salva antes: o que vai para o cliente e o que esta na caixa. + return pedirTermo("PUT", "/" + encodeURIComponent(versao), { texto: texto }) + .then(function () { + return acao === "publicar" ? pedirTermo("POST", "/" + encodeURIComponent(versao) + "/publicar") : null; + }) + .then(carregarTermo) + .catch(function (e) { erro.textContent = e.message; botao.disabled = false; }); + } + + $("termoVersoes").addEventListener("click", function (ev) { + var b = ev.target.closest ? ev.target.closest("[data-termo-acao]") : null; + if (b) agirNoTermo(b); + }); + $("termoNovaVersao").addEventListener("click", function () { + pedirTermo("POST").then(carregarTermo).catch(function (e) { + $("termoVersoes").insertAdjacentHTML("afterbegin", '

    ' + esc(e.message) + "

    "); + }); + }); + /* ── montagem ───────────────────────────────────────────────────────────── */ function pintar() { @@ -3149,6 +3377,7 @@ data: document.querySelectorAll(".painel-aba").forEach(function (p) { p.classList.toggle("oculto", p.id !== "aba-" + b.getAttribute("data-aba")); }); + if (b.getAttribute("data-aba") === "termo") carregarTermo(); }); });