wiki/z-archive/ui_nyla/feature-trustee/doc_trustee_feature_ui_specification.md

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-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
  2. Container und Views
  3. RBAC-Integration
  4. Routen-Übersicht
  5. UI-Requirements
  6. Implementierungsstruktur
  7. Entscheidungen
  8. Implementierungsanleitung
  9. 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 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 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 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 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 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 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 für vollständige API-Dokumentation.


RBAC-Integration

Berechtigungsabfrage beim UI-Start

Beim Initialisieren des Trustee-Containers:

  1. Alle Berechtigungen abrufen:

    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"):

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:

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):
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/).

  1. 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:

  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:

{
    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:

// 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.

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:

  • 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:

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 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)
// 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 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: PositionDocumentsTable fü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.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.