687 lines
24 KiB
Markdown
687 lines
24 KiB
Markdown
# Trustee Feature - RBAC Architektur & Gateway-Readiness
|
|
|
|
## Übersicht
|
|
|
|
Dieses Dokument beschreibt die **Zwei-Stufen-RBAC-Architektur** für das Trustee Feature und beantwortet die Frage, ob das Gateway bereit für die UI-Entwicklung ist.
|
|
|
|
**Erstellungsdatum**: 2026-01-23
|
|
**Status**: Review
|
|
|
|
---
|
|
|
|
## 1. Gateway-Status
|
|
|
|
### 1.1 Ist das Gateway bereit für UI-Arbeit?
|
|
|
|
**Ja, das Gateway ist vollständig bereit.**
|
|
|
|
| Komponente | Datei | Status |
|
|
|------------|-------|--------|
|
|
| **Routes** | `routeFeatureTrustee.py` | ✅ Komplett |
|
|
| **Interface** | `interfaceFeatureTrustee.py` | ✅ Komplett |
|
|
| **Datamodel** | `datamodelFeatureTrustee.py` | ✅ Komplett |
|
|
| **Feature-RBAC** | Via `TrusteeAccess` Tabelle | ✅ Implementiert |
|
|
|
|
### 1.2 Verfügbare API-Endpoints
|
|
|
|
Alle Endpoints verwenden das URL-Pattern: `/api/trustee/{instanceId}/...`
|
|
|
|
| Endpoint-Gruppe | Basis-Route | CRUD | Options |
|
|
|-----------------|-------------|------|---------|
|
|
| Organisations | `/api/trustee/{instanceId}/organisations` | ✅ | ✅ |
|
|
| Roles | `/api/trustee/{instanceId}/roles` | ✅ | ✅ |
|
|
| **Access** | `/api/trustee/{instanceId}/access` | ✅ | - |
|
|
| Contracts | `/api/trustee/{instanceId}/contracts` | ✅ | ✅ |
|
|
| Documents | `/api/trustee/{instanceId}/documents` | ✅ | ✅ |
|
|
| Positions | `/api/trustee/{instanceId}/positions` | ✅ | ✅ |
|
|
| Position-Documents | `/api/trustee/{instanceId}/position-documents` | ✅ | - |
|
|
| **User Options** | `/api/users/options` | - | ✅ |
|
|
|
|
### 1.3 Wichtige Änderung vs. UI-Spezifikation
|
|
|
|
Die ursprüngliche UI-Spezifikation (`doc_trustee_feature_ui_specification.md`) verwendet URLs wie:
|
|
```
|
|
/api/trustee/organisations/
|
|
```
|
|
|
|
Die **aktuelle Implementierung** verwendet:
|
|
```
|
|
/api/trustee/{instanceId}/organisations/
|
|
```
|
|
|
|
**Grund**: Multi-Tenancy - mehrere Treuhandbüros (Feature-Instanzen) pro Mandate möglich.
|
|
|
|
---
|
|
|
|
## 2. Architektur: Zwei-Stufen-RBAC für Features
|
|
|
|
### 2.1 Das Problem
|
|
|
|
Ein Treuhandbüro (Feature-Instanz) verwaltet mehrere Kunden (Organisationen). Jeder Mitarbeiter soll nur die Kunden sehen, für die er zuständig ist:
|
|
|
|
- **User A** → Kunde 1, Kunde 2
|
|
- **User B** → Kunde 2, Kunde 3
|
|
- **User C** → Alle Kunden (Admin)
|
|
|
|
Dies erfordert eine **Feature-interne Isolation**, die über das System-RBAC hinausgeht.
|
|
|
|
### 2.2 Lösung: Zwei-Stufen-RBAC
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ STUFE 1: SYSTEM-RBAC │
|
|
│ (Mandate + Feature Instance Zugang) │
|
|
├─────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ User ──► UserMandate ──► Mandate │
|
|
│ │ │
|
|
│ └──► FeatureAccess ──► FeatureInstance (Treuhandbüro) │
|
|
│ │ │
|
|
│ └── Rollen: feature-admin, feature-user │
|
|
│ │
|
|
│ Frage: Hat der User überhaupt Zugang zu diesem Feature? │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ STUFE 2: FEATURE-SPEZIFISCHES RBAC │
|
|
│ (Isolation innerhalb des Features) │
|
|
├─────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ TrusteeAccess │
|
|
│ ├── userId → Welcher User │
|
|
│ ├── organisationId → Welcher Kunde (Isolationsobjekt) │
|
|
│ ├── roleId → Welche Feature-Rolle (admin, operate, userreport)│
|
|
│ └── contractId → Optional: Einschränkung auf bestimmten Vertrag │
|
|
│ │
|
|
│ Frage: Welche Kunden darf der User sehen? Mit welchen Rechten? │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 2.3 Vergleich: System-RBAC vs. Feature-RBAC
|
|
|
|
| Aspekt | System-RBAC | Feature-RBAC (Trustee) |
|
|
|--------|-------------|------------------------|
|
|
| **Tabelle** | `AccessRule` | `TrusteeAccess` |
|
|
| **Scope** | Global / Mandate | Feature-Instanz |
|
|
| **Objekte** | Tables, UI-Items, API-Resources | Organisationen, Contracts |
|
|
| **Verwaltung** | SysAdmin | Feature-Admin |
|
|
| **Zweck** | "Was darf der User technisch?" | "Welche Daten darf der User sehen?" |
|
|
|
|
### 2.4 Feature-spezifische Rollen
|
|
|
|
Das Trustee-Feature definiert drei Rollen:
|
|
|
|
| Rolle | Beschreibung | Berechtigungen |
|
|
|-------|--------------|----------------|
|
|
| **admin** | Feature-Administrator | Voller CRUD auf Organisation + alle Contracts |
|
|
| **operate** | Sachbearbeiter | CRUD für Contracts, Documents, Positions |
|
|
| **userreport** | Endbenutzer | Nur eigene erstellte Records (Documents, Positions) |
|
|
|
|
### 2.5 Contract-Level Isolation (optional)
|
|
|
|
`TrusteeAccess.contractId` ermöglicht feinere Isolation:
|
|
|
|
- **Leer/NULL**: Zugriff auf alle Contracts der Organisation
|
|
- **Gesetzt**: Zugriff nur auf diesen spezifischen Contract
|
|
|
|
```
|
|
Beispiel:
|
|
- User A hat Zugriff auf Organisation "Kunde-X" mit contractId=NULL
|
|
→ Sieht alle Contracts von Kunde-X
|
|
|
|
- User B hat Zugriff auf Organisation "Kunde-X" mit contractId="contract-123"
|
|
→ Sieht nur Contract "contract-123" von Kunde-X
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Implementierung im Gateway
|
|
|
|
### 3.1 Datenmodell: TrusteeAccess
|
|
|
|
```python
|
|
class TrusteeAccess(BaseModel):
|
|
id: str # UUID
|
|
organisationId: str # FK zu TrusteeOrganisation
|
|
roleId: str # FK zu TrusteeRole (admin, operate, userreport)
|
|
userId: str # FK zu User
|
|
contractId: Optional[str] # Optional: FK zu TrusteeContract
|
|
mandateId: Optional[str] # System-Kontext
|
|
featureInstanceId: Optional[str] # Feature-Instanz
|
|
```
|
|
|
|
### 3.2 Kombinierte Berechtigungsprüfung
|
|
|
|
Die Methode `checkCombinedPermission()` in `interfaceFeatureTrustee.py` kombiniert beide Stufen:
|
|
|
|
```python
|
|
def checkCombinedPermission(self, modelClass, operation, organisationId, contractId, recordCreatedBy):
|
|
"""
|
|
Kombiniert System-RBAC + Feature-Level RBAC.
|
|
|
|
1. Prüft System-RBAC (AccessLevel: ALL, GROUP, MY, NONE)
|
|
2. Bei ALL: Bypass Feature-Level (SysAdmin)
|
|
3. Sonst: Prüft TrusteeAccess für User + Organisation + Contract
|
|
"""
|
|
|
|
# Stufe 1: System-RBAC
|
|
accessLevel = self.getRbacAccessLevel(modelClass, operation)
|
|
if accessLevel == AccessLevel.NONE:
|
|
return False
|
|
if accessLevel == AccessLevel.ALL:
|
|
return True # SysAdmin bypass
|
|
|
|
# Stufe 2: Feature-RBAC via TrusteeAccess
|
|
roles = self.getUserTrusteeRoles(self.userId, organisationId, contractId)
|
|
|
|
if "admin" in roles:
|
|
return True
|
|
if "operate" in roles and modelClass in (TrusteeContract, TrusteeDocument, ...):
|
|
return True
|
|
if "userreport" in roles and recordCreatedBy == self.userId:
|
|
return True
|
|
|
|
return False
|
|
```
|
|
|
|
### 3.3 Automatische Filterung
|
|
|
|
Die Methode `filterRecordsByTrusteeAccess()` filtert Ergebnisse basierend auf User-Zugriff:
|
|
|
|
```python
|
|
def filterRecordsByTrusteeAccess(self, records, modelClass):
|
|
"""
|
|
Filtert Records basierend auf TrusteeAccess des Users.
|
|
|
|
- admin: Sieht alle Records der Organisation
|
|
- operate: Sieht alle Records der Organisation
|
|
- userreport: Sieht nur eigene Records (_createdBy = userId)
|
|
"""
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Generisches Muster für andere Features
|
|
|
|
### 4.1 Feature-Isolation-Pattern
|
|
|
|
Jedes Feature kann sein eigenes Isolationsmodell definieren:
|
|
|
|
```
|
|
{Feature}Access: User → {Isolationsobjekt} → Role [→ Unter-Objekt]
|
|
```
|
|
|
|
### 4.2 Beispiele für andere Features
|
|
|
|
| Feature | Access-Tabelle | Isolationsobjekt | Unter-Objekt |
|
|
|---------|----------------|------------------|--------------|
|
|
| **Trustee** | `TrusteeAccess` | Organisation (Kunde) | Contract |
|
|
| **RealEstate** | `RealEstateAccess` | Property (Liegenschaft) | Unit (Wohnung) |
|
|
| **ProjectMgmt** | `ProjectAccess` | Project | - |
|
|
| **Accounting** | `AccountingAccess` | Company | FiscalYear |
|
|
|
|
### 4.3 Implementierungsvorlage
|
|
|
|
Für ein neues Feature mit Isolation:
|
|
|
|
1. **Datamodel**: `{Feature}Access` mit `userId`, `{isolationObject}Id`, `roleId`
|
|
2. **Interface**: `checkCombinedPermission()` und `filterRecordsByAccess()`
|
|
3. **Routes**: CRUD für `{Feature}Access`
|
|
4. **Roles**: Feature-spezifische Rollen definieren
|
|
|
|
---
|
|
|
|
## 5. API-Referenz für UI-Entwicklung
|
|
|
|
### 5.1 Access-Verwaltung (User-Zuweisung zu Kunden)
|
|
|
|
```
|
|
# Alle Access-Records abrufen
|
|
GET /api/trustee/{instanceId}/access
|
|
|
|
# Access für bestimmte Organisation
|
|
GET /api/trustee/{instanceId}/access/organisation/{orgId}
|
|
|
|
# Access für bestimmten User
|
|
GET /api/trustee/{instanceId}/access/user/{userId}
|
|
|
|
# Neuen Access erstellen (User zu Kunde zuweisen)
|
|
POST /api/trustee/{instanceId}/access
|
|
Body: {
|
|
"userId": "user-uuid",
|
|
"organisationId": "kunde-id",
|
|
"roleId": "operate",
|
|
"contractId": null // Optional
|
|
}
|
|
|
|
# Access aktualisieren
|
|
PUT /api/trustee/{instanceId}/access/{accessId}
|
|
|
|
# Access löschen
|
|
DELETE /api/trustee/{instanceId}/access/{accessId}
|
|
```
|
|
|
|
### 5.2 Options-Endpoints für Dropdowns
|
|
|
|
```
|
|
# Organisationen für Dropdown
|
|
GET /api/trustee/{instanceId}/organisations/options
|
|
→ [{ "value": "org-id", "label": "Kunde AG" }, ...]
|
|
|
|
# Rollen für Dropdown
|
|
GET /api/trustee/{instanceId}/roles/options
|
|
→ [{ "value": "admin", "label": "Administrator" }, ...]
|
|
|
|
# Contracts für Dropdown (dynamisch nach Organisation)
|
|
GET /api/trustee/{instanceId}/contracts/options
|
|
→ [{ "value": "contract-uuid", "label": "Vertrag 2026" }, ...]
|
|
|
|
# Users für Dropdown (aus aktuellem Mandate)
|
|
GET /api/users/options
|
|
→ [{ "value": "user-uuid", "label": "Max Muster" }, ...]
|
|
```
|
|
|
|
---
|
|
|
|
## 6. UI-Anforderungen
|
|
|
|
### 6.1 Access-View (User-Verwaltung)
|
|
|
|
Eine View zur Verwaltung der User-Zuweisungen:
|
|
|
|
| Spalte | Typ | Quelle |
|
|
|--------|-----|--------|
|
|
| User | Select | `/api/users/options` |
|
|
| Organisation | Select | `/api/trustee/{instanceId}/organisations/options` |
|
|
| Rolle | Select | `/api/trustee/{instanceId}/roles/options` |
|
|
| Contract | Select (optional) | `/api/trustee/{instanceId}/contracts/options` |
|
|
|
|
**Hinweis**: Das `contractId` Dropdown sollte dynamisch nach Auswahl der Organisation gefiltert werden.
|
|
|
|
### 6.2 Berechtigungen für Access-View
|
|
|
|
- **Sichtbar für**: Feature-Admins (`admin` Rolle in mindestens einer Organisation)
|
|
- **Create/Update/Delete**: Nur für Organisationen, in denen User Admin ist
|
|
|
|
---
|
|
|
|
## 7. Offene Punkte / Entscheidungen
|
|
|
|
### 7.1 Geklärt
|
|
|
|
- [x] Ist das Gateway bereit? → **Ja**
|
|
- [x] Sind Feature-spezifische Rollen gleich wie System-Rollen? → **Nein, bewusst getrennt**
|
|
- [x] Ist zweistufiges RBAC nötig? → **Ja, bereits implementiert**
|
|
- [x] Existiert der Users-Options-Endpoint? → **Ja, `/api/users/options`**
|
|
|
|
### 7.2 Offen für UI-Review
|
|
|
|
- [ ] Soll die Access-View eine eigene Seite sein oder Tab in Organisation?
|
|
- [ ] Wie wird Contract-Dropdown bei organisationId-Änderung aktualisiert?
|
|
- [ ] Braucht es eine "Bulk-Zuweisung" (User zu mehreren Orgs gleichzeitig)?
|
|
|
|
---
|
|
|
|
## 8. Zusammenfassung
|
|
|
|
### Das Gateway bietet:
|
|
|
|
1. **Vollständige CRUD-Routes** für alle Trustee-Entities
|
|
2. **Zwei-Stufen-RBAC** (System + Feature-Level)
|
|
3. **Automatische Filterung** basierend auf User-Access
|
|
4. **Options-Endpoints** für alle Dropdowns
|
|
|
|
### Das UI muss implementieren:
|
|
|
|
1. **Access-View** zur Verwaltung von User-Zuweisungen
|
|
2. **Dynamische Dropdowns** mit Abhängigkeiten (Contract nach Organisation)
|
|
3. **instanceId** in allen API-Calls verwenden
|
|
|
|
### Architektur-Prinzip:
|
|
|
|
```
|
|
System-RBAC → Zugang zum Feature
|
|
Feature-RBAC → Isolation innerhalb des Features
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Implementierungsplan: Fehlende Elemente
|
|
|
|
### 9.1 Aktueller Stand (Analyse)
|
|
|
|
| Komponente | Status | Details |
|
|
|------------|--------|---------|
|
|
| **Gateway (Backend)** | ✅ Komplett | Routes, Interface, Datamodel, RBAC |
|
|
| **API Hooks** (`useTrustee.ts`) | ✅ Komplett | CRUD-Hooks für alle Entities |
|
|
| **API Client** (`trusteeApi.ts`) | ✅ Komplett | Alle Endpoints implementiert |
|
|
| **Views (Grundgerüst)** | ⚠️ Teilweise | Tabellen existieren, aber ohne Forms |
|
|
| **Create/Edit Formulare** | ❌ Fehlt | Buttons vorhanden, aber ohne Funktion |
|
|
| **Label-Auflösung** | ❌ Fehlt | Zeigt IDs statt Labels (userId, orgId) |
|
|
| **Dynamische Dropdowns** | ❌ Fehlt | Contract abhängig von Organisation |
|
|
|
|
### 9.2 Fehlende Implementierungen
|
|
|
|
#### P1: Create/Edit Formulare für alle Views
|
|
|
|
**Problem:** Die "Neu erstellen" und "Bearbeiten" Buttons existieren, aber öffnen kein Formular.
|
|
|
|
**Betroffene Views:**
|
|
- `TrusteeOrganisationsView.tsx`
|
|
- `TrusteeRolesView.tsx`
|
|
- `TrusteeAccessView.tsx`
|
|
- `TrusteeContractsView.tsx`
|
|
- `TrusteeDocumentsView.tsx`
|
|
- `TrusteePositionsView.tsx`
|
|
|
|
**Lösung:** Pro View ein Modal mit EditForm implementieren, das die automatisch generierten Felder aus `generateEditFieldsFromAttributes()` verwendet.
|
|
|
|
**Beispiel-Pattern:**
|
|
|
|
```tsx
|
|
// In TrusteeOrganisationsView.tsx
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [editingItem, setEditingItem] = useState<TrusteeOrganisation | null>(null);
|
|
const { generateCreateFieldsFromAttributes, generateEditFieldsFromAttributes } = useTrusteeOrganisations();
|
|
const { handleCreate, handleUpdate } = useTrusteeOrganisationOperations();
|
|
|
|
// Create-Button
|
|
<button onClick={() => { setEditingItem(null); setIsModalOpen(true); }}>
|
|
+ Neue Organisation
|
|
</button>
|
|
|
|
// Edit-Button
|
|
<button onClick={() => { setEditingItem(org); setIsModalOpen(true); }}>
|
|
✏️
|
|
</button>
|
|
|
|
// Modal mit EditForm
|
|
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
|
|
<EditForm
|
|
fields={editingItem ? generateEditFieldsFromAttributes() : generateCreateFieldsFromAttributes()}
|
|
initialData={editingItem || {}}
|
|
onSave={async (data) => {
|
|
if (editingItem) {
|
|
await handleUpdate(editingItem.id, data);
|
|
} else {
|
|
await handleCreate(data);
|
|
}
|
|
setIsModalOpen(false);
|
|
refetch();
|
|
}}
|
|
/>
|
|
</Modal>
|
|
```
|
|
|
|
---
|
|
|
|
#### P2: Label-Auflösung für Foreign Keys
|
|
|
|
**Problem:** `TrusteeAccessView` zeigt:
|
|
- `userId: "uuid-123"` statt `"Max Muster"`
|
|
- `organisationId: "org-1"` statt `"Kunde AG"`
|
|
- `roleId: "admin"` statt `"Administrator"`
|
|
|
|
**Lösung:** Options aus den entsprechenden Endpoints laden und als Lookup-Map verwenden.
|
|
|
|
**Implementierung:**
|
|
|
|
```tsx
|
|
// In TrusteeAccessView.tsx
|
|
const [userOptions, setUserOptions] = useState<{value: string, label: string}[]>([]);
|
|
const [orgOptions, setOrgOptions] = useState<{value: string, label: string}[]>([]);
|
|
const [roleOptions, setRoleOptions] = useState<{value: string, label: string}[]>([]);
|
|
|
|
useEffect(() => {
|
|
const loadOptions = async () => {
|
|
const [users, orgs, roles] = await Promise.all([
|
|
api.get('/api/users/options'),
|
|
api.get(`/api/trustee/${instanceId}/organisations/options`),
|
|
api.get(`/api/trustee/${instanceId}/roles/options`)
|
|
]);
|
|
setUserOptions(users.data);
|
|
setOrgOptions(orgs.data);
|
|
setRoleOptions(roles.data);
|
|
};
|
|
loadOptions();
|
|
}, [instanceId]);
|
|
|
|
// Lookup-Funktion
|
|
const getLabel = (options: {value: string, label: string}[], value: string) =>
|
|
options.find(o => o.value === value)?.label || value;
|
|
|
|
// In der Tabelle
|
|
<td>{getLabel(userOptions, access.userId)}</td>
|
|
<td>{getLabel(orgOptions, access.organisationId)}</td>
|
|
<td>{getLabel(roleOptions, access.roleId)}</td>
|
|
```
|
|
|
|
---
|
|
|
|
#### P3: Dynamische Dropdown-Filterung
|
|
|
|
**Problem:** Im AccessView/ContractView soll das Contract-Dropdown nur Contracts der ausgewählten Organisation zeigen.
|
|
|
|
**Lösung:** `dependsOn`-Attribut im Datamodel nutzen + dynamisches Nachladen.
|
|
|
|
**Implementierung im EditForm:**
|
|
|
|
```tsx
|
|
// Wenn organisationId geändert wird, Contracts neu laden
|
|
const handleFieldChange = async (fieldKey: string, value: any) => {
|
|
setFormData(prev => ({ ...prev, [fieldKey]: value }));
|
|
|
|
// Dynamische Abhängigkeiten prüfen
|
|
const dependentFields = fields.filter(f => f.dependsOn === fieldKey);
|
|
for (const depField of dependentFields) {
|
|
if (depField.optionsReference?.includes('contracts')) {
|
|
// Contracts für diese Organisation laden
|
|
const contracts = await api.get(
|
|
`/api/trustee/${instanceId}/contracts/options?organisationId=${value}`
|
|
);
|
|
// Options aktualisieren...
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
**Backend-Erweiterung nötig:** Der `/contracts/options` Endpoint muss einen `organisationId` Query-Parameter unterstützen.
|
|
|
|
---
|
|
|
|
#### P4: Generischer Options-Hook
|
|
|
|
**Problem:** Options-Laden ist in jeder View dupliziert.
|
|
|
|
**Lösung:** Zentraler Hook für Trustee-Options.
|
|
|
|
```tsx
|
|
// hooks/useTrusteeOptions.ts
|
|
export function useTrusteeOptions() {
|
|
const instanceId = useInstanceId();
|
|
const [options, setOptions] = useState<TrusteeOptionsMap>({});
|
|
|
|
const loadOptions = useCallback(async (entity: 'organisations' | 'roles' | 'contracts' | 'users') => {
|
|
if (entity === 'users') {
|
|
const res = await api.get('/api/users/options');
|
|
setOptions(prev => ({ ...prev, users: res.data }));
|
|
} else {
|
|
const res = await api.get(`/api/trustee/${instanceId}/${entity}/options`);
|
|
setOptions(prev => ({ ...prev, [entity]: res.data }));
|
|
}
|
|
}, [instanceId]);
|
|
|
|
const getLabel = useCallback((entity: string, value: string) => {
|
|
return options[entity]?.find(o => o.value === value)?.label || value;
|
|
}, [options]);
|
|
|
|
return { options, loadOptions, getLabel };
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 9.3 Implementierungsreihenfolge
|
|
|
|
| Phase | Aufgabe | Aufwand | Priorität |
|
|
|-------|---------|---------|-----------|
|
|
| **1** | `useTrusteeOptions` Hook erstellen | 2h | Hoch |
|
|
| **2** | Label-Auflösung in allen Views | 3h | Hoch |
|
|
| **3** | Create/Edit Modal in `TrusteeOrganisationsView` | 3h | Hoch |
|
|
| **4** | Create/Edit Modal für alle anderen Views kopieren | 4h | Mittel |
|
|
| **5** | Dynamische Contract-Filterung | 2h | Mittel |
|
|
| **6** | Backend: `/contracts/options?organisationId=` | 1h | Mittel |
|
|
| **7** | Position-Document Verknüpfungs-UI | 4h | Niedrig |
|
|
| **8** | Testing & Bugfixes | 4h | Hoch |
|
|
|
|
**Geschätzter Gesamtaufwand:** ~23h (3-4 Arbeitstage)
|
|
|
|
---
|
|
|
|
### 9.4 Detailplan Phase 1-3 (MVP)
|
|
|
|
#### Schritt 1: `useTrusteeOptions` Hook
|
|
|
|
**Datei:** `frontend_nyla/src/hooks/useTrusteeOptions.ts`
|
|
|
|
```typescript
|
|
/**
|
|
* Hook für Trustee-Options (Dropdowns, Label-Auflösung)
|
|
*/
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
import api from '../api';
|
|
import { useInstanceId } from './useCurrentInstance';
|
|
|
|
interface Option {
|
|
value: string;
|
|
label: string;
|
|
}
|
|
|
|
interface TrusteeOptions {
|
|
users: Option[];
|
|
organisations: Option[];
|
|
roles: Option[];
|
|
contracts: Option[];
|
|
documents: Option[];
|
|
positions: Option[];
|
|
}
|
|
|
|
export function useTrusteeOptions(autoLoad: (keyof TrusteeOptions)[] = []) {
|
|
const instanceId = useInstanceId();
|
|
const [options, setOptions] = useState<Partial<TrusteeOptions>>({});
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const loadOptions = useCallback(async (
|
|
entities: (keyof TrusteeOptions)[],
|
|
filters?: { organisationId?: string }
|
|
) => {
|
|
if (!instanceId) return;
|
|
setLoading(true);
|
|
|
|
try {
|
|
const promises = entities.map(async (entity) => {
|
|
let url: string;
|
|
if (entity === 'users') {
|
|
url = '/api/users/options';
|
|
} else {
|
|
url = `/api/trustee/${instanceId}/${entity}/options`;
|
|
if (filters?.organisationId && entity === 'contracts') {
|
|
url += `?organisationId=${filters.organisationId}`;
|
|
}
|
|
}
|
|
const res = await api.get(url);
|
|
return { entity, data: res.data };
|
|
});
|
|
|
|
const results = await Promise.all(promises);
|
|
const newOptions: Partial<TrusteeOptions> = {};
|
|
results.forEach(({ entity, data }) => {
|
|
newOptions[entity] = data;
|
|
});
|
|
setOptions(prev => ({ ...prev, ...newOptions }));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [instanceId]);
|
|
|
|
const getLabel = useCallback((entity: keyof TrusteeOptions, value: string): string => {
|
|
return options[entity]?.find(o => o.value === value)?.label || value;
|
|
}, [options]);
|
|
|
|
// Auto-Load bei Mount
|
|
useEffect(() => {
|
|
if (autoLoad.length > 0 && instanceId) {
|
|
loadOptions(autoLoad);
|
|
}
|
|
}, [instanceId, autoLoad.join(',')]);
|
|
|
|
return { options, loadOptions, getLabel, loading };
|
|
}
|
|
```
|
|
|
|
#### Schritt 2: TrusteeAccessView mit Labels
|
|
|
|
**Datei:** `frontend_nyla/src/pages/views/trustee/TrusteeAccessView.tsx`
|
|
|
|
- Import `useTrusteeOptions`
|
|
- Auto-Load: `['users', 'organisations', 'roles']`
|
|
- Tabelle: `getLabel()` für userId, organisationId, roleId
|
|
|
|
#### Schritt 3: Create/Edit Modal
|
|
|
|
**Ansatz:** Bestehende `EditPopup`-Komponente aus anderen Views (z.B. Prompts) wiederverwenden.
|
|
|
|
**Dateien:**
|
|
- `TrusteeOrganisationsView.tsx` - Modal hinzufügen
|
|
- Nutzt `generateCreateFieldsFromAttributes()` für Feld-Definitionen
|
|
- `handleCreate()` / `handleUpdate()` für API-Calls
|
|
|
|
---
|
|
|
|
### 9.5 Backend-Erweiterung (optional)
|
|
|
|
Für dynamische Contract-Filterung nach Organisation:
|
|
|
|
**Datei:** `gateway/modules/features/trustee/routeFeatureTrustee.py`
|
|
|
|
```python
|
|
@router.get("/{instanceId}/contracts/options", response_model=List[Dict[str, Any]])
|
|
async def getContractOptions(
|
|
request: Request,
|
|
instanceId: str = Path(...),
|
|
organisationId: Optional[str] = Query(None), # NEU
|
|
context: RequestContext = Depends(getRequestContext)
|
|
) -> List[Dict[str, Any]]:
|
|
"""Get contract options, optionally filtered by organisation."""
|
|
mandateId = await _validateInstanceAccess(instanceId, context)
|
|
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
|
|
|
if organisationId:
|
|
contracts = interface.getContractsByOrganisation(organisationId)
|
|
else:
|
|
result = interface.getAllContracts(None)
|
|
contracts = result.items if hasattr(result, 'items') else result
|
|
|
|
return [{"value": c.id, "label": c.label or c.name or c.id} for c in contracts]
|
|
```
|
|
|
|
---
|
|
|
|
### 9.6 Offene Entscheidungen für Review
|
|
|
|
| # | Frage | Optionen |
|
|
|---|-------|----------|
|
|
| 1 | **Modal vs. Inline-Edit** | A) Modal-Dialog / B) Inline in Tabelle / C) Separate Seite |
|
|
| 2 | **Bulk-Operationen** | A) Ja, Multi-Select + Batch-Aktionen / B) Nein, nur einzeln |
|
|
| 3 | **Position-Document UI** | A) Separate View / B) Inline in Position-View / C) Beides |
|
|
| 4 | **Validierung** | A) Client-only / B) Server-only / C) Beides |
|
|
|
|
---
|
|
|
|
**Dokumentversion**: 1.1
|
|
**Letzte Aktualisierung**: 2026-01-23
|
|
**Autor**: Claude (AI-Assistent)
|
|
**Status**: Zur Überprüfung
|