fixes before document generation refactory styles

This commit is contained in:
ValueOn AG 2026-04-29 22:54:26 +02:00
parent 8cecf3b320
commit 70459d57e3
4 changed files with 152 additions and 166 deletions

View file

@ -221,6 +221,8 @@ export interface FormGeneratorTableProps<T = any> {
groupActions?: (groupKey: string, groupRows: T[]) => React.ReactNode; groupActions?: (groupKey: string, groupRows: T[]) => React.ReactNode;
initialSearchTerm?: string; initialSearchTerm?: string;
initialSort?: Array<{ key: string; direction: 'asc' | 'desc' }>; initialSort?: Array<{ key: string; direction: 'asc' | 'desc' }>;
/** Pre-set column filters on mount (e.g. {workflowId: "abc"}). Reacts to prop changes. */
initialFilters?: Record<string, any>;
rowDraggable?: boolean; rowDraggable?: boolean;
onRowDragStart?: (e: React.DragEvent<HTMLTableRowElement>, row: T) => void; onRowDragStart?: (e: React.DragEvent<HTMLTableRowElement>, row: T) => void;
} }
@ -580,6 +582,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
groupActions, groupActions,
initialSearchTerm = '', initialSearchTerm = '',
initialSort, initialSort,
initialFilters,
rowDraggable = false, rowDraggable = false,
onRowDragStart, onRowDragStart,
}: FormGeneratorTableProps<T>) { }: FormGeneratorTableProps<T>) {
@ -724,7 +727,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
const [filterFocused, setFilterFocused] = useState<Record<string, boolean>>({}); const [filterFocused, setFilterFocused] = useState<Record<string, boolean>>({});
// Multi-column sorting: array of sort configs in order of priority // Multi-column sorting: array of sort configs in order of priority
const [sortConfigs, setSortConfigs] = useState<Array<{ key: string; direction: 'asc' | 'desc' }>>(initialSort ?? []); const [sortConfigs, setSortConfigs] = useState<Array<{ key: string; direction: 'asc' | 'desc' }>>(initialSort ?? []);
const [filters, setFilters] = useState<Record<string, any>>({}); const [filters, setFilters] = useState<Record<string, any>>(initialFilters || {});
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({}); const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
// Actions column width - resizable, default based on number of buttons // Actions column width - resizable, default based on number of buttons
const [actionsColumnWidth, setActionsColumnWidth] = useState<number | null>(null); const [actionsColumnWidth, setActionsColumnWidth] = useState<number | null>(null);

View file

@ -10,17 +10,21 @@ export interface Tab {
export interface TabsProps { export interface TabsProps {
tabs: Tab[]; tabs: Tab[];
defaultTabId?: string; defaultTabId?: string;
/** Controlled active tab. When provided, internal state is ignored. */
activeTabId?: string;
onTabChange?: (tabId: string) => void; onTabChange?: (tabId: string) => void;
className?: string; className?: string;
} }
export function Tabs({ tabs, defaultTabId, onTabChange, className = '' }: TabsProps) { export function Tabs({ tabs, defaultTabId, activeTabId: controlledTabId, onTabChange, className = '' }: TabsProps) {
const [activeTabId, setActiveTabId] = useState<string>( const [internalTabId, setInternalTabId] = useState<string>(
defaultTabId || tabs[0]?.id || '' defaultTabId || tabs[0]?.id || ''
); );
const activeTabId = controlledTabId ?? internalTabId;
const handleTabClick = (tabId: string) => { const handleTabClick = (tabId: string) => {
setActiveTabId(tabId); if (!controlledTabId) setInternalTabId(tabId);
onTabChange?.(tabId); onTabChange?.(tabId);
}; };

View file

@ -7,7 +7,7 @@
*/ */
import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react'; import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate, useSearchParams } from 'react-router-dom';
import { FaSync, FaPlay, FaCog, FaChartBar, FaDownload, FaCheck, FaBan, FaPen, FaEye, FaTimes, FaStream, FaStop } from 'react-icons/fa'; import { FaSync, FaPlay, FaCog, FaChartBar, FaDownload, FaCheck, FaBan, FaPen, FaEye, FaTimes, FaStream, FaStop } from 'react-icons/fa';
import { FormGeneratorTable, type ColumnConfig } from '../components/FormGenerator'; import { FormGeneratorTable, type ColumnConfig } from '../components/FormGenerator';
import { Tabs } from '../components/UiComponents/Tabs'; import { Tabs } from '../components/UiComponents/Tabs';
@ -15,7 +15,7 @@ import { useToast } from '../contexts/ToastContext';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { useApiRequest } from '../hooks/useApi'; import { useApiRequest } from '../hooks/useApi';
import { formatUnixTimestamp } from '../utils/time'; import { formatUnixTimestamp } from '../utils/time';
import { updateWorkflow, executeGraph, deleteSystemWorkflow, fetchWorkspaceRuns, fetchWorkspaceRunDetail, type WorkspaceRun } from '../api/workflowApi'; import { updateWorkflow, executeGraph, deleteSystemWorkflow, fetchWorkspaceRunDetail } from '../api/workflowApi';
import { fetchAttributes } from '../api/attributesApi'; import { fetchAttributes } from '../api/attributesApi';
import type { AttributeDefinition } from '../api/attributesApi'; import type { AttributeDefinition } from '../api/attributesApi';
import { resolveColumnTypes } from '../utils/columnTypeResolver'; import { resolveColumnTypes } from '../utils/columnTypeResolver';
@ -424,7 +424,12 @@ const _RunTracingModal: React.FC<_RunTracingModalProps> = ({ run, onClose }) =>
// DashboardTab — Metrics + Runs table with backend pagination // DashboardTab — Metrics + Runs table with backend pagination
// =========================================================================== // ===========================================================================
const _DashboardTab: React.FC = () => { interface _DashboardTabProps {
workflowFilter?: string | null;
onRunClick?: (runId: string) => void;
}
const _DashboardTab: React.FC<_DashboardTabProps> = ({ workflowFilter, onRunClick }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const { request } = useApiRequest(); const { request } = useApiRequest();
const { showError } = useToast(); const { showError } = useToast();
@ -491,8 +496,7 @@ const _DashboardTab: React.FC = () => {
useEffect(() => { useEffect(() => {
_loadMetrics(); _loadMetrics();
_loadRuns(); }, [_loadMetrics]);
}, [_loadMetrics, _loadRuns]);
const hasRunningRuns = runs.some((r) => r.status === 'running' || r.status === 'paused'); const hasRunningRuns = runs.some((r) => r.status === 'running' || r.status === 'paused');
useEffect(() => { useEffect(() => {
@ -531,13 +535,19 @@ const _DashboardTab: React.FC = () => {
} }
}, [showError, t]); }, [showError, t]);
const _initialFilters = useMemo(() => {
if (!workflowFilter) return undefined;
return { workflowId: workflowFilter };
}, [workflowFilter]);
const _rawRunColumns: ColumnConfig[] = useMemo(() => [ const _rawRunColumns: ColumnConfig[] = useMemo(() => [
{ {
key: 'workflowLabel', key: 'workflowId',
label: t('Workflow'), label: t('Workflow'),
width: 200, width: 200,
sortable: true, sortable: true,
formatter: (v: string, row: WorkflowRun) => v || row.workflowId || t('—'), filterable: true,
displayField: 'workflowLabel',
}, },
{ {
key: 'mandateId', key: 'mandateId',
@ -643,7 +653,9 @@ const _DashboardTab: React.FC = () => {
</div> </div>
)} )}
<h3 style={{ fontSize: '0.95rem', fontWeight: 600, marginBottom: 8, flexShrink: 0 }}>{t('Letzte Runs')}</h3> <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8, flexShrink: 0 }}>
<h3 style={{ fontSize: '0.95rem', fontWeight: 600, margin: 0 }}>{t('Letzte Runs')}</h3>
</div>
<div className={styles.tableContainer}> <div className={styles.tableContainer}>
<FormGeneratorTable<WorkflowRun> <FormGeneratorTable<WorkflowRun>
data={runs} data={runs}
@ -656,7 +668,9 @@ const _DashboardTab: React.FC = () => {
sortable={true} sortable={true}
selectable={true} selectable={true}
initialSort={[{ key: 'startedAt', direction: 'desc' }]} initialSort={[{ key: 'startedAt', direction: 'desc' }]}
initialFilters={_initialFilters}
apiEndpoint="/api/system/workflow-runs" apiEndpoint="/api/system/workflow-runs"
onRowClick={(row) => onRunClick?.(row.id)}
customActions={[ customActions={[
{ {
id: 'tracing', id: 'tracing',
@ -686,7 +700,11 @@ const _DashboardTab: React.FC = () => {
// WorkflowsTab — Central workflow management across all instances // WorkflowsTab — Central workflow management across all instances
// =========================================================================== // ===========================================================================
const _WorkflowsTab: React.FC = () => { interface _WorkflowsTabProps {
onWorkflowClick?: (workflowId: string) => void;
}
const _WorkflowsTab: React.FC<_WorkflowsTabProps> = ({ onWorkflowClick }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const navigate = useNavigate(); const navigate = useNavigate();
const { request } = useApiRequest(); const { request } = useApiRequest();
@ -1051,6 +1069,7 @@ const _WorkflowsTab: React.FC = () => {
}, },
]} ]}
onDelete={(row) => _handleDelete(row.id)} onDelete={(row) => _handleDelete(row.id)}
onRowClick={(row) => onWorkflowClick?.(row.id)}
hookData={_hookData} hookData={_hookData}
emptyMessage={t('Keine Workflows gefunden.')} emptyMessage={t('Keine Workflows gefunden.')}
/> />
@ -1061,45 +1080,24 @@ const _WorkflowsTab: React.FC = () => {
}; };
// =========================================================================== // ===========================================================================
// Workspace Tab (user-facing workflow run history) // Workspace Tab (run detail only — no table)
// =========================================================================== // ===========================================================================
const _WorkspaceTab: React.FC = () => { interface _WorkspaceTabProps {
runId: string | null;
onBack: () => void;
}
const _WorkspaceTab: React.FC<_WorkspaceTabProps> = ({ runId, onBack }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const { request } = useApiRequest(); const { request } = useApiRequest();
const navigate = useNavigate();
const [runs, setRuns] = useState<WorkspaceRun[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [scope, setScope] = useState<'mine' | 'mandate'>('mine');
const [statusFilter, setStatusFilter] = useState<string>('');
const [selectedRunId, setSelectedRunId] = useState<string | null>(null);
const [runDetail, setRunDetail] = useState<Awaited<ReturnType<typeof fetchWorkspaceRunDetail>> | null>(null); const [runDetail, setRunDetail] = useState<Awaited<ReturnType<typeof fetchWorkspaceRunDetail>> | null>(null);
const [detailLoading, setDetailLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false);
const _loadRuns = useCallback(async () => { const _loadDetail = useCallback(async (id: string) => {
setLoading(true);
try {
const data = await fetchWorkspaceRuns(request, {
scope,
status: statusFilter || undefined,
limit: 50,
});
setRuns(data.runs || []);
setTotal(data.total || 0);
} catch (e) {
console.error('Workspace runs load failed', e);
} finally {
setLoading(false);
}
}, [request, scope, statusFilter]);
useEffect(() => { _loadRuns(); }, [_loadRuns]);
const _loadDetail = useCallback(async (runId: string) => {
setDetailLoading(true); setDetailLoading(true);
try { try {
const detail = await fetchWorkspaceRunDetail(request, runId); const detail = await fetchWorkspaceRunDetail(request, id);
setRunDetail(detail); setRunDetail(detail);
} catch (e) { } catch (e) {
console.error('Workspace run detail failed', e); console.error('Workspace run detail failed', e);
@ -1109,166 +1107,144 @@ const _WorkspaceTab: React.FC = () => {
}, [request]); }, [request]);
useEffect(() => { useEffect(() => {
if (selectedRunId) _loadDetail(selectedRunId); if (runId) _loadDetail(runId);
else setRunDetail(null); else setRunDetail(null);
}, [selectedRunId, _loadDetail]); }, [runId, _loadDetail]);
if (selectedRunId && runDetail) { if (!runId) {
const { run, steps, files, workflow } = runDetail;
return ( return (
<div style={{ padding: '1rem' }}> <div style={{ padding: '1rem', color: 'var(--text-secondary)' }}>
<button type="button" className="btn-link" onClick={() => setSelectedRunId(null)} style={{ marginBottom: '1rem' }}> <p>{t('Wähle einen Run im Dashboard aus, um die Details anzuzeigen.')}</p>
{t('Zurück zur Liste')}
</button>
<h3 style={{ margin: '0.5rem 0' }}>{run.workflowLabel || run.workflowId}</h3>
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', fontSize: '0.85rem', color: 'var(--text-secondary)', marginBottom: '1rem' }}>
<span><strong>{t('Status')}:</strong> {run.status}</span>
{run.startedAt && <span><strong>{t('Start')}:</strong> {formatUnixTimestamp(run.startedAt)}</span>}
{run.completedAt && <span><strong>{t('Ende')}:</strong> {formatUnixTimestamp(run.completedAt)}</span>}
{workflow?.targetFeatureInstanceId && <span><strong>{t('Ziel-Instanz')}:</strong> {run.targetInstanceLabel || workflow.targetFeatureInstanceId}</span>}
{(run.costTokens ?? 0) > 0 && <span><strong>Tokens:</strong> {run.costTokens}</span>}
</div>
{run.error && (
<div style={{ padding: '0.5rem', background: 'rgba(220,53,69,0.1)', borderRadius: 6, marginBottom: '1rem', color: 'var(--danger-color)' }}>
{run.error}
</div>
)}
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Schritte')}</h4>
{steps.length === 0 ? (
<p style={{ color: 'var(--text-secondary)' }}>{t('Keine Schritte protokolliert.')}</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{steps.map((step) => (
<details key={step.id} style={{ border: '1px solid var(--border-color)', borderRadius: 6, padding: '0.5rem' }}>
<summary style={{ cursor: 'pointer', fontWeight: 500 }}>
<span style={{ marginRight: '0.5rem', fontSize: '0.75rem', padding: '2px 6px', borderRadius: 4, background: step.status === 'completed' ? 'rgba(40,167,69,0.15)' : step.status === 'failed' ? 'rgba(220,53,69,0.15)' : 'rgba(0,123,255,0.15)', color: step.status === 'completed' ? 'var(--success-color)' : step.status === 'failed' ? 'var(--danger-color)' : 'var(--primary-color)' }}>
{step.status}
</span>
{step.nodeType} ({step.nodeId})
{step.durationMs != null && <span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{step.durationMs}ms</span>}
</summary>
{step.output && Object.keys(step.output).length > 0 && (
<pre style={{ fontSize: '0.75rem', maxHeight: 300, overflow: 'auto', marginTop: '0.5rem', background: 'var(--bg-secondary)', padding: '0.5rem', borderRadius: 4 }}>
{JSON.stringify(step.output, null, 2)}
</pre>
)}
{step.error && <p style={{ color: 'var(--danger-color)', marginTop: '0.25rem' }}>{step.error}</p>}
</details>
))}
</div>
)}
{files.length > 0 && (
<>
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Dokumente')}</h4>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
{files.map((f) => (
<a
key={f.id}
href={`/api/files/${f.id}/download`}
download
style={{ padding: '0.5rem 1rem', border: '1px solid var(--border-color)', borderRadius: 6, textDecoration: 'none', color: 'var(--primary-color)', fontSize: '0.85rem' }}
>
<FaDownload style={{ marginRight: 4 }} />
{f.fileName || f.id}
</a>
))}
</div>
</>
)}
</div> </div>
); );
} }
if (detailLoading || !runDetail) {
return <div style={{ padding: '1rem' }}><p>{t('Laden…')}</p></div>;
}
const { run, steps, files, workflow } = runDetail;
return ( return (
<div style={{ padding: '1rem' }}> <div style={{ padding: '1rem' }}>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center', marginBottom: '1rem' }}> <button type="button" className={styles.secondaryButton} onClick={onBack} style={{ marginBottom: '1rem' }}>
<select value={scope} onChange={(e) => setScope(e.target.value as 'mine' | 'mandate')} style={{ padding: '0.3rem 0.5rem' }}> {t('Zurück zum Dashboard')}
<option value="mine">{t('Meine Runs')}</option> </button>
<option value="mandate">{t('Alle zugänglichen')}</option> <h3 style={{ margin: '0.5rem 0' }}>{run.workflowLabel || run.workflowId}</h3>
</select> <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap', fontSize: '0.85rem', color: 'var(--text-secondary)', marginBottom: '1rem' }}>
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} style={{ padding: '0.3rem 0.5rem' }}> <span><strong>{t('Status')}:</strong> {run.status}</span>
<option value="">{t('Alle Status')}</option> {run.startedAt && <span><strong>{t('Start')}:</strong> {_formatTs(run.startedAt)}</span>}
<option value="completed">{t('Abgeschlossen')}</option> {run.completedAt && <span><strong>{t('Ende')}:</strong> {_formatTs(run.completedAt)}</span>}
<option value="running">{t('Läuft')}</option> {workflow?.targetFeatureInstanceId && <span><strong>{t('Ziel-Instanz')}:</strong> {run.targetInstanceLabel || workflow.targetFeatureInstanceId}</span>}
<option value="failed">{t('Fehlgeschlagen')}</option> {(run.costTokens ?? 0) > 0 && <span><strong>Tokens:</strong> {run.costTokens}</span>}
<option value="paused">{t('Pausiert')}</option>
</select>
<button type="button" onClick={_loadRuns} style={{ padding: '0.3rem 0.8rem' }}>
<FaSync style={{ marginRight: 4 }} /> {t('Aktualisieren')}
</button>
<span style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>{total} {t('Runs')}</span>
</div> </div>
{loading ? ( {run.error && (
<p>{t('Laden…')}</p> <div style={{ padding: '0.5rem', background: 'rgba(220,53,69,0.1)', borderRadius: 6, marginBottom: '1rem', color: 'var(--danger-color)' }}>
) : runs.length === 0 ? ( {run.error}
<p style={{ color: 'var(--text-secondary)' }}>{t('Keine Workflow-Runs gefunden.')}</p> </div>
)}
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Schritte')}</h4>
{steps.length === 0 ? (
<p style={{ color: 'var(--text-secondary)' }}>{t('Keine Schritte protokolliert.')}</p>
) : ( ) : (
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
<thead> {steps.map((step) => (
<tr style={{ borderBottom: '2px solid var(--border-color)', textAlign: 'left' }}> <details key={step.id} style={{ border: '1px solid var(--border-color)', borderRadius: 6, padding: '0.5rem' }}>
<th style={{ padding: '0.5rem' }}>{t('Workflow')}</th> <summary style={{ cursor: 'pointer', fontWeight: 500 }}>
<th style={{ padding: '0.5rem' }}>{t('Status')}</th> <span style={{ marginRight: '0.5rem', fontSize: '0.75rem', padding: '2px 6px', borderRadius: 4, background: step.status === 'completed' ? 'rgba(40,167,69,0.15)' : step.status === 'failed' ? 'rgba(220,53,69,0.15)' : 'rgba(0,123,255,0.15)', color: step.status === 'completed' ? 'var(--success-color)' : step.status === 'failed' ? 'var(--danger-color)' : 'var(--primary-color)' }}>
<th style={{ padding: '0.5rem' }}>{t('Gestartet')}</th> {step.status}
<th style={{ padding: '0.5rem' }}>{t('Ziel-Instanz')}</th> </span>
<th style={{ padding: '0.5rem' }}>Tokens</th> {step.nodeType} ({step.nodeId})
</tr> {step.durationMs != null && <span style={{ marginLeft: '0.5rem', fontSize: '0.75rem', color: 'var(--text-secondary)' }}>{step.durationMs}ms</span>}
</thead> </summary>
<tbody> {step.output && Object.keys(step.output).length > 0 && (
{runs.map((run) => ( <pre style={{ fontSize: '0.75rem', maxHeight: 300, overflow: 'auto', marginTop: '0.5rem', background: 'var(--bg-secondary)', padding: '0.5rem', borderRadius: 4 }}>
<tr {JSON.stringify(step.output, null, 2)}
key={run.id} </pre>
onClick={() => setSelectedRunId(run.id)} )}
style={{ borderBottom: '1px solid var(--border-color)', cursor: 'pointer' }} {step.error && <p style={{ color: 'var(--danger-color)', marginTop: '0.25rem' }}>{step.error}</p>}
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--bg-hover, rgba(0,0,0,0.03))')} </details>
onMouseLeave={(e) => (e.currentTarget.style.background = '')} ))}
</div>
)}
{files.length > 0 && (
<>
<h4 style={{ margin: '1rem 0 0.5rem' }}>{t('Dokumente')}</h4>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
{files.map((f) => (
<a
key={f.id}
href={`/api/files/${f.id}/download`}
download
style={{ padding: '0.5rem 1rem', border: '1px solid var(--border-color)', borderRadius: 6, textDecoration: 'none', color: 'var(--primary-color)', fontSize: '0.85rem' }}
> >
<td style={{ padding: '0.5rem' }}>{run.workflowLabel || run.workflowId}</td> <FaDownload style={{ marginRight: 4 }} />
<td style={{ padding: '0.5rem' }}> {f.fileName || f.id}
<span style={{ padding: '2px 8px', borderRadius: 10, fontSize: '0.75rem', fontWeight: 600, background: run.status === 'completed' ? 'rgba(40,167,69,0.15)' : run.status === 'failed' ? 'rgba(220,53,69,0.15)' : run.status === 'running' ? 'rgba(0,123,255,0.15)' : 'rgba(255,193,7,0.15)', color: run.status === 'completed' ? 'var(--success-color)' : run.status === 'failed' ? 'var(--danger-color)' : run.status === 'running' ? 'var(--primary-color)' : 'var(--warning-color)' }}> </a>
{run.status}
</span>
</td>
<td style={{ padding: '0.5rem' }}>{run.startedAt ? formatUnixTimestamp(run.startedAt) : '—'}</td>
<td style={{ padding: '0.5rem' }}>{run.targetInstanceLabel || '—'}</td>
<td style={{ padding: '0.5rem' }}>{run.costTokens ?? 0}</td>
</tr>
))} ))}
</tbody> </div>
</table> </>
)} )}
</div> </div>
); );
}; };
// =========================================================================== // ===========================================================================
// Main page with Tabs // Main page with Tabs (Workflows → Dashboard → Workspace)
// =========================================================================== // ===========================================================================
export const AutomationsDashboardPage: React.FC = () => { export const AutomationsDashboardPage: React.FC = () => {
const { t } = useLanguage(); const { t } = useLanguage();
const [searchParams] = useSearchParams();
const initialTab = searchParams.get('tab') || 'workflows';
const initialRunId = searchParams.get('runId') || null;
const [activeTab, setActiveTab] = useState<string>(initialRunId ? 'workspace' : initialTab);
const [selectedRunId, setSelectedRunId] = useState<string | null>(initialRunId);
const [workflowFilter, setWorkflowFilter] = useState<string | null>(null);
const _handleWorkflowClick = useCallback((workflowId: string) => {
setWorkflowFilter(workflowId);
setActiveTab('dashboard');
}, []);
useEffect(() => {
if (workflowFilter) setWorkflowFilter(null);
}, [workflowFilter]);
const _handleRunClick = useCallback((runId: string) => {
setSelectedRunId(runId);
setActiveTab('workspace');
}, []);
const _handleBackFromWorkspace = useCallback(() => {
setSelectedRunId(null);
setActiveTab('dashboard');
}, []);
const tabs = useMemo(() => [ const tabs = useMemo(() => [
{
id: 'dashboard',
label: t('Dashboard'),
content: <_DashboardTab />,
},
{ {
id: 'workflows', id: 'workflows',
label: t('Workflows'), label: t('Workflows'),
content: <_WorkflowsTab />, content: <_WorkflowsTab onWorkflowClick={_handleWorkflowClick} />,
},
{
id: 'dashboard',
label: t('Dashboard'),
content: <_DashboardTab workflowFilter={workflowFilter} onRunClick={_handleRunClick} />,
}, },
{ {
id: 'workspace', id: 'workspace',
label: t('Workspace'), label: t('Workspace'),
content: <_WorkspaceTab />, content: <_WorkspaceTab runId={selectedRunId} onBack={_handleBackFromWorkspace} />,
}, },
], [t]); ], [t, _handleWorkflowClick, workflowFilter, _handleRunClick, selectedRunId, _handleBackFromWorkspace]);
return ( return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}> <div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<h1 className={styles.pageTitle} style={{ flexShrink: 0 }}>{t('Automatisierung')}</h1> <h1 className={styles.pageTitle} style={{ flexShrink: 0 }}>{t('Automatisierung')}</h1>
<Tabs tabs={tabs} defaultTabId="dashboard" /> <Tabs tabs={tabs} activeTabId={activeTab} onTabChange={setActiveTab} />
</div> </div>
); );
}; };

View file

@ -408,6 +408,9 @@ export const TrusteeAnalyseView: React.FC = () => {
<div style={{ fontWeight: 600, marginBottom: '0.5rem', fontSize: '0.875rem' }}> <div style={{ fontWeight: 600, marginBottom: '0.5rem', fontSize: '0.875rem' }}>
{t('Budget-Excel hochladen')} {t('Budget-Excel hochladen')}
</div> </div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary, #666)', marginBottom: '0.5rem' }}>
{t('Ergebnis: Excel-Bericht mit Konten-Tabelle, Uebersichts-Chart und Management-Summary.')}
</div>
{budgetFileName ? ( {budgetFileName ? (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}> <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ fontSize: '0.875rem' }}>📄 {budgetFileName}</span> <span style={{ fontSize: '0.875rem' }}>📄 {budgetFileName}</span>