Contabilidad en vivo

VENTAS TOTALES
$0
todos los pedidos
PEDIDOS
0
total período
GASTO ADS
$0
Meta Ads período
ROAS PROMEDIO
retorno sobre inversión
Bienvenido de vuelta,
Admin
Dropshipping · Dropflow
COBRADO
$0
COSTOS
$0
UTILIDAD
$0
PROYECC.
$0
COMPARATIVO
Executive
DASHBOARD CONTABLE
VENTAS TOTALES
$0
vs período anterior
Cobrado Costos
0%
MARGEN PROYECTADO
FUNNEL DE VENTAS
CPA ANALYSIS
TOTAL
REAL
PROY.
MARGEN
0%
margen
GASTOS OPER.
$0
prov+flete+ads
VENTAS VS GASTO ADS
ESTADO PEDIDOS
RENDIMIENTO DE CAMPAÑAS
FLUJO DE CAJA
✅ COBRADO $0
🚚 EN RUTA $0
💸 SALIDAS $0
CAJA HOY $0
TOP CAMPAÑAS
Total pedidos
0
en el período
Entregados
0
0% del total
En camino
0
0% del total
Devueltos / Cancelados
0
0% del total
Total ventas
$0
pedidos entregados
Gasto proveedor
$0
0% ventas
Gasto transporte
$0
0% ventas
Gasto Meta Ads
$0
CPA total período
Utilidad real
$0
margen 0%
IDFechaProductoEstado Venta Proveedor Flete CPA Prom. Total Utilidad Margen Ciudad
Importa un Excel de Dropi para ver tus pedidos
Ventas · Costos · Utilidad por día
Distribución de estados
Mapa de ventas — Chile
Utilidad acumulada
📊 MÉTRICAS DE:
Gasto total
$0
Valor ventas atribuidas
$0
compras × valor conv.
ROAS promedio
0x
CPA global
$0
Compras totales
0
Impresiones
0
Clics únicos
0
CTR promedio
0%
Gasto/entregado
$0
Presupuesto total período ($)
GASTO vs PPTO
💡 Filtro de fechas: Con CSV manual el período está fijo al exportado. Para filtrar por fecha usa Meta Ads en vivo (Configuración → Meta Ads).
0 anuncios
Anuncio Estado Inicio Presupuesto Gasto total Impr. CPM ▲$4.500 Clics CTR ▲2.5% CPC ▲$250 Compras CPA ▲$4.000 ROAS
Importa un CSV de Meta Ads en Configuración
Gasto vs ROAS por anuncio (top 8)
Distribución de gasto por estado
CPA por anuncio
🎯 Campañas ganadoras vs perdedoras — ¿Cuántas ventas necesito para cubrir el déficit?
Importa datos de Meta Ads para ver el análisis

Control de presupuesto publicitario

Monitorea cómo se gasta tu presupuesto y recibe recomendaciones en tiempo real

⚙️ Configurar presupuesto
Presupuesto total ($)
Fecha inicio
Fecha término

Flujo de caja

Control real del dinero — entradas, salidas y proyección de disponibilidad

⚙️ Parámetros de caja
Días entrega promedio
Días para cobro tras entrega
Saldo inicial caja ($)
Cobrado confirmado
$0
pedidos entregados
Pendientes de confirmación
$0
llegará en ~7 días
$0
utilidad estimada
Total salidas
$0
ads + proveedor + flete
Pérdida por devoluciones
$0
flete no recuperado
Caja disponible HOY
$0
saldo real
💰 ENTRADAS DE DINERO
💸 SALIDAS DE DINERO
📦 IMPACTO EN CAJA POR ESTADO DE PEDIDO
TOTAL ENTRADAS
$0
TOTAL SALIDAS
$0
RESULTADO NETO
$0
📅 PROYECCIÓN DE CAJA — PRÓXIMOS 30 DÍAS

Comparación de períodos

Compara cualquier período con otro

Importa datos de Dropi para ver la comparación
Proyección basada en pedidos reales 75% tasa de entrega
Pedidos pendientes
0
100% total
Se entregarán
0
75% estimado
No se entregarán
0
25% devolución
Costo devoluciones
$0
flete × 25%
Utilidad real
$0
todos los costos descontados
+
Utilidad proyectada
$0
75% de pendientes
=
Total estimado
$0
Margen: 0%
Proyección vs. pérdida por no entrega

Análisis comparativo — Top apps contabilidad dropshipping

Basado en BeProfit, Lifetimely, Triple Whale, Northbeam y herramientas líderes para Shopify.

📊Rentabilidad por productoCRÍTICO

Margen real por SKU incluyendo costo proveedor, flete, ads atribuidos y devoluciones. Identifica qué productos convienen realmente. BeProfit y Lifetimely son líderes en esto.

💰P&L mensual automáticoCRÍTICO

Estado de resultados con ingresos, COGS, marketing, logística y utilidad neta. Esencial para decisiones de escala y reportes a socios. Triple Whale lo genera automáticamente.

🎯Atribución ads por pedidoCRÍTICO

Vincular gasto de Meta Ads directamente a cada pedido via UTMs. CPA real por venta, no promedio global. Permite optimizar campañas con datos reales post-devolución.

⚠️Alertas automáticasCRÍTICO

Notificación por email/WhatsApp cuando el margen cae bajo umbral, el CPA supera el límite rentable, o la tasa de devolución sube. Ya tienes la base en Dropflow.

📈LTV del clienteIMPORTANTE

Valor de vida del cliente en el tiempo. Cambia completamente el CPA tolerado si hay recompra. Lifetimely es el líder para Shopify en esta métrica.

🔄Comparación de periodosIMPORTANTE

Semana vs anterior, mes vs anterior, mismo periodo año pasado. Detecta si el crecimiento es real o estacional. Todas las apps enterprise lo incluyen.

🚚Dashboard de devolucionesIMPORTANTE

Análisis por transportadora, ciudad, producto y rango de precio. Identifica qué rutas o productos tienen mayor tasa de rechazo para optimizar operaciones.

🤖Proyección con IARECOMENDADO

Usar historial para predecir ventas del próximo mes, detectar estacionalidad y sugerir inversión en ads para alcanzar meta de utilidad. Triple Whale y Northbeam ya lo incluyen.

Elige tu plan
Todos los planes incluyen acceso a dropflow.cl · Cancela cuando quieras
FREEMIUM
Gratis
para siempre
${['Dashboard básico','Drag & drop Excel Dropi','Hasta 50 pedidos/mes','5 alertas de semáforo','1 usuario'].map(f=>'
'+f+'
').join('')} ${['Meta Ads en vivo','IA Dropflow','Comparación períodos','Exportar reportes'].map(f=>'
'+f+'
').join('')}
MÁS POPULAR
PRO
$9.990
CLP / mes
${['Todo del Freemium','Pedidos ilimitados','Meta Ads en vivo con fechas','Alertas inteligentes completas','Comparación de períodos','IA Dropflow con voz','Gráficos avanzados','Hasta 3 usuarios'].map(f=>'
'+f+'
').join('')}
BUSINESS
$19.990
CLP / mes
${['Todo del Pro','Usuarios ilimitados','Exportar PDF y Excel','Soporte prioritario WhatsApp','Conexión Dropi en vivo*','Dashboard multi-tienda*'].map(f=>'
'+f+'
').join('')}
* Próximamente · ¿Dudas? Escríbenos a hola@dropflow.cl
🤖
IA Dropflow
Analiza tus datos en tiempo real · Powered by Claude

Pregúntame sobre tus pedidos, campañas, rentabilidad, tendencias o cualquier análisis de tu negocio. Tengo acceso a todos tus datos importados.

Preguntas sugeridas
🤖

Hola 👋 Soy la IA de Dropflow. Tengo acceso a todos tus datos de pedidos, campañas de Meta Ads y métricas de rentabilidad.

Puedes preguntarme cualquier cosa sobre tu negocio — análisis de campañas, pedidos más rentables, tendencias, proyecciones, o cualquier otra consulta. ¿Por dónde empezamos?

⚙️ Parámetros operacionales
Tasa de entrega estimada (%)
Umbral alerta margen (%)
🎯 Umbrales personalizados alertas Meta Ads
CPA máximo ($)
CPC máximo ($)
CPM máximo ($)
CTR mínimo (%)
ROAS mínimo (x)
🎨 Apariencia
Modo de visualización
Alterna entre modo nocturno y modo día
🛍️ Shopify — Conexión en vivo
🟢
Sincronización de pedidos en tiempo real
Conecta tu tienda Shopify para importar pedidos automáticamente
Shopify Store URL
Client ID
Client Secret
Flete promedio por pedido ($)
Costo de despacho que pagas a la transportista
Dev Dashboard → tu app Dropflow API → Configuración → copia "ID de cliente" y "Secreto"
Luego instala la app en tu tienda desde el Dev Dashboard
⚠️ Shopify no incluye costo de proveedor ni flete de Dropi. Para datos completos de rentabilidad, también importa el Excel de Dropi.
📦 Dropi — Conexión en vivo
🔴
EN VIVO · Sincronización automática de pedidos
Conecta tu cuenta Dropi para importar pedidos automáticamente con todos los costos
🔐 Ingresa tus credenciales de Dropi — Dropflow las usa solo para leer tus pedidos
Email de Dropi
Contraseña de Dropi
O importar manualmente (Excel)
📊
Arrastra el Excel de Dropi aquí
Dropi → Mis Pedidos → Exportar Excel · .xlsx
📈 Meta Ads — Conexión en vivo
🔴
EN VIVO · Actualización automática cada 15 min
Campañas, gastos y métricas en tiempo real sin importar archivos
1. Access Token de Meta
developers.facebook.com → Graph API Explorer → Generate Access Token
Permisos: ads_read, ads_management
2. Ad Account ID
Formato: act_123456789 — Meta Ads Manager → URL del navegador
O importar manualmente (CSV)
📁
Arrastra CSV de Meta Ads
exportación manual desde Ads Manager
💡 Historial de Insights
Alertas cerradas · ordenadas por fecha
No hay insights guardados todavía.
Cierra una alerta del dashboard para guardarla acá.
Pregúntale a la IA
`; } function renderDashFunnel(pedidos) { const R2 = calcR(pedidos); const stages = [ {label:'Total', n:pedidos.length}, {label:'En proceso', n:R2.enTransito+R2.pendientes}, {label:'Entregado', n:R2.entregados}, {label:'No entregó', n:R2.devueltos+R2.cancelados}, ]; renderFunnelSVG('db-funnel', stages, {width:220, height:200}); } function renderMetaFunnelGlobal(m) { if(!m) return; const stages = [ {label:'Impresiones', n:Math.round(m.impresiones||0)}, {label:'Clics', n:Math.round(m.clics||0)}, {label:'Compras', n:Math.round(m.compras||0)}, ]; renderFunnelSVG('embudo-global', stages, {width:240, height:210}); } function renderMetaFunnelCampana(campana) { if(!campana) return; const stages = [ {label:'Impresiones', n:Math.round(campana.impresiones||0)}, {label:'Clics', n:Math.round(campana.clics||0)}, {label:'Compras', n:Math.round(campana.compras||0)}, ]; renderFunnelSVG('embudo-campana', stages, {width:240, height:210}); } function toggleMapTooltip(el){ const t=el.querySelector('.mtt'); if(t)t.style.display=t.style.display==='block'?'none':'block'; } function toggleCmpCustom(){ const panel = document.getElementById('cmp-custom-panel'); if(!panel)return; const visible = panel.style.display!=='none'; panel.style.display = visible?'none':'block'; document.querySelectorAll('[id^="cmp-"]').forEach(b=>b.classList.remove('active')); if(!visible) document.getElementById('cmp-custom')?.classList.add('active'); // Pre-fill with current period if(!visible){ const hoy=new Date(),fmt=d=>d.toISOString().split('T')[0]; const inicio=fmt(new Date(hoy.getFullYear(),hoy.getMonth(),1)); const finAnt=fmt(new Date(hoy.getFullYear(),hoy.getMonth(),0)); const inicioAnt=fmt(new Date(hoy.getFullYear(),hoy.getMonth()-1,1)); const h=document.getElementById('cmp-a-desde');if(h)h.value=inicio; const h2=document.getElementById('cmp-a-hasta');if(h2)h2.value=fmt(hoy); const h3=document.getElementById('cmp-b-desde');if(h3)h3.value=inicioAnt; const h4=document.getElementById('cmp-b-hasta');if(h4)h4.value=finAnt; } } function compararPersonalizado(){ const aDesde=document.getElementById('cmp-a-desde')?.value; const aHasta=document.getElementById('cmp-a-hasta')?.value; const bDesde=document.getElementById('cmp-b-desde')?.value; const bHasta=document.getElementById('cmp-b-hasta')?.value; if(!aDesde||!aHasta||!bDesde||!bHasta){toast('Completa todas las fechas',true);return;} renderComparacion(aDesde,aHasta,bDesde,bHasta,'Período A','Período B'); } function compararPeriodo(tipo){ document.getElementById('cmp-custom-panel').style.display='none'; document.querySelectorAll('[id^="cmp-"]').forEach(b=>b.classList.remove('active')); document.getElementById('cmp-'+tipo)?.classList.add('active'); if(!S.pedidos.length){ const cont=document.getElementById('comparacion-contenido'); if(cont)cont.innerHTML='
Importa datos de Dropi para ver la comparación
'; return; } const hoy=new Date(); const fmt=d=>d.toISOString().split('T')[0]; let actualDesde,actualHasta,anteriorDesde,anteriorHasta,labelActual,labelAnterior; if(tipo==='semana'){ const lunes=new Date(hoy);lunes.setDate(hoy.getDate()-hoy.getDay()+1); const lunesAnt=new Date(lunes);lunesAnt.setDate(lunes.getDate()-7); const domAnt=new Date(lunes);domAnt.setDate(lunes.getDate()-1); actualDesde=fmt(lunes);actualHasta=fmt(hoy); anteriorDesde=fmt(lunesAnt);anteriorHasta=fmt(domAnt); labelActual='Esta semana';labelAnterior='Semana anterior'; } else { const inicioMes=new Date(hoy.getFullYear(),hoy.getMonth(),1); const inicioMesAnt=new Date(hoy.getFullYear(),hoy.getMonth()-1,1); const finMesAnt=new Date(hoy.getFullYear(),hoy.getMonth(),0); actualDesde=fmt(inicioMes);actualHasta=fmt(hoy); anteriorDesde=fmt(inicioMesAnt);anteriorHasta=fmt(finMesAnt); labelActual='Este mes';labelAnterior='Mes anterior'; } // Filtrar pedidos por período renderComparacion(actualDesde,actualHasta,anteriorDesde,anteriorHasta,labelActual,labelAnterior); } function renderComparacion(aDesde,aHasta,bDesde,bHasta,labelA,labelB){ const filtrarPor=(desde,hasta)=>S.pedidos.filter(p=>{ const iso=toISOfecha(p.fecha); return iso>=desde&&iso<=hasta; }); const actual=calcR(filtrarPor(aDesde,aHasta)); const anterior=calcR(filtrarPor(bDesde,bHasta)); const delta=(a,b)=>{ if(!b||b===0)return a>0?{val:'+∞%',color:'var(--green)',arrow:'▲'}:{val:'—',color:'var(--text3)',arrow:''}; const pct=((a-b)/Math.abs(b))*100; return pct>=0?{val:'+'+pct.toFixed(1)+'%',color:'var(--green)',arrow:'▲'}:{val:pct.toFixed(1)+'%',color:'var(--red)',arrow:'▼'}; }; const metricas=[ {label:'Ingresos reales',actual:actual.ingresosReales,anterior:anterior.ingresosReales,fmt:fmtCLP,icon:'💰'}, {label:'Pedidos entregados',actual:actual.entregados,anterior:anterior.entregados,fmt:v=>v,icon:'📦'}, {label:'Utilidad real',actual:actual.utilidadReal,anterior:anterior.utilidadReal,fmt:fmtCLP,icon:'📈'}, {label:'Margen real',actual:actual.margenReal,anterior:anterior.margenReal,fmt:fmtPct,icon:'%'}, {label:'Tasa de entrega',actual:actual.tasaReal,anterior:anterior.tasaReal,fmt:fmtPct,icon:'🎯'}, {label:'Costos totales',actual:actual.costosTotal,anterior:anterior.costosTotal,fmt:fmtCLP,icon:'💸',invertir:true}, {label:'Devueltos',actual:actual.devueltos,anterior:anterior.devueltos,fmt:v=>v,icon:'↩️',invertir:true}, {label:'Gasto Meta Ads',actual:actual.adsGasto,anterior:anterior.adsGasto,fmt:fmtCLP,icon:'📣',invertir:true}, ]; const cont=document.getElementById('comparacion-contenido'); if(!cont)return; cont.innerHTML= '
'+ '
'+ '
'+labelA.toUpperCase()+'
'+ '
'+aDesde+' → '+aHasta+'
'+ '
'+ '
'+ '
'+labelB.toUpperCase()+'
'+ '
'+bDesde+' → '+bHasta+'
'+ '
'+ '
'+ '
'+ metricas.map(m=>{ const d=delta(m.actual,m.anterior); const color=m.invertir?(d.arrow==='▲'?'var(--red)':'var(--green)'):d.color; return '
'+ '
'+m.icon+' '+m.label+'
'+ '
'+m.fmt(m.actual)+'
'+ '
'+m.fmt(m.anterior)+'
'+ '
'+d.arrow+' '+d.val+'
'+ '
'; }).join('')+ '
'; } // ── VOZ IA ──────────────────────────────────────────────────────────────────── let _vozActiva=false; let _vozSynth=window.speechSynthesis; function hablarIA(texto, onEnd){ if(!_vozSynth||!_vozActiva)return; _vozSynth.cancel(); // Limpiar texto para síntesis — quitar markdown, emojis const limpiar=t=>t .replace(/\*\*([^*]+)\*\*/g,'$1') .replace(/\*([^*]+)\*/g,'$1') .replace(/[#►•→←↑↓▲▼●○]/g,'') .replace(/[\u{1F300}-\u{1F9FF}]/gu,'') .replace(/\s+/g,' ') .trim(); const hablar=()=>{ const voces=_vozSynth.getVoices(); // Orden de preferencia para voz tipo Jarvis (grave, clara, masculina) const candidatos=[ // Google en español — las mejores para síntesis voces.find(v=>v.name==='Google español de Estados Unidos'), voces.find(v=>v.name==='Google español'), voces.find(v=>v.name.includes('Jorge')), // Microsoft Jorge voces.find(v=>v.name.includes('Carlos')), voces.find(v=>v.name.includes('Alvaro')), voces.find(v=>v.name.includes('Diego')), // Voz masculina en español genérica voces.find(v=>v.lang.startsWith('es')&&v.name.toLowerCase().includes('male')), voces.find(v=>v.lang==='es-CL'), voces.find(v=>v.lang==='es-ES'), voces.find(v=>v.lang.startsWith('es')), // Fallback: inglés masculino (más grave generalmente) voces.find(v=>v.name==='Google UK English Male'), voces.find(v=>v.name.includes('Daniel')&&v.lang.startsWith('en')), voces[0], ]; const voz = candidatos.find(v=>v!=null); // Dividir en fragmentos para hablar más natural const fragmentos = limpiar(texto).match(/[^.!?]+[.!?]*/g) || [texto]; const hablarFragmento=(i)=>{ if(i>=fragmentos.length){if(onEnd)onEnd();return;} const utt=new SpeechSynthesisUtterance(fragmentos[i].trim()); if(voz)utt.voice=voz; utt.lang=voz?.lang||'es-CL'; utt.rate=0.88; // Más pausado = más autoritario utt.pitch=0.72; // Muy grave = tipo Jarvis utt.volume=1.0; utt.onend=()=>hablarFragmento(i+1); utt.onerror=()=>hablarFragmento(i+1); _vozSynth.speak(utt); }; hablarFragmento(0); }; if(_vozSynth.getVoices().length>0){hablar();} else{_vozSynth.addEventListener('voiceschanged',hablar,{once:true});} } function toggleVozIA(){ _vozActiva=!_vozActiva; const btn=document.getElementById('btn-voz-ia'); if(btn){ btn.textContent=_vozActiva?'🔊':'🔇'; btn.title=_vozActiva?'Voz activada — clic para silenciar':'Voz silenciada — clic para activar'; } if(!_vozActiva&&_vozSynth)_vozSynth.cancel(); localStorage.setItem('df_voz',_vozActiva?'1':'0'); toast(_vozActiva?'🔊 Voz IA activada':'🔇 Voz IA silenciada'); } // ── MENSAJE DE BIENVENIDA ───────────────────────────────────────────────────── function mostrarMensajeBienvenida(){ if(!S.pedidos.length)return; const R=calcR(filtF(S.pedidos)); const nombre=USUARIO?.nombre?.split(' ')[0]||''; let msg=''; if(R.margenReal>=20&&R.entregados>=10){ msg=`¡Hola ${nombre}! 🚀 Tus números están excelentes hoy. Margen del ${R.margenReal.toFixed(1)}% con ${R.entregados} pedidos entregados. Es un gran momento para escalar. ¡Vamos con todo!`; } else if(R.margenReal>=10){ msg=`¡Buenos días ${nombre}! 📈 Vas bien, margen del ${R.margenReal.toFixed(1)}%. Con algunas optimizaciones podemos llevarlo al siguiente nivel hoy. ¿Comenzamos?`; } else if(R.margenReal>=0){ msg=`Hola ${nombre} 👋 El margen está ajustado hoy (${R.margenReal.toFixed(1)}%), pero tranquilo — cada día es una oportunidad de mejorar. Revisemos juntos qué podemos optimizar.`; } else { msg=`Hola ${nombre}, no te preocupes 💪 Los números no están donde queremos hoy, pero esto es parte del proceso. Estoy aquí para ayudarte a encontrar la solución. ¡Tú puedes con esto!`; } // Mostrar como toast especial const toast_el=document.createElement('div'); toast_el.style.cssText='position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:9999;max-width:420px;width:90%;background:linear-gradient(135deg,var(--bg2),var(--bg3));border:1px solid rgba(0,230,153,0.3);border-radius:var(--radius);padding:16px 20px;box-shadow:0 8px 32px rgba(0,0,0,0.4);animation:fadeInUp 0.4s ease;font-size:13px;color:var(--text2);line-height:1.6;display:flex;align-items:flex-start;gap:12px'; toast_el.innerHTML='
🤖
IA DROPFLOW
'+msg+'
'; document.body.appendChild(toast_el); // Hablar si voz activa if(_vozActiva)hablarIA(msg); setTimeout(()=>{ toast_el.style.transition='all 0.4s'; toast_el.style.opacity='0'; toast_el.style.transform='translateX(-50%) translateY(20px)'; setTimeout(()=>toast_el.remove(),400); },7000); } // ── MODO DÍA / NOCHE ────────────────────────────────────────────────────────── function toggleModo(){ const esDia=document.body.classList.toggle('modo-dia'); localStorage.setItem('df_modo', esDia?'dia':'noche'); const icono=document.getElementById('modo-icono'); const texto=document.getElementById('modo-texto'); if(icono) icono.textContent = esDia?'🌙':'☀️'; if(texto) texto.textContent = esDia?'Modo noche':'Modo día'; toast(esDia?'☀️ Modo día activado':'🌙 Modo noche activado'); } function aplicarModoGuardado(){ // Restaurar preferencia de voz const vozGuardada=localStorage.getItem('df_voz'); if(vozGuardada==='1'){ _vozActiva=true; const btn=document.getElementById('btn-voz-ia'); if(btn)btn.textContent='🔊'; } const modo=localStorage.getItem('df_modo'); if(modo==='dia'){ document.body.classList.add('modo-dia'); const icono=document.getElementById('modo-icono'); const texto=document.getElementById('modo-texto'); if(icono) icono.textContent='🌙'; if(texto) texto.textContent='Modo noche'; } } function toast(msg,err=false){let t=document.querySelector('.df-toast');if(!t){t=document.createElement('div');t.className='df-toast';document.body.appendChild(t);}t.textContent=msg;t.className='df-toast show '+(err?'err':'ok');clearTimeout(t._t);t._t=setTimeout(()=>t.classList.remove('show'),3500);} // ═══ DRAG & DROP ═══ // ── META ADS EN VIVO ──────────────────────────────────────────────────────── let _metaLiveInterval = null; let _metaConectado = false; async function conectarMetaVivo() { const token = document.getElementById('meta-token-vivo')?.value?.trim(); const actId = document.getElementById('meta-account-vivo')?.value?.trim(); const statusEl = document.getElementById('meta-vivo-status'); const infoEl = document.getElementById('meta-vivo-info'); if (!token || !actId) { mostrarMetaStatus('Completa el token y el Ad Account ID', false); return; } if (!actId.startsWith('act_')) { mostrarMetaStatus('El Ad Account ID debe comenzar con "act_"', false); return; } mostrarMetaStatus('Conectando con Meta Ads...', null); try { // Guardar token en servidor const r = await apiFetch('/api/meta/token', { method: 'POST', body: JSON.stringify({ token, adAccountId: actId }) }); if (!r.ok) { mostrarMetaStatus('Error guardando token', false); return; } // Probar conexión cargando datos await cargarMetaLive(); _metaConectado = true; mostrarMetaStatus('✓ Conectado correctamente — actualizando cada 15 min', true); if (infoEl) infoEl.style.display = 'block'; // Guardar en localStorage para saber que hay conexión localStorage.setItem('df_meta_live', JSON.stringify({ actId, conectado: true })); // Iniciar actualización automática cada 15 min if (_metaLiveInterval) clearInterval(_metaLiveInterval); _metaLiveInterval = setInterval(cargarMetaLive, 15 * 60 * 1000); toast('✓ Meta Ads conectado en vivo'); } catch (e) { mostrarMetaStatus('Error: ' + e.message, false); } } async function cargarMetaLive() { const desde = S.filtroDesde || new Date(Date.now()-30*24*60*60*1000).toISOString().split('T')[0]; const hasta = S.filtroHasta || new Date().toISOString().split('T')[0]; console.log('Cargando Meta Ads:', desde, '→', hasta); // Mostrar loading en KPI gasto const gastoEl = document.getElementById('meta-gasto'); if(gastoEl) gastoEl.textContent = '⏳'; try { const r = await apiFetch('/api/meta/live?desde='+desde+'&hasta='+hasta); if (!r.ok) { const err = await r.json(); throw new Error(err.error || 'Error cargando Meta'); } const data = await r.json(); console.log('Meta Ads recibido:', data.ads?.length, 'anuncios para período', desde, '→', hasta); procesarDatosMetaLive(data); } catch (e) { console.warn('Error Meta live:', e.message); if(gastoEl) gastoEl.textContent = '❌'; throw e; } } function procesarDatosMetaLive(data) { const { ads, summary, fecha } = data; if (!ads || !ads.length) return; // El servidor ahora devuelve los campos directo en cada ad (ya combinados) const campanas = ads.map(ad => ({ nombre: ad.nombre || ad.name || 'Sin nombre', estado: ad.estado || ad.effective_status?.toLowerCase() || 'unknown', fecha: ad.fecha || '', presupuesto: ad.presupuesto || 0, gasto: ad.gasto || 0, impresiones: ad.impresiones || 0, clics: ad.clics || 0, compras: ad.compras || 0, roas: ad.roas || 0, ctr: ad.ctr || 0, cpc: ad.cpc || 0, cpm: ad.cpm || 0, frecuencia: ad.frecuencia || 0, })); const sm = k => campanas.reduce((s, c) => s + (c[k] || 0), 0); const gt = sm('gasto'), im = sm('impresiones'), cl = sm('clics'), co = sm('compras'); S.meta = { campanas, gastoTotal: gt, impresiones: im, clics: cl, compras: co, ctr: im > 0 ? (cl / im) * 100 : 0, cpa: co > 0 ? gt / co : 0, ultimaActualizacion: fecha, esLive: true, }; // Persistir en servidor para sobrevivir refresh try { guardarDatosServidor(); } catch(e) {} // Actualizar UI renderMeta(); renderDashboard(); renderPedidos(); // Mostrar timestamp en sidebar const ts = document.getElementById('last-update'); if (ts) ts.textContent = 'Meta: ' + new Date(fecha).toLocaleTimeString('es-CL'); // Info en config const det = document.getElementById('meta-vivo-detalles'); if (det) { det.innerHTML = '● EN VIVO · ' + campanas.length + ' anuncios · Última actualización: ' + new Date(fecha).toLocaleTimeString('es-CL') + '
Próxima actualización en 15 minutos'; } } function mostrarMetaStatus(msg, ok) { const el = document.getElementById('meta-vivo-status'); if (!el) return; el.style.display = 'block'; if (ok === true) { el.style.background = 'var(--green-bg)'; el.style.color = 'var(--green)'; } else if (ok === false) { el.style.background = 'var(--red-bg)'; el.style.color = 'var(--red)'; } else { el.style.background = 'var(--amber-bg)'; el.style.color = 'var(--amber)'; } el.textContent = msg; } // ── SHOPIFY EN VIVO ────────────────────────────────────────────────────────── async function conectarShopify(){ const shopUrl = document.getElementById('shopify-url')?.value?.trim(); const clientId = document.getElementById('shopify-client-id')?.value?.trim(); const clientSecret = document.getElementById('shopify-client-secret')?.value?.trim(); const statusEl = document.getElementById('shopify-status'); const infoEl = document.getElementById('shopify-info'); if(!shopUrl||!clientId||!clientSecret){ mostrarShopifyStatus('Completa URL, Client ID y Client Secret', false); return; } mostrarShopifyStatus('Conectando con Shopify...', null); try { // 1. Guardar credenciales const r = await apiFetch('/api/shopify/token', {method:'POST', body:JSON.stringify({shopUrl, clientId, clientSecret})}); if(!r.ok){ mostrarShopifyStatus('Error guardando credenciales', false); return; } // 2. Probar conexión const r2 = await apiFetch('/api/shopify/test'); const data = await r2.json(); if(!r2.ok){ mostrarShopifyStatus('Error: '+data.error, false); return; } mostrarShopifyStatus('✅ Conectado a Shopify', true); localStorage.setItem('df_shopify_live', JSON.stringify({shopUrl, conectado:true})); // Mostrar info de la tienda if(infoEl){ infoEl.style.display='block'; const det=document.getElementById('shopify-detalles'); if(det) det.innerHTML=''+data.shop+'
'+data.domain+' · Plan: '+data.plan+''; } toast('✅ Shopify conectado — '+data.shop); // 3. Sincronizar pedidos automáticamente await sincronizarShopify(); } catch(e) { mostrarShopifyStatus('Error: '+e.message, false); } } async function sincronizarShopify(){ const desde = S.filtroDesde || new Date(Date.now()-30*24*60*60*1000).toISOString().split('T')[0]; const hasta = S.filtroHasta || new Date().toISOString().split('T')[0]; mostrarShopifyStatus('Sincronizando pedidos...', null); try { const flete = parseFloat(document.getElementById('shopify-flete')?.value||0); const r = await apiFetch('/api/shopify/orders?desde='+desde+'&hasta='+hasta+'&flete_promedio='+flete); const data = await r.json(); if(!r.ok){ mostrarShopifyStatus('Error: '+data.error, false); return; } // Merge con pedidos existentes de Dropi (no reemplazar, complementar) const shopifyIds = new Set(data.pedidos.map(p=>p.id)); const pedidosBase = S.pedidos.filter(p=>!shopifyIds.has(p.id)); S.pedidos = [...pedidosBase, ...data.pedidos.map(p=>({...p, fuente:'shopify'}))]; await guardarDatosServidor(); renderDashboard(); renderPedidos(); mostrarRangoArchivo(S.pedidos); const msg = '✅ '+data.pedidos.length+' pedidos de Shopify sincronizados'; mostrarShopifyStatus(msg, true); if(data.avisos?.length){ data.avisos.forEach(a=>toast('ℹ️ '+a)); // Mostrar también en status if(data.stats?.sinCosto>0){ mostrarShopifyStatus(msg+' ('+data.stats.sinCosto+' sin costo proveedor)', true); } } toast(msg); } catch(e) { mostrarShopifyStatus('Error sincronizando: '+e.message, false); } } function mostrarShopifyStatus(msg, ok){ const el = document.getElementById('shopify-status'); if(!el) return; el.style.display='block'; el.style.background = ok===true?'var(--green-bg)':ok===false?'var(--red-bg)':'var(--amber-bg)'; el.style.color = ok===true?'var(--green)':ok===false?'var(--red)':'var(--amber)'; el.style.border = '1px solid '+(ok===true?'rgba(0,230,153,0.2)':ok===false?'rgba(244,63,94,0.2)':'rgba(251,191,36,0.2)'); el.textContent = msg; } // ── DROPI EN VIVO ───────────────────────────────────────────────────────────── async function conectarDropiVivo() { const email = document.getElementById('dropi-email')?.value?.trim(); const password = document.getElementById('dropi-password')?.value?.trim(); const infoEl = document.getElementById('dropi-vivo-info'); if (!email || !password) { mostrarDropiStatus('Completa email y contraseña', false); return; } mostrarDropiStatus('Iniciando sesión en Dropi...', null); try { // Login con email + password const r = await apiFetch('/api/dropi/login', { method: 'POST', body: JSON.stringify({ email, password }) }); const data = await r.json(); if (!r.ok) { mostrarDropiStatus('Error: ' + (data.error || 'Credenciales incorrectas'), false); return; } mostrarDropiStatus('✓ Sesión iniciada — cargando pedidos...', null); localStorage.setItem('df_dropi_live', JSON.stringify({ email, conectado: true })); // Cargar pedidos await cargarDropiVivo(); mostrarDropiStatus('✓ Dropi conectado — pedidos sincronizados', true); if (infoEl) { infoEl.style.display = 'block'; const det = document.getElementById('dropi-vivo-detalles'); if (det) det.innerHTML = '● EN VIVO · '+S.pedidos.length+' pedidos · '+new Date().toLocaleTimeString('es-CL'); } toast('✓ Dropi conectado en vivo — ' + S.pedidos.length + ' pedidos'); } catch (e) { mostrarDropiStatus('Error: ' + e.message, false); } } async function cargarDropiVivo() { const desde = S.filtroDesde || new Date(Date.now()-30*24*60*60*1000).toISOString().split('T')[0]; const hasta = S.filtroHasta || new Date().toISOString().split('T')[0]; const r = await apiFetch('/api/dropi/pedidos?desde='+desde+'&hasta='+hasta); if (!r.ok) { const err = await r.json(); throw new Error(err.error || 'Error cargando Dropi'); } const data = await r.json(); if (data.pedidos?.length) { S.pedidos = data.pedidos.map(p => ({...p, fecha: toISOfecha(p.fecha)||p.fecha})); guardarDatosServidor(); renderDashboard(); renderPedidos(); const dot = document.getElementById('sync-dot'); if (dot) dot.className = 'sync-dot live'; tx('sync-label', data.pedidos.length + ' pedidos en vivo'); const det = document.getElementById('dropi-vivo-detalles'); if (det) det.innerHTML = '● EN VIVO · ' + data.pedidos.length + ' pedidos · Última sync: ' + new Date().toLocaleTimeString('es-CL'); } } function mostrarDropiStatus(msg, ok) { const el = document.getElementById('dropi-vivo-status'); if (!el) return; el.style.display = 'block'; if (ok === true) { el.style.background = 'var(--green-bg)'; el.style.color = 'var(--green)'; } else if (ok === false) { el.style.background = 'var(--red-bg)'; el.style.color = 'var(--red)'; } else { el.style.background = 'var(--amber-bg)'; el.style.color = 'var(--amber)'; } el.textContent = msg; } async function intentarReconectarMeta() { const saved = localStorage.getItem('df_meta_live'); if (!saved) return; try { // Set default period (this month) if no filter set if(!S.filtroDesde){ const hoy=new Date(); S.filtroDesde=new Date(hoy.getFullYear(),hoy.getMonth(),1).toISOString().split('T')[0]; S.filtroHasta=hoy.toISOString().split('T')[0]; } await cargarMetaLive(); _metaConectado = true; if (_metaLiveInterval) clearInterval(_metaLiveInterval); _metaLiveInterval = setInterval(cargarMetaLive, 15 * 60 * 1000); console.log('Meta Ads reconectado automáticamente'); } catch (e) { console.warn('No se pudo reconectar Meta:', e.message); // Si falla la reconexión pero hay datos en DB, los mostramos igual if(S.meta) { renderMeta(); renderDashboard(); } } } function setupDnD(){ const ov=$('drop-overlay');let n=0; const showOv=()=>{if(ov)ov.style.display='flex';}; const hideOv=()=>{if(ov)ov.style.display='none';}; document.addEventListener('dragenter',e=>{e.preventDefault();n++;showOv();}); document.addEventListener('dragleave',()=>{n--;if(n<=0){n=0;hideOv();}}); document.addEventListener('dragover',e=>e.preventDefault()); document.addEventListener('drop',e=>{e.preventDefault();n=0;hideOv();ov?.classList.remove('visible');const file=e.dataTransfer?.files?.[0];if(!file)return;const nm=file.name.toLowerCase();if(nm.endsWith('.xlsx')||nm.endsWith('.xls'))handleDropiFile(file);else if(nm.endsWith('.csv'))handleMetaFile(file);else toast('Formato no reconocido',true);}); ['dz-dropi','dz-meta'].forEach(id=>{const dz=$(id);if(!dz)return;dz.addEventListener('dragover',e=>{e.preventDefault();e.stopPropagation();dz.classList.add('drag-over');});dz.addEventListener('dragleave',()=>dz.classList.remove('drag-over'));dz.addEventListener('drop',e=>{e.preventDefault();e.stopPropagation();dz.classList.remove('drag-over');const file=e.dataTransfer?.files?.[0];if(!file)return;if(id==='dz-dropi')handleDropiFile(file);else handleMetaFile(file);});}); } // ═══ INIT ═══ document.addEventListener('DOMContentLoaded',()=>{ tx('date-badge',new Date().toLocaleDateString('es-CL',{weekday:'long',day:'numeric',month:'long'})); setupDnD(); // Entrar directo — sin login, sin backend mostrarApp(); renderPedidos(); tx('sync-label','Sin datos — importa tu Excel'); setTimeout(mostrarMensajeBienvenida, 1500); }); // ═══ IA DROPFLOW ═══ function construirContexto(){ if(!S.pedidos.length) return null; const R=calcR(S.pedidos); const top5Margen=[...S.pedidos].filter(p=>p.estado==='entregado'&&p.venta>0).sort((a,b)=>b.margen-a.margen).slice(0,5); const top5Util=[...S.pedidos].filter(p=>p.estado==='entregado').sort((a,b)=>b.utilidad-a.utilidad).slice(0,5); const ciudades={}; S.pedidos.filter(p=>p.estado==='entregado').forEach(p=>{if(!ciudades[p.ciudad])ciudades[p.ciudad]={venta:0,util:0,n:0};ciudades[p.ciudad].venta+=p.venta;ciudades[p.ciudad].util+=p.utilidad;ciudades[p.ciudad].n++;}); const topCiudades=Object.entries(ciudades).sort((a,b)=>b[1].venta-a[1].venta).slice(0,5).map(([c,d])=>({ciudad:c,venta:d.venta,utilidad:d.util,pedidos:d.n,margen:d.venta>0?(d.util/d.venta*100).toFixed(1)+'%':'0%'})); const productos={}; S.pedidos.filter(p=>p.estado==='entregado').forEach(p=>{const k=p.producto;if(!productos[k])productos[k]={venta:0,util:0,n:0,proveedor:0,flete:0};productos[k].venta+=p.venta;productos[k].util+=p.utilidad;productos[k].n++;productos[k].proveedor+=p.proveedor;productos[k].flete+=p.flete;}); const topProductos=Object.entries(productos).sort((a,b)=>b[1].util-a[1].util).slice(0,5).map(([p,d])=>({producto:p,ventas:d.venta,utilidad:d.util,pedidos:d.n,margen:d.venta>0?(d.util/d.venta*100).toFixed(1)+'%':'0%'})); let metaCtx='No hay datos de Meta Ads importados.'; if(S.meta){ const campRank=[...S.meta.campanas].filter(c=>c.gasto>0).sort((a,b)=>{const roasA=a.compras>0?a.compras*19990/a.gasto:0;const roasB=b.compras>0?b.compras*19990/b.gasto:0;return roasB-roasA;}); metaCtx='Gasto total Meta Ads: $'+Math.round(S.meta.gastoTotal).toLocaleString('es-CL')+' CLP\nCTR promedio: '+S.meta.ctr.toFixed(2)+'%\nCPA global: $'+Math.round(S.meta.cpa).toLocaleString('es-CL')+' CLP\nCompras atribuidas: '+S.meta.compras+'\nCampañas/anuncios ('+S.meta.campanas.length+' total):\n'+campRank.slice(0,8).map(c=>' - '+c.nombre+': gasto $'+Math.round(c.gasto).toLocaleString('es-CL')+', compras '+c.compras+', CPA $'+(c.compras>0?Math.round(c.gasto/c.compras).toLocaleString('es-CL'):'N/A')).join('\n'); } const ctx=[]; ctx.push('Eres la IA de Dropflow, asistente de contabilidad y análisis para un negocio de dropshipping en Chile.'); ctx.push('Tienes acceso a los datos reales del negocio. Responde siempre en español, de forma directa y con números concretos.'); ctx.push('Usa formato claro con negritas para los datos importantes. Cuando muestres montos usa formato CLP (pesos chilenos).'); ctx.push(''); ctx.push('=== RESUMEN FINANCIERO ==='); ctx.push('Total pedidos: '+R.total); ctx.push('Pedidos entregados: '+R.entregados); ctx.push('En tránsito/pendientes: '+(R.enTransito+R.pendientes)); ctx.push('Devueltos: '+R.devueltos+' | Cancelados: '+R.cancelados); ctx.push('Tasa real de entrega: '+R.tasaReal.toFixed(1)+'%'); ctx.push(''); ctx.push('Ingresos reales: $'+Math.round(R.ingresosReales).toLocaleString('es-CL')+' CLP'); ctx.push('Costo proveedor: $'+Math.round(R.costoProveedor).toLocaleString('es-CL')+' CLP'); ctx.push('Costo flete: $'+Math.round(R.costoFlete).toLocaleString('es-CL')+' CLP'); ctx.push('Gasto Meta Ads: $'+Math.round(R.adsGasto).toLocaleString('es-CL')+' CLP'); ctx.push('Costos totales: $'+Math.round(R.costosTotal).toLocaleString('es-CL')+' CLP'); ctx.push('Utilidad real (con ads): $'+Math.round(R.utilidadReal).toLocaleString('es-CL')+' CLP'); ctx.push('Margen real: '+R.margenReal.toFixed(1)+'%'); const mgOp=R.ingresosReales>0?((R.ingresosReales-R.costoProveedor-R.costoFlete)/R.ingresosReales*100).toFixed(1):0; ctx.push('Margen operacional (sin ads): '+mgOp+'%'); ctx.push('Proyeccion pendientes 75%: $'+Math.round(R.proyUtilidad).toLocaleString('es-CL')+' CLP'); ctx.push('Utilidad total estimada: $'+Math.round(R.utilidadTotal).toLocaleString('es-CL')+' CLP'); ctx.push(''); ctx.push('=== TOP 5 PEDIDOS POR MARGEN ==='); top5Margen.forEach((p,i)=>ctx.push((i+1)+'. ID '+p.id+' | '+p.producto+' | Venta: $'+Math.round(p.venta).toLocaleString('es-CL')+' | Utilidad: $'+Math.round(p.utilidad).toLocaleString('es-CL')+' | Margen: '+p.margen.toFixed(1)+'% | Ciudad: '+p.ciudad)); ctx.push(''); ctx.push('=== TOP 5 PEDIDOS POR UTILIDAD ==='); top5Util.forEach((p,i)=>ctx.push((i+1)+'. ID '+p.id+' | '+p.producto+' | Venta: $'+Math.round(p.venta).toLocaleString('es-CL')+' | Utilidad: $'+Math.round(p.utilidad).toLocaleString('es-CL')+' | Margen: '+p.margen.toFixed(1)+'%')); ctx.push(''); ctx.push('=== TOP CIUDADES ==='); topCiudades.forEach((c,i)=>ctx.push((i+1)+'. '+c.ciudad+': '+c.pedidos+' pedidos | Venta $'+Math.round(c.venta).toLocaleString('es-CL')+' | Utilidad $'+Math.round(c.utilidad).toLocaleString('es-CL')+' | Margen '+c.margen)); ctx.push(''); ctx.push('=== TOP PRODUCTOS ==='); topProductos.forEach((p,i)=>ctx.push((i+1)+'. '+p.producto+': '+p.pedidos+' pedidos | Venta $'+Math.round(p.ventas).toLocaleString('es-CL')+' | Utilidad $'+Math.round(p.utilidad).toLocaleString('es-CL')+' | Margen '+p.margen)); ctx.push(''); ctx.push('=== META ADS ==='); ctx.push(metaCtx); ctx.push(''); ctx.push('=== PARAMETROS ==='); ctx.push('Tasa de entrega configurada: '+S.tasaEntrega+'%'); ctx.push('Umbral de alerta margen: '+S.umbralMargen+'%'); return ctx.join('\n') } let iaHistorial=[]; function autoResizeIA(el){el.style.height='44px';el.style.height=Math.min(el.scrollHeight,120)+'px';} function agregarMensaje(texto,esUser=false){ const cont=$('ia-mensajes');if(!cont)return; const div=document.createElement('div'); div.className='ia-msg '+(esUser?'ia-msg-user':'ia-msg-bot'); const avatar=document.createElement('div'); avatar.className='ia-avatar'; avatar.textContent=esUser?'👤':'🤖'; const burbuja=document.createElement('div'); burbuja.className='ia-burbuja'; // Formatear markdown básico const html='

'+texto.replace(/\*\*(.*?)\*\*/g,'$1').replace(/\n\n/g,'

').replace(/\n/g,'
')+'

'; burbuja.innerHTML=html; div.appendChild(avatar);div.appendChild(burbuja); cont.appendChild(div); cont.scrollTop=cont.scrollHeight; } function mostrarTyping(){ const cont=$('ia-mensajes');if(!cont)return null; const div=document.createElement('div'); div.className='ia-msg ia-msg-bot';div.id='ia-typing-indicator'; div.innerHTML='
🤖
'; cont.appendChild(div);cont.scrollTop=cont.scrollHeight; return div; } async function enviarMensajeIA(){ const input=$('ia-input');if(!input)return; const texto=input.value.trim();if(!texto)return; if(!S.pedidos.length){ agregarMensaje('⚠️ Primero importa el Excel de Dropi para que pueda analizar tus datos.',false); return; } agregarMensaje(texto,true); input.value='';input.style.height='44px'; const btn=$('ia-btn-enviar');if(btn)btn.style.opacity='0.5'; const typing=mostrarTyping(); // Agregar al historial iaHistorial.push({role:'user',content:texto}); try{ const contexto=construirContexto(); const mensajes=[]; // Incluir historial (máximo últimos 8 mensajes para no exceder tokens) const histReciente=iaHistorial.slice(-8); histReciente.forEach(m=>mensajes.push(m)); const resp=await apiFetch('/api/ia',{ method:'POST', body:JSON.stringify({ model:'claude-haiku-4-5-20251001', max_tokens:1000, system:contexto, messages:mensajes }) }); const data=await resp.json(); if(typing)typing.remove(); if(data.content&&data.content[0]){ const respTexto=data.content[0].text; iaHistorial.push({role:'assistant',content:respTexto}); agregarMensaje(respTexto,false); } else if(data.error){ agregarMensaje(`Error: ${data.error.message}`,false); } }catch(err){ if(typing)typing.remove(); agregarMensaje('Error conectando con la IA. Verifica tu conexión e intenta de nuevo.',false); console.error(err); }finally{ if(btn)btn.style.opacity='1'; } } function preguntarIA(pregunta){ navegarA('ia'); setTimeout(()=>{ const input=$('ia-input'); if(input){input.value=pregunta;input.focus();enviarMensajeIA();} },100); } /* ───────────────────────────────────────────────────── DEMO PATCHES — overwrite server functions after decl. apiFetch: short-circuits demo token before any fetch. cargarDatosServidor: returns demo data, no network. guardarDatosServidor: no-op, no network. cerrarSesion: clears demo state cleanly. Missing UI fields: populated after mostrarApp(). ───────────────────────────────────────────────────── */ if (typeof DEMO_MODE !== 'undefined' && DEMO_MODE) { /* Auto-authenticate: set TOKEN + USUARIO before DOMContentLoaded fires so the main init finds them and calls mostrarApp() directly */ TOKEN = DEMO_TOKEN; USUARIO = { nombre: 'Felipe', email: 'demo@dropflow.cl', plan: 'pro' }; localStorage.setItem('df_token', DEMO_TOKEN); localStorage.setItem('df_usuario', JSON.stringify(USUARIO)); /* Override hacerLogin so the login button also works in demo mode */ hacerLogin = async function(){ TOKEN = DEMO_TOKEN; USUARIO = { nombre: 'Felipe', email: 'demo@dropflow.cl', plan: 'pro' }; await cargarDatosServidor(); mostrarApp(); }; var _origApiFetch = apiFetch; apiFetch = function(url, opts){ if (TOKEN === DEMO_TOKEN) { /* IA endpoint — let it show a friendly message */ if (url.includes('/api/ia')) { return Promise.resolve({ ok:true, json:function(){ return Promise.resolve({ content:[{text:'**Modo demo activo.** Importa tu Excel de Dropi y CSV de Meta Ads para activar el análisis con datos reales. Tus datos nunca salen de tu navegador.'}] });} }); } /* All other endpoints — silent 401-style resolve */ return Promise.resolve({ ok:false, status:403, json:function(){ return Promise.resolve({error:'demo-mode'}); } }); } return _origApiFetch(url, opts); }; /* Replace server data functions */ cargarDatosServidor = function(){ S.pedidos = DEMO_PEDIDOS; S.meta = DEMO_META; S.tasaEntrega = 75; S.umbralMargen = 18; S.cpaMax = 4000; S.cpcMax = 250; S.cpmMax = 4500; S.ctrMin = 1.5; S.roasMin = 2.0; return Promise.resolve(); }; guardarDatosServidor = function(){ return Promise.resolve(true); }; /* cerrarSesion: clean demo state, show login screen */ var _origCerrar = cerrarSesion; cerrarSesion = function(){ localStorage.removeItem('df_token'); localStorage.removeItem('df_usuario'); TOKEN = null; USUARIO = null; S.pedidos = []; S.meta = null; var ls = document.getElementById('login-screen'); if (ls) ls.style.display = 'flex'; var sb = document.querySelector('.sidebar'); if (sb) sb.style.display = 'none'; var mn = document.querySelector('.main'); if (mn) mn.style.display = 'none'; }; /* After app renders, populate UI elements */ document.addEventListener('DOMContentLoaded', function(){ setTimeout(function(){ /* Topbar sync indicator */ var dot = document.getElementById('sync-dot'); if (dot) { dot.className = 'sync-dot live'; } var lbl = document.getElementById('sync-label'); if (lbl) lbl.textContent = DEMO_PEDIDOS.length + ' pedidos · DEMO'; /* Demo toast */ if (typeof toast === 'function') { setTimeout(function(){ toast('Modo demo — importa tu Excel de Dropi para datos reales'); }, 1200); } }, 400); }); } ''+'