// /public/assets/Analytics.jsx function AnalyticsPage() { const { AppBootstrap } = window.__CRM__; const { Container, Stack, Typography, Paper, Chip, Grid, Divider, CircularProgress, Button, Box, LinearProgress, useMediaQuery } = MaterialUI; // ===== Helpers ===== const isXs = useMediaQuery('(max-width:600px)'); const fmtInt = React.useMemo(() => new Intl.NumberFormat('pt-BR'), []); const parseId = () => { // #/campaigns/:id/analytics const m = (location.hash || '').match(/#\/campaigns\/(\d+)\/analytics/i); return m ? parseInt(m[1], 10) : NaN; }; const fmtDT = (dt) => dt ? dt.replace(' ', ' • ') : '-'; const statusChip = (s) => { const map = { Draft:'default', Scheduled:'info', Running:'success', Paused:'warning', Stopped:'error', Completed:'success', Failed:'error' }; return ; }; const isTerminal = (s) => (s==='Completed'||s==='Failed'||s==='Stopped'); // deep compare somente nos campos relevantes para evitar re-render function shallowEquals(a,b){ if (a===b) return true; if (!a || !b) return false; const keys = ['id','name','status','total_contacts','valid_contacts','delivered','inbox_name','list_name','scheduled_at','completed_at']; for (const k of keys){ if (a[k] !== b[k]) return false; } return true; } // ===== Tween hook para números ===== function useTweenedNumber(target, dur=450){ const [val, setVal] = React.useState(target||0); const rafRef = React.useRef(null); const startRef = React.useRef(0); React.useEffect(()=>{ const from = val; const to = Number.isFinite(target) ? target : 0; if (from === to) return; cancelAnimationFrame(rafRef.current); let start = 0; const step = (ts) => { if (!start) start = ts; const t = Math.min(1, (ts - start)/dur); const eased = 1 - Math.pow(1 - t, 3); const cur = Math.round(from + (to - from) * eased); setVal(cur); if (t < 1) rafRef.current = requestAnimationFrame(step); }; rafRef.current = requestAnimationFrame(step); return () => cancelAnimationFrame(rafRef.current); }, [target, dur]); // eslint-disable-line react-hooks/exhaustive-deps return val; } // ===== State ===== const [initialLoading, setInitialLoading] = React.useState(true); const [data, setData] = React.useState(null); const pollRef = React.useRef(null); const delivered = data?.delivered ?? 0; const total = data?.total_contacts ?? 0; const valid = data?.valid_contacts ?? 0; const percent = total > 0 ? Math.min(100, Math.round((delivered / total) * 100)) : 0; // números animados const tweenDelivered = useTweenedNumber(delivered); const tweenTotal = useTweenedNumber(total); const tweenValid = useTweenedNumber(valid); // ===== Data fetch ===== const load = React.useCallback(async ()=>{ const id = parseId(); if (!id) return; const r = await fetch(`/api/campaigns_analytics.php?id=${id}`, { credentials:'include', cache:'no-store' }); if (!r.ok) return; const j = await r.json(); if (!j.ok || !j.campaign) return; setData(prev => { if (shallowEquals(prev, j.campaign)) return prev; return j.campaign; }); if (isTerminal(j.campaign.status)) { if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } } else { if (!pollRef.current) pollRef.current = setInterval(load, 4000); } if (initialLoading) setInitialLoading(false); }, [initialLoading]); // eslint-disable-line react-hooks/exhaustive-deps React.useEffect(()=>{ let alive = true; (async () => { if (alive) await load(); })(); const onHash = ()=>load(); window.addEventListener('hashchange', onHash); return ()=>{ alive = false; window.removeEventListener('hashchange', onHash); if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; } }; }, [load]); // ===== Handlers ===== const downloadReport = React.useCallback(()=>{ if (!data?.id) return; const url = `/api/campaigns_report_contacts.php?id=${data.id}`; window.open(url, '_blank', 'noopener,noreferrer'); }, [data]); // ===== UI ===== return ( {initialLoading ? ( ) : !data ? ( Relatório não encontrado ) : ( {/* Header */} Relatório de {data.name} {statusChip(data.status)} {/* Progresso */} Progresso de Entrega {fmtInt.format(tweenDelivered)} de {fmtInt.format(tweenTotal)} contatos ({percent}%) {/* Cards */} Contatos Válidos {fmtInt.format(tweenValid)} Entregues {fmtInt.format(tweenDelivered)} Caixa de Saída {data.inbox_name || '-'} Lista de Contatos {data.list_name || '-'} Agendamento {fmtDT(data.scheduled_at)} Conclusão {fmtDT(data.completed_at)} Status {data.status} • {fmtInt.format(tweenDelivered)} de {fmtInt.format(tweenTotal)} )} ); } window.AnalyticsPage = AnalyticsPage;