NODO
NODO
Panel interno. Ingresá con tu cuenta del equipo NODO.
Resumen
Cómo va NODO hoy

Resumen

Toda la red

Canjes por mes

Toda la red, últimos 6 meses

Empresas por adopción

Top del mes

Locales por canjes

Top del mes

Empresas

Alta de empleados

Cargá la base que te pasó la empresa. A cada empleado le llega un mail para definir su contraseña.

1 · Elegí la empresa

2 · Cargá los empleados

Podés cargarlos uno por uno (manual) o pegar/subir toda la lista (masivo). Las dos formas suman a la misma lista de abajo.

3 · Enviar invitaciones

Creamos el usuario de cada empleado y le mandamos el mail para que defina su contraseña.
Cada empleado que carga una empresa (o que cargás vos) queda acá esperando tu validación. Al aprobar un alta se le enviará el mail de invitación; al aprobar una baja, esa persona ya no podrá ingresar.

Pendientes

Locales

Beneficios

Retail Media

Campañas que impactan a los empleados

Visibilidad

Resultados de las campañas de retail media

Consultas

Audiencias

Entendé a los empleados para salir a buscar mejores beneficios

Códigos sin canjear

Como "carritos abandonados": se generó el QR pero no se usó

Búsquedas más frecuentes

Qué buscan los empleados en el catálogo

Demografía

De las encuestas periódicas a empleados

Zonas de canje

Desde dónde canjean en Rosario

Acuerdos

NODO carga el acuerdo (paquete de Retail Media, descuento adicional, etc.), el local lo aprueba y recién ahí se emite la Factura o la Solicitud de NC.
'],{type:'application/vnd.ms-excel'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='nodo-campanas.xls'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href); } function exportPDF(){ if(!window.jspdf||!window.jspdf.jsPDF){ alert('No se pudo cargar el generador de PDF. Revisá la conexión y probá de nuevo.'); return; } const { jsPDF }=window.jspdf; const doc=new jsPDF({orientation:'landscape',unit:'pt',format:'a4'}); doc.setFont('helvetica','bold'); doc.setFontSize(16); doc.setTextColor(20); doc.text('NODO · Resultados de campañas',40,42); doc.setFont('helvetica','normal'); doc.setFontSize(9); doc.setTextColor(120); doc.text('Generado el '+new Date().toLocaleDateString('es-AR'),40,58); const cols=[40,150,250,318,388,452,505,575,628,705,775]; let y=84; doc.setFontSize(8); doc.setTextColor(20); doc.setFont('helvetica','bold'); CAMP_HEAD.forEach((h,i)=>doc.text(String(h),cols[i],y)); y+=4; doc.setDrawColor(200); doc.line(40,y,800,y); y+=14; doc.setFont('helvetica','normal'); campRows().forEach(r=>{ r.forEach((c,i)=>{ let v=String(c); if(i===0&&v.length>16) v=v.slice(0,15)+'…'; if(i===1&&v.length>14) v=v.slice(0,13)+'…'; if(i>=8&&typeof c==='number') v=c.toLocaleString('es-AR'); doc.text(v,cols[i],y); }); y+=16; if(y>540){ doc.addPage(); y=50; } }); doc.save('nodo-campanas.pdf'); } // ---------- consultas ---------- let conFilter="todas"; function setConFilter(f,el){ conFilter=f; document.querySelectorAll('#conSeg button').forEach(b=>b.classList.remove('on')); el.classList.add('on'); renderConsultas(); } function renderConsultas(){ const list=consultas.filter(c=>conFilter==='todas'||c.estado===conFilter); document.getElementById('conCount').textContent=`${consultas.filter(c=>c.estado==='nueva').length} nuevas · ${consultas.length} en total`; const tcol={Empresa:"var(--orange)",Comercio:"var(--green)",Empleado:"var(--magenta)"}; document.getElementById('conRows').innerHTML=list.map(c=>`
${c.nombre} · ${c.tipo}${c.msg} ${c.canal}${c.fecha} ${c.estado[0].toUpperCase()+c.estado.slice(1)}
`).join('')||`
No hay consultas en este estado.
`; } function detalleConsulta(id){ const c=consultas.find(x=>x.id===id); openModal(`

${c.nombre}

${c.tipo} · ${c.canal} · ${c.fecha} · ${c.estado}
${c.msg}
`); } function setConEstado(id,estado){ const c=consultas.find(x=>x.id===id); c.estado=estado; closeModal(); renderConsultas(); } function responderConsulta(id){ const c=consultas.find(x=>x.id===id); if(c.estado==='nueva') c.estado='progreso'; closeModal(); renderConsultas(); } // ---------- audiencias ---------- const insights={ cods:{generados:1240, canjeados:939}, abandonados:[ {ben:"2x1 en el cine · Cine El Cairo",emp:"Lucas P. · TechRosario",t:"hace 2 h"}, {ben:"20% en farmacia · Farmacia del Litoral",emp:"Mariana S. · Clínica Norte",t:"hace 5 h"}, {ben:"1er mes gratis · SportClub",emp:"Diego R. · Logística del Litoral",t:"ayer"}, {ben:"15% en almuerzo · La Parrilla",emp:"Sofía G. · Molinos del Sur",t:"ayer"}, {ben:"10% efectivo · Tienda Pampa",emp:"Tomás V. · TechRosario",t:"hace 2 días"} ], busquedas:[["gimnasio",182],["café",146],["farmacia",118],["cine",97],["sushi",74],["indumentaria",61]], genero:[["Femenino",54,"#E6347E"],["Masculino",43,"#FF5A1F"],["Otro / NS",3,"#6A5AE0"]], hijos:38, mascotas:61, zonas:[["Centro",32,"#FF5A1F"],["Pichincha",27,"#12A66B"],["Echesortu",16,"#E6347E"],["Fisherton",13,"#6A5AE0"],["Otras",12,"#E0A422"]] }; function renderAudiencias(){ const g=insights.cods.generados, c=insights.cods.canjeados, sin=g-c, tasa=Math.round(sin/g*100); document.getElementById('audKpis').innerHTML=[ kpi('rgba(106,90,224,.12)','#6A5AE0','',g.toLocaleString('es-AR'),"códigos generados","",true), kpi('rgba(18,166,107,.12)','#12A66B','',c.toLocaleString('es-AR'),"códigos canjeados","",true), kpi('rgba(230,52,126,.12)','#E6347E','',sin.toLocaleString('es-AR'),"sin canjear (abandonados)","",true), kpi('rgba(224,164,34,.14)','#E0A422','',tasa+"%","tasa de abandono","",true) ].join(''); document.getElementById('audAband').innerHTML=`
`+insights.abandonados.map(a=>`
!${a.ben}${a.emp}${a.t}
`).join('')+`
`; const maxB=Math.max(...insights.busquedas.map(b=>b[1])); document.getElementById('audBusq').innerHTML=insights.busquedas.map(b=>barRow('"'+b[0]+'"',Math.round(b[1]/maxB*100),'#6A5AE0')).join(''); document.getElementById('audDemo').innerHTML= `
Género
`+insights.genero.map(x=>barRow(x[0],x[1],x[2])).join('') +`
${insights.hijos}%
con hijos
${insights.mascotas}%
con mascotas
`; document.getElementById('audZonas').innerHTML=insights.zonas.map(z=>barRow(z[0],z[1],z[2])).join(''); } // ---------- acuerdos ---------- const ACU_LBL={borrador:"Borrador",enviado:"Enviado al local",aprobado:"Aprobado",rechazado:"Rechazado",facturado:"Facturado"}; const ACU_CHIP={borrador:"pausado",enviado:"onboarding",aprobado:"activo",rechazado:"nueva",facturado:"plan"}; let acuerdos=[ {id:1,local:"Café Tostado",tipo:"Paquete Retail Media",desc:"Home banner + push, 30 días",monto:180000,periodo:"Jun 2026",estado:"aprobado",doc:""}, {id:2,local:"Cine El Cairo",tipo:"Descuento adicional",desc:"2x1 extendido a viernes",monto:0,periodo:"Jun 2026",estado:"enviado",doc:""}, {id:3,local:"SportClub",tipo:"Paquete Retail Media",desc:"Category banner Bienestar, 30 días",monto:120000,periodo:"May 2026",estado:"facturado",doc:"Factura"}, {id:4,local:"Tienda Pampa",tipo:"Canje especial",desc:"Bonificación por volumen",monto:0,periodo:"Jun 2026",estado:"borrador",doc:""}, {id:5,local:"Farmacia del Litoral",tipo:"Nota de crédito",desc:"Ajuste por campaña no entregada",monto:-15000,periodo:"May 2026",estado:"aprobado",doc:""} ]; function renderAcuerdos(){ document.getElementById('acuCount').textContent=`${acuerdos.length} acuerdos · ${acuerdos.filter(a=>a.estado==='aprobado').length} listos para facturar`; document.getElementById('acuRows').innerHTML=acuerdos.map(a=>`
${a.local}${a.tipo} · ${a.periodo} ${a.monto?money(a.monto):'—'}Monto ${a.doc?`${a.doc}Documento`:''} ${ACU_LBL[a.estado]}
`).join('')||`
Todavía no hay acuerdos.
`; } function detalleAcuerdo(id){ const a=acuerdos.find(x=>x.id===id); let actions=''; if(a.estado==='borrador') actions=``; else if(a.estado==='enviado') actions=``; else if(a.estado==='aprobado'){ const nc=a.monto<0||a.tipo==='Nota de crédito'; actions=``; } else if(a.estado==='facturado') actions=``; else actions=``; openModal(`

${a.local}

${a.tipo} · ${a.periodo} · ${ACU_LBL[a.estado]}
${a.desc}
Monto${a.monto?money(a.monto):'Sin cargo'}
Documento a emitir${a.doc||(a.monto<0||a.tipo==='Nota de crédito'?'Solicitud de NC':'Factura')}
Estado${ACU_LBL[a.estado]}
`); } function acuEstado(id,estado){ const a=acuerdos.find(x=>x.id===id); a.estado=estado; closeModal(); renderAcuerdos(); } function acuEmitir(id,doc){ const a=acuerdos.find(x=>x.id===id); a.estado='facturado'; a.doc=doc; closeModal(); renderAcuerdos(); } function formAcuerdo(){ openModal(`

Nuevo acuerdo

Cargá el acuerdo. Queda en borrador hasta que lo envíes al local.
`); } function saveAcuerdo(){ const local=document.getElementById('a-local').value, tipo=document.getElementById('a-tipo').value; const desc=document.getElementById('a-desc').value.trim()||'—', monto=parseInt(document.getElementById('a-monto').value)||0; const periodo=document.getElementById('a-per').value.trim()||'—'; acuerdos.unshift({id:Date.now(),local,tipo,desc,monto,periodo,estado:"borrador",doc:""}); closeModal(); renderAcuerdos(); } // ---------- conexión ---------- // Pegá acá los datos de tu proyecto (Project URL y clave pública / anon). const SUPABASE_URL = 'https://uknshevrltbjhvdcshst.supabase.co'; const SUPABASE_KEY = 'sb_publishable_2ws3ODTLiN72bH6ValNAsA_av-h-WLN'; let altaReady=false; let COMERCIOS=[]; try{ if(typeof NODO==='undefined') throw new Error('No cargó nodo-supabase.js (serví los archivos con Live Server).'); if(!SUPABASE_URL.startsWith('PEGÁ') && !SUPABASE_KEY.startsWith('PEGÁ')){ NODO.init(SUPABASE_URL, SUPABASE_KEY); altaReady=true; } }catch(e){ console.error('NODO · conexión:', e); } // ---------- login del panel (rol nodo) ---------- async function login(){ const err=document.getElementById('gateErr'), btn=document.getElementById('gateBtn'); err.textContent=''; if(!altaReady){ err.textContent='Pegá tu Project URL y tu clave pública en el archivo (cerca del final del script).'; return; } btn.disabled=true; btn.textContent='Ingresando…'; try{ await NODO.auth.signIn(document.getElementById('gateEmail').value.trim(), document.getElementById('gatePass').value); const p=NODO.auth.perfil(); if(!p || p.rol!=='nodo'){ err.textContent='Esta cuenta no es del equipo NODO.'; await NODO.auth.signOut(); return; } entrarPanel(); }catch(e){ err.textContent=(e&&e.message)?e.message:'No pudimos ingresar.'; } finally{ btn.disabled=false; btn.textContent='Ingresar'; } } async function entrarPanel(){ document.getElementById('loginGate').style.display='none'; cargarEmpresasSelect(); try{ COMERCIOS = await NODO.comercios.lista(); }catch(_){} cargarBeneficios(); cargarPendientes(); } async function cargarEmpresasSelect(){ if(!altaReady) return; try{ const empresas=await NODO.empresas.lista(); const sel=document.getElementById('altaEmpresa'); if(sel) sel.innerHTML = empresas.map(e=>``).join(''); }catch(e){ console.error(e); } } // ---------- alta de empleados: manual / masivo ---------- function altaTab(mode){ document.getElementById('altaManual').style.display = mode==='manual'?'block':'none'; document.getElementById('altaMasivo').style.display = mode==='masivo'?'block':'none'; document.getElementById('altaTabManual').classList.toggle('btn-primary', mode==='manual'); document.getElementById('altaTabMasivo').classList.toggle('btn-primary', mode==='masivo'); } function altaAgregarManual(){ const g=id=>document.getElementById(id).value.trim(); const email=g('m-email'); if(!email){ altaPreviewMsg('Falta el email para agregar a la persona.'); return; } const linea=[g('m-nombre'),g('m-apellido'),g('m-legajo'),email,g('m-dni'),g('m-area')].join(','); const ta=document.getElementById('altaTexto'); ta.value = (ta.value.trim()? ta.value.trim()+'\n':'') + linea; ['m-nombre','m-apellido','m-legajo','m-email','m-dni','m-area'].forEach(id=>document.getElementById(id).value=''); document.getElementById('m-nombre').focus(); altaPreview(); } function altaLimpiar(){ document.getElementById('altaTexto').value=''; document.getElementById('altaResult').innerHTML=''; altaPreview(); } function altaFile(input){ const f=input.files&&input.files[0]; if(!f) return; const r=new FileReader(); r.onload=()=>{ document.getElementById('altaTexto').value=r.result; altaPreview(); }; r.readAsText(f); } function parseEmpleados(txt){ txt=(txt||'').trim(); if(!txt) return []; const lines=txt.split(/\r?\n/).filter(l=>l.trim()); let start=0, map=['nombre','apellido','legajo','email','dni','area']; if(/email|mail/i.test(lines[0]) && /legajo|nombre/i.test(lines[0])){ map=lines[0].split(/\t|,/).map(h=>{ h=h.trim().toLowerCase(); return h.startsWith('nombre')?'nombre':h.startsWith('apellido')?'apellido':h.includes('legajo')?'legajo':h.includes('mail')?'email':(h.includes('dni')||h.includes('documento'))?'dni':(h.includes('area')||h.includes('área'))?'area':h; }); start=1; } const out=[]; for(let i=start;is.trim()); const o={}; map.forEach((k,idx)=>o[k]=parts[idx]||''); if(o.email) out.push(o); } return out; } function altaParse(){ return parseEmpleados(document.getElementById('altaTexto').value); } function altaPreviewMsg(m){ document.getElementById('altaPreview').textContent=m; } function altaPreview(){ const n=altaParse().length; altaPreviewMsg(n? `${n} empleado(s) en la lista para invitar.` : 'Todavía no hay empleados con email en la lista.'); } async function enviarAlta(){ const btn=document.getElementById('altaSendBtn'), res=document.getElementById('altaResult'); const empresa=document.getElementById('altaEmpresa').value, empleados=altaParse(); res.innerHTML=''; if(!empleados.length){ res.innerHTML='
No hay empleados con email en la lista.
'; return; } btn.disabled=true; btn.textContent='Enviando…'; try{ const r=await NODO.onboarding.cargar(empresa, empleados); const fallos=(r.resultados||[]).filter(x=>!x.ok); let html=`
✓ ${r.dados_de_alta} de ${r.total} dados de alta. Les llegará el mail para definir su contraseña.
`; if(fallos.length){ html+=`
${fallos.length} con problema:
`; html+=fallos.map(f=>`
• ${f.legajo||f.email||'—'}: ${f.motivo}
`).join(''); } res.innerHTML=html; }catch(e){ res.innerHTML=`
No se pudo: ${(e&&e.message)?e.message:e}
`; } finally{ btn.disabled=false; btn.textContent='Cargar y enviar invitaciones'; } } // ---------- init ---------- buildNav(); renderResumen(); renderEmpresas(); renderLocales(); renderBeneficios(); renderBanners(); renderVisibilidad(); renderConsultas(); renderAudiencias(); renderAcuerdos(); altaTab('manual'); document.getElementById('altaTexto').addEventListener('input', altaPreview); // si ya había sesión de NODO, entramos directo if(altaReady){ NODO.auth.cargarPerfil().then(p=>{ if(p && p.rol==='nodo') entrarPanel(); }).catch(()=>{}); }