- {/* "All" option to clear filter */}
-
clearFilter(column.key)}
- >
- ({t('formgen.filter.all', 'All')})
-
- {/* Filter values - loaded from backend or static filterOptions */}
- {filterValuesLoading[column.key] ? (
-
- {t('formgen.filter.loading', 'Lade Filterwerte...')}
-
- ) : (() => {
- const allValues = getUniqueValuesForColumn(column.key);
- const isExpanded = expandedFilterColumns.has(column.key);
- const displayLimit = isExpanded ? allValues.length : 100;
- const visibleValues = allValues.slice(0, displayLimit);
- const remaining = allValues.length - displayLimit;
+ {(() => {
+ const colType = column.type || 'text';
+ const isBool = isCheckboxType(colType as AttributeType);
+ const isDate = isDateTimeType(colType as AttributeType);
+
+ if (isBool) {
+ const currentVal = filters[column.key];
+ return (
+ <>
+
clearFilter(column.key)}
+ >
+ ({t('formgen.filter.all', 'Alle')})
+
+
handleFilter(column.key, 'true')}
+ >
+ {t('formgen.filter.yes', 'Ja')}
+
+
handleFilter(column.key, 'false')}
+ >
+ {t('formgen.filter.no', 'Nein')}
+
+ >
+ );
+ }
+
+ if (isDate) {
+ const rangeVal = (typeof filters[column.key] === 'object' && filters[column.key]?.value) || {};
+ return (
+
+
clearFilter(column.key)}
+ >
+ ({t('formgen.filter.all', 'Alle')})
+
+
+
{
+ const from = e.target.value;
+ const to = rangeVal.to || '';
+ if (!from && !to) {
+ clearFilter(column.key);
+ } else {
+ handleFilter(column.key, { operator: 'between', value: { from, to } }, true);
+ }
+ }}
+ />
+
+
{
+ const to = e.target.value;
+ const from = rangeVal.from || '';
+ if (!from && !to) {
+ clearFilter(column.key);
+ } else {
+ handleFilter(column.key, { operator: 'between', value: { from, to } }, true);
+ }
+ }}
+ />
+
+ );
+ }
return (
<>
- {visibleValues.map(value => (
-
handleFilter(column.key, value)}
- title={value}
- >
- {value.length > 30 ? value.substring(0, 30) + '...' : value}
-
- ))}
- {remaining > 0 && (
-
_toggleFilterExpand(column.key)}
- style={{ cursor: 'pointer', color: 'var(--primary-blue, #003d7a)' }}
- >
- + {remaining} {t('formgen.filter.more', 'weitere anzeigen')}
-
- )}
- {isExpanded && allValues.length > 100 && (
-
_toggleFilterExpand(column.key)}
- style={{ cursor: 'pointer', color: 'var(--primary-blue, #003d7a)' }}
- >
- {t('formgen.filter.less', 'Weniger anzeigen')}
+
clearFilter(column.key)}
+ >
+ ({t('formgen.filter.all', 'Alle')})
+
+ {filterValuesLoading[column.key] ? (
+
+ {t('formgen.filter.loading', 'Lade Filterwerte...')}
+ ) : (
+
handleFilter(column.key, value)}
+ />
)}
>
);
diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx
index 21a8b40..31922b5 100644
--- a/src/config/pageRegistry.tsx
+++ b/src/config/pageRegistry.tsx
@@ -22,6 +22,7 @@ import {
FaListAlt, FaCogs, FaChartLine, FaFileAlt, FaUserShield, FaDatabase,
FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt, FaClock,
FaHeadset, FaVideo, FaHatWizard, FaStore, FaUserTie, FaClipboardList,
+ FaFileContract,
} from 'react-icons/fa';
// =============================================================================
@@ -66,6 +67,7 @@ export const PAGE_ICONS: Record = {
'page.admin.user-access-overview': ,
'page.admin.userAccessOverview': ,
'page.admin.billing': ,
+ 'page.admin.subscriptions': ,
'page.admin.automationEvents': ,
'page.admin.automation-events': ,
'page.admin.logs': ,
diff --git a/src/hooks/useAdminSubscriptions.ts b/src/hooks/useAdminSubscriptions.ts
new file mode 100644
index 0000000..02acd1f
--- /dev/null
+++ b/src/hooks/useAdminSubscriptions.ts
@@ -0,0 +1,84 @@
+import { useState, useEffect, useCallback } from 'react';
+import { useApiRequest } from './useApi';
+
+interface PaginationParams {
+ page?: number;
+ pageSize?: number;
+ sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
+ filters?: Record;
+ search?: string;
+}
+
+interface PaginationState {
+ currentPage: number;
+ pageSize: number;
+ totalItems: number;
+ totalPages: number;
+}
+
+const _STATUS_LABELS: Record = {
+ PENDING: 'Ausstehend',
+ SCHEDULED: 'Geplant',
+ TRIALING: 'Testphase',
+ ACTIVE: 'Aktiv',
+ PAST_DUE: 'Überfällig',
+ EXPIRED: 'Abgelaufen',
+};
+
+export function useAdminSubscriptions() {
+ const [subscriptions, setSubscriptions] = useState([]);
+ const [pagination, setPagination] = useState(null);
+ const { request, isLoading: loading, error } = useApiRequest();
+
+ const refetch = useCallback(async (params?: PaginationParams) => {
+ try {
+ const requestParams: Record = {};
+
+ if (params) {
+ const paginationObj: any = {};
+ if (params.page !== undefined) paginationObj.page = params.page;
+ if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
+ if (params.sort) paginationObj.sort = params.sort;
+ if (params.filters) paginationObj.filters = params.filters;
+ if (params.search) paginationObj.search = params.search;
+
+ if (Object.keys(paginationObj).length > 0) {
+ requestParams.pagination = JSON.stringify(paginationObj);
+ }
+ }
+
+ const data = await request({
+ url: '/api/subscription/admin/all',
+ method: 'get',
+ params: requestParams,
+ });
+
+ if (data && typeof data === 'object' && 'items' in data) {
+ const items = Array.isArray(data.items) ? data.items : [];
+ setSubscriptions(items.map(_enrichRow));
+ if (data.pagination) {
+ setPagination(data.pagination);
+ }
+ } else {
+ const items = Array.isArray(data) ? data : [];
+ setSubscriptions(items.map(_enrichRow));
+ setPagination(null);
+ }
+ } catch {
+ setSubscriptions([]);
+ setPagination(null);
+ }
+ }, [request]);
+
+ useEffect(() => { refetch(); }, [refetch]);
+
+ return { data: subscriptions, pagination, loading, error, refetch };
+}
+
+function _enrichRow(row: any): any {
+ return {
+ ...row,
+ _rawStatus: row.status,
+ status: _STATUS_LABELS[row.status] || row.status,
+ };
+}
diff --git a/src/hooks/useAutomations.ts b/src/hooks/useAutomations.ts
index 69a6cbf..28897ac 100644
--- a/src/hooks/useAutomations.ts
+++ b/src/hooks/useAutomations.ts
@@ -472,22 +472,30 @@ export function useAutomationOperations() {
export function useAutomationTemplates() {
const [templates, setTemplates] = useState([]);
const [attributes, setAttributes] = useState([]);
+ const [pagination, setPagination] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const { request } = useApiRequest();
const { checkPermission } = usePermissions();
const [permissions, setPermissions] = useState(null);
- const fetchTemplates = useCallback(async () => {
+ const fetchTemplates = useCallback(async (params?: any) => {
setLoading(true);
setError(null);
try {
- const data = await fetchTemplatesApi(request);
- setTemplates(data);
+ const data = await fetchTemplatesApi(request, params);
+ if (data && typeof data === 'object' && 'items' in data) {
+ setTemplates(Array.isArray(data.items) ? data.items : []);
+ if (data.pagination) setPagination(data.pagination);
+ } else {
+ setTemplates(Array.isArray(data) ? data : []);
+ setPagination(null);
+ }
} catch (e: any) {
console.error('Error fetching templates:', e);
setError(e.message || 'Failed to fetch templates');
setTemplates([]);
+ setPagination(null);
} finally {
setLoading(false);
}
@@ -555,11 +563,12 @@ export function useAutomationTemplates() {
return {
templates,
- data: templates, // Alias for FormGenerator compatibility
+ data: templates,
attributes,
loading,
error,
permissions,
+ pagination,
refetch,
fetchTemplates,
fetchAttributes,
diff --git a/src/pages/admin/AdminAutomationEventsPage.tsx b/src/pages/admin/AdminAutomationEventsPage.tsx
index dee1558..2433199 100644
--- a/src/pages/admin/AdminAutomationEventsPage.tsx
+++ b/src/pages/admin/AdminAutomationEventsPage.tsx
@@ -41,18 +41,37 @@ const _formatNextRun = (nextRunTime: string | null): string => {
export const AdminAutomationEventsPage: React.FC = () => {
const [events, setEvents] = useState([]);
+ const [pagination, setPagination] = useState(null);
const [loading, setLoading] = useState(true);
const [syncing, setSyncing] = useState(false);
const [error, setError] = useState(null);
const [syncResult, setSyncResult] = useState(null);
- const _fetchEvents = useCallback(async () => {
+ const _fetchEvents = useCallback(async (params?: any) => {
try {
setLoading(true);
setError(null);
- const response = await api.get('/api/admin/automation-events');
- // Map eventId to id for FormGeneratorTable compatibility
- setEvents(response.data.map((e: any) => ({ ...e, id: e.eventId })));
+ const requestParams: Record = {};
+ if (params && typeof params === 'object') {
+ const paginationObj: any = {};
+ if (params.page !== undefined) paginationObj.page = params.page;
+ if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
+ if (params.sort) paginationObj.sort = params.sort;
+ if (params.filters) paginationObj.filters = params.filters;
+ if (params.search) paginationObj.search = params.search;
+ if (Object.keys(paginationObj).length > 0) {
+ requestParams.pagination = JSON.stringify(paginationObj);
+ }
+ }
+ const response = await api.get('/api/admin/automation-events', { params: requestParams });
+ const data = response.data;
+ if (data && typeof data === 'object' && 'items' in data) {
+ setEvents((data.items || []).map((e: any) => ({ ...e, id: e.eventId })));
+ if (data.pagination) setPagination(data.pagination);
+ } else {
+ setEvents((Array.isArray(data) ? data : []).map((e: any) => ({ ...e, id: e.eventId })));
+ setPagination(null);
+ }
} catch (err: any) {
setError(err.response?.data?.detail || 'Fehler beim Laden der Events');
} finally {
@@ -196,6 +215,7 @@ export const AdminAutomationEventsPage: React.FC = () => {
{
hookData={{
handleDelete: _handleDelete,
refetch: _fetchEvents,
+ pagination,
}}
emptyMessage="Keine Automationen gefunden. Nutzen Sie 'Sync All', um Automationen zu synchronisieren."
/>
diff --git a/src/pages/admin/AdminFeatureRolesPage.tsx b/src/pages/admin/AdminFeatureRolesPage.tsx
index da1697d..cfd035c 100644
--- a/src/pages/admin/AdminFeatureRolesPage.tsx
+++ b/src/pages/admin/AdminFeatureRolesPage.tsx
@@ -73,8 +73,10 @@ export const AdminFeatureRolesPage: React.FC = () => {
}, []);
+ const [pagination, setPagination] = useState(null);
+
// Load roles when feature changes
- const fetchRoles = useCallback(async () => {
+ const fetchRoles = useCallback(async (params?: any) => {
if (!selectedFeatureCode) {
setRoles([]);
return;
@@ -83,15 +85,32 @@ export const AdminFeatureRolesPage: React.FC = () => {
setLoading(true);
setError(null);
try {
- const response = await api.get(`/api/features/templates/roles`, {
- params: { featureCode: selectedFeatureCode }
- });
- const roleList = response.data || [];
- setRoles(Array.isArray(roleList) ? roleList : []);
+ const requestParams: Record = { featureCode: selectedFeatureCode };
+ if (params && typeof params === 'object') {
+ const paginationObj: any = {};
+ if (params.page !== undefined) paginationObj.page = params.page;
+ if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
+ if (params.sort) paginationObj.sort = params.sort;
+ if (params.filters) paginationObj.filters = params.filters;
+ if (params.search) paginationObj.search = params.search;
+ if (Object.keys(paginationObj).length > 0) {
+ requestParams.pagination = JSON.stringify(paginationObj);
+ }
+ }
+ const response = await api.get(`/api/features/templates/roles`, { params: requestParams });
+ const data = response.data;
+ if (data && typeof data === 'object' && 'items' in data) {
+ setRoles(Array.isArray(data.items) ? data.items : []);
+ if (data.pagination) setPagination(data.pagination);
+ } else {
+ setRoles(Array.isArray(data) ? data : []);
+ setPagination(null);
+ }
} catch (err: any) {
console.error('Error loading feature roles:', err);
setError('Fehler beim Laden der Feature-Rollen');
setRoles([]);
+ setPagination(null);
} finally {
setLoading(false);
}
@@ -383,6 +402,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
onDelete={handleDeleteRole}
hookData={{
refetch: fetchRoles,
+ pagination,
handleDelete: handleDeleteRole,
}}
emptyMessage="Keine Feature-Rollen gefunden"
diff --git a/src/pages/admin/AdminInvitationsPage.tsx b/src/pages/admin/AdminInvitationsPage.tsx
index 6105266..8f42a62 100644
--- a/src/pages/admin/AdminInvitationsPage.tsx
+++ b/src/pages/admin/AdminInvitationsPage.tsx
@@ -379,7 +379,7 @@ export const AdminInvitationsPage: React.FC = () => {
]}
hookData={{
handleDelete: handleDeleteInvitation,
- refetch: () => fetchInvitations(selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }),
+ refetch: (params?: any) => fetchInvitations(params || selectedMandateId, { includeExpired: showExpired, includeUsed: showUsed }),
pagination,
}}
emptyMessage="Keine Einladungen gefunden"
diff --git a/src/pages/basedata/FilesPage.tsx b/src/pages/basedata/FilesPage.tsx
index bec6348..57eb64e 100644
--- a/src/pages/basedata/FilesPage.tsx
+++ b/src/pages/basedata/FilesPage.tsx
@@ -49,6 +49,7 @@ export const FilesPage: React.FC = () => {
loading,
error,
refetch,
+ pagination,
fetchFileById,
updateFileOptimistically,
} = useUserFiles();
@@ -479,6 +480,7 @@ export const FilesPage: React.FC = () => {
onDeleteMultiple={handleDeleteMultiple}
hookData={{
refetch: _tableRefetch,
+ pagination,
permissions,
handleDelete: handleFileDelete,
handleInlineUpdate,
diff --git a/src/pages/basedata/PromptsPage.tsx b/src/pages/basedata/PromptsPage.tsx
index f3d4435..6a7e407 100644
--- a/src/pages/basedata/PromptsPage.tsx
+++ b/src/pages/basedata/PromptsPage.tsx
@@ -168,7 +168,7 @@ export const PromptsPage: React.FC = () => {
}
return (
-
+
Prompts
@@ -194,68 +194,45 @@ export const PromptsPage: React.FC = () => {
- {loading && (!prompts || prompts.length === 0) ? (
-
- ) : !prompts || prompts.length === 0 ? (
-
-
-
Keine Prompts vorhanden
-
- Erstellen Sie einen neuen Prompt, um loszulegen.
-
- {canCreate && (
-
- )}
-
- ) : (
-
deletingPrompts.has(row.id),
- }] : []),
- ]}
- onDelete={handleDelete}
- hookData={{
- refetch,
- permissions,
- pagination,
- handleDelete: handlePromptDelete,
- handleInlineUpdate,
- updateOptimistically,
- }}
- emptyMessage="Keine Prompts gefunden"
- />
- )}
+ deletingPrompts.has(row.id),
+ }] : []),
+ ]}
+ onDelete={handleDelete}
+ hookData={{
+ refetch,
+ permissions,
+ pagination,
+ handleDelete: handlePromptDelete,
+ handleInlineUpdate,
+ updateOptimistically,
+ }}
+ emptyMessage="Keine Prompts gefunden"
+ />
{/* Create Modal */}
diff --git a/src/pages/billing/AdminSubscriptionsPage.tsx b/src/pages/billing/AdminSubscriptionsPage.tsx
index 36450d0..7c16320 100644
--- a/src/pages/billing/AdminSubscriptionsPage.tsx
+++ b/src/pages/billing/AdminSubscriptionsPage.tsx
@@ -1,62 +1,31 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
-import { useApiRequest } from '../../hooks/useApi';
+import { useAdminSubscriptions } from '../../hooks/useAdminSubscriptions';
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
= {
- 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 },
+ { key: 'mandateName', label: 'Mandant', type: 'text', sortable: true, filterable: true, width: 180 },
+ { key: 'planTitle', label: 'Plan', type: 'text', sortable: true, filterable: true, width: 180 },
+ { key: 'status', label: 'Status', type: 'text', sortable: true, filterable: true, width: 110 },
+ { key: 'recurring', label: 'Wiederkehrend', type: 'boolean', sortable: true, filterable: true, width: 120 },
+ { key: 'activeUsers', label: 'User', type: 'number', sortable: true, width: 70 },
+ { key: 'activeInstances', label: 'Instanzen', type: 'number', sortable: true, width: 90 },
+ { key: 'monthlyRevenueCHF', label: 'Revenue/Mt (CHF)', type: 'number', sortable: true, width: 140 },
+ { key: 'startedAt', label: 'Gestartet', type: 'date', sortable: true, filterable: true, width: 130 },
+ { key: 'currentPeriodEnd', label: 'Periodenende', type: 'date', sortable: true, filterable: true, width: 130 },
+ { key: 'snapshotPricePerUserCHF', label: 'Preis/User', type: 'number', sortable: true, width: 100 },
+ { key: 'snapshotPricePerInstanceCHF', label: 'Preis/Instanz', type: 'number', sortable: true, width: 110 },
];
const AdminSubscriptionsPage: React.FC = () => {
const navigate = useNavigate();
- const { request } = useApiRequest();
const { confirm, ConfirmDialog } = useConfirm();
- const [subscriptions, setSubscriptions] = useState([]);
- 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 { data: subscriptions, pagination, loading, refetch } = useAdminSubscriptions();
const _handleForceCancel = useCallback(async (row: any) => {
const ok = await confirm(
@@ -67,15 +36,15 @@ const AdminSubscriptionsPage: React.FC = () => {
try {
await api.post('/api/subscription/force-cancel', { subscriptionId: row.id });
- await _loadSubscriptions();
+ await refetch();
} catch (err) {
console.error('Force cancel failed:', err);
}
- }, [confirm, _loadSubscriptions]);
+ }, [confirm, refetch]);
return (
-