| 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> |
| ); |
| }; |
|
|