diff --git a/src/api/billingApi.ts b/src/api/billingApi.ts index c43c40f..800adf1 100644 --- a/src/api/billingApi.ts +++ b/src/api/billingApi.ts @@ -252,3 +252,28 @@ export async function fetchTransactionsAdmin( params: { limit } }); } + +/** + * User summary for billing admin + */ +export interface MandateUserSummary { + id: string; + email?: string; + firstName?: string; + lastName?: string; + displayName?: string; +} + +/** + * Fetch all users for a mandate (Admin) + * Endpoint: GET /api/billing/admin/users/{mandateId} + */ +export async function fetchUsersForMandateAdmin( + request: ApiRequestFunction, + mandateId: string +): Promise { + return await request({ + url: `/api/billing/admin/users/${mandateId}`, + method: 'get' + }); +} diff --git a/src/components/Navigation/UserSection.tsx b/src/components/Navigation/UserSection.tsx index b78b874..1d7ba38 100644 --- a/src/components/Navigation/UserSection.tsx +++ b/src/components/Navigation/UserSection.tsx @@ -34,6 +34,11 @@ export const UserSection: React.FC = () => { setShowMenu(false); }; + const handleBilling = () => { + navigate('/billing'); + setShowMenu(false); + }; + const handleLegal = () => { setShowLegalModal(true); setShowMenu(false); @@ -72,6 +77,14 @@ export const UserSection: React.FC = () => { {showMenu && (
+ + -
- - -
- - {loading ? ( -
Lade Provider...
- ) : ( -
- {allowedProviders.map((provider) => ( -
)}
diff --git a/src/hooks/playground/useDashboardInputForm.ts b/src/hooks/playground/useDashboardInputForm.ts index 8650fdf..b41c1ea 100644 --- a/src/hooks/playground/useDashboardInputForm.ts +++ b/src/hooks/playground/useDashboardInputForm.ts @@ -30,7 +30,7 @@ export function useDashboardInputForm(instanceId: string) { const [optimisticMessage, setOptimisticMessage] = useState(null); const [selectedPromptId, setSelectedPromptId] = useState(null); const [workflowMode, setWorkflowMode] = useState<'Dynamic' | 'Automation' | null>(null); - const [selectedProvider, setSelectedProvider] = useState(''); // AI provider selection + const [selectedProviders, setSelectedProviders] = useState([]); // AI provider selection (multiselect) const { checkPermission, canView } = usePermissions(); const [playgroundUIPermission, setPlaygroundUIPermission] = useState(false); @@ -596,7 +596,7 @@ export function useDashboardInputForm(instanceId: string) { prompt: trimmedInput, listFileId: fileIdsToSend.length > 0 ? fileIdsToSend : undefined, userLanguage: 'en', - preferredProvider: selectedProvider || undefined // AI provider selection + preferredProviders: selectedProviders.length > 0 ? selectedProviders : undefined // AI provider selection (multiselect) }; const result = await startWorkflow(requestBody, workflowOptions); @@ -638,7 +638,7 @@ export function useDashboardInputForm(instanceId: string) { setWorkflowStatusOptimistic('idle'); } } - }, [inputValue, pendingFiles, isRunning, workflowId, startingWorkflow, startWorkflow, stopWorkflow, resetWorkflow, refetchWorkflows, selectWorkflowFromContext, selectWorkflow, chatWorkflowPermission, workflowMode, selectedProvider, setWorkflowStatusOptimistic]); + }, [inputValue, pendingFiles, isRunning, workflowId, startingWorkflow, startWorkflow, stopWorkflow, resetWorkflow, refetchWorkflows, selectWorkflowFromContext, selectWorkflow, chatWorkflowPermission, workflowMode, selectedProviders, setWorkflowStatusOptimistic]); useEffect(() => { const handleWorkflowCleared = () => { @@ -823,9 +823,9 @@ export function useDashboardInputForm(instanceId: string) { handleFileAttach, handleFileUploadAndAttach, latestStats, - // AI Provider selection - selectedProvider, - onProviderSelect: setSelectedProvider + // AI Provider selection (multiselect) + selectedProviders, + onProvidersChange: setSelectedProviders }; } diff --git a/src/hooks/playground/useWorkflowLifecycle.ts b/src/hooks/playground/useWorkflowLifecycle.ts index c06b601..039864f 100644 --- a/src/hooks/playground/useWorkflowLifecycle.ts +++ b/src/hooks/playground/useWorkflowLifecycle.ts @@ -479,25 +479,36 @@ export function useWorkflowLifecycle(instanceId: string) { // Continue polling if: // 1. Workflow is currently running, OR - // 2. Workflow just completed (within last 10 seconds) - grace period to catch final messages + // 2. Workflow just completed (within last 5 seconds) - grace period to catch final messages // Stop polling for failed or stopped workflows immediately - // Use ref for statusChangedFromRunningAt to get latest value (state updates are async) const changedAtRef = statusChangedFromRunningAtRef.current; - const shouldPoll = workflowStatus === 'running' || - (workflowStatus === 'completed' && changedAtRef !== null && Date.now() - changedAtRef < 10000); + const gracePeriodMs = 5000; // 5 seconds grace period + const timeSinceCompletion = changedAtRef !== null ? Date.now() - changedAtRef : Infinity; + const isInGracePeriod = workflowStatus === 'completed' && changedAtRef !== null && timeSinceCompletion < gracePeriodMs; + const shouldPoll = workflowStatus === 'running' || isInGracePeriod; if (shouldPoll) { - // Reset lastRenderedTimestamp for first poll (fetch all historical data) - if (lastRenderedTimestampRef.current === null) { - lastRenderedTimestampRef.current = null; // null means fetch all - } - // Start polling pollingControllerRef.current.startPolling(workflowId, pollWorkflowData); + + // If in grace period, set a timer to stop polling after grace period expires + if (isInGracePeriod) { + const remainingGraceTime = gracePeriodMs - timeSinceCompletion; + const graceTimer = setTimeout(() => { + pollingControllerRef.current.stopPolling(); + setStatusChangedFromRunningAt(null); + statusChangedFromRunningAtRef.current = null; + }, remainingGraceTime + 100); // Small buffer + + return () => { + clearTimeout(graceTimer); + pollingControllerRef.current.stopPolling(); + }; + } } else { // Stop polling for failed, stopped, or completed (after grace period) workflows pollingControllerRef.current.stopPolling(); - // Clear the status change timestamp when we stop polling (only if not already null) + // Clear the status change timestamp when we stop polling if (statusChangedFromRunningAt !== null) { setStatusChangedFromRunningAt(null); statusChangedFromRunningAtRef.current = null; @@ -507,7 +518,7 @@ export function useWorkflowLifecycle(instanceId: string) { return () => { pollingControllerRef.current.stopPolling(); }; - }, [workflowStatus, workflowId, pollWorkflowData]); + }, [workflowStatus, workflowId, pollWorkflowData, statusChangedFromRunningAt]); const handleStartWorkflow = useCallback(async ( workflowData: StartWorkflowRequest, diff --git a/src/hooks/useBilling.ts b/src/hooks/useBilling.ts index 70298aa..8b79f77 100644 --- a/src/hooks/useBilling.ts +++ b/src/hooks/useBilling.ts @@ -18,6 +18,7 @@ import { addCreditAdmin, fetchAccountsAdmin, fetchTransactionsAdmin, + fetchUsersForMandateAdmin, type BillingBalance, type BillingTransaction, type BillingSettings, @@ -25,6 +26,7 @@ import { type UsageReport, type AccountSummary, type CreditAddRequest, + type MandateUserSummary, } from '../api/billingApi'; // Re-export types @@ -36,6 +38,7 @@ export type { UsageReport, AccountSummary, CreditAddRequest, + MandateUserSummary, }; export type { BillingModel, TransactionType, ReferenceType } from '../api/billingApi'; @@ -145,6 +148,7 @@ export function useBillingAdmin(mandateId?: string) { const [settings, setSettings] = useState(null); const [accounts, setAccounts] = useState([]); const [transactions, setTransactions] = useState([]); + const [users, setUsers] = useState([]); const { request, isLoading: loading, error } = useApiRequest(); // Fetch settings for a mandate @@ -232,12 +236,29 @@ export function useBillingAdmin(mandateId?: string) { } }, [request, mandateId]); + // Fetch users for a mandate + const loadUsers = useCallback(async (targetMandateId?: string) => { + const mId = targetMandateId || mandateId; + if (!mId) return []; + + try { + const data = await fetchUsersForMandateAdmin(request, mId); + setUsers(Array.isArray(data) ? data : []); + return data; + } catch (err) { + console.error('Error loading users:', err); + setUsers([]); + return []; + } + }, [request, mandateId]); + // Load data when mandateId changes useEffect(() => { if (mandateId) { loadSettings(); loadAccounts(); loadTransactions(); + loadUsers(); } }, [mandateId]); @@ -245,6 +266,7 @@ export function useBillingAdmin(mandateId?: string) { settings, accounts, transactions, + users, loading, error, loadSettings, @@ -252,11 +274,13 @@ export function useBillingAdmin(mandateId?: string) { addCredit, loadAccounts, loadTransactions, + loadUsers, refetch: () => { if (mandateId) { loadSettings(); loadAccounts(); loadTransactions(); + loadUsers(); } }, }; diff --git a/src/pages/billing/Billing.module.css b/src/pages/billing/Billing.module.css index f7c7304..35ce33a 100644 --- a/src/pages/billing/Billing.module.css +++ b/src/pages/billing/Billing.module.css @@ -5,25 +5,26 @@ ============================================================================ */ .billingDashboard { - padding: var(--spacing-lg); + padding: 1.5rem; max-width: 1200px; margin: 0 auto; + min-height: 100%; } .pageHeader { - margin-bottom: var(--spacing-xl); + margin-bottom: 2rem; } .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; + font-size: 1.5rem; + font-weight: 600; + color: var(--text-primary, #e0e0e0); + margin: 0 0 0.25rem 0; } .subtitle { - font-size: var(--font-size-base); - color: var(--color-text-secondary); + font-size: 0.875rem; + color: var(--text-secondary, #888); margin: 0; } @@ -32,23 +33,23 @@ ============================================================================ */ .section { - margin-bottom: var(--spacing-xl); + margin-bottom: 2rem; } .sectionHeader { display: flex; justify-content: space-between; align-items: center; - margin-bottom: var(--spacing-md); + margin-bottom: 1rem; flex-wrap: wrap; - gap: var(--spacing-sm); + gap: 0.5rem; } .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; + font-size: 1.125rem; + font-weight: 600; + color: var(--text-primary, #e0e0e0); + margin: 0 0 1rem 0; } .sectionHeader .sectionTitle { @@ -62,65 +63,65 @@ .balanceGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: var(--spacing-md); + gap: 1rem; } .balanceCard { - background: var(--color-bg-card); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-lg); - padding: var(--spacing-lg); + background: var(--surface-color, #1e1e1e); + border: 1px solid var(--border-color, #333); + border-radius: 12px; + padding: 1.5rem; cursor: pointer; transition: all 0.2s ease; } .balanceCard:hover { - border-color: var(--color-primary); - box-shadow: var(--shadow-md); + border-color: var(--primary-color, #f25843); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); } .balanceCard.warning { - border-color: var(--color-warning); - background: var(--color-warning-bg, rgba(255, 193, 7, 0.1)); + border-color: #ffc107; + background: rgba(255, 193, 7, 0.1); } .balanceHeader { display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: var(--spacing-md); + margin-bottom: 1rem; } .mandateName { - font-size: var(--font-size-base); - font-weight: var(--font-weight-semibold); - color: var(--color-text-primary); + font-size: 0.875rem; + font-weight: 600; + color: var(--text-primary, #e0e0e0); margin: 0; } .billingModel { - font-size: var(--font-size-xs); - color: var(--color-text-secondary); - background: var(--color-bg-secondary); + font-size: 0.75rem; + color: var(--text-secondary, #888); + background: var(--bg-secondary, #2a2a2a); padding: 2px 8px; - border-radius: var(--border-radius-sm); + border-radius: 4px; } .balanceAmount { - font-size: var(--font-size-2xl); - font-weight: var(--font-weight-bold); - color: var(--color-text-primary); - margin-bottom: var(--spacing-sm); + font-size: 1.5rem; + font-weight: 700; + color: var(--text-primary, #e0e0e0); + margin-bottom: 0.5rem; } .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)); + font-size: 0.75rem; + color: #856404; + background: rgba(255, 193, 7, 0.3); padding: 4px 8px; - border-radius: var(--border-radius-sm); - font-weight: var(--font-weight-medium); + border-radius: 4px; + font-weight: 500; } /* ============================================================================ @@ -129,56 +130,56 @@ .periodSelector { display: flex; - gap: var(--spacing-sm); + gap: 0.5rem; 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); + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color, #333); + border-radius: 6px; + background: var(--surface-color, #1e1e1e); + color: var(--text-primary, #e0e0e0); + font-size: 0.875rem; cursor: pointer; } .select:focus { outline: none; - border-color: var(--color-primary); + border-color: var(--primary-color, #f25843); } .statisticsChart { - background: var(--color-bg-card); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-lg); - padding: var(--spacing-lg); + background: var(--surface-color, #1e1e1e); + border: 1px solid var(--border-color, #333); + border-radius: 12px; + padding: 1.5rem; } .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); + padding: 1.5rem; + background: var(--bg-secondary, #2a2a2a); + border-radius: 8px; + margin-bottom: 1.5rem; } .totalLabel { - font-size: var(--font-size-sm); - color: var(--color-text-secondary); - margin-bottom: var(--spacing-xs); + font-size: 0.875rem; + color: var(--text-secondary, #888); + margin-bottom: 0.25rem; } .totalAmount { - font-size: var(--font-size-3xl); - font-weight: var(--font-weight-bold); - color: var(--color-text-primary); + font-size: 2rem; + font-weight: 700; + color: var(--text-primary, #e0e0e0); } .chartSection { - margin-bottom: var(--spacing-lg); + margin-bottom: 1.5rem; } .chartSection:last-child { @@ -186,10 +187,10 @@ } .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; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-secondary, #888); + margin: 0 0 1rem 0; text-transform: uppercase; letter-spacing: 0.5px; } @@ -197,34 +198,34 @@ .barChart { display: flex; flex-direction: column; - gap: var(--spacing-sm); + gap: 0.5rem; } .barRow { display: flex; align-items: center; - gap: var(--spacing-sm); + gap: 0.5rem; } .barLabel { width: 100px; - font-size: var(--font-size-sm); - color: var(--color-text-primary); + font-size: 0.875rem; + color: var(--text-primary, #e0e0e0); text-transform: capitalize; } .barContainer { flex: 1; height: 24px; - background: var(--color-bg-secondary); - border-radius: var(--border-radius-sm); + background: var(--bg-secondary, #2a2a2a); + border-radius: 4px; overflow: hidden; } .bar { height: 100%; - background: var(--color-primary); - border-radius: var(--border-radius-sm); + background: var(--primary-color, #f25843); + border-radius: 4px; transition: width 0.3s ease; min-width: 4px; } @@ -232,36 +233,36 @@ .barValue { width: 100px; text-align: right; - font-size: var(--font-size-sm); - color: var(--color-text-secondary); - font-family: var(--font-mono); + font-size: 0.875rem; + color: var(--text-secondary, #888); + font-family: monospace; } .featureList { display: flex; flex-direction: column; - gap: var(--spacing-xs); + gap: 0.25rem; } .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); + padding: 0.5rem; + background: var(--bg-secondary, #2a2a2a); + border-radius: 4px; } .featureLabel { - font-size: var(--font-size-sm); - color: var(--color-text-primary); + font-size: 0.875rem; + color: var(--text-primary, #e0e0e0); text-transform: capitalize; } .featureValue { - font-size: var(--font-size-sm); - color: var(--color-text-secondary); - font-family: var(--font-mono); + font-size: 0.875rem; + color: var(--text-secondary, #888); + font-family: monospace; } /* ============================================================================ @@ -275,46 +276,46 @@ .transactionsTable th, .transactionsTable td { - padding: var(--spacing-sm) var(--spacing-md); + padding: 0.75rem 1rem; text-align: left; - border-bottom: 1px solid var(--color-border); + border-bottom: 1px solid var(--border-color, #333); } .transactionsTable th { - font-size: var(--font-size-xs); - font-weight: var(--font-weight-semibold); - color: var(--color-text-secondary); + font-size: 0.75rem; + font-weight: 600; + color: var(--text-secondary, #888); text-transform: uppercase; letter-spacing: 0.5px; - background: var(--color-bg-secondary); + background: var(--bg-secondary, #2a2a2a); } .transactionsTable td { - font-size: var(--font-size-sm); - color: var(--color-text-primary); + font-size: 0.875rem; + color: var(--text-primary, #e0e0e0); } .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); + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; } .transactionType.credit { - background: var(--color-success-bg, rgba(40, 167, 69, 0.1)); - color: var(--color-success, #28a745); + background: rgba(40, 167, 69, 0.1); + color: #28a745; } .transactionType.debit { - background: var(--color-error-bg, rgba(220, 53, 69, 0.1)); - color: var(--color-error, #dc3545); + background: rgba(220, 53, 69, 0.1); + color: #dc3545; } .transactionType.adjustment { - background: var(--color-info-bg, rgba(23, 162, 184, 0.1)); - color: var(--color-info, #17a2b8); + background: rgba(23, 162, 184, 0.1); + color: #17a2b8; } /* ============================================================================ @@ -322,86 +323,86 @@ ============================================================================ */ .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); + background: var(--surface-color, #1e1e1e); + border: 1px solid var(--border-color, #333); + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1.5rem; } .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; + font-size: 1rem; + font-weight: 600; + color: var(--text-primary, #e0e0e0); + margin: 0 0 1rem 0; } .formRow { display: flex; - gap: var(--spacing-md); - margin-bottom: var(--spacing-md); + gap: 1rem; + margin-bottom: 1rem; } .formGroup { flex: 1; display: flex; flex-direction: column; - gap: var(--spacing-xs); + gap: 0.25rem; } .formGroup label { - font-size: var(--font-size-sm); - font-weight: var(--font-weight-medium); - color: var(--color-text-secondary); + font-size: 0.875rem; + font-weight: 500; + color: var(--text-secondary, #888); } .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); + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color, #333); + border-radius: 6px; + background: var(--surface-color, #1e1e1e); + color: var(--text-primary, #e0e0e0); + font-size: 0.875rem; } .input:focus { outline: none; - border-color: var(--color-primary); + border-color: var(--primary-color, #f25843); } .accountsGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); - gap: var(--spacing-md); + gap: 1rem; } .accountCard { - background: var(--color-bg-secondary); - border: 1px solid var(--color-border); - border-radius: var(--border-radius-md); - padding: var(--spacing-md); + background: var(--bg-secondary, #2a2a2a); + border: 1px solid var(--border-color, #333); + border-radius: 8px; + padding: 1rem; } .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; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-primary, #e0e0e0); + margin: 0 0 0.5rem 0; } .accountInfo { display: flex; flex-direction: column; - gap: var(--spacing-xs); - font-size: var(--font-size-sm); + gap: 0.25rem; + font-size: 0.875rem; } .accountInfo span { - color: var(--color-text-secondary); + color: var(--text-secondary, #888); } .accountInfo strong { - color: var(--color-text-primary); + color: var(--text-primary, #e0e0e0); } /* ============================================================================ @@ -409,22 +410,22 @@ ============================================================================ */ .button { - padding: var(--spacing-sm) var(--spacing-md); + padding: 0.5rem 1rem; border: none; - border-radius: var(--border-radius-md); - font-size: var(--font-size-sm); - font-weight: var(--font-weight-medium); + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; cursor: pointer; transition: all 0.2s ease; } .buttonPrimary { - background: var(--color-primary); + background: var(--primary-color, #f25843); color: white; } .buttonPrimary:hover { - background: var(--color-primary-dark); + background: var(--primary-dark, #d94d3a); } .buttonPrimary:disabled { @@ -433,13 +434,13 @@ } .buttonSecondary { - background: var(--color-bg-secondary); - color: var(--color-text-primary); - border: 1px solid var(--color-border); + background: var(--bg-secondary, #2a2a2a); + color: var(--text-primary, #e0e0e0); + border: 1px solid var(--border-color, #333); } .buttonSecondary:hover { - background: var(--color-bg-hover); + background: var(--surface-color, #1e1e1e); } /* ============================================================================ @@ -450,37 +451,37 @@ display: flex; align-items: center; justify-content: center; - padding: var(--spacing-xl); - color: var(--color-text-secondary); - font-size: var(--font-size-sm); + padding: 2rem; + color: var(--text-secondary, #888); + font-size: 0.875rem; } .noData { display: flex; align-items: center; justify-content: center; - padding: var(--spacing-lg); - color: var(--color-text-tertiary); - font-size: var(--font-size-sm); + padding: 1.5rem; + color: var(--text-tertiary, #666); + font-size: 0.875rem; 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); + background: rgba(220, 53, 69, 0.1); + color: #dc3545; + padding: 0.75rem 1rem; + border-radius: 6px; + font-size: 0.875rem; + margin-bottom: 1rem; } .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); + background: rgba(40, 167, 69, 0.1); + color: #28a745; + padding: 0.75rem 1rem; + border-radius: 6px; + font-size: 0.875rem; + margin-bottom: 1rem; } /* ============================================================================ @@ -489,7 +490,7 @@ @media (max-width: 768px) { .billingDashboard { - padding: var(--spacing-md); + padding: 1rem; } .balanceGrid { @@ -513,6 +514,6 @@ .barLabel, .barValue { width: 80px; - font-size: var(--font-size-xs); + font-size: 0.75rem; } } diff --git a/src/pages/billing/BillingAdmin.tsx b/src/pages/billing/BillingAdmin.tsx index fc16ac7..1cddd88 100644 --- a/src/pages/billing/BillingAdmin.tsx +++ b/src/pages/billing/BillingAdmin.tsx @@ -8,7 +8,7 @@ */ import React, { useState, useEffect, useCallback } from 'react'; -import { useBillingAdmin, type BillingSettings, type AccountSummary } from '../../hooks/useBilling'; +import { useBillingAdmin, type BillingSettings, type AccountSummary, type MandateUserSummary } from '../../hooks/useBilling'; import { useAdminMandates } from '../../hooks/useMandates'; import styles from './Billing.module.css'; @@ -191,10 +191,11 @@ const SettingsEditor: React.FC = ({ settings, onSave, loadi interface CreditAdderProps { settings: BillingSettings | null; accounts: AccountSummary[]; + users: MandateUserSummary[]; onAddCredit: (userId: string | undefined, amount: number, description: string) => Promise; } -const CreditAdder: React.FC = ({ settings, accounts, onAddCredit }) => { +const CreditAdder: React.FC = ({ settings, accounts, users, onAddCredit }) => { const [selectedUserId, setSelectedUserId] = useState(''); const [amount, setAmount] = useState(10); const [description, setDescription] = useState('Manuelles Aufladen'); @@ -203,6 +204,14 @@ const CreditAdder: React.FC = ({ settings, accounts, onAddCred const isPrepayUser = settings?.billingModel === 'PREPAY_USER'; + // Map accounts by userId for balance lookup + const accountsByUserId = accounts + .filter(acc => acc.accountType === 'USER') + .reduce((map, acc) => { + if (acc.userId) map[acc.userId] = acc; + return map; + }, {} as Record); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (amount <= 0) { @@ -246,21 +255,23 @@ const CreditAdder: React.FC = ({ settings, accounts, onAddCred {isPrepayUser && (
- +
@@ -355,7 +366,7 @@ const AccountsOverview: React.FC = ({ accounts, loading } export const BillingAdmin: React.FC = () => { const [selectedMandateId, setSelectedMandateId] = useState(null); - const { settings, accounts, loading, loadSettings, saveSettings, addCredit, loadAccounts } = useBillingAdmin(selectedMandateId || undefined); + const { settings, accounts, users, loading, loadSettings, saveSettings, addCredit, loadAccounts } = useBillingAdmin(selectedMandateId || undefined); const handleMandateSelect = (mandateId: string) => { setSelectedMandateId(mandateId || null); @@ -397,6 +408,7 @@ export const BillingAdmin: React.FC = () => { diff --git a/src/pages/workflows/PlaygroundPage.tsx b/src/pages/workflows/PlaygroundPage.tsx index 4cf2b8a..0733329 100644 --- a/src/pages/workflows/PlaygroundPage.tsx +++ b/src/pages/workflows/PlaygroundPage.tsx @@ -15,7 +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 { ProviderMultiSelect } from '../../components/ProviderSelector'; import type { Message } from '../../components/UiComponents/Messages/MessagesTypes'; import api from '../../api'; import styles from './PlaygroundPage.module.css'; @@ -58,8 +58,8 @@ export const PlaygroundPage: React.FC = () => { deletingFiles, previewingFiles, downloadingFiles, - selectedProvider, - onProviderSelect, + selectedProviders, + onProvidersChange, } = hookData; const { prompts, refetch: refetchPrompts } = usePrompts(); @@ -544,9 +544,6 @@ export const PlaygroundPage: React.FC = () => { ); }; - // Debug: Log permission status - console.log('🔐 PlaygroundPage permission check:', { playgroundUIPermission }); - // Permission check - also show while loading if (playgroundUIPermission === false) { return ( @@ -785,9 +782,9 @@ export const PlaygroundPage: React.FC = () => { > -