/** * BillingDataView * * Unified billing page with internal tabs: * - Tab "Übersicht": Balance cards + Statistics (from BillingDashboard) * - Tab "Transaktionen": Transaction table with FormGeneratorTable */ import React, { useState, useEffect, useMemo, useCallback } from 'react'; import { FormGeneratorTable, ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable'; import api from '../../api'; import { useBilling, type BillingBalance, type UsageReport } from '../../hooks/useBilling'; import { UserTransaction } from '../../api/billingApi'; import styles from './Billing.module.css'; // ============================================================================ // BALANCE CARD COMPONENT // ============================================================================ interface BalanceCardProps { balance: BillingBalance; } const BalanceCard: React.FC = ({ balance }) => { const formatCurrency = (amount: number) => { return new Intl.NumberFormat('de-CH', { style: 'currency', currency: 'CHF' }).format(amount); }; const getBillingModelLabel = (model: string) => { switch (model) { case 'PREPAY_MANDATE': return 'Prepaid (Mandant)'; case 'PREPAY_USER': return 'Prepaid (Benutzer)'; case 'CREDIT_POSTPAY': return 'Kredit'; case 'UNLIMITED': return 'Unlimited'; default: return model; } }; return (

{balance.mandateName}

{getBillingModelLabel(balance.billingModel)}
{formatCurrency(balance.balance)}
{balance.isWarning && (
Niedriges Guthaben
)}
); }; // ============================================================================ // STATISTICS CHART COMPONENT // ============================================================================ interface StatisticsChartProps { statistics: UsageReport | null; loading?: boolean; } const StatisticsChart: React.FC = ({ statistics, loading }) => { const formatCurrency = (amount: number) => { return new Intl.NumberFormat('de-CH', { style: 'currency', currency: 'CHF' }).format(amount); }; if (loading) { return
Lade Statistiken...
; } if (!statistics) { return
Keine Statistiken verfügbar
; } const maxProviderCost = Math.max(...Object.values(statistics.costByProvider), 1); return (
Gesamtkosten {formatCurrency(statistics.totalCost)}

Kosten nach Anbieter

{Object.entries(statistics.costByProvider).length === 0 ? (
Keine Daten
) : (
{Object.entries(statistics.costByProvider).map(([provider, cost]) => (
{provider}
{formatCurrency(cost)}
))}
)}

Kosten nach Feature

{Object.entries(statistics.costByFeature).length === 0 ? (
Keine Daten
) : (
{Object.entries(statistics.costByFeature).map(([feature, cost]) => (
{feature} {formatCurrency(cost)}
))}
)}
); }; // ============================================================================ // TAB NAVIGATION COMPONENT // ============================================================================ type TabType = 'overview' | 'transactions'; interface TabNavProps { activeTab: TabType; onTabChange: (tab: TabType) => void; } const TabNav: React.FC = ({ activeTab, onTabChange }) => { const navLinkStyle = (isActive: boolean) => ({ padding: '8px 16px', textDecoration: 'none', borderRadius: '4px', backgroundColor: isActive ? 'var(--color-primary, #3b82f6)' : 'transparent', color: isActive ? 'white' : 'var(--color-text, #e0e0e0)', fontWeight: isActive ? 600 : 400, cursor: 'pointer', border: 'none', fontSize: '14px', }); return ( ); }; // ============================================================================ // MAIN COMPONENT // ============================================================================ export const BillingDataView: React.FC = () => { const [activeTab, setActiveTab] = useState('overview'); // Dashboard state (for Overview tab) const { balances, statistics, loading: dashboardLoading, loadStatistics } = useBilling(); const [selectedPeriod, setSelectedPeriod] = useState<'month' | 'year'>('month'); const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()); const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth() + 1); // Transactions state (for Transactions tab) const [transactions, setTransactions] = useState([]); const [transactionsLoading, setTransactionsLoading] = useState(false); const [transactionsError, setTransactionsError] = useState(null); // Load statistics when period changes useEffect(() => { if (selectedPeriod === 'month') { loadStatistics('month', selectedYear); } else { loadStatistics('year', selectedYear); } }, [selectedPeriod, selectedYear, loadStatistics]); // Load transactions const loadTransactions = useCallback(async () => { try { setTransactionsLoading(true); setTransactionsError(null); const response = await api.get('/api/billing/view/users/transactions', { params: { limit: 500 } }); setTransactions(response.data || []); } catch (err: any) { console.error('Failed to load transactions:', err); setTransactionsError(err.response?.data?.detail || err.message || 'Fehler beim Laden der Transaktionen'); } finally { setTransactionsLoading(false); } }, []); // Load transactions when switching to transactions tab useEffect(() => { if (activeTab === 'transactions' && transactions.length === 0) { loadTransactions(); } }, [activeTab, transactions.length, loadTransactions]); // Available years const availableYears = useMemo(() => { const current = new Date().getFullYear(); return [current, current - 1, current - 2]; }, []); // Available months const availableMonths = [ { value: 1, label: 'Januar' }, { value: 2, label: 'Februar' }, { value: 3, label: 'März' }, { value: 4, label: 'April' }, { value: 5, label: 'Mai' }, { value: 6, label: 'Juni' }, { value: 7, label: 'Juli' }, { value: 8, label: 'August' }, { value: 9, label: 'September' }, { value: 10, label: 'Oktober' }, { value: 11, label: 'November' }, { value: 12, label: 'Dezember' }, ]; // Transform transactions for table display const tableData = useMemo(() => { return transactions.map((t, index) => ({ _uniqueId: `${t.id}-${t.mandateId}-${index}`, id: t.id, createdAt: t.createdAt, mandateId: t.mandateId, mandateName: t.mandateName || '-', userId: t.userId, userName: t.userName || '-', transactionType: t.transactionType, description: t.description || '-', aicoreProvider: t.aicoreProvider || '-', featureCode: t.featureCode || '-', amount: t.transactionType === 'DEBIT' ? -t.amount : t.amount, })); }, [transactions]); // Table column definitions const columns: ColumnConfig[] = useMemo(() => [ { key: 'createdAt', label: 'Datum', type: 'datetime', sortable: true, width: 160, }, { key: 'mandateName', label: 'Mandant', type: 'text', sortable: true, filterable: true, searchable: true, width: 150, }, { key: 'userName', label: 'Benutzer', type: 'text', sortable: true, filterable: true, searchable: true, width: 150, }, { key: 'transactionType', label: 'Typ', type: 'text', sortable: true, filterable: true, filterOptions: ['CREDIT', 'DEBIT', 'ADJUSTMENT'], width: 100, }, { key: 'description', label: 'Beschreibung', type: 'text', searchable: true, width: 250, }, { key: 'aicoreProvider', label: 'Anbieter', type: 'text', sortable: true, filterable: true, width: 120, }, { key: 'featureCode', label: 'Feature', type: 'text', sortable: true, filterable: true, width: 120, }, { key: 'amount', label: 'Betrag (CHF)', type: 'number', sortable: true, width: 120, }, ], []); return (

Billing

Guthaben, Statistiken und Transaktionen

{/* Overview Tab */} {activeTab === 'overview' && ( <> {/* Balance Cards */}

Guthaben

{dashboardLoading ? (
Lade Guthaben...
) : balances.length === 0 ? (
Keine Abrechnungskonten vorhanden
) : (
{balances.map((balance) => ( ))}
)}
{/* Statistics */}

Nutzungsstatistik

{selectedPeriod === 'month' && ( )}
)} {/* Transactions Tab */} {activeTab === 'transactions' && ( <> {transactionsError && (
{transactionsError}
)} )}
); }; export default BillingDataView;