File size: 17,164 Bytes
47cf216
 
 
 
 
 
 
 
 
 
 
 
89ff6a0
 
 
47cf216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89ff6a0
 
 
 
 
 
 
 
47cf216
 
 
89ff6a0
 
47cf216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89ff6a0
 
47cf216
 
 
 
 
 
 
 
 
 
 
 
89ff6a0
47cf216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
148
149
150
151
152
153
154
155
156
157
"use client"

import { useState, useEffect } from "react"
import { useRouter } from "next/navigation"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge"
import { ScrollArea } from "@/components/ui/scroll-area"
import { ArrowLeft, Calendar, Database, FileText, Trash2, Clock, Activity, ThumbsUp, LogOut, ArrowRightLeft, Trophy } from "lucide-react"
import Link from "next/link"
import ComparisonChart from "@/components/neurolink/comparison-chart"

// --- CORRECTION ICI : On récupère l'URL du Cloud définie dans Vercel ---
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"

const CircularProgress = ({ value, color, label, icon: Icon }: any) => {
    const radius = 30; const circumference = 2 * Math.PI * radius; const offset = circumference - (value / 100) * circumference
    return (
      <div className="flex flex-col items-center justify-center space-y-2">
        <div className="relative flex items-center justify-center">
          <svg className="transform -rotate-90 w-24 h-24">
            <circle cx="48" cy="48" r={radius} stroke="#e2e8f0" strokeWidth="8" fill="transparent" />
            <circle cx="48" cy="48" r={radius} stroke={color} strokeWidth="8" fill="transparent" strokeDasharray={circumference} strokeDashoffset={offset} strokeLinecap="round" className="transition-all duration-1000 ease-out" />
          </svg>
          <div className="absolute inset-0 flex items-center justify-center flex-col"><span className="text-lg font-bold text-slate-900">{value}%</span></div>
        </div>
        <div className="flex items-center gap-1 text-xs font-medium text-slate-500 uppercase tracking-wide">{Icon && <Icon className="w-3 h-3" />} {label}</div>
      </div>
    )
}

export default function AdminPage() {
  const [sessions, setSessions] = useState<any[]>([])
  const [isAuthorized, setIsAuthorized] = useState(false)
  const router = useRouter()
  const [isCompareMode, setIsCompareMode] = useState(false)
  const [sessionA, setSessionA] = useState<any>(null); const [dataA, setDataA] = useState<any[]>([]); const [statsA, setStatsA] = useState<any>(null)
  const [sessionB, setSessionB] = useState<any>(null); const [dataB, setDataB] = useState<any[]>([]); const [statsB, setStatsB] = useState<any>(null)

  useEffect(() => {
    const token = localStorage.getItem("startech_admin_token")
    if (token !== "authorized_access_granted") { router.push("/admin/login") } 
    else { 
        setIsAuthorized(true); 
        // --- UTILISATION DE L'API CLOUD ---
        fetch(`${API_URL}/api/sessions`)
            .then(res => res.json())
            .then(data => setSessions(data))
            .catch(err => console.error("Erreur chargement sessions:", err))
    }
  }, [])

  const handleSelectSession = (sessionId: number) => {
    // --- UTILISATION DE L'API CLOUD ---
    fetch(`${API_URL}/api/sessions/${sessionId}`).then(res => res.json()).then(response => {
        const info = response.info; const data = response.data; const stats = calculateStatsInternal(data)
        if (isCompareMode) {
            if (!sessionA) { setSessionA(info); setDataA(data); setStatsA(stats) } 
            else if (!sessionB && sessionId !== sessionA.id) { setSessionB(info); setDataB(data); setStatsB(stats) } 
            else if (sessionA && sessionB) { setSessionA(info); setDataA(data); setStatsA(stats); setSessionB(null); setDataB([]); setStatsB(null) }
        } else { setSessionA(info); setDataA(data); setStatsA(stats); setSessionB(null); setDataB([]); setStatsB(null) }
      })
  }

  const calculateStatsInternal = (data: any[]) => {
    if (data.length === 0) return null
    const avg = (key: string) => Math.round(data.reduce((acc, curr) => acc + curr[key], 0) / data.length)
    return { duration: data.length, avg_engagement: avg('engagement_val'), avg_satisfaction: avg('satisfaction_val'), dominant_label: data[data.length - 1]?.satisfaction_lbl || "N/A" }
  }
  const toggleCompareMode = () => { setIsCompareMode(!isCompareMode); setSessionB(null); setDataB([]); setStatsB(null) }
  const handleDeleteSession = async (e: React.MouseEvent, sessionId: number) => {
    e.stopPropagation(); if (!confirm("Supprimer définitivement ?")) return
    // --- UTILISATION DE L'API CLOUD ---
    const res = await fetch(`${API_URL}/api/sessions/${sessionId}`, { method: 'DELETE' })
    if (res.ok) { setSessions(prev => prev.filter(s => s.id !== sessionId)); if (sessionA?.id === sessionId) { setSessionA(null); setDataA([]); setStatsA(null) }; if (sessionB?.id === sessionId) { setSessionB(null); setDataB([]); setStatsB(null) } }
  }
  const handleExport = (session: any, data: any) => {
    if (!data.length) return
    const headers = ["Temps", "Emotion", "Score IA", "Engagement", "Label Engagement", "Satisfaction", "Label Satisfaction", "Confiance", "Fidelite", "Avis"]
    const rows = data.map((row: any) => [ String(row.session_time).replace('.', ','), row.emotion, row.emotion_score ? String(row.emotion_score.toFixed(2)).replace('.', ',') : "0", String(row.engagement_val), `"${row.engagement_lbl}"`, String(row.satisfaction_val), `"${row.satisfaction_lbl}"`, String(row.trust_val), String(row.loyalty_val), `"${row.opinion_lbl}"` ])
    const csvContent = [headers.join(";"), ...rows.map((e: any) => e.join(";"))].join("\n")
    const blob = new Blob(["\uFEFF" + csvContent], { type: "text/csv;charset=utf-8;" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.setAttribute("download", `Rapport_${session.first_name}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link)
  }
  
  const formatDate = (dateStr: string) => {
      if (!dateStr) return "Date inconnue";
      try { return new Date(dateStr).toLocaleString('fr-FR', { day: 'numeric', month: 'short', hour: '2-digit', minute:'2-digit' }) } catch (e) { return "Erreur date" }
  }

  if (!isAuthorized) return null

  return (
    <div className="min-h-screen bg-slate-50 text-slate-900 flex flex-col font-sans">
      <header className="border-b border-slate-200 bg-white p-4 flex items-center justify-between sticky top-0 z-50 shadow-sm">
        <div className="flex items-center gap-4">
            <Link href="/"><Button variant="ghost" size="icon" className="text-slate-500 hover:text-blue-600 hover:bg-blue-50"><ArrowLeft className="w-5 h-5" /></Button></Link>
            <div><h1 className="text-xl font-bold flex items-center gap-2 text-slate-900"><Database className="w-5 h-5 text-blue-600" /> STARTECH <span className="text-slate-400">ADMIN</span></h1></div>
        </div>
        <div className="flex items-center gap-4">
            <Button onClick={toggleCompareMode} className={`gap-2 border transition-colors duration-200 ${isCompareMode ? "bg-purple-600 border-purple-600 text-white hover:bg-purple-700 hover:text-white" : "bg-white border-slate-300 text-slate-700 hover:bg-slate-100 hover:text-slate-900"}`}><ArrowRightLeft className="w-4 h-4" /> {isCompareMode ? "Mode Comparaison Actif" : "Comparer deux sessions"}</Button>
            <Button variant="ghost" onClick={() => {localStorage.removeItem("startech_admin_token"); router.push("/admin/login")}} className="text-slate-500 hover:text-red-600 hover:bg-red-50 gap-2"><LogOut className="w-4 h-4" /></Button>
        </div>
      </header>

      <main className="flex-1 overflow-hidden grid grid-cols-12 h-[calc(100vh-64px)]">
        <div className="col-span-3 border-r border-slate-200 bg-white flex flex-col h-full">
            <div className="p-4 border-b border-slate-200 bg-slate-50/50"><h2 className="text-xs font-bold uppercase tracking-widest text-slate-500">Historique</h2></div>
            <ScrollArea className="flex-1 bg-white">
                <div className="flex flex-col">
                    {sessions.map((session) => {
                        const isA = sessionA?.id === session.id; const isB = sessionB?.id === session.id
                        let activeClass = "border-l-4 border-l-transparent"
                        if (isA) activeClass = "bg-blue-50 border-l-blue-500"; if (isB) activeClass = "bg-orange-50 border-l-orange-500"
                        return (
                            <div key={session.id} onClick={() => handleSelectSession(session.id)} className={`group flex items-center justify-between p-4 border-b border-slate-100 hover:bg-slate-50 cursor-pointer transition-all ${activeClass}`}>
                                <div>
                                    <div className="font-bold flex items-center gap-2 text-slate-800">{session.first_name} {session.last_name} {isA && <Badge className="bg-blue-500 h-4 px-1 text-[9px]">A</Badge>} {isB && <Badge className="bg-orange-500 h-4 px-1 text-[9px]">B</Badge>}</div>
                                    <div className="text-xs text-slate-500 mt-1 flex items-center gap-2"><Calendar className="w-3 h-3" /> {formatDate(session.created_at)}</div>
                                </div>
                                <Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 opacity-0 group-hover:opacity-100" onClick={(e) => handleDeleteSession(e, session.id)}><Trash2 className="w-4 h-4 hover:text-red-500" /></Button>
                            </div>
                        )
                    })}
                </div>
            </ScrollArea>
        </div>

        <div className="col-span-9 p-6 overflow-y-auto bg-slate-50">
            {!isCompareMode && sessionA && statsA && (
                <div className="max-w-5xl mx-auto space-y-6 animate-in fade-in slide-in-from-bottom-4">
                     <div className="flex justify-between items-start">
                        <div><h2 className="text-3xl font-bold text-slate-900">{sessionA.first_name} {sessionA.last_name}</h2><p className="text-slate-500 flex items-center gap-2 mt-1"><Clock className="w-4 h-4" /> Durée: {statsA.duration}s | ID: {sessionA.client_id || "N/A"}</p></div>
                        <Button variant="outline" className="bg-white border-slate-200 text-slate-700 hover:bg-slate-50" onClick={() => handleExport(sessionA, dataA)}><FileText className="w-4 h-4 mr-2" /> CSV</Button>
                    </div>
                    <div className="grid grid-cols-3 gap-6">
                        <Card className="bg-white shadow-sm border-slate-200"><CardContent className="pt-6"><CircularProgress value={statsA.avg_engagement} color="#3b82f6" label="Engagement" icon={Activity} /></CardContent></Card>
                        <Card className="bg-white shadow-sm border-slate-200"><CardContent className="pt-6"><CircularProgress value={statsA.avg_satisfaction} color="#22c55e" label="Satisfaction" icon={ThumbsUp} /></CardContent></Card>
                        <Card className="flex items-center justify-center bg-slate-900 text-white shadow-sm border-slate-900"><div className="text-center"><Badge variant="outline" className="mb-2 text-slate-400 border-slate-700">Verdict</Badge><div className="text-xl font-bold">{statsA.dominant_label}</div></div></Card>
                    </div>
                    <Card className="bg-white shadow-sm border-slate-200"><CardHeader><CardTitle className="text-slate-900">Analyse Temporelle</CardTitle></CardHeader><CardContent><ComparisonChart dataA={dataA} dataB={[]} nameA={`${sessionA.first_name} (Engagement)`} nameB="" /></CardContent></Card>
                    <Card className="bg-white shadow-sm border-slate-200"><CardHeader><CardTitle className="text-slate-900">Données Brutes Complètes</CardTitle></CardHeader><CardContent><div className="rounded-md border border-slate-200 overflow-hidden"><ScrollArea className="h-[400px] w-full"><table className="w-full text-sm text-left text-slate-700"><thead className="bg-slate-100 font-medium sticky top-0 z-10 text-slate-900"><tr><th className="p-3">Temps</th><th className="p-3">Émotion</th><th className="p-3">Score IA</th><th className="p-3 border-l border-slate-200">Engagement</th><th className="p-3 border-l border-slate-200">Satisfaction</th><th className="p-3 border-l border-slate-200">Confiance</th><th className="p-3 border-l border-slate-200">Fidélité</th><th className="p-3 border-l border-slate-200">Avis</th></tr></thead><tbody className="divide-y divide-slate-100">{dataA.map((row, i) => (<tr key={i} className="hover:bg-slate-50 transition-colors"><td className="p-3 font-mono text-xs">{row.session_time}s</td><td className="p-3 capitalize flex items-center gap-2"><Badge variant="secondary" className="text-[10px] bg-slate-100 text-slate-700">{row.emotion}</Badge></td><td className="p-3 font-mono text-xs text-slate-500">{row.emotion_score?.toFixed(1)}%</td><td className="p-3 border-l border-slate-100"><div className="flex flex-col"><span className="font-bold text-xs">{row.engagement_val}%</span><span className="text-[10px] text-slate-400">{row.engagement_lbl}</span></div></td><td className="p-3 border-l border-slate-100"><div className="flex flex-col"><span className={`font-bold text-xs ${row.satisfaction_val > 50 ? "text-green-600" : "text-orange-500"}`}>{row.satisfaction_val}%</span><span className="text-[10px] text-slate-400">{row.satisfaction_lbl}</span></div></td><td className="p-3 border-l border-slate-100"><div className="flex flex-col"><span className="font-bold text-xs">{row.trust_val}%</span><span className="text-[10px] text-slate-400">{row.trust_lbl}</span></div></td><td className="p-3 border-l border-slate-100"><div className="flex flex-col"><span className="font-bold text-xs">{row.loyalty_val}%</span><span className="text-[10px] text-slate-400">{row.loyalty_lbl}</span></div></td><td className="p-3 border-l border-slate-100"><span className="text-xs">{row.opinion_lbl}</span></td></tr>))}</tbody></table></ScrollArea></div></CardContent></Card>
                </div>
            )}
            {isCompareMode && sessionA && sessionB && statsA && statsB && (
                 <div className="max-w-6xl mx-auto space-y-6 animate-in fade-in zoom-in-95">
                    <div className="grid grid-cols-3 items-center text-center bg-white shadow-sm p-6 rounded-2xl border border-slate-200"><div className="text-blue-600"><div className="text-2xl font-bold">{sessionA.first_name}</div><div className="text-sm opacity-70">Session A</div></div><div className="flex justify-center"><div className="bg-slate-100 rounded-full px-4 py-1 text-xs font-bold text-slate-500 border border-slate-200">VS</div></div><div className="text-orange-500"><div className="text-2xl font-bold">{sessionB.first_name}</div><div className="text-sm opacity-70">Session B</div></div></div>
                    <Card className="border-slate-200 bg-white shadow-sm"><CardHeader><CardTitle className="text-slate-900">Duel d'Engagement</CardTitle></CardHeader><CardContent><ComparisonChart dataA={dataA} dataB={dataB} nameA={`Engagement ${sessionA.first_name}`} nameB={`Engagement ${sessionB.first_name}`} /></CardContent></Card>
                    <div className="grid grid-cols-2 gap-6"><Card className={`border-t-4 border-t-blue-500 shadow-sm bg-white ${statsA.avg_engagement > statsB.avg_engagement ? "bg-blue-50" : ""}`}><CardHeader><CardTitle className="flex justify-between text-slate-900">{sessionA.first_name}{statsA.avg_engagement > statsB.avg_engagement && <Badge className="bg-yellow-400 text-black gap-1 hover:bg-yellow-500"><Trophy className="w-3 h-3" /> Vainqueur</Badge>}</CardTitle></CardHeader><CardContent className="grid grid-cols-2 gap-4"><CircularProgress value={statsA.avg_engagement} color="#3b82f6" label="Engagement" icon={Activity} /><CircularProgress value={statsA.avg_satisfaction} color="#3b82f6" label="Satisfaction" icon={ThumbsUp} /></CardContent></Card><Card className={`border-t-4 border-t-orange-500 shadow-sm bg-white ${statsB.avg_engagement > statsA.avg_engagement ? "bg-orange-50" : ""}`}><CardHeader><CardTitle className="flex justify-between text-slate-900">{sessionB.first_name}{statsB.avg_engagement > statsA.avg_engagement && <Badge className="bg-yellow-400 text-black gap-1 hover:bg-yellow-500"><Trophy className="w-3 h-3" /> Vainqueur</Badge>}</CardTitle></CardHeader><CardContent className="grid grid-cols-2 gap-4"><CircularProgress value={statsB.avg_engagement} color="#f97316" label="Engagement" icon={Activity} /><CircularProgress value={statsB.avg_satisfaction} color="#f97316" label="Satisfaction" icon={ThumbsUp} /></CardContent></Card></div>
                 </div>
            )}
            {(!sessionA) && <div className="h-full flex flex-col items-center justify-center text-slate-400 opacity-50"><ArrowLeft className="w-12 h-12 mb-4 animate-pulse" /><p>Sélectionnez une session à gauche pour commencer.</p></div>}
            {(isCompareMode && sessionA && !sessionB) && <div className="h-full flex flex-col items-center justify-center text-slate-400 opacity-50"><div className="text-blue-600 font-bold mb-2">Session A : {sessionA.first_name} sélectionnée.</div><p>Maintenant, sélectionnez la Session B dans la liste.</p></div>}
        </div>
      </main>
    </div>
  )
}