frontend_nyla/src/pages/billing/BillingAdmin.tsx
2026-02-06 16:18:44 +01:00

431 lines
14 KiB
TypeScript

/**
* Billing Admin Page
*
* Admin-Seite für Billing-Verwaltung (SysAdmin only).
* - Settings verwalten
* - Guthaben aufladen
* - Konten übersicht
*/
import React, { useState, useEffect, useCallback } from 'react';
import { useBillingAdmin, type BillingSettings, type AccountSummary, type MandateUserSummary } from '../../hooks/useBilling';
import { useAdminMandates } from '../../hooks/useMandates';
import styles from './Billing.module.css';
// ============================================================================
// MANDATE SELECTOR
// ============================================================================
interface MandateSelectorProps {
selectedMandateId: string | null;
onSelect: (mandateId: string) => void;
}
const MandateSelector: React.FC<MandateSelectorProps> = ({ selectedMandateId, onSelect }) => {
const { mandates, loading } = useAdminMandates();
return (
<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}>
{mandate.name || mandate.id}
</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 || '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 (
<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 any }))}
>
<option value="UNLIMITED">Unlimited</option>
<option value="PREPAY_MANDATE">Prepaid (Mandant)</option>
<option value="PREPAY_USER">Prepaid (Benutzer)</option>
<option value="CREDIT_POSTPAY">Kredit (Postpay)</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 className={styles.formGroup}>
<label>&nbsp;</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={formData.blockOnZeroBalance}
onChange={(e) => setFormData(prev => ({ ...prev, blockOnZeroBalance: e.target.checked }))}
/>
Bei Guthaben 0 blockieren
</label>
</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<void>;
}
const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, users, onAddCredit }) => {
const [selectedUserId, setSelectedUserId] = useState<string>('');
const [amount, setAmount] = useState<number>(10);
const [description, setDescription] = useState<string>('Manuelles Aufladen');
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const isPrepayUser = settings?.billingModel === 'PREPAY_USER';
// Map accounts by userId for balance lookup
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();
if (amount <= 0) {
setMessage({ type: 'error', text: 'Betrag muss positiv sein' });
return;
}
setSaving(true);
setMessage(null);
try {
await onAddCredit(isPrepayUser ? selectedUserId : undefined, amount, description);
setMessage({ type: 'success', text: `${amount} CHF erfolgreich gutgeschrieben!` });
setAmount(10);
setDescription('Manuelles Aufladen');
} catch (err: any) {
setMessage({ type: 'error', text: err.message || 'Fehler beim Aufladen' });
} finally {
setSaving(false);
}
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
};
return (
<div className={styles.adminSection}>
<h3>Guthaben 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.email || 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(Number(e.target.value))}
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="Grund für Gutschrift"
/>
</div>
</div>
<button
type="submit"
className={`${styles.button} ${styles.buttonPrimary}`}
disabled={saving || (isPrepayUser && !selectedUserId)}
>
{saving ? 'Aufladen...' : 'Guthaben aufladen'}
</button>
</form>
</div>
);
};
// ============================================================================
// ACCOUNTS OVERVIEW
// ============================================================================
interface AccountsOverviewProps {
accounts: AccountSummary[];
loading: boolean;
}
const AccountsOverview: React.FC<AccountsOverviewProps> = ({ accounts, loading }) => {
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('de-CH', {
style: 'currency',
currency: 'CHF'
}).format(amount);
};
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: {account.userId}</span>}
<span>Guthaben: <strong>{formatCurrency(account.balance)}</strong></span>
{account.creditLimit && <span>Limit: {formatCurrency(account.creditLimit)}</span>}
<span>Status: {account.enabled ? 'Aktiv' : 'Deaktiviert'}</span>
</div>
</div>
))}
</div>
</div>
);
};
// ============================================================================
// MAIN COMPONENT
// ============================================================================
export const BillingAdmin: React.FC = () => {
const [selectedMandateId, setSelectedMandateId] = useState<string | null>(null);
const { settings, accounts, users, loading, loadSettings, saveSettings, addCredit, loadAccounts } = 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) return;
await addCredit({ userId, amount, description });
await loadAccounts();
}, [selectedMandateId, addCredit, loadAccounts]);
return (
<div className={styles.billingDashboard}>
<header className={styles.pageHeader}>
<h1>Billing Administration</h1>
<p className={styles.subtitle}>Verwaltung von Abrechnungseinstellungen und Guthaben</p>
</header>
<section className={styles.section}>
<MandateSelector
selectedMandateId={selectedMandateId}
onSelect={handleMandateSelect}
/>
</section>
{selectedMandateId && (
<>
<SettingsEditor
settings={settings}
onSave={handleSaveSettings}
loading={loading}
/>
<CreditAdder
settings={settings}
accounts={accounts}
users={users}
onAddCredit={handleAddCredit}
/>
<AccountsOverview
accounts={accounts}
loading={loading}
/>
</>
)}
{!selectedMandateId && (
<div className={styles.noData}>
Bitte wählen Sie einen Mandanten aus.
</div>
)}
</div>
);
};
export default BillingAdmin;