/** * 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 { useBillingAdmin, type BillingSettings, type AccountSummary, type MandateUserSummary } from '../../hooks/useBilling'; import { useAdminMandates } from '../../hooks/useMandates'; import styles from './Billing.module.css'; const _formatCurrency = (amount: number) => { return new Intl.NumberFormat('de-CH', { style: 'currency', currency: 'CHF' }).format(amount); }; // ============================================================================ // MANDATE SELECTOR // ============================================================================ interface MandateSelectorProps { selectedMandateId: string | null; onSelect: (mandateId: string) => void; } const MandateSelector: React.FC = ({ selectedMandateId, onSelect }) => { const { mandates, loading } = useAdminMandates(); return (
); }; // ============================================================================ // SETTINGS EDITOR // ============================================================================ interface SettingsEditorProps { settings: BillingSettings | null; onSave: (settings: Partial) => Promise; loading: boolean; } const SettingsEditor: React.FC = ({ settings, onSave, loading }) => { const [formData, setFormData] = useState({ billingModel: settings?.billingModel || 'UNLIMITED', defaultUserCredit: settings?.defaultUserCredit || 10, warningThresholdPercent: settings?.warningThresholdPercent || 10, blockOnZeroBalance: settings?.blockOnZeroBalance ?? true, 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, defaultUserCredit: settings.defaultUserCredit, warningThresholdPercent: settings.warningThresholdPercent, blockOnZeroBalance: settings.blockOnZeroBalance, notifyOnWarning: settings.notifyOnWarning, }); } }, [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 (

Billing-Einstellungen

{message && (
{message.text}
)}
setFormData(prev => ({ ...prev, defaultUserCredit: Number(e.target.value) }))} min="0" step="0.01" />
setFormData(prev => ({ ...prev, warningThresholdPercent: Number(e.target.value) }))} min="0" max="100" step="1" />
); }; // ============================================================================ // CREDIT ADDER // ============================================================================ interface CreditAdderProps { settings: BillingSettings | null; accounts: AccountSummary[]; users: MandateUserSummary[]; onAddCredit: (userId: string | undefined, amount: number, description: string) => Promise; } const CreditAdder: React.FC = ({ settings, accounts, users, onAddCredit }) => { const [selectedUserId, setSelectedUserId] = useState(''); const [amount, setAmount] = useState(''); const [description, setDescription] = useState('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); 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 (

Guthaben manuell aufladen

{message && (
{message.text}
)}
{isPrepayUser && (
)}
setAmount(e.target.value)} placeholder="z.B. 50" min="0.01" step="0.01" required />
setDescription(e.target.value)} placeholder="Beschreibung der Gutschrift" />
); }; // ============================================================================ // ACCOUNTS OVERVIEW // ============================================================================ interface AccountsOverviewProps { accounts: AccountSummary[]; users: MandateUserSummary[]; loading: boolean; } const AccountsOverview: React.FC = ({ 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(); 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
Lade Konten...
; } if (accounts.length === 0) { return
Keine Konten vorhanden
; } return (

Konten

{accounts.map((account) => (

{account.accountType === 'MANDATE' ? 'Mandanten-Konto' : 'Benutzer-Konto'}

{account.userId && User: {_userNameMap.get(account.userId) || account.userId}} Guthaben: {formatCurrency(account.balance)} {account.creditLimit && Limit: {formatCurrency(account.creditLimit)}} Status: {account.enabled ? 'Aktiv' : 'Deaktiviert'}
))}
); }; // ============================================================================ // MAIN COMPONENT // ============================================================================ export const BillingAdmin: React.FC = () => { const [selectedMandateId, setSelectedMandateId] = useState(null); const { settings, accounts, users, loading, saveSettings, addCredit, loadAccounts } = useBillingAdmin(selectedMandateId || undefined); const handleMandateSelect = (mandateId: string) => { setSelectedMandateId(mandateId || null); }; const handleSaveSettings = useCallback(async (settingsUpdate: Partial) => { 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]); return (

Billing Administration

Verwaltung von Abrechnungseinstellungen und Guthaben

{selectedMandateId && ( <> )} {!selectedMandateId && (
Bitte wählen Sie einen Mandanten aus.
)}
); }; export default BillingAdmin;