69 KiB
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.
UI-Demo: Eine interaktive HTML-Demo zur Visualisierung der UI-Struktur findet sich in 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-OrganisationenTrusteeRole- Feature-spezifische Rollen (userreport, admin, operate)TrusteeAccess- Benutzerzugriffe auf Organisationen (mit optionalem Contract)TrusteeContract- KundenverträgeTrusteeDocument- Dokumente/BelegeTrusteePosition- BuchungspositionenTrusteePositionDocument- 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
- FormGenerator ist eine reine Präsentationskomponente: FormGenerator rendert nur die Tabelle mit Filter, Sort, Pagination, Suche
- Logic-Hooks handhaben Geschäftslogik:
- Daten laden (via
use*Hooks wieusePrompts,useWorkflows) - CRUD-Operationen
- State-Management (loading, error, editModalOpen, etc.)
- Column-Konfigurationen
- Actions-Konfigurationen
- Edit-Field-Konfigurationen
- Daten laden (via
- Table-Komponenten wrappen FormGenerator:
- Verwenden Logic-Hook
- Übergeben Props an FormGenerator
- Rendern Edit-Popup separat
- Backend-Metadaten: Feld-Konfigurationen können vom Backend über
/api/attributes/{entityType}geladen werden - Minimaler Code: Nur Custom Logic muss implementiert werden (MwSt-Berechnung, Referenzen, Validierungen)
- Konsistenz: Pattern sorgt für konsistente UI/UX über alle Views hinweg
Automatische Generierung
Wichtig: Das System verwendet ein automatisches Generierungs-Pattern:
- Backend-Datenmodelle definieren Metadaten (
json_schema_extra) für jedes Feld - API-Endpunkt
/api/attributes/{entityType}liefert Feld-Konfigurationen - FormGenerator generiert automatisch:
- Tabellen-Spalten (wenn keine
columnsübergeben) - Formular-Felder basierend auf Backend-Attributen
- Feld-Typen, Validierung, Readonly-Status
- Tabellen-Spalten (wenn keine
- 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
searchableSpalten - 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
- Text-Filter: Freitext-Suche über alle
- ✅ 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
actionsProp (z.B. Edit, Delete, Download) - ✅ Custom Formatter: Pro Spalte
formatterFunktion 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
- UI-Architektur
- Container und Views
- RBAC-Integration
- Routen-Übersicht
- UI-Requirements
- Implementierungsstruktur
- Entscheidungen
- Implementierungsanleitung
- 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(oderui.trustee.view = truemit 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
contractIdin trustee.access leer: Zugriff auf alle Contracts der Organisation - Wenn
contractIdgesetzt: Zugriff nur auf diesen spezifischen Contract Die Filterung erfolgt automatisch auf DB-Ebene, das UI erhält nur die erlaubten Daten.
- Wenn
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
createBerechtigung vorhanden - Read: Gefiltert nach RBAC (sysadmin: alle, admin: eigene Gruppe)
- Update: Erlaubt wenn
updateBerechtigung vorhanden - Delete: Erlaubt wenn
deleteBerechtigung vorhanden
API-Routen
Siehe Architektur-Dokument 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 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 zutrustee.organisation.id, required)roleId(String, FK zutrustee.role.id, required)userId(String, required)contractId(UUID, FK zutrustee.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
organisationIdgeändert wird - Placeholder: "Alle Contracts (gesamte Organisation)" oder leer lassen für Organisation-Zugriff
API-Routen
Siehe Architektur-Dokument 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 zutrustee.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
organisationIdkann NICHT nach Erstellung geändert werden (immutable)- Backend-Validierung: Prüft bei Update, ob
organisationIdgeändert wurde → Fehler - Frontend-Readonly:
organisationIdwird auf readonly gesetzt wennidvorhanden (non-blank) ist - Logik: ID kann nur gespeichert werden, wenn non-blank. Eine non-blank ID kann nicht mehr geändert werden
- Backend-Validierung: Prüft bei Update, ob
- Dropdown für
organisationId:- Backend-Filterung: API-Route
/api/trustee/organisations/filtert automatisch basierend auf RBAC - Frontend erhält nur die erlaubten Options
contractIdDropdown wird dynamisch aktualisiert wennorganisationIdgeändert wirdcontractIdDropdown ist leer wenn keineorganisationIdausgewählt
- Backend-Filterung: API-Route
- Neue Records: Default auf eigene
organisationId(falls User nur eine hat)
API-Routen
Siehe Architektur-Dokument 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 zutrustee.organisation.id, required)contractId(UUID, FK zutrustee.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
xpositiondocumentTabelle) - 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
- Anzeige verknüpfter Positionen (über
- Filterung nach
contractIdundorganisationId
API-Routen
Siehe Architektur-Dokument 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 zutrustee.organisation.id, required)contractId(UUID, FK zutrustee.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
bookingAmountodervatPercentage - Manuelle Überschreibung:
vatAmountkann manuell geändert werden - Re-Berechnung: Wenn
vatAmountmanuell 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:
vatAmountwird automatisch berechnet:bookingAmount * vatPercentage / 100 - Manuelle Überschreibung:
vatAmountkann manuell überschrieben werden - Validierung: Warnung wenn
vatAmountnicht mit berechnetem Wert übereinstimmt - Referenzen zu Documents: Anzeige verknüpfter Dokumente (über
xpositiondocumentTabelle) - Filterung nach
contractIdundorganisationId
API-Routen
Siehe Architektur-Dokument 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 zutrustee.organisation.id, required)contractId(UUID, FK zutrustee.contract.id, required)documentId(UUID, FK zutrustee.document.id, required)positionId(UUID, FK zutrustee.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 für vollständige API-Dokumentation.
RBAC-Integration
Berechtigungsabfrage beim UI-Start
Beim Initialisieren des Trustee-Containers:
-
Alle Berechtigungen abrufen:
GET /api/rbac/permissions/all?context=UI -
Container-Sichtbarkeit prüfen:
- Container "Treuhand" nur anzeigen wenn
permissions.ui.trustee.view === true
- Container "Treuhand" nur anzeigen wenn
-
View-Sichtbarkeit prüfen:
- Jede View nur anzeigen wenn entsprechende Berechtigung vorhanden:
permissions.ui.trustee.organisation.viewfür Organisationenpermissions.ui.trustee.role.viewfür Rollen- etc.
- Jede View nur anzeigen wenn entsprechende Berechtigung vorhanden:
Dynamische Berechtigungsprüfung
Für spezifische Aktionen (z.B. Button "Erstellen"):
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
-
RBAC-Integration:
- Container und Views nur anzeigen wenn entsprechende
viewBerechtigung vorhanden - CRUD-Buttons (Create, Update, Delete) nur anzeigen wenn entsprechende Berechtigung vorhanden
- Read-Operationen nach RBAC filtern
- Container und Views nur anzeigen wenn entsprechende
-
FormGeneric-Pattern:
- Alle Views verwenden FormGeneric für CRUD-Operationen
- Konsistente UI/UX über alle Views hinweg
- Automatische Feld-Generierung basierend auf Backend-Schema
-
Navigation:
- TODO: Wie sollen die Views organisiert sein? (Tabs, Sidebar-Navigation, etc.)
-
Filterung:
- Filterung nach
organisationIdin allen Views (außer Organisationen selbst) - Filterung nach
contractIdin Documents und Positions - Filterung nach User (für
userreportRolle)
- Filterung nach
Spezifische Anforderungen
Organisationen-View
- Validierung:
idmuss alphanumerisch, Bindestriche, Unterstriche sein (3-50 Zeichen) idist 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,userIdmüssen gefiltert sein - Unique Constraint:
(organisationId, roleId, userId)darf nicht doppelt sein
Contracts-View
organisationIdist nach Erstellung immutable (nicht änderbar)- Default
organisationIdauf 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
vatAmountbei Eingabe vonvatPercentageundbookingAmount - Manuelle Überschreibung:
vatAmountkann manuell überschrieben werden - Validierung: Warnung wenn
vatAmountnicht mit berechnetem Wert übereinstimmt - Referenzen: Anzeige verknüpfter Dokumente (wie genau? Liste, Links, etc.)
- Datum/Zeit-Formatierung für
valutaundtransactionDateTime
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:
- Tabellen-Spalten: Wenn keine
columnsübergeben werden, erkennt FormGenerator automatisch Spalten aus den Daten - Formular-Felder: Basierend auf Backend-Attributen (
/api/attributes/{entityType}) - Feld-Typen: Automatisch basierend auf
frontend_typeaus Pydantic-Modellen - Validierung: Basierend auf
frontend_requiredund Feld-Typen - Readonly-Felder: Basierend auf
frontend_readonly - Select-Optionen: Basierend auf
frontend_options(Array oder String-Referenz)
Backend-Attribut-System
Jedes Pydantic-Modell definiert Metadaten in json_schema_extra:
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:
- String-Referenz (für dynamische Options):
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/).
- Array mit statischen Options:
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:
{
"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:
- Custom Validierungen: Z.B. MwSt-Berechnung, Organisation-ID-Validierung
- Custom Formatter: Für spezielle Darstellungen (z.B. Datum-Formatierung, Links)
- Custom Actions: Z.B. Download-Button für Documents, Verknüpfungs-Management
- Custom Filterung: Z.B. Filterung nach Organisation basierend auf RBAC
- 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:
- Logic Hook (
*Logic.tsx): Enthält Geschäftslogik, State-Management, API-Calls, Column/Action/Field-Konfigurationen - Table Component (
*Table.tsx): Verwendet Logic-Hook, rendert FormGenerator + Edit-Popup - Interfaces (
*Interfaces.ts): TypeScript-Typen und Interfaces - CSS Module (
*.module.css): Styling
FormGenerator Pattern
FormGenerator ist eine reine Präsentationskomponente:
- Rendert Tabelle mit Filter, Sort, Pagination, Suche
- Bekommt
data,columns,actionsals 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:
{
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:
PositionDocumentsfü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
vatPercentageundbookingAmount: Automatische Berechnung vonvatAmount vatAmountFeld ist editierbar für manuelle Überschreibung- Validierung: Warnung (Toast) wenn
vatAmountnicht mit berechnetem Wert übereinstimmt
Code-Beispiel:
// 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:
PositionDocumentsView 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.
Schritt 1: Hooks für jede Resource erstellen
Beispiel: src/hooks/useTrusteeOrganisationen.ts
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<TrusteeOrganisation[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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<TrusteeOrganisation>) => {
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<TrusteeOrganisation>) => {
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
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)
- Frontend erhält nur erlaubte Daten
Schritt 3: RBAC-Permissions Hook erstellen (optional, nur für Sichtbarkeitsprüfung)
Datei: src/hooks/useRbacPermissions.ts
import { useState, useEffect } from 'react';
import api from '../api';
export interface RbacPermissions {
ui: Record<string, {
view: boolean;
read: string | null;
create: string | null;
update: string | null;
delete: string | null;
}>;
resource: Record<string, {
view: boolean;
read: string | null;
create: string | null;
update: string | null;
delete: string | null;
}>;
}
export const useRbacPermissions = () => {
const [permissions, setPermissions] = useState<RbacPermissions | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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:
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<any>(null);
const [fieldConfigs, setFieldConfigs] = useState<EditFieldConfig[]>([]);
// 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<string, () => Promise<any>> = {
'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
// 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:
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 (
<div className={styles.errorState}>
<p>{t('trustee.organisation.error.loading', 'Error loading organisations:')} {error}</p>
<button onClick={fetchOrganisationen} className={styles.retryButton}>
{t('common.retry', 'Retry')}
</button>
</div>
);
}
return (
<div className={styles.organisationenTable}>
<FormGenerator
data={organisationen}
columns={columns}
title={t('trustee.organisation.title', 'Organisations')}
searchable={true}
filterable={true}
sortable={true}
resizable={true}
pagination={true}
pageSize={10}
selectable={true}
loading={loading}
actions={actions}
onDelete={handleDeleteSingle}
onDeleteMultiple={handleDeleteMultiple}
className={styles.organisationenFormGenerator}
/>
{/* Edit Modal */}
<Popup
isOpen={editPopupOpen}
title={t('trustee.organisation.modal.edit.title', 'Edit Organisation')}
onClose={handleCancel}
size="large"
>
{editingOrganisation && (
<EditForm
data={editingOrganisation}
fields={editOrganisationFields}
onSave={handleSave}
onCancel={handleCancel}
saveButtonText={t('trustee.organisation.modal.edit.save', 'Save Changes')}
cancelButtonText={t('common.cancel', 'Cancel')}
/>
)}
</Popup>
</div>
);
}
Wichtig:
columnsist 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:
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 (
<div className={sharedStyles.pageContainer}>
<div className={sharedStyles.pageCard}>
<div className={sharedStyles.pageHeader}>
<h1 className={sharedStyles.pageTitle}>{t('trustee.title', 'Treuhand')}</h1>
</div>
<div className={sharedStyles.horizontalDivider}></div>
<div className={sharedStyles.contentArea}>
{/* Organisationen View */}
{hasPermission('ui', 'trustee.organisation', 'view') && (
<div>
<h2>{t('trustee.organisation.title', 'Organisationen')}</h2>
<OrganisationenTable />
</div>
)}
{/* Weitere Views... */}
</div>
</div>
</div>
);
}
export default Trustee;
Wichtig:
- RBAC-Sichtbarkeit wird für Container und Views geprüft
- EditForm verwendet automatisch generierte
fieldConfigsvom 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
bookingAmountodervatPercentage - Wenn
vatAmountmanuell geändert wird, wird automatische Berechnung erneut durchgeführt - Toast-Warnung erscheint bereits beim Ändern (nicht erst beim Speichern)
// 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
onChangeHandler 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/deleteBerechtigungen gesteuert werden
Hinweis: Backend filtert Daten automatisch basierend auf RBAC. UI erhält nur erlaubte Daten. Siehe Architektur-Dokument für Details zur RBAC-Filterung.
In pageConfigs.ts:
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.
Bidirektionale Referenzen (Custom Logic)
Implementierung: Beides möglich - Separate View + Inline-Verwaltung
- Separate View:
PositionDocumentsTablefür explizite Verwaltung - Inline: Multi-Select in Position/Document Views für schnelle Zuordnung
Custom Spalten für Referenzen in PositionsTable.tsx:
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 <span>-</span>;
}
return (
<div>
{row.linkedDocuments.map((doc: any) => (
<a key={doc.id} href={`/trustee/documents/${doc.id}`}>
{doc.documentName}
</a>
))}
</div>
);
},
width: 200
}
];
Custom Feld für Multi-Select in Edit-Modal:
// 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 (
<MultiSelect
options={availableDocuments}
value={value || []}
onChange={onChange}
/>
);
}
});
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.
Phase 1: Frontend-Grundlagen
- Hooks für alle Resources erstellen (
useTrustee*) - API-Funktionen erweitern (
api.tsmittrusteeApi) - 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_optionsArray - 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
organisationIdgeändert wurde → Fehler - Frontend-Readonly:
organisationIdwird readonly wennidvorhanden (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
contractIdDropdown wird dynamisch aktualisiert wennorganisationIdgeä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
bookingAmountodervatPercentage - Wenn
vatAmountmanuell 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:
mandateautomatisch auscurrentUser.mandateId,organisationIdDropdown 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.