ui-nyla/src/pages/billing/AdminSubscriptionsPage.tsx
2026-04-12 14:05:01 +02:00

84 lines
3.6 KiB
TypeScript

import React, { useCallback } from 'react';
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
import { useAdminSubscriptions } from '../../hooks/useAdminSubscriptions';
import { useConfirm } from '../../hooks/useConfirm';
import api from '../../api';
import styles from './Billing.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
const _TERMINAL_STATUSES = new Set(['EXPIRED']);
function _getColumns(t: (key: string) => string): ColumnConfig[] {
return [
{ key: 'mandateName', label: t('Mandant'), type: 'text', sortable: true, filterable: true, width: 180 },
{ key: 'planTitle', label: t('Plan'), type: 'text', sortable: true, filterable: true, width: 180 },
{ key: 'status', label: t('Status'), type: 'text', sortable: true, filterable: true, width: 110 },
{ key: 'recurring', label: t('Wiederkehrend'), type: 'boolean', sortable: true, filterable: true, width: 120 },
{ key: 'activeUsers', label: t('Benutzer'), type: 'number', sortable: true, width: 70 },
{ key: 'activeInstances', label: t('Module'), type: 'number', sortable: true, width: 90 },
{ key: 'monthlyRevenueCHF', label: t('Umsatz pro Monat'), type: 'number', sortable: true, width: 140 },
{ key: 'startedAt', label: t('Gestartet'), type: 'date', sortable: true, filterable: true, width: 130 },
{ key: 'currentPeriodEnd', label: t('Periodenende'), type: 'date', sortable: true, filterable: true, width: 130 },
{ key: 'snapshotPricePerUserCHF', label: t('Preis pro Benutzer'), type: 'number', sortable: true, width: 100 },
{ key: 'snapshotPricePerInstanceCHF', label: t('Preis pro Modul'), type: 'number', sortable: true, width: 110 },
];
}
const AdminSubscriptionsPage: React.FC = () => {
const { t } = useLanguage();
const { confirm, ConfirmDialog } = useConfirm();
const { data: subscriptions, pagination, loading, refetch } = useAdminSubscriptions();
const _handleForceCancel = useCallback(async (row: any) => {
const ok = await confirm(
t('Subscription «{plan}» für Mandant «{mandate}» sofort kündigen? Dies wird auch auf Stripe sofort storniert.', { plan: row.planTitle, mandate: row.mandateName }),
{ confirmLabel: t('Sofort kündigen'), cancelLabel: t('Abbrechen'), variant: 'danger' },
);
if (!ok) return;
try {
await api.post('/api/subscription/force-cancel', { subscriptionId: row.id });
await refetch();
} catch (err) {
console.error('Force cancel failed:', err);
}
}, [confirm, refetch]);
return (
<div className={styles.billingDashboard} style={{ minHeight: 0 }}>
<header className={styles.pageHeader} style={{ flexShrink: 0 }}>
<h1>{t('Abonnementübersicht')}</h1>
<p className={styles.subtitle}>{t('Alle Abonnements aller Mandanten')}</p>
</header>
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
<FormGeneratorTable
data={subscriptions}
columns={_getColumns(t)}
apiEndpoint="/api/subscription/admin/all"
loading={loading}
pagination={true}
pageSize={50}
selectable={false}
hookData={{ refetch, pagination }}
customActions={[
{
id: 'forceCancel',
title: t('Sofort stornieren'),
icon: '✕',
onClick: (row: any) => _handleForceCancel(row),
visible: (row: any) => !_TERMINAL_STATUSES.has(row._rawStatus),
},
]}
emptyMessage={t('Keine Abonnements vorhanden')}
/>
</div>
<ConfirmDialog />
</div>
);
};
export default AdminSubscriptionsPage;