import React, { useEffect, useState } from 'react'; import type { IconType } from 'react-icons'; import { FaBarcode, FaBookOpen, FaChevronLeft, FaChevronRight, FaCoins, FaDownload, FaMagic, FaRegFileAlt, FaRegImage, FaRegNewspaper, FaSatelliteDish, FaShieldAlt, FaSpinner, FaTimes, } from 'react-icons/fa'; import { CREDIT_HISTORY_PAGE_SIZE, EMPTY_CREDIT_HISTORY, type ICreditTransaction, type ICreditTransactionsResponse, } from '../../service/credits/credits.interface'; import { fetchCreditTransactionsExport, getCreditTransactions, } from '../../service/credits/credits.service'; /** Joins truthy class fragments (this app has no `cn` helper). */ function cx(...parts: Array): string { return parts.filter(Boolean).join(' '); } interface ReasonMeta { label: string; icon: IconType; } const REASON_META: Record = { CONTENT_GEN: { label: 'Content generation', icon: FaRegFileAlt }, PRODUCT_SCAN: { label: 'Product scan', icon: FaBarcode }, INLINE_FIX: { label: 'Inline fix', icon: FaMagic }, GAP_VERIFICATION: { label: 'Gap verification', icon: FaShieldAlt }, KB_DOC: { label: 'Knowledge base', icon: FaBookOpen }, ARTICLE_GEN: { label: 'Content generation', icon: FaRegNewspaper }, ARTICLE_IMAGE: { label: 'Content image', icon: FaRegImage }, VISIBILITY_SCAN: { label: 'Visibility scan', icon: FaSatelliteDish }, }; function reasonMeta(reason: string): ReasonMeta { return ( REASON_META[reason] ?? { label: reason .toLowerCase() .split('_') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '), icon: FaCoins, } ); } function formatTimestamp(iso: string): string { const date = new Date(iso); if (Number.isNaN(date.getTime())) return '—'; return date.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } interface CreditHistoryTableProps { token: string; clientId?: string | null; } /** * Self-contained, paginated table of credit consumption/grants (newest first) * with a date-range filter and an xlsx export. Fetches client-side from the * credits service and manages its own pagination/filter state. * * @param {CreditHistoryTableProps} props - Recomaze auth. * @return {React.ReactElement} The credit-history table. */ export function CreditHistoryTable({ token, clientId, }: CreditHistoryTableProps) { const [data, setData] = useState(EMPTY_CREDIT_HISTORY); const [page, setPage] = useState(1); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [loading, setLoading] = useState(true); const [exporting, setExporting] = useState(false); const [exportError, setExportError] = useState(null); useEffect(() => { if (!token) return; let cancelled = false; setLoading(true); getCreditTransactions( token, clientId, page, CREDIT_HISTORY_PAGE_SIZE, startDate || undefined, endDate || undefined ) .then(response => { if (!cancelled) setData(response); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [token, clientId, page, startDate, endDate]); const transactions = data.transactions; const totalPages = data.total_pages; const total = data.total; const hasPrev = page > 1; const hasNext = page < totalPages; const hasFilter = Boolean(startDate || endDate); const handleDateChange = (next: { start?: string; end?: string }) => { if (next.start !== undefined) setStartDate(next.start); if (next.end !== undefined) setEndDate(next.end); setPage(1); }; const handleExport = async () => { if (!token) return; setExporting(true); setExportError(null); try { const { blob, filename } = await fetchCreditTransactionsExport( token, clientId, startDate || undefined, endDate || undefined ); const objectUrl = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = objectUrl; anchor.download = filename; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(objectUrl); } catch { console.warn('[Credits] export failed'); setExportError('Could not export credit history. Please try again.'); } finally { setExporting(false); } }; return (

Credit history

{total.toLocaleString()} transaction{total === 1 ? '' : 's'}
{hasFilter && ( )}
{exportError && (
{exportError}
)} {loading ? (
{Array.from({ length: 5 }).map((_, index) => (
))}
) : transactions.length === 0 ? (

{hasFilter ? 'No transactions in this range' : 'No credit usage yet'}

{hasFilter ? 'Try widening the date range or clearing the filter.' : 'Credits you spend on content and scans will show up here.'}

) : (
{transactions.map((tx: ICreditTransaction) => { const meta = reasonMeta(tx.reason); const Icon = meta.icon; const isSpend = tx.amount < 0; const magnitude = Math.abs(tx.amount).toLocaleString(); return ( ); })}
When Description Type Credits Balance
{formatTimestamp(tx.created_at)} {tx.note ?? meta.label} {meta.label} {isSpend ? `-${magnitude}` : `+${magnitude}`} {tx.balance_after.toLocaleString()}
)} {totalPages > 1 && (
Page {page} of {totalPages}
setPage(p => Math.max(1, p - 1))} > Previous setPage(p => p + 1)} > Next
)}
); } function PagerButton({ disabled, onClick, children, }: { disabled: boolean; onClick: () => void; children: React.ReactNode; }) { return ( ); }