# FormGenerator -- Referenz
## Uebersicht
Der FormGenerator besteht aus mehreren Komponenten:
| Komponente | Datei | Zweck |
|------------|-------|-------|
| `FormGeneratorTable` | `components/FormGenerator/FormGeneratorTable/FormGeneratorTable.tsx` | Generische Datentabelle mit Pagination, Sortierung, Filterung, Suche, Selektion, Bulk-Actions |
| `FormGeneratorControls` | `components/FormGenerator/FormGeneratorControls/FormGeneratorControls.tsx` | Toolbar: Suche, Pagination, Batch-Action-Buttons, "Select All Filtered"-Banner |
| `FormGeneratorForm` | `components/FormGenerator/FormGeneratorForm/FormGeneratorForm.tsx` | Dynamische Formulare aus Backend-Attribut-Definitionen |
| `FormGeneratorList` | `components/FormGenerator/FormGeneratorList/FormGeneratorList.tsx` | Listen-Darstellung |
Diese Referenz fokussiert auf **FormGeneratorTable** und das zugehoerige Backend-API-Pattern.
---
## Page Layout Chain (Pflicht)
`FormGeneratorTable` rendert intern mit `flex:1; min-height:0; overflow:hidden`. Damit die Tabelle die volle Seitenbreite/-hoehe ausnuetzt und nicht abgeschnitten wird, **muss** die Eltern-Hierarchie eine **bounded height chain** liefern. Andernfalls passiert eines davon:
- Tabelle ist 0 px hoch (kollabiert) -- nur Toolbar sichtbar.
- Tabelle waechst ueber den Viewport hinaus, horizontaler Scroll fehlt -- letzte Spalten werden abgeschnitten.
- Page-Container hat `max-width` -> Tabelle ist auf z.B. 800 px begrenzt.
### Korrektes Pattern (verwendet PromptsPage, TrusteePositionsView, TrusteeDataTablesView)
```tsx
import adminStyles from '../../admin/Admin.module.css';
export const MyPage: React.FC = () => (
);
```
Die drei CSS-Klassen aus `frontend_nyla/src/pages/admin/Admin.module.css`:
| Klasse | Wirkung |
|--------|---------|
| `.adminPage` | `display:flex; flex-direction:column; width:100%; box-sizing:border-box; flex:0 0 auto` (Default fuer Standard-Seiten ohne Tabelle) |
| `.adminPageFill` | `flex:1 1 auto; min-height:0; overflow:hidden` -- aktiviert die bounded height chain |
| `.tableContainer` | `flex:1; min-height:0; overflow:hidden; display:flex; flex-direction:column` -- direktes Eltern-Element der Tabelle |
`FeatureView`/`MainLayout` liefern bereits `height:100%` mit `min-height:0` -- Page-Komponenten brauchen also nur `adminPage adminPageFill` und einen `tableContainer` direkt um die `FormGeneratorTable`.
### Bei Tabs / Wrapper-Komponenten
Wenn die Tabelle in einem Tab-Wrapper (z.B. `TrusteeDataTab`) gemountet wird, muss **jeder** Wrapper die Flex-Chain weiterreichen. Pattern:
```tsx
const _rootStyle: React.CSSProperties = {
display: 'flex',
flexDirection: 'column',
flex: 1,
minHeight: 0,
width: '100%',
};
const _tableWrapStyle: React.CSSProperties = {
flex: 1,
minHeight: 0,
display: 'flex',
flexDirection: 'column',
width: '100%',
};
```
Eine Toolbar oberhalb der Tabelle bekommt `flexShrink: 0`, der Tabellenwrapper `flex: 1; minHeight: 0`.
### Anti-Patterns (verursachen das "Tabelle abgeschnitten"-Bug)
- Outer-Wrapper mit `max-width: 800px` (z.B. `.expenseImportSection` aus `TrusteeViews.module.css`) -- begrenzt die Breite.
- Outer `` ohne `flex:1; min-height:0` -- Tabelle kollabiert auf 0 px Hoehe.
- `.adminPage` ohne `.adminPageFill` -- Default ist `flex:0 0 auto`, das innere `.tableContainer flex:1` hat keinen Platz.
- Padding auf einem Zwischen-Wrapper -- bricht das Box-Sizing der Chain.
---
## FormGeneratorTable -- Props
### Datenanbindung
| Prop | Typ | Beschreibung |
|------|-----|-------------|
| `data` | `T[]` | Anzuzeigende Datensaetze (aktuelle Seite) |
| `columns` | `ColumnConfig[]` | Spaltendefinitionen (key, label, type, sortable, filterable, searchable, filterOptions, width) |
| `apiEndpoint` | `string?` | Backend-Endpunkt fuer automatische Filter-/ID-Abfragen |
| `hookData` | `{ refetch, pagination, fetchFilterValues? }` | Daten-Hook mit Refetch-Callback und Pagination-Metadata |
| `supportsBackendPagination` | `boolean` | Aktiviert serverseitige Pagination, Sortierung und Filterung |
### Interaktion
| Prop | Typ | Beschreibung |
|------|-----|-------------|
| `selectable` | `boolean` | Aktiviert Zeilen-Selektion (Checkboxen) |
| `onSelectionChange` | `(selectedIds: Set) => void` | Callback bei Aenderung der Selektion |
| `idField` | `string` | Feld fuer eindeutige ID (Default: `"id"`) |
| `isRowSelectable` | `(row: T) => boolean` | Bestimmt, ob eine Zeile selektierbar ist |
| `batchActions` | `BatchAction[]` | Bulk-Aktionen fuer selektierte Zeilen |
| `actionButtons` | `ActionButton[]` | Zeilen-Aktionen (Edit, Delete etc.) |
### BatchAction Interface
```typescript
interface BatchAction {
label: string;
icon?: React.ReactNode;
loading?: boolean;
onClick: (selectedRows: T[]) => void | Promise;
isApplicable?: (row: T) => boolean;
}
```
`isApplicable` ermoeglicht pro Action zu bestimmen, welche der selektierten Zeilen fuer diese Aktion qualifizieren. Der Button zeigt dann `(applicableCount/totalSelected)` und ist `disabled` wenn `applicableCount === 0`.
---
## Unified Filter API
### Prinzip
Alle Daten-Endpunkte unterstuetzen drei Modi ueber Query-Parameter auf dem **gleichen** Endpunkt:
| Modus | Query-Parameter | Response | Zweck |
|-------|----------------|----------|-------|
| Normal (default) | `pagination={...}` | `{ items: T[], pagination: PaginationMetadata }` | Paginierte Datenliste |
| `filterValues` | `mode=filterValues&column=xxx&pagination={crossFilters}` | `string[]` | Distinct-Werte fuer Spaltenfilter-Dropdown |
| `ids` | `mode=ids&pagination={filters}` | `string[]` | Alle IDs der gefilterten Datensaetze |
**Kein separater `/filter-values`-Endpunkt.** Alle alten `/filter-values`-Sub-Pfade wurden entfernt (Stand 2026-04-13).
### Cross-Filtering
Bei `mode=filterValues` werden alle aktiven Filter **ausser** dem angeforderten Column-Filter angewendet (Cross-Filtering). So zeigt z.B. der Status-Filter nur Werte an, die bei den aktuell gesetzten Mandant- und Typ-Filtern vorkommen.
### Beispiele
```
# Normale Liste (Seite 1, 20 Eintraege)
GET /api/users/?pagination={"page":1,"pageSize":20,"sort":[{"field":"name","direction":"asc"}]}
# Filter-Werte fuer Spalte "status" (mit Cross-Filter auf mandateLabel)
GET /api/users/?mode=filterValues&column=status&pagination={"filters":{"mandateLabel":"Demo AG"}}
# Alle IDs der gefilterten Ansicht (fuer "Select All Filtered")
GET /api/users/?mode=ids&pagination={"filters":{"status":"active"},"search":"admin"}
```
### Backend-Implementierung
Zentrale Hilfsfunktionen in `gateway/modules/routes/routeHelpers.py`:
| Funktion | Zweck |
|----------|-------|
| `handleFilterValuesMode(db, modelClass, column, ...)` | SQL-basierte Distinct-Werte via DB-Connector |
| `handleIdsMode(db, modelClass, ...)` | SQL-basierte ID-Liste via DB-Connector |
| `handleFilterValuesInMemory(items, column, ...)` | In-Memory Distinct-Werte (fuer enriched/aggregierte Listen) |
| `handleIdsInMemory(items, ...)` | In-Memory ID-Liste |
| `parseCrossFilterPagination(column, paginationJson)` | Parsed Pagination-JSON und entfernt den angeforderten Column-Filter |
| `_applyFiltersAndSort(items, paginationParams)` | In-Memory Filterung und Sortierung |
| `_extractDistinctValues(items, column, ...)` | Distinct-Werte aus Item-Liste extrahieren |
| `paginateInMemory(items, paginationParams)` | In-Memory Paginierung |
### Route-Integration Pattern
```python
@router.get("/", response_model=PaginatedResponse[MyModel])
def list_items(
request: Request,
pagination: Optional[str] = Query(None),
mode: Optional[str] = Query(None, description="'filterValues' or 'ids'"),
column: Optional[str] = Query(None, description="Column key"),
context: RequestContext = Depends(getRequestContext),
):
if mode in ("filterValues", "ids"):
from modules.routes.routeHelpers import handleFilterValuesInMemory, handleIdsInMemory
items = _buildFullList(context)
if mode == "filterValues":
if not column:
raise HTTPException(status_code=400, detail="column required")
return handleFilterValuesInMemory(items, column, pagination)
return handleIdsInMemory(items, pagination)
# Normal paginated list...
```
---
## Selektion
### ID-basierte Selektion
Die Selektion arbeitet mit **IDs** (nicht Zeilen-Indizes). Das `idField`-Prop bestimmt, welches Feld als eindeutiger Schluessel verwendet wird (Default: `"id"`).
| State | Typ | Beschreibung |
|-------|-----|-------------|
| `selectedIds` | `Set` | Menge der selektierten IDs |
### Selektion-Reset
Die Selektion wird automatisch zurueckgesetzt bei:
- Seitenwechsel (currentPage)
- Seitengroesse-Aenderung (pageSize)
- Filter-Aenderung
- Suchbegriff-Aenderung
- Sortier-Aenderung
### Select All Filtered
Wenn `apiEndpoint` und `supportsBackendPagination` gesetzt sind, zeigt die Toolbar einen "Alle X Eintraege auswaehlen"-Button. Dieser ruft `GET {apiEndpoint}?mode=ids&pagination={currentFilters}` auf und fuegt alle zurueckgegebenen IDs zur Selektion hinzu.
Ein "Auswahl aufheben"-Button erscheint, wenn "Select All Filtered" aktiv ist.
### Row-Level Delete
Wenn eine Zeile geloescht wird (via `actionButtons` Delete), wird deren ID automatisch aus `selectedIds` entfernt.
---
## FilterValuesList (Spaltenfilter-Dropdown)
Wenn ein filterbarer Spalten-Header geklickt wird, laedt das Dropdown die Distinct-Werte:
1. `column.filterOptions` (statische Enum) -- wird direkt verwendet, kein Backend-Call
2. `hookData.fetchFilterValues(columnKey, crossFilters)` -- falls im Hook bereitgestellt
3. `GET {apiEndpoint}?mode=filterValues&column=xxx&pagination={currentFilters}` -- automatisch
Bei mehr als 10 Werten erscheint ein **Suchfeld** (Client-seitige Filterung der geladenen Werte).
Boolean-Spalten rendern als "Ja"/"Nein". Datum-Spalten rendern als Range-Picker.
---
## Migrierte Endpunkte (Stand 2026-04-13)
Alle folgenden Endpunkte unterstuetzen `mode=filterValues` und `mode=ids`:
### Core Routes
| Route-Datei | Endpunkt | Methode |
|-------------|----------|---------|
| `routeDataUsers.py` | `GET /api/users/` | SQL + In-Memory (je nach Scope) |
| `routeDataConnections.py` | `GET /api/connections/` | In-Memory |
| `routeDataPrompts.py` | `GET /api/prompts/` | In-Memory |
| `routeInvitations.py` | `GET /api/invitations/` | In-Memory |
| `routeSubscription.py` | `GET /api/subscriptions/admin/all` | In-Memory |
| `routeAdminFeatures.py` | `GET /api/admin/features/instances` | In-Memory |
| `routeAdminFeatures.py` | `GET /api/admin/features/templates/roles` | In-Memory |
| `routeAdminFeatures.py` | `GET /api/admin/features/instances/{id}/users` | In-Memory |
| `routeAdminRbacRules.py` | `GET /api/admin/rbac/roles` | In-Memory |
| `routeDataMandates.py` | `GET /api/mandates/` | SQL + In-Memory (je nach Scope) |
| `routeDataMandates.py` | `GET /api/mandates/{id}/users` | In-Memory |
| `routeDataFiles.py` | `GET /api/files/list` | SQL + In-Memory Fallback |
| `routeWorkflowDashboard.py` | `GET /api/automation/dashboard/` (Runs) | Enriched + SQL |
| `routeWorkflowDashboard.py` | `GET /api/automation/dashboard/workflows` | Enriched + SQL |
| `routeBilling.py` | `GET /api/billing/view/users/transactions` | Billing-Interface |
### Feature Routes
| Route-Datei | Endpunkt | Methode |
|-------------|----------|---------|
| `routeFeatureTrustee.py` | `GET /api/trustee/{id}/documents` | RBAC SQL + In-Memory Fallback |
| `routeFeatureTrustee.py` | `GET /api/trustee/{id}/positions` | RBAC SQL + In-Memory Fallback |
| `routeFeatureRealEstate.py` | `GET /api/realestate/{id}/projects` | In-Memory |
| `routeFeatureRealEstate.py` | `GET /api/realestate/{id}/parcels` | In-Memory |