// Copyright (c) 2026 PowerOn AG // All rights reserved. /** * Billing Mandate View Page * * Shows mandate-level balances and transactions for SysAdmins. * Includes filtering by mandate. */ import React, { useEffect, useState, useMemo } from 'react'; import { useApiRequest } from '../../hooks/useApi'; import { fetchMandateViewBalances, fetchMandateViewTransactions, type MandateBalance, type BillingTransaction } from '../../api/billingApi'; import { BillingNav } from './BillingNav'; import { StackLayout } from '../../components/Layout/StackLayout'; import { Panel } from '../../components/Layout/Panel'; import styles from './Billing.module.css'; import { useLanguage } from '../../providers/language/LanguageContext'; // ============================================================================ // MANDATE BALANCE TABLE // ============================================================================ interface MandateBalanceTableProps { balances: MandateBalance[]; selectedMandateId: string | null; onSelectMandate: (mandateId: string | null) => void; } const MandateBalanceTable: React.FC = ({ balances, selectedMandateId, onSelectMandate }) => { const { t } = useLanguage(); const formatCurrency = (amount: number) => { return new Intl.NumberFormat('de-CH', { style: 'currency', currency: 'CHF' }).format(amount); }; return (
{balances.map((balance) => ( ))}
{t('Mandant')} {t('Anzahl Benutzer')} {t('Warnschwelle')} {t('Gesamtguthaben')} {t('Aktion')}
{balance.mandateName || balance.mandateId} {balance.userCount} {balance.warningThresholdPercent}% {formatCurrency(balance.totalBalance)}
); }; // ============================================================================ // TRANSACTION TABLE // ============================================================================ interface TransactionTableProps { transactions: BillingTransaction[]; } const TransactionTable: React.FC = ({ transactions }) => { const { t } = useLanguage(); 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 t('Gutschrift'); case 'DEBIT': return t('Belastung'); case 'ADJUSTMENT': return t('Korrektur'); default: return type; } }; return (
{transactions.map((txn) => ( ))}
{t('Datum')} {t('Mandant')} {t('Typ')} {t('Beschreibung')} {t('Anbieter')} {t('Modell')} {t('Feature')} {t('Betrag')}
{formatDate(txn.sysCreatedAt)} {txn.mandateName || '-'} {getTypeLabel(txn.transactionType)} {txn.description} {txn.aicoreProvider || '-'} {txn.aicoreModel || '-'} {txn.featureCode || '-'} {txn.transactionType === 'DEBIT' ? '-' : '+'}{formatCurrency(txn.amount)}
); }; // ============================================================================ // MAIN COMPONENT // ============================================================================ interface BillingMandateViewProps { embedded?: boolean; } export const BillingMandateView: React.FC = ({ embedded = false }) => { const { t } = useLanguage(); const { request, isLoading: loading } = useApiRequest(); const [balances, setBalances] = useState([]); const [transactions, setTransactions] = useState([]); const [selectedMandateId, setSelectedMandateId] = useState(null); const [limit, setLimit] = useState(100); // Load data useEffect(() => { const loadData = async () => { try { const [balanceData, transactionData] = await Promise.all([ fetchMandateViewBalances(request), fetchMandateViewTransactions(request, limit) ]); setBalances(Array.isArray(balanceData) ? balanceData : []); setTransactions(Array.isArray(transactionData) ? transactionData : []); } catch (err) { console.error('Error loading mandate view data:', err); setBalances([]); setTransactions([]); } }; loadData(); }, [request, limit]); // Filter transactions by selected mandate const filteredTransactions = useMemo(() => { if (!selectedMandateId) return transactions; return transactions.filter(tx => tx.mandateId === selectedMandateId); }, [transactions, selectedMandateId]); const handleLoadMore = () => { setLimit(prev => prev + 100); }; const _filteredMandateName = selectedMandateId ? balances.find(b => b.mandateId === selectedMandateId)?.mandateName || selectedMandateId : null; const _transactionsSubtitle = selectedMandateId ? t('(gefiltert nach {name})', { name: _filteredMandateName ?? selectedMandateId }) : undefined; const _body = ( <> {loading && balances.length === 0 ? (
{t('Daten laden')}
) : balances.length === 0 ? (
{t('Keine Mandanten mit Billing-Einstellungen vorhanden')}
) : ( )}
{loading && transactions.length === 0 ? (
{t('Transaktionen laden')}
) : filteredTransactions.length === 0 ? (
{t('Keine Transaktionen vorhanden')}
) : ( <> {transactions.length >= limit && (
)} )}
); if (embedded) { return
{_body}
; } return (

{t('Mandanten-Billing')}

{t('Guthaben und Transaktionen pro Mandant')}

{_body}
); }; export default BillingMandateView;