frontend_nyla/src/pages/billing/BillingAdmin.tsx
2026-03-23 00:05:24 +01:00

815 lines
28 KiB
TypeScript

/**
* Billing Admin Page
*
* Admin-Seite für Billing-Verwaltung (SysAdmin only).
* - Settings verwalten
* - Guthaben aufladen
* - Konten übersicht
*/
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useBillingAdmin, type BillingSettings, type AccountSummary, type MandateUserSummary } from '../../hooks/useBilling';
import type { CheckoutCreateRequest } from '../../api/billingApi';
import { useUserMandates, type Mandate as UserMandateRow } from '../../hooks/useUserMandates';
import { useCurrentUser } from '../../hooks/useUsers';
import { useApiRequest } from '../../hooks/useApi';
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
import { SubscriptionTab } from './SubscriptionTab';
import api from '../../api';
import { getUserDataCache } from '../../utils/userCache';
import styles from './Billing.module.css';
type AdminTabType = 'settings' | 'credit' | 'subscription' | 'transactions';
const _formatCurrency = (amount: number) => {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
};
const _mandateDisplayLabel = (m: UserMandateRow): string => {
if (m.label) return m.label;
if (typeof m.name === 'object' && m.name) {
const n = m.name as Record<string, string>;
return n.de || n.en || Object.values(n)[0] || m.id;
}
return (m.name as string) || m.id;
};
// ============================================================================
// MANDATE SELECTOR
// ============================================================================
interface MandateSelectorProps {
mandates: UserMandateRow[];
loading: boolean;
selectedMandateId: string | null;
onSelect: (mandateId: string) => void;
}
const MandateSelector: React.FC<MandateSelectorProps> = ({
mandates,
loading,
selectedMandateId,
onSelect,
}) => (
<div className={styles.formGroup}>
<label>Mandant auswählen</label>
<select
className={styles.select}
value={selectedMandateId || ''}
onChange={e => onSelect(e.target.value)}
disabled={loading}
>
<option value="">-- Mandant wählen --</option>
{mandates.map(mandate => (
<option key={mandate.id} value={mandate.id}>
{_mandateDisplayLabel(mandate)}
</option>
))}
</select>
</div>
);
// ============================================================================
// SETTINGS EDITOR
// ============================================================================
interface SettingsEditorProps {
settings: BillingSettings | null;
onSave: (settings: Partial<BillingSettings>) => Promise<void>;
loading: boolean;
}
const SettingsEditor: React.FC<SettingsEditorProps> = ({ settings, onSave, loading }) => {
const [formData, setFormData] = useState({
billingModel: (settings?.billingModel === 'PREPAY_USER' ? 'PREPAY_USER' : 'PREPAY_MANDATE') as BillingSettings['billingModel'],
defaultUserCredit: Number(settings?.defaultUserCredit ?? 0),
warningThresholdPercent: Number(settings?.warningThresholdPercent ?? 10),
notifyOnWarning: settings?.notifyOnWarning ?? true,
});
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
useEffect(() => {
if (settings) {
setFormData({
billingModel: settings.billingModel === 'PREPAY_USER' ? 'PREPAY_USER' : 'PREPAY_MANDATE',
defaultUserCredit: Number(settings.defaultUserCredit ?? 0),
warningThresholdPercent: Number(settings.warningThresholdPercent ?? 10),
notifyOnWarning: settings.notifyOnWarning ?? true,
});
}
}, [settings]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
setMessage(null);
try {
await onSave(formData);
setMessage({ type: 'success', text: 'Einstellungen gespeichert!' });
} catch (err: any) {
setMessage({ type: 'error', text: err.message || 'Fehler beim Speichern' });
} finally {
setSaving(false);
}
};
return (
<div className={styles.adminSection}>
<h3>Billing-Einstellungen</h3>
{message && (
<div className={message.type === 'success' ? styles.successMessage : styles.errorMessage}>
{message.text}
</div>
)}
<form onSubmit={handleSubmit}>
<div className={styles.formRow}>
<div className={styles.formGroup}>
<label>Abrechnungsmodell</label>
<select
className={styles.select}
value={formData.billingModel}
onChange={(e) => setFormData(prev => ({ ...prev, billingModel: e.target.value as BillingSettings['billingModel'] }))}
>
<option value="PREPAY_MANDATE">Prepaid (Mandant)</option>
<option value="PREPAY_USER">Prepaid (Benutzer)</option>
</select>
</div>
<div className={styles.formGroup}>
<label>Standard-Guthaben (CHF)</label>
<input
type="number"
className={styles.input}
value={formData.defaultUserCredit}
onChange={(e) => setFormData(prev => ({ ...prev, defaultUserCredit: Number(e.target.value) }))}
min="0"
step="0.01"
/>
</div>
</div>
<div className={styles.formRow}>
<div className={styles.formGroup}>
<label>Warnschwelle (%)</label>
<input
type="number"
className={styles.input}
value={formData.warningThresholdPercent}
onChange={(e) => setFormData(prev => ({ ...prev, warningThresholdPercent: Number(e.target.value) }))}
min="0"
max="100"
step="1"
/>
</div>
</div>
<div className={styles.formRow}>
<div className={styles.formGroup}>
<label>&nbsp;</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={formData.notifyOnWarning}
onChange={(e) => setFormData(prev => ({ ...prev, notifyOnWarning: e.target.checked }))}
/>
Bei Warnung benachrichtigen
</label>
</div>
</div>
<button
type="submit"
className={`${styles.button} ${styles.buttonPrimary}`}
disabled={saving || loading}
>
{saving ? 'Speichern...' : 'Einstellungen speichern'}
</button>
</form>
</div>
);
};
// ============================================================================
// CREDIT ADDER
// ============================================================================
interface CreditAdderProps {
settings: BillingSettings | null;
accounts: AccountSummary[];
users: MandateUserSummary[];
onAddCredit: (userId: string | undefined, amount: number, description: string) => Promise<any>;
}
const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, users, onAddCredit }) => {
const [selectedUserId, setSelectedUserId] = useState<string>('');
const [amount, setAmount] = useState<string>('');
const [description, setDescription] = useState<string>('Manuelle Buchung durch Admin');
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const isPrepayUser = settings?.billingModel === 'PREPAY_USER';
const accountsByUserId = accounts
.filter(acc => acc.accountType === 'USER')
.reduce((map, acc) => {
if (acc.userId) map[acc.userId] = acc;
return map;
}, {} as Record<string, AccountSummary>);
const _handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const numAmount = parseFloat(amount);
if (!numAmount || numAmount === 0) {
setMessage({ type: 'error', text: 'Betrag darf nicht null sein' });
return;
}
setSaving(true);
setMessage(null);
try {
await onAddCredit(isPrepayUser ? selectedUserId : undefined, numAmount, description);
const label = numAmount > 0
? `${_formatCurrency(numAmount)} erfolgreich gutgeschrieben.`
: `${_formatCurrency(Math.abs(numAmount))} erfolgreich abgezogen.`;
setMessage({ type: 'success', text: label });
setAmount('');
} catch (err: any) {
setMessage({ type: 'error', text: err.message || 'Fehler bei der Buchung' });
} finally {
setSaving(false);
}
};
return (
<div className={styles.adminSection}>
<h3>Guthaben manuell verwalten</h3>
{message && (
<div className={message.type === 'success' ? styles.successMessage : styles.errorMessage}>
{message.text}
</div>
)}
<form onSubmit={_handleSubmit}>
{isPrepayUser && (
<div className={styles.formRow}>
<div className={styles.formGroup}>
<label>Benutzer</label>
<select
className={styles.select}
value={selectedUserId}
onChange={(e) => setSelectedUserId(e.target.value)}
required
>
<option value="">-- Benutzer wählen --</option>
{users.map((user) => {
const account = accountsByUserId[user.id];
const balanceInfo = account ? ` (${_formatCurrency(account.balance)})` : ' (kein Konto)';
return (
<option key={user.id} value={user.id}>
{user.displayName || user.username || user.id}{balanceInfo}
</option>
);
})}
</select>
</div>
</div>
)}
<div className={styles.formRow}>
<div className={styles.formGroup}>
<label>Betrag (CHF)</label>
<input
type="number"
className={styles.input}
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="z.B. 50 oder -20"
step="0.01"
required
/>
</div>
<div className={styles.formGroup}>
<label>Beschreibung</label>
<input
type="text"
className={styles.input}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Beschreibung der Gutschrift"
/>
</div>
</div>
<button
type="submit"
className={`${styles.button} ${styles.buttonPrimary}`}
disabled={saving || (isPrepayUser && !selectedUserId) || !amount}
>
{saving ? 'Wird verbucht...' : (parseFloat(amount) < 0 ? 'Guthaben abziehen' : 'Guthaben aufladen')}
</button>
</form>
</div>
);
};
// ============================================================================
// ACCOUNTS OVERVIEW
// ============================================================================
interface AccountsOverviewProps {
accounts: AccountSummary[];
users: MandateUserSummary[];
loading: boolean;
}
const AccountsOverview: React.FC<AccountsOverviewProps> = ({ accounts, users, loading }) => {
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
};
// Build a lookup map: userId -> display name
const _userNameMap = useMemo(() => {
const map = new Map<string, string>();
for (const user of users) {
const displayName = user.displayName
|| [user.firstName, user.lastName].filter(Boolean).join(' ')
|| user.username
|| user.id;
map.set(user.id, displayName);
}
return map;
}, [users]);
if (loading) {
return <div className={styles.loadingPlaceholder}>Lade Konten...</div>;
}
if (accounts.length === 0) {
return <div className={styles.noData}>Keine Konten vorhanden</div>;
}
return (
<div className={styles.adminSection}>
<h3>Konten</h3>
<div className={styles.accountsGrid}>
{accounts.map((account) => (
<div key={account.id} className={styles.accountCard}>
<h4>{account.accountType === 'MANDATE' ? 'Mandanten-Konto' : 'Benutzer-Konto'}</h4>
<div className={styles.accountInfo}>
{account.userId && <span>User: {_userNameMap.get(account.userId) || account.userId}</span>}
<span>Guthaben: <strong>{formatCurrency(account.balance)}</strong></span>
<span>Status: {account.enabled ? 'Aktiv' : 'Deaktiviert'}</span>
</div>
</div>
))}
</div>
</div>
);
};
// ============================================================================
// MANDATE ADMIN — STRIPE TOP-UP (same URL as SysAdmin billing admin)
// ============================================================================
interface MandateStripeTopUpProps {
mandateId: string;
createCheckout: (
checkoutRequest: CheckoutCreateRequest,
targetMandateId?: string
) => Promise<{ redirectUrl?: string } | null>;
}
const MandateStripeTopUp: React.FC<MandateStripeTopUpProps> = ({ mandateId, createCheckout }) => {
const [amount, setAmount] = useState('');
const [busy, setBusy] = useState(false);
const [localMsg, setLocalMsg] = useState<string | null>(null);
const _handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const n = parseFloat(amount);
if (!n || n <= 0) {
setLocalMsg('Betrag muss positiv sein');
return;
}
setBusy(true);
setLocalMsg(null);
try {
const currentUser = getUserDataCache();
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.delete('success');
currentUrl.searchParams.delete('canceled');
currentUrl.searchParams.delete('session_id');
currentUrl.hash = '';
const returnUrl = `${currentUrl.origin}${currentUrl.pathname}${currentUrl.search}`;
const result = await createCheckout(
{ userId: currentUser?.id, amount: n, returnUrl },
mandateId
);
if (result?.redirectUrl) {
window.location.href = result.redirectUrl;
}
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : 'Checkout fehlgeschlagen';
setLocalMsg(msg);
setBusy(false);
}
};
return (
<div className={styles.adminSection}>
<h3>Guthaben via Stripe aufladen</h3>
<p style={{ fontSize: '13px', color: 'var(--text-secondary, #64748b)', marginTop: 0 }}>
Sie werden zu Stripe weitergeleitet. Nach erfolgreicher Zahlung kehren Sie hierher zurück.
</p>
{localMsg && <div className={styles.errorMessage}>{localMsg}</div>}
<form onSubmit={_handleSubmit}>
<div className={styles.formRow}>
<div className={styles.formGroup}>
<label>Betrag (CHF)</label>
<input
type="number"
className={styles.input}
value={amount}
onChange={e => setAmount(e.target.value)}
placeholder="z.B. 50"
min="0.01"
step="0.01"
required
/>
</div>
</div>
<button
type="submit"
className={`${styles.button} ${styles.buttonPrimary}`}
disabled={busy || !amount}
>
{busy ? 'Weiterleitung...' : 'Mit Stripe bezahlen'}
</button>
</form>
</div>
);
};
// ============================================================================
// MANDATE TRANSACTIONS TAB (FormGeneratorTable with filters, search, export)
// ============================================================================
const _mandateTxColumns: ColumnConfig[] = [
{ key: 'createdAt', label: 'Datum', type: 'timestamp' as any, sortable: true, width: 160 },
{ key: 'userName', label: 'Benutzer', type: 'text' as any, sortable: true, filterable: true, searchable: true, width: 150 },
{ 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 },
];
interface MandateTransactionsTabProps {
mandateId: string;
}
const MandateTransactionsTab: React.FC<MandateTransactionsTabProps> = ({ mandateId }) => {
const { request, isLoading: loading } = useApiRequest();
const [transactions, setTransactions] = useState<any[]>([]);
const [pagination, setPagination] = useState<any>(null);
const [error, setError] = useState<string | null>(null);
const _loadTransactions = useCallback(async (params?: any) => {
try {
setError(null);
const requestParams: Record<string, string> = {};
if (params) {
const paginationObj: any = {};
if (params.page !== undefined) paginationObj.page = params.page;
if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
if (params.sort) paginationObj.sort = params.sort;
if (params.filters) paginationObj.filters = params.filters;
if (params.search) paginationObj.search = params.search;
if (Object.keys(paginationObj).length > 0) {
requestParams.pagination = JSON.stringify(paginationObj);
}
}
const data = await request({
url: `/api/billing/admin/transactions/${mandateId}`,
method: 'get',
params: requestParams,
});
if (data && typeof data === 'object' && 'items' in data) {
setTransactions(Array.isArray(data.items) ? data.items : []);
if (data.pagination) setPagination(data.pagination);
} else {
setTransactions(Array.isArray(data) ? data : []);
setPagination(null);
}
} catch (err: any) {
setError(err?.response?.data?.detail || err.message || 'Fehler beim Laden');
setTransactions([]);
setPagination(null);
}
}, [request, mandateId]);
useEffect(() => {
_loadTransactions();
}, [_loadTransactions]);
return (
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: '400px' }}>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary, #888)', margin: '0 0 1rem 0' }}>
AI-Verbrauch und Guthaben-Transaktionen. Subscription-Gebühren werden separat über Stripe abgerechnet.
</p>
{error && <div className={styles.errorMessage}>{error}</div>}
<FormGeneratorTable
data={transactions}
columns={_mandateTxColumns}
apiEndpoint={`/api/billing/admin/transactions/${mandateId}`}
loading={loading}
pagination={true}
pageSize={25}
searchable={true}
filterable={true}
sortable={true}
selectable={false}
emptyMessage="Keine Transaktionen für diesen Mandanten"
onRefresh={() => _loadTransactions()}
hookData={{ refetch: _loadTransactions, pagination }}
/>
</div>
);
};
// ============================================================================
// MAIN COMPONENT
// ============================================================================
export const BillingAdmin: React.FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const { user: currentUser } = useCurrentUser();
const isSysAdmin = currentUser?.isSysAdmin === true;
const [selectedMandateId, setSelectedMandateId] = useState<string | null>(
searchParams.get('mandate') || null
);
const [mandateList, setMandateList] = useState<UserMandateRow[]>([]);
const [mandatesLoading, setMandatesLoading] = useState(true);
const { fetchMandates } = useUserMandates();
const {
settings,
accounts,
users,
loading,
saveSettings,
addCredit,
loadAccounts,
createCheckout,
} = useBillingAdmin(selectedMandateId || undefined);
const handleMandateSelect = (mandateId: string) => {
setSelectedMandateId(mandateId || null);
};
const handleSaveSettings = useCallback(async (settingsUpdate: Partial<BillingSettings>) => {
if (!selectedMandateId) return;
await saveSettings(settingsUpdate);
}, [selectedMandateId, saveSettings]);
const _handleAddCredit = useCallback(async (userId: string | undefined, amount: number, description: string) => {
if (!selectedMandateId) throw new Error('Mandant nicht ausgewählt');
const result = await addCredit({ userId, amount, description });
if (!result) throw new Error('Gutschrift konnte nicht erstellt werden');
await loadAccounts();
return result;
}, [selectedMandateId, addCredit, loadAccounts]);
useEffect(() => {
let cancelled = false;
(async () => {
setMandatesLoading(true);
try {
const data = await fetchMandates();
if (!cancelled) {
setMandateList(Array.isArray(data) ? data : []);
}
} finally {
if (!cancelled) setMandatesLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [fetchMandates]);
useEffect(() => {
if (!isSysAdmin && mandateList.length === 1 && selectedMandateId === null) {
setSelectedMandateId(mandateList[0].id);
}
}, [isSysAdmin, mandateList, selectedMandateId]);
const [stripeReturnMessage, setStripeReturnMessage] = useState<{
type: 'success' | 'error';
text: string;
} | null>(null);
const successParam = searchParams.get('success');
const canceledParam = searchParams.get('canceled');
const sessionIdParam = searchParams.get('session_id');
const _initialAdminTab = (searchParams.get('tab') as AdminTabType) || 'settings';
const [adminTab, setAdminTab] = useState<AdminTabType>(
['settings', 'credit', 'subscription', 'transactions'].includes(_initialAdminTab) ? _initialAdminTab : 'settings'
);
useEffect(() => {
if (adminTab === 'subscription' || searchParams.get('tab') === 'subscription') return;
let cancelled = false;
const _confirmCheckoutIfNeeded = async () => {
if (successParam !== 'true') {
if (canceledParam === 'true' && !cancelled) {
setStripeReturnMessage({ type: 'error', text: 'Zahlung abgebrochen.' });
}
return;
}
if (!sessionIdParam) {
if (!cancelled) {
setStripeReturnMessage({
type: 'success',
text: 'Zahlung erfolgreich. Guthaben wird gutgeschrieben.',
});
}
if (selectedMandateId) await loadAccounts(selectedMandateId);
return;
}
try {
await api.post('/api/billing/checkout/confirm', { sessionId: sessionIdParam });
if (!cancelled) {
setStripeReturnMessage({
type: 'success',
text: 'Zahlung erfolgreich. Guthaben wurde verbucht.',
});
}
} catch (err: unknown) {
const detail = (err as { response?: { data?: { detail?: string } } })?.response?.data?.detail;
if (!cancelled) {
setStripeReturnMessage({
type: 'error',
text:
detail ||
'Zahlung erfolgreich, aber Verbuchung konnte nicht bestätigt werden.',
});
}
} finally {
if (selectedMandateId) await loadAccounts(selectedMandateId);
}
};
_confirmCheckoutIfNeeded();
return () => {
cancelled = true;
};
}, [adminTab, successParam, canceledParam, sessionIdParam, selectedMandateId, loadAccounts]);
const _clearStripeParams = useCallback(() => {
searchParams.delete('success');
searchParams.delete('canceled');
searchParams.delete('session_id');
searchParams.delete('mandate');
setSearchParams(searchParams, { replace: true });
setStripeReturnMessage(null);
}, [searchParams, setSearchParams]);
const showStripeForMandateAdmin = !!selectedMandateId && !!settings;
const _tabStyle = (isActive: boolean) => ({
padding: '8px 16px',
textDecoration: 'none',
borderRadius: '4px',
backgroundColor: isActive ? 'var(--color-primary, #3b82f6)' : 'transparent',
color: isActive ? 'white' : 'var(--color-text, #e0e0e0)',
fontWeight: isActive ? 600 : 400,
cursor: 'pointer',
border: 'none',
fontSize: '14px',
});
return (
<div className={styles.billingDashboard}>
<header className={styles.pageHeader}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div>
<h1>Billing-Verwaltung</h1>
<p className={styles.subtitle}>
Abrechnungseinstellungen, Guthaben und Abonnement pro Mandant
</p>
</div>
</div>
</header>
{stripeReturnMessage && (
<div
className={
stripeReturnMessage.type === 'success' ? styles.successMessage : styles.errorMessage
}
style={{ marginBottom: '1rem' }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '12px', alignItems: 'center' }}>
<span>{stripeReturnMessage.text}</span>
<button type="button" className={styles.button} onClick={_clearStripeParams}>
OK
</button>
</div>
</div>
)}
<section className={styles.section}>
<MandateSelector
mandates={mandateList}
loading={mandatesLoading}
selectedMandateId={selectedMandateId}
onSelect={handleMandateSelect}
/>
</section>
{selectedMandateId ? (
<>
<nav style={{
display: 'flex',
gap: '8px',
marginBottom: '24px',
borderBottom: '1px solid var(--color-border, #333)',
paddingBottom: '8px',
}}>
<button onClick={() => setAdminTab('settings')} style={_tabStyle(adminTab === 'settings')}>
Einstellungen
</button>
<button onClick={() => setAdminTab('credit')} style={_tabStyle(adminTab === 'credit')}>
Guthaben
</button>
<button onClick={() => setAdminTab('subscription')} style={_tabStyle(adminTab === 'subscription')}>
Abonnement
</button>
<button onClick={() => setAdminTab('transactions')} style={_tabStyle(adminTab === 'transactions')}>
Transaktionen
</button>
</nav>
{adminTab === 'settings' && (
<SettingsEditor
settings={settings}
onSave={handleSaveSettings}
loading={loading}
/>
)}
{adminTab === 'credit' && (
<>
{isSysAdmin && (
<CreditAdder
settings={settings}
accounts={accounts}
users={users}
onAddCredit={_handleAddCredit}
/>
)}
{showStripeForMandateAdmin && (
<MandateStripeTopUp mandateId={selectedMandateId} createCheckout={createCheckout} />
)}
<AccountsOverview accounts={accounts} users={users} loading={loading} />
</>
)}
{adminTab === 'subscription' && (
<SubscriptionTab mandateId={selectedMandateId} />
)}
{adminTab === 'transactions' && (
<MandateTransactionsTab mandateId={selectedMandateId} />
)}
</>
) : (
<div className={styles.noData}>Bitte wählen Sie einen Mandanten aus.</div>
)}
</div>
);
};
export default BillingAdmin;