From 843b481c36d3ed4b0bf78fa96e8b5d976b0eacaf Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Sun, 22 Feb 2026 00:07:40 +0100 Subject: [PATCH] sync trustee feature with rma --- src/App.tsx | 1 + src/api/trusteeApi.ts | 2 + .../FormGeneratorControls.tsx | 21 ++ .../FormGeneratorTable/FormGeneratorTable.tsx | 33 ++- src/config/pageRegistry.tsx | 3 + src/contexts/FileContext.tsx | 2 +- src/pages/FeatureView.tsx | 2 + .../trustee/TrusteeAccountingSettingsView.tsx | 36 ++- .../trustee/TrusteeExpenseImportView.tsx | 89 ++++-- .../views/trustee/TrusteePositionsView.tsx | 272 +++++++++++++++++- .../views/trustee/TrusteeScanUploadView.tsx | 272 ++++++++++++++++++ src/pages/views/trustee/index.ts | 1 + src/types/mandate.ts | 2 + 13 files changed, 691 insertions(+), 45 deletions(-) create mode 100644 src/pages/views/trustee/TrusteeScanUploadView.tsx 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 && ( + ))} + + ); + }, + }), [handleDownloadDocument, downloadingDocIds]); + + // Map positionId -> display sync status: prefer synced over error (successful retry hides old error) + const syncByPosition = useMemo(() => { + const m = new Map(); + for (const s of syncStatusItems) { + const cur = m.get(s.positionId); + const prefer = + !cur || + s.syncStatus === 'synced' || + (cur.syncStatus !== 'synced' && s.syncStatus === 'error'); + if (prefer) m.set(s.positionId, { syncStatus: s.syncStatus, errorMessage: s.errorMessage }); + } + return m; + }, [syncStatusItems]); + + const syncStatusColumn: ColumnConfig = useMemo( + () => ({ + key: '_syncStatus', + label: 'Sync-Status', + sortable: false, + filterable: false, + searchable: false, + width: 160, + minWidth: 100, + maxWidth: 280, + formatter: (_value: unknown, row: TrusteePosition) => { + const info = syncByPosition.get(row.id); + if (!info) + return ; + if (info.syncStatus === 'error') + return ( + + Fehler{info.errorMessage ? ': ' + (info.errorMessage.length > 40 ? info.errorMessage.slice(0, 37) + '…' : info.errorMessage) : ''} + + ); + if (info.syncStatus === 'synced') + return Synchronisiert; + return {info.syncStatus}; + }, + }), + [syncByPosition] + ); + + // Desired column order: Belege (icons), Sync-Status, Erstellt am, Valuta, Tags, Company, then the rest + const positionColumnOrder = [ + '_documentRefs', // Belege (download icons) + '_syncStatus', // Sync-Status + '_createdAt', // Erstellt am + 'valuta', // Valuta date + 'tags', + 'company', + ]; + + // Generate columns from attributes + synthetic columns, then reorder const columns = useMemo(() => { const attrColumns = (attributes || []) .filter(attr => !hiddenColumns.includes(attr.name)) @@ -67,9 +277,8 @@ export const TrusteePositionsView: React.FC = () => { minWidth: attr.minWidth || 100, maxWidth: attr.maxWidth || 400, })); - - // Add _createdAt system column - attrColumns.push({ + + const createdAtCol = { key: '_createdAt', label: 'Erstellt am', type: 'timestamp' as any, @@ -79,10 +288,26 @@ export const TrusteePositionsView: React.FC = () => { width: 150, minWidth: 120, maxWidth: 200, - }); - - return attrColumns; - }, [attributes]); + }; + + const allColumns = [...attrColumns, belegeColumn, syncStatusColumn, createdAtCol]; + const byKey = new Map(allColumns.map(c => [c.key, c])); + + const ordered: typeof allColumns = []; + for (const key of positionColumnOrder) { + const col = byKey.get(key); + if (col) { + ordered.push(col); + byKey.delete(key); + } + } + const restKeys = allColumns.map(c => c.key).filter(k => byKey.has(k)); + for (const key of restKeys) { + const col = byKey.get(key); + if (col) ordered.push(col); + } + return ordered; + }, [attributes, belegeColumn, syncStatusColumn]); // Check permissions const canCreate = permissions?.create !== 'n'; @@ -229,7 +454,15 @@ export const TrusteePositionsView: React.FC = () => { searchable={true} filterable={true} sortable={true} - selectable={false} + selectable={true} + batchActions={[ + { + label: 'Buchhaltung synchronisieren', + icon: , + loading: syncingPositionIds.size > 0, + onClick: handleBatchSyncToAccounting, + }, + ]} actionButtons={[ ...(canUpdate ? [{ type: 'edit' as const, @@ -242,6 +475,15 @@ export const TrusteePositionsView: React.FC = () => { loading: (row: TrusteePosition) => deletingItems.has(row.id), }] : []), ]} + customActions={[ + { + id: 'sync-accounting', + icon: , + title: 'In Buchhaltung synchronisieren', + onClick: handleSingleSyncToAccounting, + loading: (row: TrusteePosition) => syncingPositionIds.has(row.id), + }, + ]} onDelete={handleDeletePos} hookData={{ refetch, diff --git a/src/pages/views/trustee/TrusteeScanUploadView.tsx b/src/pages/views/trustee/TrusteeScanUploadView.tsx new file mode 100644 index 0000000..3226e9f --- /dev/null +++ b/src/pages/views/trustee/TrusteeScanUploadView.tsx @@ -0,0 +1,272 @@ +/** + * TrusteeScanUploadView (UC1) + * + * Mobile-friendly scan/upload: photo, drag-and-drop, or file picker. + * Uploads files, then starts the trustee pipeline (extract → process → sync) with fileIds. + */ + +import React, { useState, useCallback, useContext } from 'react'; +import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; +import { useToast } from '../../../contexts/ToastContext'; +import { FileContext } from '../../../contexts/FileContext'; +import api from '../../../api'; +import styles from './TrusteeViews.module.css'; + +const DEFAULT_EXTRACTION_PROMPT = `Extrahiere Spesendaten aus dem Dokument. Gib Dokumenttyp (INVOICE, EXPENSE_RECEIPT, …) und Datensätze zurück. CSV-Spalten: valuta,company,desc,bookingAmount,bookingCurrency,vatPercentage,vatAmount,debitAccountNumber,creditAccountNumber,tags.`; + +interface UploadedEntry { + fileId: string; + fileName: string; +} + +const parseErrorDetail = (detail: unknown): string => { + if (typeof detail === 'string') return detail; + if (Array.isArray(detail)) { + return (detail as Array<{ msg?: string }>).map((e) => e.msg || JSON.stringify(e)).join(', '); + } + if (detail && typeof detail === 'object' && 'msg' in (detail as object)) return (detail as { msg: string }).msg; + if (detail && typeof detail === 'object' && 'message' in (detail as object)) return (detail as { message: string }).message; + return String(detail); +}; + +export const TrusteeScanUploadView: React.FC = () => { + const { instanceId } = useCurrentInstance(); + const { showSuccess, showError } = useToast(); + const fileContext = useContext(FileContext); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [isDragging, setIsDragging] = useState(false); + const [isStarting, setIsStarting] = useState(false); + const [error, setError] = useState(null); + + const handleFileUpload = fileContext?.handleFileUpload; + const uploadingFile = fileContext?.uploadingFile ?? false; + + const addFile = useCallback((fileId: string, fileName: string) => { + setUploadedFiles((prev) => { + if (prev.some((f) => f.fileId === fileId)) return prev; + return [...prev, { fileId, fileName }]; + }); + }, []); + + const removeFile = useCallback((fileId: string) => { + setUploadedFiles((prev) => prev.filter((f) => f.fileId !== fileId)); + }, []); + + const onFiles = useCallback( + async (files: FileList | null) => { + if (!files?.length || !handleFileUpload || !instanceId) return; + setError(null); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const name = file.name.toLowerCase(); + if (!name.endsWith('.pdf') && !name.endsWith('.jpg') && !name.endsWith('.jpeg')) { + showError('Invalid file', `${file.name}: only PDF and JPG are supported.`); + continue; + } + const result = await handleFileUpload(file); + if (result.success && result.fileData) { + const data = result.fileData?.file || result.fileData; + const id = data?.id || result.fileData?.id; + if (id) addFile(id, file.name); + else showError('Upload', 'Could not get file ID from server.'); + } else { + showError('Upload failed', result.error || 'Unknown error'); + } + } + }, + [handleFileUpload, instanceId, showError, addFile] + ); + + const onDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + onFiles(e.dataTransfer.files); + }, + [onFiles] + ); + + const onDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(true); + }, []); + + const onDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setIsDragging(false); + }, []); + + const onInputChange = useCallback( + (e: React.ChangeEvent) => { + onFiles(e.target.files); + e.target.value = ''; + }, + [onFiles] + ); + + const buildTemplate = useCallback( + (fileIds: string[]) => ({ + overview: 'Trustee pipeline from uploaded files', + tasks: [ + { + id: 'Task01', + title: 'Extract, process, sync', + objective: 'Run trustee pipeline on uploaded files', + actionList: [ + { + execMethod: 'trustee', + execAction: 'extractFromFiles', + execParameters: { + fileIds, + 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 handleStart = useCallback(async () => { + if (!instanceId || uploadedFiles.length === 0) { + showError('Missing data', 'Please upload at least one PDF or JPG first.'); + return; + } + setIsStarting(true); + setError(null); + try { + const fileIds = uploadedFiles.map((f) => f.fileId); + const template = buildTemplate(fileIds); + const prompt = `\n${JSON.stringify(template)}\n`; + await api.post(`/api/chatplayground/${instanceId}/start`, { prompt }, { params: { workflowMode: 'Automation' } }); + showSuccess('Started', 'Pipeline started: Extract → Process → Sync. You can follow progress in Workflows.'); + } catch (err: any) { + const msg = parseErrorDetail(err.response?.data?.detail) || err.message || 'Failed to start workflow'; + setError(msg); + showError('Error', msg); + } finally { + setIsStarting(false); + } + }, [instanceId, uploadedFiles, buildTemplate, showSuccess, showError]); + + if (!fileContext) { + return ( +
+

File upload is not available in this context.

+
+ ); + } + + return ( +
+
+

Scan / Upload

+

+ Upload PDF or JPG documents (receipts, invoices). Then start the pipeline: extract data → create positions → sync to accounting. +

+ {error &&
{error}
} + + {/* Drop zone + file input */} +
+

+ Drag files here or choose below. PDF and JPG only. +

+ + {/* Mobile: optional camera capture */} + +
+ + {/* Uploaded list */} + {uploadedFiles.length > 0 && ( + <> +

+ {uploadedFiles.length} file(s) ready. Click Start to run the pipeline. +

+
    + {uploadedFiles.map(({ fileId, fileName }) => ( +
  • + {fileName} + +
  • + ))} +
+ + + )} +
+
+ ); +}; + +export default TrusteeScanUploadView; diff --git a/src/pages/views/trustee/index.ts b/src/pages/views/trustee/index.ts index 5d0583f..85705ff 100644 --- a/src/pages/views/trustee/index.ts +++ b/src/pages/views/trustee/index.ts @@ -7,4 +7,5 @@ export { TrusteeDocumentsView } from './TrusteeDocumentsView'; export { TrusteePositionsView } from './TrusteePositionsView'; export { TrusteeInstanceRolesView } from './TrusteeInstanceRolesView'; export { TrusteeExpenseImportView } from './TrusteeExpenseImportView'; +export { TrusteeScanUploadView } from './TrusteeScanUploadView'; export { TrusteeAccountingSettingsView } from './TrusteeAccountingSettingsView'; diff --git a/src/types/mandate.ts b/src/types/mandate.ts index 1a8a2b8..3af3f82 100644 --- a/src/types/mandate.ts +++ b/src/types/mandate.ts @@ -208,7 +208,9 @@ export const FEATURE_REGISTRY: Record = { { code: 'documents', label: { de: 'Dokumente', en: 'Documents' }, path: 'documents' }, { code: 'position-documents', label: { de: 'Zuordnungen', en: 'Assignments' }, path: 'position-documents' }, { code: 'expense-import', label: { de: 'Spesen Import', en: 'Expense Import' }, path: 'expense-import' }, + { code: 'scan-upload', label: { de: 'Scannen / Hochladen', en: 'Scan / Upload' }, path: 'scan-upload' }, { code: 'instance-roles', label: { de: 'Rollen & Rechte', en: 'Roles & Permissions' }, path: 'instance-roles', adminOnly: true }, + { code: 'settings', label: { de: 'Buchhaltungseinstellungen', en: 'Accounting Settings' }, path: 'settings' }, ] }, chatworkflow: {