ui-nyla/src/pages/views/trustee/TrusteeAccountingSettingsView.tsx
ValueOn AG 36e57a0ab4
All checks were successful
Deploy Nyla Frontend to Production / deploy (push) Successful in 45s
fix: load accounting connectors even when config endpoint fails
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-11 15:41:21 +02:00

684 lines
30 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2026 PowerOn AG
// All rights reserved.
/**
* 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 } from 'react';
import { useSearchParams } from 'react-router-dom';
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 { useBackgroundJob } from '../../../hooks/useBackgroundJob';
import {
fetchAccountingConnectors,
fetchAccountingConfig,
saveAccountingConfig,
deleteAccountingConfig,
testAccountingConnection,
exportAccountingData,
type AccountingConnectorInfo,
type AccountingConfig,
} from '../../../api/trusteeApi';
import { PeriodPicker, type PeriodValue } from '../../../components/PeriodPicker';
import styles from './TrusteeViews.module.css';
const _SETTINGS_TABS = [
{ id: 'settings', icon: '\u2699\uFE0F', color: '#2196F3' },
{ id: 'import-data', icon: '\u2B07\uFE0F', color: '#FF9800' },
];
function _settingsTabLabel(tabId: string, t: (k: string) => string): string {
switch (tabId) {
case 'settings': return t('Verbindungseinstellungen');
case 'import-data': return t('Buchhaltungsdaten importieren');
default: return tabId;
}
}
export const TrusteeAccountingSettingsView: React.FC = () => {
const { t } = useLanguage();
const { instanceId } = useCurrentInstance();
const { request } = useApiRequest();
const { showSuccess, showError } = useToast();
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = searchParams.get('tab') || _SETTINGS_TABS[0].id;
const _setActiveTab = useCallback((tab: string) => {
setSearchParams({ tab }, { replace: true });
}, [setSearchParams]);
const [connectors, setConnectors] = useState<AccountingConnectorInfo[]>([]);
const [existingConfig, setExistingConfig] = useState<AccountingConfig | null>(null);
const [selectedType, setSelectedType] = useState<string>('');
const [displayLabel, setDisplayLabel] = useState('');
const [configValues, setConfigValues] = useState<Record<string, string>>({});
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<Record<string, any> | null>(null);
const [importStatus, setImportStatus] = useState<Record<string, any> | null>(null);
const [importJobId, setImportJobId] = useState<string | null>(null);
const [clearingCache, setClearingCache] = useState(false);
const [exporting, setExporting] = useState(false);
const [importPeriod, setImportPeriod] = useState<PeriodValue | null>(null);
const [wipingData, setWipingData] = useState(false);
const { confirm, ConfirmDialog } = useConfirm();
useEffect(() => {
if (!importDone) return;
const importResetTimer = setTimeout(() => {
setImporting(false);
setImportDone(false);
setImportJobId(null);
}, 5000);
return () => clearTimeout(importResetTimer);
}, [importDone]);
const loadData = useCallback(async () => {
if (!instanceId) return;
setLoading(true);
try {
const connectorsPromise = fetchAccountingConnectors(request, instanceId);
let config: AccountingConfig = { configured: false };
try {
config = await fetchAccountingConfig(request, instanceId);
} catch (configErr: any) {
console.error('Failed to load accounting config:', configErr);
}
const availableConnectors = await connectorsPromise;
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<string, string>);
}
}
} catch (err: any) {
console.error('Failed to load accounting settings:', err);
} finally {
setLoading(false);
}
}, [instanceId, request]);
useEffect(() => {
loadData();
}, [loadData]);
const _loadImportStatus = useCallback(async () => {
if (!instanceId) return;
try {
const data = await request({ url: `/api/trustee/${instanceId}/accounting/import-status`, method: 'get' });
setImportStatus(data);
} catch (err) {
console.error('[Trustee] import-status fetch failed:', err);
}
}, [instanceId, request]);
useEffect(() => {
if (existingConfig?.configured) _loadImportStatus();
}, [existingConfig, _loadImportStatus]);
const { job: importJob } = useBackgroundJob(importJobId, {
enabled: !!importJobId,
pollMs: 2000,
onSuccess: (j) => {
const summary = (j.result || {}) as Record<string, any>;
setImportResult(summary);
const errs: string[] = Array.isArray(summary.errors) ? summary.errors : [];
if (errs.length) {
showError(t('Import teilweise fehlgeschlagen'), errs.join('; '));
} else {
showSuccess(t('Import abgeschlossen'),
t('{konten} Konten, {buchungen} Buchungen, {kontakte} Kontakte, {salden} Salden importiert.', {
konten: String(summary.accounts || 0),
buchungen: String(summary.journalEntries || 0),
kontakte: String(summary.contacts || 0),
salden: String(summary.accountBalances || 0),
}));
}
_loadImportStatus();
void loadData();
setImportDone(true);
},
onError: (j) => {
showError(t('Import fehlgeschlagen'), j.errorMessage || t('Unbekannter Fehler'));
setImportDone(true);
},
});
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 <div className={styles.loading}>{t('Buchhaltungseinstellungen werden geladen…')}</div>;
}
return (
<div className={styles.listView}>
<div className={styles.expenseImportSection}>
<h3 className={styles.sectionTitle}>{t('Buchhaltungssystem-Anbindung')}</h3>
<div style={{ display: 'flex', gap: '0.25rem', marginBottom: '1.5rem', borderBottom: '2px solid var(--border-color, #e0e0e0)', paddingBottom: 0 }}>
{_SETTINGS_TABS.map((tab) => (
<button
key={tab.id}
onClick={() => _setActiveTab(tab.id)}
style={{
padding: '0.625rem 1rem',
border: 'none',
borderBottom: activeTab === tab.id ? `3px solid ${tab.color}` : '3px solid transparent',
background: 'transparent',
color: activeTab === tab.id ? 'var(--text-primary, #1a1a1a)' : 'var(--text-secondary, #666)',
fontWeight: activeTab === tab.id ? 600 : 400,
fontSize: '0.875rem',
cursor: 'pointer',
transition: 'all 0.2s',
marginBottom: '-2px',
}}
>
<span style={{ marginRight: '0.375rem' }}>{tab.icon}</span>
{_settingsTabLabel(tab.id, t)}
</button>
))}
</div>
{activeTab === 'settings' && (
<>
<p className={styles.sectionDescription}>
{t('Verbinden Sie ein Buchhaltungssystem, um Buchungen aus dieser Trustee-Instanz automatisch zu synchronisieren.')}
</p>
{existingConfig?.configured && (
<div className={styles.successMessage} style={{ marginBottom: '0.5rem' }}>
<strong>{t('Verbunden:')}</strong> {existingConfig.displayLabel || existingConfig.connectorType}
</div>
)}
{existingConfig?.configured && existingConfig.lastSyncStatus === 'error' && (
<div className={styles.setupStep} style={{ marginTop: 0, marginBottom: '1rem' }}>
<div className={styles.stepNumber} style={{ visibility: 'hidden' }}>0</div>
<div className={styles.stepContent}>
<h4 style={{ marginTop: 0 }}>{t('Letzter Sync fehlgeschlagen')}</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{existingConfig.lastSyncAt != null && (
<div style={{ fontSize: '0.9rem' }}>
<strong>{t('Zeitpunkt:')}</strong>{' '}
{new Date(existingConfig.lastSyncAt * 1000).toLocaleString()}
</div>
)}
<div className={styles.errorMessage} style={{ marginTop: '0.25rem', padding: '0.75rem' }}>
{(existingConfig.lastSyncErrorMessage ?? '').trim() !== ''
? existingConfig.lastSyncErrorMessage
: t('Der letzte Sync ist fehlgeschlagen. Details pro Position finden Sie unter Positionen (Spalte Sync-Status).')}
</div>
</div>
</div>
</div>
)}
{/* Step 1: Select system */}
<div className={styles.setupStep}>
<div className={styles.stepNumber}>1</div>
<div className={styles.stepContent}>
<h4>{t('Buchhaltungssystem')}</h4>
<select
className={styles.folderSelect}
value={selectedType}
onChange={e => handleTypeChange(e.target.value)}
>
<option value="">{t('System auswählen…')}</option>
{connectors.map(c => (
<option key={c.connectorType} value={c.connectorType}>
{c.label || c.connectorType}
</option>
))}
</select>
</div>
</div>
{/* Step 2: Credentials */}
{selectedConnector && (
<div className={styles.setupStep}>
<div className={styles.stepNumber}>2</div>
<div className={styles.stepContent}>
<h4>{t('Zugangsdaten')}</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<div>
<label style={{ display: 'block', marginBottom: '0.25rem', fontSize: '0.85rem' }}>
{t('Anzeigename')}
</label>
<input
type="text"
className={styles.folderSelect}
value={displayLabel}
onChange={e => setDisplayLabel(e.target.value)}
placeholder={t('z. B. Run My Accounts Muster AG')}
/>
</div>
{selectedConnector.configFields.map(field => {
const datalistId = field.suggestions && field.suggestions.length > 0
? `dl-${selectedConnector.connectorType}-${field.key}`
: undefined;
return (
<div key={field.key}>
<label style={{ display: 'block', marginBottom: '0.25rem', fontSize: '0.85rem' }}>
{field.label || field.key}
{field.required && <span style={{ color: 'var(--error-color, #dc2626)' }}> *</span>}
</label>
<input
type={field.secret ? 'password' : 'text'}
className={styles.folderSelect}
value={configValues[field.key] || ''}
onChange={e => handleConfigChange(field.key, e.target.value)}
placeholder={field.placeholder || ''}
autoComplete={field.secret ? 'new-password' : 'off'}
list={datalistId}
/>
{datalistId && (
<datalist id={datalistId}>
{field.suggestions!.map(s => (
<option key={s} value={s} />
))}
</datalist>
)}
</div>
);
})}
</div>
</div>
</div>
)}
{/* Step 3: Save & Test */}
{selectedConnector && (
<div className={styles.setupStep}>
<div className={styles.stepNumber}>3</div>
<div className={styles.stepContent}>
<h4>{t('Test speichern')}</h4>
{testResult && (
<div className={testResult.success ? styles.successMessage : styles.errorMessage} style={{ marginBottom: '0.75rem' }}>
{testResult.success ? t('Verbindung erfolgreich!') : t('Verbindung fehlgeschlagen: {message}', { message: testResult.message || t('Unbekannter Fehler') })}
</div>
)}
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<button
className={styles.primaryButton}
onClick={handleSave}
disabled={saving}
>
{saving ? t('Speichern…') : t('Konfiguration speichern')}
</button>
{existingConfig?.configured && (
<button
className={styles.secondaryButton}
onClick={handleTestConnection}
disabled={testing}
>
{testing ? t('Teste…') : t('Verbindung testen')}
</button>
)}
{existingConfig?.configured && (
<button
className={styles.secondaryButton}
onClick={handleRemove}
disabled={saving}
style={{ color: 'var(--error-color, #dc2626)' }}
>
{t('Anbindung entfernen')}
</button>
)}
</div>
</div>
</div>
)}
</>
)}
{activeTab === 'import-data' && (
<>
{!existingConfig?.configured && (
<div className={styles.infoBox}>
<p>
{t('Bevor Sie Daten importieren können, richten Sie zuerst die Verbindung zum Buchhaltungssystem im Tab «Verbindungseinstellungen» ein.')}
</p>
</div>
)}
{existingConfig?.configured && (
<div className={styles.setupStep}>
<div className={styles.stepNumber} style={{ visibility: 'hidden' }}>0</div>
<div className={styles.stepContent}>
<h4 style={{ marginTop: 0 }}>{t('Buchhaltungsdaten importieren')}</h4>
<p style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginBottom: '0.75rem' }}>
{t('Kontenplan, Buchungen, Kontakte und Salden aus dem Buchhaltungssystem einlesen. Diese Daten stehen anschließend im KI-Workspace für Analysen zur Verfügung.')}
</p>
{(() => {
const lastSyncAt = importStatus?.lastSyncAt as number | null | undefined;
const winFrom = importStatus?.lastSyncDateFrom as string | undefined;
const winTo = importStatus?.lastSyncDateTo as string | undefined;
const counts = (importStatus?.lastSyncCounts || {}) as Record<string, any>;
const oldestBooking = counts.oldestBookingDate as string | null | undefined;
const newestBooking = counts.newestBookingDate as string | null | undefined;
const timeWindow = winFrom && winTo
? t('{from} bis {to}', { from: winFrom, to: winTo })
: winFrom
? t('ab {from}', { from: winFrom })
: winTo
? t('bis {to}', { to: winTo })
: null;
const dataWindow = oldestBooking && newestBooking
? t('{from} bis {to}', { from: oldestBooking, to: newestBooking })
: oldestBooking
? t('ab {from}', { from: oldestBooking })
: newestBooking
? t('bis {to}', { to: newestBooking })
: null;
return (
<div style={{
fontSize: '0.8rem',
color: 'var(--text-secondary)',
background: 'var(--surface-color, #f5f5f5)',
border: '1px solid var(--border-color, #e0e0e0)',
borderRadius: '6px',
padding: '0.5rem 0.75rem',
marginBottom: '0.75rem',
}}>
{lastSyncAt ? (
<>
<div>
<strong>{t('Letzter Import:')}</strong> {new Date(lastSyncAt * 1000).toLocaleString()}
{timeWindow && (
<> {' '}&middot; <strong>{t('Angefragtes Zeitfenster:')}</strong> {timeWindow}</>
)}
</div>
{dataWindow && (
<div style={{ marginTop: '0.2rem' }}>
<strong>{t('Tatsächlich erhaltene Buchungen:')}</strong>{' '}
{dataWindow}
<span style={{ marginLeft: '0.5rem', fontStyle: 'italic' }}>
({t('älteste/neuste Buchung im Import — zur Vollständigkeitsprüfung')})
</span>
</div>
)}
<div style={{ marginTop: '0.2rem' }}>
{t('{konten} Konten, {buchungen} Buchungen ({zeilen} Zeilen), {kontakte} Kontakte, {salden} Salden', {
konten: String(counts.accounts ?? 0),
buchungen: String(counts.journalEntries ?? 0),
zeilen: String(counts.journalLines ?? 0),
kontakte: String(counts.contacts ?? 0),
salden: String(counts.accountBalances ?? 0),
})}
</div>
</>
) : (
<div>
<em>{t('Noch kein Import durchgeführt. Wähle unten ein Zeitfenster und klicke auf «Daten jetzt einlesen».')}</em>
</div>
)}
</div>
);
})()}
<div style={{ display: 'flex', gap: '0.75rem', marginBottom: '0.75rem', flexWrap: 'wrap', alignItems: 'flex-end' }}>
<div>
<label style={{ display: 'block', fontSize: '0.8rem', marginBottom: '0.25rem' }}>{t('Zeitraum (optional)')}</label>
<PeriodPicker
value={importPeriod}
onChange={setImportPeriod}
direction="past"
placeholder={t('Alle Daten')}
/>
</div>
</div>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
<button
className={styles.primaryButton}
disabled={importing}
onClick={async () => {
if (!instanceId) return;
setImporting(true);
setImportResult(null);
setImportJobId(null);
try {
const body: Record<string, string> = {};
if (importPeriod?.fromDate) body.dateFrom = importPeriod.fromDate;
if (importPeriod?.toDate) body.dateTo = importPeriod.toDate;
const result = await request({ url: `/api/trustee/${instanceId}/accounting/import-data`, method: 'post', data: body });
const newJobId: string | undefined = result?.jobId;
if (newJobId) {
setImportJobId(newJobId);
} else {
showError(t('Import fehlgeschlagen'), t('Kein jobId vom Server erhalten'));
setImportDone(true);
}
} catch (err: any) {
showError(t('Import fehlgeschlagen'), err.response?.data?.detail || err.message || t('Unbekannter Fehler'));
setImportDone(true);
}
}}
>
{importing
? (importJob?.progressMessage || t('Importiere…'))
: t('Daten jetzt einlesen')}
</button>
{importing && importJob && (
<div style={{ flex: '1 1 100%', marginTop: '0.5rem' }}>
<div style={{
width: '100%', height: '6px', background: 'var(--surface-color, #eee)',
borderRadius: '3px', overflow: 'hidden',
}}>
<div style={{
width: `${Math.max(2, importJob.progress || 0)}%`,
height: '100%',
background: 'var(--primary-color, #4CAF50)',
transition: 'width 0.4s ease',
}} />
</div>
<div style={{ fontSize: '0.75rem', color: 'var(--text-secondary)', marginTop: '0.25rem' }}>
{importJob.progress}% {importJob.progressMessage ? `· ${importJob.progressMessage}` : ''}
</div>
</div>
)}
<button
className={styles.secondaryButton}
disabled={clearingCache}
title={t('Leert nur den Antwort-Cache des KI-Agenten (~5 Min). Synchronisierte Daten bleiben unverändert.')}
onClick={async () => {
if (!instanceId) return;
setClearingCache(true);
try {
const result = await request({ url: `/api/trustee/${instanceId}/accounting/clear-cache`, method: 'post' });
showSuccess(t('KI-Antwort-Cache geleert'), t('{n} gecachte KI-Antworten entfernt. Die nächste KI-Abfrage berechnet frische Antworten aus den synchronisierten Tabellen. Diese Aktion löscht KEINE importierten Buchungen.', { n: String(result?.cleared ?? 0) }));
} catch (err: any) {
showError(t('Fehler'), err.response?.data?.detail || err.message || t('Cache konnte nicht geleert werden.'));
} finally {
setClearingCache(false);
}
}}
>
{clearingCache ? t('Leere…') : t('KI-Antwort-Cache leeren')}
</button>
<button
className={styles.secondaryButton}
disabled={wipingData}
title={t('Löscht alle importierten Buchungen, Konten, Kontakte und Salden für diese Instanz. Die Verbindungseinstellungen bleiben erhalten.')}
style={{ color: 'var(--error-color, #dc2626)' }}
onClick={async () => {
if (!instanceId) return;
const ok = await confirm(
t('Wirklich alle importierten Buchhaltungsdaten dieser Instanz aus der lokalen Datenbank löschen? Die Verbindungseinstellungen bleiben erhalten. Sie können danach jederzeit erneut importieren.'),
{
title: t('Importierte Daten löschen'),
confirmLabel: t('Löschen'),
variant: 'danger',
},
);
if (!ok) return;
setWipingData(true);
try {
const result = await request({ url: `/api/trustee/${instanceId}/accounting/wipe-imported-data`, method: 'post' });
showSuccess(
t('Daten gelöscht'),
t('{n} Datensätze entfernt. Sie können nun einen frischen Import starten.', { n: String(result?.totalRemoved ?? 0) }),
);
_loadImportStatus();
void loadData();
} catch (err: any) {
showError(t('Fehler'), err.response?.data?.detail || err.message || t('Daten konnten nicht gelöscht werden.'));
} finally {
setWipingData(false);
}
}}
>
{wipingData ? t('Lösche…') : t('Importierte Daten löschen')}
</button>
<button
className={styles.secondaryButton}
disabled={exporting}
onClick={async () => {
if (!instanceId) return;
setExporting(true);
try {
await exportAccountingData(request, instanceId);
showSuccess(t('Export gestartet'), t('Die Daten werden als JSON-Datei heruntergeladen.'));
} catch (err: any) {
showError(t('Fehler'), err.response?.data?.detail || err.message || t('Export fehlgeschlagen.'));
} finally {
setExporting(false);
}
}}
>
{exporting ? t('Exportiere…') : t('Alle Daten exportieren (JSON)')}
</button>
</div>
{importResult && !importResult.errors?.length && (
<div className={styles.successMessage} style={{ marginTop: '0.75rem' }}>
{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),
})}
</div>
)}
{importStatus && (importStatus.accounts > 0 || importStatus.journalEntries > 0) && (
<div style={{ marginTop: '0.75rem', fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
<strong>{t('Aktueller Datenbestand:')}</strong>{' '}
{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 && (
<> &middot; {t('Letzter Import:')} {new Date(importStatus.lastSyncAt * 1000).toLocaleString()}</>
)}
</div>
)}
</div>
</div>
)}
</>
)}
</div>
<ConfirmDialog />
</div>
);
};
export default TrusteeAccountingSettingsView;