diff --git a/src/api/billingApi.ts b/src/api/billingApi.ts index 69ebf34..7c78496 100644 --- a/src/api/billingApi.ts +++ b/src/api/billingApi.ts @@ -36,11 +36,16 @@ export interface BillingTransaction { description: string; referenceType?: ReferenceType; workflowId?: string; + featureInstanceId?: string; featureCode?: string; aicoreProvider?: string; + aicoreModel?: string; + createdByUserId?: string; createdAt?: string; mandateId?: string; mandateName?: string; + userId?: string; + userName?: string; } export interface BillingSettings { @@ -70,6 +75,7 @@ export interface UsageReport { totalCost: number; transactionCount: number; costByProvider: Record; + costByModel: Record; costByFeature: Record; } @@ -260,6 +266,7 @@ export async function fetchTransactionsAdmin( */ export interface MandateUserSummary { id: string; + username?: string; email?: string; firstName?: string; lastName?: string; diff --git a/src/components/ProviderSelector/ProviderSelector.tsx b/src/components/ProviderSelector/ProviderSelector.tsx index 55596a8..b9706d3 100644 --- a/src/components/ProviderSelector/ProviderSelector.tsx +++ b/src/components/ProviderSelector/ProviderSelector.tsx @@ -137,21 +137,19 @@ export const ProviderMultiSelect: React.FC = ({ } }, [isExpanded, handleClickOutside]); - // Check if all providers are selected (or none selected = all used) - const isAllSelected = selectedProviders.length === 0 || - (allowedProviders.length > 0 && selectedProviders.length === allowedProviders.length); + // Check if all providers are explicitly selected + const isAllSelected = allowedProviders.length > 0 && selectedProviders.length === allowedProviders.length; - // For checkbox display: if none selected, show all as checked (since all are used) - const effectiveSelection = selectedProviders.length === 0 ? allowedProviders : selectedProviders; + // Check if no providers are selected (= no restriction, all allowed by default) + const isNoneSelected = selectedProviders.length === 0; const handleToggle = (provider: string) => { - // Use effectiveSelection for toggle logic (handles empty = all case) - if (effectiveSelection.includes(provider)) { - // Deactivate: remove from effective selection - onChange(effectiveSelection.filter((p) => p !== provider)); + if (selectedProviders.includes(provider)) { + // Deactivate: remove from selection + onChange(selectedProviders.filter((p) => p !== provider)); } else { - // Activate: add to effective selection - onChange([...effectiveSelection, provider]); + // Activate: add to selection + onChange([...selectedProviders, provider]); } }; @@ -165,14 +163,11 @@ export const ProviderMultiSelect: React.FC = ({ // Summary icon for button const summaryIcon = useMemo(() => { - if (selectedProviders.length === 0 || selectedProviders.length === allowedProviders.length) { - return '🤖'; - } if (selectedProviders.length === 1) { return PROVIDER_ICONS[selectedProviders[0]] || '🔌'; } return '🤖'; - }, [selectedProviders, allowedProviders]); + }, [selectedProviders]); return (
= ({ type="button" onClick={handleSelectNone} disabled={disabled} - className={styles.actionButton} + className={`${styles.actionButton} ${isNoneSelected ? styles.active : ''}`} > Keine @@ -225,7 +220,7 @@ export const ProviderMultiSelect: React.FC = ({ > handleToggle(provider)} disabled={disabled} /> diff --git a/src/pages/billing/BillingAdmin.tsx b/src/pages/billing/BillingAdmin.tsx index 8efb3cf..8a27eda 100644 --- a/src/pages/billing/BillingAdmin.tsx +++ b/src/pages/billing/BillingAdmin.tsx @@ -268,7 +268,7 @@ const CreditAdder: React.FC = ({ settings, accounts, users, on const balanceInfo = account ? ` (${formatCurrency(account.balance)})` : ' (kein Konto)'; return ( ); })} @@ -339,7 +339,7 @@ const AccountsOverview: React.FC = ({ accounts, users, lo for (const user of users) { const displayName = user.displayName || [user.firstName, user.lastName].filter(Boolean).join(' ') - || user.email + || user.username || user.id; map.set(user.id, displayName); } diff --git a/src/pages/billing/BillingDashboard.tsx b/src/pages/billing/BillingDashboard.tsx index 4e3f963..8e73efb 100644 --- a/src/pages/billing/BillingDashboard.tsx +++ b/src/pages/billing/BillingDashboard.tsx @@ -114,6 +114,28 @@ const StatisticsChart: React.FC = ({ statistics, loading } )}
+
+

Kosten nach Modell

+ {Object.entries(statistics.costByModel || {}).length === 0 ? ( +
Keine Daten
+ ) : ( +
+ {Object.entries(statistics.costByModel || {}).map(([model, cost]) => ( +
+ {model} +
+
+
+ {formatCurrency(cost)} +
+ ))} +
+ )} +
+

Kosten nach Feature

{Object.entries(statistics.costByFeature).length === 0 ? ( diff --git a/src/pages/billing/BillingDataView.tsx b/src/pages/billing/BillingDataView.tsx index f11360d..504bedc 100644 --- a/src/pages/billing/BillingDataView.tsx +++ b/src/pages/billing/BillingDataView.tsx @@ -35,6 +35,7 @@ interface ViewStatistics { totalCost: number; transactionCount: number; costByProvider: Record; + costByModel: Record; costByFeature: Record; costByMandate: Record; timeSeries: Array<{ date: string; cost: number; count: number }>; @@ -134,6 +135,7 @@ function _recordToChartData(record: Record): ReportChartDataPoin function _buildOverviewSections(viewStats: ViewStatistics): ReportSection[] { const topProvider = Object.entries(viewStats.costByProvider).sort((a, b) => b[1] - a[1])[0]; + const topModel = Object.entries(viewStats.costByModel || {}).sort((a, b) => b[1] - a[1])[0]; const topFeature = Object.entries(viewStats.costByFeature).sort((a, b) => b[1] - a[1])[0]; return [ @@ -150,15 +152,15 @@ function _buildOverviewSections(viewStats: ViewStatistics): ReportSection[] { value: Object.keys(viewStats.costByProvider).length, subtitle: topProvider ? `Top: ${topProvider[0]}` : 'Keine Nutzung' }, + { + label: 'Modelle', + value: Object.keys(viewStats.costByModel || {}).length, + subtitle: topModel ? `Top: ${topModel[0]}` : 'Keine Nutzung' + }, { label: 'Features', value: Object.keys(viewStats.costByFeature).length, subtitle: topFeature ? `Top: ${topFeature[0]}` : 'Keine Nutzung' - }, - { - label: 'Mandanten', - value: Object.keys(viewStats.costByMandate).length, - subtitle: 'aktiv genutzt' } ] }, @@ -169,6 +171,13 @@ function _buildOverviewSections(viewStats: ViewStatistics): ReportSection[] { formatValue: _formatCurrency, span: 'half' as const }, + { + type: 'horizontalBar', + title: 'Kosten nach Modell', + data: _recordToChartData(viewStats.costByModel || {}), + formatValue: _formatCurrency, + span: 'half' as const + }, { type: 'horizontalBar', title: 'Kosten nach Feature', @@ -206,6 +215,14 @@ function _buildStatisticsSections(viewStats: ViewStatistics): ReportSection[] { donut: true, span: 'half' as const }, + { + type: 'pieChart', + title: 'Verteilung nach Modell', + data: _recordToChartData(viewStats.costByModel || {}), + formatValue: _formatCurrency, + donut: true, + span: 'half' as const + }, { type: 'pieChart', title: 'Verteilung nach Feature', @@ -234,6 +251,7 @@ function _buildStatisticsSections(viewStats: ViewStatistics): ReportSection[] { { metric: 'Transaktionen', value: String(viewStats.transactionCount) }, { metric: 'Durchschnitt / Transaktion', value: _formatCurrency(avgCost) }, { metric: 'Anbieter', value: String(Object.keys(viewStats.costByProvider).length) }, + { metric: 'Modelle', value: String(Object.keys(viewStats.costByModel || {}).length) }, { metric: 'Features', value: String(Object.keys(viewStats.costByFeature).length) }, { metric: 'Mandanten', value: String(Object.keys(viewStats.costByMandate).length) } ] @@ -304,8 +322,10 @@ export const BillingDataView: React.FC = () => { setTransactionsError(null); const params: any = {}; - if (paginationParams) { - params.pagination = JSON.stringify(paginationParams); + // Only serialize if it's a plain pagination object (not a React event or other non-serializable object) + if (paginationParams && typeof paginationParams === 'object' && 'page' in paginationParams) { + const { page, pageSize, sortBy, sortDirection, search, filters } = paginationParams; + params.pagination = JSON.stringify({ page, pageSize, sortBy, sortDirection, search, filters }); } const response = await api.get('/api/billing/view/users/transactions', { params }); @@ -353,6 +373,7 @@ export const BillingDataView: React.FC = () => { { key: 'transactionType', label: 'Typ', type: 'text' as any, sortable: true, filterable: true, width: 100 }, { key: 'description', label: 'Beschreibung', type: 'text' as any, searchable: true, width: 250 }, { key: 'aicoreProvider', label: 'Anbieter', type: 'text' as any, sortable: true, filterable: true, width: 120 }, + { key: 'aicoreModel', label: 'Modell', type: 'text' as any, sortable: true, filterable: true, width: 150 }, { key: 'featureCode', label: 'Feature', type: 'text' as any, sortable: true, filterable: true, width: 120 }, { key: 'amount', label: 'Betrag (CHF)', type: 'number' as any, sortable: true, width: 120 }, ], []); diff --git a/src/pages/billing/BillingMandateView.tsx b/src/pages/billing/BillingMandateView.tsx index 82d022f..f6792c6 100644 --- a/src/pages/billing/BillingMandateView.tsx +++ b/src/pages/billing/BillingMandateView.tsx @@ -146,6 +146,7 @@ const TransactionTable: React.FC = ({ transactions }) => Typ Beschreibung Anbieter + Modell Feature Betrag @@ -162,6 +163,7 @@ const TransactionTable: React.FC = ({ transactions }) => {t.description} {t.aicoreProvider || '-'} + {t.aicoreModel || '-'} {t.featureCode || '-'} {t.transactionType === 'DEBIT' ? '-' : '+'}{formatCurrency(t.amount)} diff --git a/src/pages/billing/BillingTransactions.tsx b/src/pages/billing/BillingTransactions.tsx index ddd7f2c..d64bde7 100644 --- a/src/pages/billing/BillingTransactions.tsx +++ b/src/pages/billing/BillingTransactions.tsx @@ -65,6 +65,7 @@ const TransactionRow: React.FC = ({ transaction }) => { {transaction.description} {transaction.aicoreProvider || '-'} + {transaction.aicoreModel || '-'} {transaction.featureCode || '-'} {transaction.transactionType === 'DEBIT' ? '-' : '+'}{formatCurrency(transaction.amount)} @@ -114,6 +115,7 @@ export const BillingTransactions: React.FC = () => { Typ Beschreibung Anbieter + Modell Feature Betrag diff --git a/src/pages/billing/BillingUserView.tsx b/src/pages/billing/BillingUserView.tsx index 194be7d..4587acc 100644 --- a/src/pages/billing/BillingUserView.tsx +++ b/src/pages/billing/BillingUserView.tsx @@ -228,6 +228,7 @@ const UserTransactionTable: React.FC = ({ Typ Beschreibung Anbieter + Modell Feature Betrag @@ -245,6 +246,7 @@ const UserTransactionTable: React.FC = ({ {t.description} {t.aicoreProvider || '-'} + {t.aicoreModel || '-'} {t.featureCode || '-'} {t.transactionType === 'DEBIT' ? '-' : '+'}{formatCurrency(t.amount)}