Files
Holzleitner-Lieferservice-App/lib/feature/settings/presentation/settings_page.dart
Dennis Nemec bc95fe9577 fix: Fehlerbehandlung beim Laden und Meldungen überarbeiten
Reklamationen der Fahrer: Meldungen verdeckten Buttons, und nach dem
Abschluss stand „Lieferung nicht in der Tour gefunden" ohne Ausweg da.

Meldungen:
- Erfolgs-SnackBars entfernt, bei Erfolg bleibt der Spinner
- einheitliche, schwebende Meldungen statt Stau über den Buttons
- Server-Rohtexte durch verständliche Texte ersetzt (api_failure_text)

Laden:
- fehlt eine geöffnete Lieferung, wird die Tour einmal neu geholt; die
  Meldung kommt nur nach erfolgreichem Abruf ohne die Lieferung
- Endlos-Spinner auf Lieferdetail, Filial-Abholung und Beladen ersetzt
  durch Kopfzeile, Fehlergrund und „Erneut versuchen"
- Tour wird nach einem Tageswechsel nachgeladen, offene Seiten bleiben
  dabei unberührt
- Abschluss und Zuweisung überschreiben kein zwischenzeitliches Neuladen,
  alte Antworten drehen einen Abschluss nicht mehr zurück
- leere Server-Antworten gelten als Fehler statt als „keine Daten"

Anmeldung:
- Token-Erneuerung wartet höchstens 12 s, der native Aufruf läuft weiter
- App-Start ohne Netz zeigt einen Hinweis mit „Erneut versuchen"
- Fehlerseite der Fahrzeugauswahl bietet „Abmelden"

Außerdem Podfile.lock an die Plugins angeglichen. Tests für die neuen
Fehlerpfade ergänzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 20:28:24 +01:00

190 lines
6.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:hl_lieferservice/data/network/backend_config.dart';
import 'package:hl_lieferservice/main.dart' show locator;
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
import 'package:hl_lieferservice/feature/settings/bloc/settings_bloc.dart';
import 'package:hl_lieferservice/feature/settings/bloc/settings_event.dart';
import 'package:hl_lieferservice/feature/settings/bloc/settings_state.dart';
import 'package:hl_lieferservice/feature/settings/model/settings.dart';
import 'package:hl_lieferservice/widget/app_message.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<StatefulWidget> createState() => _SettingsPage();
}
class _SettingsPage extends State<SettingsPage> {
/// Bestätigt das Abmelden und triggert dann `LogoutRequested` am
/// [AuthBloc]. Die Navigation zur LoginPage übernimmt der globale
/// `LoginEnforcer` automatisch beim Wechsel auf `Unauthenticated` —
/// gleicher Pfad wie der Abmelden-Button im Drawer
/// (`home_drawer._confirmLogout`).
///
/// Wording bewusst identisch zum Drawer-Dialog, damit es egal ist, von
/// welchem Einstiegspunkt der Fahrer kommt.
Future<void> _logout() async {
final authBloc = context.read<AuthBloc>();
final confirmed = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text("Abmelden"),
content: const Text(
"Möchten Sie sich wirklich abmelden? "
"Beim nächsten Start ist eine erneute Anmeldung erforderlich.",
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text("Abbrechen"),
),
FilledButton(
style: FilledButton.styleFrom(backgroundColor: Colors.red),
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text("Abmelden"),
),
],
),
);
if (confirmed != true) return;
authBloc.add(const LogoutRequested());
}
// ─── Konto / Keycloak ─────────────────────────────────────────────────
/// Öffnet die Keycloak-Account-Konsole im externen Browser. Dort kann
/// der Fahrer sein Passwort ändern, hinterlegte E-Mail aktualisieren
/// und (sofern aktiviert) Zwei-Faktor-Authentifizierung verwalten.
///
/// URL-Pfad `/account/` ist die Standard-Route der Keycloak-26-Account-
/// Konsole; sie hängt sich an den Realm-Issuer aus [BackendConfig].
/// Externer Browser statt In-App-WebView, damit ggf. im Browser
/// gespeicherte Credentials / Authenticator-Apps weiter funktionieren.
Future<void> _openAccountConsole() async {
final issuer = locator<BackendConfig>().keycloakIssuerUrl;
final uri = Uri.parse('$issuer/account/');
final ok = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!ok && mounted) {
context.showAppMessage('Kontoeinstellungen konnten nicht geöffnet werden');
}
}
Widget _scanSettings() {
return BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, state) {
final currentState = state;
if (currentState is AppSettingsLoaded) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text(
"Scaneinstellungen",
style: Theme.of(context).textTheme.headlineSmall,
),
),
ListTile(
title: const Text("Hardware-Scanner"),
subtitle: const Text(
"Schaltet die Kamera beim Scannen aus und nutzt den Hardware-Scanner",
),
trailing: Switch(
value: currentState.settings.useHardwareScanner,
onChanged: (value) {
Settings newSettings = currentState.settings.copyWith();
newSettings.useHardwareScanner = value;
context.read<SettingsBloc>().add(
UpdateSettings(settings: newSettings),
);
},
),
tileColor: Theme.of(context).colorScheme.onSecondary,
),
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(20),
child: Text(
"Scaneinstellungen",
style: Theme.of(context).textTheme.headlineSmall,
),
),
Card(
color: Theme.of(context).colorScheme.onSecondary,
child: Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: Text("Fehler beim Lesen der Scan-Einstellungen"),
),
),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: [
_scanSettings(),
Padding(
padding: const EdgeInsets.all(20),
child: Text(
"Kontoeinstellungen",
style: Theme.of(context).textTheme.headlineSmall,
),
),
ListTile(
title: const Text("Konto verwalten"),
subtitle: const Text(
"Passwort, E-Mail und Sicherheitseinstellungen — öffnet die Konto-Seite im Browser",
),
trailing: Padding(
padding: const EdgeInsets.all(2),
child: FilledButton.icon(
onPressed: _openAccountConsole,
icon: const Icon(Icons.open_in_new),
label: const Text("Öffnen"),
),
),
tileColor: Theme.of(context).colorScheme.onSecondary,
),
ListTile(
title: const Text("Ausloggen"),
// Ganze Zeile tappbar machen — sonst muss der Fahrer das kleine
// Icon präzise treffen.
onTap: _logout,
trailing: const Icon(Icons.logout, color: Colors.redAccent),
tileColor: Theme.of(context).colorScheme.onSecondary,
),
],
),
appBar: AppBar(
title: const Text("Einstellungen"),
backgroundColor: Theme.of(context).primaryColor,
foregroundColor: Theme.of(context).colorScheme.onSecondary,
),
);
}
}