From 1ef3c57e39a3421893add8926781822f56b24a4c Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Sun, 4 Jan 2026 01:35:18 +0100 Subject: [PATCH] feature trustee --- .../doc_trustee_feature_architecture.md | 1815 +++++++++++++++++ .../doc_trustee_feature_ui_demo.html | 1339 ++++++++++++ .../doc_trustee_feature_ui_specification.md | 1749 ++++++++++++++++ 3 files changed, 4903 insertions(+) create mode 100644 ui_nyla/feature-trustee/doc_trustee_feature_architecture.md create mode 100644 ui_nyla/feature-trustee/doc_trustee_feature_ui_demo.html create mode 100644 ui_nyla/feature-trustee/doc_trustee_feature_ui_specification.md diff --git a/ui_nyla/feature-trustee/doc_trustee_feature_architecture.md b/ui_nyla/feature-trustee/doc_trustee_feature_architecture.md new file mode 100644 index 0000000..09e0f8e --- /dev/null +++ b/ui_nyla/feature-trustee/doc_trustee_feature_architecture.md @@ -0,0 +1,1815 @@ +# Trustee Feature - Architektur und Implementierungsplan + +## Executive Summary + +Dieses Dokument beschreibt die Architektur und den Implementierungsplan für das **Trustee** Feature, welches das erste Feature in einem neuen Client-Management-System ist. Das Trustee Feature ermöglicht es Treuhandgesellschaften, die Spesenabrechnung und zugehörige Dokumente für ihre Kunden zentral zu verwalten. + +## Inhaltsverzeichnis + +1. [Übersicht](#overview) +2. [Architektur-Analyse](#architecture-analysis) +3. [Architektur-Entscheidungen](#architecture-decisions) +4. [System-Architektur](#system-architecture) +5. [Datenmodell](#data-model) +6. [RBAC-Integration](#rbac-integration) +7. [API-Design](#api-design) +8. [Implementierungsplan](#implementation-plan) +9. [Migrationsstrategie](#migration-strategy) +10. [Teststrategie](#testing-strategy) +11. [Implementierungsdetails](#implementation-details) + +**Zugehörige Dokumente**: +- [UI-Spezifikation](./doc_trustee_feature_ui_specification.md) - Frontend-Implementierung (UI-Komponenten, FormGenerator-Pattern, React/TypeScript) + +--- + +## Übersicht + +### Zweck + +Das Trustee Feature bietet ein zentralisiertes Spesenabrechnungssystem, bei dem: +- Benutzer ihre Spesen an ihre Treuhandgesellschaft melden können +- Treuhandgesellschaften Spesendaten zentral für alle ihre Kunden verwalten können +- Der Zugriff über ein feature-basiertes RBAC-System gesteuert wird + +### Schlüsselkonzepte + +1. **Feature-basiertes System**: Das System definiert Zugriffe für Features. "Trustee" ist das erste Feature. +2. **Datenbank-Interface**: Jedes Feature hat ein Datenbank-Interface (wie "chat") mit automatisierten Systemattributen (mandate, created, updated, etc.) +3. **RBAC-Integration**: Feature-Zugriff wird über RBAC-Ressourcen gesteuert +4. **Organisations-basierter Zugriff**: Der Zugriff wird auf Organisationsebene innerhalb des Trustee Features gesteuert + +--- + +## Architektur-Analyse + +### Verständnis des aktuellen Systems + +Basierend auf der Codebase-Analyse: + +1. **Interface Pattern**: Custom interfaces follow the pattern of `interfaceDbChatObjects.py`: + - Use `DatabaseConnector` for database operations + - Implement CRUD operations + - Integrate with RBAC through `interfaceRbac.py` + - Support standard attributes (mandate, _createdAt, _modifiedAt, _createdBy, _modifiedBy) - automatisch vom DatabaseConnector gesetzt + +2. **RBAC System**: + - Uses `AccessRuleContext` enum: DATA, UI, RESOURCE + - Access levels: ALL, MY, GROUP, NONE + - Resources are defined as cascading strings (e.g., "ai.model.anthropic") + - UI elements use cascading strings (e.g., "playground.voice.settings") + - **Filterung auf DB-Ebene**: `getRecordsetWithRBAC()` filtert Daten automatisch basierend auf: + - `ALL`: Keine Filterung (alle Records) + - `MY`: Filter nach `_createdBy = userId` (nur eigene Records) + - `GROUP`: Filter nach `mandateId = user.mandateId` (nur Records der eigenen Gruppe) + - `NONE`: Keine Records sichtbar (`1 = 0` WHERE-Clause) + - **View-Permission**: Wird zuerst geprüft - wenn `view=false`, werden keine Records zurückgegeben + +3. **Route Pattern**: Routes follow `routeData*.py` pattern: + - Use FastAPI routers + - Integrate with authentication via `getCurrentUser` + - Support pagination + - Use interface methods for data access + +4. **Data Model Pattern**: Models in `datamodels/datamodel*.py`: + - Use Pydantic BaseModel + - Register labels with `registerModelLabels` + - Include frontend metadata in `json_schema_extra` + +5. **DatabaseConnector Pattern**: + - **Automatische Tabellenerstellung**: `_ensureTableExists()` erstellt Tabellen automatisch aus Pydantic-Modellen + - **Automatische Spaltenerstellung**: Spalten werden aus Modell-Feldern generiert + - **Systemattribute**: Automatisch hinzugefügt: `_createdAt`, `_modifiedAt`, `_createdBy`, `_modifiedBy` + - **Index-Erstellung**: Automatische Indizes für Foreign Keys (Felder die auf `Id` enden) + - **Migration**: Fehlende Spalten werden automatisch hinzugefügt (additive Migration) + - **SQL-Typ-Mapping**: Automatische Konvertierung von Python-Typen zu PostgreSQL-Typen + +### Architektur-Entscheidungen + +1. **Feature-Registrierung**: Das Trustee Feature wird als neues Interface ähnlich wie Chat registriert +2. **RBAC-Ressourcen**: + - `ui.trustee` - UI-Zugriffskontrolle + - `resource.trustee` - Feature-Ressourcen-Zugriff +3. **Datenbank-Isolation**: Jedes Feature hat sein eigenes Datenbank-Interface, teilt aber dieselbe Datenbankinstanz +4. **Organisation vs Mandate**: + - **Mandate**: System-Level-Organisation (bestehendes Konzept) + - **Organisation**: Trustee-Feature-spezifische Firma (neues Konzept innerhalb des Trustee Features) + - Dies sind separate Konzepte - Organisation ist auf das Trustee Feature beschränkt + - **Beziehung**: + - Eine `mandate` kann mehrere `organisationId`s haben + - Eine `organisationId` gehört zu genau einer `mandate` + - `mandate` wird automatisch aus `currentUser.mandateId` gesetzt + - `organisationId` Dropdown zeigt alle gelieferten Organisationen (RBAC filtert automatisch) + - `organisationId`s können nicht über `mandate`-Grenzen hinweg geteilt werden + +--- + +## System-Architektur + +### Komponenten-Übersicht + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Frontend (React/TS) │ +│ - Trustee UI Components │ +│ - Organisation Management │ +│ - Contract Management │ +│ - Expense Reporting (Position + Document) │ +└──────────────────────┬──────────────────────────────────────┘ + │ + │ HTTP/REST API + │ +┌──────────────────────▼──────────────────────────────────────┐ +│ API Routes Layer │ +│ - routeDataTrusteeOrganisations.py │ +│ - routeDataTrusteeRoles.py │ +│ - routeDataTrusteeAccess.py │ +│ - routeDataTrusteeContracts.py │ +│ - routeDataTrusteeDocuments.py │ +│ - routeDataTrusteePositions.py │ +│ - routeDataTrusteePositionDocuments.py │ +└──────────────────────┬──────────────────────────────────────┘ + │ + │ Interface Methods + │ +┌──────────────────────▼──────────────────────────────────────┐ +│ Interface Layer (interfaceDbTrusteeObjects) │ +│ - CRUD Operations │ +│ - RBAC Filtering │ +│ - Business Logic │ +└──────────────────────┬──────────────────────────────────────┘ + │ + │ Database Connector + │ +┌──────────────────────▼──────────────────────────────────────┐ +│ Database Layer (PostgreSQL) │ +│ - trustee.organisation │ +│ - trustee.role │ +│ - trustee.access │ +│ - trustee.contract │ +│ - trustee.document │ +│ - trustee.position │ +│ - trustee.xpositiondocument │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ RBAC System │ +│ - resource.trustee (feature access) │ +│ - ui.trustee (UI access) │ +│ - Table-level access (DATA context) │ +│ - Feature-level access (trustee.access table) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Interface-Struktur + +Das Trustee-Interface folgt dem Muster von `interfaceDbChatObjects.py`: + +```python +# gateway/modules/interfaces/interfaceDbTrusteeObjects.py + +class TrusteeInterface: + """ + Interface for Trustee feature database operations. + Similar to ChatInterface but for trustee-specific data. + """ + + def __init__(self, currentUser: User): + self.currentUser = currentUser + self.db = DatabaseConnector(...) # Trustee database + self.rbac = RbacClass(self.db, dbApp=dbApp) + + # Organisation CRUD + def createOrganisation(...) + def getOrganisation(...) + def updateOrganisation(...) + def deleteOrganisation(...) + def getAllOrganisations(...) + + # Role CRUD + def createRole(...) + def getAllRoles(...) + + # Access CRUD + def createAccess(...) + def getUserAccessForOrganisation(...) + def checkUserPermission(...) + + # Contract CRUD + def createContract(...) + def getContract(...) + # ... etc + + # Document CRUD + def createDocument(...) + def getDocument(...) + def getDocumentData(...) # Binary data + + # Position CRUD + def createPosition(...) + def getPosition(...) + # ... etc + + # Cross-reference operations + def linkPositionDocument(...) + def unlinkPositionDocument(...) + def getDocumentsForPosition(...) + def getPositionsForDocument(...) +``` + +--- + +## Datenmodell + +### Tabelle: `trustee.organisation` + +Repräsentiert Treuhandgesellschaften (Organisationen) innerhalb des Trustee Features. + +```python +class TrusteeOrganisation(BaseModel): + id: str = Field( # Unique string label (PK), not UUID + description="Unique organisation identifier (label)", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": False, # Änderbar bei Erstellung, danach readonly + "frontend_required": True + } + ) + label: str = Field( + description="Company name", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": False, + "frontend_required": True + } + ) + enabled: bool = Field( + default=True, + description="Whether the organisation is enabled", + json_schema_extra={ + "frontend_type": "checkbox", + "frontend_readonly": False, + "frontend_required": False + } + ) + mandate: str = Field( # System attribute + description="Mandate ID (system-level organisation)", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + # Systemattribute werden automatisch vom DatabaseConnector gesetzt: + # _createdAt, _modifiedAt, _createdBy, _modifiedBy + # Diese werden nicht im Modell definiert, aber im Backend automatisch verwaltet + # Systemattribute-Verwaltung: + # - _createdBy: Automatisch aus currentUser.id gesetzt + # - _createdAt: Automatisch beim Erstellen gesetzt (float, UTC timestamp in Sekunden) + # - _modifiedAt: Automatisch beim Update gesetzt (float, UTC timestamp in Sekunden) + # - _modifiedBy: Automatisch aus currentUser.id beim Update gesetzt + # - Frontend: Diese Felder werden als readonly angezeigt + # - Formatierung: Timestamps als float (UI rendert gemäß Zeitzoneneinstellungen), User-Namen statt User-ID + # - Sichtbarkeit: Kann in FormGeneric definiert werden, welche Felder angezeigt werden, aber sie müssen ans UI geliefert werden über die Routes + +registerModelLabels( + "TrusteeOrganisation", + {"en": "Organisation", "fr": "Organisation"}, + { + "id": {"en": "ID", "fr": "ID"}, + "label": {"en": "Label", "fr": "Libellé"}, + "enabled": {"en": "Enabled", "fr": "Activé"}, + "mandate": {"en": "Mandate", "fr": "Mandat"}, + }, +) +``` + +**RBAC Rules**: +- `sysadmin`: Can manage all organisations +- `admin`: Can manage organisations for their group (mandate) + +**Indexes**: +- Primary key on `id` +- Index on `mandate` for filtering + +### Tabelle: `trustee.role` + +Definiert Rollen innerhalb des Trustee Features. + +```python +class TrusteeRole(BaseModel): + id: str = Field( # Unique string label (PK), not UUID + description="Unique role identifier (label)", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": False, + "frontend_required": True + } + ) + desc: str = Field( + description="Role description", + json_schema_extra={ + "frontend_type": "textarea", + "frontend_readonly": False, + "frontend_required": True + } + ) + mandate: str = Field( # System attribute + description="Mandate ID", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + # Systemattribute werden automatisch vom DatabaseConnector gesetzt + +registerModelLabels( + "TrusteeRole", + {"en": "Role", "fr": "Rôle"}, + { + "id": {"en": "ID", "fr": "ID"}, + "desc": {"en": "Description", "fr": "Description"}, + "mandate": {"en": "Mandate", "fr": "Mandat"}, + }, +) +``` + +**Initiale Rollen**: +- `userreport`: Kann Benutzerdokumente an das System liefern +- `admin`: Kann den Zugriff administrieren +- `operate`: Kann Daten für Operationen verwenden + +**RBAC Rules**: +- `sysadmin`: Can manage all roles + +**Indexes**: +- Primary key on `id` +- Index on `mandate` for filtering + +### Tabelle: `trustee.access` + +Definiert Benutzerzugriff auf Organisationen mit spezifischen Rollen. Der Zugriff kann optional auf einen spezifischen Contract beschränkt werden. + +```python +class TrusteeAccess(BaseModel): + id: str = Field( # UUID PK + default_factory=lambda: str(uuid.uuid4()), + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + organisationId: str = Field( + description="Reference to trustee.organisation.id", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": "trustee.organisation" # String-Referenz für dynamische Options + } + ) + roleId: str = Field( + description="Reference to trustee.role.id", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": "trustee.role" # String-Referenz für dynamische Options + } + ) + userId: str = Field( + description="User ID assigned to this role", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": "user" # String-Referenz für User-Liste + } + ) + contractId: Optional[str] = Field( + default=None, + description="Optional reference to trustee.contract.id. If None, access is for full organisation. If set, access is limited to this specific contract.", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": False, + "frontend_options": "trustee.contract", # String-Referenz, dynamisch gefiltert nach organisationId + "frontend_depends_on": "organisationId" # Dropdown wird aktualisiert wenn organisationId geändert wird + } + ) + mandate: str = Field( # System attribute + description="Mandate ID", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + # Systemattribute werden automatisch vom DatabaseConnector gesetzt + +registerModelLabels( + "TrusteeAccess", + {"en": "Access", "fr": "Accès"}, + { + "id": {"en": "ID", "fr": "ID"}, + "organisationId": {"en": "Organisation", "fr": "Organisation"}, + "roleId": {"en": "Role", "fr": "Rôle"}, + "userId": {"en": "User", "fr": "Utilisateur"}, + "contractId": {"en": "Contract (optional)", "fr": "Contrat (optionnel)"}, + "mandate": {"en": "Mandate", "fr": "Mandat"}, + }, +) +``` + +**Zugriffslogik**: +- **Ohne `contractId` (None)**: Zugriff gilt für die gesamte Organisation +- **Mit `contractId`**: Zugriff ist auf diesen spezifischen Contract beschränkt +- **RBAC-Filterung**: Bei der Datenfilterung wird geprüft: + - Hat User Access für Organisation (ohne Contract) → Zugriff auf alle Contracts dieser Organisation + - Hat User Access für Organisation + spezifischen Contract → Zugriff nur auf diesen Contract + +**RBAC Rules**: +- `sysadmin`: Can manage all access records +- `admin`: Can manage access for their group (mandate) +- Users with `admin` role in trustee.access can manage access for their organisation + +**Indexes**: +- Primary key on `id` +- Unique constraint on `(organisationId, roleId, userId, contractId)` to prevent duplicates (erlaubt aber mehrere Rollen pro Benutzer-Organisation durch separate Records) +- Index on `organisationId` +- Index on `userId` +- Index on `roleId` +- Index on `contractId` +- Index on `mandate` + +### Tabelle: `trustee.contract` + +Definiert Kundenverträge innerhalb von Organisationen. + +```python +class TrusteeContract(BaseModel): + id: str = Field( # UUID PK + default_factory=lambda: str(uuid.uuid4()), + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + organisationId: str = Field( + description="Reference to trustee.organisation.id", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, # Änderbar bei Erstellung, danach readonly (immutable) + "frontend_required": True, + "frontend_options": "trustee.organisation" + } + ) + label: str = Field( + description="Label for the customer contract (e.g., 'Muster AG 2026')", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": False, + "frontend_required": True + } + ) + enabled: bool = Field( + default=True, + description="Whether the contract is enabled", + json_schema_extra={ + "frontend_type": "checkbox", + "frontend_readonly": False, + "frontend_required": False + } + ) + mandate: str = Field( # System attribute + description="Mandate ID", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + # Systemattribute werden automatisch vom DatabaseConnector gesetzt + +registerModelLabels( + "TrusteeContract", + {"en": "Contract", "fr": "Contrat"}, + { + "id": {"en": "ID", "fr": "ID"}, + "organisationId": {"en": "Organisation", "fr": "Organisation"}, + "label": {"en": "Label", "fr": "Libellé"}, + "enabled": {"en": "Enabled", "fr": "Activé"}, + "mandate": {"en": "Mandate", "fr": "Mandat"}, + }, +) +``` + +**RBAC Rules**: +- `sysadmin`: Can manage all contracts +- `admin`: Can manage contracts for their group (mandate) +- Users with `admin` role in trustee.access: Can CRUD contracts for their organisationId + - Wenn `contractId` in trustee.access leer: Zugriff auf alle Contracts der Organisation + - Wenn `contractId` gesetzt: Zugriff nur auf diesen spezifischen Contract + - New records default to their own organisationId + - Dropdown to select from granted organisationIds (und optional Contracts) + +**Wichtig**: Verträge sind unveränderlich bezüglich `organisationId` - kann nach der Erstellung nicht mehr geändert werden. + +**Implementierung**: +- **Backend-Validierung**: In `updateContract()` prüfen: Wenn `organisationId` im Update vorhanden und unterschiedlich zum bestehenden Wert → Fehler +- **Frontend-Readonly**: `organisationId` wird auf readonly gesetzt wenn `id` vorhanden (non-blank) ist +- **Logik**: ID kann nur gespeichert werden, wenn non-blank. Eine non-blank ID kann nicht mehr geändert werden + +**Indexes**: +- Primary key on `id` +- Index on `organisationId` +- Index on `mandate` +- **Wichtig**: `organisationId` ist immutable nach Erstellung - keine Updates erlaubt + +**Validierung**: +- `id` Format: Alphanumerisch + Bindestrich/Unterstrich +- Länge: 3-50 Zeichen +- Validierung: Backend + Frontend + +### Tabelle: `trustee.document` + +Enthält Dokumentreferenzen und Belege für Buchungen. + +```python +class TrusteeDocument(BaseModel): + id: str = Field( # UUID PK + default_factory=lambda: str(uuid.uuid4()), + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + organisationId: str = Field( + description="Reference to trustee.organisation.id", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": "trustee.organisation" + } + ) + contractId: str = Field( + description="Reference to trustee.contract.id", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": "trustee.contract" # Gefiltert nach organisationId + } + ) + documentData: bytes = Field( # Binary data + description="The file content", + json_schema_extra={ + "frontend_type": "file", # Für File Upload + "frontend_readonly": False, + "frontend_required": True + } + ) + documentName: str = Field( + description="File name (e.g., 'Beleg.pdf')", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": False, + "frontend_required": True + } + ) + documentMimeType: str = Field( + description="MIME type of the document", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": [ + {"value": "application/pdf", "label": {"en": "PDF", "fr": "PDF"}}, + {"value": "image/jpeg", "label": {"en": "JPEG", "fr": "JPEG"}}, + {"value": "image/png", "label": {"en": "PNG", "fr": "PNG"}}, + # ... weitere MIME-Types + ] + } + ) + mandate: str = Field( # System attribute + description="Mandate ID", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + # Systemattribute werden automatisch vom DatabaseConnector gesetzt + +registerModelLabels( + "TrusteeDocument", + {"en": "Document", "fr": "Document"}, + { + "id": {"en": "ID", "fr": "ID"}, + "organisationId": {"en": "Organisation", "fr": "Organisation"}, + "contractId": {"en": "Contract", "fr": "Contrat"}, + "documentData": {"en": "Document Data", "fr": "Données du document"}, + "documentName": {"en": "Document Name", "fr": "Nom du document"}, + "documentMimeType": {"en": "MIME Type", "fr": "Type MIME"}, + "mandate": {"en": "Mandate", "fr": "Mandat"}, + }, +) +``` + +**RBAC Rules**: +- `sysadmin`: Can manage all documents +- `admin`: Can manage documents for their group (mandate) +- Users with `operate` role in trustee.access: Can CRUD documents in their organisationId +- Users with `userreport` role in trustee.access: Can CRUD own documents (_createdBy = userId, automatisch über Systemattribute gesetzt) + +**Speicherung**: +- Dokument-Binärdaten werden in PostgreSQL BYTEA-Spalte gespeichert +- Einfach, transaktional, einfaches Backup + +**File Upload/Download**: +- **Nicht direkt integriert**: File Upload/Download erfolgt über das Workflow-System mit einer Action +- Die Action erstellt automatisch die Datensätze in `trustee.document` +- Dies ist nicht Teil der direkten Trustee-Feature-Implementierung + +**Indexes**: +- Primary key on `id` +- Index on `organisationId` +- Index on `contractId` +- Index on `mandate` +- Index on `_created_at` (für userreport Filterung) +- Index auf `_created_by` (automatisch über Systemattribute, für userreport Filterung) + +### Tabelle: `trustee.position` + +Enthält Buchungspositionen (Speseneinträge). + +```python +class TrusteePosition(BaseModel): + id: str = Field( # UUID PK + default_factory=lambda: str(uuid.uuid4()), + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + organisationId: str = Field( + description="Reference to trustee.organisation.id", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": "trustee.organisation" + } + ) + contractId: str = Field( + description="Reference to trustee.contract.id", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": "trustee.contract" # Gefiltert nach organisationId + } + ) + valuta: date = Field( + description="Value date", + json_schema_extra={ + "frontend_type": "date", + "frontend_readonly": False, + "frontend_required": True + } + ) + transactionDateTime: datetime = Field( + description="Transaction timestamp", + json_schema_extra={ + "frontend_type": "timestamp", + "frontend_readonly": False, + "frontend_required": True + } + ) + company: str = Field( + default="", + description="Company name", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": False, + "frontend_required": False + } + ) + desc: str = Field( + default="", + description="Description", + json_schema_extra={ + "frontend_type": "textarea", + "frontend_readonly": False, + "frontend_required": False + } + ) + tags: str = Field( + default="", + description="Tags (comma-separated or JSON)", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": False, + "frontend_required": False + } + ) + bookingCurrency: str = Field( + description="Booking currency code", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": [ + {"value": "CHF", "label": {"en": "CHF", "fr": "CHF"}}, + {"value": "EUR", "label": {"en": "EUR", "fr": "EUR"}}, + {"value": "USD", "label": {"en": "USD", "fr": "USD"}}, + # ... weitere Währungen + ] + } + ) + bookingAmount: float = Field( + description="Booking amount", + json_schema_extra={ + "frontend_type": "number", + "frontend_readonly": False, + "frontend_required": True + } + ) + originalCurrency: str = Field( + description="Original currency code", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": [ + {"value": "CHF", "label": {"en": "CHF", "fr": "CHF"}}, + {"value": "EUR", "label": {"en": "EUR", "fr": "EUR"}}, + {"value": "USD", "label": {"en": "USD", "fr": "USD"}}, + ] + } + ) + originalAmount: float = Field( + description="Original amount (manuelle Eingabe in Phase 1, keine automatische Währungsumrechnung)", + json_schema_extra={ + "frontend_type": "number", + "frontend_readonly": False, + "frontend_required": True + } + ) + vatPercentage: float = Field( + default=0.0, + description="VAT percentage", + json_schema_extra={ + "frontend_type": "number", + "frontend_readonly": False, + "frontend_required": False + } + ) + vatAmount: float = Field( + default=0.0, + description="VAT amount (wird automatisch berechnet: bookingAmount * vatPercentage / 100, kann manuell überschrieben werden)", + json_schema_extra={ + "frontend_type": "number", + "frontend_readonly": False, # Editierbar für manuelle Überschreibung + "frontend_required": False + } + ) + # MwSt-Berechnung: + # - Automatisch beim Ändern von bookingAmount oder vatPercentage + # - Wenn vatAmount manuell geändert wird, wird automatische Berechnung erneut durchgeführt + # - Warnung (Toast) erscheint bereits beim Ändern, nicht erst beim Speichern + mandate: str = Field( # System attribute + description="Mandate ID", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + # Systemattribute werden automatisch vom DatabaseConnector gesetzt + +registerModelLabels( + "TrusteePosition", + {"en": "Position", "fr": "Position"}, + { + "id": {"en": "ID", "fr": "ID"}, + "organisationId": {"en": "Organisation", "fr": "Organisation"}, + "contractId": {"en": "Contract", "fr": "Contrat"}, + "valuta": {"en": "Value Date", "fr": "Date de valeur"}, + "transactionDateTime": {"en": "Transaction Date/Time", "fr": "Date/Heure de transaction"}, + "company": {"en": "Company", "fr": "Entreprise"}, + "desc": {"en": "Description", "fr": "Description"}, + "tags": {"en": "Tags", "fr": "Tags"}, + "bookingCurrency": {"en": "Booking Currency", "fr": "Devise de comptabilisation"}, + "bookingAmount": {"en": "Booking Amount", "fr": "Montant de comptabilisation"}, + "originalCurrency": {"en": "Original Currency", "fr": "Devise d'origine"}, + "originalAmount": {"en": "Original Amount", "fr": "Montant d'origine"}, + "vatPercentage": {"en": "VAT Percentage", "fr": "Pourcentage TVA"}, + "vatAmount": {"en": "VAT Amount", "fr": "Montant TVA"}, + "mandate": {"en": "Mandate", "fr": "Mandat"}, + }, +) +``` + +**RBAC Rules**: +- `sysadmin`: Can manage all positions +- `admin`: Can manage positions for their group (mandate) +- Users with `operate` role in trustee.access: Can CRUD positions in their organisationId + - Wenn `contractId` in trustee.access leer: Zugriff auf alle Positions der Organisation + - Wenn `contractId` gesetzt: Zugriff nur auf Positions dieses Contracts +- Users with `userreport` role in trustee.access: Can CRUD own positions (_createdBy = userId, automatisch über Systemattribute gesetzt) + - Contract-Filterung gilt auch für userreport (nur eigene Positions in erlaubten Contracts) + +**Indexes**: +- Primary key on `id` +- Index on `organisationId` +- Index on `contractId` +- Index on `valuta` (for date-based queries) +- Index on `transactionDateTime` +- Index on `mandate` +- Index auf `_createdBy` (automatisch über Systemattribute, für userreport Filterung) + +### Tabelle: `trustee.xpositiondocument` + +Verknüpfungstabelle, die Positionen mit Dokumenten verknüpft (viele-zu-viele). + +```python +class TrusteePositionDocument(BaseModel): + id: str = Field( # UUID PK + default_factory=lambda: str(uuid.uuid4()), + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + organisationId: str = Field( + description="Reference to trustee.organisation.id", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": "trustee.organisation" + } + ) + contractId: str = Field( + description="Reference to trustee.contract.id", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": "trustee.contract" + } + ) + documentId: str = Field( + description="Reference to trustee.document.id", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": "trustee.document" # Gefiltert nach organisationId/contractId + } + ) + positionId: str = Field( + description="Reference to trustee.position.id", + json_schema_extra={ + "frontend_type": "select", + "frontend_readonly": False, + "frontend_required": True, + "frontend_options": "trustee.position" # Gefiltert nach organisationId/contractId + } + ) + mandate: str = Field( # System attribute + description="Mandate ID", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, + "frontend_required": False + } + ) + # Systemattribute werden automatisch vom DatabaseConnector gesetzt + +registerModelLabels( + "TrusteePositionDocument", + {"en": "Position-Document Link", "fr": "Lien Position-Document"}, + { + "id": {"en": "ID", "fr": "ID"}, + "organisationId": {"en": "Organisation", "fr": "Organisation"}, + "contractId": {"en": "Contract", "fr": "Contrat"}, + "documentId": {"en": "Document", "fr": "Document"}, + "positionId": {"en": "Position", "fr": "Position"}, + "mandate": {"en": "Mandate", "fr": "Mandat"}, + }, +) +``` +<|tool▁calls▁begin|><|tool▁call▁begin|> +read_file + +**RBAC Rules**: +- `sysadmin`: Can manage all cross-references +- `admin`: Can manage cross-references for their group (mandate) +- Users with `operate` role in trustee.access: Can CRUD cross-references in their organisationId + - Wenn `contractId` in trustee.access leer: Zugriff auf alle Cross-References der Organisation + - Wenn `contractId` gesetzt: Zugriff nur auf Cross-References dieses Contracts +- Users with `userreport` role in trustee.access: Can CRUD cross-references for their own positions/documents (createdBy = userId, automatisch über Systemattribute gesetzt) + - Contract-Filterung gilt auch für userreport (nur eigene Cross-References in erlaubten Contracts) + +**Wichtig**: Verknüpfungen sind optional - Positionen können ohne Dokumente existieren, Dokumente können ohne Positionen existieren. + +**Indexes**: +- Primary key on `id` +- Unique constraint on `(positionId, documentId)` to prevent duplicates +- Index on `organisationId` +- Index on `contractId` +- Index on `documentId` +- Index on `positionId` +- Index on `mandate` +- Index auf `_created_by` (automatisch über Systemattribute, für userreport Filterung) + +--- + +## RBAC-Integration + +### System-Level RBAC + +Das Trustee Feature integriert sich auf mehreren Ebenen mit dem bestehenden RBAC-System: + +#### 1. Feature Access (`resource.trustee`) + +**AccessRule**: +```python +AccessRule( + roleLabel="admin", # or appropriate system role + context=AccessRuleContext.RESOURCE, + item="trustee", + view=True, + # No read/create/update/delete for RESOURCE context +) +``` + +**Zweck**: Steuert, ob ein Benutzer überhaupt auf das Trustee Feature zugreifen kann. + +#### 2. UI Access (`ui.trustee`) + +**AccessRule**: +```python +AccessRule( + roleLabel="admin", + context=AccessRuleContext.UI, + item="trustee", + view=True, +) +``` + +**Zweck**: Steuert die UI-Sichtbarkeit für das Trustee Feature. + +#### 3. Table-Level Access (DATA context) + +Each trustee table needs DATA context access rules: + +```python +# Example for trustee.organisation +AccessRule( + roleLabel="admin", + context=AccessRuleContext.DATA, + item="trustee.organisation", + view=True, + read=AccessLevel.GROUP, # Can read group records + create=AccessLevel.GROUP, + update=AccessLevel.GROUP, + delete=AccessLevel.GROUP, +) +``` + +### Feature-Level RBAC (trustee.access Tabelle) + +Die `trustee.access` Tabelle erweitert System-RBAC mit Feature-spezifischen Berechtigungen: + +1. **Rollen-basierter Zugriff**: Benutzern werden Rollen (`userreport`, `admin`, `operate`) für spezifische Organisationen zugewiesen +2. **Contract-basierter Zugriff (optional)**: + - Wenn `contractId` nicht gesetzt (None): Zugriff gilt für die gesamte Organisation + - Wenn `contractId` gesetzt: Zugriff ist auf diesen spezifischen Contract beschränkt +3. **Berechtigungsprüfung**: Bei der Durchführung von Operationen prüfen: + - System RBAC (Feature-Zugriff, Tabellen-Zugriff) + - Feature RBAC (trustee.access Tabelle für Organisation, Rolle und optional Contract) + +### Permission Checking Logic + +```python +def checkTrusteePermission( + user: User, + organisationId: str, + operation: str, # 'read', 'create', 'update', 'delete' + resource: str, # 'contract', 'document', 'position', etc. + recordId: Optional[str] = None, + contractId: Optional[str] = None # Optional: für Contract-basierte Filterung +) -> bool: + """ + Check if user has permission for trustee operation. + + Steps: + 1. Check system RBAC: resource.trustee access + 2. Check system RBAC: table-level access (trustee.{resource}) + 3. Check feature RBAC: trustee.access table + - Get user's roles for organisationId (und optional contractId) + - Check if role allows operation on resource + - Wenn contractId angegeben: Prüfe ob User Access für diesen Contract hat + """ + + # Step 1: Feature access + if not checkSystemRbac(user, "resource.trustee"): + return False + + # Step 2: Table access + tableName = f"trustee.{resource}" + if not checkSystemRbac(user, tableName, operation): + return False + + # Step 3: Feature-level access + # Get user's access records for this organisation + userAccessRecords = getTrusteeAccessForUser(user.id, organisationId) + + # Filter by contract if contractId is specified + if contractId: + # User muss Access für diesen spezifischen Contract haben + # Oder Access ohne contractId (full organisation access) + userAccessRecords = [ + acc for acc in userAccessRecords + if acc.contractId is None or acc.contractId == contractId + ] + else: + # Wenn kein contractId angegeben: User muss Access haben (mit oder ohne contractId) + pass + + userRoles = [acc.roleId for acc in userAccessRecords] + + # Check role permissions + if "admin" in userRoles: + # Admin can do everything for their organisation (or specific contract) + return True + + if operation in ["read", "create", "update", "delete"]: + if "operate" in userRoles: + # Operate can CRUD for organisation (or specific contract) + if resource in ["contract", "document", "position", "xpositiondocument"]: + return True + + if "userreport" in userRoles: + # Userreport can CRUD own records + if recordId: + record = getRecord(resource, recordId) + if record._createdBy == user.id: # _createdBy wird automatisch vom DatabaseConnector gesetzt + return True + else: + # Creating new record - allowed + return True + + return False +``` + +### Rollen-Berechtigungs-Matrix + +| Role | Organisation | Contract | Document | Position | PositionDocument | +|------|--------------|----------|-----------|----------|------------------| +| **admin** (system) | CRUD (group) | CRUD (group) | CRUD (group) | CRUD (group) | CRUD (group) | +| **admin** (trustee.access) | CRUD (org) | CRUD (org) | CRUD (org) | CRUD (org) | CRUD (org) | +| **operate** | Read (org) | CRUD (org) | CRUD (org) | CRUD (org) | CRUD (org) | +| **userreport** | Read (org) | Read (org) | CRUD (own) | CRUD (own) | CRUD (own) | + +**Legend**: +- CRUD: Create, Read, Update, Delete +- (group): Within user's mandate/group +- (org): Within assigned organisation +- (own): Only records created by the user + +--- + +## API-Design + +### RBAC-Berechtigungsabfrage + +Das UI benötigt Berechtigungsinformationen, um die richtigen Komponenten zu rendern. Hierfür stehen folgende RBAC-Routen zur Verfügung: + +#### Einzelne Berechtigung abfragen +```python +GET /api/rbac/permissions?context=UI&item=trustee +GET /api/rbac/permissions?context=RESOURCE&item=trustee +``` +Gibt `UserPermissions` für einen spezifischen Kontext und Item zurück. + +#### Alle Berechtigungen abrufen (für UI-Initialisierung) +```python +GET /api/rbac/permissions/all # Alle UI- und RESOURCE-Berechtigungen +GET /api/rbac/permissions/all?context=UI # Nur UI-Berechtigungen +GET /api/rbac/permissions/all?context=RESOURCE # Nur RESOURCE-Berechtigungen +``` +Gibt alle Berechtigungen des aktuellen Users zurück: +```json +{ + "ui": { + "trustee": {"view": true, "read": null, "create": null, "update": null, "delete": null}, + "trustee.organisation": {"view": true, ...}, + "trustee.role": {"view": true, ...}, + ... + }, + "resource": { + "trustee": {"view": true, ...}, + ... + } +} +``` + +**Verwendung**: +- **UI-Initialisierung**: Einmaliger Call zu `/api/rbac/permissions/all` beim Laden der Anwendung +- **Spezifische Prüfung**: `/api/rbac/permissions?context=UI&item=trustee.organisation` für dynamische Berechtigungsprüfungen + +### Route-Struktur + +Alle Trustee-Routen folgen dem Muster `/api/trustee/{resource}`: + +#### Organisation Routes (`routeDataTrusteeOrganisations.py`) + +```python +GET /api/trustee/organisations/ # List organisations (filtered by RBAC) +GET /api/trustee/organisations/{id} # Get organisation +POST /api/trustee/organisations/ # Create organisation +PUT /api/trustee/organisations/{id} # Update organisation +DELETE /api/trustee/organisations/{id} # Delete organisation +``` + +#### Role Routes (`routeDataTrusteeRoles.py`) + +```python +GET /api/trustee/roles/ # List roles +GET /api/trustee/roles/{id} # Get role +POST /api/trustee/roles/ # Create role (sysadmin only) +PUT /api/trustee/roles/{id} # Update role (sysadmin only) +DELETE /api/trustee/roles/{id} # Delete role (sysadmin only) +``` + +#### Access Routes (`routeDataTrusteeAccess.py`) + +```python +GET /api/trustee/access/ # List access records (filtered) +GET /api/trustee/access/{id} # Get access record +POST /api/trustee/access/ # Create access record +PUT /api/trustee/access/{id} # Update access record +DELETE /api/trustee/access/{id} # Delete access record +GET /api/trustee/access/organisation/{orgId} # Get access for organisation +GET /api/trustee/access/user/{userId} # Get access for user +``` + +#### Contract Routes (`routeDataTrusteeContracts.py`) + +```python +GET /api/trustee/contracts/ # List contracts (filtered) +GET /api/trustee/contracts/{id} # Get contract +POST /api/trustee/contracts/ # Create contract +PUT /api/trustee/contracts/{id} # Update contract +DELETE /api/trustee/contracts/{id} # Delete contract +GET /api/trustee/contracts/organisation/{orgId} # Get contracts for organisation +``` + +#### Document Routes (`routeDataTrusteeDocuments.py`) + +```python +GET /api/trustee/documents/ # List documents (filtered) +GET /api/trustee/documents/{id} # Get document metadata +GET /api/trustee/documents/{id}/data # Get document binary data +POST /api/trustee/documents/ # Create document (with upload) +PUT /api/trustee/documents/{id} # Update document metadata +DELETE /api/trustee/documents/{id} # Delete document +GET /api/trustee/documents/contract/{contractId} # Get documents for contract +``` + +#### Position Routes (`routeDataTrusteePositions.py`) + +```python +GET /api/trustee/positions/ # List positions (filtered) +GET /api/trustee/positions/{id} # Get position +POST /api/trustee/positions/ # Create position +PUT /api/trustee/positions/{id} # Update position +DELETE /api/trustee/positions/{id} # Delete position +GET /api/trustee/positions/contract/{contractId} # Get positions for contract +GET /api/trustee/positions/organisation/{orgId} # Get positions for organisation +``` + +#### Position-Document Cross-Reference Routes (`routeDataTrusteePositionDocuments.py`) + +```python +GET /api/trustee/position-documents/ # List cross-references +GET /api/trustee/position-documents/{id} # Get cross-reference +POST /api/trustee/position-documents/ # Link position to document +DELETE /api/trustee/position-documents/{id} # Unlink position from document +GET /api/trustee/position-documents/position/{positionId} # Get documents for position +GET /api/trustee/position-documents/document/{documentId} # Get positions for document +``` + +### Request/Response Examples + +#### Create Organisation + +```http +POST /api/trustee/organisations/ +Content-Type: application/json + +{ + "id": "acme-corp", + "label": "ACME Corporation", + "enabled": true +} +``` + +Response: +```json +{ + "id": "acme-corp", + "label": "ACME Corporation", + "enabled": true, + "mandate": "mandate-123", + "created": 1704067200.0, + "updated": 1704067200.0 +} +``` + +#### Create Access Record + +```http +POST /api/trustee/access/ +Content-Type: application/json + +{ + "organisationId": "acme-corp", + "roleId": "operate", + "userId": "user-456" +} +``` + +#### Upload Document + +```http +POST /api/trustee/documents/ +Content-Type: multipart/form-data + +organisationId: acme-corp +contractId: contract-789 +documentName: receipt.pdf +documentMimeType: application/pdf +file: +``` + +--- + +## Implementierungsplan + +### Phase 1: Grundlagen (Woche 1-2) + +#### 1.1 Datenmodelle +- [ ] Create `datamodelTrustee.py` with all model classes +- [ ] Register model labels +- [ ] Add frontend metadata +- [ ] Define validation rules + +#### 1.2 Datenbank-Interface +- [ ] Create `interfaceDbTrusteeObjects.py` +- [ ] Implement database connector initialization +- [ ] Implement table creation/initialization +- [ ] Implement basic CRUD operations for all tables +- [ ] Add RBAC integration helpers + +#### 1.3 RBAC Setup +- [ ] Create initial AccessRules for: + - `resource.trustee` + - `ui.trustee` + - All trustee tables (DATA context) +- [ ] Create bootstrap script to initialize roles +- [ ] Document RBAC configuration + +### Phase 2: Core API (Woche 3-4) + +#### 2.1 Route Implementation +- [ ] `routeDataTrusteeOrganisations.py` +- [ ] `routeDataTrusteeRoles.py` +- [ ] `routeDataTrusteeAccess.py` +- [ ] `routeDataTrusteeContracts.py` +- [ ] `routeDataTrusteeDocuments.py` +- [ ] `routeDataTrusteePositions.py` +- [ ] `routeDataTrusteePositionDocuments.py` + +#### 2.2 Berechtigungsprüfung +- [ ] Implement `checkTrusteePermission()` helper +- [ ] Integrate permission checks in all routes +- [ ] Add permission checks in interface methods +- [ ] Test permission scenarios + +#### 2.3 Route Registration +- [ ] Register routes in main app +- [ ] Add route documentation +- [ ] Test API endpoints + +### Phase 3: Advanced Features (Week 5-6) + +#### 3.1 Dokumentenspeicherung +- [ ] Decide on storage approach (DB vs file system) +- [ ] Implement document upload/download +- [ ] Add file size limits +- [ ] Add MIME type validation + +#### 3.2 Query Optimization +- [ ] Add database indexes +- [ ] Optimize RBAC filtering queries +- [ ] Add pagination support +- [ ] Add filtering and sorting + +#### 3.3 Validierung & Fehlerbehandlung +- [ ] Add input validation +- [ ] Add foreign key validation +- [ ] Improve error messages +- [ ] Add audit logging + +### Phase 4: Frontend-Integration (Woche 7-8) + +#### 4.1 UI Components +- [ ] Organisation management UI +- [ ] Role management UI (sysadmin only) +- [ ] Access management UI +- [ ] Contract management UI +- [ ] Document upload/management UI +- [ ] Position entry/management UI +- [ ] Position-document linking UI + +#### 4.2 RBAC UI Integration +- [ ] Hide UI elements based on `ui.trustee` access +- [ ] Show/hide features based on roles +- [ ] Add permission error messages + +### Phase 5: Testing & Dokumentation (Woche 9-10) + +#### 5.1 Unit Tests +- [ ] Test data models +- [ ] Test interface methods +- [ ] Test permission checking +- [ ] Test RBAC integration + +#### 5.2 Integration Tests +- [ ] Test API endpoints +- [ ] Test RBAC scenarios +- [ ] Test document upload/download +- [ ] Test cross-reference operations + +#### 5.3 Documentation +- [ ] API documentation +- [ ] User guide +- [ ] Admin guide +- [ ] Developer guide + +--- + +## Migrationsstrategie + +### Datenbank-Migration + +1. **Tabellen erstellen**: Interface-Initialisierung verwenden, um Tabellen zu erstellen +2. **Initialdaten**: Bootstrap-Skript zum Erstellen initialer Rollen +3. **RBAC-Regeln**: Migrationsskript zum Erstellen von AccessRules +4. **Datenmigration**: Falls Migration von bestehendem System, Migrationsskript erstellen + +### Rollout-Plan + +1. **Entwicklung**: Implementierung in Entwicklungsumgebung +2. **Testing**: Deployment in Testumgebung, Integrationstests durchführen +3. **Staging**: Deployment in Staging, User Acceptance Testing +4. **Produktion**: Schrittweiser Rollout mit Feature-Flag + +### Rückwärtskompatibilität + +- Keine Breaking Changes am bestehenden System +- Trustee Feature ist additiv +- Bestehendes RBAC-System bleibt unverändert + +--- + +## Testing Strategy + +### Unit Tests + +```python +# Test data models +def test_trustee_organisation_model(): + org = TrusteeOrganisation( + id="test-org", + label="Test Organisation", + enabled=True + ) + assert org.id == "test-org" + +# Test interface methods +def test_create_organisation(): + interface = TrusteeInterface(user) + org = interface.createOrganisation(...) + assert org.id is not None + +# Test permission checking +def test_check_trustee_permission(): + assert checkTrusteePermission(user, orgId, "read", "contract") == True +``` + +### Integrations-Tests + +```python +# Test API endpoints +def test_create_organisation_api(): + response = client.post("/api/trustee/organisations/", json={...}) + assert response.status_code == 201 + +# Test RBAC filtering +def test_organisation_list_filtered_by_rbac(): + # User should only see organisations they have access to + response = client.get("/api/trustee/organisations/") + assert all(org in allowed_orgs for org in response.json()) +``` + +### RBAC-Test-Szenarien + +1. **Sysadmin**: Kann auf alle Organisationen zugreifen +2. **Admin (Gruppe)**: Kann auf Organisationen in ihrer Gruppe zugreifen +3. **Admin (Trustee)**: Kann auf Organisationen zugreifen, denen sie zugewiesen sind +4. **Operate**: Kann CRUD für Verträge/Dokumente/Positionen in zugewiesenen Organisationen +5. **Userreport**: Kann nur eigene Datensätze CRUD +6. **Kein Zugriff**: Kann nicht auf Trustee Feature zugreifen + +--- + +## Implementierungsdetails + +### DatabaseConnector - Automatische Tabellenerstellung + +Der `DatabaseConnector` erstellt Tabellen automatisch aus Pydantic-Modellen: + +#### Prozess + +1. **Tabellen-Erstellung**: `_ensureTableExists(model_class)` wird bei jedem Datenbankzugriff aufgerufen +2. **Spalten-Generierung**: Spalten werden aus Modell-Feldern generiert: + - `id`: VARCHAR(255) PRIMARY KEY + - Andere Felder: Automatische Typ-Konvertierung (str → TEXT, int → INTEGER, float → DOUBLE PRECISION, bool → BOOLEAN, date → DATE, datetime → TIMESTAMP, bytes → BYTEA) + - Systemattribute: Automatisch hinzugefügt (`_createdAt`, `_modifiedAt`, `_createdBy`, `_modifiedBy`) +3. **Index-Erstellung**: Automatische Indizes für Foreign Keys (Felder die auf `Id` enden) +4. **Migration**: Fehlende Spalten werden automatisch hinzugefügt (additive Migration, keine Spalten-Löschung) + +#### Beispiel Trustee-Modell + +```python +class TrusteeOrganisation(BaseModel): + id: str = Field(...) # VARCHAR(255) PRIMARY KEY + label: str = Field(...) # TEXT + enabled: bool = Field(...) # BOOLEAN + mandate: str = Field(...) # TEXT + # Systemattribute werden automatisch hinzugefügt: + # _createdAt: DOUBLE PRECISION + # _modifiedAt: DOUBLE PRECISION + # _createdBy: VARCHAR(255) + # _modifiedBy: VARCHAR(255) +``` + +**Ergebnis**: Tabelle `TrusteeOrganisation` wird automatisch erstellt mit allen Spalten. + +### RBAC-Filterung auf DB-Ebene + +Das RBAC-System filtert Daten automatisch auf Datenbank-Ebene: + +#### Funktion: `getRecordsetWithRBAC()` + +```python +records = getRecordsetWithRBAC( + connector=db, + modelClass=TrusteeOrganisation, + currentUser=user, + recordFilter={"enabled": True} # Zusätzliche Filter +) +``` + +#### Filterung basierend auf AccessLevel + +1. **View-Permission prüfen**: Wenn `permissions.view = false`, werden keine Records zurückgegeben +2. **Read-Level Filterung**: + - `ALL`: Keine WHERE-Clause (alle Records) + - `MY`: `WHERE "_createdBy" = %s` (nur eigene Records) + - `GROUP`: `WHERE "mandateId" = %s` (nur Records der eigenen Gruppe) + - `NONE`: `WHERE 1 = 0` (keine Records) + +#### Trustee-spezifische Filterung + +Für Trustee-Feature müssen zusätzliche Filter implementiert werden: + +1. **Organisation-Filterung**: Basierend auf `trustee.access` Tabelle + - User mit `admin` Rolle: Nur Records mit `organisationId` aus eigenen Zugriffen + - User mit `operate` Rolle: Nur Records mit `organisationId` aus eigenen Zugriffen + - User mit `userreport` Rolle: Nur eigene Records (`_createdBy = userId`) + +2. **Contract-Filterung**: Basierend auf `contractId` in `trustee.access` Tabelle + - **Ohne `contractId` (None)**: User hat Zugriff auf alle Contracts der Organisation + - **Mit `contractId`**: User hat Zugriff nur auf diesen spezifischen Contract + - **Filter-Logik**: + - Wenn User Access ohne `contractId` hat → Zugriff auf alle Contracts der Organisation + - Wenn User Access mit spezifischem `contractId` hat → Zugriff nur auf diesen Contract + - Wenn User beide hat → Zugriff auf alle Contracts (weil Access ohne contractId übergeordnet ist) + - **Anwendung**: Bei Queries für `contract`, `document`, `position` wird zusätzlich nach `contractId` gefiltert: + - Records mit `contractId` werden nur angezeigt wenn User Access für diesen Contract hat (oder Access ohne contractId) + - Records ohne `contractId` werden angezeigt wenn User Access ohne contractId hat + +**Implementierung**: +- **Zwei-Stufen-Filterung**: Zuerst System-RBAC (`getRecordsetWithRBAC()`), dann zusätzliche Trustee-Filterung in der Interface-Schicht +- Diese Filterung muss in der Trustee-Interface-Schicht implementiert werden, da sie feature-spezifisch ist +- **Wichtig**: Für das UI ist diese Logik nicht relevant, da RBAC automatisch alle Daten korrekt gefiltert liefert + +### FormGenerator - Automatische UI-Generierung + +**FormGenerator** bietet folgende automatische Features: + +#### Tabellen-Features (automatisch) +- ✅ Auto-Spalten-Erkennung (wenn keine `columns` übergeben) +- ✅ Sortierung (Klick auf Header: asc → desc → keine) +- ✅ Filter (Text, Boolean, Enum, Date) +- ✅ Pagination (konfigurierbare `pageSize`) +- ✅ Spalten-Resize (Drag & Drop) +- ✅ Row Selection (Checkboxen) +- ✅ Custom Actions (Buttons pro Zeile) +- ✅ Custom Formatter (pro Spalte) +- ✅ Loading State + +#### Formular-Features (automatisch) +- ✅ Feld-Typen basierend auf `frontend_type` +- ✅ Validierung basierend auf `frontend_required` +- ✅ Readonly-Felder basierend auf `frontend_readonly` +- ✅ Select-Optionen aus `frontend_options` +- ✅ Floating Labels + +#### Custom Logic (manuell implementieren) +- 🔧 Custom Validierungen (z.B. MwSt-Berechnung) +- 🔧 Custom Formatter (z.B. Links zu verknüpften Records) +- 🔧 Custom Actions (z.B. Download-Button) +- 🔧 Custom Business Logic + +## Implementierungsdetails + +### Dokumentenspeicherung + +**Entscheidung**: Dokument-Binärdaten werden in PostgreSQL BYTEA-Spalte gespeichert. +- Einfach, transaktional, einfaches Backup +- Für Phase 1 ausreichend + +### Organisation ID Format + +**Entscheidung**: String-Label (z.B. "acme-corp") +- Menschenlesbar, benutzerfreundlich +- Validierung: Alphanumerisch, Bindestriche, Unterstriche +- Längenbegrenzung: 3-50 Zeichen +- Groß-/Kleinschreibung-unabhängige Eindeutigkeitsprüfung + +### Rollen-Management + +**Entscheidung**: Rollen sind bearbeitbar +- Rollen werden in `trustee.role` Tabelle gespeichert +- Initiale Rollen werden beim Bootstrap erstellt +- Rollen können bearbeitet werden, aber nicht gelöscht werden, wenn sie in Verwendung sind + +### Access-Record Eindeutigkeit + +**Entscheidung**: Mehrere Access-Records erlaubt +- Ein Benutzer kann mehrere Rollen für dieselbe Organisation haben +- Unique Constraint auf `(organisationId, roleId, userId)` verhindert Duplikate +- Separate Records für jede Rollenkombination + +### Vertrags-Organisations-Beziehung + +**Entscheidung**: Verträge sind unveränderlich +- `contract.organisationId` kann nach der Erstellung NICHT mehr geändert werden +- Datenintegrität und Audit-Trail werden dadurch gewährleistet + +### Position-Dokument-Beziehung + +**Entscheidung**: Verknüpfungen sind optional +- Positionen können ohne Dokumente existieren +- Dokumente können ohne Positionen existieren +- Viele-zu-viele-Beziehung über `xpositiondocument` Tabelle + +### Währungsumrechnung + +**Entscheidung**: Phase 1 - Manuelle Eingabe, keine automatische Umrechnung +- Beide Währungen werden gespeichert: `bookingCurrency/bookingAmount` und `originalCurrency/originalAmount` +- Manuelle Eingabe durch Benutzer +- Wechselkurstabelle und automatische Umrechnung können in Phase 2 hinzugefügt werden + +### MwSt-Berechnung + +**Entscheidung**: Automatische Berechnung mit manueller Überschreibung +- `vatAmount = bookingAmount * vatPercentage / 100` wird automatisch berechnet +- Manuelle Überschreibung erlaubt, falls erforderlich +- Validierungswarnung, wenn Werte nicht übereinstimmen + +--- + +## Nächste Schritte + +1. **Dokument mit Stakeholdern final überprüfen** +2. **Detaillierte technische Spezifikationen** für jede Komponente erstellen +3. **Entwicklungsumgebung** und Projektstruktur einrichten +4. **Phase 1 Implementierung beginnen** + +--- + +## Anhang + +### A. Datenbank-Schema SQL + +```sql +-- Organisation table +CREATE TABLE trustee_organisation ( + id VARCHAR(255) PRIMARY KEY, + label VARCHAR(255) NOT NULL, + enabled BOOLEAN DEFAULT TRUE, + mandate VARCHAR(255) NOT NULL, + _created_at FLOAT NOT NULL, -- Systemattribut + _modified_at FLOAT NOT NULL, -- Systemattribut + _created_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt + _modified_by VARCHAR(255) -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt +); + +CREATE INDEX idx_trustee_organisation_mandate ON trustee_organisation(mandate); + +-- Role table (Rollen sind bearbeitbar) +CREATE TABLE trustee_role ( + id VARCHAR(255) PRIMARY KEY, -- String-Label (z.B. "userreport", "admin", "operate") + desc TEXT NOT NULL, + mandate VARCHAR(255) NOT NULL, + _created_at FLOAT NOT NULL, -- Systemattribut + _modified_at FLOAT NOT NULL, -- Systemattribut + _created_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt + _modified_by VARCHAR(255) -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt +); + +CREATE INDEX idx_trustee_role_mandate ON trustee_role(mandate); + +-- Access table (ein Benutzer kann mehrere Rollen für dieselbe Organisation haben) +CREATE TABLE trustee_access ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organisation_id VARCHAR(255) NOT NULL REFERENCES trustee_organisation(id), + role_id VARCHAR(255) NOT NULL REFERENCES trustee_role(id), + user_id VARCHAR(255) NOT NULL, + mandate VARCHAR(255) NOT NULL, + _created_at FLOAT NOT NULL, -- Systemattribut + _modified_at FLOAT NOT NULL, -- Systemattribut + _created_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt + _modified_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt + UNIQUE(organisation_id, role_id, user_id) -- Verhindert Duplikate, erlaubt aber mehrere Rollen pro Benutzer-Organisation +); + +CREATE INDEX idx_trustee_access_organisation ON trustee_access(organisation_id); +CREATE INDEX idx_trustee_access_user ON trustee_access(user_id); +CREATE INDEX idx_trustee_access_role ON trustee_access(role_id); +CREATE INDEX idx_trustee_access_mandate ON trustee_access(mandate); + +-- Contract table (Verträge sind unveränderlich: organisation_id kann nicht geändert werden) +CREATE TABLE trustee_contract ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organisation_id VARCHAR(255) NOT NULL REFERENCES trustee_organisation(id), -- Immutable nach Erstellung + label VARCHAR(255) NOT NULL, + enabled BOOLEAN DEFAULT TRUE, + mandate VARCHAR(255) NOT NULL, + _created_at FLOAT NOT NULL, -- Systemattribut + _modified_at FLOAT NOT NULL, -- Systemattribut + _created_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt + _modified_by VARCHAR(255) -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt +); + +CREATE INDEX idx_trustee_contract_organisation ON trustee_contract(organisation_id); +CREATE INDEX idx_trustee_contract_mandate ON trustee_contract(mandate); + +-- Document table +CREATE TABLE trustee_document ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organisation_id VARCHAR(255) NOT NULL REFERENCES trustee_organisation(id), + contract_id UUID NOT NULL REFERENCES trustee_contract(id), + document_data BYTEA NOT NULL, -- Binärdaten werden direkt in Datenbank gespeichert + document_name VARCHAR(255) NOT NULL, + document_mime_type VARCHAR(100) NOT NULL, + mandate VARCHAR(255) NOT NULL, + created FLOAT NOT NULL, + updated FLOAT NOT NULL, + created_by VARCHAR(255) NOT NULL +); + +CREATE INDEX idx_trustee_document_organisation ON trustee_document(organisation_id); +CREATE INDEX idx_trustee_document_contract ON trustee_document(contract_id); +CREATE INDEX idx_trustee_document_mandate ON trustee_document(mandate); +CREATE INDEX idx_trustee_document_created_by ON trustee_document(_created_by); + +-- Position table +CREATE TABLE trustee_position ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organisation_id VARCHAR(255) NOT NULL REFERENCES trustee_organisation(id), + contract_id UUID NOT NULL REFERENCES trustee_contract(id), + valuta DATE NOT NULL, + transaction_date_time TIMESTAMP NOT NULL, + company VARCHAR(255), + desc TEXT, + tags VARCHAR(255), + booking_currency VARCHAR(10) NOT NULL, + booking_amount FLOAT NOT NULL, + original_currency VARCHAR(10) NOT NULL, + original_amount FLOAT NOT NULL, + vat_percentage FLOAT DEFAULT 0.0, + vat_amount FLOAT DEFAULT 0.0, + mandate VARCHAR(255) NOT NULL, + created FLOAT NOT NULL, + updated FLOAT NOT NULL, + created_by VARCHAR(255) NOT NULL +); + +CREATE INDEX idx_trustee_position_organisation ON trustee_position(organisation_id); +CREATE INDEX idx_trustee_position_contract ON trustee_position(contract_id); +CREATE INDEX idx_trustee_position_valuta ON trustee_position(valuta); +CREATE INDEX idx_trustee_position_transaction_date_time ON trustee_position(transaction_date_time); +CREATE INDEX idx_trustee_position_mandate ON trustee_position(mandate); +CREATE INDEX idx_trustee_position_created_by ON trustee_position(_created_by); -- Für userreport Filterung + +-- Position-Document cross-reference table (Tabellenname in Kleinbuchstaben: xpositiondocument) +CREATE TABLE trustee_xpositiondocument ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + organisation_id VARCHAR(255) NOT NULL REFERENCES trustee_organisation(id), + contract_id UUID NOT NULL REFERENCES trustee_contract(id), + document_id UUID NOT NULL REFERENCES trustee_document(id), + position_id UUID NOT NULL REFERENCES trustee_position(id), + mandate VARCHAR(255) NOT NULL, + _created_at FLOAT NOT NULL, -- Systemattribut + _modified_at FLOAT NOT NULL, -- Systemattribut + _created_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt + _modified_by VARCHAR(255), -- Wird automatisch über Systemattribute vom DatabaseConnector gesetzt + UNIQUE(position_id, document_id) -- Verknüpfungen sind optional: Positionen/Dokumente können unabhängig existieren +); + +CREATE INDEX idx_trustee_xpd_organisation ON trustee_xpositiondocument(organisation_id); +CREATE INDEX idx_trustee_xpd_contract ON trustee_xpositiondocument(contract_id); +CREATE INDEX idx_trustee_xpd_document ON trustee_xpositiondocument(document_id); +CREATE INDEX idx_trustee_xpd_position ON trustee_xpositiondocument(position_id); +CREATE INDEX idx_trustee_xpd_mandate ON trustee_xpositiondocument(mandate); +CREATE INDEX idx_trustee_xpd_created_by ON trustee_xpositiondocument(_created_by); -- Für userreport Filterung +``` + +### B. Initial Roles Bootstrap + +**Implementierung**: Bootstrap-Script erstellt initiale Rollen automatisch beim ersten Start des Trustee-Features (ähnlich wie `initBootstrap()` für andere System-Tabellen). + +```python +# Bootstrap script to create initial roles +initial_roles = [ + { + "id": "userreport", + "desc": "Can deliver user documents to the system" + }, + { + "id": "admin", + "desc": "Can administrate the access" + }, + { + "id": "operate", + "desc": "Can use data for operations" + } +] +``` + +### C. RBAC Access Rules Bootstrap + +```python +# Bootstrap script to create initial AccessRules +initial_access_rules = [ + # Feature access + { + "roleLabel": "sysadmin", + "context": "RESOURCE", + "item": "trustee", + "view": True + }, + # UI access + { + "roleLabel": "sysadmin", + "context": "UI", + "item": "trustee", + "view": True + }, + # Table access rules for each table... +] +``` + +--- + +**Dokumentversion**: 1.0 +**Letzte Aktualisierung**: 2025-01-03 +**Autor**: Architektur-Review +**Status**: Ausstehende Überprüfung diff --git a/ui_nyla/feature-trustee/doc_trustee_feature_ui_demo.html b/ui_nyla/feature-trustee/doc_trustee_feature_ui_demo.html new file mode 100644 index 0000000..e2a65fc --- /dev/null +++ b/ui_nyla/feature-trustee/doc_trustee_feature_ui_demo.html @@ -0,0 +1,1339 @@ + + + + + + Trustee Feature - UI Demo + + + +
+
+

🏢 Trustee Feature - UI Demo

+

Demonstration der UI-Komponenten basierend auf FormGenerator-Pattern

+
+ +
+ Hinweis: Dies ist eine statische Demo zur Visualisierung der UI-Struktur. + Die tatsächliche Implementierung verwendet React/TypeScript mit FormGenerator-Komponente. +
+ +
+ + + + + + + +
+ + +
+
+

Organisationen

+ +
+ +
+ RBAC: sysadmin kann alle verwalten, admin kann für Gruppe verwalten
+ Komponente: FormGenerator mit Logic-Hook +
+ +
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDLabelEnabledMandateAktionen
acme-corpACME CorporationJamandate-001 + + +
tech-solutionsTech Solutions AGJamandate-001 + + +
global-trustGlobal Trust Ltd.Neinmandate-002 + + +
+
+ + +
+ + +
+
+

Rollen

+ +
+ +
+ RBAC: Nur sysadmin kann Rollen verwalten
+ Initiale Rollen: userreport, admin, operate (werden automatisch erstellt) +
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
IDBeschreibungAktionen
userreportKann Benutzerdokumente an das System liefern + + +
adminKann den Zugriff administrieren + + +
operateKann Daten für Operationen verwenden + + +
+
+
+ + +
+
+

Access

+ +
+ +
+ RBAC: sysadmin kann alle verwalten, admin kann für Gruppe verwalten
+ Besonderheit: Dropdowns zeigen nur erlaubte Optionen (RBAC-gefiltert)
+ Contract-Zugriff: contractId ist optional - wenn leer, Zugriff auf gesamte Organisation; wenn gesetzt, nur auf diesen Contract +
+ +
+ +
+ + + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDOrganisationRolleUser IDContractAktionen
a1b2c3d4-...ACME Corporationadminuser-123Gesamte Organisation + + +
e5f6g7h8-...Tech Solutions AGoperateuser-456Gesamte Organisation + + +
i9j0k1l2-...ACME Corporationuserreportuser-789Muster AG 2026 + + +
m3n4o5p6-...ACME Corporationoperateuser-999Muster AG 2026 + + +
+
+
+ + +
+
+

Contracts

+ +
+ +
+ RBAC: sysadmin, admin (für Gruppe), trustee.admin (für zugewiesene Organisationen)
+ Besonderheit: organisationId ist immutable nach Erstellung (readonly wenn id vorhanden) +
+ +
+ +
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
IDOrganisationLabelEnabledAktionen
c1d2e3f4-...ACME CorporationMuster AG 2026Ja + + +
g5h6i7j8-...Tech Solutions AGVertrag 2025Ja + + +
+
+
+ + +
+
+

Documents

+ +
+ +
+ RBAC: sysadmin, admin (für Gruppe), trustee.operate (für Organisationen), trustee.userreport (eigene Records)
+ Besonderheit: File Upload/Download erfolgt über Workflow-System, nicht direkt integriert +
+ +
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDOrganisationContractDocument NameMIME TypeVerknüpfte PositionenAktionen
d1e2f3a4-...ACME CorporationMuster AG 2026Beleg_2026_01.pdfapplication/pdf2 Positionen + + + +
b5c6d7e8-...Tech Solutions AGVertrag 2025Rechnung_2025_12.pdfapplication/pdf1 Position + + + +
+
+
+ + +
+
+

Positions

+ +
+ +
+ RBAC: sysadmin, admin (für Gruppe), trustee.operate (für Organisationen), trustee.userreport (eigene Records)
+ Besonderheit: MwSt-Berechnung automatisch (vatAmount = bookingAmount * vatPercentage / 100) +
+ +
+ +
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDOrganisationContractValutaCompanyBooking AmountMwSt %MwSt BetragVerknüpfte DocumentsAktionen
p1q2r3s4-...ACME CorporationMuster AG 20262026-01-15Hotel ABC150.00 CHF7.7%11.55 CHF2 Documents + + +
t5u6v7w8-...Tech Solutions AGVertrag 20252025-12-20Restaurant XYZ85.50 EUR19.0%16.25 EUR1 Document + + +
+
+
+ + +
+
+

Position-Document Verknüpfungen

+ +
+ +
+ RBAC: sysadmin, admin (für Gruppe), trustee.operate (für Organisationen), trustee.userreport (eigene Records)
+ Besonderheit: Verknüpfungen sind optional - Positionen/Dokumente können unabhängig existieren +
+ +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
IDOrganisationPosition IDDocument IDAktionen
pd1-...ACME Corporationp1q2r3s4-...d1e2f3a4-... + +
pd2-...ACME Corporationp1q2r3s4-...b5c6d7e8-... + +
+
+
+
+ + + + + + + + + + diff --git a/ui_nyla/feature-trustee/doc_trustee_feature_ui_specification.md b/ui_nyla/feature-trustee/doc_trustee_feature_ui_specification.md new file mode 100644 index 0000000..eba70a8 --- /dev/null +++ b/ui_nyla/feature-trustee/doc_trustee_feature_ui_specification.md @@ -0,0 +1,1749 @@ +# Trustee Feature - UI-Spezifikation + +## Übersicht + +Dieses Dokument beschreibt die **UI-Architektur und Frontend-Implementierung** für das Trustee Feature. Die UI basiert auf dem **FormGenerator-Pattern** (React/TypeScript) für CRUD-Operationen und verwendet RBAC für die Zugriffskontrolle. + +**Fokus**: Dieses Dokument konzentriert sich ausschließlich auf **Frontend-Komponenten** und **UI-Implementierung**. Backend-Implementierungsdetails (Datenmodelle, API-Routen, DatabaseConnector, RBAC-Filterung) finden sich im [Architektur-Dokument](./doc_trustee_feature_architecture.md). + +**UI-Demo**: Eine interaktive HTML-Demo zur Visualisierung der UI-Struktur findet sich in [`doc_trustee_feature_ui_demo.html`](./doc_trustee_feature_ui_demo.html). + +**Frontend-Stack**: React 19, TypeScript, Vite +**Pattern**: +- **Logic-Hook** (`use*Logic.tsx`) - Handhabt Daten laden, CRUD, State-Management, Column/Action/Field-Konfigurationen +- **Table-Komponente** (`*Table.tsx`) - Verwendet Logic-Hook, rendert FormGenerator + Edit-Popup +- **FormGenerator** - Reine Präsentationskomponente für Tabellen (Filter, Sort, Pagination, Suche) + +### Wichtige Erkenntnisse + +1. **FormGenerator ist eine reine Präsentationskomponente**: FormGenerator rendert nur die Tabelle mit Filter, Sort, Pagination, Suche +2. **Logic-Hooks handhaben Geschäftslogik**: + - Daten laden (via `use*` Hooks wie `usePrompts`, `useWorkflows`) + - CRUD-Operationen + - State-Management (loading, error, editModalOpen, etc.) + - Column-Konfigurationen + - Actions-Konfigurationen + - Edit-Field-Konfigurationen +3. **Table-Komponenten wrappen FormGenerator**: + - Verwenden Logic-Hook + - Übergeben Props an FormGenerator + - Rendern Edit-Popup separat +4. **Backend-Metadaten**: Feld-Konfigurationen können vom Backend über `/api/attributes/{entityType}` geladen werden +5. **Minimaler Code**: Nur Custom Logic muss implementiert werden (MwSt-Berechnung, Referenzen, Validierungen) +6. **Konsistenz**: Pattern sorgt für konsistente UI/UX über alle Views hinweg + +### Automatische Generierung + +**Wichtig**: Das System verwendet ein automatisches Generierungs-Pattern: + +1. **Backend-Datenmodelle** definieren Metadaten (`json_schema_extra`) für jedes Feld +2. **API-Endpunkt** `/api/attributes/{entityType}` liefert Feld-Konfigurationen +3. **FormGenerator** generiert automatisch: + - Tabellen-Spalten (wenn keine `columns` übergeben) + - Formular-Felder basierend auf Backend-Attributen + - Feld-Typen, Validierung, Readonly-Status +4. **Nur Custom Logic** muss manuell implementiert werden: + - Custom Validierungen (z.B. MwSt-Berechnung) + - Custom Formatter (z.B. Links, Datum-Formatierung) + - Custom Actions (z.B. Download-Button) + - Custom Business Logic + +### FormGenerator Features (automatisch verfügbar) + +**FormGenerator** bietet folgende Features automatisch: + +#### Tabellen-Features +- ✅ **Auto-Spalten-Erkennung**: Wenn keine `columns` übergeben werden, erkennt FormGenerator automatisch Spalten aus den Daten +- ✅ **Sortierung**: Klick auf Spalten-Header sortiert (asc → desc → keine Sortierung) +- ✅ **Filter**: + - Text-Filter: Freitext-Suche über alle `searchable` Spalten + - Spalten-Filter: Pro Spalte individuell filterbar (Text, Boolean, Enum, Date) + - Boolean-Filter: Dropdown (Ja/Nein) + - Enum-Filter: Dropdown mit `filterOptions` + - Date-Filter: Automatische Formatierung DD.MM.YYYY mit Validierung +- ✅ **Pagination**: Automatische Seitenaufteilung mit konfigurierbarer `pageSize` (10, 25, 50, 100) +- ✅ **Spalten-Resize**: Spaltenbreiten können per Drag & Drop angepasst werden +- ✅ **Row Selection**: Checkboxen für Einzel- und Mehrfachauswahl +- ✅ **Custom Actions**: Buttons pro Zeile über `actions` Prop (z.B. Edit, Delete, Download) +- ✅ **Custom Formatter**: Pro Spalte `formatter` Funktion für spezielle Darstellung +- ✅ **Loading State**: Automatische Loading-Anzeige während Daten geladen werden + +#### Formular-Features (EditForm) +- ✅ **Feld-Typen**: Automatisch basierend auf `frontend_type` (text, select, checkbox, textarea, email, date, timestamp, number) +- ✅ **Validierung**: Automatisch basierend auf `frontend_required` +- ✅ **Readonly-Felder**: Automatisch basierend auf `frontend_readonly` +- ✅ **Select-Optionen**: Automatisch aus `frontend_options` (Array oder String-Referenz) +- ✅ **Floating Labels**: Automatische Label-Animation bei Fokus/Eingabe + +## Inhaltsverzeichnis + +1. [UI-Architektur](#ui-architektur) +2. [Container und Views](#container-und-views) +3. [RBAC-Integration](#rbac-integration) +4. [Routen-Übersicht](#routen-übersicht) +5. [UI-Requirements](#ui-requirements) +6. [Implementierungsstruktur](#implementierungsstruktur) +7. [Entscheidungen](#entscheidungen) +8. [Implementierungsanleitung](#implementierungsanleitung) +9. [Implementierungs-Checkliste](#implementierungs-checkliste) + +--- + +## UI-Architektur + +### Container-Struktur + +``` +Container "Treuhand" +├── View: Organisationen (trustee.organisation) +├── View: Rollen (trustee.role) +├── View: Access (trustee.access) +├── View: Contracts (trustee.contract) +├── View: Documents (trustee.document) +└── View: Positions (trustee.position) +``` + +**Wichtig**: Container und Views sind nur sichtbar, wenn der User `view=true` Zugriff auf die entsprechenden RBAC-Objekte hat. + +### Sichtbarkeitsregeln + +- **Container "Treuhand"**: Sichtbar wenn `ui.trustee.view = true` +- **View Organisationen**: Sichtbar wenn `ui.trustee.organisation.view = true` (oder `ui.trustee.view = true` mit cascading) +- **View Rollen**: Sichtbar wenn `ui.trustee.role.view = true` +- **View Access**: Sichtbar wenn `ui.trustee.access.view = true` +- **View Contracts**: Sichtbar wenn `ui.trustee.contract.view = true` +- **View Documents**: Sichtbar wenn `ui.trustee.document.view = true` +- **View Positions**: Sichtbar wenn `ui.trustee.position.view = true` + +**Hinweis**: RBAC-Logik ist für das UI nicht relevant (nur als Kontext-Info), da RBAC automatisch alle Daten korrekt gefiltert liefert. Das Backend filtert Daten basierend auf: +- System-RBAC (mandateId, _createdBy) +- Trustee-spezifische RBAC (organisationId aus trustee.access) +- Contract-basierte RBAC (optional contractId aus trustee.access) + - Wenn `contractId` in trustee.access leer: Zugriff auf alle Contracts der Organisation + - Wenn `contractId` gesetzt: Zugriff nur auf diesen spezifischen Contract +Die Filterung erfolgt automatisch auf DB-Ebene, das UI erhält nur die erlaubten Daten. + +--- + +## Container und Views + +### 1. View: Organisationen (`trustee.organisation`) + +**Komponente**: FormGeneric +**Tabelle**: `trustee.organisation` +**RBAC-Objekt**: `ui.trustee.organisation` + +#### Felder +- `id` (String-Label, PK, required, readonly nach Erstellung) + - **Format**: Alphanumerisch + Bindestrich/Unterstrich + - **Länge**: 3-50 Zeichen + - **Validierung**: Backend + Frontend +- `label` (String, required) +- `enabled` (Boolean, default: true) +- Systemattribute: `mandate`, `_createdAt`, `_modifiedAt`, `_createdBy`, `_modifiedBy` + - **Verwaltung**: Automatisch vom DatabaseConnector gesetzt + - **Anzeige**: Readonly im Frontend + - **Formatierung**: Timestamps als float (UI rendert gemäß Zeitzoneneinstellungen), User-Namen statt User-ID + - **Sichtbarkeit**: Kann in FormGeneric definiert werden, welche Felder angezeigt werden + +#### CRUD-Operationen +- **Create**: Erlaubt wenn `create` Berechtigung vorhanden +- **Read**: Gefiltert nach RBAC (sysadmin: alle, admin: eigene Gruppe) +- **Update**: Erlaubt wenn `update` Berechtigung vorhanden +- **Delete**: Erlaubt wenn `delete` Berechtigung vorhanden + +#### API-Routen +Siehe [Architektur-Dokument](./doc_trustee_feature_architecture.md) für vollständige API-Dokumentation. + +**Wichtig**: Backend filtert automatisch basierend auf RBAC. Frontend erhält nur erlaubte Daten. + +--- + +### 2. View: Rollen (`trustee.role`) + +**Komponente**: FormGeneric +**Tabelle**: `trustee.role` +**RBAC-Objekt**: `ui.trustee.role` + +#### Felder +- `id` (String-Label, PK, required) +- `desc` (String, required) +- Systemattribute: `mandate`, `_createdAt`, `_modifiedAt`, `_createdBy`, `_modifiedBy` + +#### CRUD-Operationen +- **Create**: Nur sysadmin +- **Read**: Nur sysadmin +- **Update**: Nur sysadmin +- **Delete**: Nur sysadmin (nicht erlaubt wenn Rolle in Verwendung) + +#### API-Routen +Siehe [Architektur-Dokument](./doc_trustee_feature_architecture.md) für vollständige API-Dokumentation. + +--- + +### 3. View: Access (`trustee.access`) + +**Komponente**: FormGeneric +**Tabelle**: `trustee.access` +**RBAC-Objekt**: `ui.trustee.access` + +#### Felder +- `id` (UUID, PK) +- `organisationId` (String, FK zu `trustee.organisation.id`, required) +- `roleId` (String, FK zu `trustee.role.id`, required) +- `userId` (String, required) +- `contractId` (UUID, FK zu `trustee.contract.id`, **optional**) + - **Wenn leer/None**: Zugriff gilt für die gesamte Organisation + - **Wenn gesetzt**: Zugriff ist auf diesen spezifischen Contract beschränkt +- Systemattribute: `mandate`, `_createdAt`, `_modifiedAt`, `_createdBy`, `_modifiedBy` + +#### CRUD-Operationen +- **Create**: sysadmin, admin (für eigene Gruppe) +- **Read**: Gefiltert nach RBAC +- **Update**: sysadmin, admin (für eigene Gruppe) +- **Delete**: sysadmin, admin (für eigene Gruppe) + +#### Besonderheiten +- Dropdown für `organisationId`: Nur Organisationen anzeigen, auf die User Zugriff hat +- Dropdown für `roleId`: Alle verfügbaren Rollen anzeigen +- Dropdown für `userId`: Nur Benutzer aus eigener Mandate/Gruppe +- Dropdown für `contractId`: + - **Optional**: Kann leer bleiben (Zugriff auf gesamte Organisation) + - **Dynamisch gefiltert**: Zeigt nur Contracts der ausgewählten `organisationId` + - **Abhängigkeit**: Wird aktualisiert wenn `organisationId` geändert wird + - **Placeholder**: "Alle Contracts (gesamte Organisation)" oder leer lassen für Organisation-Zugriff + +#### API-Routen +Siehe [Architektur-Dokument](./doc_trustee_feature_architecture.md) für vollständige API-Dokumentation. + +--- + +### 4. View: Contracts (`trustee.contract`) + +**Komponente**: FormGeneric +**Tabelle**: `trustee.contract` +**RBAC-Objekt**: `ui.trustee.contract` + +#### Felder +- `id` (UUID, PK) +- `organisationId` (String, FK zu `trustee.organisation.id`, required, **immutable**) +- `label` (String, required) +- `enabled` (Boolean, default: true) +- Systemattribute: `mandate`, `_createdAt`, `_modifiedAt`, `_createdBy`, `_modifiedBy` + +#### CRUD-Operationen +- **Create**: sysadmin, admin (für eigene Gruppe), trustee.admin (für zugewiesene Organisationen) +- **Read**: Gefiltert nach RBAC +- **Update**: sysadmin, admin (für eigene Gruppe), trustee.admin (für zugewiesene Organisationen) +- **Delete**: sysadmin, admin (für eigene Gruppe), trustee.admin (für zugewiesene Organisationen) + +#### Besonderheiten +- `organisationId` kann **NICHT** nach Erstellung geändert werden (immutable) + - **Backend-Validierung**: Prüft bei Update, ob `organisationId` geändert wurde → Fehler + - **Frontend-Readonly**: `organisationId` wird auf readonly gesetzt wenn `id` vorhanden (non-blank) ist + - **Logik**: ID kann nur gespeichert werden, wenn non-blank. Eine non-blank ID kann nicht mehr geändert werden +- Dropdown für `organisationId`: + - **Backend-Filterung**: API-Route `/api/trustee/organisations/` filtert automatisch basierend auf RBAC + - Frontend erhält nur die erlaubten Options + - `contractId` Dropdown wird dynamisch aktualisiert wenn `organisationId` geändert wird + - `contractId` Dropdown ist leer wenn keine `organisationId` ausgewählt +- Neue Records: Default auf eigene `organisationId` (falls User nur eine hat) + +#### API-Routen +Siehe [Architektur-Dokument](./doc_trustee_feature_architecture.md) für vollständige API-Dokumentation. + +--- + +### 5. View: Documents (`trustee.document`) + +**Komponente**: FormGeneric +**Tabelle**: `trustee.document` +**RBAC-Objekt**: `ui.trustee.document` + +#### Felder +- `id` (UUID, PK) +- `organisationId` (String, FK zu `trustee.organisation.id`, required) +- `contractId` (UUID, FK zu `trustee.contract.id`, required) +- `documentData` (BYTEA, binary, required) +- `documentName` (String, required) +- `documentMimeType` (String, required) +- Systemattribute: `mandate`, `_createdAt`, `_modifiedAt`, `_createdBy`, `_modifiedBy` + +#### CRUD-Operationen +- **Create**: sysadmin, admin (für eigene Gruppe), trustee.operate (für zugewiesene Organisationen), trustee.userreport (eigene Records) +- **Read**: Gefiltert nach RBAC +- **Update**: sysadmin, admin (für eigene Gruppe), trustee.operate (für zugewiesene Organisationen), trustee.userreport (eigene Records) +- **Delete**: sysadmin, admin (für eigene Gruppe), trustee.operate (für zugewiesene Organisationen), trustee.userreport (eigene Records) + +#### Besonderheiten +- **File Upload/Download**: + - **Nicht direkt integriert**: File Upload/Download erfolgt über das Workflow-System mit einer Action + - Die Action erstellt automatisch die Datensätze in `trustee.document` + - Dies ist nicht Teil der direkten Trustee-Feature-Implementierung +- **Referenzen zu Positionen**: + - Anzeige verknüpfter Positionen (über `xpositiondocument` Tabelle) + - **Bidirektionale Verknüpfungen**: Separate View + Inline-Verwaltung möglich + - Separate View (`PositionDocumentsTable`) für explizite Verwaltung + - Inline in Position/Document Views: Multi-Select für Verknüpfungen +- Filterung nach `contractId` und `organisationId` + +#### API-Routen +Siehe [Architektur-Dokument](./doc_trustee_feature_architecture.md) für vollständige API-Dokumentation. + +--- + +### 6. View: Positions (`trustee.position`) + +**Komponente**: FormGeneric +**Tabelle**: `trustee.position` +**RBAC-Objekt**: `ui.trustee.position` + +#### Felder +- `id` (UUID, PK) +- `organisationId` (String, FK zu `trustee.organisation.id`, required) +- `contractId` (UUID, FK zu `trustee.contract.id`, required) +- `valuta` (Date, required) +- `transactionDateTime` (Timestamp, required) +- `company` (String) +- `desc` (String) +- `tags` (String) +- `bookingCurrency` (String, required) +- `bookingAmount` (Float, required) +- `originalCurrency` (String, required) +- `originalAmount` (Float, required) +- `vatPercentage` (Float, default: 0.0) +- `vatAmount` (Float, default: 0.0) - **wird automatisch berechnet** +- Systemattribute: `mandate`, `_createdAt`, `_modifiedAt`, `_createdBy`, `_modifiedBy` + +#### MwSt-Berechnung (Custom Logic) +- **Automatische Berechnung**: `vatAmount = bookingAmount * vatPercentage / 100` +- **Trigger**: Automatisch beim Ändern von `bookingAmount` oder `vatPercentage` +- **Manuelle Überschreibung**: `vatAmount` kann manuell geändert werden +- **Re-Berechnung**: Wenn `vatAmount` manuell geändert wird, wird automatische Berechnung erneut durchgeführt +- **Warnung**: Toast-Warnung erscheint bereits beim Ändern (nicht erst beim Speichern) + +#### CRUD-Operationen +- **Create**: sysadmin, admin (für eigene Gruppe), trustee.operate (für zugewiesene Organisationen), trustee.userreport (eigene Records) +- **Read**: Gefiltert nach RBAC +- **Update**: sysadmin, admin (für eigene Gruppe), trustee.operate (für zugewiesene Organisationen), trustee.userreport (eigene Records) +- **Delete**: sysadmin, admin (für eigene Gruppe), trustee.operate (für zugewiesene Organisationen), trustee.userreport (eigene Records) + +#### Besonderheiten +- **MwSt-Berechnung**: `vatAmount` wird automatisch berechnet: `bookingAmount * vatPercentage / 100` +- **Manuelle Überschreibung**: `vatAmount` kann manuell überschrieben werden +- **Validierung**: Warnung wenn `vatAmount` nicht mit berechnetem Wert übereinstimmt +- **Referenzen zu Documents**: Anzeige verknüpfter Dokumente (über `xpositiondocument` Tabelle) +- Filterung nach `contractId` und `organisationId` + +#### API-Routen +Siehe [Architektur-Dokument](./doc_trustee_feature_architecture.md) für vollständige API-Dokumentation. + +--- + +### 7. Position-Document Verknüpfungen + +**Komponente**: FormGeneric (optional, kann auch inline in Position/Document Views sein) +**Tabelle**: `trustee.xpositiondocument` +**RBAC-Objekt**: `ui.trustee.xpositiondocument` + +#### Felder +- `id` (UUID, PK) +- `organisationId` (String, FK zu `trustee.organisation.id`, required) +- `contractId` (UUID, FK zu `trustee.contract.id`, required) +- `documentId` (UUID, FK zu `trustee.document.id`, required) +- `positionId` (UUID, FK zu `trustee.position.id`, required) +- Systemattribute: `mandate`, `_createdAt`, `_modifiedAt`, `_createdBy`, `_modifiedBy` + +#### CRUD-Operationen +- **Create**: sysadmin, admin (für eigene Gruppe), trustee.operate (für zugewiesene Organisationen), trustee.userreport (eigene Records) +- **Read**: Gefiltert nach RBAC +- **Delete**: sysadmin, admin (für eigene Gruppe), trustee.operate (für zugewiesene Organisationen), trustee.userreport (eigene Records) + +#### Besonderheiten +- **Verknüpfungen sind optional**: Positionen können ohne Dokumente existieren, Dokumente können ohne Positionen existieren +- **Viele-zu-viele**: Eine Position kann mehrere Dokumente haben, ein Dokument kann zu mehreren Positionen gehören +- **UI-Integration**: Kann als separate View oder inline in Position/Document Views dargestellt werden + +#### API-Routen +Siehe [Architektur-Dokument](./doc_trustee_feature_architecture.md) für vollständige API-Dokumentation. + +--- + +## RBAC-Integration + +### Berechtigungsabfrage beim UI-Start + +Beim Initialisieren des Trustee-Containers: + +1. **Alle Berechtigungen abrufen**: + ```javascript + GET /api/rbac/permissions/all?context=UI + ``` + +2. **Container-Sichtbarkeit prüfen**: + - Container "Treuhand" nur anzeigen wenn `permissions.ui.trustee.view === true` + +3. **View-Sichtbarkeit prüfen**: + - Jede View nur anzeigen wenn entsprechende Berechtigung vorhanden: + - `permissions.ui.trustee.organisation.view` für Organisationen + - `permissions.ui.trustee.role.view` für Rollen + - etc. + +### Dynamische Berechtigungsprüfung + +Für spezifische Aktionen (z.B. Button "Erstellen"): + +```javascript +GET /api/rbac/permissions?context=UI&item=trustee.organisation +// Prüfe permissions.create für Create-Button +``` + +### RBAC-Objekte für Trustee Feature + +| UI-Objekt | RBAC-Item | Beschreibung | +|-----------|-----------|--------------| +| Container Treuhand | `ui.trustee` | Hauptcontainer | +| View Organisationen | `ui.trustee.organisation` | Organisationen-Verwaltung | +| View Rollen | `ui.trustee.role` | Rollen-Verwaltung | +| View Access | `ui.trustee.access` | Zugriffs-Verwaltung | +| View Contracts | `ui.trustee.contract` | Vertrags-Verwaltung | +| View Documents | `ui.trustee.document` | Dokument-Verwaltung | +| View Positions | `ui.trustee.position` | Position-Verwaltung | +| View Position-Document | `ui.trustee.xpositiondocument` | Verknüpfungs-Verwaltung | + +--- + +## Routen-Übersicht + +### RBAC-Routen + +| Route | Methode | Zweck | +|-------|---------|-------| +| `/api/rbac/permissions/all` | GET | Alle UI/RESOURCE-Berechtigungen abrufen (für UI-Initialisierung) | +| `/api/rbac/permissions/all?context=UI` | GET | Nur UI-Berechtigungen | +| `/api/rbac/permissions/all?context=RESOURCE` | GET | Nur RESOURCE-Berechtigungen | +| `/api/rbac/permissions?context=UI&item=trustee` | GET | Spezifische Berechtigung abfragen | + +### Trustee-Routen + +| Resource | Route | Methode | Zweck | +|----------|-------|---------|-------| +| Organisationen | `/api/trustee/organisations/` | GET | Liste (gefiltert) | +| Organisationen | `/api/trustee/organisations/{id}` | GET | Einzelner Eintrag | +| Organisationen | `/api/trustee/organisations/` | POST | Erstellen | +| Organisationen | `/api/trustee/organisations/{id}` | PUT | Aktualisieren | +| Organisationen | `/api/trustee/organisations/{id}` | DELETE | Löschen | +| Rollen | `/api/trustee/roles/` | GET | Liste | +| Rollen | `/api/trustee/roles/{id}` | GET | Einzelner Eintrag | +| Rollen | `/api/trustee/roles/` | POST | Erstellen (sysadmin) | +| Rollen | `/api/trustee/roles/{id}` | PUT | Aktualisieren (sysadmin) | +| Rollen | `/api/trustee/roles/{id}` | DELETE | Löschen (sysadmin) | +| Access | `/api/trustee/access/` | GET | Liste (gefiltert) | +| Access | `/api/trustee/access/{id}` | GET | Einzelner Eintrag | +| Access | `/api/trustee/access/` | POST | Erstellen | +| Access | `/api/trustee/access/{id}` | PUT | Aktualisieren | +| Access | `/api/trustee/access/{id}` | DELETE | Löschen | +| Access | `/api/trustee/access/organisation/{orgId}` | GET | Access für Organisation | +| Access | `/api/trustee/access/user/{userId}` | GET | Access für User | +| Contracts | `/api/trustee/contracts/` | GET | Liste (gefiltert) | +| Contracts | `/api/trustee/contracts/{id}` | GET | Einzelner Eintrag | +| Contracts | `/api/trustee/contracts/` | POST | Erstellen | +| Contracts | `/api/trustee/contracts/{id}` | PUT | Aktualisieren (organisationId immutable) | +| Contracts | `/api/trustee/contracts/{id}` | DELETE | Löschen | +| Contracts | `/api/trustee/contracts/organisation/{orgId}` | GET | Contracts für Organisation | +| Documents | `/api/trustee/documents/` | GET | Liste (gefiltert) | +| Documents | `/api/trustee/documents/{id}` | GET | Metadaten | +| Documents | `/api/trustee/documents/{id}/data` | GET | Binärdaten (Download) | +| Documents | `/api/trustee/documents/` | POST | Erstellen (mit Upload) | +| Documents | `/api/trustee/documents/{id}` | PUT | Metadaten aktualisieren | +| Documents | `/api/trustee/documents/{id}` | DELETE | Löschen | +| Documents | `/api/trustee/documents/contract/{contractId}` | GET | Documents für Contract | +| Documents | `/api/trustee/documents/position/{positionId}` | GET | Documents für Position | +| Positions | `/api/trustee/positions/` | GET | Liste (gefiltert) | +| Positions | `/api/trustee/positions/{id}` | GET | Einzelner Eintrag | +| Positions | `/api/trustee/positions/` | POST | Erstellen | +| Positions | `/api/trustee/positions/{id}` | PUT | Aktualisieren | +| Positions | `/api/trustee/positions/{id}` | DELETE | Löschen | +| Positions | `/api/trustee/positions/contract/{contractId}` | GET | Positions für Contract | +| Positions | `/api/trustee/positions/organisation/{orgId}` | GET | Positions für Organisation | +| Positions | `/api/trustee/positions/document/{documentId}` | GET | Positions für Document | +| Position-Documents | `/api/trustee/position-documents/` | GET | Liste Verknüpfungen | +| Position-Documents | `/api/trustee/position-documents/{id}` | GET | Einzelne Verknüpfung | +| Position-Documents | `/api/trustee/position-documents/` | POST | Verknüpfung erstellen | +| Position-Documents | `/api/trustee/position-documents/{id}` | DELETE | Verknüpfung löschen | +| Position-Documents | `/api/trustee/position-documents/position/{positionId}` | GET | Documents für Position | +| Position-Documents | `/api/trustee/position-documents/document/{documentId}` | GET | Positions für Document | + +--- + +## UI-Requirements + +### Allgemeine Anforderungen + +1. **RBAC-Integration**: + - Container und Views nur anzeigen wenn entsprechende `view` Berechtigung vorhanden + - CRUD-Buttons (Create, Update, Delete) nur anzeigen wenn entsprechende Berechtigung vorhanden + - Read-Operationen nach RBAC filtern + +2. **FormGeneric-Pattern**: + - Alle Views verwenden FormGeneric für CRUD-Operationen + - Konsistente UI/UX über alle Views hinweg + - Automatische Feld-Generierung basierend auf Backend-Schema + +3. **Navigation**: + - **TODO**: Wie sollen die Views organisiert sein? (Tabs, Sidebar-Navigation, etc.) + +4. **Filterung**: + - Filterung nach `organisationId` in allen Views (außer Organisationen selbst) + - Filterung nach `contractId` in Documents und Positions + - Filterung nach User (für `userreport` Rolle) + +### Spezifische Anforderungen + +#### Organisationen-View +- Validierung: `id` muss alphanumerisch, Bindestriche, Unterstriche sein (3-50 Zeichen) +- `id` ist nach Erstellung nicht mehr änderbar + +#### Rollen-View +- Nur sysadmin hat Zugriff +- Löschen nicht erlaubt wenn Rolle in Verwendung (in `trustee.access`) + +#### Access-View +- Dropdowns für `organisationId`, `roleId`, `userId` müssen gefiltert sein +- Unique Constraint: `(organisationId, roleId, userId)` darf nicht doppelt sein + +#### Contracts-View +- `organisationId` ist nach Erstellung **immutable** (nicht änderbar) +- Default `organisationId` auf erste verfügbare Organisation des Users + +#### Documents-View +- **File Upload**: Unterstützung für Datei-Upload (multipart/form-data) +- **File Download**: Link/Button zum Download über `/api/trustee/documents/{id}/data` +- **Referenzen**: Anzeige verknüpfter Positionen (wie genau? Liste, Links, etc.) +- MIME-Type Validierung + +#### Positions-View +- **MwSt-Berechnung**: Automatische Berechnung von `vatAmount` bei Eingabe von `vatPercentage` und `bookingAmount` +- **Manuelle Überschreibung**: `vatAmount` kann manuell überschrieben werden +- **Validierung**: Warnung wenn `vatAmount` nicht mit berechnetem Wert übereinstimmt +- **Referenzen**: Anzeige verknüpfter Dokumente (wie genau? Liste, Links, etc.) +- Datum/Zeit-Formatierung für `valuta` und `transactionDateTime` + +#### Position-Document Verknüpfungen +- **TODO**: Wie sollen Verknüpfungen dargestellt werden? + - Option A: Separate View für Verknüpfungen + - Option B: Inline in Position/Document Views (z.B. Tabs oder Sidebar) + - Option C: Beides möglich + +--- + +## Automatische Generierung durch FormGenerator + +### Was wird automatisch generiert? + +**FormGenerator** und das Backend-Attribut-System generieren automatisch: + +1. **Tabellen-Spalten**: Wenn keine `columns` übergeben werden, erkennt FormGenerator automatisch Spalten aus den Daten +2. **Formular-Felder**: Basierend auf Backend-Attributen (`/api/attributes/{entityType}`) +3. **Feld-Typen**: Automatisch basierend auf `frontend_type` aus Pydantic-Modellen +4. **Validierung**: Basierend auf `frontend_required` und Feld-Typen +5. **Readonly-Felder**: Basierend auf `frontend_readonly` +6. **Select-Optionen**: Basierend auf `frontend_options` (Array oder String-Referenz) + +### Backend-Attribut-System + +Jedes Pydantic-Modell definiert Metadaten in `json_schema_extra`: + +```python +class TrusteeOrganisation(BaseModel): + id: str = Field( + description="Unique organisation identifier", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": True, # Nach Erstellung nicht änderbar + "frontend_required": False + } + ) + label: str = Field( + description="Company name", + json_schema_extra={ + "frontend_type": "text", + "frontend_readonly": False, + "frontend_required": True + } + ) + enabled: bool = Field( + default=True, + json_schema_extra={ + "frontend_type": "checkbox", + "frontend_readonly": False, + "frontend_required": False + } + ) +``` + +**Wichtig**: Für Select-Felder mit Foreign Keys gibt es zwei Optionen: + +1. **String-Referenz** (für dynamische Options): +```python +organisationId: str = Field( + json_schema_extra={ + "frontend_type": "select", + "frontend_options": "trustee.organisation" # String-Referenz + } +) +``` +Das Frontend muss dann die Options dynamisch laden (z.B. über `/api/trustee/organisations/`). + +2. **Array mit statischen Options**: +```python +bookingCurrency: str = Field( + json_schema_extra={ + "frontend_type": "select", + "frontend_options": [ + {"value": "CHF", "label": {"en": "CHF", "fr": "CHF"}}, + {"value": "EUR", "label": {"en": "EUR", "fr": "EUR"}}, + ] + } +) +``` + +**API-Endpunkt**: `GET /api/attributes/{entityType}` gibt diese Metadaten zurück. + +**Beispiel-Response**: +```json +{ + "attributes": [ + { + "name": "id", + "type": "text", + "label": "ID", + "required": false, + "readonly": true, + "editable": false + }, + { + "name": "label", + "type": "text", + "label": "Label", + "required": true, + "readonly": false, + "editable": true + }, + { + "name": "organisationId", + "type": "select", + "label": "Organisation", + "required": true, + "readonly": false, + "editable": true, + "options": "trustee.organisation" // String-Referenz + } + ] +} +``` + +### Was muss manuell implementiert werden? + +Nur **Custom Logic** muss implementiert werden: + +1. **Custom Validierungen**: Z.B. MwSt-Berechnung, Organisation-ID-Validierung +2. **Custom Formatter**: Für spezielle Darstellungen (z.B. Datum-Formatierung, Links) +3. **Custom Actions**: Z.B. Download-Button für Documents, Verknüpfungs-Management +4. **Custom Filterung**: Z.B. Filterung nach Organisation basierend auf RBAC +5. **Custom Business Logic**: Z.B. automatische Berechnung von `vatAmount` + +--- + +## Implementierungsstruktur + +### Frontend-Architektur (React/TypeScript) + +**Wichtig**: FormGenerator handhabt alles automatisch basierend auf einer Konfiguration (wie `formGeneric.js` im alten Frontend)! + +Das Frontend verwendet folgendes Pattern (basierend auf bestehender Codebase): + +``` +src/ +├── components/ +│ └── Trustee/ +│ ├── Organisationen/ +│ │ ├── OrganisationenTable.tsx # Table-Komponente +│ │ ├── organisationenLogic.tsx # Logic-Hook +│ │ ├── organisationenInterfaces.ts +│ │ └── OrganisationenTable.module.css +│ ├── Rollen/ +│ │ ├── RollenTable.tsx +│ │ ├── rollenLogic.tsx +│ │ ├── rollenInterfaces.ts +│ │ └── RollenTable.module.css +│ ├── Access/ +│ │ ├── AccessTable.tsx +│ │ ├── accessLogic.tsx +│ │ ├── accessInterfaces.ts +│ │ └── AccessTable.module.css +│ ├── Contracts/ +│ │ ├── ContractsTable.tsx +│ │ ├── contractsLogic.tsx +│ │ ├── contractsInterfaces.ts +│ │ └── ContractsTable.module.css +│ ├── Documents/ +│ │ ├── DocumentsTable.tsx +│ │ ├── documentsLogic.tsx +│ │ ├── documentsInterfaces.ts +│ │ └── DocumentsTable.module.css +│ ├── Positions/ +│ │ ├── PositionsTable.tsx +│ │ ├── positionsLogic.tsx +│ │ ├── positionsInterfaces.ts +│ │ └── PositionsTable.module.css +│ └── PositionDocuments/ +│ ├── PositionDocumentsTable.tsx +│ ├── positionDocumentsLogic.tsx +│ ├── positionDocumentsInterfaces.ts +│ └── PositionDocumentsTable.module.css +├── hooks/ +│ ├── useTrusteeOrganisationen.ts +│ ├── useTrusteeRollen.ts +│ ├── useTrusteeAccess.ts +│ ├── useTrusteeContracts.ts +│ ├── useTrusteeDocuments.ts +│ ├── useTrusteePositions.ts +│ ├── useTrusteePositionDocuments.ts +│ └── useRbacPermissions.ts +├── pages/ +│ └── Home/ +│ └── Trustee.tsx +└── api.ts (erweitern mit Trustee-API-Calls) +``` + +### Komponenten-Pattern + +Jede View folgt dem bestehenden Pattern: + +1. **Logic Hook** (`*Logic.tsx`): Enthält Geschäftslogik, State-Management, API-Calls, Column/Action/Field-Konfigurationen +2. **Table Component** (`*Table.tsx`): Verwendet Logic-Hook, rendert FormGenerator + Edit-Popup +3. **Interfaces** (`*Interfaces.ts`): TypeScript-Typen und Interfaces +4. **CSS Module** (`*.module.css`): Styling + +### FormGenerator Pattern + +**FormGenerator** ist eine reine Präsentationskomponente: + +- Rendert Tabelle mit Filter, Sort, Pagination, Suche +- Bekommt `data`, `columns`, `actions` als Props +- Handhabt keine Daten-Laden oder CRUD-Operationen selbst +- Rendert kein Edit-Popup selbst + +**Logic-Hook** handhabt: +- Daten laden (via `use*` Hooks) +- CRUD-Operationen +- State-Management +- Column/Action/Field-Konfigurationen + +**Table-Komponente**: +- Verwendet Logic-Hook +- Rendert FormGenerator mit Props +- Rendert Edit-Popup separat (Popup + EditForm) + +### Seiten-Konfiguration + +Die Trustee-Seite wird in `pageConfigs.ts` hinzugefügt: + +```typescript +{ + path: 'trustee', + component: Trustee, + persistent: false, + preload: true, + moduleEnabled: true, // Wird basierend auf RBAC gesetzt + id: '8', + name: 'Treuhand', + icon: TrusteeIcon, // z.B. von react-icons + order: 7, + showInSidebar: true, // Nur wenn ui.trustee.view = true +} +``` + +--- + +## Entscheidungen + +### 1. Navigation und View-Organisation + +**Entscheidung**: Standard-Navigation (Sidebar) - die Entwicklerin kennt das Pattern. + +Die Views werden als separate Seiten/Sub-Routen innerhalb des Trustee-Containers organisiert. Die Navigation erfolgt über die Standard-Sidebar-Navigation. + +--- + +### 2. Referenzen zwischen Documents und Positions + +**Entscheidung**: Option C (bidirektional) + Option D (separate View für Verwaltung) + +- **Bidirektional**: In Position-View werden verknüpfte Documents angezeigt, in Document-View werden verknüpfte Positions angezeigt +- **Separate View**: Zusätzlich gibt es eine separate View für die Verwaltung aller Verknüpfungen + +**Implementierung**: +- In Position-View: Spalte "Documents" mit Links/Liste zu verknüpften Documents +- In Document-View: Spalte "Positions" mit Links/Liste zu verknüpften Positions +- Separate View: `PositionDocuments` für CRUD-Operationen auf Verknüpfungen + +--- + +### 3. Filterung nach Organisation + +**Entscheidung**: Option C - Persistente Filterung basierend auf User-Berechtigungen (automatisch) + +Die Filterung erfolgt automatisch im Backend basierend auf RBAC-Berechtigungen. Das Frontend muss keine zusätzliche Filterung implementieren, da die API bereits gefilterte Daten zurückgibt. + +**Ausnahme**: Für Views, die mehrere Organisationen anzeigen können (z.B. Access-View), kann ein Dropdown-Filter oder Register hinzugefügt werden, um zwischen Organisationen zu wechseln. + +--- + +### 4. File Upload/Download UI + +**Entscheidung**: Nicht Teil dieser Implementierung + +File Upload/Download wird in einer späteren Phase implementiert. Für Phase 1 werden nur Metadaten verwaltet. + +--- + +### 5. MwSt-Berechnung UI-Verhalten + +**Entscheidung**: Option B - Automatische Berechnung bei Eingabe, `vatAmount` Feld ist editierbar (manuelle Überschreibung) + +**Implementierung**: +- Bei Eingabe von `vatPercentage` und `bookingAmount`: Automatische Berechnung von `vatAmount` +- `vatAmount` Feld ist editierbar für manuelle Überschreibung +- Validierung: Warnung (Toast) wenn `vatAmount` nicht mit berechnetem Wert übereinstimmt + +**Code-Beispiel**: +```typescript +// In positionsLogic.tsx +const handleVatCalculation = (bookingAmount: number, vatPercentage: number) => { + const calculatedVat = bookingAmount * (vatPercentage / 100); + setEditedData(prev => ({ + ...prev, + vatAmount: calculatedVat + })); + + // Warnung wenn manuell überschrieben + if (editedData.vatAmount && Math.abs(editedData.vatAmount - calculatedVat) > 0.01) { + showWarning('MwSt-Betrag wurde manuell überschrieben'); + } +}; +``` + +--- + +### 6. Position-Document Verknüpfungen UI + +**Entscheidung**: Option E - Kombination A + D (separate View + bidirektionale Inline-Verwaltung) + +**Implementierung**: +- **Separate View**: `PositionDocuments` View für CRUD-Operationen +- **Inline in Position-View**: + - Spalte "Documents" zeigt verknüpfte Documents als Links + - In Edit-Modal: Multi-Select oder Checkbox-Liste zum Verknüpfen von Documents +- **Inline in Document-View**: + - Spalte "Positions" zeigt verknüpfte Positions als Links + - In Edit-Modal: Multi-Select oder Checkbox-Liste zum Verknüpfen von Positions + +--- + +### 7. Validierung und Fehlerbehandlung + +**Entscheidung**: Option B - Toast/Notification Messages (Standard) + +Fehler und Warnungen werden über Toast/Notification Messages angezeigt (Standard-Pattern der Anwendung). + +--- + +## Implementierungsanleitung + +**Hinweis**: Backend-Implementierungsdetails finden sich im [Architektur-Dokument](./doc_trustee_feature_architecture.md). + +### Schritt 1: Hooks für jede Resource erstellen + +**Beispiel**: `src/hooks/useTrusteeOrganisationen.ts` + +```typescript +import { useState, useEffect, useCallback } from 'react'; +import { trusteeApi } from '../api'; + +export interface TrusteeOrganisation { + id: string; + label: string; + enabled: boolean; + mandate: string; + _createdAt: number; + _modifiedAt: number; + _createdBy?: string; + _modifiedBy?: string; +} + +export const useTrusteeOrganisationen = () => { + const [organisationen, setOrganisationen] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchOrganisationen = useCallback(async () => { + try { + setLoading(true); + const response = await trusteeApi.getOrganisationen(); + setOrganisationen(response.data.items || response.data); + setError(null); + } catch (err: any) { + setError(err.response?.data?.detail || 'Failed to fetch organisations'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchOrganisationen(); + }, [fetchOrganisationen]); + + const createOrganisation = async (data: Partial) => { + try { + const response = await trusteeApi.createOrganisation(data); + await fetchOrganisationen(); + return response.data; + } catch (err: any) { + throw new Error(err.response?.data?.detail || 'Failed to create organisation'); + } + }; + + const updateOrganisation = async (id: string, data: Partial) => { + try { + const response = await trusteeApi.updateOrganisation(id, data); + await fetchOrganisationen(); + return response.data; + } catch (err: any) { + throw new Error(err.response?.data?.detail || 'Failed to update organisation'); + } + }; + + const deleteOrganisation = async (id: string) => { + try { + await trusteeApi.deleteOrganisation(id); + await fetchOrganisationen(); + } catch (err: any) { + throw new Error(err.response?.data?.detail || 'Failed to delete organisation'); + } + }; + + return { + organisationen, + loading, + error, + fetchOrganisationen, + createOrganisation, + updateOrganisation, + deleteOrganisation + }; +}; +``` + +**Wichtig**: Die Interfaces sollten mit den Backend-Modellen übereinstimmen. Systemattribute (`_createdAt`, `_createdBy`, etc.) werden automatisch vom Backend gesetzt. + +### Schritt 2: API-Funktionen erweitern + +**Datei**: `src/api.ts` oder `src/api/trusteeApi.ts` + +```typescript +export const trusteeApi = { + getOrganisationen: () => api.get('/api/trustee/organisations/'), + getOrganisation: (id: string) => api.get(`/api/trustee/organisations/${id}`), + createOrganisation: (data: any) => api.post('/api/trustee/organisations/', data), + updateOrganisation: (id: string, data: any) => api.put(`/api/trustee/organisations/${id}`, data), + deleteOrganisation: (id: string) => api.delete(`/api/trustee/organisations/${id}`), + // ... weitere API-Calls für andere Ressourcen +}; +``` + +**Wichtig**: +- Backend filtert automatisch basierend auf RBAC (siehe [Architektur-Dokument](./doc_trustee_feature_architecture.md)) +- Frontend erhält nur erlaubte Daten + +### Schritt 3: RBAC-Permissions Hook erstellen (optional, nur für Sichtbarkeitsprüfung) + +**Datei**: `src/hooks/useRbacPermissions.ts` + +```typescript +import { useState, useEffect } from 'react'; +import api from '../api'; + +export interface RbacPermissions { + ui: Record; + resource: Record; +} + +export const useRbacPermissions = () => { + const [permissions, setPermissions] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchPermissions = async () => { + try { + const response = await api.get('/api/rbac/permissions/all?context=UI'); + setPermissions(response.data); + setLoading(false); + } catch (err) { + setError('Failed to load permissions'); + setLoading(false); + } + }; + + fetchPermissions(); + }, []); + + const hasPermission = (context: 'ui' | 'resource', item: string, action: 'view' | 'read' | 'create' | 'update' | 'delete'): boolean => { + if (!permissions) return false; + const itemPermissions = permissions[context][item]; + if (!itemPermissions) return false; + + if (action === 'view') return itemPermissions.view; + // Für read/create/update/delete: Prüfe ob nicht null und nicht 'n' (NONE) + return itemPermissions[action] !== null && itemPermissions[action] !== 'n'; + }; + + return { permissions, loading, error, hasPermission }; +}; +``` + +### Schritt 4: Logic-Komponenten erstellen + +**Beispiel-Struktur**: `src/components/Trustee/Organisationen/organisationenLogic.tsx` + +Folgt dem Pattern von `promptsLogic.tsx`: + +```typescript +import { useState, useEffect, useMemo } from 'react'; +import { useTrusteeOrganisationen } from '../../../hooks/useTrusteeOrganisationen'; +import { useLanguage } from '../../../contexts/LanguageContext'; +import api from '../../../api'; +import { ColumnConfig } from '../../FormGenerator'; +import { EditFieldConfig } from '../../Popup'; + +export function useOrganisationenLogic() { + const { t } = useLanguage(); + const { + organisationen, + loading, + error, + fetchOrganisationen, + createOrganisation, + updateOrganisation, + deleteOrganisation + } = useTrusteeOrganisationen(); + + const [editPopupOpen, setEditPopupOpen] = useState(false); + const [editingOrganisation, setEditingOrganisation] = useState(null); + const [fieldConfigs, setFieldConfigs] = useState([]); + + // Automatisch Feld-Konfigurationen vom Backend laden + useEffect(() => { + const loadFieldConfigs = async () => { + try { + const response = await api.get('/api/attributes/TrusteeOrganisation'); + const attributes = response.data.attributes || []; + + // Konvertiere Backend-Attribute zu EditFieldConfig + const configs: EditFieldConfig[] = await Promise.all( + attributes.map(async (attr: any) => { + let options = attr.options; + + // Wenn options eine String-Referenz ist, dynamisch laden + if (typeof attr.options === 'string' && attr.type === 'select') { + // Beispiel: "trustee.organisation" -> API-Call zu /api/trustee/organisations/ + if (attr.options.startsWith('trustee.')) { + const resource = attr.options.replace('trustee.', ''); + try { + const response = await trusteeApi[`get${resource.charAt(0).toUpperCase() + resource.slice(1)}`](); + options = (response.data.items || response.data).map((item: any) => ({ + value: item.id, + label: item.label || item.name || item.id + })); + } catch (err) { + console.error(`Error loading options for ${attr.options}:`, err); + options = []; + } + } + } + + return { + key: attr.name, + label: attr.label, + type: attr.type === 'text' ? 'string' : + attr.type === 'checkbox' ? 'boolean' : + attr.type === 'select' ? 'enum' : + attr.type === 'textarea' ? 'textarea' : + attr.type === 'email' ? 'email' : + attr.type === 'timestamp' ? 'readonly' : 'string', + editable: attr.editable && !attr.readonly, + required: attr.required, + options: options, // Array oder undefined + formatter: attr.type === 'timestamp' ? (value: number) => { + if (!value) return 'N/A'; + return new Date(value * 1000).toLocaleString(); + } : undefined + }; + }) + ); + + setFieldConfigs(configs); + } catch (err) { + console.error('Error loading field configs:', err); + } + }; + + loadFieldConfigs(); + }, []); + + // Column-Konfigurationen (können auch automatisch generiert werden) + const columns: ColumnConfig[] = [ + { + key: 'id', + label: t('trustee.organisation.id', 'ID'), + type: 'string', + width: 150, + sortable: true, + filterable: true + }, + { + key: 'label', + label: t('trustee.organisation.label', 'Label'), + type: 'string', + width: 200, + sortable: true, + filterable: true, + searchable: true + }, + { + key: 'enabled', + label: t('trustee.organisation.enabled', 'Enabled'), + type: 'boolean', + width: 100, + sortable: true, + filterable: true + } + ]; + + // CRUD-Handler + const handleCreate = () => { + setEditingOrganisation({}); + setEditPopupOpen(true); + }; + + const handleEdit = (organisation: any) => { + setEditingOrganisation(organisation); + setEditPopupOpen(true); + }; + + const handleSave = async (data: any) => { + try { + if (editingOrganisation.id) { + await updateOrganisation(editingOrganisation.id, data); + } else { + await createOrganisation(data); + } + setEditPopupOpen(false); + setEditingOrganisation(null); + } catch (err) { + console.error('Error saving organisation:', err); + throw err; + } + }; + + const handleDelete = async (organisation: any) => { + if (window.confirm(t('trustee.organisation.confirm_delete', 'Delete organisation?'))) { + await deleteOrganisation(organisation.id); + } + }; + + return { + organisationen, + loading, + error, + columns, + fieldConfigs, // Automatisch generiert vom Backend + editPopupOpen, + editingOrganisation, + handleCreate, + handleEdit, + handleSave, + handleDelete, + handleCancel: () => { + setEditPopupOpen(false); + setEditingOrganisation(null); + } + }; +} +``` + +**Wichtig**: +- Feld-Konfigurationen werden automatisch vom Backend geladen (`/api/attributes/TrusteeOrganisation`) +- Nur Custom Logic muss manuell implementiert werden (z.B. Validierungen, Formatierungen) +- Column-Konfigurationen können auch automatisch generiert werden, wenn keine übergeben werden + +**Datei**: `src/api.ts` erweitern + +```typescript +// Trustee API functions +export const trusteeApi = { + // Organisationen + getOrganisationen: () => api.get('/api/trustee/organisations/'), + getOrganisation: (id: string) => api.get(`/api/trustee/organisations/${id}`), + createOrganisation: (data: any) => api.post('/api/trustee/organisations/', data), + updateOrganisation: (id: string, data: any) => api.put(`/api/trustee/organisations/${id}`, data), + deleteOrganisation: (id: string) => api.delete(`/api/trustee/organisations/${id}`), + + // Rollen + getRollen: () => api.get('/api/trustee/roles/'), + getRolle: (id: string) => api.get(`/api/trustee/roles/${id}`), + createRolle: (data: any) => api.post('/api/trustee/roles/', data), + updateRolle: (id: string, data: any) => api.put(`/api/trustee/roles/${id}`, data), + deleteRolle: (id: string) => api.delete(`/api/trustee/roles/${id}`), + + // Access + getAccess: () => api.get('/api/trustee/access/'), + getAccessForOrganisation: (orgId: string) => api.get(`/api/trustee/access/organisation/${orgId}`), + createAccess: (data: any) => api.post('/api/trustee/access/', data), + updateAccess: (id: string, data: any) => api.put(`/api/trustee/access/${id}`, data), + deleteAccess: (id: string) => api.delete(`/api/trustee/access/${id}`), + + // Contracts + getContracts: () => api.get('/api/trustee/contracts/'), + getContractsForOrganisation: (orgId: string) => api.get(`/api/trustee/contracts/organisation/${orgId}`), + createContract: (data: any) => api.post('/api/trustee/contracts/', data), + updateContract: (id: string, data: any) => api.put(`/api/trustee/contracts/${id}`, data), + deleteContract: (id: string) => api.delete(`/api/trustee/contracts/${id}`), + + // Documents + getDocuments: () => api.get('/api/trustee/documents/'), + getDocument: (id: string) => api.get(`/api/trustee/documents/${id}`), + getDocumentData: (id: string) => api.get(`/api/trustee/documents/${id}/data`, { responseType: 'blob' }), + getDocumentsForContract: (contractId: string) => api.get(`/api/trustee/documents/contract/${contractId}`), + getDocumentsForPosition: (positionId: string) => api.get(`/api/trustee/documents/position/${positionId}`), + createDocument: (data: FormData) => api.post('/api/trustee/documents/', data, { headers: { 'Content-Type': 'multipart/form-data' } }), + updateDocument: (id: string, data: any) => api.put(`/api/trustee/documents/${id}`, data), + deleteDocument: (id: string) => api.delete(`/api/trustee/documents/${id}`), + + // Positions + getPositions: () => api.get('/api/trustee/positions/'), + getPosition: (id: string) => api.get(`/api/trustee/positions/${id}`), + getPositionsForContract: (contractId: string) => api.get(`/api/trustee/positions/contract/${contractId}`), + getPositionsForDocument: (documentId: string) => api.get(`/api/trustee/positions/document/${documentId}`), + createPosition: (data: any) => api.post('/api/trustee/positions/', data), + updatePosition: (id: string, data: any) => api.put(`/api/trustee/positions/${id}`, data), + deletePosition: (id: string) => api.delete(`/api/trustee/positions/${id}`), + + // Position-Documents + getPositionDocuments: () => api.get('/api/trustee/position-documents/'), + getPositionDocumentsForPosition: (positionId: string) => api.get(`/api/trustee/position-documents/position/${positionId}`), + getPositionDocumentsForDocument: (documentId: string) => api.get(`/api/trustee/position-documents/document/${documentId}`), + createPositionDocument: (data: any) => api.post('/api/trustee/position-documents/', data), + deletePositionDocument: (id: string) => api.delete(`/api/trustee/position-documents/${id}`), +}; +``` + +### Schritt 5: Table-Komponenten erstellen + +**Beispiel**: `src/components/Trustee/Organisationen/OrganisationenTable.tsx` + +Folgt dem Pattern von `PromptsTable.tsx`: + +```typescript +import { FormGenerator } from '../../FormGenerator/FormGenerator'; +import { Popup } from '../../Popup/Popup'; +import { EditForm } from '../../Popup/EditForm'; +import { useOrganisationenLogic } from './organisationenLogic'; +import { useLanguage } from '../../../contexts/LanguageContext'; +import styles from './OrganisationenTable.module.css'; + +export function OrganisationenTable() { + const { t } = useLanguage(); + const { + organisationen, + loading, + error, + columns, + actions, + handleDeleteSingle, + handleDeleteMultiple, + editPopupOpen, + editingOrganisation, + editOrganisationFields, + handleSave, + handleCancel + } = useOrganisationenLogic(); + + if (error) { + return ( +
+

{t('trustee.organisation.error.loading', 'Error loading organisations:')} {error}

+ +
+ ); + } + + return ( +
+ + + {/* Edit Modal */} + + {editingOrganisation && ( + + )} + +
+ ); +} +``` + +**Wichtig**: +- `columns` ist optional - FormGenerator kann automatisch Spalten erkennen +- Alle Standard-Features (Suche, Filter, Sortierung, Pagination) werden automatisch von FormGenerator bereitgestellt + +### Schritt 6: Page-Komponente erstellen + +**Datei**: `src/pages/Home/Trustee.tsx` + +Struktur ähnlich wie `Connections.tsx`: + +```typescript +import { useState } from 'react'; +import sharedStyles from '../../components/PageManager/pages.module.css'; +import { + OrganisationenTable +} from '../../components/Trustee/Organisationen'; +import { useRbacPermissions } from '../../hooks/useRbacPermissions'; +import { useLanguage } from '../../contexts/LanguageContext'; + +function Trustee() { + const { t } = useLanguage(); + const { hasPermission } = useRbacPermissions(); + + // Prüfe Container-Sichtbarkeit + const showTrustee = hasPermission('ui', 'trustee', 'view'); + if (!showTrustee) { + return null; // Container nicht anzeigen + } + + return ( +
+
+
+

{t('trustee.title', 'Treuhand')}

+
+ +
+ +
+ {/* Organisationen View */} + {hasPermission('ui', 'trustee.organisation', 'view') && ( +
+

{t('trustee.organisation.title', 'Organisationen')}

+ +
+ )} + + {/* Weitere Views... */} +
+
+
+ ); +} + +export default Trustee; +``` + +**Wichtig**: +- RBAC-Sichtbarkeit wird für Container und Views geprüft +- EditForm verwendet automatisch generierte `fieldConfigs` vom Backend +- Nur Custom Logic muss manuell implementiert werden + +### Schritt 7: Spezielle Features implementieren (Custom Logic) + +#### MwSt-Berechnung in Positions-View + +**Custom Logic** in `positionsLogic.tsx`: + +**Anforderungen**: +- Automatische Berechnung beim Ändern von `bookingAmount` oder `vatPercentage` +- Wenn `vatAmount` manuell geändert wird, wird automatische Berechnung erneut durchgeführt +- Toast-Warnung erscheint bereits beim Ändern (nicht erst beim Speichern) + +```typescript +// Erweitere automatisch generierte fieldConfigs mit Custom Logic +const enhancedFieldConfigs = useMemo(() => { + return fieldConfigs.map(field => { + // Custom Logic für bookingAmount + if (field.key === 'bookingAmount') { + return { + ...field, + onChange: (value: number) => { + handleFieldChange('bookingAmount', value); + // Automatische MwSt-Berechnung + const vatPercentage = editedData.vatPercentage || 0; + const calculatedVat = value * (vatPercentage / 100); + handleFieldChange('vatAmount', calculatedVat); + } + }; + } + + // Custom Logic für vatPercentage + if (field.key === 'vatPercentage') { + return { + ...field, + onChange: (value: number) => { + handleFieldChange('vatPercentage', value); + // Automatische MwSt-Berechnung + const bookingAmount = editedData.bookingAmount || 0; + const calculatedVat = bookingAmount * (value / 100); + handleFieldChange('vatAmount', calculatedVat); + } + }; + } + + // Custom Logic für vatAmount (manuelle Überschreibung) + if (field.key === 'vatAmount') { + return { + ...field, + onChange: (value: number) => { + handleFieldChange('vatAmount', value); + // Warnung wenn manuell überschrieben + const bookingAmount = editedData.bookingAmount || 0; + const vatPercentage = editedData.vatPercentage || 0; + const calculatedVat = bookingAmount * (vatPercentage / 100); + if (Math.abs(value - calculatedVat) > 0.01) { + // Toast-Warnung anzeigen + showWarning('MwSt-Betrag wurde manuell überschrieben'); + } + } + }; + } + + return field; + }); +}, [fieldConfigs, editedData]); +``` + +**Wichtig**: +- Basis-Feld-Konfigurationen kommen automatisch vom Backend +- Nur Custom Logic (MwSt-Berechnung, Validierungen) muss manuell hinzugefügt werden +- Custom `onChange` Handler erweitern die automatisch generierten Felder + +### Schritt 8: RBAC-Integration + +**RBAC-Integration erfolgt automatisch**: +- Container-Sichtbarkeit: Prüfung in `Trustee.tsx` (siehe Schritt 6) +- View-Sichtbarkeit: Prüfung für jede View +- Button-Sichtbarkeit: Kann basierend auf `create`/`update`/`delete` Berechtigungen gesteuert werden + +**Hinweis**: Backend filtert Daten automatisch basierend auf RBAC. UI erhält nur erlaubte Daten. Siehe [Architektur-Dokument](./doc_trustee_feature_architecture.md) für Details zur RBAC-Filterung. + +**In `pageConfigs.ts`**: + +```typescript +import { lazy } from 'react'; +import { FaHandshake } from 'react-icons/fa'; + +const Trustee = lazy(() => import('../../pages/Home/Trustee')); + +export const pageConfigs: PageConfig[] = [ + // ... andere Configs + { + path: 'trustee', + component: Trustee, + persistent: false, + preload: true, + moduleEnabled: true, + id: '8', + name: 'Treuhand', + icon: FaHandshake, + order: 7, + showInSidebar: true, // Wird dynamisch basierend auf RBAC gefiltert + onActivate: async () => { + if (import.meta.env.DEV) console.log('Trustee activated'); + } + } +]; +``` + +**Hinweis**: Die Sidebar filtert automatisch basierend auf RBAC-Berechtigungen. Die `showInSidebar` Property kann auch dynamisch basierend auf Permissions gesetzt werden. + +--- + +## Zusammenfassung + +Dieses Dokument beschreibt ausschließlich die **Frontend-Implementierung** für das Trustee Feature: + +- ✅ **UI-Komponenten**: Logic-Hooks, Table-Komponenten, Page-Komponenten +- ✅ **FormGenerator-Pattern**: Verwendung und Custom Logic +- ✅ **RBAC-Integration**: Sichtbarkeitsprüfung im Frontend +- ✅ **Implementierungsanleitung**: Schritt-für-Schritt Anleitung für Frontend-Entwickler + +**Backend-Implementierungsdetails** (Datenmodelle, API-Routen, DatabaseConnector, RBAC-Filterung) finden sich im [Architektur-Dokument](./doc_trustee_feature_architecture.md). + +--- + +#### Bidirektionale Referenzen (Custom Logic) + +**Implementierung**: Beides möglich - Separate View + Inline-Verwaltung +- **Separate View**: `PositionDocumentsTable` für explizite Verwaltung +- **Inline**: Multi-Select in Position/Document Views für schnelle Zuordnung + +**Custom Spalten** für Referenzen in `PositionsTable.tsx`: + +```typescript +const columns: ColumnConfig[] = [ + // ... Standard-Spalten (automatisch generiert) + { + key: 'documents', + label: t('trustee.position.documents', 'Documents'), + type: 'string', + formatter: (value: any, row: any) => { + // Custom Formatter: Zeige Links zu verknüpften Documents + if (!row.linkedDocuments || row.linkedDocuments.length === 0) { + return -; + } + return ( +
+ {row.linkedDocuments.map((doc: any) => ( + + {doc.documentName} + + ))} +
+ ); + }, + width: 200 + } +]; +``` + +**Custom Feld** für Multi-Select in Edit-Modal: + +```typescript +// Erweitere fieldConfigs mit Custom Multi-Select für Document-Verknüpfungen +const enhancedFieldConfigs = useMemo(() => { + const baseConfigs = [...fieldConfigs]; + + // Füge Custom Multi-Select für Documents hinzu + baseConfigs.push({ + key: 'linkedDocuments', + label: t('trustee.position.linked_documents', 'Linked Documents'), + type: 'multiselect', // Custom Type + editable: true, + required: false, + options: availableDocuments.map(doc => ({ + value: doc.id, + label: doc.documentName + })), + // Custom Renderer für Multi-Select + render: (value: string[], onChange: (value: string[]) => void) => { + return ( + + ); + } + }); + + return baseConfigs; +}, [fieldConfigs, availableDocuments]); +``` + +**Wichtig**: +- Standard-Felder werden automatisch generiert +- Custom Felder (Multi-Select, Referenzen) müssen manuell hinzugefügt werden +- Custom Formatter für Tabellen-Spalten müssen manuell definiert werden + +--- + +## Implementierungs-Checkliste + +**Hinweis**: Backend-Implementierungsdetails finden sich im [Architektur-Dokument](./doc_trustee_feature_architecture.md). + + +### Phase 1: Frontend-Grundlagen +- [ ] Hooks für alle Resources erstellen (`useTrustee*`) +- [ ] API-Funktionen erweitern (`api.ts` mit `trusteeApi`) +- [ ] RBAC-Permissions Hook erstellen (`useRbacPermissions.ts`) - optional, nur für Sichtbarkeitsprüfung +- [ ] Logic-Komponenten für alle Views erstellen (`*Logic.tsx`) +- [ ] Table-Komponenten für alle Views erstellen (`*Table.tsx`) +- [ ] Page-Komponente erstellen (`Trustee.tsx`) +- [ ] Page-Konfiguration hinzufügen (`pageConfigs.ts`) + +### Phase 2: Basis-Views (automatisch generiert) +- [ ] Organisationen-View: + - [ ] Logic-Hook mit automatischer Feld-Generierung + - [ ] Table-Komponente (FormGenerator) + - [ ] Custom Logic: Organisation-ID-Validierung +- [ ] Rollen-View: + - [ ] Logic-Hook mit automatischer Feld-Generierung + - [ ] Table-Komponente + - [ ] Custom Logic: Lösch-Schutz wenn Rolle in Verwendung +- [ ] Access-View: + - [ ] Logic-Hook mit automatischer Feld-Generierung + - [ ] Table-Komponente + - [ ] Custom Logic: Dropdown-Filterung für organisationId, roleId, userId +- [ ] Contracts-View: + - [ ] Logic-Hook mit automatischer Feld-Generierung + - [ ] Table-Komponente + - [ ] Custom Logic: organisationId immutable nach Erstellung + +### Phase 3: Erweiterte Views (mit Custom Logic) +- [ ] Documents-View: + - [ ] Logic-Hook mit automatischer Feld-Generierung + - [ ] Table-Komponente + - [ ] Custom Logic: File Upload (später), Download-Links, Referenzen zu Positions +- [ ] Positions-View: + - [ ] Logic-Hook mit automatischer Feld-Generierung + - [ ] Table-Komponente + - [ ] **Custom Logic**: MwSt-Berechnung (automatisch + manuelle Überschreibung) + - [ ] Custom Logic: Referenzen zu Documents +- [ ] PositionDocuments-View: + - [ ] Logic-Hook mit automatischer Feld-Generierung + - [ ] Table-Komponente + - [ ] Custom Logic: Bidirektionale Verknüpfungs-Verwaltung + +### Phase 4: RBAC-Integration +- [ ] RBAC-Sichtbarkeitsprüfung in Trustee-Page (Container und Views) +- [ ] RBAC-Filterung testen (automatisch im Backend, UI erhält nur gefilterte Daten) + +### Phase 6: Custom Features +- [ ] MwSt-Berechnung implementieren (Custom onChange Handler) +- [ ] Bidirektionale Referenzen (Custom Spalten + Multi-Select) +- [ ] Validierungen (Custom Validators in fieldConfigs) +- [ ] Fehlerbehandlung (Toast Messages) + +### Phase 7: Testing +- [ ] Unit-Tests für Custom Logic +- [ ] Integration-Tests für API-Calls +- [ ] RBAC-Tests für alle Views +- [ ] UI-Tests für CRUD-Operationen +- [ ] Test der automatischen Feld-Generierung + +--- + +## Zusammenfassung: Was wird automatisch generiert vs. Custom Logic + +### Automatisch generiert (durch FormGenerator + Backend-Attribute) + +✅ **Tabellen-Spalten**: Automatische Erkennung wenn keine `columns` übergeben +✅ **Formular-Felder**: Automatisch aus `/api/attributes/{entityType}` +✅ **Feld-Typen**: Basierend auf `frontend_type` (text, select, checkbox, textarea, email, date, timestamp, number, file, multilingual) +✅ **Validierung**: Basierend auf `frontend_required` +✅ **Readonly-Status**: Basierend auf `frontend_readonly` +✅ **Select-Optionen**: + - Statische Options: Direkt aus `frontend_options` Array + - Dynamische Options: String-Referenz (z.B. "trustee.organisation") wird automatisch zu API-Call aufgelöst +✅ **Labels**: Automatisch aus `registerModelLabels` (mehrsprachig) +✅ **CRUD-Operationen**: Standard CRUD durch FormGenerator +✅ **Suche, Filter, Sortierung**: Automatisch von FormGenerator +✅ **Pagination**: Automatisch von FormGenerator +✅ **Formular-Rendering**: Automatisch basierend auf Feld-Typen + +### Custom Logic (muss in Config definiert werden) + +🔧 **MwSt-Berechnung**: Custom `onChange` Handler für `bookingAmount`, `vatPercentage`, `vatAmount` +🔧 **Bidirektionale Referenzen**: Custom Spalten-Formatter + Multi-Select für Verknüpfungen +🔧 **Custom Validierungen**: Z.B. Organisation-ID-Format (alphanumerisch, 3-50 Zeichen), Contract-Immutable-Prüfung +🔧 **Custom Actions**: Z.B. Download-Button für Documents, Verknüpfungs-Management +🔧 **Custom Formatter**: Z.B. Datum-Formatierung, Links zu verknüpften Records +🔧 **RBAC-Integration**: Sichtbarkeitsprüfung für Container/Views/Buttons (RBAC-Logik selbst ist für UI nicht relevant, da Backend automatisch filtert) +🔧 **Dropdown-Filterung**: Backend filtert automatisch, Frontend erhält nur erlaubte Options + + + +--- + +## Implementierungsentscheidungen (geklärt) + +### RBAC-Integration +- **Zwei-Stufen-Filterung**: System-RBAC zuerst, dann Trustee-spezifische Filterung in Interface-Schicht +- **Für UI**: RBAC-Logik ist nicht relevant, da Backend automatisch alle Daten korrekt gefiltert liefert + +### Contract Immutability +- **Backend-Validierung**: Prüft bei Update, ob `organisationId` geändert wurde → Fehler +- **Frontend-Readonly**: `organisationId` wird readonly wenn `id` vorhanden (non-blank) +- **Logik**: ID kann nur gespeichert werden, wenn non-blank. Eine non-blank ID kann nicht mehr geändert werden + +### Dropdown-Filterung +- **Backend-Filterung**: API-Routen filtern automatisch basierend auf RBAC +- Frontend erhält nur erlaubte Options +- `contractId` Dropdown wird dynamisch aktualisiert wenn `organisationId` geändert wird + +### Position-Document Verknüpfungen +- **Beides möglich**: Separate View + Inline-Verwaltung +- Separate View für explizite Verwaltung, Inline für schnelle Zuordnung + +### MwSt-Berechnung +- Automatisch beim Ändern von `bookingAmount` oder `vatPercentage` +- Wenn `vatAmount` manuell geändert wird, wird automatische Berechnung erneut durchgeführt +- Toast-Warnung erscheint bereits beim Ändern + +### Weitere Entscheidungen +- **Organisation-ID Format**: Alphanumerisch + Bindestrich/Unterstrich, 3-50 Zeichen +- **File Upload/Download**: Über Workflow-System, nicht direkt integriert +- **Initiale Rollen**: Bootstrap-Script erstellt automatisch +- **Mandate vs. Organisation**: `mandate` automatisch aus `currentUser.mandateId`, `organisationId` Dropdown zeigt alle gelieferten (RBAC filtert) +- **Systemattribute**: Readonly im Frontend, Timestamps als float, User-Namen statt User-ID + +--- + +**Dokumentversion**: 5.0 +**Letzte Aktualisierung**: 2025-01-04 +**Status**: Fertig für Implementierung - Fokus auf UI-Komponenten und Frontend-Implementierung + +**Hinweis**: Backend-Implementierungsdetails (Datenmodelle, API-Routen, DatabaseConnector, RBAC-Filterung) finden sich im [Architektur-Dokument](./doc_trustee_feature_architecture.md).