- Nach "Abschliessen" oeffnet sich ein Blatt "Interne Notiz" (optional,
max. 2000 Zeichen). Ohne Eingabe genuegt ein Tipp auf "Ohne Notiz
abschliessen"; "Zurueck zur Unterschrift" bricht nur das Abschliessen ab
- Die Notiz reist im Abschluss-Aufruf mit (internalNote) und landet im
Lieferbericht, nicht in den Kunden-Notizen
- API-Client aus aktueller Backend-Spec neu generiert
- Widget-Tests fuer das Blatt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Neuer Workflow-Step "Zahlung" zeigt ausgelieferte Artikel mit Menge und
Preis, Betragsaufstellung, Zahlungsstatus und einen grossen Button
"Zahlung abwickeln" (fest unten)
- Zahlungs-Modal: grosser offener Betrag, Methoden als Cards aus den
Backend-Stammdaten, bei EC-Karte gross die Kundennummer; schliesst nur
ueber Abbrechen, "Zahlung erhalten" oder "Lieferung abbrechen" (mit Grund)
- Bestaetigung wird am Server protokolliert (RecordDeliveryPayment); bis
dahin sind "Weiter" und die Uebersicht gesperrt, ausser es ist nichts
offen (bereits bezahlt). Aendert sich der Betrag danach, wird die Zahlung
als veraltet markiert und muss erneut abgewickelt werden
- Uebersicht zeigt die abgewickelte Zahlung statt eigener Methodenauswahl;
Unterschrift-Flow ohne doppelte Inkasso-Stufe; Abschluss sendet Methode
und Inkasso-Flag der gueltigen Zahlung
- DeliveryPaymentStatus als einzige Quelle fuer offenen Betrag
(gleiche Formel wie Backend); Artikelliste/Betragskarte als geteilte
Widgets; Begruendungs-Dialog geteilt mit Info-Step
- API-Client aus aktueller Backend-Spec neu generiert
- Tests fuer Zahlungsstatus und Modal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Umgebungen (Name, API-URL, Keycloak-Issuer) kommen aus
assets/hl_server_config.json statt hartcodiertem BackendConfig.prod;
Testumgebung 192.168.1.9:3001 ist dort hinterlegt
- AppBloc laedt die Config, aktiviert die gespeicherte Wahl (Fallback
Default) und registriert das Networking; Wechsel persistiert per
SharedPreferences und registriert neu
- Bloc-Baum ist pro Umgebung gekeyt: beim Wechsel entstehen alle Blocs
mit dem neuen API-Client neu
- Login-Screen: Zahnrad oben rechts oeffnet Einstellungen mit Umgebungs-
Cards (Name + IP:Port); Hinweis auf aktive Umgebung unter dem Button
- Fahrzeugauswahl pro Umgebung getrennt (eigene DB = eigene Car-IDs)
- Tests fuer Config-Parsing, Auswahl-Fallback, Card-Wechsel, Re-Registrierung
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kunde meldete haeufig 'Sitzung abgelaufen' und danach dauerhaft rotes
'Sitzung abgelaufen' bei den Zahlungsmethoden trotz erfolgreichem Login.
Token-Refresh:
- _performRefresh wertete JEDEN Fehler als tote Session (Logout + Refresh-
Token geloescht) - auch Mobilfunk-/VPN-Aussetzer. Jetzt gilt nur noch eine
echte Ablehnung durch Keycloak (OAuth invalid_grant o. ae.) als abgelaufen;
voruebergehende Fehler behalten die Session, nutzen den noch gueltigen
Access-Token weiter oder werfen AuthTemporarilyUnavailableException
- Interceptor schickt Requests nicht mehr tokenlos weiter (ergab 401 ->
irrefuehrendes 'Sitzung abgelaufen'), sondern bricht als Verbindungsfehler ab
- restoreSession verwirft den Refresh-Token bei fehlendem Netz nicht mehr
Zahlungsmethoden:
- Cubit lud nur einmal beim App-Start (vor dem Login -> 401) und blieb danach
fuer immer im Fehlerzustand. Jetzt Reload bei jedem Wechsel auf Authenticated
- Fehlerkarte in der Uebersicht hat einen 'Erneut laden'-Button
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nach fehlgeschlagenem/timeout Login blieb der Browser beim naechsten Tap
zu: ein per UI-Timeout verwaister flutter_appauth-Flow blockierte nativ
('Connection already in progress').
- Provider dedupet login() (nur EIN authorizeAndExchangeCode gleichzeitig,
race-sicher via identical-Guard) + discardPendingLogin() gibt die Sperre
nach UI-Timeout frei, damit der naechste Tap frisch startet
- Best-Effort-Retry, falls der native Flow noch offen ist
- AuthBloc: discardPendingLogin() bei Timeout; kein zweiter Handler waehrend
Authenticating
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Neue Kalender-Pille im Phase-Stepper: zeigt das Tour-Datum und oeffnet einen
Datepicker, um die Tour eines anderen Tages zu laden (z. B. morgen vorab).
Fahrzeugwechsel bleibt ueber den Drawer erreichbar.
- TourDateCubit haelt das gewaehlte Datum (null = heute, Server autoritativ)
- LoadTour({date}) + TourBloc merkt das Datum (RefreshTour laedt denselben Tag)
- Repository getMyTourDetails({date}) -> API listMyTours(date:) -> GET /me/tours?date=
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clean-Arch-Schichten für Cars:
- lib/domain/entity/car.dart: UUID-id, accountId (Personalnummer),
plate, active. Pendant zum Backend-Schema.
- lib/domain/repository/cars_repository.dart: Port — listMine,
create, update. Keine teamId/personalnummer-Parameter, der
Account fließt serverseitig aus dem JWT.
- lib/data/mapper/car_mapper.dart: API-DTO (built_value) → Domain.
- lib/data/repository/cars_repository_impl.dart: konkrete Impl via
generierter CarsApi (dio), mit DioException → CarsRepositoryException-
Übersetzung.
Feature-Cars-Refactoring:
- CarsBloc nimmt jetzt die Domain-Repository-Schnittstelle. Events:
CarLoad/CarAdd/CarEdit/CarDeactivate (statt CarDelete). Keine
teamId-Parameter mehr. Kein authBloc-Bezug, Session-Expiry läuft
über den globalen Provider-Stream.
- CarsState sealed mit CarsInitial/Loading/LoadingFailed/Loaded.
- Pages: car_management_page, car_management, car_card, car_fail_page,
car_selection_page komplett auf die neue Entity und Event-Signaturen.
- Alte lib/feature/cars/service/cars_service.dart und
lib/feature/cars/repository/cars_repository.dart gelöscht.
CarSelectBloc + Storage:
- CarSelection.selectedCarId von int? auf String? umgestellt.
- CarSelectionRepository persistiert die UUID jetzt als String;
defensive Migration für noch vorhandene int-Werte (alte
Pre-Migration-Installations) verwirft den Wert leise und
erzwingt Neuauswahl.
Konsequenz-Cleanup im Tour-Code (Phase-D-Vorbereitung):
- Delivery.carId String? statt int?.
- Tour.hasUndeliveredLoadedArticles / getFinishedDeliveries auf
String carId.
- _selectedCarId / int? carId / int selectedCarId in DeliveryOverview,
LoadingCustomerPage/OverviewPage, Home, DeliverySelection/SortPage,
DeliveryInfo/List, CustomSortDialog, SortableDeliveryList auf
String umgestellt.
- TourRepository ersetzt int.parse(carId)/int.tryParse-Zuweisungen
direkt durch String.
- lib/model/car.dart wird zum Re-Export der neuen Domain-Entity,
damit Legacy-Imports während Phase-D-Übergang weiter compilieren.
DI:
- app.dart: CarsBloc bekommt CarsRepositoryImpl(locator<HolzleitnerApi>())
statt der alten CarsRepository(service: CarService()).
Build (flutter build apk --debug) durch, flutter analyze ohne
errors.
- BackendConfig.localDev nutzt jetzt die LAN-IP des Dev-Macs
(192.168.0.138) statt localhost. Notwendig zum Testen auf einem
realen Android-Gerät über WLAN. Auf dem iOS-Simulator
zurückwechseln oder per Build-Flag injizieren.
- AuthBloc.on<SessionExpiredEvent> wird zum No-Op (mit Log).
Begründung: die alten ERPframe-Repos rufen das nach jedem 401 auf,
weil ihr Cookie-Login serverseitig weg ist. Solange Phase D diese
Repos nicht ersetzt hat, wäre ein echter Logout daraus fatal —
der erste TourBloc-Load nach Keycloak-Login würde die Session
sofort wieder wegwerfen. Die legitime SessionExpired-Quelle bleibt
der Provider-Stream (Refresh-Failure).
- CarSelectionPage hat jetzt durchgehend eine AppBar (vorher nur
im 'wechseln'-Modus) plus ein Account-Popup oben rechts mit
Personalnummer + roter Abmelden-Aktion. Der Drawer ist sonst
nur an Home, und solange Cars-Loading per 401 blockt, kommt der
User ohne Pre-Home-Logout nicht raus.
Drei zusammenhängende Korrekturen, die den OIDC-Flow auf realen Geräten
durchgehen lassen:
1. taskAffinity="" raus aus MainActivity — sonst landet die
RedirectUriReceiverActivity beim Rücksprung aus Samsung Internet
Custom Tabs (FLAG_ACTIVITY_NEW_TASK) in einem zweiten Task und
zweitem App-Prozess, AppAuth findet seinen in-memory PKCE-State
nicht und meldet 'No stored state - unable to handle response'.
2. network_security_config.xml: base-config cleartextTrafficPermitted
statt einzelner localhost/10.0.2.2-Domains. Notwendig für Tests
gegen die LAN-IP des Dev-Macs (z.B. 192.168.x.x); AndroidConfig kann
keine IP-Wildcards. Klar als Dev-only markiert.
3. promptValues=['login'] auf der AuthorizationTokenRequest — verhindert
den Instant-SSO-Cookie-Redirect-Race, bei dem Chrome Custom Tabs
schliesst, bevor der Redirect-Intent ankommt; AppAuth wuerde sonst
'User cancelled flow' melden, obwohl der Nutzer nicht abgebrochen
hat. UX-mässig auch gewollt: jeder Login frisch (Account-Wechsel
am gleichen Geraet ist denkbar), Restore laeuft über den Refresh-
Token aus der Secure Storage.
Beobachtung: Nach 'flutter_secure_storage' frisch dazugepackt ohne
Cold-Restart kam eine MissingPluginException auf dem AuthBloc-Stream
durch (read auf channel plugins.it_nomads.com/flutter_secure_storage)
und hat den ganzen Bloc-Event-Loop mitgerissen.
Fix:
- KeycloakOidcTokenProvider.restoreSession / _persistRefreshToken /
logout fangen Plugin-Exceptions ab und loggen sie über debugPrint,
statt sie hochzureichen. Restore-Pfad endet sauber mit 'kein Restore
möglich', Login-Pfad hält den Token in Memory weiter.
- AuthBloc._handleRestore mit eigener try/catch als zweite Schutzschicht
für jeden anderen Fehler aus dem Provider.
Bestehender Cold-Restart-Workaround (App stoppen + flutter run) für die
ursprüngliche MissingPluginException bleibt natürlich nötig — diese
Änderung sorgt nur dafür, dass künftige Storage-Probleme (Keychain
zerschossen, Restore-Backup, …) nicht die Auth komplett killen.
App-Code:
- KeycloakOidcTokenProvider: PKCE-Login via flutter_appauth, Refresh via
Refresh-Token aus flutter_secure_storage, Session-Restore beim
App-Start, Logout.
- AuthSessionEvent als Provider→Bloc-Brücke (LoggedIn/LoggedOut/
SessionExpired) auf einem Broadcast-Stream.
- AuthBloc komplett umgebaut: nimmt jetzt den KeycloakOidcTokenProvider
statt UserInfoService, mappt eingehende Provider-Events auf eigene
Zustände. Authenticated.fromClaims() liest personalnummer + Name aus
dem ID-Token-Payload.
- LoginPage: kein Browser+Deep-Link mehr — Button feuert
LoginRequested, der Provider übernimmt den restlichen Flow.
- network_locator: produktiver KeycloakOidcTokenProvider, doppelt
registriert (KeycloakOidcTokenProvider für AuthBloc,
AuthTokenProvider für Interceptor).
- Auth-State trägt zusätzlich personalnummer/displayName/email; das
Legacy-User-Objekt + sessionId bleiben temporär drin, damit die
alten ERPframe-Services (Phase D) noch kompilieren.
Plattform-Setup:
- Android: appAuthRedirectScheme=holzleitner in build.gradle.kts,
NetworkSecurityConfig erlaubt HTTP zu localhost/10.0.2.2/127.0.0.1.
- iOS: holzleitner als URL-Scheme im Info.plist, ATS-Ausnahme für
localhost (HTTP-Keycloak im Dev-Setup).
Out of scope:
- Keine echte App-Run-Smoke — kommt mit dem User-Test.
- iOS-pod-install läuft beim ersten 'flutter run ios' automatisch.
- Old ERPframe-Services bleiben aktiv und werfen ab jetzt 401 (kein
Cookie-Session-Token mehr) — wird in Phase D entfernt.
OpenAPI-Generator-Setup:
- tool/generate_api_client.sh: Direkter Aufruf der openapi-generator-cli.jar
(Java-CLI statt Dart-build_runner-Integration — vermeidet die
analyzer-/source_gen-Version-Hölle mit json_serializable)
- tool/fetch_openapi_generator.sh: lädt die JAR (29 MB) nach (gitignored)
- openapi/holzleitner.json: Snapshot der Backend-Spec für reproduzierbare
Generation
- packages/holzleitner_api/: generiertes Dart-Sub-Package (built_value +
dio), per path-dep im Haupt-pubspec eingehängt
Netzwerk-Layer (lib/data/network/):
- BackendConfig: API- und Keycloak-Endpoints für Local-Dev (localhost
wegen Keycloak-iss-Claim).
- AuthTokenProvider-Schnittstelle.
- DevPasswordGrantTokenProvider: Phase-A-Provider via Keycloak
password-grant, Token-Caching mit Expiry-Check (Phase B ersetzt das
durch flutter_appauth PKCE).
- HolzleitnerAuthInterceptor: dynamischer Bearer-Inject pro Request.
- HolzleitnerApiFactory: baut die generierte HolzleitnerApi-Klasse
mit unserem Interceptor statt der vier Default-Auth-Interceptors.
- network_locator.registerNetworking(): get_it-Setup, in main() vor
runApp() aufgerufen.
Clean-Arch-Scaffolding (lib/data/, lib/domain/):
- Verzeichnisstruktur für Phase C+D angelegt (mapper/, repository/,
entity/, repository/) — befüllt sich in den Folge-Phasen.
Smoke-Test:
- tool/smoke_test_api.dart ruft /health (ungeschützt) und /me/cars
(mit Bearer) via generiertem Client — grün gegen lokales Backend.