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;
initialSearchTerm?: string;
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;
onRowDragStart?: (e: React.DragEvent<HTMLTableRowElement>, row: T) => void;
}
@ -580,6 +582,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
groupActions,
initialSearchTerm = '',
initialSort,
initialFilters,
rowDraggable = false,
onRowDragStart,
}: FormGeneratorTableProps<T>) {
@ -724,7 +727,7 @@ export function FormGeneratorTable<T extends Record<string, any>>({
const [filterFocused, setFilterFocused] = useState<Record<string, boolean>>({});
// Multi-column sorting: array of sort configs in order of priority
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>>({});
// Actions column width - resizable, default based on number of buttons
const [actionsColumnWidth, setActionsColumnWidth] = useState<number | null>(null);

View file

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

View file

@ -7,7 +7,7 @@
*/
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 { FormGeneratorTable, type ColumnConfig } from '../components/FormGenerator';
import { Tabs } from '../components/UiComponents/Tabs';
@ -15,7 +15,7 @@ import { useToast } from '../contexts/ToastContext';
import { usePrompt } from '../hooks/usePrompt';
import { useApiRequest } from '../hooks/useApi';
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 type { AttributeDefinition } from '../api/attributesApi';
import { resolveColumnTypes } from '../utils/columnTypeResolver';
@ -424,7 +424,12 @@ const _RunTracingModal: React.FC<_RunTracingModalProps> = ({ run, onClose }) =>
// 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 { request } = useApiRequest();
const { showError } = useToast();
@ -491,8 +496,7 @@ const _DashboardTab: React.FC = () => {
useEffect(() => {
_loadMetrics();
_loadRuns();
}, [_loadMetrics, _loadRuns]);
}, [_loadMetrics]);
const hasRunningRuns = runs.some((r) => r.status === 'running' || r.status === 'paused');
useEffect(() => {
@ -531,13 +535,19 @@ const _DashboardTab: React.FC = () => {
}
}, [showError, t]);
const _initialFilters = useMemo(() => {
if (!workflowFilter) return undefined;
return { workflowId: workflowFilter };
}, [workflowFilter]);
const _rawRunColumns: ColumnConfig[] = useMemo(() => [
{
key: 'workflowLabel',
key: 'workflowId',
label: t('Workflow'),
width: 200,
sortable: true,
formatter: (v: string, row: WorkflowRun) => v || row.workflowId || t('—'),
filterable: true,
displayField: 'workflowLabel',
},
{
key: 'mandateId',
@ -643,7 +653,9 @@ const _DashboardTab: React.FC = () => {
</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}>
<FormGeneratorTable<WorkflowRun>
data={runs}
@ -656,7 +668,9 @@ const _DashboardTab: React.FC = () => {
sortable={true}
selectable={true}
initialSort={[{ key: 'startedAt', direction: 'desc' }]}
initialFilters={_initialFilters}
apiEndpoint="/api/system/workflow-runs"
onRowClick={(row) => onRunClick?.(row.id)}
customActions={[
{
id: 'tracing',
@ -686,7 +700,11 @@ const _DashboardTab: React.FC = () => {
// 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 navigate = useNavigate();
const { request } = useApiRequest();
@ -1051,6 +1069,7 @@ const _WorkflowsTab: React.FC = () => {
},
]}
onDelete={(row) => _handleDelete(row.id)}
onRowClick={(row) => onWorkflowClick?.(row.id)}
hookData={_hookData}
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 { 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 [detailLoading, setDetailLoading] = useState(false);
const _loadRuns = useCallback(async () => {
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) => {
const _loadDetail = useCallback(async (id: string) => {
setDetailLoading(true);
try {
const detail = await fetchWorkspaceRunDetail(request, runId);
const detail = await fetchWorkspaceRunDetail(request, id);
setRunDetail(detail);
} catch (e) {
console.error('Workspace run detail failed', e);
@ -1109,22 +1107,34 @@ const _WorkspaceTab: React.FC = () => {
}, [request]);
useEffect(() => {
if (selectedRunId) _loadDetail(selectedRunId);
if (runId) _loadDetail(runId);
else setRunDetail(null);
}, [selectedRunId, _loadDetail]);
}, [runId, _loadDetail]);
if (!runId) {
return (
<div style={{ padding: '1rem', color: 'var(--text-secondary)' }}>
<p>{t('Wähle einen Run im Dashboard aus, um die Details anzuzeigen.')}</p>
</div>
);
}
if (detailLoading || !runDetail) {
return <div style={{ padding: '1rem' }}><p>{t('Laden…')}</p></div>;
}
if (selectedRunId && runDetail) {
const { run, steps, files, workflow } = runDetail;
return (
<div style={{ padding: '1rem' }}>
<button type="button" className="btn-link" onClick={() => setSelectedRunId(null)} style={{ marginBottom: '1rem' }}>
{t('Zurück zur Liste')}
<button type="button" className={styles.secondaryButton} onClick={onBack} style={{ marginBottom: '1rem' }}>
{t('Zurück zum Dashboard')}
</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>}
{run.startedAt && <span><strong>{t('Start')}:</strong> {_formatTs(run.startedAt)}</span>}
{run.completedAt && <span><strong>{t('Ende')}:</strong> {_formatTs(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>
@ -1177,98 +1187,64 @@ const _WorkspaceTab: React.FC = () => {
)}
</div>
);
}
return (
<div style={{ padding: '1rem' }}>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center', marginBottom: '1rem' }}>
<select value={scope} onChange={(e) => setScope(e.target.value as 'mine' | 'mandate')} style={{ padding: '0.3rem 0.5rem' }}>
<option value="mine">{t('Meine Runs')}</option>
<option value="mandate">{t('Alle zugänglichen')}</option>
</select>
<select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} style={{ padding: '0.3rem 0.5rem' }}>
<option value="">{t('Alle Status')}</option>
<option value="completed">{t('Abgeschlossen')}</option>
<option value="running">{t('Läuft')}</option>
<option value="failed">{t('Fehlgeschlagen')}</option>
<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>
{loading ? (
<p>{t('Laden…')}</p>
) : runs.length === 0 ? (
<p style={{ color: 'var(--text-secondary)' }}>{t('Keine Workflow-Runs gefunden.')}</p>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.85rem' }}>
<thead>
<tr style={{ borderBottom: '2px solid var(--border-color)', textAlign: 'left' }}>
<th style={{ padding: '0.5rem' }}>{t('Workflow')}</th>
<th style={{ padding: '0.5rem' }}>{t('Status')}</th>
<th style={{ padding: '0.5rem' }}>{t('Gestartet')}</th>
<th style={{ padding: '0.5rem' }}>{t('Ziel-Instanz')}</th>
<th style={{ padding: '0.5rem' }}>Tokens</th>
</tr>
</thead>
<tbody>
{runs.map((run) => (
<tr
key={run.id}
onClick={() => setSelectedRunId(run.id)}
style={{ borderBottom: '1px solid var(--border-color)', cursor: 'pointer' }}
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--bg-hover, rgba(0,0,0,0.03))')}
onMouseLeave={(e) => (e.currentTarget.style.background = '')}
>
<td style={{ padding: '0.5rem' }}>{run.workflowLabel || run.workflowId}</td>
<td style={{ padding: '0.5rem' }}>
<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)' }}>
{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>
</table>
)}
</div>
);
};
// ===========================================================================
// Main page with Tabs
// Main page with Tabs (Workflows → Dashboard → Workspace)
// ===========================================================================
export const AutomationsDashboardPage: React.FC = () => {
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(() => [
{
id: 'dashboard',
label: t('Dashboard'),
content: <_DashboardTab />,
},
{
id: 'workflows',
label: t('Workflows'),
content: <_WorkflowsTab />,
content: <_WorkflowsTab onWorkflowClick={_handleWorkflowClick} />,
},
{
id: 'dashboard',
label: t('Dashboard'),
content: <_DashboardTab workflowFilter={workflowFilter} onRunClick={_handleRunClick} />,
},
{
id: 'workspace',
label: t('Workspace'),
content: <_WorkspaceTab />,
content: <_WorkspaceTab runId={selectedRunId} onBack={_handleBackFromWorkspace} />,
},
], [t]);
], [t, _handleWorkflowClick, workflowFilter, _handleRunClick, selectedRunId, _handleBackFromWorkspace]);
return (
<div className={`${styles.adminPage} ${styles.adminPageFill}`}>
<h1 className={styles.pageTitle} style={{ flexShrink: 0 }}>{t('Automatisierung')}</h1>
<Tabs tabs={tabs} defaultTabId="dashboard" />
<Tabs tabs={tabs} activeTabId={activeTab} onTabChange={setActiveTab} />
</div>
);
};

View file

@ -408,6 +408,9 @@ export const TrusteeAnalyseView: React.FC = () => {
<div style={{ fontWeight: 600, marginBottom: '0.5rem', fontSize: '0.875rem' }}>
{t('Budget-Excel hochladen')}
</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 ? (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
<span style={{ fontSize: '0.875rem' }}>📄 {budgetFileName}</span>