Phasenbasierte Lieferübersicht + Beladen-Flow, plus Migrationsplan für Rust-Backend

UI-Restructuring:
- TabBar in scan_page durch dedizierte Phasen ersetzt: Sortieren / Beladen / Ausliefern
- PhaseBloc + PhaseService leiten Phase aus Tour-/Item-States ab
- DeliverySelectionPage (ab 2 Autos) und DeliverySortPage als eigene Flows
- LoadingOverviewPage / LoadingCustomerPage für die Beladephase
- PhaseStepper-Widget im Home für Phasen-Anzeige
- Lager-Differenzierung (Standardlager 0 vs. Außenlager) via WarehouseBadge

Process-Stubs:
- ProcessRepository für Hold/Cancel/Sort/Assign-Flows (stub, bereit für Backend-Anbindung)

Doku:
- docs/BACKEND_MIGRATION.md: Phasenplan für Umstellung auf das neue
  Rust-Backend (OpenAPI-Generator, Keycloak OIDC, Clean-Arch-Layering)
This commit is contained in:
Dennis Nemec
2026-05-14 22:27:56 +02:00
parent ac6b03227d
commit 456fb59668
29 changed files with 5425 additions and 1015 deletions

189
docs/BACKEND_MIGRATION.md Normal file
View File

@ -0,0 +1,189 @@
# Backend-Migration: ERPframe → Rust-Backend
Stand: 2026-05-14
## Ziel
Die Holzleitner-Lieferservice-App wird vom alten ERPframe/DOCUframe-Backend
auf das neue Rust-Backend (`/Users/dennis/Desktop/Arbeit/Holzleitner-Backend`)
umgestellt. Auf Client-Seite kommt Clean Architecture konsequent durch:
data-Layer (generierte API-DTOs), domain-Layer (App-Entities), Mapper,
Repositories, Blocs.
## Entscheidungen
| Entscheidung | Wahl | Begründung |
|---|---|---|
| API-Client | **OpenAPI-Generator** (`dart` mit `dio` als Provider) | Wir haben sauberes OpenAPI im Backend; handschriftliche Services wären 1000 Zeilen Drift-Risiko. |
| Discount/Payment-Features | **Feature-Flag mit Stubs**, UI-Pfade bleiben kompilierbar | Diese Features existieren im neuen Backend nicht. Stubs werfen `UnsupportedFeatureException`. UI-Aufrufer können sie defensiv behandeln. Spätere Reaktivierung möglich, wenn Kunde bestätigt. |
| Offline-Scan-Queue | **Später**, erst Online-Pfad | Erstmal sauberer Online-Flow. Outbox-Pattern kommt als separate Iteration, wenn das Online-Verhalten sitzt. |
| Phasenfolge | A → B → C → D → E → F | Jede Phase einzeln smoke-getestet, bevor die nächste beginnt. |
## Backend-Endpunkte, die wir treffen
Aus dem fertigen Rust-Backend (Stand 2026-05-14):
| HTTP | Pfad | Use Case |
|---|---|---|
| GET | `/me/tours/today` | Liste der heutigen Touren des angemeldeten Fahrers |
| GET | `/tours/{id}` | Tour-Aggregat (eine Lieferung pro Roundtrip) |
| PUT | `/tours/{id}/delivery-order` | Sortierreihenfolge schreiben |
| POST | `/scans` | Bulk-Scan-Endpoint mit `clientScanId` für Idempotenz |
| POST | `/deliveries/{id}/hold\|resume\|cancel\|complete` | Lifecycle-Übergänge |
| PUT | `/deliveries/{id}/assigned-car` | Fahrzeug-Zuordnung |
| POST | `/deliveries/{id}/notes` | Notiz anlegen (Text/Bild-Anhang) |
| GET | `/me/cars[?includeInactive=true]` | Fahrzeuge des Fahrers |
| POST | `/me/cars` | Fahrzeug anlegen |
| PATCH | `/me/cars/{id}` | Fahrzeug ändern/deaktivieren |
| GET | `/accounts/{personalnummer}` | Account-Stammdaten |
Auth: Keycloak OIDC, JWT Bearer, OIDC-Discovery via
`http://localhost:8080/realms/holzleitner/.well-known/openid-configuration`,
Audience `holzleitner-api`, Client `holzleitner-app` (public + PKCE).
## Modell-Alignment
Die App-Modelle (`lib/model/*`) entsprechen nicht den Backend-Modellen.
Wichtigste Differenzen, die der Mapper auflösen muss:
| App heute | Backend |
|---|---|
| `Article.amount`, `scannedAmount`, `isParent`, `components[]` | `DeliveryItem.requiredQuantity`, `scanState`, flache Liste, `komponentenArtikelNr?` |
| `DeliveryState.{ongoing, finished, onhold, canceled}` | `DeliveryState.{active, completed, held, canceled}` |
| `Car { int id, String plate }` | `Car { UUID id, i64 accountId, String plate, bool active }` |
| `Note { int id, String content }` | `DeliveryNote { UUID, text?, imageAttachment?, authorPersonalnummer, authorCarId?, createdAt }` |
| `Tour { Driver driver, deliveriesPerCar }` | `TourDetails { Tour, Vec<DeliveryWithItems>, customers, articles, warehouses, notes }` |
| `Discount { article, note, noteId }` | — kein Backend-Pendant |
| `Payment { id, description, shortcode }` | — kein Backend-Pendant |
Stücklisten-Komponenten als eigene Entity entfallen. Die Hierarchie kann
die UI aus `komponentenArtikelNr` rekonstruieren, wenn gebraucht.
## Was als Feature-Flag stubbt
Discount- und Payment-Pfade bleiben in der UI sichtbar, aber:
- `DiscountRepository.add/remove/update``throw UnsupportedFeatureException("discounts")`
- `PaymentRepository.list/select` → liefert `[]` bzw. `null`
- Aufrufende Stellen (z. B. `TourBloc`) fangen die Exception, zeigen
Snackbar "Funktion nicht verfügbar" und machen weiter.
Konfigurierbar über `FeatureFlags.discountsEnabled = false` und
`FeatureFlags.paymentsEnabled = false` (Default beides `false`).
## Phasen
### Phase A — Foundation
- `dio: ^5` und `dio_smart_retry` (optional) in `pubspec.yaml`.
- `openapi_generator` als `dev_dependency`; Generator-Aufruf in
`tool/generate_api_client.sh` (oder build_runner) verdrahten.
- OpenAPI-Spec lokal aus dem laufenden Backend ziehen
(`curl http://localhost:3000/openapi.json -o openapi/holzleitner.json`),
einchecken (zur Reproduzierbarkeit), generierten Code unter
`lib/data/api/` ablegen, gitignoren.
- `AuthInterceptor` für `dio`: Bearer-Header einhängen, bei 401 versuchen
zu refreshen, sonst Logout-Event feuern.
- `get_it`-Registrierungen für `Dio`, generierte API-Klassen,
Repositories.
Smoke: `GET /health` und `GET /me/cars` (mit hartkodiertem Token aus
Keycloak) via generiertem Client erreichbar.
### Phase B — Auth (Keycloak OIDC)
- `flutter_appauth: ^7` für PKCE-Flow.
- `flutter_secure_storage` für Refresh-Token.
- `KeycloakAuthService` mit `login()`, `logout()`, `refresh()`,
`currentAccessToken()`.
- `LoginPage` umbauen: kein Browser-Redirect + Deep-Link, sondern
PKCE-Flow via `flutter_appauth`. Bei Erfolg: Tokens in Secure Storage,
AuthBloc-Event `SetAuthenticatedEvent`.
- `AuthBloc.Authenticated` State trägt jetzt
`{ accessToken, refreshToken, idTokenClaims }` statt `sessionId`.
- `AuthInterceptor` aus Phase A nutzt diesen State.
Smoke: Login → Token landet im Secure Storage → `GET /me/cars` über
Interceptor klappt.
### Phase C — Domain & Mapper
- Generierte DTOs (`lib/data/api/model/`) bleiben unverändert.
- Domain-Entities in `lib/domain/entity/` definieren — analog zu heute,
aber an Backend-Felder angepasst (Enum-Werte, UUID-IDs etc.).
- Mapper unter `lib/data/mapper/` pro Entity:
`TourMapper.fromDto(TourDetailsDto) -> Tour`.
- `lib/model/*` und `lib/dto/*` werden **gelöscht**, sobald alle
Aufrufer auf die neuen domain-Entities umgestellt sind. Bis dahin
laufen beide parallel.
Kein UI-Test nötig — diese Phase ist reines Refactoring.
### Phase D — Repositories
- Pro Repository abstraktes Interface in `lib/domain/repository/`,
konkrete Implementierung in `lib/data/repository/`.
- Implementierungen rufen den generierten API-Client auf und mappen
DTO ↔ Entity.
- Neu/umgestellt:
- `TourRepository``GET /me/tours/today` + `GET /tours/{id}` +
`PUT /tours/{id}/delivery-order`
- `DeliveryRepository``POST /deliveries/{id}/{hold,resume,cancel,complete}`,
`PUT /deliveries/{id}/assigned-car`
- `ScanRepository``POST /scans` (bulk)
- `NoteRepository``POST /deliveries/{id}/notes`
- `CarsRepository``GET/POST/PATCH /me/cars`
- `ProcessRepository` (heute Stubs) → bekommt echte Calls
- Wegfall:
- `DiscountRepository` → Stub mit `UnsupportedFeatureException`
- Payment-Methoden in `TourRepository` → Stub `[]`
Smoke: Login → `loadTourOfToday()` → Tour kommt durch.
### Phase E — Blocs
- `TourBloc`: an neues `Tour`-Aggregat anpassen, Discount/Payment-Events
hinter Feature-Flag.
- `CarsBloc`: nutzt neue `CarsRepository`-Signaturen (UUID statt int,
`active` statt delete).
- `CarSelectBloc`: speichert Car-UUID statt `int` in
SharedPreferences (Migration der bestehenden gespeicherten Auswahl:
bei UUID-Parse-Fehler einfach `null` und neu auswählen lassen).
- `AuthBloc`: bereits in Phase B angepasst, hier nur noch
Konsistenz-Check.
- `PhaseBloc`: bleibt UI-seitig, konsumiert das neue Aggregat.
Smoke: Tab-Navigation funktioniert, jeder Tab lädt ohne Crash.
### Phase F — Smoke des kompletten Flows
Manuell durchklicken:
1. Login (Keycloak)
2. Fahrzeugauswahl
3. Sortier-Phase (Reihenfolge per Drag & Drop, Persistieren)
4. Beladen-Phase (Scan, Hold, Remove)
5. Auslieferung-Phase (Complete, Notizen, Cancel)
6. Logout
Bei jedem Schritt: Backend-Logs prüfen (richtige Endpoints gehen rein,
keine 4xx/5xx).
## Was die App nach der Migration *nicht* mehr kann
- Rabatte (Discount-Pfade als Stub, UI zeigt Snackbar)
- Zahlungsarten-Auswahl (Payment-Pfade als Stub)
- Lieferung-spezifische ERP-Optionen (`DeliveryOption`) — fielen
ohnehin durch das Modell raus
Diese Features kommen zurück, sobald sie im Backend modelliert sind.
## Was die App nach der Migration *neu* kann
- Idempotente Bulk-Scans (Offline-Wiederholbarkeit auf Protokoll-Ebene
schon vorbereitet)
- Saubere Audit-Spuren (`actorCarId` an Scans + Notizen)
- Fahrzeug-Soft-Delete (statt Hardes Löschen)
- Server-vergebene `tour_date` (statt App-Datum)
## Out of Scope dieser Migration
- Offline-Queue für Scans (kommt später)
- Object-Storage für Bilder (heute schicken wir Strings, später
pre-signed URLs)
- Cars-Verwaltung von ERPframe migrieren — Fahrzeuge werden neu
angelegt
- ERP-Sync (POST /sync/tour) — das ist DOCUcontrol-Seite, nicht App