Implemented settings, new scan, enhanced UI/UX

This commit is contained in:
Dennis Nemec
2025-11-04 16:52:39 +01:00
parent b19a6e1cd4
commit 7ea9108f62
79 changed files with 3306 additions and 566 deletions

View File

@ -1,25 +1,48 @@
import 'package:flutter/cupertino.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart';
import 'package:hl_lieferservice/repository/user_repository.dart';
import 'package:hl_lieferservice/feature/authentication/service/userinfo.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/main.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
UserRepository repository;
UserInfoService service;
OperationBloc operationBloc;
AuthBloc({required this.repository, required this.operationBloc})
: super(Unauthenticated()) {
on<Authenticate>(_auth);
AuthBloc({required this.service, required this.operationBloc})
: super(Unauthenticated()) {
on<SetAuthenticatedEvent>(_auth);
on<Logout>(_logout);
}
Future<void> _auth(Authenticate event, Emitter<AuthState> emit) async {
Future<void> _auth(
SetAuthenticatedEvent event,
Emitter<AuthState> emit,
) async {
operationBloc.add(LoadOperation());
await Future.delayed(Duration(seconds: 5));
emit(Authenticated(teamId: event.username));
operationBloc.add(FinishOperation());
try {
debugPrint("Retrieve user information");
var response = await service.getUserinfo(event.sessionId);
var state = Authenticated(sessionId: event.sessionId, user: response);
locator.registerSingleton<Authenticated>(state);
emit(state);
operationBloc.add(FinishOperation());
} catch (err, st) {
debugPrint("Failed to retrieve user information");
debugPrint(err.toString());
debugPrint(st.toString());
operationBloc.add(
FailOperation(
message: "Login war nicht erfolgreich. Probieren Sie es erneut.",
),
);
}
}
Future<void> _logout(Logout event, Emitter<AuthState> emit) async {

View File

@ -1,10 +1,9 @@
abstract class AuthEvent {}
class Authenticate extends AuthEvent {
String username;
String password;
class SetAuthenticatedEvent extends AuthEvent {
String sessionId;
Authenticate({required this.username, required this.password});
SetAuthenticatedEvent({required this.sessionId});
}
class Logout extends AuthEvent {

View File

@ -1,9 +1,11 @@
import 'package:hl_lieferservice/feature/authentication/model/user.dart';
abstract class AuthState {}
class Unauthenticated extends AuthState {}
class Authenticated extends AuthState {
String teamId;
Authenticated({required this.teamId});
}
User user;
String sessionId;
Authenticated({required this.user, required this.sessionId});
}

View File

@ -0,0 +1 @@
class UserUnauthorized implements Exception {}

View File

@ -0,0 +1,18 @@
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable()
class User {
User({ required this.number, required this.firstName, required this.lastName, required this.mail });
String number;
String lastName;
String firstName;
String mail;
factory User.fromJson(Map<String, dynamic> json) =>
_$UserFromJson(json);
Map<dynamic, dynamic> toJson() => _$UserToJson(this);
}

View File

@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
User _$UserFromJson(Map<String, dynamic> json) => User(
number: json['number'] as String,
firstName: json['firstName'] as String,
lastName: json['lastName'] as String,
mail: json['mail'] as String,
);
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'number': instance.number,
'lastName': instance.lastName,
'firstName': instance.firstName,
'mail': instance.mail,
};

View File

@ -1,11 +1,10 @@
import 'package:flutter/material.dart';
import 'package:app_links/app_links.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart';
import 'package:hl_lieferservice/feature/authentication/bloc/auth_event.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart';
import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart';
import 'package:hl_lieferservice/widget/operations/presentation/operation_view_enforcer.dart';
import '../bloc/auth_bloc.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:async';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@ -16,27 +15,111 @@ class LoginPage extends StatefulWidget {
class _LoginPageState extends State<LoginPage> {
final _loginFormKey = GlobalKey<FormState>();
final TextEditingController _passwordEditingController =
TextEditingController();
final TextEditingController _userIdEditingController =
TextEditingController();
bool _isLoading = false;
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
bool _isEmpty = false;
void onChanged(String value) {
setState(() {
_isEmpty = value.isEmpty;
});
@override
void initState() {
super.initState();
_appLinks = AppLinks();
}
void _onPressLogin(BuildContext context) async {
if (context.mounted) {
context.read<AuthBloc>().add(
Authenticate(
username: _userIdEditingController.text,
password: _passwordEditingController.text,
),
@override
void dispose() {
_linkSubscription?.cancel();
super.dispose();
}
void _onPressLogin() async {
setState(() => _isLoading = true);
try {
debugPrint("🔵 Setting up deep link listener...");
final completer = Completer<Uri>();
// Listen for deep links BEFORE opening browser
_linkSubscription = _appLinks.uriLinkStream.listen(
(Uri uri) {
debugPrint("🟢 Deep link received: $uri");
if (uri.scheme == 'myapp' && !completer.isCompleted) {
completer.complete(uri);
}
},
onError: (err) {
debugPrint("🔴 Deep link error: $err");
if (!completer.isCompleted) {
completer.completeError(err);
}
},
);
// Small delay to ensure listener is ready
await Future.delayed(const Duration(milliseconds: 500));
debugPrint("🔵 Opening browser to: http://localhost:3000/login");
final loginUrl = Uri.parse('http://192.168.1.9:3000/login');
final launched = await launchUrl(
loginUrl,
mode: LaunchMode.externalApplication,
);
if (!launched) {
throw Exception('Could not launch browser');
}
debugPrint("🔵 Browser opened. Waiting for callback...");
// Wait for the deep link callback
final callbackUri = await completer.future.timeout(
const Duration(minutes: 5),
onTimeout: () {
debugPrint("⏱️ Timeout - no callback received");
throw TimeoutException('Login timeout');
},
);
final sessionId = callbackUri.queryParameters['session_id']!;
debugPrint("✅ Success! Callback: $callbackUri");
debugPrint("✅ Session ID: $sessionId");
await _linkSubscription?.cancel();
_linkSubscription = null;
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Login erfolgreich!'),
backgroundColor: Colors.green,
),
);
context.read<AuthBloc>().add(SetAuthenticatedEvent(sessionId: sessionId));
}
} on TimeoutException {
debugPrint("❌ Timeout");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Login Timeout')),
);
}
} catch (e) {
debugPrint("❌ Error: $e");
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Fehler: $e')),
);
}
} finally {
await _linkSubscription?.cancel();
_linkSubscription = null;
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@ -75,38 +158,19 @@ class _LoginPageState extends State<LoginPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 20),
child: TextFormField(
decoration: const InputDecoration(
labelText: "Personalnummer",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(
Radius.circular(10.0),
),
),
),
controller: _userIdEditingController,
onChanged: onChanged,
),
),
TextFormField(
decoration: const InputDecoration(
labelText: "Passwort",
border: OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0)),
),
),
controller: _passwordEditingController,
obscureText: true,
onChanged: onChanged,
),
Padding(
padding: const EdgeInsets.only(top: 15, bottom: 15),
child: OutlinedButton(
onPressed:
!_isEmpty ? () => _onPressLogin(context) : null,
child: const Text("Anmelden"),
child: _isLoading
? const Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Warte auf Login...'),
],
)
: OutlinedButton(
onPressed: _onPressLogin,
child: const Text("Anmelden mit Holzleitner Login"),
),
),
],
@ -118,4 +182,4 @@ class _LoginPageState extends State<LoginPage> {
),
);
}
}
}

View File

@ -0,0 +1,21 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:hl_lieferservice/feature/authentication/model/user.dart';
import 'package:http/http.dart';
class UserInfoService {
String url;
UserInfoService({ required this.url });
Future<User> getUserinfo(String sessionId) async {
var headers = {
"Cookie": "session_id=$sessionId"
};
var result = await get(Uri.parse("$url/userinfo"), headers: headers);
debugPrint("USERINFO: ${result.body}");
return User.fromJson(jsonDecode(result.body));
}
}