trustee connections

This commit is contained in:
ValueOn AG 2026-02-21 00:56:50 +01:00
parent d45dab587f
commit cdea97e2cf
9 changed files with 444 additions and 124 deletions

View file

@ -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'
});
}

View file

@ -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 />,

View file

@ -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);

View file

@ -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,

View 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 && (
<> &mdash; 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;

View file

@ -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.statContent}>
<div className={styles.statValue}>
{isLoading ? '...' : links.length}
<div className={styles.statIcon}>
{accountingConfig?.configured ? '✓' : '○'}
</div>
<div className={styles.statLabel}>Zuordnungen</div>
<div className={styles.statContent}>
<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}>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>

View file

@ -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

View file

@ -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(() => {

View file

@ -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';