File size: 36,079 Bytes
b150436
0349430
83c2a9a
a343cb3
83c2a9a
cc442ef
b150436
0349430
a343cb3
0349430
b150436
 
 
0349430
 
a343cb3
b150436
0349430
 
 
 
 
 
 
 
 
 
 
 
b150436
0349430
b150436
0349430
b150436
 
 
 
0349430
 
 
 
b150436
0349430
b150436
0349430
b150436
0349430
b150436
0349430
b150436
 
0349430
 
 
 
 
 
 
6c2d132
cc442ef
0349430
b150436
 
6c2d132
 
b150436
6c2d132
b150436
 
 
 
6c2d132
b150436
 
 
 
 
0349430
d8249dc
b150436
 
a343cb3
b150436
a343cb3
d8249dc
b150436
 
 
 
 
 
a343cb3
b150436
 
 
 
 
a343cb3
b150436
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a343cb3
b150436
 
a343cb3
b150436
a343cb3
 
 
b150436
 
 
 
a343cb3
b150436
 
 
 
 
 
d8249dc
b150436
 
 
a343cb3
 
 
 
cc442ef
 
b150436
 
a343cb3
 
b150436
 
 
a343cb3
b150436
 
a343cb3
b150436
 
 
a343cb3
b150436
 
a343cb3
b150436
 
 
a343cb3
 
 
b150436
 
 
a343cb3
b150436
 
 
 
6c2d132
b150436
a343cb3
 
 
b150436
a343cb3
b150436
a343cb3
 
 
 
 
 
b150436
 
 
a343cb3
 
b150436
 
 
a343cb3
b150436
a343cb3
b150436
 
a343cb3
b150436
a343cb3
b150436
 
cc442ef
b150436
a343cb3
b150436
 
a343cb3
b150436
a343cb3
b150436
a343cb3
b150436
 
a343cb3
b150436
a343cb3
b150436
6c2d132
b150436
 
 
 
 
 
a343cb3
b150436
a343cb3
 
 
 
 
 
 
 
 
 
b150436
 
 
 
a343cb3
b150436
 
a343cb3
 
b150436
 
a343cb3
b150436
 
 
a343cb3
 
b150436
 
 
 
a343cb3
b150436
a343cb3
b150436
 
a343cb3
b150436
a343cb3
b150436
 
a343cb3
b150436
 
 
a343cb3
b150436
 
a343cb3
b150436
a343cb3
b150436
a343cb3
b150436
 
 
 
6c2d132
b150436
 
a343cb3
b150436
 
 
 
a343cb3
 
b150436
 
a343cb3
 
b150436
 
 
 
a343cb3
 
b150436
 
 
a343cb3
6c2d132
b150436
 
 
6c2d132
b150436
 
 
3b473c3
 
a343cb3
3b473c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a343cb3
b150436
 
 
6c2d132
b150436
 
3b473c3
b150436
 
a343cb3
b150436
 
a343cb3
b150436
a343cb3
 
 
b150436
3b473c3
 
 
6c2d132
b150436
3b473c3
b150436
 
cc442ef
3b473c3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cc442ef
 
 
 
 
 
b150436
 
 
 
 
 
 
a343cb3
b150436
 
 
 
 
cc442ef
 
 
 
0349430
 
b150436
a343cb3
 
 
83c2a9a
a343cb3
 
b150436
0349430
 
b150436
 
 
a343cb3
b150436
 
 
 
0349430
b150436
0349430
b150436
0349430
a343cb3
 
 
 
 
 
83c2a9a
a343cb3
 
0349430
 
 
 
 
 
cc442ef
 
0349430
 
 
a343cb3
 
0349430
 
 
 
cc442ef
b150436
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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
import { BrowserRouter as Router, Routes, Route, Link, Navigate, useNavigate, useParams } from 'react-router-dom';
import { useEffect, useState, createContext, useContext } from 'react';
import { Users, PlayCircle, CheckCircle, Lightbulb, Download, BookOpen, Plus, Edit2, Trash2, ChevronRight, X, Save, BarChart2, DollarSign, ArrowLeft, Mic, Activity } from 'lucide-react';
import LiveFeed from './pages/LiveFeed';
import TrainingLab from './pages/TrainingLab';

const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001';
const SESSION_KEY = 'edtech_admin_key';
export const AuthContext = createContext<{ apiKey: string | null; login: (k: string) => void; logout: () => void; }>({ apiKey: null, login: () => { }, logout: () => { } });
function AuthProvider({ children }: { children: React.ReactNode }) {
    const [apiKey, setApiKey] = useState<string | null>(() => sessionStorage.getItem(SESSION_KEY));
    const login = (k: string) => { sessionStorage.setItem(SESSION_KEY, k); setApiKey(k); };
    const logout = () => { sessionStorage.removeItem(SESSION_KEY); setApiKey(null); };
    return <AuthContext.Provider value={{ apiKey, login, logout }}>{children}</AuthContext.Provider>;
}
export const useAuth = () => useContext(AuthContext);
const ah = (k: string) => ({ 'Authorization': `Bearer ${k}`, 'Content-Type': 'application/json' });
function ProtectedRoute({ children }: { children: React.ReactNode }) {
    const { apiKey } = useAuth();
    if (!apiKey) return <Navigate to="/login" replace />;
    return <>{children}</>;
}

function LoginPage() {
    const { login, apiKey } = useAuth();
    const navigate = useNavigate();
    const [key, setKey] = useState('');
    const [error, setError] = useState('');
    const [loading, setLoading] = useState(false);
    useEffect(() => { if (apiKey) navigate('/', { replace: true }); }, [apiKey, navigate]);
    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault(); setError(''); setLoading(true);
        try {
            const res = await fetch(`${API_URL}/v1/admin/stats`, { headers: { 'Authorization': `Bearer ${key}` } });
            if (res.ok) { login(key); navigate('/', { replace: true }); }
            else setError('Clé API invalide.');
        } catch { setError('Impossible de joindre le serveur.'); } finally { setLoading(false); }
    };
    return (
        <div className="min-h-screen bg-slate-900 flex items-center justify-center p-4">
            <div className="bg-white rounded-2xl shadow-2xl p-8 w-full max-w-sm">
                <div className="text-center mb-6"><div className="text-3xl mb-2">🔐</div>
                    <h1 className="text-2xl font-bold text-slate-800">Admin Access</h1>
                    <p className="text-sm text-slate-500 mt-1">Entrez votre ADMIN_API_KEY</p></div>
                <form onSubmit={handleSubmit} className="space-y-4">
                    <input id="apiKey" type="password" required placeholder="sk-admin-..." value={key}
                        onChange={e => setKey(e.target.value)}
                        className="w-full border border-slate-200 rounded-xl px-4 py-3 text-sm outline-none focus:ring-2 focus:ring-slate-400" />
                    {error && <p className="text-red-500 text-sm">{error}</p>}
                    <button type="submit" disabled={loading}
                        className="w-full bg-slate-900 hover:bg-slate-700 text-white py-3 rounded-xl font-bold text-sm transition disabled:opacity-50">
                        {loading ? 'Vérification...' : 'Se connecter'}
                    </button>
                </form>
            </div>
        </div>
    );
}

function Dashboard() {
    const { apiKey, logout } = useAuth();
    const [stats, setStats] = useState<any>(null);
    const [enrollments, setEnrollments] = useState<any[]>([]);
    const [loading, setLoading] = useState(true);
    useEffect(() => {
        (async () => {
            try {
                const h = { 'Authorization': `Bearer ${apiKey}` };
                const [sRes, eRes] = await Promise.all([
                    fetch(`${API_URL}/v1/admin/stats`, { headers: h }),
                    fetch(`${API_URL}/v1/admin/enrollments`, { headers: h })
                ]);
                if (sRes.status === 401) { logout(); return; }
                setStats(await sRes.json());
                setEnrollments(await eRes.json());
            } finally { setLoading(false); }
        })();
    }, [apiKey, logout]);
    const exportCSV = () => {
        if (!enrollments.length) return alert('Aucune inscription.');
        const rows = enrollments.map((e: any) => [e.id, e.user?.phone, e.track?.title, e.status, e.currentDay, e.startedAt]);
        const csv = [['ID', 'Phone', 'Track', 'Status', 'Day', 'Started'].join(','), ...rows.map(r => r.join(','))].join('\n');
        const a = document.createElement('a'); a.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }));
        a.download = `enrollments_${new Date().toISOString().slice(0, 10)}.csv`; a.click();
    };
    if (loading) return <div className="p-8 text-slate-400">Chargement...</div>;
    const statCards = [
        { icon: <Users className="w-6 h-6 text-slate-400" />, label: 'Utilisateurs', value: stats?.totalUsers || 0, color: 'text-slate-900' },
        { icon: <PlayCircle className="w-6 h-6 text-blue-400" />, label: 'Actifs', value: stats?.activeEnrollments || 0, color: 'text-blue-600' },
        { icon: <CheckCircle className="w-6 h-6 text-green-400" />, label: 'Complétés', value: stats?.completedEnrollments || 0, color: 'text-green-600' },
        { icon: <Lightbulb className="w-6 h-6 text-purple-400" />, label: 'Parcours', value: stats?.totalTracks || 0, color: 'text-purple-600' },
        { icon: <DollarSign className="w-6 h-6 text-emerald-400" />, label: 'Revenus', value: `${(stats?.totalRevenue || 0).toLocaleString()} XOF`, color: 'text-emerald-600' },
    ];
    return (
        <div className="p-8">
            <h1 className="text-3xl font-bold mb-8 text-slate-800">Dashboard</h1>
            <div className="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-8">
                {statCards.map((s, i) => (
                    <div key={i} className="bg-white p-5 rounded-xl shadow-sm border border-slate-100 flex flex-col items-center gap-2">
                        {s.icon}<p className="text-xs font-semibold text-slate-500 uppercase tracking-wider">{s.label}</p>
                        <p className={`text-2xl font-bold ${s.color}`}>{s.value}</p>
                    </div>
                ))}
            </div>
            <div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
                <div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center">
                    <h2 className="text-lg font-semibold text-slate-800">Inscriptions récentes</h2>
                    <button onClick={exportCSV} className="flex items-center gap-2 bg-emerald-600 hover:bg-emerald-700 text-white px-4 py-2 rounded-lg text-sm font-medium">
                        <Download className="w-4 h-4" /><span>Export CSV</span>
                    </button>
                </div>
                <table className="w-full text-sm">
                    <thead className="bg-slate-50 text-xs text-slate-500 uppercase">
                        <tr>{['Téléphone', 'Parcours', 'Statut', 'Jour', 'Date'].map(h => <th key={h} className="px-6 py-3 text-left">{h}</th>)}</tr>
                    </thead>
                    <tbody>
                        {enrollments.map((e: any) => (
                            <tr key={e.id} className="border-t border-slate-50 hover:bg-slate-50/50">
                                <td className="px-6 py-4 font-medium text-slate-900">{e.user?.phone || '—'}</td>
                                <td className="px-6 py-4">{e.track?.title || '—'}</td>
                                <td className="px-6 py-4"><span className={`px-2 py-1 rounded-full text-xs font-medium ${e.status === 'ACTIVE' ? 'bg-blue-100 text-blue-800' : e.status === 'COMPLETED' ? 'bg-green-100 text-green-800' : 'bg-slate-100 text-slate-800'}`}>{e.status}</span></td>
                                <td className="px-6 py-4">Jour {e.currentDay}</td>
                                <td className="px-6 py-4 text-slate-500">{new Date(e.startedAt).toLocaleDateString('fr-FR')}</td>
                            </tr>
                        ))}
                        {!enrollments.length && <tr><td colSpan={5} className="px-6 py-8 text-center text-slate-400">Aucune inscription</td></tr>}
                    </tbody>
                </table>
            </div>
        </div>
    );
}

function TrackList() {
    const { apiKey } = useAuth(); const navigate = useNavigate();
    const [tracks, setTracks] = useState<any[]>([]); const [loading, setLoading] = useState(true);
    const load = async () => { const r = await fetch(`${API_URL}/v1/admin/tracks`, { headers: ah(apiKey!) }); setTracks(await r.json()); setLoading(false); };
    useEffect(() => { load(); }, []);
    const del = async (id: string) => { if (!confirm('Supprimer ce parcours ?')) return; await fetch(`${API_URL}/v1/admin/tracks/${id}`, { method: 'DELETE', headers: ah(apiKey!) }); load(); };
    if (loading) return <div className="p-8 text-slate-400">Chargement...</div>;
    return (
        <div className="p-8">
            <div className="flex justify-between items-center mb-6">
                <h1 className="text-3xl font-bold text-slate-800">Parcours</h1>
                <button onClick={() => navigate('/content/new')} className="flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-xl text-sm font-bold hover:bg-slate-700">
                    <Plus className="w-4 h-4" /> Nouveau parcours
                </button>
            </div>
            <div className="grid gap-4">
                {tracks.map((t: any) => (
                    <div key={t.id} className="bg-white rounded-xl border border-slate-100 p-5 flex items-center justify-between shadow-sm hover:shadow-md transition">
                        <div className="flex items-center gap-4">
                            <div className="bg-purple-100 p-3 rounded-xl"><BookOpen className="w-5 h-5 text-purple-600" /></div>
                            <div>
                                <div className="flex items-center gap-2">
                                    <h3 className="font-bold text-slate-800">{t.title}</h3>
                                    {t.isPremium && <span className="bg-amber-100 text-amber-700 text-xs px-2 py-0.5 rounded-full font-medium">Premium</span>}
                                    <span className="bg-slate-100 text-slate-600 text-xs px-2 py-0.5 rounded-full">{t.language}</span>
                                </div>
                                <p className="text-sm text-slate-500 mt-0.5">{t._count?.days || 0} jours · {t._count?.enrollments || 0} inscrits · {t.duration}j</p>
                            </div>
                        </div>
                        <div className="flex items-center gap-2">
                            <button onClick={() => navigate(`/content/${t.id}`)} className="p-2 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg"><Edit2 className="w-4 h-4" /></button>
                            <button onClick={() => navigate(`/content/${t.id}/days`)} className="flex items-center gap-1 text-sm text-slate-600 hover:text-slate-900 px-3 py-2 rounded-lg hover:bg-slate-50">Jours <ChevronRight className="w-4 h-4" /></button>
                            <button onClick={() => del(t.id)} className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg"><Trash2 className="w-4 h-4" /></button>
                        </div>
                    </div>
                ))}
                {!tracks.length && <div className="text-center py-16 text-slate-400"><BookOpen className="w-12 h-12 mx-auto mb-3 opacity-30" /><p>Aucun parcours. Créez-en un !</p></div>}
            </div>
        </div>
    );
}

function TrackForm() {
    const { apiKey } = useAuth(); const { id } = useParams<{ id: string }>(); const navigate = useNavigate();
    const isNew = id === 'new';
    const [form, setForm] = useState({ title: '', description: '', duration: 7, language: 'FR', isPremium: false, priceAmount: 0, stripePriceId: '' });
    const [saving, setSaving] = useState(false);
    useEffect(() => { if (!isNew) fetch(`${API_URL}/v1/admin/tracks/${id}`, { headers: ah(apiKey!) }).then(r => r.json()).then(t => setForm({ title: t.title, description: t.description || '', duration: t.duration, language: t.language, isPremium: t.isPremium, priceAmount: t.priceAmount || 0, stripePriceId: t.stripePriceId || '' })); }, [id]);
    const inp = "w-full border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-slate-300";
    const handleSubmit = async (e: React.FormEvent) => {
        e.preventDefault(); setSaving(true);
        const url = isNew ? `${API_URL}/v1/admin/tracks` : `${API_URL}/v1/admin/tracks/${id}`;
        await fetch(url, { method: isNew ? 'POST' : 'PUT', headers: ah(apiKey!), body: JSON.stringify({ ...form, priceAmount: form.priceAmount || undefined, stripePriceId: form.stripePriceId || undefined }) });
        navigate('/content');
    };
    return (
        <div className="p-8 max-w-xl">
            <div className="flex items-center gap-3 mb-6">
                <button onClick={() => navigate('/content')} className="p-2 hover:bg-slate-100 rounded-lg"><ArrowLeft className="w-4 h-4" /></button>
                <h1 className="text-2xl font-bold text-slate-800">{isNew ? 'Nouveau parcours' : 'Modifier le parcours'}</h1>
            </div>
            <form onSubmit={handleSubmit} className="bg-white rounded-2xl border border-slate-100 p-6 space-y-4 shadow-sm">
                <div><label className="text-sm font-medium text-slate-700 mb-1 block">Titre *</label>
                    <input required className={inp} value={form.title} onChange={e => setForm(f => ({ ...f, title: e.target.value }))} /></div>
                <div><label className="text-sm font-medium text-slate-700 mb-1 block">Description</label>
                    <textarea className={inp} rows={3} value={form.description} onChange={e => setForm(f => ({ ...f, description: e.target.value }))} /></div>
                <div className="grid grid-cols-2 gap-4">
                    <div><label className="text-sm font-medium text-slate-700 mb-1 block">Durée (jours)</label>
                        <input type="number" min={1} required className={inp} value={form.duration} onChange={e => setForm(f => ({ ...f, duration: parseInt(e.target.value) }))} /></div>
                    <div><label className="text-sm font-medium text-slate-700 mb-1 block">Langue</label>
                        <select className={inp} value={form.language} onChange={e => setForm(f => ({ ...f, language: e.target.value }))}>
                            <option value="FR">Français</option><option value="WOLOF">Wolof</option>
                        </select></div>
                </div>
                <label className="flex items-center gap-3 p-3 bg-amber-50 rounded-xl cursor-pointer">
                    <input type="checkbox" checked={form.isPremium} onChange={e => setForm(f => ({ ...f, isPremium: e.target.checked }))} className="w-4 h-4" />
                    <span className="text-sm font-medium text-amber-800">Formation Premium (payante)</span>
                </label>
                {form.isPremium && <div className="grid grid-cols-2 gap-4">
                    <div><label className="text-sm font-medium text-slate-700 mb-1 block">Prix (XOF)</label>
                        <input type="number" className={inp} value={form.priceAmount} onChange={e => setForm(f => ({ ...f, priceAmount: parseInt(e.target.value) }))} /></div>
                    <div><label className="text-sm font-medium text-slate-700 mb-1 block">Stripe Price ID</label>
                        <input className={inp} value={form.stripePriceId} onChange={e => setForm(f => ({ ...f, stripePriceId: e.target.value }))} /></div>
                </div>}
                <div className="flex gap-3 pt-2">
                    <button type="button" onClick={() => navigate('/content')} className="flex-1 border border-slate-200 text-slate-600 py-2.5 rounded-xl text-sm hover:bg-slate-50">Annuler</button>
                    <button type="submit" disabled={saving} className="flex-1 bg-slate-900 text-white py-2.5 rounded-xl text-sm font-bold flex items-center justify-center gap-2 hover:bg-slate-700 disabled:opacity-50">
                        <Save className="w-4 h-4" />{saving ? 'Enregistrement...' : 'Enregistrer'}
                    </button>
                </div>
            </form>
        </div>
    );
}

function TrackDays() {
    const { apiKey } = useAuth(); const { trackId } = useParams<{ trackId: string }>(); const navigate = useNavigate();
    const [days, setDays] = useState<any[]>([]); const [track, setTrack] = useState<any>(null); const [editing, setEditing] = useState<any>(null); const [saving, setSaving] = useState(false);
    const load = async () => { const [tR, dR] = await Promise.all([fetch(`${API_URL}/v1/admin/tracks/${trackId}`, { headers: ah(apiKey!) }), fetch(`${API_URL}/v1/admin/tracks/${trackId}/days`, { headers: ah(apiKey!) })]); setTrack(await tR.json()); setDays(await dR.json()); };
    useEffect(() => { load(); }, []);
    const emptyDay = { dayNumber: (days.length || 0) + 1, title: '', lessonText: '', audioUrl: '', exerciseType: 'TEXT', exercisePrompt: '', validationKeyword: '' };
    const saveDay = async (e: React.FormEvent) => {
        e.preventDefault(); setSaving(true);
        const url = editing.id ? `${API_URL}/v1/admin/tracks/${trackId}/days/${editing.id}` : `${API_URL}/v1/admin/tracks/${trackId}/days`;
        await fetch(url, { method: editing.id ? 'PUT' : 'POST', headers: ah(apiKey!), body: JSON.stringify(editing) });
        setEditing(null); load(); setSaving(false);
    };
    const del = async (dayId: string) => { if (!confirm('Supprimer ce jour?')) return; await fetch(`${API_URL}/v1/admin/tracks/${trackId}/days/${dayId}`, { method: 'DELETE', headers: ah(apiKey!) }); load(); };
    const inp = "w-full border border-slate-200 rounded-xl px-4 py-2.5 text-sm outline-none focus:ring-2 focus:ring-slate-300";
    return (
        <div className="p-8">
            <div className="flex items-center gap-3 mb-6">
                <button onClick={() => navigate('/content')} className="p-2 hover:bg-slate-100 rounded-lg"><ArrowLeft className="w-4 h-4" /></button>
                <div><h1 className="text-2xl font-bold text-slate-800">{track?.title}</h1>
                    <p className="text-sm text-slate-500">{days.length} jours configurés</p></div>
                <button onClick={() => setEditing(emptyDay)} className="ml-auto flex items-center gap-2 bg-slate-900 text-white px-4 py-2 rounded-xl text-sm font-bold hover:bg-slate-700">
                    <Plus className="w-4 h-4" /> Ajouter un jour
                </button>
            </div>
            {editing && (
                <div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4">
                    <div className="bg-white rounded-2xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
                        <div className="flex items-center justify-between p-5 border-b">
                            <h2 className="font-bold text-slate-800">{editing.id ? `Modifier Jour ${editing.dayNumber}` : 'Nouveau jour'}</h2>
                            <button onClick={() => setEditing(null)}><X className="w-5 h-5 text-slate-400" /></button>
                        </div>
                        <form onSubmit={saveDay} className="p-5 space-y-4">
                            <div className="grid grid-cols-2 gap-3">
                                <div><label className="text-xs font-medium text-slate-600 mb-1 block">Numéro du jour</label>
                                    <input type="number" min={1} required className={inp} value={editing.dayNumber} onChange={e => setEditing((d: any) => ({ ...d, dayNumber: parseInt(e.target.value) }))} /></div>
                                <div><label className="text-xs font-medium text-slate-600 mb-1 block">Titre</label>
                                    <input className={inp} value={editing.title || ''} onChange={e => setEditing((d: any) => ({ ...d, title: e.target.value }))} /></div>
                            </div>
                            <div><label className="text-xs font-medium text-slate-600 mb-1 block">Texte de la leçon</label>
                                <textarea className={inp} rows={5} value={editing.lessonText || ''} onChange={e => setEditing((d: any) => ({ ...d, lessonText: e.target.value }))} placeholder="Contenu pédagogique..." /></div>
                            <div><label className="text-xs font-medium text-slate-600 mb-1 block">URL Audio (optionnel)</label>
                                <input className={inp} value={editing.audioUrl || ''} onChange={e => setEditing((d: any) => ({ ...d, audioUrl: e.target.value }))} placeholder="https://..." /></div>
                            <div className="grid grid-cols-2 gap-3">
                                <div><label className="text-xs font-medium text-slate-600 mb-1 block">Type exercice</label>
                                    <select className={inp} value={editing.exerciseType} onChange={e => setEditing((d: any) => ({ ...d, exerciseType: e.target.value }))}>
                                        <option value="TEXT">Texte libre</option><option value="AUDIO">Audio</option><option value="BUTTON">Boutons</option>
                                    </select></div>
                                <div><label className="text-xs font-medium text-slate-600 mb-1 block">Mot-clé validation</label>
                                    <input className={inp} value={editing.validationKeyword || ''} onChange={e => setEditing((d: any) => ({ ...d, validationKeyword: e.target.value }))} /></div>
                            </div>
                            <div><label className="text-xs font-medium text-slate-600 mb-1 block">Prompt exercice</label>
                                <textarea className={inp} rows={2} value={editing.exercisePrompt || ''} onChange={e => setEditing((d: any) => ({ ...d, exercisePrompt: e.target.value }))} placeholder="Question posée à l'étudiant..." /></div>
                            <div className="flex gap-3">
                                <button type="button" onClick={() => setEditing(null)} className="flex-1 border border-slate-200 text-slate-600 py-2.5 rounded-xl text-sm hover:bg-slate-50">Annuler</button>
                                <button type="submit" disabled={saving} className="flex-1 bg-slate-900 text-white py-2.5 rounded-xl text-sm font-bold flex items-center justify-center gap-2 hover:bg-slate-700 disabled:opacity-50">
                                    <Save className="w-4 h-4" />{saving ? 'Enregistrement...' : 'Enregistrer'}
                                </button>
                            </div>
                        </form>
                    </div>
                </div>
            )}
            <div className="grid gap-3">
                {days.map((d: any) => (
                    <div key={d.id} className="bg-white rounded-xl border border-slate-100 p-4 flex items-start justify-between shadow-sm">
                        <div className="flex gap-4">
                            <div className="bg-slate-900 text-white w-9 h-9 rounded-lg flex items-center justify-center text-sm font-bold shrink-0">{d.dayNumber}</div>
                            <div>
                                <p className="font-medium text-slate-800">{d.title || `Jour ${d.dayNumber}`}</p>
                                <p className="text-xs text-slate-500 mt-0.5 line-clamp-1">{d.lessonText?.substring(0, 100) || 'Pas de texte'}</p>
                                <div className="flex gap-2 mt-1.5">
                                    <span className="text-xs bg-slate-100 text-slate-600 px-2 py-0.5 rounded-full">{d.exerciseType}</span>
                                    {d.audioUrl && <span className="text-xs bg-green-100 text-green-700 px-2 py-0.5 rounded-full">🎵 Audio</span>}
                                    {d.exercisePrompt && <span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full">🎯 Exercice</span>}
                                </div>
                            </div>
                        </div>
                        <div className="flex gap-1 shrink-0 ml-4">
                            <button onClick={() => setEditing(d)} className="p-2 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg"><Edit2 className="w-4 h-4" /></button>
                            <button onClick={() => del(d.id)} className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg"><Trash2 className="w-4 h-4" /></button>
                        </div>
                    </div>
                ))}
                {!days.length && <div className="text-center py-12 text-slate-400 bg-white rounded-xl border border-dashed border-slate-200"><p>Aucun jour. Ajoutez le contenu pédagogique !</p></div>}
            </div>
        </div>
    );
}

function UserList() {
    const { apiKey } = useAuth();
    const [users, setUsers] = useState<any[]>([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(true);
    const [selectedUser, setSelectedUser] = useState<any>(null); const [messages, setMessages] = useState<any[]>([]); const [loadingMsg, setLoadingMsg] = useState(false);

    useEffect(() => { fetch(`${API_URL}/v1/admin/users`, { headers: ah(apiKey!) }).then(r => r.json()).then(d => { setUsers(d.users || d); setTotal(d.total || 0); setLoading(false); }); }, []);

    const viewMessages = async (userId: string) => {
        setLoadingMsg(true); setSelectedUser({ id: userId });
        try {
            const res = await fetch(`${API_URL}/v1/admin/users/${userId}/messages`, { headers: ah(apiKey!) });
            const data = await res.json();
            setSelectedUser(data.user);
            setMessages(data.messages || []);
        } catch (e) {
            alert("Erreur lors du chargement des messages.");
        } finally {
            setLoadingMsg(false);
        }
    };

    if (loading) return <div className="p-8 text-slate-400">Chargement...</div>;
    return (
        <div className="p-8">
            <h1 className="text-3xl font-bold mb-6 text-slate-800">Utilisateurs <span className="text-lg font-normal text-slate-400">({total})</span></h1>
            <div className="bg-white rounded-xl shadow-sm border border-slate-100 overflow-hidden">
                <table className="w-full text-sm">
                    <thead className="bg-slate-50 text-xs text-slate-500 uppercase">
                        <tr>{['Téléphone', 'Nom', 'Langue', 'Secteur', 'Inscrip.', 'Réponses', 'Date', 'Actions'].map(h => <th key={h} className="px-5 py-3 text-left">{h}</th>)}</tr>
                    </thead>
                    <tbody>
                        {users.map((u: any) => (
                            <tr key={u.id} className="border-t border-slate-50 hover:bg-slate-50/50">
                                <td className="px-5 py-3 font-medium">{u.phone}</td>
                                <td className="px-5 py-3 text-slate-600">{u.name || '—'}</td>
                                <td className="px-5 py-3"><span className="bg-blue-100 text-blue-700 text-xs px-2 py-0.5 rounded-full">{u.language}</span></td>
                                <td className="px-5 py-3 text-slate-500 text-xs">{u.activity || '—'}</td>
                                <td className="px-5 py-3 text-center">{u._count?.enrollments || 0}</td>
                                <td className="px-5 py-3 text-center">{u._count?.responses || 0}</td>
                                <td className="px-5 py-3 text-slate-400 text-xs">{new Date(u.createdAt).toLocaleDateString('fr-FR')}</td>
                                <td className="px-5 py-3 text-right">
                                    <button onClick={() => viewMessages(u.id)} className="text-xs bg-slate-100 hover:bg-slate-200 text-slate-700 px-3 py-1.5 rounded-lg font-medium transition-colors">Conversation</button>
                                </td>
                            </tr>
                        ))}
                        {!users.length && <tr><td colSpan={8} className="px-5 py-8 text-center text-slate-400">Aucun utilisateur</td></tr>}
                    </tbody>
                </table>
            </div>

            {selectedUser && (
                <div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
                    <div className="bg-slate-50 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col overflow-hidden">
                        <div className="bg-white px-6 py-4 flex items-center justify-between border-b border-slate-200">
                            <div>
                                <h3 className="font-bold text-slate-800">{selectedUser.name || 'Chat Utilisateur'}</h3>
                                <p className="text-xs text-slate-500">{selectedUser.phone}</p>
                            </div>
                            <button onClick={() => { setSelectedUser(null); setMessages([]); }} className="p-2 hover:bg-slate-100 rounded-full"><X className="w-5 h-5 text-slate-500" /></button>
                        </div>
                        <div className="flex-1 overflow-y-auto p-6 space-y-4 bg-[#e5ddd5]">
                            {loadingMsg ? (
                                <div className="text-center text-slate-500 py-10">Chargement de l'historique...</div>
                            ) : messages.length === 0 ? (
                                <div className="text-center text-slate-500 py-10 bg-white/50 rounded-xl">Aucun message pour cet utilisateur.</div>
                            ) : (
                                messages.map((m: any) => {
                                    const isBot = m.direction === 'OUTBOUND';
                                    return (
                                        <div key={m.id} className={`flex ${isBot ? 'justify-start' : 'justify-end'}`}>
                                            <div className={`max-w-[80%] rounded-2xl px-4 py-2.5 shadow-sm text-sm ${isBot ? 'bg-white text-slate-800 rounded-tl-none' : 'bg-[#dcf8c6] text-slate-900 rounded-tr-none'}`}>
                                                {m.mediaUrl && (
                                                    <div className="mb-2">
                                                        {m.mediaUrl.endsWith('.mp3') || m.mediaUrl.endsWith('.ogg') || m.mediaUrl.endsWith('.webm') ?
                                                            <audio src={m.mediaUrl} controls className="h-10 max-w-full" /> :
                                                            <a href={m.mediaUrl} target="_blank" rel="noreferrer" className="text-blue-600 underline">Voir Media</a>
                                                        }
                                                    </div>
                                                )}
                                                {m.content && <p className="whitespace-pre-wrap">{m.content}</p>}
                                                <p className={`text-[10px] mt-1 text-right ${isBot ? 'text-slate-400' : 'text-slate-500'}`}>
                                                    {new Date(m.createdAt).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })}
                                                </p>
                                            </div>
                                        </div>
                                    );
                                })
                            )}
                        </div>
                    </div>
                </div>
            )}
        </div>
    );
}

function Settings() {
    return (
        <div className="p-8 max-w-xl">
            <h1 className="text-3xl font-bold mb-6 text-slate-800">Configuration</h1>
            <div className="bg-white rounded-2xl border border-slate-100 p-6 shadow-sm space-y-3">
                <div className="flex items-center justify-between p-4 bg-slate-50 rounded-xl">
                    <div><p className="font-medium text-slate-800">API URL</p><p className="text-sm text-slate-500 font-mono">{API_URL}</p></div>
                </div>
                <p className="text-sm font-medium text-slate-600">Variables Railway requises :</p>
                {['WHATSAPP_VERIFY_TOKEN', 'WHATSAPP_APP_SECRET', 'WHATSAPP_ACCESS_TOKEN', 'OPENAI_API_KEY', 'DATABASE_URL', 'REDIS_URL', 'API_URL', 'ADMIN_API_KEY'].map(v => (
                    <div key={v} className="flex items-center gap-3 p-3 border border-slate-100 rounded-xl">
                        <span className="font-mono text-xs text-slate-700">{v}</span>
                    </div>
                ))}
            </div>
        </div>
    );
}

function AppShell() {
    const { logout } = useAuth();
    const navItems = [
        { to: '/', label: 'Dashboard', icon: <BarChart2 className="w-4 h-4" /> },
        { to: '/content', label: 'Parcours', icon: <BookOpen className="w-4 h-4" /> },
        { to: '/live-feed', label: 'Modération', icon: <Mic className="w-4 h-4 text-emerald-500" /> },
        { to: '/training', label: 'Training Lab', icon: <Activity className="w-4 h-4 text-purple-400" /> },
        { to: '/users', label: 'Utilisateurs', icon: <Users className="w-4 h-4" /> },
        { to: '/settings', label: 'Paramètres', icon: <Lightbulb className="w-4 h-4" /> },
    ];
    return (
        <div className="min-h-screen bg-gray-50 flex">
            <aside className="w-56 bg-slate-900 text-white p-5 flex flex-col shrink-0">
                <div className="text-lg font-bold mb-8 flex items-center gap-2"><span className="text-2xl">🎓</span>EdTech Admin</div>
                <nav className="space-y-1 flex-1">
                    {navItems.map(n => (
                        <Link key={n.to} to={n.to} className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium text-slate-300 hover:text-white hover:bg-slate-800 transition">
                            {n.icon}{n.label}
                        </Link>
                    ))}
                </nav>
                <button onClick={logout} className="text-xs text-slate-500 hover:text-white transition px-3 py-2 text-left">🔓 Se déconnecter</button>
            </aside>
            <main className="flex-1 overflow-auto">
                <Routes>
                    <Route path="/" element={<Dashboard />} />
                    <Route path="/content" element={<TrackList />} />
                    <Route path="/content/new" element={<TrackForm />} />
                    <Route path="/content/:id" element={<TrackForm />} />
                    <Route path="/content/:trackId/days" element={<TrackDays />} />
                    <Route path="/live-feed" element={<LiveFeed />} />
                    <Route path="/training" element={<TrainingLab />} />
                    <Route path="/users" element={<UserList />} />
                    <Route path="/settings" element={<Settings />} />
                </Routes>
            </main>
        </div>
    );
}

function App() {
    return (
        <AuthProvider>
            <Router>
                <Routes>
                    <Route path="/login" element={<LoginPage />} />
                    <Route path="/*" element={<ProtectedRoute><AppShell /></ProtectedRoute>} />
                </Routes>
            </Router>
        </AuthProvider>
    );
}
export default App;