billing rbac
This commit is contained in:
parent
8f29bdb270
commit
15c93b3bf0
8 changed files with 77 additions and 26 deletions
|
|
@ -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<string, number>;
|
||||
costByModel: Record<string, number>;
|
||||
costByFeature: Record<string, number>;
|
||||
}
|
||||
|
||||
|
|
@ -260,6 +266,7 @@ export async function fetchTransactionsAdmin(
|
|||
*/
|
||||
export interface MandateUserSummary {
|
||||
id: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
|
|
|
|||
|
|
@ -137,21 +137,19 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
}
|
||||
}, [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<ProviderMultiSelectProps> = ({
|
|||
|
||||
// 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 (
|
||||
<div
|
||||
|
|
@ -208,7 +203,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
type="button"
|
||||
onClick={handleSelectNone}
|
||||
disabled={disabled}
|
||||
className={styles.actionButton}
|
||||
className={`${styles.actionButton} ${isNoneSelected ? styles.active : ''}`}
|
||||
>
|
||||
Keine
|
||||
</button>
|
||||
|
|
@ -225,7 +220,7 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
|||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={effectiveSelection.includes(provider)}
|
||||
checked={selectedProviders.includes(provider)}
|
||||
onChange={() => handleToggle(provider)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, users, on
|
|||
const balanceInfo = account ? ` (${formatCurrency(account.balance)})` : ' (kein Konto)';
|
||||
return (
|
||||
<option key={user.id} value={user.id}>
|
||||
{user.displayName || user.email || user.id}{balanceInfo}
|
||||
{user.displayName || user.username || user.id}{balanceInfo}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
|
|
@ -339,7 +339,7 @@ const AccountsOverview: React.FC<AccountsOverviewProps> = ({ 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,6 +114,28 @@ const StatisticsChart: React.FC<StatisticsChartProps> = ({ statistics, loading }
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.chartSection}>
|
||||
<h4>Kosten nach Modell</h4>
|
||||
{Object.entries(statistics.costByModel || {}).length === 0 ? (
|
||||
<div className={styles.noData}>Keine Daten</div>
|
||||
) : (
|
||||
<div className={styles.barChart}>
|
||||
{Object.entries(statistics.costByModel || {}).map(([model, cost]) => (
|
||||
<div key={model} className={styles.barRow}>
|
||||
<span className={styles.barLabel}>{model}</span>
|
||||
<div className={styles.barContainer}>
|
||||
<div
|
||||
className={styles.bar}
|
||||
style={{ width: `${(cost / maxProviderCost) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className={styles.barValue}>{formatCurrency(cost)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.chartSection}>
|
||||
<h4>Kosten nach Feature</h4>
|
||||
{Object.entries(statistics.costByFeature).length === 0 ? (
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ interface ViewStatistics {
|
|||
totalCost: number;
|
||||
transactionCount: number;
|
||||
costByProvider: Record<string, number>;
|
||||
costByModel: Record<string, number>;
|
||||
costByFeature: Record<string, number>;
|
||||
costByMandate: Record<string, number>;
|
||||
timeSeries: Array<{ date: string; cost: number; count: number }>;
|
||||
|
|
@ -134,6 +135,7 @@ function _recordToChartData(record: Record<string, number>): 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 },
|
||||
], []);
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ const TransactionTable: React.FC<TransactionTableProps> = ({ transactions }) =>
|
|||
<th>Typ</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Anbieter</th>
|
||||
<th>Modell</th>
|
||||
<th>Feature</th>
|
||||
<th style={{ textAlign: 'right' }}>Betrag</th>
|
||||
</tr>
|
||||
|
|
@ -162,6 +163,7 @@ const TransactionTable: React.FC<TransactionTableProps> = ({ transactions }) =>
|
|||
</td>
|
||||
<td>{t.description}</td>
|
||||
<td>{t.aicoreProvider || '-'}</td>
|
||||
<td>{t.aicoreModel || '-'}</td>
|
||||
<td>{t.featureCode || '-'}</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
{t.transactionType === 'DEBIT' ? '-' : '+'}{formatCurrency(t.amount)}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ const TransactionRow: React.FC<TransactionRowProps> = ({ transaction }) => {
|
|||
</td>
|
||||
<td>{transaction.description}</td>
|
||||
<td>{transaction.aicoreProvider || '-'}</td>
|
||||
<td>{transaction.aicoreModel || '-'}</td>
|
||||
<td>{transaction.featureCode || '-'}</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
{transaction.transactionType === 'DEBIT' ? '-' : '+'}{formatCurrency(transaction.amount)}
|
||||
|
|
@ -114,6 +115,7 @@ export const BillingTransactions: React.FC = () => {
|
|||
<th>Typ</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Anbieter</th>
|
||||
<th>Modell</th>
|
||||
<th>Feature</th>
|
||||
<th style={{ textAlign: 'right' }}>Betrag</th>
|
||||
</tr>
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ const UserTransactionTable: React.FC<UserTransactionTableProps> = ({
|
|||
<th>Typ</th>
|
||||
<th>Beschreibung</th>
|
||||
<th>Anbieter</th>
|
||||
<th>Modell</th>
|
||||
<th>Feature</th>
|
||||
<th style={{ textAlign: 'right' }}>Betrag</th>
|
||||
</tr>
|
||||
|
|
@ -245,6 +246,7 @@ const UserTransactionTable: React.FC<UserTransactionTableProps> = ({
|
|||
</td>
|
||||
<td>{t.description}</td>
|
||||
<td>{t.aicoreProvider || '-'}</td>
|
||||
<td>{t.aicoreModel || '-'}</td>
|
||||
<td>{t.featureCode || '-'}</td>
|
||||
<td style={{ textAlign: 'right' }}>
|
||||
{t.transactionType === 'DEBIT' ? '-' : '+'}{formatCurrency(t.amount)}
|
||||
|
|
|
|||
Loading…
Reference in a new issue