diff --git a/src/App.tsx b/src/App.tsx
index f505a0d..afa094b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -150,6 +150,7 @@ function App() {
} />
} />
} />
+ } />
} />
{/* Chat Playground Feature Views */}
diff --git a/src/api/trusteeApi.ts b/src/api/trusteeApi.ts
index 82aeb41..543c70a 100644
--- a/src/api/trusteeApi.ts
+++ b/src/api/trusteeApi.ts
@@ -126,6 +126,8 @@ export interface AccountingConfig {
isActive?: boolean;
lastSyncAt?: number;
lastSyncStatus?: string;
+ /** Error message when lastSyncStatus is "error". */
+ lastSyncErrorMessage?: string;
/** Masked config for form prefill: secret fields are "***", others have saved values. */
configMasked?: Record;
}
diff --git a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx
index 719be78..7134bb0 100644
--- a/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx
+++ b/src/components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx
@@ -38,6 +38,14 @@ export interface FormGeneratorControlsProps {
// Delete handlers
onDeleteSingle?: () => void;
onDeleteMultiple?: () => void;
+
+ // Optional batch actions (e.g. "Sync to Accounting") – shown when items are selected
+ batchActions?: {
+ label: string;
+ onClick: () => void | Promise;
+ loading?: boolean;
+ icon?: React.ReactNode;
+ }[];
// Refresh handler
onRefresh?: () => void;
@@ -76,6 +84,7 @@ export function FormGeneratorControls({
displayData,
onDeleteSingle,
onDeleteMultiple,
+ batchActions,
onRefresh,
searchable = true,
selectable = true,
@@ -127,6 +136,18 @@ export function FormGeneratorControls({
: t('formgen.delete.multiple', `Delete ${selectedCount} selected items`).replace('{count}', selectedCount.toString())}
)}
+ {batchActions?.map((action, idx) => (
+
+ ))}
)}
diff --git a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx
index 8374550..bcf3efd 100644
--- a/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx
+++ b/src/components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx
@@ -148,6 +148,13 @@ export interface FormGeneratorTableProps {
}[];
onDelete?: (row: T) => void;
onDeleteMultiple?: (rows: T[]) => void;
+ /** Batch actions shown when rows are selected (e.g. Sync to Accounting). Selection is cleared on success. */
+ batchActions?: {
+ label: string;
+ onClick: (rows: T[]) => void | Promise;
+ loading?: boolean;
+ icon?: React.ReactNode;
+ }[];
onRefresh?: () => void;
className?: string;
getRowDataAttributes?: (row: T, index: number) => Record;
@@ -189,6 +196,7 @@ export function FormGeneratorTable>({
customActions = [],
onDelete,
onDeleteMultiple,
+ batchActions = [],
onRefresh,
className = '',
getRowDataAttributes,
@@ -1387,6 +1395,11 @@ export function FormGeneratorTable>({
// Format cell value
const formatCellValue = (value: any, column: ColumnConfig, row: T) => {
+ // Custom formatter must run even when value is null/undefined (e.g. synthetic columns like _documentRefs)
+ if (column.formatter) {
+ return column.formatter(value, row);
+ }
+
if (value === null || value === undefined) {
return '-';
}
@@ -1495,11 +1508,6 @@ export function FormGeneratorTable>({
return displayValues.join(', ');
}
- // Use custom formatter if provided (but only if not an ID/hash field)
- if (column.formatter) {
- return column.formatter(value, row);
- }
-
// Check if this is a timestamp field based on name OR explicit type
// Do NOT treat arbitrary numbers as timestamps - only if field name suggests it
const isTimestampField = /(at|date|time|timestamp|created|updated|expires|checked|valuta)$/i.test(column.key);
@@ -1644,6 +1652,21 @@ export function FormGeneratorTable>({
const allSelected = selectedRows.size === selectableIndices.length && selectableIndices.length > 0;
return (selectedRows.size > 1 || allSelected) ? handleDeleteMultiple : undefined;
})()}
+ batchActions={batchActions.length > 0 ? batchActions.map((ba) => ({
+ label: ba.label,
+ icon: ba.icon,
+ loading: ba.loading,
+ onClick: async () => {
+ const rows = Array.from(selectedRows).map((i) => displayData[i]);
+ try {
+ await Promise.resolve(ba.onClick(rows));
+ setSelectedRows(new Set());
+ onRowSelect?.([]);
+ } catch {
+ // Keep selection on error so user can retry
+ }
+ },
+ })) : undefined}
onRefresh={onRefresh}
searchable={searchable}
selectable={selectable}
diff --git a/src/config/pageRegistry.tsx b/src/config/pageRegistry.tsx
index 35ccc0e..31373e2 100644
--- a/src/config/pageRegistry.tsx
+++ b/src/config/pageRegistry.tsx
@@ -69,13 +69,16 @@ export const PAGE_ICONS: Record = {
'page.admin.automation-events': ,
'page.admin.logs': ,
'page.admin.mandate-wizard': ,
+ 'page.admin.mandateWizard': ,
'page.admin.invitation-wizard': ,
+ 'page.admin.invitationWizard': ,
// Feature pages - Trustee
'page.feature.trustee.dashboard': ,
'page.feature.trustee.positions': ,
'page.feature.trustee.documents': ,
'page.feature.trustee.expense-import': ,
+ 'page.feature.trustee.scan-upload': ,
'page.feature.trustee.instance-roles': ,
'page.feature.trustee.settings': ,
diff --git a/src/contexts/FileContext.tsx b/src/contexts/FileContext.tsx
index e099da7..4d20e64 100644
--- a/src/contexts/FileContext.tsx
+++ b/src/contexts/FileContext.tsx
@@ -16,7 +16,7 @@ interface FileContextType {
downloadingFiles: Set;
}
-const FileContext = createContext(undefined);
+export const FileContext = createContext(undefined);
export function FileProvider({ children }: { children: React.ReactNode }) {
const { data: files, loading, error, refetch: refetchFiles, removeFileOptimistically } = useUserFiles();
diff --git a/src/pages/FeatureView.tsx b/src/pages/FeatureView.tsx
index 7afe1b4..83cca54 100644
--- a/src/pages/FeatureView.tsx
+++ b/src/pages/FeatureView.tsx
@@ -17,6 +17,7 @@ import { TrusteePositionsView } from './views/trustee/TrusteePositionsView';
import { TrusteeDashboardView } from './views/trustee/TrusteeDashboardView';
import { TrusteeInstanceRolesView } from './views/trustee/TrusteeInstanceRolesView';
import { TrusteeExpenseImportView } from './views/trustee/TrusteeExpenseImportView';
+import { TrusteeScanUploadView } from './views/trustee/TrusteeScanUploadView';
import { TrusteeAccountingSettingsView } from './views/trustee/TrusteeAccountingSettingsView';
// Chatbot Views
@@ -97,6 +98,7 @@ const VIEW_COMPONENTS: Record> = {
positions: TrusteePositionsView,
'instance-roles': TrusteeInstanceRolesView,
'expense-import': TrusteeExpenseImportView,
+ 'scan-upload': TrusteeScanUploadView,
settings: TrusteeAccountingSettingsView,
},
chatworkflow: {
diff --git a/src/pages/views/trustee/TrusteeAccountingSettingsView.tsx b/src/pages/views/trustee/TrusteeAccountingSettingsView.tsx
index 29797cb..66927da 100644
--- a/src/pages/views/trustee/TrusteeAccountingSettingsView.tsx
+++ b/src/pages/views/trustee/TrusteeAccountingSettingsView.tsx
@@ -151,14 +151,44 @@ export const TrusteeAccountingSettingsView: React.FC = () => {
{existingConfig?.configured && (
-
+
Connected: {existingConfig.displayLabel || existingConfig.connectorType}
- {existingConfig.lastSyncStatus && (
- <> — Last sync: {existingConfig.lastSyncStatus}>
+ {existingConfig.lastSyncStatus != null && existingConfig.lastSyncStatus !== '' && (
+ <> · Last sync: {existingConfig.lastSyncStatus}>
)}
)}
+ {existingConfig?.configured && (existingConfig.lastSyncAt != null || existingConfig.lastSyncStatus != null) && (
+
+
0
+
+
Sync-Status / Fehlerprotokoll
+
+ {existingConfig.lastSyncAt != null && (
+
+ Letzter Sync:{' '}
+ {new Date(existingConfig.lastSyncAt * 1000).toLocaleString()}
+ {existingConfig.lastSyncStatus != null && existingConfig.lastSyncStatus !== '' && (
+ <> · Status: {existingConfig.lastSyncStatus}>
+ )}
+
+ )}
+ {existingConfig.lastSyncStatus === 'error' && (existingConfig.lastSyncErrorMessage ?? '').trim() !== '' && (
+
+ {existingConfig.lastSyncErrorMessage}
+
+ )}
+ {existingConfig.lastSyncStatus === 'error' && (!existingConfig.lastSyncErrorMessage || existingConfig.lastSyncErrorMessage.trim() === '') && (
+
+ Der letzte Sync ist fehlgeschlagen. Details pro Position finden Sie unter Positionen (Spalte Sync-Status).
+
+ )}
+
+
+
+ )}
+
{/* Step 1: Select system */}
1
diff --git a/src/pages/views/trustee/TrusteeExpenseImportView.tsx b/src/pages/views/trustee/TrusteeExpenseImportView.tsx
index 506d6ec..5609ec5 100644
--- a/src/pages/views/trustee/TrusteeExpenseImportView.tsx
+++ b/src/pages/views/trustee/TrusteeExpenseImportView.tsx
@@ -135,6 +135,7 @@ export const TrusteeExpenseImportView: React.FC = () => {
const [existingAutomation, setExistingAutomation] = useState
(null);
const [isLoadingAutomation, setIsLoadingAutomation] = useState(true);
const [showInfoTooltip, setShowInfoTooltip] = useState(false);
+ const [isRunningNow, setIsRunningNow] = useState(false);
// Find all active Microsoft connections
useEffect(() => {
@@ -318,6 +319,41 @@ export const TrusteeExpenseImportView: React.FC = () => {
setIsConnecting(false);
}
};
+
+ const buildTrusteeTemplate = useCallback((connectionRef: string, folder: string) => ({
+ overview: "Trustee document pipeline: extract, process, sync to accounting",
+ tasks: [{
+ id: "Task01",
+ title: "Trustee expense import",
+ description: "Extract from SharePoint, create positions, sync to accounting",
+ objective: "Run trustee.extractFromFiles, processDocuments, syncToAccounting",
+ actionList: [
+ {
+ execMethod: "trustee",
+ execAction: "extractFromFiles",
+ execParameters: {
+ connectionReference: connectionRef,
+ sharepointFolder: folder,
+ featureInstanceId: instanceId,
+ prompt: DEFAULT_EXTRACTION_PROMPT
+ },
+ execResultLabel: "extract_result"
+ },
+ {
+ execMethod: "trustee",
+ execAction: "processDocuments",
+ execParameters: { documentList: [], featureInstanceId: instanceId },
+ execResultLabel: "process_result"
+ },
+ {
+ execMethod: "trustee",
+ execAction: "syncToAccounting",
+ execParameters: { documentList: [], featureInstanceId: instanceId },
+ execResultLabel: "sync_result"
+ }
+ ]
+ }]
+ }), [instanceId]);
const handleSave = async (activate: boolean = true) => {
// Validate required fields with user feedback
@@ -341,29 +377,11 @@ export const TrusteeExpenseImportView: React.FC = () => {
try {
const connectionReference = `connection:msft:${msftConnection.externalUsername || msftConnection.accountName || msftConnection.id}`;
+ const template = buildTrusteeTemplate(connectionReference, selectedFolder);
const automationData = {
label: 'Expense Import',
schedule: '0 22 * * *', // Daily at 22:00
- template: JSON.stringify({
- overview: "Expenses PDF to Trustee Position",
- tasks: [{
- id: "Task01",
- title: "Extract Expenses from SharePoint PDFs",
- description: "Automatic expense extraction",
- objective: "Extract expense data from PDF documents",
- actionList: [{
- execMethod: "sharepoint",
- execAction: "getExpensesFromPdf",
- execParameters: {
- connectionReference: connectionReference,
- sharepointFolder: selectedFolder,
- featureInstanceId: instanceId,
- prompt: DEFAULT_EXTRACTION_PROMPT
- },
- execResultLabel: "expense_extraction_result"
- }]
- }]
- }),
+ template: JSON.stringify(template),
placeholders: {
connectionName: connectionReference,
sharepointFolder: selectedFolder,
@@ -414,7 +432,29 @@ export const TrusteeExpenseImportView: React.FC = () => {
setIsActivating(false);
}
};
-
+
+ const handleRunNow = async () => {
+ if (!msftConnection || !selectedFolder || !instanceId) {
+ showError('Missing data', 'Please select connection and folder first.');
+ return;
+ }
+ setIsRunningNow(true);
+ setError(null);
+ try {
+ const connectionRef = getConnectionReference(msftConnection);
+ const template = buildTrusteeTemplate(connectionRef, selectedFolder);
+ const prompt = `\n${JSON.stringify(template)}\n`;
+ await api.post(`/api/chatplayground/${instanceId}/start`, { prompt }, { params: { workflowMode: 'Automation' } });
+ showSuccess('Started', 'Workflow started. Extract → Process → Sync will run once.');
+ } catch (err: any) {
+ const msg = parseErrorDetail(err.response?.data?.detail) || err.message || 'Failed to start workflow';
+ setError(msg);
+ showError('Error', msg);
+ } finally {
+ setIsRunningNow(false);
+ }
+ };
+
const handleDeactivate = async () => {
if (!existingAutomation) return;
@@ -662,6 +702,13 @@ export const TrusteeExpenseImportView: React.FC = () => {
>
{isActivating ? 'Saving...' : (existingAutomation ? 'Save & Activate' : 'Activate Daily Import')}
+
{existingAutomation && existingAutomation.active && (