', tool:'https://search.google.com/test/rich-results'}, {label:'Sitemap.xml',status:'ok'}, ].map(item=>{ const stLabel={ok:'Correcto',warn:'Mejorar',err:'Falta'}[item.status]; const stIcon={ok:'ti-circle-check',warn:'ti-alert-triangle',err:'ti-circle-x'}[item.status]; const stColor={ok:'#16A34A',warn:'#D97706',err:'#DC2626'}[item.status]; const stBg={ok:'#F0FDF4',warn:'#FFFBEB',err:'#FEF2F2'}[item.status]; const hasDetails=item.fix&&item.status!=='ok'; return `
${item.label} ${stLabel} ${hasDetails?'':''}
${hasDetails?``:''}
`; }).join('')}
Keywords monitoreadas
${EMPTY('ti-key','Sin keywords todavia','Agrega keywords para rastrear tu posicion en Google y detectar oportunidades de contenido.', ``)}
`; } window.addKeyword = () => { const kw = prompt('Keyword a monitorear:'); if (kw) alert('Keyword agregada: '+kw); }; async function vNetworks() { const id = cid(); const [accD, statusD] = await Promise.allSettled([ get(`${API}/companies/${id}/accounts`), get(`${API}/oauth/status`), ]); const accs = accD.value?.accounts || []; const oauthStatus = statusD.value?.platforms || {}; const NETS = [ { id:'facebook', name:'Facebook', desc:'Paginas de Facebook' }, { id:'instagram', name:'Instagram', desc:'Perfil Business/Creator' }, { id:'tiktok', name:'TikTok', desc:'Cuenta TikTok Business' }, { id:'youtube', name:'YouTube', desc:'Canal de YouTube' }, { id:'linkedin', name:'LinkedIn', desc:'Perfil o pagina de empresa' }, { id:'twitter', name:'X (Twitter)', desc:'Cuenta de X' }, { id:'whatsapp', name:'WhatsApp Business', desc:'WhatsApp Cloud API' }, { id:'google_business', name:'Google Business', desc:'Perfil de negocio en Google' }, ]; const connMap = {}; accs.forEach(a => { if (!connMap[a.platform]) connMap[a.platform] = []; connMap[a.platform].push(a); }); const totalConnected = accs.length; const activePlatforms = Object.keys(connMap).length; return ` ${pageHero('ti-plug','Redes conectadas','Conecta tus cuentas via OAuth seguro — nunca compartimos tu contrasena', ``)}
Conexion segura por OAuth
Al conectar, te redirigimos al login oficial de cada red donde autorizas el acceso. Nunca vemos tu contraseña. Instagram debe ser cuenta Business o Creator vinculada a una página de Facebook.
Cuentas conectadas
${totalConnected}
Plataformas activas
${activePlatforms}
Total seguidores
${fmt(accs.reduce((s,a)=>s+(a.followers_count||0),0))}
${NETS.map(net => { const connected = connMap[net.id] || []; const isConfigured = oauthStatus[net.id]?.configured; const isConn = connected.length > 0; return `
${net.name}
${net.desc}
${isConn ? ` Conectado` : ''}
${connected.length ? connected.map(a=>`
${a.profile_image_url?``:(a.account_name?.slice(0,2).toUpperCase()||'?')}
${a.account_name}
${fmt(a.followers_count||0)} seguidores
`).join('') : ''}
${isConfigured ? `` : `

El administrador debe configurar ${net.name} primero

`}
`; }).join('')}
`; } window.switchCompany = async (companyId, companyName) => { if (!confirm(`Cambiar a la empresa "${companyName}"?`)) return; try { const d = await get(`${API}/companies/${companyId}`); if (d) { sc(d); company = d; if(user){user.company_id=companyId;su(user);} currentView='dashboard'; renderApp(); } } catch(e) { alert('Error al cambiar empresa: ' + e.message); } }; window.createCompany = () => { modal('Nueva empresa', `
`); }; window.doCreateCompany = async () => { const name=document.getElementById('nc-name')?.value?.trim(); if(!name){setAlert('nc-alert','El nombre es obligatorio');return;} try { const r=await post(`${API}/companies`,{name,industry:document.getElementById('nc-industry')?.value||'',website_url:document.getElementById('nc-web')?.value||''}); const newCo=r?.company||r; if(newCo?.id){sc(newCo);company=newCo;if(user){user.company_id=newCo.id;su(user);}closeModal();currentView='dashboard';renderApp();} } catch(e){setAlert('nc-alert',e.message);} }; // OAuth: abre popup al endpoint de autorizacion y espera el postMessage del callback window.oauthConnect = async (platform, name) => { try { const r = await get(`${API}/oauth/${platform}/start?companyId=${cid()}`); if (!r?.url) { alert('No se pudo iniciar la conexion'); return; } const w = 600, h = 720; const left = (screen.width - w) / 2, top = (screen.height - h) / 2; const popup = window.open(r.url, 'oauth_'+platform, `width=${w},height=${h},left=${left},top=${top}`); if (!popup) { alert('Permite las ventanas emergentes para conectar tu cuenta'); return; } // Modal de espera modal(`Conectando ${name}`, `
Esperando autorizacion...

Completa el inicio de sesion en la ventana emergente de ${name}.

`); const handler = (ev) => { if (ev.data?.type !== 'oauth_result') return; window.removeEventListener('message', handler); closeModal(); if (ev.data.success) { modal('Conexion exitosa', `
Cuenta conectada

${ev.data.message||'Tu cuenta esta lista para publicar.'}

`); } else { modal('No se pudo conectar', `

${ev.data.message||'Intenta de nuevo.'}

`); } }; window.addEventListener('message', handler); // Si cierran el popup sin completar const checkClosed = setInterval(() => { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', handler); setTimeout(() => { if (document.getElementById('modal-root')) { closeModal(); showView('networks'); } }, 500); } }, 800); } catch(e) { alert('Error: ' + e.message); } }; window.disconnectNet = async (id, name) => { if (!confirm(`Desconectar ${name}?`)) return; await del(`${API}/companies/${cid()}/accounts/${id}`).catch(e => alert(e.message)); showView('networks'); }; async function vTeam() { const id = cid(); const d = await get(`${API}/companies/${id}/team`).catch(() => ({})); const ROLES = { company_owner:'Propietario', company_admin:'Admin', editor:'Editor', community_manager:'Community Mgr', analyst:'Analista', viewer:'Solo lectura' }; const allMembers = [d?.owner ? {...d.owner, role:'company_owner'} : null, ...(d?.members||[])].filter(Boolean); const ROLE_COLORS = {company_owner:'blue',company_admin:'purple',editor:'green',community_manager:'amber',analyst:'gray',viewer:'gray'}; const ROLE_ICONS = {company_owner:'ti-crown',company_admin:'ti-shield',editor:'ti-pencil',community_manager:'ti-message',analyst:'ti-chart-bar',viewer:'ti-eye'}; return ` ${pageHero('ti-users','Equipo','Gestiona los miembros y sus niveles de acceso', `
`)}
Colaboracion en equipo
Cada rol tiene permisos distintos. Editor: crea contenido. Community Mgr: responde mensajes. Analista: solo ve reportes. Admin: todo excepto facturacion.
Miembros totales
${allMembers.length}
Activos
${allMembers.filter(m=>m.status!=='pending').length}
Invitaciones pendientes
${allMembers.filter(m=>m.status==='pending').length}
${allMembers.length} miembro${allMembers.length!==1?'s':''}
${allMembers.length ? allMembers.map(m => { const role = m.role || m.team_role || 'viewer'; const roleLabel = ROLES[role] || role; const roleColor = ROLE_COLORS[role] || 'gray'; const roleIcon = ROLE_ICONS[role] || 'ti-user'; const isOwner = role === 'company_owner'; const initials = (m.name||'?').slice(0,2).toUpperCase(); const avatarGrad = isOwner ? 'var(--grad-primary)' : 'linear-gradient(135deg,#16A34A,#4ADE80)'; return `
${m.profile_image_url ? `` : initials}
${m.name||'—'}
${m.email||''}
${roleLabel} ${m.status==='pending' ? `Pendiente` : ''} ${!isOwner ? `` : ''}
`; }).join('') : `
${EMPTY('ti-users','Sin miembros aun','Invita a tu equipo para colaborar en el contenido', '')}
`}
Permisos por rol
${[ ['company_owner','Propietario','ti-crown','blue','Todo: facturacion, equipo, config'], ['company_admin','Admin','ti-shield','purple','Todo menos facturacion'], ['editor','Editor','ti-pencil','green','Crea y programa contenido'], ['community_manager','Community Mgr','ti-message','amber','Responde inbox y aprueba'], ['analyst','Analista','ti-chart-bar','gray','Solo ve metricas y reportes'], ['viewer','Lector','ti-eye','gray','Solo lectura del contenido'], ].map(([,label,ic,color,desc])=>`
${label}
${desc}
`).join('')}
`; } window.inviteTeam = () => modal('Invitar miembro al equipo', `
`); window.doInvite = async () => { const email = document.getElementById('inv-email')?.value; if (!email) { setAlert('inv-alert','Email requerido'); return; } try { await post(`${API}/companies/${cid()}/team/invite`,{email,role:document.getElementById('inv-role')?.value}); closeModal(); showView('team'); } catch(e) { setAlert('inv-alert',e.message); } }; window.removeTeamMember = async (uid, name) => { if (!confirm(`Remover a ${name}?`)) return; await del(`${API}/companies/${cid()}/team/${uid}`).catch(e=>alert(e.message)); showView('team'); }; async function vBilling() { const id = cid(); const d = await get(`${API}/companies/${id}/billing`).catch(() => ({})); const sub = d?.subscription, plans = d?.available_plans||[]; // Usage bar helper const usageBar = (used, max, label, color) => { const pct = max <= 0 ? 0 : Math.min(100, Math.round((used/max)*100)); const barColor = pct >= 90 ? 'var(--red)' : pct >= 70 ? 'var(--amber)' : color||'var(--indigo)'; return `
${label} ${used} / ${max===-1?'∞':max}
${pct>=90&&max!==-1?`
Casi al limite
`:''}
`; }; return ` ${pageHero('ti-credit-card','Facturacion','Tu plan, uso y metodos de pago', `
${sub?``:''}
`)}
Pagos seguros via Stripe
Cancela o cambia de plan cuando quieras, sin penalidad. Los cambios aplican al inicio del siguiente ciclo de facturacion.
${sub ? `
Plan actual
${sub.plan_name||'Starter'}
${sub.status==='active'?'Activo':'Estado: '+sub.status}${sub.current_period_end?' · Renueva '+fmtD(sub.current_period_end):''}
$${sub.price_monthly||0}/mes
Uso este mes
${usageBar(sub.posts_used_this_month||0, sub.max_posts_per_month||0, 'Publicaciones', 'var(--indigo)')} ${usageBar(sub.ai_gen_used_this_month||0, sub.max_ai_generations||0, 'Generaciones IA', 'var(--blue)')}
${sub.max_social_accounts===-1?'∞':sub.max_social_accounts}
Redes sociales
${sub.max_team_members===-1?'∞':sub.max_team_members}
Miembros equipo
` : ''}
Planes disponibles
Stripe — pagos seguros
${plans.map(p => { const isCurrent = sub?.plan_id===p.id; const isPopular = p.name?.toLowerCase().includes('pro')||p.name?.toLowerCase().includes('growth'); return `
${isCurrent?`
PLAN ACTUAL
`:''} ${isPopular&&!isCurrent?`
⭐ MAS POPULAR
`:''}
${p.name}
$${p.price_monthly}/mes
${[[`${p.max_posts_per_month===-1?'Posts ilimitados':p.max_posts_per_month+' posts/mes'}`,true],[`${p.max_social_accounts===-1?'Redes ilimitadas':p.max_social_accounts+' redes'}`,true],[`${p.feature_ai_content?'IA generativa':' Sin IA generativa'}`,!!p.feature_ai_content],[`${p.max_team_members>1?p.max_team_members+' miembros':'Solo propietario'}`,p.max_team_members>1]].map(([feat,ok])=>`
${feat}
`).join('')}
${isCurrent?`
Tu plan actual
`:``}
`; }).join('')}
window.billingPortal = async () => { try { const r = await post(`${API}/companies/${cid()}/billing/portal`,{}); if(r?.url) window.open(r.url,'_blank'); } catch(e) { alert(e.message); } }; window.checkout = async planId => { try { const r = await post(`${API}/companies/${cid()}/billing/checkout`,{planId,billingCycle:'monthly'}); if(r?.url) location.href=r.url; } catch(e) { alert(e.message); } }; async function vSettings() { const id = cid(); const d = await get(`${API}/companies/${id}`).catch(() => ({})); return ` ${pageHero('ti-settings','Configuracion','Ajustes de tu empresa, cuenta y preferencias de la IA', `
`)}
Voz de marca
La Voz de marca es la instruccion que la IA usa en cada generacion de contenido. Cuanto mas detallada, mas consistente sera tu contenido. Incluye tono, estilo, palabras que evitar y publico objetivo.
Informacion de la empresa
Voz de marca para la IA
Importante
Consejos para una buena voz de marca:
${['Define el tono (formal/informal/inspiracional/divertido)','Especifica tu publico objetivo (edad, pais, intereses)','Menciona palabras o frases que EVITAR','Lista los valores y diferenciadores de tu marca','Incluye ejemplos de posts que te gustan'].map(t=>`
${t}
`).join('')}
Mi cuenta
${(user?.name||'?').slice(0,2).toUpperCase()}
${user?.name||'—'}
${user?.email||''}
${{company_owner:'Propietario',company_admin:'Admin',editor:'Editor',community_manager:'Community Mgr',analyst:'Analista',viewer:'Lector'}[user?.role]||user?.role||'—'}
Notificaciones por email
${[ ['Post publicado exitosamente','Cuando una publicacion se publica correctamente',true], ['Error al publicar','Si falla una publicacion programada',true], ['Nuevo comentario o mensaje','Actividad en tu Bandeja de entrada',false], ['Reporte semanal','Resumen de desempeno cada lunes',true], ['Trial por terminar','Aviso 3 dias antes de que expire el trial',true], ['Articulo de blog generado','Cuando la IA termina un articulo',false], ].map(([label,desc,checked])=>`
${label}
${desc}
`).join('')}
Zona peligrosa
Exportar mis datos
Descarga todos tus posts y configuracion
Eliminar empresa
Elimina todos los datos permanentemente
`; } window.saveSettings = async () => { try { await put(`${API}/companies/${cid()}`,{name:document.getElementById('set-name')?.value,website_url:document.getElementById('set-web')?.value,industry:document.getElementById('set-ind')?.value,brand_voice:document.getElementById('set-voice')?.value}); setAlert('set-alert','Cambios guardados correctamente','ok'); setTimeout(()=>setAlert('set-alert','','ok'),3000); } catch(e) { setAlert('set-alert',e.message); } }; window.changePass = () => modal('Cambiar contrasena',`
`); window.doChangePass = async () => { const oldP=document.getElementById('cp-old')?.value,newP=document.getElementById('cp-new')?.value,conf=document.getElementById('cp-conf')?.value; if(!oldP||!newP){setAlert('cp-alert','Todos los campos son requeridos');return} if(newP!==conf){setAlert('cp-alert','Las contrasenhas no coinciden');return} if(newP.length<8){setAlert('cp-alert','Minimo 8 caracteres');return} try{await post(`${API}/auth/change-password`,{currentPassword:oldP,newPassword:newP});closeModal();alert('Contrasena cambiada exitosamente')} catch(e){setAlert('cp-alert',e.message)} }; // ── COMPANIES ───────────────────────────────────────────────── async function vCompanies() { const id = cid(); const [dR, accsR, postsR, teamR] = await Promise.allSettled([ get(`${API}/companies/${id}`), get(`${API}/companies/${id}/accounts`), get(`${API}/companies/${id}/posts?limit=200`), get(`${API}/companies/${id}/team`), ]); const d = dR.value || {}; const nets = (accsR.value?.accounts || []).filter(a => a.is_active); const posts = postsR.value?.posts || []; const team = teamR.value ? [ ...(teamR.value.owner ? [teamR.value.owner] : []), ...(teamR.value.members || []) ] : []; const now = new Date(); const postsThisMonth = posts.filter(p => { const t = new Date(p.published_at||p.created_at); return t.getMonth()===now.getMonth() && t.getFullYear()===now.getFullYear(); }).length; const scheduled = posts.filter(p => p.status==='scheduled').length; const published = posts.filter(p => p.status==='published').length; const stats = [ { label:'Redes activas', value:nets.length, ic:'ti-plug-connected', grad:'linear-gradient(135deg,#5B4FE0,#7C5CFC)' }, { label:'Posts este mes', value:postsThisMonth, ic:'ti-send', grad:'linear-gradient(135deg,#0EA5E9,#38BDF8)' }, { label:'Programados', value:scheduled, ic:'ti-clock', grad:'linear-gradient(135deg,#D97706,#FBBF24)' }, { label:'Miembros equipo', value:d.team_count||team.length||1, ic:'ti-users', grad:'linear-gradient(135deg,#16A34A,#4ADE80)' }, ]; // Multi-company: fetch all companies for this user const companiesListR = await get(`${API}/companies`).catch(()=>({companies:[]})); const allCompanies = companiesListR?.companies || []; const currentCid = cid(); return ` ${pageHero('ti-building','Empresas','Gestiona el perfil, las redes y el equipo de cada empresa', `
`)} ${allCompanies.length>1?`
Tus empresas (${allCompanies.length})
${allCompanies.map(c=>{ const isCurrent=c.id===currentCid||c.id===company?.id; return `
${(c.name||'?').slice(0,1).toUpperCase()}
${c.name}
${c.industry||'General'}
${isCurrent?`Activa`:``}
`; }).join('')}
`:allCompanies.length<=1?`
Puedes crear o unirte a mas empresas para administrarlas desde aqui.
`:''}
${stats.map(s=>`
${s.label}
${s.value}
`).join('')}
${company?.name || 'Mi Empresa'}
${d?.industry || 'General'}${d?.website_url?' · '+d.website_url.replace(/^https?:\/\/(www\.)?/,''):''}
${nets.length ? nets.slice(0,6).map(a=>`${a.platform.charAt(0).toUpperCase()+a.platform.slice(1)}`).join('') : 'Sin redes conectadas aun'}
Activa
Resumen de actividad
Publicados${published}
Programados${scheduled}
Este mes${postsThisMonth}
Equipo
${team.length ? team.slice(0,5).map(m=>`
${(m.name||m.email||'?').slice(0,1).toUpperCase()}
${m.name||m.email}
${m.role||'Miembro'}
`).join('') : EMPTY('ti-users','Solo tu por ahora','Invita a tu equipo para colaborar', ``)}
`; } // ── VIDEOS CON AVATAR ───────────────────────────────────────── async function vVideos() { // Avatares: ranuras configurables. El usuario sube/licencia sus propias imagenes // (con derechos para uso comercial). NO se incrustan fotos de personas reales scrapeadas. // avatarSource lee de la biblioteca de medios de la empresa cuando exista. const id = cid(); const lib = await get(`${API}/companies/${id}/media?type=image`).catch(()=>({files:[]})); const avatars = (lib?.files||[]); const VID_DURS = ['15s','30s','60s','90s']; const VID_FMTS = [{id:'9:16',label:'9:16 Vertical',ic:'ti-device-mobile',note:'Reels & Stories'},{id:'1:1',label:'1:1 Cuadrado',ic:'ti-square',note:'Feed'},{id:'16:9',label:'16:9 Landscape',ic:'ti-device-tv',note:'YouTube'}]; const VID_MUSIC = [{id:'corporativo',label:'Corporativo',emoji:'💼'},{id:'energetico',label:'Energetico',emoji:'⚡'},{id:'suave',label:'Suave',emoji:'🎵'},{id:'ninguna',label:'Sin musica',emoji:'🔇'}]; return ` ${pageHero('ti-user-circle','Avatar IA','Crea videos con tu imagen para Reels, Stories y TikTok', ``)}
1
Elige tu avatar
La imagen de fondo del video
${avatars.length ? `
${avatars.map((a,i)=>`
${i===0?`
`:''}
`).join('')}
Agregar
` : `
Sin avatares aun

Sube fotos propias, de tu marca, o generadas por IA con licencia comercial. Se muestran aqui como opciones de avatar.

Solo imagenes con derechos de uso comercial
`}
2
Texto del video
Se superpone sobre la imagen
0/300
PequeñoGrande
3
Configuracion del video
Formato, duracion y musica
${VID_FMTS.map((f,i)=>` `).join('')}
${VID_DURS.map((d,i)=>` `).join('')}
${VID_MUSIC.map((m,i)=>` `).join('')}
${[{id:'es-CR',label:'🇨🇷 Español CR'},{id:'es-ES',label:'🇪🇸 Español ES'},{id:'en-US',label:'🇺🇸 English'}].map((v,i)=>` `).join('')}
Requiere HeyGen API configurada en el backoffice para sintesis de avatar
Vista previa
Elige avatar
Tu empresa · Ahora
Ver mas ▾
1.2k
48
89
9:16 · Reel / Story / TikTok
Plataformas ideales
${[ {ic:'ti-brand-instagram',label:'Instagram Reels',color:'#E1306C',note:'Hasta 90s'}, {ic:'ti-brand-tiktok',label:'TikTok',color:'#010101',note:'Hasta 60s'}, {ic:'ti-brand-youtube',label:'YouTube Shorts',color:'#FF0000',note:'Hasta 60s'}, {ic:'ti-brand-facebook',label:'Facebook Reels',color:'#1877F2',note:'Hasta 60s'}, ].map(p=>`
${p.label}
${p.note}
`).join('')}
La superposicion de texto funciona sin APIs externas. La sintesis de avatar requiere HeyGen configurado.
`; } // Helpers de seleccion para vVideos window.selectVidFmt = (id, btn) => { document.getElementById('vid-fmt').value = id; document.querySelectorAll('#vid-fmt-grid button').forEach(b => { const a = b.dataset.fmt === id; b.style.border = a ? '1.5px solid var(--indigo)' : '1.5px solid var(--color-border-tertiary)'; b.style.background = a ? 'var(--indigo-light)' : 'var(--color-background-primary)'; b.querySelector('i').style.color = a ? 'var(--indigo)' : 'var(--color-text-secondary)'; }); }; window.selectVidDur = (id, btn) => { document.getElementById('vid-dur').value = id; document.querySelectorAll('#vid-dur-grid button').forEach(b => { const a = b.dataset.dur === id; b.style.border = a ? '1.5px solid var(--indigo)' : '1.5px solid var(--color-border-tertiary)'; b.style.background = a ? 'var(--indigo-light)' : 'var(--color-background-primary)'; }); }; window.selectVidMusic = (id, btn) => { document.getElementById('vid-music').value = id; document.querySelectorAll('#vid-music-grid button').forEach(b => { const a = b.dataset.music === id; b.style.border = a ? '1.5px solid var(--indigo)' : '1.5px solid var(--color-border-tertiary)'; b.style.background = a ? 'var(--indigo-light)' : 'var(--color-background-primary)'; }); }; window.selectVidVoice = (id, btn) => { document.getElementById('vid-voice').value = id; document.querySelectorAll('#vid-voice-grid button').forEach(b => { const a = b.dataset.voice === id; b.style.border = a ? '1.5px solid var(--indigo)' : '1.5px solid var(--color-border-tertiary)'; b.style.background = a ? 'var(--indigo-light)' : 'var(--color-background-primary)'; }); }; window.selAvatar = (el, url) => { document.querySelectorAll('.avatar-pick').forEach(a=>a.classList.remove('selected')); el.classList.add('selected'); window._selectedAvatar = url; updateAvatarPreview(); }; window.updateAvatarPreview = () => { const txt = document.getElementById('av-text')?.value || ''; const size = document.getElementById('av-fontsize')?.value || 5; const cc = document.getElementById('av-charcount'); if (cc) cc.textContent = txt.length; const img = document.getElementById('av-preview-img'); if (window._selectedAvatar && img) { img.innerHTML = ``; img.className = ''; } const t = document.getElementById('av-preview-text'); if (t) { t.textContent = txt; t.style.fontSize = (size*2.2)+'px'; } }; window.genVideo = () => alert('Configura una API de avatares (HeyGen) en el backoffice para generar el video. La superposicion de texto e imagen ya funciona en la vista previa.'); window.selAv = el => { document.querySelectorAll('.avatar-card').forEach(c=>c.classList.remove('selected')); el.classList.add('selected'); }; window.genVScript = async () => { const btn = document.getElementById('vid-script-btn'); if(!btn) return; btn.disabled = true; btn.innerHTML = ' Generando guion...'; try { const r = await post(`${API}/companies/${cid()}/ai/generate`, { topic: 'Guion de video corto de 30 segundos presentando nuestra empresa y sus servicios, con gancho inicial y llamada a la accion', platform: 'tiktok', tone: 'informal', language: 'es', variants: 1 }); const v = r?.variants || []; if (v.length) { const ta = document.getElementById('video-script'); if(ta) ta.value = v[0].content; } } catch(e) { alert('Error: ' + e.message); } finally { btn.disabled = false; btn.innerHTML = 'Generar guion con IA'; } }; window.genVideo = () => alert('Para generar videos con avatar necesitas configurar la API key de HeyGen en el backoffice admin (admin.socialflowai.cloud).'); // ── BIBLIOTECA ──────────────────────────────────────────────── async function vLibrary() { const id = cid(); const d = await get(`${API}/companies/${id}/media`).catch(() => ({ media: [] })); const media = d?.media || []; const imgs = media.filter(m=>m.media_type!=='video'); const vids = media.filter(m=>m.media_type==='video'); const totalSize = media.reduce((s,m)=>s+(m.file_size||0),0); const fmtSize = b => b>1048576?(b/1048576).toFixed(1)+' MB':b>1024?(b/1024).toFixed(0)+' KB':b+' B'; return ` ${pageHero('ti-photo','Biblioteca de medios','Almacena y gestiona tus archivos multimedia en la nube', `
`)}
Almacenamiento en la nube
Sube imagenes y videos una vez y usalos en publicaciones o como avatar. Formatos: JPG, PNG, GIF, MP4 hasta 50 MB. Los archivos se guardan en tu cuenta privada.
Total archivos
${media.length}
Imagenes
${imgs.length}
Videos
${vids.length}
Espacio usado
${totalSize>0?fmtSize(totalSize):'—'}
${!media.length?`
Arrastra tus archivos aqui
o haz clic para seleccionar desde tu dispositivo
${['JPG','PNG','GIF','WebP','MP4','WebM'].map(f=>`${f}`).join('')}
Max 50 MB por archivo · Guardado en tu nube privada
`:''} ${media.length?`
`:''}
${media.map(m=>{ const isVideo=m.media_type==='video'; const name=(m.filename||m.url?.split('/').pop()||'archivo'); const size=m.file_size?fmtSize(m.file_size):''; const url=m.url||m.storage_url||''; return `
${isVideo?`
`:``}
${name.length>22?name.slice(0,20)+'..':name}
${isVideo?'Video':'Imagen'}${size}
`; }).join('')}
Subir archivo
JPG · PNG · MP4
hasta 50 MB
`; } window.upLib = inp => { if(inp.files.length) upLibFiles(inp.files); }; window.upLibFiles = async files => { const prog=document.getElementById('lib-upload-progress'); if(prog){prog.style.display='block';prog.innerHTML=`
Subiendo ${files.length} archivo${files.length>1?'s':''}...
`;} try{ const id=cid(); for(const f of Array.from(files)){ const fd=new FormData();fd.append('file',f); await fetch(`${API}/companies/${id}/media`,{method:'POST',headers:{Authorization:`Bearer ${gt()}`},body:fd}); } if(prog)prog.style.display='none'; showView('library'); }catch(e){if(prog)prog.innerHTML=`
${e.message||'Error al subir'}
`;} }; window.copyLibUrl = url => { navigator.clipboard?.writeText(url).then(()=>{const t=document.createElement('div');t.textContent='URL copiada ✓';t.style.cssText='position:fixed;bottom:24px;right:24px;background:var(--indigo);color:#fff;padding:10px 20px;border-radius:10px;font-size:13px;font-weight:600;z-index:9999';document.body.appendChild(t);setTimeout(()=>t.remove(),2000);}); }; window.delMedia = async (mid, url) => { if(!confirm('Eliminar este archivo? Esta accion no se puede deshacer.')) return; try{if(mid) await del(`${API}/companies/${cid()}/media/${mid}`).catch(()=>{});showView('library');}catch(e){alert(e.message);} }; window.libFilter = (type, btn) => { document.querySelectorAll('#lib-filter-btns button').forEach(b=>{b.style.border='1.5px solid var(--color-border-tertiary)';b.style.background='var(--color-background-primary)';}); btn.style.border='1.5px solid var(--indigo)';btn.style.background='var(--indigo-light)'; document.querySelectorAll('#lib-grid .lib-card').forEach(c=>{c.style.display=(type==='all'||c.dataset.type===type)?'':'none';}); }; window.setLibView = (mode, btn) => { document.querySelectorAll('[onclick*=setLibView]').forEach(b=>{b.style.border='0.5px solid var(--color-border-tertiary)';b.style.background='var(--color-background-primary)';}); btn.style.border='0.5px solid var(--indigo)';btn.style.background='var(--indigo-light)'; const grid=document.getElementById('lib-grid'); if(!grid) return; grid.style.gridTemplateColumns=mode==='list'?'1fr':'repeat(auto-fill,minmax(160px,1fr))'; }; // ── RECOMENDACIONES ─────────────────────────────────────────── async function vRecommendations() { const id = cid(); const d = await get(`${API}/companies/${id}/recommendations`).catch(() => []); let recos = Array.isArray(d) ? d : (d?.recommendations || []); if (!recos.length) { try { const gen = await post(`${API}/companies/${id}/ai/recommendations`, {}); recos = gen?.recommendations || []; } catch {} } const finalRecos = recos.length ? recos : [ { title:'Timing optimo de publicacion', description:'Tus publicaciones de jueves entre 6-8pm obtienen 3x mas engagement. Mueve los posts programados a este horario.', action_label:'Aplicar automaticamente', type:'timing' }, { title:'Formato de contenido sugerido', description:'Los Reels de 15-30s tienen 3.2x mas alcance que fotos estaticas para tu audiencia. Considera convertir 3 posts a video.', action_label:'Generar Reels', type:'format' }, { title:'Hashtags en tendencia', description:'#IA2025 y #Marketing estan trending +180% esta semana. Usarlos en tus proximas 5 publicaciones puede aumentar el alcance.', action_label:'Actualizar hashtags', type:'hashtags' }, { title:'Audiencia y segmentacion', description:'El 68% de tu audiencia tiene entre 25-34 anios. Ajusta el tono del contenido para captar mayor engagement en este segmento.', action_label:'Ver estrategia', type:'audience' }, { title:'Oportunidad de crecimiento', description:'TikTok tiene el mayor engagement rate (11.4%). Incrementar de 18 a 30 posts mensuales puede triplicar el alcance.', action_label:'Ver plan', type:'growth' }, ]; const iconMap = { timing:'ti-clock', format:'ti-photo', hashtags:'ti-hash', audience:'ti-target', growth:'ti-chart-line', content:'ti-bulb', general:'ti-bulb' }; const colorMap = { timing:'#2563EB', format:'#0EA5E9', hashtags:'#7C3AED', audience:'#DC2626', growth:'#16A34A', content:'#F59E0B', general:'#F59E0B' }; return ` ${pageHero('ti-bulb','Recomendaciones','Sugerencias inteligentes de IA para mejorar tus resultados')}
${finalRecos.slice(0,3).map(r => `
${r.title}

${r.description}

${r.impact_estimate?`
${r.impact_estimate}
`:''}
`).join('')}
${finalRecos.slice(3,5).map(r => `
${r.title}

${r.description}

`).join('')}
`; } window.applyReco = async (id, label) => { if (id) await post(`${API}/companies/${cid()}/recommendations/${id}/apply`, {}).catch(()=>{}); const l = label.toLowerCase(); if (l.includes('reel') || l.includes('video')) showView('videos'); else if (l.includes('hashtag')) showView('ai'); else if (l.includes('plan') || l.includes('estrategia')) showView('analytics'); else showView('create'); }; window.refreshRecos = async () => { const el = document.getElementById('view-content'); el.innerHTML = SP; try { await post(`${API}/companies/${cid()}/ai/recommendations`, {}); } catch {} el.innerHTML = await vRecommendations(); }; // ════════════════════════════════════════════════════════════ // BLOG SEO AUTOMATICO — calendario de contenido + plan (estilo AutoSEO) // ════════════════════════════════════════════════════════════ let blogCalRef = new Date(); async function vBlog() { const id = cid(); const [stats, articles, cfg] = await Promise.allSettled([ get(`${API}/companies/${id}/blog/stats`), get(`${API}/companies/${id}/blog/articles?limit=300`), get(`${API}/companies/${id}/blog/config`), ]); const s = stats.value || {}; const arts = articles.value?.articles || []; const config = cfg.value || {}; const hasContent = arts.length > 0; // Stat header (estilo AutoSEO) const blogConnected = !!(config.webhook_verified && config.webhook_url) || config.blog_type === 'wordpress'; const blogHeaderAction = blogConnected ? `
Blog conectado
` : ``; const statHeader = ` ${pageHero('ti-article','Blog SEO Automatico','Articulos optimizados que Google adora, publicados automaticamente en tu sitio', blogHeaderAction)}
Como funciona
La IA genera un plan en clusters tematicos, escribe cada articulo con SEO optimizado y lo publica automaticamente en tu sitio. Configuras una vez y el sistema trabaja solo.
Articulos generados
${arts.length}
Palabras escritas
${fmt(s.total_words||0)}
Ahorro vs redactor
$${fmt(s.savings_usd||0)}
Horas ahorradas
${fmt(s.time_saved_hours||0)}h
`; if (!hasContent) { // Estado vacío: invitar a generar el plan return statHeader + `
Blog SEO automatico con IA

La IA genera tu calendario editorial, escribe cada articulo y lo publica solo en tu sitio.

IA genera el plan
Clusters tematicos y keywords de alto potencial para tu nicho
Escribe cada articulo
~3000 palabras con H1/H2, meta SEO, FAQ y tabla de contenidos
Publica automatico
Envia a tu sitio via webhook o WordPress sin intervencion manual

Configura el autopost en Configuracion del blog antes de generar

`; } // Tabs del blog return statHeader + `
${renderBlogCalendar(arts)}
`; } window.blogTab = async (tab) => { ['calendar','list','analytics','config'].forEach(t => document.getElementById('bt-'+t)?.classList.toggle('active', t===tab)); const el = document.getElementById('blog-tab-content'); if (!el) return; el.innerHTML = SP; if (tab === 'analytics') { el.innerHTML = await renderBlogAnalytics(); return; } const arts = (await get(`${API}/companies/${cid()}/blog/articles?limit=300`).catch(()=>({})))?.articles || []; if (tab === 'calendar') el.innerHTML = renderBlogCalendar(arts); else if (tab === 'list') el.innerHTML = renderBlogList(arts); else if (tab === 'config') el.innerHTML = await renderBlogConfig(); }; async function renderBlogAnalytics() { const d = await get(`${API}/companies/${cid()}/blog/analytics`).catch(()=>({articles:[],total_views:0,total_clicks:0,tracking_active:false})); const arts = d.articles || []; const snip = await get(`${API}/companies/${cid()}/blog/tracking-snippet`).catch(()=>({snippet:''})); const notice = d.tracking_active ? `
Tracking activo: estos son datos reales reportados por tu sitio.
` : `
Aun no hay visitas registradas. Los articulos publicados ya incluyen el pixel de tracking automaticamente; los numeros apareceran cuando alguien visite tus articulos en tu sitio. Tambien puedes pegar este snippet en tu plantilla del blog para medir todas las paginas.
`; return ` ${notice}
Visitas totales
${fmt(d.total_views||0)}
Clicks totales
${fmt(d.total_clicks||0)}
Articulos publicados
${arts.length}
Historico de publicaciones
${arts.length ? `
${arts.map(a=>``).join('')}
ArticuloPublicadoVisitasClicks
${a.title}
${a.slug?`
/${a.slug}
`:''}
${a.published_at?fmtDT(a.published_at):'—'} ${fmt(a.views||0)} ${fmt(a.clicks||0)}
` : EMPTY('ti-chart-bar','Sin articulos publicados aun','Publica articulos para empezar a medir su trafico')}
${snip.snippet ? `
Snippet de tracking (opcional)

Los articulos autopublicados ya lo incluyen. Si quieres medir TODO tu blog, pega esto antes de </body> en tu plantilla:

${snip.snippet.replace(//g,'>')}
` : ''}`; } function renderBlogCalendar(arts) { const byDate = {}; arts.forEach(a => { if (a.scheduled_for) { const k = a.scheduled_for.slice(0,10); (byDate[k] = byDate[k]||[]).push(a); } }); const ref = blogCalRef; const year = ref.getFullYear(), month = ref.getMonth(); const first = new Date(year, month, 1); const startDay = (first.getDay()+6)%7; // lunes=0 const daysInMonth = new Date(year, month+1, 0).getDate(); const monthName = ref.toLocaleDateString('es-CR',{month:'long',year:'numeric'}); const today = new Date().toISOString().slice(0,10); let cells = ''; for (let i=0;i`; for (let d=1; d<=daysInMonth; d++) { const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`; const dayArts = byDate[dateStr]||[]; const isToday = dateStr===today; cells += `
${d}
${dayArts.map(a => { const st = a.status; const stColor = st==='published'?'#16A34A':st==='scheduled'?'#2563EB':st==='draft'?'#D97706':st==='generating'?'#7C3AED':st==='failed'?'#DC2626':'#94A3B8'; const stBg = st==='published'?'#F0FDF4':st==='scheduled'?'#EFF6FF':st==='draft'?'#FFFBEB':st==='generating'?'#F5F3FF':st==='failed'?'#FEF2F2':'#F8FAFC'; const stLabel = {published:'Publicado',scheduled:'Programado',draft:'Borrador',generating:'Generando',planned:'Planificado',failed:'Error'}[st]||st; return `
${st==='published'?'':st==='generating'?'':''} ${stLabel}
${a.title}
`; }).join('')}
`; } // Stats del mes visible para el blog calendar const monthArts = arts.filter(a => { if (!a.scheduled_for) return false; const d=new Date(a.scheduled_for); return d.getFullYear()===year && d.getMonth()===month; }); const mPub = monthArts.filter(a=>a.status==='published').length; const mSched = monthArts.filter(a=>a.status==='scheduled').length; const mPlan = monthArts.filter(a=>['planned','draft','generating'].includes(a.status)).length; return `
${mPub}
Publicados este mes
${mSched}
Programados
${mPlan}
Por generar
${monthName}
${[['Publicado','#16A34A'],['Programado','#2563EB'],['Borrador','#D97706'],['Planificado','#7C3AED'],['Error','#DC2626']].map(([l,c])=>`
${l}
`).join('')}
${['Lun','Mar','Mie','Jue','Vie','Sab','Dom'].map(d=>`
${d}
`).join('')}
${cells}
`; } window.blogCalMove = (dir) => { blogCalRef.setMonth(blogCalRef.getMonth()+dir); blogTab('calendar'); }; function renderBlogList(arts) { const order = {generating:0,planned:1,draft:2,scheduled:3,failed:4,published:5}; const sorted = [...arts].sort((a,b)=> (a.scheduled_for||'').localeCompare(b.scheduled_for||'')); // Stats para el header const stCounts = {published:0,scheduled:0,draft:0,generating:0,planned:0,failed:0}; arts.forEach(a => { if (stCounts[a.status]!==undefined) stCounts[a.status]++; }); return `
${[['Publicados',stCounts.published,'#16A34A','#F0FDF4'],['Programados',stCounts.scheduled,'#2563EB','#EFF6FF'],['Borradores',stCounts.draft,'#D97706','#FFFBEB'],['Generando',stCounts.generating,'#7C3AED','#F5F3FF'],['Planificados',stCounts.planned,'#64748B','#F8FAFC']].map(([l,n,c,bg])=>`
${n}
${l}
`).join('')}
${arts.length} articulos en el plan
${sorted.map(a => { const st = a.status; const cls = st==='published'?'green':st==='scheduled'?'blue':st==='draft'?'amber':st==='failed'?'red':'gray'; const lbl = {published:'Publicado',scheduled:'Programado',draft:'Borrador',generating:'Generando',planned:'Planificado',failed:'Error'}[st]||st; return ``; }).join('')}
ArticuloSearch termEstadoFecha
${a.title}
${a.cluster_name?`
${a.cluster_name}
`:''}
${a.search_term||'—'} ${bdg(lbl,cls)} ${fmtD(a.scheduled_for)} ${st==='planned'?``:''} ${(st==='draft'||st==='scheduled')?` `:''} ${st==='published'?`${a.live_url?` Ver live`:bdg('Live','green')}`:''} ${st==='failed'?``:''}
`; } async function renderBlogConfig() { const c = await get(`${API}/companies/${cid()}/blog/config`).catch(()=>({})); const tog = (k,label,desc) => `
${label}
${desc}
`; return `
Publicacion automatica
${tog('auto_publish','Auto-publicar articulos','Publica automaticamente en tu sitio sin revision manual')} ${tog('include_hero_image','Incluir imagen destacada','Genera una imagen IA al inicio de cada articulo')} ${tog('include_key_takeaways','Incluir puntos clave','Caja resumen con 4-5 bullets despues de la intro')} ${tog('include_toc','Tabla de contenidos','Indice clickeable con enlaces a cada seccion')} ${tog('include_faq','Preguntas frecuentes','Seccion FAQ al final de cada articulo')} ${tog('include_external_links','Enlaces externos','Agrega enlaces autoritativos (ej. Wikipedia)')}
Longitud del articulo
Frecuencia de publicacion
Autopost a tu sitio (Webhook)

SocialFlow enviara cada articulo a tu sitio mediante un POST firmado. Implementa un endpoint que reciba el JSON y publique el post.

${c.webhook_verified?` Verificado`:c.webhook_url?bdg('Sin verificar','amber'):''}
Formato del payload

POST { event, article:{ title, slug, html, meta_description, hero_image_url }, site }

Header: X-SocialFlow-Signature: sha256=... (HMAC con tu secret)

Call-to-Action e instrucciones
`; } window.openBlogConnect = async () => { const id = cid(); const cfg = await get(`${API}/companies/${id}/blog/config`).catch(()=>({})); const comp = await get(`${API}/companies/${id}`).catch(()=>({})); const verified = cfg.webhook_verified; const lastStatus = cfg.webhook_last_status; modal('Conectar el blog de tu sitio web', `

Conecta tu sitio para que los articulos generados con IA se publiquen automaticamente en tu blog. Funciona con WordPress, Webflow, Ghost o cualquier sitio que reciba un webhook.

Tu sitio recibira un POST con el articulo (titulo, contenido HTML, imagen, meta). Te damos un secret para validar la firma.
${verified?'Webhook verificado':'Sin verificar'} ${lastStatus&&lastStatus!=='ok'?`
Ultimo error: ${lastStatus}
`:''} ${cfg.webhook_secret_set?'
Secret configurado
':'
Aun no generas un secret
'}
`, '620px'); }; window.genBlogConnectSecret = async () => { try { const r = await post(`${API}/companies/${cid()}/blog/webhook/secret`, {}); const box = document.getElementById('blogc-secret'); if (box && r.secret) box.innerHTML = `
Guarda este secret (se muestra una sola vez):
${r.secret}
Usalo en tu sitio para validar la firma del webhook (header X-SocialFlow-Signature).
`; } catch(e) { setAlert('blogc-alert', e.message); } }; window.testBlogWebhook = async () => { // Guardar primero la URL para que el test la use const webhook_url = document.getElementById('blogc-webhook')?.value?.trim(); if (!webhook_url) { setAlert('blogc-alert','Ingresa primero la URL del webhook'); return; } try { await put(`${API}/companies/${cid()}/blog/config`, { webhook_url }); const r = await post(`${API}/companies/${cid()}/blog/webhook/test`, {}); if (r.success) setAlert('blogc-alert','✓ Conexion exitosa. Tu sitio respondio correctamente.', 'ok'); else setAlert('blogc-alert', 'La prueba fallo: ' + (r.error||'tu sitio no respondio correctamente')); } catch(e) { setAlert('blogc-alert', e.message); } }; window.saveBlogConnect = async () => { const website_url = document.getElementById('blogc-website')?.value?.trim(); const blog_type = document.getElementById('blogc-type')?.value; const webhook_url = document.getElementById('blogc-webhook')?.value?.trim(); try { await put(`${API}/companies/${cid()}`, { website_url, blog_type }); await put(`${API}/companies/${cid()}/blog/config`, { webhook_url }); closeModal(); setAlert('plan-alert','Blog conectado correctamente.', 'ok'); } catch(e) { setAlert('blogc-alert', e.message); } }; window.generatePlan = async () => { const btn = document.getElementById('gen-plan-btn'); const count = document.getElementById('plan-count')?.value || 30; if (btn) { btn.disabled = true; btn.innerHTML = ' Generando plan...'; } setAlert('plan-alert','La IA esta creando tu calendario de contenido. Esto puede tomar unos segundos...','info'); try { const r = await post(`${API}/companies/${cid()}/blog/plan`, { count: parseInt(count) }); setAlert('plan-alert',`Plan creado: ${r.articles_planned} articulos en ${r.clusters} clusters tematicos`,'ok'); setTimeout(()=>showView('blog'), 1200); } catch(e) { setAlert('plan-alert', e.message, 'err'); if (btn) { btn.disabled = false; btn.innerHTML = 'Generar plan con IA'; } } }; window.clearPlan = async () => { if (!confirm('Esto eliminara todos los articulos no publicados del plan. Continuar?')) return; await del(`${API}/companies/${cid()}/blog/plan`).catch(e=>alert(e.message)); showView('blog'); }; window.genArticle = async (id) => { const r = await get(`${API}/companies/${cid()}/blog/articles/${id}`).catch(()=>({})); modal('Generando articulo', `
${r.title||'Articulo'}

La IA esta escribiendo ~${r.word_count||3000} palabras optimizadas para SEO. Espera un momento...

`); try { await post(`${API}/companies/${cid()}/blog/articles/${id}/generate`, {}); closeModal(); blogTab('list'); } catch(e) { closeModal(); alert('Error: '+e.message); } }; window.publishArticle = async (id) => { if (!confirm('Publicar este articulo en tu sitio ahora?')) return; try { const r = await post(`${API}/companies/${cid()}/blog/articles/${id}/publish`, {}); alert(r.live_url ? 'Publicado: '+r.live_url : (r.message||'Articulo publicado')); blogTab('list'); } catch(e) { alert('Error: '+e.message); } }; window.openArticle = async (id) => { const a = await get(`${API}/companies/${cid()}/blog/articles/${id}`).catch(()=>null); if (!a) return; const stLabel = {published:'Publicado',scheduled:'Programado',draft:'Borrador',generating:'Generando',planned:'Planificado',failed:'Error'}[a.status]||a.status; modal(a.title, `
${bdg(stLabel, a.status==='published'?'green':a.status==='draft'?'amber':'blue')} ${a.word_count?bdg(a.word_count+' palabras','gray'):''} ${a.search_term?bdg('SEO: '+a.search_term,'blue'):''}
${a.error_message?`
${a.error_message}
`:''} ${a.html ? `
${a.html}
` : `
Sin contenido aun

Este articulo todavia no ha sido generado

`}
${a.status==='planned'?``:''} ${(a.status==='draft'||a.status==='scheduled')?``:''} ${a.live_url?` Ver en vivo`:''}
`, '720px'); }; window.saveBlogConfig = async (key, value) => { try { await put(`${API}/companies/${cid()}/blog/config`, { [key]: value }); setAlert('bc-alert','Guardado','ok'); setTimeout(()=>setAlert('bc-alert','','ok'),1500); } catch(e) { setAlert('bc-alert', e.message, 'err'); } }; window.genWebhookSecret = async () => { try { const r = await post(`${API}/companies/${cid()}/blog/webhook/secret`, {}); document.getElementById('wh-secret').innerHTML = `
Secret (guardalo, solo se muestra una vez):
${r.secret}
`; } catch(e) { setAlert('wh-alert', e.message, 'err'); } }; window.testWebhookBtn = async () => { setAlert('wh-alert','Enviando ping de prueba...','info'); try { const r = await post(`${API}/companies/${cid()}/blog/webhook/test`, {}); setAlert('wh-alert', r.success ? `Conexion exitosa (HTTP ${r.status})` : `Fallo: ${r.error||'HTTP '+r.status}`, r.success?'ok':'err'); } catch(e) { setAlert('wh-alert', e.message, 'err'); } }; // ════════════════════════════════════════════════════════════ // BACKLINKS — comparacion vs competidores (estilo AutoSEO) // ════════════════════════════════════════════════════════════ async function vBacklinks() { const id = cid(); const d = await get(`${API}/companies/${id}/blog/competitors`).catch(()=>({competitors:[], moz_configured:false})); const comps = d?.competitors || []; const mozOn = !!d?.moz_configured; const myDomains = d?.my_referring_domains; // null si no hay dato real const myDA = d?.my_domain_authority; const sorted = [...comps].sort((a,b)=>(b.referring_domains||0)-(a.referring_domains||0)); const withData = sorted.filter(c=>(c.referring_domains||0)>0); const median = withData.length ? withData[Math.floor(withData.length/2)]?.referring_domains || 0 : 0; const gap = (myDomains>0 && median>0) ? (median/myDomains).toFixed(1)+'x' : '—'; // Datos para grafico comparativo (mi sitio + competidores con dato real) const chartRows = [ ...(myDomains!=null ? [{ domain:(d.website||'Tu sitio').replace(/^https?:\/\/(www\.)?/,''), rd:myDomains, da:myDA||0, mine:true }] : []), ...withData.map(c=>({ domain:c.domain, rd:c.referring_domains||0, da:c.domain_authority||0, mine:false })), ].sort((a,b)=>b.rd-a.rd); const maxRd = Math.max(...chartRows.map(r=>r.rd), 1); return ` ${pageHero('ti-link','Analisis de Backlinks','Compara tu autoridad de dominio con la competencia para decidir tu estrategia SEO', `
`)}
Estrategia de backlinks
Mas dominios de referencia = mas autoridad para Google. Compara tu sitio con competidores e identifica la brecha. Conecta Moz API para ver datos reales en tiempo real.
${!mozOn ? `
Conecta datos reales de backlinks. Para ver dominios de referencia y autoridad reales (tuyos y de competidores), agrega tu Moz Access ID y Secret Key en el backoffice (Config -> API Keys -> Moz). Sin eso, no mostramos numeros porque no serian reales. Obtener claves de Moz
` : ''}
Tus dominios de referencia
${myDomains!=null?fmt(myDomains):'—'}
Tu autoridad de dominio
${myDA!=null?myDA:'—'}
Brecha vs mediana
${gap}
Comparativa de dominios de referencia
${chartRows.length ? `
${chartRows.map(r=>`
${r.mine?' ':''}${r.domain} ${fmt(r.rd)} dominios · DA ${r.da}
`).join('')}
` : EMPTY('ti-chart-bar', mozOn?'Agrega competidores':'Sin datos reales aun', mozOn?'Agrega dominios de competidores para comparar':'Conecta Moz y agrega competidores para ver la comparativa')}
Competidores
${sorted.length ? sorted.map((c,i)=>``).join('') : ``}
#DominioDominios ref.Autoridad
${i+1} ${c.domain}${c.description?`
${c.description}
`:''}
${(c.referring_domains||0)>0?fmt(c.referring_domains):''} ${(c.domain_authority||0)>0?c.domain_authority:'—'}
${EMPTY('ti-link','Sin competidores','Agrega dominios de competidores para analizar')}
Como tomar decisiones con esto

${myDomains!=null ? `Tu sitio tiene ${fmt(myDomains)} dominios enlazando y autoridad ${myDA||0}. ${median>0?`La mediana de tus competidores es ${fmt(median)}.`:''} Google usa estos enlaces como "votos" de confianza. Prioriza pocos enlaces relevantes y de alta autoridad sobre muchos irrelevantes; el blog SEO automatico te ayuda a atraerlos de forma organica.` : `Conecta Moz para ver tu perfil real de backlinks y el de tus competidores, y asi decidir donde enfocar tu estrategia de enlaces.`}

`; } window.refreshBacklinks = async () => { const btn = event?.target?.closest('button'); if (btn) { btn.disabled = true; btn.innerHTML = ' Actualizando...'; } try { await post(`${API}/companies/${cid()}/blog/competitors/refresh`, {}); showView('backlinks'); } catch(e){ alert(e.message); if(btn){btn.disabled=false; btn.innerHTML=' Actualizar datos';} } }; window.addCompetitor = () => modal('Agregar competidor', `

Si tienes Moz conectado, traemos los dominios de referencia y la autoridad automaticamente al agregar el dominio.

`); window.doAddCompetitor = async () => { const domain = document.getElementById('comp-domain')?.value?.trim(); if (!domain) { setAlert('comp-alert','El dominio es requerido'); return; } try { await post(`${API}/companies/${cid()}/blog/competitors`, { domain, description: document.getElementById('comp-desc')?.value, }); closeModal(); showView('backlinks'); } catch(e) { setAlert('comp-alert', e.message); } }; window.delCompetitor = async (id) => { if (!confirm('Eliminar competidor?')) return; await del(`${API}/companies/${cid()}/blog/competitors/${id}`).catch(e=>alert(e.message)); showView('backlinks'); }; // ════════════════════════════════════════════════════════════ // MAPA DE TEMAS — topic clusters mind map (estilo AutoSEO) // ════════════════════════════════════════════════════════════ async function vTopics() { const id = cid(); const d = await get(`${API}/companies/${id}/blog/clusters`).catch(()=>({clusters:[]})); const clusters = d?.clusters || []; if (!clusters.length) { return `
${EMPTY('ti-sitemap','Sin mapa de temas aun','Genera un plan de contenido en Blog SEO para visualizar tus clusters tematicos', ``)}
`; } const W = 1000, H = 700, cx = W/2, cy = H/2; const brandName = company?.name || 'Mi Marca'; let svg = ``; // Lineas const n = clusters.length; const clusterPositions = clusters.map((c,i)=>{ const angle = (i/n)*2*Math.PI - Math.PI/2; return { x: cx + Math.cos(angle)*230, y: cy + Math.sin(angle)*210, color: c.color||'#2563EB', cluster: c, angle }; }); // dibujar lineas cluster->centro y articulos->cluster clusterPositions.forEach(p => { svg += ``; const arts = (p.cluster.articles||[]).slice(0,4); arts.forEach((a,j)=>{ const spread = (j-(arts.length-1)/2)*0.5; const aa = p.angle + spread; const ax = p.x + Math.cos(aa)*150, ay = p.y + Math.sin(aa)*110; svg += ``; const w=130,h=40; svg += `
${(a.title||'').slice(0,48)}
`; }); }); // nodos cluster clusterPositions.forEach(p=>{ const w=170,h=52; svg += `
${p.cluster.name}
`; }); // centro svg += ``; svg += `
${brandName}
`; svg += `
`; const totalArts = clusters.reduce((s,c)=>s+(c.articles?.length||0),0); return ` ${pageHero('ti-sitemap','Mapa de Temas','Visualiza tus clusters tematicos de contenido SEO', `
`)}
Mapa interactivo
Cada nodo es un cluster tematico. Usa el scroll para hacer zoom y arrastra para explorar el mapa completo.
Clusters tematicos
${clusters.length}
Articulos planificados
${totalArts}
Promedio por cluster
${clusters.length?Math.round(totalArts/clusters.length):0}
${clusters.length} clusters · ${totalArts} articulos
${svg}
Scroll para zoom · Arrastra para mover el mapa
${clusters.map(c=>`
${c.name}
${c.articles?.length||0} articulos
${(c.articles||[]).slice(0,4).map(a=>`
${a.title}
`).join('')} ${(c.articles||[]).length>4?`
+${(c.articles||[]).length-4} mas
`:''}
`).join('')}
`; } (function(){ let scale=1,tx=0,ty=0,dragging=false,sx=0,sy=0; function els(){ return [document.getElementById('topic-map-wrap'),document.getElementById('topic-map-inner')]; } function applyT(){ const [,inner]=els(); if(inner) inner.style.transform=`scale(${scale}) translate(${tx}px,${ty}px)`; } window.topicZoom = f => { scale=Math.max(0.3,Math.min(4,scale*f)); applyT(); }; window.topicZoomReset = () => { scale=1;tx=0;ty=0;applyT(); }; document.addEventListener('wheel', e => { const [w]=els(); if(!w||!w.contains(e.target)) return; e.preventDefault(); topicZoom(e.deltaY<0?1.12:0.9); },{passive:false}); document.addEventListener('mousedown', e => { const [w]=els(); if(!w||!w.contains(e.target)) return; dragging=true;sx=e.clientX-tx*scale;sy=e.clientY-ty*scale;w.style.cursor='grabbing'; }); document.addEventListener('mousemove', e => { if(!dragging) return; tx=(e.clientX-sx)/scale;ty=(e.clientY-sy)/scale;applyT(); }); document.addEventListener('mouseup', () => { dragging=false; const [w]=els(); if(w) w.style.cursor='grab'; }); })(); // ════════════════════════════════════════════════════════════ // APROBACIONES — bandeja de revisión + colaboración // ════════════════════════════════════════════════════════════ async function vApprovals() { const id = cid(); const d = await get(`${API}/companies/${id}/approvals`).catch(() => ({ posts: [] })); const posts = d?.posts || []; if (!posts.length) { return ` ${pageHero('ti-checklist','Aprobaciones','Revisa y aprueba el contenido de tu equipo', ``)}
${EMPTY('ti-checklist','Todo al día','No hay publicaciones pendientes de aprobación. Cuando un miembro del equipo cree contenido que requiera revisión, aparecerá aquí.')}
`; } const nUrgent = posts.filter(p=>{ const h=(Date.now()-new Date(p.created_at||0).getTime())/3600000; return h>24; }).length; const authors = [...new Set(posts.map(p=>p.author_name).filter(Boolean))].length; return ` ${pageHero('ti-checklist','Aprobaciones','Revisa y aprueba el contenido de tu equipo', ``)}
Flujo de aprobacion
Aprobar publica el post segun su fecha programada. Solicitar cambios lo devuelve a borrador y avisa al autor. Puedes comentar en el hilo antes de decidir.
${posts.length}
Pendiente${posts.length!==1?'s':''} de revision
${nUrgent}
Esperando mas de 24h
${authors}
Autor${authors!==1?'es':''}
${posts.map(p => { const plats = (p.platforms||'').split(',').filter(Boolean); const hoursWaiting = Math.round((Date.now()-new Date(p.created_at||0).getTime())/3600000); const urgentTag = hoursWaiting>24 ? ` ${hoursWaiting}h esperando` : ` hace ${hoursWaiting}h`; return `
${(p.author_name||'?').slice(0,2).toUpperCase()}
${p.author_name||'Equipo'}
${p.scheduled_at?`Programado: ${fmtD(p.scheduled_at)}`:'Sin fecha de publicacion'}
${plats.map(pl=>`
`).join('')}
${urgentTag} ${p.comment_count>0?` ${p.comment_count}`:''}
${p.title?`
${p.title}
`:''}
${(p.content_master||'').slice(0,400)}${(p.content_master||'').length>400?'…':''}
`; }).join('')}
`; } window.toggleComments = async (postId) => { const el = document.getElementById('comments-'+postId); if (!el) return; if (el.style.display === 'block') { el.style.display='none'; return; } el.style.display = 'block'; el.innerHTML = SP; const d = await get(`${API}/companies/${cid()}/posts/${postId}/comments`).catch(()=>({comments:[]})); const comments = d?.comments || []; el.innerHTML = `
${comments.length ? comments.map(c=>`
${(c.author_name||'?').slice(0,2).toUpperCase()}
${c.author_name} ${c.type!=='comment'?`· ${({approval:'aprobó',rejection:'rechazó',change_request:'solicitó cambios'})[c.type]||''}`:''} · ${fmtD(c.created_at)}
${c.body}
`).join('') : '
Sin comentarios aún
'}
`; }; window.addComment = async (postId) => { const input = document.getElementById('cmt-input-'+postId); const body = input?.value?.trim(); if (!body) return; await post(`${API}/companies/${cid()}/posts/${postId}/comments`, { body, type: 'comment' }).catch(e=>alert(e.message)); toggleComments(postId); toggleComments(postId); }; window.approvePost = async (postId) => { await post(`${API}/companies/${cid()}/posts/${postId}/approve`, {}).catch(e=>alert(e.message)); showView('approvals'); }; window.rejectPost = async (postId) => { const reason = prompt('Motivo del rechazo (opcional):'); if (reason === null) return; await post(`${API}/companies/${cid()}/posts/${postId}/reject`, { reason }).catch(e=>alert(e.message)); showView('approvals'); }; window.requestChanges = async (postId) => { const body = prompt('¿Qué cambios necesita esta publicación?'); if (!body?.trim()) return; await post(`${API}/companies/${cid()}/posts/${postId}/comments`, { body, type: 'change_request' }).catch(e=>alert(e.message)); showView('approvals'); }; // ════════════════════════════════════════════════════════════ // BANDEJA DE ENTRADA — inbox unificado de comentarios/DMs // ════════════════════════════════════════════════════════════ let inboxFilter = { status: '', platform: '', kind: '' }; async function vInbox() { const id = cid(); const qs = new URLSearchParams(Object.entries(inboxFilter).filter(([,v])=>v)).toString(); const d = await get(`${API}/companies/${id}/inbox${qs?'?'+qs:''}`).catch(() => ({ items: [], unread: 0 })); const items = d?.items || []; const filterBtn = (label, key, val) => ``; const nComments = items.filter(i=>i.kind==='comment').length; const nDMs = items.filter(i=>i.kind==='dm').length; const nPositive = items.filter(i=>i.sentiment==='positive').length; return ` ${pageHero('ti-inbox','Bandeja de entrada','Comentarios y mensajes de todas tus redes', ``)}
Bandeja unificada
Comentarios, menciones y mensajes de todas tus redes en un solo lugar. Usa Sugerir con IA para responder rapido. Los puntos de color indican el sentimiento del mensaje.
${items.length}
Total
${d.unread||0}
Sin leer
${nComments}
Comentarios
${nPositive}
Positivos
${filterBtn('Todo','status','')} ${filterBtn('Sin leer','status','unread')} ${filterBtn('Respondidos','status','replied')} ${filterBtn('Comentarios','kind','comment')} ${filterBtn('Mensajes','kind','dm')}
${d.unread>0?` ${d.unread} sin leer`:''}
${items.length ? `
${items.map(it=>`
${it.author_avatar?``:``}
${it.author_name||it.author_handle||'Usuario'} ${it.kind==='dm'?'Mensaje directo':it.kind==='mention'?'Mención':it.kind==='review'?'Reseña':'Comentario'} · ${fmtD(it.received_at)} ${it.sentiment?``:''}
${it.message||''}
${it.reply_text?`
Tu respuesta: ${it.reply_text}
`:''}
${it.status!=='replied'?``:''} ${it.status==='unread'?``:''} ${it.permalink?``:''}
`).join('')}
` : `
${EMPTY('ti-inbox','Bandeja vacía','Aquí aparecerán los comentarios, menciones y mensajes directos de tus redes sociales conectadas. Conecta tus redes y sincroniza para empezar a recibir interacciones en un solo lugar.')}
`}`; } window.setInboxFilter = (key, val) => { if (key==='status') { inboxFilter.status = val; inboxFilter.kind = ''; } else { inboxFilter.kind = val; inboxFilter.status = ''; } showView('inbox'); }; window.markInbox = async (itemId, status) => { await put(`${API}/companies/${cid()}/inbox/${itemId}`, { status }).catch(e=>alert(e.message)); showView('inbox'); }; window.replyInbox = (itemId, author) => { modal(`Responder a ${author}`, `
`); }; window.suggestReply = async (itemId) => { const ta = document.getElementById('reply-text'); if (ta) ta.value = 'Generando...'; try { const r = await post(`${API}/companies/${cid()}/inbox/${itemId}/suggest`, {}); if (ta) ta.value = r.suggestion || ''; } catch(e) { if (ta) ta.value = ''; setAlert('reply-alert', e.message); } }; window.doReply = async (itemId) => { const text = document.getElementById('reply-text')?.value?.trim(); if (!text) { setAlert('reply-alert','Escribe una respuesta'); return; } try { const r = await post(`${API}/companies/${cid()}/inbox/${itemId}/reply`, { text }); closeModal(); if (r.note) alert(r.note); showView('inbox'); } catch(e) { setAlert('reply-alert', e.message); } }; // ════════════════════════════════════════════════════════════ // PLANTILLAS — biblioteca de contenido reutilizable // ════════════════════════════════════════════════════════════ let tplCategory = 'all'; async function vTemplates() { const id = cid(); const d = await get(`${API}/companies/${id}/templates${tplCategory!=='all'?'?category='+tplCategory:''}`).catch(() => ({ templates: [] })); const templates = d?.templates || []; const CATS = [['all','Todas'],['general','General'],['promo','Promoción'],['educativo','Educativo'],['engagement','Engagement'],['festivo','Festivo']]; return ` ${pageHero('ti-template','Plantillas','Contenido reutilizable para publicar mas rapido', ``)}
Ahorra tiempo
Guarda formatos que funcionan una sola vez y usalos en segundos. Clic en Usar lleva el texto directo al editor. Organiza por categoria para encontrarlos rapido.
${CATS.map(([k,l])=>``).join('')}
${templates.length} plantilla${templates.length!==1?'s':''}
${templates.length ? `
${templates.map(t=>{ let hashtags=[]; try{hashtags=JSON.parse(t.hashtags||'[]')}catch{} return `
${t.category||'general'} ${t.use_count>0?` Usada ${t.use_count}×`:'Nueva'}
${t.name}
${(t.body||'').slice(0,140)}${(t.body||'').length>140?'…':''}
${hashtags.length?`
${hashtags.slice(0,3).map(h=>'#'+h.replace(/^#/,'')).join(' ')}
`:''}
${!t.is_shared?``:'Compartida'}
`; }).join('')}
` : `
${EMPTY('ti-template','Sin plantillas','Crea plantillas reutilizables para no escribir el mismo contenido una y otra vez. Ideal para promociones recurrentes, saludos festivos o formatos que funcionan.', ``)}
`}`; } window.setTplCat = (c) => { tplCategory = c; showView('templates'); }; window.newTemplate = (tpl) => { modal(tpl?'Editar plantilla':'Nueva plantilla', `
`); }; window.editTemplate = async (id) => { const d = await get(`${API}/companies/${cid()}/templates`).catch(()=>({templates:[]})); const tpl = (d.templates||[]).find(t=>t.id===id); if (tpl) newTemplate(tpl); }; window.saveTemplate = async (id) => { const name = document.getElementById('tpl-name')?.value?.trim(); const body = document.getElementById('tpl-body')?.value?.trim(); if (!name || !body) { setAlert('tpl-alert','Nombre y contenido son requeridos'); return; } const hashtags = (document.getElementById('tpl-tags')?.value||'').split(',').map(s=>s.trim().replace(/^#/,'')).filter(Boolean); const payload = { name, body, category: document.getElementById('tpl-cat')?.value, hashtags }; try { if (id) await put(`${API}/companies/${cid()}/templates/${id}`, payload); else await post(`${API}/companies/${cid()}/templates`, payload); closeModal(); showView('templates'); } catch(e) { setAlert('tpl-alert', e.message); } }; window.useTemplate = async (id) => { const d = await get(`${API}/companies/${cid()}/templates`).catch(()=>({templates:[]})); const tpl = (d.templates||[]).find(t=>t.id===id); if (!tpl) return; await post(`${API}/companies/${cid()}/templates/${id}/use`, {}).catch(()=>{}); // Llevar el contenido a Crear publicación sessionStorage.setItem('sf_template', JSON.stringify(tpl)); showView('create'); }; window.delTemplate = async (id) => { if (!confirm('¿Eliminar esta plantilla?')) return; await del(`${API}/companies/${cid()}/templates/${id}`).catch(e=>alert(e.message)); showView('templates'); }; // ════════════════════════════════════════════════════════════ // REPORTES PDF — white-label // ════════════════════════════════════════════════════════════ async function vReports() { const id = cid(); const apiBase = API.replace('/api/v1',''); const today = new Date().toISOString().slice(0,10); const monthAgo = new Date(Date.now()-30*86400000).toISOString().slice(0,10); // Periodos predefinidos const last7 = new Date(Date.now()-7*86400000).toISOString().slice(0,10); const last90 = new Date(Date.now()-90*86400000).toISOString().slice(0,10); return ` ${pageHero('ti-file-text','Reportes PDF','Reportes profesionales white-label para tus clientes', ``)}
Reportes white-label
Genera reportes profesionales con tu logo y colores para entregar a clientes. Se abren en una nueva pestana — usa Cmd/Ctrl+P → Guardar como PDF para descargar.
1
Periodo del reporte
${[ {label:'Ultimos 7 dias', start:last7, end:today}, {label:'Ultimos 30 dias', start:monthAgo, end:today}, {label:'Ultimos 90 dias', start:last90, end:today}, ].map((p,i)=>` `).join('')}
2
Marca de tu agencia (white-label)
${['#5B4FE0','#2563EB','#16A34A','#DC2626','#D97706','#0EA5E9'].map(c=>`
`).join('')}
3
Secciones del reporte
${[ ['rep-sec-overview','Resumen ejecutivo','Alcance, engagement e impresiones del periodo',true], ['rep-sec-posts','Top publicaciones','Las 10 publicaciones con mejor desempeno',true], ['rep-sec-growth','Crecimiento de seguidores','Evolucion de seguidores por red',true], ['rep-sec-audience','Audiencia','Datos demograficos y geograficos',false], ['rep-sec-blog','Blog SEO','Visitas y posicionamiento de articulos',false], ['rep-sec-reco','Recomendaciones IA','Sugerencias automaticas para el proximo periodo',true], ].map(([id,label,desc,checked])=>`
${label}
${desc}
`).join('')}
Tu Agencia
Reporte de Desempeno
Vista previa del reporte
${[['Alcance total','—','var(--indigo)'],['Publicaciones','—','var(--blue)'],['Engagement','—','var(--green)']].map(([l,v,c])=>`
${l} ${v}
`).join('')}
Incluye recomendaciones IA
Como descargar el PDF
${[['1','Haz clic en Generar reporte','Se abre en nueva pestana'],['2','Presiona Cmd+P (Mac) o Ctrl+P (Windows)','Abre el dialogo de impresion'],['3','Destino: Guardar como PDF','Selecciona en la lista de impresoras'],['4','Haz clic en Guardar','PDF listo para enviar al cliente']].map(([n,t,d])=>`
${n}
${t}
${d}
`).join('')}
`; } window.setRepPeriod = (start, end, btn) => { if (start) { document.getElementById('rep-start').value = start; document.getElementById('rep-end').value = end; } document.querySelectorAll('#rep-preset-grid button').forEach(b => { b.style.border = b === btn ? '1.5px solid var(--indigo)' : '1.5px solid var(--color-border-tertiary)'; b.style.background = b === btn ? 'var(--indigo-light)' : 'var(--color-background-primary)'; }); }; window.updateRepPreview = () => { const color = document.getElementById('rep-color')?.value || '#2563EB'; const name = document.getElementById('rep-agency')?.value || 'Tu Agencia'; const hdr = document.getElementById('rep-preview-header'); const nm = document.getElementById('rep-preview-name'); if (hdr) hdr.style.background = color; if (nm) nm.textContent = name; }; window.genReport = () => { const start = document.getElementById('rep-start')?.value; const end = document.getElementById('rep-end')?.value; const tk = localStorage.getItem('sf_token') || sessionStorage.getItem('sf_token') || ''; const url = `${API}/companies/${cid()}/report?start=${start}&end=${end}`; // Abrir con auth via fetch y blob (porque requiere token) fetch(url, { headers: { Authorization: 'Bearer ' + tk } }) .then(r => r.text()) .then(html => { const w = window.open('', '_blank'); w.document.write(html); w.document.close(); }) .catch(e => alert('Error generando reporte: ' + e.message)); }; window.saveReportBranding = async () => { const payload = { report_agency_name: document.getElementById('rep-agency')?.value, report_logo_url: document.getElementById('rep-logo')?.value, report_accent: document.getElementById('rep-color')?.value, }; try { await put(`${API}/companies/${cid()}`, payload); alert('Marca guardada'); } catch(e) { alert(e.message); } }; // ════════════════════════════════════════════════════════════ // AUDIENCIA — mapa por region + heatmap de actividad // ════════════════════════════════════════════════════════════ let audPlatform = 'all'; async function vAudience() { const id = cid(); const [mapD, heatD] = await Promise.allSettled([ get(`${API}/companies/${id}/analytics/audience-map${audPlatform!=='all'?'?platform='+audPlatform:''}`), get(`${API}/companies/${id}/analytics/activity-heatmap`), ]); const map = mapD.value || { regions:[], has_data:false }; const heat = heatD.value || { grid:[], has_data:false }; return ` ${pageHero('ti-map-2','Audiencia','Conoce de donde es tu audiencia y cuando esta mas activa', ``)}
Datos reales de tu audiencia
El mapa muestra de donde son tus seguidores por pais. El heatmap revela los dias y horas con mas engagement — usa esos datos para programar tus publicaciones.
Total seguidores
${map.has_data ? fmt(map.total_followers||0) : '—'}
Paises con presencia
${map.has_data ? (map.regions||[]).length : '—'}
Mejor horario
${heat.has_data && heat.best_slot ? (['Dom','Lun','Mar','Mie','Jue','Vie','Sab'][heat.best_slot.dow]+' '+heat.best_slot.hour+':00') : '—'}
${renderAudienceMap(map)}
${renderActivityHeatmap(heat)}`; } function renderAudienceMap(map) { const PLATS = [['all','Todas'],['facebook','Facebook'],['instagram','Instagram'],['tiktok','TikTok'],['youtube','YouTube']]; if (!map.has_data) { // Empty state como invitacion a actuar (segun guia de diseno) return `
Audiencia por region
Aun no hay datos de audiencia

Cuando conectes tus redes y autorices el acceso, mostraremos aqui un mapa con la ubicacion de tus seguidores. Empieza conectando una cuenta.

`; } const maxF = Math.max(...map.regions.map(r=>r.followers), 1); return `
Audiencia por region
${PLATS.map(([k,l])=>``).join('')}
${fmt(map.total_followers)} seguidores en ${map.regions.length} pais${map.regions.length>1?'es':''}
${map.regions.slice(0,8).map(r=>`
${r.country_name||r.country_code}
${fmt(r.followers)} ${r.percentage}%
`).join('')}
Pais principal
${map.top_country?.country_name||map.top_country?.country_code||'—'}
${fmt(map.top_country?.followers||0)} seguidores · ${map.top_country?.percentage||0}%
Programa contenido en el huso horario de tu pais principal para mayor alcance.
`; } window.setAudPlat = (p) => { audPlatform = p; loadView(); }; function renderActivityHeatmap(heat) { if (!heat.has_data) { return `
Mejores horarios para publicar
Sin actividad todavia

En cuanto publiques y tus posts reciban interacciones, este mapa de calor te mostrara los dias y horas en que tu audiencia responde mejor.

`; } const dias = ['Lun','Mar','Mie','Jue','Vie','Sab','Dom']; const dowOrder = [1,2,3,4,5,6,0]; // reordenar para empezar en lunes const max = heat.max_engagement || 1; const cellColor = (v) => { if (v === 0) return 'var(--bg2)'; const t = v / max; if (t > 0.75) return '#1E40AF'; if (t > 0.5) return '#2563EB'; if (t > 0.25) return '#60A5FA'; return '#BFDBFE'; }; // horas agrupadas de 3 en 3 para compactar (0-2,3-5,...21-23) const hourBuckets = [[0,'0-3'],[3,'3-6'],[6,'6-9'],[9,'9-12'],[12,'12-15'],[15,'15-18'],[18,'18-21'],[21,'21-24']]; const bestTxt = heat.best_slot ? `${['Dom','Lun','Mar','Mie','Jue','Vie','Sab'][heat.best_slot.dow]} a las ${heat.best_slot.hour}:00` : '—'; return `
Mejores horarios para publicar
Mejor: ${bestTxt}

Basado en ${heat.total_posts} publicaciones y el engagement que recibieron. Mas oscuro = mas interaccion.

${hourBuckets.map(([,l])=>``).join('')} ${dowOrder.map((dow,i)=>` ${hourBuckets.map(([h])=>{ // sumar las 3 horas del bucket let v=0; for(let k=0;k<3;k++){ v += (heat.grid[dow]&&heat.grid[dow][h+k])||0; } return ``; }).join('')} `).join('')}
${l}
${dias[i]}
Menos ${['var(--bg2)','#BFDBFE','#60A5FA','#2563EB','#1E40AF'].map(c=>``).join('')} Mas
`; } // ════════════════════════════════════════════════════════════ // AUTOMATIZACION — reglas, cola evergreen, registro // ════════════════════════════════════════════════════════════ const AUTO_TYPES = { recycle: { label:'Reciclar contenido top', ic:'ti-recycle', desc:'Vuelve a publicar tus posts con mejor rendimiento despues de un tiempo.' }, evergreen: { label:'Cola evergreen', ic:'ti-infinity', desc:'Programa automaticamente contenido reutilizable en los huecos de tu calendario.' }, recurring: { label:'Publicacion recurrente', ic:'ti-repeat', desc:'Publica un mensaje fijo cada cierto intervalo.' }, auto_reply:{ label:'Respuesta automatica', ic:'ti-message-bolt', desc:'Responde comentarios y mensajes que contengan ciertas palabras clave.' }, }; async function vAutomation() { const id = cid(); const [rulesD, everD, logD] = await Promise.allSettled([ get(`${API}/companies/${id}/automation/rules`), get(`${API}/companies/${id}/automation/evergreen`), get(`${API}/companies/${id}/automation/log`), ]); const rules = rulesD.value?.rules || []; const evergreen = everD.value?.items || []; const log = logD.value?.log || []; const activeCount = rules.filter(r=>r.enabled).length; return ` ${pageHero('ti-robot','Automatizacion','Reglas que trabajan solas mientras tu te enfocas en lo importante', `
`)}
Automatiza tu presencia
Reciclar: republica tu mejor contenido. Evergreen: mantiene la cola activa. Recurrente: publica mensajes fijos. Auto-respuesta: contesta comentarios por palabra clave.
Reglas activas
${activeCount}
Cola evergreen
${evergreen.filter(e=>e.active).length}
Acciones realizadas
${log.length}
Total reglas
${rules.length}
Reglas de automatizacion
${rules.length ? `
${rules.map(r=>{ const meta = AUTO_TYPES[r.type] || { label:r.type, ic:'ti-bolt', desc:'' }; const typeGrad = {recycle:'linear-gradient(135deg,#2563EB,#3B82F6)',evergreen:'linear-gradient(135deg,#7C3AED,#A78BFA)',recurring:'linear-gradient(135deg,#16A34A,#4ADE80)',auto_reply:'linear-gradient(135deg,#D97706,#FBBF24)'}[r.type]||'var(--grad-primary)'; return `
${r.name}
${r.run_count>0 ? `${r.run_count}× ejecutada` : ''}
${meta.label}${r.last_run_at?' · Ultima vez: '+fmtD(r.last_run_at):' · Nunca ejecutada'}
`; }).join('')}
` : EMPTY('ti-robot','Sin reglas activas','Crea tu primera automatizacion para ahorrar trabajo manual: recicla tu mejor contenido, manten una cola evergreen siempre activa, o responde comentarios al instante.', ``)}
Cola evergreen
${evergreen.length ? `
${evergreen.map(e=>`
${(e.content||'').slice(0,110)}${(e.content||'').length>110?'…':''}
Publicado ${e.times_posted||0}×
`).join('')}
` : `

La cola esta vacia. Agrega contenido reutilizable y una regla evergreen lo programara solo.

`}
Actividad reciente
${log.length ? `
${log.slice(0,12).map(l=>`
${l.detail||l.action}
${l.rule_name?l.rule_name+' · ':''}${fmtD(l.created_at)}
`).join('')}
` : `

Sin actividad todavia. Cuando tus reglas se ejecuten, veras el registro aqui.

`}
`; } window.newAutomation = () => { modal('Nueva automatizacion', `
${Object.entries(AUTO_TYPES).map(([k,v],i)=>``).join('')}
`, '560px'); selectAutoType('recycle'); }; window.selectAutoType = (type) => { const radios = document.getElementsByName('auto-type'); for (const r of radios) r.checked = (r.value===type); const cfg = document.getElementById('auto-config'); if (!cfg) return; const forms = { recycle: `
`, evergreen: `

Agrega contenido a la cola evergreen abajo; esta regla lo programara automaticamente.

`, recurring: `
`, auto_reply: `
`, }; cfg.innerHTML = forms[type] || ''; }; window.saveAutomation = async () => { const type = [...document.getElementsByName('auto-type')].find(r=>r.checked)?.value; const name = document.getElementById('auto-name')?.value?.trim(); if (!name) { setAlert('auto-alert','El nombre es requerido'); return; } let config = {}; if (type==='recycle') config = { after_days:+document.getElementById('cfg-after').value, min_engagement:+document.getElementById('cfg-mineng').value }; else if (type==='evergreen') config = { min_gap_hours:+document.getElementById('cfg-gap').value }; else if (type==='recurring') config = { content:document.getElementById('cfg-content').value, interval_hours:+document.getElementById('cfg-interval').value }; else if (type==='auto_reply') config = { rules:[{ keyword:document.getElementById('cfg-keyword').value, reply:document.getElementById('cfg-reply').value }] }; try { await post(`${API}/companies/${cid()}/automation/rules`, { name, type, config }); closeModal(); showView('automation'); } catch(e) { setAlert('auto-alert', e.message); } }; window.toggleAutomation = async (id) => { await post(`${API}/companies/${cid()}/automation/rules/${id}/toggle`, {}).catch(e=>alert(e.message)); }; window.delAutomation = async (id) => { if(!confirm('¿Eliminar esta regla?'))return; await del(`${API}/companies/${cid()}/automation/rules/${id}`).catch(e=>alert(e.message)); showView('automation'); }; window.newEvergreen = () => { modal('Agregar a cola evergreen', `
`); }; window.saveEvergreen = async () => { const content = document.getElementById('ever-content')?.value?.trim(); if (!content) { setAlert('ever-alert','El contenido es requerido'); return; } try { await post(`${API}/companies/${cid()}/automation/evergreen`, { content, hashtags:document.getElementById('ever-tags')?.value||'', platforms:[] }); closeModal(); showView('automation'); } catch(e) { setAlert('ever-alert', e.message); } }; window.toggleEvergreen = async (id) => { await post(`${API}/companies/${cid()}/automation/evergreen/${id}/toggle`, {}).catch(e=>alert(e.message)); showView('automation'); }; window.delEvergreen = async (id) => { if(!confirm('¿Eliminar de la cola?'))return; await del(`${API}/companies/${cid()}/automation/evergreen/${id}`).catch(e=>alert(e.message)); showView('automation'); }; // ════════════════════════════════════════════════════════════ // LINK-IN-BIO — editor de micro-landing page // ════════════════════════════════════════════════════════════ async function vBioLink() { const id = cid(); const d = await get(`${API}/companies/${id}/bio`).catch(() => ({ page:null, links:[] })); const page = d.page || {}; const links = d.links || []; const apiBase = API.replace('/api/v1',''); const publicUrl = `${apiBase}/bio/${page.slug||''}`; return ` ${pageHero('ti-link','Link in Bio','Tu micro-landing con todos tus enlaces en un solo sitio')}
Tu pagina publica
${publicUrl}
${fmt(page.views||0)} visitas
Perfil
Enlaces
${links.length ? `` : `

Agrega tu primer enlace: catalogo, WhatsApp, redes...

`}
Vista previa
`; } window.saveBioSlug = async () => { const slug = document.getElementById('bio-slug')?.value?.trim(); try { await put(`${API}/companies/${cid()}/bio`, { slug }); showView('biolink'); } catch(e){ alert(e.message); } }; window.saveBioProfile = async () => { const payload = { title: document.getElementById('bio-title')?.value, bio: document.getElementById('bio-desc')?.value, avatar_url: document.getElementById('bio-avatar')?.value, theme: document.getElementById('bio-theme')?.value, accent: document.getElementById('bio-accent')?.value, }; try { await put(`${API}/companies/${cid()}/bio`, payload); showView('biolink'); } catch(e){ alert(e.message); } }; window.addBioLink = () => { modal('Agregar enlace', `
`); }; window.saveBioLink = async () => { const label = document.getElementById('bl-label')?.value?.trim(); const url = document.getElementById('bl-url')?.value?.trim(); if (!label || !url) { setAlert('biolink-alert','Etiqueta y URL son requeridas'); return; } try { await post(`${API}/companies/${cid()}/bio/links`, { label, url }); closeModal(); showView('biolink'); } catch(e){ setAlert('biolink-alert', e.message); } }; window.delBioLink = async (id) => { if (!confirm('¿Eliminar este enlace?')) return; try { await del(`${API}/companies/${cid()}/bio/links/${id}`); showView('biolink'); } catch(e){ alert(e.message); } }; // ── LOGIN ───────────────────────────────────────────────────── // ── GUIAS CONTEXTUALES por vista ───────────────────────────── const GUIDES = { dashboard: { t:'Dashboard', steps:[ 'Aqui ves el resumen de tus redes: alcance, engagement, impresiones y seguidores de los ultimos 30 dias.', 'Usa los accesos rapidos para ir directo a crear contenido, programar o ver metricas.', 'Los paneles de abajo muestran tus publicaciones recientes y el estado de tus redes conectadas.'] }, create: { t:'Crear publicacion', steps:[ 'Escribe tu contenido y selecciona en que redes publicar (solo apareceran las que tengas conectadas).', 'Usa el boton IA junto a hashtags para que la IA sugiera etiquetas relevantes.', 'Programa con "Mejor hora" para publicar cuando tu audiencia esta mas activa.', 'El "primer comentario" se publica automaticamente al salir el post: ideal para enlaces o hashtags extra.', 'Para muchos posts a la vez, usa "Programacion masiva".'] }, calendar: { t:'Calendario', steps:[ 'Ves tus publicaciones de redes y articulos del blog en un solo calendario unificado.', 'Filtra por Todo, Redes o Blog con los chips de arriba a la derecha.', 'Doble clic en un dia para crear contenido con esa fecha ya precargada.', 'El color de cada evento indica su estado: verde=publicado, azul=programado, amarillo=borrador.', 'Clic en un evento para abrirlo y editarlo directamente.'] }, automation: { t:'Automatizacion', steps:[ 'Crea reglas que trabajan solas para reducir tu trabajo manual.', 'Reciclar: re-publica tu mejor contenido pasado un tiempo. Evergreen: programa contenido reutilizable en huecos.', 'Recurrente: publica un mensaje fijo cada cierto intervalo. Respuesta automatica: contesta comentarios por palabra clave.', 'Activa o pausa cada regla con el switch. El registro muestra lo que el motor ha hecho.'] }, approvals: { t:'Aprobaciones', steps:[ 'Aqui llegan las publicaciones que tu equipo crea y requieren tu revision.', 'Aprueba, solicita cambios o rechaza. Puedes comentar en el hilo de cada post.', 'Solicitar cambios devuelve el post a borrador y avisa al autor.'] }, inbox: { t:'Bandeja de entrada', steps:[ 'Comentarios, menciones y mensajes de tus redes en un solo lugar.', 'Filtra por sin leer, respondidos o por tipo. Responde directo o usa "Sugerir con IA".', 'Nota: requiere que tus redes esten conectadas via OAuth para recibir interacciones.'] }, templates: { t:'Plantillas', steps:[ 'Guarda contenido reutilizable para no escribir lo mismo una y otra vez.', 'Organiza por categoria. Clic en "Usar" lleva el texto directo a Crear publicacion.'] }, reports: { t:'Reportes PDF', steps:[ 'Selecciona el periodo: 7, 30 o 90 dias, o personaliza las fechas.', 'Configura tu marca (logo, color, nombre de agencia) para reportes white-label.', 'Elige que secciones incluir: resumen, top posts, crecimiento, audiencia, blog, recomendaciones IA.', 'Haz clic en Generar reporte — se abre en nueva pestana.', 'Para descargar como PDF: Cmd+P (Mac) o Ctrl+P (Windows) → Destino: Guardar como PDF.'] }, audience: { t:'Audiencia', steps:[ 'El mapa muestra de donde son tus seguidores (se llena cuando conectas tus redes).', 'El heatmap usa tus datos reales para mostrar los mejores dias y horas para publicar.'] }, biolink: { t:'Link in Bio', steps:[ 'Crea una micro-pagina con todos tus enlaces, ideal para la bio de Instagram/TikTok.', 'Personaliza titulo, descripcion, avatar, tema y color. Agrega los enlaces que quieras.', 'Comparte tu URL publica; cuenta visitas y clics por enlace. La vista previa es en vivo.'] }, blog: { t:'Blog SEO Automatico', steps:[ 'La IA genera un calendario de articulos en clusters tematicos optimizados para Google.', 'Cada articulo incluye H1/H2, meta descripcion, tabla de contenidos, FAQ y enlaces externos.', 'Conecta tu sitio con webhook o WordPress para publicacion automatica sin intervencion.', 'En la tab Configuracion define frecuencia, longitud y CTA de cada articulo.', 'La tab Analitica muestra visitas y clicks reales cuando el tracking pixel esta activo.'] }, networks: { t:'Redes conectadas', steps:[ 'Conecta cada red con un clic via OAuth seguro: nunca compartes tu contrasena.', 'Necesitas cuentas Business/Creator en Instagram, vinculadas a una pagina de Facebook.', 'Una vez conectadas, podras publicar, ver audiencia y recibir interacciones.'] }, analytics: { t:'Metricas', steps:[ 'Tu desempeno en redes: alcance, engagement e impresiones, con cambios vs el periodo anterior.', 'Las metricas se llenan a medida que publicas y tus redes conectadas reportan datos.'] }, videos: { t:'Avatar IA', steps:[ 'Sube una imagen propia o licenciada como avatar — nunca usamos fotos de terceros sin permisos.', 'Escribe el texto que quieres superponer y ajusta el tamano de fuente con el slider.', 'Elige el avatar de la galeria y genera el video. Se guarda en tu Biblioteca para reutilizarlo.'] }, library: { t:'Biblioteca de medios', steps:[ 'Sube imagenes y videos una vez y usalos en cualquier publicacion futura.', 'Formatos soportados: JPG, PNG, GIF, MP4 hasta 50 MB por archivo.', 'Los archivos se guardan en la nube y estan disponibles desde el editor al crear contenido.'] }, backlinks: { t:'Analisis de Backlinks', steps:[ 'Conecta tu Moz API Key en el backoffice (Config -> API Keys) para ver datos reales.', 'Agrega dominios de competidores con el boton "+ Agregar competidor" para comparar.', 'La brecha indica cuantas veces mas dominios de referencia tiene la mediana vs tu sitio.', 'Mas dominios de referencia = mas autoridad para Google = mejor posicion organica.'] }, seo: { t:'SEO & Google Ads', steps:[ 'La puntuacion SEO audita los elementos tecnicos clave de tu sitio web.', 'Agrega keywords para monitorear tu posicion en Google y detectar oportunidades de contenido.', 'Rojo = falta, Amarillo = mejorar, Verde = correcto. Atiende primero los rojos.'] }, ai: { t:'IA Generativa', steps:[ 'Describe el tema: cuanto mas detallado, mejor el resultado.', 'Elige la plataforma correcta: cada red tiene limites y estilos distintos.', 'Genera 3 o 5 variantes y elige la que mejor suene. Luego editala en el editor.', 'Clic en "Usar en editor" lleva el texto directo a Crear publicacion con un clic.'] }, automation: { t:'Automatizacion', steps:[ 'Crea reglas con el boton "Nueva regla". Cada tipo tiene un comportamiento distinto.', 'Reciclar: republica tus posts con mas engagement pasado el tiempo configurado.', 'Evergreen: mantiene la cola activa programando contenido reutilizable en huecos.', 'Recurrente: publica un mensaje fijo (ej: promo del viernes) en el intervalo que definas.', 'Auto-respuesta: contesta comentarios que contengan palabras clave especificas.', 'Activa/pausa cada regla con el switch. El registro muestra lo que se ejecuto.'] }, networks: { t:'Redes conectadas', steps:[ 'Conecta cada red con un clic — te redirigimos al login oficial, nunca vemos tu contrasena.', 'Instagram requiere cuenta Business o Creator vinculada a una pagina de Facebook.', 'Puedes conectar multiples cuentas de la misma plataforma (ej: 2 Instagram).', 'Si el boton aparece desactivado, el administrador debe configurar esa plataforma primero.', 'Revoca el acceso en cualquier momento desde la propia red o presionando el boton Desconectar.'] }, team: { t:'Equipo', steps:[ 'Invita miembros por email. Recibiran un enlace para crear su cuenta y acceder.', 'Propietario: acceso total. Admin: todo excepto facturacion. Editor: crea y programa.', 'Community Mgr: responde inbox y puede aprobar. Analista: solo ve reportes.', 'Puedes cambiar el rol de un miembro en cualquier momento desde su perfil.', 'Las publicaciones de Editores pueden requerir aprobacion segun tu configuracion.'] }, billing: { t:'Facturacion', steps:[ 'El plan actual se muestra con su uso del mes en barras de progreso.', 'Si llegas al 90% del limite, veras una alerta — considera actualizar antes de que falle.', 'Cambia de plan con un clic. Los cambios aplican al siguiente ciclo de facturacion.', 'Para cancelar, usa el boton Gestionar suscripcion que te lleva al portal de Stripe.', 'Los pagos son procesados exclusivamente por Stripe — nunca almacenamos datos de tarjeta.'] }, settings: { t:'Configuracion', steps:[ 'La Voz de marca es la instruccion que la IA usa en cada generacion — completala bien.', 'Incluye tono, publico objetivo, palabras a evitar y valores de tu marca.', 'Las notificaciones por email se pueden personalizar individualmente.', 'Exporta tus datos en cualquier momento desde la Zona peligrosa.', 'Los cambios se guardan con el boton Guardar en la esquina superior derecha.'] }, chatbot: { t:'Chatbot IA 24/7', steps:[ 'Crea un chatbot por canal (WhatsApp, Instagram, Facebook, TikTok, Web).', 'Define la personalidad: dale un nombre, un rol y una meta clara (agendar / vender / informar).', 'Conecta el canal en Redes conectadas primero para que el chatbot pueda enviar mensajes.', 'El chatbot responde en segundos, califica leads y los pasa al CRM automaticamente.', 'Revisa las conversaciones recientes para mejorar el guion de la IA.'] }, callcenter: { t:'Call Center IA', steps:[ 'Crea una campana saliente: sube contactos y define el guion de la llamada.', 'La IA llama, detecta si es persona real o contestador y adapta la conversacion.', 'Configura llamadas entrantes para que la IA atienda a quienes te llaman 24/7.', 'Cada llamada queda grabada y transcrita para que puedas revisar el resultado.', 'Los leads calificados por llamada pasan automaticamente al CRM.'] }, appointments: { t:'Agendamientos', steps:[ 'Conecta tu Google Calendar u Outlook para que la IA vea tu disponibilidad en tiempo real.', 'El chatbot o call center propone horarios y el cliente confirma directamente en la conversacion.', 'Activa recordatorios automaticos 24h y 1h antes para reducir las cancelaciones.', 'El seguimiento post-cita pide referidos y feedback automaticamente.', 'Configura la duracion de las citas y el buffer entre ellas en la seccion de disponibilidad.'] }, leads: { t:'Calificacion de Leads', steps:[ 'Define 3-5 preguntas clave de calificacion (presupuesto, urgencia, decision).', 'El sistema asigna puntos segun las respuestas y calcula un score del 0 al 100.', 'Leads calientes (80+) son enviados al equipo de ventas de inmediato.', 'Leads tibios (50-79) entran a secuencia de nurturing automatica.', 'Leads frios (-50) se nutren con contenido hasta que esten listos.'] }, crm: { t:'CRM de Clientes', steps:[ 'El CRM se llena automaticamente cuando el chatbot o call center captura un lead.', 'El pipeline visual muestra donde esta cada contacto en el proceso de venta.', 'Puedes importar contactos desde un CSV con nombre, telefono y email.', 'Cada contacto tiene historial de conversaciones, citas y puntuacion de lead.', 'Usa el boton Contactar para iniciar una conversacion desde el CRM.'] }, }; window.showGuide = () => { const g = GUIDES[currentView]; if (!g) { modal('Guia', '

Esta seccion no tiene guia todavia. Explora los botones; cada accion tiene una etiqueta clara.

'); return; } modal(`Como usar: ${g.t}`, `
${g.steps.map((s,i)=>`
${i+1}
${s}
`).join('')}
`, '560px'); }; // ════════════════════════════════════════════════════════════ // IA COMERCIAL — Funcionalidades inspiradas en Zolutium // Chatbot IA 24/7, Call Center IA, Agendamientos, // Calificación de Leads, CRM de Clientes // ════════════════════════════════════════════════════════════ // ── CHATBOT IA 24/7 ────────────────────────────────────────── async function vChatbot() { const id = cid(); const [botsR, statsR, convR] = await Promise.allSettled([ get(`${API}/companies/${id}/chatbot/bots`).catch(()=>({bots:[]})), get(`${API}/companies/${id}/chatbot/stats`).catch(()=>({})), get(`${API}/companies/${id}/chatbot/conversations?limit=10`).catch(()=>({conversations:[]})), ]); const bots = botsR.value?.bots || []; const stats = statsR.value || {}; const convs = convR.value?.conversations || []; const activeBots = bots.filter(b=>b.active).length; return ` ${pageHero('ti-message-chatbot','Chatbot IA 24/7','Atiende, califica y vende automaticamente en WhatsApp, IG, FB y TikTok', `
`)}
Atencion automatica 24/7
Tu chatbot IA responde en segundos, califica leads, agenda citas y cierra ventas mientras duermes. Funciona en WhatsApp, Instagram, Facebook, TikTok y tu sitio web.
Chatbots activos
${activeBots}
Conversaciones hoy
${fmt(stats.conversations_today||0)}
Leads calificados
${fmt(stats.leads_qualified||0)}
Tasa de respuesta
${stats.response_rate||0}%
Mis chatbots (${bots.length})
${bots.length ? `
${bots.map(b => { const platColors = { whatsapp:'#25D366', instagram:'#E1306C', facebook:'#1877F2', tiktok:'#010101', web:'#5B4FE0' }; const platIc = { whatsapp:'ti-brand-whatsapp', instagram:'ti-brand-instagram', facebook:'ti-brand-facebook', tiktok:'ti-brand-tiktok', web:'ti-world' }; return `
${b.name}
${b.platform?.charAt(0).toUpperCase()+b.platform?.slice(1)} · ${b.conversations_count||0} conversaciones · ${b.leads_count||0} leads
${b.active?'Activo':'Pausado'}
`; }).join('')}
` : `
Sin chatbots configurados

Crea tu primer chatbot IA y empieza a atender clientes automaticamente en WhatsApp, Instagram o Facebook.

`}
Conversaciones recientes
${convs.length ? `
${convs.map(c => { const sentiment = c.sentiment==='positive'?{color:'#16A34A',label:'Positivo'}:c.sentiment==='negative'?{color:'#DC2626',label:'Negativo'}:{color:'#94A3B8',label:'Neutral'}; const platIc2 = { whatsapp:'ti-brand-whatsapp', instagram:'ti-brand-instagram', facebook:'ti-brand-facebook', tiktok:'ti-brand-tiktok' }; return `
${(c.contact_name||'?').slice(0,2).toUpperCase()}
${c.contact_name||c.contact_phone||'Usuario'}
${fmtD(c.last_message_at)}
${c.last_message||'Sin mensajes'}
${c.is_lead?` Lead`:''} ${sentiment.label} ${c.appointment_booked?` Cita agendada`:''}
`; }).join('')}
` : `
No hay conversaciones todavia. Activa un chatbot para empezar.
`}
Chatbot en ${fmt(stats.total_conversations||0)} conversaciones
Respondiendo 24 horas al dia
Canales disponibles
${[ {ic:'ti-brand-whatsapp',name:'WhatsApp',color:'#25D366',note:'API oficial · Mayor conversion'}, {ic:'ti-brand-instagram',name:'Instagram',color:'#E1306C',note:'DMs y comentarios'}, {ic:'ti-brand-facebook',name:'Facebook',color:'#1877F2',note:'Messenger y comentarios'}, {ic:'ti-brand-tiktok',name:'TikTok',color:'#010101',note:'Comentarios en videos'}, {ic:'ti-world',name:'Web Chat',color:'#5B4FE0',note:'Widget en tu sitio web'}, ].map(ch=>`
${ch.name}
${ch.note}
`).join('')}
Mejores practicas
${['Responde en menos de 5 segundos — la velocidad multiplica las conversiones','Personaliza el saludo con el nombre del contacto','Califica con 2-3 preguntas clave antes de pasar a ventas','Programa recordatorios automaticos para quien no respondio'].map(t=>`
${t}
`).join('')}
`; } // ── CALL CENTER IA ──────────────────────────────────────────── async function vCallCenter() { const id = cid(); const [callsR, statsR] = await Promise.allSettled([ get(`${API}/companies/${id}/callcenter/calls?limit=15`).catch(()=>({calls:[]})), get(`${API}/companies/${id}/callcenter/stats`).catch(()=>({})), ]); const calls = callsR.value?.calls || []; const stats = statsR.value || {}; const CALL_STATUS = { completed:'green', failed:'red', busy:'amber', no_answer:'gray', in_progress:'blue' }; const CALL_LABEL = { completed:'Completada', failed:'Fallida', busy:'Ocupado', no_answer:'Sin respuesta', in_progress:'En curso', outbound:'Saliente', inbound:'Entrante' }; return ` ${pageHero('ti-phone-call','Call Center IA','Llama y atiende clientes automaticamente sin contratar personal', `
`)}
Llamadas automaticas con IA
La IA llama a tus prospectos, los atiende y agenda citas automaticamente. Llamadas entrantes y salientes sin necesidad de un call center humano.
Llamadas hoy
${fmt(stats.calls_today||0)}
Citas agendadas
${fmt(stats.appointments_booked||0)}
Duracion promedio
${stats.avg_duration_sec?Math.round(stats.avg_duration_sec/60)+'m':'—'}
Tasa de contacto
${stats.contact_rate||0}%
Campanas de llamadas
Configura tu primera campana

Sube una lista de contactos, define el guion de la IA y lanza llamadas automaticas masivas con un solo clic.

Registro de llamadas recientes
${calls.length ? `
${calls.map(c=>``).join('')}
ContactoTipoEstadoDuracionResultadoFecha
${c.contact_name||c.phone||'—'}
${c.phone||''}
${c.direction==='outbound'?'Saliente':'Entrante'} ${bdg(CALL_LABEL[c.status]||c.status, CALL_STATUS[c.status]||'gray')} ${c.duration_sec?Math.floor(c.duration_sec/60)+'m '+( c.duration_sec%60)+'s':'—'} ${c.outcome||'—'} ${fmtD(c.created_at)}
` : `
No hay llamadas todavia. Configura una campana para comenzar.
`}
Como funciona
${[['1','Sube contactos','Importa una lista CSV o usa leads del CRM'],['2','Define el guion','La IA aprende tu propuesta de valor y objeciones'],['3','Lanza la campana','La IA llama automaticamente y agenda citas'],['4','Revisa resultados','Ve grabaciones, transcripciones y conversiones']].map(([n,t,d])=>`
${n}
${t}
${d}
`).join('')}
Resultados tipicos
${[['85%','Reduccion de curiosos / leads no calificados'],['3x','Mas citas agendadas vs llamadas manuales'],['24/7','Atencion sin contratar mas personal'],['5 min','Tiempo de respuesta promedio de la IA']].map(([v,l])=>`
${v}
${l}
`).join('')}
`; } // ── AGENDAMIENTOS ───────────────────────────────────────────── async function vAppointments() { const id = cid(); const today = new Date().toISOString().slice(0,10); const [appsR, statsR, calR] = await Promise.allSettled([ get(`${API}/companies/${id}/appointments?limit=20`).catch(()=>({appointments:[]})), get(`${API}/companies/${id}/appointments/stats`).catch(()=>({})), get(`${API}/companies/${id}/appointments/calendar-config`).catch(()=>({})), ]); const apps = appsR.value?.appointments || []; const stats = statsR.value || {}; const calCfg = calR.value || {}; const todayApps = apps.filter(a=>a.date?.startsWith(today)); const pendingApps = apps.filter(a=>a.status==='pending'); const confirmedApps = apps.filter(a=>a.status==='confirmed'); return ` ${pageHero('ti-calendar-check','Agendamientos con IA','Tu calendario siempre lleno, automaticamente', `
`)}
Agenda automatica 24/7
El chatbot agenda citas directamente en tu calendario, envia recordatorios automaticos y reduce las cancelaciones. Conecta con Google Calendar o Calendly.
Citas hoy
${todayApps.length}
Confirmadas
${confirmedApps.length}
Pendientes
${pendingApps.length}
Tasa de asistencia
${stats.attendance_rate||0}%
Proximas citas
${apps.length ? `
${apps.slice(0,10).map(a=>{ const statusMap = { confirmed:{c:'green',l:'Confirmada'}, pending:{c:'amber',l:'Pendiente'}, cancelled:{c:'red',l:'Cancelada'}, completed:{c:'gray',l:'Completada'}, no_show:{c:'red',l:'No asistio'} }; const st = statusMap[a.status] || {c:'gray',l:a.status||'—'}; return `
${new Date(a.date||a.scheduled_at||'').getDate()||'—'}
${new Date(a.date||a.scheduled_at||'').toLocaleString('es-CR',{month:'short'})}
${a.contact_name||a.client_name||'Cliente'}
${a.service||a.type||'Cita'} · ${a.time||new Date(a.scheduled_at||'').toLocaleTimeString('es-CR',{hour:'2-digit',minute:'2-digit'})}
${bdg(st.l, st.c)}
`; }).join('')}
` : `
Sin citas programadas

Conecta tu calendario y el chatbot empezara a agendar citas automaticamente.

`}
Disponibilidad y recordatorios
${[['apt-reminder-24','Recordatorio 24h antes','Envia WhatsApp/SMS 24 horas antes de la cita',true],['apt-reminder-1','Recordatorio 1h antes','Envia WhatsApp/SMS 1 hora antes de la cita',true],['apt-followup','Seguimiento post-cita','Mensaje automatico 2h despues para pedir referidos',false]].map(([id,label,desc,checked])=>`
${label}
${desc}
`).join('')}
Integraciones de calendario
${[{ic:'ti-brand-google',name:'Google Calendar',color:'#4285F4',note:'Sincronizacion en tiempo real'},{ic:'ti-calendar-time',name:'Calendly',color:'#006BFF',note:'Comparte tu link de reservas'},{ic:'ti-calendar',name:'Outlook',color:'#0078D4',note:'Microsoft 365 sync'}].map(cal=>`
${cal.name}
${cal.note}
`).join('')}
El chatbot puede
${['Ver tu disponibilidad en tiempo real','Proponer horarios disponibles al cliente','Confirmar la cita y enviar recordatorios','Reagendar si el cliente lo solicita','Avisar si el cliente cancela'].map(t=>`
${t}
`).join('')}
`; } // ── CALIFICACION DE LEADS ───────────────────────────────────── async function vLeads() { const id = cid(); const [leadsR, statsR, rulesR] = await Promise.allSettled([ get(`${API}/companies/${id}/leads?limit=20`).catch(()=>({leads:[]})), get(`${API}/companies/${id}/leads/stats`).catch(()=>({})), get(`${API}/companies/${id}/leads/qualification-rules`).catch(()=>({rules:[]})), ]); const leads = leadsR.value?.leads || []; const stats = leadsR.value?.stats || statsR.value || {}; const rules = rulesR.value?.rules || []; const hotLeads = leads.filter(l=>l.score>=80).length; const warmLeads = leads.filter(l=>l.score>=50&&l.score<80).length; const coldLeads = leads.filter(l=>l.score<50).length; return ` ${pageHero('ti-filter','Calificacion de Leads','Elimina curiosos y enfocate solo en quienes tienen intencion real de compra', `
`)}
Filtra el +85% de curiosos
El sistema califica automaticamente cada lead con un score del 0-100 basado en sus respuestas, comportamiento y datos. Solo te llegan los que tienen intencion real.
Total leads
${leads.length}
Calientes (80+)
${hotLeads}
Tibios (50-79)
${warmLeads}
Frios (-50)
${coldLeads}
Leads calificados (${leads.length})
${leads.length ? `
${leads.map(l=>{ const score = l.score||0; const scoreColor = score>=80?'#DC2626':score>=50?'#D97706':'#64748B'; const scoreBg = score>=80?'#FEF2F2':score>=50?'#FFFBEB':'#F8FAFC'; const scoreLabel = score>=80?'Caliente':score>=50?'Tibio':'Frio'; return ``; }).join('')}
ContactoCanalScoreEstadoFecha
${l.name||l.phone||'Lead'}
${l.email||l.phone||''}
${l.source||'Web'}
${score}
${bdg(scoreLabel, score>=80?'red':score>=50?'amber':'gray')} ${fmtD(l.created_at)}
` : `
Sin leads todavia

Configura las preguntas de calificacion y el chatbot empezara a filtrar leads automaticamente.

`}
Reglas de calificacion
${rules.length ? `
${rules.map(r=>`
${r.question}
+${r.score_if_yes||0} pts si responde Si · Tipo: ${r.type||'texto'}
`).join('')}
` : `
Ejemplos de preguntas de calificacion:
${['¿Tienes un presupuesto definido para este mes?','¿Cuantas personas trabajan en tu empresa?','¿Ya usas algun CRM actualmente?','¿Necesitas la solucion en menos de 30 dias?'].map((q,i)=>`
${q}
`).join('')}
`}
Sistema de scoring
${[{label:'Caliente',range:'80-100',color:'#DC2626',bg:'#FEF2F2',desc:'Accion inmediata — llama ahora'},{label:'Tibio',range:'50-79',color:'#D97706',bg:'#FFFBEB',desc:'Seguimiento en 24h'},{label:'Frio',range:'0-49',color:'#64748B',bg:'#F8FAFC',desc:'Nurturing automatico'}].map(s=>`
${s.label}
${s.range} puntos
${s.desc}
`).join('')}
El score aumenta con cada respuesta positiva a tus preguntas de calificacion y disminuye con senales de baja intencion.
Resultado tipico
85% menos curiosos
Solo interactuas con leads que tienen presupuesto, necesidad y urgencia definidos. Tu tiempo vale mas.
`; } // ── CRM DE CLIENTES ────────────────────────────────────────── async function vCRM() { const id = cid(); const [contactsR, statsR, pipelineR] = await Promise.allSettled([ get(`${API}/companies/${id}/crm/contacts?limit=25`).catch(()=>({contacts:[]})), get(`${API}/companies/${id}/crm/stats`).catch(()=>({})), get(`${API}/companies/${id}/crm/pipeline`).catch(()=>({stages:[]})), ]); const contacts = contactsR.value?.contacts || []; const stats = statsR.value || {}; const stages = pipelineR.value?.stages || [ {id:'new',name:'Nuevo',color:'#64748B',count:0}, {id:'contacted',name:'Contactado',color:'#2563EB',count:0}, {id:'qualified',name:'Calificado',color:'#D97706',count:0}, {id:'proposal',name:'Propuesta',color:'#7C3AED',count:0}, {id:'won',name:'Ganado',color:'#16A34A',count:0}, {id:'lost',name:'Perdido',color:'#DC2626',count:0}, ]; // Count by stage contacts.forEach(c => { const s=stages.find(st=>st.id===c.stage); if(s) s.count=(s.count||0)+1; }); return ` ${pageHero('ti-address-book','CRM de Clientes','Todos tus contactos, conversaciones y pipeline de ventas en un solo lugar', `
`)}
CRM conectado con tu IA
Todos tus contactos, historial de conversaciones, pipeline de ventas y seguimientos automaticos en un solo lugar. Conectado con el chatbot y el call center.
Total contactos
${contacts.length}
Valor pipeline
$${fmt(stats.pipeline_value||0)}
Tasa de cierre
${stats.close_rate||0}%
Nuevos este mes
${stats.new_this_month||0}
Pipeline de ventas
${stages.map(s=>`
${s.name}
${s.count||0}
${contacts.filter(c=>c.stage===s.id).slice(0,3).map(c=>`
${c.name||c.phone||'Contacto'}
`).join('')} ${contacts.filter(c=>c.stage===s.id).length>3?`
+${contacts.filter(c=>c.stage===s.id).length-3} mas
`:''}
`).join('')}
Contactos (${contacts.length})
${contacts.length ? `
${contacts.map(c=>{ const stg = stages.find(s=>s.id===c.stage)||stages[0]; const score = c.lead_score||0; const scoreColor = score>=80?'#DC2626':score>=50?'#D97706':'#64748B'; return ``; }).join('')}
NombreContactoEtapaScoreOrigenUltima interaccion
${(c.name||'?').slice(0,2).toUpperCase()}
${c.name||'—'}
${c.phone||''}
${c.email||''}
${stg.name}
${score}
${c.source||'Manual'} ${fmtD(c.last_interaction_at||c.updated_at)}
` : `
Sin contactos todavia

El CRM se llena automaticamente cuando el chatbot o call center captura un lead. Tambien puedes importar contactos desde un CSV.

`}
`; } // ── CRM/Leads JS helpers ────────────────────────────────────── window.filterCRMContacts = q => { document.querySelectorAll('#crm-contacts-table tbody tr').forEach(r => { r.style.display = r.textContent.toLowerCase().includes(q.toLowerCase()) ? '' : 'none'; }); }; window.newChatbot = () => modal('Nuevo chatbot', `
`); window.doNewChatbot = async () => { const name=document.getElementById('cb-name')?.value?.trim(); if(!name){setAlert('cb-alert','El nombre es obligatorio');return;} try{ await post(`${API}/companies/${cid()}/chatbot/bots`,{name,platform:document.getElementById('cb-platform')?.value,persona:document.getElementById('cb-persona')?.value||''}); closeModal(); showView('chatbot'); }catch(e){setAlert('cb-alert',e.message);} }; window.toggleChatbot = async id => { try{await put(`${API}/companies/${cid()}/chatbot/bots/${id}/toggle`,{});showView('chatbot');}catch(e){alert(e.message);} }; window.editChatbot = id => alert('Editor de chatbot — proximamente'); window.connectChatbotChannel = ch => alert(`Conectar ${ch} — configura el OAuth en Redes conectadas primero`); window.newCampaign = () => modal('Nueva campana de llamadas', `
`); window.doNewCampaign = async () => { const name=document.getElementById('camp-name')?.value?.trim(); if(!name){setAlert('camp-alert','El nombre es obligatorio');return;} try{ const contacts=(document.getElementById('camp-contacts')?.value||'').split(' ').map(s=>s.trim()).filter(Boolean); await post(`${API}/companies/${cid()}/callcenter/campaigns`,{name,script:document.getElementById('camp-script')?.value||'',contacts}); closeModal(); showView('callcenter'); }catch(e){setAlert('camp-alert',e.message);} }; window.configInbound = () => alert('Configurar llamadas entrantes — conecta tu numero en Redes conectadas'); window.newAppointment = () => modal('Nueva cita', `
`); window.doNewAppointment = async () => { const client=document.getElementById('apt-client')?.value?.trim(); if(!client){setAlert('apt-alert','El nombre es obligatorio');return;} try{ await post(`${API}/companies/${cid()}/appointments`,{client_name:client,date:document.getElementById('apt-date')?.value,time:document.getElementById('apt-time')?.value,service:document.getElementById('apt-service')?.value||''}); closeModal(); showView('appointments'); }catch(e){setAlert('apt-alert',e.message);} }; window.editAppointment = id => alert('Editar cita — proximamente'); window.importAppointments = () => alert('Importar citas desde CSV — proximamente'); window.saveAptConfig = async (key,val) => { try{await put(`${API}/companies/${cid()}/appointments/calendar-config`,{[key]:val});}catch{} }; window.saveAptConfigAll = async () => { try{await put(`${API}/companies/${cid()}/appointments/calendar-config`,{from:document.getElementById('apt-from')?.value,to:document.getElementById('apt-to')?.value});alert('Configuracion guardada');}catch(e){alert(e.message);} }; window.connectCalendar = cal => alert(`Conectar ${cal} — proximamente. Configuracion en Redes conectadas`); window.newLeadRule = () => modal('Nueva regla de calificacion', `
`); window.doNewLeadRule = async () => { const q=document.getElementById('lr-q')?.value?.trim(); if(!q){setAlert('lr-alert','La pregunta es obligatoria');return;} try{ await post(`${API}/companies/${cid()}/leads/qualification-rules`,{question:q,score_if_yes:parseInt(document.getElementById('lr-score')?.value||25),type:document.getElementById('lr-type')?.value}); closeModal(); showView('leads'); }catch(e){setAlert('lr-alert',e.message);} }; window.quickLeadRule = async q => { try{ await post(`${API}/companies/${cid()}/leads/qualification-rules`,{question:q,score_if_yes:25,type:'yes_no'}); showView('leads'); }catch(e){alert(e.message);} }; window.delLeadRule = async id => { if(!confirm('Eliminar esta regla?')) return; try{await del(`${API}/companies/${cid()}/leads/qualification-rules/${id}`);showView('leads');}catch(e){alert(e.message);} }; window.exportLeads = () => { get(`${API}/companies/${cid()}/leads?limit=1000`).then(d=>{ const leads=d?.leads||[]; if(!leads.length){alert('No hay leads para exportar');return;} const csv='Nombre,Telefono,Email,Score,Estado,Fuente,Fecha '+leads.map(l=>[l.name||'',l.phone||'',l.email||'',l.score||0,l.stage||'',l.source||'',l.created_at||''].join(',')).join(' '); const a=document.createElement('a');a.href='data:text/csv;charset=utf-8,'+encodeURIComponent(csv);a.download='leads.csv';a.click(); }).catch(e=>alert(e.message)); }; window.contactLead = id => { showView('crm'); }; window.newContact = () => modal('Nuevo contacto', `
`); window.doNewContact = async () => { const name=document.getElementById('ct-name')?.value?.trim(); if(!name){setAlert('ct-alert','El nombre es obligatorio');return;} try{ await post(`${API}/companies/${cid()}/crm/contacts`,{name,phone:document.getElementById('ct-phone')?.value||'',email:document.getElementById('ct-email')?.value||'',stage:document.getElementById('ct-stage')?.value||'new'}); closeModal(); showView('crm'); }catch(e){setAlert('ct-alert',e.message);} }; window.openContact = id => alert('Vista detalle de contacto — proximamente'); window.messageContact = id => showView('chatbot'); window.importContacts = () => { const inp=document.createElement('input');inp.type='file';inp.accept='.csv'; inp.onchange=async e=>{ const f=e.target.files[0]; if(!f) return; const fd=new FormData();fd.append('file',f); try{await fetch(`${API}/companies/${cid()}/crm/contacts/import`,{method:'POST',headers:{Authorization:`Bearer ${gt()}`},body:fd});showView('crm');} catch(err){alert('Error importando: '+err.message);} };inp.click(); }; // ── GUIDES para nuevas vistas ───────────────────────────────── function renderLogin() { document.getElementById('root').innerHTML = `
`; document.getElementById('l-pass')?.addEventListener('keydown', e => { if (e.key==='Enter') doLogin(); }); } window.doLogin = async () => { const email = document.getElementById('l-email')?.value?.trim(); const pass = document.getElementById('l-pass')?.value; if (!email || !pass) { setAlert('login-alert','Email y contrasena son requeridos'); return; } const btn = document.getElementById('l-btn'); btn.disabled=true; btn.textContent='Ingresando...'; try { const res = await fetch(`${API}/auth/login`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email,password:pass})}); const d = await res.json(); if (!res.ok || !d.token) throw new Error(d.error || 'Error al iniciar sesion'); localStorage.setItem('sf_token', d.token); su(d.user); user = d.user; if (d.company) { sc(d.company); company = d.company; } if (user.role === 'superadmin') { location.href = 'https://admin.socialflowai.cloud'; return; } renderApp(); } catch(e) { setAlert('login-alert', e.message); btn.disabled=false; btn.textContent='Iniciar sesion'; } }; window.showRegister = () => { document.getElementById('root').innerHTML = `
`; }; window.doRegister = async () => { const name=document.getElementById('r-name')?.value?.trim(),email=document.getElementById('r-email')?.value?.trim(),pass=document.getElementById('r-pass')?.value,company_name=document.getElementById('r-company')?.value?.trim(),company_industry=document.getElementById('r-ind')?.value; if (!name||!email||!pass||!company_name) { setAlert('reg-alert','Todos los campos con * son obligatorios'); return; } if (pass.length<8) { setAlert('reg-alert','La contrasena debe tener minimo 8 caracteres'); return; } const btn=document.getElementById('r-btn'); btn.disabled=true; btn.textContent='Creando cuenta...'; try { const res=await fetch(`${API}/auth/register`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name,email,password:pass,company_name,company_industry})}); const d=await res.json(); if (!res.ok||!d.token) throw new Error(d.error||'Error al crear la cuenta'); localStorage.setItem('sf_token',d.token); su(d.user); user=d.user; if (d.company){sc(d.company);company=d.company;} renderApp(); } catch(e) { setAlert('reg-alert',e.message); btn.disabled=false; btn.textContent='Crear cuenta gratis'; } }; // ── INIT ───────────────────────────────────────────────────── async function init() { if (!gt() || !user) { renderLogin(); return; } try { const d = await get(`${API}/auth/me`); if (!d) { renderLogin(); return; } su(d.user); user = d.user; if (d.company) { sc(d.company); company = d.company; } if (!company && user.company_id) { const cd = await get(`${API}/companies/${user.company_id}`).catch(() => null); if (cd) { sc(cd); company = cd; } } if (user.role === 'superadmin') { location.href = 'https://admin.socialflowai.cloud'; return; } } catch(e) { renderLogin(); return; } currentView = location.hash.replace('#','') || 'dashboard'; renderApp(); } window.addEventListener('hashchange', () => { const v = location.hash.replace('#','') || 'dashboard'; if (v !== currentView) { currentView = v; if (document.getElementById('view-content')) { buildNav(); const t=document.getElementById('topbar-title'); if(t) t.textContent=VIEW_TITLES[v]||v; loadView(); } } }); init();