int #6
5 changed files with 48 additions and 195 deletions
|
|
@ -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 }
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// =============================================================================
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue