/** * Trustee API * * API-Funktionen für das Trustee-Feature. * Alle Endpunkte erfordern eine instanceId für den Feature-Instanz-Kontext. * * URL-Struktur: /api/trustee/{instanceId}/{entity} */ import { ApiRequestOptions } from '../hooks/useApi'; // ============================================================================ // TYPES & INTERFACES // ============================================================================ export interface TrusteeOrganisation { id: string; label: string; enabled: boolean; mandateId?: string; sysCreatedAt?: number; sysModifiedAt?: number; sysCreatedBy?: string; sysModifiedBy?: string; [key: string]: any; } export interface TrusteeRole { id: string; desc: string; mandateId?: string; sysCreatedAt?: number; sysModifiedAt?: number; sysCreatedBy?: string; sysModifiedBy?: string; [key: string]: any; } export interface TrusteeAccess { id: string; organisationId: string; roleId: string; userId: string; contractId?: string | null; mandateId?: string; sysCreatedAt?: number; sysModifiedAt?: number; sysCreatedBy?: string; sysModifiedBy?: string; [key: string]: any; } export interface TrusteeContract { id: string; organisationId: string; label: string; enabled: boolean; mandateId?: string; sysCreatedAt?: number; sysModifiedAt?: number; sysCreatedBy?: string; sysModifiedBy?: string; [key: string]: any; } export interface TrusteeDocument { id: string; organisationId: string; contractId: string; documentName: string; documentMimeType: string; documentData?: any; mandateId?: string; sysCreatedAt?: number; sysModifiedAt?: number; sysCreatedBy?: string; sysModifiedBy?: string; [key: string]: any; } export interface TrusteePosition { id: string; documentId?: string; valuta?: string; transactionDateTime?: number; company: string; desc: string; tags: string; bookingCurrency: string; bookingAmount: number; originalCurrency: string; originalAmount: number; vatPercentage: number; vatAmount: number; debitAccountNumber?: string; creditAccountNumber?: string; taxCode?: string; costCenter?: string; bookingReference?: string; mandateId?: string; sysCreatedAt?: number; sysModifiedAt?: number; sysCreatedBy?: string; sysModifiedBy?: string; [key: string]: any; } export interface AccountingConnectorInfo { connectorType: string; label: string; configFields: Array<{ key: string; label: string; fieldType: string; secret: boolean; required: boolean; placeholder?: string; suggestions?: string[]; }>; } export interface AccountingConfig { configured: boolean; id?: string; connectorType?: string; displayLabel?: string; isActive?: boolean; lastSyncAt?: number; lastSyncStatus?: string; /** Error message when lastSyncStatus is "error". */ lastSyncErrorMessage?: string; /** Masked config for form prefill: secret fields are "***", others have saved values. */ configMasked?: Record; } export interface AccountingSyncStatus { id: string; positionId: string; connectorType: string; externalId?: string; syncStatus: string; syncedAt?: number; errorMessage?: string; } export interface PaginationParams { page?: number; pageSize?: number; sort?: Array<{ field: string; direction: 'asc' | 'desc' }>; filters?: Record; search?: string; } export interface PaginatedResponse { items: T[]; pagination?: { currentPage: number; pageSize: number; totalItems: number; totalPages: number; }; } export type ApiRequestFunction = (options: ApiRequestOptions) => Promise; // ============================================================================ // HELPER FUNCTIONS // ============================================================================ function _buildPaginationParams(params?: PaginationParams): Record { const requestParams: any = {}; if (params) { const paginationObj: any = {}; if (params.page !== undefined) paginationObj.page = params.page; if (params.pageSize !== undefined) paginationObj.pageSize = params.pageSize; if (params.sort) paginationObj.sort = params.sort; if (params.filters) paginationObj.filters = params.filters; if (params.search) paginationObj.search = params.search; if (Object.keys(paginationObj).length > 0) { requestParams.pagination = JSON.stringify(paginationObj); } } return requestParams; } /** * Erstellt die Basis-URL für Trustee-Endpunkte */ function _getTrusteeBaseUrl(instanceId: string): string { return `/api/trustee/${instanceId}`; } // ============================================================================ // ORGANISATION API // ============================================================================ export async function fetchOrganisations( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeOrganisation[]> { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/organisations`, method: 'get', params: _buildPaginationParams(params) }); } export async function fetchOrganisationById( request: ApiRequestFunction, instanceId: string, orgId: string ): Promise { try { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`, method: 'get' }); } catch (error: any) { console.error('Error fetching organisation by ID:', error); return null; } } export async function createOrganisation( request: ApiRequestFunction, instanceId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/organisations`, method: 'post', data }); } export async function updateOrganisation( request: ApiRequestFunction, instanceId: string, orgId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`, method: 'put', data }); } export async function deleteOrganisation( request: ApiRequestFunction, instanceId: string, orgId: string ): Promise { await request({ url: `${_getTrusteeBaseUrl(instanceId)}/organisations/${orgId}`, method: 'delete' }); } // ============================================================================ // ROLE API // ============================================================================ export async function fetchRoles( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeRole[]> { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/roles`, method: 'get', params: _buildPaginationParams(params) }); } export async function fetchRoleById( request: ApiRequestFunction, instanceId: string, roleId: string ): Promise { try { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`, method: 'get' }); } catch (error: any) { console.error('Error fetching role by ID:', error); return null; } } export async function createRole( request: ApiRequestFunction, instanceId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/roles`, method: 'post', data }); } export async function updateRole( request: ApiRequestFunction, instanceId: string, roleId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`, method: 'put', data }); } export async function deleteRole( request: ApiRequestFunction, instanceId: string, roleId: string ): Promise { await request({ url: `${_getTrusteeBaseUrl(instanceId)}/roles/${roleId}`, method: 'delete' }); } // ============================================================================ // ACCESS API // ============================================================================ export async function fetchAccess( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeAccess[]> { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/access`, method: 'get', params: _buildPaginationParams(params) }); } export async function fetchAccessById( request: ApiRequestFunction, instanceId: string, accessId: string ): Promise { try { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`, method: 'get' }); } catch (error: any) { console.error('Error fetching access by ID:', error); return null; } } export async function fetchAccessByOrganisation( request: ApiRequestFunction, instanceId: string, orgId: string ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/access/organisation/${orgId}`, method: 'get' }); } export async function fetchAccessByUser( request: ApiRequestFunction, instanceId: string, userId: string ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/access/user/${userId}`, method: 'get' }); } export async function createAccess( request: ApiRequestFunction, instanceId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/access`, method: 'post', data }); } export async function updateAccess( request: ApiRequestFunction, instanceId: string, accessId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`, method: 'put', data }); } export async function deleteAccess( request: ApiRequestFunction, instanceId: string, accessId: string ): Promise { await request({ url: `${_getTrusteeBaseUrl(instanceId)}/access/${accessId}`, method: 'delete' }); } // ============================================================================ // CONTRACT API // ============================================================================ export async function fetchContracts( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeContract[]> { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/contracts`, method: 'get', params: _buildPaginationParams(params) }); } export async function fetchContractById( request: ApiRequestFunction, instanceId: string, contractId: string ): Promise { try { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`, method: 'get' }); } catch (error: any) { console.error('Error fetching contract by ID:', error); return null; } } export async function fetchContractsByOrganisation( request: ApiRequestFunction, instanceId: string, orgId: string ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/contracts/organisation/${orgId}`, method: 'get' }); } export async function createContract( request: ApiRequestFunction, instanceId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/contracts`, method: 'post', data }); } export async function updateContract( request: ApiRequestFunction, instanceId: string, contractId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`, method: 'put', data }); } export async function deleteContract( request: ApiRequestFunction, instanceId: string, contractId: string ): Promise { await request({ url: `${_getTrusteeBaseUrl(instanceId)}/contracts/${contractId}`, method: 'delete' }); } // ============================================================================ // DOCUMENT API // ============================================================================ export async function fetchDocuments( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeDocument[]> { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/documents`, method: 'get', params: _buildPaginationParams(params) }); } export async function fetchDocumentById( request: ApiRequestFunction, instanceId: string, documentId: string ): Promise { try { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`, method: 'get' }); } catch (error: any) { console.error('Error fetching document by ID:', error); return null; } } export async function fetchDocumentsByContract( request: ApiRequestFunction, instanceId: string, contractId: string ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/documents/contract/${contractId}`, method: 'get' }); } export async function createDocument( request: ApiRequestFunction, instanceId: string, data: Partial ): Promise { // If documentData is a File, convert to base64 let processedData = { ...data }; if (data.documentData instanceof File) { const file = data.documentData as File; const arrayBuffer = await file.arrayBuffer(); const base64 = btoa( new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), '') ); processedData.documentData = base64 as any; // Auto-set MIME type from file if not provided if (!processedData.documentMimeType && file.type) { processedData.documentMimeType = file.type; } // Auto-set name from file if not provided if (!processedData.documentName && file.name) { processedData.documentName = file.name; } } return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/documents`, method: 'post', data: processedData }); } export async function updateDocument( request: ApiRequestFunction, instanceId: string, documentId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`, method: 'put', data }); } export async function deleteDocument( request: ApiRequestFunction, instanceId: string, documentId: string ): Promise { await request({ url: `${_getTrusteeBaseUrl(instanceId)}/documents/${documentId}`, method: 'delete' }); } // ============================================================================ // POSITION API // ============================================================================ export async function fetchPositions( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteePosition[]> { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/positions`, method: 'get', params: _buildPaginationParams(params) }); } export async function fetchPositionById( request: ApiRequestFunction, instanceId: string, positionId: string ): Promise { try { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`, method: 'get' }); } catch (error: any) { console.error('Error fetching position by ID:', error); return null; } } export async function fetchPositionsByContract( request: ApiRequestFunction, instanceId: string, contractId: string ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/positions/contract/${contractId}`, method: 'get' }); } export async function fetchPositionsByOrganisation( request: ApiRequestFunction, instanceId: string, orgId: string ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/positions/organisation/${orgId}`, method: 'get' }); } export async function createPosition( request: ApiRequestFunction, instanceId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/positions`, method: 'post', data }); } export async function updatePosition( request: ApiRequestFunction, instanceId: string, positionId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`, method: 'put', data }); } export async function deletePosition( request: ApiRequestFunction, instanceId: string, positionId: string ): Promise { await request({ url: `${_getTrusteeBaseUrl(instanceId)}/positions/${positionId}`, method: 'delete' }); } // ============================================================================ // POSITION-DOCUMENT LINK API // ============================================================================ export interface TrusteePositionDocument { id: string; positionId: string; documentId: string; mandateId?: string; featureInstanceId?: string; sysCreatedAt?: number; sysModifiedAt?: number; [key: string]: any; } export async function fetchPositionDocuments( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteePositionDocument[]> { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`, method: 'get', params: _buildPaginationParams(params) }); } export async function fetchPositionDocumentById( request: ApiRequestFunction, instanceId: string, linkId: string ): Promise { try { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`, method: 'get' }); } catch { return null; } } export async function createPositionDocument( request: ApiRequestFunction, instanceId: string, data: Partial ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/position-documents`, method: 'post', data }); } export async function deletePositionDocument( request: ApiRequestFunction, instanceId: string, linkId: string ): Promise { await request({ url: `${_getTrusteeBaseUrl(instanceId)}/position-documents/${linkId}`, method: 'delete' }); } // ============================================================================ // QUICK ACTIONS API // ============================================================================ export interface QuickActionResponse { actions: Array<{ id: string; label: string; description: string; icon: string; color: string; category: string; actionType: 'agentPrompt' | 'workflow' | 'link'; config: Record; sortOrder: number; }>; categories: Array<{ id: string; label: string; sortOrder: number; }>; } export async function fetchQuickActions( request: ApiRequestFunction, instanceId: string, language: string = 'de' ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/quick-actions`, method: 'get', params: { language } }); } // ============================================================================ // ACCOUNTING API // ============================================================================ export async function fetchAccountingConnectors( request: ApiRequestFunction, instanceId: string ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/accounting/connectors`, method: 'get' }); } export async function fetchAccountingConfig( request: ApiRequestFunction, instanceId: string ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/accounting/config`, method: 'get' }); } export async function saveAccountingConfig( request: ApiRequestFunction, instanceId: string, data: { connectorType: string; displayLabel: string; config: Record } ): Promise { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/accounting/config`, method: 'post', data }); } export async function deleteAccountingConfig( request: ApiRequestFunction, instanceId: string ): Promise { await request({ 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> { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/accounting/chart-of-accounts`, method: 'get' }); } /** * Submits a background job that pushes positions to the accounting system and * polls `/api/jobs/{jobId}` until the job reaches a terminal status. Returns * the same `{ total, success, skipped, errors, results }` payload that the * legacy synchronous endpoint used to return -- but does NOT block the user * while the (potentially long) external accounting calls run in the worker. */ export async function syncPositionsToAccounting( request: ApiRequestFunction, instanceId: string, positionIds: string[], opts?: { pollMs?: number; onProgress?: (progress: number, message?: string | null) => void } ): Promise<{ total: number; success: number; skipped?: number; errors: number; results: any[] }> { const submission = await request({ url: `${_getTrusteeBaseUrl(instanceId)}/accounting/sync`, method: 'post', data: { positionIds } }); const jobId: string | undefined = submission?.jobId; if (!jobId) { throw new Error('Background job could not be started (missing jobId).'); } const pollMs = opts?.pollMs ?? 1500; const TERMINAL = new Set(['SUCCESS', 'ERROR', 'CANCELLED']); while (true) { const job = await request({ url: `/api/jobs/${jobId}`, method: 'get' }); if (opts?.onProgress) { opts.onProgress(Number(job?.progress ?? 0), job?.progressMessage ?? null); } if (job?.status && TERMINAL.has(job.status)) { if (job.status === 'SUCCESS' && job.result) { return job.result; } throw new Error(job?.errorMessage || 'Sync-Job fehlgeschlagen'); } await new Promise((resolve) => setTimeout(resolve, pollMs)); } } export async function fetchSyncStatus( request: ApiRequestFunction, instanceId: string ): Promise<{ items: AccountingSyncStatus[] }> { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/accounting/sync-status`, method: 'get' }); } // ============================================================================ // READ-ONLY DATA TABLE API (Daten-Tabellen page) // ============================================================================ // // Generic read-only endpoints for the consolidated data tables view. // All entities are paginated, sortable, filterable via the Unified Filter API // (mode=filterValues / mode=ids); no CRUD writes are exposed by these helpers. export interface TrusteeDataAccount { id: string; [key: string]: any; } export interface TrusteeDataJournalEntry { id: string; [key: string]: any; } export interface TrusteeDataJournalLine { id: string; [key: string]: any; } export interface TrusteeDataContact { id: string; [key: string]: any; } export interface TrusteeDataAccountBalance { id: string; [key: string]: any; } export interface TrusteeAccountingConfigRecord { id: string; [key: string]: any; } export interface TrusteeAccountingSyncRecord { id: string; [key: string]: any; } async function _fetchReadOnlyTable( request: ApiRequestFunction, instanceId: string, pathSegment: string, params?: PaginationParams ): Promise | T[]> { return await request({ url: `${_getTrusteeBaseUrl(instanceId)}/${pathSegment}`, method: 'get', params: _buildPaginationParams(params), }); } export async function fetchDataAccounts( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeDataAccount[]> { return _fetchReadOnlyTable(request, instanceId, 'data/accounts', params); } export async function fetchDataJournalEntries( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeDataJournalEntry[]> { return _fetchReadOnlyTable(request, instanceId, 'data/journal-entries', params); } export async function fetchDataJournalLines( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeDataJournalLine[]> { return _fetchReadOnlyTable(request, instanceId, 'data/journal-lines', params); } export async function fetchDataContacts( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeDataContact[]> { return _fetchReadOnlyTable(request, instanceId, 'data/contacts', params); } export async function fetchDataAccountBalances( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeDataAccountBalance[]> { return _fetchReadOnlyTable(request, instanceId, 'data/account-balances', params); } export async function fetchAccountingConfigs( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeAccountingConfigRecord[]> { return _fetchReadOnlyTable(request, instanceId, 'accounting/configs', params); } export async function fetchAccountingSyncs( request: ApiRequestFunction, instanceId: string, params?: PaginationParams ): Promise | TrusteeAccountingSyncRecord[]> { return _fetchReadOnlyTable(request, instanceId, 'accounting/syncs', params); } export async function exportAccountingData( request: ApiRequestFunction, instanceId: string ): Promise { const url = `${_getTrusteeBaseUrl(instanceId)}/accounting/export-data`; const response = await request({ url, method: 'get' }); const blob = new Blob([JSON.stringify(response, null, 2)], { type: 'application/json' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `trustee_data_${instanceId.slice(0, 8)}.json`; link.click(); URL.revokeObjectURL(link.href); }