File size: 7,437 Bytes
b43e552 6dd9bad a966957 b43e552 6dd9bad b43e552 a966957 b43e552 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | import { useEffect, useState } from 'react';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts';
import { TrendingUp, Users, CheckCircle2, Eye, AlertCircle } from 'lucide-react';
import { useAuth } from '../lib/auth';
import { useTenant } from '../lib/tenant';
import { api } from '../lib/api';
import { logError } from '../lib/logger';
interface AnalyticsData {
summary: {
total: number;
sent: number;
delivered: number;
read: number;
failed: number;
deliveryRate: number;
readRate: number;
};
funnel: Array<{ name: string; value: number; fill: string }>;
}
export const CRMAnalytics = () => {
const { token } = useAuth();
const { selectedOrgId } = useTenant();
const [data, setData] = useState<AnalyticsData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchAnalytics = async () => {
if (!token || !selectedOrgId) return;
try {
const result = await api.get('/v1/analytics/campaigns', token, selectedOrgId);
setData(result);
} catch (err) {
logError("Failed to fetch analytics:", err);
} finally {
setLoading(false);
}
};
fetchAnalytics();
}, [token, selectedOrgId]);
if (loading) return (
<div className="flex items-center justify-center p-12 bg-white rounded-[2rem] border border-slate-100 shadow-sm animate-pulse">
<div className="text-slate-400 font-medium">Chargement des données...</div>
</div>
);
if (!data) return null;
return (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-700">
{/* KPI Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="p-5 bg-white border border-slate-100 rounded-3xl shadow-sm hover:shadow-md transition-shadow">
<div className="w-10 h-10 bg-indigo-50 rounded-xl flex items-center justify-center mb-3 text-indigo-600">
<Users className="w-5 h-5" />
</div>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-wider">Total</p>
<p className="text-2xl font-black text-slate-900">{data.summary.total}</p>
</div>
<div className="p-5 bg-white border border-slate-100 rounded-3xl shadow-sm hover:shadow-md transition-shadow">
<div className="w-10 h-10 bg-emerald-50 rounded-xl flex items-center justify-center mb-3 text-emerald-600">
<CheckCircle2 className="w-5 h-5" />
</div>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-wider">Délivré</p>
<p className="text-2xl font-black text-slate-900">{data.summary.deliveryRate.toFixed(1)}%</p>
</div>
<div className="p-5 bg-white border border-slate-100 rounded-3xl shadow-sm hover:shadow-md transition-shadow">
<div className="w-10 h-10 bg-pink-50 rounded-xl flex items-center justify-center mb-3 text-pink-600">
<Eye className="w-5 h-5" />
</div>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-wider">Lecture</p>
<p className="text-2xl font-black text-slate-900">{data.summary.readRate.toFixed(1)}%</p>
</div>
<div className="p-5 bg-white border border-slate-100 rounded-3xl shadow-sm hover:shadow-md transition-shadow">
<div className="w-10 h-10 bg-amber-50 rounded-xl flex items-center justify-center mb-3 text-amber-600">
<TrendingUp className="w-5 h-5" />
</div>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-wider">Engagement</p>
<p className="text-2xl font-black text-slate-900">Elevé</p>
</div>
</div>
{/* Funnel Chart */}
<div className="bg-white border border-slate-100 rounded-[2.5rem] p-8 shadow-sm">
<div className="flex items-center justify-between mb-8">
<div>
<h4 className="font-black text-slate-900 text-lg">Entonnoir de Campagne</h4>
<p className="text-xs text-slate-500 font-medium">Répartition des statuts de distribution</p>
</div>
{data.summary.failed > 0 && (
<div className="flex items-center gap-2 px-3 py-1 bg-red-50 text-red-600 rounded-full text-[10px] font-bold">
<AlertCircle className="w-3 h-3" />
{data.summary.failed} ÉCHECS
</div>
)}
</div>
<div style={{ width: '100%', minHeight: '300px' }}>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={data.funnel} margin={{ top: 20, right: 30, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fill: '#94a3b8', fontSize: 12, fontWeight: 700 }}
dy={10}
/>
<YAxis hide />
<Tooltip
cursor={{ fill: 'transparent' }}
content={({ active, payload }) => {
if (active && payload && payload.length) {
return (
<div className="bg-slate-900 text-white p-3 rounded-2xl shadow-xl border border-slate-800 animate-in zoom-in-95 duration-200">
<p className="text-[10px] font-black uppercase mb-1 opacity-60">{payload[0].payload.name}</p>
<p className="text-sm font-black">{payload[0].value} Messages</p>
</div>
);
}
return null;
}}
/>
<Bar
dataKey="value"
radius={[12, 12, 12, 12]}
barSize={60}
animationDuration={1500}
>
{data.funnel.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.fill} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
);
};
|