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 {
|
export interface TrusteePosition {
|
||||||
id: string;
|
id: string;
|
||||||
organisationId: string;
|
documentId?: string;
|
||||||
contractId: string;
|
|
||||||
valuta?: string;
|
valuta?: string;
|
||||||
transactionDateTime?: number;
|
transactionDateTime?: number;
|
||||||
company: string;
|
company: string;
|
||||||
|
|
@ -93,6 +92,11 @@ export interface TrusteePosition {
|
||||||
originalAmount: number;
|
originalAmount: number;
|
||||||
vatPercentage: number;
|
vatPercentage: number;
|
||||||
vatAmount: number;
|
vatAmount: number;
|
||||||
|
debitAccountNumber?: string;
|
||||||
|
creditAccountNumber?: string;
|
||||||
|
taxCode?: string;
|
||||||
|
costCenter?: string;
|
||||||
|
bookingReference?: string;
|
||||||
mandateId?: string;
|
mandateId?: string;
|
||||||
_createdAt?: number;
|
_createdAt?: number;
|
||||||
_modifiedAt?: number;
|
_modifiedAt?: number;
|
||||||
|
|
@ -101,18 +105,39 @@ export interface TrusteePosition {
|
||||||
[key: string]: any;
|
[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;
|
id: string;
|
||||||
organisationId: string;
|
|
||||||
contractId: string;
|
|
||||||
documentId: string;
|
|
||||||
positionId: string;
|
positionId: string;
|
||||||
mandateId?: string;
|
connectorType: string;
|
||||||
_createdAt?: number;
|
externalId?: string;
|
||||||
_modifiedAt?: number;
|
syncStatus: string;
|
||||||
_createdBy?: string;
|
syncedAt?: number;
|
||||||
_modifiedBy?: string;
|
errorMessage?: string;
|
||||||
[key: string]: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginationParams {
|
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,
|
request: ApiRequestFunction,
|
||||||
instanceId: string,
|
instanceId: string
|
||||||
params?: PaginationParams
|
): Promise<AccountingConnectorInfo[]> {
|
||||||
): Promise<PaginatedResponse<TrusteePositionDocument> | TrusteePositionDocument[]> {
|
|
||||||
return await request({
|
return await request({
|
||||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`,
|
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/connectors`,
|
||||||
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}`,
|
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPositionsForDocument(
|
export async function fetchAccountingConfig(
|
||||||
request: ApiRequestFunction,
|
request: ApiRequestFunction,
|
||||||
instanceId: string,
|
instanceId: string
|
||||||
documentId: string
|
): Promise<AccountingConfig> {
|
||||||
): Promise<TrusteePositionDocument[]> {
|
|
||||||
return await request({
|
return await request({
|
||||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/document/${documentId}`,
|
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/config`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createPositionDocument(
|
export async function saveAccountingConfig(
|
||||||
request: ApiRequestFunction,
|
request: ApiRequestFunction,
|
||||||
instanceId: string,
|
instanceId: string,
|
||||||
data: Partial<TrusteePositionDocument>
|
data: { connectorType: string; displayLabel: string; config: Record<string, string> }
|
||||||
): Promise<TrusteePositionDocument> {
|
): Promise<any> {
|
||||||
return await request({
|
return await request({
|
||||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`,
|
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/config`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updatePositionDocument(
|
export async function deleteAccountingConfig(
|
||||||
request: ApiRequestFunction,
|
request: ApiRequestFunction,
|
||||||
instanceId: string,
|
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
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await request({
|
await request({
|
||||||
url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`,
|
url: `${_getTrusteeBaseUrl(instanceId)}/accounting/config`,
|
||||||
method: 'delete'
|
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.dashboard': <FaChartLine />,
|
||||||
'page.feature.trustee.positions': <FaDatabase />,
|
'page.feature.trustee.positions': <FaDatabase />,
|
||||||
'page.feature.trustee.documents': <FaFileAlt />,
|
'page.feature.trustee.documents': <FaFileAlt />,
|
||||||
'page.feature.trustee.position-documents': <FaLink />,
|
|
||||||
'page.feature.trustee.expense-import': <FaFileAlt />,
|
'page.feature.trustee.expense-import': <FaFileAlt />,
|
||||||
'page.feature.trustee.instance-roles': <FaUserShield />,
|
'page.feature.trustee.instance-roles': <FaUserShield />,
|
||||||
|
'page.feature.trustee.settings': <FaCog />,
|
||||||
|
|
||||||
// Feature pages - Real Estate
|
// Feature pages - Real Estate
|
||||||
'page.feature.realestate.projects': <FaProjectDiagram />,
|
'page.feature.realestate.projects': <FaProjectDiagram />,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import {
|
||||||
type TrusteeContract,
|
type TrusteeContract,
|
||||||
type TrusteeDocument,
|
type TrusteeDocument,
|
||||||
type TrusteePosition,
|
type TrusteePosition,
|
||||||
type TrusteePositionDocument,
|
|
||||||
type PaginationParams,
|
type PaginationParams,
|
||||||
// Organisation API
|
// Organisation API
|
||||||
fetchOrganisations as fetchOrganisationsApi,
|
fetchOrganisations as fetchOrganisationsApi,
|
||||||
|
|
@ -56,14 +55,8 @@ import {
|
||||||
createPosition as createPositionApi,
|
createPosition as createPositionApi,
|
||||||
updatePosition as updatePositionApi,
|
updatePosition as updatePositionApi,
|
||||||
deletePosition as deletePositionApi,
|
deletePosition as deletePositionApi,
|
||||||
// Position-Document API
|
|
||||||
fetchPositionDocuments as fetchPositionDocumentsApi,
|
|
||||||
createPositionDocument as createPositionDocumentApi,
|
|
||||||
updatePositionDocument as updatePositionDocumentApi,
|
|
||||||
deletePositionDocument as deletePositionDocumentApi,
|
|
||||||
} from '../api/trusteeApi';
|
} from '../api/trusteeApi';
|
||||||
|
|
||||||
// Re-export types
|
|
||||||
export type {
|
export type {
|
||||||
TrusteeOrganisation,
|
TrusteeOrganisation,
|
||||||
TrusteeRole,
|
TrusteeRole,
|
||||||
|
|
@ -71,7 +64,6 @@ export type {
|
||||||
TrusteeContract,
|
TrusteeContract,
|
||||||
TrusteeDocument,
|
TrusteeDocument,
|
||||||
TrusteePosition,
|
TrusteePosition,
|
||||||
TrusteePositionDocument,
|
|
||||||
PaginationParams
|
PaginationParams
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -586,18 +578,3 @@ const positionConfig: TrusteeEntityConfig<TrusteePosition> = {
|
||||||
export const useTrusteePositions = _createTrusteeEntityHook(positionConfig);
|
export const useTrusteePositions = _createTrusteeEntityHook(positionConfig);
|
||||||
export const useTrusteePositionOperations = _createTrusteeOperationsHook(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
|
// Note: TrusteeOrganisationsView and TrusteeContractsView removed - Feature-Instanz = Organisation
|
||||||
import { TrusteeDocumentsView } from './views/trustee/TrusteeDocumentsView';
|
import { TrusteeDocumentsView } from './views/trustee/TrusteeDocumentsView';
|
||||||
import { TrusteePositionsView } from './views/trustee/TrusteePositionsView';
|
import { TrusteePositionsView } from './views/trustee/TrusteePositionsView';
|
||||||
import { TrusteePositionDocumentsView } from './views/trustee/TrusteePositionDocumentsView';
|
|
||||||
import { TrusteeDashboardView } from './views/trustee/TrusteeDashboardView';
|
import { TrusteeDashboardView } from './views/trustee/TrusteeDashboardView';
|
||||||
import { TrusteeInstanceRolesView } from './views/trustee/TrusteeInstanceRolesView';
|
import { TrusteeInstanceRolesView } from './views/trustee/TrusteeInstanceRolesView';
|
||||||
import { TrusteeExpenseImportView } from './views/trustee/TrusteeExpenseImportView';
|
import { TrusteeExpenseImportView } from './views/trustee/TrusteeExpenseImportView';
|
||||||
|
import { TrusteeAccountingSettingsView } from './views/trustee/TrusteeAccountingSettingsView';
|
||||||
|
|
||||||
// Chatbot Views
|
// Chatbot Views
|
||||||
import { ChatbotConversationsView } from './views/chatbot/ChatbotConversationsView';
|
import { ChatbotConversationsView } from './views/chatbot/ChatbotConversationsView';
|
||||||
|
|
@ -95,9 +95,9 @@ const VIEW_COMPONENTS: Record<string, Record<string, ViewComponent>> = {
|
||||||
dashboard: TrusteeDashboardView,
|
dashboard: TrusteeDashboardView,
|
||||||
documents: TrusteeDocumentsView,
|
documents: TrusteeDocumentsView,
|
||||||
positions: TrusteePositionsView,
|
positions: TrusteePositionsView,
|
||||||
'position-documents': TrusteePositionDocumentsView,
|
|
||||||
'instance-roles': TrusteeInstanceRolesView,
|
'instance-roles': TrusteeInstanceRolesView,
|
||||||
'expense-import': TrusteeExpenseImportView,
|
'expense-import': TrusteeExpenseImportView,
|
||||||
|
settings: TrusteeAccountingSettingsView,
|
||||||
},
|
},
|
||||||
chatworkflow: {
|
chatworkflow: {
|
||||||
dashboard: ChatworkflowDashboard,
|
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
|
* TrusteeDashboardView
|
||||||
*
|
*
|
||||||
* Übersicht/Dashboard für eine Trustee-Instanz.
|
* Overview dashboard for a Trustee instance.
|
||||||
* Zeigt Statistiken über Positionen, Dokumente und Verknüpfungen.
|
* 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 { 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';
|
import styles from './TrusteeViews.module.css';
|
||||||
|
|
||||||
export const TrusteeDashboardView: React.FC = () => {
|
export const TrusteeDashboardView: React.FC = () => {
|
||||||
const { instance } = useCurrentInstance();
|
const { instance, instanceId } = useCurrentInstance();
|
||||||
const { items: positions, loading: posLoading } = useTrusteePositions();
|
const { items: positions, loading: posLoading } = useTrusteePositions();
|
||||||
const { items: documents, loading: docsLoading } = useTrusteeDocuments();
|
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 (
|
return (
|
||||||
<div className={styles.dashboardView}>
|
<div className={styles.dashboardView}>
|
||||||
<div className={styles.statsGrid}>
|
<div className={styles.statsGrid}>
|
||||||
{/* Positionen Card */}
|
|
||||||
<div className={styles.statCard}>
|
<div className={styles.statCard}>
|
||||||
<div className={styles.statIcon}>📊</div>
|
<div className={styles.statIcon}>📊</div>
|
||||||
<div className={styles.statContent}>
|
<div className={styles.statContent}>
|
||||||
|
|
@ -32,7 +59,6 @@ export const TrusteeDashboardView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dokumente Card */}
|
|
||||||
<div className={styles.statCard}>
|
<div className={styles.statCard}>
|
||||||
<div className={styles.statIcon}>📄</div>
|
<div className={styles.statIcon}>📄</div>
|
||||||
<div className={styles.statContent}>
|
<div className={styles.statContent}>
|
||||||
|
|
@ -43,24 +69,28 @@ export const TrusteeDashboardView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Verknüpfungen Card */}
|
|
||||||
<div className={styles.statCard}>
|
<div className={styles.statCard}>
|
||||||
<div className={styles.statIcon}>🔗</div>
|
<div className={styles.statIcon}>
|
||||||
|
{accountingConfig?.configured ? '✓' : '○'}
|
||||||
|
</div>
|
||||||
<div className={styles.statContent}>
|
<div className={styles.statContent}>
|
||||||
<div className={styles.statValue}>
|
<div className={styles.statValueSmall}>
|
||||||
{isLoading ? '...' : links.length}
|
{isLoading ? '...' : (
|
||||||
|
accountingConfig?.configured
|
||||||
|
? <>{syncedCount} synced{syncErrorCount > 0 && <span style={{ color: 'var(--error-color, #dc2626)' }}> / {syncErrorCount} errors</span>}</>
|
||||||
|
: 'Not configured'
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.statLabel}>Zuordnungen</div>
|
<div className={styles.statLabel}>Buchhaltung</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rollen Card */}
|
|
||||||
<div className={styles.statCard}>
|
<div className={styles.statCard}>
|
||||||
<div className={styles.statIcon}>👤</div>
|
<div className={styles.statIcon}>👤</div>
|
||||||
<div className={styles.statContent}>
|
<div className={styles.statContent}>
|
||||||
<div className={styles.statValueSmall}>
|
<div className={styles.statValueSmall}>
|
||||||
{instance?.userRoles?.length ? (
|
{instance?.userRoles?.length ? (
|
||||||
instance.userRoles.map((role, idx) => (
|
instance.userRoles.map((role: string, idx: number) => (
|
||||||
<div key={idx}>{role}</div>
|
<div key={idx}>{role}</div>
|
||||||
))
|
))
|
||||||
) : '-'}
|
) : '-'}
|
||||||
|
|
@ -72,7 +102,6 @@ export const TrusteeDashboardView: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info-Bereich */}
|
|
||||||
<div className={styles.infoSection}>
|
<div className={styles.infoSection}>
|
||||||
<h3>Instanz-Details</h3>
|
<h3>Instanz-Details</h3>
|
||||||
<div className={styles.infoGrid}>
|
<div className={styles.infoGrid}>
|
||||||
|
|
@ -84,6 +113,15 @@ export const TrusteeDashboardView: React.FC = () => {
|
||||||
<span className={styles.infoLabel}>Mandant:</span>
|
<span className={styles.infoLabel}>Mandant:</span>
|
||||||
<span className={styles.infoValue}>{instance?.mandateName}</span>
|
<span className={styles.infoValue}>{instance?.mandateName}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import api from '../../../api';
|
||||||
import styles from './TrusteeViews.module.css';
|
import styles from './TrusteeViews.module.css';
|
||||||
|
|
||||||
// Default extraction prompt (from automation template)
|
// 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:
|
AUFGABE:
|
||||||
Extrahiere alle Speseneinträge aus dem bereitgestellten PDF-Dokument und gib sie im CSV-Format zurück.
|
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
|
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
|
5. Tags müssen aus dieser Liste gewählt werden: customer, meeting, license, subscription, fuel, food, material
|
||||||
- Mehrere zutreffende Tags mit Komma trennen
|
- Mehrere zutreffende Tags mit Komma trennen
|
||||||
|
6. Buchhalterische Kontierung: Schlage Soll-/Haben-Kontonummern vor basierend auf Schweizer Kontenrahmen (KMU)
|
||||||
|
|
||||||
CSV-SPALTEN (in dieser Reihenfolge):
|
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:
|
DATENFORMAT:
|
||||||
- valuta: YYYY-MM-DD (Valutadatum)
|
- valuta: YYYY-MM-DD (Valutadatum)
|
||||||
|
|
@ -42,6 +43,21 @@ DATENFORMAT:
|
||||||
- originalAmount: Original-Betrag als Dezimalzahl
|
- originalAmount: Original-Betrag als Dezimalzahl
|
||||||
- vatPercentage: MwSt-Prozentsatz (z.B. 8.1 für 8.1%)
|
- vatPercentage: MwSt-Prozentsatz (z.B. 8.1 für 8.1%)
|
||||||
- vatAmount: MwSt-Betrag als Dezimalzahl
|
- 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:
|
HINWEISE:
|
||||||
- Wenn nur ein MwSt-Satz vorhanden ist, einen Datensatz erstellen
|
- Wenn nur ein MwSt-Satz vorhanden ist, einen Datensatz erstellen
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,8 @@ export const TrusteePositionsView: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [instanceId]);
|
}, [instanceId]);
|
||||||
|
|
||||||
// Hidden columns (not shown in table view, but available in form)
|
// Hidden columns (not shown in table view, but available in edit form)
|
||||||
const hiddenColumns = ['desc', 'featureInstanceId', 'mandateId'];
|
const hiddenColumns = ['desc', 'featureInstanceId', 'mandateId', 'taxCode', 'costCenter'];
|
||||||
|
|
||||||
// Generate columns from attributes + add system columns
|
// Generate columns from attributes + add system columns
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,10 @@
|
||||||
/**
|
/**
|
||||||
* Trustee Views Export
|
* Trustee Views Export
|
||||||
*
|
|
||||||
* Note: TrusteeOrganisationsView, TrusteeContractsView, TrusteeRolesView, TrusteeAccessView
|
|
||||||
* wurden entfernt - Feature-Instanz = Organisation
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export { TrusteeDashboardView } from './TrusteeDashboardView';
|
export { TrusteeDashboardView } from './TrusteeDashboardView';
|
||||||
export { TrusteeDocumentsView } from './TrusteeDocumentsView';
|
export { TrusteeDocumentsView } from './TrusteeDocumentsView';
|
||||||
export { TrusteePositionsView } from './TrusteePositionsView';
|
export { TrusteePositionsView } from './TrusteePositionsView';
|
||||||
export { TrusteePositionDocumentsView } from './TrusteePositionDocumentsView';
|
|
||||||
export { TrusteeInstanceRolesView } from './TrusteeInstanceRolesView';
|
export { TrusteeInstanceRolesView } from './TrusteeInstanceRolesView';
|
||||||
export { TrusteeExpenseImportView } from './TrusteeExpenseImportView';
|
export { TrusteeExpenseImportView } from './TrusteeExpenseImportView';
|
||||||
|
export { TrusteeAccountingSettingsView } from './TrusteeAccountingSettingsView';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue