| import { useState, useEffect } from 'react'; |
| import { useTranslation } from 'react-i18next'; |
| import { useAuth } from '@/lib/auth'; |
| import { api } from '@/lib/api'; |
| import { useToast } from '@/hooks/useToast'; |
| import { CreditCard, Plus, X } from 'lucide-react'; |
|
|
| export default function BillingManager() { |
| const { t } = useTranslation(); |
| const { token } = useAuth(); |
| const toast = useToast(); |
| const [transactions, setTransactions] = useState<any[]>([]); |
| const [total, setTotal] = useState(0); |
| const [page, setPage] = useState(1); |
| const [loading, setLoading] = useState(true); |
| const [showAddCredits, setShowAddCredits] = useState(false); |
| const [creditOrgId, setCreditOrgId] = useState(''); |
| const [creditAmount, setCreditAmount] = useState(''); |
| const [creditDesc, setCreditDesc] = useState(''); |
| const [saving, setSaving] = useState(false); |
|
|
| const LIMIT = 20; |
|
|
| async function load() { |
| if (!token) return; |
| setLoading(true); |
| try { |
| const data = await api.get(`/v1/super-admin/billing/transactions?page=${page}&limit=${LIMIT}`, token); |
| setTransactions(data.data ?? []); |
| setTotal(data.total ?? 0); |
| } catch { toast.error(t('super_admin.err_load_transactions')); } |
| finally { setLoading(false); } |
| } |
|
|
| useEffect(() => { load(); }, [page, token]); |
|
|
| async function handleAddCredits() { |
| if (!creditOrgId || !creditAmount) return; |
| setSaving(true); |
| try { |
| const result = await api.post('/v1/super-admin/billing/credits', { |
| orgId: creditOrgId, |
| amount: parseInt(creditAmount), |
| description: creditDesc || undefined, |
| }, token); |
| toast.success(t('super_admin.credits_added', { amount: creditAmount, balance: result.newBalance })); |
| setShowAddCredits(false); |
| setCreditOrgId(''); setCreditAmount(''); setCreditDesc(''); |
| load(); |
| } catch { toast.error(t('super_admin.err_add_credits')); } |
| finally { setSaving(false); } |
| } |
|
|
| const TYPE_COLORS: Record<string, string> = { |
| TOP_UP_MANUAL: 'text-emerald-400', |
| TOP_UP_PAYMENT: 'text-emerald-400', |
| DEBIT_AI: 'text-red-400', |
| }; |
|
|
| return ( |
| <div className="space-y-4"> |
| <div className="flex items-center justify-between"> |
| <div> |
| <h1 className="text-xl font-bold text-white">{t('super_admin.billing_title')}</h1> |
| <p className="text-sm text-slate-400 mt-0.5">{t('super_admin.billing_transactions', { count: total })}</p> |
| </div> |
| <button |
| onClick={() => setShowAddCredits(true)} |
| className="flex items-center gap-2 bg-violet-600 hover:bg-violet-500 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors" |
| > |
| <Plus className="w-4 h-4" /> |
| {t('super_admin.billing_add_credits')} |
| </button> |
| </div> |
| |
| <div className="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden"> |
| {loading ? ( |
| <div className="p-8 text-center text-slate-500 animate-pulse">{t('super_admin.org_loading')}</div> |
| ) : transactions.length === 0 ? ( |
| <div className="p-12 flex flex-col items-center gap-3 text-slate-500"> |
| <CreditCard className="w-8 h-8" /> |
| <p>{t('super_admin.billing_no_transactions')}</p> |
| </div> |
| ) : ( |
| <div className="overflow-x-auto"> |
| <table className="w-full text-sm"> |
| <thead> |
| <tr className="border-b border-slate-800"> |
| <th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_date')}</th> |
| <th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_org')}</th> |
| <th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_type')}</th> |
| <th className="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_description')}</th> |
| <th className="text-right px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_amount')}</th> |
| <th className="text-right px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.col_balance_after')}</th> |
| </tr> |
| </thead> |
| <tbody className="divide-y divide-slate-800"> |
| {transactions.map(tx => ( |
| <tr key={tx.id} className="hover:bg-slate-800/50 transition-colors"> |
| <td className="px-4 py-3 text-xs text-slate-400">{new Date(tx.createdAt).toLocaleDateString()}</td> |
| <td className="px-4 py-3 text-slate-300">{tx.organization?.name || '—'}</td> |
| <td className="px-4 py-3"> |
| <span className={`text-xs font-medium ${TYPE_COLORS[tx.type] ?? 'text-slate-300'}`}>{tx.type}</span> |
| </td> |
| <td className="px-4 py-3 text-xs text-slate-400 max-w-xs truncate">{tx.description || '—'}</td> |
| <td className={`px-4 py-3 text-right font-medium text-sm ${tx.amount > 0 ? 'text-emerald-400' : 'text-red-400'}`}> |
| {(tx.amount ?? 0) > 0 ? '+' : ''}{(tx.amount ?? 0).toLocaleString()} |
| </td> |
| <td className="px-4 py-3 text-right text-slate-300">{tx.balanceAfter?.toLocaleString()}</td> |
| </tr> |
| ))} |
| </tbody> |
| </table> |
| </div> |
| )} |
| </div> |
| |
| {total > LIMIT && ( |
| <div className="flex items-center justify-between text-sm text-slate-400"> |
| <span>{t('super_admin.pagination_info', { from: ((page - 1) * LIMIT) + 1, to: Math.min(page * LIMIT, total), total })}</span> |
| <div className="flex gap-2"> |
| <button onClick={() => setPage(p => Math.max(1, p - 1))} disabled={page === 1} className="px-3 py-1.5 bg-slate-900 border border-slate-700 rounded-lg disabled:opacity-40 hover:bg-slate-800 transition-colors">{t('super_admin.prev')}</button> |
| <button onClick={() => setPage(p => p + 1)} disabled={page * LIMIT >= total} className="px-3 py-1.5 bg-slate-900 border border-slate-700 rounded-lg disabled:opacity-40 hover:bg-slate-800 transition-colors">{t('super_admin.next')}</button> |
| </div> |
| </div> |
| )} |
| |
| {showAddCredits && ( |
| <div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center"> |
| <div className="bg-slate-900 border border-slate-800 rounded-xl p-6 w-96 space-y-4"> |
| <div className="flex items-center justify-between"> |
| <h2 className="text-base font-bold text-white">{t('super_admin.modal_add_credits')}</h2> |
| <button onClick={() => setShowAddCredits(false)}><X className="w-4 h-4 text-slate-400" /></button> |
| </div> |
| <div className="space-y-3"> |
| <div> |
| <label className="text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.label_org_id')}</label> |
| <input value={creditOrgId} onChange={e => setCreditOrgId(e.target.value)} placeholder="org-uuid..." className="mt-1.5 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-violet-500" /> |
| </div> |
| <div> |
| <label className="text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.label_credits_amount')}</label> |
| <input type="number" value={creditAmount} onChange={e => setCreditAmount(e.target.value)} placeholder="100" className="mt-1.5 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-violet-500" /> |
| </div> |
| <div> |
| <label className="text-xs font-medium text-slate-400 uppercase tracking-wider">{t('super_admin.label_description_optional')}</label> |
| <input value={creditDesc} onChange={e => setCreditDesc(e.target.value)} placeholder={t('super_admin.credits_desc_placeholder')} className="mt-1.5 w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-sm text-white placeholder-slate-500 focus:outline-none focus:border-violet-500" /> |
| </div> |
| </div> |
| <div className="flex gap-2"> |
| <button onClick={() => setShowAddCredits(false)} className="flex-1 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 text-sm rounded-lg transition-colors">{t('super_admin.cancel')}</button> |
| <button onClick={handleAddCredits} disabled={saving || !creditOrgId || !creditAmount} className="flex-1 py-2 bg-violet-600 hover:bg-violet-500 disabled:opacity-50 text-white text-sm font-medium rounded-lg transition-colors"> |
| {saving ? t('super_admin.adding') : t('super_admin.add')} |
| </button> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| ); |
| } |
|
|