// /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;