117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
|
|
import { useApiRequest } from '../../hooks/useApi';
|
|
import { useConfirm } from '../../hooks/useConfirm';
|
|
import api from '../../api';
|
|
import styles from './Billing.module.css';
|
|
|
|
const _TERMINAL_STATUSES = new Set(['EXPIRED']);
|
|
|
|
const _STATUS_LABELS: Record<string, string> = {
|
|
PENDING: 'Ausstehend',
|
|
SCHEDULED: 'Geplant',
|
|
TRIALING: 'Testphase',
|
|
ACTIVE: 'Aktiv',
|
|
PAST_DUE: 'Überfällig',
|
|
EXPIRED: 'Abgelaufen',
|
|
};
|
|
|
|
const _COLUMNS: ColumnConfig[] = [
|
|
{ key: 'mandateName', label: 'Mandant', type: 'text' as any, sortable: true, filterable: true, width: 180 },
|
|
{ key: 'planTitle', label: 'Plan', type: 'text' as any, sortable: true, filterable: true, width: 180 },
|
|
{ key: 'status', label: 'Status', type: 'text' as any, sortable: true, filterable: true, width: 110 },
|
|
{ key: 'recurring', label: 'Wiederkehrend', type: 'boolean' as any, sortable: true, filterable: true, width: 120 },
|
|
{ key: 'activeUsers', label: 'User', type: 'number' as any, sortable: true, width: 70 },
|
|
{ key: 'activeInstances', label: 'Instanzen', type: 'number' as any, sortable: true, width: 90 },
|
|
{ key: 'monthlyRevenueCHF', label: 'Revenue/Mt (CHF)', type: 'number' as any, sortable: true, width: 140 },
|
|
{ key: 'startedAt', label: 'Gestartet', type: 'date' as any, sortable: true, width: 130 },
|
|
{ key: 'currentPeriodEnd', label: 'Periodenende', type: 'date' as any, sortable: true, width: 130 },
|
|
{ key: 'snapshotPricePerUserCHF', label: 'Preis/User', type: 'number' as any, sortable: true, width: 100 },
|
|
{ key: 'snapshotPricePerInstanceCHF', label: 'Preis/Instanz', type: 'number' as any, sortable: true, width: 110 },
|
|
];
|
|
|
|
const AdminSubscriptionsPage: React.FC = () => {
|
|
const navigate = useNavigate();
|
|
const { request } = useApiRequest();
|
|
const { confirm, ConfirmDialog } = useConfirm();
|
|
const [subscriptions, setSubscriptions] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const _loadSubscriptions = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await request({ url: '/api/subscription/admin/all', method: 'get' });
|
|
const rows = (Array.isArray(data) ? data : []).map((row: any) => ({
|
|
...row,
|
|
status: _STATUS_LABELS[row.status] || row.status,
|
|
_rawStatus: row.status,
|
|
}));
|
|
setSubscriptions(rows);
|
|
} catch (err) {
|
|
console.error('Failed to load subscriptions:', err);
|
|
setSubscriptions([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [request]);
|
|
|
|
useEffect(() => { _loadSubscriptions(); }, [_loadSubscriptions]);
|
|
|
|
const _handleForceCancel = useCallback(async (row: any) => {
|
|
const ok = await confirm(
|
|
`Subscription «${row.planTitle}» für Mandant «${row.mandateName}» sofort kündigen? Dies wird auch auf Stripe sofort storniert.`,
|
|
{ confirmLabel: 'Sofort kündigen', cancelLabel: 'Abbrechen', variant: 'danger' },
|
|
);
|
|
if (!ok) return;
|
|
|
|
try {
|
|
await api.post('/api/subscription/force-cancel', { subscriptionId: row.id });
|
|
await _loadSubscriptions();
|
|
} catch (err) {
|
|
console.error('Force cancel failed:', err);
|
|
}
|
|
}, [confirm, _loadSubscriptions]);
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<header className={styles.header}>
|
|
<h1>Subscription-Übersicht</h1>
|
|
<p className={styles.subtitle}>Alle Abonnements aller Mandanten</p>
|
|
<button
|
|
type="button"
|
|
className={styles.button}
|
|
onClick={() => navigate('/admin/billing')}
|
|
style={{ marginTop: 8 }}
|
|
>
|
|
← Zurück zur Billing-Verwaltung
|
|
</button>
|
|
</header>
|
|
|
|
{loading ? (
|
|
<div className={styles.noData}>Lade Subscriptions…</div>
|
|
) : (
|
|
<FormGeneratorTable
|
|
data={subscriptions}
|
|
columns={_COLUMNS}
|
|
apiEndpoint="/api/subscription/admin/all"
|
|
customActions={[
|
|
{
|
|
id: 'forceCancel',
|
|
label: 'Sofort kündigen',
|
|
icon: '✕',
|
|
variant: 'danger' as any,
|
|
onClick: (_id: string, row: any) => _handleForceCancel(row),
|
|
isVisible: (row: any) => !_TERMINAL_STATUSES.has(row._rawStatus),
|
|
},
|
|
]}
|
|
emptyMessage="Keine Subscriptions vorhanden."
|
|
/>
|
|
)}
|
|
|
|
<ConfirmDialog />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminSubscriptionsPage;
|