tool fixes
This commit is contained in:
parent
05317e64ca
commit
0367563ea8
4 changed files with 165 additions and 24 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
|
import { FormGeneratorTable, type ColumnConfig } from '../../components/FormGenerator/FormGeneratorTable';
|
||||||
import { useAdminSubscriptions } from '../../hooks/useAdminSubscriptions';
|
import { useAdminSubscriptions } from '../../hooks/useAdminSubscriptions';
|
||||||
import { useConfirm } from '../../hooks/useConfirm';
|
import { useConfirm } from '../../hooks/useConfirm';
|
||||||
|
|
@ -23,7 +22,6 @@ const _COLUMNS: ColumnConfig[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const AdminSubscriptionsPage: React.FC = () => {
|
const AdminSubscriptionsPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const { confirm, ConfirmDialog } = useConfirm();
|
const { confirm, ConfirmDialog } = useConfirm();
|
||||||
const { data: subscriptions, pagination, loading, refetch } = useAdminSubscriptions();
|
const { data: subscriptions, pagination, loading, refetch } = useAdminSubscriptions();
|
||||||
|
|
||||||
|
|
@ -47,14 +45,6 @@ const AdminSubscriptionsPage: React.FC = () => {
|
||||||
<header className={styles.pageHeader} style={{ flexShrink: 0 }}>
|
<header className={styles.pageHeader} style={{ flexShrink: 0 }}>
|
||||||
<h1>Subscription-Übersicht</h1>
|
<h1>Subscription-Übersicht</h1>
|
||||||
<p className={styles.subtitle}>Alle Abonnements aller Mandanten</p>
|
<p className={styles.subtitle}>Alle Abonnements aller Mandanten</p>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={styles.button}
|
|
||||||
onClick={() => navigate('/admin/billing')}
|
|
||||||
style={{ marginTop: 8 }}
|
|
||||||
>
|
|
||||||
← Zurück zur Billing-Verwaltung
|
|
||||||
</button>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
<div style={{ flex: 1, minHeight: 0, overflow: 'auto' }}>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||||
import { useSearchParams, Link } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { useBillingAdmin, type BillingSettings, type AccountSummary, type MandateUserSummary } from '../../hooks/useBilling';
|
import { useBillingAdmin, type BillingSettings, type AccountSummary, type MandateUserSummary } from '../../hooks/useBilling';
|
||||||
import type { CheckoutCreateRequest } from '../../api/billingApi';
|
import type { CheckoutCreateRequest } from '../../api/billingApi';
|
||||||
import { useUserMandates, type Mandate as UserMandateRow } from '../../hooks/useUserMandates';
|
import { useUserMandates, type Mandate as UserMandateRow } from '../../hooks/useUserMandates';
|
||||||
|
|
@ -695,7 +695,7 @@ export const BillingAdmin: React.FC = () => {
|
||||||
setStripeReturnMessage(null);
|
setStripeReturnMessage(null);
|
||||||
}, [searchParams, setSearchParams]);
|
}, [searchParams, setSearchParams]);
|
||||||
|
|
||||||
const showStripeForMandateAdmin = !isSysAdmin && !!selectedMandateId && !!settings;
|
const showStripeForMandateAdmin = !!selectedMandateId && !!settings;
|
||||||
|
|
||||||
const _tabStyle = (isActive: boolean) => ({
|
const _tabStyle = (isActive: boolean) => ({
|
||||||
padding: '8px 16px',
|
padding: '8px 16px',
|
||||||
|
|
@ -719,15 +719,6 @@ export const BillingAdmin: React.FC = () => {
|
||||||
Abrechnungseinstellungen, Guthaben und Abonnement pro Mandant
|
Abrechnungseinstellungen, Guthaben und Abonnement pro Mandant
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{isSysAdmin && (
|
|
||||||
<Link
|
|
||||||
to="/admin/subscriptions"
|
|
||||||
className={`${styles.button} ${styles.buttonPrimary}`}
|
|
||||||
style={{ whiteSpace: 'nowrap', marginTop: 4 }}
|
|
||||||
>
|
|
||||||
Alle Abonnements →
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
||||||
155
src/pages/views/workspace/WorkspaceGeneralSettings.tsx
Normal file
155
src/pages/views/workspace/WorkspaceGeneralSettings.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
/**
|
||||||
|
* WorkspaceGeneralSettings -- Per-user workspace settings (e.g. max agent rounds).
|
||||||
|
*
|
||||||
|
* The user can override the instance default. Setting a field to null reverts to the default.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { useApiRequest } from '../../../hooks/useApi';
|
||||||
|
import styles from './WorkspaceSettings.module.css';
|
||||||
|
|
||||||
|
interface GeneralSettingsProps {
|
||||||
|
instanceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MaxAgentRoundsInfo {
|
||||||
|
effective: number;
|
||||||
|
userOverride: number | null;
|
||||||
|
instanceDefault: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WorkspaceGeneralSettings: React.FC<GeneralSettingsProps> = ({ instanceId }) => {
|
||||||
|
const { request } = useApiRequest();
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [maxRoundsInfo, setMaxRoundsInfo] = useState<MaxAgentRoundsInfo>({
|
||||||
|
effective: 25,
|
||||||
|
userOverride: null,
|
||||||
|
instanceDefault: 25,
|
||||||
|
});
|
||||||
|
const [inputValue, setInputValue] = useState<string>('');
|
||||||
|
|
||||||
|
const _loadSettings = useCallback(async () => {
|
||||||
|
if (!instanceId) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/workspace/${instanceId}/settings/general`,
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
const info = (data as any)?.maxAgentRounds;
|
||||||
|
if (info) {
|
||||||
|
setMaxRoundsInfo(info);
|
||||||
|
setInputValue(info.userOverride != null ? String(info.userOverride) : '');
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err?.message || 'Fehler beim Laden der Einstellungen');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [instanceId, request]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
_loadSettings();
|
||||||
|
}, [_loadSettings]);
|
||||||
|
|
||||||
|
const _handleSave = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
setSuccess(null);
|
||||||
|
try {
|
||||||
|
const val = inputValue.trim() === '' ? null : parseInt(inputValue, 10);
|
||||||
|
if (val !== null && (isNaN(val) || val < 1 || val > 100)) {
|
||||||
|
setError('Wert muss zwischen 1 und 100 liegen.');
|
||||||
|
setSaving(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/workspace/${instanceId}/settings/general`,
|
||||||
|
method: 'put',
|
||||||
|
data: { maxAgentRounds: val },
|
||||||
|
});
|
||||||
|
const info = (data as any)?.maxAgentRounds;
|
||||||
|
if (info) {
|
||||||
|
setMaxRoundsInfo(info);
|
||||||
|
setInputValue(info.userOverride != null ? String(info.userOverride) : '');
|
||||||
|
}
|
||||||
|
setSuccess('Einstellungen gespeichert.');
|
||||||
|
setTimeout(() => setSuccess(null), 3000);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err?.message || 'Fehler beim Speichern');
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const _handleReset = () => {
|
||||||
|
setInputValue('');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <div className={styles.loading}>Lade Einstellungen...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasOverride = inputValue.trim() !== '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.settings}>
|
||||||
|
<h2 className={styles.heading}>Generelle Einstellungen</h2>
|
||||||
|
|
||||||
|
{error && <div className={styles.error}>{error}</div>}
|
||||||
|
{success && <div className={styles.success}>{success}</div>}
|
||||||
|
|
||||||
|
<div className={styles.section}>
|
||||||
|
<h3 className={styles.sectionTitle}>Agenten-Konfiguration</h3>
|
||||||
|
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label className={styles.label}>
|
||||||
|
Max. Agenten-Runden
|
||||||
|
</label>
|
||||||
|
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className={styles.input}
|
||||||
|
style={{ maxWidth: 120 }}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={e => setInputValue(e.target.value)}
|
||||||
|
placeholder={String(maxRoundsInfo.instanceDefault)}
|
||||||
|
min={1}
|
||||||
|
max={100}
|
||||||
|
/>
|
||||||
|
{hasOverride && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.removeBtn}
|
||||||
|
onClick={_handleReset}
|
||||||
|
title="Auf Standard zurücksetzen"
|
||||||
|
>
|
||||||
|
Zurücksetzen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '0.8rem', color: 'var(--text-secondary, #888)', marginTop: 4, display: 'block' }}>
|
||||||
|
Standard der Instanz: {maxRoundsInfo.instanceDefault}.
|
||||||
|
{maxRoundsInfo.userOverride != null && (
|
||||||
|
<> Ihr Override: {maxRoundsInfo.userOverride}.</>
|
||||||
|
)}
|
||||||
|
{' '}Effektiv: {maxRoundsInfo.effective}.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={styles.saveBtn}
|
||||||
|
onClick={_handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
{saving ? 'Speichern...' : 'Einstellungen speichern'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -8,21 +8,23 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
import { useInstanceId } from '../../../hooks/useCurrentInstance';
|
||||||
import { WorkspaceSettings } from './WorkspaceSettings';
|
import { WorkspaceSettings } from './WorkspaceSettings';
|
||||||
|
import { WorkspaceGeneralSettings } from './WorkspaceGeneralSettings';
|
||||||
|
|
||||||
type SettingsTab = 'voice';
|
type SettingsTab = 'general' | 'voice';
|
||||||
|
|
||||||
const _TABS: { key: SettingsTab; label: string }[] = [
|
const _TABS: { key: SettingsTab; label: string }[] = [
|
||||||
|
{ key: 'general', label: 'Generelle Einstellungen' },
|
||||||
{ key: 'voice', label: 'Sprache & Stimme' },
|
{ key: 'voice', label: 'Sprache & Stimme' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const WorkspaceSettingsPage: React.FC = () => {
|
export const WorkspaceSettingsPage: React.FC = () => {
|
||||||
const instanceId = useInstanceId();
|
const instanceId = useInstanceId();
|
||||||
const [activeTab, setActiveTab] = useState<SettingsTab>('voice');
|
const [activeTab, setActiveTab] = useState<SettingsTab>('general');
|
||||||
|
|
||||||
if (!instanceId) {
|
if (!instanceId) {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 32, textAlign: 'center', color: '#999' }}>
|
<div style={{ padding: 32, textAlign: 'center', color: '#999' }}>
|
||||||
Keine Workspace-Instanz ausgewaehlt.
|
Keine Workspace-Instanz ausgewählt.
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +63,9 @@ export const WorkspaceSettingsPage: React.FC = () => {
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div style={{ flex: 1, overflow: 'auto', padding: '16px 24px' }}>
|
<div style={{ flex: 1, overflow: 'auto', padding: '16px 24px' }}>
|
||||||
|
{activeTab === 'general' && (
|
||||||
|
<WorkspaceGeneralSettings instanceId={instanceId} />
|
||||||
|
)}
|
||||||
{activeTab === 'voice' && (
|
{activeTab === 'voice' && (
|
||||||
<WorkspaceSettings instanceId={instanceId} />
|
<WorkspaceSettings instanceId={instanceId} />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue