// SATMELI — Dashboard con 3 variantes de tarjeta: monitor / ticket / radar. // ── Radar scope animado (panel oscuro embebido) ─────────────────────────── function RadarScope({ count, active, size = 124, blips = [] }) { const cx = size / 2, r = size / 2 - 6; return (
{[1, 0.66, 0.33].map((f, i) => ( ))} {/* blips */} {blips.map((b, i) => { const a = b.a * Math.PI / 180, rad = b.r * r; const x = cx + Math.cos(a) * rad, y = cx + Math.sin(a) * rad; return ; })} {/* sweep */} {active && ( )}
{count}
HALLAZGOS
); } // blips derivados del id de búsqueda (estables) function blipsFor(s) { const out = []; const n = Math.min(6, Math.max(2, Math.round(s.matches / 3))); for (let i = 0; i < n; i++) { out.push({ a: (i * 73 + s.id.charCodeAt(1) * 31) % 360, r: 0.3 + ((i * 0.17 + 0.2) % 0.65), big: i === 0 }); } return out; } function CatChip({ cat }) { const c = SAT_CATS[cat]; return ( {c.label} ); } function priceRangeLabel(s) { if (s.priceMin && s.priceMax) return `${fmtK(s.priceMin)}–${fmtK(s.priceMax)}`; if (s.priceMax) return `hasta ${fmtARS(s.priceMax)}`; if (s.priceMin) return `desde ${fmtARS(s.priceMin)}`; return 'sin tope'; } function MenuBtn({ onClick, name = 'dots' }) { return ( ); } // ── Variante MONITOR ─────────────────────────────────────────────────────── function CardMonitor({ s, onOpen, onToggle }) { return (
{s.active ? 'EN VIVO' : 'PAUSADA'}
{s.term}
{priceRangeLabel(s)} {condLabel(s.condition)} {s.location}
{s.matches} coincid.
{s.active ? `revisado ${ago(s.lastCheck)}` : 'monitoreo en pausa'}
{freqLabel(s.freq).toUpperCase()}
); } // ── Variante TICKET / RECIBO ─────────────────────────────────────────────── function Perfs({ where }) { return (
{Array.from({ length: 16 }).map((_, i) => ( ))}
); } function ReceiptRow({ k, v, accent }) { return (
{k} {v}
); } function CardTicket({ s, onOpen, onToggle }) { return (
SATMELI · MONITOR {s.active ? 'ACTIVA' : 'EN PAUSA'}
{s.term}
#{s.id.toUpperCase()} · alta {ago(s.createdAt * 60).replace('hace ', 'hace ')}
{/* tear line */}
COINCIDENCIAS
{s.matches}
{/* faux barcode */}
{[3,1,2,1,3,2,1,1,3,1,2,3,1,2,1,3,1].map((w, i) => ( ))}
); } // ── Variante RADAR ───────────────────────────────────────────────────────── function CardRadar({ s, onOpen, onToggle }) { return (
{s.active ? '◈ RASTREANDO' : '◈ EN PAUSA'}
{s.term}
{priceRangeLabel(s)} {condLabel(s.condition)}
{s.active ? `revisado ${ago(s.lastCheck)}` : 'pausada'} · {freqLabel(s.freq)}
); } const CARD_VARIANTS = { monitor: CardMonitor, ticket: CardTicket, radar: CardRadar }; // ── Dashboard ────────────────────────────────────────────────────────────── function Dashboard({ searches, notifs, variant, mode, onOpen, onToggle, onCreate }) { const Card = CARD_VARIANTS[variant] || CardMonitor; const active = searches.filter((s) => s.active).length; const unread = notifs.filter((n) => !n.read).length; const todayMatches = notifs.filter((n) => n.foundAt < 1440).length; const desktop = mode === 'desktop'; const stats = [ { k: 'Centinelas activos', v: `${active}/${searches.length}`, icon: 'radar' }, { k: 'Hallazgos hoy', v: todayMatches, icon: 'zap', accent: true }, { k: 'Sin leer', v: unread, icon: 'bell', alert: unread > 0 }, ]; return (
{/* header */}
Panel de monitoreo

Tus centinelas

{active} {active === 1 ? 'búsqueda vigilando' : 'búsquedas vigilando'} Mercado Libre por vos.

{desktop && Nueva búsqueda}
{/* stats strip */}
{stats.map((st) => (
{st.k}
{st.v}
))}
{/* cards */}
{searches.map((s, i) => (
onOpen(s.id)} onToggle={() => onToggle(s.id)} />
))}
); } Object.assign(window, { Dashboard, RadarScope, CardMonitor, CardTicket, CardRadar, priceRangeLabel, CatChip });