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')}
{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) => (
))}
{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}
/>
);
}