/** * 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 { useLanguage } from '../../../providers/language/LanguageContext'; 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 { t } = useLanguage(); 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 [clearingCache, setClearingCache] = useState(false); const [dateFrom, setDateFrom] = useState(''); const [dateTo, setDateTo] = useState(''); const mountedRef = useRef(true); const { confirm, ConfirmDialog } = useConfirm(); useEffect(() => { if (!importDone) return; const importResetTimer = setTimeout(() => { setImporting(false); setImportDone(false); }, 5000); return () => clearTimeout(importResetTimer); }, [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(t('Gespeichert'), t('Die Buchhaltungskonfiguration wurde erfolgreich gespeichert.')); await loadData(); } catch (err: any) { showError(t('Fehler'), err.response?.data?.detail || err.message || t('Speichern der Konfiguration fehlgeschlagen.')); } 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(t('Verbindung OK'), t('Verbindung zum Buchhaltungssystem erfolgreich.')); } else { showError(t('Verbindung fehlgeschlagen'), result.errorMessage || t('Keine Verbindung möglich.')); } } catch (err: any) { const msg = err.response?.data?.detail || err.message || t('Verbindungstest fehlgeschlagen.'); setTestResult({ success: false, message: msg }); showError(t('Fehler'), msg); } finally { setTesting(false); } }; const handleRemove = async () => { if (!instanceId) return; const ok = await confirm(t('Buchhaltungsanbindung entfernen? Synchronisierte Daten bleiben erhalten.'), { title: t('Anbindung entfernen'), confirmLabel: t('Entfernen'), variant: 'danger', }); if (!ok) return; setSaving(true); try { await deleteAccountingConfig(request, instanceId); showSuccess(t('Entfernt'), t('Buchhaltungsanbindung wurde entfernt.')); setSelectedType(''); setDisplayLabel(''); setConfigValues({}); setTestResult(null); await loadData(); } catch (err: any) { showError(t('Fehler'), err.message || t('Entfernen der Konfiguration fehlgeschlagen.')); } finally { setSaving(false); } }; const selectedConnector = _getSelectedConnector(); if (loading) { return
{t('Buchhaltungseinstellungen werden geladen…')}
; } return (

{t('Buchhaltungssystem-Anbindung')}

{t('Verbinden Sie ein Buchhaltungssystem, um Buchungen aus dieser Trustee-Instanz automatisch zu synchronisieren.')}

{existingConfig?.configured && (
{t('Verbunden:')} {existingConfig.displayLabel || existingConfig.connectorType} {existingConfig.lastSyncStatus != null && existingConfig.lastSyncStatus !== '' && ( <> · {t('Letzter Sync:')} {existingConfig.lastSyncStatus} )}
)} {existingConfig?.configured && (existingConfig.lastSyncAt != null || existingConfig.lastSyncStatus != null) && (
0

{t('Syncstatus Fehlerprotokoll')}

{existingConfig.lastSyncAt != null && (
{t('Letzter Sync:')}{' '} {new Date(existingConfig.lastSyncAt * 1000).toLocaleString()} {existingConfig.lastSyncStatus != null && existingConfig.lastSyncStatus !== '' && ( <> · {t('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

{t('Buchhaltungssystem')}

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

{t('Zugangsdaten')}

setDisplayLabel(e.target.value)} placeholder={t('z. B. 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

{t('Test speichern')}

{testResult && (
{testResult.success ? t('Verbindung erfolgreich!') : t('Verbindung fehlgeschlagen: {message}', { message: testResult.message || t('Unbekannter Fehler') })}
)}
{existingConfig?.configured && ( )} {existingConfig?.configured && ( )}
)} {/* Step 4: Import Accounting Data */} {existingConfig?.configured && (
4

{t('Buchhaltungsdaten importieren')}

{t('Kontenplan, Buchungen, Kontakte und Salden aus dem Buchhaltungssystem einlesen. Diese Daten stehen anschließend im KI-Workspace für Analysen zur Verfügung.')}

setDateFrom(e.target.value)} style={{ width: '160px' }} />
setDateTo(e.target.value)} style={{ width: '160px' }} />
{[ { label: t('Laufendes Jahr'), from: `${new Date().getFullYear()}-01-01`, to: new Date().toISOString().slice(0, 10) }, { label: t('Letztes Jahr'), from: `${new Date().getFullYear() - 1}-01-01`, to: `${new Date().getFullYear() - 1}-12-31`, }, { label: t('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 && (
{t('Import abgeschlossen in {sek}s:', { sek: String(importResult.durationSeconds) })}{' '} {t('{konten} Konten, {buchungen} Buchungen ({zeilen} Zeilen), {kontakte} Kontakte, {salden} Salden', { konten: String(importResult.accounts), buchungen: String(importResult.journalEntries), zeilen: String(importResult.journalLines), kontakte: String(importResult.contacts), salden: String(importResult.accountBalances), })}
)} {importStatus && (importStatus.accounts > 0 || importStatus.journalEntries > 0) && (
{t('Aktueller Datenbestand:')}{' '} {t('{konten} Konten, {buchungen} Buchungen, {zeilen} Zeilen, {kontakte} Kontakte, {salden} Salden', { konten: String(importStatus.accounts), buchungen: String(importStatus.journalEntries), zeilen: String(importStatus.journalLines), kontakte: String(importStatus.contacts), salden: String(importStatus.accountBalances), })} {importStatus.lastSyncAt && ( <> · {t('Letzter Import:')} {new Date(importStatus.lastSyncAt * 1000).toLocaleString()} )}
)}
)}
); }; export default TrusteeAccountingSettingsView;