import { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Brain, MessageCircle, TrendingUp, Send, Loader2, AlertCircle, Zap, X, Flame, HelpCircle, Download } from 'lucide-react'; import { api } from '@/lib/api'; import { useAuth } from '@/lib/auth'; import { useTenant } from '@/lib/tenant'; const SUPPORT_WA_NUMBER = import.meta.env.VITE_SUPPORT_WA_NUMBER as string | undefined; const SUPPORT_WA_URL = SUPPORT_WA_NUMBER ? `https://wa.me/${SUPPORT_WA_NUMBER}?text=Bonjour%2C%20je%20souhaite%20recharger%20mes%20cr%C3%A9dits%20Xaml%C3%A9.` : null; function RechargeModal({ onClose }: { onClose: () => void }) { const { t } = useTranslation(); const CREDIT_PACKS = [ { labelKey: 'billing.pack_500_label', priceKey: 'billing.pack_500_price', credits: 500, popular: false }, { labelKey: 'billing.pack_2000_label', priceKey: 'billing.pack_2000_price', credits: 2000, popular: true }, { labelKey: 'billing.pack_5000_label', priceKey: 'billing.pack_5000_price', credits: 5000, popular: false }, ]; useEffect(() => { document.body.style.overflow = 'hidden'; const onKey = (e: KeyboardEvent) => e.key === 'Escape' && onClose(); window.addEventListener('keydown', onKey); return () => { document.body.style.overflow = ''; window.removeEventListener('keydown', onKey); }; }, [onClose]); return (
e.stopPropagation()} >

{t('billing.modal_title')}

{t('billing.modal_rate')}

{CREDIT_PACKS.map(pack => ( {pack.popular && ( {t('billing.modal_popular')} )}

{t(pack.labelKey)}

{t('billing.modal_ai_messages', { count: pack.credits })}

{t(pack.priceKey)}
))}

{t('billing.modal_footer')}

); } interface WalletData { walletBalance: number; isHardStopped: boolean; transactions: Array<{ id: string; amount: number; balanceAfter: number; type: string; description: string | null; byok: boolean; createdAt: string; }>; } interface BillingSummary { plan: string; planLabel: string; period: { start: string; end: string }; ai: { creditsUsed: number; creditsLimit: number; percentUsed: number; totalCalls: number; costFcfa: number; }; whatsapp: { messagesSent: number }; } interface HistoryDay { date: string; aiCalls: number; whatsappMessages: number; costFcfa: number; } interface BreakdownItem { feature: string; calls: number; } interface ChatMessage { role: 'user' | 'assistant'; text: string; } function DaysRemainingBadge({ walletBalance, transactions }: { walletBalance: number; transactions: WalletData['transactions'] }) { const { t } = useTranslation(); const sevenDaysAgo = Date.now() - 7 * 86_400_000; const weeklyDebit = transactions .filter(tx => tx.amount < 0 && new Date(tx.createdAt).getTime() > sevenDaysAgo) .reduce((sum, tx) => sum + Math.abs(tx.amount), 0); if (weeklyDebit === 0) return null; const dailyBurn = weeklyDebit / 7; const days = Math.floor(walletBalance / dailyBurn); const color = days <= 3 ? 'text-red-600 bg-red-50' : days <= 10 ? 'text-amber-600 bg-amber-50' : 'text-emerald-600 bg-emerald-50'; const icon = days <= 3 ? '🔴' : days <= 10 ? '🟡' : '🟢'; const label = days > 1 ? t('billing.days_remaining_plural', { days }) : t('billing.days_remaining', { days }); return ( {icon} {label} ); } export default function BillingPage() { const { t } = useTranslation(); const { token, user } = useAuth(); const { selectedOrgId } = useTenant(); const orgId = selectedOrgId; const [summary, setSummary] = useState(null); const [wallet, setWallet] = useState(null); const [history, setHistory] = useState([]); const [breakdown, setBreakdown] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [showRecharge, setShowRecharge] = useState(false); const [chatMessages, setChatMessages] = useState([]); const [chatInput, setChatInput] = useState(''); const [chatLoading, setChatLoading] = useState(false); const chatEndRef = useRef(null); const QUICK_QUESTIONS = [ t('billing.quick_q1'), t('billing.quick_q2'), t('billing.quick_q3'), t('billing.quick_q4'), ]; const TX_LABELS: Record = { TOP_UP_MANUAL: t('billing.tx_TOP_UP_MANUAL'), TOP_UP_PAYMENT: t('billing.tx_TOP_UP_PAYMENT'), ADJUSTMENT: t('billing.tx_ADJUSTMENT'), DEBIT_AI: t('billing.tx_DEBIT_AI'), DEBIT_WHATSAPP: t('billing.tx_DEBIT_WHATSAPP'), DEBIT_BROADCAST: t('billing.tx_DEBIT_BROADCAST'), }; useEffect(() => { if (!orgId || !token) return; setLoading(true); Promise.all([ api.get('/v1/billing/summary', token, orgId), api.get('/v1/billing/history?days=30', token, orgId), api.get('/v1/billing/breakdown', token, orgId), api.get('/v1/billing/wallet', token, orgId), ]).then(([s, h, b, w]) => { setSummary(s); setHistory(h.history ?? []); setBreakdown(b.breakdown ?? []); setWallet(w); }).catch(err => setError(err.message)) .finally(() => setLoading(false)); }, [orgId, token]); useEffect(() => { chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [chatMessages]); const sendChat = async (question?: string) => { const q = (question ?? chatInput).trim(); if (!q || chatLoading || !orgId || !token) return; setChatInput(''); setChatMessages(prev => [...prev, { role: 'user', text: q }]); setChatLoading(true); try { const res = await api.post('/v1/billing/chat', { question: q, language: user?.language ?? 'FR' }, token, orgId); setChatMessages(prev => [...prev, { role: 'assistant', text: res.answer }]); } catch { setChatMessages(prev => [...prev, { role: 'assistant', text: t('billing.chat_error') }]); } finally { setChatLoading(false); } }; function downloadTransactionsCSV() { if (!wallet?.transactions.length) return; const rows = [ [ t('billing.csv_col_date'), t('billing.csv_col_type'), t('billing.csv_col_desc'), t('billing.csv_col_amount'), t('billing.csv_col_balance'), ], ...wallet.transactions.map(tx => [ new Date(tx.createdAt).toLocaleString(), TX_LABELS[tx.type] ?? tx.type, tx.description ?? '', tx.amount.toString(), tx.balanceAfter >= 0 ? tx.balanceAfter.toString() : '', ]), ]; const csv = rows.map(r => r.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')).join('\n'); const blob = new Blob(['' + csv], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `transactions-${new Date().toISOString().slice(0, 10)}.csv`; a.click(); URL.revokeObjectURL(url); } const maxCalls = Math.max(...history.map(d => d.aiCalls + d.whatsappMessages), 1); if (!orgId) { return (

{t('billing.select_org')}

); } if (loading) { return (
); } if (error) { return (
{t('billing.error_prefix')} {error}
); } if (!summary) return null; const periodLabel = new Date(summary.period.start).toLocaleDateString(undefined, { month: 'long', year: 'numeric' }); const walletExhausted = wallet ? (wallet.isHardStopped || wallet.walletBalance <= 0) : false; const walletLow = wallet ? (wallet.walletBalance > 0 && wallet.walletBalance <= 200) : false; return (
{showRecharge && setShowRecharge(false)} />} {/* Suspended service alert */} {walletExhausted && (

{t('billing.alert_exhausted_title')}

{t('billing.alert_exhausted_desc')}

)} {/* Low balance alert */} {!walletExhausted && walletLow && (

{t('billing.alert_low_title', { count: wallet!.walletBalance })}

{t('billing.alert_low_desc')}

)} {/* Header */}

{t('billing.page_title')}

{t('billing.period_label')} {periodLabel}

{/* Wallet card */} {wallet && (
💳

{t('billing.wallet_label')}

{wallet.walletBalance.toLocaleString()} {t('billing.wallet_unit')}

{t('billing.wallet_fcfa', { amount: (wallet.walletBalance * 10).toLocaleString() })}

{t('billing.wallet_rate')}

)} {/* Metric cards */}
{/* AI messages this month */}

{t('billing.ai_messages_title')}

{summary.ai.totalCalls.toLocaleString()}

{t('billing.ai_cost_label', { cost: summary.ai.costFcfa.toLocaleString() })}

{summary.ai.percentUsed > 85 && (
)}
{/* WhatsApp messages this month */}

{t('billing.wa_messages_title')}

{summary.whatsapp.messagesSent.toLocaleString()}

{t('billing.wa_free_note')}

{/* 30-day activity chart */}

{t('billing.activity_title')}

{t('billing.activity_subtitle')}

{history.length === 0 ? (

{t('billing.activity_empty')}

) : (
{history.map(day => { const total = day.aiCalls + day.whatsappMessages; const heightPct = Math.round((total / maxCalls) * 100); const dateLabel = new Date(day.date).toLocaleDateString(undefined, { day: '2-digit', month: '2-digit' }); return (
); })}
)}
{/* Feature breakdown */} {breakdown.length > 0 && (

{t('billing.breakdown_title')}

{t('billing.breakdown_subtitle')}

{breakdown.slice(0, 5).map(item => { const totalCalls = breakdown.reduce((s, i) => s + i.calls, 0); const pct = totalCalls > 0 ? Math.round((item.calls / totalCalls) * 100) : 0; const featureKey = `billing.feature_${item.feature}` as const; return (
{t(featureKey, { defaultValue: item.feature })}
{pct}%
); })}
)} {/* Transaction history */} {wallet && wallet.transactions.length > 0 && (

{t('billing.transactions_title')}

{t('billing.transactions_subtitle')}

{wallet.transactions.filter(tx => !tx.byok).map(tx => { const isCredit = tx.amount > 0; return (
{isCredit ? '➕' : '▪️'}

{TX_LABELS[tx.type] ?? tx.type}

{new Date(tx.createdAt).toLocaleDateString(undefined, { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' })}

{isCredit ? '+' : ''}{tx.amount.toLocaleString()} {t('billing.wallet_unit')}

{t('billing.balance_label')} {tx.balanceAfter >= 0 ? tx.balanceAfter.toLocaleString() : '—'}

); })}
)} {/* Billing AI assistant */}

{t('billing.chat_title')}

{t('billing.chat_subtitle')}

{/* Suggested questions */} {chatMessages.length === 0 && (
{QUICK_QUESTIONS.map(q => ( ))}
)} {/* Messages */}
{chatMessages.length === 0 && (

{t('billing.chat_empty')}

)} {chatMessages.map((msg, i) => (
{msg.text}
))} {chatLoading && (
)}
{/* Input */}
setChatInput(e.target.value)} onKeyDown={e => e.key === 'Enter' && sendChat()} placeholder={t('billing.chat_placeholder')} 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" disabled={chatLoading} />
); }