dead code cleanup + admin sessions UX: remove unused billingApi/useBilling functions, remove fetchAvailableFeatures ghost, add sessions link and per-user session action to AdminUsersPage, add URL param support and per-device revoke to AdminSessionsPage
All checks were successful
Deploy Nyla Frontend to Integration / deploy (push) Successful in 1m29s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-06-12 00:36:00 +02:00
parent 27abfde833
commit a01ebed7af
5 changed files with 48 additions and 195 deletions

View file

@ -38,29 +38,6 @@ export interface BillingTransaction {
userName?: string;
}
/** Pagination request for GET /api/billing/transactions with `pagination` JSON (table + grouping). */
export interface BillingTransactionsPaginationParams {
page?: number;
pageSize?: number;
sort?: Array<{ field: string; direction: 'asc' | 'desc' }>;
filters?: Record<string, any>;
search?: string;
viewKey?: string;
groupByLevels?: Array<{ field: string; nullLabel?: string; direction?: 'asc' | 'desc' }>;
}
export interface BillingTransactionsPaginatedResponse {
items: BillingTransaction[];
pagination?: {
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
};
groupLayout?: import('./connectionApi').GroupLayout;
appliedView?: { viewKey?: string; displayName?: string };
}
export interface BillingSettings {
id: string;
mandateId: string;
@ -159,46 +136,6 @@ export async function fetchBalanceForMandate(
});
}
/**
* Fetch transaction history (table UI: pagination, filters, sort, saved views, grouping).
* Endpoint: GET /api/billing/transactions?pagination=...
*/
export async function fetchTransactionsPaginated(
request: ApiRequestFunction,
params?: BillingTransactionsPaginationParams
): Promise<BillingTransactionsPaginatedResponse> {
const paginationObj: Record<string, unknown> = {};
if (params?.page !== undefined) paginationObj.page = params.page;
if (params?.pageSize !== undefined) paginationObj.pageSize = params.pageSize;
if (params?.sort?.length) paginationObj.sort = params.sort;
if (params?.filters && Object.keys(params.filters).length > 0) paginationObj.filters = params.filters;
if (params?.search) paginationObj.search = params.search;
if (params?.viewKey) paginationObj.viewKey = params.viewKey;
if (params?.groupByLevels !== undefined) paginationObj.groupByLevels = params.groupByLevels;
return await request({
url: '/api/billing/transactions',
method: 'get',
params: { pagination: JSON.stringify(paginationObj) },
});
}
/**
* Fetch transaction history (legacy array window)
* Endpoint: GET /api/billing/transactions
*/
export async function fetchTransactions(
request: ApiRequestFunction,
limit: number = 50,
offset: number = 0
): Promise<BillingTransaction[]> {
return await request({
url: '/api/billing/transactions',
method: 'get',
params: { limit, offset }
});
}
/**
* Fetch usage statistics for an explicit date range.
* Endpoint: GET /api/billing/statistics
@ -406,51 +343,7 @@ export async function fetchMandateViewTransactions(
});
}
// ============================================================================
// USER VIEW TYPES & API FUNCTIONS
// ============================================================================
export interface UserBalance {
accountId: string;
mandateId: string;
mandateName: string;
userId: string;
userName: string;
balance: number;
warningThreshold: number;
isWarning: boolean;
enabled: boolean;
}
export interface UserTransaction extends BillingTransaction {
userId?: string;
userName?: string;
}
/**
* Fetch user-level balances (RBAC-based)
* Endpoint: GET /api/billing/view/users/balances
*/
export async function fetchUserViewBalances(
request: ApiRequestFunction
): Promise<UserBalance[]> {
return await request({
url: '/api/billing/view/users/balances',
method: 'get'
});
}
/**
* Fetch user-level transactions (RBAC-based)
* Endpoint: GET /api/billing/view/users/transactions
*/
export async function fetchUserViewTransactions(
request: ApiRequestFunction,
limit: number = 100
): Promise<UserTransaction[]> {
return await request({
url: '/api/billing/view/users/transactions',
method: 'get',
params: { limit }
});
}

View file

@ -11,7 +11,6 @@ import api from '../api';
import type {
FeaturesMyResponse,
Mandate,
MandateFeature,
FeatureInstance,
InstancePermissions,
AccessLevel,
@ -156,22 +155,6 @@ export async function fetchMyFeatures(): Promise<FeaturesMyResponse> {
}
}
/**
* Lädt die verfügbaren Features (für Admin - Feature-Instanz erstellen)
*
* Endpoint: GET /api/features/available
*/
export async function fetchAvailableFeatures(): Promise<MandateFeature[]> {
if (USE_MOCK) {
return [
{ code: 'trustee', label: 'Treuhand', icon: 'briefcase', instances: [] },
];
}
const response = await api.get<MandateFeature[]>('/api/features/available');
return response.data;
}
// =============================================================================
// TYPE GUARDS
// =============================================================================

View file

@ -12,8 +12,6 @@ import { useApiRequest } from './useApi';
import {
fetchBalances,
fetchBalanceForMandate,
fetchTransactions,
fetchTransactionsPaginated,
fetchStatistics,
fetchAllowedProviders,
fetchSettingsAdmin,
@ -34,9 +32,7 @@ import {
type MandateUserSummary,
type StatisticsRangeRequest,
type BillingBucketSize,
type BillingTransactionsPaginationParams,
} from '../api/billingApi';
import type { GroupLayout } from '../api/connectionApi';
// Re-export types
export type {
@ -52,25 +48,13 @@ export type {
BillingBucketSize,
};
export type { TransactionType, ReferenceType, BillingTransactionsPaginationParams } from '../api/billingApi';
export type { TransactionType, ReferenceType } from '../api/billingApi';
/**
* Hook for user billing operations
*/
export function useBilling() {
const [balances, setBalances] = useState<BillingBalance[]>([]);
const [transactions, setTransactions] = useState<BillingTransaction[]>([]);
const [transactionsPagination, setTransactionsPagination] = useState<{
currentPage: number;
pageSize: number;
totalItems: number;
totalPages: number;
} | null>(null);
const [transactionsGroupLayout, setTransactionsGroupLayout] = useState<GroupLayout | null>(null);
const [transactionsAppliedView, setTransactionsAppliedView] = useState<{
viewKey?: string;
displayName?: string;
} | null>(null);
const [statistics, setStatistics] = useState<UsageReport | null>(null);
const [allowedProviders, setAllowedProviders] = useState<string[]>([]);
const { request, isLoading: loading, error } = useApiRequest();
@ -98,43 +82,6 @@ export function useBilling() {
}
}, [request]);
// Fetch transactions
const loadTransactions = useCallback(async (limit: number = 50, offset: number = 0) => {
try {
const data = await fetchTransactions(request, limit, offset);
setTransactions(Array.isArray(data) ? data : []);
setTransactionsPagination(null);
setTransactionsGroupLayout(null);
setTransactionsAppliedView(null);
return data;
} catch (err) {
console.error('Error loading transactions:', err);
setTransactions([]);
setTransactionsPagination(null);
setTransactionsGroupLayout(null);
setTransactionsAppliedView(null);
return [];
}
}, [request]);
const refetchTransactions = useCallback(async (params?: BillingTransactionsPaginationParams) => {
try {
const data = await fetchTransactionsPaginated(request, params);
setTransactions(Array.isArray(data.items) ? data.items : []);
setTransactionsPagination(data.pagination ?? null);
setTransactionsGroupLayout(data.groupLayout ?? null);
setTransactionsAppliedView(data.appliedView ?? null);
return data;
} catch (err) {
console.error('Error loading transactions:', err);
setTransactions([]);
setTransactionsPagination(null);
setTransactionsGroupLayout(null);
setTransactionsAppliedView(null);
return null;
}
}, [request]);
const loadStatistics = useCallback(async (range: StatisticsRangeRequest) => {
try {
const data = await fetchStatistics(request, range);
@ -168,18 +115,12 @@ export function useBilling() {
return {
balances,
transactions,
transactionsPagination,
transactionsGroupLayout,
transactionsAppliedView,
statistics,
allowedProviders,
loading,
error,
loadBalances,
loadBalanceForMandate,
loadTransactions,
refetchTransactions,
loadStatistics,
loadAllowedProviders,
refetch: loadBalances,

View file

@ -6,7 +6,8 @@
* Admin page for viewing and managing active sessions and trusted devices per user.
*/
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import api from '../../api';
import { StackLayout } from '../../components/Layout/StackLayout';
import { Panel } from '../../components/Layout/Panel';
@ -33,7 +34,8 @@ interface TrustedDeviceEntry {
export const AdminSessionsPage: React.FC = () => {
const { t } = useLanguage();
const [userId, setUserId] = useState('');
const [searchParams] = useSearchParams();
const [userId, setUserId] = useState(searchParams.get('userId') || '');
const [sessions, setSessions] = useState<SessionEntry[]>([]);
const [trustedDevices, setTrustedDevices] = useState<TrustedDeviceEntry[]>([]);
const [loading, setLoading] = useState(false);
@ -86,6 +88,21 @@ export const AdminSessionsPage: React.FC = () => {
}
}, [userId]);
const revokeTrustedDevice = useCallback(async (deviceId: string) => {
try {
await api.delete(`/api/admin/trusted-devices/${deviceId}`);
setTrustedDevices(prev => prev.filter(d => d.id !== deviceId));
} catch (e: any) {
setError(e.response?.data?.detail || 'Failed to revoke device');
}
}, []);
useEffect(() => {
if (searchParams.get('userId') && userId) {
loadData();
}
}, []);
const formatTimestamp = (ts: number) => {
if (!ts) return '-';
return new Date(ts * 1000).toLocaleString();
@ -175,21 +192,27 @@ export const AdminSessionsPage: React.FC = () => {
<th style={{ textAlign: 'left', padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}>IP</th>
<th style={{ textAlign: 'left', padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}>Trusted Until</th>
<th style={{ textAlign: 'left', padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}>Status</th>
<th style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color)' }}></th>
</tr>
</thead>
<tbody>
{trustedDevices.map((d, i) => (
<tr key={i}>
{trustedDevices.map((d) => (
<tr key={d.id}>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>
<span title={d.userAgent || ''}>{d.id}</span>
<span title={d.userAgent || ''}><code>{d.id.slice(0, 8)}...</code></span>
</td>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>{d.ipAddress || '-'}</td>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>{formatTimestamp(d.trustedUntil)}</td>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)' }}>
<span style={{ color: d.isExpired ? 'var(--color-error)' : 'var(--color-success)' }}>
{d.isExpired ? 'Expired' : 'Active'}
{d.isExpired ? t('Abgelaufen') : t('Aktiv')}
</span>
</td>
<td style={{ padding: '0.5rem', borderBottom: '1px solid var(--border-color-light)', textAlign: 'right' }}>
<button onClick={() => revokeTrustedDevice(d.id)} className={styles.dangerButton} style={{ fontSize: '0.75rem' }}>
<FaTrash />
</button>
</td>
</tr>
))}
</tbody>

View file

@ -11,7 +11,7 @@ import { useNavigate } from 'react-router-dom';
import { useOrgUsers, useUserOperations } from '../../hooks/useUsers';
import { FormGeneratorTable } from '../../components/FormGenerator/FormGeneratorTable';
import { FormGeneratorForm } from '../../components/FormGenerator/FormGeneratorForm';
import { FaPlus, FaSync, FaKey, FaEnvelopeOpenText, FaUserShield } from 'react-icons/fa';
import { FaPlus, FaSync, FaKey, FaEnvelopeOpenText, FaUserShield, FaDesktop } from 'react-icons/fa';
import { StackLayout } from '../../components/Layout/StackLayout';
import { Panel } from '../../components/Layout/Panel';
import styles from './Admin.module.css';
@ -211,6 +211,13 @@ export const AdminUsersPage: React.FC = () => {
>
<FaEnvelopeOpenText /> {t('Einladungen')}
</button>
<button
type="button"
className={styles.secondaryButton}
onClick={() => navigate('/admin/sessions')}
>
<FaDesktop /> {t('Sessions')}
</button>
<button
className={styles.secondaryButton}
onClick={() => _tableRefetch()}
@ -252,15 +259,21 @@ export const AdminUsersPage: React.FC = () => {
title: t('Löschen'),
}] : []),
]}
customActions={canUpdate ? [
{
customActions={[
...(canUpdate ? [{
id: 'sendPasswordLink',
icon: <FaKey />,
onClick: handleSendPassword,
title: t('Passwort-Link senden'),
loading: (row: User) => sendingPasswordLinkState.has(row.id),
}
] : []}
}] : []),
{
id: 'viewSessions',
icon: <FaDesktop />,
onClick: (row: User) => navigate(`/admin/sessions?userId=${row.id}`),
title: t('Sessions anzeigen'),
},
]}
onDelete={handleDeleteUser}
hookData={{
refetch: _tableRefetch,