>({
handleRowSelect(globalIndex)}
+ checked={selectedIds.has(_getRowId(row))}
+ onChange={() => handleRowSelect(row)}
onClick={(e) => e.stopPropagation()}
disabled={isRowSelectable && !isRowSelectable(row)}
title={
@@ -2184,7 +2276,7 @@ export function FormGeneratorTable>({
return (
onRowClick?.(row, index)}
draggable={rowDraggable}
onDragStart={rowDraggable && onRowDragStart ? (e) => onRowDragStart(e, row) : undefined}
@@ -2196,8 +2288,8 @@ export function FormGeneratorTable>({
handleRowSelect(index)}
+ checked={selectedIds.has(_getRowId(row))}
+ onChange={() => handleRowSelect(row)}
onClick={(e) => e.stopPropagation()}
disabled={isRowSelectable && !isRowSelectable(row)}
title={
diff --git a/src/hooks/useFeatureAccess.ts b/src/hooks/useFeatureAccess.ts
index e67658e..658d32a 100644
--- a/src/hooks/useFeatureAccess.ts
+++ b/src/hooks/useFeatureAccess.ts
@@ -303,6 +303,31 @@ export function useFeatureAccess() {
}
}, []);
+ /**
+ * Sync workflows for a feature instance from templates
+ */
+ const syncInstanceWorkflows = useCallback(async (
+ mandateId: string,
+ instanceId: string
+ ): Promise<{ success: boolean; data?: { added: number; skipped: number; total: number }; error?: string }> => {
+ setLoading(true);
+ setError(null);
+ try {
+ const response = await api.post(`/api/features/instances/${instanceId}/sync-workflows`, {}, {
+ headers: {
+ 'X-Mandate-Id': mandateId
+ }
+ });
+ return { success: true, data: response.data };
+ } catch (err: any) {
+ const errorMessage = err.response?.data?.detail || err.message || 'Failed to sync instance workflows';
+ setError(errorMessage);
+ return { success: false, error: errorMessage };
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
/**
* Get current user's feature instances (grouped by mandate)
*/
@@ -495,6 +520,7 @@ export function useFeatureAccess() {
updateInstance,
deleteInstance,
syncInstanceRoles,
+ syncInstanceWorkflows,
fetchMyFeatureInstances,
fetchTemplateRoles,
// Instance users management
diff --git a/src/pages/AutomationsDashboardPage.tsx b/src/pages/AutomationsDashboardPage.tsx
index 4d4d1a5..d229ffe 100644
--- a/src/pages/AutomationsDashboardPage.tsx
+++ b/src/pages/AutomationsDashboardPage.tsx
@@ -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, deleteWorkflow } from '../api/workflowApi';
+import { updateWorkflow, executeGraph, deleteSystemWorkflow } from '../api/workflowApi';
import api from '../api';
import { useLanguage } from '../providers/language/LanguageContext';
import styles from './admin/Admin.module.css';
@@ -392,6 +392,7 @@ const _DashboardTab: React.FC = () => {
const [loading, setLoading] = useState(true);
const [paginationMeta, setPaginationMeta] = useState(null);
const [tracingRun, setTracingRun] = useState(null);
+ const lastPaginationParamsRef = useRef(null);
const _loadMetrics = useCallback(async () => {
try {
@@ -403,14 +404,19 @@ const _DashboardTab: React.FC = () => {
}, []);
const _loadRuns = useCallback(async (paginationParams?: any) => {
+ if (paginationParams !== undefined) {
+ lastPaginationParamsRef.current = paginationParams;
+ }
+ const effectiveParams = paginationParams ?? lastPaginationParamsRef.current;
setLoading(true);
try {
+ const defaultSort = [{ field: 'sysCreatedAt', direction: 'desc' }];
const pag = {
- page: paginationParams?.page || 1,
- pageSize: paginationParams?.pageSize || 25,
- ...(paginationParams?.sort ? { sort: paginationParams.sort } : {}),
- ...(paginationParams?.search ? { search: paginationParams.search } : {}),
- ...(paginationParams?.filters ? { filters: paginationParams.filters } : {}),
+ page: effectiveParams?.page || 1,
+ pageSize: effectiveParams?.pageSize || 25,
+ sort: effectiveParams?.sort || defaultSort,
+ ...(effectiveParams?.search ? { search: effectiveParams.search } : {}),
+ ...(effectiveParams?.filters ? { filters: effectiveParams.filters } : {}),
};
const params: Record = { pagination: JSON.stringify(pag) };
const resp = await api.get('/api/system/workflow-runs', { params });
@@ -474,6 +480,15 @@ const _DashboardTab: React.FC = () => {
}
}, [showError, t]);
+ const _STATUS_LABELS: Record = useMemo(() => ({
+ running: t('Laufend'),
+ completed: t('Abgeschlossen'),
+ failed: t('Fehlgeschlagen'),
+ cancelled: t('Abgebrochen'),
+ paused: t('Pausiert'),
+ stopped: t('Gestoppt'),
+ }), [t]);
+
const _runColumns: ColumnConfig[] = useMemo(() => [
{
key: 'workflowLabel',
@@ -484,20 +499,24 @@ const _DashboardTab: React.FC = () => {
formatter: (v: string, row: WorkflowRun) => v || row.workflowId || t('—'),
},
{
- key: 'mandateLabel',
+ key: 'mandateId',
label: t('Mandant'),
type: 'string',
width: 140,
sortable: true,
filterable: true,
+ fkSource: '/api/mandates/',
+ fkDisplayField: 'label',
},
{
- key: 'instanceLabel',
+ key: 'featureInstanceId',
label: t('Instanz'),
type: 'string',
width: 140,
sortable: true,
filterable: true,
+ fkSource: '/api/features/instances',
+ fkDisplayField: 'label',
},
{
key: 'status',
@@ -507,9 +526,10 @@ const _DashboardTab: React.FC = () => {
sortable: true,
filterable: true,
filterOptions: ['running', 'completed', 'failed', 'cancelled', 'paused'],
+ filterLabelResolver: (v: string) => _STATUS_LABELS[v] || v,
formatter: (v: string) => (
- {v === 'completed' ? t('Abgeschlossen') : v === 'failed' ? t('Fehlgeschlagen') : v === 'running' ? t('Laufend') : v}
+ {_STATUS_LABELS[v] || v}
),
},
@@ -526,9 +546,10 @@ const _DashboardTab: React.FC = () => {
label: t('Beendet'),
type: 'number',
width: 150,
+ sortable: true,
formatter: (v: number) => _formatTs(v),
},
- ], [t]);
+ ], [t, _STATUS_LABELS]);
const _hookData = useMemo(() => ({
refetch: _loadRuns,
@@ -605,7 +626,8 @@ const _DashboardTab: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
+ initialSort={[{ key: 'sysCreatedAt', direction: 'desc' }]}
apiEndpoint="/api/system/workflow-runs"
customActions={[
{
@@ -649,20 +671,26 @@ const _WorkflowsTab: React.FC = () => {
const [togglingId, setTogglingId] = useState(null);
const [activeFilter, setActiveFilter] = useState<'all' | 'active' | 'inactive'>('all');
const [paginationMeta, setPaginationMeta] = useState(null);
+ const lastPaginationParamsRef = useRef(null);
const _load = useCallback(async (paginationParams?: any) => {
+ if (paginationParams !== undefined) {
+ lastPaginationParamsRef.current = paginationParams;
+ }
+ const effectiveParams = paginationParams ?? lastPaginationParamsRef.current;
setLoading(true);
try {
const params: Record = {};
if (activeFilter === 'active') params.active = true;
if (activeFilter === 'inactive') params.active = false;
+ const defaultSort = [{ field: 'createdAt', direction: 'desc' }];
const pag = {
- page: paginationParams?.page || 1,
- pageSize: paginationParams?.pageSize || 25,
- ...(paginationParams?.sort ? { sort: paginationParams.sort } : {}),
- ...(paginationParams?.search ? { search: paginationParams.search } : {}),
- ...(paginationParams?.filters ? { filters: paginationParams.filters } : {}),
+ page: effectiveParams?.page || 1,
+ pageSize: effectiveParams?.pageSize || 25,
+ sort: effectiveParams?.sort || defaultSort,
+ ...(effectiveParams?.search ? { search: effectiveParams.search } : {}),
+ ...(effectiveParams?.filters ? { filters: effectiveParams.filters } : {}),
};
params.pagination = JSON.stringify(pag);
@@ -691,14 +719,13 @@ const _WorkflowsTab: React.FC = () => {
const _handleEdit = useCallback((row: SystemWorkflow) => {
if (!row.mandateId || !row.featureInstanceId) return;
- navigate(`/mandates/${row.mandateId}/graphicalEditor/${row.featureInstanceId}/editor?workflowId=${row.id}`);
+ const fc = (row as any).featureCode || 'graphicalEditor';
+ navigate(`/mandates/${row.mandateId}/${fc}/${row.featureInstanceId}/editor?workflowId=${row.id}`);
}, [navigate]);
const _handleDelete = useCallback(async (workflowId: string): Promise => {
- const wf = workflows.find(w => w.id === workflowId);
- if (!wf?.featureInstanceId) return false;
try {
- await deleteWorkflow(request, wf.featureInstanceId, workflowId);
+ await deleteSystemWorkflow(request, workflowId);
showSuccess(t('Workflow gelöscht'));
await _load();
return true;
@@ -706,7 +733,7 @@ const _WorkflowsTab: React.FC = () => {
showError(t('Fehler: {msg}', { msg: e?.message || t('Löschen fehlgeschlagen') }));
return false;
}
- }, [workflows, request, showSuccess, showError, _load, t]);
+ }, [request, showSuccess, showError, _load, t]);
const _handleToggleActive = useCallback(async (row: SystemWorkflow) => {
if (!row.featureInstanceId) return;
@@ -794,28 +821,22 @@ const _WorkflowsTab: React.FC = () => {
}, []);
const _columns: ColumnConfig[] = useMemo(() => [
- { key: 'label', label: t('Workflow'), type: 'string', width: 200, sortable: true },
- { key: 'mandateLabel', label: t('Mandant'), type: 'string', width: 140, sortable: true, filterable: true },
- { key: 'instanceLabel', label: t('Instanz'), type: 'string', width: 140, sortable: true, filterable: true },
+ { key: 'label', label: t('Workflow'), type: 'string', width: 200, sortable: true, filterable: true },
+ { key: 'mandateId', label: t('Mandant'), type: 'string', width: 140, sortable: true, filterable: true, fkSource: '/api/mandates/', fkDisplayField: 'label' },
+ { key: 'featureInstanceId', label: t('Instanz'), type: 'string', width: 140, sortable: true, filterable: true, fkSource: '/api/features/instances', fkDisplayField: 'label' },
{
key: 'active',
- label: t('Aktiv (Spalte)'),
+ label: t('Aktiv'),
type: 'boolean',
width: 80,
- formatter: (value: boolean) =>
- value !== false
- ? {t('Ja')}
- : {t('Nein')},
+ sortable: true,
+ filterable: true,
},
{
key: 'isRunning',
- label: t('läuft'),
+ label: t('Läuft'),
type: 'boolean',
width: 80,
- formatter: (value: boolean) =>
- value
- ? {t('Ja')}
- : {t('Nein')},
},
{
key: 'sysCreatedAt',
@@ -827,7 +848,7 @@ const _WorkflowsTab: React.FC = () => {
},
{
key: 'lastStartedAt',
- label: t('zuletzt gestartet'),
+ label: t('Zuletzt gestartet'),
type: 'number',
width: 160,
formatter: (v: number) => _formatTs(v),
@@ -884,7 +905,8 @@ const _WorkflowsTab: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
+ initialSort={[{ key: 'createdAt', direction: 'desc' }]}
apiEndpoint="/api/system/workflow-runs/workflows"
actionButtons={[
{
diff --git a/src/pages/admin/AdminDemoConfigPage.tsx b/src/pages/admin/AdminDemoConfigPage.tsx
index 243c50f..975cea4 100644
--- a/src/pages/admin/AdminDemoConfigPage.tsx
+++ b/src/pages/admin/AdminDemoConfigPage.tsx
@@ -11,6 +11,7 @@ import api from '../../api';
import styles from './Admin.module.css';
import demoStyles from './AdminDemoConfigPage.module.css';
import { useLanguage } from '../../providers/language/LanguageContext';
+import { useConfirm } from '../../hooks/useConfirm';
interface _DemoConfig {
code: string;
@@ -28,6 +29,7 @@ interface _ActionResult {
export const AdminDemoConfigPage: React.FC = () => {
const { t } = useLanguage();
+ const { confirm, ConfirmDialog } = useConfirm();
const [configs, setConfigs] = useState<_DemoConfig[]>([]);
const [loading, setLoading] = useState(false);
const [actionInProgress, setActionInProgress] = useState(null);
@@ -67,7 +69,11 @@ export const AdminDemoConfigPage: React.FC = () => {
const _handleRemove = async (code: string) => {
if (actionInProgress) return;
- if (!window.confirm(t('Are you sure you want to remove all demo data for this configuration?'))) return;
+ const ok = await confirm(
+ t('Alle Demo-Daten für diese Konfiguration wirklich entfernen?'),
+ { confirmLabel: t('Entfernen'), cancelLabel: t('Abbrechen'), variant: 'danger' },
+ );
+ if (!ok) return;
setActionInProgress(code);
setLastResult(null);
try {
@@ -143,6 +149,8 @@ export const AdminDemoConfigPage: React.FC = () => {
))}
)}
+
+
);
};
diff --git a/src/pages/admin/AdminFeatureAccessPage.tsx b/src/pages/admin/AdminFeatureAccessPage.tsx
index 81973bb..0f18c62 100644
--- a/src/pages/admin/AdminFeatureAccessPage.tsx
+++ b/src/pages/admin/AdminFeatureAccessPage.tsx
@@ -36,6 +36,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
updateInstance,
deleteInstance,
syncInstanceRoles,
+ syncInstanceWorkflows,
} = useFeatureAccess();
const { fetchMandates } = useUserMandates();
@@ -50,6 +51,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
const [editingInstance, setEditingInstance] = useState(null);
const [, setIsSubmitting] = useState(false);
const [syncingInstance, setSyncingInstance] = useState(null);
+ const [syncingWorkflowsInstance, setSyncingWorkflowsInstance] = useState(null);
const [backendAttributes, setBackendAttributes] = useState([]);
// Chatbot configuration state
@@ -312,6 +314,29 @@ export const AdminFeatureAccessPage: React.FC = () => {
}
};
+ // Handle sync workflows
+ const _handleSyncWorkflows = async (instance: FeatureInstance) => {
+ if (!selectedMandateId) return;
+ setSyncingWorkflowsInstance(instance.id);
+ try {
+ const result = await syncInstanceWorkflows(selectedMandateId, instance.id);
+ if (result.success && result.data) {
+ showSuccess(
+ t('Workflows synchronisiert'),
+ t('Hinzugefügt: {added}\nÜbersprungen: {skipped}\nTotal Templates: {total}', {
+ added: result.data.added,
+ skipped: result.data.skipped,
+ total: result.data.total,
+ })
+ );
+ } else {
+ showError(t('Synchronisierung fehlgeschlagen'), result.error || t('Fehler beim Synchronisieren der Workflows'));
+ }
+ } finally {
+ setSyncingWorkflowsInstance(null);
+ }
+ };
+
// Get mandate name
const getMandateName = (mandate: Mandate) => {
return mandate.label || mandate.name || mandate.id;
@@ -444,7 +469,7 @@ export const AdminFeatureAccessPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
{
type: 'delete' as const,
@@ -465,6 +490,14 @@ export const AdminFeatureAccessPage: React.FC = () => {
title: t('Rollen synchronisieren'),
loading: (row: FeatureInstance) => syncingInstance === row.id,
disabled: (row: FeatureInstance) => !row.enabled,
+ },
+ {
+ id: 'syncWorkflows',
+ icon: ,
+ onClick: _handleSyncWorkflows,
+ title: t('Workflows synchronisieren'),
+ loading: (row: FeatureInstance) => syncingWorkflowsInstance === row.id,
+ disabled: (row: FeatureInstance) => !row.enabled,
}
]}
hookData={{
diff --git a/src/pages/admin/AdminFeatureInstanceUsersPage.tsx b/src/pages/admin/AdminFeatureInstanceUsersPage.tsx
index e69068d..8dca85a 100644
--- a/src/pages/admin/AdminFeatureInstanceUsersPage.tsx
+++ b/src/pages/admin/AdminFeatureInstanceUsersPage.tsx
@@ -528,7 +528,7 @@ export const AdminFeatureInstanceUsersPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
{
type: 'edit' as const,
diff --git a/src/pages/admin/AdminFeatureRolesPage.tsx b/src/pages/admin/AdminFeatureRolesPage.tsx
index faed9ae..38a2895 100644
--- a/src/pages/admin/AdminFeatureRolesPage.tsx
+++ b/src/pages/admin/AdminFeatureRolesPage.tsx
@@ -364,7 +364,7 @@ export const AdminFeatureRolesPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
{
type: 'edit' as const,
diff --git a/src/pages/admin/AdminInvitationsPage.tsx b/src/pages/admin/AdminInvitationsPage.tsx
index 032d960..0b1c7e0 100644
--- a/src/pages/admin/AdminInvitationsPage.tsx
+++ b/src/pages/admin/AdminInvitationsPage.tsx
@@ -345,7 +345,7 @@ export const AdminInvitationsPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
{
type: 'delete' as const,
diff --git a/src/pages/admin/AdminMandateRolesPage.tsx b/src/pages/admin/AdminMandateRolesPage.tsx
index a9c6690..6156787 100644
--- a/src/pages/admin/AdminMandateRolesPage.tsx
+++ b/src/pages/admin/AdminMandateRolesPage.tsx
@@ -407,7 +407,7 @@ export const AdminMandateRolesPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
{
type: 'edit' as const,
diff --git a/src/pages/admin/AdminMandatesPage.tsx b/src/pages/admin/AdminMandatesPage.tsx
index 673ff0d..f48e8f4 100644
--- a/src/pages/admin/AdminMandatesPage.tsx
+++ b/src/pages/admin/AdminMandatesPage.tsx
@@ -214,7 +214,7 @@ export const AdminMandatesPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
diff --git a/src/pages/admin/AdminUserMandatesPage.tsx b/src/pages/admin/AdminUserMandatesPage.tsx
index 0be162c..3af48eb 100644
--- a/src/pages/admin/AdminUserMandatesPage.tsx
+++ b/src/pages/admin/AdminUserMandatesPage.tsx
@@ -342,7 +342,7 @@ export const AdminUserMandatesPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
{
type: 'edit' as const,
diff --git a/src/pages/admin/AdminUsersPage.tsx b/src/pages/admin/AdminUsersPage.tsx
index 71e38ef..eb05462 100644
--- a/src/pages/admin/AdminUsersPage.tsx
+++ b/src/pages/admin/AdminUsersPage.tsx
@@ -194,7 +194,7 @@ export const AdminUsersPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
diff --git a/src/pages/basedata/ConnectionsPage.tsx b/src/pages/basedata/ConnectionsPage.tsx
index be8ff49..20288a9 100644
--- a/src/pages/basedata/ConnectionsPage.tsx
+++ b/src/pages/basedata/ConnectionsPage.tsx
@@ -317,7 +317,7 @@ export const ConnectionsPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
...(canUpdate ? [{
type: 'edit' as const,
diff --git a/src/pages/basedata/PromptsPage.tsx b/src/pages/basedata/PromptsPage.tsx
index 4a97e3e..2fa7740 100644
--- a/src/pages/basedata/PromptsPage.tsx
+++ b/src/pages/basedata/PromptsPage.tsx
@@ -197,7 +197,7 @@ export const PromptsPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
{
type: 'copy' as const,
diff --git a/src/pages/billing/BillingDataView.tsx b/src/pages/billing/BillingDataView.tsx
index e73679e..bc4f017 100644
--- a/src/pages/billing/BillingDataView.tsx
+++ b/src/pages/billing/BillingDataView.tsx
@@ -455,7 +455,7 @@ export const BillingDataView: React.FC = () => {
if (crossFilters && Object.keys(crossFilters).length > 0) {
params.pagination = JSON.stringify({ filters: crossFilters });
}
- const resp = await api.get('/api/billing/view/users/transactions/filter-values', { params });
+ const resp = await api.get('/api/billing/view/users/transactions', { params: { ...params, mode: 'filterValues' } });
return Array.isArray(resp.data) ? resp.data : [];
}, [_scopeParams]);
diff --git a/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx b/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx
index 68827bf..2cc4b2e 100644
--- a/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx
+++ b/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx
@@ -263,7 +263,7 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
{
type: 'edit',
diff --git a/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx b/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx
index 72b6943..3a0d11c 100644
--- a/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx
+++ b/src/pages/views/graphicalEditor/GraphicalEditorWorkflowsPage.tsx
@@ -294,7 +294,7 @@ export const GraphicalEditorWorkflowsPage: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
{
type: 'edit',
diff --git a/src/pages/views/realestate/RealEstateParcelsView.tsx b/src/pages/views/realestate/RealEstateParcelsView.tsx
index d7a0642..cc41cef 100644
--- a/src/pages/views/realestate/RealEstateParcelsView.tsx
+++ b/src/pages/views/realestate/RealEstateParcelsView.tsx
@@ -176,7 +176,7 @@ export const RealEstateParcelsView: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
...(canUpdate
? [
diff --git a/src/pages/views/realestate/RealEstateProjectsView.tsx b/src/pages/views/realestate/RealEstateProjectsView.tsx
index 34b1f5b..1a83e73 100644
--- a/src/pages/views/realestate/RealEstateProjectsView.tsx
+++ b/src/pages/views/realestate/RealEstateProjectsView.tsx
@@ -162,7 +162,7 @@ export const RealEstateProjectsView: React.FC = () => {
searchable={true}
filterable={true}
sortable={true}
- selectable={false}
+ selectable={true}
actionButtons={[
...(canUpdate ? [{ type: 'edit' as const, onAction: handleEditClick, title: t('Bearbeiten') }] : []),
...(canDelete ? [{ type: 'delete' as const, title: t('Löschen'), loading: (row: RealEstateProject) => deletingItems.has(row.id) }] : []),
diff --git a/src/pages/views/trustee/TrusteeAnalyseView.tsx b/src/pages/views/trustee/TrusteeAnalyseView.tsx
index c32883f..2194842 100644
--- a/src/pages/views/trustee/TrusteeAnalyseView.tsx
+++ b/src/pages/views/trustee/TrusteeAnalyseView.tsx
@@ -14,6 +14,7 @@ import { useToast } from '../../../contexts/ToastContext';
import api from '../../../api';
import styles from './TrusteeViews.module.css';
import { useLanguage } from '../../../providers/language/LanguageContext';
+import { FaUpload, FaTimes } from 'react-icons/fa';
// ---------------------------------------------------------------------------
// Tab definitions
@@ -90,6 +91,14 @@ export const TrusteeAnalyseView: React.FC = () => {
const pollTimerRef = useRef(null);
const isPollingRef = useRef(false);
+ const [resultText, setResultText] = useState(null);
+ const [resultDocuments, setResultDocuments] = useState>([]);
+
+ const [budgetFileId, setBudgetFileId] = useState(null);
+ const [budgetFileName, setBudgetFileName] = useState(null);
+ const [uploading, setUploading] = useState(false);
+ const fileInputRef = useRef(null);
+
// Load workflows for this instance once
useEffect(() => {
if (!instanceId) return;
@@ -151,6 +160,12 @@ export const TrusteeAnalyseView: React.FC = () => {
if (running.length === 0 && completed.length === steps.length && steps.length > 0) {
setRunState('completed');
_stopPolling();
+ const lastStep = [...steps].reverse().find((s) => s.status === 'completed' && s.output);
+ if (lastStep?.output) {
+ setResultText(lastStep.output.response || lastStep.output.context || null);
+ const docs = lastStep.output.documents || lastStep.output.documentList || [];
+ setResultDocuments(Array.isArray(docs) ? docs : []);
+ }
showSuccess(t('Abgeschlossen'), t('Analyse-Workflow erfolgreich beendet.'));
return;
}
@@ -177,6 +192,25 @@ export const TrusteeAnalyseView: React.FC = () => {
useEffect(() => () => { _stopPolling(); }, [_stopPolling]);
+ const _extractResults = useCallback((nodeOutputs: Record) => {
+ const analyseOut = nodeOutputs?.analyse || nodeOutputs?.result;
+ if (!analyseOut) {
+ for (const key of Object.keys(nodeOutputs || {})) {
+ const v = nodeOutputs[key];
+ if (v && typeof v === 'object' && (v.response || v.documents)) {
+ setResultText(v.response || v.context || null);
+ const docs = v.documents || v.documentList || [];
+ setResultDocuments(Array.isArray(docs) ? docs : []);
+ return;
+ }
+ }
+ return;
+ }
+ setResultText(analyseOut.response || analyseOut.context || null);
+ const docs = analyseOut.documents || analyseOut.documentList || [];
+ setResultDocuments(Array.isArray(docs) ? docs : []);
+ }, []);
+
// Reset run state when tab changes
useEffect(() => {
_stopPolling();
@@ -184,8 +218,36 @@ export const TrusteeAnalyseView: React.FC = () => {
setRunId(null);
setRunSummary('');
setRunError(null);
+ setResultText(null);
+ setResultDocuments([]);
}, [activeTab, _stopPolling]);
+ const _handleBudgetUpload = useCallback(async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file || !instanceId) return;
+ setUploading(true);
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+ formData.append('featureInstanceId', instanceId);
+ const res = await api.post('/api/files/upload', formData);
+ const fileData = res.data?.file || res.data;
+ setBudgetFileId(fileData.id);
+ setBudgetFileName(fileData.fileName || file.name);
+ showSuccess(t('Datei hochgeladen'), file.name);
+ } catch (err: any) {
+ showError(t('Upload fehlgeschlagen'), err.message || t('Datei konnte nicht hochgeladen werden.'));
+ } finally {
+ setUploading(false);
+ if (fileInputRef.current) fileInputRef.current.value = '';
+ }
+ }, [instanceId, showSuccess, showError, t]);
+
+ const _handleRemoveBudgetFile = useCallback(() => {
+ setBudgetFileId(null);
+ setBudgetFileName(null);
+ }, []);
+
// Execute workflow
const _handleExecute = useCallback(async () => {
const wf = _findWorkflow(activeTab);
@@ -193,11 +255,21 @@ export const TrusteeAnalyseView: React.FC = () => {
showError(t('Fehler'), t('Kein Workflow für diesen Tab gefunden.'));
return;
}
+ if (activeTab === 'budget' && !budgetFileId) {
+ showError(t('Budget-Datei fehlt'), t('Bitte laden Sie zuerst die Budget-Excel-Datei hoch.'));
+ return;
+ }
setRunState('starting');
setRunError(null);
setRunSummary(t('Workflow wird gestartet…'));
+ setResultText(null);
+ setResultDocuments([]);
try {
- const res = await api.post(`/api/workflows/${instanceId}/execute`, { workflowId: wf.id });
+ const executeBody: Record = { workflowId: wf.id };
+ if (activeTab === 'budget' && budgetFileId) {
+ executeBody.payload = { documentList: [budgetFileId] };
+ }
+ const res = await api.post(`/api/workflows/${instanceId}/execute`, executeBody);
const rid = res?.data?.runId;
if (rid) {
setRunId(rid);
@@ -206,6 +278,9 @@ export const TrusteeAnalyseView: React.FC = () => {
} else if (res?.data?.success) {
setRunState('completed');
setRunSummary(t('Workflow synchron abgeschlossen.'));
+ if (res.data.nodeOutputs) {
+ _extractResults(res.data.nodeOutputs);
+ }
showSuccess(t('Abgeschlossen'), t('Analyse-Workflow erfolgreich beendet.'));
} else {
throw new Error(res?.data?.error || t('Unerwartete Antwort'));
@@ -216,7 +291,7 @@ export const TrusteeAnalyseView: React.FC = () => {
setRunError(typeof msg === 'string' ? msg : JSON.stringify(msg));
showError(t('Fehler'), typeof msg === 'string' ? msg : JSON.stringify(msg));
}
- }, [activeTab, instanceId, _findWorkflow, showError, showSuccess, t]);
+ }, [activeTab, instanceId, _findWorkflow, budgetFileId, showError, showSuccess, t]);
const currentTab = _TABS.find((tabItem) => tabItem.id === activeTab) || _TABS[0];
const currentWorkflow = _findWorkflow(activeTab);
@@ -275,10 +350,56 @@ export const TrusteeAnalyseView: React.FC = () => {
+ {activeTab === 'budget' && (
+
+
+ {t('Budget-Excel hochladen')}
+
+ {budgetFileName ? (
+
+ 📄 {budgetFileName}
+
+
+ ) : (
+
+ )}
+
+ )}
+
| |