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:
189
docs/BACKEND_MIGRATION.md
Normal file
189
docs/BACKEND_MIGRATION.md
Normal 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
|
||||
Reference in New Issue
Block a user