Gateway - InsufficientBalanceException: billingModel, userAction (TOP_UP_SELF / CONTACT_MANDATE_ADMIN), DE/EN-Texte, toClientDict(), fromBalanceCheck() - HTTP 402 + JSON detail für globale API-Fehlerbehandlung - AI/Chatbot: vor Raise ggf. E-Mail an BillingSettings.notifyEmails (PREPAY_MANDATE, Throttle 1h/Mandat) via billingExhaustedNotify - Agent-Loop & Workspace-Route: SSE-ERROR mit strukturiertem Billing-Payload - datamodelBilling: notifyEmails-Doku für Pool-Alerts frontend_nyla - useWorkspace: SSE onError für INSUFFICIENT_BALANCE mit messageDe/En und Hinweis auf Billing-Pfad bei TOP_UP_SELF
672 lines
22 KiB
TypeScript
672 lines
22 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 { Link, 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 api from '../../api';
|
|
import { getUserDataCache } from '../../utils/userCache';
|
|
import styles from './Billing.module.css';
|
|
|
|
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> </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>('Manuelles Aufladen 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 muss positiv sein' });
|
|
return;
|
|
}
|
|
|
|
setSaving(true);
|
|
setMessage(null);
|
|
|
|
try {
|
|
await onAddCredit(isPrepayUser ? selectedUserId : undefined, numAmount, description);
|
|
setMessage({ type: 'success', text: `${_formatCurrency(numAmount)} erfolgreich gutgeschrieben.` });
|
|
setAmount('');
|
|
} catch (err: any) {
|
|
setMessage({ type: 'error', text: err.message || 'Fehler beim Aufladen' });
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={styles.adminSection}>
|
|
<h3>Guthaben manuell aufladen</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"
|
|
min="0.01"
|
|
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 gutgeschrieben...' : 'Manuell 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>
|
|
);
|
|
};
|
|
|
|
// ============================================================================
|
|
// 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>(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');
|
|
|
|
useEffect(() => {
|
|
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;
|
|
};
|
|
}, [successParam, canceledParam, sessionIdParam, selectedMandateId, loadAccounts]);
|
|
|
|
const _clearStripeParams = useCallback(() => {
|
|
searchParams.delete('success');
|
|
searchParams.delete('canceled');
|
|
searchParams.delete('session_id');
|
|
setSearchParams(searchParams, { replace: true });
|
|
setStripeReturnMessage(null);
|
|
}, [searchParams, setSearchParams]);
|
|
|
|
const showStripeForMandateAdmin = !isSysAdmin && !!selectedMandateId && !!settings;
|
|
|
|
return (
|
|
<div className={styles.billingDashboard}>
|
|
<header className={styles.pageHeader}>
|
|
<h1>Billing Administration</h1>
|
|
<p className={styles.subtitle}>
|
|
{isSysAdmin
|
|
? 'Verwaltung von Abrechnungseinstellungen und Guthaben'
|
|
: 'Guthaben und Konten für Ihre Mandanten'}
|
|
</p>
|
|
{isSysAdmin && (
|
|
<p style={{ marginTop: '8px' }}>
|
|
<Link to="/admin/billing/mandates" style={{ color: 'var(--color-primary)' }}>
|
|
Mandanten-Übersicht (Balances & Transaktionen)
|
|
</Link>
|
|
</p>
|
|
)}
|
|
</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 && (
|
|
<>
|
|
{isSysAdmin && (
|
|
<SettingsEditor
|
|
settings={settings}
|
|
onSave={handleSaveSettings}
|
|
loading={loading}
|
|
/>
|
|
)}
|
|
|
|
{isSysAdmin && (
|
|
<CreditAdder
|
|
settings={settings}
|
|
accounts={accounts}
|
|
users={users}
|
|
onAddCredit={_handleAddCredit}
|
|
/>
|
|
)}
|
|
|
|
{showStripeForMandateAdmin && (
|
|
<MandateStripeTopUp mandateId={selectedMandateId} createCheckout={createCheckout} />
|
|
)}
|
|
|
|
<AccountsOverview accounts={accounts} users={users} loading={loading} />
|
|
</>
|
|
)}
|
|
|
|
{!selectedMandateId && (
|
|
<div className={styles.noData}>Bitte wählen Sie einen Mandanten aus.</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BillingAdmin;
|