billing fixes
This commit is contained in:
parent
46e58bf019
commit
a544ab2c78
10 changed files with 482 additions and 257 deletions
|
|
@ -252,3 +252,28 @@ export async function fetchTransactionsAdmin(
|
||||||
params: { limit }
|
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<MandateUserSummary[]> {
|
||||||
|
return await request({
|
||||||
|
url: `/api/billing/admin/users/${mandateId}`,
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@ export const UserSection: React.FC = () => {
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBilling = () => {
|
||||||
|
navigate('/billing');
|
||||||
|
setShowMenu(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleLegal = () => {
|
const handleLegal = () => {
|
||||||
setShowLegalModal(true);
|
setShowLegalModal(true);
|
||||||
setShowMenu(false);
|
setShowMenu(false);
|
||||||
|
|
@ -72,6 +77,14 @@ export const UserSection: React.FC = () => {
|
||||||
|
|
||||||
{showMenu && (
|
{showMenu && (
|
||||||
<div className={styles.menu}>
|
<div className={styles.menu}>
|
||||||
|
<button
|
||||||
|
className={styles.menuItem}
|
||||||
|
onClick={handleBilling}
|
||||||
|
>
|
||||||
|
<span className={styles.menuIcon}>💰</span>
|
||||||
|
Guthaben
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className={styles.menuItem}
|
className={styles.menuItem}
|
||||||
onClick={handleSettings}
|
onClick={handleSettings}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,110 @@
|
||||||
.providerMultiSelect {
|
.providerMultiSelect {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-sm, 8px);
|
gap: var(--spacing-xs, 4px);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.providerMultiSelect.collapsed {
|
||||||
|
/* Collapsed state styles */
|
||||||
|
}
|
||||||
|
|
||||||
|
.providerMultiSelect.expanded {
|
||||||
|
/* Expanded state styles */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapsible Header Button */
|
||||||
|
.collapseHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs, 4px);
|
||||||
|
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;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapseHeader:hover:not(:disabled) {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background: var(--color-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapseHeader:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryIcons {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryText {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandIcon {
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Expandable Content - opens upward */
|
||||||
|
.expandableContent {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
margin-bottom: var(--spacing-xs, 4px);
|
||||||
|
padding: var(--spacing-sm, 8px);
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: var(--border-radius-md, 6px);
|
||||||
|
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.4);
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandableContent .label {
|
||||||
|
color: #b0b0b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandableContent .checkboxList {
|
||||||
|
background: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandableContent .checkboxItem {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandableContent .checkboxItem:hover {
|
||||||
|
background: #3a3a3a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandableContent .providerName {
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandableContent .hint {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandableContent .actionButton {
|
||||||
|
background: #3a3a3a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expandableContent .actionButton:hover:not(:disabled) {
|
||||||
|
background: #4a4a4a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectActions {
|
.selectActions {
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
* - Lädt verfügbare Provider aus dem Billing-System
|
* - Lädt verfügbare Provider aus dem Billing-System
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useBilling } from '../../hooks/useBilling';
|
import { useBilling } from '../../hooks/useBilling';
|
||||||
import styles from './ProviderSelector.module.css';
|
import styles from './ProviderSelector.module.css';
|
||||||
|
|
||||||
|
|
@ -99,6 +99,7 @@ interface ProviderMultiSelectProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
|
defaultExpanded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
|
|
@ -106,9 +107,11 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
onChange,
|
onChange,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
className,
|
className,
|
||||||
label = 'Erlaubte AI-Provider',
|
label = 'AI-Provider',
|
||||||
showLabel = true,
|
showLabel = true,
|
||||||
|
defaultExpanded = false,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [isExpanded, setIsExpanded] = useState(defaultExpanded);
|
||||||
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
const { allowedProviders, loadAllowedProviders, loading } = useBilling();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -133,56 +136,92 @@ export const ProviderMultiSelect: React.FC<ProviderMultiSelectProps> = ({
|
||||||
onChange([]);
|
onChange([]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Summary text for collapsed view
|
||||||
|
const summaryText = useMemo(() => {
|
||||||
|
if (selectedProviders.length === 0) {
|
||||||
|
return 'Alle Provider';
|
||||||
|
}
|
||||||
|
if (selectedProviders.length === 1) {
|
||||||
|
return PROVIDER_LABELS[selectedProviders[0]] || selectedProviders[0];
|
||||||
|
}
|
||||||
|
return `${selectedProviders.length} Provider`;
|
||||||
|
}, [selectedProviders]);
|
||||||
|
|
||||||
|
// Summary icons for collapsed view
|
||||||
|
const summaryIcons = useMemo(() => {
|
||||||
|
if (selectedProviders.length === 0) {
|
||||||
|
return '🤖';
|
||||||
|
}
|
||||||
|
return selectedProviders.slice(0, 3).map(p => PROVIDER_ICONS[p] || '🔌').join('');
|
||||||
|
}, [selectedProviders]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${styles.providerMultiSelect} ${className || ''}`}>
|
<div className={`${styles.providerMultiSelect} ${className || ''} ${isExpanded ? styles.expanded : styles.collapsed}`}>
|
||||||
{showLabel && <label className={styles.label}>{label}</label>}
|
{/* Collapsible Header */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.collapseHeader}
|
||||||
|
onClick={() => setIsExpanded(!isExpanded)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<span className={styles.summaryIcons}>{summaryIcons}</span>
|
||||||
|
<span className={styles.summaryText}>{summaryText}</span>
|
||||||
|
<span className={styles.expandIcon}>{isExpanded ? '▲' : '▼'}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className={styles.selectActions}>
|
{/* Expandable Content */}
|
||||||
<button
|
{isExpanded && (
|
||||||
type="button"
|
<div className={styles.expandableContent}>
|
||||||
onClick={handleSelectAll}
|
{showLabel && <label className={styles.label}>{label}</label>}
|
||||||
disabled={disabled}
|
|
||||||
className={styles.actionButton}
|
<div className={styles.selectActions}>
|
||||||
>
|
<button
|
||||||
Alle
|
type="button"
|
||||||
</button>
|
onClick={handleSelectAll}
|
||||||
<button
|
disabled={disabled}
|
||||||
type="button"
|
className={styles.actionButton}
|
||||||
onClick={handleSelectNone}
|
|
||||||
disabled={disabled}
|
|
||||||
className={styles.actionButton}
|
|
||||||
>
|
|
||||||
Keine
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className={styles.loading}>Lade Provider...</div>
|
|
||||||
) : (
|
|
||||||
<div className={styles.checkboxList}>
|
|
||||||
{allowedProviders.map((provider) => (
|
|
||||||
<label
|
|
||||||
key={provider}
|
|
||||||
className={`${styles.checkboxItem} ${disabled ? styles.disabled : ''}`}
|
|
||||||
>
|
>
|
||||||
<input
|
Alle
|
||||||
type="checkbox"
|
</button>
|
||||||
checked={selectedProviders.includes(provider)}
|
<button
|
||||||
onChange={() => handleToggle(provider)}
|
type="button"
|
||||||
disabled={disabled}
|
onClick={handleSelectNone}
|
||||||
/>
|
disabled={disabled}
|
||||||
<span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span>
|
className={styles.actionButton}
|
||||||
<span className={styles.providerName}>
|
>
|
||||||
{PROVIDER_LABELS[provider] || provider}
|
Keine
|
||||||
</span>
|
</button>
|
||||||
</label>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
{loading ? (
|
||||||
)}
|
<div className={styles.loading}>Lade Provider...</div>
|
||||||
|
) : (
|
||||||
{selectedProviders.length === 0 && !loading && (
|
<div className={styles.checkboxList}>
|
||||||
<div className={styles.hint}>
|
{allowedProviders.map((provider) => (
|
||||||
Wenn keine Provider ausgewählt sind, werden alle erlaubten Provider verwendet.
|
<label
|
||||||
|
key={provider}
|
||||||
|
className={`${styles.checkboxItem} ${disabled ? styles.disabled : ''}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedProviders.includes(provider)}
|
||||||
|
onChange={() => handleToggle(provider)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<span className={styles.icon}>{PROVIDER_ICONS[provider] || '🔌'}</span>
|
||||||
|
<span className={styles.providerName}>
|
||||||
|
{PROVIDER_LABELS[provider] || provider}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedProviders.length === 0 && !loading && (
|
||||||
|
<div className={styles.hint}>
|
||||||
|
Wenn keine Provider ausgewählt sind, werden alle erlaubten Provider verwendet.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ export function useDashboardInputForm(instanceId: string) {
|
||||||
const [optimisticMessage, setOptimisticMessage] = useState<WorkflowMessage | null>(null);
|
const [optimisticMessage, setOptimisticMessage] = useState<WorkflowMessage | null>(null);
|
||||||
const [selectedPromptId, setSelectedPromptId] = useState<string | null>(null);
|
const [selectedPromptId, setSelectedPromptId] = useState<string | null>(null);
|
||||||
const [workflowMode, setWorkflowMode] = useState<'Dynamic' | 'Automation' | null>(null);
|
const [workflowMode, setWorkflowMode] = useState<'Dynamic' | 'Automation' | null>(null);
|
||||||
const [selectedProvider, setSelectedProvider] = useState<string>(''); // AI provider selection
|
const [selectedProviders, setSelectedProviders] = useState<string[]>([]); // AI provider selection (multiselect)
|
||||||
|
|
||||||
const { checkPermission, canView } = usePermissions();
|
const { checkPermission, canView } = usePermissions();
|
||||||
const [playgroundUIPermission, setPlaygroundUIPermission] = useState<boolean>(false);
|
const [playgroundUIPermission, setPlaygroundUIPermission] = useState<boolean>(false);
|
||||||
|
|
@ -596,7 +596,7 @@ export function useDashboardInputForm(instanceId: string) {
|
||||||
prompt: trimmedInput,
|
prompt: trimmedInput,
|
||||||
listFileId: fileIdsToSend.length > 0 ? fileIdsToSend : undefined,
|
listFileId: fileIdsToSend.length > 0 ? fileIdsToSend : undefined,
|
||||||
userLanguage: 'en',
|
userLanguage: 'en',
|
||||||
preferredProvider: selectedProvider || undefined // AI provider selection
|
preferredProviders: selectedProviders.length > 0 ? selectedProviders : undefined // AI provider selection (multiselect)
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await startWorkflow(requestBody, workflowOptions);
|
const result = await startWorkflow(requestBody, workflowOptions);
|
||||||
|
|
@ -638,7 +638,7 @@ export function useDashboardInputForm(instanceId: string) {
|
||||||
setWorkflowStatusOptimistic('idle');
|
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(() => {
|
useEffect(() => {
|
||||||
const handleWorkflowCleared = () => {
|
const handleWorkflowCleared = () => {
|
||||||
|
|
@ -823,9 +823,9 @@ export function useDashboardInputForm(instanceId: string) {
|
||||||
handleFileAttach,
|
handleFileAttach,
|
||||||
handleFileUploadAndAttach,
|
handleFileUploadAndAttach,
|
||||||
latestStats,
|
latestStats,
|
||||||
// AI Provider selection
|
// AI Provider selection (multiselect)
|
||||||
selectedProvider,
|
selectedProviders,
|
||||||
onProviderSelect: setSelectedProvider
|
onProvidersChange: setSelectedProviders
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -479,25 +479,36 @@ export function useWorkflowLifecycle(instanceId: string) {
|
||||||
|
|
||||||
// Continue polling if:
|
// Continue polling if:
|
||||||
// 1. Workflow is currently running, OR
|
// 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
|
// 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 changedAtRef = statusChangedFromRunningAtRef.current;
|
||||||
const shouldPoll = workflowStatus === 'running' ||
|
const gracePeriodMs = 5000; // 5 seconds grace period
|
||||||
(workflowStatus === 'completed' && changedAtRef !== null && Date.now() - changedAtRef < 10000);
|
const timeSinceCompletion = changedAtRef !== null ? Date.now() - changedAtRef : Infinity;
|
||||||
|
const isInGracePeriod = workflowStatus === 'completed' && changedAtRef !== null && timeSinceCompletion < gracePeriodMs;
|
||||||
|
const shouldPoll = workflowStatus === 'running' || isInGracePeriod;
|
||||||
|
|
||||||
if (shouldPoll) {
|
if (shouldPoll) {
|
||||||
// Reset lastRenderedTimestamp for first poll (fetch all historical data)
|
|
||||||
if (lastRenderedTimestampRef.current === null) {
|
|
||||||
lastRenderedTimestampRef.current = null; // null means fetch all
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start polling
|
// Start polling
|
||||||
pollingControllerRef.current.startPolling(workflowId, pollWorkflowData);
|
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 {
|
} else {
|
||||||
// Stop polling for failed, stopped, or completed (after grace period) workflows
|
// Stop polling for failed, stopped, or completed (after grace period) workflows
|
||||||
pollingControllerRef.current.stopPolling();
|
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) {
|
if (statusChangedFromRunningAt !== null) {
|
||||||
setStatusChangedFromRunningAt(null);
|
setStatusChangedFromRunningAt(null);
|
||||||
statusChangedFromRunningAtRef.current = null;
|
statusChangedFromRunningAtRef.current = null;
|
||||||
|
|
@ -507,7 +518,7 @@ export function useWorkflowLifecycle(instanceId: string) {
|
||||||
return () => {
|
return () => {
|
||||||
pollingControllerRef.current.stopPolling();
|
pollingControllerRef.current.stopPolling();
|
||||||
};
|
};
|
||||||
}, [workflowStatus, workflowId, pollWorkflowData]);
|
}, [workflowStatus, workflowId, pollWorkflowData, statusChangedFromRunningAt]);
|
||||||
|
|
||||||
const handleStartWorkflow = useCallback(async (
|
const handleStartWorkflow = useCallback(async (
|
||||||
workflowData: StartWorkflowRequest,
|
workflowData: StartWorkflowRequest,
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
addCreditAdmin,
|
addCreditAdmin,
|
||||||
fetchAccountsAdmin,
|
fetchAccountsAdmin,
|
||||||
fetchTransactionsAdmin,
|
fetchTransactionsAdmin,
|
||||||
|
fetchUsersForMandateAdmin,
|
||||||
type BillingBalance,
|
type BillingBalance,
|
||||||
type BillingTransaction,
|
type BillingTransaction,
|
||||||
type BillingSettings,
|
type BillingSettings,
|
||||||
|
|
@ -25,6 +26,7 @@ import {
|
||||||
type UsageReport,
|
type UsageReport,
|
||||||
type AccountSummary,
|
type AccountSummary,
|
||||||
type CreditAddRequest,
|
type CreditAddRequest,
|
||||||
|
type MandateUserSummary,
|
||||||
} from '../api/billingApi';
|
} from '../api/billingApi';
|
||||||
|
|
||||||
// Re-export types
|
// Re-export types
|
||||||
|
|
@ -36,6 +38,7 @@ export type {
|
||||||
UsageReport,
|
UsageReport,
|
||||||
AccountSummary,
|
AccountSummary,
|
||||||
CreditAddRequest,
|
CreditAddRequest,
|
||||||
|
MandateUserSummary,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { BillingModel, TransactionType, ReferenceType } from '../api/billingApi';
|
export type { BillingModel, TransactionType, ReferenceType } from '../api/billingApi';
|
||||||
|
|
@ -145,6 +148,7 @@ export function useBillingAdmin(mandateId?: string) {
|
||||||
const [settings, setSettings] = useState<BillingSettings | null>(null);
|
const [settings, setSettings] = useState<BillingSettings | null>(null);
|
||||||
const [accounts, setAccounts] = useState<AccountSummary[]>([]);
|
const [accounts, setAccounts] = useState<AccountSummary[]>([]);
|
||||||
const [transactions, setTransactions] = useState<BillingTransaction[]>([]);
|
const [transactions, setTransactions] = useState<BillingTransaction[]>([]);
|
||||||
|
const [users, setUsers] = useState<MandateUserSummary[]>([]);
|
||||||
const { request, isLoading: loading, error } = useApiRequest();
|
const { request, isLoading: loading, error } = useApiRequest();
|
||||||
|
|
||||||
// Fetch settings for a mandate
|
// Fetch settings for a mandate
|
||||||
|
|
@ -232,12 +236,29 @@ export function useBillingAdmin(mandateId?: string) {
|
||||||
}
|
}
|
||||||
}, [request, mandateId]);
|
}, [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
|
// Load data when mandateId changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mandateId) {
|
if (mandateId) {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
loadTransactions();
|
loadTransactions();
|
||||||
|
loadUsers();
|
||||||
}
|
}
|
||||||
}, [mandateId]);
|
}, [mandateId]);
|
||||||
|
|
||||||
|
|
@ -245,6 +266,7 @@ export function useBillingAdmin(mandateId?: string) {
|
||||||
settings,
|
settings,
|
||||||
accounts,
|
accounts,
|
||||||
transactions,
|
transactions,
|
||||||
|
users,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
loadSettings,
|
loadSettings,
|
||||||
|
|
@ -252,11 +274,13 @@ export function useBillingAdmin(mandateId?: string) {
|
||||||
addCredit,
|
addCredit,
|
||||||
loadAccounts,
|
loadAccounts,
|
||||||
loadTransactions,
|
loadTransactions,
|
||||||
|
loadUsers,
|
||||||
refetch: () => {
|
refetch: () => {
|
||||||
if (mandateId) {
|
if (mandateId) {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
loadTransactions();
|
loadTransactions();
|
||||||
|
loadUsers();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,26 @@
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
|
|
||||||
.billingDashboard {
|
.billingDashboard {
|
||||||
padding: var(--spacing-lg);
|
padding: 1.5rem;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageHeader {
|
.pageHeader {
|
||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pageHeader h1 {
|
.pageHeader h1 {
|
||||||
font-size: var(--font-size-2xl);
|
font-size: 1.5rem;
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: 600;
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
margin: 0 0 var(--spacing-xs) 0;
|
margin: 0 0 0.25rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
font-size: var(--font-size-base);
|
font-size: 0.875rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--text-secondary, #888);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,23 +33,23 @@
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionHeader {
|
.sectionHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: var(--spacing-md);
|
margin-bottom: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: var(--spacing-sm);
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionTitle {
|
.sectionTitle {
|
||||||
font-size: var(--font-size-lg);
|
font-size: 1.125rem;
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: 600;
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
margin: 0 0 var(--spacing-md) 0;
|
margin: 0 0 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sectionHeader .sectionTitle {
|
.sectionHeader .sectionTitle {
|
||||||
|
|
@ -62,65 +63,65 @@
|
||||||
.balanceGrid {
|
.balanceGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: var(--spacing-md);
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.balanceCard {
|
.balanceCard {
|
||||||
background: var(--color-bg-card);
|
background: var(--surface-color, #1e1e1e);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--border-color, #333);
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: 12px;
|
||||||
padding: var(--spacing-lg);
|
padding: 1.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.balanceCard:hover {
|
.balanceCard:hover {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--primary-color, #f25843);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.balanceCard.warning {
|
.balanceCard.warning {
|
||||||
border-color: var(--color-warning);
|
border-color: #ffc107;
|
||||||
background: var(--color-warning-bg, rgba(255, 193, 7, 0.1));
|
background: rgba(255, 193, 7, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.balanceHeader {
|
.balanceHeader {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
margin-bottom: var(--spacing-md);
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mandateName {
|
.mandateName {
|
||||||
font-size: var(--font-size-base);
|
font-size: 0.875rem;
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: 600;
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.billingModel {
|
.billingModel {
|
||||||
font-size: var(--font-size-xs);
|
font-size: 0.75rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--text-secondary, #888);
|
||||||
background: var(--color-bg-secondary);
|
background: var(--bg-secondary, #2a2a2a);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.balanceAmount {
|
.balanceAmount {
|
||||||
font-size: var(--font-size-2xl);
|
font-size: 1.5rem;
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: 700;
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
margin-bottom: var(--spacing-sm);
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.warningBadge {
|
.warningBadge {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: var(--font-size-xs);
|
font-size: 0.75rem;
|
||||||
color: var(--color-warning-text, #856404);
|
color: #856404;
|
||||||
background: var(--color-warning-badge-bg, rgba(255, 193, 7, 0.3));
|
background: rgba(255, 193, 7, 0.3);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: 4px;
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
|
|
@ -129,56 +130,56 @@
|
||||||
|
|
||||||
.periodSelector {
|
.periodSelector {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-sm);
|
gap: 0.5rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select {
|
.select {
|
||||||
padding: var(--spacing-xs) var(--spacing-sm);
|
padding: 0.5rem 0.75rem;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--border-color, #333);
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: 6px;
|
||||||
background: var(--color-bg-input);
|
background: var(--surface-color, #1e1e1e);
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select:focus {
|
.select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--color-primary);
|
border-color: var(--primary-color, #f25843);
|
||||||
}
|
}
|
||||||
|
|
||||||
.statisticsChart {
|
.statisticsChart {
|
||||||
background: var(--color-bg-card);
|
background: var(--surface-color, #1e1e1e);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--border-color, #333);
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: 12px;
|
||||||
padding: var(--spacing-lg);
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totalCost {
|
.totalCost {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--spacing-lg);
|
padding: 1.5rem;
|
||||||
background: var(--color-bg-secondary);
|
background: var(--bg-secondary, #2a2a2a);
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: 8px;
|
||||||
margin-bottom: var(--spacing-lg);
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totalLabel {
|
.totalLabel {
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--text-secondary, #888);
|
||||||
margin-bottom: var(--spacing-xs);
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totalAmount {
|
.totalAmount {
|
||||||
font-size: var(--font-size-3xl);
|
font-size: 2rem;
|
||||||
font-weight: var(--font-weight-bold);
|
font-weight: 700;
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chartSection {
|
.chartSection {
|
||||||
margin-bottom: var(--spacing-lg);
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chartSection:last-child {
|
.chartSection:last-child {
|
||||||
|
|
@ -186,10 +187,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.chartSection h4 {
|
.chartSection h4 {
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: 600;
|
||||||
color: var(--color-text-secondary);
|
color: var(--text-secondary, #888);
|
||||||
margin: 0 0 var(--spacing-md) 0;
|
margin: 0 0 1rem 0;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
@ -197,34 +198,34 @@
|
||||||
.barChart {
|
.barChart {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-sm);
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.barRow {
|
.barRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-sm);
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.barLabel {
|
.barLabel {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.barContainer {
|
.barContainer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
background: var(--color-bg-secondary);
|
background: var(--bg-secondary, #2a2a2a);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar {
|
.bar {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: var(--color-primary);
|
background: var(--primary-color, #f25843);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: 4px;
|
||||||
transition: width 0.3s ease;
|
transition: width 0.3s ease;
|
||||||
min-width: 4px;
|
min-width: 4px;
|
||||||
}
|
}
|
||||||
|
|
@ -232,36 +233,36 @@
|
||||||
.barValue {
|
.barValue {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--text-secondary, #888);
|
||||||
font-family: var(--font-mono);
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.featureList {
|
.featureList {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-xs);
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.featureRow {
|
.featureRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: var(--spacing-sm);
|
padding: 0.5rem;
|
||||||
background: var(--color-bg-secondary);
|
background: var(--bg-secondary, #2a2a2a);
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.featureLabel {
|
.featureLabel {
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.featureValue {
|
.featureValue {
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--text-secondary, #888);
|
||||||
font-family: var(--font-mono);
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
|
|
@ -275,46 +276,46 @@
|
||||||
|
|
||||||
.transactionsTable th,
|
.transactionsTable th,
|
||||||
.transactionsTable td {
|
.transactionsTable td {
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
padding: 0.75rem 1rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--border-color, #333);
|
||||||
}
|
}
|
||||||
|
|
||||||
.transactionsTable th {
|
.transactionsTable th {
|
||||||
font-size: var(--font-size-xs);
|
font-size: 0.75rem;
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: 600;
|
||||||
color: var(--color-text-secondary);
|
color: var(--text-secondary, #888);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
background: var(--color-bg-secondary);
|
background: var(--bg-secondary, #2a2a2a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.transactionsTable td {
|
.transactionsTable td {
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.transactionType {
|
.transactionType {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
border-radius: var(--border-radius-sm);
|
border-radius: 4px;
|
||||||
font-size: var(--font-size-xs);
|
font-size: 0.75rem;
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transactionType.credit {
|
.transactionType.credit {
|
||||||
background: var(--color-success-bg, rgba(40, 167, 69, 0.1));
|
background: rgba(40, 167, 69, 0.1);
|
||||||
color: var(--color-success, #28a745);
|
color: #28a745;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transactionType.debit {
|
.transactionType.debit {
|
||||||
background: var(--color-error-bg, rgba(220, 53, 69, 0.1));
|
background: rgba(220, 53, 69, 0.1);
|
||||||
color: var(--color-error, #dc3545);
|
color: #dc3545;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transactionType.adjustment {
|
.transactionType.adjustment {
|
||||||
background: var(--color-info-bg, rgba(23, 162, 184, 0.1));
|
background: rgba(23, 162, 184, 0.1);
|
||||||
color: var(--color-info, #17a2b8);
|
color: #17a2b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
|
|
@ -322,86 +323,86 @@
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
|
|
||||||
.adminSection {
|
.adminSection {
|
||||||
background: var(--color-bg-card);
|
background: var(--surface-color, #1e1e1e);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--border-color, #333);
|
||||||
border-radius: var(--border-radius-lg);
|
border-radius: 12px;
|
||||||
padding: var(--spacing-lg);
|
padding: 1.5rem;
|
||||||
margin-bottom: var(--spacing-lg);
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.adminSection h3 {
|
.adminSection h3 {
|
||||||
font-size: var(--font-size-base);
|
font-size: 1rem;
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: 600;
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
margin: 0 0 var(--spacing-md) 0;
|
margin: 0 0 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formRow {
|
.formRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-md);
|
gap: 1rem;
|
||||||
margin-bottom: var(--spacing-md);
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formGroup {
|
.formGroup {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-xs);
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.formGroup label {
|
.formGroup label {
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: 500;
|
||||||
color: var(--color-text-secondary);
|
color: var(--text-secondary, #888);
|
||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
padding: var(--spacing-sm);
|
padding: 0.5rem 0.75rem;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--border-color, #333);
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: 6px;
|
||||||
background: var(--color-bg-input);
|
background: var(--surface-color, #1e1e1e);
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
font-size: var(--font-size-base);
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:focus {
|
.input:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--color-primary);
|
border-color: var(--primary-color, #f25843);
|
||||||
}
|
}
|
||||||
|
|
||||||
.accountsGrid {
|
.accountsGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
gap: var(--spacing-md);
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accountCard {
|
.accountCard {
|
||||||
background: var(--color-bg-secondary);
|
background: var(--bg-secondary, #2a2a2a);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--border-color, #333);
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: 8px;
|
||||||
padding: var(--spacing-md);
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accountCard h4 {
|
.accountCard h4 {
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
font-weight: var(--font-weight-semibold);
|
font-weight: 600;
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
margin: 0 0 var(--spacing-sm) 0;
|
margin: 0 0 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accountInfo {
|
.accountInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-xs);
|
gap: 0.25rem;
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accountInfo span {
|
.accountInfo span {
|
||||||
color: var(--color-text-secondary);
|
color: var(--text-secondary, #888);
|
||||||
}
|
}
|
||||||
|
|
||||||
.accountInfo strong {
|
.accountInfo strong {
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
|
|
@ -409,22 +410,22 @@
|
||||||
============================================================================ */
|
============================================================================ */
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
padding: 0.5rem 1rem;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: 6px;
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonPrimary {
|
.buttonPrimary {
|
||||||
background: var(--color-primary);
|
background: var(--primary-color, #f25843);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonPrimary:hover {
|
.buttonPrimary:hover {
|
||||||
background: var(--color-primary-dark);
|
background: var(--primary-dark, #d94d3a);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonPrimary:disabled {
|
.buttonPrimary:disabled {
|
||||||
|
|
@ -433,13 +434,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonSecondary {
|
.buttonSecondary {
|
||||||
background: var(--color-bg-secondary);
|
background: var(--bg-secondary, #2a2a2a);
|
||||||
color: var(--color-text-primary);
|
color: var(--text-primary, #e0e0e0);
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--border-color, #333);
|
||||||
}
|
}
|
||||||
|
|
||||||
.buttonSecondary:hover {
|
.buttonSecondary:hover {
|
||||||
background: var(--color-bg-hover);
|
background: var(--surface-color, #1e1e1e);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
|
|
@ -450,37 +451,37 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: var(--spacing-xl);
|
padding: 2rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--text-secondary, #888);
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.noData {
|
.noData {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: var(--spacing-lg);
|
padding: 1.5rem;
|
||||||
color: var(--color-text-tertiary);
|
color: var(--text-tertiary, #666);
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorMessage {
|
.errorMessage {
|
||||||
background: var(--color-error-bg, rgba(220, 53, 69, 0.1));
|
background: rgba(220, 53, 69, 0.1);
|
||||||
color: var(--color-error, #dc3545);
|
color: #dc3545;
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
padding: 0.75rem 1rem;
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: 6px;
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
margin-bottom: var(--spacing-md);
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.successMessage {
|
.successMessage {
|
||||||
background: var(--color-success-bg, rgba(40, 167, 69, 0.1));
|
background: rgba(40, 167, 69, 0.1);
|
||||||
color: var(--color-success, #28a745);
|
color: #28a745;
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
padding: 0.75rem 1rem;
|
||||||
border-radius: var(--border-radius-md);
|
border-radius: 6px;
|
||||||
font-size: var(--font-size-sm);
|
font-size: 0.875rem;
|
||||||
margin-bottom: var(--spacing-md);
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
|
|
@ -489,7 +490,7 @@
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.billingDashboard {
|
.billingDashboard {
|
||||||
padding: var(--spacing-md);
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.balanceGrid {
|
.balanceGrid {
|
||||||
|
|
@ -513,6 +514,6 @@
|
||||||
.barLabel,
|
.barLabel,
|
||||||
.barValue {
|
.barValue {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
font-size: var(--font-size-xs);
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
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 { useAdminMandates } from '../../hooks/useMandates';
|
||||||
import styles from './Billing.module.css';
|
import styles from './Billing.module.css';
|
||||||
|
|
||||||
|
|
@ -191,10 +191,11 @@ const SettingsEditor: React.FC<SettingsEditorProps> = ({ settings, onSave, loadi
|
||||||
interface CreditAdderProps {
|
interface CreditAdderProps {
|
||||||
settings: BillingSettings | null;
|
settings: BillingSettings | null;
|
||||||
accounts: AccountSummary[];
|
accounts: AccountSummary[];
|
||||||
|
users: MandateUserSummary[];
|
||||||
onAddCredit: (userId: string | undefined, amount: number, description: string) => Promise<void>;
|
onAddCredit: (userId: string | undefined, amount: number, description: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, onAddCredit }) => {
|
const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, users, onAddCredit }) => {
|
||||||
const [selectedUserId, setSelectedUserId] = useState<string>('');
|
const [selectedUserId, setSelectedUserId] = useState<string>('');
|
||||||
const [amount, setAmount] = useState<number>(10);
|
const [amount, setAmount] = useState<number>(10);
|
||||||
const [description, setDescription] = useState<string>('Manuelles Aufladen');
|
const [description, setDescription] = useState<string>('Manuelles Aufladen');
|
||||||
|
|
@ -203,6 +204,14 @@ const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, onAddCred
|
||||||
|
|
||||||
const isPrepayUser = settings?.billingModel === 'PREPAY_USER';
|
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<string, AccountSummary>);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (amount <= 0) {
|
if (amount <= 0) {
|
||||||
|
|
@ -246,21 +255,23 @@ const CreditAdder: React.FC<CreditAdderProps> = ({ settings, accounts, onAddCred
|
||||||
{isPrepayUser && (
|
{isPrepayUser && (
|
||||||
<div className={styles.formRow}>
|
<div className={styles.formRow}>
|
||||||
<div className={styles.formGroup}>
|
<div className={styles.formGroup}>
|
||||||
<label>Benutzer-Konto</label>
|
<label>Benutzer</label>
|
||||||
<select
|
<select
|
||||||
className={styles.select}
|
className={styles.select}
|
||||||
value={selectedUserId}
|
value={selectedUserId}
|
||||||
onChange={(e) => setSelectedUserId(e.target.value)}
|
onChange={(e) => setSelectedUserId(e.target.value)}
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
<option value="">-- Konto wählen --</option>
|
<option value="">-- Benutzer wählen --</option>
|
||||||
{accounts
|
{users.map((user) => {
|
||||||
.filter(acc => acc.accountType === 'USER')
|
const account = accountsByUserId[user.id];
|
||||||
.map((acc) => (
|
const balanceInfo = account ? ` (${formatCurrency(account.balance)})` : ' (kein Konto)';
|
||||||
<option key={acc.id} value={acc.userId || ''}>
|
return (
|
||||||
{acc.userId} - {formatCurrency(acc.balance)}
|
<option key={user.id} value={user.id}>
|
||||||
|
{user.displayName || user.email || user.id}{balanceInfo}
|
||||||
</option>
|
</option>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -355,7 +366,7 @@ const AccountsOverview: React.FC<AccountsOverviewProps> = ({ accounts, loading }
|
||||||
|
|
||||||
export const BillingAdmin: React.FC = () => {
|
export const BillingAdmin: React.FC = () => {
|
||||||
const [selectedMandateId, setSelectedMandateId] = useState<string | null>(null);
|
const [selectedMandateId, setSelectedMandateId] = useState<string | null>(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) => {
|
const handleMandateSelect = (mandateId: string) => {
|
||||||
setSelectedMandateId(mandateId || null);
|
setSelectedMandateId(mandateId || null);
|
||||||
|
|
@ -397,6 +408,7 @@ export const BillingAdmin: React.FC = () => {
|
||||||
<CreditAdder
|
<CreditAdder
|
||||||
settings={settings}
|
settings={settings}
|
||||||
accounts={accounts}
|
accounts={accounts}
|
||||||
|
users={users}
|
||||||
onAddCredit={handleAddCredit}
|
onAddCredit={handleAddCredit}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import { useCurrentInstance } from '../../hooks/useCurrentInstance';
|
||||||
import { FaComment, FaTasks, FaPaperPlane, FaStop, FaFile, FaPlus, FaMicrophone, FaSquare, FaFileAlt } from 'react-icons/fa';
|
import { FaComment, FaTasks, FaPaperPlane, FaStop, FaFile, FaPlus, FaMicrophone, FaSquare, FaFileAlt } from 'react-icons/fa';
|
||||||
import { useToast } from '../../contexts/ToastContext';
|
import { useToast } from '../../contexts/ToastContext';
|
||||||
import { useVoiceLanguage, VoiceLanguageSelect, Messages } from '../../components/UiComponents';
|
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 type { Message } from '../../components/UiComponents/Messages/MessagesTypes';
|
||||||
import api from '../../api';
|
import api from '../../api';
|
||||||
import styles from './PlaygroundPage.module.css';
|
import styles from './PlaygroundPage.module.css';
|
||||||
|
|
@ -58,8 +58,8 @@ export const PlaygroundPage: React.FC = () => {
|
||||||
deletingFiles,
|
deletingFiles,
|
||||||
previewingFiles,
|
previewingFiles,
|
||||||
downloadingFiles,
|
downloadingFiles,
|
||||||
selectedProvider,
|
selectedProviders,
|
||||||
onProviderSelect,
|
onProvidersChange,
|
||||||
} = hookData;
|
} = hookData;
|
||||||
|
|
||||||
const { prompts, refetch: refetchPrompts } = usePrompts();
|
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
|
// Permission check - also show while loading
|
||||||
if (playgroundUIPermission === false) {
|
if (playgroundUIPermission === false) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -785,9 +782,9 @@ export const PlaygroundPage: React.FC = () => {
|
||||||
>
|
>
|
||||||
<FaPlus />
|
<FaPlus />
|
||||||
</button>
|
</button>
|
||||||
<ProviderSelect
|
<ProviderMultiSelect
|
||||||
value={selectedProvider}
|
selectedProviders={selectedProviders}
|
||||||
onChange={onProviderSelect}
|
onChange={onProvidersChange}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
/>
|
/>
|
||||||
<VoiceLanguageSelect
|
<VoiceLanguageSelect
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue