+
+
+ >
+ );
+
+ if (embedded) {
+ return
{_body}
;
+ }
+
+ return (
+
+
+
+
{t('Mandanten-Billing')}
+
{t('Guthaben und Transaktionen pro Mandant')}
+
+
+
+
+ {_body}
+
+
);
};
diff --git a/src/pages/billing/BillingNav.tsx b/src/pages/billing/BillingNav.tsx
index c8c3f82..a2cfe27 100644
--- a/src/pages/billing/BillingNav.tsx
+++ b/src/pages/billing/BillingNav.tsx
@@ -2,58 +2,42 @@
// All rights reserved.
/**
* Billing Navigation Component
- *
- * Provides navigation between billing views.
- * Simplified: Übersicht (Dashboard) + Daten (FormGeneratorTable view)
+ *
+ * Subcomponent: Panel toolbar only (no StackLayout).
*/
import React from 'react';
import { NavLink } from 'react-router-dom';
+import { Panel } from '../../components/Layout/Panel';
import styles from './Billing.module.css';
-
import { useLanguage } from '../../providers/language/LanguageContext';
export const BillingNav: React.FC = () => {
const { t } = useLanguage();
- const navLinkStyle = (isActive: boolean) => ({
- padding: '8px 16px',
- textDecoration: 'none',
- borderRadius: '4px',
- backgroundColor: isActive ? 'var(--color-primary)' : 'transparent',
- color: isActive ? 'white' : 'var(--color-text)',
- fontWeight: isActive ? 600 : 400,
- });
-
return (
-
+
+
+
);
};
diff --git a/src/pages/billing/BillingTransactions.tsx b/src/pages/billing/BillingTransactions.tsx
deleted file mode 100644
index 3c73660..0000000
--- a/src/pages/billing/BillingTransactions.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-// Copyright (c) 2026 PowerOn AG
-// All rights reserved.
-/**
- * Billing Transactions Page
- *
- * Transaktionshistorie mit FormGeneratorTable (Suche, Filter, Sortierung, Ansichten, Gruppierung).
- */
-
-import React, { useMemo } from 'react';
-import { useBilling, type BillingTransaction } from '../../hooks/useBilling';
-import { BillingNav } from './BillingNav';
-import styles from './Billing.module.css';
-import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
-import { useLanguage } from '../../providers/language/LanguageContext';
-
-function typePillClass(type: string): string {
- switch (type) {
- case 'CREDIT':
- return styles.credit;
- case 'DEBIT':
- return styles.debit;
- case 'ADJUSTMENT':
- return styles.adjustment;
- default:
- return '';
- }
-}
-
-function typeLabel(type: string, t: (k: string) => string): string {
- switch (type) {
- case 'CREDIT':
- return t('Gutschrift');
- case 'DEBIT':
- return t('Belastung');
- case 'ADJUSTMENT':
- return t('Korrektur');
- default:
- return type;
- }
-}
-
-export const BillingTransactions: React.FC = () => {
- const { t } = useLanguage();
- const {
- transactions,
- loading,
- refetchTransactions,
- transactionsPagination,
- transactionsGroupLayout,
- transactionsAppliedView,
- } = useBilling();
-
- const columns = useMemo((): ColumnConfig[] => {
- const fmtChf = (amount: number) =>
- new Intl.NumberFormat('de-CH', { style: 'currency', currency: 'CHF' }).format(amount);
-
- return [
- {
- key: 'sysCreatedAt',
- label: t('Datum'),
- type: 'date',
- sortable: true,
- filterable: false,
- searchable: true,
- width: 170,
- },
- {
- key: 'mandateName',
- label: t('Mandant'),
- type: 'string',
- sortable: true,
- filterable: true,
- searchable: true,
- width: 160,
- },
- {
- key: 'transactionType',
- label: t('Typ'),
- type: 'string',
- sortable: true,
- filterable: true,
- searchable: true,
- width: 130,
- formatter: (_v, row: BillingTransaction) => (
-
- {typeLabel(row.transactionType, t)}
-
- ),
- },
- {
- key: 'description',
- label: t('Beschreibung'),
- type: 'string',
- sortable: true,
- filterable: true,
- searchable: true,
- minWidth: 180,
- },
- {
- key: 'aicoreProvider',
- label: t('Anbieter'),
- type: 'string',
- sortable: true,
- filterable: true,
- searchable: true,
- width: 120,
- },
- {
- key: 'aicoreModel',
- label: t('Modell'),
- type: 'string',
- sortable: true,
- filterable: true,
- searchable: true,
- width: 120,
- },
- {
- key: 'featureCode',
- label: t('Feature'),
- type: 'string',
- sortable: true,
- filterable: true,
- searchable: true,
- width: 110,
- },
- {
- key: 'amount',
- label: t('Betrag'),
- type: 'number',
- sortable: true,
- filterable: true,
- width: 120,
- formatter: (v, row: BillingTransaction) => {
- const n = Number(v);
- const abs = fmtChf(Math.abs(n));
- const prefix = row.transactionType === 'DEBIT' ? '-' : '+';
- return (
-
- {prefix}
- {abs}
-
- );
- },
- },
- ];
- }, [t]);
-
- return (
-
-
-
-
-
-
-
- data={transactions}
- columns={columns}
- apiEndpoint="/api/billing/transactions"
- tableContextKey="billing/transactions"
- loading={loading}
- pagination={true}
- pageSize={25}
- searchable={true}
- filterable={true}
- sortable={true}
- selectable={false}
- hookData={{
- refetch: refetchTransactions,
- pagination: transactionsPagination ?? undefined,
- groupLayout: transactionsGroupLayout ?? undefined,
- appliedView: transactionsAppliedView ?? undefined,
- }}
- emptyMessage={t('Keine Transaktionen vorhanden')}
- />
-
-
- );
-};
-
-export default BillingTransactions;
diff --git a/src/pages/billing/BillingUserView.tsx b/src/pages/billing/BillingUserView.tsx
deleted file mode 100644
index 4aba8f6..0000000
--- a/src/pages/billing/BillingUserView.tsx
+++ /dev/null
@@ -1,384 +0,0 @@
-// Copyright (c) 2026 PowerOn AG
-// All rights reserved.
-/**
- * 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';
-
-import { useLanguage } from '../../providers/language/LanguageContext';
-
-// ============================================================================
-// 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 { t } = useLanguage();
- 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 */}
-
-
-
-
- | {t('Mandant')} |
- {t('Benutzer')} |
- {t('Guthaben')} |
- {t('Warnschwelle')} |
- {t('Status')} |
-
-
-
- {filteredBalances.map((balance) => (
-
- | {balance.mandateName || balance.mandateId} |
- {balance.userName || balance.userId} |
- {formatCurrency(balance.balance)} |
- {formatCurrency(balance.warningThreshold)} |
-
- {balance.isWarning ? (
-
- {t('Niedrig')}
-
- ) : balance.enabled ? (
- {t('Aktiv')}
- ) : (
- {t('Deaktiviert')}
- )}
- |
-
- ))}
-
-
-
- >
- );
-};
-
-// ============================================================================
-// USER TRANSACTION TABLE
-// ============================================================================
-
-interface UserTransactionTableProps {
- transactions: UserTransaction[];
- selectedMandateId: string | null;
- selectedUserId: string | null;
-}
-
-const UserTransactionTable: React.FC = ({
- transactions,
- selectedMandateId,
- selectedUserId
-}) => {
- 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 '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 (
-
-
-
-
- | Datum |
- {t('Mandant')} |
- {t('Benutzer')} |
- Typ |
- {t('Beschreibung')} |
- Anbieter |
- Modell |
- Feature |
- {t('Betrag')} |
-
-
-
- {filteredTransactions.map((txn) => (
-
- | {formatDate(txn.sysCreatedAt)} |
- {txn.mandateName || '-'} |
- {txn.userName || '-'} |
-
-
- {getTypeLabel(txn.transactionType)}
-
- |
- {txn.description} |
- {txn.aicoreProvider || '-'} |
- {txn.aicoreModel || '-'} |
- {txn.featureCode || '-'} |
-
- {txn.transactionType === 'DEBIT' ? '-' : '+'}{formatCurrency(txn.amount)}
- |
-
- ))}
-
-
-
- );
-};
-
-// ============================================================================
-// MAIN COMPONENT
-// ============================================================================
-
-export const BillingUserView: React.FC = () => {
- const { t } = useLanguage();
- 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 (
-
-
-
-
-
- {/* User Balances */}
-
- {t('Benutzer-Guthaben')}
- {loading && balances.length === 0 ? (
- {t('Daten laden')}
- ) : balances.length === 0 ? (
- {t('Keine Benutzerkonten vorhanden')}
- ) : (
-
- )}
-
-
- {/* Transactions */}
-
-
-
- {t('Transaktionen')}
- {(selectedMandateId || selectedUserId) && (
-