From 97bef862765bacd3fa6da40879aa9f37ab8d4563 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Tue, 21 Apr 2026 00:50:21 +0200 Subject: [PATCH] google configs --- b-reference/frontend-nyla/formgenerator.md | 66 +++ .../2026-04-trustee-data-tables-page.md | 159 +++++++ ...4-period-picker-billing-audit-migration.md | 267 ++++++++++++ d-guides/google-oauth-setup.md | 20 +- d-guides/google-oauth-verification.md | 390 ++++++++++++++++++ d-guides/stripe-ch-vat.md | 155 +++++++ 6 files changed, 1047 insertions(+), 10 deletions(-) create mode 100644 c-work/2-build/2026-04-trustee-data-tables-page.md create mode 100644 c-work/4-done/2026-04-period-picker-billing-audit-migration.md create mode 100644 d-guides/google-oauth-verification.md create mode 100644 d-guides/stripe-ch-vat.md diff --git a/b-reference/frontend-nyla/formgenerator.md b/b-reference/frontend-nyla/formgenerator.md index eb41926..b7304fc 100644 --- a/b-reference/frontend-nyla/formgenerator.md +++ b/b-reference/frontend-nyla/formgenerator.md @@ -19,6 +19,72 @@ Diese Referenz fokussiert auf **FormGeneratorTable** und das zugehoerige Backend --- +## 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 diff --git a/c-work/2-build/2026-04-trustee-data-tables-page.md b/c-work/2-build/2026-04-trustee-data-tables-page.md new file mode 100644 index 0000000..fbcf072 --- /dev/null +++ b/c-work/2-build/2026-04-trustee-data-tables-page.md @@ -0,0 +1,159 @@ + + + + +# Trustee: Konsolidierte Daten-Tabellen-Seite + +## Beschreibung und Kontext + +Aktuell hat das Trustee-Feature zwei separate Top-Level-Seiten für Datensichten (`Positionen`, `Dokumente`). Die übrigen 11 Trustee-Tabellen (Stammdaten + Sync-Daten + Buchhaltungs-Config/Sync) sind im UI **nicht sichtbar** – nur als JSON-Export oder per AI-Agent erreichbar. Das ist intransparent: Anwender und Auditor:innen sehen die importierten Konten / Buchungen / Kontakte / Salden nirgends. + +Lösung: Eine neue konsolidierte Seite `Daten-Tabellen` mit einem Tab pro Trustee-Tabelle. Jeder Tab nutzt `FormGeneratorTable` mit Pagination, Sortierung, Filterung und Suche – exakt das Pattern, das `PromptsPage` und die bestehenden `TrusteePositionsView` / `TrusteeDocumentsView` etabliert haben. + +Business-Treiber: +- Transparenz für Treuhänder:innen: «Was wurde wirklich aus Bexio/RMA importiert?» +- Audit-Fähigkeit: Sync-Status und Sync-Snapshots direkt im UI prüfen. +- Vereinheitlichung: Konsistente UI über alle Tabellen statt zwei isolierter Spezialseiten. + +Risiko bei Verzicht: Anwender greifen via Export/Excel auf interne Daten zu (umständlich, fehleranfällig); Sync-Probleme bleiben unbemerkt. + +## Fokus und kritische Details + +- **FormGeneratorTable-Vertrag** (siehe `wiki/b-reference/frontend-nyla/formgenerator.md`): Jede Tabelle braucht (a) ein `apiEndpoint` mit Unified Filter API (`mode=filterValues|ids`), (b) einen Hook mit `refetch / pagination / attributes / permissions`, (c) saubere Spaltendefinitionen aus `getModelAttributeDefinitions(...)`. +- **`getModelAttributeDefinitions` ist bereits da** (`/api/trustee/{instanceId}/attributes/{entityType}`) – aber nur registriert für die **6 CRUD-Modelle** (`_TRUSTEE_ENTITY_MODELS` in `routeFeatureTrustee.py`, Zeile 195). Die 7 fehlenden Modelle (TrusteeData* + Accounting*) müssen in dieser Map ergänzt werden. +- **Sync-Tabellen sind read-only** (User-Entscheidung). Endpunkte für TrusteeData*/AccountingSync sind aktuell **nicht** als generische REST-CRUDs implementiert – nur Aggregat-Endpunkte (`/accounting/import-status`, `/accounting/sync-status`, `/accounting/export-data`). Es braucht **neue Read-Endpunkte** mit Pagination + Unified Filter API. +- **RBAC-Konsistenz**: Jede Tabelle hat in `mainTrustee.DATA_OBJECTS` einen `data.feature.trustee.`-Key. Der Hook (`_createTrusteeEntityHook`) löst Permissions via `data.feature.trustee.{entityName}` auf – das funktioniert bereits für die bestehenden 6 Modelle, muss für die neuen 7 ebenfalls greifen. +- **Backwards-Compat**: Die bestehenden Seiten `Positionen` und `Dokumente` bleiben **vorerst** erhalten (Navigation + Routes + RBAC). Erst wenn die neue Seite verifiziert ist, werden sie in einer Folge-Iteration entfernt. +- **Tab-State via URL** (`?tab=positions`): Konsistent mit `TrusteeImportProcessView`, `TrusteeAccountingSettingsView`, `TrusteeAbschlussView`. Erlaubt Deep-Links (z.B. Quick Actions / Notification-Links) auf konkrete Tabellen. +- **Performance**: Lazy-Mount pro Tab (Hook ruft nur Daten, wenn Tab aktiv ist) – sonst werden 13 Tabellen parallel geladen. +- **i18n-Pflicht**: Alle Tab-Labels via `t(...)`. Spalten-Labels kommen aus dem Backend-`@i18nModel`. +- **Hidden System-Felder**: `mandateId`, `featureInstanceId`, technische `id`-UUIDs sollen tabellenweit hidden sein (gleiche Liste wie in PromptsPage `hiddenColumns`). +- **Naming**: Funktionen `_`-Prefix für intern (`_buildEntityHookConfig`, `_renderTabBar`); camelCase überall. + +## Ziel und Nicht-Ziele + +- **Ziel**: Neue Seite `Daten-Tabellen` (URL: `/mandates/{mandateId}/trustee/{instanceId}/data-tables[?tab=]`) mit einem Tab pro Trustee-Tabelle. Jeder Tab nutzt `FormGeneratorTable` mit Pagination/Sort/Filter/Search/Inline-Edit (für CRUD-Tabellen) bzw. read-only (für Sync-Tabellen). +- **Ziel**: Backend liefert Attribute & paginierte Daten via Unified Filter API auch für die 7 bisher UI-losen Tabellen. +- **Ziel**: Tab-Sichtbarkeit folgt RBAC (`data.feature.trustee.` view-Permission). +- **Ziel**: Konsistentes UX-Pattern mit `PromptsPage` (Header / Refresh / FormGeneratorTable / optional Edit-Modal via FormGeneratorForm für CRUD-Tabellen). +- **Explizit NICHT**: Entfernen der alten `Positionen`/`Dokumente`-Seiten in dieser Iteration (Folge-Plan). Routen + Quick Actions + RBAC bleiben unangetastet. +- **Explizit NICHT**: Inline-Edit oder Bulk-Sync für TrusteeData*-Tabellen – sie sind reine Sync-Spiegel. +- **Explizit NICHT**: Neuer DB-Schema-Wandel oder Migration – nur Read-Endpunkte ergänzen. +- **Explizit NICHT**: Beleg-Download-Spalte oder Sync-Status-Spalte im Positions-Tab – die Spezialdarstellung bleibt der eigenständigen `TrusteePositionsView` vorbehalten (Generic-Tab zeigt das «rohe» Modell). + +## Betroffene Module + +- **Gateway**: + - `gateway/modules/features/trustee/routeFeatureTrustee.py`: `_TRUSTEE_ENTITY_MODELS` um 7 Modelle erweitern; neue Read-Endpunkte mit Pagination + Unified Filter API für `TrusteeDataAccount`, `TrusteeDataJournalEntry`, `TrusteeDataJournalLine`, `TrusteeDataContact`, `TrusteeDataAccountBalance`, `TrusteeAccountingConfig`, `TrusteeAccountingSync`. + - `gateway/modules/features/trustee/mainTrustee.py`: `UI_OBJECTS` um `ui.feature.trustee.data-tables` ergänzen; `TEMPLATE_ROLES.accessRules` der relevanten Rollen erweitern. +- **Frontend**: + - **Neu**: `frontend_nyla/src/pages/views/trustee/TrusteeDataTablesView.tsx` (Container mit Tab-Bar + URL-Sync). + - **Neu**: `frontend_nyla/src/pages/views/trustee/dataTables/TrusteeDataTab.tsx` (generischer Tab-Body mit FormGeneratorTable für CRUD- und Read-only-Modi). + - **Erweitern**: `frontend_nyla/src/hooks/useTrustee.ts` um Hooks für die 7 neuen Modelle (über die bestehenden Factories `_createTrusteeEntityHook` / `_createTrusteeOperationsHook` – read-only Variante für Sync-Tabellen). + - **Erweitern**: `frontend_nyla/src/api/trusteeApi.ts` um `fetchTrusteeData*`-Funktionen. + - **Anpassen**: `frontend_nyla/src/pages/FeatureView.tsx` (`VIEW_COMPONENTS.trustee` um `'data-tables': TrusteeDataTablesView` ergänzen). + - **Anpassen**: `frontend_nyla/src/pages/views/trustee/index.ts` (Export). + - **Anpassen**: `frontend_nyla/src/App.tsx` (Route `data-tables` registrieren). + - **Optional**: `frontend_nyla/src/types/mandate.ts` (`FEATURE_REGISTRY.trustee.views`) um `data-tables` ergänzen, falls noch genutzt. +- **DB-Migration**: nein. +- **Andere Komponenten**: keine. + +## Entscheidungen + +| Datum | Entscheidung | Begründung | +|-------|-------------|------------| +| 2026-04-20 | Seitenname: `Daten-Tabellen` | User-Entscheidung; klar, präzise, dt. | +| 2026-04-20 | Alte Seiten `Positionen` + `Dokumente` bleiben vorerst | User-Entscheidung; Sicherheitsnetz bis neue Seite verifiziert ist – Aufräumen in Folge-Plan. | +| 2026-04-20 | Sync-Tabellen (`TrusteeData*`, `TrusteeAccountingSync`, `TrusteeAccountingConfig`) sind read-only im UI | User-Entscheidung; Daten kommen aus externem System – Edits würden bei nächstem Sync überschrieben. | +| 2026-04-20 | Alle 13 Tabellen erhalten einen Tab (User wählte "alle" – im Datenmodell sind es 13: 4 Stammdaten + 2 CRUD-Daten + 5 Sync-Daten + 2 Accounting). | User-Entscheidung; vollständige Transparenz. | +| 2026-04-20 | Tab-State via URL-Param `?tab=` | Konsistent mit allen anderen Trustee-Tab-Seiten. | +| 2026-04-20 | Lazy-Mount pro Tab (kein Pre-Fetch der inaktiven Tabs) | Performance: 13 paginierte Endpunkte parallel laden ist unnötig. | +| 2026-04-20 | Generische Tab-Komponente (`TrusteeDataTab`) statt 13 spezialisierter Views | DRY; Spezialisierungen (Beleg-Download/Sync-Status-Spalte) bleiben ausschliesslich in `TrusteePositionsView` / `TrusteeDocumentsView`. | +| 2026-04-20 | Keine Bulk-Aktionen im neuen Tab (kein Sync-Button etc.) | Bulk-Sync ist im Positions-Tab schon vorhanden – Generic-Seite bleibt schlank. | + +## Umsetzungs-Checkliste + +### Backend (Gateway) + +- [x] `_TRUSTEE_ENTITY_MODELS` in `routeFeatureTrustee.py` um die 7 neuen Modelle erweitert. +- [x] Neue paginierte Read-Endpunkte (analog `get_documents`/`get_positions` mit `_handleDocumentMode`-Pattern) für jede der 7 Tabellen: + - `GET /api/trustee/{instanceId}/data/accounts` + - `GET /api/trustee/{instanceId}/data/journal-entries` + - `GET /api/trustee/{instanceId}/data/journal-lines` + - `GET /api/trustee/{instanceId}/data/contacts` + - `GET /api/trustee/{instanceId}/data/account-balances` + - `GET /api/trustee/{instanceId}/accounting/configs` + - `GET /api/trustee/{instanceId}/accounting/syncs` +- [x] Helper extrahiert: `_paginatedReadEndpoint(...)` haelt Logik fuer alle 7 Endpunkte zentral. +- [x] RBAC: jeder neue Endpunkt prüft via `_validateInstanceAccess`; Datenzugriff wird auf SQL-Ebene durch `getRecordsetPaginatedWithRBAC` (DATA-Context, `data.feature.trustee.`) gefiltert – identisch zum bestehenden Documents/Positions-Pattern. +- [x] `mainTrustee.py`: Neuer UI-Object-Eintrag `ui.feature.trustee.data-tables` (Label «Daten-Tabellen»). +- [x] `mainTrustee.py`: AccessRule fuer `ui.feature.trustee.data-tables` bei `trustee-viewer`, `trustee-user`, `trustee-accountant` ergaenzt; Admin hat Wildcard. + +### Frontend + +- [x] `frontend_nyla/src/api/trusteeApi.ts`: 7 neue Read-Funktionen (`fetchDataAccounts`, `fetchDataJournalEntries`, `fetchDataJournalLines`, `fetchDataContacts`, `fetchDataAccountBalances`, `fetchAccountingConfigs`, `fetchAccountingSyncs`) plus passende Typen. +- [x] `frontend_nyla/src/hooks/useTrustee.ts`: 7 neue Read-only-Hooks via `_createTrusteeEntityHook` (`_buildReadOnlyConfig` injiziert no-op-Mutatoren). +- [x] **Neu** `frontend_nyla/src/pages/views/trustee/dataTables/TrusteeDataTab.tsx` – generischer Tab-Body, bindet `FormGeneratorTable` ein, respektiert `readOnly`. +- [x] **Neu** `frontend_nyla/src/pages/views/trustee/TrusteeDataTablesView.tsx`: + - 13 Tab-Defs, jede mit eigenem Wrapper-Component, das den passenden Hook aufruft. + - URL-Sync via `?tab=`, Lazy-Mount (nur aktiver Wrapper rendert -> kein Datenfetch fuer inaktive Tabs). + - Tab-Bar im Stil von `TrusteeAbschlussView`/`TrusteeImportProcessView`; Sync-Tabs zeigen «(read-only)»-Hinweis. + - RBAC-Filter im Frontend deaktiviert: einzelne Tab-Hooks blockieren Datenzugriff serverseitig (vermeidet sync-Permission-Lookups; spaetere Iteration kann Tab-Hiding nachziehen). +- [x] `frontend_nyla/src/pages/views/trustee/index.ts`: Export ergaenzt. +- [x] `frontend_nyla/src/pages/FeatureView.tsx`: Mapping `'data-tables': TrusteeDataTablesView` + Import. +- [x] `frontend_nyla/src/App.tsx`: Route `` ergaenzt. +- [x] `frontend_nyla/src/types/mandate.ts`: `FEATURE_REGISTRY.trustee.views` um `'data-tables'` ergaenzt. + +### Cross-Cutting + +- [ ] RBAC / Permissions: alle neuen Endpunkte enforced via bestehendes `_validateInstanceAccess`; UI-Sichtbarkeit per `data.feature.trustee.`-Check (Hook liefert das via `permissions.view`). +- [ ] Neutralisierung betroffen? **Nein** – keine AI-Calls, kein neuer Datenfluss zu externen Systemen. +- [ ] Navigation / Routing: neue UI-Route `/mandates/{m}/trustee/{i}/data-tables[?tab=]`. +- [ ] Billing-Impact? **Nein**. +- [ ] i18n: alle Tab-Labels und Header-Strings via `t(...)`; Spalten-Labels kommen aus `@i18nModel`-getaggten Modellen (bereits vorhanden für alle 13). +- [ ] Quick Actions: keine Änderung – Quick Actions zeigen weiterhin auf `import-process` / `settings`. Optional in Folge-Iteration: Direktlinks auf einzelne Datentabs. + +## Akzeptanzkriterien + +| # | Kriterium (Given-When-Then) | Prio | +|---|----------------------------|------| +| 1 | Given ein Trustee-Admin auf einer Trustee-Instanz, When er auf Navigation «Daten-Tabellen» klickt, Then erscheint eine Seite mit Tab-Bar und allen 13 Trustee-Tabellen als Tabs. | must | +| 2 | Given die Seite «Daten-Tabellen» mit Default-Tab, When der User sie öffnet, Then wird sofort der erste Tab geladen (FormGeneratorTable mit Daten, Pagination, Spalten aus `attributes`-API). | must | +| 3 | Given ein Tab mit Sync-Daten (z.B. «Buchungen (Sync)»), When der User die Zeile inspiziert, Then sind keine Edit-/Delete-Buttons sichtbar (read-only). | must | +| 4 | Given ein Tab mit CRUD-Daten (z.B. «Position»), When der User auf Edit klickt, Then erscheint ein FormGeneratorForm-Modal und Speichern persistiert via PUT-Endpunkt. | must | +| 5 | Given die Seite «Daten-Tabellen», When der User auf Tab «Kontakte (Sync)» wechselt, Then ändert sich die URL auf `?tab=contacts` und nur der Kontakte-Tab ist gemountet (Network-Tab zeigt nur den Kontakte-API-Call). | must | +| 6 | Given ein Tab mit aktiviertem `apiEndpoint`, When der User in einer Spalte einen Filter setzt, Then werden Distinct-Werte vom Backend via `?mode=filterValues&column=` geladen. | must | +| 7 | Given ein User OHNE Permission `data.feature.trustee.TrusteeDataContact` (view), When er die Seite öffnet, Then ist der Tab «Kontakte (Sync)» nicht sichtbar oder disabled. | should | +| 8 | Given die Seite mit Deep-Link `?tab=accounts`, When der User per URL einsteigt, Then wird der Konten-Tab direkt aktiv geladen. | should | +| 9 | Given die alten Seiten «Positionen»/«Dokumente», When der User sie aufruft, Then funktionieren sie unverändert weiter (kein Regress). | must | +| 10 | Given ein Tab mit grosser Datenmenge (>1'000 Einträge), When der User scrollt, Then wird Server-Pagination angewandt (page-Size 25, kein Frontend-Speicher-Overload). | must | +| 11 | Given Tabellen-Sortierung, When der User auf einen Spalten-Header klickt, Then wird per `pagination.sort` serverseitig sortiert. | must | + +## Testplan + +| ID | AC | Art | Automatisiert | Repo-Pfad | Status | +|----|----|-----|--------------|-----------|--------| +| T1 | 1 | manual UI | nein | – | pending | +| T2 | 2,5 | api | ja | gateway/tests/test_routeFeatureTrustee_data_tables.py | pending | +| T3 | 3,4 | api | ja | gateway/tests/test_routeFeatureTrustee_data_tables.py | pending | +| T4 | 6 | api | ja | gateway/tests/test_routeFeatureTrustee_data_tables.py (Unified Filter API: mode=filterValues / mode=ids) | pending | +| T5 | 7 | api | ja | gateway/tests/test_routeFeatureTrustee_data_tables.py (RBAC-Gate) | pending | +| T6 | 8 | manual UI | nein | – | pending | +| T7 | 9 | manual + e2e | teils | bestehende Position/Document Tests laufen weiter | pending | +| T8 | 10,11 | api | ja | gateway/tests/test_routeFeatureTrustee_data_tables.py (Pagination/Sort) | pending | + +## Links + +- PR: – +- Issue: – +- Vorlage: `wiki/b-reference/frontend-nyla/formgenerator.md` +- Vorbild-Seite: `frontend_nyla/src/pages/basedata/PromptsPage.tsx` +- Vorbild-Tabs: `frontend_nyla/src/pages/views/trustee/TrusteeAbschlussView.tsx`, `TrusteeAccountingSettingsView.tsx` + +## Abschluss + +- [ ] `b-reference/gateway/features/trustee.md` (oder analog) aktualisieren: neue Endpunkte + neue UI-Seite dokumentieren. +- [ ] `b-reference/frontend-nyla/architecture.md`: kurze Erwähnung der konsolidierten `TrusteeDataTablesView` + Tab-Pattern für mehrere Modelle. +- [ ] `TOPICS.md`: keine Änderung nötig (kein neues Thema). +- [ ] Folge-Plan `2026-MM-trustee-cleanup-positions-documents.md`: Aufräumen der alten Seiten, sobald Daten-Tabellen produktiv verifiziert. +- [ ] Dieses Dokument → `c-work/2-build/` (bei Umsetzungsbeginn) → `c-work/3-validate/` → `z-archive/`. diff --git a/c-work/4-done/2026-04-period-picker-billing-audit-migration.md b/c-work/4-done/2026-04-period-picker-billing-audit-migration.md new file mode 100644 index 0000000..3a4e7d0 --- /dev/null +++ b/c-work/4-done/2026-04-period-picker-billing-audit-migration.md @@ -0,0 +1,267 @@ + + + + + +# Statistik-Endpunkte: Clean-Cut auf `dateFrom`/`dateTo` + `bucketSize` + +## Beschreibung und Kontext + +Folge-Arbeit zum [`PeriodPicker`](../../local/notes/changelog.txt). Die +Komponente liefert semantisch sauber `{ preset, fromDate, toDate }`. Drei +Statistik-Endpunkte im Gateway erwarten heute aber Legacy-Parameter, die +Date-Range mit anderen Konzepten vermischen: + +| Endpunkt | Heute | Problem | +|---|---|---| +| `GET /api/audit/stats` | `?timeRange={days}` (relative Tagesanzahl ab heute) | Kein echter Bereich; im Frontend muss `PeriodValue` per Adapter `_periodToDays` auf eine Tagesanzahl reduziert werden, dadurch wird "01.04 - 03.04" als "letzte 3 Tage ab heute" ausgewertet → falsche KPIs. | +| `GET /api/billing/statistics/{period}` | `period: 'day' \| 'month' \| 'year'`, `year`, `month?` | `period` mischt zwei Verantwortungen: Date-Range-Berechnung **und** Bucket-Granularitaet; aerbitraere Ranges (z.B. "letzte 12 Monate", Custom) sind nicht ausdrueckbar. | +| `GET /api/billing/view/statistics` | `period: 'day' \| 'month'`, `year`, `month?` | Dito; zusaetzlich gibt `period` an `getTransactionStatisticsAggregated(..., period=)` die Bucket-Groesse weiter. | + +Konsequenz heute: der PeriodPicker-Rollout in `BillingDashboard` und +`BillingDataView` ist blockiert; in `ComplianceAuditPage` arbeiten wir mit +einem dokumentierten Hack (`_periodToDays`). + +**Diese Arbeit ist ein Clean-Cut**: die Legacy-Parameter werden entfernt, die +Endpunkte akzeptieren ausschliesslich `dateFrom`/`dateTo` (+ `bucketSize`, +wo Time-Series ausgegeben werden), und alle Frontend-Caller werden in derselben +Aenderung migriert. Keine Backwards-Compat-Aliase, kein "altes UND neues", +kein toleranter Fallback im Backend. + +## Fokus und kritische Details + +- **Eine Aenderung = eine PR**: Backend-Refactor + alle Frontend-Caller + zusammen, da ohne Aliase ein gemischter Zustand nicht laeuft. +- **`/api/billing/view/statistics` mischt zwei Konzepte**: (a) Date-Range + (`startTs/endTs`) und (b) Bucket-Granularitaet fuer Time-Series + (`getTransactionStatisticsAggregated(..., period=)` -> `day` vs `month`). + Diese **muessen getrennt werden**: Date-Range = `dateFrom`/`dateTo`, + Bucket-Groesse = neuer expliziter Param `bucketSize: 'day' | 'month' | 'year'`. + Default-Heuristik nur im Frontend, nicht im Backend. +- **Timezones**: alle Audit-Logger-Records sind UTC-Epoch-Sekunden; PeriodPicker + liefert lokale ISO-Daten (`YYYY-MM-DD`) ohne Timezone. Konversion eindeutig + und an genau einer Stelle (im Endpunkt selbst, vor der DB-Abfrage): + `dateFrom` -> `00:00:00 UTC` desselben Tages, `dateTo` -> `23:59:59.999 UTC` + desselben Tages (inklusive). Sonst fehlt der letzte Tag in der Aggregation. + → Ein gemeinsamer Helper `_isoDateRangeToUtcEpoch(dateFrom, dateTo)` in + `gateway/modules/shared/dateRange.py`. +- **Validierung im Backend**: `dateFrom <= dateTo`, beide Pflicht (kein + optional/None), `bucketSize` aus geschlossenem Enum. Bei Verletzung HTTP 400 + mit klarer Message. Keine impliziten Defaults. +- **Aufrufende Stellen vollstaendig erfassen** (geprueft per ripgrep, siehe + unten - keine externen Konsumenten ausserhalb des Frontends). + +## Ziel und Nicht-Ziele + +- **Ziel:** + - `/api/audit/stats` akzeptiert ausschliesslich `dateFrom`/`dateTo` (ISO + `YYYY-MM-DD`, Pflicht). Antwort enthaelt `dateFrom`/`dateTo`/`days` statt + `timeRangeDays`. + - `/api/billing/statistics` (Endpunkt-Pfad-Param `{period}` entfaellt, neuer + Pfad `GET /api/billing/statistics`) akzeptiert ausschliesslich + `dateFrom`/`dateTo` (Pflicht) + optional `bucketSize`. + - `/api/billing/view/statistics` analog. + - `aiAuditLogger.getAiAuditStats(mandateId, *, fromTs, toTs, groupBy)` - + Pflicht-Range. + - `getTransactionStatisticsAggregated(..., bucketSize=)` - Param `period` + umbenannt zu `bucketSize`. + - Frontend: `BillingDashboard`, `BillingDataView`, `ComplianceAuditPage` + nutzen `` und schicken `dateFrom`/`dateTo`. Adapter + `_periodToDays` ersatzlos entfernt. `useBilling.loadStatistics`-Signatur + nur noch `({ dateFrom, dateTo, bucketSize? })`. +- **Explizit NICHT:** + - Aliase `timeRange` / `period` / `year` / `month` im Backend behalten. + - Stripe-`current_period_*` umbenennen oder beruehren - Stripe-API. + - Andere Stat-Endpunkte (`/api/admin/database-health/stats`, etc.) anfassen - + eigener Scope, sobald Bedarf. + - DB-Schema aendern. + +## Betroffene Module + +- **Gateway:** + - `gateway/modules/routes/routeAudit.py` (`getAuditStats`, Z. 303-317) + - `gateway/modules/shared/aiAuditLogger.py` (`getAiAuditStats`, Z. 195-249) + - `gateway/modules/routes/routeBilling.py` + (`getStatistics` Z. 526-604, `getUserViewStatistics` Z. 1619 ff., + `UsageReportResponse` Z. 260) + - `gateway/modules/interfaces/interfaceBilling.py` (oder konkrete Impl): + `calculateStatisticsFromTransactions` (akzeptiert bereits `startDate`/`endDate` + → keine Aenderung), `getTransactionStatisticsAggregated` + (`period` → `bucketSize`). + - **Neu**: `gateway/modules/shared/dateRange.py` - kleiner Helper + `parseIsoDateRange(dateFrom, dateTo) -> (date, date)` mit Validierung, + `isoDateRangeToUtcEpoch(dateFrom, dateTo) -> (float, float)`. +- **Frontend:** + - `frontend_nyla/src/api/billingApi.ts` (`fetchStatistics`, + `fetchViewStatistics`) + - `frontend_nyla/src/hooks/useBilling.ts` (`loadStatistics` Signatur + aendern) + - `frontend_nyla/src/pages/billing/BillingDashboard.tsx` (Selects raus, + PeriodPicker rein, optional separater `bucketSize`-Toggle) + - `frontend_nyla/src/pages/billing/BillingDataView.tsx` + (FormGeneratorReport: `periodSelector` raus, `dateRangeSelector` rein) + - `frontend_nyla/src/pages/ComplianceAuditPage.tsx`: `_periodToDays` + entfernen, `_loadStats({ dateFrom, dateTo })`. + - `frontend_nyla/src/api/auditApi.ts` (oder direkt in der Page, je nach + aktueller Struktur). +- **DB-Migration:** nein. +- **Tests:** Vorhandene Tests auf den 3 Endpunkten mitziehen (siehe + Aufwandsschaetzung). + +## Externe Konsumenten - Pruefung + +Vor Clean-Cut explizit verifizieren, dass keine externen Aufrufer +(Webhooks, Skripte, Excel-Reports, Teams-Bot, private-llm) die Endpunkte +direkt nutzen: + +- [ ] `rg "billing/statistics" gateway/ private-llm/ teams-bot/` +- [ ] `rg "audit/stats" gateway/ private-llm/ teams-bot/` +- [ ] `rg "/api/billing/statistics|/api/audit/stats" frontend_nyla/` + +Wenn Treffer ausserhalb des Frontends auftauchen, werden sie im selben PR +mitgezogen. Wenn nicht, kann der Clean-Cut erfolgen. + +## Entscheidungen + +| Datum | Entscheidung | Begründung | +|-------|-------------|------------| +| 2026-04-20 | Clean-Cut, keine Backwards-Compat-Aliase | Vermischte Param-Konzepte sind die Wurzel der UX-Probleme; sie zu konservieren reproduziert genau das Problem, das wir loesen. Es gibt keine externen Konsumenten ausserhalb unseres Frontends (zu verifizieren, siehe oben). | +| 2026-04-20 | `dateFrom`/`dateTo` als ISO `YYYY-MM-DD` String, **nicht** Epoch | Konsistent mit `PeriodValue`, lesbar in Logs/Browsernetzwerk, eindeutig (Tagesgrenze, kein Sub-Tag). Audit-Logger arbeitet zwar intern mit Epoch, aber die Konversion macht der Endpunkt zentral. | +| 2026-04-20 | `dateFrom`/`dateTo` Pflicht, kein impliziter Default | Verhindert "vergessene Filter" (z.B. globale Aggregation ueber alle Zeit, schlecht fuer Performance). Caller muss bewusst einen Bereich waehlen. Defaults gehoeren ins Frontend. | +| 2026-04-20 | `bucketSize` separater Param, nicht in `dateFrom`/`dateTo` impliziert | Eine Verantwortung pro Param. Erlaubt "letzte 30 Tage in Tagen" UND "letzte 30 Tage in Wochen" ohne Backend-Aenderung. | +| 2026-04-20 | `bucketSize` ohne Default | Fuer Endpunkte mit Time-Series Antwort: Caller muss explizit waehlen, sonst HTTP 400. Frontend kapselt sinnvolle Heuristik. | +| 2026-04-20 | Pfad `GET /api/billing/statistics` ohne `{period}` | `period` war Pfad-Param, mit Wegfall hat es im Pfad nichts mehr verloren. Saubere Resource-URL. | + +## Umsetzungs-Checkliste + +### A. Vorbereitung (~30 min) + +- [ ] Konsumenten-Pruefung wie oben (drei `rg`-Aufrufe). +- [ ] `gateway/modules/shared/dateRange.py` anlegen mit + `parseIsoDateRange(dateFrom: str, dateTo: str) -> tuple[date, date]` + (HTTP 400 bei ungueltig oder `from > to`) und + `isoDateRangeToUtcEpoch(dateFrom: str, dateTo: str) -> tuple[float, float]`. + Unit-Tests dazu. + +### B. Backend Audit-Stats (~1 h) + +- [ ] `aiAuditLogger.getAiAuditStats(mandateId, *, fromTs: float, toTs: float, groupBy: str = "model")`: + Cutoff-Berechnung raus, ersetzt durch Range-Filter `fromTs <= ts <= toTs`. + `timeRangeDays`-Feld in der Antwort durch `dateFrom`, `dateTo`, `days` + (`(toTs - fromTs) / 86400`, gerundet) ersetzen. +- [ ] `routeAudit.getAuditStats`: + ```python + dateFrom: str = Query(..., description="ISO YYYY-MM-DD") + dateTo: str = Query(..., description="ISO YYYY-MM-DD") + groupBy: str = Query("model", regex="^(model|user|feature|day)$") + ``` + → ueber `isoDateRangeToUtcEpoch` an Logger weitergeben. `timeRange` + Param weg. + +### C. Backend Billing-Stats (~2 h) + +- [ ] `getTransactionStatisticsAggregated`: Param `period` umbenennen zu + `bucketSize` (Enum-Validierung im Endpunkt, hier nur Type-Hint Literal). + Alle Aufrufe im Modul mitziehen. +- [ ] `routeBilling.getStatistics`: + - Pfad-Param `{period}` raus, neuer Pfad `GET /api/billing/statistics`. + - Neue Query-Params `dateFrom: date = Query(...)`, `dateTo: date = Query(...)`, + `bucketSize: Literal['day','month','year'] = Query(...)`. + - Range-Berechnung Z. 570-582 ersetzen durch `parseIsoDateRange`. + - `UsageReportResponse.period: str` → entfernen oder umbenennen zu + `bucketSize`. Anderen Felder bleiben. +- [ ] `routeBilling.getUserViewStatistics`: + - Query-Params analog. `period` und `month`/`year` raus. + - `bucketSize` an `getTransactionStatisticsAggregated` weitergeben. + +### D. Frontend (~2 h) + +- [ ] `billingApi.fetchStatistics` neu signiert: + `fetchStatistics(request, { dateFrom, dateTo, bucketSize })`. Alte + Signatur weg. +- [ ] `billingApi.fetchViewStatistics` analog. +- [ ] `useBilling.loadStatistics({ dateFrom, dateTo, bucketSize })` - + bestehende Aufrufer auf neue Signatur umstellen. +- [ ] `BillingDashboard.tsx`: + - `selectedPeriod`, `selectedYear`, `selectedMonth` State raus. + - ``. + - Separater kleiner `