CognxSafeTrack
Claude Sonnet 4.6
refactor(debt): resolve all 10 technical debt items from audit
a966957 Download apps/admin/src/components/CRMAnalytics.tsx from safetrack/edtech: direct link, hf CLI and curl.
- Browser
- Download file 7.44 kB
-
https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/admin/src/components/CRMAnalytics.tsx
- Command line
-
hf download hf://spaces/safetrack/edtech/apps/admin/src/components/CRMAnalytics.tsx
-
curl -L -o CRMAnalytics.tsx https://huggingface.co/spaces/safetrack/edtech/resolve/main/apps/admin/src/components/CRMAnalytics.tsx
7.44 kB
| 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> | |
| ); | |
| }; | |