import { useCallback, useEffect, useMemo, useState } from 'react';
import { Link, useParams } from 'react-router-dom';
import apiClient from '../api/client';
function formatNumber(value) {
return new Intl.NumberFormat().format(Number(value || 0));
}
function formatRate(value) {
if (typeof value !== 'number' || Number.isNaN(value)) return '—';
return `${(value * 100).toFixed(1)}%`;
}
function formatLastTriggered(value) {
if (!value) return '—';
try {
return new Date(value).toLocaleString([], {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
});
} catch {
return '—';
}
}
function buildLast30DaysSeries(rows = []) {
const rowByDate = new Map(
rows.map((row) => [String(row.date || ''), row])
);
const output = [];
const today = new Date();
today.setHours(0, 0, 0, 0);
for (let offset = 29; offset >= 0; offset -= 1) {
const date = new Date(today);
date.setDate(today.getDate() - offset);
const key = date.toISOString().slice(0, 10);
const row = rowByDate.get(key);
output.push({
key,
label: date.toLocaleDateString([], { month: 'short', day: 'numeric' }),
triggeredCount: Number(row?.triggeredCount || 0),
failedCount: Number(row?.failedCount || 0),
});
}
return output;
}
function getStatusAppearance(status) {
switch (status) {
case 'live':
return 'border-emerald-200 bg-emerald-50 text-emerald-700';
case 'paused':
return 'border-slate-200 bg-slate-50 text-slate-600';
case 'pending':
return 'border-amber-200 bg-amber-50 text-amber-700';
case 'custom':
return 'border-violet-200 bg-violet-50 text-violet-700';
default:
return 'border-gray-200 bg-gray-50 text-gray-500';
}
}
function StatCard({ title, value, subtitle, accentClassName }) {
return (
{title}
{value}
{subtitle}
);
}
function AnalyticsTrendChart({ rows }) {
const width = 720;
const height = 280;
const padding = { top: 18, right: 18, bottom: 34, left: 40 };
const innerWidth = width - padding.left - padding.right;
const innerHeight = height - padding.top - padding.bottom;
const maxValue = Math.max(
1,
...rows.flatMap((row) => [row.triggeredCount, row.failedCount]),
);
const triggeredPoints = rows.map((row, index) => {
const x = padding.left + (rows.length === 1 ? innerWidth / 2 : (index / (rows.length - 1)) * innerWidth);
const y = padding.top + innerHeight - ((row.triggeredCount || 0) / maxValue) * innerHeight;
return `${x},${y}`;
}).join(' ');
const failedPoints = rows.map((row, index) => {
const x = padding.left + (rows.length === 1 ? innerWidth / 2 : (index / (rows.length - 1)) * innerWidth);
const y = padding.top + innerHeight - ((row.failedCount || 0) / maxValue) * innerHeight;
return `${x},${y}`;
}).join(' ');
const gridLines = Array.from({ length: 4 }, (_, index) => {
const ratio = index / 3;
const y = padding.top + innerHeight - ratio * innerHeight;
const label = Math.round(ratio * maxValue);
return { y, label };
});
return (
Trigger Volume, Last 30 Days
Triggered vs failed SMS attempts
);
}
export default function Analytics() {
const { businessId } = useParams();
const [overview, setOverview] = useState(null);
const [eventRows, setEventRows] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const loadAnalytics = useCallback(async () => {
setLoading(true);
setError('');
try {
const [overviewRes, eventsRes] = await Promise.all([
apiClient.get(`/api/businesses/${businessId}/analytics/overview`),
apiClient.get(`/api/businesses/${businessId}/analytics/events`),
]);
setOverview(overviewRes.data);
setEventRows(eventsRes.data?.events || []);
} catch (err) {
setError(err.response?.data?.error || 'Failed to load analytics');
} finally {
setLoading(false);
}
}, [businessId]);
useEffect(() => {
loadAnalytics();
}, [loadAnalytics]);
const chartRows = useMemo(
() => buildLast30DaysSeries(overview?.chart || []),
[overview?.chart],
);
if (loading) {
return (
);
}
if (error) {
return (
);
}
const metrics = overview?.metrics || {};
const deliveryRateSubtitle = metrics.deliveryRateMode === 'send_fallback'
? 'Using send success until provider callbacks are connected'
: 'Based on delivery outcomes recorded so far';
return (
Analytics
Event trigger counts, operational health, and fallback delivery performance for this business.
Event Health
Per-event trigger counts and runtime status
View all events
{eventRows.length === 0 ? (
No analytics have been recorded for this business yet.
) : (
| Event |
Status |
Triggered Today |
Total Trigger Count |
Delivery Rate |
Last Triggered |
Action |
{eventRows.map((row) => (
|
{row.eventLabel}
{row.eventSlug}
|
{row.statusLabel}
|
{formatNumber(row.triggeredToday)}
|
{formatNumber(row.totalTriggerCount)}
|
{formatRate(row.deliveryRate)}
{row.deliveryRateMode === 'send_fallback' ? 'Send fallback' : row.deliveryRateMode === 'callback' ? 'Callback-based' : 'No data'}
|
{formatLastTriggered(row.lastTriggeredAt)}
|
Manage
|
))}
)}
);
}