NAVEGAÇÃO DE ETAPAS COM TRAVA DE PAGAMENTO EM TEMPO REAL ======================================================== */ function mostrarEtapa(etapaId) { // REQUISITO 1: BLOQUEIO // A etapa de atendimento (etapa-3) ou conclusão (etapa-4) SÓ pode ser // acessada se houver confirmação de pagamento para o cliente atual. if (etapaId === "etapa-3" || etapaId === "etapa-4") { const pago = verificarStatusPagamentoArmazenado(externalReferenceAtual); if (!pago && !paymentConfirmed) { // Bloqueia e redireciona de volta para a tela de pagamento pendente mostrarToast("Pagamento pendente — aguardando confirmação em tempo real..."); etapaId = "etapa-2"; const lockOverlay = document.getElementById("chat-lock-overlay"); if (lockOverlay) lockOverlay.style.display = "flex"; } else { paymentConfirmed = true; const lockOverlay = document.getElementById("chat-lock-overlay"); if (lockOverlay) lockOverlay.style.display = "none"; } } document.querySelectorAll(".flow-stage").forEach(el => el.style.display = "none"); const target = document.getElementById(etapaId); if (target) target.style.display = "block"; const stepper = document.getElementById("flow-stepper"); const navBtnAdmin = document.getElementById("nav-btn-admin"); const navBtnConsulta = document.getElementById("nav-btn-consulta"); if (etapaId === "painel-admin") { if (stepper) stepper.style.display = "none"; navBtnAdmin.classList.remove("btn-nav-outline"); navBtnAdmin.classList.add("btn-nav-primary"); navBtnConsulta.classList.remove("btn-nav-primary"); navBtnConsulta.classList.add("btn-nav-outline"); } else { if (stepper) stepper.style.display = "flex"; navBtnAdmin.classList.remove("btn-nav-primary"); navBtnAdmin.classList.add("btn-nav-outline"); navBtnConsulta.classList.remove("btn-nav-outline"); navBtnConsulta.classList.add("btn-nav-primary"); // Atualizar classes do Stepper const stepNum = parseInt(etapaId.replace("etapa-", "")) || 1; for (let i = 1; i <= 4; i++) { const item = document.getElementById(`step-nav-${i}`); if (item) { item.classList.remove("active", "completed"); if (i === stepNum) item.classList.add("active"); else if (i < stepNum) item.classList.add("completed"); } } } if (etapaId === "etapa-2") { prepararEtapaPagamento(); } if (etapaId === "etapa-3" && !chatIniciado && paymentConfirmed) { chatIniciado = true; iniciarChatDrRafael(); } window.scrollTo({ top: 0, behavior: "smooth" }); } function abrirPainelAdmin() { mostrarEtapa("painel-admin"); renderizarVendas(); carregarConfiguracaoGatewayUI(); renderizarClientesAdmin(); } ======= INICIALIZAÇÃO NO CARREGAMENTO DA PÁGINA ======================================================== */ document.addEventListener("DOMContentLoaded", () => { inicializarStorage(); // REQUISITO 4: LISTENER DE STORAGE PARA LIBERAÇÃO MANUAL DE BACKUP // Quando o admin confirmar no painel (em outra aba ou mesma origem), // a página da consulta do cliente desbloqueia automaticamente sem recarregar. window.addEventListener("storage", (e) => { if (e.key && e.key.startsWith("rs_pagamento_")) { const refDaChave = e.key.replace("rs_pagamento_", ""); if (refDaChave === externalReferenceAtual && e.newValue === "true") { desbloquearPagamentoSucessoEmTempoReal(); } } if (e.key === "rs_admin_payment_confirmed_event") { try { const data = JSON.parse(e.newValue || "{}"); if (data.ref && data.ref === externalReferenceAtual) { desbloquearPagamentoSucessoEmTempoReal(); } } catch (_) {} } }); // Recuperar sessão salva do cliente se houver const sessaoSalva = localStorage.getItem("rs_cliente_ativo_sessao"); if (sessaoSalva) { try { const parsed = JSON.parse(sessaoSalva); if (parsed && parsed.nome) { clienteAtivo = { ...clienteAtivo, ...parsed }; externalReferenceAtual = clienteAtivo.externalReference || ""; // Preenche campos do formulário da etapa 1 if (document.getElementById("cad-nome")) document.getElementById("cad-nome").value = clienteAtivo.nome || ""; if (document.getElementById("cad-cpf")) document.getElementById("cad-cpf").value = clienteAtivo.cpf || ""; if (document.getElementById("cad-email")) document.getElementById("cad-email").value = clienteAtivo.email || ""; if (document.getElementById("cad-celular")) document.getElementById("cad-celular").value = clienteAtivo.celular || ""; if (document.getElementById("cad-area") && clienteAtivo.area) document.getElementById("cad-area").value = clienteAtivo.area; if (document.getElementById("cad-descricao")) document.getElementById("cad-descricao").value = clienteAtivo.descricao || ""; } } catch (_) {} } // REQUISITO 6: PERSISTÊNCIA // Se o cliente recarregar a página, checa se a referência atual já foi confirmada if (externalReferenceAtual && verificarStatusPagamentoArmazenado(externalReferenceAtual)) { paymentConfirmed = true; } ======= CONFIGURAÇÃO DA API CAKTO (ADMIN & ENDPOINTS) ======================================================== */ function obterConfiguracaoCakto() { const raw = localStorage.getItem("rs_cakto_config"); if (raw) { try { return JSON.parse(raw); } catch (_) {} } return { gateway: "Cakto", clientId: CAKTO_DEFAULT_CLIENT_ID, clientSecret: CAKTO_DEFAULT_CLIENT_SECRET, webhookSecret: CAKTO_DEFAULT_WEBHOOK_SECRET, offerId: "" }; } function mascararTexto(texto, visiveisFinais = 4) { if (!texto) return "Não configurado"; const str = String(texto).trim(); if (str.length <= visiveisFinais) return str; const ultimos = str.slice(-visiveisFinais); return "••••••••••••" + ultimos; } function carregarConfiguracaoGatewayUI() { const cfg = obterConfiguracaoCakto(); const inputClientId = document.getElementById("cfg-cakto-clientid"); const inputClientSecret = document.getElementById("cfg-cakto-clientsecret"); const inputWebhook = document.getElementById("cfg-cakto-webhook"); const inputOfferId = document.getElementById("cfg-cakto-offerid"); const maskClientId = document.getElementById("mask-clientid-display"); const maskSecret = document.getElementById("mask-secret-display"); const maskWebhook = document.getElementById("mask-webhook-display"); if (inputClientId) inputClientId.value = cfg.clientId || CAKTO_DEFAULT_CLIENT_ID; if (inputClientSecret) inputClientSecret.value = cfg.clientSecret || CAKTO_DEFAULT_CLIENT_SECRET; if (inputWebhook) inputWebhook.value = cfg.webhookSecret || CAKTO_DEFAULT_WEBHOOK_SECRET; if (inputOfferId) inputOfferId.value = cfg.offerId || ""; if (maskClientId) maskClientId.textContent = mascararTexto(cfg.clientId || CAKTO_DEFAULT_CLIENT_ID, 4); if (maskSecret) maskSecret.textContent = mascararTexto(cfg.clientSecret || CAKTO_DEFAULT_CLIENT_SECRET, 4); if (maskWebhook) maskWebhook.textContent = mascararTexto(cfg.webhookSecret || CAKTO_DEFAULT_WEBHOOK_SECRET, 4); } function salvarConfiguracaoCakto(e) { e.preventDefault(); const offerId = document.getElementById("cfg-cakto-offerid").value.trim(); const atual = obterConfiguracaoCakto(); atual.offerId = offerId; localStorage.setItem("rs_cakto_config", JSON.stringify(atual)); const ind = document.getElementById("gateway-saved-indicator"); if (ind) { ind.style.display = "inline-block"; setTimeout(() => { ind.style.display = "none"; }, 3500); } mostrarToast("Configurações da Cakto salvas com sucesso!"); } // Endpoint 1: Obter Token Cakto com Cache em localStorage (rs_cakto_token) async function obterTokenCakto(forcarRenovacao = false) { const cfg = obterConfiguracaoCakto(); const clientId = cfg.clientId || CAKTO_DEFAULT_CLIENT_ID; const clientSecret = cfg.clientSecret || CAKTO_DEFAULT_CLIENT_SECRET; if (!forcarRenovacao) { const cachedRaw = localStorage.getItem("rs_cakto_token"); if (cachedRaw) { try { const cached = JSON.parse(cachedRaw); const agora = Date.now(); // Renova 2 minutos antes do vencimento if (cached && cached.access_token && cached.expires_at > (agora + 120000)) { return cached.access_token; } } catch (_) {} } } const bodyParams = new URLSearchParams(); bodyParams.append("client_id", clientId); bodyParams.append("client_secret", clientSecret); const resp = await fetch(`${CAKTO_BASE_URL}/public_api/token/`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: bodyParams.toString() }); if (!resp.ok) { const errText = await resp.text().catch(() => ""); throw new Error(`Falha de autenticação Cakto (HTTP ${resp.status}): ${errText}`); } const data = await resp.json(); const expiresInSec = data.expires_in || 36000; const tokenObj = { access_token: data.access_token, token_type: data.token_type || "Bearer", expires_at: Date.now() + (expiresInSec * 1000) }; localStorage.setItem("rs_cakto_token", JSON.stringify(tokenObj)); return data.access_token; } // Botão Testar Conexão async function testarConexaoCakto() { const resEl = document.getElementById("test-connection-result"); const btn = document.getElementById("btn-testar-conexao"); if (resEl) { resEl.style.display = "block"; resEl.style.background = "rgba(16, 32, 58, 0.8)"; resEl.style.color = "#cbd5e1"; resEl.textContent = "Testando conexão com a API Cakto..."; } if (btn) btn.disabled = true; try { const token = await obterTokenCakto(true); if (resEl) { resEl.style.background = "rgba(34, 197, 94, 0.15)"; resEl.style.border = "1px solid #22c55e"; resEl.style.color = "#4ade80"; resEl.innerHTML = "✓ Conectado à Cakto com sucesso! Token OAuth2 gerado e validado."; } mostrarToast("Conectado à Cakto ✓"); } catch (err) { console.error("Erro ao testar conexão Cakto:", err); if (resEl) { resEl.style.background = "rgba(239, 68, 68, 0.15)"; resEl.style.border = "1px solid #ef4444"; resEl.style.color = "#f87171"; resEl.innerHTML = `✗ Erro na conexão Cakto: ${err.message}`; } mostrarToast("Falha ao conectar com Cakto."); } finally { if (btn) btn.disabled = false; } } ======= ETAPA 2: MOTOR DE COBRANÇA PIX CAKTO & DEVOLUTIVA REAL-TIME ======================================================== */ function prepararEtapaPagamento() { const cfg = obterConfiguracaoCakto(); const alertOffer = document.getElementById("cakto-missing-offer-alert"); const btnGerar = document.getElementById("btn-gerar-cobranca-pix"); document.getElementById("pay-client-name").textContent = clienteAtivo.nome || "Cliente"; document.getElementById("pay-gateway-name").textContent = "Cakto"; // Verifica se o ID da oferta foi configurado if (!cfg.offerId || cfg.offerId.trim() === "") { if (alertOffer) alertOffer.style.display = "block"; if (btnGerar) btnGerar.disabled = true; } else { if (alertOffer) alertOffer.style.display = "none"; if (btnGerar) btnGerar.disabled = false; } // Recupera pedido da sessão se houver const pedidoSalvo = obterPedidoSalvoLocal(clienteAtivo.id); if (pedidoSalvo && pedidoSalvo.id) { pedidoAtivoCakto = pedidoSalvo; exibirDetalhesPedidoCakto(pedidoSalvo); // Se já confirmado if (verificarStatusPagamentoArmazenado(pedidoSalvo.id)) { desbloquearPagamentoSucessoEmTempoReal(); return; } // Continua polling do pedido salvo bloquearBotaoContinuar(); iniciarPollingCakto(pedidoSalvo.id); return; } // Se já estava com flag pago para este cliente if (clienteAtivo.id && verificarStatusPagamentoArmazenado(clienteAtivo.id)) { desbloquearPagamentoSucessoEmTempoReal(); return; } // Visual padrão aguardando o cliente clicar em gerar QR Code PIX bloquearBotaoContinuar(); document.getElementById("pay-order-ref").textContent = "-"; document.getElementById("cakto-qr-display-section").style.display = "none"; document.getElementById("cakto-btn-gerar-section").style.display = "block"; atualizarTextoPolling("Aguardando geração do QR Code PIX..."); } // Endpoint 2: CRIAR COBRANÇA PIX NA CAKTO async function gerarCobrancaPixCakto() { const cfg = obterConfiguracaoCakto(); if (!cfg.offerId || cfg.offerId.trim() === "") { mostrarToast("Configure o ID da Oferta no painel administrativo."); abrirPainelAdminComAba("gateway"); return; } if (!clienteAtivo.nome || !clienteAtivo.email || !clienteAtivo.cpf) { mostrarToast("Preencha seus dados de cadastro antes de pagar."); mostrarEtapa("etapa-1"); return; } const btn = document.getElementById("btn-gerar-cobranca-pix"); const btnText = document.getElementById("btn-gerar-pix-text"); const failedBox = document.getElementById("pay-status-failed"); if (failedBox) failedBox.style.display = "none"; if (btn) btn.disabled = true; if (btnText) btnText.textContent = "Gerando cobrança PIX na Cakto..."; atualizarTextoPolling("Conectando à Cakto e gerando PIX..."); try { const token = await obterTokenCakto(); // Limpeza de telefone (somente dígitos) const apenasDigitosTel = (clienteAtivo.celular || "").replace(/\D/g, ""); const phoneFormatado = apenasDigitosTel.startsWith("55") ? apenasDigitosTel : `55${apenasDigitosTel}`; // CPF apenas dígitos const apenasDigitosCpf = (clienteAtivo.cpf || "").replace(/\D/g, ""); // Fingerprint persistido do navegador const fingerprint = localStorage.getItem("rs_browser_fingerprint") || ("fp_" + Date.now()); // Idempotency-Key única por intenção de compra if (!idempotencyKeyAtual) { idempotencyKeyAtual = (crypto && crypto.randomUUID) ? crypto.randomUUID() : ("ik_" + Date.now() + "_" + Math.random().toString(36).substring(2, 9)); } const payload = { paymentMethod: "pix", customer: { name: clienteAtivo.nome, email: clienteAtivo.email, phone: phoneFormatado, fingerprint: fingerprint, docType: "cpf", docNumber: apenasDigitosCpf }, items: [ { offerId: cfg.offerId.trim(), quantity: 1, offerType: "main" } ], pixExpiresIn: 3600 }; const resp = await fetch(`${CAKTO_BASE_URL}/public_api/payments/`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", "X-Idempotency-Key": idempotencyKeyAtual }, body: JSON.stringify(payload) }); if (resp.status === 401) { // Token expirado, renova e tenta mais uma vez const novoToken = await obterTokenCakto(true); const retryResp = await fetch(`${CAKTO_BASE_URL}/public_api/payments/`, { method: "POST", headers: { "Authorization": `Bearer ${novoToken}`, "Content-Type": "application/json", "X-Idempotency-Key": idempotencyKeyAtual }, body: JSON.stringify(payload) }); if (!retryResp.ok) { const errJson = await retryResp.json().catch(() => ({})); throw new Error(errJson.message || errJson.error || `HTTP ${retryResp.status}`); } const retryData = await retryResp.json(); processarRespostaCobrancaCakto(retryData); return; } if (!resp.ok) { let errDetail = `Erro HTTP ${resp.status}`; try { const errJson = await resp.json(); errDetail = errJson.message || errJson.error || JSON.stringify(errJson); } catch (_) {} throw new Error(errDetail); } const data = await resp.json(); processarRespostaCobrancaCakto(data); } catch (err) { console.warn("Falha na chamada da API Cakto:", err); // REQUISITO: Falha de CORS ou rede NÃO desbloqueia; mantém "Pagamento pendente" atualizarTextoPolling("Aguardando confirmação do pagamento em tempo real..."); mostrarToast(`Aviso: ${err.message || "Aguardando confirmação do gateway"}`); // Fallback visual com dados mock da intenção para permitir exibição do QR de contingência e liberação pelo admin contingenciaCobrancaCakto(); } finally { if (btn) btn.disabled = false; if (btnText) btnText.textContent = "Gerar QR Code PIX (Cakto)"; } } ======= ======= * { =======
1 Cadastro
2 Pagamento
3 Atendimento Jurídico
4 Conclusão

Inicie sua Consulta Jurídica

Entenda seus direitos com quem possui muita experiência na atuação jurídica combativa.

Falar com o Advogado
Documento gerado com sucesso.