/* Live dashboard mockup with animated chart + AI typing */ const useTicker = (interval = 1500) => { const [tick, setTick] = React.useState(0); React.useEffect(() => { const id = setInterval(() => setTick(t => t + 1), interval); return () => clearInterval(id); }, [interval]); return tick; }; const AreaChart = () => { const tick = useTicker(2200); const points = React.useMemo(() => { const n = 28; let v = 50 + Math.random() * 20; const out = []; for (let i = 0; i < n; i++) { v += (Math.random() - 0.45) * 16; v = Math.max(20, Math.min(96, v)); out.push(v); } return out; }, [tick]); const points2 = React.useMemo(() => { return points.map(p => Math.max(15, Math.min(80, p - 12 - Math.random() * 12))); }, [points]); const W = 600, H = 130, P = 4; const path = (arr) => { return arr.map((y, i) => { const x = P + (i / (arr.length - 1)) * (W - P*2); const yy = H - P - (y / 100) * (H - P*2); return `${i === 0 ? 'M' : 'L'} ${x.toFixed(1)} ${yy.toFixed(1)}`; }).join(' '); }; const area = (arr) => { return path(arr) + ` L ${W - P} ${H - P} L ${P} ${H - P} Z`; }; return ( {[0,1,2,3].map(i => ( ))} {[0,1,2,3].map(i => ( ))} ); }; const TypingAI = () => { const messages = [ { p: 'Detecté un patrón en las citas de mañana. ', hl: '3 clientes recurrentes podrían cambiar al horario AM.', s: ' Sugiero contactarlos hoy.' }, { p: '', hl: 'Inventario crítico:', s: ' 4 productos bajo el umbral. Generé orden automática a proveedor.' }, { p: 'El restaurante de Sur tiene ', hl: '+18% de ventas los jueves.', s: ' Activo promo automatizada para replicar en otras sucursales.' }, ]; const [idx, setIdx] = React.useState(0); const [shown, setShown] = React.useState(''); const [phase, setPhase] = React.useState(0); // 0: prefix, 1: hl, 2: suffix, 3: hold React.useEffect(() => { const m = messages[idx]; let target = ''; if (phase === 0) target = m.p; else if (phase === 1) target = m.p + m.hl; else if (phase === 2) target = m.p + m.hl + m.s; if (phase === 3) { const id = setTimeout(() => { setIdx(i => (i + 1) % messages.length); setShown(''); setPhase(0); }, 4200); return () => clearTimeout(id); } if (shown.length < target.length) { const id = setTimeout(() => setShown(target.slice(0, shown.length + 1)), 22 + Math.random() * 18); return () => clearTimeout(id); } else { setPhase(p => p + 1); } }, [shown, phase, idx]); // render with highlighting const m = messages[idx]; const pLen = m.p.length; const hlLen = m.hl.length; const visible = shown; const partP = visible.slice(0, Math.min(pLen, visible.length)); const partHL = visible.slice(pLen, Math.min(pLen + hlLen, visible.length)); const partS = visible.slice(pLen + hlLen); return (

{partP}{partHL}{partS}

); }; const useCount = (target, duration = 1600) => { const [v, setV] = React.useState(0); const ref = React.useRef(null); React.useEffect(() => { let started = false; const obs = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting && !started) { started = true; const start = performance.now(); const tick = (now) => { const t = Math.min(1, (now - start) / duration); const eased = 1 - Math.pow(1 - t, 3); setV(Math.round(target * eased)); if (t < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); } }); }, { threshold: 0.4 }); if (ref.current) obs.observe(ref.current); return () => obs.disconnect(); }, [target, duration]); return [v, ref]; }; const Counter = ({ target, suffix = '', prefix = '' }) => { const [v, ref] = useCount(target); return {prefix}{v.toLocaleString('es-MX')}{suffix}; }; const KPI = ({ label, value, delta, down }) => { const tick = useTicker(2400); const display = React.useMemo(() => { const variation = (Math.sin(tick * 1.3) * 0.04) + 1; if (typeof value === 'number') return Math.round(value * variation).toLocaleString('es-MX'); return value; }, [tick, value]); return (
{label}
{display}
{down ? '↓' : '↑'} {delta}
); }; const Icon = ({ name, size = 16 }) => { const paths = { home: <>, activity: , users: <>, box: <>, settings: <>, chart: <>, bell: <>, sparkles: <>, }; return ( {paths[name]} ); }; // Section content components const AIPopup = () => { const [open, setOpen] = React.useState(false); return ( <> {open && (
setOpen(false)} style={{ position: 'absolute', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 99, borderRadius: 8, }} /> )} {open && (
Asistente IA
)} ); }; const ResumenView = () => (
Flujo de pacientes · 28 días Atendidos Programados
MR
María Rodríguez · Consulta general
15:00 Confirmada
JL
Javier López · Seguimiento
15:30 Recordatorio enviado
AC
Ana Carrillo · Primera vez
16:00 Falta confirmar
Asistente IA
); const PacientesView = () => (
Paciente Última visita Estado Acciones
{[ { name: 'María Rodríguez', last: '15 abr 2026', status: 'Activo' }, { name: 'Javier López', last: '22 abr 2026', status: 'Activo' }, { name: 'Ana Carrillo', last: '29 abr 2026', status: 'Pendiente' }, { name: 'Carlos Méndez', last: '10 abr 2026', status: 'Inactivo' }, { name: 'Laura Vega', last: '18 abr 2026', status: 'Activo' }, ].map((p, i) => (
{p.name} {p.last} {p.status}
))}
); const CitasView = () => (
{[ { name: 'María Rodríguez · Consulta general', time: '15:00', status: 'Confirmada' }, { name: 'Javier López · Seguimiento', time: '15:30', status: 'info', statusText: 'Recordatorio enviado' }, { name: 'Ana Carrillo · Primera vez', time: '16:00', status: 'warn', statusText: 'Falta confirmar' }, { name: 'Carlos Méndez · Cardiología', time: '16:30', status: 'Confirmada' }, { name: 'Laura Vega · Dermatología', time: '17:00', status: 'Confirmada' }, { name: 'Roberto Sánchez · Laboratorio', time: '17:30', status: 'info', statusText: 'En espera' }, ].map((c, i) => (
{c.name.split(' ').map(n => n[0]).join('')}
{c.name}
{c.time} {c.statusText || c.status}
))}
); const InventarioView = () => (
Producto Categoría Stock Estado
{[ { name: 'Paracetamol 500mg', cat: 'Medicamentos', stock: 145, status: 'OK' }, { name: 'Ibuprofeno 400mg', cat: 'Medicamentos', stock: 89, status: 'OK' }, { name: 'Guantes quirúrgicos', cat: 'Suministros', stock: 23, status: 'warn', statusText: 'Bajo' }, { name: 'Jeringas 5ml', cat: 'Suministros', stock: 156, status: 'OK' }, { name: 'Alcohol etílico', cat: 'Limpieza', stock: 8, status: 'warn', statusText: 'Crítico' }, ].map((p, i) => (
{p.name} {p.cat} {p.stock} {p.statusText || p.status}
))}
); const AsistenteView = () => (
Asistente IA · Historial
Sugerencia 14:25

Detecté un patrón en las citas de mañana. 3 clientes recurrentes podrían cambiar al horario AM. Sugiero contactarlos hoy.

Alerta 14:18

Inventario crítico: 4 productos bajo el umbral. Generé orden automática a proveedor.

Oportunidad 13:52

El restaurante de Sur tiene +18% de ventas los jueves. Activo promo automatizada para replicar en otras sucursales.

); const ReportesView = () => (
Ingresos por servicio · 6 meses
Servicio Ingresos Citas Tendencia
{[ { name: 'Consulta general', income: '$45,200', citas: 234, trend: '↑ +12%' }, { name: 'Cardiología', income: '$38,100', citas: 156, trend: '↑ +8%' }, { name: 'Dermatología', income: '$29,800', citas: 189, trend: '↑ +15%' }, { name: 'Laboratorio', income: '$22,400', citas: 312, trend: '↓ -3%' }, { name: 'Vacunación', income: '$18,600', citas: 445, trend: '↑ +22%' }, ].map((s, i) => (
{s.name} {s.income} {s.citas} {s.trend}
))}
); const AlertasView = () => (
{[ { type: 'critical', title: 'Stock crítico - Alcohol etílico', desc: '8 unidades restantes. Umbral mínimo: 20', time: 'Hace 5 min' }, { type: 'warning', title: 'No-show elevado - Dr. Méndez', desc: '18% no-show vs promedio 12%. Revisar recordatorios', time: 'Hace 23 min' }, { type: 'info', title: 'Nueva integración - HubSpot', desc: 'Sincronización completada exitosamente', time: 'Hace 1h' }, ].map((a, i) => (
{a.title}
{a.desc}
{a.time} Ver
))}
); const AjustesView = () => (

General

Notificaciones

Integraciones

● Conectado
● Conectado
); const Dashboard = () => { const [activeView, setActiveView] = React.useState('resumen'); const views = { resumen: { title: 'Resumen de hoy', component: ResumenView }, pacientes: { title: 'Pacientes', component: PacientesView }, citas: { title: 'Citas', component: CitasView }, inventario: { title: 'Inventario', component: InventarioView }, asistente: { title: 'Asistente IA', component: AsistenteView }, reportes: { title: 'Reportes', component: ReportesView }, alertas: { title: 'Alertas', component: AlertasView }, ajustes: { title: 'Ajustes', component: AjustesView }, }; const currentView = views[activeView]; return (
app.martzlab.com/clinica-noria

{currentView.title}

Martes · 29 abr · 14:32
); }; window.Dashboard = Dashboard; window.Counter = Counter; window.Icon = Icon;