/** * Billing User View Page * * Shows user-level balances and transactions. * RBAC-based: Users see only their own data, Admins see all. * Includes filtering by mandate and user. */ import React, { useEffect, useState, useMemo } from 'react'; import { useApiRequest } from '../../hooks/useApi'; import { fetchUserViewBalances, fetchUserViewTransactions, type UserBalance, type UserTransaction } from '../../api/billingApi'; import { BillingNav } from './BillingNav'; import styles from './Billing.module.css'; // ============================================================================ // USER BALANCE TABLE // ============================================================================ interface UserBalanceTableProps { balances: UserBalance[]; selectedMandateId: string | null; selectedUserId: string | null; onSelectMandate: (mandateId: string | null) => void; onSelectUser: (userId: string | null) => void; } const UserBalanceTable: React.FC = ({ balances, selectedMandateId, selectedUserId, onSelectMandate, onSelectUser }) => { const formatCurrency = (amount: number) => { return new Intl.NumberFormat('de-CH', { style: 'currency', currency: 'CHF' }).format(amount); }; // Get unique mandates and users for filter dropdowns const uniqueMandates = useMemo(() => { const mandates = new Map(); balances.forEach(b => { if (b.mandateId && !mandates.has(b.mandateId)) { mandates.set(b.mandateId, b.mandateName || b.mandateId); } }); return Array.from(mandates.entries()); }, [balances]); const uniqueUsers = useMemo(() => { const users = new Map(); balances.forEach(b => { if (b.userId && !users.has(b.userId)) { users.set(b.userId, b.userName || b.userId); } }); return Array.from(users.entries()); }, [balances]); // Apply filters const filteredBalances = useMemo(() => { let result = balances; if (selectedMandateId) { result = result.filter(b => b.mandateId === selectedMandateId); } if (selectedUserId) { result = result.filter(b => b.userId === selectedUserId); } return result; }, [balances, selectedMandateId, selectedUserId]); return ( <> {/* Filter Controls */}
{/* Table */}
{filteredBalances.map((balance) => ( ))}
Mandant Benutzer Guthaben Warnschwelle Status
{balance.mandateName || balance.mandateId} {balance.userName || balance.userId} {formatCurrency(balance.balance)} {formatCurrency(balance.warningThreshold)} {balance.isWarning ? ( Niedrig ) : balance.enabled ? ( Aktiv ) : ( Deaktiviert )}
); }; // ============================================================================ // USER TRANSACTION TABLE // ============================================================================ interface UserTransactionTableProps { transactions: UserTransaction[]; selectedMandateId: string | null; selectedUserId: string | null; } const UserTransactionTable: React.FC = ({ transactions, selectedMandateId, selectedUserId }) => { const formatCurrency = (amount: number) => { return new Intl.NumberFormat('de-CH', { style: 'currency', currency: 'CHF' }).format(amount); }; const formatDate = (dateString?: string) => { if (!dateString) return '-'; return new Date(dateString).toLocaleString('de-CH', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }); }; const getTypeClass = (type: string) => { switch (type) { case 'CREDIT': return styles.credit; case 'DEBIT': return styles.debit; case 'ADJUSTMENT': return styles.adjustment; default: return ''; } }; const getTypeLabel = (type: string) => { switch (type) { case 'CREDIT': return 'Gutschrift'; case 'DEBIT': return 'Belastung'; case 'ADJUSTMENT': return 'Korrektur'; default: return type; } }; // Apply filters const filteredTransactions = useMemo(() => { let result = transactions; if (selectedMandateId) { result = result.filter(t => t.mandateId === selectedMandateId); } if (selectedUserId) { result = result.filter(t => t.userId === selectedUserId); } return result; }, [transactions, selectedMandateId, selectedUserId]); return (
{filteredTransactions.map((t) => ( ))}
Datum Mandant Benutzer Typ Beschreibung Anbieter Modell Feature Betrag
{formatDate(t.createdAt)} {t.mandateName || '-'} {t.userName || '-'} {getTypeLabel(t.transactionType)} {t.description} {t.aicoreProvider || '-'} {t.aicoreModel || '-'} {t.featureCode || '-'} {t.transactionType === 'DEBIT' ? '-' : '+'}{formatCurrency(t.amount)}
); }; // ============================================================================ // MAIN COMPONENT // ============================================================================ export const BillingUserView: React.FC = () => { const { request, isLoading: loading } = useApiRequest(); const [balances, setBalances] = useState([]); const [transactions, setTransactions] = useState([]); const [selectedMandateId, setSelectedMandateId] = useState(null); const [selectedUserId, setSelectedUserId] = useState(null); const [limit, setLimit] = useState(100); // Load data useEffect(() => { const loadData = async () => { try { const [balanceData, transactionData] = await Promise.all([ fetchUserViewBalances(request), fetchUserViewTransactions(request, limit) ]); setBalances(Array.isArray(balanceData) ? balanceData : []); setTransactions(Array.isArray(transactionData) ? transactionData : []); } catch (err) { console.error('Error loading user view data:', err); setBalances([]); setTransactions([]); } }; loadData(); }, [request, limit]); const handleLoadMore = () => { setLimit(prev => prev + 100); }; // Count filtered transactions for display const filteredTransactionCount = useMemo(() => { let result = transactions; if (selectedMandateId) { result = result.filter(t => t.mandateId === selectedMandateId); } if (selectedUserId) { result = result.filter(t => t.userId === selectedUserId); } return result.length; }, [transactions, selectedMandateId, selectedUserId]); return (

Benutzer-Billing

Guthaben und Transaktionen pro Benutzer

{/* User Balances */}

Benutzer-Guthaben

{loading && balances.length === 0 ? (
Lade Daten...
) : balances.length === 0 ? (
Keine Benutzer-Konten vorhanden
) : ( )}
{/* Transactions */}

Transaktionen {(selectedMandateId || selectedUserId) && ( ({filteredTransactionCount} gefiltert) )}

{loading && transactions.length === 0 ? (
Lade Transaktionen...
) : transactions.length === 0 ? (
Keine Transaktionen vorhanden
) : ( <> {transactions.length >= limit && (
)} )}
); }; export default BillingUserView;