* Próximamente · Pagos seguros via Mercado Pago · Cancela cuando quieras
Contabilidad en vivo
Seleccionar período
RANGO PERSONALIZADO
DESDE
→
HASTA
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
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%
ID
Fecha
Producto
Estado
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
—
🔍 Diagnóstico del embudo publicitario
Campaña:
⬇ EMBUDO GLOBAL
⬇ POR CAMPAÑA
💡 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
Presupuesto total
$0
— días
Gastado hasta hoy
$0
0% del presupuesto
Saldo restante
$0
— días restantes
Gasto diario promedio
$0
recomendado: $0/día
EJECUCIÓN DEL PRESUPUESTO
Tiempo transcurrido
Gasto real
📈 Proyección
🎯 Recomendaciones
🔚 Estrategias de cierre de período
📅 Gasto día a día vs ideal
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
PERÍODO A (actual)
→
PERÍODO B (comparar con)
→
Importa datos de Dropi para ver la comparación
Proyección basada en pedidos reales75% 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?
👥 Gestión de usuarios
Crear nuevo usuario
Nombre
Email
Contraseña
Rol
⚙️ 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
CONECTADO ✅
⚠️ 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
ESTADO DE CONEXIÓN
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
⚠️ El token de Meta expira cada ~60 días.
Si Meta Ads no carga, genera uno nuevo en: developers.facebook.com → Graph API Explorer → Generate Access Token
ESTADO DE CONEXIÓN
O importar manualmente (CSV)
📁
Arrastra CSV de Meta Ads
exportación manual desde Ads Manager
📂
Suelta el archivo aquí
Excel de Dropi (.xlsx) o CSV de Meta Ads (.csv)
💡 Historial de Insights
Alertas cerradas · ordenadas por fecha
No hay insights guardados todavía. Cierra una alerta del dashboard para guardarla acá.
';
}
// ── 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='