trustee connections
This commit is contained in:
parent
d45dab587f
commit
cdea97e2cf
9 changed files with 444 additions and 124 deletions
|
|
@ -80,8 +80,7 @@ export interface TrusteeDocument {
|
|||
|
||||
export interface TrusteePosition {
|
||||
id: string;
|
||||
organisationId: string;
|
||||
contractId: string;
|
||||
documentId?: string;
|
||||
valuta?: string;
|
||||
transactionDateTime?: number;
|
||||
company: string;
|
||||
|
|
@ -93,6 +92,11 @@ export interface TrusteePosition {
|
|||
originalAmount: number;
|
||||
vatPercentage: number;
|
||||
vatAmount: number;
|
||||
debitAccountNumber?: string;
|
||||
creditAccountNumber?: string;
|
||||
taxCode?: string;
|
||||
costCenter?: string;
|
||||
bookingReference?: string;
|
||||
mandateId?: string;
|
||||
_createdAt?: number;
|
||||
_modifiedAt?: number;
|
||||
|
|
@ -101,18 +105,39 @@ export interface TrusteePosition {
|
|||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface TrusteePositionDocument {
|
||||
export interface AccountingConnectorInfo {
|
||||
connectorType: string;
|
||||
label: Record<string, string>;
|
||||
configFields: Array<{
|
||||
key: string;
|
||||
label: Record<string, string>;
|
||||
fieldType: string;
|
||||
secret: boolean;
|
||||
required: boolean;
|
||||
placeholder?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AccountingConfig {
|
||||
configured: boolean;
|
||||
id?: string;
|
||||
connectorType?: string;
|
||||
displayLabel?: string;
|
||||
isActive?: boolean;
|
||||
lastSyncAt?: number;
|
||||
lastSyncStatus?: string;
|
||||
/** Masked config for form prefill: secret fields are "***", others have saved values. */
|
||||
configMasked?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface AccountingSyncStatus {
|
||||
id: string;
|
||||
organisationId: string;
|
||||
contractId: string;
|
||||
documentId: string;
|
||||
positionId: string;
|
||||
mandateId?: string;
|
||||
_createdAt?: number;
|
||||
_modifiedAt?: number;
|
||||
_createdBy?: string;
|
||||
_modifiedBy?: string;
|
||||
[key: string]: any;
|
||||
connectorType: string;
|
||||
externalId?: string;
|
||||
syncStatus: string;
|
||||
syncedAt?: number;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface PaginationParams {
|
||||
|
|
@ -660,91 +685,89 @@ export async function deletePosition(
|
|||
}
|
||||
|
||||
// ============================================================================
|
||||
// POSITION-DOCUMENT API
|
||||
// ACCOUNTING API
|
||||
// ============================================================================
|
||||
|
||||
export async function fetchPositionDocuments(
|
||||
export async function fetchAccountingConnectors(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
params?: PaginationParams
|
||||
): Promise<PaginatedResponse<TrusteePositionDocument> | TrusteePositionDocument[]> {
|
||||
instanceId: string
|
||||
): Promise<AccountingConnectorInfo[]> {
|
||||
return await request({
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`,
|
||||
method: 'get',
|
||||
params: _buildPaginationParams(params)
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchPositionDocumentById(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
linkId: string
|
||||
): Promise<TrusteePositionDocument | null> {
|
||||
try {
|
||||
return await request({
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`,
|
||||
method: 'get'
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching position-document link by ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchDocumentsForPosition(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
positionId: string
|
||||
): Promise<TrusteePositionDocument[]> {
|
||||
return await request({
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/position/${positionId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/connectors`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchPositionsForDocument(
|
||||
export async function fetchAccountingConfig(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
documentId: string
|
||||
): Promise<TrusteePositionDocument[]> {
|
||||
instanceId: string
|
||||
): Promise<AccountingConfig> {
|
||||
return await request({
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/document/${documentId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/config`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export async function createPositionDocument(
|
||||
export async function saveAccountingConfig(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
data: Partial<TrusteePositionDocument>
|
||||
): Promise<TrusteePositionDocument> {
|
||||
data: { connectorType: string; displayLabel: string; config: Record<string, string> }
|
||||
): Promise<any> {
|
||||
return await request({
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/config`,
|
||||
method: 'post',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export async function updatePositionDocument(
|
||||
export async function deleteAccountingConfig(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
linkId: string,
|
||||
data: Partial<TrusteePositionDocument>
|
||||
): Promise<TrusteePositionDocument> {
|
||||
return await request({
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`,
|
||||
method: 'put',
|
||||
data
|
||||
});
|
||||
}
|
||||
|
||||
export async function deletePositionDocument(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
linkId: string
|
||||
instanceId: string
|
||||
): Promise<void> {
|
||||
await request({
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`,
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/config`,
|
||||
method: 'delete'
|
||||
});
|
||||
}
|
||||
|
||||
export async function testAccountingConnection(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string
|
||||
): Promise<{ success: boolean; errorMessage?: string }> {
|
||||
return await request({
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/test-connection`,
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchChartOfAccounts(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string
|
||||
): Promise<Array<{ accountNumber: string; label: string; accountType?: string }>> {
|
||||
return await request({
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/chart-of-accounts`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export async function syncPositionsToAccounting(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string,
|
||||
positionIds: string[]
|
||||
): Promise<{ total: number; success: number; errors: number; results: any[] }> {
|
||||
return await request({
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/sync`,
|
||||
method: 'post',
|
||||
data: { positionIds }
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchSyncStatus(
|
||||
request: ApiRequestFunction,
|
||||
instanceId: string
|
||||
): Promise<{ items: AccountingSyncStatus[] }> {
|
||||
return await request({
|
||||
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/sync-status`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,9 +75,9 @@ export const PAGE_ICONS: Record<string, React.ReactNode> = {
|
|||
'page.feature.trustee.dashboard': <FaChartLine />,
|
||||
'page.feature.trustee.positions': <FaDatabase />,
|
||||
'page.feature.trustee.documents': <FaFileAlt />,
|
||||
'page.feature.trustee.position-documents': <FaLink />,
|
||||
'page.feature.trustee.expense-import': <FaFileAlt />,
|
||||
'page.feature.trustee.instance-roles': <FaUserShield />,
|
||||
'page.feature.trustee.settings': <FaCog />,
|
||||
|
||||
// Feature pages - Real Estate
|
||||
'page.feature.realestate.projects': <FaProjectDiagram />,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import {
|
|||
type TrusteeContract,
|
||||
type TrusteeDocument,
|
||||
type TrusteePosition,
|
||||
type TrusteePositionDocument,
|
||||
type PaginationParams,
|
||||
// Organisation API
|
||||
fetchOrganisations as fetchOrganisationsApi,
|
||||
|
|
@ -56,14 +55,8 @@ import {
|
|||
createPosition as createPositionApi,
|
||||
updatePosition as updatePositionApi,
|
||||
deletePosition as deletePositionApi,
|
||||
// Position-Document API
|
||||
fetchPositionDocuments as fetchPositionDocumentsApi,
|
||||
createPositionDocument as createPositionDocumentApi,
|
||||
updatePositionDocument as updatePositionDocumentApi,
|
||||
deletePositionDocument as deletePositionDocumentApi,
|
||||
} from '../api/trusteeApi';
|
||||
|
||||
// Re-export types
|
||||
export type {
|
||||
TrusteeOrganisation,
|
||||
TrusteeRole,
|
||||
|
|
@ -71,7 +64,6 @@ export type {
|
|||
TrusteeContract,
|
||||
TrusteeDocument,
|
||||
TrusteePosition,
|
||||
TrusteePositionDocument,
|
||||
PaginationParams
|
||||
};
|
||||
|
||||
|
|
@ -586,18 +578,3 @@ const positionConfig: TrusteeEntityConfig<TrusteePosition> = {
|
|||
export const useTrusteePositions = _createTrusteeEntityHook(positionConfig);
|
||||
export const useTrusteePositionOperations = _createTrusteeOperationsHook(positionConfig);
|
||||
|
||||
// ============================================================================
|
||||
// POSITION-DOCUMENT HOOKS
|
||||
// ============================================================================
|
||||
|
||||
const positionDocumentConfig: TrusteeEntityConfig<TrusteePositionDocument> = {
|
||||
entityName: 'TrusteePositionDocument',
|
||||
fetchAll: fetchPositionDocumentsApi,
|
||||
fetchById: async () => null,
|
||||
create: createPositionDocumentApi,
|
||||
update: updatePositionDocumentApi,
|
||||
deleteItem: deletePositionDocumentApi
|
||||
};
|
||||
|
||||
export const useTrusteePositionDocuments = _createTrusteeEntityHook(positionDocumentConfig);
|
||||
export const useTrusteePositionDocumentOperations = _createTrusteeOperationsHook(positionDocumentConfig);
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@ import { getLabel, FEATURE_REGISTRY } from '../types/mandate';
|
|||
// Note: TrusteeOrganisationsView and TrusteeContractsView removed - Feature-Instanz = Organisation
|
||||
import { TrusteeDocumentsView } from './views/trustee/TrusteeDocumentsView';
|
||||
import { TrusteePositionsView } from './views/trustee/TrusteePositionsView';
|
||||
import { TrusteePositionDocumentsView } from './views/trustee/TrusteePositionDocumentsView';
|
||||
import { TrusteeDashboardView } from './views/trustee/TrusteeDashboardView';
|
||||
import { TrusteeInstanceRolesView } from './views/trustee/TrusteeInstanceRolesView';
|
||||
import { TrusteeExpenseImportView } from './views/trustee/TrusteeExpenseImportView';
|
||||
import { TrusteeAccountingSettingsView } from './views/trustee/TrusteeAccountingSettingsView';
|
||||
|
||||
// Chatbot Views
|
||||
import { ChatbotConversationsView } from './views/chatbot/ChatbotConversationsView';
|
||||
|
|
@ -95,9 +95,9 @@ const VIEW_COMPONENTS: Record<string, Record<string, ViewComponent>> = {
|
|||
dashboard: TrusteeDashboardView,
|
||||
documents: TrusteeDocumentsView,
|
||||
positions: TrusteePositionsView,
|
||||
'position-documents': TrusteePositionDocumentsView,
|
||||
'instance-roles': TrusteeInstanceRolesView,
|
||||
'expense-import': TrusteeExpenseImportView,
|
||||
settings: TrusteeAccountingSettingsView,
|
||||
},
|
||||
chatworkflow: {
|
||||
dashboard: ChatworkflowDashboard,
|
||||
|
|
|
|||
269
src/pages/views/trustee/TrusteeAccountingSettingsView.tsx
Normal file
269
src/pages/views/trustee/TrusteeAccountingSettingsView.tsx
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
/**
|
||||
* 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 { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||
import { useApiRequest } from '../../../hooks/useApi';
|
||||
import { useToast } from '../../../contexts/ToastContext';
|
||||
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<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 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<string, string>);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Failed to load accounting settings:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [instanceId, request]);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
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;
|
||||
if (!window.confirm('Remove the accounting integration? This does not delete synced data.')) 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 <div className={styles.loading}>Loading accounting settings...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.listView}>
|
||||
<div className={styles.expenseImportSection}>
|
||||
<h3 className={styles.sectionTitle}>Accounting System Integration</h3>
|
||||
<p className={styles.sectionDescription}>
|
||||
Connect an accounting system to automatically sync bookings from this Trustee instance.
|
||||
</p>
|
||||
|
||||
{existingConfig?.configured && (
|
||||
<div className={styles.successMessage} style={{ marginBottom: '1rem' }}>
|
||||
<strong>Connected:</strong> {existingConfig.displayLabel || existingConfig.connectorType}
|
||||
{existingConfig.lastSyncStatus && (
|
||||
<> — Last sync: {existingConfig.lastSyncStatus}</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 1: Select system */}
|
||||
<div className={styles.setupStep}>
|
||||
<div className={styles.stepNumber}>1</div>
|
||||
<div className={styles.stepContent}>
|
||||
<h4>Accounting System</h4>
|
||||
<select
|
||||
className={styles.folderSelect}
|
||||
value={selectedType}
|
||||
onChange={e => handleTypeChange(e.target.value)}
|
||||
>
|
||||
<option value="">Select a system...</option>
|
||||
{connectors.map(c => (
|
||||
<option key={c.connectorType} value={c.connectorType}>
|
||||
{c.label?.de || c.label?.en || 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>Credentials</h4>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
<div>
|
||||
<label style={{ display: 'block', marginBottom: '0.25rem', fontSize: '0.85rem' }}>
|
||||
Display Label
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className={styles.folderSelect}
|
||||
value={displayLabel}
|
||||
onChange={e => setDisplayLabel(e.target.value)}
|
||||
placeholder="e.g. Run My Accounts - Muster AG"
|
||||
/>
|
||||
</div>
|
||||
{selectedConnector.configFields.map(field => (
|
||||
<div key={field.key}>
|
||||
<label style={{ display: 'block', marginBottom: '0.25rem', fontSize: '0.85rem' }}>
|
||||
{field.label?.de || field.label?.en || 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'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Save & Test */}
|
||||
{selectedConnector && (
|
||||
<div className={styles.setupStep}>
|
||||
<div className={styles.stepNumber}>3</div>
|
||||
<div className={styles.stepContent}>
|
||||
<h4>Save & Test</h4>
|
||||
{testResult && (
|
||||
<div className={testResult.success ? styles.successMessage : styles.errorMessage} style={{ marginBottom: '0.75rem' }}>
|
||||
{testResult.success ? 'Connection successful!' : `Connection failed: ${testResult.message || 'Unknown error'}`}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
className={styles.primaryButton}
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Configuration'}
|
||||
</button>
|
||||
{existingConfig?.configured && (
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={handleTestConnection}
|
||||
disabled={testing}
|
||||
>
|
||||
{testing ? 'Testing...' : 'Test Connection'}
|
||||
</button>
|
||||
)}
|
||||
{existingConfig?.configured && (
|
||||
<button
|
||||
className={styles.secondaryButton}
|
||||
onClick={handleRemove}
|
||||
disabled={saving}
|
||||
style={{ color: 'var(--error-color, #dc2626)' }}
|
||||
>
|
||||
Remove Integration
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrusteeAccountingSettingsView;
|
||||
|
|
@ -1,27 +1,54 @@
|
|||
/**
|
||||
* TrusteeDashboardView
|
||||
*
|
||||
* Übersicht/Dashboard für eine Trustee-Instanz.
|
||||
* Zeigt Statistiken über Positionen, Dokumente und Verknüpfungen.
|
||||
* Overview dashboard for a Trustee instance.
|
||||
* Shows statistics about positions, documents, and accounting sync status.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useCurrentInstance } from '../../../hooks/useCurrentInstance';
|
||||
import { useTrusteePositions, useTrusteeDocuments, useTrusteePositionDocuments } from '../../../hooks/useTrustee';
|
||||
import { useTrusteePositions, useTrusteeDocuments } from '../../../hooks/useTrustee';
|
||||
import { useApiRequest } from '../../../hooks/useApi';
|
||||
import { fetchAccountingConfig, fetchSyncStatus, type AccountingConfig, type AccountingSyncStatus } from '../../../api/trusteeApi';
|
||||
import styles from './TrusteeViews.module.css';
|
||||
|
||||
export const TrusteeDashboardView: React.FC = () => {
|
||||
const { instance } = useCurrentInstance();
|
||||
const { instance, instanceId } = useCurrentInstance();
|
||||
const { items: positions, loading: posLoading } = useTrusteePositions();
|
||||
const { items: documents, loading: docsLoading } = useTrusteeDocuments();
|
||||
const { items: links, loading: linksLoading } = useTrusteePositionDocuments();
|
||||
const { request } = useApiRequest();
|
||||
|
||||
const isLoading = posLoading || docsLoading || linksLoading;
|
||||
const [accountingConfig, setAccountingConfig] = useState<AccountingConfig | null>(null);
|
||||
const [syncItems, setSyncItems] = useState<AccountingSyncStatus[]>([]);
|
||||
const [accountingLoading, setAccountingLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!instanceId) return;
|
||||
const loadAccountingData = async () => {
|
||||
setAccountingLoading(true);
|
||||
try {
|
||||
const [config, syncData] = await Promise.all([
|
||||
fetchAccountingConfig(request, instanceId),
|
||||
fetchSyncStatus(request, instanceId),
|
||||
]);
|
||||
setAccountingConfig(config);
|
||||
setSyncItems(syncData?.items || []);
|
||||
} catch {
|
||||
// Accounting not configured is fine
|
||||
} finally {
|
||||
setAccountingLoading(false);
|
||||
}
|
||||
};
|
||||
loadAccountingData();
|
||||
}, [instanceId, request]);
|
||||
|
||||
const isLoading = posLoading || docsLoading || accountingLoading;
|
||||
const syncedCount = syncItems.filter(s => s.syncStatus === 'synced').length;
|
||||
const syncErrorCount = syncItems.filter(s => s.syncStatus === 'error').length;
|
||||
|
||||
return (
|
||||
<div className={styles.dashboardView}>
|
||||
<div className={styles.statsGrid}>
|
||||
{/* Positionen Card */}
|
||||
<div className={styles.statCard}>
|
||||
<div className={styles.statIcon}>📊</div>
|
||||
<div className={styles.statContent}>
|
||||
|
|
@ -32,7 +59,6 @@ export const TrusteeDashboardView: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dokumente Card */}
|
||||
<div className={styles.statCard}>
|
||||
<div className={styles.statIcon}>📄</div>
|
||||
<div className={styles.statContent}>
|
||||
|
|
@ -43,24 +69,28 @@ export const TrusteeDashboardView: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verknüpfungen Card */}
|
||||
<div className={styles.statCard}>
|
||||
<div className={styles.statIcon}>🔗</div>
|
||||
<div className={styles.statIcon}>
|
||||
{accountingConfig?.configured ? '✓' : '○'}
|
||||
</div>
|
||||
<div className={styles.statContent}>
|
||||
<div className={styles.statValue}>
|
||||
{isLoading ? '...' : links.length}
|
||||
<div className={styles.statValueSmall}>
|
||||
{isLoading ? '...' : (
|
||||
accountingConfig?.configured
|
||||
? <>{syncedCount} synced{syncErrorCount > 0 && <span style={{ color: 'var(--error-color, #dc2626)' }}> / {syncErrorCount} errors</span>}</>
|
||||
: 'Not configured'
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.statLabel}>Zuordnungen</div>
|
||||
<div className={styles.statLabel}>Buchhaltung</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rollen Card */}
|
||||
<div className={styles.statCard}>
|
||||
<div className={styles.statIcon}>👤</div>
|
||||
<div className={styles.statContent}>
|
||||
<div className={styles.statValueSmall}>
|
||||
{instance?.userRoles?.length ? (
|
||||
instance.userRoles.map((role, idx) => (
|
||||
instance.userRoles.map((role: string, idx: number) => (
|
||||
<div key={idx}>{role}</div>
|
||||
))
|
||||
) : '-'}
|
||||
|
|
@ -72,7 +102,6 @@ export const TrusteeDashboardView: React.FC = () => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info-Bereich */}
|
||||
<div className={styles.infoSection}>
|
||||
<h3>Instanz-Details</h3>
|
||||
<div className={styles.infoGrid}>
|
||||
|
|
@ -84,6 +113,15 @@ export const TrusteeDashboardView: React.FC = () => {
|
|||
<span className={styles.infoLabel}>Mandant:</span>
|
||||
<span className={styles.infoValue}>{instance?.mandateName}</span>
|
||||
</div>
|
||||
{accountingConfig?.configured && (
|
||||
<div className={styles.infoItem}>
|
||||
<span className={styles.infoLabel}>Buchhaltungssystem:</span>
|
||||
<span className={styles.infoValue}>
|
||||
{accountingConfig.displayLabel || accountingConfig.connectorType}
|
||||
{accountingConfig.lastSyncStatus && ` (${accountingConfig.lastSyncStatus})`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import api from '../../../api';
|
|||
import styles from './TrusteeViews.module.css';
|
||||
|
||||
// Default extraction prompt (from automation template)
|
||||
const DEFAULT_EXTRACTION_PROMPT = `Du bist ein Spezialist für die Extraktion von Spesendaten aus PDF-Dokumenten.
|
||||
const DEFAULT_EXTRACTION_PROMPT = `Du bist ein Spezialist für die Extraktion von Spesendaten aus PDF-Dokumenten und deren buchhalterische Kontierung.
|
||||
|
||||
AUFGABE:
|
||||
Extrahiere alle Speseneinträge aus dem bereitgestellten PDF-Dokument und gib sie im CSV-Format zurück.
|
||||
|
|
@ -26,9 +26,10 @@ WICHTIGE REGELN:
|
|||
4. Feld "company" enthält den Lieferanten/Verkäufer der Buchung
|
||||
5. Tags müssen aus dieser Liste gewählt werden: customer, meeting, license, subscription, fuel, food, material
|
||||
- Mehrere zutreffende Tags mit Komma trennen
|
||||
6. Buchhalterische Kontierung: Schlage Soll-/Haben-Kontonummern vor basierend auf Schweizer Kontenrahmen (KMU)
|
||||
|
||||
CSV-SPALTEN (in dieser Reihenfolge):
|
||||
valuta,transactionDateTime,company,desc,tags,bookingCurrency,bookingAmount,originalCurrency,originalAmount,vatPercentage,vatAmount
|
||||
valuta,transactionDateTime,company,desc,tags,bookingCurrency,bookingAmount,originalCurrency,originalAmount,vatPercentage,vatAmount,debitAccountNumber,creditAccountNumber,taxCode,costCenter,bookingReference
|
||||
|
||||
DATENFORMAT:
|
||||
- valuta: YYYY-MM-DD (Valutadatum)
|
||||
|
|
@ -42,6 +43,21 @@ DATENFORMAT:
|
|||
- originalAmount: Original-Betrag als Dezimalzahl
|
||||
- vatPercentage: MwSt-Prozentsatz (z.B. 8.1 für 8.1%)
|
||||
- vatAmount: MwSt-Betrag als Dezimalzahl
|
||||
- debitAccountNumber: Soll-Konto (Aufwandkonto, z.B. 4200=Materialaufwand, 4400=Büromaterial, 6000=Mietaufwand, 6500=Reisespesen)
|
||||
- creditAccountNumber: Haben-Konto (z.B. 1020=Durchlaufkonto, 1000=Kasse, 1100=Debitoren)
|
||||
- taxCode: Steuercode falls erkennbar (z.B. VM77=Vorsteuer 7.7%, VM81=Vorsteuer 8.1%)
|
||||
- costCenter: Kostenstelle falls erkennbar (leer lassen wenn unbekannt)
|
||||
- bookingReference: Belegnummer/Rechnungsnummer vom Dokument
|
||||
|
||||
KONTIERUNGSREGELN (Schweizer Kontenrahmen KMU):
|
||||
- Spesenbelege: Soll=Aufwandkonto (4xxx-6xxx), Haben=1020 (Durchlaufkonto)
|
||||
- Materialkosten: Soll=4200, Haben=1020
|
||||
- Büromaterial: Soll=4400, Haben=1020
|
||||
- Reisespesen/Transport: Soll=6500, Haben=1020
|
||||
- Verpflegung: Soll=6510, Haben=1020
|
||||
- Lizenzen/Abos: Soll=6800, Haben=1020
|
||||
- Treibstoff: Soll=6200, Haben=1020
|
||||
- Wenn unsicher: debitAccountNumber und creditAccountNumber leer lassen
|
||||
|
||||
HINWEISE:
|
||||
- Wenn nur ein MwSt-Satz vorhanden ist, einen Datensatz erstellen
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ export const TrusteePositionsView: React.FC = () => {
|
|||
}
|
||||
}, [instanceId]);
|
||||
|
||||
// Hidden columns (not shown in table view, but available in form)
|
||||
const hiddenColumns = ['desc', 'featureInstanceId', 'mandateId'];
|
||||
// Hidden columns (not shown in table view, but available in edit form)
|
||||
const hiddenColumns = ['desc', 'featureInstanceId', 'mandateId', 'taxCode', 'costCenter'];
|
||||
|
||||
// Generate columns from attributes + add system columns
|
||||
const columns = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
/**
|
||||
* Trustee Views Export
|
||||
*
|
||||
* Note: TrusteeOrganisationsView, TrusteeContractsView, TrusteeRolesView, TrusteeAccessView
|
||||
* wurden entfernt - Feature-Instanz = Organisation
|
||||
*/
|
||||
|
||||
export { TrusteeDashboardView } from './TrusteeDashboardView';
|
||||
export { TrusteeDocumentsView } from './TrusteeDocumentsView';
|
||||
export { TrusteePositionsView } from './TrusteePositionsView';
|
||||
export { TrusteePositionDocumentsView } from './TrusteePositionDocumentsView';
|
||||
export { TrusteeInstanceRolesView } from './TrusteeInstanceRolesView';
|
||||
export { TrusteeExpenseImportView } from './TrusteeExpenseImportView';
|
||||
export { TrusteeAccountingSettingsView } from './TrusteeAccountingSettingsView';
|
||||
|
|
|
|||
Loading…
Reference in a new issue