# 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). ### Backend-Status ✅ IMPLEMENTIERT Das Backend ist vollständig implementiert und bereit für die Frontend-Entwicklung: | Komponente | Datei | Status | |------------|-------|--------| | **Datenmodelle** | `gateway/modules/datamodels/datamodelTrustee.py` | ✅ | | **Interface** | `gateway/modules/interfaces/interfaceDbTrusteeObjects.py` | ✅ | | **API-Routes** | `gateway/modules/routes/routeDataTrustee.py` | ✅ | | **RBAC-Regeln** | `gateway/modules/interfaces/interfaceBootstrap.py` | ✅ | | **App-Registrierung** | `gateway/app.py` | ✅ | **API-Basis-URL**: `/api/trustee/` **Implementierte Entities**: - `TrusteeOrganisation` - Trustee-Organisationen - `TrusteeRole` - Feature-spezifische Rollen (userreport, admin, operate) - `TrusteeAccess` - Benutzerzugriffe auf Organisationen (mit optionalem Contract) - `TrusteeContract` - Kundenverträge - `TrusteeDocument` - Dokumente/Belege - `TrusteePosition` - Buchungspositionen - `TrusteePositionDocument` - Verknüpfung Position-Dokument **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 // Backend verwendet jetzt PascalCase-Referenzen: TrusteeOrganisation, TrusteeRole, etc. if (typeof attr.options === 'string' && attr.type === 'select') { // Beispiel: "TrusteeOrganisation" -> API-Call zu /api/trustee/organisations const optionMap: Record Promise> = { 'TrusteeOrganisation': trusteeApi.getOrganisationen, 'TrusteeRole': trusteeApi.getRollen, 'TrusteeContract': trusteeApi.getContracts, 'TrusteeDocument': trusteeApi.getDocuments, 'TrusteePosition': trusteeApi.getPositions, 'User': () => api.get('/api/users'), }; if (optionMap[attr.options]) { try { const response = await optionMap[attr.options](); options = (response.data.items || response.data).map((item: any) => ({ value: item.id, label: item.label || item.name || item.fullName || 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 - Implementiert in gateway/modules/routes/routeDataTrustee.py export const trusteeApi = { // Organisationen (TrusteeOrganisation) getOrganisationen: (pagination?: PaginationParams) => api.get('/api/trustee/organisations', { params: { pagination: pagination ? JSON.stringify(pagination) : undefined } }), 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 (TrusteeRole) getRollen: (pagination?: PaginationParams) => api.get('/api/trustee/roles', { params: { pagination: pagination ? JSON.stringify(pagination) : undefined } }), 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 (TrusteeAccess) getAccess: (pagination?: PaginationParams) => api.get('/api/trustee/access', { params: { pagination: pagination ? JSON.stringify(pagination) : undefined } }), getAccessById: (id: string) => api.get(`/api/trustee/access/${id}`), getAccessForOrganisation: (orgId: string) => api.get(`/api/trustee/access/organisation/${orgId}`), getAccessForUser: (userId: string) => api.get(`/api/trustee/access/user/${userId}`), 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 (TrusteeContract) getContracts: (pagination?: PaginationParams) => api.get('/api/trustee/contracts', { params: { pagination: pagination ? JSON.stringify(pagination) : undefined } }), getContract: (id: string) => api.get(`/api/trustee/contracts/${id}`), 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 (TrusteeDocument) getDocuments: (pagination?: PaginationParams) => api.get('/api/trustee/documents', { params: { pagination: pagination ? JSON.stringify(pagination) : undefined } }), 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}`), 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 (TrusteePosition) getPositions: (pagination?: PaginationParams) => api.get('/api/trustee/positions', { params: { pagination: pagination ? JSON.stringify(pagination) : undefined } }), getPosition: (id: string) => api.get(`/api/trustee/positions/${id}`), getPositionsForContract: (contractId: string) => api.get(`/api/trustee/positions/contract/${contractId}`), getPositionsForOrganisation: (orgId: string) => api.get(`/api/trustee/positions/organisation/${orgId}`), 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 (TrusteePositionDocument) getPositionDocuments: (pagination?: PaginationParams) => api.get('/api/trustee/position-documents', { params: { pagination: pagination ? JSON.stringify(pagination) : undefined } }), getPositionDocument: (id: string) => api.get(`/api/trustee/position-documents/${id}`), getDocumentsForPosition: (positionId: string) => api.get(`/api/trustee/position-documents/position/${positionId}`), getPositionsForDocument: (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).