CognxSafeTrack Claude Sonnet 4.6 commited on
Commit
f5ea14d
·
1 Parent(s): 30a407a

feat(billing): refonte UX non-tech + enrichissement contexte IA

Browse files

Frontend (BillingPage):
- Renommage "Wallet" → "Crédits disponibles" (langage non-technique)
- Badge projection dynamique "À ce rythme : encore ~X jours" calculé
depuis les transactions des 7 derniers jours
- Questions suggérées cliquables (quick-reply chips) avant le 1er message
- Suppression "Tokens entrants/sortants" (jargon incompréhensible)
- Transactions filtrées : BYOK masqué, layout liste simple vs tableau
- Section "Où vont vos crédits IA ?" remplace le jargon feature/type
- Indicateur de frappe animé (3 points) dans le chat
- Packs recharge avec explication "≈ N messages IA" et badge populaire
- Texte WhatsApp clarifié : "Gratuit pour vous — Meta facture directement"

Backend (billing.ts /chat):
- Wallet balance + isHardStopped ajoutés dans le contexte IA
→ l'assistant peut maintenant répondre "combien de crédits me reste-t-il"
- Burn rate (débit moyen 7j) calculé et injecté : "À ce rythme,
votre solde durera encore X jours"
- Statut wallet (SUSPENDU / bas / actif) explicité dans le contexte
- Requêtes regroupées en Promise.all (1 aller-retour DB au lieu de 4)
- System prompt révisé : sans jargon, rassurante, pour non-techniques

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

apps/admin/src/pages/BillingPage.tsx CHANGED
@@ -1,16 +1,34 @@
1
  import { useState, useEffect, useRef } from 'react';
2
- import { Brain, MessageCircle, TrendingUp, Send, Loader2, AlertCircle, Zap, X } from 'lucide-react';
3
  import { api } from '@/lib/api';
4
  import { useAuth } from '@/lib/auth';
5
  import { useTenant } from '@/lib/tenant';
6
 
7
  const CREDIT_PACKS = [
8
- { label: '500 crédits', price: '10 000 FCFA', credits: 500 },
9
- { label: '2 000 crédits', price: '35 000 FCFA', credits: 2000 },
10
- { label: '5 000 crédits', price: '75 000 FCFA', credits: 5000 },
11
  ];
12
 
13
- const SUPPORT_WA_URL = 'https://wa.me/221700000000?text=Bonjour%2C%20je%20souhaite%20recharger%20mes%20cr%C3%A9dits%20IA%20Xaml%C3%A9%20Studio.';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  function RechargeModal({ onClose }: { onClose: () => void }) {
16
  useEffect(() => {
@@ -32,18 +50,19 @@ function RechargeModal({ onClose }: { onClose: () => void }) {
32
  className="bg-white w-full sm:max-w-md rounded-t-3xl sm:rounded-2xl shadow-2xl flex flex-col max-h-[90vh]"
33
  onClick={e => e.stopPropagation()}
34
  >
35
- {/* Header — fixe */}
36
  <div className="flex items-center justify-between px-6 py-5 border-b border-slate-100 shrink-0">
37
- <h2 className="text-lg font-bold text-slate-900">Recharger les crédits IA</h2>
 
 
 
38
  <button
39
  onClick={onClose}
40
- className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-slate-100 text-slate-400 hover:text-slate-600 transition-colors"
41
  >
42
  <X className="w-5 h-5" />
43
  </button>
44
  </div>
45
 
46
- {/* Contenu scrollable */}
47
  <div className="overflow-y-auto flex-1 px-6 py-5 space-y-3">
48
  {CREDIT_PACKS.map(pack => (
49
  <a
@@ -51,21 +70,25 @@ function RechargeModal({ onClose }: { onClose: () => void }) {
51
  href={`${SUPPORT_WA_URL}%20-%20Pack%20${pack.label}`}
52
  target="_blank"
53
  rel="noopener noreferrer"
54
- className="flex items-center justify-between p-4 border border-slate-200 rounded-xl hover:border-indigo-400 hover:bg-indigo-50 active:bg-indigo-100 transition-all group"
55
  >
 
 
 
 
 
56
  <div>
57
  <p className="font-semibold text-slate-800 group-hover:text-indigo-700">{pack.label}</p>
58
- <p className="text-xs text-slate-400">Pack recharge unique</p>
59
  </div>
60
  <span className="text-indigo-600 font-bold shrink-0 ml-4">{pack.price}</span>
61
  </a>
62
  ))}
63
  </div>
64
 
65
- {/* Footer — fixe */}
66
  <div className="px-6 py-4 border-t border-slate-100 shrink-0">
67
  <p className="text-xs text-slate-400 text-center">
68
- En cliquant sur un pack, vous serez redirigé vers WhatsApp pour finaliser votre commande avec notre équipe.
69
  </p>
70
  </div>
71
  </div>
@@ -90,34 +113,27 @@ interface WalletData {
90
  interface BillingSummary {
91
  plan: string;
92
  planLabel: string;
93
- subscriptionStatus: string;
94
  period: { start: string; end: string };
95
  ai: {
96
  creditsUsed: number;
97
  creditsLimit: number;
98
  percentUsed: number;
99
  totalCalls: number;
100
- tokensIn: number;
101
- tokensOut: number;
102
- costUsd: number;
103
  costFcfa: number;
104
  };
105
- whatsapp: { messagesSent: number; note: string };
106
  }
107
 
108
  interface HistoryDay {
109
  date: string;
110
  aiCalls: number;
111
  whatsappMessages: number;
112
- costUsd: number;
113
  costFcfa: number;
114
  }
115
 
116
  interface BreakdownItem {
117
  feature: string;
118
- type: string;
119
  calls: number;
120
- costFcfa: number;
121
  }
122
 
123
  interface ChatMessage {
@@ -125,23 +141,26 @@ interface ChatMessage {
125
  text: string;
126
  }
127
 
128
- const FEATURE_LABELS: Record<string, string> = {
129
- LESSON: '📚 Leçons',
130
- FEEDBACK: '✅ Feedbacks exercice',
131
- DEEPDIVE: '🔍 Deep dives',
132
- TRANSCRIPTION: '🎤 Transcriptions audio',
133
- IMAGE_ANALYSIS: '🖼️ Analyses image',
134
- CAMPAIGN: '📣 Campagnes',
135
- ONBOARDING: '👋 Onboarding',
136
- OTHER: '⚙️ Autres',
137
- };
138
 
139
- const PLAN_COLORS: Record<string, string> = {
140
- STARTER: 'bg-slate-100 text-slate-700',
141
- GROWTH: 'bg-emerald-100 text-emerald-700',
142
- SCALE: 'bg-indigo-100 text-indigo-700',
143
- ENTERPRISE: 'bg-amber-100 text-amber-700',
144
- };
 
 
 
 
 
 
 
 
145
 
146
  export default function BillingPage() {
147
  const { token, user } = useAuth();
@@ -156,7 +175,6 @@ export default function BillingPage() {
156
  const [error, setError] = useState<string | null>(null);
157
  const [showRecharge, setShowRecharge] = useState(false);
158
 
159
- // Chat state
160
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
161
  const [chatInput, setChatInput] = useState('');
162
  const [chatLoading, setChatLoading] = useState(false);
@@ -183,14 +201,14 @@ export default function BillingPage() {
183
  chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
184
  }, [chatMessages]);
185
 
186
- const sendChat = async () => {
187
- if (!chatInput.trim() || chatLoading || !orgId || !token) return;
188
- const question = chatInput.trim();
189
  setChatInput('');
190
- setChatMessages(prev => [...prev, { role: 'user', text: question }]);
191
  setChatLoading(true);
192
  try {
193
- const res = await api.post('/v1/billing/chat', { question, language: user?.language ?? 'FR' }, token, orgId);
194
  setChatMessages(prev => [...prev, { role: 'assistant', text: res.answer }]);
195
  } catch {
196
  setChatMessages(prev => [...prev, { role: 'assistant', text: 'Désolé, je ne peux pas répondre pour le moment.' }]);
@@ -199,7 +217,6 @@ export default function BillingPage() {
199
  }
200
  };
201
 
202
- // Bar chart: max value for scaling
203
  const maxCalls = Math.max(...history.map(d => d.aiCalls + d.whatsappMessages), 1);
204
 
205
  if (!orgId) {
@@ -230,20 +247,31 @@ export default function BillingPage() {
230
  if (!summary) return null;
231
 
232
  const periodLabel = new Date(summary.period.start).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
233
-
234
  const walletExhausted = wallet ? (wallet.isHardStopped || wallet.walletBalance <= 0) : false;
235
  const walletLow = wallet ? (wallet.walletBalance > 0 && wallet.walletBalance <= 200) : false;
236
 
 
 
 
 
 
 
 
 
 
237
  return (
238
  <div className="p-6 max-w-5xl mx-auto space-y-6">
239
  {showRecharge && <RechargeModal onClose={() => setShowRecharge(false)} />}
240
 
241
- {/* Wallet exhausted banner */}
242
  {walletExhausted && (
243
  <div className="flex items-center justify-between gap-4 px-5 py-4 bg-red-50 border border-red-200 rounded-2xl">
244
  <div className="flex items-center gap-3 text-red-700">
245
  <AlertCircle className="w-5 h-5 shrink-0" />
246
- <span className="font-semibold text-sm">Wallet épuisé — vos services sont suspendus. Rechargez pour rétablir.</span>
 
 
 
247
  </div>
248
  <button
249
  onClick={() => setShowRecharge(true)}
@@ -254,12 +282,15 @@ export default function BillingPage() {
254
  </div>
255
  )}
256
 
257
- {/* Wallet low banner */}
258
  {!walletExhausted && walletLow && (
259
  <div className="flex items-center justify-between gap-4 px-5 py-4 bg-amber-50 border border-amber-200 rounded-2xl">
260
  <div className="flex items-center gap-3 text-amber-700">
261
  <AlertCircle className="w-5 h-5 shrink-0" />
262
- <span className="font-semibold text-sm">Solde bas — {wallet!.walletBalance} crédits restants. Rechargez pour éviter une interruption.</span>
 
 
 
263
  </div>
264
  <button
265
  onClick={() => setShowRecharge(true)}
@@ -270,131 +301,117 @@ export default function BillingPage() {
270
  </div>
271
  )}
272
 
273
- {/* Header */}
274
  <div className="flex items-center justify-between">
275
  <div>
276
- <h1 className="text-2xl font-bold text-slate-900">Facturation</h1>
277
  <p className="text-slate-500 text-sm mt-1">Période : {periodLabel}</p>
278
  </div>
279
- <div className="flex items-center gap-3">
280
- <span className={`px-3 py-1 rounded-full text-sm font-semibold ${PLAN_COLORS[summary.plan] ?? PLAN_COLORS.STARTER}`}>
281
- Plan {summary.planLabel}
282
- </span>
283
- <button
284
- onClick={() => setShowRecharge(true)}
285
- className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-xl transition-colors shadow-sm"
286
- >
287
- <Zap className="w-4 h-4" />
288
- Recharger
289
- </button>
290
- </div>
291
  </div>
292
 
293
- {/* Wallet balance card */}
294
  {wallet && (
295
- <div className={`bg-white rounded-2xl border p-6 shadow-sm flex items-center justify-between ${walletExhausted ? 'border-red-300 bg-red-50' : walletLow ? 'border-amber-300 bg-amber-50' : 'border-slate-200'}`}>
296
- <div className="flex items-center gap-4">
297
- <div className={`w-12 h-12 rounded-2xl flex items-center justify-center text-xl ${walletExhausted ? 'bg-red-100' : walletLow ? 'bg-amber-100' : 'bg-emerald-100'}`}>
298
- 💰
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  </div>
300
- <div>
301
- <p className="text-sm text-slate-500">Solde Wallet</p>
302
- <p className={`text-3xl font-black ${walletExhausted ? 'text-red-600' : walletLow ? 'text-amber-600' : 'text-emerald-600'}`}>
303
- {wallet.walletBalance.toLocaleString('fr-FR')}
304
- <span className="text-base font-normal text-slate-400 ml-1">crédits</span>
305
- </p>
306
- <p className="text-xs text-slate-400 mt-0.5">
307
- {(wallet.walletBalance * 10).toLocaleString('fr-FR')} FCFA · 1 crédit = 10 FCFA
308
- </p>
309
  </div>
310
  </div>
311
- <button
312
- onClick={() => setShowRecharge(true)}
313
- className={`px-5 py-2.5 rounded-xl font-bold text-sm transition-colors ${walletExhausted ? 'bg-red-600 hover:bg-red-700 text-white' : 'bg-slate-900 hover:bg-slate-700 text-white'}`}
314
- >
315
- {walletExhausted ? '🔴 Recharger' : 'Recharger'}
316
- </button>
317
  </div>
318
  )}
319
 
320
- {/* 2 metric cards */}
321
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
322
 
323
- {/* AI Credits Card */}
324
- <div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
325
- <div className="flex items-center gap-3 mb-4">
326
  <div className="w-10 h-10 bg-indigo-100 rounded-xl flex items-center justify-center">
327
  <Brain className="w-5 h-5 text-indigo-600" />
328
  </div>
329
- <div>
330
- <p className="text-sm text-slate-500">Crédits IA</p>
331
- <p className="text-2xl font-bold text-slate-900">
332
- {summary.ai.creditsUsed.toLocaleString('fr-FR')}
333
- <span className="text-base font-normal text-slate-400"> / {summary.ai.creditsLimit.toLocaleString('fr-FR')}</span>
334
- </p>
335
- </div>
336
- </div>
337
- {/* Progress bar */}
338
- <div className="w-full bg-slate-100 rounded-full h-2.5 mb-3">
339
- <div
340
- className={`h-2.5 rounded-full transition-all ${summary.ai.percentUsed > 85 ? 'bg-red-500' : summary.ai.percentUsed > 60 ? 'bg-amber-500' : 'bg-indigo-500'}`}
341
- style={{ width: `${Math.min(summary.ai.percentUsed, 100)}%` }}
342
- />
343
- </div>
344
- <div className="flex justify-between text-sm text-slate-500">
345
- <span>{summary.ai.percentUsed}% utilisé</span>
346
- <span className="font-semibold text-slate-700">
347
- ~{summary.ai.costFcfa.toLocaleString('fr-FR')} FCFA
348
- </span>
349
  </div>
 
 
 
 
 
 
350
  {summary.ai.percentUsed > 85 && (
351
- <button
352
- onClick={() => setShowRecharge(true)}
353
- className="mt-3 w-full flex items-center gap-2 text-xs text-red-600 bg-red-50 hover:bg-red-100 px-3 py-2 rounded-lg transition-colors text-left"
354
- >
355
- <AlertCircle className="w-3.5 h-3.5 flex-shrink-0" />
356
- Quota bientôt épuisé — cliquez pour recharger
357
- </button>
 
 
 
 
 
358
  )}
359
  </div>
360
 
361
- {/* WhatsApp Messages Card */}
362
- <div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
363
- <div className="flex items-center gap-3 mb-4">
364
  <div className="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center">
365
  <MessageCircle className="w-5 h-5 text-emerald-600" />
366
  </div>
367
- <div>
368
- <p className="text-sm text-slate-500">Messages WhatsApp</p>
369
- <p className="text-2xl font-bold text-slate-900">
370
- {summary.whatsapp.messagesSent.toLocaleString('fr-FR')}
371
- </p>
372
- </div>
373
  </div>
374
- <div className="bg-emerald-50 rounded-xl p-3 mt-2">
 
 
 
375
  <p className="text-xs text-emerald-700">
376
- Aucun coût pour vous — Meta facture directement sur votre compte WABA
377
  </p>
378
  </div>
379
- <div className="mt-3 grid grid-cols-2 gap-2 text-sm">
380
- <div className="text-slate-500">Appels IA total</div>
381
- <div className="text-right font-semibold text-slate-700">{summary.ai.totalCalls.toLocaleString('fr-FR')}</div>
382
- <div className="text-slate-500">Tokens entrants</div>
383
- <div className="text-right font-semibold text-slate-700">{(summary.ai.tokensIn / 1000).toFixed(1)}K</div>
384
- <div className="text-slate-500">Tokens sortants</div>
385
- <div className="text-right font-semibold text-slate-700">{(summary.ai.tokensOut / 1000).toFixed(1)}K</div>
386
- </div>
387
  </div>
388
  </div>
389
 
390
- {/* Bar chart — last 30 days */}
391
  <div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
392
- <div className="flex items-center gap-2 mb-4">
393
  <TrendingUp className="w-4 h-4 text-slate-400" />
394
  <h2 className="text-sm font-semibold text-slate-700">Activité des 30 derniers jours</h2>
395
  </div>
 
396
  {history.length === 0 ? (
397
- <p className="text-slate-400 text-sm text-center py-8">Aucune donnée pour cette période</p>
398
  ) : (
399
  <div className="flex items-end gap-1 h-28 overflow-x-auto pb-2">
400
  {history.map(day => {
@@ -404,38 +421,35 @@ export default function BillingPage() {
404
  return (
405
  <div key={day.date} className="flex flex-col items-center gap-1 flex-1 min-w-[24px] group relative">
406
  <div
407
- className="w-full rounded-t-sm bg-indigo-400 group-hover:bg-indigo-600 transition-colors cursor-pointer"
408
  style={{ height: `${Math.max(heightPct, 2)}%` }}
409
- title={`${dateLabel} — IA: ${day.aiCalls} | WA: ${day.whatsappMessages} | ${day.costFcfa} FCFA`}
410
  />
411
  </div>
412
  );
413
  })}
414
  </div>
415
  )}
416
- <div className="flex gap-4 mt-3 text-xs text-slate-400">
417
- <span className="flex items-center gap-1"><span className="w-3 h-3 bg-indigo-400 rounded-sm inline-block" /> Appels IA + Messages WA</span>
418
- </div>
419
  </div>
420
 
421
- {/* Feature breakdown */}
422
  {breakdown.length > 0 && (
423
  <div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
424
- <h2 className="text-sm font-semibold text-slate-700 mb-4">Répartition par fonctionnalité (ce mois)</h2>
425
- <div className="space-y-2">
426
- {breakdown.slice(0, 6).map(item => {
 
427
  const totalCalls = breakdown.reduce((s, i) => s + i.calls, 0);
428
  const pct = totalCalls > 0 ? Math.round((item.calls / totalCalls) * 100) : 0;
429
  return (
430
- <div key={`${item.feature}-${item.type}`} className="flex items-center gap-3">
431
- <span className="text-sm text-slate-600 w-44 flex-shrink-0">
432
  {FEATURE_LABELS[item.feature] ?? item.feature}
433
  </span>
434
- <div className="flex-1 bg-slate-100 rounded-full h-2">
435
- <div className="h-2 rounded-full bg-indigo-400" style={{ width: `${pct}%` }} />
436
  </div>
437
- <span className="text-xs text-slate-400 w-8 text-right">{pct}%</span>
438
- <span className="text-xs text-slate-500 w-24 text-right">{item.calls} appels</span>
439
  </div>
440
  );
441
  })}
@@ -443,76 +457,85 @@ export default function BillingPage() {
443
  </div>
444
  )}
445
 
446
- {/* Wallet transactions */}
447
  {wallet && wallet.transactions.length > 0 && (
448
  <div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
449
  <div className="px-6 py-4 border-b border-slate-100">
450
- <h2 className="text-sm font-semibold text-slate-700">Dernières transactions wallet</h2>
 
451
  </div>
452
- <div className="overflow-x-auto">
453
- <table className="w-full text-sm">
454
- <thead>
455
- <tr className="border-b border-slate-100 text-xs text-slate-400 uppercase">
456
- <th className="px-6 py-3 text-left font-semibold">Date</th>
457
- <th className="px-6 py-3 text-left font-semibold">Type</th>
458
- <th className="px-6 py-3 text-left font-semibold">Description</th>
459
- <th className="px-6 py-3 text-right font-semibold">Montant</th>
460
- <th className="px-6 py-3 text-right font-semibold">Solde après</th>
461
- </tr>
462
- </thead>
463
- <tbody>
464
- {wallet.transactions.map(tx => {
465
- const isCredit = tx.amount > 0;
466
- const typeLabels: Record<string, string> = {
467
- TOP_UP_MANUAL: ' Recharge manuelle',
468
- TOP_UP_PAYMENT: '➕ Paiement',
469
- ADJUSTMENT: '🔧 Ajustement',
470
- DEBIT_AI: '🤖 IA',
471
- DEBIT_WHATSAPP: '💬 WhatsApp',
472
- DEBIT_BROADCAST: '📣 Broadcast',
473
- };
474
- return (
475
- <tr key={tx.id} className="border-b border-slate-50 hover:bg-slate-50 transition-colors">
476
- <td className="px-6 py-3 text-slate-500 whitespace-nowrap">
477
- {new Date(tx.createdAt).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })}
478
- </td>
479
- <td className="px-6 py-3 text-slate-600 whitespace-nowrap">
480
- {typeLabels[tx.type] ?? tx.type}
481
- {tx.byok && <span className="ml-1 text-xs text-slate-400">(BYOK)</span>}
482
- </td>
483
- <td className="px-6 py-3 text-slate-500 max-w-[200px] truncate">
484
- {tx.description ?? '—'}
485
- </td>
486
- <td className={`px-6 py-3 text-right font-mono font-semibold whitespace-nowrap ${isCredit ? 'text-emerald-600' : 'text-slate-700'}`}>
487
- {isCredit ? '+' : ''}{tx.amount.toLocaleString('fr-FR')} cr
488
- </td>
489
- <td className="px-6 py-3 text-right font-mono text-slate-500 whitespace-nowrap">
490
- {tx.balanceAfter >= 0 ? tx.balanceAfter.toLocaleString('fr-FR') : '—'}
491
- </td>
492
- </tr>
493
- );
494
- })}
495
- </tbody>
496
- </table>
497
  </div>
498
  </div>
499
  )}
500
 
501
- {/* AI Chat Widget */}
502
- <div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
503
- <div className="px-6 py-4 border-b border-slate-100 bg-slate-50">
504
- <h2 className="text-sm font-semibold text-slate-700">💬 Pose une question sur ta facturation</h2>
505
- <p className="text-xs text-slate-400 mt-0.5">Exemples : "Combien j'ai dépensé cette semaine ?" · "Quelle fonctionnalité consomme le plus ?"</p>
 
 
 
506
  </div>
507
 
508
- {/* Chat messages */}
509
- <div className="p-4 space-y-3 max-h-64 overflow-y-auto">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  {chatMessages.length === 0 && (
511
- <p className="text-slate-300 text-sm text-center py-4">Aucune question posée pour l'instant</p>
 
 
 
512
  )}
513
  {chatMessages.map((msg, i) => (
514
  <div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
515
- <div className={`max-w-xs lg:max-w-md px-4 py-2 rounded-2xl text-sm leading-relaxed ${
516
  msg.role === 'user'
517
  ? 'bg-indigo-600 text-white rounded-br-sm'
518
  : 'bg-slate-100 text-slate-800 rounded-bl-sm'
@@ -523,8 +546,10 @@ export default function BillingPage() {
523
  ))}
524
  {chatLoading && (
525
  <div className="flex justify-start">
526
- <div className="bg-slate-100 px-4 py-2 rounded-2xl rounded-bl-sm">
527
- <Loader2 className="w-4 h-4 animate-spin text-slate-400" />
 
 
528
  </div>
529
  </div>
530
  )}
@@ -538,14 +563,14 @@ export default function BillingPage() {
538
  value={chatInput}
539
  onChange={e => setChatInput(e.target.value)}
540
  onKeyDown={e => e.key === 'Enter' && sendChat()}
541
- placeholder="Pose ta question..."
542
- className="flex-1 text-sm border border-slate-200 rounded-xl px-4 py-2 outline-none focus:ring-2 focus:ring-indigo-300"
543
  disabled={chatLoading}
544
  />
545
  <button
546
- onClick={sendChat}
547
  disabled={!chatInput.trim() || chatLoading}
548
- className="bg-indigo-600 hover:bg-indigo-700 disabled:opacity-40 text-white rounded-xl px-4 py-2 flex items-center gap-1 text-sm font-medium transition-colors"
549
  >
550
  <Send className="w-4 h-4" />
551
  </button>
 
1
  import { useState, useEffect, useRef } from 'react';
2
+ import { Brain, MessageCircle, TrendingUp, Send, Loader2, AlertCircle, Zap, X, Flame, HelpCircle } from 'lucide-react';
3
  import { api } from '@/lib/api';
4
  import { useAuth } from '@/lib/auth';
5
  import { useTenant } from '@/lib/tenant';
6
 
7
  const CREDIT_PACKS = [
8
+ { label: '500 crédits', price: '5 000 FCFA', credits: 500, popular: false },
9
+ { label: '2 000 crédits', price: '18 000 FCFA', credits: 2000, popular: true },
10
+ { label: '5 000 crédits', price: '40 000 FCFA', credits: 5000, popular: false },
11
  ];
12
 
13
+ const SUPPORT_WA_URL = 'https://wa.me/221700000000?text=Bonjour%2C%20je%20souhaite%20recharger%20mes%20cr%C3%A9dits%20Xaml%C3%A9.';
14
+
15
+ const QUICK_QUESTIONS = [
16
+ 'Combien de crédits me reste-t-il ?',
17
+ 'Dans combien de jours serai-je à court ?',
18
+ 'Quelle fonctionnalité consomme le plus ?',
19
+ "Combien j'ai dépensé cette semaine ?",
20
+ ];
21
+
22
+ const FEATURE_LABELS: Record<string, string> = {
23
+ LESSON: '📚 Leçons envoyées',
24
+ FEEDBACK: '✅ Feedbacks exercices',
25
+ DEEPDIVE: '🔍 Approfondissements',
26
+ TRANSCRIPTION: '🎤 Transcriptions audio',
27
+ IMAGE_ANALYSIS: '🖼️ Analyses image',
28
+ CAMPAIGN: '📣 Campagnes',
29
+ ONBOARDING: '👋 Accueil nouveaux apprenants',
30
+ OTHER: '⚙️ Autres',
31
+ };
32
 
33
  function RechargeModal({ onClose }: { onClose: () => void }) {
34
  useEffect(() => {
 
50
  className="bg-white w-full sm:max-w-md rounded-t-3xl sm:rounded-2xl shadow-2xl flex flex-col max-h-[90vh]"
51
  onClick={e => e.stopPropagation()}
52
  >
 
53
  <div className="flex items-center justify-between px-6 py-5 border-b border-slate-100 shrink-0">
54
+ <div>
55
+ <h2 className="text-lg font-bold text-slate-900">Recharger les crédits</h2>
56
+ <p className="text-xs text-slate-400 mt-0.5">1 crédit = 10 FCFA · utilisé pour chaque message IA</p>
57
+ </div>
58
  <button
59
  onClick={onClose}
60
+ className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-slate-100 text-slate-400 transition-colors"
61
  >
62
  <X className="w-5 h-5" />
63
  </button>
64
  </div>
65
 
 
66
  <div className="overflow-y-auto flex-1 px-6 py-5 space-y-3">
67
  {CREDIT_PACKS.map(pack => (
68
  <a
 
70
  href={`${SUPPORT_WA_URL}%20-%20Pack%20${pack.label}`}
71
  target="_blank"
72
  rel="noopener noreferrer"
73
+ className={`relative flex items-center justify-between p-4 border rounded-xl transition-all group ${pack.popular ? 'border-indigo-400 bg-indigo-50' : 'border-slate-200 hover:border-indigo-300 hover:bg-indigo-50'}`}
74
  >
75
+ {pack.popular && (
76
+ <span className="absolute -top-2.5 left-4 px-2 py-0.5 bg-indigo-600 text-white text-xs font-bold rounded-full">
77
+ Le plus populaire
78
+ </span>
79
+ )}
80
  <div>
81
  <p className="font-semibold text-slate-800 group-hover:text-indigo-700">{pack.label}</p>
82
+ <p className="text-xs text-slate-400 mt-0.5"> {pack.credits} messages IA</p>
83
  </div>
84
  <span className="text-indigo-600 font-bold shrink-0 ml-4">{pack.price}</span>
85
  </a>
86
  ))}
87
  </div>
88
 
 
89
  <div className="px-6 py-4 border-t border-slate-100 shrink-0">
90
  <p className="text-xs text-slate-400 text-center">
91
+ Cliquez sur un pack WhatsApp s'ouvre notre équipe finalise la recharge en moins de 2 heures.
92
  </p>
93
  </div>
94
  </div>
 
113
  interface BillingSummary {
114
  plan: string;
115
  planLabel: string;
 
116
  period: { start: string; end: string };
117
  ai: {
118
  creditsUsed: number;
119
  creditsLimit: number;
120
  percentUsed: number;
121
  totalCalls: number;
 
 
 
122
  costFcfa: number;
123
  };
124
+ whatsapp: { messagesSent: number };
125
  }
126
 
127
  interface HistoryDay {
128
  date: string;
129
  aiCalls: number;
130
  whatsappMessages: number;
 
131
  costFcfa: number;
132
  }
133
 
134
  interface BreakdownItem {
135
  feature: string;
 
136
  calls: number;
 
137
  }
138
 
139
  interface ChatMessage {
 
141
  text: string;
142
  }
143
 
144
+ function DaysRemainingBadge({ walletBalance, transactions }: { walletBalance: number; transactions: WalletData['transactions'] }) {
145
+ const sevenDaysAgo = Date.now() - 7 * 86_400_000;
146
+ const weeklyDebit = transactions
147
+ .filter(tx => tx.amount < 0 && new Date(tx.createdAt).getTime() > sevenDaysAgo)
148
+ .reduce((sum, tx) => sum + Math.abs(tx.amount), 0);
 
 
 
 
 
149
 
150
+ if (weeklyDebit === 0) return null;
151
+
152
+ const dailyBurn = weeklyDebit / 7;
153
+ const days = Math.floor(walletBalance / dailyBurn);
154
+
155
+ const color = days <= 3 ? 'text-red-600 bg-red-50' : days <= 10 ? 'text-amber-600 bg-amber-50' : 'text-emerald-600 bg-emerald-50';
156
+ const icon = days <= 3 ? '🔴' : days <= 10 ? '🟡' : '🟢';
157
+
158
+ return (
159
+ <span className={`inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-semibold ${color}`}>
160
+ {icon} À ce rythme : encore <strong>~{days} jour{days > 1 ? 's' : ''}</strong>
161
+ </span>
162
+ );
163
+ }
164
 
165
  export default function BillingPage() {
166
  const { token, user } = useAuth();
 
175
  const [error, setError] = useState<string | null>(null);
176
  const [showRecharge, setShowRecharge] = useState(false);
177
 
 
178
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
179
  const [chatInput, setChatInput] = useState('');
180
  const [chatLoading, setChatLoading] = useState(false);
 
201
  chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
202
  }, [chatMessages]);
203
 
204
+ const sendChat = async (question?: string) => {
205
+ const q = (question ?? chatInput).trim();
206
+ if (!q || chatLoading || !orgId || !token) return;
207
  setChatInput('');
208
+ setChatMessages(prev => [...prev, { role: 'user', text: q }]);
209
  setChatLoading(true);
210
  try {
211
+ const res = await api.post('/v1/billing/chat', { question: q, language: user?.language ?? 'FR' }, token, orgId);
212
  setChatMessages(prev => [...prev, { role: 'assistant', text: res.answer }]);
213
  } catch {
214
  setChatMessages(prev => [...prev, { role: 'assistant', text: 'Désolé, je ne peux pas répondre pour le moment.' }]);
 
217
  }
218
  };
219
 
 
220
  const maxCalls = Math.max(...history.map(d => d.aiCalls + d.whatsappMessages), 1);
221
 
222
  if (!orgId) {
 
247
  if (!summary) return null;
248
 
249
  const periodLabel = new Date(summary.period.start).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' });
 
250
  const walletExhausted = wallet ? (wallet.isHardStopped || wallet.walletBalance <= 0) : false;
251
  const walletLow = wallet ? (wallet.walletBalance > 0 && wallet.walletBalance <= 200) : false;
252
 
253
+ const TX_LABELS: Record<string, string> = {
254
+ TOP_UP_MANUAL: '➕ Recharge',
255
+ TOP_UP_PAYMENT: '➕ Recharge',
256
+ ADJUSTMENT: '🔧 Ajustement',
257
+ DEBIT_AI: '🤖 Message IA',
258
+ DEBIT_WHATSAPP: '💬 Message WhatsApp',
259
+ DEBIT_BROADCAST: '📣 Campagne',
260
+ };
261
+
262
  return (
263
  <div className="p-6 max-w-5xl mx-auto space-y-6">
264
  {showRecharge && <RechargeModal onClose={() => setShowRecharge(false)} />}
265
 
266
+ {/* Alerte service suspendu */}
267
  {walletExhausted && (
268
  <div className="flex items-center justify-between gap-4 px-5 py-4 bg-red-50 border border-red-200 rounded-2xl">
269
  <div className="flex items-center gap-3 text-red-700">
270
  <AlertCircle className="w-5 h-5 shrink-0" />
271
+ <div>
272
+ <p className="font-bold text-sm">Service suspendu — vos crédits sont épuisés</p>
273
+ <p className="text-xs text-red-500 mt-0.5">Rechargez pour rétablir les messages IA immédiatement.</p>
274
+ </div>
275
  </div>
276
  <button
277
  onClick={() => setShowRecharge(true)}
 
282
  </div>
283
  )}
284
 
285
+ {/* Alerte solde bas */}
286
  {!walletExhausted && walletLow && (
287
  <div className="flex items-center justify-between gap-4 px-5 py-4 bg-amber-50 border border-amber-200 rounded-2xl">
288
  <div className="flex items-center gap-3 text-amber-700">
289
  <AlertCircle className="w-5 h-5 shrink-0" />
290
+ <div>
291
+ <p className="font-bold text-sm">Crédits presque épuisés — {wallet!.walletBalance} crédits restants</p>
292
+ <p className="text-xs text-amber-600 mt-0.5">Rechargez avant interruption du service.</p>
293
+ </div>
294
  </div>
295
  <button
296
  onClick={() => setShowRecharge(true)}
 
301
  </div>
302
  )}
303
 
304
+ {/* En-tête */}
305
  <div className="flex items-center justify-between">
306
  <div>
307
+ <h1 className="text-2xl font-bold text-slate-900">Mes crédits & consommation</h1>
308
  <p className="text-slate-500 text-sm mt-1">Période : {periodLabel}</p>
309
  </div>
310
+ <button
311
+ onClick={() => setShowRecharge(true)}
312
+ className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold rounded-xl transition-colors shadow-sm"
313
+ >
314
+ <Zap className="w-4 h-4" />
315
+ Recharger
316
+ </button>
 
 
 
 
 
317
  </div>
318
 
319
+ {/* Carte principale Crédits disponibles */}
320
  {wallet && (
321
+ <div className={`rounded-2xl border p-6 shadow-sm ${walletExhausted ? 'border-red-200 bg-red-50' : walletLow ? 'border-amber-200 bg-amber-50' : 'bg-white border-slate-200'}`}>
322
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
323
+ <div className="flex items-center gap-4">
324
+ <div className={`w-14 h-14 rounded-2xl flex items-center justify-center text-2xl flex-shrink-0 ${walletExhausted ? 'bg-red-100' : walletLow ? 'bg-amber-100' : 'bg-emerald-100'}`}>
325
+ 💳
326
+ </div>
327
+ <div>
328
+ <p className="text-sm font-medium text-slate-500">Crédits disponibles</p>
329
+ <p className={`text-4xl font-black leading-tight ${walletExhausted ? 'text-red-600' : walletLow ? 'text-amber-600' : 'text-slate-900'}`}>
330
+ {wallet.walletBalance.toLocaleString('fr-FR')}
331
+ <span className="text-lg font-normal text-slate-400 ml-1.5">crédits</span>
332
+ </p>
333
+ <p className="text-sm text-slate-400 mt-1">
334
+ ≈ {(wallet.walletBalance * 10).toLocaleString('fr-FR')} FCFA
335
+ </p>
336
+ <div className="mt-2">
337
+ <DaysRemainingBadge walletBalance={wallet.walletBalance} transactions={wallet.transactions} />
338
+ </div>
339
+ </div>
340
  </div>
341
+ <div className="flex flex-col items-start sm:items-end gap-2 shrink-0">
342
+ <button
343
+ onClick={() => setShowRecharge(true)}
344
+ className="px-5 py-2.5 bg-slate-900 hover:bg-slate-700 text-white rounded-xl font-bold text-sm transition-colors"
345
+ >
346
+ Recharger les crédits
347
+ </button>
348
+ <p className="text-xs text-slate-400">1 crédit = 1 message IA = 10 FCFA</p>
 
349
  </div>
350
  </div>
 
 
 
 
 
 
351
  </div>
352
  )}
353
 
354
+ {/* Deux métriques simples */}
355
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
356
 
357
+ {/* Messages IA ce mois */}
358
+ <div className="bg-white rounded-2xl border border-slate-200 p-5 shadow-sm">
359
+ <div className="flex items-center gap-3 mb-3">
360
  <div className="w-10 h-10 bg-indigo-100 rounded-xl flex items-center justify-center">
361
  <Brain className="w-5 h-5 text-indigo-600" />
362
  </div>
363
+ <p className="text-sm font-semibold text-slate-600">Messages IA ce mois</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  </div>
365
+ <p className="text-3xl font-black text-slate-900">
366
+ {summary.ai.totalCalls.toLocaleString('fr-FR')}
367
+ </p>
368
+ <p className="text-sm text-slate-400 mt-1">
369
+ ≈ {summary.ai.costFcfa.toLocaleString('fr-FR')} FCFA de coût IA
370
+ </p>
371
  {summary.ai.percentUsed > 85 && (
372
+ <div className="mt-3">
373
+ <div className="w-full bg-slate-100 rounded-full h-2 mb-1">
374
+ <div className="h-2 rounded-full bg-red-500" style={{ width: `${Math.min(summary.ai.percentUsed, 100)}%` }} />
375
+ </div>
376
+ <button
377
+ onClick={() => setShowRecharge(true)}
378
+ className="flex items-center gap-1.5 text-xs text-red-600 hover:underline mt-1"
379
+ >
380
+ <AlertCircle className="w-3.5 h-3.5" />
381
+ Quota mensuel bientôt atteint ({summary.ai.percentUsed}%)
382
+ </button>
383
+ </div>
384
  )}
385
  </div>
386
 
387
+ {/* Messages WhatsApp ce mois */}
388
+ <div className="bg-white rounded-2xl border border-slate-200 p-5 shadow-sm">
389
+ <div className="flex items-center gap-3 mb-3">
390
  <div className="w-10 h-10 bg-emerald-100 rounded-xl flex items-center justify-center">
391
  <MessageCircle className="w-5 h-5 text-emerald-600" />
392
  </div>
393
+ <p className="text-sm font-semibold text-slate-600">Messages WhatsApp ce mois</p>
 
 
 
 
 
394
  </div>
395
+ <p className="text-3xl font-black text-slate-900">
396
+ {summary.whatsapp.messagesSent.toLocaleString('fr-FR')}
397
+ </p>
398
+ <div className="mt-3 bg-emerald-50 rounded-xl px-3 py-2.5">
399
  <p className="text-xs text-emerald-700">
400
+ Gratuit pour vous — Meta facture directement votre compte WhatsApp Business
401
  </p>
402
  </div>
 
 
 
 
 
 
 
 
403
  </div>
404
  </div>
405
 
406
+ {/* Graphique activité 30 jours */}
407
  <div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
408
+ <div className="flex items-center gap-2 mb-1">
409
  <TrendingUp className="w-4 h-4 text-slate-400" />
410
  <h2 className="text-sm font-semibold text-slate-700">Activité des 30 derniers jours</h2>
411
  </div>
412
+ <p className="text-xs text-slate-400 mb-4">Chaque barre = nombre de messages IA + WhatsApp ce jour-là</p>
413
  {history.length === 0 ? (
414
+ <p className="text-slate-400 text-sm text-center py-8">Aucune activité sur cette période</p>
415
  ) : (
416
  <div className="flex items-end gap-1 h-28 overflow-x-auto pb-2">
417
  {history.map(day => {
 
421
  return (
422
  <div key={day.date} className="flex flex-col items-center gap-1 flex-1 min-w-[24px] group relative">
423
  <div
424
+ className="w-full rounded-t-sm bg-indigo-300 group-hover:bg-indigo-500 transition-colors cursor-pointer"
425
  style={{ height: `${Math.max(heightPct, 2)}%` }}
426
+ title={`${dateLabel} — IA: ${day.aiCalls} msg | WA: ${day.whatsappMessages} msg`}
427
  />
428
  </div>
429
  );
430
  })}
431
  </div>
432
  )}
 
 
 
433
  </div>
434
 
435
+ {/* Répartition par fonctionnalité */}
436
  {breakdown.length > 0 && (
437
  <div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm">
438
+ <h2 className="text-sm font-semibold text-slate-700 mb-1"> vont vos crédits IA ?</h2>
439
+ <p className="text-xs text-slate-400 mb-4">Répartition de l'utilisation par type de message ce mois</p>
440
+ <div className="space-y-3">
441
+ {breakdown.slice(0, 5).map(item => {
442
  const totalCalls = breakdown.reduce((s, i) => s + i.calls, 0);
443
  const pct = totalCalls > 0 ? Math.round((item.calls / totalCalls) * 100) : 0;
444
  return (
445
+ <div key={item.feature} className="flex items-center gap-3">
446
+ <span className="text-sm text-slate-600 w-48 flex-shrink-0 truncate">
447
  {FEATURE_LABELS[item.feature] ?? item.feature}
448
  </span>
449
+ <div className="flex-1 bg-slate-100 rounded-full h-2.5">
450
+ <div className="h-2.5 rounded-full bg-indigo-400" style={{ width: `${pct}%` }} />
451
  </div>
452
+ <span className="text-sm font-semibold text-slate-700 w-10 text-right">{pct}%</span>
 
453
  </div>
454
  );
455
  })}
 
457
  </div>
458
  )}
459
 
460
+ {/* Historique des transactions — simplifié */}
461
  {wallet && wallet.transactions.length > 0 && (
462
  <div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden">
463
  <div className="px-6 py-4 border-b border-slate-100">
464
+ <h2 className="text-sm font-semibold text-slate-700">Derniers mouvements de crédits</h2>
465
+ <p className="text-xs text-slate-400 mt-0.5">Les 20 dernières opérations sur votre solde</p>
466
  </div>
467
+ <div className="divide-y divide-slate-50">
468
+ {wallet.transactions.filter(tx => !tx.byok).map(tx => {
469
+ const isCredit = tx.amount > 0;
470
+ return (
471
+ <div key={tx.id} className="flex items-center justify-between px-6 py-3 hover:bg-slate-50 transition-colors">
472
+ <div className="flex items-center gap-3 min-w-0">
473
+ <div className={`w-8 h-8 rounded-full flex items-center justify-center text-sm flex-shrink-0 ${isCredit ? 'bg-emerald-100' : 'bg-slate-100'}`}>
474
+ {isCredit ? '➕' : '▪️'}
475
+ </div>
476
+ <div className="min-w-0">
477
+ <p className="text-sm font-medium text-slate-700 truncate">
478
+ {TX_LABELS[tx.type] ?? tx.type}
479
+ </p>
480
+ <p className="text-xs text-slate-400">
481
+ {new Date(tx.createdAt).toLocaleDateString('fr-FR', {
482
+ day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit'
483
+ })}
484
+ </p>
485
+ </div>
486
+ </div>
487
+ <div className="text-right ml-4 flex-shrink-0">
488
+ <p className={`text-sm font-bold ${isCredit ? 'text-emerald-600' : 'text-slate-700'}`}>
489
+ {isCredit ? '+' : ''}{tx.amount.toLocaleString('fr-FR')} cr
490
+ </p>
491
+ <p className="text-xs text-slate-400">
492
+ Solde : {tx.balanceAfter >= 0 ? tx.balanceAfter.toLocaleString('fr-FR') : ''}
493
+ </p>
494
+ </div>
495
+ </div>
496
+ );
497
+ })}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  </div>
499
  </div>
500
  )}
501
 
502
+ {/* Assistant IA facturation */}
503
+ <div className="bg-white rounded-2xl border border-indigo-100 shadow-sm overflow-hidden">
504
+ <div className="px-6 py-4 border-b border-slate-100 bg-gradient-to-r from-indigo-50 to-white">
505
+ <div className="flex items-center gap-2">
506
+ <HelpCircle className="w-4 h-4 text-indigo-500" />
507
+ <h2 className="text-sm font-bold text-slate-800">Posez une question sur votre consommation</h2>
508
+ </div>
509
+ <p className="text-xs text-slate-400 mt-0.5">Réponses basées sur vos vraies données · en français</p>
510
  </div>
511
 
512
+ {/* Questions suggérées */}
513
+ {chatMessages.length === 0 && (
514
+ <div className="px-4 pt-4 flex flex-wrap gap-2">
515
+ {QUICK_QUESTIONS.map(q => (
516
+ <button
517
+ key={q}
518
+ onClick={() => sendChat(q)}
519
+ disabled={chatLoading}
520
+ className="text-xs px-3 py-1.5 bg-slate-100 hover:bg-indigo-100 hover:text-indigo-700 text-slate-600 rounded-full transition-colors border border-transparent hover:border-indigo-200"
521
+ >
522
+ {q}
523
+ </button>
524
+ ))}
525
+ </div>
526
+ )}
527
+
528
+ {/* Messages */}
529
+ <div className="p-4 space-y-3 max-h-72 overflow-y-auto">
530
  {chatMessages.length === 0 && (
531
+ <p className="text-slate-300 text-sm text-center py-6">
532
+ <Flame className="w-8 h-8 mx-auto mb-2 text-slate-200" />
533
+ Cliquez sur une question ci-dessus ou écrivez la vôtre
534
+ </p>
535
  )}
536
  {chatMessages.map((msg, i) => (
537
  <div key={i} className={`flex ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
538
+ <div className={`max-w-xs lg:max-w-md px-4 py-2.5 rounded-2xl text-sm leading-relaxed whitespace-pre-wrap ${
539
  msg.role === 'user'
540
  ? 'bg-indigo-600 text-white rounded-br-sm'
541
  : 'bg-slate-100 text-slate-800 rounded-bl-sm'
 
546
  ))}
547
  {chatLoading && (
548
  <div className="flex justify-start">
549
+ <div className="bg-slate-100 px-4 py-3 rounded-2xl rounded-bl-sm flex items-center gap-1.5">
550
+ <span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
551
+ <span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
552
+ <span className="w-1.5 h-1.5 bg-slate-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
553
  </div>
554
  </div>
555
  )}
 
563
  value={chatInput}
564
  onChange={e => setChatInput(e.target.value)}
565
  onKeyDown={e => e.key === 'Enter' && sendChat()}
566
+ placeholder="Ex: Combien de crédits ai-je utilisé cette semaine ?"
567
+ className="flex-1 text-sm border border-slate-200 rounded-xl px-4 py-2.5 outline-none focus:ring-2 focus:ring-indigo-300 focus:border-indigo-300"
568
  disabled={chatLoading}
569
  />
570
  <button
571
+ onClick={() => sendChat()}
572
  disabled={!chatInput.trim() || chatLoading}
573
+ className="bg-indigo-600 hover:bg-indigo-700 disabled:opacity-40 text-white rounded-xl px-4 py-2.5 flex items-center gap-1 text-sm font-medium transition-colors"
574
  >
575
  <Send className="w-4 h-4" />
576
  </button>
apps/api/src/routes/billing.ts CHANGED
@@ -144,53 +144,91 @@ export async function billingRoutes(fastify: FastifyInstance) {
144
  const { question, language = 'FR' } = req.body;
145
  if (!question?.trim()) return reply.code(400).send({ error: 'Question is required' });
146
 
147
- // Fetch context
148
  const org = await prisma.organization.findUnique({
149
  where: { id: organizationId },
150
- select: { name: true, subscriptionPlan: true, aiCreditsUsed: true, aiCreditsLimit: true, whatsappMessagesSent: true, billingPeriodStart: true }
 
 
 
 
 
151
  });
152
  if (!org) return reply.code(404).send({ error: 'Organization not found' });
153
 
154
  const periodStart = org.billingPeriodStart ?? new Date(new Date().getFullYear(), new Date().getMonth(), 1);
155
- const costAgg = await prisma.usageEvent.aggregate({
156
- where: { organizationId, createdAt: { gte: periodStart } },
157
- _sum: { costUsd: true },
158
- _count: { id: true },
159
- });
160
-
161
- const week = new Date(); week.setDate(week.getDate() - 7);
162
- const weekAgg = await prisma.usageEvent.aggregate({
163
- where: { organizationId, createdAt: { gte: week }, type: { not: 'WHATSAPP_SENT' } },
164
- _sum: { costUsd: true },
165
- _count: { id: true },
166
- });
167
 
168
- const breakdown = await prisma.usageEvent.groupBy({
169
- by: ['feature'],
170
- where: { organizationId, createdAt: { gte: periodStart }, type: { not: 'WHATSAPP_SENT' } },
171
- _count: { id: true },
172
- orderBy: { _count: { id: 'desc' } },
173
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
  const topFeature = breakdown[0]?.feature ?? 'FEEDBACK';
176
  const topFeaturePct = breakdown[0] && costAgg._count.id > 0
177
  ? Math.round((breakdown[0]._count.id / costAgg._count.id) * 100)
178
  : 0;
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  const context = `Organisation: ${org.name}
181
  Plan: ${org.subscriptionPlan}
182
  Période actuelle: depuis le ${periodStart.toLocaleDateString('fr-FR')}
183
- Crédits IA utilisés: ${org.aiCreditsUsed} / ${org.aiCreditsLimit}
184
- Appels IA ce mois: ${costAgg._count.id}
185
- Coût IA ce mois: ${Math.round((costAgg._sum.costUsd ?? 0) * 600)} FCFA ($${((costAgg._sum.costUsd ?? 0)).toFixed(4)})
186
- Appels IA cette semaine: ${weekAgg._count.id}
187
- Coût IA cette semaine: ${Math.round((weekAgg._sum.costUsd ?? 0) * 600)} FCFA
188
- Messages WhatsApp envoyés ce mois: ${org.whatsappMessagesSent}
189
- Feature la plus consommatrice: ${topFeature} (${topFeaturePct}% des appels)`;
 
 
 
 
190
 
191
  const systemPrompt = language === 'FR'
192
- ? `Tu es un assistant facturation pour la plateforme Welfin/Xamlé. Tu aides les administrateurs à comprendre leur consommation IA et WhatsApp. Réponds de façon simple, concrète, en français. Utilise les données ci-dessous. Ne fais pas de suppositions au-delà des données fournies.`
193
- : `You are a billing assistant for the Welfin/Xamlé platform. Help administrators understand their AI and WhatsApp usage. Reply simply and concretely in English. Use the data below. Do not assume beyond the data provided.`;
194
 
195
  try {
196
  const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
 
144
  const { question, language = 'FR' } = req.body;
145
  if (!question?.trim()) return reply.code(400).send({ error: 'Question is required' });
146
 
147
+ // Fetch context (org + wallet)
148
  const org = await prisma.organization.findUnique({
149
  where: { id: organizationId },
150
+ select: {
151
+ name: true, subscriptionPlan: true,
152
+ aiCreditsUsed: true, aiCreditsLimit: true,
153
+ whatsappMessagesSent: true, billingPeriodStart: true,
154
+ walletBalance: true, isHardStopped: true,
155
+ }
156
  });
157
  if (!org) return reply.code(404).send({ error: 'Organization not found' });
158
 
159
  const periodStart = org.billingPeriodStart ?? new Date(new Date().getFullYear(), new Date().getMonth(), 1);
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
+ const [costAgg, weekAgg, breakdown, recentDebits] = await Promise.all([
162
+ prisma.usageEvent.aggregate({
163
+ where: { organizationId, createdAt: { gte: periodStart } },
164
+ _sum: { costUsd: true },
165
+ _count: { id: true },
166
+ }),
167
+ prisma.usageEvent.aggregate({
168
+ where: { organizationId, type: { not: 'WHATSAPP_SENT' }, createdAt: { gte: new Date(Date.now() - 7 * 86_400_000) } },
169
+ _sum: { costUsd: true },
170
+ _count: true,
171
+ }),
172
+ prisma.usageEvent.groupBy({
173
+ by: ['feature'],
174
+ where: { organizationId, createdAt: { gte: periodStart }, type: { not: 'WHATSAPP_SENT' } },
175
+ _count: { id: true },
176
+ orderBy: { _count: { id: 'desc' } },
177
+ }),
178
+ // Last 7 days of wallet debits to estimate burn rate
179
+ prisma.walletTransaction.aggregate({
180
+ where: {
181
+ organizationId,
182
+ amount: { lt: 0 },
183
+ createdAt: { gte: new Date(Date.now() - 7 * 86_400_000) },
184
+ },
185
+ _sum: { amount: true },
186
+ }),
187
+ ]);
188
 
189
  const topFeature = breakdown[0]?.feature ?? 'FEEDBACK';
190
  const topFeaturePct = breakdown[0] && costAgg._count.id > 0
191
  ? Math.round((breakdown[0]._count.id / costAgg._count.id) * 100)
192
  : 0;
193
 
194
+ const featureLabels: Record<string, string> = {
195
+ LESSON: 'Leçons', FEEDBACK: 'Feedbacks exercices', DEEPDIVE: 'Approfondissements',
196
+ TRANSCRIPTION: 'Transcriptions audio', IMAGE_ANALYSIS: 'Analyses image',
197
+ CAMPAIGN: 'Campagnes', ONBOARDING: 'Onboarding', OTHER: 'Autres',
198
+ };
199
+
200
+ // Burn rate: average daily wallet debit over last 7 days
201
+ const weeklyDebit = Math.abs(recentDebits._sum.amount ?? 0);
202
+ const dailyBurn = weeklyDebit / 7;
203
+ const daysRemaining = dailyBurn > 0 ? Math.floor(org.walletBalance / dailyBurn) : null;
204
+ const burnInfo = daysRemaining !== null
205
+ ? `À ce rythme (${Math.round(dailyBurn)} crédits/jour), le solde durera encore environ ${daysRemaining} jour(s).`
206
+ : 'Aucune consommation récente détectée.';
207
+
208
+ const walletStatus = org.isHardStopped
209
+ ? 'SERVICE SUSPENDU (solde épuisé)'
210
+ : org.walletBalance <= 200
211
+ ? `Solde bas (${org.walletBalance} crédits)`
212
+ : `Actif (${org.walletBalance} crédits)`;
213
+
214
  const context = `Organisation: ${org.name}
215
  Plan: ${org.subscriptionPlan}
216
  Période actuelle: depuis le ${periodStart.toLocaleDateString('fr-FR')}
217
+ --- SOLDE DE CRÉDITS ---
218
+ Statut: ${walletStatus}
219
+ Crédits disponibles: ${org.walletBalance} (1 crédit = 10 FCFA)
220
+ Projection: ${burnInfo}
221
+ --- UTILISATION CE MOIS ---
222
+ Appels IA: ${costAgg._count.id}
223
+ Messages WhatsApp envoyés: ${org.whatsappMessagesSent}
224
+ Fonctionnalité la plus utilisée: ${featureLabels[topFeature] ?? topFeature} (${topFeaturePct}% des appels)
225
+ --- UTILISATION CETTE SEMAINE ---
226
+ Appels IA: ${typeof weekAgg._count === 'number' ? weekAgg._count : (weekAgg._count as any)?.id ?? 0}
227
+ Crédits consommés: ${weeklyDebit} crédits (= ${weeklyDebit * 10} FCFA)`;
228
 
229
  const systemPrompt = language === 'FR'
230
+ ? `Tu es un assistant facturation pour la plateforme Xamlé. Tu aides des administrateurs non-techniques à comprendre leur consommation et leur solde de crédits. Réponds de façon simple, concrète et rassurante, en français. Évite tout jargon technique (tokens, API, etc.). Utilise uniquement les données ci-dessous. Si tu ne sais pas, dis-le clairement.`
231
+ : `You are a billing assistant for the Xamlé platform. Help non-technical administrators understand their credit balance and usage. Reply simply and concretely in English. Avoid technical jargon. Use only the data below. If you don't know, say so clearly.`;
232
 
233
  try {
234
  const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });