/** * TrusteeAccountingSettingsView * * Settings page for configuring the accounting system integration. * Allows selecting a connector (RMA, Bexio, Abacus), entering credentials, * testing the connection, and removing the integration. */ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useCurrentInstance } from '../../../hooks/useCurrentInstance'; import { useApiRequest } from '../../../hooks/useApi'; import { useToast } from '../../../contexts/ToastContext'; import { useConfirm } from '../../../hooks/useConfirm'; import { fetchAccountingConnectors, fetchAccountingConfig, saveAccountingConfig, deleteAccountingConfig, testAccountingConnection, type AccountingConnectorInfo, type AccountingConfig, } from '../../../api/trusteeApi'; import styles from './TrusteeViews.module.css'; export const TrusteeAccountingSettingsView: React.FC = () => { const { instanceId } = useCurrentInstance(); const { request } = useApiRequest(); const { showSuccess, showError } = useToast(); const [connectors, setConnectors] = useState([]); const [existingConfig, setExistingConfig] = useState(null); const [selectedType, setSelectedType] = useState(''); const [displayLabel, setDisplayLabel] = useState(''); const [configValues, setConfigValues] = useState>({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState<{ success: boolean; message?: string } | null>(null); const [importing, setImporting] = useState(false); const [importDone, setImportDone] = useState(false); const [importResult, setImportResult] = useState | null>(null); const [importStatus, setImportStatus] = useState | null>(null); const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const mountedRef = useRef(true); const { confirm, ConfirmDialog } = useConfirm(); useEffect(() => { if (!importDone) return; const t = setTimeout(() => { setImporting(false); setImportDone(false); }, 5000); return () => clearTimeout(t); }, [importDone]); const loadData = useCallback(async () => { if (!instanceId) return; setLoading(true); try { const [availableConnectors, config] = await Promise.all([ fetchAccountingConnectors(request, instanceId), fetchAccountingConfig(request, instanceId), ]); setConnectors(availableConnectors || []); setExistingConfig(config); if (config?.configured && config.connectorType) { setSelectedType(config.connectorType); setDisplayLabel(config.displayLabel || ''); if (config.configMasked && typeof config.configMasked === 'object') { setConfigValues(config.configMasked as Record); } } } catch (err: any) { console.error('Failed to load accounting settings:', err); } finally { setLoading(false); } }, [instanceId, request]); useEffect(() => { loadData(); return () => { mountedRef.current = false; }; }, [loadData]); const _loadImportStatus = useCallback(async () => { if (!instanceId) return; try { const res = await request({ url: `/api/trustee/${instanceId}/accounting/import-status`, method: 'get' }); if (mountedRef.current) setImportStatus(res.data); } catch { /* ignore */ } }, [instanceId, request]); useEffect(() => { if (existingConfig?.configured) _loadImportStatus(); }, [existingConfig, _loadImportStatus]); const _getSelectedConnector = (): AccountingConnectorInfo | undefined => { return connectors.find(c => c.connectorType === selectedType); }; const handleTypeChange = (newType: string) => { setSelectedType(newType); setConfigValues({}); setTestResult(null); }; const handleConfigChange = (key: string, value: string) => { setConfigValues(prev => ({ ...prev, [key]: value })); }; const handleSave = async () => { if (!instanceId || !selectedType) return; setSaving(true); try { await saveAccountingConfig(request, instanceId, { connectorType: selectedType, displayLabel, config: configValues, }); showSuccess('Saved', 'Accounting configuration saved successfully.'); await loadData(); } catch (err: any) { showError('Error', err.response?.data?.detail || err.message || 'Failed to save configuration.'); } finally { setSaving(false); } }; const handleTestConnection = async () => { if (!instanceId) return; setTesting(true); setTestResult(null); try { const result = await testAccountingConnection(request, instanceId); setTestResult({ success: result.success, message: result.errorMessage }); if (result.success) { showSuccess('Connection OK', 'Successfully connected to the accounting system.'); } else { showError('Connection Failed', result.errorMessage || 'Could not connect.'); } } catch (err: any) { const msg = err.response?.data?.detail || err.message || 'Connection test failed.'; setTestResult({ success: false, message: msg }); showError('Error', msg); } finally { setTesting(false); } }; const handleRemove = async () => { if (!instanceId) return; const ok = await confirm('Remove the accounting integration? This does not delete synced data.', { title: 'Remove Integration', confirmLabel: 'Remove', variant: 'danger', }); if (!ok) return; setSaving(true); try { await deleteAccountingConfig(request, instanceId); showSuccess('Removed', 'Accounting integration removed.'); setSelectedType(''); setDisplayLabel(''); setConfigValues({}); setTestResult(null); await loadData(); } catch (err: any) { showError('Error', err.message || 'Failed to remove configuration.'); } finally { setSaving(false); } }; const selectedConnector = _getSelectedConnector(); if (loading) { return
Loading accounting settings...
; } return (

Accounting System Integration

Connect an accounting system to automatically sync bookings from this Trustee instance.

{existingConfig?.configured && (
Connected: {existingConfig.displayLabel || existingConfig.connectorType} {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

Accounting System

{/* Step 2: Credentials */} {selectedConnector && (
2

Credentials

setDisplayLabel(e.target.value)} placeholder="e.g. Run My Accounts - Muster AG" />
{selectedConnector.configFields.map(field => (
handleConfigChange(field.key, e.target.value)} placeholder={field.placeholder || ''} autoComplete={field.secret ? 'new-password' : 'off'} />
))}
)} {/* Step 3: Save & Test */} {selectedConnector && (
3

Save & Test

{testResult && (
{testResult.success ? 'Connection successful!' : `Connection failed: ${testResult.message || 'Unknown error'}`}
)}
{existingConfig?.configured && ( )} {existingConfig?.configured && ( )}
)} {/* Step 4: Import Accounting Data */} {existingConfig?.configured && (
4

Buchhaltungsdaten importieren

Kontenplan, Buchungen, Kontakte und Salden aus dem Buchhaltungssystem einlesen. Diese Daten stehen anschliessend im AI Workspace fuer Analysen zur Verfuegung.

setDateFrom(e.target.value)} style={{ width: '160px' }} />
setDateTo(e.target.value)} style={{ width: '160px' }} />
{[ { label: 'YTD', from: `${new Date().getFullYear()}-01-01`, to: new Date().toISOString().slice(0, 10) }, { label: 'Letztes Jahr', from: `${new Date().getFullYear() - 1}-01-01`, to: `${new Date().getFullYear() - 1}-12-31`, }, { label: 'Letzter Monat', from: (() => { const d = new Date(); d.setDate(1); d.setMonth(d.getMonth() - 1); return d.toISOString().slice(0, 10); })(), to: (() => { const d = new Date(); d.setDate(0); return d.toISOString().slice(0, 10); })(), }, ].map(s => ( ))}
{importResult && !importResult.errors?.length && (
Import abgeschlossen in {importResult.durationSeconds}s: {' '}{importResult.accounts} Konten, {importResult.journalEntries} Buchungen ({importResult.journalLines} Zeilen), {' '}{importResult.contacts} Kontakte, {importResult.accountBalances} Salden
)} {importStatus && (importStatus.accounts > 0 || importStatus.journalEntries > 0) && (
Aktueller Datenbestand:{' '} {importStatus.accounts} Konten, {importStatus.journalEntries} Buchungen, {' '}{importStatus.journalLines} Zeilen, {importStatus.contacts} Kontakte, {' '}{importStatus.accountBalances} Salden {importStatus.lastSyncAt && ( <> · Letzter Import: {new Date(importStatus.lastSyncAt * 1000).toLocaleString()} )}
)}
)}
); }; export default TrusteeAccountingSettingsView;