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)
8.5 KiB
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: ^5unddio_smart_retry(optional) inpubspec.yaml.openapi_generatoralsdev_dependency; Generator-Aufruf intool/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 unterlib/data/api/ablegen, gitignoren. AuthInterceptorfürdio: Bearer-Header einhängen, bei 401 versuchen zu refreshen, sonst Logout-Event feuern.get_it-Registrierungen fürDio, 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: ^7für PKCE-Flow.flutter_secure_storagefür Refresh-Token.KeycloakAuthServicemitlogin(),logout(),refresh(),currentAccessToken().LoginPageumbauen: kein Browser-Redirect + Deep-Link, sondern PKCE-Flow viaflutter_appauth. Bei Erfolg: Tokens in Secure Storage, AuthBloc-EventSetAuthenticatedEvent.AuthBloc.AuthenticatedState trägt jetzt{ accessToken, refreshToken, idTokenClaims }stattsessionId.AuthInterceptoraus 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/*undlib/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 inlib/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-orderDeliveryRepository→POST /deliveries/{id}/{hold,resume,cancel,complete},PUT /deliveries/{id}/assigned-carScanRepository→POST /scans(bulk)NoteRepository→POST /deliveries/{id}/notesCarsRepository→GET/POST/PATCH /me/carsProcessRepository(heute Stubs) → bekommt echte Calls
- Wegfall:
DiscountRepository→ Stub mitUnsupportedFeatureException- Payment-Methoden in
TourRepository→ Stub[]
Smoke: Login → loadTourOfToday() → Tour kommt durch.
Phase E — Blocs
TourBloc: an neuesTour-Aggregat anpassen, Discount/Payment-Events hinter Feature-Flag.CarsBloc: nutzt neueCarsRepository-Signaturen (UUID statt int,activestatt delete).CarSelectBloc: speichert Car-UUID stattintin SharedPreferences (Migration der bestehenden gespeicherten Auswahl: bei UUID-Parse-Fehler einfachnullund 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:
- Login (Keycloak)
- Fahrzeugauswahl
- Sortier-Phase (Reihenfolge per Drag & Drop, Persistieren)
- Beladen-Phase (Scan, Hold, Remove)
- Auslieferung-Phase (Complete, Notizen, Cancel)
- 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 (
actorCarIdan 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