// Retos de gamificación — "estado sin terminar" para retención diaria
// Basado en framework Nick Saraev: progreso visible, recompensa clara, urgencia.
(function () {
const RETOS_CSS = `
@keyframes reto-shine {
0% { background-position: -200% center; }
100% { background-position: 200% center; }
}
@keyframes reto-ping {
0% { transform: scale(1); opacity: 1; }
70% { transform: scale(2.2); opacity: 0; }
100% { transform: scale(2.2); opacity: 0; }
}
@keyframes reto-pop {
0% { transform: scale(0.7); opacity: 0; }
60% { transform: scale(1.12); }
100% { transform: scale(1); opacity: 1; }
}
.reto-shine-bar {
background: linear-gradient(90deg,
oklch(0.58 0.12 155) 0%,
oklch(0.78 0.18 90) 40%,
oklch(0.58 0.12 155) 100%
);
background-size: 200% auto;
animation: reto-shine 1.8s linear infinite;
}
.reto-badge-pop {
animation: reto-pop 0.45s cubic-bezier(.2,.9,.3,1) both;
}
.reto-ping {
animation: reto-ping 1.4s cubic-bezier(0,.2,.8,1) infinite;
}
`;
if (!document.getElementById('retos-styles')) {
const styleEl = document.createElement('style');
styleEl.id = 'retos-styles';
styleEl.textContent = RETOS_CSS;
document.head.appendChild(styleEl);
}
// ── helpers ─────────────────────────────────────────────
function mesActual(pedidos) {
const now = new Date();
return (pedidos || []).filter(
(p) =>
p.fechaCreacion.getMonth() === now.getMonth() &&
p.fechaCreacion.getFullYear() === now.getFullYear()
);
}
function diasRestantesMes() {
const now = new Date();
const ultimo = new Date(now.getFullYear(), now.getMonth() + 1, 0);
return ultimo.getDate() - now.getDate();
}
// ── Barra de progreso ────────────────────────────────────
function BarraProgreso({ pct, completo }) {
const T = window.T;
return (
= 70
? T.warn
: T.accent,
borderRadius: 999,
transition: 'width 0.7s cubic-bezier(.2,.9,.3,1)',
}}
/>
);
}
// ── Badge de completado ──────────────────────────────────
function BadgeCompleto({ label }) {
const T = window.T;
return (
{label}
);
}
// ── Tarjeta de reto individual ───────────────────────────
function RetoCard({ icono, titulo, descripcion, pct, recompensa, diasRestantes, completo, valorActual, valorMeta, unidad }) {
const T = window.T;
const pctDisplay = Math.min(100, Math.round(pct));
return (
{/* Ping dot — solo si está incompleto y cerca */}
{!completo && pct >= 60 && (
)}
{/* Icono + título */}
{/* Progreso numérico */}
{valorActual}
{' '}/ {valorMeta} {unidad}
= 70 ? T.warn : T.accent,
fontWeight: 600,
}}
>
{pctDisplay}%
{/* Footer: recompensa o badge */}
{completo ? (
) : (
{recompensa}
)}
{!completo && (
{diasRestantes}d
)}
);
}
// ── Sección principal ────────────────────────────────────
function RetosSection({ vendedora, pedidos, billing }) {
const T = window.T;
const { Icon } = window;
const pedidosMes = mesActual(pedidos || []);
const dias = diasRestantesMes();
const state = billing && billing.state ? billing.state : { facturas: [], pagos: [] };
// ── Reto 1: Inicio Rápido ──
// "3 productos en un solo pedido" — mide el pedido con más productos
const maxProductosEnPedido = (pedidos || []).reduce(
(max, p) => Math.max(max, (p.productos || []).reduce((s, x) => s + (x.cantidad || 1), 0)),
0
);
const META_PRODUCTOS = 3;
const pctInicio = (maxProductosEnPedido / META_PRODUCTOS) * 100;
// ── Reto 2: Facturación mensual ──
const facturasAprobadas = (state.facturas || []).filter(
(f) => !f.estado || f.estado === 'procesada'
);
const now = new Date();
const facturasEstesMes = facturasAprobadas.filter((f) => {
const d = f.fecha ? new Date(f.fecha) : null;
return d && d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear();
});
const totalFacturado = facturasEstesMes.reduce(
(s, f) => s + (f.total || f.monto || 0),
0
);
// fallback: suma de pedidos del mes si no hay facturas
const totalFallback = pedidosMes.reduce(
(s, p) => s + (p.productos || []).reduce((a, x) => a + x.precio * x.cantidad, 0),
0
);
const totalMes = totalFacturado > 0 ? totalFacturado : totalFallback;
const META_FACTURACION = 200;
const pctFacturacion = (totalMes / META_FACTURACION) * 100;
// ── Reto 3: Diversidad de marcas ──
const MARCAS_CONOCIDAS = ['Lbel', 'CyZone', 'Esika', 'lbel', 'cyzone', 'esika'];
const marcasUnicas = new Set(
(pedidosMes || []).flatMap((p) =>
(p.productos || []).map((x) => (x.catalogo || x.marca || '').toLowerCase())
)
).size;
const META_MARCAS = 2;
const pctMarcas = (marcasUnicas / META_MARCAS) * 100;
const retos = [
{
id: 'inicio',
icono: 'bolt',
titulo: 'Arranque fuerte',
descripcion: '3+ productos en un pedido',
pct: pctInicio,
completo: maxProductosEnPedido >= META_PRODUCTOS,
valorActual: maxProductosEnPedido,
valorMeta: META_PRODUCTOS,
unidad: 'prod.',
recompensa: '⚡ Vendedora activa',
diasRestantes: dias,
},
{
id: 'facturacion',
icono: 'trending',
titulo: 'Meta del mes',
descripcion: `Factura $${META_FACTURACION} este mes`,
pct: pctFacturacion,
completo: totalMes >= META_FACTURACION,
valorActual: '$' + totalMes.toFixed(0),
valorMeta: '$' + META_FACTURACION,
unidad: '',
recompensa: '🏆 Top vendedora',
diasRestantes: dias,
},
{
id: 'diversidad',
icono: 'sparkle',
titulo: 'Colección completa',
descripcion: 'Vende de 2 marcas distintas',
pct: pctMarcas,
completo: marcasUnicas >= META_MARCAS,
valorActual: marcasUnicas,
valorMeta: META_MARCAS,
unidad: 'marcas',
recompensa: '✨ Catálogo Pro',
diasRestantes: dias,
},
];
const completados = retos.filter((r) => r.completo).length;
return (
{/* Encabezado de sección */}
{completados}/{retos.length} completados
{/* Carrusel de tarjetas */}
{retos.map((r) => (
))}
{/* Hint de estado incompleto */}
{completados < retos.length && (
{dias === 0
? 'Último día de campaña'
: `${dias} ${dias === 1 ? 'día' : 'días'} para cerrar esta campaña`}
)}
);
}
window.RetosSection = RetosSection;
})();