/** * TrusteeAnalyseView * * Tab-based analysis page. Each tab maps to a bootstrapped template workflow * (created from TEMPLATE_WORKFLOWS when the feature instance was set up). * The workflow is loaded from the instance, executed via the workflow engine, * and results/status are shown inline with polling. */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import { useToast } from '../../../contexts/ToastContext'; import api from '../../../api'; import styles from './TrusteeViews.module.css'; import { useLanguage } from '../../../providers/language/LanguageContext'; // --------------------------------------------------------------------------- // Tab definitions // --------------------------------------------------------------------------- interface TabDef { id: string; templateTag: string; icon: string; color: string; } const _TABS: TabDef[] = [ { id: 'budget', templateTag: 'template:trustee-budget-comparison', icon: '\uD83D\uDCCA', color: '#2196F3' }, { id: 'kpi', templateTag: 'template:trustee-kpi-dashboard', icon: '\uD83D\uDCF0', color: '#9C27B0' }, { id: 'cashflow', templateTag: 'template:trustee-cashflow', icon: '\uD83D\uDCB0', color: '#009688' }, { id: 'forecast', templateTag: 'template:trustee-forecast', icon: '\uD83D\uDCC8', color: '#E91E63' }, ]; const _TAB_LABELS: Record> = { budget: { de: 'Budget-Vergleich', en: 'Budget Comparison', fr: 'Comparaison budgétaire' }, kpi: { de: 'KPI-Dashboard', en: 'KPI Dashboard', fr: 'Tableau de bord KPI' }, cashflow: { de: 'Cashflow-Rechnung', en: 'Cash Flow Statement', fr: 'Flux de trésorerie' }, forecast: { de: 'Prognose', en: 'Forecast', fr: 'Prévision' }, }; const _TAB_DESCRIPTIONS: Record> = { budget: { de: 'Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel', en: 'Compare actuals vs. budget from Excel' }, kpi: { de: 'Kennzahlen berechnen und visualisieren', en: 'Calculate and visualize key metrics' }, cashflow: { de: 'Cashflow berechnen und analysieren', en: 'Calculate and analyze cash flow' }, forecast: { de: 'Trend-Analyse und Prognose der nächsten Monate', en: 'Trend analysis and forecast for coming months' }, }; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface WorkflowSummary { id: string; label: string; tags: string[]; } type RunState = 'idle' | 'starting' | 'running' | 'completed' | 'error'; // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export const TrusteeAnalyseView: React.FC = () => { const { t, currentLanguage } = useLanguage(); const lang = currentLanguage || 'de'; const { instanceId } = useCurrentInstance(); const { showSuccess, showError } = useToast(); const [searchParams, setSearchParams] = useSearchParams(); const activeTab = searchParams.get('tab') || _TABS[0].id; const _setActiveTab = useCallback((tab: string) => { setSearchParams({ tab }, { replace: true }); }, [setSearchParams]); const [workflows, setWorkflows] = useState([]); const [workflowsLoading, setWorkflowsLoading] = useState(true); const [runState, setRunState] = useState('idle'); const [runId, setRunId] = useState(null); const [runSummary, setRunSummary] = useState(''); const [runError, setRunError] = useState(null); const pollTimerRef = useRef(null); const isPollingRef = useRef(false); // Load workflows for this instance once useEffect(() => { if (!instanceId) return; const _load = async () => { setWorkflowsLoading(true); try { const res = await api.get(`/api/workflows/${instanceId}/workflows`); const items: WorkflowSummary[] = (res.data?.workflows || res.data?.items || []).map((w: any) => ({ id: w.id, label: w.label, tags: w.tags || [], })); setWorkflows(items); } catch { setWorkflows([]); } finally { setWorkflowsLoading(false); } }; _load(); }, [instanceId]); // Find the workflow for the active tab const _findWorkflow = useCallback((tab: string): WorkflowSummary | undefined => { const tabDef = _TABS.find((t) => t.id === tab); if (!tabDef) return undefined; return workflows.find((w) => w.tags.includes(tabDef.templateTag)); }, [workflows]); // Polling const _stopPolling = useCallback(() => { if (pollTimerRef.current !== null) { window.clearInterval(pollTimerRef.current); pollTimerRef.current = null; } isPollingRef.current = false; }, []); const _pollRun = useCallback(async (rid: string) => { if (!instanceId || !rid || isPollingRef.current) return; isPollingRef.current = true; try { const res = await api.get(`/api/workflows/${instanceId}/runs/${rid}/steps`); const steps: any[] = Array.isArray(res?.data?.steps) ? res.data.steps : []; const completed = steps.filter((s) => s.status === 'completed'); const failed = steps.filter((s) => s.status === 'failed'); const running = steps.filter((s) => s.status === 'running'); setRunSummary(`${completed.length}/${steps.length} ${t('trusteeAnalyse.stepsCompleted', 'Schritte abgeschlossen')}`); if (failed.length > 0) { const errMsg = failed[failed.length - 1].error || 'Step failed'; setRunState('error'); setRunError(errMsg); _stopPolling(); showError('Pipeline error', errMsg); return; } if (running.length === 0 && completed.length === steps.length && steps.length > 0) { setRunState('completed'); _stopPolling(); showSuccess(t('trusteeAnalyse.completed', 'Abgeschlossen'), t('trusteeAnalyse.workflowDone', 'Analyse-Workflow erfolgreich beendet.')); return; } setRunState('running'); } catch (err: any) { if (err?.response?.status === 404) { setRunState('running'); return; } setRunState('error'); setRunError(err.message || 'Polling failed'); _stopPolling(); } finally { isPollingRef.current = false; } }, [instanceId, showError, showSuccess, _stopPolling, t]); useEffect(() => { if (!instanceId || !runId || (runState !== 'running' && runState !== 'starting')) return; void _pollRun(runId); pollTimerRef.current = window.setInterval(() => { void _pollRun(runId); }, 3000); return () => { _stopPolling(); }; }, [instanceId, runId, runState, _pollRun, _stopPolling]); useEffect(() => () => { _stopPolling(); }, [_stopPolling]); // Reset run state when tab changes useEffect(() => { _stopPolling(); setRunState('idle'); setRunId(null); setRunSummary(''); setRunError(null); }, [activeTab, _stopPolling]); // Execute workflow const _handleExecute = useCallback(async () => { const wf = _findWorkflow(activeTab); if (!wf || !instanceId) { showError('Error', t('trusteeAnalyse.noWorkflow', 'Kein Workflow für diesen Tab gefunden.')); return; } setRunState('starting'); setRunError(null); setRunSummary(t('trusteeAnalyse.starting', 'Workflow wird gestartet...')); try { const res = await api.post(`/api/workflows/${instanceId}/execute`, { workflowId: wf.id }); const rid = res?.data?.runId; if (rid) { setRunId(rid); setRunState('running'); setRunSummary(`Run ${rid.slice(0, 8)} ${t('trusteeAnalyse.started', 'gestartet')}`); } else if (res?.data?.success) { setRunState('completed'); setRunSummary(t('trusteeAnalyse.completedSync', 'Workflow synchron abgeschlossen.')); showSuccess(t('trusteeAnalyse.completed', 'Abgeschlossen'), t('trusteeAnalyse.workflowDone', 'Analyse-Workflow erfolgreich beendet.')); } else { throw new Error(res?.data?.error || 'Unexpected response'); } } catch (err: any) { const msg = err?.response?.data?.detail || err.message || 'Failed to start workflow'; setRunState('error'); setRunError(typeof msg === 'string' ? msg : JSON.stringify(msg)); showError('Error', typeof msg === 'string' ? msg : JSON.stringify(msg)); } }, [activeTab, instanceId, _findWorkflow, showError, showSuccess, t]); const currentTab = _TABS.find((t) => t.id === activeTab) || _TABS[0]; const currentWorkflow = _findWorkflow(activeTab); return (

{t('trusteeAnalyse.title', 'Analyse & Reporting')}

{/* Tab bar */}
{_TABS.map((tab) => ( ))}
{/* Tab content */}

{_TAB_DESCRIPTIONS[activeTab]?.[lang] || _TAB_DESCRIPTIONS[activeTab]?.de || ''}

{workflowsLoading ? (

{t('trusteeAnalyse.loadingWorkflows', 'Workflows werden geladen...')}

) : !currentWorkflow ? (

{t('trusteeAnalyse.noWorkflowInfo', 'Für diesen Tab wurde kein Workflow in der Instanz gefunden. Der Workflow wird beim Erstellen der Instanz automatisch angelegt.')}

) : ( <>
{currentTab.icon}
{currentWorkflow.label}
Workflow ID: {currentWorkflow.id.slice(0, 8)}...
)} {/* Pipeline status */} {runState !== 'idle' && (
{t('trusteeAnalyse.status', 'Status')}:{' '} {runState === 'starting' && t('trusteeAnalyse.starting', 'Wird gestartet...')} {runState === 'running' && t('trusteeAnalyse.runningLabel', 'Läuft')} {runState === 'completed' && t('trusteeAnalyse.completedLabel', 'Abgeschlossen')} {runState === 'error' && t('trusteeAnalyse.errorLabel', 'Fehler')} {runSummary &&
{runSummary}
} {runError &&
{runError}
}
)}
); }; export default TrusteeAnalyseView;