{"id":726,"date":"2025-11-20T08:55:04","date_gmt":"2025-11-20T08:55:04","guid":{"rendered":"https:\/\/fulltv.cl\/?page_id=726"},"modified":"2026-04-03T01:51:06","modified_gmt":"2026-04-03T01:51:06","slug":"portal-clientes","status":"publish","type":"page","link":"https:\/\/fulltv.cl\/index.php\/portal-clientes\/","title":{"rendered":"Portal Clientes"},"content":{"rendered":"\n<div id=\"ftv-portal\">\n  <style>\n    :root{\n      --ftv-blue:#1f4bb8;\n      --ftv-green:#2bb673;\n      --ftv-dark:#0b1220;\n      --ftv-muted:#6b7280;\n      --ftv-border:#e5e7eb;\n      --ftv-bg:#f6f8fb;\n      --ftv-card:#ffffff;\n      --ftv-radius:18px;\n      --ftv-shadow: 0 10px 25px rgba(10,20,40,.08);\n    }\n\n    #ftv-portal{font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background:var(--ftv-bg); color:#0f172a;}\n    #ftv-portal *{box-sizing:border-box;}\n    #ftv-portal a{color:inherit; text-decoration:none;}\n\n    .ftv-wrap{max-width:1100px; margin:0 auto; padding:24px;}\n    .ftv-topbar{\n      display:flex; align-items:center; justify-content:space-between;\n      padding:18px 22px; background:var(--ftv-card); border:1px solid var(--ftv-border);\n      border-radius:var(--ftv-radius); box-shadow:var(--ftv-shadow);\n      gap:16px;\n    }\n    .ftv-brand{display:flex; align-items:center; gap:14px;}\n    .ftv-logo{\n      width:44px; height:44px; border-radius:12px;\n      background: linear-gradient(135deg, var(--ftv-green), var(--ftv-blue));\n      display:grid; place-items:center; color:#fff; font-weight:800;\n      letter-spacing:.5px;\n    }\n    .ftv-brand h1{font-size:18px; margin:0; line-height:1.1;}\n    .ftv-brand small{display:block; color:var(--ftv-muted); margin-top:3px;}\n\n    .ftv-actions{display:flex; gap:10px; align-items:center; flex-wrap:wrap;}\n    .ftv-pill{\n      padding:10px 12px; border:1px solid var(--ftv-border); background:#fff;\n      border-radius:999px; font-size:13px; color:var(--ftv-muted);\n    }\n    .ftv-btn{\n      border:none; border-radius:12px; padding:11px 14px; font-weight:700; cursor:pointer;\n      background:var(--ftv-blue); color:#fff;\n    }\n    .ftv-btn.secondary{background:#fff; color:var(--ftv-blue); border:1px solid rgba(31,75,184,.25);}\n    .ftv-btn.danger{background:#ef4444;}\n    .ftv-btn:disabled{opacity:.55; cursor:not-allowed;}\n\n    .ftv-grid{display:grid; grid-template-columns: 320px 1fr; gap:18px; margin-top:18px;}\n    @media (max-width: 900px){ .ftv-grid{grid-template-columns:1fr;} }\n\n    .ftv-card{\n      background:var(--ftv-card); border:1px solid var(--ftv-border);\n      border-radius:var(--ftv-radius); box-shadow:var(--ftv-shadow);\n      padding:18px;\n    }\n\n    .ftv-title{margin:0 0 10px; font-size:16px;}\n    .ftv-sub{margin:0 0 14px; color:var(--ftv-muted); font-size:13px; line-height:1.4;}\n\n    .ftv-field{display:flex; flex-direction:column; gap:7px; margin-bottom:12px;}\n    .ftv-field label{font-size:12px; color:var(--ftv-muted); font-weight:700; letter-spacing:.2px;}\n    .ftv-input{\n      border:1px solid var(--ftv-border); border-radius:12px; padding:12px 12px;\n      outline:none; background:#fff; font-size:14px;\n    }\n    .ftv-input:focus{border-color: rgba(31,75,184,.45); box-shadow: 0 0 0 4px rgba(31,75,184,.08);}\n    .ftv-row{display:flex; gap:10px; flex-wrap:wrap; align-items:center;}\n    .ftv-note{\n      font-size:12px; color:var(--ftv-muted);\n      padding:10px 12px; border:1px dashed rgba(31,75,184,.25);\n      border-radius:12px; background:rgba(31,75,184,.04);\n    }\n\n    .ftv-tabs{\n      display:flex; gap:8px; flex-wrap:wrap; margin-bottom:12px;\n    }\n    .ftv-tab{\n      padding:10px 12px; border:1px solid var(--ftv-border); background:#fff;\n      border-radius:999px; font-weight:700; font-size:13px; cursor:pointer;\n    }\n    .ftv-tab.active{background:rgba(43,182,115,.12); border-color: rgba(43,182,115,.35);}\n\n    .ftv-kpis{display:grid; grid-template-columns: repeat(3, 1fr); gap:12px; margin:10px 0 4px;}\n    @media (max-width: 700px){ .ftv-kpis{grid-template-columns:1fr;} }\n    .ftv-kpi{\n      border:1px solid var(--ftv-border); border-radius:16px; padding:14px;\n      background:linear-gradient(180deg, #fff, rgba(255,255,255,.7));\n    }\n    .ftv-kpi b{display:block; font-size:18px;}\n    .ftv-kpi span{display:block; color:var(--ftv-muted); font-size:12px; margin-top:4px;}\n\n    .ftv-table{width:100%; border-collapse:collapse; overflow:hidden; border-radius:14px; border:1px solid var(--ftv-border);}\n    .ftv-table th, .ftv-table td{padding:12px 12px; text-align:left; font-size:13px; border-bottom:1px solid var(--ftv-border);}\n    .ftv-table th{background:rgba(15,23,42,.03); color:#334155; font-weight:800;}\n    .ftv-badge{display:inline-block; padding:6px 10px; border-radius:999px; font-size:12px; font-weight:800;}\n    .ok{background:rgba(43,182,115,.14); color:#0f5132;}\n    .warn{background:rgba(245,158,11,.16); color:#7c4a03;}\n    .bad{background:rgba(239,68,68,.14); color:#7f1d1d;}\n\n    .ftv-hide{display:none;}\n    .ftv-loading{color:var(--ftv-muted); font-size:13px;}\n    .ftv-error{color:#b91c1c; font-size:13px; font-weight:700;}\n    .ftv-success{color:#0f5132; font-size:13px; font-weight:700;}\n    .ftv-footer{margin-top:14px; color:var(--ftv-muted); font-size:12px; text-align:center;}\n  <\/style>\n\n  <div class=\"ftv-wrap\">\n    <div class=\"ftv-topbar\">\n      <div class=\"ftv-brand\">\n        <div class=\"ftv-logo\">FTV<\/div>\n        <div>\n          <h1>Portal de Clientes<\/h1>\n          <small>Revisa tu servicio, boletas y soporte<\/small>\n        <\/div>\n      <\/div>\n\n      <div class=\"ftv-actions\">\n        <div id=\"ftvUserPill\" class=\"ftv-pill ftv-hide\">\u2014<\/div>\n        <button id=\"ftvLogoutBtn\" class=\"ftv-btn danger ftv-hide\" type=\"button\">Cerrar sesi\u00f3n<\/button>\n        <a class=\"ftv-btn secondary\" href=\"\/webpay\/\" target=\"_blank\" rel=\"noopener\">Pagar (WebPay)<\/a>\n      <\/div>\n    <\/div>\n\n    <div class=\"ftv-grid\">\n      <!-- Login card -->\n      <div id=\"ftvLoginCard\" class=\"ftv-card\">\n        <h2 class=\"ftv-title\">Ingresar<\/h2>\n        <p class=\"ftv-sub\">Usa el mismo RUT y correo que utilizas en WebPay.<\/p>\n\n        <div class=\"ftv-field\">\n          <label for=\"ftvRut\">RUT<\/label>\n          <input id=\"ftvRut\" class=\"ftv-input\" placeholder=\"Ej: 15622353-0\" \/>\n        <\/div>\n\n        <div class=\"ftv-field\">\n          <label for=\"ftvEmail\">E-mail<\/label>\n          <input id=\"ftvEmail\" class=\"ftv-input\" placeholder=\"ejemplo@correo.cl\" \/>\n        <\/div>\n\n        <div class=\"ftv-row\">\n          <button id=\"ftvLoginBtn\" class=\"ftv-btn\" type=\"button\">Ingresar<\/button>\n          <span id=\"ftvLoginMsg\" class=\"ftv-loading\"><\/span>\n        <\/div>\n\n        <div style=\"margin-top:12px\" class=\"ftv-note\">\n          Consejo: si te da \u201ccredenciales inv\u00e1lidas\u201d, prueba exactamente el mismo correo con el que pagas en la web.\n        <\/div>\n      <\/div>\n\n      <!-- Dashboard -->\n      <div id=\"ftvDashCard\" class=\"ftv-card ftv-hide\">\n        <div class=\"ftv-tabs\">\n          <button class=\"ftv-tab active\" data-tab=\"estado\">Estado<\/button>\n          <button class=\"ftv-tab\" data-tab=\"plan\">Plan<\/button>\n          <button class=\"ftv-tab\" data-tab=\"boletas\">Boletas<\/button>\n          <button class=\"ftv-tab\" data-tab=\"speedtest\">Speedtest<\/button>\n          <button class=\"ftv-tab\" data-tab=\"tickets\">Soporte<\/button>\n        <\/div>\n\n        <!-- Estado -->\n        <div class=\"ftv-panel\" data-panel=\"estado\">\n          <h2 class=\"ftv-title\">Estado del servicio<\/h2>\n          <p class=\"ftv-sub\">Resumen r\u00e1pido de tu conexi\u00f3n y situaci\u00f3n de pago.<\/p>\n\n          <div class=\"ftv-kpis\">\n            <div class=\"ftv-kpi\">\n              <b id=\"kpiEstado\">\u2014<\/b>\n              <span>Estado<\/span>\n            <\/div>\n            <div class=\"ftv-kpi\">\n              <b id=\"kpiDeuda\">$\u2014<\/b>\n              <span>Deuda actual<\/span>\n            <\/div>\n            <div class=\"ftv-kpi\">\n              <b id=\"kpiPlan\">\u2014<\/b>\n              <span>Plan<\/span>\n            <\/div>\n          <\/div>\n\n          <div id=\"estadoMsg\" class=\"ftv-loading\">Cargando\u2026<\/div>\n        <\/div>\n\n        <!-- Plan -->\n        <div class=\"ftv-panel ftv-hide\" data-panel=\"plan\">\n          <h2 class=\"ftv-title\">Tu plan<\/h2>\n          <p class=\"ftv-sub\">Detalles del plan contratado.<\/p>\n          <div id=\"planBox\" class=\"ftv-loading\">Cargando\u2026<\/div>\n        <\/div>\n\n        <!-- Boletas -->\n        <div class=\"ftv-panel ftv-hide\" data-panel=\"boletas\">\n          <h2 class=\"ftv-title\">Boletas<\/h2>\n          <p class=\"ftv-sub\">Historial y pendientes de pago.<\/p>\n          <div id=\"boletasBox\" class=\"ftv-loading\">Cargando\u2026<\/div>\n        <\/div>\n\n        <!-- Speedtest -->\n        <div class=\"ftv-panel ftv-hide\" data-panel=\"speedtest\">\n          <h2 class=\"ftv-title\">Test de velocidad<\/h2>\n          <p class=\"ftv-sub\">Abrimos un test externo y puedes guardar el resultado como ticket.<\/p>\n          <div class=\"ftv-row\">\n            <a class=\"ftv-btn\" href=\"https:\/\/fast.com\/\" target=\"_blank\" rel=\"noopener\">Abrir FAST.com<\/a>\n            <a class=\"ftv-btn secondary\" href=\"https:\/\/www.speedtest.net\/\" target=\"_blank\" rel=\"noopener\">Abrir Speedtest.net<\/a>\n          <\/div>\n          <div style=\"margin-top:12px\" class=\"ftv-note\">\n            Tip: haz el test conectado por cable o cerca del Wi-Fi y sin descargas activas.\n          <\/div>\n        <\/div>\n\n        <!-- Tickets -->\n        <div class=\"ftv-panel ftv-hide\" data-panel=\"tickets\">\n          <h2 class=\"ftv-title\">Soporte \/ Ticket<\/h2>\n          <p class=\"ftv-sub\">Crea una solicitud r\u00e1pida. (Luego lo conectamos a tu sistema real).<\/p>\n\n          <div class=\"ftv-field\">\n            <label>Asunto<\/label>\n            <input id=\"tkAsunto\" class=\"ftv-input\" placeholder=\"Ej: Sin internet \/ IPTV \/ WiFi lento\" \/>\n          <\/div>\n          <div class=\"ftv-field\">\n            <label>Detalle<\/label>\n            <textarea id=\"tkDetalle\" class=\"ftv-input\" rows=\"4\" placeholder=\"Describe el problema, direcci\u00f3n, horario y un tel\u00e9fono de contacto.\"><\/textarea>\n          <\/div>\n          <div class=\"ftv-row\">\n            <button id=\"tkEnviar\" class=\"ftv-btn\" type=\"button\">Enviar ticket<\/button>\n            <span id=\"tkMsg\" class=\"ftv-loading\"><\/span>\n          <\/div>\n\n          <div style=\"margin-top:12px\" class=\"ftv-note\">\n            Pr\u00f3ximo paso: conectamos esto a WhatsApp \/ correo \/ tu m\u00f3dulo OT del sistema.\n          <\/div>\n        <\/div>\n\n        <div class=\"ftv-footer\">\u00a9 FullTV \u2014 Portal Clientes<\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <script>\n    (function(){\n      \/\/ \u2705 AQU\u00cd se cambia la base de la API (seg\u00fan tu DevTools es webpay.fulltv.cl)\n      const API_BASE = \"https:\/\/webpay.fulltv.cl\";\n\n      const els = {\n        loginCard: document.getElementById(\"ftvLoginCard\"),\n        dashCard: document.getElementById(\"ftvDashCard\"),\n        rut: document.getElementById(\"ftvRut\"),\n        email: document.getElementById(\"ftvEmail\"),\n        loginBtn: document.getElementById(\"ftvLoginBtn\"),\n        loginMsg: document.getElementById(\"ftvLoginMsg\"),\n        logoutBtn: document.getElementById(\"ftvLogoutBtn\"),\n        userPill: document.getElementById(\"ftvUserPill\"),\n\n        kpiEstado: document.getElementById(\"kpiEstado\"),\n        kpiDeuda: document.getElementById(\"kpiDeuda\"),\n        kpiPlan: document.getElementById(\"kpiPlan\"),\n        estadoMsg: document.getElementById(\"estadoMsg\"),\n\n        planBox: document.getElementById(\"planBox\"),\n        boletasBox: document.getElementById(\"boletasBox\"),\n\n        tkEnviar: document.getElementById(\"tkEnviar\"),\n        tkAsunto: document.getElementById(\"tkAsunto\"),\n        tkDetalle: document.getElementById(\"tkDetalle\"),\n        tkMsg: document.getElementById(\"tkMsg\")\n      };\n\n      const storageKey = \"ftvPortalSession_v2\";\n\n      const getSession = () => {\n        try { return JSON.parse(localStorage.getItem(storageKey) || \"null\"); } catch(e){ return null; }\n      };\n      const setSession = (s) => localStorage.setItem(storageKey, JSON.stringify(s));\n      const clearSession = () => localStorage.removeItem(storageKey);\n\n      function fmtCLP(n){\n        const x = Number(n || 0);\n        return x.toLocaleString(\"es-CL\", { style:\"currency\", currency:\"CLP\", maximumFractionDigits:0 });\n      }\n\n      function normalizeRut(rut){\n        return (rut || \"\").trim();\n      }\n\n      function normalizeEmail(email){\n        return (email || \"\").trim().toLowerCase();\n      }\n\n      async function api(path, opts={}){\n        const session = getSession();\n        const token = session && session.token ? session.token : null;\n\n        const controller = new AbortController();\n        const timeout = setTimeout(() => controller.abort(), 20000);\n\n        try{\n          const res = await fetch(API_BASE + path, {\n            method: opts.method || \"POST\",          \/\/ tus endpoints son POST\n            mode: \"cors\",\n            credentials: \"omit\",                   \/\/ se usa Bearer token, no cookie\n            signal: controller.signal,\n            headers: {\n              \"Content-Type\": \"application\/json;charset=UTF-8\",\n              ...(token ? { \"Authorization\": \"Bearer \" + token } : {}),\n              ...(opts.headers || {})\n            },\n            body: opts.body ? JSON.stringify(opts.body) : (opts.method === \"GET\" ? undefined : \"{}\")\n          });\n\n          const text = await res.text();\n          let data = null;\n          try { data = JSON.parse(text); } catch(e){ data = { raw: text }; }\n\n          if(!res.ok){\n            const msg = (data && (data.error || data.message)) ? (data.error || data.message) : (\"Error HTTP \" + res.status);\n            throw new Error(msg);\n          }\n          return data;\n        } finally {\n          clearTimeout(timeout);\n        }\n      }\n\n      function showLoggedInUI(session){\n        els.loginCard.classList.add(\"ftv-hide\");\n        els.dashCard.classList.remove(\"ftv-hide\");\n        els.logoutBtn.classList.remove(\"ftv-hide\");\n        els.userPill.classList.remove(\"ftv-hide\");\n\n        const name = session.nombreCompleto ? (\" \u2022 \" + session.nombreCompleto) : \"\";\n        els.userPill.textContent = `${session.rut} \u2022 ${session.email}${name}`;\n      }\n\n      function showLoggedOutUI(){\n        els.loginCard.classList.remove(\"ftv-hide\");\n        els.dashCard.classList.add(\"ftv-hide\");\n        els.logoutBtn.classList.add(\"ftv-hide\");\n        els.userPill.classList.add(\"ftv-hide\");\n      }\n\n      \/\/ ====== CARGAS (mapeadas a tus endpoints reales) ======\n\n      async function loadDatosUsuario(){\n        \/\/ POST \/datos_usuario  -> { id, rut, email, nombres, apellidos }\n        const u = await api(\"\/datos_usuario\", { method:\"POST\" });\n        return u;\n      }\n\n      async function loadServiciosUsuario(){\n        \/\/ POST \/servicios_usuario -> [ { idSolicitud, arrServicios:[{nombre,monto}...] } ]\n        const s = await api(\"\/servicios_usuario\", { method:\"POST\" });\n        return s;\n      }\n\n      async function loadPlanUsuario(){\n        \/\/ POST \/plan_usuario -> { idSolicitud, jpPlan:{ nombre, monto } }\n        const p = await api(\"\/plan_usuario\", { method:\"POST\" });\n        return p;\n      }\n\n      async function loadBoletasUsuario(){\n        \/\/ POST \/boletas_usuario -> a veces [] o lista\/objeto\n        const b = await api(\"\/boletas_usuario\", { method:\"POST\" });\n        return b;\n      }\n\n      function inferBoletasItems(raw){\n        \/\/ Soporta varios formatos sin romper\n        if(Array.isArray(raw)) return raw;\n        if(raw && Array.isArray(raw.items)) return raw.items;\n        if(raw && Array.isArray(raw.boletas)) return raw.boletas;\n        return [];\n      }\n\n      function sumDeudaFromBoletas(items){\n        \/\/ Si viene campo \"estado\" \/ \"pagada\" \/ \"pendiente\", tratamos de sumar pendientes.\n        let deuda = 0;\n        for(const it of items){\n          const estado = (it.estado || it.status || \"\").toString().toLowerCase();\n          const pagada = it.pagada === true;\n          const pendiente = it.pendiente === true;\n          const monto = Number(it.monto || it.total || it.amount || 0);\n\n          const isPendiente = pendiente || (!pagada && (estado.includes(\"pend\") || estado.includes(\"debe\") || estado.includes(\"no pag\")));\n          if(isPendiente) deuda += monto;\n        }\n        return deuda;\n      }\n\n      async function loadEstado(){\n        els.estadoMsg.textContent = \"Cargando\u2026\";\n        els.estadoMsg.className = \"ftv-loading\";\n\n        try{\n          const [planRaw, servRaw, bolRaw] = await Promise.all([\n            loadPlanUsuario(),\n            loadServiciosUsuario(),\n            loadBoletasUsuario()\n          ]);\n\n          \/\/ KPI Plan\n          const planNombre = (planRaw && planRaw.jpPlan && planRaw.jpPlan.nombre) ? planRaw.jpPlan.nombre : \"\u2014\";\n          els.kpiPlan.textContent = planNombre;\n\n          \/\/ Servicios tabla\n          let serviciosHTML = \"\";\n          const servArr = Array.isArray(servRaw) ? servRaw : [];\n          const serv0 = servArr[0] || null;\n          const listaServ = serv0 && Array.isArray(serv0.arrServicios) ? serv0.arrServicios : [];\n\n          if(listaServ.length){\n            const rows = listaServ.map(x => `\n              <tr>\n                <td>${(x.nombre || \"-\")}<\/td>\n                <td>${fmtCLP(x.monto || 0)}<\/td>\n              <\/tr>\n            `).join(\"\");\n            serviciosHTML = `\n              <div style=\"margin-top:12px\">\n                <table class=\"ftv-table\">\n                  <thead><tr><th>Servicio<\/th><th>Monto<\/th><\/tr><\/thead>\n                  <tbody>${rows}<\/tbody>\n                <\/table>\n              <\/div>\n            `;\n          } else {\n            serviciosHTML = `<div style=\"margin-top:12px\" class=\"ftv-note\">No hay servicios para mostrar.<\/div>`;\n          }\n\n          \/\/ Boletas \/ deuda\n          const boletasItems = inferBoletasItems(bolRaw);\n          const deuda = sumDeudaFromBoletas(boletasItems);\n\n          els.kpiDeuda.textContent = fmtCLP(deuda);\n          els.kpiEstado.textContent = deuda > 0 ? \"Con deuda\" : \"Al d\u00eda\";\n          els.kpiEstado.className = \"\";\n\n          els.estadoMsg.className = \"ftv-success\";\n          els.estadoMsg.innerHTML = `\n            <div class=\"ftv-note\">Tu informaci\u00f3n se carga desde WebPay.<\/div>\n            ${serviciosHTML}\n          `;\n        }catch(err){\n          els.estadoMsg.textContent = err.message || \"Error al cargar.\";\n          els.estadoMsg.className = \"ftv-error\";\n          els.kpiEstado.textContent = \"\u2014\";\n          els.kpiDeuda.textContent = \"$\u2014\";\n          els.kpiPlan.textContent = \"\u2014\";\n        }\n      }\n\n      async function loadPlan(){\n        els.planBox.textContent = \"Cargando\u2026\";\n        els.planBox.className = \"ftv-loading\";\n        try{\n          const data = await loadPlanUsuario();\n          const jp = (data && data.jpPlan) ? data.jpPlan : {};\n          els.planBox.className = \"\";\n          els.planBox.innerHTML = `\n            <div class=\"ftv-kpis\">\n              <div class=\"ftv-kpi\"><b>${jp.nombre || \"\u2014\"}<\/b><span>Plan<\/span><\/div>\n              <div class=\"ftv-kpi\"><b>${fmtCLP(jp.monto || 0)}<\/b><span>Precio<\/span><\/div>\n              <div class=\"ftv-kpi\"><b>${data.idSolicitud ? (\"#\"+data.idSolicitud) : \"\u2014\"}<\/b><span>ID Solicitud<\/span><\/div>\n            <\/div>\n            <div class=\"ftv-note\" style=\"margin-top:12px\">Si tus montos no cuadran, se debe a combos\/promos del sistema de WebPay.<\/div>\n          `;\n        }catch(err){\n          els.planBox.textContent = err.message;\n          els.planBox.className = \"ftv-error\";\n        }\n      }\n\n      async function loadBoletas(){\n        els.boletasBox.textContent = \"Cargando\u2026\";\n        els.boletasBox.className = \"ftv-loading\";\n        try{\n          const raw = await loadBoletasUsuario();\n          const items = inferBoletasItems(raw);\n\n          const rows = items.map(b => {\n            const periodo = b.periodo || b.mes || b.fecha || \"-\";\n            const rango = b.rango || b.rango_facturacion || \"-\";\n            const monto = Number(b.monto || b.total || 0);\n            const estado = b.estado || b.status || (b.pagada ? \"Pagada\" : (b.pendiente ? \"Pendiente\" : \"\"));\n            const badgeClass = (estado || \"\").toString().toLowerCase().includes(\"pag\") ? \"ok\" :\n                              (estado || \"\").toString().toLowerCase().includes(\"pend\") ? \"warn\" : \"bad\";\n            const detalle = b.detalle || b.glosa || \"-\";\n\n            return `\n              <tr>\n                <td>${periodo}<\/td>\n                <td>${rango}<\/td>\n                <td>${fmtCLP(monto)}<\/td>\n                <td>${estado ? `<span class=\"ftv-badge ${badgeClass}\">${estado}<\/span>` : \"-\"}<\/td>\n                <td>${detalle}<\/td>\n              <\/tr>\n            `;\n          }).join(\"\");\n\n          els.boletasBox.className = \"\";\n          els.boletasBox.innerHTML = `\n            <table class=\"ftv-table\">\n              <thead>\n                <tr><th>Per\u00edodo<\/th><th>Rango<\/th><th>Monto<\/th><th>Estado<\/th><th>Detalle<\/th><\/tr>\n              <\/thead>\n              <tbody>\n                ${rows || `<tr><td colspan=\"5\" style=\"color:#6b7280\">No existen boletas por pagar asociadas.<\/td><\/tr>`}\n              <\/tbody>\n            <\/table>\n            <div style=\"margin-top:12px\" class=\"ftv-note\">\n              Para pagar, usa el bot\u00f3n <b>Pagar (WebPay)<\/b> arriba.\n            <\/div>\n          `;\n        }catch(err){\n          els.boletasBox.textContent = err.message;\n          els.boletasBox.className = \"ftv-error\";\n        }\n      }\n\n      function initTabs(){\n        const tabs = document.querySelectorAll(\"#ftv-portal .ftv-tab\");\n        const panels = document.querySelectorAll(\"#ftv-portal .ftv-panel\");\n\n        tabs.forEach(t => t.addEventListener(\"click\", async () => {\n          tabs.forEach(x => x.classList.remove(\"active\"));\n          t.classList.add(\"active\");\n\n          const name = t.getAttribute(\"data-tab\");\n          panels.forEach(p => p.classList.toggle(\"ftv-hide\", p.getAttribute(\"data-panel\") !== name));\n\n          if(name === \"estado\") await loadEstado();\n          if(name === \"plan\") await loadPlan();\n          if(name === \"boletas\") await loadBoletas();\n        }));\n      }\n\n      async function doLogin(){\n        els.loginMsg.textContent = \"Ingresando\u2026\";\n        els.loginMsg.className = \"ftv-loading\";\n        els.loginBtn.disabled = true;\n\n        try{\n          const rut = normalizeRut(els.rut.value);\n          const email = normalizeEmail(els.email.value);\n          if(!rut || !email) throw new Error(\"Completa RUT y correo.\");\n\n          \/\/ POST \/login -> { web: \"JWT\" }  (seg\u00fan tu captura)\n          const data = await api(\"\/login\", { method:\"POST\", body:{ rut, email } });\n\n          const token = data && (data.web || data.token || data.jwt);\n          if(!token) throw new Error(\"Login OK pero no lleg\u00f3 token (web). Revisa respuesta de \/login.\");\n\n          \/\/ Cargamos datos de usuario para mostrar nombre\n          setSession({ rut, email, token, ts: Date.now(), ok: true });\n\n          const u = await loadDatosUsuario();\n          const nombreCompleto = [u.nombres, u.apellidos].filter(Boolean).join(\" \").trim();\n\n          const session = getSession();\n          setSession({ ...session, nombreCompleto });\n\n          els.loginMsg.textContent = \"Login correcto\";\n          els.loginMsg.className = \"ftv-success\";\n\n          showLoggedInUI({ rut, email, nombreCompleto });\n\n          await loadEstado();\n        }catch(err){\n          els.loginMsg.textContent = err.message;\n          els.loginMsg.className = \"ftv-error\";\n        }finally{\n          els.loginBtn.disabled = false;\n        }\n      }\n\n      function doLogout(){\n        clearSession();\n        showLoggedOutUI();\n        els.loginMsg.textContent = \"\";\n        els.rut.value = \"\";\n        els.email.value = \"\";\n      }\n\n      \/\/ Tickets (demo)\n      function sendTicketMock(){\n        const asunto = (els.tkAsunto.value || \"\").trim();\n        const detalle = (els.tkDetalle.value || \"\").trim();\n        if(!asunto || !detalle){\n          els.tkMsg.textContent = \"Completa asunto y detalle.\";\n          els.tkMsg.className = \"ftv-error\";\n          return;\n        }\n        els.tkMsg.textContent = \"Ticket enviado (demo).\";\n        els.tkMsg.className = \"ftv-success\";\n        els.tkAsunto.value = \"\";\n        els.tkDetalle.value = \"\";\n      }\n\n      \/\/ Init\n      initTabs();\n      els.loginBtn.addEventListener(\"click\", doLogin);\n      els.logoutBtn.addEventListener(\"click\", doLogout);\n      els.tkEnviar.addEventListener(\"click\", sendTicketMock);\n\n      \/\/ Auto login si hay sesi\u00f3n guardada\n      const session = getSession();\n      if(session && session.ok && session.token){\n        showLoggedInUI(session);\n        loadEstado();\n      }else{\n        showLoggedOutUI();\n      }\n    })();\n  <\/script>\n<\/div>\n\n\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>FTV Portal de Clientes Revisa tu servicio, boletas y soporte \u2014 Cerrar sesi\u00f3n Pagar (WebPay) Ingresar Usa el mismo RUT [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_uag_custom_page_level_css":"","site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"class_list":["post-726","page","type-page","status-publish","hentry"],"uagb_featured_image_src":{"full":false,"thumbnail":false,"medium":false,"medium_large":false,"large":false,"1536x1536":false,"2048x2048":false},"uagb_author_info":{"display_name":"admin","author_link":"https:\/\/fulltv.cl\/index.php\/author\/admincris\/"},"uagb_comment_info":0,"uagb_excerpt":"FTV Portal de Clientes Revisa tu servicio, boletas y soporte \u2014 Cerrar sesi\u00f3n Pagar (WebPay) Ingresar Usa el mismo RUT [&hellip;]","_links":{"self":[{"href":"https:\/\/fulltv.cl\/index.php\/wp-json\/wp\/v2\/pages\/726","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/fulltv.cl\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/fulltv.cl\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/fulltv.cl\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/fulltv.cl\/index.php\/wp-json\/wp\/v2\/comments?post=726"}],"version-history":[{"count":6,"href":"https:\/\/fulltv.cl\/index.php\/wp-json\/wp\/v2\/pages\/726\/revisions"}],"predecessor-version":[{"id":850,"href":"https:\/\/fulltv.cl\/index.php\/wp-json\/wp\/v2\/pages\/726\/revisions\/850"}],"wp:attachment":[{"href":"https:\/\/fulltv.cl\/index.php\/wp-json\/wp\/v2\/media?parent=726"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}