feat(billing): scope filter, user balance cards, CSV export fix
- Scope dropdown (Meine Kosten / Mandant / Alle) on Overview and Statistics tabs - All user balance cards shown in Overview when scope is mandate or all - Balance cards and statistics react to scope selection - CSV export endpoint fixed: /api/billing/view/users/transactions - Scope selector hidden on Transactions tab (FormGeneratorTable handles own filters) - Fixed unused VoiceLanguage import in TeamsbotSettingsView Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c58bc77154
commit
40fe8a0a31
2 changed files with 139 additions and 35 deletions
|
|
@ -266,11 +266,18 @@ function _buildStatisticsSections(viewStats: ViewStatistics): ReportSection[] {
|
|||
export const BillingDataView: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('overview');
|
||||
|
||||
// Scope filter: 'personal' | 'all' | mandateId
|
||||
const [selectedScope, setSelectedScope] = useState<string>('personal');
|
||||
|
||||
// Dashboard state (for Overview tab)
|
||||
const {
|
||||
balances,
|
||||
loading: dashboardLoading,
|
||||
} = useBilling();
|
||||
|
||||
// All user balances (for admin overview cards)
|
||||
const [allUserBalances, setAllUserBalances] = useState<any[]>([]);
|
||||
const [allUserBalancesLoading, setAllUserBalancesLoading] = useState(false);
|
||||
|
||||
// Statistics state (shared by Overview and Statistics tabs)
|
||||
const [viewStats, setViewStats] = useState<ViewStatistics | null>(null);
|
||||
|
|
@ -282,6 +289,19 @@ export const BillingDataView: React.FC = () => {
|
|||
const [transactionsError, setTransactionsError] = useState<string | null>(null);
|
||||
const [transactionsPagination, setTransactionsPagination] = useState<any>(null);
|
||||
|
||||
// Load all user balances for admin overview
|
||||
const _loadAllUserBalances = useCallback(async () => {
|
||||
try {
|
||||
setAllUserBalancesLoading(true);
|
||||
const response = await api.get('/api/billing/view/users/balances');
|
||||
setAllUserBalances(Array.isArray(response.data) ? response.data : []);
|
||||
} catch {
|
||||
setAllUserBalances([]);
|
||||
} finally {
|
||||
setAllUserBalancesLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Load aggregated statistics from the view/statistics route
|
||||
const _loadViewStatistics = useCallback(async (period: string, year: number, month?: number) => {
|
||||
try {
|
||||
|
|
@ -290,6 +310,15 @@ export const BillingDataView: React.FC = () => {
|
|||
if (period === 'day' && month) {
|
||||
params.month = month;
|
||||
}
|
||||
// Apply scope filter
|
||||
if (selectedScope === 'personal') {
|
||||
params.scope = 'personal';
|
||||
} else if (selectedScope !== 'all') {
|
||||
params.scope = 'mandate';
|
||||
params.mandateId = selectedScope;
|
||||
} else {
|
||||
params.scope = 'all';
|
||||
}
|
||||
const response = await api.get('/api/billing/view/statistics', { params });
|
||||
setViewStats(response.data);
|
||||
} catch (err: any) {
|
||||
|
|
@ -298,7 +327,7 @@ export const BillingDataView: React.FC = () => {
|
|||
} finally {
|
||||
setStatsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [selectedScope]);
|
||||
|
||||
// Handle filter changes from FormGeneratorReport (user changes period/year/month)
|
||||
const _handleStatsFilterChange = useCallback((filterState: ReportFilterState) => {
|
||||
|
|
@ -313,7 +342,10 @@ export const BillingDataView: React.FC = () => {
|
|||
if (activeTab === 'overview' || activeTab === 'statistics') {
|
||||
_loadViewStatistics('month', new Date().getFullYear());
|
||||
}
|
||||
}, [activeTab, _loadViewStatistics]);
|
||||
if (activeTab === 'overview') {
|
||||
_loadAllUserBalances();
|
||||
}
|
||||
}, [activeTab, _loadViewStatistics, _loadAllUserBalances, selectedScope]);
|
||||
|
||||
// Load transactions with pagination support
|
||||
const _loadTransactions = useCallback(async (paginationParams?: any) => {
|
||||
|
|
@ -399,11 +431,46 @@ export const BillingDataView: React.FC = () => {
|
|||
defaultMonth: new Date().getMonth() + 1
|
||||
}), []);
|
||||
|
||||
// Build scope options from balances (mandates the user has access to)
|
||||
const scopeOptions = useMemo(() => {
|
||||
const options: Array<{ value: string; label: string }> = [
|
||||
{ value: 'personal', label: 'Meine Kosten' },
|
||||
];
|
||||
// Add mandate options from balances
|
||||
const seen = new Set<string>();
|
||||
for (const b of balances) {
|
||||
if (!seen.has(b.mandateId)) {
|
||||
seen.add(b.mandateId);
|
||||
options.push({ value: b.mandateId, label: `Mandant: ${b.mandateName}` });
|
||||
}
|
||||
}
|
||||
options.push({ value: 'all', label: 'Alle (RBAC)' });
|
||||
return options;
|
||||
}, [balances]);
|
||||
|
||||
return (
|
||||
<div className={styles.billingDashboard}>
|
||||
<header className={styles.pageHeader}>
|
||||
<h1>Billing</h1>
|
||||
<p className={styles.subtitle}>Guthaben, Statistiken und Transaktionen</p>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<h1>Billing</h1>
|
||||
<p className={styles.subtitle}>Guthaben, Statistiken und Transaktionen</p>
|
||||
</div>
|
||||
{activeTab !== 'transactions' && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<label style={{ fontSize: '13px', opacity: 0.7 }}>Ansicht:</label>
|
||||
<select
|
||||
className={styles.select || ''}
|
||||
value={selectedScope}
|
||||
onChange={(e) => setSelectedScope(e.target.value)}
|
||||
>
|
||||
{scopeOptions.map(opt => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<TabNav activeTab={activeTab} onTabChange={setActiveTab} />
|
||||
|
|
@ -411,36 +478,73 @@ export const BillingDataView: React.FC = () => {
|
|||
{/* ================================================================ */}
|
||||
{/* Tab: Übersicht (My Overview) */}
|
||||
{/* ================================================================ */}
|
||||
{activeTab === 'overview' && (
|
||||
<>
|
||||
{/* Balance Cards */}
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Mein Guthaben</h2>
|
||||
{dashboardLoading ? (
|
||||
<div className={styles.loadingPlaceholder}>Lade Guthaben...</div>
|
||||
) : balances.length === 0 ? (
|
||||
<div className={styles.noData}>Keine Abrechnungskonten vorhanden</div>
|
||||
) : (
|
||||
<div className={styles.balanceGrid}>
|
||||
{balances.map((balance) => (
|
||||
<BalanceCard key={balance.mandateId} balance={balance} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
{activeTab === 'overview' && (() => {
|
||||
// Filter balances and user accounts by scope
|
||||
const filteredBalances = selectedScope === 'personal' || selectedScope === 'all'
|
||||
? balances
|
||||
: balances.filter(b => b.mandateId === selectedScope);
|
||||
|
||||
const filteredUserBalances = selectedScope === 'personal'
|
||||
? [] // personal view: only own balance cards, no other users
|
||||
: selectedScope === 'all'
|
||||
? allUserBalances
|
||||
: allUserBalances.filter(ub => ub.mandateId === selectedScope);
|
||||
|
||||
{/* Usage Statistics via FormGeneratorReport (no period selector - always full year) */}
|
||||
<section className={styles.section}>
|
||||
<FormGeneratorReport
|
||||
title="Nutzungsübersicht"
|
||||
loading={statsLoading}
|
||||
sections={overviewSections}
|
||||
noDataMessage="Keine Statistiken verfügbar"
|
||||
currencyCode="CHF"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
return (
|
||||
<>
|
||||
{/* Balance Cards - own balances */}
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Mein Guthaben</h2>
|
||||
{dashboardLoading ? (
|
||||
<div className={styles.loadingPlaceholder}>Lade Guthaben...</div>
|
||||
) : filteredBalances.length === 0 ? (
|
||||
<div className={styles.noData}>Keine Abrechnungskonten vorhanden</div>
|
||||
) : (
|
||||
<div className={styles.balanceGrid}>
|
||||
{filteredBalances.map((balance) => (
|
||||
<BalanceCard key={balance.mandateId} balance={balance} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* All User Balance Cards (mandate/all scope) */}
|
||||
{filteredUserBalances.length > 0 && (
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionTitle}>Benutzer-Guthaben</h2>
|
||||
{allUserBalancesLoading ? (
|
||||
<div className={styles.loadingPlaceholder}>Lade Benutzer-Guthaben...</div>
|
||||
) : (
|
||||
<div className={styles.balanceGrid}>
|
||||
{filteredUserBalances.map((ub, idx) => (
|
||||
<div key={`${ub.userId}-${ub.mandateId}-${idx}`} className={styles.balanceCard}>
|
||||
<div className={styles.balanceHeader}>
|
||||
<h3 className={styles.mandateName}>{ub.userName || ub.userId?.slice(0, 8)}</h3>
|
||||
<span className={styles.billingModel}>{ub.mandateName}</span>
|
||||
</div>
|
||||
<div className={styles.balanceAmount}>
|
||||
{_formatCurrency(ub.balance || 0)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Usage Statistics via FormGeneratorReport */}
|
||||
<section className={styles.section}>
|
||||
<FormGeneratorReport
|
||||
title={selectedScope === 'personal' ? 'Meine Nutzung' : 'Nutzungsübersicht'}
|
||||
loading={statsLoading}
|
||||
sections={overviewSections}
|
||||
noDataMessage="Keine Statistiken verfügbar"
|
||||
currencyCode="CHF"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* ================================================================ */}
|
||||
{/* Tab: Statistik (Dashboard) */}
|
||||
|
|
@ -474,7 +578,7 @@ export const BillingDataView: React.FC = () => {
|
|||
<FormGeneratorTable
|
||||
data={transactions}
|
||||
columns={columns}
|
||||
apiEndpoint="/api/billing/balance"
|
||||
apiEndpoint="/api/billing/view/users/transactions"
|
||||
loading={transactionsLoading}
|
||||
pagination={true}
|
||||
pageSize={25}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||
import * as teamsbotApi from '../../../api/teamsbotApi';
|
||||
import type { TeamsbotConfig, ConfigUpdateRequest, VoiceLanguage, VoiceOption } from '../../../api/teamsbotApi';
|
||||
import type { TeamsbotConfig, ConfigUpdateRequest, VoiceOption } from '../../../api/teamsbotApi';
|
||||
import { FaPlay, FaSpinner } from 'react-icons/fa';
|
||||
import styles from './Teamsbot.module.css';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue