Der Fertig-Button der Filial-Abholung ersetzt die Scan-Page jetzt durch
DeliveryDetail (pushReplacement) statt zur Übersicht zurückzukehren.
Zurück aus der Lieferung führt weiterhin zur Übersicht.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Schalter stehen in assets/feature_flags.json (Key -> enabled +
Beschreibung) statt als Konstanten im Code
- Enum Feature buendelt die stabilen Keys; FeatureFlags liest die Datei,
ignoriert unbekannte Keys und nutzt fuer fehlende den Default aus dem Enum
- Laden beim App-Start im AppBloc, bereitgestellt per RepositoryProvider;
FeatureGate blendet Bereiche bei inaktivem Feature komplett aus
- articles.credit_section = false: Abschnitt "Gutschriften" im
Artikel-Step ist ausgeblendet
- Bisheriges Stepper-Flag migriert (articles.credit_amount_stepper); die
fuenf ungenutzten statischen Flags mit veralteten Kommentaren entfernt
- Tests: Asset enthaelt alle Keys, Parsing, Defaults, FeatureGate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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>
Inventur aller Lese-Requests (Tour, Fahrzeuge, Zahlungsmethoden,
Foto-Vorschauen) und aller Stellen, an denen ihr Fehlerzustand gerendert wird.
Geschlossene Luecken:
- Sortieren-Seite: TourLoadFailed fiel in den Lade-Zweig -> Endlos-Spinner
ohne Ausweg. Jetzt Fehlerseite mit 'Erneut versuchen' im Header-Rahmen
- Home: scheiterte beim Start CarLoad oder LoadTour, wurde die Phase nie
geladen -> Endlos-Spinner. Jetzt Fehlerseite mit Retry (+ Drawer)
- Beladen-/Auswaehlen-Seite: Fehlerseite stand ohne Scaffold/Header/Drawer
da; jetzt im selben Rahmen wie die uebrigen Zustaende
- Hintergrund-Reload (refreshError): Snackbar jetzt MIT Retry-Aktion und
zentral im PhaseStepper-Header -> gilt in allen Phasen (vorher nur
Ausliefern, ohne Retry; auf Sortieren/Beladen blieb der Fehler stumm)
- AttachmentImage: Fehleranzeige ist antippbar ('Erneut laden')
Zahlungsmethoden haben den Retry bereits aus dem vorigen Commit.
Co-Authored-By: Claude Fable 5.1 <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>
An einem Tag ohne Tour konnte man in der AppBar durch alle Stepper-Schritte
navigieren. Ursache: an leeren Tagen gilt der GETEILTE 'no-tour'-Persistenz-
Eimer; einmal dort aufgeschlossener Fortschritt (hohes Max) machte an JEDEM
leeren Tag Beladen/Ausliefern anklickbar.
- Stepper: ohne geladene Tour (TourEmpty/loading) ist maxReached hart der
erste sichtbare Schritt -> weitere Schritte nicht anklickbar, keine Haekchen
- PhaseBloc._load: ohne Tour ausschliesslich Eintrittsphase, Persistenz wird
weder gelesen noch geschrieben (kein Fortschritt im 'no-tour'-Eimer)
- PhaseBloc._set: ohne Tour jeden Vorwaerts-Wechsel verwerfen, nichts
persistieren - unabhaengig vom ausloesenden Button/Tap
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nach einem Tour-Wechsel (Tageswechsel im Kopf-Kalender, Resync) blieben bis
zum asynchronen Nachladen aus den SharedPreferences noch Phase und Haekchen
(maxReached) der vorherigen Tour sichtbar; zudem konnte ein persistiertes
Max uebernommen werden, obwohl fuer die neue Tour-Version gar keine Phase
persistiert war.
- Bei Tour-Wechsel eines bereits geladenen Autos den In-Memory-Stand SOFORT
synchron auf die Eintrittsphase setzen (nur Sortieren aktiv, keine
Haekchen); ein persistierter Stand der neuen Tour folgt per PhaseLoaded
- persistedMax nur uebernehmen, wenn fuer diese Tour-Version auch eine Phase
persistiert ist (frische Tour-Version startet immer ohne ererbte Haekchen)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beim Datumswechsel im Kopf-Kalender blieben Beladen/Ausliefern des alten
Tags 'erledigt': PhaseBloc._load brach bei bereits geladenem Auto ab und
Home stiess den Load nur einmal pro Auto an - der In-Memory-Stand wurde
beim Tour-/Token-Wechsel nie invalidiert. Die Persistenz ist bereits pro
Tour-Token (syncedAt) getrennt, nur das Nachladen fehlte.
- PhaseBloc: Guard tokenbewusst (_loadedTokenByCar); anderer Token laedt den
persistierten Stand der neuen Tour nach (neuer Tag -> Eintrittsphase,
Rueckkehr -> alter Stand wiederhergestellt)
- Home._ensurePhaseLoaded: Guard auf carId|tourToken statt nur carId
- Token-Ableitung zentral als TourState.phaseToken (tour_state.dart), genutzt
von tourTokenResolver (app.dart) UND Home - eine Quelle statt Duplikat
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bei TourEmpty/TourLoading rendert die Beladen-Seite jetzt denselben
PhaseStepper-Header wie im geladenen Zustand statt einer nackten
'Beladung'-AppBar. Damit bleiben Kopf-Kalender (Datumswechsel), Menue und
Reload an einem Tag ohne Tour erreichbar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Der Header-Kalender zeigte nach Neustart/Retry noch 'morgen', geladen wurde
aber 'heute' (leer). Ursache: Initial-Load und diverse Retry-Buttons
dispatchten LoadTour() ohne Datum -> immer heute.
- LoadTour(): reiner Reload, behaelt das gemerkte Datum
- LoadTour.forDate(date): setzt gezielt ein Datum (Kalender + Initial-Load)
- Initial-Load in home.dart liest das Datum aus dem TourDateCubit
- Retry-Buttons (Fail-/Sort-/Loading-Seite) laden dadurch den angezeigten Tag
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
Nach dem Anzeige-/Zuordnungs-Fix erschien eine neu gesyncte, noch NICHT
beladene Lieferung sofort in der Auslieferung — obwohl im Standardlager
nichts geladen wurde. Sie gehört aber noch in die Beladen-Phase.
Auslieferung zeigt aktive Lieferungen jetzt nur, wenn ihre Standardlager-
Beladung fertig ist (standardWarehouseLoadingDone). Pausiert/fertig/
abgebrochen (post-Beladung) bleiben unverändert sichtbar; unbeladene
erscheinen weiter in Beladen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_ensureSingleCarAssignment lief nur EINMAL pro Session (bool-Guard je Auto).
Eine nach dem ersten Auto-Assign gesyncte Lieferung (Reload/Pull-to-Refresh)
bekam daher kein assignedCarId mehr und erschien erst nach App-Neustart in
der Auslieferung. Jetzt wird jede (auch neu dazugekommene) noch nicht
zugeordnete Lieferung genau einmal zugewiesen (Set dispatchter Ids statt
bool), ohne die laufende Zuweisung bei jedem Build zu respammen.
Ergänzt den Anzeige-Fix 30fdde1 (Auslieferung == Beladen-Filter).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Die Auslieferungs-Übersicht filterte immer nach assignedCarId==selectedCarId,
während die Beladen-Übersicht bei Single-Car-Teams ALLE Lieferungen zeigt.
Folge: eine neu importierte, noch nicht zugeordnete Lieferung
(assignedCarId == null) erschien in Beladen (und ließ sich scannen), aber
nach dem Scannen NICHT in der Auslieferung.
Auslieferung nutzt jetzt dieselbe multiCarTeam-Logik (CarsBloc,
cars.length >= 2): Single-Car → alle Lieferungen, Multi-Car → nach
assignedCarId filtern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ein nicht-scanbarer Set-Kopf (Parent-Artikel) hat keinen eigenen
Scan-Status. Sobald alle scanbaren, nicht entfernten Komponenten fertig
(isDone) sind, wird der Kopf jetzt ebenfalls grün dargestellt
(effectiveDone = isDone || setParentComplete) und zeigt statt des
unterdrückten Hinweises ein grünes „Komplett geladen".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ein nicht-scanbarer Set-Kopf (Parent-Artikel) wird im Beladen-Screen über
seinen Komponenten gezeigt. Der Hinweis „Kein Scanvorgang notwendig" ist
dort irreführend, weil die (scanbaren) Komponenten darunter sehr wohl
gescannt werden. _ItemRow bekommt suppressScanHint; gesetzt nur für
Set-Köpfe (Artikelnummer wird von einer Komponente als parentArtikelNr
referenziert). Komponenten/echte Dienstleistungen behalten den Hinweis.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ein Set kann mehrfach bestellt sein (Oberartikel-Menge = Set-Anzahl),
daher bleibt der Mengen-Stepper beim Entfernen über den Oberartikel
erhalten. Die gewählte Set-Anzahl kaskadiert PROPORTIONAL auf die
Komponenten (Stückzahl je Set × entfernte Sets, geklemmt auf Restmenge)
— funktioniert für 1:1-Mengen wie für Komponenten mit Stückzahl je Set.
Einzelne Komponenten bleiben weiterhin nicht direkt entfernbar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A) Nach erfolgreichem Abschluss (aktiv→completed) poppt die Detail-Page
automatisch zurück zur Übersicht. Scaffold ist jetzt StatefulWidget
mit BlocListener<TourBloc>; nur „gearmt", wenn die Lieferung beim
Öffnen aktiv war → erneutes Öffnen einer fertigen Lieferung poppt nicht.
B) Step „Info": Artikelliste zeigt weiter die Ursprungsmenge
(requiredQuantity). Bei entfernten/teilweise gutgeschriebenen Positionen
erscheint pro Zeile ein „Menge geändert"-Hinweis + ein tappbares Banner,
das zu Step 3 „Artikel" springt.
C) Beladen: nicht-scanbare Set-Köpfe (Parent-Komponenten) werden jetzt
IMMER mit ihrem Set gezeigt — als Kopf in der Lagergruppe ihrer
Komponenten statt isoliert unter „Dienstleistungen". _ItemRow leitet
scanNotRequired aus der Artikel-Scanbarkeit ab.
D) Step „Übersicht": Wording der Zahlungsweise-Sperre bei offen==0
präzisiert („Keine Zahlung mehr offen (bereits bezahlt)").
E) Step „Artikel": Komponenten eines Sets sind einzeln nicht mehr
entfernbar (kein Button + Hinweis). Das Entfernen/Wiederherstellen läuft
nur über den Oberartikel und kaskadiert auf das ganze Set (ganz oder
gar nix). Set-Entfernen ist blockiert, solange eine Komponente noch
nicht verladen ist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Steht kein offener Betrag mehr aus (vollständig vorab bezahlt oder per
Gutschrift ausgeglichen), wird die Zahlungsmethoden-Auswahl gesperrt und
ein erklärender Hinweis angezeigt — analog zur Sperre bei bereits
abgeschlossener Lieferung.
- Offener-Betrag-Formel in Helper _openAmount(delivery, credit)
extrahiert (Single Source; vorher nur in _PaymentSummary).
- _PaymentMethodPicker bekommt die Gutschrift und sperrt das Dropdown
bei state != active ODER offen == 0 (editable = active && offen > 0).
- Sperr-/Info-Hinweis in wiederverwendbares _PickerHint-Widget gezogen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sobald im Beladen-Scanner der letzte Pflicht-Scan erledigt ist (alle eigenen,
aktiven Lieferungen im Standardlager fertig - gleiche Bedingung wie der
'Auslieferungs-Phase starten'-Gate der Uebersicht), erscheint einmalig ein
Dialog: bestaetigt 'alles gescannt' und fragt, ob die Auslieferung starten soll.
'Auslieferung starten' -> PhaseSet(ausliefern) + Scanner schliessen; 'Spaeter'
-> nur schliessen. BlocConsumer<TourBloc>-Listener + Einmal-Flag (Reset wenn
wieder etwas offen).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auch der Lade-Zwischenzustand (state is! TourLoaded, u.a. waehrend 'Neu laden')
zeigt jetzt die PhaseStepper-AppBar statt der schlichten AppBar -> die Leiste
springt beim Reload nicht mehr kurz auf den Theme-Default zurueck.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Der TourEmpty-Zweig der Sortieren-Seite rendert jetzt den PhaseStepper
(Steps, theme.primaryColor) als AppBar — die Phasen-Navigation bleibt sichtbar
und die Optik ist primary statt der Theme-Default-AppBar. Die mittige
'Keine Lieferungen heute'-Nachricht (inkl. 'Neu laden') bleibt unveraendert.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TourEmpty-Leerzustand der Sortieren-Seite ('Keine Lieferungen heute') bekommt
einen 'Neu laden'-Button (LoadTour) — analog zum Beladen-Empty-State.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Haengt der interaktive Login (Browser-Tab/Token-Exchange) bei Verbindungsabbruch/Issuer-Hang, bricht er nach 10s ab; LoginPage zeigt 'Einloggen nicht moeglich. Spaeter erneut versuchen.' (Unauthenticated.loginTimedOut).
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.
- AuthBootstrapping als neuer Initial-State im AuthBloc. Beim Cold-Start
bleibt die App im Splash, bis restoreSession entweder Authenticated
oder Unauthenticated emittiert — kein sichtbarer LoginPage-Flash mehr
für Nutzer mit gespeicherter Session.
- LoginEnforcer rendert für AuthBootstrapping ein eigenes Splash-Widget
mit Logo + Spinner, für Unauthenticated weiterhin die LoginPage.
- AuthBloc._handleRestore emittiert Unauthenticated explizit, wenn
restoreSession false liefert oder wirft — sonst bliebe der Bootstrap-
State hängen.
- HomeAppDrawer zeigt jetzt displayName + Personalnummer aus dem
Authenticated-State im Header und bekommt einen Abmelden-Eintrag
unten (rot, Confirm-Dialog), der LogoutRequested feuert. Der
Provider löscht den Refresh-Token aus der Secure Storage und der
LoginEnforcer routet automatisch zurück auf die LoginPage.
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.
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)