diff --git a/src/App.tsx b/src/App.tsx
index 0a7591b..41c6028 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -46,6 +46,9 @@ import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandat
// Basedata Pages (global)
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
+// Billing Pages
+import { BillingDashboard, BillingTransactions, BillingAdmin } from './pages/billing';
+
function App() {
// Load saved theme preference and set app name on app mount
useEffect(() => {
@@ -110,6 +113,14 @@ function App() {
} />
+ {/* ============================================== */}
+ {/* BILLING ROUTES */}
+ {/* ============================================== */}
+
+ } />
+ } />
+
+
{/* ============================================== */}
{/* FEATURE-INSTANZ ROUTES */}
{/* /mandates/:mandateId/:featureCode/:instanceId */}
@@ -165,6 +176,7 @@ function App() {
} />
} />
} />
+ } />
diff --git a/src/api/automationApi.ts b/src/api/automationApi.ts
index b6f0628..296933b 100644
--- a/src/api/automationApi.ts
+++ b/src/api/automationApi.ts
@@ -17,6 +17,7 @@ export interface Automation {
lastExecution?: number;
nextExecution?: number;
executionLogs?: AutomationLog[];
+ allowedProviders?: string[];
_createdAt?: number;
_updatedAt?: number;
_createdByUserName?: string;
diff --git a/src/api/billingApi.ts b/src/api/billingApi.ts
new file mode 100644
index 0000000..c43c40f
--- /dev/null
+++ b/src/api/billingApi.ts
@@ -0,0 +1,254 @@
+import { ApiRequestOptions } from '../hooks/useApi';
+
+// ============================================================================
+// TYPES & INTERFACES
+// ============================================================================
+
+export type BillingModel = 'PREPAY_MANDATE' | 'PREPAY_USER' | 'CREDIT_POSTPAY' | 'UNLIMITED';
+export type TransactionType = 'CREDIT' | 'DEBIT' | 'ADJUSTMENT';
+export type ReferenceType = 'WORKFLOW' | 'PAYMENT' | 'ADMIN' | 'SYSTEM';
+
+export interface BillingAddress {
+ company: string;
+ street: string;
+ zip: string;
+ city: string;
+ country: string;
+ vatNumber?: string;
+}
+
+export interface BillingBalance {
+ mandateId: string;
+ mandateName: string;
+ billingModel: BillingModel;
+ balance: number;
+ currency: string;
+ warningThreshold: number;
+ isWarning: boolean;
+ creditLimit?: number;
+}
+
+export interface BillingTransaction {
+ id: string;
+ accountId: string;
+ transactionType: TransactionType;
+ amount: number;
+ description: string;
+ referenceType?: ReferenceType;
+ workflowId?: string;
+ featureCode?: string;
+ aicoreProvider?: string;
+ createdAt?: string;
+}
+
+export interface BillingSettings {
+ id: string;
+ mandateId: string;
+ billingModel: BillingModel;
+ defaultUserCredit: number;
+ warningThresholdPercent: number;
+ blockOnZeroBalance: boolean;
+ notifyOnWarning: boolean;
+ notifyEmails: string[];
+ billingAddress?: BillingAddress;
+}
+
+export interface BillingSettingsUpdate {
+ billingModel?: BillingModel;
+ defaultUserCredit?: number;
+ warningThresholdPercent?: number;
+ blockOnZeroBalance?: boolean;
+ notifyOnWarning?: boolean;
+ notifyEmails?: string[];
+ billingAddress?: BillingAddress;
+}
+
+export interface UsageReport {
+ period: string;
+ totalCost: number;
+ transactionCount: number;
+ costByProvider: Record;
+ costByFeature: Record;
+}
+
+export interface AccountSummary {
+ id: string;
+ mandateId: string;
+ userId?: string;
+ accountType: string;
+ balance: number;
+ creditLimit?: number;
+ warningThreshold: number;
+ enabled: boolean;
+}
+
+export interface CreditAddRequest {
+ userId?: string;
+ amount: number;
+ description?: string;
+}
+
+// Type for the request function passed to API functions
+export type ApiRequestFunction = (options: ApiRequestOptions) => Promise;
+
+// ============================================================================
+// USER API FUNCTIONS
+// ============================================================================
+
+/**
+ * Fetch billing balances for all mandates the user belongs to
+ * Endpoint: GET /api/billing/balance
+ */
+export async function fetchBalances(
+ request: ApiRequestFunction
+): Promise {
+ return await request({
+ url: '/api/billing/balance',
+ method: 'get'
+ });
+}
+
+/**
+ * Fetch billing balance for a specific mandate
+ * Endpoint: GET /api/billing/balance/{mandateId}
+ */
+export async function fetchBalanceForMandate(
+ request: ApiRequestFunction,
+ mandateId: string
+): Promise {
+ return await request({
+ url: `/api/billing/balance/${mandateId}`,
+ method: 'get'
+ });
+}
+
+/**
+ * Fetch transaction history
+ * Endpoint: GET /api/billing/transactions
+ */
+export async function fetchTransactions(
+ request: ApiRequestFunction,
+ limit: number = 50,
+ offset: number = 0
+): Promise {
+ return await request({
+ url: '/api/billing/transactions',
+ method: 'get',
+ params: { limit, offset }
+ });
+}
+
+/**
+ * Fetch usage statistics
+ * Endpoint: GET /api/billing/statistics/{period}
+ */
+export async function fetchStatistics(
+ request: ApiRequestFunction,
+ period: 'day' | 'month' | 'year',
+ year: number,
+ month?: number
+): Promise {
+ const params: Record = { year };
+ if (month !== undefined) {
+ params.month = month;
+ }
+
+ return await request({
+ url: `/api/billing/statistics/${period}`,
+ method: 'get',
+ params
+ });
+}
+
+/**
+ * Fetch allowed AICore providers
+ * Endpoint: GET /api/billing/providers
+ */
+export async function fetchAllowedProviders(
+ request: ApiRequestFunction
+): Promise {
+ return await request({
+ url: '/api/billing/providers',
+ method: 'get'
+ });
+}
+
+// ============================================================================
+// ADMIN API FUNCTIONS
+// ============================================================================
+
+/**
+ * Fetch billing settings for a mandate (Admin)
+ * Endpoint: GET /api/billing/admin/settings/{mandateId}
+ */
+export async function fetchSettingsAdmin(
+ request: ApiRequestFunction,
+ mandateId: string
+): Promise {
+ return await request({
+ url: `/api/billing/admin/settings/${mandateId}`,
+ method: 'get'
+ });
+}
+
+/**
+ * Create or update billing settings (Admin)
+ * Endpoint: POST /api/billing/admin/settings/{mandateId}
+ */
+export async function updateSettingsAdmin(
+ request: ApiRequestFunction,
+ mandateId: string,
+ settings: BillingSettingsUpdate
+): Promise {
+ return await request({
+ url: `/api/billing/admin/settings/${mandateId}`,
+ method: 'post',
+ data: settings
+ });
+}
+
+/**
+ * Add credit to an account (Admin)
+ * Endpoint: POST /api/billing/admin/credit/{mandateId}
+ */
+export async function addCreditAdmin(
+ request: ApiRequestFunction,
+ mandateId: string,
+ creditRequest: CreditAddRequest
+): Promise {
+ return await request({
+ url: `/api/billing/admin/credit/${mandateId}`,
+ method: 'post',
+ data: creditRequest
+ });
+}
+
+/**
+ * Fetch all accounts for a mandate (Admin)
+ * Endpoint: GET /api/billing/admin/accounts/{mandateId}
+ */
+export async function fetchAccountsAdmin(
+ request: ApiRequestFunction,
+ mandateId: string
+): Promise {
+ return await request({
+ url: `/api/billing/admin/accounts/${mandateId}`,
+ method: 'get'
+ });
+}
+
+/**
+ * Fetch all transactions for a mandate (Admin)
+ * Endpoint: GET /api/billing/admin/transactions/{mandateId}
+ */
+export async function fetchTransactionsAdmin(
+ request: ApiRequestFunction,
+ mandateId: string,
+ limit: number = 100
+): Promise {
+ return await request({
+ url: `/api/billing/admin/transactions/${mandateId}`,
+ method: 'get',
+ params: { limit }
+ });
+}
diff --git a/src/components/AutomationEditor/AutomationEditor.tsx b/src/components/AutomationEditor/AutomationEditor.tsx
index 677daea..255e404 100644
--- a/src/components/AutomationEditor/AutomationEditor.tsx
+++ b/src/components/AutomationEditor/AutomationEditor.tsx
@@ -13,6 +13,7 @@
import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react';
import { FaTimes, FaSave, FaChevronLeft, FaChevronRight, FaRocket, FaFileAlt, FaCode, FaExclamationTriangle, FaMagic, FaFolder, FaFolderOpen, FaArrowUp, FaSpinner } from 'react-icons/fa';
import { ActionsPanel } from '../ActionsPanel';
+import { ProviderMultiSelect } from '../ProviderSelector';
import { useToast } from '../../contexts/ToastContext';
import { useLanguage } from '../../providers/language/LanguageContext';
import { useWorkflowActions } from '../../hooks/useAutomations';
@@ -368,6 +369,7 @@ export const AutomationEditor: React.FC = ({
const [label, setLabel] = useState('');
const [schedule, setSchedule] = useState('0 22 * * *');
const [active, setActive] = useState(false);
+ const [allowedProviders, setAllowedProviders] = useState([]);
// Template multilingual fields
const [labelMulti, setLabelMulti] = useState({ en: '', de: '' });
@@ -530,6 +532,7 @@ export const AutomationEditor: React.FC = ({
setLabel(def.label || '');
setSchedule(def.schedule || '0 22 * * *');
setActive(def.active ?? false);
+ setAllowedProviders(def.allowedProviders || []);
}
// Extract template JSON
@@ -684,7 +687,8 @@ export const AutomationEditor: React.FC = ({
schedule,
active,
template: templateJson,
- placeholders
+ placeholders,
+ allowedProviders
};
}
@@ -700,7 +704,7 @@ export const AutomationEditor: React.FC = ({
} finally {
setIsSaving(false);
}
- }, [label, schedule, active, labelMulti, overviewMulti, templateJson, placeholders, mode, initialData, onSave, showError]);
+ }, [label, schedule, active, allowedProviders, labelMulti, overviewMulti, templateJson, placeholders, mode, initialData, onSave, showError]);
// Computed values
const editorTitle = title || (mode === 'template'
@@ -831,6 +835,18 @@ export const AutomationEditor: React.FC = ({
Automatisierung ist aktiv und wird planmässig ausgeführt
+
+ {/* Allowed AI Providers */}
+
+
+
+ Beschränkt die Automation auf bestimmte AI-Provider. Leer = alle erlaubt.
+
+
)}
diff --git a/src/components/ProviderSelector/ProviderSelector.module.css b/src/components/ProviderSelector/ProviderSelector.module.css
new file mode 100644
index 0000000..465ed1e
--- /dev/null
+++ b/src/components/ProviderSelector/ProviderSelector.module.css
@@ -0,0 +1,161 @@
+/* Provider Selector Component Styles */
+
+/* ============================================================================
+ SINGLE SELECT
+ ============================================================================ */
+
+.providerSelect {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xs, 4px);
+}
+
+.label {
+ font-size: var(--font-size-sm, 0.875rem);
+ font-weight: var(--font-weight-medium, 500);
+ color: var(--color-text-secondary);
+}
+
+.select {
+ padding: var(--spacing-xs, 4px) var(--spacing-sm, 8px);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-md, 6px);
+ background: var(--color-bg-input);
+ color: var(--color-text-primary);
+ font-size: var(--font-size-sm, 0.875rem);
+ cursor: pointer;
+ min-width: 150px;
+}
+
+.select:focus {
+ outline: none;
+ border-color: var(--color-primary);
+}
+
+.select:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* ============================================================================
+ MULTI SELECT
+ ============================================================================ */
+
+.providerMultiSelect {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm, 8px);
+}
+
+.selectActions {
+ display: flex;
+ gap: var(--spacing-xs, 4px);
+}
+
+.actionButton {
+ padding: 2px 8px;
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-sm, 4px);
+ background: var(--color-bg-secondary);
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-xs, 0.75rem);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.actionButton:hover:not(:disabled) {
+ background: var(--color-bg-hover);
+ border-color: var(--color-primary);
+}
+
+.actionButton:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.checkboxList {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xs, 4px);
+ padding: var(--spacing-sm, 8px);
+ background: var(--color-bg-secondary);
+ border-radius: var(--border-radius-md, 6px);
+}
+
+.checkboxItem {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm, 8px);
+ padding: var(--spacing-xs, 4px) var(--spacing-sm, 8px);
+ border-radius: var(--border-radius-sm, 4px);
+ cursor: pointer;
+ transition: background 0.2s ease;
+}
+
+.checkboxItem:hover {
+ background: var(--color-bg-hover);
+}
+
+.checkboxItem.disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.checkboxItem input[type="checkbox"] {
+ width: 16px;
+ height: 16px;
+ cursor: inherit;
+}
+
+.icon {
+ font-size: 1.1em;
+}
+
+.providerName {
+ font-size: var(--font-size-sm, 0.875rem);
+ color: var(--color-text-primary);
+}
+
+.hint {
+ font-size: var(--font-size-xs, 0.75rem);
+ color: var(--color-text-tertiary);
+ font-style: italic;
+ padding: var(--spacing-xs, 4px) 0;
+}
+
+.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--spacing-md, 16px);
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-sm, 0.875rem);
+}
+
+/* ============================================================================
+ PROVIDER BADGES
+ ============================================================================ */
+
+.providerBadges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--spacing-xs, 4px);
+}
+
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-sm, 4px);
+ font-size: var(--font-size-xs, 0.75rem);
+ color: var(--color-text-primary);
+}
+
+.allProviders {
+ font-size: var(--font-size-xs, 0.75rem);
+ color: var(--color-text-secondary);
+ font-style: italic;
+}
diff --git a/src/components/ProviderSelector/ProviderSelector.tsx b/src/components/ProviderSelector/ProviderSelector.tsx
new file mode 100644
index 0000000..fb36ea5
--- /dev/null
+++ b/src/components/ProviderSelector/ProviderSelector.tsx
@@ -0,0 +1,221 @@
+/**
+ * ProviderSelector Component
+ *
+ * Wiederverwendbare Komponente zur Auswahl von AICore-Providern.
+ * Kann im Chat Playground und Automation Editor verwendet werden.
+ *
+ * Features:
+ * - Dropdown für Einzelauswahl
+ * - Checkbox-Liste für Mehrfachauswahl
+ * - Lädt verfügbare Provider aus dem Billing-System
+ */
+
+import React, { useEffect, useMemo } from 'react';
+import { useBilling } from '../../hooks/useBilling';
+import styles from './ProviderSelector.module.css';
+
+// Provider display names
+const PROVIDER_LABELS: Record = {
+ anthropic: 'Anthropic (Claude)',
+ openai: 'OpenAI (GPT)',
+ perplexity: 'Perplexity',
+ tavily: 'Tavily (Web Search)',
+ internal: 'Internal',
+};
+
+// Provider icons (emojis for simplicity)
+const PROVIDER_ICONS: Record = {
+ anthropic: '🤖',
+ openai: '💬',
+ perplexity: '🔍',
+ tavily: '🌐',
+ internal: '🏠',
+};
+
+// ============================================================================
+// SINGLE SELECT COMPONENT
+// ============================================================================
+
+interface ProviderSelectProps {
+ value: string;
+ onChange: (provider: string) => void;
+ disabled?: boolean;
+ className?: string;
+ label?: string;
+ showLabel?: boolean;
+}
+
+export const ProviderSelect: React.FC = ({
+ value,
+ onChange,
+ disabled = false,
+ className,
+ label = 'AI-Provider',
+ showLabel = true,
+}) => {
+ const { allowedProviders, loadAllowedProviders, loading } = useBilling();
+
+ useEffect(() => {
+ if (allowedProviders.length === 0 && !loading) {
+ loadAllowedProviders();
+ }
+ }, []);
+
+ const providerOptions = useMemo(() => {
+ return allowedProviders.map((provider) => ({
+ value: provider,
+ label: `${PROVIDER_ICONS[provider] || '🔌'} ${PROVIDER_LABELS[provider] || provider}`,
+ }));
+ }, [allowedProviders]);
+
+ return (
+
+ {showLabel && }
+
+
+ );
+};
+
+// ============================================================================
+// MULTI SELECT COMPONENT (Checkbox List)
+// ============================================================================
+
+interface ProviderMultiSelectProps {
+ selectedProviders: string[];
+ onChange: (providers: string[]) => void;
+ disabled?: boolean;
+ className?: string;
+ label?: string;
+ showLabel?: boolean;
+}
+
+export const ProviderMultiSelect: React.FC = ({
+ selectedProviders,
+ onChange,
+ disabled = false,
+ className,
+ label = 'Erlaubte AI-Provider',
+ showLabel = true,
+}) => {
+ const { allowedProviders, loadAllowedProviders, loading } = useBilling();
+
+ useEffect(() => {
+ if (allowedProviders.length === 0 && !loading) {
+ loadAllowedProviders();
+ }
+ }, []);
+
+ const handleToggle = (provider: string) => {
+ if (selectedProviders.includes(provider)) {
+ onChange(selectedProviders.filter((p) => p !== provider));
+ } else {
+ onChange([...selectedProviders, provider]);
+ }
+ };
+
+ const handleSelectAll = () => {
+ onChange(allowedProviders);
+ };
+
+ const handleSelectNone = () => {
+ onChange([]);
+ };
+
+ return (
+
+ {showLabel &&
}
+
+
+
+
+
+
+ {loading ? (
+
Lade Provider...
+ ) : (
+
+ {allowedProviders.map((provider) => (
+
+ ))}
+
+ )}
+
+ {selectedProviders.length === 0 && !loading && (
+
+ Wenn keine Provider ausgewählt sind, werden alle erlaubten Provider verwendet.
+
+ )}
+
+ );
+};
+
+// ============================================================================
+// COMPACT PROVIDER BADGE LIST
+// ============================================================================
+
+interface ProviderBadgesProps {
+ providers: string[];
+ className?: string;
+}
+
+export const ProviderBadges: React.FC = ({
+ providers,
+ className,
+}) => {
+ if (providers.length === 0) {
+ return Alle Provider;
+ }
+
+ return (
+
+ {providers.map((provider) => (
+
+ {PROVIDER_ICONS[provider] || '🔌'} {PROVIDER_LABELS[provider] || provider}
+
+ ))}
+
+ );
+};
+
+// Default export
+export default ProviderSelect;
diff --git a/src/components/ProviderSelector/index.ts b/src/components/ProviderSelector/index.ts
new file mode 100644
index 0000000..afe1b42
--- /dev/null
+++ b/src/components/ProviderSelector/index.ts
@@ -0,0 +1,10 @@
+/**
+ * Provider Selector Component Exports
+ */
+
+export {
+ ProviderSelect,
+ ProviderMultiSelect,
+ ProviderBadges
+} from './ProviderSelector';
+export { default } from './ProviderSelector';
diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx
index 62f8828..35785cc 100644
--- a/src/config/pageRegistry.tsx
+++ b/src/config/pageRegistry.tsx
@@ -20,7 +20,7 @@ import {
FaCubes, FaEnvelopeOpenText, FaKey, FaUsersCog, FaCube, FaShieldAlt,
FaLightbulb, FaRegFileAlt, FaLink, FaComments, FaChartBar, FaMicrophone,
FaListAlt, FaCogs, FaChartLine, FaFileAlt, FaUserShield, FaDatabase,
- FaProjectDiagram, FaMapMarkedAlt
+ FaProjectDiagram, FaMapMarkedAlt, FaWallet, FaMoneyBillAlt
} from 'react-icons/fa';
// =============================================================================
@@ -47,6 +47,10 @@ export const PAGE_ICONS: Record = {
'page.system.pek': ,
'page.system.speech': ,
+ // Billing pages
+ 'page.billing.dashboard': ,
+ 'page.billing.transactions': ,
+
// Admin pages
'page.admin.access': ,
'page.admin.users': ,
@@ -59,6 +63,7 @@ export const PAGE_ICONS: Record = {
'page.admin.feature-instances': ,
'page.admin.feature-users': ,
'page.admin.user-access-overview': ,
+ 'page.admin.billing': ,
// Feature pages - Trustee
'page.feature.trustee.dashboard': ,
diff --git a/src/hooks/useBilling.ts b/src/hooks/useBilling.ts
new file mode 100644
index 0000000..70298aa
--- /dev/null
+++ b/src/hooks/useBilling.ts
@@ -0,0 +1,265 @@
+/**
+ * useBilling Hook
+ *
+ * Hook für die Verwaltung von Billing-Daten.
+ * Bietet Zugriff auf Guthaben, Transaktionen, Statistiken und Admin-Funktionen.
+ */
+
+import { useState, useEffect, useCallback } from 'react';
+import { useApiRequest } from './useApi';
+import {
+ fetchBalances,
+ fetchBalanceForMandate,
+ fetchTransactions,
+ fetchStatistics,
+ fetchAllowedProviders,
+ fetchSettingsAdmin,
+ updateSettingsAdmin,
+ addCreditAdmin,
+ fetchAccountsAdmin,
+ fetchTransactionsAdmin,
+ type BillingBalance,
+ type BillingTransaction,
+ type BillingSettings,
+ type BillingSettingsUpdate,
+ type UsageReport,
+ type AccountSummary,
+ type CreditAddRequest,
+} from '../api/billingApi';
+
+// Re-export types
+export type {
+ BillingBalance,
+ BillingTransaction,
+ BillingSettings,
+ BillingSettingsUpdate,
+ UsageReport,
+ AccountSummary,
+ CreditAddRequest,
+};
+
+export type { BillingModel, TransactionType, ReferenceType } from '../api/billingApi';
+
+/**
+ * Hook for user billing operations
+ */
+export function useBilling() {
+ const [balances, setBalances] = useState([]);
+ const [transactions, setTransactions] = useState([]);
+ const [statistics, setStatistics] = useState(null);
+ const [allowedProviders, setAllowedProviders] = useState([]);
+ const { request, isLoading: loading, error } = useApiRequest();
+
+ // Fetch all balances for the user
+ const loadBalances = useCallback(async () => {
+ try {
+ const data = await fetchBalances(request);
+ setBalances(Array.isArray(data) ? data : []);
+ return data;
+ } catch (err) {
+ console.error('Error loading balances:', err);
+ setBalances([]);
+ return [];
+ }
+ }, [request]);
+
+ // Fetch balance for a specific mandate
+ const loadBalanceForMandate = useCallback(async (mandateId: string) => {
+ try {
+ return await fetchBalanceForMandate(request, mandateId);
+ } catch (err) {
+ console.error('Error loading balance for mandate:', err);
+ return null;
+ }
+ }, [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 : []);
+ return data;
+ } catch (err) {
+ console.error('Error loading transactions:', err);
+ setTransactions([]);
+ return [];
+ }
+ }, [request]);
+
+ // Fetch statistics
+ const loadStatistics = useCallback(async (
+ period: 'day' | 'month' | 'year',
+ year: number,
+ month?: number
+ ) => {
+ try {
+ const data = await fetchStatistics(request, period, year, month);
+ setStatistics(data);
+ return data;
+ } catch (err) {
+ console.error('Error loading statistics:', err);
+ setStatistics(null);
+ return null;
+ }
+ }, [request]);
+
+ // Fetch allowed providers
+ const loadAllowedProviders = useCallback(async () => {
+ try {
+ const data = await fetchAllowedProviders(request);
+ setAllowedProviders(Array.isArray(data) ? data : []);
+ return data;
+ } catch (err) {
+ console.error('Error loading allowed providers:', err);
+ setAllowedProviders([]);
+ return [];
+ }
+ }, [request]);
+
+ // Initial load
+ useEffect(() => {
+ loadBalances();
+ loadAllowedProviders();
+ }, []);
+
+ return {
+ balances,
+ transactions,
+ statistics,
+ allowedProviders,
+ loading,
+ error,
+ loadBalances,
+ loadBalanceForMandate,
+ loadTransactions,
+ loadStatistics,
+ loadAllowedProviders,
+ refetch: loadBalances,
+ };
+}
+
+/**
+ * Hook for admin billing operations
+ */
+export function useBillingAdmin(mandateId?: string) {
+ const [settings, setSettings] = useState(null);
+ const [accounts, setAccounts] = useState([]);
+ const [transactions, setTransactions] = useState([]);
+ const { request, isLoading: loading, error } = useApiRequest();
+
+ // Fetch settings for a mandate
+ const loadSettings = useCallback(async (targetMandateId?: string) => {
+ const mId = targetMandateId || mandateId;
+ if (!mId) return null;
+
+ try {
+ const data = await fetchSettingsAdmin(request, mId);
+ setSettings(data);
+ return data;
+ } catch (err) {
+ console.error('Error loading billing settings:', err);
+ setSettings(null);
+ return null;
+ }
+ }, [request, mandateId]);
+
+ // Update settings
+ const saveSettings = useCallback(async (
+ settingsUpdate: BillingSettingsUpdate,
+ targetMandateId?: string
+ ) => {
+ const mId = targetMandateId || mandateId;
+ if (!mId) return null;
+
+ try {
+ const data = await updateSettingsAdmin(request, mId, settingsUpdate);
+ setSettings(data);
+ return data;
+ } catch (err) {
+ console.error('Error saving billing settings:', err);
+ throw err;
+ }
+ }, [request, mandateId]);
+
+ // Add credit
+ const addCredit = useCallback(async (
+ creditRequest: CreditAddRequest,
+ targetMandateId?: string
+ ) => {
+ const mId = targetMandateId || mandateId;
+ if (!mId) return null;
+
+ try {
+ const result = await addCreditAdmin(request, mId, creditRequest);
+ // Reload accounts after adding credit
+ await loadAccounts(mId);
+ return result;
+ } catch (err) {
+ console.error('Error adding credit:', err);
+ throw err;
+ }
+ }, [request, mandateId]);
+
+ // Fetch accounts for a mandate
+ const loadAccounts = useCallback(async (targetMandateId?: string) => {
+ const mId = targetMandateId || mandateId;
+ if (!mId) return [];
+
+ try {
+ const data = await fetchAccountsAdmin(request, mId);
+ setAccounts(Array.isArray(data) ? data : []);
+ return data;
+ } catch (err) {
+ console.error('Error loading accounts:', err);
+ setAccounts([]);
+ return [];
+ }
+ }, [request, mandateId]);
+
+ // Fetch transactions for a mandate
+ const loadTransactions = useCallback(async (targetMandateId?: string, limit: number = 100) => {
+ const mId = targetMandateId || mandateId;
+ if (!mId) return [];
+
+ try {
+ const data = await fetchTransactionsAdmin(request, mId, limit);
+ setTransactions(Array.isArray(data) ? data : []);
+ return data;
+ } catch (err) {
+ console.error('Error loading transactions:', err);
+ setTransactions([]);
+ return [];
+ }
+ }, [request, mandateId]);
+
+ // Load data when mandateId changes
+ useEffect(() => {
+ if (mandateId) {
+ loadSettings();
+ loadAccounts();
+ loadTransactions();
+ }
+ }, [mandateId]);
+
+ return {
+ settings,
+ accounts,
+ transactions,
+ loading,
+ error,
+ loadSettings,
+ saveSettings,
+ addCredit,
+ loadAccounts,
+ loadTransactions,
+ refetch: () => {
+ if (mandateId) {
+ loadSettings();
+ loadAccounts();
+ loadTransactions();
+ }
+ },
+ };
+}
+
+export default useBilling;
diff --git a/src/pages/billing/Billing.module.css b/src/pages/billing/Billing.module.css
new file mode 100644
index 0000000..f7c7304
--- /dev/null
+++ b/src/pages/billing/Billing.module.css
@@ -0,0 +1,518 @@
+/* Billing Pages Styles */
+
+/* ============================================================================
+ PAGE LAYOUT
+ ============================================================================ */
+
+.billingDashboard {
+ padding: var(--spacing-lg);
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.pageHeader {
+ margin-bottom: var(--spacing-xl);
+}
+
+.pageHeader h1 {
+ font-size: var(--font-size-2xl);
+ font-weight: var(--font-weight-bold);
+ color: var(--color-text-primary);
+ margin: 0 0 var(--spacing-xs) 0;
+}
+
+.subtitle {
+ font-size: var(--font-size-base);
+ color: var(--color-text-secondary);
+ margin: 0;
+}
+
+/* ============================================================================
+ SECTIONS
+ ============================================================================ */
+
+.section {
+ margin-bottom: var(--spacing-xl);
+}
+
+.sectionHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: var(--spacing-md);
+ flex-wrap: wrap;
+ gap: var(--spacing-sm);
+}
+
+.sectionTitle {
+ font-size: var(--font-size-lg);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-primary);
+ margin: 0 0 var(--spacing-md) 0;
+}
+
+.sectionHeader .sectionTitle {
+ margin-bottom: 0;
+}
+
+/* ============================================================================
+ BALANCE CARDS
+ ============================================================================ */
+
+.balanceGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: var(--spacing-md);
+}
+
+.balanceCard {
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-lg);
+ padding: var(--spacing-lg);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.balanceCard:hover {
+ border-color: var(--color-primary);
+ box-shadow: var(--shadow-md);
+}
+
+.balanceCard.warning {
+ border-color: var(--color-warning);
+ background: var(--color-warning-bg, rgba(255, 193, 7, 0.1));
+}
+
+.balanceHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: var(--spacing-md);
+}
+
+.mandateName {
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-primary);
+ margin: 0;
+}
+
+.billingModel {
+ font-size: var(--font-size-xs);
+ color: var(--color-text-secondary);
+ background: var(--color-bg-secondary);
+ padding: 2px 8px;
+ border-radius: var(--border-radius-sm);
+}
+
+.balanceAmount {
+ font-size: var(--font-size-2xl);
+ font-weight: var(--font-weight-bold);
+ color: var(--color-text-primary);
+ margin-bottom: var(--spacing-sm);
+}
+
+.warningBadge {
+ display: inline-block;
+ font-size: var(--font-size-xs);
+ color: var(--color-warning-text, #856404);
+ background: var(--color-warning-badge-bg, rgba(255, 193, 7, 0.3));
+ padding: 4px 8px;
+ border-radius: var(--border-radius-sm);
+ font-weight: var(--font-weight-medium);
+}
+
+/* ============================================================================
+ STATISTICS
+ ============================================================================ */
+
+.periodSelector {
+ display: flex;
+ gap: var(--spacing-sm);
+ align-items: center;
+}
+
+.select {
+ padding: var(--spacing-xs) var(--spacing-sm);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-md);
+ background: var(--color-bg-input);
+ color: var(--color-text-primary);
+ font-size: var(--font-size-sm);
+ cursor: pointer;
+}
+
+.select:focus {
+ outline: none;
+ border-color: var(--color-primary);
+}
+
+.statisticsChart {
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-lg);
+ padding: var(--spacing-lg);
+}
+
+.totalCost {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding: var(--spacing-lg);
+ background: var(--color-bg-secondary);
+ border-radius: var(--border-radius-md);
+ margin-bottom: var(--spacing-lg);
+}
+
+.totalLabel {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
+ margin-bottom: var(--spacing-xs);
+}
+
+.totalAmount {
+ font-size: var(--font-size-3xl);
+ font-weight: var(--font-weight-bold);
+ color: var(--color-text-primary);
+}
+
+.chartSection {
+ margin-bottom: var(--spacing-lg);
+}
+
+.chartSection:last-child {
+ margin-bottom: 0;
+}
+
+.chartSection h4 {
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-secondary);
+ margin: 0 0 var(--spacing-md) 0;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.barChart {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-sm);
+}
+
+.barRow {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-sm);
+}
+
+.barLabel {
+ width: 100px;
+ font-size: var(--font-size-sm);
+ color: var(--color-text-primary);
+ text-transform: capitalize;
+}
+
+.barContainer {
+ flex: 1;
+ height: 24px;
+ background: var(--color-bg-secondary);
+ border-radius: var(--border-radius-sm);
+ overflow: hidden;
+}
+
+.bar {
+ height: 100%;
+ background: var(--color-primary);
+ border-radius: var(--border-radius-sm);
+ transition: width 0.3s ease;
+ min-width: 4px;
+}
+
+.barValue {
+ width: 100px;
+ text-align: right;
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
+ font-family: var(--font-mono);
+}
+
+.featureList {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xs);
+}
+
+.featureRow {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: var(--spacing-sm);
+ background: var(--color-bg-secondary);
+ border-radius: var(--border-radius-sm);
+}
+
+.featureLabel {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-primary);
+ text-transform: capitalize;
+}
+
+.featureValue {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-secondary);
+ font-family: var(--font-mono);
+}
+
+/* ============================================================================
+ TRANSACTIONS
+ ============================================================================ */
+
+.transactionsTable {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.transactionsTable th,
+.transactionsTable td {
+ padding: var(--spacing-sm) var(--spacing-md);
+ text-align: left;
+ border-bottom: 1px solid var(--color-border);
+}
+
+.transactionsTable th {
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ background: var(--color-bg-secondary);
+}
+
+.transactionsTable td {
+ font-size: var(--font-size-sm);
+ color: var(--color-text-primary);
+}
+
+.transactionType {
+ display: inline-block;
+ padding: 2px 8px;
+ border-radius: var(--border-radius-sm);
+ font-size: var(--font-size-xs);
+ font-weight: var(--font-weight-medium);
+}
+
+.transactionType.credit {
+ background: var(--color-success-bg, rgba(40, 167, 69, 0.1));
+ color: var(--color-success, #28a745);
+}
+
+.transactionType.debit {
+ background: var(--color-error-bg, rgba(220, 53, 69, 0.1));
+ color: var(--color-error, #dc3545);
+}
+
+.transactionType.adjustment {
+ background: var(--color-info-bg, rgba(23, 162, 184, 0.1));
+ color: var(--color-info, #17a2b8);
+}
+
+/* ============================================================================
+ ADMIN STYLES
+ ============================================================================ */
+
+.adminSection {
+ background: var(--color-bg-card);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-lg);
+ padding: var(--spacing-lg);
+ margin-bottom: var(--spacing-lg);
+}
+
+.adminSection h3 {
+ font-size: var(--font-size-base);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-primary);
+ margin: 0 0 var(--spacing-md) 0;
+}
+
+.formRow {
+ display: flex;
+ gap: var(--spacing-md);
+ margin-bottom: var(--spacing-md);
+}
+
+.formGroup {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xs);
+}
+
+.formGroup label {
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-medium);
+ color: var(--color-text-secondary);
+}
+
+.input {
+ padding: var(--spacing-sm);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-md);
+ background: var(--color-bg-input);
+ color: var(--color-text-primary);
+ font-size: var(--font-size-base);
+}
+
+.input:focus {
+ outline: none;
+ border-color: var(--color-primary);
+}
+
+.accountsGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
+ gap: var(--spacing-md);
+}
+
+.accountCard {
+ background: var(--color-bg-secondary);
+ border: 1px solid var(--color-border);
+ border-radius: var(--border-radius-md);
+ padding: var(--spacing-md);
+}
+
+.accountCard h4 {
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-semibold);
+ color: var(--color-text-primary);
+ margin: 0 0 var(--spacing-sm) 0;
+}
+
+.accountInfo {
+ display: flex;
+ flex-direction: column;
+ gap: var(--spacing-xs);
+ font-size: var(--font-size-sm);
+}
+
+.accountInfo span {
+ color: var(--color-text-secondary);
+}
+
+.accountInfo strong {
+ color: var(--color-text-primary);
+}
+
+/* ============================================================================
+ BUTTONS
+ ============================================================================ */
+
+.button {
+ padding: var(--spacing-sm) var(--spacing-md);
+ border: none;
+ border-radius: var(--border-radius-md);
+ font-size: var(--font-size-sm);
+ font-weight: var(--font-weight-medium);
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.buttonPrimary {
+ background: var(--color-primary);
+ color: white;
+}
+
+.buttonPrimary:hover {
+ background: var(--color-primary-dark);
+}
+
+.buttonPrimary:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.buttonSecondary {
+ background: var(--color-bg-secondary);
+ color: var(--color-text-primary);
+ border: 1px solid var(--color-border);
+}
+
+.buttonSecondary:hover {
+ background: var(--color-bg-hover);
+}
+
+/* ============================================================================
+ UTILITY CLASSES
+ ============================================================================ */
+
+.loadingPlaceholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--spacing-xl);
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-sm);
+}
+
+.noData {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--spacing-lg);
+ color: var(--color-text-tertiary);
+ font-size: var(--font-size-sm);
+ font-style: italic;
+}
+
+.errorMessage {
+ background: var(--color-error-bg, rgba(220, 53, 69, 0.1));
+ color: var(--color-error, #dc3545);
+ padding: var(--spacing-sm) var(--spacing-md);
+ border-radius: var(--border-radius-md);
+ font-size: var(--font-size-sm);
+ margin-bottom: var(--spacing-md);
+}
+
+.successMessage {
+ background: var(--color-success-bg, rgba(40, 167, 69, 0.1));
+ color: var(--color-success, #28a745);
+ padding: var(--spacing-sm) var(--spacing-md);
+ border-radius: var(--border-radius-md);
+ font-size: var(--font-size-sm);
+ margin-bottom: var(--spacing-md);
+}
+
+/* ============================================================================
+ RESPONSIVE
+ ============================================================================ */
+
+@media (max-width: 768px) {
+ .billingDashboard {
+ padding: var(--spacing-md);
+ }
+
+ .balanceGrid {
+ grid-template-columns: 1fr;
+ }
+
+ .sectionHeader {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .periodSelector {
+ width: 100%;
+ flex-wrap: wrap;
+ }
+
+ .formRow {
+ flex-direction: column;
+ }
+
+ .barLabel,
+ .barValue {
+ width: 80px;
+ font-size: var(--font-size-xs);
+ }
+}
diff --git a/src/pages/billing/BillingAdmin.tsx b/src/pages/billing/BillingAdmin.tsx
new file mode 100644
index 0000000..fc16ac7
--- /dev/null
+++ b/src/pages/billing/BillingAdmin.tsx
@@ -0,0 +1,419 @@
+/**
+ * Billing Admin Page
+ *
+ * Admin-Seite für Billing-Verwaltung (SysAdmin only).
+ * - Settings verwalten
+ * - Guthaben aufladen
+ * - Konten übersicht
+ */
+
+import React, { useState, useEffect, useCallback } from 'react';
+import { useBillingAdmin, type BillingSettings, type AccountSummary } from '../../hooks/useBilling';
+import { useAdminMandates } from '../../hooks/useMandates';
+import styles from './Billing.module.css';
+
+// ============================================================================
+// MANDATE SELECTOR
+// ============================================================================
+
+interface MandateSelectorProps {
+ selectedMandateId: string | null;
+ onSelect: (mandateId: string) => void;
+}
+
+const MandateSelector: React.FC = ({ selectedMandateId, onSelect }) => {
+ const { mandates, loading } = useAdminMandates();
+
+ return (
+
+
+
+
+ );
+};
+
+// ============================================================================
+// SETTINGS EDITOR
+// ============================================================================
+
+interface SettingsEditorProps {
+ settings: BillingSettings | null;
+ onSave: (settings: Partial) => Promise;
+ loading: boolean;
+}
+
+const SettingsEditor: React.FC = ({ settings, onSave, loading }) => {
+ const [formData, setFormData] = useState({
+ billingModel: settings?.billingModel || 'UNLIMITED',
+ defaultUserCredit: settings?.defaultUserCredit || 10,
+ warningThresholdPercent: settings?.warningThresholdPercent || 10,
+ blockOnZeroBalance: settings?.blockOnZeroBalance ?? true,
+ notifyOnWarning: settings?.notifyOnWarning ?? true,
+ });
+ const [saving, setSaving] = useState(false);
+ const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
+
+ useEffect(() => {
+ if (settings) {
+ setFormData({
+ billingModel: settings.billingModel,
+ defaultUserCredit: settings.defaultUserCredit,
+ warningThresholdPercent: settings.warningThresholdPercent,
+ blockOnZeroBalance: settings.blockOnZeroBalance,
+ notifyOnWarning: settings.notifyOnWarning,
+ });
+ }
+ }, [settings]);
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setSaving(true);
+ setMessage(null);
+
+ try {
+ await onSave(formData);
+ setMessage({ type: 'success', text: 'Einstellungen gespeichert!' });
+ } catch (err: any) {
+ setMessage({ type: 'error', text: err.message || 'Fehler beim Speichern' });
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+
+
Billing-Einstellungen
+
+ {message && (
+
+ {message.text}
+
+ )}
+
+
+
+ );
+};
+
+// ============================================================================
+// CREDIT ADDER
+// ============================================================================
+
+interface CreditAdderProps {
+ settings: BillingSettings | null;
+ accounts: AccountSummary[];
+ onAddCredit: (userId: string | undefined, amount: number, description: string) => Promise;
+}
+
+const CreditAdder: React.FC = ({ settings, accounts, onAddCredit }) => {
+ const [selectedUserId, setSelectedUserId] = useState('');
+ const [amount, setAmount] = useState(10);
+ const [description, setDescription] = useState('Manuelles Aufladen');
+ const [saving, setSaving] = useState(false);
+ const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
+
+ const isPrepayUser = settings?.billingModel === 'PREPAY_USER';
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (amount <= 0) {
+ setMessage({ type: 'error', text: 'Betrag muss positiv sein' });
+ return;
+ }
+
+ setSaving(true);
+ setMessage(null);
+
+ try {
+ await onAddCredit(isPrepayUser ? selectedUserId : undefined, amount, description);
+ setMessage({ type: 'success', text: `${amount} CHF erfolgreich gutgeschrieben!` });
+ setAmount(10);
+ setDescription('Manuelles Aufladen');
+ } catch (err: any) {
+ setMessage({ type: 'error', text: err.message || 'Fehler beim Aufladen' });
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('de-CH', {
+ style: 'currency',
+ currency: 'CHF'
+ }).format(amount);
+ };
+
+ return (
+
+
Guthaben aufladen
+
+ {message && (
+
+ {message.text}
+
+ )}
+
+
+
+ );
+};
+
+// ============================================================================
+// ACCOUNTS OVERVIEW
+// ============================================================================
+
+interface AccountsOverviewProps {
+ accounts: AccountSummary[];
+ loading: boolean;
+}
+
+const AccountsOverview: React.FC = ({ accounts, loading }) => {
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('de-CH', {
+ style: 'currency',
+ currency: 'CHF'
+ }).format(amount);
+ };
+
+ if (loading) {
+ return Lade Konten...
;
+ }
+
+ if (accounts.length === 0) {
+ return Keine Konten vorhanden
;
+ }
+
+ return (
+
+
Konten
+
+ {accounts.map((account) => (
+
+
{account.accountType === 'MANDATE' ? 'Mandanten-Konto' : 'Benutzer-Konto'}
+
+ {account.userId && User: {account.userId}}
+ Guthaben: {formatCurrency(account.balance)}
+ {account.creditLimit && Limit: {formatCurrency(account.creditLimit)}}
+ Status: {account.enabled ? 'Aktiv' : 'Deaktiviert'}
+
+
+ ))}
+
+
+ );
+};
+
+// ============================================================================
+// MAIN COMPONENT
+// ============================================================================
+
+export const BillingAdmin: React.FC = () => {
+ const [selectedMandateId, setSelectedMandateId] = useState(null);
+ const { settings, accounts, loading, loadSettings, saveSettings, addCredit, loadAccounts } = useBillingAdmin(selectedMandateId || undefined);
+
+ const handleMandateSelect = (mandateId: string) => {
+ setSelectedMandateId(mandateId || null);
+ };
+
+ const handleSaveSettings = useCallback(async (settingsUpdate: Partial) => {
+ if (!selectedMandateId) return;
+ await saveSettings(settingsUpdate);
+ }, [selectedMandateId, saveSettings]);
+
+ const handleAddCredit = useCallback(async (userId: string | undefined, amount: number, description: string) => {
+ if (!selectedMandateId) return;
+ await addCredit({ userId, amount, description });
+ await loadAccounts();
+ }, [selectedMandateId, addCredit, loadAccounts]);
+
+ return (
+
+
+
+
+
+ {selectedMandateId && (
+ <>
+
+
+
+
+
+ >
+ )}
+
+ {!selectedMandateId && (
+
+ Bitte wählen Sie einen Mandanten aus.
+
+ )}
+
+ );
+};
+
+export default BillingAdmin;
diff --git a/src/pages/billing/BillingDashboard.tsx b/src/pages/billing/BillingDashboard.tsx
new file mode 100644
index 0000000..2576818
--- /dev/null
+++ b/src/pages/billing/BillingDashboard.tsx
@@ -0,0 +1,247 @@
+/**
+ * Billing Dashboard Page
+ *
+ * Zeigt Guthaben, Statistiken und Transaktionen für den Benutzer.
+ */
+
+import React, { useState, useEffect, useMemo } from 'react';
+import { useBilling, type BillingBalance, type UsageReport } from '../../hooks/useBilling';
+import styles from './Billing.module.css';
+
+// ============================================================================
+// BALANCE CARD COMPONENT
+// ============================================================================
+
+interface BalanceCardProps {
+ balance: BillingBalance;
+ onClick?: () => void;
+}
+
+const BalanceCard: React.FC = ({ balance, onClick }) => {
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('de-CH', {
+ style: 'currency',
+ currency: 'CHF'
+ }).format(amount);
+ };
+
+ const getBillingModelLabel = (model: string) => {
+ switch (model) {
+ case 'PREPAY_MANDATE': return 'Prepaid (Mandant)';
+ case 'PREPAY_USER': return 'Prepaid (Benutzer)';
+ case 'CREDIT_POSTPAY': return 'Kredit';
+ case 'UNLIMITED': return 'Unlimited';
+ default: return model;
+ }
+ };
+
+ return (
+
+
+
{balance.mandateName}
+ {getBillingModelLabel(balance.billingModel)}
+
+
+ {formatCurrency(balance.balance)}
+
+ {balance.isWarning && (
+
+ Niedriges Guthaben
+
+ )}
+
+ );
+};
+
+// ============================================================================
+// STATISTICS CHART COMPONENT
+// ============================================================================
+
+interface StatisticsChartProps {
+ statistics: UsageReport | null;
+ loading?: boolean;
+}
+
+const StatisticsChart: React.FC = ({ statistics, loading }) => {
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('de-CH', {
+ style: 'currency',
+ currency: 'CHF'
+ }).format(amount);
+ };
+
+ if (loading) {
+ return Lade Statistiken...
;
+ }
+
+ if (!statistics) {
+ return Keine Statistiken verfügbar
;
+ }
+
+ // Calculate max cost for bar scaling
+ const maxProviderCost = Math.max(...Object.values(statistics.costByProvider), 1);
+
+ return (
+
+
+ Gesamtkosten
+ {formatCurrency(statistics.totalCost)}
+
+
+
+
Kosten nach Anbieter
+ {Object.entries(statistics.costByProvider).length === 0 ? (
+
Keine Daten
+ ) : (
+
+ {Object.entries(statistics.costByProvider).map(([provider, cost]) => (
+
+
{provider}
+
+
{formatCurrency(cost)}
+
+ ))}
+
+ )}
+
+
+
+
Kosten nach Feature
+ {Object.entries(statistics.costByFeature).length === 0 ? (
+
Keine Daten
+ ) : (
+
+ {Object.entries(statistics.costByFeature).map(([feature, cost]) => (
+
+ {feature}
+ {formatCurrency(cost)}
+
+ ))}
+
+ )}
+
+
+ );
+};
+
+// ============================================================================
+// MAIN COMPONENT
+// ============================================================================
+
+export const BillingDashboard: React.FC = () => {
+ const {
+ balances,
+ statistics,
+ loading,
+ loadBalances,
+ loadStatistics
+ } = useBilling();
+
+ const [selectedPeriod, setSelectedPeriod] = useState<'month' | 'year'>('month');
+ const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
+ const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth() + 1);
+
+ // Load statistics when period changes
+ useEffect(() => {
+ if (selectedPeriod === 'month') {
+ loadStatistics('month', selectedYear);
+ } else {
+ loadStatistics('year', selectedYear);
+ }
+ }, [selectedPeriod, selectedYear, loadStatistics]);
+
+ // Available years (current and last 2 years)
+ const availableYears = useMemo(() => {
+ const current = new Date().getFullYear();
+ return [current, current - 1, current - 2];
+ }, []);
+
+ // Available months
+ const availableMonths = [
+ { value: 1, label: 'Januar' },
+ { value: 2, label: 'Februar' },
+ { value: 3, label: 'März' },
+ { value: 4, label: 'April' },
+ { value: 5, label: 'Mai' },
+ { value: 6, label: 'Juni' },
+ { value: 7, label: 'Juli' },
+ { value: 8, label: 'August' },
+ { value: 9, label: 'September' },
+ { value: 10, label: 'Oktober' },
+ { value: 11, label: 'November' },
+ { value: 12, label: 'Dezember' },
+ ];
+
+ return (
+
+
+
+ {/* Balance Cards */}
+
+ Guthaben
+ {loading ? (
+ Lade Guthaben...
+ ) : balances.length === 0 ? (
+ Keine Abrechnungskonten vorhanden
+ ) : (
+
+ {balances.map((balance) => (
+
+ ))}
+
+ )}
+
+
+ {/* Statistics */}
+
+
+
Nutzungsstatistik
+
+
+
+ {selectedPeriod === 'month' && (
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default BillingDashboard;
diff --git a/src/pages/billing/BillingTransactions.tsx b/src/pages/billing/BillingTransactions.tsx
new file mode 100644
index 0000000..f584bce
--- /dev/null
+++ b/src/pages/billing/BillingTransactions.tsx
@@ -0,0 +1,142 @@
+/**
+ * Billing Transactions Page
+ *
+ * Zeigt die Transaktionshistorie für den Benutzer.
+ */
+
+import React, { useEffect, useState } from 'react';
+import { useBilling, type BillingTransaction } from '../../hooks/useBilling';
+import styles from './Billing.module.css';
+
+// ============================================================================
+// TRANSACTION ROW COMPONENT
+// ============================================================================
+
+interface TransactionRowProps {
+ transaction: BillingTransaction;
+}
+
+const TransactionRow: React.FC = ({ transaction }) => {
+ const formatCurrency = (amount: number) => {
+ return new Intl.NumberFormat('de-CH', {
+ style: 'currency',
+ currency: 'CHF'
+ }).format(amount);
+ };
+
+ const formatDate = (dateString?: string) => {
+ if (!dateString) return '-';
+ return new Date(dateString).toLocaleString('de-CH', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
+
+ const getTypeClass = (type: string) => {
+ switch (type) {
+ case 'CREDIT': return styles.credit;
+ case 'DEBIT': return styles.debit;
+ case 'ADJUSTMENT': return styles.adjustment;
+ default: return '';
+ }
+ };
+
+ const getTypeLabel = (type: string) => {
+ switch (type) {
+ case 'CREDIT': return 'Gutschrift';
+ case 'DEBIT': return 'Belastung';
+ case 'ADJUSTMENT': return 'Korrektur';
+ default: return type;
+ }
+ };
+
+ return (
+
+ | {formatDate(transaction.createdAt)} |
+
+
+ {getTypeLabel(transaction.transactionType)}
+
+ |
+ {transaction.description} |
+ {transaction.aicoreProvider || '-'} |
+ {transaction.featureCode || '-'} |
+
+ {transaction.transactionType === 'DEBIT' ? '-' : '+'}{formatCurrency(transaction.amount)}
+ |
+
+ );
+};
+
+// ============================================================================
+// MAIN COMPONENT
+// ============================================================================
+
+export const BillingTransactions: React.FC = () => {
+ const { transactions, loading, loadTransactions } = useBilling();
+ const [limit, setLimit] = useState(50);
+
+ useEffect(() => {
+ loadTransactions(limit);
+ }, [limit, loadTransactions]);
+
+ const handleLoadMore = () => {
+ setLimit(prev => prev + 50);
+ };
+
+ return (
+
+
+
+
+ {loading && transactions.length === 0 ? (
+ Lade Transaktionen...
+ ) : transactions.length === 0 ? (
+ Keine Transaktionen vorhanden
+ ) : (
+ <>
+
+
+
+
+ | Datum |
+ Typ |
+ Beschreibung |
+ Anbieter |
+ Feature |
+ Betrag |
+
+
+
+ {transactions.map((transaction) => (
+
+ ))}
+
+
+
+
+ {transactions.length >= limit && (
+
+
+
+ )}
+ >
+ )}
+
+
+ );
+};
+
+export default BillingTransactions;
diff --git a/src/pages/billing/index.ts b/src/pages/billing/index.ts
new file mode 100644
index 0000000..eb4f6c7
--- /dev/null
+++ b/src/pages/billing/index.ts
@@ -0,0 +1,7 @@
+/**
+ * Billing Pages Exports
+ */
+
+export { BillingDashboard } from './BillingDashboard';
+export { BillingTransactions } from './BillingTransactions';
+export { BillingAdmin } from './BillingAdmin';
diff --git a/src/pages/workflows/PlaygroundPage.tsx b/src/pages/workflows/PlaygroundPage.tsx
index ba20467..f85a71b 100644
--- a/src/pages/workflows/PlaygroundPage.tsx
+++ b/src/pages/workflows/PlaygroundPage.tsx
@@ -15,6 +15,7 @@ import { useCurrentInstance } from '../../hooks/useCurrentInstance';
import { FaComment, FaTasks, FaPaperPlane, FaStop, FaFile, FaPlus, FaMicrophone, FaSquare, FaFileAlt } from 'react-icons/fa';
import { useToast } from '../../contexts/ToastContext';
import { useVoiceLanguage, VoiceLanguageSelect, Messages } from '../../components/UiComponents';
+import { ProviderSelect } from '../../components/ProviderSelector';
import type { Message } from '../../components/UiComponents/Messages/MessagesTypes';
import api from '../../api';
import styles from './PlaygroundPage.module.css';
@@ -92,6 +93,9 @@ export const PlaygroundPage: React.FC = () => {
// Prompts dropdown state
const [selectedPromptId, setSelectedPromptId] = useState('');
+ // AI Provider selection state
+ const [selectedProvider, setSelectedProvider] = useState('');
+
// Load prompts on mount
useEffect(() => {
refetchPrompts();
@@ -782,6 +786,11 @@ export const PlaygroundPage: React.FC = () => {
>
+